Update from Google.
--
MOE_MIGRATED_REVID=85702957
diff --git a/src/main/java/com/google/devtools/build/docgen/BlazeRuleHelpPrinter.java b/src/main/java/com/google/devtools/build/docgen/BlazeRuleHelpPrinter.java
new file mode 100644
index 0000000..12687db
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/docgen/BlazeRuleHelpPrinter.java
@@ -0,0 +1,60 @@
+// Copyright 2014 Google Inc. 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.docgen;
+
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A helper class to load and store printable build rule documentation. The doc
+ * printed here doesn't contain attribute and implicit output definitions, just
+ * the general rule documentation and examples.
+ */
+public class BlazeRuleHelpPrinter {
+
+ private static Map<String, RuleDocumentation> ruleDocMap = null;
+
+ /**
+ * Returns the documentation of the given rule to be printed on the console.
+ */
+ public static String getRuleDoc(String ruleName, ConfiguredRuleClassProvider provider) {
+ if (ruleDocMap == null) {
+ try {
+ BuildEncyclopediaProcessor processor = new BuildEncyclopediaProcessor(provider);
+ Set<RuleDocumentation> ruleDocs = processor.collectAndProcessRuleDocs(
+ new String[] {"java/com/google/devtools/build/lib/view",
+ "java/com/google/devtools/build/lib/rules"}, false);
+ ruleDocMap = new HashMap<>();
+ for (RuleDocumentation ruleDoc : ruleDocs) {
+ ruleDocMap.put(ruleDoc.getRuleName(), ruleDoc);
+ }
+ } catch (BuildEncyclopediaDocException e) {
+ return e.getErrorMsg();
+ } catch (IOException e) {
+ return e.getMessage();
+ }
+ }
+ // Every rule should be documented and this method should be called only
+ // for existing rules (a check is performed in HelpCommand).
+ Preconditions.checkState(ruleDocMap.containsKey(ruleName), String.format(
+ "ERROR: Documentation of rule %s does not exist.", ruleName));
+ return "Rule " + ruleName + ":"
+ + ruleDocMap.get(ruleName).getCommandLineDocumentation() + "\n";
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/docgen/BuildEncyclopediaDocException.java b/src/main/java/com/google/devtools/build/docgen/BuildEncyclopediaDocException.java
new file mode 100644
index 0000000..d3b12c4
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/docgen/BuildEncyclopediaDocException.java
@@ -0,0 +1,48 @@
+// Copyright 2014 Google Inc. 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.docgen;
+
+/**
+ * An exception for Build Encyclopedia generation implementing the common BLAZE
+ * error formatting, i.e. displaying file name and line number.
+ */
+public class BuildEncyclopediaDocException extends Exception {
+
+ private String fileName;
+ private int lineNumber;
+ private String errorMsg;
+
+ public BuildEncyclopediaDocException(String fileName, int lineNumber, String errorMsg) {
+ this.fileName = fileName;
+ this.lineNumber = lineNumber;
+ this.errorMsg = errorMsg;
+ }
+
+ public String getFileName() {
+ return fileName;
+ }
+
+ public int getLineNumber() {
+ return lineNumber;
+ }
+
+ public String getErrorMsg() {
+ return errorMsg;
+ }
+
+ @Override
+ public String getMessage() {
+ return "Error in " + fileName + ":" + lineNumber + ": " + errorMsg;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/docgen/BuildEncyclopediaGenerator.java b/src/main/java/com/google/devtools/build/docgen/BuildEncyclopediaGenerator.java
new file mode 100644
index 0000000..e2bb844
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/docgen/BuildEncyclopediaGenerator.java
@@ -0,0 +1,68 @@
+// Copyright 2014 Google Inc. 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.docgen;
+
+import com.google.devtools.build.lib.Constants;
+import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+/**
+ * The main class for the docgen project. The class checks the input arguments
+ * and uses the BuildEncyclopediaProcessor for the actual documentation generation.
+ */
+public class BuildEncyclopediaGenerator {
+
+ private static boolean checkArgs(String[] args) {
+ if (args.length < 1) {
+ System.err.println("There has to be one or two input parameters\n"
+ + " - a comma separated list for input directories\n"
+ + " - an output directory (optional).");
+ return false;
+ }
+ return true;
+ }
+
+ private static void fail(Throwable e, boolean printStackTrace) {
+ System.err.println("ERROR: " + e.getMessage());
+ if (printStackTrace) {
+ e.printStackTrace();
+ }
+ Runtime.getRuntime().exit(1);
+ }
+
+ private static ConfiguredRuleClassProvider createRuleClassProvider()
+ throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException,
+ IllegalAccessException {
+ Class<?> providerClass = Class.forName(Constants.MAIN_RULE_CLASS_PROVIDER);
+ Method createMethod = providerClass.getMethod("create");
+ return (ConfiguredRuleClassProvider) createMethod.invoke(null);
+ }
+
+ public static void main(String[] args) {
+ if (checkArgs(args)) {
+ // TODO(bazel-team): use flags
+ try {
+ BuildEncyclopediaProcessor processor = new BuildEncyclopediaProcessor(
+ createRuleClassProvider());
+ processor.generateDocumentation(args[0].split(","), args.length > 1 ? args[1] : null);
+ } catch (BuildEncyclopediaDocException e) {
+ fail(e, false);
+ } catch (Throwable e) {
+ fail(e, true);
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/docgen/BuildEncyclopediaProcessor.java b/src/main/java/com/google/devtools/build/docgen/BuildEncyclopediaProcessor.java
new file mode 100644
index 0000000..fb52a4c
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/docgen/BuildEncyclopediaProcessor.java
@@ -0,0 +1,403 @@
+// Copyright 2014 Google Inc. 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.docgen;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.LinkedListMultimap;
+import com.google.common.collect.ListMultimap;
+import com.google.devtools.build.docgen.DocgenConsts.RuleType;
+import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider;
+import com.google.devtools.build.lib.analysis.RuleDefinition;
+import com.google.devtools.build.lib.packages.Attribute;
+import com.google.devtools.build.lib.packages.RuleClass;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+/**
+ * A class to assemble documentation for the Build Encyclopedia. The
+ * program parses the documentation fragments of rule-classes and
+ * generates the html format documentation.
+ */
+public class BuildEncyclopediaProcessor {
+
+ private ConfiguredRuleClassProvider ruleClassProvider;
+
+ /**
+ * Creates the BuildEncyclopediaProcessor instance. The ruleClassProvider parameter
+ * is used for rule class hierarchy and attribute checking.
+ *
+ */
+ public BuildEncyclopediaProcessor(ConfiguredRuleClassProvider ruleClassProvider) {
+ this.ruleClassProvider = Preconditions.checkNotNull(ruleClassProvider);
+ }
+
+ /**
+ * Collects and processes all the rule and attribute documentation in inputDirs and
+ * generates the Build Encyclopedia into the outputRootDir.
+ */
+ public void generateDocumentation(String[] inputDirs, String outputRootDir)
+ throws BuildEncyclopediaDocException, IOException {
+ BufferedWriter bw = null;
+ File buildEncyclopediaPath = setupDirectories(outputRootDir);
+ try {
+ bw = new BufferedWriter(new FileWriter(buildEncyclopediaPath));
+ bw.write(DocgenConsts.HEADER_COMMENT);
+
+ Set<RuleDocumentation> ruleDocEntries = collectAndProcessRuleDocs(inputDirs, false);
+ writeRuleClassDocs(ruleDocEntries, bw);
+
+ bw.write(SourceFileReader.readTemplateContents(DocgenConsts.FOOTER_TEMPLATE));
+
+ } finally {
+ if (bw != null) {
+ bw.close();
+ }
+ }
+ }
+
+ /**
+ * Collects all the rule and attribute documentation present in inputDirs, integrates the
+ * attribute documentation in the rule documentation and returns the rule documentation.
+ */
+ public Set<RuleDocumentation> collectAndProcessRuleDocs(String[] inputDirs,
+ boolean printMessages) throws BuildEncyclopediaDocException, IOException {
+ // RuleDocumentations are generated in order (based on rule type then alphabetically).
+ // The ordering is also used to determine in which rule doc the common attribute docs are
+ // generated (they are generated at the first appearance).
+ Set<RuleDocumentation> ruleDocEntries = new TreeSet<>();
+ // RuleDocumentationAttribute objects equal based on attributeName so they have to be
+ // collected in a List instead of a Set.
+ ListMultimap<String, RuleDocumentationAttribute> attributeDocEntries =
+ LinkedListMultimap.create();
+ for (String inputDir : inputDirs) {
+ if (printMessages) {
+ System.out.println(" Processing input directory: " + inputDir);
+ }
+ int ruleNum = ruleDocEntries.size();
+ collectDocs(ruleDocEntries, attributeDocEntries, new File(inputDir));
+ if (printMessages) {
+ System.out.println(
+ " " + (ruleDocEntries.size() - ruleNum) + " rule documentations found.");
+ }
+ }
+
+ processAttributeDocs(ruleDocEntries, attributeDocEntries);
+ return ruleDocEntries;
+ }
+
+ /**
+ * Go through all attributes of all documented rules and search the best attribute documentation
+ * if exists. The best documentation is the closest documentation in the ancestor graph. E.g. if
+ * java_library.deps documented in $rule and $java_rule then the one in $java_rule is going to
+ * apply since it's a closer ancestor of java_library.
+ */
+ private void processAttributeDocs(Set<RuleDocumentation> ruleDocEntries,
+ ListMultimap<String, RuleDocumentationAttribute> attributeDocEntries)
+ throws BuildEncyclopediaDocException {
+ for (RuleDocumentation ruleDoc : ruleDocEntries) {
+ RuleClass ruleClass = ruleClassProvider.getRuleClassMap().get(ruleDoc.getRuleName());
+ if (ruleClass != null) {
+ if (ruleClass.isDocumented()) {
+ Class<? extends RuleDefinition> ruleDefinition =
+ ruleClassProvider.getRuleClassDefinition(ruleDoc.getRuleName());
+ for (Attribute attribute : ruleClass.getAttributes()) {
+ String attrName = attribute.getName();
+ List<RuleDocumentationAttribute> attributeDocList =
+ attributeDocEntries.get(attrName);
+ if (attributeDocList != null) {
+ // There are attribute docs for this attribute.
+ // Search the closest one in the ancestor graph.
+ // Note that there can be only one 'closest' attribute since we forbid multiple
+ // inheritance of the same attribute in RuleClass.
+ int minLevel = Integer.MAX_VALUE;
+ RuleDocumentationAttribute bestAttributeDoc = null;
+ for (RuleDocumentationAttribute attributeDoc : attributeDocList) {
+ int level = attributeDoc.getDefinitionClassAncestryLevel(ruleDefinition);
+ if (level >= 0 && level < minLevel) {
+ bestAttributeDoc = attributeDoc;
+ minLevel = level;
+ }
+ }
+ if (bestAttributeDoc != null) {
+ ruleDoc.addAttribute(bestAttributeDoc);
+ // If there is no matching attribute doc try to add the common.
+ } else if (ruleDoc.getRuleType().equals(RuleType.BINARY)
+ && PredefinedAttributes.BINARY_ATTRIBUTES.containsKey(attrName)) {
+ ruleDoc.addAttribute(PredefinedAttributes.BINARY_ATTRIBUTES.get(attrName));
+ } else if (ruleDoc.getRuleType().equals(RuleType.TEST)
+ && PredefinedAttributes.TEST_ATTRIBUTES.containsKey(attrName)) {
+ ruleDoc.addAttribute(PredefinedAttributes.TEST_ATTRIBUTES.get(attrName));
+ } else if (PredefinedAttributes.COMMON_ATTRIBUTES.containsKey(attrName)) {
+ ruleDoc.addAttribute(PredefinedAttributes.COMMON_ATTRIBUTES.get(attrName));
+ }
+ }
+ }
+ }
+ } else {
+ throw ruleDoc.createException("Can't find RuleClass for " + ruleDoc.getRuleName());
+ }
+ }
+ }
+
+ /**
+ * Categorizes, checks and prints all the rule-class documentations.
+ */
+ private void writeRuleClassDocs(Set<RuleDocumentation> docEntries, BufferedWriter bw)
+ throws BuildEncyclopediaDocException, IOException {
+ Set<RuleDocumentation> binaryDocs = new TreeSet<>();
+ Set<RuleDocumentation> libraryDocs = new TreeSet<>();
+ Set<RuleDocumentation> testDocs = new TreeSet<>();
+ Set<RuleDocumentation> generateDocs = new TreeSet<>();
+ Set<RuleDocumentation> otherDocs = new TreeSet<>();
+
+ for (RuleDocumentation doc : docEntries) {
+ RuleClass ruleClass = ruleClassProvider.getRuleClassMap().get(doc.getRuleName());
+ if (ruleClass.isDocumented()) {
+ if (doc.isLanguageSpecific()) {
+ switch(doc.getRuleType()) {
+ case BINARY:
+ binaryDocs.add(doc);
+ break;
+ case LIBRARY:
+ libraryDocs.add(doc);
+ break;
+ case TEST:
+ testDocs.add(doc);
+ break;
+ case OTHER:
+ otherDocs.add(doc);
+ break;
+ }
+ } else {
+ otherDocs.add(doc);
+ }
+ }
+ }
+
+ bw.write(SourceFileReader.readTemplateContents(DocgenConsts.HEADER_TEMPLATE,
+ generateBEHeaderMapping(docEntries)));
+
+ Map<String, String> sectionMapping = ImmutableMap.of(
+ DocgenConsts.VAR_SECTION_BINARY, getRuleDocs(binaryDocs),
+ DocgenConsts.VAR_SECTION_LIBRARY, getRuleDocs(libraryDocs),
+ DocgenConsts.VAR_SECTION_TEST, getRuleDocs(testDocs),
+ DocgenConsts.VAR_SECTION_GENERATE, getRuleDocs(generateDocs),
+ DocgenConsts.VAR_SECTION_OTHER, getRuleDocs(otherDocs));
+ bw.write(SourceFileReader.readTemplateContents(DocgenConsts.BODY_TEMPLATE, sectionMapping));
+ }
+
+ private Map<String, String> generateBEHeaderMapping(Set<RuleDocumentation> docEntries)
+ throws BuildEncyclopediaDocException {
+ StringBuilder sb = new StringBuilder();
+
+ sb.append("<table id=\"rules\" summary=\"Table of rules sorted by language\">\n")
+ .append("<colgroup span=\"5\" width=\"20%\"></colgroup>\n")
+ .append("<tr><th>Language</th><th>Binary rules</th><th>Library rules</th>"
+ + "<th>Test rules</th><th>Other rules</th><th></th></tr>\n");
+
+ // Separate rule families into language-specific and generic ones.
+ Set<String> languageSpecificRuleFamilies = new TreeSet<>();
+ Set<String> genericRuleFamilies = new TreeSet<>();
+ separateRuleFamilies(docEntries, languageSpecificRuleFamilies, genericRuleFamilies);
+
+ // Create a mapping of rules based on rule type and family.
+ Map<String, ListMultimap<RuleType, RuleDocumentation>> ruleMapping = new HashMap<>();
+ createRuleMapping(docEntries, ruleMapping);
+
+ // Generate the table.
+ for (String ruleFamily : languageSpecificRuleFamilies) {
+ generateHeaderTableRuleFamily(sb, ruleMapping.get(ruleFamily), ruleFamily);
+ }
+
+ sb.append("<tr><th> </th></tr>");
+ sb.append("<tr><th colspan=\"5\">Rules that do not apply to a "
+ + "specific programming language</th></tr>");
+ for (String ruleFamily : genericRuleFamilies) {
+ generateHeaderTableRuleFamily(sb, ruleMapping.get(ruleFamily), ruleFamily);
+ }
+ sb.append("</table>\n");
+ return ImmutableMap.<String, String>of(DocgenConsts.VAR_HEADER_TABLE, sb.toString(),
+ DocgenConsts.VAR_COMMON_ATTRIBUTE_DEFINITION, generateCommonAttributeDocs(
+ PredefinedAttributes.COMMON_ATTRIBUTES, DocgenConsts.COMMON_ATTRIBUTES),
+ DocgenConsts.VAR_TEST_ATTRIBUTE_DEFINITION, generateCommonAttributeDocs(
+ PredefinedAttributes.TEST_ATTRIBUTES, DocgenConsts.TEST_ATTRIBUTES),
+ DocgenConsts.VAR_BINARY_ATTRIBUTE_DEFINITION, generateCommonAttributeDocs(
+ PredefinedAttributes.BINARY_ATTRIBUTES, DocgenConsts.BINARY_ATTRIBUTES),
+ DocgenConsts.VAR_LEFT_PANEL, generateLeftNavigationPanel(docEntries));
+ }
+
+ /**
+ * Create a mapping of rules based on rule type and family.
+ */
+ private void createRuleMapping(Set<RuleDocumentation> docEntries,
+ Map<String, ListMultimap<RuleType, RuleDocumentation>> ruleMapping)
+ throws BuildEncyclopediaDocException {
+ for (RuleDocumentation ruleDoc : docEntries) {
+ RuleClass ruleClass = ruleClassProvider.getRuleClassMap().get(ruleDoc.getRuleName());
+ if (ruleClass != null) {
+ String ruleFamily = ruleDoc.getRuleFamily();
+ if (!ruleMapping.containsKey(ruleFamily)) {
+ ruleMapping.put(ruleFamily, LinkedListMultimap.<RuleType, RuleDocumentation>create());
+ }
+ if (ruleClass.isDocumented()) {
+ ruleMapping.get(ruleFamily).put(ruleDoc.getRuleType(), ruleDoc);
+ }
+ } else {
+ throw ruleDoc.createException("Can't find RuleClass for " + ruleDoc.getRuleName());
+ }
+ }
+ }
+
+ /**
+ * Separates all rule families in docEntries into language-specific rules and generic rules.
+ */
+ private void separateRuleFamilies(Set<RuleDocumentation> docEntries,
+ Set<String> languageSpecificRuleFamilies, Set<String> genericRuleFamilies)
+ throws BuildEncyclopediaDocException {
+ for (RuleDocumentation ruleDoc : docEntries) {
+ if (ruleDoc.isLanguageSpecific()) {
+ if (genericRuleFamilies.contains(ruleDoc.getRuleFamily())) {
+ throw ruleDoc.createException("The rule is marked as being language-specific, but other "
+ + "rules of the same family have already been marked as being not.");
+ }
+ languageSpecificRuleFamilies.add(ruleDoc.getRuleFamily());
+ } else {
+ if (languageSpecificRuleFamilies.contains(ruleDoc.getRuleFamily())) {
+ throw ruleDoc.createException("The rule is marked as being generic, but other rules of "
+ + "the same family have already been marked as being language-specific.");
+ }
+ genericRuleFamilies.add(ruleDoc.getRuleFamily());
+ }
+ }
+ }
+
+ private String generateLeftNavigationPanel(Set<RuleDocumentation> docEntries) {
+ // Order the rules alphabetically. At this point they are ordered according to
+ // RuleDocumentation.compareTo() which is not alphabetical.
+ TreeMap<String, String> ruleNames = new TreeMap<>();
+ for (RuleDocumentation ruleDoc : docEntries) {
+ String ruleName = ruleDoc.getRuleName();
+ ruleNames.put(ruleName.toLowerCase(), ruleName);
+ }
+ StringBuilder sb = new StringBuilder();
+ for (String ruleName : ruleNames.values()) {
+ RuleClass ruleClass = ruleClassProvider.getRuleClassMap().get(ruleName);
+ Preconditions.checkNotNull(ruleClass);
+ if (ruleClass.isDocumented()) {
+ sb.append(String.format("<a href=\"#%s\">%s</a><br/>\n", ruleName, ruleName));
+ }
+ }
+ return sb.toString();
+ }
+
+ private String generateCommonAttributeDocs(Map<String, RuleDocumentationAttribute> attributes,
+ String attributeGroupName) throws BuildEncyclopediaDocException {
+ RuleDocumentation ruleDoc = new RuleDocumentation(
+ attributeGroupName, "OTHER", null, null, 0, null, ImmutableSet.<String>of(),
+ ruleClassProvider);
+ for (RuleDocumentationAttribute attribute : attributes.values()) {
+ ruleDoc.addAttribute(attribute);
+ }
+ return ruleDoc.generateAttributeDefinitions();
+ }
+
+ private void generateHeaderTableRuleFamily(StringBuilder sb,
+ ListMultimap<RuleType, RuleDocumentation> ruleTypeMap, String ruleFamily) {
+ sb.append("<tr>\n")
+ .append(String.format("<td class=\"lang\">%s</td>\n", ruleFamily));
+ boolean otherRulesSplitted = false;
+ for (RuleType ruleType : DocgenConsts.RuleType.values()) {
+ sb.append("<td>");
+ int i = 0;
+ List<RuleDocumentation> ruleDocList = ruleTypeMap.get(ruleType);
+ for (RuleDocumentation ruleDoc : ruleDocList) {
+ if (i > 0) {
+ if (ruleType.equals(RuleType.OTHER)
+ && ruleDocList.size() >= 4 && i == (ruleDocList.size() + 1) / 2) {
+ // Split 'other rules' into two columns if there are too many of them.
+ sb.append("</td>\n<td>");
+ otherRulesSplitted = true;
+ } else {
+ sb.append("<br/>");
+ }
+ }
+ String ruleName = ruleDoc.getRuleName();
+ String deprecatedString = ruleDoc.hasFlag(DocgenConsts.FLAG_DEPRECATED)
+ ? " class=\"deprecated\"" : "";
+ sb.append(String.format("<a href=\"#%s\"%s>%s</a>", ruleName, deprecatedString, ruleName));
+ i++;
+ }
+ sb.append("</td>\n");
+ }
+ // There should be 6 columns.
+ if (!otherRulesSplitted) {
+ sb.append("<td></td>\n");
+ }
+ sb.append("</tr>\n");
+ }
+
+ private String getRuleDocs(Iterable<RuleDocumentation> docEntries) {
+ StringBuilder sb = new StringBuilder();
+ for (RuleDocumentation doc : docEntries) {
+ sb.append(doc.getHtmlDocumentation());
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Goes through all the html files and subdirs under inputPath and collects the rule
+ * and attribute documentations using the ruleDocEntries and attributeDocEntries variable.
+ */
+ public void collectDocs(Set<RuleDocumentation> ruleDocEntries,
+ ListMultimap<String, RuleDocumentationAttribute> attributeDocEntries,
+ File inputPath) throws BuildEncyclopediaDocException, IOException {
+ if (inputPath.isFile()) {
+ if (DocgenConsts.JAVA_SOURCE_FILE_SUFFIX.apply(inputPath.getName())) {
+ SourceFileReader sfr = new SourceFileReader(
+ ruleClassProvider, inputPath.getAbsolutePath());
+ sfr.readDocsFromComments();
+ ruleDocEntries.addAll(sfr.getRuleDocEntries());
+ if (attributeDocEntries != null) {
+ // Collect all attribute documentations from this file.
+ attributeDocEntries.putAll(sfr.getAttributeDocEntries());
+ }
+ }
+ } else if (inputPath.isDirectory()) {
+ for (File childPath : inputPath.listFiles()) {
+ collectDocs(ruleDocEntries, attributeDocEntries, childPath);
+ }
+ }
+ }
+
+ private File setupDirectories(String outputRootDir) {
+ if (outputRootDir != null) {
+ File outputRootPath = new File(outputRootDir);
+ outputRootPath.mkdirs();
+ return new File(outputRootDir + File.separator + DocgenConsts.BUILD_ENCYCLOPEDIA_NAME);
+ } else {
+ return new File(DocgenConsts.BUILD_ENCYCLOPEDIA_NAME);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/docgen/DocCheckerUtils.java b/src/main/java/com/google/devtools/build/docgen/DocCheckerUtils.java
new file mode 100644
index 0000000..3b64362
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/docgen/DocCheckerUtils.java
@@ -0,0 +1,95 @@
+// Copyright 2014 Google Inc. 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.docgen;
+
+import com.google.common.collect.ImmutableSet;
+
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A utility class to check the generated documentations.
+ */
+public class DocCheckerUtils {
+
+ // TODO(bazel-team): remove elements from this list and clean up the tested documentations.
+ private static final ImmutableSet<String> UNCHECKED_HTML_TAGS = ImmutableSet.<String>of(
+ "br", "li", "ul", "p");
+
+ private static final Pattern TAG_PATTERN = Pattern.compile(
+ "<([/]?[a-z0-9_]+)"
+ + "([^>]*)"
+ + ">",
+ Pattern.CASE_INSENSITIVE);
+
+ private static final Pattern COMMENT_PATTERN = Pattern.compile(
+ "<!--.*?-->",
+ Pattern.CASE_INSENSITIVE);
+
+ /**
+ * Returns the first unmatched html tag of srcs or null if no such tag exists.
+ * Note that this check is not performed on br, ul, li and p tags. The method also
+ * prints some help in case an unmatched tag is found. The check is performed
+ * inside comments too.
+ */
+ public static String getFirstUnclosedTagAndPrintHelp(String src) {
+ return getFirstUnclosedTag(src, true);
+ }
+
+ static String getFirstUnclosedTag(String src) {
+ return getFirstUnclosedTag(src, false);
+ }
+
+ // TODO(bazel-team): run this on the Skylark docs too.
+ private static String getFirstUnclosedTag(String src, boolean printHelp) {
+ Matcher commentMatcher = COMMENT_PATTERN.matcher(src);
+ src = commentMatcher.replaceAll("");
+ Matcher tagMatcher = TAG_PATTERN.matcher(src);
+ Deque<String> tagStack = new ArrayDeque<>();
+ while (tagMatcher.find()) {
+ String tag = tagMatcher.group(1);
+ String rest = tagMatcher.group(2);
+ String strippedTag = tag.substring(1);
+
+ // Ignoring self closing tags.
+ if (!rest.endsWith("/")
+ // Ignoring unchecked tags.
+ && !UNCHECKED_HTML_TAGS.contains(tag) && !UNCHECKED_HTML_TAGS.contains(strippedTag)) {
+ if (tag.startsWith("/")) {
+ // Closing tag. Removing '/' from the beginning.
+ tag = strippedTag;
+ String lastTag = tagStack.removeLast();
+ if (!lastTag.equals(tag)) {
+ if (printHelp) {
+ System.err.println(
+ "Unclosed tag: " + lastTag + "\n"
+ + "Trying to close with: " + tag + "\n"
+ + "Stack of open tags: " + tagStack + "\n"
+ + "Last 200 characters:\n"
+ + src.substring(Math.max(tagMatcher.start() - 200, 0), tagMatcher.start()));
+ }
+ return lastTag;
+ }
+ } else {
+ // Starting tag.
+ tagStack.addLast(tag);
+ }
+ }
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/docgen/DocgenConsts.java b/src/main/java/com/google/devtools/build/docgen/DocgenConsts.java
new file mode 100644
index 0000000..a2e7583
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/docgen/DocgenConsts.java
@@ -0,0 +1,169 @@
+// Copyright 2014 Google Inc. 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.docgen;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.util.FileType;
+import com.google.devtools.build.lib.util.FileTypeSet;
+
+import java.util.Map;
+import java.util.regex.Pattern;
+
+/**
+ * All the constants for the Docgen project.
+ */
+public class DocgenConsts {
+
+ public static final String LS = "\n";
+
+ public static final String HEADER_TEMPLATE = "templates/be-header.html";
+ public static final String FOOTER_TEMPLATE = "templates/be-footer.html";
+ public static final String BODY_TEMPLATE = "templates/be-body.html";
+ public static final String SKYLARK_BODY_TEMPLATE = "templates/skylark-body.html";
+
+ public static final String VAR_LEFT_PANEL = "LEFT_PANEL";
+
+ public static final String VAR_SECTION_BINARY = "SECTION_BINARY";
+ public static final String VAR_SECTION_LIBRARY = "SECTION_LIBRARY";
+ public static final String VAR_SECTION_TEST = "SECTION_TEST";
+ public static final String VAR_SECTION_GENERATE = "SECTION_GENERATE";
+ public static final String VAR_SECTION_OTHER = "SECTION_OTHER";
+
+ public static final String VAR_IMPLICIT_OUTPUTS = "IMPLICIT_OUTPUTS";
+ public static final String VAR_ATTRIBUTE_SIGNATURE = "ATTRIBUTE_SIGNATURE";
+ public static final String VAR_ATTRIBUTE_DEFINITION = "ATTRIBUTE_DEFINITION";
+ public static final String VAR_NAME = "NAME";
+ public static final String VAR_HEADER_TABLE = "HEADER_TABLE";
+ public static final String VAR_COMMON_ATTRIBUTE_DEFINITION = "COMMON_ATTRIBUTE_DEFINITION";
+ public static final String VAR_TEST_ATTRIBUTE_DEFINITION = "TEST_ATTRIBUTE_DEFINITION";
+ public static final String VAR_BINARY_ATTRIBUTE_DEFINITION = "BINARY_ATTRIBUTE_DEFINITION";
+ public static final String VAR_SYNOPSIS = "SYNOPSIS";
+
+ public static final String VAR_SECTION_SKYLARK_BUILTIN = "SECTION_BUILTIN";
+
+ public static final String COMMON_ATTRIBUTES = "common";
+ public static final String TEST_ATTRIBUTES = "test";
+ public static final String BINARY_ATTRIBUTES = "binary";
+
+ /**
+ * Mark the attribute as deprecated in the Build Encyclopedia.
+ */
+ public static final String FLAG_DEPRECATED = "DEPRECATED";
+ public static final String FLAG_GENERIC_RULE = "GENERIC_RULE";
+
+ public static final String HEADER_COMMENT =
+ "<!DOCTYPE html>\n"
+ + "<!--\n"
+ + " This document is synchronized with Blaze releases.\n"
+ + " To edit, submit changes to the Blaze source code.\n"
+ + " Generated by: blaze build java/com/google/devtools/build/docgen:build-encyclopedia.html\n"
+ + "-->\n";
+
+ public static final String BUILD_ENCYCLOPEDIA_NAME = "build-encyclopedia.html";
+
+ public static final FileTypeSet JAVA_SOURCE_FILE_SUFFIX = FileTypeSet.of(FileType.of(".java"));
+
+ public static final String META_KEY_NAME = "NAME";
+ public static final String META_KEY_TYPE = "TYPE";
+ public static final String META_KEY_FAMILY = "FAMILY";
+
+ /**
+ * Types a rule can have (Binary, Library, Test or Other).
+ */
+ public static enum RuleType {
+ BINARY, LIBRARY, TEST, OTHER
+ }
+
+ /**
+ * i.e. <!-- #BLAZE_RULE(NAME = RULE_NAME, TYPE = RULE_TYPE, FAMILY = RULE_FAMILY) -->
+ * i.e. <!-- #BLAZE_RULE(...)[DEPRECATED] -->
+ */
+ public static final Pattern BLAZE_RULE_START = Pattern.compile(
+ "^[\\s]*/\\*[\\s]*\\<!\\-\\-[\\s]*#BLAZE_RULE[\\s]*\\(([\\w\\s=,+/()-]+)\\)"
+ + "(\\[[\\w,]+\\])?[\\s]*\\-\\-\\>");
+ /**
+ * i.e. <!-- #END_BLAZE_RULE -->
+ */
+ public static final Pattern BLAZE_RULE_END = Pattern.compile(
+ "^[\\s]*\\<!\\-\\-[\\s]*#END_BLAZE_RULE[\\s]*\\-\\-\\>[\\s]*\\*/");
+ /**
+ * i.e. <!-- #BLAZE_RULE.EXAMPLE -->
+ */
+ public static final Pattern BLAZE_RULE_EXAMPLE_START = Pattern.compile(
+ "[\\s]*\\<!--[\\s]*#BLAZE_RULE.EXAMPLE[\\s]*--\\>[\\s]*");
+ /**
+ * i.e. <!-- #BLAZE_RULE.END_EXAMPLE -->
+ */
+ public static final Pattern BLAZE_RULE_EXAMPLE_END = Pattern.compile(
+ "[\\s]*\\<!--[\\s]*#BLAZE_RULE.END_EXAMPLE[\\s]*--\\>[\\s]*");
+ /**
+ * i.e. <!-- #BLAZE_RULE(RULE_NAME).VARIABLE_NAME -->
+ */
+ public static final Pattern BLAZE_RULE_VAR_START = Pattern.compile(
+ "^[\\s]*/\\*[\\s]*\\<!\\-\\-[\\s]*#BLAZE_RULE\\(([\\w\\$]+)\\)\\.([\\w]+)[\\s]*\\-\\-\\>");
+ /**
+ * i.e. <!-- #END_BLAZE_RULE.VARIABLE_NAME -->
+ */
+ public static final Pattern BLAZE_RULE_VAR_END = Pattern.compile(
+ "^[\\s]*\\<!\\-\\-[\\s]*#END_BLAZE_RULE\\.([\\w]+)[\\s]*\\-\\-\\>[\\s]*\\*/");
+ /**
+ * i.e. <!-- #BLAZE_RULE(RULE_NAME).ATTRIBUTE(ATTR_NAME) -->
+ * i.e. <!-- #BLAZE_RULE(RULE_NAME).ATTRIBUTE(ATTR_NAME)[DEPRECATED] -->
+ */
+ public static final Pattern BLAZE_RULE_ATTR_START = Pattern.compile(
+ "^[\\s]*/\\*[\\s]*\\<!\\-\\-[\\s]*#BLAZE_RULE\\(([\\w\\$]+)\\)\\."
+ + "ATTRIBUTE\\(([\\w]+)\\)(\\[[\\w,]+\\])?[\\s]*\\-\\-\\>");
+ /**
+ * i.e. <!-- #END_BLAZE_RULE.ATTRIBUTE -->
+ */
+ public static final Pattern BLAZE_RULE_ATTR_END = Pattern.compile(
+ "^[\\s]*\\<!\\-\\-[\\s]*#END_BLAZE_RULE\\.ATTRIBUTE[\\s]*\\-\\-\\>[\\s]*\\*/");
+
+ public static final Pattern BLAZE_RULE_FLAGS = Pattern.compile("^.*\\[(.*)\\].*$");
+
+ public static final Map<String, Integer> ATTRIBUTE_ORDERING = ImmutableMap
+ .<String, Integer>builder()
+ .put("name", -99)
+ .put("deps", -98)
+ .put("src", -97)
+ .put("srcs", -96)
+ .put("data", -95)
+ .put("resource", -94)
+ .put("resources", -93)
+ .put("out", -92)
+ .put("outs", -91)
+ .put("hdrs", -90)
+ .build();
+
+ static String toCommandLineFormat(String cmdDoc) {
+ // Replace html <br> tags with line breaks
+ cmdDoc = cmdDoc.replaceAll("(<br>|<br[\\s]*/>)", "\n") + "\n";
+ // Replace other links <a href=".*">s with human readable links
+ cmdDoc = cmdDoc.replaceAll("\\<a href=\"([^\"]+)\">[^\\<]*\\</a\\>", "$1");
+ // Delete other html tags
+ cmdDoc = cmdDoc.replaceAll("\\<[/]?[^\\>]+\\>", "");
+ // Delete docgen variables
+ cmdDoc = cmdDoc.replaceAll("\\$\\{[\\w_]+\\}", "");
+ // Substitute more than 2 line breaks in a row with 2 line breaks
+ cmdDoc = cmdDoc.replaceAll("[\\n]{2,}", "\n\n");
+ // Ensure that the doc starts and ends with exactly two line breaks
+ cmdDoc = cmdDoc.replaceAll("^[\\n]+", "\n\n");
+ cmdDoc = cmdDoc.replaceAll("[\\n]+$", "\n\n");
+ return cmdDoc;
+ }
+
+ static String removeDuplicatedNewLines(String doc) {
+ return doc.replaceAll("[\\n][\\s]*[\\n]", "\n");
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/docgen/PredefinedAttributes.java b/src/main/java/com/google/devtools/build/docgen/PredefinedAttributes.java
new file mode 100644
index 0000000..c885cc1
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/docgen/PredefinedAttributes.java
@@ -0,0 +1,347 @@
+// Copyright 2014 Google Inc. 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.docgen;
+
+import com.google.common.collect.ImmutableMap;
+
+import java.util.Map;
+
+/**
+ * A class to contain the base definition of common BUILD rule attributes.
+ */
+public class PredefinedAttributes {
+
+ public static final Map<String, RuleDocumentationAttribute> COMMON_ATTRIBUTES = ImmutableMap
+ .<String, RuleDocumentationAttribute>builder()
+ .put("deps", RuleDocumentationAttribute.create("deps", DocgenConsts.COMMON_ATTRIBUTES,
+ "A list of dependencies of this rule.\n"
+ + "<i>(List of <a href=\"build-ref.html#labels\">labels</a>; optional)</i><br/>\n"
+ + "The precise semantics of what it means for this rule to depend on\n"
+ + "another using <code>deps</code> are specific to the kind of this rule,\n"
+ + "and the rule-specific documentation below goes into more detail.\n"
+ + "At a minimum, though, the targets named via <code>deps</code> will\n"
+ + "appear in the <code>*.runfiles</code> area of this rule, if it has\n"
+ + "one.\n"
+ + "<p>Most often, a <code>deps</code> dependency is used to allow one\n"
+ + "module to use symbols defined in another module written in the\n"
+ + "same programming language and separately compiled. Cross-language\n"
+ + "dependencies are also permitted in many cases: for example,\n"
+ + "a <code>java_library</code> rule may depend on C++ code in\n"
+ + "a <code>cc_library</code> rule, by declaring the latter in\n"
+ + "the <code>deps</code> attribute. See the definition\n"
+ + "of <a href=\"build-ref.html#deps\">dependencies</a> for more\n"
+ + "information.</p>\n"
+ + "<p>Almost all rules permit a <code>deps</code> attribute, but where\n"
+ + "this attribute is not allowed, this fact is documented under the\n"
+ + "specific rule.</p>"))
+ .put("data", RuleDocumentationAttribute.create("data", DocgenConsts.COMMON_ATTRIBUTES,
+ "The list of files needed by this rule at runtime.\n"
+ + "<i>(List of <a href=\"build-ref.html#labels\">labels</a>; optional)</i><br/>\n"
+ + "Targets named in the <code>data</code> attribute will appear in\n"
+ + "the <code>*.runfiles</code> area of this rule, if it has one. This\n"
+ + "may include data files needed by a binary or library, or other\n"
+ + "programs needed by it. See the\n"
+ + "<a href=\"build-ref.html#data\">data dependencies</a> section for more\n"
+ + "information about how to depend on and use data files.\n"
+ + "<p>Almost all rules permit a <code>data</code> attribute, but where\n"
+ + "this attribute is not allowed, this fact is documented under the\n"
+ + "specific rule.</p>"))
+ .put("licenses", RuleDocumentationAttribute.create("licenses",
+ DocgenConsts.COMMON_ATTRIBUTES,
+ "<i>(List of strings; optional)</i><br/>\n"
+ + "A list of license-type strings to be used for this particular build rule.\n"
+ + "Overrides the <code>BUILD</code>-file scope defaults defined by the\n"
+ + "<a href=\"#licenses\"><code>licenses()</code></a> directive."))
+ .put("distribs", RuleDocumentationAttribute.create("distribs",
+ DocgenConsts.COMMON_ATTRIBUTES,
+ "<i>(List of strings; optional)</i><br/>\n"
+ + "A list of distribution-method strings to be used for this particular build rule.\n"
+ + "Overrides the <code>BUILD</code>-file scope defaults defined by the\n"
+ + "<a href=\"#distribs\"><code>distribs()</code></a> directive."))
+ .put("deprecation", RuleDocumentationAttribute.create("deprecation",
+ DocgenConsts.COMMON_ATTRIBUTES,
+ "<i>(String; optional)</i><br/>\n"
+ + "An explanatory warning message associated with this rule.\n"
+ + "Typically this is used to notify users that a rule has become obsolete,\n"
+ + "or has become superseded by another rule, is private to a package, or is\n"
+ + "perhaps \"considered harmful\" for some reason. It is a good idea to include\n"
+ + "some reference (like a webpage, a bug number or example migration CLs) so\n"
+ + "that one can easily find out what changes are required to avoid the message.\n"
+ + "If there is a new target that can be used as a drop in replacement, it is a good idea\n"
+ + "to just migrate all users of the old target.\n"
+ + "<p>\n"
+ + "This attribute has no effect on the way things are built, but it\n"
+ + "may affect a build tool's diagnostic output. The build tool issues a\n"
+ + "warning when a rule with a <code>deprecation</code> attribute is\n"
+ + "depended upon by another rule.</p>\n"
+ + "<p>\n"
+ + "Intra-package dependencies are exempt from this warning, so that,\n"
+ + "for example, building the tests of a deprecated rule does not\n"
+ + "encounter a warning.</p>\n"
+ + "<p>\n"
+ + "If a deprecated rule depends on another deprecated rule, no warning\n"
+ + "message is issued.</p>\n"
+ + "<p>\n"
+ + "Once people have stopped using it, the package can be removed or marked as\n"
+ + "<a href=\"#common.obsolete\"><code>obsolete</code></a>.</p>"))
+ .put("obsolete", RuleDocumentationAttribute.create("obsolete",
+ DocgenConsts.COMMON_ATTRIBUTES,
+ "<i>(Boolean; optional; default 0)</i><br/>\n"
+ + "If 1, only obsolete targets can depend on this target. It is an error when\n"
+ + "a non-obsolete target depends on an obsolete target.\n"
+ + "<p>\n"
+ + "As a transition, one can first mark a package as in\n"
+ + "<a href=\"#common.deprecation\"><code>deprecation</code></a>.</p>\n"
+ + "<p>\n"
+ + "This attribute is useful when you want to prevent a target from\n"
+ + "being used but are yet not ready to delete the sources.</p>"))
+ .put("testonly", RuleDocumentationAttribute.create("testonly",
+ DocgenConsts.COMMON_ATTRIBUTES,
+ "<i>(Boolean; optional; default 0 except as noted)</i><br />\n"
+ + "If 1, only testonly targets (such as tests) can depend on this target.\n"
+ + "<p>Equivalently, a rule that is not <code>testonly</code> is not allowed to\n"
+ + "depend on any rule that is <code>testonly</code>.</p>\n"
+ + "<p>Tests (<code>*_test</code> rules)\n"
+ + "and test suites (<a href=\"#test_suite\">test_suite</a> rules)\n"
+ + "are <code>testonly</code> by default.</p>\n"
+ + "<p>By virtue of\n"
+ + "<a href=\"#package.default_testonly\"><code>default_testonly</code></a>,\n"
+ + "targets under <code>javatests</code> are <code>testonly</code> by default.</p>\n"
+ + "<p>This attribute is intended to mean that the target should not be\n"
+ + "contained in binaries that are released to production.</p>\n"
+ + "<p>Because testonly is enforced at build time, not run time, and propagates\n"
+ + "virally through the dependency tree, it should be applied judiciously. For\n"
+ + "example, stubs and fakes that\n"
+ + "are useful for unit tests may also be useful for integration tests\n"
+ + "involving the same binaries that will be released to production, and\n"
+ + "therefore should probably not be marked testonly. Conversely, rules that\n"
+ + "are dangerous to even link in, perhaps because they unconditionally\n"
+ + "override normal behavior, should definitely be marked testonly.</p>"))
+ .put("tags", RuleDocumentationAttribute.create("tags", DocgenConsts.COMMON_ATTRIBUTES,
+ "List of arbitrary text tags. Tags may be any valid string; default is the\n"
+ + "empty list.<br/>\n"
+ + "<i>Tags</i> can be used on any rule; but <i>tags</i> are most useful\n"
+ + "on test and <code>test_suite</code> rules. Tags on non-test rules\n"
+ + "are only useful to humans and/or external programs.\n"
+ + "<i>Tags</i> are generally used to annotate a test's role in your debug\n"
+ + "and release process. Typically, tags are most useful for C++ and\n"
+ + "Python tests, which\n"
+ + "lack any runtime annotation ability. The use of tags and size elements\n"
+ + "gives flexibility in assembling suites of tests based around codebase\n"
+ + "check-in policy.\n"
+ + "<p>\n"
+ + "A few tags have special meaning to the build tool, such as\n"
+ + "indicating that a particular test cannot be run remotely, for\n"
+ + "example. Consult\n"
+ + "the <a href='blaze-user-manual.html#tags_keywords'>Blaze\n"
+ + "documentation</a> for details.\n"
+ + "</p>"))
+ .put("visibility", RuleDocumentationAttribute.create("visibility",
+ DocgenConsts.COMMON_ATTRIBUTES,
+ "<i>(List of <a href=\"build-ref.html#labels\">"
+ + "labels</a>; optional; default private)</i><br/>\n"
+ + "<p>The <code>visibility</code> attribute on a rule controls whether\n"
+ + "the rule can be used by other packages. Rules are always visible to\n"
+ + "other rules declared in the same package.</p>\n"
+ + "<p>There are five forms (and one temporary form) a visibility label can take:\n"
+ + "<ul>\n"
+ + "<li><code>[\"//visibility:public\"]</code>: Anyone can use this rule.</li>\n"
+ + "<li><code>[\"//visibility:private\"]</code>: Only rules in this package\n"
+ + "can use this rule. Rules in <code>javatests/foo/bar</code>\n"
+ + "can always use rules in <code>java/foo/bar</code>.\n"
+ + "</li>\n"
+ + "<li><code>[\"//some/package:__pkg__\", \"//other/package:__pkg__\"]</code>:\n"
+ + "Only rules in <code>some/package</code> and <code>other/package</code>\n"
+ + "(defined in <code>some/package/BUILD</code> and\n"
+ + "<code>other/package/BUILD</code>) have access to this rule. Note that\n"
+ + "sub-packages do not have access to the rule; for example,\n"
+ + "<code>//some/package/foo:bar</code> or\n"
+ + "<code>//other/package/testing:bla</code> wouldn't have access.\n"
+ + "<code>__pkg__</code> is a special target and must be used verbatim.\n"
+ + "It represents all of the rules in the package.\n"
+ + "</li>\n"
+ + "<li><code>[\"//project:__subpackages__\", \"//other:__subpackages__\"]</code>:\n"
+ + "Only rules in packages <code>project</code> or <code>other</code> or\n"
+ + "in one of their sub-packages have access to this rule. For example,\n"
+ + "<code>//project:rule</code>, <code>//project/library:lib</code> or\n"
+ + "<code>//other/testing/internal:munge</code> are allowed to depend on\n"
+ + "this rule (but not <code>//independent:evil</code>)\n"
+ + "</li>\n"
+ + "<li><code>[\"//some/package:my_package_group\"]</code>:\n"
+ + "A <a href=\"#package_group\">package group</a> is\n"
+ + "a named set of package names. Package groups can also grant access rights\n"
+ + "to entire subtrees, e.g.<code>//myproj/...</code>.\n"
+ + "</li>\n"
+ + "<li><code>[\"//visibility:legacy_public\"]</code>: Anyone can use this\n"
+ + "rule (for now). <i>Developer action is needed</i>.\n"
+ + "<p>This value has been used during the transition to the new\n"
+ + "<code>[\"//visibility:private\"]</code> default, on June 6, 2011.\n"
+ + "<i>We will eventually deprecate and then disallow this value.</i>\n"
+ + "</li>\n"
+ + "</ul>\n"
+ + "<p>The visibility specifications of <code>//visibility:public</code>,\n"
+ + "<code>//visibility:private</code> and\n"
+ + "<code>//visibility:legacy_public</code>\n"
+ + "can not be combined with any other visibility specifications.\n"
+ + "A visibility specification may contain a combination of package labels\n"
+ + "(i.e. //foo:__pkg__) and package_groups.</p>\n"
+ + "<p>If a rule does not specify the visibility attribute,\n"
+ + "the <code><a href=\"#package\">default_visibility</a></code>\n"
+ + "attribute of the <code><a href=\"#package\">package</a></code>\n"
+ + "statement in the BUILD file containing the rule is used\n"
+ + "(except <a href=\"#exports_files\">exports_files</a> and\n"
+ + "<a href=\"#cc_public_library\">cc_public_library</a>, which always default to\n"
+ + "public).</p>\n"
+ + "<p>If the default visibility for the package is not specified,\n"
+ + "the rule is private: on June 6, 2011, in order to prevent teams\n"
+ + "from reaching into private code, the default has been changed\n"
+ + "to <code>[\"//visibility:private\"]</code>.</p>\n"
+ + "<p><b>Example</b>:</p>\n"
+ + "<p>\n"
+ + "File <code>//frobber/bin/BUILD</code>:\n"
+ + "</p>\n"
+ + "<pre class=\"code\">\n"
+ + "# This rule is visible to everyone\n"
+ + "py_binary(\n"
+ + " name = \"executable\",\n"
+ + " visibility = [\"//visibility:public\"],\n"
+ + " deps = [\":library\"],\n"
+ + ")\n"
+ + "\n"
+ + "# This rule is visible only to rules declared in the same package\n"
+ + "py_library(\n"
+ + " name = \"library\",\n"
+ + " visibility = [\"//visibility:private\"],\n"
+ + ")\n"
+ + "\n"
+ + "# This rule is visible to rules in package //object and //noun\n"
+ + "py_library(\n"
+ + " name = \"subject\",\n"
+ + " visibility = [\n"
+ + " \"//noun:__pkg__\",\n"
+ + " \"//object:__pkg__\",\n"
+ + " ],\n"
+ + ")\n"
+ + "\n"
+ + "# See package group //frobber:friends (below) for who can access this rule.\n"
+ + "py_library(\n"
+ + " name = \"thingy\",\n"
+ + " visibility = [\"//frobber:friends\"],\n"
+ + ")\n"
+ + "</pre>\n"
+ + "<p>\n"
+ + "File <code>//frobber/BUILD</code>:\n"
+ + "</p>\n"
+ + "<pre class=\"code\">\n"
+ + "# This is the package group declaration to which rule //frobber/bin:thingy refers.\n"
+ + "#\n"
+ + "# Our friends are packages //frobber, //fribber and any subpackage of //fribber.\n"
+ + "package_group(\n"
+ + " name = \"friends\",\n"
+ + " packages = [\n"
+ + " \"//fribber/...\",\n"
+ + " \"//frobber\",\n"
+ + " ],\n"
+ + ")\n"
+ + "</pre>"))
+ .build();
+
+ public static final Map<String, RuleDocumentationAttribute> BINARY_ATTRIBUTES = ImmutableMap.of(
+ "args", RuleDocumentationAttribute.create("args", DocgenConsts.BINARY_ATTRIBUTES,
+ "Add these arguments to the target when executed by\n"
+ + "<code>blaze run</code>.\n"
+ + "<i>(List of strings; optional; subject to\n"
+ + "<a href=\"#make_variables\">\"Make variable\"</a> substitution and\n"
+ + "<a href=\"#sh-tokenization\">Bourne shell tokenization</a>)</i><br/>\n"
+ + "These arguments are passed to the target before the target options\n"
+ + "specified on the <code>blaze run</code> command line.\n"
+ + "<p>Most binary rules permit an <code>args</code> attribute, but where\n"
+ + "this attribute is not allowed, this fact is documented under the\n"
+ + "specific rule.</p>"),
+ "output_licenses", RuleDocumentationAttribute.create("output_licenses",
+ DocgenConsts.BINARY_ATTRIBUTES,
+ "The licenses of the output files that this binary generates.\n"
+ + "<i>(List of strings; optional)</i><br/>\n"
+ + "Describes the licenses of the output of the binary generated by\n"
+ + "the rule. When a binary is referenced in a host attribute (for\n"
+ + "example, the <code>tools</code> attribute of\n"
+ + "a <code>genrule</code>), this license declaration is used rather\n"
+ + "than the union of the licenses of its transitive closure. This\n"
+ + "argument is useful when a binary is used as a tool during the\n"
+ + "build of a rule, and it is not desirable for its license to leak\n"
+ + "into the license of that rule. If this attribute is missing, the\n"
+ + "license computation proceeds as if the host dependency was a\n"
+ + "regular dependency.\n"
+ + "<p>(For more about the distinction between host and target\n"
+ + "configurations,\n"
+ + "see <a href=\"blaze-user-manual.html#configurations\">"
+ + "Build configurations</a> in the Blaze manual.)\n"
+ + "<p><em class=\"harmful\">WARNING: in some cases (specifically, in\n"
+ + "genrules) the build tool cannot guarantee that the binary\n"
+ + "referenced by this attribute is actually used as a tool, and is\n"
+ + "not, for example, copied to the output. In these cases, it is the\n"
+ + "responsibility of the user to make sure that this is\n"
+ + "true.</em></p>"));
+
+ public static final Map<String, RuleDocumentationAttribute> TEST_ATTRIBUTES = ImmutableMap
+ .<String, RuleDocumentationAttribute>builder()
+ .put("args", RuleDocumentationAttribute.create("args", DocgenConsts.TEST_ATTRIBUTES,
+ "Add these arguments to the <code>--test_arg</code>\n"
+ + "when executed by <code>blaze test</code>.\n"
+ + "<i>(List of strings; optional; subject to\n"
+ + "<a href=\"#make_variables\">\"Make variable\"</a> substitution and\n"
+ + "<a href=\"#sh-tokenization\">Bourne shell tokenization</a>)</i><br/>\n"
+ + "These arguments are passed before the <code>--test_arg</code> values\n"
+ + "specified on the <code>blaze test</code> command line."))
+ .put("size", RuleDocumentationAttribute.create("size", DocgenConsts.TEST_ATTRIBUTES,
+ "How \"heavy\" the test is\n"
+ + "<i>(String \"enormous\", \"large\" \"medium\" or \"small\",\n"
+ + "default is \"medium\")</i><br/>\n"
+ + "A classification of the test's \"heaviness\": how much time/resources\n"
+ + "it needs to run."
+ + "Unittests are considered \"small\", integration tests \"medium\", "
+ + "and end-to-end tests \"large\" or \"enormous\". "
+ + "Blaze uses the size only to determine a default timeout."))
+ .put("timeout", RuleDocumentationAttribute.create("timeout", DocgenConsts.TEST_ATTRIBUTES,
+ "How long the test is\n"
+ + "normally expected to run before returning.\n"
+ + "<i>(String \"eternal\", \"long\", \"moderate\", or \"short\"\n"
+ + "with the default derived from a test's size attribute)</i><br/>\n"
+ + "While a test's size attribute controls resource estimation, a test's\n"
+ + "timeout may be set independently. If not explicitly specified, the\n"
+ + "timeout is based on the test's size (with \"small\" ⇒ \"short\",\n"
+ + "\"medium\" ⇒ \"moderate\", etc...). "
+ + "\"short\" means 1 minute, \"moderate\" 5 minutes, and \"long\" 15 minutes."))
+ .put("flaky", RuleDocumentationAttribute.create("flaky", DocgenConsts.TEST_ATTRIBUTES,
+ "Marks test as flaky. <i>(Boolean; optional)</i><br/>\n"
+ + "If set, executes the test up to 3 times before being declared as failed.\n"
+ + "By default this attribute is set to 0 and test is considered to be stable.\n"
+ + "Note, that use of this attribute is generally discouraged - we do prefer\n"
+ + "all tests to be stable."))
+ .put("shard_count", RuleDocumentationAttribute.create("shard_count",
+ DocgenConsts.TEST_ATTRIBUTES,
+ "Specifies the number of parallel shards\n"
+ + "to use to run the test. <i>(Non-negative integer less than or equal to 50;\n"
+ + "optional)</i><br/>\n"
+ + "This value will override any heuristics used to determine the number of\n"
+ + "parallel shards with which to run the test. Note that for some test\n"
+ + "rules, this parameter may be required to enable sharding\n"
+ + "in the first place. Also see --test_sharding_strategy."))
+ .put("local", RuleDocumentationAttribute.create("local", DocgenConsts.TEST_ATTRIBUTES,
+ "Forces the test to be run locally. <i>(Boolean; optional)</i><br/>\n"
+ + "By default this attribute is set to 0 and the default testing strategy is\n"
+ + "used. This is equivalent to providing \"local\" as a tag\n"
+ + "(<code>tags=[\"local\"]</code>)."))
+ .build();
+}
diff --git a/src/main/java/com/google/devtools/build/docgen/RuleDocumentation.java b/src/main/java/com/google/devtools/build/docgen/RuleDocumentation.java
new file mode 100644
index 0000000..e8835f4
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/docgen/RuleDocumentation.java
@@ -0,0 +1,353 @@
+// Copyright 2014 Google Inc. 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.docgen;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.docgen.DocgenConsts.RuleType;
+import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider;
+import com.google.devtools.build.lib.packages.Attribute;
+import com.google.devtools.build.lib.packages.RuleClass;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.TreeSet;
+
+/**
+ * A class representing the documentation of a rule along with some meta-data. The sole ruleName
+ * field is used as a key for comparison, equals and hashcode.
+ *
+ * <p> The class contains meta information about the rule:
+ * <ul>
+ * <li> Rule type: categorizes the rule based on it's general (language independent) purpose,
+ * see {@link RuleType}.
+ * <li> Rule family: categorizes the rule based on language.
+ * </ul>
+ *
+ * <p> The class also contains physical information about the documentation,
+ * such as declaring file name and the first line of the raw documentation. This can be useful for
+ * proper error signaling during documentation processing.
+ */
+class RuleDocumentation implements Comparable<RuleDocumentation> {
+
+ private final String ruleName;
+ private final RuleType ruleType;
+ private final String ruleFamily;
+ private final String htmlDocumentation;
+ // Store these information for error messages
+ private final int startLineCount;
+ private final String fileName;
+ private final ImmutableSet<String> flags;
+
+ private final Map<String, String> docVariables = new HashMap<>();
+ // Only one attribute per attributeName is allowed
+ private final Set<RuleDocumentationAttribute> attributes = new TreeSet<>();
+ private final ConfiguredRuleClassProvider ruleClassProvider;
+
+ /**
+ * Creates a RuleDocumentation from the rule's name, type, family and raw html documentation
+ * (meaning without expanding the variables in the doc).
+ */
+ RuleDocumentation(String ruleName, String ruleType, String ruleFamily,
+ String htmlDocumentation, int startLineCount, String fileName, ImmutableSet<String> flags,
+ ConfiguredRuleClassProvider ruleClassProvider)
+ throws BuildEncyclopediaDocException {
+ Preconditions.checkNotNull(ruleName);
+ this.ruleName = ruleName;
+ try {
+ this.ruleType = RuleType.valueOf(ruleType);
+ } catch (IllegalArgumentException e) {
+ throw new BuildEncyclopediaDocException(
+ fileName, startLineCount, "Invalid rule type " + ruleType);
+ }
+ this.ruleFamily = ruleFamily;
+ this.htmlDocumentation = htmlDocumentation;
+ this.startLineCount = startLineCount;
+ this.fileName = fileName;
+ this.flags = flags;
+ this.ruleClassProvider = ruleClassProvider;
+ }
+
+ /**
+ * Returns the name of the rule.
+ */
+ String getRuleName() {
+ return ruleName;
+ }
+
+ /**
+ * Returns the type of the rule
+ */
+ RuleType getRuleType() {
+ return ruleType;
+ }
+
+ /**
+ * Returns the family of the rule. The family is usually the corresponding programming language,
+ * except for rules independent of language, such as genrule. E.g. the family of the java_library
+ * rule is 'JAVA', the family of genrule is 'GENERAL'.
+ */
+ String getRuleFamily() {
+ return ruleFamily;
+ }
+
+ /**
+ * Returns the number of first line of the rule documentation in its declaration file.
+ */
+ int getStartLineCount() {
+ return startLineCount;
+ }
+
+ /**
+ * Returns true if this rule documentation has the parameter flag.
+ */
+ boolean hasFlag(String flag) {
+ return flags.contains(flag);
+ }
+
+ /**
+ * Returns true if this rule applies to a specific programming language (e.g. java_library),
+ * returns false if it is a generic action (e.g. genrule, filegroup).
+ *
+ * A rule is considered to be specific to a programming language by default. Generic rules have
+ * to be marked with the flag GENERIC_RULE in their #BLAZE_RULE definition.
+ */
+ boolean isLanguageSpecific() {
+ return !flags.contains(DocgenConsts.FLAG_GENERIC_RULE);
+ }
+
+ /**
+ * Adds a variable name - value pair to the documentation to be substituted.
+ */
+ void addDocVariable(String varName, String value) {
+ docVariables.put(varName, value);
+ }
+
+ /**
+ * Adds a rule documentation attribute to this rule documentation.
+ */
+ void addAttribute(RuleDocumentationAttribute attribute) {
+ attributes.add(attribute);
+ }
+
+ /**
+ * Returns the html documentation in the exact format it should be written into the Build
+ * Encyclopedia (expanding variables).
+ */
+ String getHtmlDocumentation() {
+ String expandedDoc = htmlDocumentation;
+ // Substituting variables
+ for (Entry<String, String> docVariable : docVariables.entrySet()) {
+ expandedDoc = expandedDoc.replace("${" + docVariable.getKey() + "}",
+ expandBuiltInVariables(docVariable.getKey(), docVariable.getValue()));
+ }
+ expandedDoc = expandedDoc.replace("${" + DocgenConsts.VAR_ATTRIBUTE_SIGNATURE + "}",
+ generateAttributeSignatures());
+ expandedDoc = expandedDoc.replace("${" + DocgenConsts.VAR_ATTRIBUTE_DEFINITION + "}",
+ generateAttributeDefinitions(true));
+ return String.format("<h3 id=\"%s\"%s>%s</h3>\n\n%s", ruleName,
+ getDeprecatedString(hasFlag(DocgenConsts.FLAG_DEPRECATED)), ruleName, expandedDoc);
+ }
+
+ /**
+ * Returns the documentation of the rule in a form which is printable on the command line.
+ */
+ String getCommandLineDocumentation() {
+ return "\n" + DocgenConsts.toCommandLineFormat(htmlDocumentation);
+ }
+
+ /**
+ * Returns the html code of the attribute definitions without the header and name
+ * attribute of the rule.
+ */
+ String generateAttributeDefinitions() {
+ return generateAttributeDefinitions(false);
+ }
+
+ private String generateAttributeDefinitions(boolean generateNameAndHeader) {
+ StringBuilder sb = new StringBuilder();
+ if (generateNameAndHeader){
+ String nameExtraHtmlDoc = docVariables.containsKey(DocgenConsts.VAR_NAME)
+ ? docVariables.get(DocgenConsts.VAR_NAME) : "";
+ sb.append(String.format(Joiner.on('\n').join(new String[] {
+ "<h4 id=\"%s_args\">Arguments</h4>",
+ "<ul>",
+ "<li id=\"%s.name\"><code>name</code>: A unique name for this rule.",
+ "<i>(<a href=\"build-ref.html#name\">Name</a>; required)</i>%s</li>\n"}),
+ ruleName, ruleName, nameExtraHtmlDoc));
+ } else {
+ sb.append("<ul>\n");
+ }
+ for (RuleDocumentationAttribute attributeDoc : attributes) {
+ // Only generate attribute documentation here if the rule and the attribute is
+ // either both user defined or built in (of common type).
+ if (isCommonType() == attributeDoc.isCommonType()) {
+ String attrName = attributeDoc.getAttributeName();
+ Attribute attribute = isCommonType() ? null
+ : ruleClassProvider.getRuleClassMap().get(ruleName).getAttributeByName(attrName);
+ sb.append(String.format("<li id=\"%s.%s\"%s><code>%s</code>:\n%s</li>\n",
+ ruleName.toLowerCase(), attrName, getDeprecatedString(
+ attributeDoc.hasFlag(DocgenConsts.FLAG_DEPRECATED)),
+ attrName, attributeDoc.getHtmlDocumentation(attribute)));
+ }
+ }
+ sb.append("</ul>\n");
+ RuleClass ruleClass = ruleClassProvider.getRuleClassMap().get(ruleName);
+ if (ruleClass != null && ruleClass.isPublicByDefault()) {
+ sb.append(
+ "The default visibility is public: <code>visibility = [\"//visibility:public\"]</code>.");
+ }
+ return sb.toString();
+ }
+
+ private String getDeprecatedString(boolean deprecated) {
+ return deprecated ? " class=\"deprecated\"" : "";
+ }
+
+ private String generateAttributeSignatures() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(String.format(
+ "<p class=\"rule-signature\">\n%s(<a href=\"#%s.name\">name</a>,\n",
+ ruleName, ruleName));
+ int i = 0;
+ for (RuleDocumentationAttribute attributeDoc : attributes) {
+ String attrName = attributeDoc.getAttributeName();
+ // Generate the link for the attribute documentation
+ sb.append(String.format("<a href=\"#%s.%s\">%s</a>",
+ attributeDoc.getGeneratedInRule(ruleName).toLowerCase(), attrName, attrName));
+ if (i < attributes.size() - 1) {
+ sb.append(",");
+ } else {
+ sb.append(")");
+ }
+ sb.append("\n");
+ i++;
+ }
+ sb.append("</p>\n");
+ return sb.toString();
+ }
+
+ private String expandBuiltInVariables(String key, String value) {
+ // Some built in BLAZE variables need special handling, e.g. adding headers
+ switch (key) {
+ case DocgenConsts.VAR_IMPLICIT_OUTPUTS:
+ return String.format("<h4 id=\"%s_implicit_outputs\">Implicit output targets</h4>\n%s",
+ ruleName.toLowerCase(), value);
+ default:
+ return value;
+ }
+ }
+
+ /**
+ * Returns a set of examples based on markups which can be used as BUILD file
+ * contents for testing.
+ */
+ Set<String> extractExamples() throws BuildEncyclopediaDocException {
+ String[] lines = htmlDocumentation.split(DocgenConsts.LS);
+ Set<String> examples = new HashSet<>();
+ StringBuilder sb = null;
+ boolean inExampleCode = false;
+ int lineCount = 0;
+ for (String line : lines) {
+ if (!inExampleCode) {
+ if (DocgenConsts.BLAZE_RULE_EXAMPLE_START.matcher(line).matches()) {
+ inExampleCode = true;
+ sb = new StringBuilder();
+ } else if (DocgenConsts.BLAZE_RULE_EXAMPLE_END.matcher(line).matches()) {
+ throw new BuildEncyclopediaDocException(fileName, startLineCount + lineCount,
+ "No matching start example tag (#BLAZE_RULE.EXAMPLE) for end example tag.");
+ }
+ } else {
+ if (DocgenConsts.BLAZE_RULE_EXAMPLE_END.matcher(line).matches()) {
+ inExampleCode = false;
+ examples.add(sb.toString());
+ sb = null;
+ } else if (DocgenConsts.BLAZE_RULE_EXAMPLE_START.matcher(line).matches()) {
+ throw new BuildEncyclopediaDocException(fileName, startLineCount + lineCount,
+ "No start example tags (#BLAZE_RULE.EXAMPLE) in a row.");
+ } else {
+ sb.append(line + DocgenConsts.LS);
+ }
+ }
+ lineCount++;
+ }
+ return examples;
+ }
+
+ /**
+ * Return true if the rule doesn't belong to a specific rule family.
+ */
+ private boolean isCommonType() {
+ return ruleFamily == null;
+ }
+
+ /**
+ * Creates a BuildEncyclopediaDocException with the file containing this rule doc and
+ * the number of the first line (where the rule doc is defined). Can be used to create
+ * general BuildEncyclopediaDocExceptions about this rule.
+ */
+ BuildEncyclopediaDocException createException(String msg) {
+ return new BuildEncyclopediaDocException(fileName, startLineCount, msg);
+ }
+
+ @Override
+ public int hashCode() {
+ return ruleName.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof RuleDocumentation)) {
+ return false;
+ }
+ return ruleName.equals(((RuleDocumentation) obj).ruleName);
+ }
+
+ private int getTypePriority() {
+ switch (ruleType) {
+ case BINARY:
+ return 1;
+ case LIBRARY:
+ return 2;
+ case TEST:
+ return 3;
+ case OTHER:
+ return 4;
+ }
+ throw new IllegalArgumentException("Illegal value of ruleType: " + ruleType);
+ }
+
+ @Override
+ public int compareTo(RuleDocumentation o) {
+ if (this.getTypePriority() < o.getTypePriority()) {
+ return -1;
+ } else if (this.getTypePriority() > o.getTypePriority()) {
+ return 1;
+ } else {
+ return this.ruleName.compareTo(o.ruleName);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s (TYPE = %s, FAMILY = %s)", ruleName, ruleType, ruleFamily);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/docgen/RuleDocumentationAttribute.java b/src/main/java/com/google/devtools/build/docgen/RuleDocumentationAttribute.java
new file mode 100644
index 0000000..0bdc1f1
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/docgen/RuleDocumentationAttribute.java
@@ -0,0 +1,248 @@
+// Copyright 2014 Google Inc. 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.docgen;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.analysis.BlazeRule;
+import com.google.devtools.build.lib.analysis.RuleDefinition;
+import com.google.devtools.build.lib.packages.Attribute;
+import com.google.devtools.build.lib.packages.TriState;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.syntax.Label;
+
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A class storing a rule attribute documentation along with some meta information.
+ * The class provides functionality to compute the ancestry level of this attribute's
+ * generator rule definition class compared to other rule definition classes.
+ *
+ * <p>Warning, two RuleDocumentationAttribute objects are equal based on only the attributeName.
+ */
+class RuleDocumentationAttribute implements Comparable<RuleDocumentationAttribute> {
+
+ private static final Map<Type<?>, String> TYPE_DESC = ImmutableMap.<Type<?>, String>builder()
+ .put(Type.BOOLEAN, "Boolean")
+ .put(Type.INTEGER, "Integer")
+ .put(Type.INTEGER_LIST, "List of Integer")
+ .put(Type.STRING, "String")
+ .put(Type.STRING_LIST, "List of String")
+ .put(Type.TRISTATE, "Integer")
+ .put(Type.LABEL, "<a href=\"build-ref.html#labels\">Label</a>")
+ .put(Type.LABEL_LIST, "List of <a href=\"build-ref.html#labels\">labels</a>")
+ .put(Type.NODEP_LABEL, "<a href=\"build-ref.html#name\">Name</a>")
+ .put(Type.NODEP_LABEL_LIST, "List of <a href=\"build-ref.html#name\">names</a>")
+ .put(Type.OUTPUT, "<a href=\"build-ref.html#filename\">Filename</a>")
+ .put(Type.OUTPUT_LIST, "List of <a href=\"build-ref.html#filename\">filenames</a>")
+ .build();
+
+ private final Class<? extends RuleDefinition> definitionClass;
+ private final String attributeName;
+ private final String htmlDocumentation;
+ private final String commonType;
+ private int startLineCnt;
+ private Set<String> flags;
+
+ /**
+ * Creates common RuleDocumentationAttribute such as deps or data.
+ * These attribute docs have no definitionClass or htmlDocumentation (it's in the BE header).
+ */
+ static RuleDocumentationAttribute create(
+ String attributeName, String commonType, String htmlDocumentation) {
+ RuleDocumentationAttribute docAttribute = new RuleDocumentationAttribute(
+ null, attributeName, htmlDocumentation, 0, ImmutableSet.<String>of(), commonType);
+ return docAttribute;
+ }
+
+ /**
+ * Creates a RuleDocumentationAttribute with all the necessary fields for explicitly
+ * defined rule attributes.
+ */
+ static RuleDocumentationAttribute create(Class<? extends RuleDefinition> definitionClass,
+ String attributeName, String htmlDocumentation, int startLineCnt, Set<String> flags) {
+ return new RuleDocumentationAttribute(definitionClass, attributeName, htmlDocumentation,
+ startLineCnt, flags, null);
+ }
+
+ private RuleDocumentationAttribute(Class<? extends RuleDefinition> definitionClass,
+ String attributeName, String htmlDocumentation, int startLineCnt, Set<String> flags,
+ String commonType) {
+ Preconditions.checkNotNull(attributeName, "AttributeName must not be null.");
+ this.definitionClass = definitionClass;
+ this.attributeName = attributeName;
+ this.htmlDocumentation = htmlDocumentation;
+ this.startLineCnt = startLineCnt;
+ this.flags = flags;
+ this.commonType = commonType;
+ }
+
+ /**
+ * Returns the name of the rule attribute.
+ */
+ String getAttributeName() {
+ return attributeName;
+ }
+
+ /**
+ * Returns the raw html documentation of the rule attribute.
+ */
+ String getHtmlDocumentation(Attribute attribute) {
+ // TODO(bazel-team): this is needed for common type attributes. Fix those and remove this.
+ if (attribute == null) {
+ return htmlDocumentation;
+ }
+ StringBuilder sb = new StringBuilder()
+ .append("<i>(")
+ .append(TYPE_DESC.get(attribute.getType()))
+ .append("; " + (attribute.isMandatory() ? "required" : "optional"))
+ .append(getDefaultValue(attribute))
+ .append(")</i><br/>\n");
+ return htmlDocumentation.replace("${" + DocgenConsts.VAR_SYNOPSIS + "}", sb.toString());
+ }
+
+ private String getDefaultValue(Attribute attribute) {
+ String prefix = "; default is ";
+ Object value = attribute.getDefaultValueForTesting();
+ if (value instanceof Boolean) {
+ return prefix + ((Boolean) value ? "1" : "0");
+ } else if (value instanceof Integer) {
+ return prefix + String.valueOf(value);
+ } else if (value instanceof String && !(((String) value).isEmpty())) {
+ return prefix + "\"" + value + "\"";
+ } else if (value instanceof TriState) {
+ switch((TriState) value) {
+ case AUTO:
+ return prefix + "-1";
+ case NO:
+ return prefix + "0";
+ case YES:
+ return prefix + "1";
+ }
+ } else if (value instanceof Label) {
+ return prefix + "<code>" + value + "</code>";
+ }
+ return "";
+ }
+
+ /**
+ * Returns the number of first line of the attribute documentation in its declaration file.
+ */
+ int getStartLineCnt() {
+ return startLineCnt;
+ }
+
+ /**
+ * Returns true if the attribute doc is of a common attribute type.
+ */
+ boolean isCommonType() {
+ return commonType != null;
+ }
+
+ /**
+ * Returns the common attribute type if this attribute doc is of a common type
+ * otherwise actualRule.
+ */
+ String getGeneratedInRule(String actualRule) {
+ return isCommonType() ? commonType : actualRule;
+ }
+
+ /**
+ * Returns true if this attribute documentation has the parameter flag.
+ */
+ boolean hasFlag(String flag) {
+ return flags.contains(flag);
+ }
+
+ /**
+ * Returns the length of a shortest path from usingClass to the definitionClass of this
+ * RuleDocumentationAttribute in the rule definition ancestry graph. Returns -1
+ * if definitionClass is not the ancestor (transitively) of usingClass.
+ */
+ int getDefinitionClassAncestryLevel(Class<? extends RuleDefinition> usingClass) {
+ if (usingClass.equals(definitionClass)) {
+ return 0;
+ }
+ // Storing nodes (rule class definitions) with the length of the shortest path from usingClass
+ Map<Class<? extends RuleDefinition>, Integer> visited = new HashMap<>();
+ LinkedList<Class<? extends RuleDefinition>> toVisit = new LinkedList<>();
+ visited.put(usingClass, 0);
+ toVisit.add(usingClass);
+ // Searching the shortest path from usingClass to this.definitionClass using BFS
+ do {
+ Class<? extends RuleDefinition> ancestor = toVisit.removeFirst();
+ visitAncestor(ancestor, visited, toVisit);
+ if (ancestor.equals(definitionClass)) {
+ return visited.get(ancestor);
+ }
+ } while (!toVisit.isEmpty());
+ return -1;
+ }
+
+ private void visitAncestor(
+ Class<? extends RuleDefinition> usingClass,
+ Map<Class<? extends RuleDefinition>, Integer> visited,
+ LinkedList<Class<? extends RuleDefinition>> toVisit) {
+ BlazeRule ann = usingClass.getAnnotation(BlazeRule.class);
+ if (ann != null) {
+ for (Class<? extends RuleDefinition> ancestor : ann.ancestors()) {
+ if (!visited.containsKey(ancestor)) {
+ toVisit.addLast(ancestor);
+ visited.put(ancestor, visited.get(usingClass) + 1);
+ }
+ }
+ }
+ }
+
+ private int getAttributeOrderingPriority(RuleDocumentationAttribute attribute) {
+ if (DocgenConsts.ATTRIBUTE_ORDERING.containsKey(attribute.attributeName)) {
+ return DocgenConsts.ATTRIBUTE_ORDERING.get(attribute.attributeName);
+ } else {
+ return 0;
+ }
+ }
+
+ @Override
+ public int compareTo(RuleDocumentationAttribute o) {
+ int thisPriority = getAttributeOrderingPriority(this);
+ int otherPriority = getAttributeOrderingPriority(o);
+ if (thisPriority > otherPriority) {
+ return 1;
+ } else if (thisPriority < otherPriority) {
+ return -1;
+ } else {
+ return this.attributeName.compareTo(o.attributeName);
+ }
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof RuleDocumentationAttribute)) {
+ return false;
+ }
+ return attributeName.equals(((RuleDocumentationAttribute) obj).attributeName);
+ }
+
+ @Override
+ public int hashCode() {
+ return attributeName.hashCode();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/docgen/RuleDocumentationVariable.java b/src/main/java/com/google/devtools/build/docgen/RuleDocumentationVariable.java
new file mode 100644
index 0000000..21cf9ec
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/docgen/RuleDocumentationVariable.java
@@ -0,0 +1,50 @@
+// Copyright 2014 Google Inc. 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.docgen;
+
+/**
+ * Rule documentation variables for modular rule documentation, e.g.
+ * separate section for Implicit outputs.
+ */
+public class RuleDocumentationVariable {
+
+ private String ruleName;
+ private String variableName;
+ private String value;
+ private int startLineCnt;
+
+ public RuleDocumentationVariable(
+ String ruleName, String variableName, String value, int startLineCnt) {
+ this.ruleName = ruleName;
+ this.variableName = variableName;
+ this.value = value;
+ this.startLineCnt = startLineCnt;
+ }
+
+ public String getRuleName() {
+ return ruleName;
+ }
+
+ public String getVariableName() {
+ return variableName;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public int getStartLineCnt() {
+ return startLineCnt;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/docgen/SkylarkDocumentationGenerator.java b/src/main/java/com/google/devtools/build/docgen/SkylarkDocumentationGenerator.java
new file mode 100644
index 0000000..c4f8cf3
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/docgen/SkylarkDocumentationGenerator.java
@@ -0,0 +1,51 @@
+// Copyright 2014 Google Inc. 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.docgen;
+
+
+/**
+ * The main class for the skylark documentation generator.
+ */
+public class SkylarkDocumentationGenerator {
+
+ private static boolean checkArgs(String[] args) {
+ if (args.length < 1) {
+ System.err.println("There has to be one input parameter\n"
+ + " - an output file.");
+ return false;
+ }
+ return true;
+ }
+
+ private static void fail(Throwable e, boolean printStackTrace) {
+ System.err.println("ERROR: " + e.getMessage());
+ if (printStackTrace) {
+ e.printStackTrace();
+ }
+ Runtime.getRuntime().exit(1);
+ }
+
+ public static void main(String[] args) {
+ if (checkArgs(args)) {
+ System.out.println("Generating Skylark documentation...");
+ SkylarkDocumentationProcessor processor = new SkylarkDocumentationProcessor();
+ try {
+ processor.generateDocumentation(args[0]);
+ } catch (Throwable e) {
+ fail(e, true);
+ }
+ System.out.println("Finished.");
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/docgen/SkylarkDocumentationProcessor.java b/src/main/java/com/google/devtools/build/docgen/SkylarkDocumentationProcessor.java
new file mode 100644
index 0000000..d2208ea
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/docgen/SkylarkDocumentationProcessor.java
@@ -0,0 +1,437 @@
+// Copyright 2014 Google Inc. 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.docgen;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.docgen.SkylarkJavaInterfaceExplorer.SkylarkBuiltinMethod;
+import com.google.devtools.build.docgen.SkylarkJavaInterfaceExplorer.SkylarkJavaMethod;
+import com.google.devtools.build.docgen.SkylarkJavaInterfaceExplorer.SkylarkModuleDoc;
+import com.google.devtools.build.lib.packages.MethodLibrary;
+import com.google.devtools.build.lib.rules.SkylarkModules;
+import com.google.devtools.build.lib.rules.SkylarkRuleContext;
+import com.google.devtools.build.lib.syntax.Environment;
+import com.google.devtools.build.lib.syntax.Environment.NoneType;
+import com.google.devtools.build.lib.syntax.EvalUtils;
+import com.google.devtools.build.lib.syntax.SkylarkBuiltin;
+import com.google.devtools.build.lib.syntax.SkylarkBuiltin.Param;
+import com.google.devtools.build.lib.syntax.SkylarkCallable;
+import com.google.devtools.build.lib.syntax.SkylarkModule;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.TreeMap;
+
+/**
+ * A class to assemble documentation for Skylark.
+ */
+public class SkylarkDocumentationProcessor {
+
+ private static final String TOP_LEVEL_ID = "_top_level";
+
+ private static final boolean USE_TEMPLATE = false;
+
+ @SkylarkModule(name = "Global objects, functions and modules",
+ doc = "Objects, functions and modules registered in the global environment.")
+ private static final class TopLevelModule {}
+
+ static SkylarkModule getTopLevelModule() {
+ return TopLevelModule.class.getAnnotation(SkylarkModule.class);
+ }
+
+ /**
+ * Generates the Skylark documentation to the given output directory.
+ */
+ public void generateDocumentation(String outputPath) throws IOException,
+ BuildEncyclopediaDocException {
+ BufferedWriter bw = null;
+ File skylarkDocPath = new File(outputPath);
+ try {
+ bw = new BufferedWriter(new FileWriter(skylarkDocPath));
+ if (USE_TEMPLATE) {
+ bw.write(SourceFileReader.readTemplateContents(DocgenConsts.SKYLARK_BODY_TEMPLATE,
+ ImmutableMap.<String, String>of(
+ DocgenConsts.VAR_SECTION_SKYLARK_BUILTIN, generateAllBuiltinDoc())));
+ } else {
+ bw.write(generateAllBuiltinDoc());
+ }
+ System.out.println("Skylark documentation generated: " + skylarkDocPath.getAbsolutePath());
+ } finally {
+ if (bw != null) {
+ bw.close();
+ }
+ }
+ }
+
+ @VisibleForTesting
+ Map<String, SkylarkModuleDoc> collectModules() {
+ Map<String, SkylarkModuleDoc> modules = new TreeMap<>();
+ Map<String, SkylarkModuleDoc> builtinModules = collectBuiltinModules();
+ Map<SkylarkModule, Class<?>> builtinJavaObjects = collectBuiltinJavaObjects();
+
+ modules.putAll(builtinModules);
+ SkylarkJavaInterfaceExplorer explorer = new SkylarkJavaInterfaceExplorer();
+ for (SkylarkModuleDoc builtinObject : builtinModules.values()) {
+ // Check the return type for built-in functions, it can be a module previously not added.
+ for (SkylarkBuiltinMethod builtinMethod : builtinObject.getBuiltinMethods().values()) {
+ Class<?> type = builtinMethod.annotation.returnType();
+ if (type.isAnnotationPresent(SkylarkModule.class)) {
+ explorer.collect(type.getAnnotation(SkylarkModule.class), type, modules);
+ }
+ }
+ explorer.collect(builtinObject.getAnnotation(), builtinObject.getClassObject(), modules);
+ }
+ for (Entry<SkylarkModule, Class<?>> builtinModule : builtinJavaObjects.entrySet()) {
+ explorer.collect(builtinModule.getKey(), builtinModule.getValue(), modules);
+ }
+ return modules;
+ }
+
+ private String generateAllBuiltinDoc() {
+ Map<String, SkylarkModuleDoc> modules = collectModules();
+
+ StringBuilder sb = new StringBuilder();
+ // Generate the top level module first in the doc
+ SkylarkModuleDoc topLevelModule = modules.remove(getTopLevelModule().name());
+ generateModuleDoc(topLevelModule, sb);
+ for (SkylarkModuleDoc module : modules.values()) {
+ if (!module.getAnnotation().hidden()) {
+ sb.append("<hr>");
+ generateModuleDoc(module, sb);
+ }
+ }
+ return sb.toString();
+ }
+
+ private void generateModuleDoc(SkylarkModuleDoc module, StringBuilder sb) {
+ SkylarkModule annotation = module.getAnnotation();
+ sb.append(String.format("<h2 id=\"modules.%s\">%s</h2>\n",
+ getModuleId(annotation),
+ annotation.name()))
+ .append(annotation.doc())
+ .append("\n");
+ sb.append("<ul>");
+ // Sort Java and SkylarkBuiltin methods together. The map key is only used for sorting.
+ TreeMap<String, Object> methodMap = new TreeMap<>();
+ for (SkylarkJavaMethod method : module.getJavaMethods()) {
+ methodMap.put(method.name + method.method.getParameterTypes().length, method);
+ }
+ for (SkylarkBuiltinMethod builtin : module.getBuiltinMethods().values()) {
+ methodMap.put(builtin.annotation.name(), builtin);
+ }
+ for (Object object : methodMap.values()) {
+ if (object instanceof SkylarkJavaMethod) {
+ SkylarkJavaMethod method = (SkylarkJavaMethod) object;
+ generateDirectJavaMethodDoc(annotation.name(), method.name, method.method,
+ method.callable, sb);
+ }
+ if (object instanceof SkylarkBuiltinMethod) {
+ generateBuiltinItemDoc(getModuleId(annotation), (SkylarkBuiltinMethod) object, sb);
+ }
+ }
+ sb.append("</ul>");
+ }
+
+ private String getModuleId(SkylarkModule annotation) {
+ if (annotation == getTopLevelModule()) {
+ return TOP_LEVEL_ID;
+ } else {
+ return annotation.name();
+ }
+ }
+
+ private void generateBuiltinItemDoc(
+ String moduleId, SkylarkBuiltinMethod method, StringBuilder sb) {
+ SkylarkBuiltin annotation = method.annotation;
+ if (annotation.hidden()) {
+ return;
+ }
+ sb.append(String.format("<li><h3 id=\"modules.%s.%s\">%s</h3>\n",
+ moduleId,
+ annotation.name(),
+ annotation.name()));
+
+ if (com.google.devtools.build.lib.syntax.Function.class.isAssignableFrom(method.fieldClass)) {
+ sb.append(getSignature(moduleId, annotation));
+ } else {
+ if (!annotation.returnType().equals(Object.class)) {
+ sb.append("<code>" + getTypeAnchor(annotation.returnType()) + "</code><br>");
+ }
+ }
+
+ sb.append(annotation.doc() + "\n");
+ printParams(moduleId, annotation, sb);
+ }
+
+ private void printParams(String moduleId, SkylarkBuiltin annotation, StringBuilder sb) {
+ if (annotation.mandatoryParams().length + annotation.optionalParams().length > 0) {
+ sb.append("<h4>Parameters</h4>\n");
+ printParams(moduleId, annotation.name(), annotation.mandatoryParams(), sb);
+ printParams(moduleId, annotation.name(), annotation.optionalParams(), sb);
+ } else {
+ sb.append("<br/>\n");
+ }
+ }
+
+ private void generateDirectJavaMethodDoc(String objectName, String methodName,
+ Method method, SkylarkCallable annotation, StringBuilder sb) {
+ if (annotation.hidden()) {
+ return;
+ }
+
+ sb.append(String.format("<li><h3 id=\"modules.%s.%s\">%s</h3>\n%s\n",
+ objectName,
+ methodName,
+ methodName,
+ getSignature(objectName, methodName, method)))
+ .append(annotation.doc())
+ .append(getReturnTypeExtraMessage(annotation))
+ .append("\n");
+ }
+
+ private String getReturnTypeExtraMessage(SkylarkCallable annotation) {
+ if (annotation.allowReturnNones()) {
+ return " May return <code>None</code>.\n";
+ }
+ return "";
+ }
+
+ private String getSignature(String objectName, String methodName, Method method) {
+ String args = method.getAnnotation(SkylarkCallable.class).structField()
+ ? "" : "(" + getParameterString(method) + ")";
+
+ return String.format("<code>%s %s.%s%s</code><br>",
+ getTypeAnchor(method.getReturnType()), objectName, methodName, args);
+ }
+
+ private String getSignature(String objectName, SkylarkBuiltin method) {
+ List<String> argList = new ArrayList<>();
+ for (Param param : method.mandatoryParams()) {
+ argList.add(param.name());
+ }
+ for (Param param : method.optionalParams()) {
+ argList.add(param.name() + "?");
+ }
+ String args = "(" + Joiner.on(", ").join(argList) + ")";
+ if (!objectName.equals(TOP_LEVEL_ID)) {
+ return String.format("<code>%s %s.%s%s</code><br>\n",
+ getTypeAnchor(method.returnType()), objectName, method.name(), args);
+ } else {
+ return String.format("<code>%s %s%s</code><br>\n",
+ getTypeAnchor(method.returnType()), method.name(), args);
+ }
+ }
+
+ private String getTypeAnchor(Class<?> returnType, Class<?> generic1) {
+ return getTypeAnchor(returnType) + " of " + getTypeAnchor(generic1) + "s";
+ }
+
+ private String getTypeAnchor(Class<?> returnType) {
+ if (returnType.equals(String.class)) {
+ return "<a class=\"anchor\" href=\"#modules.string\">string</a>";
+ } else if (Map.class.isAssignableFrom(returnType)) {
+ return "<a class=\"anchor\" href=\"#modules.dict\">dict</a>";
+ } else if (returnType.equals(Void.TYPE) || returnType.equals(NoneType.class)) {
+ return "<a class=\"anchor\" href=\"#modules." + TOP_LEVEL_ID + ".None\">None</a>";
+ } else if (returnType.isAnnotationPresent(SkylarkModule.class)) {
+ // TODO(bazel-team): this can produce dead links for types don't show up in the doc.
+ // The correct fix is to generate those types (e.g. SkylarkFileType) too.
+ String module = returnType.getAnnotation(SkylarkModule.class).name();
+ return "<a class=\"anchor\" href=\"#modules." + module + "\">" + module + "</a>";
+ } else {
+ return EvalUtils.getDataTypeNameFromClass(returnType);
+ }
+ }
+
+ private String getParameterString(Method method) {
+ return Joiner.on(", ").join(Iterables.transform(
+ ImmutableList.copyOf(method.getParameterTypes()), new Function<Class<?>, String>() {
+ @Override
+ public String apply(Class<?> input) {
+ return getTypeAnchor(input);
+ }
+ }));
+ }
+
+ private void printParams(String moduleId, String methodName,
+ Param[] params, StringBuilder sb) {
+ if (params.length > 0) {
+ sb.append("<ul>\n");
+ for (Param param : params) {
+ String paramType = param.type().equals(Object.class) ? ""
+ : (param.generic1().equals(Object.class)
+ ? " (" + getTypeAnchor(param.type()) + ")"
+ : " (" + getTypeAnchor(param.type(), param.generic1()) + ")");
+ sb.append(String.format("\t<li id=\"modules.%s.%s.%s\"><code>%s%s</code>: ",
+ moduleId,
+ methodName,
+ param.name(),
+ param.name(),
+ paramType))
+ .append(param.doc())
+ .append("\n\t</li>\n");
+ }
+ sb.append("</ul>\n");
+ }
+ }
+
+ private Map<String, SkylarkModuleDoc> collectBuiltinModules() {
+ Map<String, SkylarkModuleDoc> modules = new HashMap<>();
+ collectBuiltinDoc(modules, Environment.class.getDeclaredFields());
+ collectBuiltinDoc(modules, MethodLibrary.class.getDeclaredFields());
+ for (Class<?> moduleClass : SkylarkModules.MODULES) {
+ collectBuiltinDoc(modules, moduleClass.getDeclaredFields());
+ }
+ return modules;
+ }
+
+ private Map<SkylarkModule, Class<?>> collectBuiltinJavaObjects() {
+ Map<SkylarkModule, Class<?>> modules = new HashMap<>();
+ collectBuiltinModule(modules, SkylarkRuleContext.class);
+ return modules;
+ }
+
+ /**
+ * Returns the top level modules and functions with their documentation in a command-line
+ * printable format.
+ */
+ public Map<String, String> collectTopLevelModules() {
+ Map<String, String> modules = new TreeMap<>();
+ for (SkylarkModuleDoc doc : collectBuiltinModules().values()) {
+ if (doc.getAnnotation() == getTopLevelModule()) {
+ for (Map.Entry<String, SkylarkBuiltinMethod> entry : doc.getBuiltinMethods().entrySet()) {
+ if (!entry.getValue().annotation.hidden()) {
+ modules.put(entry.getKey(),
+ DocgenConsts.toCommandLineFormat(entry.getValue().annotation.doc()));
+ }
+ }
+ } else {
+ modules.put(doc.getAnnotation().name(),
+ DocgenConsts.toCommandLineFormat(doc.getAnnotation().doc()));
+ }
+ }
+ return modules;
+ }
+
+ /**
+ * Returns the API doc for the specified Skylark object in a command line printable format,
+ * params[0] identifies either a module or a top-level object, the optional params[1] identifies a
+ * method in the module.<br>
+ * Returns null if no Skylark object is found.
+ */
+ public String getCommandLineAPIDoc(String[] params) {
+ Map<String, SkylarkModuleDoc> modules = collectModules();
+ SkylarkModuleDoc toplevelModuleDoc = modules.get(getTopLevelModule().name());
+ if (modules.containsKey(params[0])) {
+ // Top level module
+ SkylarkModuleDoc module = modules.get(params[0]);
+ if (params.length == 1) {
+ String moduleName = module.getAnnotation().name();
+ StringBuilder sb = new StringBuilder();
+ sb.append(moduleName).append("\n\t").append(module.getAnnotation().doc()).append("\n");
+ // Print the signature of all built-in methods
+ for (SkylarkBuiltinMethod method : module.getBuiltinMethods().values()) {
+ printBuiltinFunctionDoc(moduleName, method.annotation, sb);
+ }
+ // Print all Java methods
+ for (SkylarkJavaMethod method : module.getJavaMethods()) {
+ printJavaFunctionDoc(moduleName, method, sb);
+ }
+ return DocgenConsts.toCommandLineFormat(sb.toString());
+ } else {
+ return getFunctionDoc(module.getAnnotation().name(), params[1], module);
+ }
+ } else if (toplevelModuleDoc.getBuiltinMethods().containsKey(params[0])){
+ // Top level object / function
+ return getFunctionDoc(null, params[0], toplevelModuleDoc);
+ }
+ return null;
+ }
+
+ private String getFunctionDoc(String moduleName, String methodName, SkylarkModuleDoc module) {
+ if (module.getBuiltinMethods().containsKey(methodName)) {
+ // Create the doc for the built-in function
+ SkylarkBuiltinMethod method = module.getBuiltinMethods().get(methodName);
+ StringBuilder sb = new StringBuilder();
+ printBuiltinFunctionDoc(moduleName, method.annotation, sb);
+ printParams(moduleName, method.annotation, sb);
+ return DocgenConsts.removeDuplicatedNewLines(DocgenConsts.toCommandLineFormat(sb.toString()));
+ } else {
+ // Search if there are matching Java functions
+ StringBuilder sb = new StringBuilder();
+ boolean foundMatchingMethod = false;
+ for (SkylarkJavaMethod method : module.getJavaMethods()) {
+ if (method.name.equals(methodName)) {
+ printJavaFunctionDoc(moduleName, method, sb);
+ foundMatchingMethod = true;
+ }
+ }
+ if (foundMatchingMethod) {
+ return DocgenConsts.toCommandLineFormat(sb.toString());
+ }
+ }
+ return null;
+ }
+
+ private void printBuiltinFunctionDoc(
+ String moduleName, SkylarkBuiltin annotation, StringBuilder sb) {
+ if (moduleName != null) {
+ sb.append(moduleName).append(".");
+ }
+ sb.append(annotation.name()).append("\n\t").append(annotation.doc()).append("\n");
+ }
+
+ private void printJavaFunctionDoc(String moduleName, SkylarkJavaMethod method, StringBuilder sb) {
+ sb.append(getSignature(moduleName, method.name, method.method))
+ .append("\t").append(method.callable.doc()).append("\n");
+ }
+
+ private void collectBuiltinModule(
+ Map<SkylarkModule, Class<?>> modules, Class<?> moduleClass) {
+ if (moduleClass.isAnnotationPresent(SkylarkModule.class)) {
+ SkylarkModule skylarkModule = moduleClass.getAnnotation(SkylarkModule.class);
+ modules.put(skylarkModule, moduleClass);
+ }
+ }
+
+ private void collectBuiltinDoc(Map<String, SkylarkModuleDoc> modules, Field[] fields) {
+ for (Field field : fields) {
+ if (field.isAnnotationPresent(SkylarkBuiltin.class)) {
+ SkylarkBuiltin skylarkBuiltin = field.getAnnotation(SkylarkBuiltin.class);
+ Class<?> moduleClass = skylarkBuiltin.objectType();
+ SkylarkModule skylarkModule = moduleClass.equals(Object.class)
+ ? getTopLevelModule()
+ : moduleClass.getAnnotation(SkylarkModule.class);
+ if (!modules.containsKey(skylarkModule.name())) {
+ modules.put(skylarkModule.name(), new SkylarkModuleDoc(skylarkModule, moduleClass));
+ }
+ modules.get(skylarkModule.name()).getBuiltinMethods()
+ .put(skylarkBuiltin.name(), new SkylarkBuiltinMethod(skylarkBuiltin, field.getType()));
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/docgen/SkylarkJavaInterfaceExplorer.java b/src/main/java/com/google/devtools/build/docgen/SkylarkJavaInterfaceExplorer.java
new file mode 100644
index 0000000..8f58f71
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/docgen/SkylarkJavaInterfaceExplorer.java
@@ -0,0 +1,160 @@
+// Copyright 2014 Google Inc. 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.docgen;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.syntax.FuncallExpression;
+import com.google.devtools.build.lib.syntax.SkylarkBuiltin;
+import com.google.devtools.build.lib.syntax.SkylarkCallable;
+import com.google.devtools.build.lib.syntax.SkylarkModule;
+import com.google.devtools.build.lib.util.StringUtilities;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+
+/**
+ * A helper class to collect all the Java objects / methods reachable from Skylark.
+ */
+public class SkylarkJavaInterfaceExplorer {
+ /**
+ * A class representing a Java method callable from Skylark with annotation.
+ */
+ static final class SkylarkJavaMethod {
+ public final String name;
+ public final Method method;
+ public final SkylarkCallable callable;
+
+ private String getName(Method method, SkylarkCallable callable) {
+ return callable.name().isEmpty()
+ ? StringUtilities.toPythonStyleFunctionName(method.getName())
+ : callable.name();
+ }
+
+ SkylarkJavaMethod(Method method, SkylarkCallable callable) {
+ this.name = getName(method, callable);
+ this.method = method;
+ this.callable = callable;
+ }
+ }
+
+ /**
+ * A class representing a Skylark built-in object or method.
+ */
+ static final class SkylarkBuiltinMethod {
+ public final SkylarkBuiltin annotation;
+ public final Class<?> fieldClass;
+
+ public SkylarkBuiltinMethod(SkylarkBuiltin annotation, Class<?> fieldClass) {
+ this.annotation = annotation;
+ this.fieldClass = fieldClass;
+ }
+ }
+
+ /**
+ * A class representing a Skylark built-in object with its {@link SkylarkBuiltin} annotation
+ * and the {@link SkylarkCallable} methods it might have.
+ */
+ static final class SkylarkModuleDoc {
+
+ private final SkylarkModule module;
+ private final Class<?> classObject;
+ private final Map<String, SkylarkBuiltinMethod> builtin;
+ private ArrayList<SkylarkJavaMethod> methods = null;
+
+ SkylarkModuleDoc(SkylarkModule module, Class<?> classObject) {
+ this.module = Preconditions.checkNotNull(module,
+ "Class has to be annotated with SkylarkModule: " + classObject);
+ this.classObject = classObject;
+ this.builtin = new TreeMap<>();
+ }
+
+ SkylarkModule getAnnotation() {
+ return module;
+ }
+
+ Class<?> getClassObject() {
+ return classObject;
+ }
+
+ private boolean javaMethodsNotCollected() {
+ return methods == null;
+ }
+
+ private void setJavaMethods(ArrayList<SkylarkJavaMethod> methods) {
+ this.methods = methods;
+ }
+
+ Map<String, SkylarkBuiltinMethod> getBuiltinMethods() {
+ return builtin;
+ }
+
+ ArrayList<SkylarkJavaMethod> getJavaMethods() {
+ return methods;
+ }
+ }
+
+ /**
+ * Collects and returns all the Java objects reachable in Skylark from (and including)
+ * firstClassObject with the corresponding SkylarkBuiltin annotations.
+ *
+ * <p>Note that the {@link SkylarkBuiltin} annotation for firstClassObject - firstAnnotation -
+ * is also an input parameter, because some top level Skylark built-in objects and methods
+ * are not annotated on the class, but on a field referencing them.
+ */
+ void collect(SkylarkModule firstModule, Class<?> firstClass,
+ Map<String, SkylarkModuleDoc> modules) {
+ Set<Class<?>> processedClasses = new HashSet<>();
+ LinkedList<Class<?>> classesToProcess = new LinkedList<>();
+ Map<Class<?>, SkylarkModule> annotations = new HashMap<>();
+
+ classesToProcess.addLast(firstClass);
+ annotations.put(firstClass, firstModule);
+
+ while (!classesToProcess.isEmpty()) {
+ Class<?> classObject = classesToProcess.removeFirst();
+ SkylarkModule annotation = annotations.get(classObject);
+ processedClasses.add(classObject);
+ if (!modules.containsKey(annotation.name())) {
+ modules.put(annotation.name(), new SkylarkModuleDoc(annotation, classObject));
+ }
+ SkylarkModuleDoc module = modules.get(annotation.name());
+
+ if (module.javaMethodsNotCollected()) {
+ ImmutableMap<Method, SkylarkCallable> methods =
+ FuncallExpression.collectSkylarkMethodsWithAnnotation(classObject);
+ ArrayList<SkylarkJavaMethod> methodList = new ArrayList<>();
+ for (Map.Entry<Method, SkylarkCallable> entry : methods.entrySet()) {
+ methodList.add(new SkylarkJavaMethod(entry.getKey(), entry.getValue()));
+ }
+ module.setJavaMethods(methodList);
+
+ for (Map.Entry<Method, SkylarkCallable> method : methods.entrySet()) {
+ Class<?> returnClass = method.getKey().getReturnType();
+ if (returnClass.isAnnotationPresent(SkylarkModule.class)
+ && !processedClasses.contains(returnClass)) {
+ classesToProcess.addLast(returnClass);
+ annotations.put(returnClass, returnClass.getAnnotation(SkylarkModule.class));
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/docgen/SourceFileReader.java b/src/main/java/com/google/devtools/build/docgen/SourceFileReader.java
new file mode 100644
index 0000000..c17b3f6
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/docgen/SourceFileReader.java
@@ -0,0 +1,322 @@
+// Copyright 2014 Google Inc. 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.docgen;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.LinkedListMultimap;
+import com.google.common.collect.ListMultimap;
+import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.regex.Matcher;
+
+/**
+ * A helper class to read and process documentations for rule classes and attributes
+ * from exactly one java source file.
+ */
+public class SourceFileReader {
+
+ private Collection<RuleDocumentation> ruleDocEntries;
+ private ListMultimap<String, RuleDocumentationAttribute> attributeDocEntries;
+ private final ConfiguredRuleClassProvider ruleClassProvider;
+ private final String javaSourceFilePath;
+
+ public SourceFileReader(
+ ConfiguredRuleClassProvider ruleClassProvider, String javaSourceFilePath) {
+ this.ruleClassProvider = ruleClassProvider;
+ this.javaSourceFilePath = javaSourceFilePath;
+ }
+
+ /**
+ * The handler class of the line read from the text file.
+ */
+ public abstract static class ReadAction {
+
+ // Text file line indexing starts from 1
+ private int lineCnt = 1;
+
+ protected abstract void readLineImpl(String line)
+ throws BuildEncyclopediaDocException, IOException;
+
+ protected int getLineCnt() {
+ return lineCnt;
+ }
+
+ public void readLine(String line)
+ throws BuildEncyclopediaDocException, IOException {
+ readLineImpl(line);
+ lineCnt++;
+ }
+ }
+
+ private static final String LS = DocgenConsts.LS;
+
+ /**
+ * Reads the attribute and rule documentation present in the file represented by
+ * SourceFileReader.javaSourceFilePath. The rule doc variables are added to the rule
+ * documentation (which therefore must be defined in the same file). The attribute docs are
+ * stored in a different class member, so they need to be handled outside this method.
+ */
+ public void readDocsFromComments() throws BuildEncyclopediaDocException, IOException {
+ final Map<String, RuleDocumentation> docMap = new HashMap<>();
+ final List<RuleDocumentationVariable> docVariables = new LinkedList<>();
+ final ListMultimap<String, RuleDocumentationAttribute> docAttributes =
+ LinkedListMultimap.create();
+ readTextFile(javaSourceFilePath, new ReadAction() {
+
+ private boolean inBlazeRuleDocs = false;
+ private boolean inBlazeRuleVarDocs = false;
+ private boolean inBlazeAttributeDocs = false;
+ private StringBuilder sb = new StringBuilder();
+ private String ruleName;
+ private String ruleType;
+ private String ruleFamily;
+ private String variableName;
+ private String attributeName;
+ private ImmutableSet<String> flags;
+ private int startLineCnt;
+
+ @Override
+ public void readLineImpl(String line) throws BuildEncyclopediaDocException {
+ // TODO(bazel-team): check if copy paste code can be reduced using inner classes
+ if (inBlazeRuleDocs) {
+ if (DocgenConsts.BLAZE_RULE_END.matcher(line).matches()) {
+ endBlazeRuleDoc(docMap);
+ } else {
+ appendLine(line);
+ }
+ } else if (inBlazeRuleVarDocs) {
+ if (DocgenConsts.BLAZE_RULE_VAR_END.matcher(line).matches()) {
+ endBlazeRuleVarDoc(docVariables);
+ } else {
+ appendLine(line);
+ }
+ } else if (inBlazeAttributeDocs) {
+ if (DocgenConsts.BLAZE_RULE_ATTR_END.matcher(line).matches()) {
+ endBlazeAttributeDoc(docAttributes);
+ } else {
+ appendLine(line);
+ }
+ }
+ Matcher ruleStartMatcher = DocgenConsts.BLAZE_RULE_START.matcher(line);
+ Matcher ruleVarStartMatcher = DocgenConsts.BLAZE_RULE_VAR_START.matcher(line);
+ Matcher ruleAttrStartMatcher = DocgenConsts.BLAZE_RULE_ATTR_START.matcher(line);
+ if (ruleStartMatcher.find()) {
+ startBlazeRuleDoc(line, ruleStartMatcher);
+ } else if (ruleVarStartMatcher.find()) {
+ startBlazeRuleVarDoc(ruleVarStartMatcher);
+ } else if (ruleAttrStartMatcher.find()) {
+ startBlazeAttributeDoc(line, ruleAttrStartMatcher);
+ }
+ }
+
+ private void appendLine(String line) {
+ // Add another line of html code to the building rule documentation
+ // Removing whitespace and java comment asterisk from the beginning of the line
+ sb.append(line.replaceAll("^[\\s]*\\*", "") + LS);
+ }
+
+ private void startBlazeRuleDoc(String line, Matcher matcher)
+ throws BuildEncyclopediaDocException {
+ checkDocValidity();
+ // Start of a new rule
+ String[] metaData = matcher.group(1).split(",");
+
+ ruleName = readMetaData(metaData, DocgenConsts.META_KEY_NAME);
+ ruleType = readMetaData(metaData, DocgenConsts.META_KEY_TYPE);
+ ruleFamily = readMetaData(metaData, DocgenConsts.META_KEY_FAMILY);
+ startLineCnt = getLineCnt();
+ addFlags(line);
+ inBlazeRuleDocs = true;
+ }
+
+ private void endBlazeRuleDoc(final Map<String, RuleDocumentation> documentations)
+ throws BuildEncyclopediaDocException {
+ // End of a rule, create RuleDocumentation object
+ documentations.put(ruleName, new RuleDocumentation(ruleName, ruleType,
+ ruleFamily, sb.toString(), getLineCnt(), javaSourceFilePath, flags,
+ ruleClassProvider));
+ sb = new StringBuilder();
+ inBlazeRuleDocs = false;
+ }
+
+ private void startBlazeRuleVarDoc(Matcher matcher) throws BuildEncyclopediaDocException {
+ checkDocValidity();
+ // Start of a new rule variable
+ ruleName = matcher.group(1).replaceAll("[\\s]", "");
+ variableName = matcher.group(2).replaceAll("[\\s]", "");
+ startLineCnt = getLineCnt();
+ inBlazeRuleVarDocs = true;
+ }
+
+ private void endBlazeRuleVarDoc(final List<RuleDocumentationVariable> docVariables) {
+ // End of a rule, create RuleDocumentationVariable object
+ docVariables.add(
+ new RuleDocumentationVariable(ruleName, variableName, sb.toString(), startLineCnt));
+ sb = new StringBuilder();
+ inBlazeRuleVarDocs = false;
+ }
+
+ private void startBlazeAttributeDoc(String line, Matcher matcher)
+ throws BuildEncyclopediaDocException {
+ checkDocValidity();
+ // Start of a new attribute
+ ruleName = matcher.group(1).replaceAll("[\\s]", "");
+ attributeName = matcher.group(2).replaceAll("[\\s]", "");
+ startLineCnt = getLineCnt();
+ addFlags(line);
+ inBlazeAttributeDocs = true;
+ }
+
+ private void endBlazeAttributeDoc(
+ final ListMultimap<String, RuleDocumentationAttribute> docAttributes) {
+ // End of a attribute, create RuleDocumentationAttribute object
+ docAttributes.put(attributeName, RuleDocumentationAttribute.create(
+ ruleClassProvider.getRuleClassDefinition(ruleName),
+ attributeName, sb.toString(), startLineCnt, flags));
+ sb = new StringBuilder();
+ inBlazeAttributeDocs = false;
+ }
+
+ private void addFlags(String line) {
+ // Add flags if there's any
+ Matcher matcher = DocgenConsts.BLAZE_RULE_FLAGS.matcher(line);
+ if (matcher.find()) {
+ flags = ImmutableSet.<String>copyOf(matcher.group(1).split(","));
+ } else {
+ flags = ImmutableSet.<String>of();
+ }
+ }
+
+ private void checkDocValidity() throws BuildEncyclopediaDocException {
+ if (inBlazeRuleDocs || inBlazeRuleVarDocs || inBlazeAttributeDocs) {
+ throw new BuildEncyclopediaDocException(javaSourceFilePath, getLineCnt(),
+ "Malformed documentation, #BLAZE_RULE started after another #BLAZE_RULE.");
+ }
+ }
+ });
+
+ // Adding rule doc variables to the corresponding rules
+ for (RuleDocumentationVariable docVariable : docVariables) {
+ if (docMap.containsKey(docVariable.getRuleName())) {
+ docMap.get(docVariable.getRuleName()).addDocVariable(
+ docVariable.getVariableName(), docVariable.getValue());
+ } else {
+ throw new BuildEncyclopediaDocException(javaSourceFilePath,
+ docVariable.getStartLineCnt(), String.format(
+ "Malformed rule variable #BLAZE_RULE(%s).%s, "
+ + "rule %s not found in file.", docVariable.getRuleName(),
+ docVariable.getVariableName(), docVariable.getRuleName()));
+ }
+ }
+ ruleDocEntries = docMap.values();
+ attributeDocEntries = docAttributes;
+ }
+
+ public Collection<RuleDocumentation> getRuleDocEntries() {
+ return ruleDocEntries;
+ }
+
+ public ListMultimap<String, RuleDocumentationAttribute> getAttributeDocEntries() {
+ return attributeDocEntries;
+ }
+
+ private String readMetaData(String[] metaData, String metaKey) {
+ for (String metaDataItem : metaData) {
+ String[] metaDataItemParts = metaDataItem.split("=", 2);
+ if (metaDataItemParts.length != 2) {
+ return null;
+ }
+
+ if (metaDataItemParts[0].trim().equals(metaKey)) {
+ return metaDataItemParts[1].trim();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Reads the template file without variable substitution.
+ */
+ public static String readTemplateContents(String templateFilePath)
+ throws BuildEncyclopediaDocException, IOException {
+ return readTemplateContents(templateFilePath, null);
+ }
+
+ /**
+ * Reads the template file and expands the variables. The variables has to have
+ * the following format in the template file: ${VARIABLE_NAME}. In the Map
+ * input parameter the key has to be VARIABLE_NAME. Variables can be null.
+ */
+ public static String readTemplateContents(
+ String templateFilePath, final Map<String, String> variables)
+ throws BuildEncyclopediaDocException, IOException {
+ final StringBuilder sb = new StringBuilder();
+ readTextFile(templateFilePath, new ReadAction() {
+ @Override
+ public void readLineImpl(String line) {
+ sb.append(expandVariables(line, variables) + LS);
+ }
+ });
+ return sb.toString();
+ }
+
+ private static String expandVariables(String line, Map<String, String> variables) {
+ if (variables != null) {
+ for (Entry<String, String> variable : variables.entrySet()) {
+ line = line.replace("${" + variable.getKey() + "}", variable.getValue());
+ }
+ }
+ return line;
+ }
+
+ public static void readTextFile(String filePath, ReadAction action)
+ throws BuildEncyclopediaDocException, IOException {
+ BufferedReader br = null;
+ try {
+ File file = new File(filePath);
+ if (file.exists()) {
+ br = new BufferedReader(new FileReader(file));
+ } else {
+ InputStream is = SourceFileReader.class.getResourceAsStream(filePath);
+ if (is != null) {
+ br = new BufferedReader(new InputStreamReader(is));
+ }
+ }
+ if (br != null) {
+ String line = null;
+ while ((line = br.readLine()) != null) {
+ action.readLine(line);
+ }
+ } else {
+ System.out.println("Couldn't find file or resource: " + filePath);
+ }
+ } finally {
+ if (br != null) {
+ br.close();
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/docgen/templates/be-body.html b/src/main/java/com/google/devtools/build/docgen/templates/be-body.html
new file mode 100644
index 0000000..d1fc7ce
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/docgen/templates/be-body.html
@@ -0,0 +1,313 @@
+<!-- ============================================
+ binary
+ ============================================
+-->
+<h2 id="binary">*_binary</h2>
+
+<p>A <code>*_binary</code> rule compiles an application. This might be
+ an executable, a <code>.jar</code> file, and/or a collection of scripts.</p>
+
+${SECTION_BINARY}
+
+<!-- ============================================
+ library
+ ============================================
+-->
+<h2 id="library">*_library</h2>
+
+<p>A <code>*_library()</code> rule compiles some sources into a library.
+ In general, a <code><var>language</var>_library</code> rule works like
+ the corresponding <code><var>language</var>_binary</code> rule, but
+ doesn't generate something executable.</p>
+
+${SECTION_LIBRARY}
+
+<!-- ============================================
+ test
+ ============================================
+-->
+<h2 id="test">*_test</h2>
+
+<p>A <code>*_test</code> rule compiles a
+test. See <a href="#common-attributes-tests">Common attributes for
+tests</a> for an explanation of the common attributes.
+
+${SECTION_TEST}
+
+<!-- ============================================
+ generate code and data
+ ============================================
+-->
+<h2>Rules to Generate Code and Data</h2>
+
+${SECTION_GENERATE}
+
+<!-- ============================================
+ variables
+ ============================================
+-->
+<h2 id="make_variables">"Make" Variables</h2>
+
+<p>
+ This section describes how to use or define a special class of string
+ variables that are called the "Make" environment. Bazel defines a set of
+ standard "Make" variables, and you can also define your own.
+</p>
+
+<p>(The reason for the term "Make" is historical: the syntax and semantics of
+ these variables are somewhat similar to those of GNU Make, and in the
+ original implementation, were implemented by GNU Make. The
+ scare-quotes are present because newer build tools support
+ "Make" variables without being implemented using GNU Make; therefore
+ it is important to read the specification below carefully to
+ understand the differences.)
+</p>
+
+<p>To see the list of all common "Make" variables and their values,
+ run <code>bazel info --show_make_env</code>.
+</p>
+
+<p>Build rules can introduce additional rule specific variables. One example is
+ the <a href="#genrule.cmd"><code>cmd</code> attribute of a genrule</a>.
+</p>
+
+<h3 id='make-var-substitution'>"Make" variable substitution</h3>
+
+<p>Variables can be referenced in attributes and other variables using either
+ <code>$(FOO)</code> or <code>varref('FOO')</code>, where <code>FOO</code> is
+ the variable name. In the attribute documentation of rules, it is mentioned
+ when an attribute is subject to "Make" variable substitution. For those
+ attributes this means that any substrings of the form <code>$(X)</code>
+ within those attributes will be interpreted as references to the "Make"
+ variable <var>X</var>, and will be replaced by the appropriate value of that
+ variable for the applicable build configuration. The parens may be omitted
+ for variables whose name is a single character.
+</p>
+<p>
+ It is an error if such attributes contain embedded strings of the
+ form <code>$(X)</code> where <var>X</var> is not the name of a
+ "Make" variable, or unclosed references such as <code>$(</code> not
+ matched by a corresponding <code>)</code>.
+</p>
+<p>
+ Within such attributes, literal dollar signs must be escaped
+ as <code>$$</code> to prevent this expansion.
+</p>
+<p>
+ Those attributes that are subject to this substitution are
+ explicitly indicated as such in their definitions in this document.
+</p>
+
+<h3 id="predefined_variables">Predefined "Make" Variables</h3>
+
+<p>Bazel defines a set of "Make" variables for you.</p>
+
+<p>The build system also provides a consistent PATH for genrules and tests which
+ need to execute shell commands. For genrules, you can indirect your commands
+ using the Make variables below. For basic Unix utilities, prefer relying on
+ the PATH variable to guarantee correct results. For genrules involving
+ compiler and platform invocation, you must use the Make variable syntax.
+ The same basic command set is also available during tests. Simply rely on the
+ PATH.</p>
+
+<p>Bazel uses a tiny Unix distribution to guarantee consistent behavior of
+ build and test steps which execute shell code across all build execution
+ hosting environments but it does not enforce a pure chroot. As such, do
+ <b>not</b> use hard coded paths, such as
+ <code>/usr/bin/foo</code>. Binaries referenced in hardcoded paths are not
+ hermetic and can lead to unexpected and non-reproducible build behavior.</p>
+
+<p><strong>Command Variables for genrules</strong></p>
+
+<p>Note that in general, you should simply refer to many common utilities as
+bare commands that the $PATH variable will correctly resolve to hermetic
+versions for you.</p>
+
+<p><strong>Path Variables</strong></p>
+
+<ul><!-- keep alphabetically sorted -->
+ <li><code>BINDIR</code>: The base of the generated binary tree for the target
+ architecture. (Note that a different tree may be used for
+ programs that run during the build on the host architecture,
+ to support cross-compiling. If you want to run a tool from
+ within a genrule, the recommended way of specifying the path to
+ the tool is to use <code>$(location <i>toolname</i>)</code>,
+ where <i>toolname</i> must be listed in the <code>tools</code>
+ attribute for the genrule.</li>
+ <li><code>GENDIR</code>: The base of the generated code
+ tree for the target architecture.</li>
+ <li><code>JAVABASE</code>:
+ The base directory containing the Java utilities.
+ It will have a "bin" subdirectory.</li>
+</ul>
+
+<p><strong>Architecture Variables</strong></p>
+
+<ul><!-- keep alphabetically sorted -->
+ <li><code>ABI</code>: The C++ ABI version. </li>
+ <li><code>ANDROID_CPU</code>: The Android target architecture's cpu. </li>
+ <li><code>JAVA_CPU</code>: The Java target architecture's cpu. </li>
+ <li> <code>TARGET_CPU</code>: The target architecture's cpu. </li>
+</ul>
+
+<p id="predefined_variables.genrule.cmd">
+ <strong>
+ Other Variables available to <a href="#genrule.cmd">the cmd attribute of a genrule</a>
+ </strong>
+</p>
+<ul><!-- keep alphabetically sorted -->
+ <li><code>OUTS</code>: The <code>outs</code> list. If you have only one output
+ file, you can also use <code>$@</code>.</li>
+ <li><code>SRCS</code>: The <code>srcs</code> list (or more
+ precisely, the pathnames of the files corresponding to
+ labels in the <code>srcs</code> list). If you have only one
+ source file, you can also use <code>$<</code>.</li>
+ <li><code><</code>: <code>srcs</code>, if it is a single file.</li>
+ <li><code>@</code>: <code>outs</code>, if it is a single file.</li>
+ <li><code>@D</code>: The output directory. If there is only
+ one filename in <code>outs</code>, this expands to the
+ directory containing that file. If there are multiple
+ filenames, this variable instead expands to the package's root
+ directory in the <code>genfiles</code> tree, <i>even if all
+ the generated files belong to the same subdirectory</i>!
+ <!-- (as a consequence of the "middleman" implementation) -->
+ If the genrule needs to generate temporary intermediate files
+ (perhaps as a result of using some other tool like a compiler)
+ then it should attempt to write the temporary files to
+ <code>@D</code> (although <code>/tmp</code> will also be
+ writable), and to remove any such generated temporary files.
+ Especially, avoid writing to directories containing inputs -
+ they may be on read-only filesystems. </li>
+</ul>
+
+</ul>
+
+<h3 id="define_your_own_make_vars">Defining Your Own Variables</h3>
+
+<p>
+You may want to use Python-style variable assignments rather than "Make"
+variables, because they work in more use cases and are less surprising. "Make"
+variables will work in the <a href="#genrule.cmd">cmd</a> attribute of genrules
+and in the key of the <a href="#cc_library.abi_deps">abi_deps</a> attribute of
+a limited number of rules, but only in very few other places.
+
+</p>
+<p>To define your "Make" own variables, first call <a
+ href="#vardef">vardef()</a> to define your variable, then call <a
+ href="#varref">varref(name)</a> to retrieve it. varref can be embedded as part
+ of a larger string. Custom "Make" variables differ from ordinary "Python"
+ variables in the BUILD language in two important ways:
+</p>
+<ul>
+ <li>Only string values are supported,</li>
+ <li>The "Make" environment is parameterized over the build
+ platform, so that variables can be conditionally defined based on
+ the target architecture, ABI or compiler, and</li>
+ <li>The values of custom "Make" variables are <i>not available</i> during
+ BUILD-file evaluation. To work around this, you must call <a
+ href="#varref">varref()</a> to retrieve the value of a variable (unlike
+ predefined values, which can be retrieved using <code>$(FOO)</code>.
+ varref defers evaluation until after BUILD file evaluation.</li>
+</ul>
+
+<h4 id="vardef">vardef()</h4>
+
+<p><code>vardef(name, value, platform)</code></p>
+
+ <p>
+ Define a variable for use within this <code>BUILD</code> file only.
+ This variable can then be used by <a href="#varref">varref()</a>.
+ The value of the variable can be overridden on the command line by using the
+ <code class='flag'><a href='bazel-user-manual.html#flag--define'>--define</a></code>
+ flag.
+ </p>
+
+ <p id="vardef_args"><strong>Arguments</strong></p>
+<ul>
+ <li><code>name</code>: The name of the variable.
+ <i>(String; required)</i><br/>
+ Convention is to use names consisting of ALL CAPS. This name must
+ be a unique identifier in this package.
+ </li>
+ <li><code>value</code>: The value to assign to this variable.
+ <i>(String; required)</i><br/>
+ The value may make use of variables you know are defined in the "Make"
+ environment.
+ </li>
+ <li><code>platform</code>: Conditionally define this variable for a given
+ platform.
+ <i>(String; optional)</i><br/>
+
+ <code>vardef</code> binds the <code>name</code> to <code>value</code> if we're
+ compiling for <code>platform</code>.
+ </li>
+</ul>
+
+<p id="vardef_notes"><strong>Notes</strong></p>
+<p>
+ Because references to "Make" variables are expanded <i>after</i>
+ BUILD file evaluation, the relative order of <code>vardef</code>
+ statements and rule declarations is unimportant; it is order of
+ <code>vardef</code> statements relative to each other, and hence the
+ state of the "Make" environment at the end of evaluation that
+ matters.
+</p>
+<p>
+ If there are multiple matching <code>vardef</code> definitions for
+ the same variable, the definition that wins is
+ the <strong>last</strong> matching definition
+ <strong>that specifies a platform</strong>, unless there are no matching
+ definitions that specify a platform, in which case the definition
+ that wins is the <strong>last</strong> definition <strong>without
+ a platform</strong>.
+</p>
+
+<!-- =================================================================
+ varref()
+ =================================================================
+ -->
+
+<h4 id="varref">varref</h4>
+
+<p><code>varref(name)</code></p>
+
+<p>
+<code>varref("FOO")</code> is equivalent of writing "$(FOO)". It is used to
+dereference variables defined with <a href="#vardef"><code>vardef</code></a>
+as well as <a href="#predefined_variables">predefined variables</a>.
+</p>
+
+<p>
+ In rule attributes that are subject to "Make" variable
+ substitution, the string produced by <code>varref(<i>name</i>)</code>
+ will expand to the value of variable <i>name</i>.
+</p>
+
+<p><code>varref(name)</code> may not be used in rule attributes that are
+not subject to "Make" variable substitution.</p>
+
+<p id="varref_args"><strong>Arguments</strong></p>
+<ul>
+ <li><code>name</code>: The name of the variable to dereference.
+ </li>
+</ul>
+
+<p id="varref_notes"><strong>Notes</strong></p>
+<ul>
+ <li><code>varref</code> can access either local or global variables.
+ It prefers the local variable, if both a local and a global exist with
+ the same name.
+ </li>
+</ul>
+
+<p id="varref_examples"><strong>Examples</strong></p>
+<p>See <a href="#vardef_examples">vardef()</a> examples.</p>
+
+
+<!-- ============================================
+ other
+ ============================================
+-->
+<h2 id="misc">Other Stuff</h2>
+
+${SECTION_OTHER}
diff --git a/src/main/java/com/google/devtools/build/docgen/templates/be-footer.html b/src/main/java/com/google/devtools/build/docgen/templates/be-footer.html
new file mode 100644
index 0000000..fb4cc67
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/docgen/templates/be-footer.html
@@ -0,0 +1,407 @@
+<!-- =================================================================
+ package()
+ =================================================================
+-->
+
+<h3 id="package">package</h3>
+
+<p>This function declares metadata that applies to every subsequent rule in the
+package.</p>
+
+<p>The <a href="build-ref.html#package">package</a>
+ function is used at most once within a build package (BUILD file).
+ It is recommended that the package function is called at the top of the
+ file, before any rule.</p>
+
+<h4 id="package_args">Arguments</h4>
+
+<ul>
+
+ <li id="package.default_visibility"><code>default_visibility</code>:
+ The default visibility of the rules in this package.
+ <i>(List of <a href="build-ref.html#labels">labels</a>; optional)</i><br/>
+
+ <p>Every rule in this package has the visibility specified in this
+ attribute, unless otherwise specified in the <code>visibility</code>
+ attribute of the rule. For detailed information about the syntax of this
+ attribute, see the documentation of the <a href="#common.visibility">visibility</a>
+ attribute.
+ </li>
+
+ <li id="package.default_obsolete"><code>default_obsolete</code>:
+ The default value of <a href="#common.obsolete"><code>obsolete</code></a> property
+ for all rules in this package. <i>(Boolean; optional; default is 0)</i>
+ </li>
+
+ <li id="package.default_deprecation"><code>default_deprecation</code>:
+ Sets the default <a href="#common.deprecation"><code>deprecation</code></a> message
+ for all rules in this package. <i>(String; optional)</i>
+ </li>
+
+ <li id="package.default_testonly"><code>default_testonly</code>:
+ Sets the default <a href="#common.testonly"><code>testonly</code></a> property
+ for all rules in this package. <i>(Boolean; optional; default is 0 except as noted)</i>
+
+ <p>In packages under <code>javatests</code> the default value is 1.</p>
+ </li>
+
+</ul>
+
+<h4 id="package_example">Examples</h4>
+
+The declaration below declares that the rules in this package are
+visible only to members of package
+group <code>//foo:target</code>. Individual visibility declarations
+on a rule, if present, override this specification.
+
+<pre class="code">
+package(default_visibility = ["//foo:target"])
+</pre>
+
+<!-- =================================================================
+ package_group()
+ =================================================================
+-->
+
+<h3 id="package_group">package_group</h3>
+
+<p><code>package_group(name, packages, includes)</code></p>
+
+<p>This function defines a set of build packages.
+
+Package groups are used for visibility control. You can grant access to a rule
+to one or more package groups, every rule, or only to rules declared
+in the same package. For more detailed description of the visibility system, see
+the <a href="#common.visibility">visibility</a> attribute.
+
+<h4 id="package_group_args">Arguments</h4>
+
+<ul>
+ <li id="package_group.name"><code>name</code>:
+ A unique name for this rule.
+ <i>(<a href="build-ref.html#name">Name</a>; required)</i>
+ </li>
+
+ <li id="package_group.packages"><code>packages</code>:
+ A complete enumeration of packages in this group.
+ <i>(List of <a href="build-ref.html#s4">Package</a>; optional)</i><br/>
+
+ <p>Packages should be referred to using their full names,
+ starting with a double slash. For
+ example, <code>//foo/bar/main</code> is a valid element
+ of this list.</p>
+
+ <p>You can also specify wildcards: the specification
+ <code>//foo/...</code> specifies every package under
+ <code>//foo</code>, including <code>//foo</code> itself.
+
+ <p>If this attribute is missing, the package group itself will contain
+ no packages (but it can still include other package groups).</p>
+ </li>
+
+ <li id="package_group.includes"><code>includes</code>:
+ Other package groups that are included in this one.
+ <i>(List of <a href="build-ref.html#labels">labels</a>; optional)</i><br/>
+
+ <p>The labels in this attribute must refer to other package
+ groups. Packages in referenced package groups are taken to be part
+ of this package group. This is transitive, that is, if package
+ group <code>a</code> contains package group <code>b</code>,
+ and <code>b</code> contains package group <code>c</code>, every
+ package in <code>c</code> will also be a member of <code>a</code>.</p>
+ </li>
+</ul>
+
+
+<h4 id="package_group_example">Examples</h4>
+
+The following <code>package_group</code> declaration specifies a
+package group called "tropical" that contains tropical fruits.
+
+<pre class="code">
+package_group(
+ name = "tropical",
+ packages = [
+ "//fruits/mango",
+ "//fruits/orange",
+ "//fruits/papaya/...",
+ ],
+)
+</pre>
+
+The following declarations specify the package groups of a fictional
+application:
+
+<pre class="code">
+package_group(
+ name = "fooapp",
+ includes = [
+ ":controller",
+ ":model",
+ ":view",
+ ],
+)
+
+package_group(
+ name = "model",
+ packages = ["//fooapp/database"],
+)
+
+package_group(
+ name = "view",
+ packages = [
+ "//fooapp/swingui",
+ "//fooapp/webui",
+ ],
+)
+
+package_group(
+ name = "controller",
+ packages = ["//fooapp/algorithm"],
+)
+</pre>
+
+
+<!-- =================================================================
+ DESCRIPTION
+ =================================================================
+ -->
+
+<h3 id="description">Description</h3>
+
+<p><code># Description: <var>...</var></code></p>
+
+ <p>
+ Each BUILD file should contain a <code>Description</code> comment.
+ </p>
+
+ <p>
+ Description comments may contain references to other
+ documentation. A string that starts with "http" will become a
+ link. HTML markup is
+ allowed in description comments, but please keep the BUILD files readable.
+ We encourage you to list the URLs of relevant design docs and howtos
+ in these description comments.
+ </p>
+
+<h3 id="distribs">distribs</h3>
+
+<p><code>distribs(distrib_methods)</code></p>
+
+<p><code>distribs()</code> specifies the default distribution method (or
+ methods) of the build rules in a <code>BUILD</code> file. The <code>distribs()</code>
+ directive
+ should appear close to the beginning of the <code>BUILD</code> file,
+ before any build rules, as it sets the <code>BUILD</code>-file scope
+ default for build rule distribution methods.
+</p>
+
+<h4 id="distribs_args">Arguments</h4>
+
+<p>The argument, <code id="distribs.distrib_methods">distrib_methods</code>,
+ is a list of distribution-method strings.
+</p>
+
+<!-- =================================================================
+ exports_files([label, ...])
+ =================================================================
+ -->
+
+<h3 id="exports_files">exports_files</h3>
+
+<p><code>exports_files([<i>label</i>, ...], visibility, licenses)</code></p>
+
+<p>
+ <code>exports_files()</code> specifies a list of files belonging to
+ this package that are exported to other packages but not otherwise
+ mentioned in the BUILD file.
+</p>
+
+<p>
+ The BUILD file for a package may only refer to files belonging to another
+ package if they are mentioned somewhere in the other packages's BUILD file,
+ whether as an input to a rule or an explicit or implicit output from a rule.
+ The remaining files are not associated with a specific rule but are just "data",
+ and for these, <code>exports_files</code> ensures that they may be referenced by
+ other packages. (One kind of data for which this is particularly useful are
+ shell and Perl scripts.)
+</p>
+
+<p>
+ Note: A BUILD file only consisting of <code>exports_files()</code> statements
+ is needless though, as there are no BUILD rules that could own any files.
+ The files listed can already be accessed through the containing package and
+ exported from there if needed.
+</p>
+
+<h4 id="exports_files_args">Arguments</h4>
+
+<p>
+ The argument is a list of names of files within the current package. A
+ visibility declaration can also be specified; in this case, the files will be
+ visible to the targets specified. If no visibility is specified, the files
+ will be visible to every package, even if a package default visibility was
+ specified in the <code><a href="#package">package</a></code> function. The
+ <a href="#common.licenses">licenses</a> can also be specified.
+</p>
+
+<!-- =================================================================
+ glob()
+ =================================================================
+ -->
+
+<h3 id="glob">glob</h3>
+
+<p><code>glob(include, exclude=[], exclude_directories=1)</code>
+</p>
+
+<p>
+Glob is a helper function that can be used anywhere a list of filenames
+is expected. It takes one or two lists of filename patterns containing
+the <code>*</code> wildcard: as per the Unix shell, this wildcard
+matches any string excluding the directory separator <code>/</code>.
+In addition filename patterns can contain the recursive <code>**</code>
+wildcard. This wildcard will match zero or more complete
+path segments separated by the directory separator <code>/</code>.
+This wildcard can only be used as a complete path segment. For example,
+<code>"x/**/*.java"</code> is legal, but <code>"test**/testdata.xml"</code>
+and <code>"**.java"</code> are both illegal. No other wildcards are supported.
+</p>
+<p>
+Glob returns a list of every file in the current build package that:
+</p>
+<ul>
+ <li style="margin-bottom: 0">
+ Matches at least one pattern in <code>include</code>. </li>
+ <li>
+ Does not match any of the patterns in <code>exclude</code> (default []).</li>
+</ul>
+<p>
+If the <code>exclude_directories</code> argument is enabled (1), files of
+type directory will be omitted from the results (default 1).
+</p>
+<p>
+There are several important limitations and caveats:
+</p>
+
+<ol>
+ <li>
+ Globs only match files in your source tree, never
+ generated files. If you are building a target that requires both
+ source and generated files, create an explicit list of generated
+ files, and use <code>+</code> to add it to the result of the
+ <code>glob()</code> call.
+ </li>
+
+ <li>
+ Globs may match files in subdirectories. And subdirectory names
+ may be wildcarded. However...
+ </li>
+
+ <li>
+ Labels are not allowed to cross the package boundary and glob does
+ not match files in subpackages.
+
+ For example, the glob expression <code>**/*.cc</code> in package
+ <code>x</code> does not include <code>x/y/z.cc</code> if
+ <code>x/y</code> exists as a package (either as
+ <code>x/y/BUILD</code>, or somewhere else on the package-path). This
+ means that the result of the glob expression actually depends on the
+ existence of BUILD files — that is, the same glob expression would
+ include <code>x/y/z.cc</code> if there was no package called
+ <code>x/y</code>.
+ </li>
+
+ <li>
+ The restriction above applies to all glob expressions,
+ no matter which wildcards they use.
+ </li>
+</ol>
+
+<p>
+In general, you should <b>try to provide an appropriate extension (e.g. *.html)
+instead of using a bare '*'</b> for a glob pattern. The more explicit name
+is both self documenting and ensures that you don't accidentally match backup
+files, or emacs/vi/... auto-save files.
+</p>
+
+<h4 id="glob_example">Glob Examples</h4>
+
+<p>Include all txt files in directory testdata except experimental.txt.
+Note that files in subdirectories of testdata will not be included. If
+you want those files to be included, use a recursive glob (**).</p>
+<pre class="code">
+java_test(
+ name = "myprog",
+ srcs = ["myprog.java"],
+ data = glob(
+ ["testdata/*.txt"],
+ exclude = ["testdata/experimental.txt"],
+ ),
+)
+</pre>
+
+<h4 id="recursive_glob_example">Recursive Glob Examples</h4>
+
+<p>Create a library built from all java files in this directory and all
+subdirectories except those whose path includes a directory named testing.
+Subdirectories containing a BUILD file are ignored.
+<b>This should be a very common pattern.</b>
+</p>
+<pre class="code">
+java_library(
+ name = "mylib",
+ srcs = glob(
+ ["**/*.java"],
+ exclude = ["**/testing/**"],
+ ),
+)
+</pre>
+
+<p>Make the test depend on all txt files in the testdata directory,
+ its subdirectories</p>
+<pre class="code">
+java_test(
+ name = "mytest",
+ srcs = ["mytest.java"],
+ data = glob(["testdata/**/*.txt"]),
+)
+</pre>
+
+<!-- =================================================================
+ licenses()
+ =================================================================
+-->
+
+<h3 id="licenses">licenses</h3>
+
+<p><code>licenses(license_types)</code></p>
+
+<p><code>licenses()</code> specifies the default license type (or types)
+ of the build rules in a <code>BUILD</code> file. The <code>licenses()</code>
+ directive should appear close to the
+ beginning of the <code>BUILD</code> file, before any build rules, as it
+ sets the <code>BUILD</code>-file scope default for build rule license
+ types.</p>
+
+<h4 id="licenses_args">Arguments</h4>
+
+<p>The argument, <code id="licenses.licence_types">license_types</code>,
+ is a list of license-type strings.
+</p>
+
+<!-- =================================================================
+ include()
+ =================================================================
+-->
+
+<h3 id="include">include</h3>
+
+<p><code>include(name)</code></p>
+
+<p><code>include()</code> incorporates build
+ language definitions from an external file into the evaluation of the
+ current <code>BUILD</code> file.</p>
+
+</body>
+</html>
diff --git a/src/main/java/com/google/devtools/build/docgen/templates/be-header.html b/src/main/java/com/google/devtools/build/docgen/templates/be-header.html
new file mode 100644
index 0000000..cdebf9b
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/docgen/templates/be-header.html
@@ -0,0 +1,612 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Bazel BUILD Encyclopedia of Functions</title>
+
+ <style type="text/css" id="internalStyle">
+ body {
+ background-color: #ffffff;
+ color: black;
+ margin-right: 10%;
+ margin-left: 10%;
+ }
+
+ h1, h2, h3, h4, h5, h6 {
+ color: #dd7755;
+ font-family: sans-serif;
+ }
+ @media print {
+ /* Darker version for printing */
+ h1, h2, h3, h4, h5, h6 {
+ color: #008000;
+ font-family: helvetica, sans-serif;
+ }
+ }
+
+ h1 {
+ text-align: center;
+ }
+ h2 {
+ margin-left: -0.5in;
+ }
+ h3 {
+ margin-left: -0.25in;
+ }
+ h4 {
+ margin-left: -0.125in;
+ }
+ hr {
+ margin-left: -1in;
+ }
+ address {
+ text-align: right;
+ }
+
+ /* A compact unordered list */
+ ul.tight > li {
+ margin-bottom: 0;
+ }
+
+ /* Use the <code> tag for bits of code and <var> for variable and object names. */
+ code,pre,samp,var {
+ color: #006000;
+ }
+ /* Use the <file> tag for file and directory paths and names. */
+ file {
+ color: #905050;
+ font-family: monospace;
+ }
+ /* Use the <kbd> tag for stuff the user should type. */
+ kbd {
+ color: #600000;
+ }
+ div.note p {
+ float: right;
+ width: 3in;
+ margin-right: 0%;
+ padding: 1px;
+ border: 2px solid #60a060;
+ background-color: #fffff0;
+ }
+
+ table.grid {
+ background-color: #ffffee;
+ border: 1px solid black;
+ border-collapse: collapse;
+ margin-left: 2mm;
+ margin-right: 2mm;
+ }
+
+ table.grid th,
+ table.grid td {
+ border: 1px solid black;
+ padding: 0 2mm 0 2mm;
+ }
+
+ /* Use pre.code for code listings.
+ Use pre.interaction for "Here's what you see when you run a.out.".
+ (Within pre.interaction, use <kbd> things the user types)
+ */
+ pre.code {
+ background-color: #FFFFEE;
+ border: 1px solid black;
+ color: #004000;
+ font-size: 10pt;
+ margin-left: 2mm;
+ margin-right: 2mm;
+ padding: 2mm;
+ -moz-border-radius: 12px 0px 0px 0px;
+ }
+
+ pre.interaction {
+ background-color: #EEFFEE;
+ color: #004000;
+ padding: 2mm;
+ }
+
+ pre.interaction kbd {
+ font-weight: bold;
+ color: #000000;
+ }
+
+ /* legacy style */
+ pre.interaction b.astyped {
+ color: #000000;
+ }
+
+ h1 { margin-bottom: 5px; }
+ .toc { margin: 0px; }
+ ul li { margin-bottom: 1em; }
+ ul.toc li { margin-bottom: 0px; }
+ em.harmful { color: red; }
+
+ .deprecated { text-decoration: line-through; }
+ .discouraged { text-decoration: line-through; }
+
+ #rules { width: 980px; border-collapse: collapse; }
+ #rules td { border-top: 1px solid gray; padding: 4px; vertical-align: top; }
+ #rules th { text-align: left; padding: 4px; }
+
+ table.layout { width: 980px; }
+ table.layout td { vertical-align: top; }
+
+ #maintainer { text-align: right; }
+
+ dt {
+ font-weight: bold;
+ margin-top: 0.5em;
+ margin-bottom: 0.5em;
+ }
+ dd dt {
+ font-weight: normal;
+ text-decoration: underline;
+ color: gray;
+ }
+ </style>
+
+ <style type="text/css">
+ .rule-signature {
+ color: #006000;
+ font-family: monospace;
+ }
+ </style>
+</head>
+
+<body>
+
+<h1>Bazel BUILD Encyclopedia of Functions</h1>
+
+<h2>Contents</h2>
+
+ <h3>Concepts and terminology</h3>
+ <table class="layout"><tr><td>
+ <ul class="toc">
+ <li><a href="#common-definitions">Common definitions</a>:
+ <ul>
+ <li><a href="#sh-tokenization">Bourne shell tokenization</a></li>
+ <li><a href="#label-expansion">Label expansion</a></li>
+ <li><a href="#common-attributes">Common attributes</a></li>
+ <li><a href="#common-attributes-tests">Common attributes for tests</a></li>
+ <li><a href="#common-attributes-binaries">Common attributes for binaries</a></li>
+ <li><a href="#implicit-outputs">Implicit output targets</a></li>
+ </ul>
+ </li>
+ </ul>
+ </td><td>
+ <ul class="toc">
+ <li><a href="#make_variables">"Make" variables</a>
+ <ul class="toc">
+ <li><a href="#make-var-substitution">"Make" variable substitution</a></li>
+ <li><a href="#predefined_variables">Predefined variables</a></li>
+ <li><a href="#define_your_own_make_vars">Defining your own variables</a>
+ <ul>
+ <li><a href="#vardef">vardef</a></li>
+ <li><a href="#varref">varref</a></li>
+ </ul>
+ </li>
+ </ul>
+ <li><a href="#predefined-python-variables">Predefined Python Variables</a></li>
+ </ul>
+ </td><td>
+ <ul class="toc">
+ <li><a href="#include">include</a></li>
+ <li><a href="#package">package</a></li>
+ <li><a href="#package_group">package_group</a></li>
+ <li><a href="#description">Description</a></li>
+ <li><a href="#distribs">distribs</a></li>
+ <li><a href="#licenses">licenses</a></li>
+ <li><a href="#exports_files">exports_files</a></li>
+ <li><a href="#glob">glob</a></li>
+ </ul>
+ </td></tr></table>
+
+${HEADER_TABLE}
+
+<h2 id="common-definitions">Common definitions</h2>
+
+<p>This section defines various terms and concepts that are common to
+many functions or build rules below.
+</p>
+
+<!-- we haven't defined 'rules' or 'attributes' yet. -->
+
+<h3 id='sh-tokenization'>Bourne shell tokenization</h3>
+<p>
+ Certain string attributes of some rules are split into multiple
+ words according to the tokenization rules of the Bourne shell:
+ unquoted spaces delimit separate words, and single- and
+ double-quotes characters and backslashes are used to prevent
+ tokenization.
+</p>
+<p>
+ Those attributes that are subject to this tokenization are
+ explicitly indicated as such in their definitions in this document.
+</p>
+<p>
+ Attributes subject to "Make" variable expansion and Bourne shell
+ tokenization are typically used for passing arbitrary options to
+ compilers and other tools, such as the <code>copts</code> attribute
+ of <code>cc_library</code>, or the <code>javacopts</code> attribute of
+ <code>java_library</code>. Together these substitutions allow a
+ single string variable to expand into a configuration-specific list
+ of option words.
+</p>
+
+<h3 id='label-expansion'>Label expansion</h3>
+<p>
+ Some string attributes of a very few rules are subject to label
+ expansion: if those strings contain a valid build label as a
+ substring, such as <code>//mypkg:target</code>, and that label is a
+ declared prerequisite of the current rule, it is expanded into the
+ pathname of the file represented by the target <code>//mypkg:target</code>.
+</p>
+<p>
+ Example attributes include the <code>cmd</code> attribute of
+ genrule, and the <code>linkopts</code> attribute
+ of <code>cc_library</code>. The details may vary significantly in
+ each case, over such issues as: whether relative labels are
+ expanded; how labels that expand to multiple files are
+ treated, etc. Consult the rule attribute documentation for
+ specifics.
+</p>
+
+<h3 id="common-attributes">Attributes common to all build rules</h3>
+
+<p>This section describes attributes that are common to all build rules.<br/>
+Please note that it is an error to list the same label twice in a list of
+labels attribute.
+</p>
+
+<ul>
+<li id="common.deps"><code>deps</code>:
+A list of dependencies of this rule.
+<i>(List of <a href="build-ref.html#labels">labels</a>; optional)</i><br/>
+The precise semantics of what it means for this rule to depend on
+another using <code>deps</code> are specific to the kind of this rule,
+and the rule-specific documentation below goes into more detail.
+At a minimum, though, the targets named via <code>deps</code> will
+appear in the <code>*.runfiles</code> area of this rule, if it has
+one.
+<p>Most often, a <code>deps</code> dependency is used to allow one
+module to use symbols defined in another module written in the
+same programming language and separately compiled. Cross-language
+dependencies are also permitted in many cases: for example,
+a <code>java_library</code> rule may depend on C++ code in
+a <code>cc_library</code> rule, by declaring the latter in
+the <code>deps</code> attribute. See the definition
+of <a href="build-ref.html#deps">dependencies</a> for more
+information.</p>
+<p>Almost all rules permit a <code>deps</code> attribute, but where
+this attribute is not allowed, this fact is documented under the
+specific rule.</p></li>
+<li id="common.data"><code>data</code>:
+The list of files needed by this rule at runtime.
+<i>(List of <a href="build-ref.html#labels">labels</a>; optional)</i><br/>
+Targets named in the <code>data</code> attribute will appear in
+the <code>*.runfiles</code> area of this rule, if it has one. This
+may include data files needed by a binary or library, or other
+programs needed by it. See the
+<a href="build-ref.html#data">data dependencies</a> section for more
+information about how to depend on and use data files.
+<p>Almost all rules permit a <code>data</code> attribute, but where
+this attribute is not allowed, this fact is documented under the
+specific rule.</p></li>
+<li id="common.deprecation"><code>deprecation</code>:
+<i>(String; optional)</i><br/>
+An explanatory warning message associated with this rule.
+Typically this is used to notify users that a rule has become obsolete,
+or has become superseded by another rule, is private to a package, or is
+perhaps "considered harmful" for some reason. It is a good idea to include
+some reference (like a webpage, a bug number or example migration CLs) so
+that one can easily find out what changes are required to avoid the message.
+If there is a new target that can be used as a drop in replacement, it is a good idea
+to just migrate all users of the old target.
+<p>
+This attribute has no effect on the way things are built, but it
+may affect a build tool's diagnostic output. The build tool issues a
+warning when a rule with a <code>deprecation</code> attribute is
+depended upon by another rule.</p>
+<p>
+Intra-package dependencies are exempt from this warning, so that,
+for example, building the tests of a deprecated rule does not
+encounter a warning.</p>
+<p>
+If a deprecated rule depends on another deprecated rule, no warning
+message is issued.</p>
+<p>
+Once people have stopped using it, the package can be removed or marked as
+<a href="#common.obsolete"><code>obsolete</code></a>.</p></li>
+<li id="common.distribs"><code>distribs</code>:
+<i>(List of strings; optional)</i><br/>
+A list of distribution-method strings to be used for this particular build rule.
+Overrides the <code>BUILD</code>-file scope defaults defined by the
+<a href="#distribs"><code>distribs()</code></a> directive.</li>
+<li id="common.licenses"><code>licenses</code>:
+<i>(List of strings; optional)</i><br/>
+A list of license-type strings to be used for this particular build rule.
+Overrides the <code>BUILD</code>-file scope defaults defined by the
+<a href="#licenses"><code>licenses()</code></a> directive.</li>
+<li id="common.obsolete"><code>obsolete</code>:
+<i>(Boolean; optional; default 0)</i><br/>
+If 1, only obsolete targets can depend on this target. It is an error when
+a non-obsolete target depends on an obsolete target.
+<p>
+As a transition, one can first mark a package as in
+<a href="#common.deprecation"><code>deprecation</code></a>.</p>
+<p>
+This attribute is useful when you want to prevent a target from
+being used but are yet not ready to delete the sources.</p></li>
+<li id="common.tags"><code>tags</code>:
+List of arbitrary text tags. Tags may be any valid string; default is the
+empty list.<br/>
+<i>Tags</i> can be used on any rule; but <i>tags</i> are most useful
+on test and <code>test_suite</code> rules. Tags on non-test rules
+are only useful to humans and/or external programs.
+<i>Tags</i> are generally used to annotate a test's role in your debug
+and release process. The use of tags and size elements
+gives flexibility in assembling suites of tests based around codebase
+check-in policy.
+<p>
+A few tags have special meaning to the build tool; consult
+the <a href='bazel-user-manual.html#tags_keywords'>Bazel
+documentation</a> for details.
+</p></li>
+<li id="common.testonly"><code>testonly</code>:
+<i>(Boolean; optional; default 0 except as noted)</i><br />
+If 1, only testonly targets (such as tests) can depend on this target.
+<p>Equivalently, a rule that is not <code>testonly</code> is not allowed to
+depend on any rule that is <code>testonly</code>.</p>
+<p>Tests (<code>*_test</code> rules)
+and test suites (<a href="#test_suite">test_suite</a> rules)
+are <code>testonly</code> by default.</p>
+<p>By virtue of
+<a href="#package.default_testonly"><code>default_testonly</code></a>,
+targets under <code>javatests</code> are <code>testonly</code> by default.</p>
+<p>This attribute is intended to mean that the target should not be
+contained in binaries that are released to production.</p>
+<p>Because testonly is enforced at build time, not run time, and propagates
+virally through the dependency tree, it should be applied judiciously. For
+example, stubs and fakes that
+are useful for unit tests may also be useful for integration tests
+involving the same binaries that will be released to production, and
+therefore should probably not be marked testonly. Conversely, rules that
+are dangerous to even link in, perhaps because they unconditionally
+override normal behavior, should definitely be marked testonly.</p></li>
+<li id="common.visibility"><code>visibility</code>:
+<i>(List of <a href="build-ref.html#labels">labels</a>; optional; default private)</i><br/>
+<p>The <code>visibility</code> attribute on a rule controls whether
+the rule can be used by other packages. Rules are always visible to
+other rules declared in the same package.</p>
+<p>There are five forms (and one temporary form) a visibility label can take:
+<ul>
+<li><code>['//visibility:public']</code>: Anyone can use this rule.</li>
+<li><code>['//visibility:private']</code>: Only rules in this package
+can use this rule. Rules in <code>javatests/foo/bar</code>
+can always use rules in <code>java/foo/bar</code>.
+</li>
+<li><code>['//some/package:__pkg__', '//other/package:__pkg__']</code>:
+Only rules in <code>some/package</code> and <code>other/package</code>
+(defined in <code>some/package/BUILD</code> and
+<code>other/package/BUILD</code>) have access to this rule. Note that
+sub-packages do not have access to the rule; for example,
+<code>//some/package/foo:bar</code> or
+<code>//other/package/testing:bla</code> wouldn't have access.
+<code>__pkg__</code> is a special target and must be used verbatim.
+It represents all of the rules in the package.
+</li>
+<li><code>['//project:__subpackages__', '//other:__subpackages__']</code>:
+Only rules in packages <code>project</code> or <code>other</code> or
+in one of their sub-packages have access to this rule. For example,
+<code>//project:rule</code>, <code>//project/library:lib</code> or
+<code>//other/testing/internal:munge</code> are allowed to depend on
+this rule (but not <code>//independent:evil</code>)
+</li>
+<li><code>['//some/package:my_package_group']</code>:
+A <a href="#package_group">package group</a> is
+a named set of package names. Package groups can also grant access rights
+to entire subtrees, e.g.<code>//myproj/...</code>.
+</li>
+</ul>
+<p>The visibility specifications of <code>//visibility:public</code>,
+<code>//visibility:private</code> and
+<code>//visibility:legacy_public</code>
+can not be combined with any other visibility specifications.
+A visibility specification may contain a combination of package labels
+(i.e. //foo:__pkg__) and package_groups.</p>
+<p>If a rule does not specify the visibility attribute,
+the <code><a href="#package">default_visibility</a></code>
+attribute of the <code><a href="#package">package</a></code>
+statement in the BUILD file containing the rule is used
+(except <a href="#exports_files">exports_files</a> and
+<a href="#cc_public_library">cc_public_library</a>, which always default to
+public).</p>
+<p><b>Example</b>:</p>
+<p>
+File <code>//frobber/bin/BUILD</code>:
+</p>
+<pre class="code">
+# This rule is visible to everyone
+java_binary(
+ name = "executable",
+ visibility = ["//visibility:public"],
+ deps = [":library"],
+)
+
+# This rule is visible only to rules declared in the same package
+java_library(
+ name = "library",
+ visibility = ["//visibility:private"],
+)
+
+# This rule is visible to rules in package //object and //noun
+java_library(
+ name = "subject",
+ visibility = [
+ "//noun:__pkg__",
+ "//object:__pkg__",
+ ],
+)
+
+# See package group //frobber:friends (below) for who can access this rule.
+java_library(
+ name = "thingy",
+ visibility = ["//frobber:friends"],
+)
+</pre>
+<p>
+File <code>//frobber/BUILD</code>:
+</p>
+<pre class="code">
+# This is the package group declaration to which rule //frobber/bin:thingy refers.
+#
+# Our friends are packages //frobber, //fribber and any subpackage of //fribber.
+package_group(
+ name = "friends",
+ packages = [
+ "//fribber/...",
+ "//frobber",
+ ],
+)
+</pre></li>
+</ul>
+
+
+<h3 id="common-attributes-tests">Attributes common to all test rules (*_test)</h3>
+
+<p>This section describes attributes that are common to all test rules.</p>
+
+<ul>
+<li id="test.args"><code>args</code>:
+Add these arguments to the <code>--test_arg</code>
+when executed by <code>bazel test</code>.
+<i>(List of strings; optional; subject to
+<a href="#make_variables">"Make variable"</a> substitution and
+<a href="#sh-tokenization">Bourne shell tokenization</a>)</i><br/>
+These arguments are passed before the <code>--test_arg</code> values
+specified on the <code>bazel test</code> command line.</li>
+<li id="test.flaky"><code>flaky</code>:
+Marks test as flaky. <i>(Boolean; optional)</i><br/>
+If set, executes the test up to 3 times before being declared as failed.
+By default this attribute is set to 0 and test is considered to be stable.
+Note, that use of this attribute is generally discouraged - we do prefer
+all tests to be stable.</li>
+<li id="test.local"><code>local</code>:
+Forces the test to be run locally. <i>(Boolean; optional)</i><br/>
+By default this attribute is set to 0 and the default testing strategy is
+used. This is equivalent to providing 'local' as a tag
+(<code>tags=["local"]</code>).</li>
+<li id="test.shard_count"><code>shard_count</code>:
+Specifies the number of parallel shards
+to use to run the test. <i>(Non-negative integer less than or equal to 50;
+optional)</i><br/>
+This value will override any heuristics used to determine the number of
+parallel shards with which to run the test.</li>
+<li id="test.size"><code>size</code>:
+How "heavy" the test is
+<i>(String "enormous", "large" "medium" or "small",
+default is "medium")</i><br/>
+A classification of the test's "heaviness": how much time/resources
+it needs to run. This is useful when deciding which tests to run.
+Before checking in a change, you might run the small tests.
+Before a big release, you might run the large tests.
+</li>
+<li id="test.timeout"><code>timeout</code>:
+How long the test is
+normally expected to run before returning.
+<i>(String "eternal", "long", "moderate", or "short"
+with the default derived from a test's size attribute)</i><br/>
+While a test's size attribute controls resource estimation, a test's
+timeout may be set independently. If not explicitly specified, the
+timeout is based on the test's size (with "small" ⇒ "short",
+"medium" ⇒ "moderate", etc...). While size and runtime are generally
+heavily correlated, they are not strictly causal, hence the ability to set
+them independently.</li>
+</ul>
+
+
+<h3 id="common-attributes-binaries">Attributes common to all binary rules (*_binary)</h3>
+
+<p>This section describes attributes that are common to all binary rules.</p>
+
+<ul>
+<li id="binary.args"><code>args</code>:
+Add these arguments to the target when executed by
+<code>bazel run</code>.
+<i>(List of strings; optional; subject to
+<a href="#make_variables">"Make variable"</a> substitution and
+<a href="#sh-tokenization">Bourne shell tokenization</a>)</i><br/>
+These arguments are passed to the target before the target options
+specified on the <code>bazel run</code> command line.
+<p>Most binary rules permit an <code>args</code> attribute, but where
+this attribute is not allowed, this fact is documented under the
+specific rule.</p></li>
+<li id="binary.output_licenses"><code>output_licenses</code>:
+The licenses of the output files that this binary generates.
+<i>(List of strings; optional)</i><br/>
+Describes the licenses of the output of the binary generated by
+the rule. When a binary is referenced in a host attribute (for
+example, the <code>tools</code> attribute of
+a <code>genrule</code>), this license declaration is used rather
+than the union of the licenses of its transitive closure. This
+argument is useful when a binary is used as a tool during the
+build of a rule, and it is not desirable for its license to leak
+into the license of that rule. If this attribute is missing, the
+license computation proceeds as if the host dependency was a
+regular dependency.
+<p><em class="harmful">WARNING: in some cases (specifically, in
+genrules) the build tool cannot guarantee that the binary
+referenced by this attribute is actually used as a tool, and is
+not, for example, copied to the output. In these cases, it is the
+responsibility of the user to make sure that this is
+true.</em></p></li>
+</ul>
+
+
+<h3 id="implicit-outputs">Implicit output targets</h3>
+
+<p>When you define a build rule in a BUILD file, you are explicitly
+ declaring a new, named rule target in a package. Many build rule
+ functions also <i>implicitly</i> entail one or more output file
+ targets, whose contents and meaning are rule-specific.
+
+ For example, when you explicitly declare a
+ <code>java_binary(name='foo', ...)</code> rule, you are also
+ <i>implicitly</i> declaring an output file
+ target <code>foo_deploy.jar</code> as a member of the same package.
+ (This particular target is a self-contained Java archive suitable
+ for deployment.)
+</p>
+
+<p>
+ Implicit output targets are first-class members of the build
+ target graph. Just like other targets, they are built on demand,
+ either when specified in the top-level built command, or when they
+ are necessary prerequisites for other build targets. They can be
+ referenced as dependencies in BUILD files, and can be observed in
+ the output of analysis tools such as <code>bazel query</code>.
+</p>
+
+<p>
+ For each kind of build rule, the rule's documentation contains a
+ special section detailing the names and contents of any implicit
+ outputs entailed by a declaration of that kind of rule.
+</p>
+
+<p>
+ Please note an important but somewhat subtle distinction between the
+ two namespaces used by the build system. Build
+ <a href="build-ref.html#labels">labels</a> identify <em>targets</em>,
+ which may be rules or files, and file targets may be divided into
+ either source (or input) file targets and derived (or output) file
+ targets. These are the things you can mention in BUILD files,
+ build from the command-line, or examine using <code>bazel query</code>;
+ this is the <em>target namespace</em>. Each file target corresponds
+ to one actual file on disk (the "file system namespace"); each rule
+ target may correspond to zero, one or more actual files on disk.
+ There may be files on disk that have no corresponding target; for
+ example, <code>.o</code> object files produced during C++ compilation
+ cannot be referenced from within BUILD files or from the command line.
+ In this way, the build tool may hide certain implementation details of
+ how it does its job. This is explained more fully in
+ the <a href="build-ref.html">BUILD Concept Reference</a>.
+</p>
diff --git a/src/main/java/com/google/devtools/build/lib/Constants.java b/src/main/java/com/google/devtools/build/lib/Constants.java
new file mode 100644
index 0000000..052b090
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/Constants.java
@@ -0,0 +1,39 @@
+// Copyright 2014 Google Inc. 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;
+
+import com.google.common.collect.ImmutableList;
+
+/**
+ * Various constants required by Bazel.
+ *
+ * <p>The extra {@code .toString()} calls are there so that javac doesn't inline these constants
+ * so that we can replace this class file in the .jar after Bazel was built.
+ */
+public class Constants {
+ private Constants() {
+ }
+
+ public static final String PRODUCT_NAME = "bazel".toString();
+ public static final ImmutableList<String> DEFAULT_PACKAGE_PATH = ImmutableList.of("%workspace%");
+ public static final String MAIN_RULE_CLASS_PROVIDER =
+ "com.google.devtools.build.lib.bazel.rules.BazelRuleClassProvider".toString();
+ public static final ImmutableList<String> IGNORED_TEST_WARNING_PREFIXES = ImmutableList.of();
+ public static final String RUNFILES_PREFIX = "".toString();
+
+ public static final ImmutableList<String> WATCHFS_BLACKLIST = ImmutableList.of();
+
+ public static final String PRELUDE_FILE_DEPOT_RELATIVE_PATH = "tools/build_rules/prelude_bazel";
+}
diff --git a/src/main/java/com/google/devtools/build/lib/UnixJniLoader.java b/src/main/java/com/google/devtools/build/lib/UnixJniLoader.java
new file mode 100644
index 0000000..ca3ca3b
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/UnixJniLoader.java
@@ -0,0 +1,39 @@
+// Copyright 2014 Google Inc. 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;
+
+import java.io.File;
+
+/**
+ * A class to load JNI dependencies for Bazel.
+ */
+public class UnixJniLoader {
+ public static void loadJni() {
+ try {
+ System.loadLibrary("unix");
+ } catch (UnsatisfiedLinkError ex) {
+ // We are probably in tests, let's try to find the library relative to where we are.
+ File cwd = new File(System.getProperty("user.dir"));
+ String libunix = "src" + File.separator + "main" + File.separator + "native" + File.separator
+ + System.mapLibraryName("unix");
+ File toTest = new File(cwd, libunix);
+ if (toTest.exists()) {
+ System.load(toTest.toString());
+ } else {
+ throw ex;
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/AbstractAction.java b/src/main/java/com/google/devtools/build/lib/actions/AbstractAction.java
new file mode 100644
index 0000000..87105d5
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/AbstractAction.java
@@ -0,0 +1,420 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.cache.MetadataHandler;
+import com.google.devtools.build.lib.actions.extra.ExtraActionInfo;
+import com.google.devtools.build.lib.collect.CollectionUtils;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.events.EventKind;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.syntax.Label;
+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 com.google.devtools.build.lib.vfs.Symlinks;
+
+import java.io.IOException;
+import java.util.Collection;
+
+/**
+ * Abstract implementation of Action which implements basic functionality: the
+ * inputs, outputs, and toString method. Both input and output sets are
+ * immutable.
+ */
+@Immutable @ThreadSafe
+public abstract class AbstractAction implements Action {
+
+ /**
+ * An arbitrary default resource set. Currently 250MB of memory, 50% CPU and 0% of total I/O.
+ */
+ public static final ResourceSet DEFAULT_RESOURCE_SET = new ResourceSet(250, 0.5, 0);
+
+ // owner/inputs/outputs attributes below should never be directly accessed even
+ // within AbstractAction itself. The appropriate getter methods should be used
+ // instead. This has to be done due to the fact that the getter methods can be
+ // overridden in subclasses.
+ private final ActionOwner owner;
+ // The variable inputs is non-final only so that actions that discover their inputs can modify it.
+ private Iterable<Artifact> inputs;
+ private final ImmutableSet<Artifact> outputs;
+
+ private int cachedInputCount = -1;
+ private String cachedKey;
+
+ /**
+ * Construct an abstract action with the specified inputs and outputs;
+ */
+ protected AbstractAction(ActionOwner owner,
+ Iterable<Artifact> inputs,
+ Iterable<Artifact> outputs) {
+ Preconditions.checkNotNull(owner);
+ // TODO(bazel-team): Use RuleContext.actionOwner here instead
+ this.owner = new ActionOwnerDescription(owner);
+ this.inputs = CollectionUtils.makeImmutable(inputs);
+ this.outputs = ImmutableSet.copyOf(outputs);
+ Preconditions.checkArgument(!this.outputs.isEmpty(), owner);
+ }
+
+ @Override
+ public final ActionOwner getOwner() {
+ return owner;
+ }
+
+ @Override
+ public boolean inputsKnown() {
+ return true;
+ }
+
+ @Override
+ public boolean discoversInputs() {
+ return false;
+ }
+
+ @Override
+ public void discoverInputs(ActionExecutionContext actionExecutionContext)
+ throws ActionExecutionException, InterruptedException {
+ throw new IllegalStateException("discoverInputs cannot be called for " + this.prettyPrint()
+ + " since it does not discover inputs");
+ }
+
+ @Override
+ public void updateInputsFromCache(
+ ArtifactResolver artifactResolver, Collection<PathFragment> inputPaths) {
+ throw new IllegalStateException(
+ "Method must be overridden for actions that may have unknown inputs.");
+ }
+
+ /**
+ * Should only be overridden by actions that need to optionally insert inputs. Actions that
+ * discover their inputs should use {@link #setInputs} to set the new iterable of inputs when they
+ * know it.
+ */
+ @Override
+ public Iterable<Artifact> getInputs() {
+ return inputs;
+ }
+
+ /**
+ * Set the inputs of the action. May only be used by an action that {@link #discoversInputs()}.
+ * The iterable passed in is automatically made immutable.
+ */
+ public void setInputs(Iterable<Artifact> inputs) {
+ Preconditions.checkState(discoversInputs());
+ this.inputs = CollectionUtils.makeImmutable(inputs);
+ cachedInputCount = -1;
+ }
+
+ /*
+ * Get count of inputs.
+ *
+ * <p>Computes the count on first invocation, returns cached value for further invocations.
+ */
+ @Override
+ @ThreadSafe
+ public synchronized int getInputCount() {
+ if (cachedInputCount == -1) {
+ cachedInputCount = Iterables.size(getInputs());
+ }
+ return cachedInputCount;
+ }
+
+ @Override
+ public ImmutableSet<Artifact> getOutputs() {
+ return outputs;
+ }
+
+ @Override
+ public Artifact getPrimaryInput() {
+ // The default behavior is to return the first input artifact.
+ // Call through the method, not the field, because it may be overridden.
+ return Iterables.getFirst(getInputs(), null);
+ }
+
+ @Override
+ public Artifact getPrimaryOutput() {
+ // Default behavior is to return the first output artifact.
+ // Use the method rather than field in case of overriding in subclasses.
+ return Iterables.getFirst(getOutputs(), null);
+ }
+
+ @Override
+ public Iterable<Artifact> getMandatoryInputs() {
+ return getInputs();
+ }
+
+ @Override
+ public String toString() {
+ return prettyPrint() + " (" + getMnemonic() + "[" + ImmutableList.copyOf(getInputs())
+ + (inputsKnown() ? " -> " : ", unknown inputs -> ")
+ + getOutputs() + "]" + ")";
+ }
+
+ @Override
+ public abstract String getMnemonic();
+ protected abstract String computeKey();
+
+ @Override
+ public synchronized final String getKey() {
+ if (cachedKey == null) {
+ cachedKey = computeKey();
+ }
+ return cachedKey;
+ }
+
+ @Override
+ public String describeKey() {
+ return null;
+ }
+
+ @Override
+ public boolean executeUnconditionally() {
+ return false;
+ }
+
+ @Override
+ public boolean isVolatile() {
+ return false;
+ }
+
+ @Override
+ public boolean showsOutputUnconditionally() {
+ return false;
+ }
+
+ @Override
+ public final String getProgressMessage() {
+ String message = getRawProgressMessage();
+ if (message == null) {
+ return null;
+ }
+ String additionalInfo = getOwner().getAdditionalProgressInfo();
+ return additionalInfo == null ? message : message + " [" + additionalInfo + "]";
+ }
+
+ /**
+ * Returns a progress message string that is specific for this action. This is
+ * then annotated with additional information, currently the string '[for host]'
+ * for actions in the host configurations.
+ *
+ * <p>A return value of null indicates no message should be reported.
+ */
+ protected String getRawProgressMessage() {
+ // A cheesy default implementation. Subclasses are invited to do something
+ // more meaningful.
+ return defaultProgressMessage();
+ }
+
+ private String defaultProgressMessage() {
+ return getMnemonic() + " " + getPrimaryOutput().prettyPrint();
+ }
+
+ @Override
+ public String prettyPrint() {
+ return "action '" + describe() + "'";
+ }
+
+ /**
+ * Deletes all of the action's output files, if they exist. If any of the
+ * Artifacts refers to a directory recursively removes the contents of the
+ * directory.
+ *
+ * @param execRoot the exec root in which this action is executed
+ */
+ protected void deleteOutputs(Path execRoot) throws IOException {
+ for (Artifact output : getOutputs()) {
+ deleteOutput(output);
+ }
+ }
+
+ /**
+ * Helper method to remove an Artifact. If the Artifact refers to a directory
+ * recursively removes the contents of the directory.
+ */
+ protected void deleteOutput(Artifact output) throws IOException {
+ Path path = output.getPath();
+ try {
+ // Optimize for the common case: output artifacts are files.
+ path.delete();
+ } catch (IOException e) {
+ // Only try to recursively delete a directory if the output root is known. This is just a
+ // sanity check so that we do not start deleting random files on disk.
+ // TODO(bazel-team): Strengthen this test by making sure that the output is part of the
+ // output tree.
+ if (path.isDirectory(Symlinks.NOFOLLOW) && output.getRoot() != null) {
+ FileSystemUtils.deleteTree(path);
+ } else {
+ throw e;
+ }
+ }
+ }
+
+ /**
+ * If the action might read directories as inputs in a way that is unsound wrt dependency
+ * checking, this method must be called.
+ */
+ protected void checkInputsForDirectories(EventHandler eventHandler,
+ MetadataHandler metadataHandler) {
+ // Report "directory dependency checking" warning only for non-generated directories (generated
+ // ones will be reported earlier).
+ for (Artifact input : getMandatoryInputs()) {
+ // Assume that if the file did not exist, we would not have gotten here.
+ if (input.isSourceArtifact() && !metadataHandler.isRegularFile(input)) {
+ eventHandler.handle(Event.warn(getOwner().getLocation(), "input '"
+ + input.prettyPrint() + "' to " + getOwner().getLabel()
+ + " is a directory; dependency checking of directories is unsound"));
+ }
+ }
+ }
+
+ @Override
+ public MiddlemanType getActionType() {
+ return MiddlemanType.NORMAL;
+ }
+
+ /**
+ * If the action might create directories as outputs this method must be called.
+ */
+ protected void checkOutputsForDirectories(EventHandler eventHandler) {
+ for (Artifact output : getOutputs()) {
+ Path path = output.getPath();
+ String ownerString = Label.print(getOwner().getLabel());
+ if (path.isDirectory()) {
+ eventHandler.handle(new Event(EventKind.WARNING, getOwner().getLocation(),
+ "output '" + output.prettyPrint() + "' of " + ownerString
+ + " is a directory; dependency checking of directories is unsound",
+ ownerString));
+ }
+ }
+ }
+
+ @Override
+ public void prepare(Path execRoot) throws IOException {
+ deleteOutputs(execRoot);
+ }
+
+ @Override
+ public String describe() {
+ String progressMessage = getProgressMessage();
+ return progressMessage != null ? progressMessage : defaultProgressMessage();
+ }
+
+ @Override
+ public abstract ResourceSet estimateResourceConsumption(Executor executor);
+
+ @Override
+ public boolean shouldReportPathPrefixConflict(Action action) {
+ return this != action;
+ }
+
+ @Override
+ public ExtraActionInfo.Builder getExtraActionInfo() {
+ return ExtraActionInfo.newBuilder()
+ .setOwner(getOwner().getLabel().toString())
+ .setId(getKey())
+ .setMnemonic(getMnemonic());
+ }
+
+ /**
+ * Returns input files that need to be present to allow extra_action rules to shadow this action
+ * correctly when run remotely. This is at least the normal inputs of the action, but may include
+ * other files as well. For example C(++) compilation may perform include file header scanning.
+ * This needs to be mirrored by the extra_action rule. Called by
+ * {@link com.google.devtools.build.lib.rules.extra.ExtraAction} at execution time.
+ *
+ * <p>As this method is called from the ExtraAction, make sure it is ok to call
+ * this method from a different thread than the one this action is executed on.
+ *
+ * @param actionExecutionContext Services in the scope of the action, like the Out/Err streams.
+ * @throws ActionExecutionException only when code called from this method
+ * throws that exception.
+ * @throws InterruptedException if interrupted
+ */
+ public Iterable<Artifact> getInputFilesForExtraAction(
+ ActionExecutionContext actionExecutionContext)
+ throws ActionExecutionException, InterruptedException {
+ return getInputs();
+ }
+
+ /**
+ * A copying implementation of {@link ActionOwner}.
+ *
+ * <p>ConfiguredTargets implement ActionOwner themselves, but we do not want actions
+ * to keep direct references to configured targets just for a label and a few strings.
+ */
+ @Immutable
+ private static class ActionOwnerDescription implements ActionOwner {
+
+ private final Location location;
+ private final Label label;
+ private final String configurationName;
+ private final String configurationMnemonic;
+ private final String configurationKey;
+ private final String targetKind;
+ private final String additionalProgressInfo;
+
+ private ActionOwnerDescription(ActionOwner originalOwner) {
+ this.location = originalOwner.getLocation();
+ this.label = originalOwner.getLabel();
+ this.configurationName = originalOwner.getConfigurationName();
+ this.configurationMnemonic = originalOwner.getConfigurationMnemonic();
+ this.configurationKey = originalOwner.getConfigurationShortCacheKey();
+ this.targetKind = originalOwner.getTargetKind();
+ this.additionalProgressInfo = originalOwner.getAdditionalProgressInfo();
+ }
+
+ @Override
+ public Location getLocation() {
+ return location;
+ }
+
+ @Override
+ public Label getLabel() {
+ return label;
+ }
+
+ @Override
+ public String getConfigurationName() {
+ return configurationName;
+ }
+
+ @Override
+ public String getConfigurationMnemonic() {
+ return configurationMnemonic;
+ }
+
+ @Override
+ public String getConfigurationShortCacheKey() {
+ return configurationKey;
+ }
+
+ @Override
+ public String getTargetKind() {
+ return targetKind;
+ }
+
+ @Override
+ public String getAdditionalProgressInfo() {
+ return additionalProgressInfo;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/AbstractActionOwner.java b/src/main/java/com/google/devtools/build/lib/actions/AbstractActionOwner.java
new file mode 100644
index 0000000..0272160
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/AbstractActionOwner.java
@@ -0,0 +1,70 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.syntax.Label;
+
+/**
+ * An action owner base class that provides default implementations for some of
+ * the {@link ActionOwner} methods.
+ */
+public abstract class AbstractActionOwner implements ActionOwner {
+
+ @Override
+ public String getAdditionalProgressInfo() {
+ return null;
+ }
+
+ @Override
+ public Location getLocation() {
+ return null;
+ }
+
+ @Override
+ public Label getLabel() {
+ return null;
+ }
+
+ @Override
+ public String getTargetKind() {
+ return "empty target kind";
+ }
+
+ @Override
+ public String getConfigurationName() {
+ return "empty configuration";
+ }
+
+ /**
+ * An action owner for special cases. Usage is strongly discouraged.
+ */
+ public static final ActionOwner SYSTEM_ACTION_OWNER = new AbstractActionOwner() {
+ @Override
+ public final String getConfigurationName() {
+ return "system";
+ }
+
+ @Override
+ public String getConfigurationMnemonic() {
+ return "system";
+ }
+
+ @Override
+ public final String getConfigurationShortCacheKey() {
+ return "system";
+ }
+ };
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/Action.java b/src/main/java/com/google/devtools/build/lib/actions/Action.java
new file mode 100644
index 0000000..4970203
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/Action.java
@@ -0,0 +1,188 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+import com.google.devtools.build.lib.actions.extra.ExtraActionInfo;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ConditionallyThreadCompatible;
+import com.google.devtools.build.lib.profiler.Describable;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.io.IOException;
+import java.util.Collection;
+
+import javax.annotation.Nullable;
+
+/**
+ * An Action represents a function from Artifacts to Artifacts executed as an
+ * atomic build step. Examples include compilation of a single C++ source
+ * file, or linking a single library.
+ */
+public interface Action extends ActionMetadata, Describable {
+
+ /**
+ * Prepares for executing this action; called by the Builder prior to
+ * executing the Action itself. This method should prepare the file system, so
+ * that the execution of the Action can write the output files. At a minimum
+ * any pre-existing and write protected output files should be removed or the
+ * permissions should be changed, so that they can be safely overwritten by
+ * the action.
+ *
+ * @throws IOException if there is an error deleting the outputs.
+ */
+ void prepare(Path execRoot) throws IOException;
+
+ /**
+ * Executes this action; called by the Builder when all of this Action's
+ * inputs have been successfully created. (Behaviour is undefined if the
+ * prerequisites are not up to date.) This method <i>actually does the work
+ * of the Action, unconditionally</i>; in other words, it is invoked by the
+ * Builder only when dependency analysis has deemed it necessary.</p>
+ *
+ * <p>The framework guarantees that the output directory for each file in
+ * <code>getOutputs()</code> has already been created, and will check to
+ * ensure that each of those files is indeed created.</p>
+ *
+ * <p>Implementations of this method should try to honour the {@link
+ * java.lang.Thread#interrupted} contract: if an interrupt is delivered to
+ * the thread in which execution occurs, the action should detect this on a
+ * best-effort basis and terminate as quickly as possible by throwing an
+ * ActionExecutionException.
+ *
+ * <p>Action execution must be ThreadCompatible in order to be safely used
+ * with a concurrent Builder implementation such as ParallelBuilder.
+ *
+ * @param actionExecutionContext Services in the scope of the action, like the output and error
+ * streams to use for messages arising during action execution.
+ * @throws ActionExecutionException if execution fails for any reason.
+ * @throws InterruptedException
+ */
+ @ConditionallyThreadCompatible
+ void execute(ActionExecutionContext actionExecutionContext)
+ throws ActionExecutionException, InterruptedException;
+
+ /**
+ * Returns true iff action must be executed regardless of its current state.
+ * Default implementation can be overridden by some actions that might be
+ * executed unconditionally under certain circumstances - e.g., if caching of
+ * test results is not requested, this method could be used to force test
+ * execution even if all dependencies are up-to-date.
+ *
+ * <p>Note, it is <b>very</b> important not to abuse this method, since it
+ * completely overrides dependency checking. Any use of this method must
+ * be carefully reviewed and proved to be necessary.
+ *
+ * <p>Note that the definition of {@link #isVolatile} depends on the
+ * definition of this method, so be sure to consider both methods together
+ * when making changes.
+ */
+ boolean executeUnconditionally();
+
+ /**
+ * Returns true if it's ever possible that {@link #executeUnconditionally}
+ * could evaluate to true during the lifetime of this instance, false
+ * otherwise.
+ */
+ boolean isVolatile();
+
+ /**
+ * Method used to find inputs before execution for an action that
+ * {@link ActionMetadata#discoversInputs}.
+ */
+ public void discoverInputs(ActionExecutionContext actionExecutionContext)
+ throws ActionExecutionException, InterruptedException;
+
+ /**
+ * Method used to update action inputs based on the information contained in
+ * the action cache. It will be called iff inputsKnown() is false for the
+ * given action instance and there is a related cache entry in the action
+ * cache.
+ *
+ * Method must be redefined for any action that may return
+ * inputsKnown() == false. It also expects that implementation will ensure
+ * that inputsKnown() returns true after call to this method.
+ *
+ * @param artifactResolver the artifact factory that can be used to manufacture artifacts
+ * @param inputPaths List of relative (to the execution root) input paths
+ */
+ public void updateInputsFromCache(
+ ArtifactResolver artifactResolver, Collection<PathFragment> inputPaths);
+
+ /**
+ * Return a best-guess estimate of the operation's resource consumption on the
+ * local host itself for use in scheduling.
+ *
+ * @param executor the application-specific value passed to the
+ * executor parameter of the top-level call to
+ * Builder.buildArtifacts().
+ */
+ @Nullable ResourceSet estimateResourceConsumption(Executor executor);
+
+ /**
+ * @return true iff path prefix conflict (conflict where two actions generate
+ * two output artifacts with one of the artifact's path being the
+ * prefix for another) between this action and another action should
+ * be reported.
+ */
+ boolean shouldReportPathPrefixConflict(Action action);
+
+ /**
+ * Returns true if the output should bypass output filtering. This is used for test actions.
+ */
+ boolean showsOutputUnconditionally();
+
+ /**
+ * Called by {@link com.google.devtools.build.lib.rules.extra.ExtraAction} at execution time to
+ * extract information from this action into a protocol buffer to be used by extra_action rules.
+ *
+ * <p>As this method is called from the ExtraAction, make sure it is ok to call this method from
+ * a different thread than the one this action is executed on.
+ */
+ ExtraActionInfo.Builder getExtraActionInfo();
+
+ /**
+ * Returns the action type. Must not be {@code null}.
+ */
+ MiddlemanType getActionType();
+
+ /**
+ * The action type.
+ */
+ public enum MiddlemanType {
+
+ /** A normal action. */
+ NORMAL,
+
+ /** A normal middleman, which just encapsulates a list of artifacts. */
+ AGGREGATING_MIDDLEMAN,
+
+ /**
+ * A middleman that enforces action ordering, is not validated by the dependency checker, but
+ * allows errors to be propagated.
+ */
+ ERROR_PROPAGATING_MIDDLEMAN,
+
+ /**
+ * A runfiles middleman, which is validated by the dependency checker, but is not expanded
+ * in blaze. Instead, the runfiles manifest is sent to remote execution client, which
+ * performs the expansion.
+ */
+ RUNFILES_MIDDLEMAN;
+
+ public boolean isMiddleman() {
+ return this != NORMAL;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/ActionCacheChecker.java b/src/main/java/com/google/devtools/build/lib/actions/ActionCacheChecker.java
new file mode 100644
index 0000000..2525c8d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/ActionCacheChecker.java
@@ -0,0 +1,341 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.Action.MiddlemanType;
+import com.google.devtools.build.lib.actions.cache.ActionCache;
+import com.google.devtools.build.lib.actions.cache.Digest;
+import com.google.devtools.build.lib.actions.cache.Metadata;
+import com.google.devtools.build.lib.actions.cache.MetadataHandler;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.events.EventKind;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nullable;
+
+/**
+ * Checks whether an {@link Action} needs to be executed, or whether it has not changed since it was
+ * last stored in the action cache. Must be informed of the new Action data after execution as well.
+ *
+ * <p>The fingerprint, input files names, and metadata (either mtimes or MD5sums) of each action are
+ * cached in the action cache to avoid unnecessary rebuilds. Middleman artifacts are handled
+ * specially, avoiding the need to create actual files corresponding to the middleman artifacts.
+ * Instead of that, results of MiddlemanAction dependency checks are cached internally and then
+ * reused whenever an input middleman artifact is encountered.
+ *
+ * <p>While instances of this class hold references to action and metadata cache instances, they are
+ * otherwise lightweight, and should be constructed anew and discarded for each build request.
+ */
+public class ActionCacheChecker {
+ private final ActionCache actionCache;
+ private final Predicate<? super Action> executionFilter;
+ private final ArtifactResolver artifactResolver;
+ // True iff --verbose_explanations flag is set.
+ private final boolean verboseExplanations;
+
+ public ActionCacheChecker(ActionCache actionCache, ArtifactResolver artifactResolver,
+ Predicate<? super Action> executionFilter, boolean verboseExplanations) {
+ this.actionCache = actionCache;
+ this.executionFilter = executionFilter;
+ this.artifactResolver = artifactResolver;
+ this.verboseExplanations = verboseExplanations;
+ }
+
+ public boolean isActionExecutionProhibited(Action action) {
+ return !executionFilter.apply(action);
+ }
+
+ /**
+ * Checks whether one of existing output paths is already used as a key.
+ * If yes, returns it - otherwise uses first output file as a key
+ */
+ private ActionCache.Entry getCacheEntry(Action action) {
+ for (Artifact output : action.getOutputs()) {
+ ActionCache.Entry entry = actionCache.get(output.getExecPathString());
+ if (entry != null) {
+ return entry;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Validate metadata state for action input or output artifacts.
+ *
+ * @param entry cached action information.
+ * @param action action to be validated.
+ * @param metadataHandler provider of metadata for the artifacts this action interacts with.
+ * @param checkOutput true to validate output artifacts, Otherwise, just
+ * validate inputs.
+ *
+ * @return true if at least one artifact has changed, false - otherwise.
+ */
+ private boolean validateArtifacts(ActionCache.Entry entry, Action action,
+ MetadataHandler metadataHandler, boolean checkOutput) {
+ Iterable<Artifact> artifacts = checkOutput
+ ? Iterables.concat(action.getOutputs(), action.getInputs())
+ : action.getInputs();
+ Map<String, Metadata> mdMap = new HashMap<>();
+ for (Artifact artifact : artifacts) {
+ mdMap.put(artifact.getExecPathString(), metadataHandler.getMetadataMaybe(artifact));
+ }
+ return !Digest.fromMetadata(mdMap).equals(entry.getFileDigest());
+ }
+
+ private void reportCommand(EventHandler handler, Action action) {
+ if (handler != null) {
+ if (verboseExplanations) {
+ String keyDescription = action.describeKey();
+ reportRebuild(handler, action,
+ keyDescription == null ? "action command has changed" :
+ "action command has changed.\nNew action: " + keyDescription);
+ } else {
+ reportRebuild(handler, action,
+ "action command has changed (try --verbose_explanations for more info)");
+ }
+ }
+ }
+
+ protected boolean unconditionalExecution(Action action) {
+ return !isActionExecutionProhibited(action) && action.executeUnconditionally();
+ }
+
+ /**
+ * Checks whether {@code action} needs to be executed and returns a non-null Token if so.
+ *
+ * <p>The method checks if any of the action's inputs or outputs have changed. Returns a non-null
+ * {@link Token} if the action needs to be executed, and null otherwise.
+ *
+ * <p>If this method returns non-null, indicating that the action will be executed, the
+ * metadataHandler's {@link MetadataHandler#discardMetadata} method must be called, so that it
+ * does not serve stale metadata for the action's outputs after the action is executed.
+ */
+ // Note: the handler should only be used for DEPCHECKER events; there's no
+ // guarantee it will be available for other events.
+ public Token getTokenIfNeedToExecute(Action action, EventHandler handler,
+ MetadataHandler metadataHandler) {
+ // TODO(bazel-team): (2010) For RunfilesAction/SymlinkAction and similar actions that
+ // produce only symlinks we should not check whether inputs are valid at all - all that matters
+ // that inputs and outputs are still exist (and new inputs have not appeared). All other checks
+ // are unnecessary. In other words, the only metadata we should check for them is file existence
+ // itself.
+
+ MiddlemanType middlemanType = action.getActionType();
+ if (middlemanType.isMiddleman()) {
+ // Some types of middlemen are not checked because they should not
+ // propagate invalidation of their inputs.
+ if (middlemanType != MiddlemanType.ERROR_PROPAGATING_MIDDLEMAN) {
+ checkMiddlemanAction(action, handler, metadataHandler);
+ }
+ return null;
+ }
+ ActionCache.Entry entry = null; // Populated lazily.
+
+ // Update action inputs from cache, if necessary.
+ boolean inputsKnown = action.inputsKnown();
+ if (!inputsKnown) {
+ Preconditions.checkState(action.discoversInputs());
+ entry = getCacheEntry(action);
+ updateActionInputs(action, entry);
+ }
+ if (mustExecute(action, entry, handler, metadataHandler)) {
+ return new Token(getKeyString(action));
+ }
+ return null;
+ }
+
+ protected boolean mustExecute(Action action, @Nullable ActionCache.Entry entry,
+ EventHandler handler, MetadataHandler metadataHandler) {
+ // Unconditional execution can be applied only for actions that are allowed to be executed.
+ if (unconditionalExecution(action)) {
+ Preconditions.checkState(action.isVolatile());
+ reportUnconditionalExecution(handler, action);
+ return true; // must execute - unconditional execution is requested.
+ }
+
+ if (entry == null) {
+ entry = getCacheEntry(action);
+ }
+ if (entry == null) {
+ reportNewAction(handler, action);
+ return true; // must execute -- no cache entry (e.g. first build)
+ }
+
+ if (entry.isCorrupted()) {
+ reportCorruptedCacheEntry(handler, action);
+ return true; // cache entry is corrupted - must execute
+ } else if (validateArtifacts(entry, action, metadataHandler, true)) {
+ reportChanged(handler, action);
+ return true; // files have changed
+ } else if (!entry.getActionKey().equals(action.getKey())){
+ reportCommand(handler, action);
+ return true; // must execute -- action key is different
+ }
+
+ entry.getFileDigest();
+ return false; // cache hit
+ }
+
+ public void afterExecution(Action action, Token token, MetadataHandler metadataHandler)
+ throws IOException {
+ Preconditions.checkArgument(token != null);
+ String key = token.cacheKey;
+ ActionCache.Entry entry = actionCache.createEntry(action.getKey());
+ for (Artifact output : action.getOutputs()) {
+ // Remove old records from the cache if they used different key.
+ String execPath = output.getExecPathString();
+ if (!key.equals(execPath)) {
+ actionCache.remove(key);
+ }
+ // Output files *must* exist and be accessible after successful action execution.
+ Metadata metadata = metadataHandler.getMetadata(output);
+ Preconditions.checkState(metadata != null);
+ entry.addFile(output.getExecPath(), metadata);
+ }
+ for (Artifact input : action.getInputs()) {
+ entry.addFile(input.getExecPath(), metadataHandler.getMetadataMaybe(input));
+ }
+ entry.getFileDigest();
+ actionCache.put(key, entry);
+ }
+
+ protected void updateActionInputs(Action action, ActionCache.Entry entry) {
+ if (entry == null || entry.isCorrupted()) {
+ return;
+ }
+
+ List<PathFragment> outputs = new ArrayList<>();
+ for (Artifact output : action.getOutputs()) {
+ outputs.add(output.getExecPath());
+ }
+ List<PathFragment> inputs = new ArrayList<>();
+ for (String path : entry.getPaths()) {
+ PathFragment execPath = new PathFragment(path);
+ // Code assumes that action has only 1-2 outputs and ArrayList.contains() will be
+ // most efficient.
+ if (!outputs.contains(execPath)) {
+ inputs.add(execPath);
+ }
+ }
+ action.updateInputsFromCache(artifactResolver, inputs);
+ }
+
+ /**
+ * Special handling for the MiddlemanAction. Since MiddlemanAction output
+ * artifacts are purely fictional and used only to stay within dependency
+ * graph model limitations (action has to depend on artifacts, not on other
+ * actions), we do not need to validate metadata for the outputs - only for
+ * inputs. We also do not need to validate MiddlemanAction key, since action
+ * cache entry key already incorporates that information for the middlemen
+ * and we will experience a cache miss when it is different. Whenever it
+ * encounters middleman artifacts as input artifacts for other actions, it
+ * consults with the aggregated middleman digest computed here.
+ */
+ protected void checkMiddlemanAction(Action action, EventHandler handler,
+ MetadataHandler metadataHandler) {
+ Artifact middleman = action.getPrimaryOutput();
+ String cacheKey = middleman.getExecPathString();
+ ActionCache.Entry entry = actionCache.get(cacheKey);
+ boolean changed = false;
+ if (entry != null) {
+ if (entry.isCorrupted()) {
+ reportCorruptedCacheEntry(handler, action);
+ changed = true;
+ } else if (validateArtifacts(entry, action, metadataHandler, false)) {
+ reportChanged(handler, action);
+ changed = true;
+ }
+ } else {
+ reportChangedDeps(handler, action);
+ changed = true;
+ }
+ if (changed) {
+ // Compute the aggregated middleman digest.
+ // Since we never validate action key for middlemen, we should not store
+ // it in the cache entry and just use empty string instead.
+ entry = actionCache.createEntry("");
+ for (Artifact input : action.getInputs()) {
+ entry.addFile(input.getExecPath(), metadataHandler.getMetadataMaybe(input));
+ }
+ }
+
+ metadataHandler.setDigestForVirtualArtifact(middleman, entry.getFileDigest());
+ if (changed) {
+ actionCache.put(cacheKey, entry);
+ }
+ }
+
+ /**
+ * Returns an action key. It is always set to the first output exec path string.
+ */
+ private static String getKeyString(Action action) {
+ Preconditions.checkState(!action.getOutputs().isEmpty());
+ return action.getOutputs().iterator().next().getExecPathString();
+ }
+
+
+ /**
+ * In most cases, this method should not be called directly - reportXXX() methods
+ * should be used instead. This is done to avoid cost associated with building
+ * the message.
+ */
+ private static void reportRebuild(@Nullable EventHandler handler, Action action, String message) {
+ // For MiddlemanAction, do not report rebuild.
+ if (handler != null && !action.getActionType().isMiddleman()) {
+ handler.handle(new Event(
+ EventKind.DEPCHECKER, null, "Executing " + action.prettyPrint() + ": " + message + "."));
+ }
+ }
+
+ // Called by IncrementalDependencyChecker.
+ protected static void reportUnconditionalExecution(
+ @Nullable EventHandler handler, Action action) {
+ reportRebuild(handler, action, "unconditional execution is requested");
+ }
+
+ private static void reportChanged(@Nullable EventHandler handler, Action action) {
+ reportRebuild(handler, action, "One of the files has changed");
+ }
+
+ private static void reportChangedDeps(@Nullable EventHandler handler, Action action) {
+ reportRebuild(handler, action, "the set of files on which this action depends has changed");
+ }
+
+ private static void reportNewAction(@Nullable EventHandler handler, Action action) {
+ reportRebuild(handler, action, "no entry in the cache (action is new)");
+ }
+
+ private static void reportCorruptedCacheEntry(@Nullable EventHandler handler, Action action) {
+ reportRebuild(handler, action, "cache entry is corrupted");
+ }
+
+ /** Wrapper for all context needed by the ActionCacheChecker to handle a single action. */
+ public static final class Token {
+ private final String cacheKey;
+
+ private Token(String cacheKey) {
+ this.cacheKey = Preconditions.checkNotNull(cacheKey);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/ActionCompletionEvent.java b/src/main/java/com/google/devtools/build/lib/actions/ActionCompletionEvent.java
new file mode 100644
index 0000000..a2cb577
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/ActionCompletionEvent.java
@@ -0,0 +1,33 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+/**
+ * An event that is fired after an action completes (either successfully or not).
+ */
+public final class ActionCompletionEvent {
+
+ private final ActionMetadata actionMetadata;
+
+ public ActionCompletionEvent(ActionMetadata actionMetadata) {
+ this.actionMetadata = actionMetadata;
+ }
+
+ /**
+ * Returns the action metadata.
+ */
+ public ActionMetadata getActionMetadata() {
+ return actionMetadata;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/ActionContextConsumer.java b/src/main/java/com/google/devtools/build/lib/actions/ActionContextConsumer.java
new file mode 100644
index 0000000..4c0eaa2
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/ActionContextConsumer.java
@@ -0,0 +1,58 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+import com.google.devtools.build.lib.actions.Executor.ActionContext;
+
+import java.util.Map;
+
+/**
+ * An object describing that actions require a particular implementation of an
+ * {@link ActionContext}.
+ *
+ * <p>This is expected to be implemented by modules that also implement actions which need these
+ * contexts. Other modules will provide implementations for various action contexts by implementing
+ * {@link ActionContextProvider}.
+ *
+ * <p>Example: a module requires {@code SpawnActionContext} to do its job, and it creates
+ * actions with the mnemonic <code>C++</code>. Then the {@link #getSpawnActionContexts} method of
+ * this module would return a map with the key <code>"C++"</code> in it.
+ *
+ * <p>The module can either decide for itself which implementation is needed and make the value
+ * associated with this key a constant or defer that decision to the user, for example, by
+ * providing a command line option and setting the value in the map based on that.
+ *
+ * <p>Other modules are free to provide different implementations of {@code SpawnActionContext}.
+ * This can be used, for example, to implement sandboxed or distributed execution of
+ * {@code SpawnAction}s in different ways, while giving the user control over how exactly they
+ * are executed.
+ */
+public interface ActionContextConsumer {
+ /**
+ * Returns a map from spawn action mnemonics created by this module to the name of the
+ * implementation of {@code SpawnActionContext} that the module wants to use for executing
+ * it.
+ *
+ * <p>If a spawn action is executed whose mnemonic maps to the empty string or is not
+ * present in the map at all, the choice of the implementation is left to Blaze.
+ */
+ public Map<String, String> getSpawnActionContexts();
+
+ /**
+ * Returns a map from action context class to the implementation required by the module.
+ *
+ * <p>If the implementation name is the empty string, the choice is left to Blaze.
+ */
+ public Map<Class<? extends ActionContext>, String> getActionContexts();
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/ActionContextMarker.java b/src/main/java/com/google/devtools/build/lib/actions/ActionContextMarker.java
new file mode 100644
index 0000000..fcb2e3d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/ActionContextMarker.java
@@ -0,0 +1,30 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+import com.google.devtools.build.lib.actions.Executor.ActionContext;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation for action contexts. Actions contexts should also implement {@link ActionContext}.
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ActionContextMarker {
+ String name();
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/ActionContextProvider.java b/src/main/java/com/google/devtools/build/lib/actions/ActionContextProvider.java
new file mode 100644
index 0000000..6e52a4a
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/ActionContextProvider.java
@@ -0,0 +1,51 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+import com.google.devtools.build.lib.actions.Executor.ActionContext;
+
+/**
+ * An object that provides execution strategies to {@link BlazeExecutor}.
+ *
+ * <p>For more information, see {@link ActionContextConsumer}.
+ */
+public interface ActionContextProvider {
+ /**
+ * Returns the execution strategies that are provided by this object.
+ *
+ * <p>These may or may not actually end up in the executor depending on the command line options
+ * and other factors influencing how the executor is set up.
+ */
+ Iterable<ActionContext> getActionContexts();
+
+ /**
+ * Called when the executor is constructed. The parameter contains all the contexts that were
+ * selected for this execution phase.
+ */
+ void executorCreated(Iterable<ActionContext> usedContexts) throws ExecutorInitException;
+
+ /**
+ * Called when the execution phase is started.
+ */
+ void executionPhaseStarting(
+ ActionInputFileCache actionInputFileCache,
+ ActionGraph actionGraph,
+ Iterable<Artifact> topLevelArtifacts)
+ throws ExecutorInitException, InterruptedException;
+
+ /**
+ * Called when the execution phase is finished.
+ */
+ void executionPhaseEnding();
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/ActionExecutedEvent.java b/src/main/java/com/google/devtools/build/lib/actions/ActionExecutedEvent.java
new file mode 100644
index 0000000..00ef9b4
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/ActionExecutedEvent.java
@@ -0,0 +1,51 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+/**
+ * This event is fired during the build, when an action is executed. It contains information about
+ * the action: the Action itself, and the output file names its stdout and stderr are recorded in.
+ */
+public class ActionExecutedEvent {
+ private final Action action;
+ private final ActionExecutionException exception;
+ private final String stdout;
+ private final String stderr;
+
+ public ActionExecutedEvent(Action action,
+ ActionExecutionException exception, String stdout, String stderr) {
+ this.action = action;
+ this.exception = exception;
+ this.stdout = stdout;
+ this.stderr = stderr;
+ }
+
+ public Action getAction() {
+ return action;
+ }
+
+ // null if action succeeded
+ public ActionExecutionException getException() {
+ return exception;
+ }
+
+ public String getStdout() {
+ return stdout;
+ }
+
+ public String getStderr() {
+ return stderr;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/ActionExecutionContext.java b/src/main/java/com/google/devtools/build/lib/actions/ActionExecutionContext.java
new file mode 100644
index 0000000..d6d08fa
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/ActionExecutionContext.java
@@ -0,0 +1,73 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+import com.google.devtools.build.lib.actions.Artifact.MiddlemanExpander;
+import com.google.devtools.build.lib.actions.cache.MetadataHandler;
+import com.google.devtools.build.lib.util.io.FileOutErr;
+
+/**
+ * A class that groups services in the scope of the action. Like the FileOutErr object.
+ */
+public class ActionExecutionContext {
+
+ private final Executor executor;
+ private final ActionInputFileCache actionInputFileCache;
+ private final MetadataHandler metadataHandler;
+ private final FileOutErr fileOutErr;
+ private final MiddlemanExpander middlemanExpander;
+
+ public ActionExecutionContext(Executor executor, ActionInputFileCache actionInputFileCache,
+ MetadataHandler metadataHandler, FileOutErr fileOutErr, MiddlemanExpander middlemanExpander) {
+ this.actionInputFileCache = actionInputFileCache;
+ this.metadataHandler = metadataHandler;
+ this.fileOutErr = fileOutErr;
+ this.executor = executor;
+ this.middlemanExpander = middlemanExpander;
+ }
+
+ public ActionInputFileCache getActionInputFileCache() {
+ return actionInputFileCache;
+ }
+
+ public MetadataHandler getMetadataHandler() {
+ return metadataHandler;
+ }
+
+ public Executor getExecutor() {
+ return executor;
+ }
+
+ public MiddlemanExpander getMiddlemanExpander() {
+ return middlemanExpander;
+ }
+
+ /**
+ * Provide that {@code FileOutErr} that the action should use for redirecting the output and error
+ * stream.
+ */
+ public FileOutErr getFileOutErr() {
+ return fileOutErr;
+ }
+
+ /**
+ * Allows us to create a new context that overrides the FileOutErr with another one. This is
+ * useful for muting the output for example.
+ */
+ public ActionExecutionContext withFileOutErr(FileOutErr fileOutErr) {
+ return new ActionExecutionContext(executor, actionInputFileCache, metadataHandler, fileOutErr,
+ middlemanExpander);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/ActionExecutionException.java b/src/main/java/com/google/devtools/build/lib/actions/ActionExecutionException.java
new file mode 100644
index 0000000..0d0d908
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/ActionExecutionException.java
@@ -0,0 +1,113 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.syntax.Label;
+
+/**
+ * This exception gets thrown if {@link Action#execute(ActionExecutionContext)} is unsuccessful.
+ * Typically these are re-raised ExecException throwables.
+ */
+@ThreadSafe
+public class ActionExecutionException extends Exception {
+
+ private final Action action;
+ private final NestedSet<Label> rootCauses;
+ private final boolean catastrophe;
+
+ public ActionExecutionException(Throwable cause, Action action, boolean catastrophe) {
+ super(cause.getMessage(), cause);
+ this.action = action;
+ this.rootCauses = rootCausesFromAction(action);
+ this.catastrophe = catastrophe;
+ }
+
+ public ActionExecutionException(String message,
+ Throwable cause, Action action, boolean catastrophe) {
+ super(message + ": " + cause.getMessage(), cause);
+ this.action = action;
+ this.rootCauses = rootCausesFromAction(action);
+ this.catastrophe = catastrophe;
+ }
+
+ public ActionExecutionException(String message, Action action, boolean catastrophe) {
+ super(message);
+ this.action = action;
+ this.rootCauses = rootCausesFromAction(action);
+ this.catastrophe = catastrophe;
+ }
+
+ public ActionExecutionException(String message, Action action,
+ NestedSet<Label> rootCauses, boolean catastrophe) {
+ super(message);
+ this.action = action;
+ this.rootCauses = rootCauses;
+ this.catastrophe = catastrophe;
+ }
+
+ public ActionExecutionException(String message, Throwable cause, Action action,
+ NestedSet<Label> rootCauses, boolean catastrophe) {
+ super(message, cause);
+ this.action = action;
+ this.rootCauses = rootCauses;
+ this.catastrophe = catastrophe;
+ }
+
+ static NestedSet<Label> rootCausesFromAction(Action action) {
+ return action == null || action.getOwner() == null || action.getOwner().getLabel() == null
+ ? NestedSetBuilder.<Label>emptySet(Order.STABLE_ORDER)
+ : NestedSetBuilder.create(Order.STABLE_ORDER, action.getOwner().getLabel());
+ }
+
+ /**
+ * Returns the action that failed.
+ */
+ public Action getAction() {
+ return action;
+ }
+
+ /**
+ * Return the root causes that should be reported. Usually the owner of the action, but it can
+ * be the label of a missing artifact.
+ */
+ public NestedSet<Label> getRootCauses() {
+ return rootCauses;
+ }
+
+ /**
+ * Returns the location of the owner of this action. May be null.
+ */
+ public Location getLocation() {
+ return action.getOwner().getLocation();
+ }
+
+ /**
+ * Catastrophic exceptions should stop builds, even if --keep_going.
+ */
+ public boolean isCatastrophe() {
+ return catastrophe;
+ }
+
+ /**
+ * Returns true if the error should be shown.
+ */
+ public boolean showError() {
+ return getMessage() != null;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/ActionExecutionStatusReporter.java b/src/main/java/com/google/devtools/build/lib/actions/ActionExecutionStatusReporter.java
new file mode 100644
index 0000000..34aadc4
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/ActionExecutionStatusReporter.java
@@ -0,0 +1,262 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.eventbus.EventBus;
+import com.google.common.eventbus.Subscribe;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.util.BlazeClock;
+import com.google.devtools.build.lib.util.Clock;
+import com.google.devtools.build.lib.util.Pair;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.concurrent.ConcurrentHashMap;
+
+import javax.annotation.Nullable;
+
+/**
+ * Implements "Still waiting..." message functionality, displaying current status for "in-flight"
+ * actions. Used by the ParallelBuilder.
+ *
+ * TODO(bazel-team): (2010) It would be nice if "duplicated" actions (e.g. test shards and multiple
+ * test runs) were merged into the single line.
+ */
+@ThreadSafe
+public final class ActionExecutionStatusReporter {
+ // Maximum number of lines to output per each status category before truncation.
+ private static final int MAX_LINES = 10;
+
+ private final EventHandler eventHandler;
+ private final Executor executor;
+ private final EventBus eventBus;
+ private final Clock clock;
+
+ /**
+ * The status of each action "in flight", i.e. whose ExecuteBuildAction.call() method is active.
+ * Used for implementing the "still waiting" message.
+ */
+ private final Map<ActionMetadata, Pair<String, Long>> actionStatus =
+ new ConcurrentHashMap<>(100);
+
+ public static ActionExecutionStatusReporter create(EventHandler eventHandler) {
+ return create(eventHandler, null, null);
+ }
+
+ @VisibleForTesting
+ static ActionExecutionStatusReporter create(EventHandler eventHandler, Clock clock) {
+ return create(eventHandler, null, null, clock);
+ }
+
+ public static ActionExecutionStatusReporter create(EventHandler eventHandler,
+ @Nullable Executor executor, @Nullable EventBus eventBus) {
+ return create(eventHandler, executor, eventBus, null);
+ }
+
+ private static ActionExecutionStatusReporter create(EventHandler eventHandler,
+ @Nullable Executor executor, @Nullable EventBus eventBus, @Nullable Clock clock) {
+ ActionExecutionStatusReporter result = new ActionExecutionStatusReporter(eventHandler, executor,
+ eventBus, clock == null ? BlazeClock.instance() : clock);
+ if (eventBus != null) {
+ eventBus.register(result);
+ }
+ return result;
+ }
+
+ private ActionExecutionStatusReporter(EventHandler eventHandler, @Nullable Executor executor,
+ @Nullable EventBus eventBus, Clock clock) {
+ this.eventHandler = Preconditions.checkNotNull(eventHandler);
+ this.executor = executor;
+ this.eventBus = eventBus;
+ this.clock = Preconditions.checkNotNull(clock);
+ }
+
+ public void unregisterFromEventBus() {
+ if (eventBus != null) {
+ eventBus.unregister(this);
+ }
+ }
+
+ private void setStatus(ActionMetadata action, String message) {
+ actionStatus.put(action, Pair.of(message, clock.nanoTime()));
+ }
+
+ /**
+ * Remove action from the list of active actions.
+ */
+ public void remove(Action action) {
+ Preconditions.checkNotNull(actionStatus.remove(action), action);
+ }
+
+ /**
+ * Set "Preparing" status.
+ */
+ public void setPreparing(Action action) {
+ updateStatus(ActionStatusMessage.preparingStrategy(action));
+ }
+
+ public void setRunningFromBuildData(ActionMetadata action) {
+ updateStatus(ActionStatusMessage.runningStrategy(action));
+ }
+
+ @Subscribe
+ public void updateStatus(ActionStatusMessage statusMsg) {
+ String message = statusMsg.getMessage();
+ ActionMetadata action = statusMsg.getActionMetadata();
+ if (statusMsg.needsStrategy()) {
+ String strategy = action.describeStrategy(executor);
+ if (strategy == null) {
+ return;
+ }
+ message = String.format(message, strategy);
+ }
+ setStatus(action, message);
+ }
+
+ public int getCount() {
+ return actionStatus.size();
+ }
+
+ private static void appendGroupStatus(StringBuilder buffer,
+ Map<ActionMetadata, Pair<String, Long>> statusMap, String status, long currentTime) {
+ List<Pair<Long, ActionMetadata>> actions = new ArrayList<>();
+ for (Map.Entry<ActionMetadata, Pair<String, Long>> entry : statusMap.entrySet()) {
+ if (entry.getValue().first.equals(status)) {
+ actions.add(Pair.of(entry.getValue().second, entry.getKey()));
+ }
+ }
+ if (actions.size() == 0) {
+ return;
+ }
+ Collections.sort(actions, Pair.<Long, ActionMetadata>compareByFirst());
+
+ buffer.append("\n " + status + ":");
+
+ boolean truncateList = actions.size() > MAX_LINES;
+ for (Pair<Long, ActionMetadata> entry : actions.subList(0,
+ truncateList ? MAX_LINES - 1 : actions.size())) {
+ String message = entry.second.getProgressMessage();
+ if (message == null) {
+ // Actions will a null progress message should run so
+ // fast we never see them here. In any case...
+ message = entry.second.prettyPrint();
+ }
+ buffer.append("\n ").append(message);
+ long runTime = (currentTime - entry.first) / 1000000000L; // Convert to seconds.
+ buffer.append(", ").append(runTime).append(" s");
+ }
+ if (truncateList) {
+ buffer.append("\n ... ").append(actions.size() - MAX_LINES + 1).append(" more jobs");
+ }
+ }
+
+ /**
+ * Get message showing currently executing actions.
+ */
+ private String getExecutionStatusMessage(Map<ActionMetadata, Pair<String, Long>> statusMap) {
+ int count = statusMap.size();
+ StringBuilder s = count != 1
+ ? new StringBuilder("Still waiting for ").append(count).append(" jobs to complete:")
+ : new StringBuilder("Still waiting for 1 job to complete:");
+
+ long currentTime = clock.nanoTime();
+
+ // A tree is just as fast as HashSet for small data sets.
+ Set<String> statuses = new TreeSet<String>();
+ for (Map.Entry<ActionMetadata, Pair<String, Long>> entry : statusMap.entrySet()) {
+ statuses.add(entry.getValue().first);
+ }
+
+ for (String status : statuses) {
+ appendGroupStatus(s, statusMap, status, currentTime);
+ }
+ return s.toString();
+ }
+
+ /**
+ * Show currently executing actions.
+ */
+ public void showCurrentlyExecutingActions(String progressPercentageMessage) {
+ // Defensive copy to ensure thread safety.
+ Map<ActionMetadata, Pair<String, Long>> statusMap = new HashMap<>(actionStatus);
+ if (statusMap.size() > 0) {
+ eventHandler.handle(
+ Event.progress(progressPercentageMessage + getExecutionStatusMessage(statusMap)));
+ }
+ }
+
+ /**
+ * Warn about actions that are still being executed.
+ * Method is used to produce informative message when build is interrupted.
+ */
+ void warnAboutCurrentlyExecutingActions() {
+ // Defensive copy to ensure thread safety.
+ Map<ActionMetadata, Pair<String, Long>> statusMap = new HashMap<>(actionStatus);
+ if (statusMap.size() == 0) {
+ // There are no tasks in the queue so there is nothing to report.
+ eventHandler.handle(Event.warn("There are no active jobs - stopping the build"));
+ return;
+ }
+ Iterator<ActionMetadata> iterator = statusMap.keySet().iterator();
+ while (iterator.hasNext()) {
+ // Filter out actions that are not executed yet.
+ if (statusMap.get(iterator.next()).first.equals(ActionStatusMessage.PREPARING)) {
+ iterator.remove();
+ }
+ }
+ if (statusMap.size() > 0) {
+ eventHandler.handle(Event.warn(getExecutionStatusMessage(statusMap)
+ + "\nBuild will be stopped after these tasks terminate"));
+ } else {
+ // It is possible that one or more tasks in "Preparing" state just started being executed.
+ // So warn user just in case.
+ eventHandler.handle(Event.warn("Still waiting for unfinished jobs"));
+ }
+ }
+
+ /**
+ * Returns the number of seconds to wait before reporting slow progress again.
+ *
+ * @param userSpecifiedProgressInterval value of the --progress_report_interval flag; 0 means
+ * use default 10, then 30, then 60 seconds wait times
+ * @param previousWaitTime previous value returned by this method
+ */
+ public static int getWaitTime(int userSpecifiedProgressInterval, int previousWaitTime) {
+ if (userSpecifiedProgressInterval > 0) {
+ return userSpecifiedProgressInterval;
+ }
+
+ // Increase waitTime to 10, then to 30 and then to 60 seconds to reduce
+ // spamming during long wait periods. If the user specified a
+ // waitTime directly through progressReportInterval, then use
+ // that value.
+ if (previousWaitTime == 0) {
+ return 10;
+ } else if (previousWaitTime == 10) {
+ return 30;
+ } else {
+ return 60;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/ActionGraph.java b/src/main/java/com/google/devtools/build/lib/actions/ActionGraph.java
new file mode 100644
index 0000000..bb2b707
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/ActionGraph.java
@@ -0,0 +1,38 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+import javax.annotation.Nullable;
+
+/**
+ * An action graph.
+ *
+ * <p>Provides lookups of generating actions for artifacts.
+ */
+public interface ActionGraph {
+
+ /**
+ * Returns the Action that, when executed, gives rise to this file.
+ *
+ * <p>If this Artifact is a source file, null is returned. (We don't try to return a "no-op
+ * action" because that would require creating a new no-op Action for every source file, since
+ * each Action knows its outputs, so sharing all the no-ops is not an option.)
+ *
+ * <p>It's also possible for derived Artifacts to have null generating Actions when these actions
+ * are unknown.
+ */
+ @Nullable
+ Action getGeneratingAction(Artifact artifact);
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/ActionGraphVisitor.java b/src/main/java/com/google/devtools/build/lib/actions/ActionGraphVisitor.java
new file mode 100644
index 0000000..4ddda4c
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/ActionGraphVisitor.java
@@ -0,0 +1,85 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+/**
+ * An abstract visitor for the action graph. Specializes {@link BipartiteVisitor} for artifacts and
+ * actions, and takes care of visiting the complete transitive closure.
+ */
+public abstract class ActionGraphVisitor extends BipartiteVisitor<Action, Artifact> {
+
+ private final ActionGraph actionGraph;
+
+ public ActionGraphVisitor(ActionGraph actionGraph) {
+ this.actionGraph = actionGraph;
+ }
+
+ /**
+ * Called for all artifacts in the visitation. Hook for subclasses.
+ *
+ * @param artifact
+ */
+ protected void visitArtifact(Artifact artifact) {}
+
+ /**
+ * Called for all actions in the visitation. Hook for subclasses.
+ *
+ * @param action
+ */
+ protected void visitAction(Action action) {}
+
+ /**
+ * Whether the given action should be visited. If this returns false, the visitation stops here,
+ * so the dependencies of this action are also not visited.
+ *
+ * @param action
+ */
+ protected boolean shouldVisit(Action action) {
+ return true;
+ }
+
+ /**
+ * Whether the given artifact should be visited. If this returns false, the visitation stops here,
+ * so dependencies of this artifact (if it is a generated one) are also not visited.
+ *
+ * @param artifact
+ */
+ protected boolean shouldVisit(Artifact artifact) {
+ return true;
+ }
+
+ @SuppressWarnings("unused")
+ protected final void visitArtifacts(Iterable<Artifact> artifacts) {
+ for (Artifact artifact : artifacts) {
+ visitArtifact(artifact);
+ }
+ }
+
+ @Override protected void white(Artifact artifact) {
+ Action action = actionGraph.getGeneratingAction(artifact);
+ visitArtifact(artifact);
+ if (action != null && shouldVisit(action)) {
+ visitBlackNode(action);
+ }
+ }
+
+ @Override protected void black(Action action) {
+ visitAction(action);
+ for (Artifact input : action.getInputs()) {
+ if (shouldVisit(input)) {
+ visitWhiteNode(input);
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/ActionInput.java b/src/main/java/com/google/devtools/build/lib/actions/ActionInput.java
new file mode 100644
index 0000000..c370591
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/ActionInput.java
@@ -0,0 +1,39 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+/**
+ * Represents an input file to a build action, with an appropriate relative path and digest
+ * value.
+ *
+ * <p>Artifact is the only notable implementer of the interface, but the interface remains
+ * because 1) some Google specific rules ship files that could be Artifacts to remote execution
+ * by instantiating ad-hoc derived classes of ActionInput. 2) historically, Google C++ rules
+ * allow underspecified C++ builds. For that case, we have extra logic to guess the undeclared
+ * header inclusions (eg. computed inclusions). The extra logic lives in a file that is not
+ * needed for remote execution, but is a dependency, and it is inserted as a non-Artifact
+ * ActionInput.
+ *
+ * <p>ActionInput is used as a cache "key" for ActionInputFileCache: for Artifacts, the
+ * digest/size is already stored in Artifact, but for non-artifacts, we use getExecPathString
+ * to find this data in a filesystem related cache.
+ */
+public interface ActionInput {
+
+ /**
+ * @return the relative path to the input file.
+ */
+ public String getExecPathString();
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/ActionInputFileCache.java b/src/main/java/com/google/devtools/build/lib/actions/ActionInputFileCache.java
new file mode 100644
index 0000000..b45e9cd
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/ActionInputFileCache.java
@@ -0,0 +1,77 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+import com.google.protobuf.ByteString;
+
+import java.io.File;
+import java.io.IOException;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * The interface for Action inputs metadata (Digest and size).
+ *
+ * NOTE: Implementations must be thread safe.
+ */
+@ThreadSafe
+public interface ActionInputFileCache {
+ /**
+ * Returns digest for the given artifact. This digest is current as of some time t >= the start of
+ * the present build. If the artifact is an output of an action that already executed at time p,
+ * then t >= p. Aside from these properties, t can be any value and may vary arbitrarily across
+ * calls.
+ *
+ * @param input the input to retrieve the digest for
+ * @return the artifact's digest or null if digest cannot be obtained (due to artifact
+ * non-existence, lookup errors, or any other reason)
+ *
+ * @throws DigestOfDirectoryException in case {@code input} is a directory.
+ * @throws IOException If the file cannot be digested.
+ *
+ */
+ @Nullable
+ ByteString getDigest(ActionInput input) throws IOException;
+
+ /**
+ * Retrieve the size of the file at the given path. Will usually return 0 on failure instead of
+ * throwing an IOException. Returns 0 for files inaccessible to user, but available to the
+ * execution environment.
+ *
+ * @param input the input.
+ * @return the file size in bytes.
+ * @throws IOException on failure.
+ */
+ long getSizeInBytes(ActionInput input) throws IOException;
+
+ /**
+ * Checks if the file is available locally, based on the assumption that previous operations on
+ * the ActionInputFileCache would have created a cache entry for it.
+ *
+ * @param digest the digest to lookup.
+ * @return true if the specified digest is backed by a locally-readable file, false otherwise
+ */
+ boolean contentsAvailableLocally(ByteString digest);
+
+ /**
+ * Concrete subclasses must implement this to provide a mapping from digest to file path,
+ * based on files previously seen as inputs.
+ *
+ * @param digest the digest.
+ * @return a File path.
+ */
+ @Nullable
+ File getFileFromDigest(ByteString digest) throws IOException;
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/ActionInputHelper.java b/src/main/java/com/google/devtools/build/lib/actions/ActionInputHelper.java
new file mode 100644
index 0000000..0fed928
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/ActionInputHelper.java
@@ -0,0 +1,160 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
+import com.google.common.base.Functions;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.Iterables;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Helper utility to create ActionInput instances.
+ */
+public final class ActionInputHelper {
+ private ActionInputHelper() {
+ }
+
+ @VisibleForTesting
+ public static Artifact.MiddlemanExpander actionGraphMiddlemanExpander(
+ final ActionGraph actionGraph) {
+ return new Artifact.MiddlemanExpander() {
+ @Override
+ public void expand(Artifact mm, Collection<? super Artifact> output) {
+ // Skyframe is stricter in that it checks that "mm" is a input of the action, because
+ // it cannot expand arbitrary middlemen without access to a global action graph.
+ // We could check this constraint here too, but it seems unnecessary. This code is
+ // going away anyway.
+ Preconditions.checkArgument(mm.isMiddlemanArtifact(),
+ "%s is not a middleman artifact", mm);
+ Action middlemanAction = actionGraph.getGeneratingAction(mm);
+ Preconditions.checkState(middlemanAction != null, mm);
+ // TODO(bazel-team): Consider expanding recursively or throwing an exception here.
+ // Most likely, this code will cause silent errors if we ever have a middleman that
+ // contains a middleman.
+ if (middlemanAction.getActionType() == Action.MiddlemanType.AGGREGATING_MIDDLEMAN) {
+ Artifact.addNonMiddlemanArtifacts(middlemanAction.getInputs(), output,
+ Functions.<Artifact>identity());
+ }
+
+ }
+ };
+ }
+
+ /**
+ * Most ActionInputs are created and never used again. On the off chance that one is, however, we
+ * implement equality via path comparison. Since file caches are keyed by ActionInput, equality
+ * checking does come up.
+ */
+ private static class BasicActionInput implements ActionInput {
+ private final String path;
+ public BasicActionInput(String path) {
+ this.path = Preconditions.checkNotNull(path);
+ }
+
+ @Override
+ public String getExecPathString() {
+ return path;
+ }
+
+ @Override
+ public int hashCode() {
+ return path.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (other == null) {
+ return false;
+ }
+ if (!this.getClass().equals(other.getClass())) {
+ return false;
+ }
+ return this.path.equals(((BasicActionInput) other).path);
+ }
+
+ @Override
+ public String toString() {
+ return "BasicActionInput: " + path;
+ }
+ }
+
+ /**
+ * Creates an ActionInput with just the given relative path and no digest.
+ *
+ * @param path the relative path of the input.
+ * @return a ActionInput.
+ */
+ public static ActionInput fromPath(String path) {
+ return new BasicActionInput(path);
+ }
+
+ private static final Function<String, ActionInput> FROM_PATH =
+ new Function<String, ActionInput>() {
+ @Override
+ public ActionInput apply(String path) {
+ return fromPath(path);
+ }
+ };
+
+ /**
+ * Creates a sequence of {@link ActionInput}s from a sequence of string paths.
+ */
+ public static Collection<ActionInput> fromPaths(Collection<String> paths) {
+ return Collections2.transform(paths, FROM_PATH);
+ }
+
+ /**
+ * Expands middleman artifacts in a sequence of {@link ActionInput}s.
+ *
+ * <p>Non-middleman artifacts are returned untouched.
+ */
+ public static List<ActionInput> expandMiddlemen(Iterable<? extends ActionInput> inputs,
+ Artifact.MiddlemanExpander middlemanExpander) {
+
+ List<ActionInput> result = new ArrayList<>();
+ List<Artifact> containedArtifacts = new ArrayList<>();
+ for (ActionInput input : inputs) {
+ if (!(input instanceof Artifact)) {
+ result.add(input);
+ continue;
+ }
+ containedArtifacts.add((Artifact) input);
+ }
+ Artifact.addExpandedArtifacts(containedArtifacts, result, middlemanExpander);
+ return result;
+ }
+
+ /** Formatter for execPath String output. Public because Artifact uses it directly. */
+ public static final Function<ActionInput, String> EXEC_PATH_STRING_FORMATTER =
+ new Function<ActionInput, String>() {
+ @Override
+ public String apply(ActionInput input) {
+ return input.getExecPathString();
+ }
+ };
+
+ public static Iterable<String> toExecPaths(Iterable<? extends ActionInput> artifacts) {
+ return Iterables.transform(artifacts, EXEC_PATH_STRING_FORMATTER);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/ActionLogBufferPathGenerator.java b/src/main/java/com/google/devtools/build/lib/actions/ActionLogBufferPathGenerator.java
new file mode 100644
index 0000000..2c80e8a
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/ActionLogBufferPathGenerator.java
@@ -0,0 +1,42 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+import com.google.devtools.build.lib.util.io.FileOutErr;
+import com.google.devtools.build.lib.vfs.Path;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * A source for generating unique action log paths.
+ */
+public final class ActionLogBufferPathGenerator {
+
+ private final AtomicInteger actionCounter = new AtomicInteger();
+
+ private final Path actionOutputRoot;
+
+ public ActionLogBufferPathGenerator(Path actionOutputRoot) {
+ this.actionOutputRoot = actionOutputRoot;
+ }
+
+ /**
+ * Generates a unique filename for an action to store its output.
+ */
+ public FileOutErr generate() {
+ int actionId = actionCounter.incrementAndGet();
+ return new FileOutErr(actionOutputRoot.getRelative("stdout-" + actionId),
+ actionOutputRoot.getRelative("stderr-" + actionId));
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/ActionMetadata.java b/src/main/java/com/google/devtools/build/lib/actions/ActionMetadata.java
new file mode 100644
index 0000000..3569f07
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/ActionMetadata.java
@@ -0,0 +1,217 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+
+import javax.annotation.Nullable;
+
+/**
+ * Side-effect free query methods for information about an {@link Action}.
+ *
+ * <p>This method is intended for use in situations when the intention is to pass around information
+ * about an action without allowing actual execution of the action.
+ *
+ * <p>The split between {@link Action} and {@link ActionMetadata} is somewhat arbitrary, other than
+ * that all methods with side effects must belong to the former.
+ */
+public interface ActionMetadata {
+ /**
+ * If this executable can supply verbose information, returns a string that can be used as a
+ * progress message while this executable is running. A return value of {@code null} indicates no
+ * message should be reported.
+ */
+ @Nullable
+ public String getProgressMessage();
+
+ /**
+ * Returns the owner of this executable if this executable can supply verbose information. This is
+ * typically the rule that constructed it; see ActionOwner class comment for details. Returns
+ * {@code null} if no owner can be determined.
+ *
+ * <p>If this executable does not supply verbose information, this function may throw an
+ * IllegalStateException.
+ */
+ public ActionOwner getOwner();
+
+ /**
+ * Returns a mnemonic (string constant) for this kind of action; written into
+ * the master log so that the appropriate parser can be invoked for the output
+ * of the action. Effectively a public method as the value is used by the
+ * extra_action feature to match actions.
+ */
+ String getMnemonic();
+
+ /**
+ * Returns a pretty string representation of this action, suitable for use in
+ * progress messages or error messages.
+ */
+ String prettyPrint();
+
+ /**
+ * Returns a string that can be used to describe the execution strategy.
+ * For example, "local".
+ *
+ * May return null if the action chooses to update its strategy
+ * locality "manually", via ActionLocalityMessage.
+ *
+ * @param executor the application-specific value passed to the
+ * executor parameter of the top-level call to
+ * Builder.buildArtifacts().
+ */
+ public String describeStrategy(Executor executor);
+
+ /**
+ * Returns true iff the getInputs set is known to be complete.
+ *
+ * <p>For most Actions, this always returns true, but in some cases (e.g. C++ compilation), inputs
+ * are dynamically discovered from the previous execution of the Action, and so before the initial
+ * execution, this method will return false in those cases.
+ *
+ * <p>Any builder <em>must</em> unconditionally execute an Action for which inputsKnown() returns
+ * false, regardless of all other inferences made by its dependency analysis. In addition, all
+ * prerequisites mentioned in the (possibly incomplete) value returned by getInputs must also be
+ * built first, as usual.
+ */
+ @ThreadSafe
+ boolean inputsKnown();
+
+ /**
+ * Returns true iff inputsKnown() may ever return false.
+ */
+ @ThreadSafe
+ boolean discoversInputs();
+
+ /**
+ * Returns the input Artifacts that this Action depends upon. May be empty.
+ *
+ * <p>For subclasses overriding getInputs(), if getInputs() could return different values in the
+ * lifetime of an object, {@link #getInputCount()} must also be overridden.
+ *
+ * <p>During execution, the {@link Iterable} returned by {@code getInputs} <em>must not</em> be
+ * concurrently modified before the value is fully read in {@code JavaDistributorDriver#exec} (via
+ * the {@code Iterable<ActionInput>} argument there). Violating this would require somewhat
+ * pathological behavior by the {@link Action}, since it would have to modify its inputs, as a
+ * list, say, without reassigning them. This should never happen with any Action subclassing
+ * AbstractAction, since AbstractAction's implementation of getInputs() returns an immutable
+ * iterable.
+ */
+ Iterable<Artifact> getInputs();
+
+ /**
+ * Returns the number of input Artifacts that this Action depends upon.
+ *
+ * <p>Must be consistent with {@link #getInputs()}.
+ */
+ int getInputCount();
+
+ /**
+ * Returns the (unordered, immutable) set of output Artifacts that
+ * this action generates. (It would not make sense for this to be empty.)
+ */
+ ImmutableSet<Artifact> getOutputs();
+
+ /**
+ * Returns the "primary" input of this action, if applicable.
+ *
+ * <p>For example, a C++ compile action would return the .cc file which is being compiled,
+ * irrespective of the other inputs.
+ *
+ * <p>May return null.
+ */
+ Artifact getPrimaryInput();
+
+ /**
+ * Returns the "primary" output of this action.
+ *
+ * <p>For example, the linked library would be the primary output of a LinkAction.
+ *
+ * <p>Never returns null.
+ */
+ Artifact getPrimaryOutput();
+
+ /**
+ * Returns an iterable of input Artifacts that MUST exist prior to executing an action. In other
+ * words, in case when action is scheduled for execution, builder will ensure that all artifacts
+ * returned by this method are present in the filesystem (artifact.getPath().exists() is true) or
+ * action execution will be aborted with an error that input file does not exist. While in
+ * majority of cases this method will return all action inputs, for some actions (e.g.
+ * CppCompileAction) it can return a subset of inputs because that not all action inputs might be
+ * mandatory for action execution to succeed (e.g. header files retrieved from *.d file from the
+ * previous build).
+ */
+ Iterable<Artifact> getMandatoryInputs();
+
+ /**
+ * <p>Returns a string encoding all of the significant behaviour of this
+ * Action that might affect the output. The general contract of
+ * <code>getKey</code> is this: if the work to be performed by the
+ * execution of this action changes, the key must change. </p>
+ *
+ * <p>As a corollary, the build system is free to omit the execution of an
+ * Action <code>a1</code> if (a) at some time in the past, it has already
+ * executed an Action <code>a0</code> with the same key as
+ * <code>a1</code>, and (b) the names and contents of the input files listed
+ * by <code>a1.getInputs()</code> are identical to the names and contents of
+ * the files listed by <code>a0.getInputs()</code>. </p>
+ *
+ * <p>Examples of changes that should affect the key are:
+ * <ul>
+ * <li>Changes to the BUILD file that materially affect the rule which gave
+ * rise to this Action.</li>
+ *
+ * <li>Changes to the command-line options, environment, or other global
+ * configuration resources which affect the behaviour of this kind of Action
+ * (other than changes to the names of the input/output files, which are
+ * handled externally).</li>
+ *
+ * <li>An upgrade to the build tools which changes the program logic of this
+ * kind of Action (typically this is achieved by incorporating a UUID into
+ * the key, which is changed each time the program logic of this action
+ * changes).</li>
+ *
+ * </ul></p>
+ */
+ String getKey();
+
+ /**
+ * Returns a human-readable description of the inputs to {@link #getKey()}.
+ * Used in the output from '--explain', and in error messages for
+ * '--check_up_to_date' and '--check_tests_up_to_date'.
+ * May return null, meaning no extra information is available.
+ *
+ * <p>If the return value is non-null, for consistency it should be a multiline message of the
+ * form:
+ * <pre>
+ * <var>Summary</var>
+ * <var>Fieldname</var>: <var>value</var>
+ * <var>Fieldname</var>: <var>value</var>
+ * ...
+ * </pre>
+ * where each line after the first one is intended two spaces, and where any fields that might
+ * contain newlines or other funny characters are escaped using {@link
+ * com.google.devtools.build.lib.shell.ShellUtils#shellEscape}.
+ * For example:
+ * <pre>
+ * Compiling foo.cc
+ * Command: /usr/bin/gcc
+ * Argument: '-c'
+ * Argument: foo.cc
+ * Argument: '-o'
+ * Argument: foo.o
+ * </pre>
+ */
+ @Nullable String describeKey();
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/ActionMiddlemanEvent.java b/src/main/java/com/google/devtools/build/lib/actions/ActionMiddlemanEvent.java
new file mode 100644
index 0000000..855d97a
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/ActionMiddlemanEvent.java
@@ -0,0 +1,54 @@
+// Copyright 2015 Google Inc. 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.actions;
+
+import com.google.common.base.Preconditions;
+
+/**
+ * This event is fired during the build, when a middleman action is executed. Middleman actions
+ * don't usually do any computation but we need them in the critical path because they depend on
+ * other actions.
+ */
+public class ActionMiddlemanEvent {
+
+ private final Action action;
+ private final long nanoTimeStart;
+
+ /**
+ * Create an event for action that has been started.
+ *
+ * @param action the middleman action.
+ * @param nanoTimeStart the time when the action was started. This allow us to record more
+ * accurately the time spent by the middleman action, since even for middleman actions we execute
+ * some.
+ */
+ public ActionMiddlemanEvent(Action action, long nanoTimeStart) {
+ Preconditions.checkArgument(action.getActionType().isMiddleman(),
+ "Only middleman actions should be passed: %s", action);
+ this.action = action;
+ this.nanoTimeStart = nanoTimeStart;
+ }
+
+ /**
+ * Returns the associated action.
+ */
+ public Action getAction() {
+ return action;
+ }
+
+ public long getNanoTimeStart() {
+ return nanoTimeStart;
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/ActionOwner.java b/src/main/java/com/google/devtools/build/lib/actions/ActionOwner.java
new file mode 100644
index 0000000..ab59f63
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/ActionOwner.java
@@ -0,0 +1,71 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.syntax.Label;
+
+/**
+ * The owner of an action is responsible for reporting conflicts in the action
+ * graph (two actions attempting to generate the same artifact).
+ *
+ * Typically an action's owner is the RuleConfiguredTarget instance responsible
+ * for creating it, but to avoid coupling between the view and actions
+ * packages, the RuleConfiguredTarget is hidden behind this interface, which
+ * exposes only the error reporting functionality.
+ */
+public interface ActionOwner {
+
+ /**
+ * Returns the location of this ActionOwner, if any; null otherwise.
+ */
+ Location getLocation();
+
+ /**
+ * Returns the label for this ActionOwner, if any; null otherwise.
+ */
+ Label getLabel();
+
+ /**
+ * Returns the name of the configuration of the action owner.
+ */
+ String getConfigurationName();
+
+ /**
+ * Returns the configuration's mnemonic.
+ */
+ String getConfigurationMnemonic();
+
+ /**
+ * Returns the short cache key for the configuration of the action owner.
+ *
+ * <p>Special action owners that are not targets can return any string here as long as it is
+ * constant. If the configuration is null, this should return "null".
+ *
+ * <p>These requirements exist so that {@link ActionOwner} instances are consistent with
+ * {@code BuildView.ActionOwnerIdentity(ConfiguredTargetValue)}.
+ */
+ String getConfigurationShortCacheKey();
+
+ /**
+ * Returns the target kind (rule class name) for this ActionOwner, if any; null otherwise.
+ */
+ String getTargetKind();
+
+ /**
+ * Returns additional information that should be displayed in progress messages, or {@code null}
+ * if nothing should be added.
+ */
+ String getAdditionalProgressInfo();
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/ActionRegistry.java b/src/main/java/com/google/devtools/build/lib/actions/ActionRegistry.java
new file mode 100644
index 0000000..db7e350
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/ActionRegistry.java
@@ -0,0 +1,47 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+import com.google.common.annotations.VisibleForTesting;
+
+/**
+ * An interface for registering actions.
+ */
+public interface ActionRegistry {
+ /**
+ * This method notifies the registry new actions.
+ */
+ void registerAction(Action... actions);
+
+ /**
+ * Get the (Label and BuildConfiguration) of the ConfiguredTarget ultimately responsible for all
+ * these actions.
+ */
+ ArtifactOwner getOwner();
+
+ /**
+ * An action registry that does exactly nothing.
+ */
+ @VisibleForTesting
+ public static final ActionRegistry NOP = new ActionRegistry() {
+ @Override
+ public void registerAction(Action... actions) {}
+
+ @Override
+ public ArtifactOwner getOwner() {
+ return ArtifactOwner.NULL_OWNER;
+ }
+ };
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/ActionStartedEvent.java b/src/main/java/com/google/devtools/build/lib/actions/ActionStartedEvent.java
new file mode 100644
index 0000000..a2a978f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/ActionStartedEvent.java
@@ -0,0 +1,46 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+/**
+ * This event is fired during the build, when an action is started.
+ */
+public class ActionStartedEvent {
+ private final Action action;
+ private final long nanoTimeStart;
+
+ /**
+ * Create an event for action that has been started.
+ *
+ * @param action the started action.
+ * @param nanoTimeStart the time when the action was started. This allow us to
+ * record more accurately the time spend by the action, since we execute some code before
+ * deciding if we execute the action or not.
+ */
+ public ActionStartedEvent(Action action, long nanoTimeStart) {
+ this.action = action;
+ this.nanoTimeStart = nanoTimeStart;
+ }
+
+ /**
+ * Returns the associated action.
+ */
+ public Action getAction() {
+ return action;
+ }
+
+ public long getNanoTimeStart() {
+ return nanoTimeStart;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/ActionStatusMessage.java b/src/main/java/com/google/devtools/build/lib/actions/ActionStatusMessage.java
new file mode 100644
index 0000000..c932a9e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/ActionStatusMessage.java
@@ -0,0 +1,69 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+/**
+ * A message used to update in-flight action status. An action's status may change low down in the
+ * execution stack (for instance, from running remotely to running locally), so this message can be
+ * used to notify any interested parties.
+ */
+public class ActionStatusMessage {
+ private final ActionMetadata action;
+ private final String message;
+ public static final String PREPARING = "Preparing";
+
+ public ActionStatusMessage(ActionMetadata action, String message) {
+ this.action = action;
+ this.message = message;
+ }
+
+ public ActionMetadata getActionMetadata() {
+ return action;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ /** Returns whether the message needs further interpolation of a 'strategy' when printed. */
+ public boolean needsStrategy() {
+ return false;
+ }
+
+ /** Creates "Analyzing" status message. */
+ public static ActionStatusMessage analysisStrategy(ActionMetadata action) {
+ return new ActionStatusMessage(action, "Analyzing");
+ }
+
+ /** Creates "Preparing" status message. */
+ public static ActionStatusMessage preparingStrategy(ActionMetadata action) {
+ return new ActionStatusMessage(action, PREPARING);
+ }
+
+ /** Creates "Scheduling" status message. */
+ public static ActionStatusMessage schedulingStrategy(ActionMetadata action) {
+ return new ActionStatusMessage(action, "Scheduling");
+ }
+
+ /** Creates "Running (%s)" status message (needs strategy interpolated). */
+ public static ActionStatusMessage runningStrategy(ActionMetadata action) {
+ return new ActionStatusMessage(action, "Running (%s)") {
+ @Override
+ public boolean needsStrategy() {
+ return true;
+ }
+ };
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/Actions.java b/src/main/java/com/google/devtools/build/lib/actions/Actions.java
new file mode 100644
index 0000000..fb2b834
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/Actions.java
@@ -0,0 +1,79 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+import com.google.common.collect.Iterables;
+import com.google.common.escape.Escaper;
+import com.google.common.escape.Escapers;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.syntax.Label;
+
+/**
+ * Helper class for actions.
+ */
+@ThreadSafe
+public final class Actions {
+ private static final Escaper PATH_ESCAPER = Escapers.builder()
+ .addEscape('_', "_U")
+ .addEscape('/', "_S")
+ .addEscape('\\', "_B")
+ .addEscape(':', "_C")
+ .build();
+
+ /**
+ * Checks if the two actions are equivalent. This method exists to support sharing actions between
+ * configured targets for cases where there is no canonical target that could own the action. In
+ * the action graph construction this case shows up as two actions generating the same output
+ * file.
+ *
+ * <p>This method implements an equivalence relationship across actions, based on the action
+ * class, the key, and the list of inputs and outputs.
+ */
+ public static boolean canBeShared(Action a, Action b) {
+ if (!a.getMnemonic().equals(b.getMnemonic())) {
+ return false;
+ }
+ if (!a.getKey().equals(b.getKey())) {
+ return false;
+ }
+ // Don't bother to check input and output counts first; the expected result for these tests is
+ // to always be true (i.e., that this method returns true).
+ if (!Iterables.elementsEqual(a.getMandatoryInputs(), b.getMandatoryInputs())) {
+ return false;
+ }
+ if (!Iterables.elementsEqual(a.getOutputs(), b.getOutputs())) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Returns the escaped name for a given relative path as a string. This takes
+ * a short relative path and turns it into a string suitable for use as a
+ * filename. Invalid filename characters are escaped with an '_' + a single
+ * character token.
+ */
+ public static String escapedPath(String path) {
+ return PATH_ESCAPER.escape(path);
+ }
+
+ /**
+ * Returns a string that is usable as a unique path component for a label. It is guaranteed
+ * that no other label maps to this string.
+ */
+ public static String escapeLabel(Label label) {
+ return PATH_ESCAPER.escape(label.getPackageName() + ":" + label.getName());
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/AlreadyReportedActionExecutionException.java b/src/main/java/com/google/devtools/build/lib/actions/AlreadyReportedActionExecutionException.java
new file mode 100644
index 0000000..4ce258b
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/AlreadyReportedActionExecutionException.java
@@ -0,0 +1,40 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+/**
+ * This wrapper exception is used as a marker class to already reported errors. Errors are reported
+ * at {@code AbstractBuilder.executeActionTask()} method in case of the builder not aborting in case
+ * of exceptions (For example keepgoing).
+ *
+ * <p>Then In upper levels we wrap catch the exception and throw a BuildFailedException
+ * unconditionally, that is caught and shown as error in AbstractBuildCommand (because the message
+ * of the exception is !=null).
+ *
+ * With this exception we detect that the error was already shown and we wrap it in a
+ * BuildFailedException without message.
+ */
+public class AlreadyReportedActionExecutionException extends ActionExecutionException {
+
+ public AlreadyReportedActionExecutionException(ActionExecutionException cause) {
+ super(cause.getMessage(), cause.getCause(), cause.getAction(), cause.getRootCauses(),
+ cause.isCatastrophe());
+ }
+
+ @Override
+ public boolean showError() {
+ return false;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/Artifact.java b/src/main/java/com/google/devtools/build/lib/actions/Artifact.java
new file mode 100644
index 0000000..2f2272b
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/Artifact.java
@@ -0,0 +1,654 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
+import com.google.common.base.Functions;
+import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.Action.MiddlemanType;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.syntax.SkylarkCallable;
+import com.google.devtools.build.lib.syntax.SkylarkModule;
+import com.google.devtools.build.lib.util.FileType;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+/**
+ * An Artifact represents a file used by the build system, whether it's a source
+ * file or a derived (output) file. Not all Artifacts have a corresponding
+ * FileTarget object in the <code>build.packages</code> API: for example,
+ * low-level intermediaries internal to a given rule, such as a Java class files
+ * or C++ object files. However all FileTargets have a corresponding Artifact.
+ *
+ * <p>In any given call to Builder#buildArtifacts(), no two Artifacts in the
+ * action graph may refer to the same path.
+ *
+ * <p>Artifacts generally fall into two classifications, source and derived, but
+ * there exist a few other cases that are fuzzy and difficult to classify. The
+ * following cases exist:
+ * <ul>
+ * <li>Well-formed source Artifacts will have null generating Actions and a root
+ * that is orthogonal to execRoot. (With the root coming from the package path.)
+ * <li>Well-formed derived Artifacts will have non-null generating Actions, and
+ * a root that is below execRoot.
+ * <li>Symlinked include source Artifacts under the output/include tree will
+ * appear to be derived artifacts with null generating Actions.
+ * <li>Some derived Artifacts, mostly in the genfiles tree and mostly discovered
+ * during include validation, will also have null generating Actions.
+ * </ul>
+ *
+ * <p>This class is "theoretically" final; it should not be subclassed except by
+ * {@link SpecialArtifact}.
+ */
+@Immutable
+@SkylarkModule(name = "File",
+ doc = "This type represents a file used by the build system. It can be "
+ + "either a source file or a derived file produced by a rule.")
+public class Artifact implements FileType.HasFilename, Comparable<Artifact>, ActionInput {
+
+ /** An object that can expand middleman artifacts. */
+ public interface MiddlemanExpander {
+
+ /**
+ * Expands the middleman artifact "mm", and populates "output" with the result.
+ *
+ * <p>{@code mm.isMiddlemanArtifact()} must be true. Only aggregating middlemen are expanded.
+ */
+ void expand(Artifact mm, Collection<? super Artifact> output);
+ }
+
+ public static final ImmutableList<Artifact> NO_ARTIFACTS = ImmutableList.of();
+
+ /**
+ * A Predicate that evaluates to true if the Artifact is not a middleman artifact.
+ */
+ public static final Predicate<Artifact> MIDDLEMAN_FILTER = new Predicate<Artifact>() {
+ @Override
+ public boolean apply(Artifact input) {
+ return !input.isMiddlemanArtifact();
+ }
+ };
+
+ private final Path path;
+ private final Root root;
+ private final PathFragment execPath;
+ private final PathFragment rootRelativePath;
+ // Non-final only for use when dealing with deserialized artifacts.
+ private ArtifactOwner owner;
+
+ /**
+ * Constructs an artifact for the specified path, root and execPath. The root must be an ancestor
+ * of path, and execPath must be a non-absolute tail of path. Outside of testing, this method
+ * should only be called by ArtifactFactory. The ArtifactOwner may be null.
+ *
+ * <p>In a source Artifact, the path tail after the root will be identical to the execPath, but
+ * the root will be orthogonal to execRoot.
+ * <pre>
+ * [path] == [/root/][execPath]
+ * </pre>
+ *
+ * <p>In a derived Artifact, the execPath will overlap with part of the root, which in turn will
+ * be below of the execRoot.
+ * <pre>
+ * [path] == [/root][pathTail] == [/execRoot][execPath] == [/execRoot][rootPrefix][pathTail]
+ * <pre>
+ */
+ @VisibleForTesting
+ public Artifact(Path path, Root root, PathFragment execPath, ArtifactOwner owner) {
+ if (root == null || !path.startsWith(root.getPath())) {
+ throw new IllegalArgumentException(root + ": illegal root for " + path);
+ }
+ if (execPath == null || execPath.isAbsolute() || !path.asFragment().endsWith(execPath)) {
+ throw new IllegalArgumentException(execPath + ": illegal execPath for " + path);
+ }
+ this.path = path;
+ this.root = root;
+ this.execPath = execPath;
+ // These two lines establish the invariant that
+ // execPath == rootRelativePath <=> execPath.equals(rootRelativePath)
+ // This is important for isSourceArtifact.
+ PathFragment rootRel = path.relativeTo(root.getPath());
+ if (!execPath.endsWith(rootRel)) {
+ throw new IllegalArgumentException(execPath + ": illegal execPath doesn't end with "
+ + rootRel + " at " + path + " with root " + root);
+ }
+ this.rootRelativePath = rootRel.equals(execPath) ? execPath : rootRel;
+ this.owner = Preconditions.checkNotNull(owner, path);
+ }
+
+ /**
+ * Constructs an artifact for the specified path, root and execPath. The root must be an ancestor
+ * of path, and execPath must be a non-absolute tail of path. Should only be called for testing.
+ *
+ * <p>In a source Artifact, the path tail after the root will be identical to the execPath, but
+ * the root will be orthogonal to execRoot.
+ * <pre>
+ * [path] == [/root/][execPath]
+ * </pre>
+ *
+ * <p>In a derived Artifact, the execPath will overlap with part of the root, which in turn will
+ * be below of the execRoot.
+ * <pre>
+ * [path] == [/root][pathTail] == [/execRoot][execPath] == [/execRoot][rootPrefix][pathTail]
+ * <pre>
+ */
+ @VisibleForTesting
+ public Artifact(Path path, Root root, PathFragment execPath) {
+ this(path, root, execPath, ArtifactOwner.NULL_OWNER);
+ }
+
+ /**
+ * Constructs a source or derived Artifact for the specified path and specified root. The root
+ * must be an ancestor of the path.
+ */
+ @VisibleForTesting // Only exists for testing.
+ public Artifact(Path path, Root root) {
+ this(path, root, root.getExecPath().getRelative(path.relativeTo(root.getPath())),
+ ArtifactOwner.NULL_OWNER);
+ }
+
+ /**
+ * Constructs a source or derived Artifact for the specified root-relative path and root.
+ */
+ @VisibleForTesting // Only exists for testing.
+ public Artifact(PathFragment rootRelativePath, Root root) {
+ this(root.getPath().getRelative(rootRelativePath), root,
+ root.getExecPath().getRelative(rootRelativePath), ArtifactOwner.NULL_OWNER);
+ }
+
+ /**
+ * Returns the location of this Artifact on the filesystem.
+ */
+ public final Path getPath() {
+ return path;
+ }
+
+ /**
+ * Returns the base file name of this artifact.
+ */
+ @Override
+ public final String getFilename() {
+ return getExecPath().getBaseName();
+ }
+
+ /**
+ * Returns the artifact owner. May be null.
+ */
+ @Nullable public final Label getOwner() {
+ return owner.getLabel();
+ }
+
+ /**
+ * Get the {@code LabelAndConfiguration} of the {@code ConfiguredTarget} that owns this artifact,
+ * if it was set. Otherwise, this should be a dummy value -- either {@link
+ * ArtifactOwner#NULL_OWNER} or a dummy owner set in tests. Such a dummy value should only occur
+ * for source artifacts if created without specifying the owner, or for special derived artifacts,
+ * such as target completion middleman artifacts, build info artifacts, and the like.
+ *
+ * When deserializing artifacts we end up with a dummy owner. In that case, it must be set using
+ * {@link #setArtifactOwner} before this method is called.
+ */
+ public final ArtifactOwner getArtifactOwner() {
+ Preconditions.checkState(owner != DESERIALIZED_MARKER_OWNER, this);
+ return owner;
+ }
+
+ /**
+ * Sets the artifact owner of this artifact. Should only be called for artifacts that were created
+ * through deserialization, and so their owner was unknown at the time of creation.
+ */
+ public final void setArtifactOwner(ArtifactOwner owner) {
+ if (this.owner == DESERIALIZED_MARKER_OWNER) {
+ // We tolerate multiple calls of this method to accommodate shared actions.
+ this.owner = Preconditions.checkNotNull(owner, this);
+ }
+ }
+
+ /**
+ * Returns the root beneath which this Artifact resides, if any. This may be one of the
+ * package-path entries (for source Artifacts), or one of the bin, genfiles or includes dirs
+ * (for derived Artifacts). It will always be an ancestor of getPath().
+ */
+ public final Root getRoot() {
+ return root;
+ }
+
+ /**
+ * Returns the exec path of this Artifact. The exec path is a relative path
+ * that is suitable for accessing this artifact relative to the execution
+ * directory for this build.
+ */
+ public final PathFragment getExecPath() {
+ return execPath;
+ }
+
+ /**
+ * Returns true iff this is a source Artifact as determined by its path and
+ * root relationships. Note that this will report all Artifacts in the output
+ * tree, including in the include symlink tree, as non-source.
+ */
+ public final boolean isSourceArtifact() {
+ return execPath == rootRelativePath;
+ }
+
+ /**
+ * Returns true iff this is a middleman Artifact as determined by its root.
+ */
+ public final boolean isMiddlemanArtifact() {
+ return getRoot().isMiddlemanRoot();
+ }
+
+ /**
+ * Returns whether the artifact represents a Fileset.
+ */
+ public boolean isFileset() {
+ return false;
+ }
+
+ /**
+ * Returns true iff metadata cache must return constant metadata for the
+ * given artifact.
+ */
+ public boolean isConstantMetadata() {
+ return false;
+ }
+
+ /**
+ * Special artifact types.
+ *
+ * @see SpecialArtifact
+ */
+ static enum SpecialArtifactType {
+ FILESET,
+ CONSTANT_METADATA,
+ }
+
+ /**
+ * A special kind of artifact that either is a fileset or needs special metadata caching behavior.
+ *
+ * <p>We subclass {@link Artifact} instead of storing the special attributes inside in order
+ * to save memory. The proportion of artifacts that are special is very small, and by not having
+ * to keep around the attribute for the rest we save some memory.
+ */
+ @Immutable
+ @VisibleForTesting
+ public static final class SpecialArtifact extends Artifact {
+ private final SpecialArtifactType type;
+
+ SpecialArtifact(Path path, Root root, PathFragment execPath, ArtifactOwner owner,
+ SpecialArtifactType type) {
+ super(path, root, execPath, owner);
+ this.type = type;
+ }
+
+ @Override
+ public final boolean isFileset() {
+ return type == SpecialArtifactType.FILESET;
+ }
+
+ @Override
+ public boolean isConstantMetadata() {
+ return type == SpecialArtifactType.CONSTANT_METADATA;
+ }
+ }
+
+ /**
+ * Returns the relative path to this artifact relative to its root. (Useful
+ * when deriving output filenames from input files, etc.)
+ */
+ public final PathFragment getRootRelativePath() {
+ return rootRelativePath;
+ }
+
+ /**
+ * Returns this.getExecPath().getPathString().
+ */
+ @Override
+ @SkylarkCallable(name = "path", structField = true,
+ doc = "The execution path of this file, relative to the execution directory.")
+ public final String getExecPathString() {
+ return getExecPath().getPathString();
+ }
+
+ @SkylarkCallable(name = "short_path", structField = true,
+ doc = "The path of this file relative to its root.")
+ public final String getRootRelativePathString() {
+ return getRootRelativePath().getPathString();
+ }
+
+ /**
+ * Returns a pretty string representation of the path denoted by this artifact, suitable for use
+ * in user error messages. Artifacts beneath a root will be printed relative to that root; other
+ * artifacts will be printed as an absolute path.
+ *
+ * <p>(The toString method is intended for developer messages since its more informative.)
+ */
+ public final String prettyPrint() {
+ // toDetailString would probably be more useful to users, but lots of tests rely on the
+ // current values.
+ return rootRelativePath.toString();
+ }
+
+ @Override
+ public final boolean equals(Object other) {
+ if (!(other instanceof Artifact)) {
+ return false;
+ }
+ // We don't bother to check root in the equivalence relation, because we
+ // assume that 'root' is an ancestor of 'path', and that all possible roots
+ // are disjoint, so unless things are really screwed up, it's ok.
+ Artifact that = (Artifact) other;
+ return this.path.equals(that.path);
+ }
+
+ @Override
+ public final int compareTo(Artifact o) {
+ // The artifact factory ensures that there is a unique artifact for a given path.
+ return this.path.compareTo(o.path);
+ }
+
+ @Override
+ public final int hashCode() {
+ return path.hashCode();
+ }
+
+ @Override
+ public final String toString() {
+ return "Artifact:" + toDetailString();
+ }
+
+ /**
+ * Returns the root-part of a given path by trimming off the end specified by
+ * a given tail. Assumes that the tail is known to match, and simply relies on
+ * the segment lengths.
+ */
+ private static PathFragment trimTail(PathFragment path, PathFragment tail) {
+ return path.subFragment(0, path.segmentCount() - tail.segmentCount());
+ }
+
+ /**
+ * Returns a string representing the complete artifact path information.
+ */
+ public final String toDetailString() {
+ if (isSourceArtifact()) {
+ // Source Artifact: relPath == execPath, & real path is not under execRoot
+ return "[" + root + "]" + rootRelativePath;
+ } else {
+ // Derived Artifact: path and root are under execRoot
+ PathFragment execRoot = trimTail(path.asFragment(), execPath);
+ return "[[" + execRoot + "]" + root.getPath().asFragment().relativeTo(execRoot) + "]"
+ + rootRelativePath;
+ }
+ }
+
+ /**
+ * Serializes this artifact to a string that has enough data to reconstruct the artifact.
+ */
+ public final String serializeToString() {
+ // In theory, it should be enough to serialize execPath and rootRelativePath (which is a suffix
+ // of execPath). However, in practice there is code around that uses other attributes which
+ // needs cleaning up.
+ String result = execPath + " /" + rootRelativePath.toString().length();
+ if (getOwner() != null) {
+ result += " " + getOwner();
+ }
+ return result;
+ }
+
+ //---------------------------------------------------------------------------
+ // Static methods to assist in working with Artifacts
+
+ /**
+ * Formatter for execPath PathFragment output.
+ */
+ private static final Function<Artifact, PathFragment> EXEC_PATH_FORMATTER =
+ new Function<Artifact, PathFragment>() {
+ @Override
+ public PathFragment apply(Artifact input) {
+ return input.getExecPath();
+ }
+ };
+
+ private static final Function<Artifact, String> ROOT_RELATIVE_PATH_STRING =
+ new Function<Artifact, String>() {
+ @Override
+ public String apply(Artifact artifact) {
+ return artifact.getRootRelativePath().getPathString();
+ }
+ };
+
+ /**
+ * Converts a collection of artifacts into execution-time path strings, and
+ * adds those to a given collection. Middleman artifacts are ignored by this
+ * method.
+ */
+ public static void addExecPaths(Iterable<Artifact> artifacts, Collection<String> output) {
+ addNonMiddlemanArtifacts(artifacts, output, ActionInputHelper.EXEC_PATH_STRING_FORMATTER);
+ }
+
+ /**
+ * Converts a collection of artifacts into the outputs computed by
+ * outputFormatter and adds them to a given collection. Middleman artifacts
+ * are ignored.
+ */
+ static <E> void addNonMiddlemanArtifacts(Iterable<Artifact> artifacts,
+ Collection<? super E> output, Function<? super Artifact, E> outputFormatter) {
+ for (Artifact artifact : artifacts) {
+ if (MIDDLEMAN_FILTER.apply(artifact)) {
+ output.add(outputFormatter.apply(artifact));
+ }
+ }
+ }
+
+ /**
+ * Lazily converts artifacts into root-relative path strings. Middleman artifacts are ignored by
+ * this method.
+ */
+ public static Iterable<String> toRootRelativePaths(Iterable<Artifact> artifacts) {
+ return Iterables.transform(
+ Iterables.filter(artifacts, MIDDLEMAN_FILTER),
+ ROOT_RELATIVE_PATH_STRING);
+ }
+
+ /**
+ * Lazily converts artifacts into execution-time path strings. Middleman artifacts are ignored by
+ * this method.
+ */
+ public static Iterable<String> toExecPaths(Iterable<Artifact> artifacts) {
+ return ActionInputHelper.toExecPaths(Iterables.filter(artifacts, MIDDLEMAN_FILTER));
+ }
+
+ /**
+ * Converts a collection of artifacts into execution-time path strings, and
+ * returns those as an immutable list. Middleman artifacts are ignored by this method.
+ */
+ public static List<String> asExecPaths(Iterable<Artifact> artifacts) {
+ return ImmutableList.copyOf(toExecPaths(artifacts));
+ }
+
+ /**
+ * Renders a collection of artifacts as execution-time paths and joins
+ * them into a single string. Middleman artifacts are ignored by this method.
+ */
+ public static String joinExecPaths(String delimiter, Iterable<Artifact> artifacts) {
+ return Joiner.on(delimiter).join(toExecPaths(artifacts));
+ }
+
+ /**
+ * Renders a collection of artifacts as root-relative paths and joins
+ * them into a single string. Middleman artifacts are ignored by this method.
+ */
+ public static String joinRootRelativePaths(String delimiter, Iterable<Artifact> artifacts) {
+ return Joiner.on(delimiter).join(toRootRelativePaths(artifacts));
+ }
+
+ /**
+ * Adds a collection of artifacts to a given collection, with
+ * {@link MiddlemanType#AGGREGATING_MIDDLEMAN} middleman actions expanded once.
+ */
+ public static void addExpandedArtifacts(Iterable<Artifact> artifacts,
+ Collection<? super Artifact> output, MiddlemanExpander middlemanExpander) {
+ addExpandedArtifacts(artifacts, output, Functions.<Artifact>identity(), middlemanExpander);
+ }
+
+ /**
+ * Converts a collection of artifacts into execution-time path strings, and
+ * adds those to a given collection. Middleman artifacts for
+ * {@link MiddlemanType#AGGREGATING_MIDDLEMAN} middleman actions are expanded
+ * once.
+ */
+ @VisibleForTesting
+ public static void addExpandedExecPathStrings(Iterable<Artifact> artifacts,
+ Collection<String> output,
+ MiddlemanExpander middlemanExpander) {
+ addExpandedArtifacts(artifacts, output, ActionInputHelper.EXEC_PATH_STRING_FORMATTER,
+ middlemanExpander);
+ }
+
+ /**
+ * Converts a collection of artifacts into execution-time path fragments, and
+ * adds those to a given collection. Middleman artifacts for
+ * {@link MiddlemanType#AGGREGATING_MIDDLEMAN} middleman actions are expanded
+ * once.
+ */
+ public static void addExpandedExecPaths(Iterable<Artifact> artifacts,
+ Collection<PathFragment> output, MiddlemanExpander middlemanExpander) {
+ addExpandedArtifacts(artifacts, output, EXEC_PATH_FORMATTER, middlemanExpander);
+ }
+
+ /**
+ * Converts a collection of artifacts into the outputs computed by
+ * outputFormatter and adds them to a given collection. Middleman artifacts
+ * are expanded once.
+ */
+ private static <E> void addExpandedArtifacts(Iterable<Artifact> artifacts,
+ Collection<? super E> output,
+ Function<? super Artifact, E> outputFormatter,
+ MiddlemanExpander middlemanExpander) {
+ for (Artifact artifact : artifacts) {
+ if (artifact.isMiddlemanArtifact()) {
+ expandMiddlemanArtifact(artifact, output, outputFormatter, middlemanExpander);
+ } else {
+ output.add(outputFormatter.apply(artifact));
+ }
+ }
+ }
+
+ private static <E> void expandMiddlemanArtifact(Artifact middleman,
+ Collection<? super E> output,
+ Function<? super Artifact, E> outputFormatter,
+ MiddlemanExpander middlemanExpander) {
+ Preconditions.checkArgument(middleman.isMiddlemanArtifact());
+ List<Artifact> artifacts = new ArrayList<>();
+ middlemanExpander.expand(middleman, artifacts);
+ for (Artifact artifact : artifacts) {
+ output.add(outputFormatter.apply(artifact));
+ }
+ }
+
+ /**
+ * Converts a collection of artifacts into execution-time path strings, and
+ * returns those as a list. Middleman artifacts are expanded once. The
+ * returned list is mutable.
+ */
+ public static List<String> asExpandedExecPathStrings(Iterable<Artifact> artifacts,
+ MiddlemanExpander middlemanExpander) {
+ List<String> result = new ArrayList<>();
+ addExpandedExecPathStrings(artifacts, result, middlemanExpander);
+ return result;
+ }
+
+ /**
+ * Converts a collection of artifacts into execution-time path fragments, and
+ * returns those as a list. Middleman artifacts are expanded once. The
+ * returned list is mutable.
+ */
+ public static List<PathFragment> asExpandedExecPaths(Iterable<Artifact> artifacts,
+ MiddlemanExpander middlemanExpander) {
+ List<PathFragment> result = new ArrayList<>();
+ addExpandedExecPaths(artifacts, result, middlemanExpander);
+ return result;
+ }
+
+ /**
+ * Converts a collection of artifacts into execution-time path strings with
+ * the root-break delimited with a colon ':', and adds those to a given list.
+ * <pre>
+ * Source: sourceRoot/rootRelative => :rootRelative
+ * Derived: execRoot/rootPrefix/rootRelative => rootPrefix:rootRelative
+ * </pre>
+ */
+ public static void addRootPrefixedExecPaths(Iterable<Artifact> artifacts,
+ List<String> output) {
+ for (Artifact artifact : artifacts) {
+ output.add(asRootPrefixedExecPath(artifact));
+ }
+ }
+
+ /**
+ * Convenience method to filter the files to build for a certain filetype.
+ *
+ * @param artifacts the files to filter
+ * @param allowedType the allowed filetype
+ * @return all members of filesToBuild that are of one of the
+ * allowed filetypes
+ */
+ public static List<Artifact> filterFiles(Iterable<Artifact> artifacts, FileType allowedType) {
+ List<Artifact> filesToBuild = new ArrayList<>();
+ for (Artifact artifact : artifacts) {
+ if (allowedType.matches(artifact.getFilename())) {
+ filesToBuild.add(artifact);
+ }
+ }
+ return filesToBuild;
+ }
+
+ @VisibleForTesting
+ static String asRootPrefixedExecPath(Artifact artifact) {
+ PathFragment execPath = artifact.getExecPath();
+ PathFragment rootRel = artifact.getRootRelativePath();
+ if (execPath.equals(rootRel)) {
+ return ":" + rootRel.getPathString();
+ } else { //if (execPath.endsWith(rootRel)) {
+ PathFragment rootPrefix = trimTail(execPath, rootRel);
+ return rootPrefix.getPathString() + ":" + rootRel.getPathString();
+ }
+ }
+
+ /**
+ * Converts artifacts into their exec paths. Returns an immutable list.
+ */
+ public static List<PathFragment> asPathFragments(Iterable<Artifact> artifacts) {
+ return ImmutableList.copyOf(Iterables.transform(artifacts, EXEC_PATH_FORMATTER));
+ }
+
+ static final ArtifactOwner DESERIALIZED_MARKER_OWNER = new ArtifactOwner() {
+ @Override
+ public Label getLabel() {
+ return null;
+ }};
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/ArtifactDeserializer.java b/src/main/java/com/google/devtools/build/lib/actions/ArtifactDeserializer.java
new file mode 100644
index 0000000..40996ac
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/ArtifactDeserializer.java
@@ -0,0 +1,40 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+import com.google.common.collect.ImmutableList;
+
+/**
+ * An interface for creating artifacts from their serialized representations.
+ *
+ * @see ArtifactSerializer
+ */
+public interface ArtifactDeserializer {
+
+ /**
+ * Looks up an artifact by an integer id.
+ *
+ * <p>This is a dual of {@link ArtifactSerializer#getArtifactId}.
+ */
+ Artifact lookupArtifactById(int artifactId);
+
+ /**
+ * Maps a list of artifact ids to a list of artifacts.
+ *
+ * <p>This is a batch version of {@link #lookupArtifactById}, provided for efficiency. It takes
+ * an iterable of boxed integers because that's what the proto wrapper provides.
+ */
+ ImmutableList<Artifact> lookupArtifactsByIds(Iterable<Integer> artifactIds);
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/ArtifactFactory.java b/src/main/java/com/google/devtools/build/lib/actions/ArtifactFactory.java
new file mode 100644
index 0000000..99a53cb
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/ArtifactFactory.java
@@ -0,0 +1,339 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSortedSet;
+import com.google.common.collect.Lists;
+import com.google.devtools.build.lib.actions.Artifact.SpecialArtifactType;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.packages.PackageIdentifier;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import javax.annotation.Nullable;
+
+/**
+ * A cache of Artifacts, keyed by Path.
+ */
+@ThreadSafe
+public class ArtifactFactory implements ArtifactResolver, ArtifactSerializer, ArtifactDeserializer {
+
+ private final Path execRoot;
+
+ /**
+ * The main Path to source artifact cache. There will always be exactly one canonical
+ * artifact for a given source path.
+ */
+ private final Map<PathFragment, Artifact> pathToSourceArtifact = new HashMap<>();
+
+ /**
+ * Map of package names to source root paths so that we can create source
+ * artifact paths given execPaths in the symlink forest.
+ */
+ private ImmutableMap<PackageIdentifier, Root> packageRoots;
+
+ /**
+ * Reverse-ordered list of derived roots for use in looking up or (in rare cases) creating
+ * derived artifacts from execPaths. The reverse order is only significant for overlapping roots
+ * so that the longest is found first.
+ */
+ private ImmutableCollection<Root> derivedRoots = ImmutableList.of();
+
+ private ArtifactIdRegistry artifactIdRegistry = new ArtifactIdRegistry();
+
+ /**
+ * Constructs a new artifact factory that will use a given execution root when
+ * creating artifacts.
+ *
+ * @param execRoot the execution root Path to use
+ */
+ public ArtifactFactory(Path execRoot) {
+ this.execRoot = execRoot;
+ }
+
+ /**
+ * Clear the cache.
+ */
+ public synchronized void clear() {
+ pathToSourceArtifact.clear();
+ packageRoots = null;
+ derivedRoots = ImmutableList.of();
+ artifactIdRegistry = new ArtifactIdRegistry();
+ clearDeserializedArtifacts();
+ }
+
+ /**
+ * Set the set of known packages and their corresponding source artifact
+ * roots. Must be called exactly once after construction or clear().
+ *
+ * @param packageRoots the map of package names to source artifact roots to
+ * use.
+ */
+ public synchronized void setPackageRoots(Map<PackageIdentifier, Root> packageRoots) {
+ this.packageRoots = ImmutableMap.copyOf(packageRoots);
+ }
+
+ /**
+ * Set the set of known derived artifact roots. Must be called exactly once
+ * after construction or clear().
+ *
+ * @param roots the set of derived artifact roots to use
+ */
+ public synchronized void setDerivedArtifactRoots(Collection<Root> roots) {
+ derivedRoots = ImmutableSortedSet.<Root>reverseOrder().addAll(roots).build();
+ }
+
+ @Override
+ public Artifact getSourceArtifact(PathFragment execPath, Root root, ArtifactOwner owner) {
+ Preconditions.checkArgument(!execPath.isAbsolute());
+ Preconditions.checkNotNull(owner, execPath);
+ execPath = execPath.normalize();
+ return getArtifact(root.getPath().getRelative(execPath), root, execPath, owner, null);
+ }
+
+ @Override
+ public Artifact getSourceArtifact(PathFragment execPath, Root root) {
+ return getSourceArtifact(execPath, root, ArtifactOwner.NULL_OWNER);
+ }
+
+ /**
+ * Only for use by BinTools! Returns an artifact for a tool at the given path
+ * fragment, relative to the exec root, creating it if not found. This method
+ * only works for normalized, relative paths.
+ */
+ public Artifact getDerivedArtifact(PathFragment execPath) {
+ Preconditions.checkArgument(!execPath.isAbsolute(), execPath);
+ Preconditions.checkArgument(execPath.isNormalized(), execPath);
+ // TODO(bazel-team): Check that either BinTools do not change over the life of the Blaze server,
+ // or require that a legitimate ArtifactOwner be passed in here to allow for ownership.
+ return getArtifact(execRoot.getRelative(execPath), Root.execRootAsDerivedRoot(execRoot),
+ execPath, ArtifactOwner.NULL_OWNER, null);
+ }
+
+ private void validatePath(PathFragment rootRelativePath, Root root) {
+ Preconditions.checkArgument(!rootRelativePath.isAbsolute(), rootRelativePath);
+ Preconditions.checkArgument(rootRelativePath.isNormalized(), rootRelativePath);
+ Preconditions.checkArgument(root.getPath().startsWith(execRoot), "%s %s", root, execRoot);
+ Preconditions.checkArgument(!root.getPath().equals(execRoot), "%s %s", root, execRoot);
+ // TODO(bazel-team): this should only accept roots from derivedRoots.
+ //Preconditions.checkArgument(derivedRoots.contains(root), "%s not in %s", root, derivedRoots);
+ }
+
+ /**
+ * Returns an artifact for a tool at the given root-relative path under the given root, creating
+ * it if not found. This method only works for normalized, relative paths.
+ *
+ * <p>The root must be below the execRoot, and the execPath of the resulting Artifact is computed
+ * as {@code root.getRelative(rootRelativePath).relativeTo(execRoot)}.
+ */
+ // TODO(bazel-team): Don't allow root == execRoot.
+ public Artifact getDerivedArtifact(PathFragment rootRelativePath, Root root,
+ ArtifactOwner owner) {
+ validatePath(rootRelativePath, root);
+ Path path = root.getPath().getRelative(rootRelativePath);
+ return getArtifact(path, root, path.relativeTo(execRoot), owner, null);
+ }
+
+ /**
+ * Returns an artifact that represents the output directory of a Fileset at the given
+ * root-relative path under the given root, creating it if not found. This method only works for
+ * normalized, relative paths.
+ *
+ * <p>The root must be below the execRoot, and the execPath of the resulting Artifact is computed
+ * as {@code root.getRelative(rootRelativePath).relativeTo(execRoot)}.
+ */
+ public Artifact getFilesetArtifact(PathFragment rootRelativePath, Root root,
+ ArtifactOwner owner) {
+ validatePath(rootRelativePath, root);
+ Path path = root.getPath().getRelative(rootRelativePath);
+ return getArtifact(path, root, path.relativeTo(execRoot), owner, SpecialArtifactType.FILESET);
+ }
+
+ public Artifact getConstantMetadataArtifact(PathFragment rootRelativePath, Root root,
+ ArtifactOwner owner) {
+ validatePath(rootRelativePath, root);
+ Path path = root.getPath().getRelative(rootRelativePath);
+ return getArtifact(
+ path, root, path.relativeTo(execRoot), owner, SpecialArtifactType.CONSTANT_METADATA);
+ }
+
+ /**
+ * Returns the Artifact for the specified path, creating one if not found and
+ * setting the <code>root</code> and <code>execPath</code> to the
+ * specified values.
+ */
+ private synchronized Artifact getArtifact(Path path, Root root, PathFragment execPath,
+ ArtifactOwner owner, @Nullable SpecialArtifactType type) {
+ Preconditions.checkNotNull(root);
+ Preconditions.checkNotNull(execPath);
+
+ if (!root.isSourceRoot()) {
+ return createArtifact(path, root, execPath, owner, type);
+ }
+
+ Artifact artifact = pathToSourceArtifact.get(execPath);
+
+ if (artifact == null || !Objects.equals(artifact.getArtifactOwner(), owner)) {
+ // There really should be a safety net that makes it impossible to create two Artifacts
+ // with the same exec path but a different Owner, but we also need to reuse Artifacts from
+ // previous builds.
+ artifact = createArtifact(path, root, execPath, owner, type);
+ pathToSourceArtifact.put(execPath, artifact);
+ } else {
+ // TODO(bazel-team): Maybe we should check for equality of the fileset bit. However, that
+ // would require us to differentiate between artifact-creating and artifact-getting calls to
+ // getDerivedArtifact().
+ Preconditions.checkState(root.equals(artifact.getRoot()),
+ "root for path %s changed from %s to %s", path, artifact.getRoot(), root);
+ Preconditions.checkState(execPath.equals(artifact.getExecPath()),
+ "execPath for path %s changed from %s to %s", path, artifact.getExecPath(), execPath);
+ }
+ return artifact;
+ }
+
+ private Artifact createArtifact(Path path, Root root, PathFragment execPath, ArtifactOwner owner,
+ @Nullable SpecialArtifactType type) {
+ Preconditions.checkNotNull(owner, path);
+ if (type == null) {
+ return new Artifact(path, root, execPath, owner);
+ } else {
+ return new Artifact.SpecialArtifact(path, root, execPath, owner, type);
+ }
+ }
+
+ @Override
+ public synchronized Artifact resolveSourceArtifact(PathFragment execPath) {
+ execPath = execPath.normalize();
+ // First try a quick map lookup to see if the artifact already exists.
+ Artifact a = pathToSourceArtifact.get(execPath);
+ if (a != null) {
+ return a;
+ }
+ // Don't create an artifact if it's derived.
+ if (findDerivedRoot(execRoot.getRelative(execPath)) != null) {
+ return null;
+ }
+ // Must be a new source artifact, so probe the known packages to find the longest package
+ // prefix, and then use the corresponding source root to create a new artifact.
+ for (PathFragment dir = execPath.getParentDirectory(); dir != null;
+ dir = dir.getParentDirectory()) {
+ Root sourceRoot = packageRoots.get(PackageIdentifier.createInDefaultRepo(dir));
+ if (sourceRoot != null) {
+ return getSourceArtifact(execPath, sourceRoot, ArtifactOwner.NULL_OWNER);
+ }
+ }
+ return null; // not a path that we can find...
+ }
+
+ /**
+ * Finds the derived root for a full path by comparing against the known
+ * derived artifact roots.
+ *
+ * @param path a Path to resolve the root for
+ * @return the root for the path or null if no root can be determined
+ */
+ @VisibleForTesting // for our own unit tests only.
+ synchronized Root findDerivedRoot(Path path) {
+ for (Root prefix : derivedRoots) {
+ if (path.startsWith(prefix.getPath())) {
+ return prefix;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns all source artifacts created by the artifact factory.
+ */
+ public synchronized Iterable<Artifact> getSourceArtifacts() {
+ return ImmutableList.copyOf(pathToSourceArtifact.values());
+ }
+
+ // Non-final only because clear()ing a map does not actually free the memory it took up, so we
+ // assign it to a new map in lieu of clearing.
+ private ConcurrentMap<PathFragment, Artifact> deserializedArtifacts =
+ new ConcurrentHashMap<>();
+
+ /**
+ * Returns the map of all artifacts that were deserialized this build. The caller should process
+ * them and then call {@link #clearDeserializedArtifacts}.
+ */
+ public Map<PathFragment, Artifact> getDeserializedArtifacts() {
+ return deserializedArtifacts;
+ }
+
+ /** Clears the map of deserialized artifacts. */
+ public void clearDeserializedArtifacts() {
+ deserializedArtifacts = new ConcurrentHashMap<>();
+ }
+
+ /**
+ * Resolves an artifact based on its deserialized representation. The artifact can be either a
+ * source or a derived one.
+ *
+ * <p>Note: this method represents a hole in the usual contract that artifacts with a random path
+ * cannot be created. Unfortunately, we currently need this in some cases.
+ *
+ * @param execPath the exec path of the artifact
+ */
+ public Artifact deserializeArtifact(PathFragment execPath, PackageRootResolver resolver) {
+ Preconditions.checkArgument(!execPath.isAbsolute(), execPath);
+ Path path = execRoot.getRelative(execPath);
+ Root root = findDerivedRoot(path);
+
+ Artifact result;
+ if (root != null) {
+ result = getDerivedArtifact(path.relativeTo(root.getPath()), root,
+ Artifact.DESERIALIZED_MARKER_OWNER);
+ Artifact oldResult = deserializedArtifacts.putIfAbsent(execPath, result);
+ if (oldResult != null) {
+ result = oldResult;
+ }
+ return result;
+ } else {
+ Map<PathFragment, Root> sourceRoots = resolver.findPackageRoots(Lists.newArrayList(execPath));
+ if (sourceRoots == null || sourceRoots.get(execPath) == null) {
+ return null;
+ }
+ return getSourceArtifact(execPath, sourceRoots.get(execPath), ArtifactOwner.NULL_OWNER);
+ }
+ }
+
+ @Override
+ public Artifact lookupArtifactById(int artifactId) {
+ return artifactIdRegistry.lookupArtifactById(artifactId);
+ }
+
+ @Override
+ public ImmutableList<Artifact> lookupArtifactsByIds(Iterable<Integer> artifactIds) {
+ return artifactIdRegistry.lookupArtifactsByIds(artifactIds);
+ }
+
+ @Override
+ public int getArtifactId(Artifact artifact) {
+ return artifactIdRegistry.getArtifactId(artifact);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/ArtifactIdRegistry.java b/src/main/java/com/google/devtools/build/lib/actions/ArtifactIdRegistry.java
new file mode 100644
index 0000000..9edf684
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/ArtifactIdRegistry.java
@@ -0,0 +1,108 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.MapMaker;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+/**
+ * A registry that keeps a map of artifacts to unique integer ids.
+ */
+class ArtifactIdRegistry implements ArtifactSerializer, ArtifactDeserializer {
+
+ /**
+ * A sequence of registered artifacts. The position in the list is the artifact's id.
+ *
+ * <p>Synchronized using {@link #artifactIdsLock}.
+ */
+ private final List<Artifact> serializedArtifactList = new ArrayList<>();
+
+ /**
+ * A map of artifacts to unique integer ids.
+ *
+ * <p>Writes to this map must be synchronized using {@link #artifactIdsLock}, in order to
+ * maintain consistency with {@link #serializedArtifactList}.
+ */
+ private final ConcurrentMap<Artifact, Integer> serializedArtifactIds =
+ new MapMaker().concurrencyLevel(1).makeMap();
+
+ /**
+ * A lock for keeping {@code serializedArtifactList} and {@code serializedArtifactIds} in sync.
+ */
+ private ReadWriteLock artifactIdsLock = new ReentrantReadWriteLock();
+
+ ArtifactIdRegistry() {
+ }
+
+ @Override
+ public int getArtifactId(Artifact artifact) {
+ Integer artifactId = serializedArtifactIds.get(artifact);
+ if (artifactId == null) {
+ artifactId = assignArtifactId(artifact);
+ }
+ return artifactId;
+ }
+
+ private Integer assignArtifactId(Artifact artifact) {
+ artifactIdsLock.writeLock().lock();
+ try {
+ Integer artifactId = serializedArtifactIds.get(artifact);
+ if (artifactId == null) {
+ artifactId = serializedArtifactList.size();
+ serializedArtifactList.add(artifact);
+ serializedArtifactIds.put(artifact, artifactId);
+ }
+ return artifactId;
+ } finally {
+ artifactIdsLock.writeLock().unlock();
+ }
+ }
+
+ @Override
+ public Artifact lookupArtifactById(int artifactId) {
+ artifactIdsLock.readLock().lock();
+ try {
+ return serializedArtifactList.get(artifactId);
+ } finally {
+ artifactIdsLock.readLock().unlock();
+ }
+ }
+
+ @Override
+ public ImmutableList<Artifact> lookupArtifactsByIds(Iterable<Integer> artifactIds) {
+ int size = Iterables.size(artifactIds);
+ Artifact[] result = new Artifact[size];
+
+ int i = 0;
+
+ artifactIdsLock.readLock().lock();
+ try {
+ for (int artifactId : artifactIds) {
+ result[i] = serializedArtifactList.get(artifactId);
+ i++;
+ }
+ } finally {
+ artifactIdsLock.readLock().unlock();
+ }
+
+ return ImmutableList.copyOf(result);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/ArtifactOwner.java b/src/main/java/com/google/devtools/build/lib/actions/ArtifactOwner.java
new file mode 100644
index 0000000..458fb42
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/ArtifactOwner.java
@@ -0,0 +1,39 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.devtools.build.lib.syntax.Label;
+
+/**
+ * An interface for {@code LabelAndConfiguration}, or at least for a {@link Label}. Only tests and
+ * internal {@link Artifact}-generators should implement this interface -- otherwise,
+ * {@code LabelAndConfiguration} should be the only implementation.
+ */
+public interface ArtifactOwner {
+ Label getLabel();
+
+ @VisibleForTesting
+ public static final ArtifactOwner NULL_OWNER = new ArtifactOwner() {
+ @Override
+ public Label getLabel() {
+ return null;
+ }
+
+ @Override
+ public String toString() {
+ return "NULL_OWNER";
+ }
+ };
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/ArtifactPrefixConflictException.java b/src/main/java/com/google/devtools/build/lib/actions/ArtifactPrefixConflictException.java
new file mode 100644
index 0000000..42ad285
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/ArtifactPrefixConflictException.java
@@ -0,0 +1,33 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+/**
+ * Exception to indicate that one {@link Action} has an output artifact whose path is a prefix of an
+ * output of another action. Since the first path cannot be both a directory and a file, this would
+ * lead to an error if both actions were executed in the same build.
+ */
+public class ArtifactPrefixConflictException extends Exception {
+ public ArtifactPrefixConflictException(PathFragment firstPath, PathFragment secondPath,
+ Label firstOwner, Label secondOwner) {
+ super(String.format(
+ "output path '%s' (belonging to %s) is a prefix of output path '%s' (belonging to %s). "
+ + "These actions cannot be simultaneously present; please rename one of the output files "
+ + "or, as a last resort, run 'blaze clean' and then build just one of them",
+ firstPath, firstOwner, secondPath, secondOwner));
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/ArtifactResolver.java b/src/main/java/com/google/devtools/build/lib/actions/ArtifactResolver.java
new file mode 100644
index 0000000..b74c17b
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/ArtifactResolver.java
@@ -0,0 +1,56 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+/**
+ * An interface for resolving artifact names to {@link Artifact} objects. Should only be used
+ * in the internal machinery of Blaze: rule implementations are not allowed to do this.
+ */
+public interface ArtifactResolver {
+ /**
+ * Returns the source Artifact for the specified path, creating it if not found and setting its
+ * root and execPath.
+ *
+ * @param execPath the path of the source artifact relative to the source root
+ * @param root the source root prefix of the path
+ * @param owner the artifact owner.
+ * @return the canonical source artifact for the given path
+ */
+ Artifact getSourceArtifact(PathFragment execPath, Root root, ArtifactOwner owner);
+
+ /**
+ * Returns the source Artifact for the specified path, creating it if not found and setting its
+ * root and execPath.
+ *
+ * @see #getSourceArtifact(PathFragment, Root, ArtifactOwner)
+ */
+ Artifact getSourceArtifact(PathFragment execPath, Root root);
+
+ /**
+ * Resolves a source Artifact given an execRoot-relative path.
+ *
+ * <p>Never creates or returns derived artifacts, only source artifacts.
+ *
+ * <p>Note: this method should only be used when the roots are unknowable, such as from the
+ * post-compile .d or manifest scanning methods.
+ *
+ * @param execPath the exec path of the artifact to resolve
+ * @return an existing or new source Artifact for the given execPath. Returns null if
+ * the root can not be determined and the artifact did not exist before.
+ */
+ Artifact resolveSourceArtifact(PathFragment execPath);
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/ArtifactSerializer.java b/src/main/java/com/google/devtools/build/lib/actions/ArtifactSerializer.java
new file mode 100644
index 0000000..703eeb7
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/ArtifactSerializer.java
@@ -0,0 +1,30 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+/**
+ * An interface for creating artifacts from their serialized representations.
+ *
+ * @see ArtifactDeserializer
+ */
+public interface ArtifactSerializer {
+
+ /**
+ * Returns a number that uniquely identifies an artifact.
+ *
+ * <p>The artifact can be retrieved again later by calling
+ * {@link ArtifactDeserializer#lookupArtifactById}.
+ */
+ int getArtifactId(Artifact artifact);
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/BaseSpawn.java b/src/main/java/com/google/devtools/build/lib/actions/BaseSpawn.java
new file mode 100644
index 0000000..dd879c2
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/BaseSpawn.java
@@ -0,0 +1,214 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.actions.extra.EnvironmentVariable;
+import com.google.devtools.build.lib.actions.extra.SpawnInfo;
+import com.google.devtools.build.lib.util.CommandDescriptionForm;
+import com.google.devtools.build.lib.util.CommandFailureUtils;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * Base implementation of a Spawn.
+ */
+@Immutable
+public class BaseSpawn implements Spawn {
+ private final ImmutableList<String> arguments;
+ private final ImmutableMap<String, String> environment;
+ private final ImmutableMap<String, String> executionInfo;
+ private final ImmutableMap<PathFragment, Artifact> runfilesManifests;
+ private final ActionMetadata action;
+ private final ResourceSet localResources;
+
+ /**
+ * Returns a new Spawn. The caller must not modify the parameters after the call; neither will
+ * this method.
+ */
+ public BaseSpawn(List<String> arguments,
+ Map<String, String> environment,
+ Map<String, String> executionInfo,
+ Map<PathFragment, Artifact> runfilesManifests,
+ ActionMetadata action,
+ ResourceSet localResources) {
+ this.arguments = ImmutableList.copyOf(arguments);
+ this.environment = ImmutableMap.copyOf(environment);
+ this.executionInfo = ImmutableMap.copyOf(executionInfo);
+ this.runfilesManifests = ImmutableMap.copyOf(runfilesManifests);
+ this.action = action;
+ this.localResources = localResources;
+ }
+
+ /**
+ * Returns a new Spawn.
+ */
+ public BaseSpawn(List<String> arguments,
+ Map<String, String> environment,
+ Map<String, String> executionInfo,
+ // TODO(bazel-team): have this always be non-null.
+ @Nullable Artifact runfilesManifest,
+ ActionMetadata action,
+ ResourceSet localResources) {
+ this(arguments, environment, executionInfo,
+ ((runfilesManifest != null)
+ ? ImmutableMap.of(runfilesForFragment(new PathFragment(arguments.get(0))),
+ runfilesManifest)
+ : ImmutableMap.<PathFragment, Artifact>of()),
+ action, localResources);
+ }
+
+ public static PathFragment runfilesForFragment(PathFragment pathFragment) {
+ return pathFragment.getParentDirectory().getChild(pathFragment.getBaseName() + ".runfiles");
+ }
+
+ /**
+ * Returns a new Spawn.
+ */
+ public BaseSpawn(List<String> arguments,
+ Map<String, String> environment,
+ Map<String, String> executionInfo,
+ ActionMetadata action,
+ ResourceSet localResources) {
+ this(arguments, environment, executionInfo,
+ ImmutableMap.<PathFragment, Artifact>of(), action, localResources);
+ }
+
+ @Override
+ public boolean isRemotable() {
+ return !executionInfo.containsKey("local");
+ }
+
+ @Override
+ public final ImmutableMap<String, String> getExecutionInfo() {
+ return executionInfo;
+ }
+
+ @Override
+ public String asShellCommand(Path workingDir) {
+ return asShellCommand(getArguments(), workingDir, getEnvironment());
+ }
+
+ @Override
+ public ImmutableMap<PathFragment, Artifact> getRunfilesManifests() {
+ return runfilesManifests;
+ }
+
+ @Override
+ public ImmutableList<Artifact> getFilesetManifests() {
+ return ImmutableList.<Artifact>of();
+ }
+
+ @Override
+ public SpawnInfo getExtraActionInfo() {
+ SpawnInfo.Builder info = SpawnInfo.newBuilder();
+
+ info.addAllArgument(getArguments());
+ for (Map.Entry<String, String> variable : getEnvironment().entrySet()) {
+ info.addVariable(EnvironmentVariable.newBuilder()
+ .setName(variable.getKey())
+ .setValue(variable.getValue()).build());
+ }
+ for (ActionInput input : getInputFiles()) {
+ // Explicitly ignore middleman artifacts here.
+ if (!(input instanceof Artifact) || !((Artifact) input).isMiddlemanArtifact()) {
+ info.addInputFile(input.getExecPathString());
+ }
+ }
+ info.addAllOutputFile(ActionInputHelper.toExecPaths(getOutputFiles()));
+ return info.build();
+ }
+
+ @Override
+ public ImmutableList<String> getArguments() {
+ // TODO(bazel-team): this method should be final, as the correct value of the args can be
+ // injected in the ctor.
+ return arguments;
+ }
+
+ @Override
+ public ImmutableMap<String, String> getEnvironment() {
+ if (getRunfilesManifests().size() != 1) {
+ return environment;
+ }
+
+ ImmutableMap.Builder<String, String> env = ImmutableMap.builder();
+ env.putAll(environment);
+ for (Map.Entry<PathFragment, Artifact> e : getRunfilesManifests().entrySet()) {
+ // TODO(bazel-team): Unify these into a single env variable.
+ env.put("JAVA_RUNFILES", e.getKey().getPathString() + "/");
+ env.put("PYTHON_RUNFILES", e.getKey().getPathString() + "/");
+ }
+ return env.build();
+ }
+
+ @Override
+ public Iterable<? extends ActionInput> getInputFiles() {
+ return action.getInputs();
+ }
+
+ @Override
+ public Collection<? extends ActionInput> getOutputFiles() {
+ return action.getOutputs();
+ }
+
+ @Override
+ public ActionMetadata getResourceOwner() {
+ return action;
+ }
+
+ @Override
+ public ResourceSet getLocalResources() {
+ return localResources;
+ }
+
+ @Override
+ public ActionOwner getOwner() { return action.getOwner(); }
+
+ @Override
+ public String getMnemonic() { return action.getMnemonic(); }
+
+ /**
+ * Convert a working dir + environment map + arg list into a Bourne shell
+ * command.
+ */
+ public static String asShellCommand(Collection<String> arguments,
+ Path workingDirectory,
+ Map<String, String> environment) {
+ // We print this command out in such a way that it can safely be
+ // copied+pasted as a Bourne shell command. This is extremely valuable for
+ // debugging.
+ return CommandFailureUtils.describeCommand(CommandDescriptionForm.COMPLETE,
+ arguments, environment, workingDirectory.getPathString());
+ }
+
+ /**
+ * A local spawn requiring zero resources.
+ */
+ public static class Local extends BaseSpawn {
+ public Local(List<String> arguments, Map<String, String> environment, ActionMetadata action) {
+ super(arguments, environment, ImmutableMap.<String, String>of("local", ""),
+ action, ResourceSet.ZERO);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/BipartiteVisitor.java b/src/main/java/com/google/devtools/build/lib/actions/BipartiteVisitor.java
new file mode 100644
index 0000000..61803c8
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/BipartiteVisitor.java
@@ -0,0 +1,98 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A visitor helper class for bipartite graphs. The alternate kinds of nodes
+ * are arbitrarily designated "black" or "white".
+ *
+ * <p> Subclasses implement the black() and white() hook functions which are
+ * called as nodes are visited. The class holds a mapping from each node to a
+ * small integer; this is available to subclasses if they wish.
+ */
+public abstract class BipartiteVisitor<BLACK, WHITE> {
+
+ protected BipartiteVisitor() {}
+
+ private int nextNodeId = 0;
+
+ // Maps each visited black node to a small integer.
+ protected final Map<BLACK, Integer> visitedBlackNodes = new HashMap<>();
+
+ // Maps each visited white node to a small integer.
+ protected final Map<WHITE, Integer> visitedWhiteNodes = new HashMap<>();
+
+ /**
+ * Visit the specified black node. If this node has not already been
+ * visited, the black() hook is called and true is returned; otherwise,
+ * false is returned.
+ */
+ public final boolean visitBlackNode(BLACK blackNode) {
+ if (blackNode == null) { throw new NullPointerException(); }
+ if (!visitedBlackNodes.containsKey(blackNode)) {
+ visitedBlackNodes.put(blackNode, nextNodeId++);
+ black(blackNode);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Visit all specified black nodes.
+ */
+ public final void visitBlackNodes(Iterable<BLACK> blackNodes) {
+ for (BLACK blackNode : blackNodes) {
+ visitBlackNode(blackNode);
+ }
+ }
+
+ /**
+ * Visit the specified white node. If this node has not already been
+ * visited, the white() hook is called and true is returned; otherwise,
+ * false is returned.
+ */
+ public final boolean visitWhiteNode(WHITE whiteNode) {
+ if (whiteNode == null) {
+ throw new NullPointerException();
+ }
+ if (!visitedWhiteNodes.containsKey(whiteNode)) {
+ visitedWhiteNodes.put(whiteNode, nextNodeId++);
+ white(whiteNode);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Visit all specified white nodes.
+ */
+ public final void visitWhiteNodes(Iterable<WHITE> whiteNodes) {
+ for (WHITE whiteNode : whiteNodes) {
+ visitWhiteNode(whiteNode);
+ }
+ }
+
+ /**
+ * Called whenever a white node is visited. Hook for subclasses.
+ */
+ protected abstract void white(WHITE whiteNode);
+
+ /**
+ * Called whenever a black node is visited. Hook for subclasses.
+ */
+ protected abstract void black(BLACK blackNode);
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/BlazeExecutor.java b/src/main/java/com/google/devtools/build/lib/actions/BlazeExecutor.java
new file mode 100644
index 0000000..fd3c3d9
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/BlazeExecutor.java
@@ -0,0 +1,233 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.eventbus.EventBus;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.events.EventKind;
+import com.google.devtools.build.lib.events.Reporter;
+import com.google.devtools.build.lib.profiler.Profiler;
+import com.google.devtools.build.lib.profiler.ProfilerTask;
+import com.google.devtools.build.lib.util.Clock;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.common.options.OptionsClassProvider;
+
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * The Executor class provides a dynamic abstraction of the various actual primitive system
+ * operations that might be performed during a build step.
+ *
+ * <p>Constructions of this class might perform distributed execution, "virtual" execution for
+ * testing purposes, or just print out the sequence of commands that would be executed, like Make's
+ * "-n" option.
+ */
+@ThreadSafe
+public final class BlazeExecutor implements Executor {
+
+ private final Path outputPath;
+ private final boolean verboseFailures;
+ private final boolean showSubcommands;
+ private final Path execRoot;
+ private final Reporter reporter;
+ private final EventBus eventBus;
+ private final Clock clock;
+ private final OptionsClassProvider options;
+ private AtomicBoolean inExecutionPhase;
+
+ private final Map<String, SpawnActionContext> spawnActionContextMap;
+ private final Map<Class<? extends ActionContext>, ActionContext> contextMap =
+ new HashMap<>();
+
+ /**
+ * Constructs an Executor, bound to a specified output base path, and which
+ * will use the specified reporter to announce SUBCOMMAND events,
+ * the given event bus to delegate events and the given output streams
+ * for streaming output. The list of
+ * strategy implementation classes is used to construct instances of the
+ * strategies mapped by their declared abstract type. This list is uniquified
+ * before using. Each strategy instance is created with a reference to this
+ * Executor as well as the given options object.
+ * <p>
+ * Don't forget to call startBuildRequest() and stopBuildRequest() for each
+ * request, and shutdown() when you're done with this executor.
+ */
+ public BlazeExecutor(Path execRoot,
+ Path outputPath,
+ Reporter reporter,
+ EventBus eventBus,
+ Clock clock,
+ OptionsClassProvider options,
+ boolean verboseFailures,
+ boolean showSubcommands,
+ List<ActionContext> contextImplementations,
+ Map<String, ActionContext> spawnContextMap,
+ Iterable<ActionContextProvider> contextProviders)
+ throws ExecutorInitException {
+ this.outputPath = outputPath;
+ this.verboseFailures = verboseFailures;
+ this.showSubcommands = showSubcommands;
+ this.execRoot = execRoot;
+ this.reporter = reporter;
+ this.eventBus = eventBus;
+ this.clock = clock;
+ this.options = options;
+ this.inExecutionPhase = new AtomicBoolean(false);
+
+ // We need to keep only the last occurrences of the entries in contextImplementations
+ // (so we respect insertion order but also instantiate them only once).
+ LinkedHashSet<ActionContext> allContexts = new LinkedHashSet<>();
+ allContexts.addAll(contextImplementations);
+
+ ImmutableMap.Builder<String, SpawnActionContext> spawnMapBuilder = ImmutableMap.builder();
+ for (Map.Entry<String, ActionContext> entry: spawnContextMap.entrySet()) {
+ spawnMapBuilder.put(entry.getKey(), (SpawnActionContext) entry.getValue());
+ allContexts.add(entry.getValue());
+ }
+
+ for (ActionContext context : contextImplementations) {
+ ExecutionStrategy annotation = context.getClass().getAnnotation(ExecutionStrategy.class);
+ if (annotation != null) {
+ contextMap.put(annotation.contextType(), context);
+ }
+ }
+ this.spawnActionContextMap = spawnMapBuilder.build();
+
+ for (ActionContextProvider factory : contextProviders) {
+ factory.executorCreated(allContexts);
+ }
+ }
+
+ @Override
+ public Path getExecRoot() {
+ return execRoot;
+ }
+
+ @Override
+ public EventHandler getEventHandler() {
+ return reporter;
+ }
+
+ @Override
+ public EventBus getEventBus() {
+ return eventBus;
+ }
+
+ @Override
+ public Clock getClock() {
+ return clock;
+ }
+
+ @Override
+ public boolean reportsSubcommands() {
+ return showSubcommands;
+ }
+
+ /**
+ * Report a subcommand event to this Executor's Reporter and, if action
+ * logging is enabled, post it on its EventBus.
+ */
+ @Override
+ public void reportSubcommand(String reason, String message) {
+ reporter.handle(new Event(EventKind.SUBCOMMAND, null, "# " + reason + "\n" + message));
+ }
+
+ /**
+ * This method is called before the start of the execution phase of each
+ * build request.
+ */
+ public void executionPhaseStarting() {
+ Preconditions.checkState(!inExecutionPhase.getAndSet(true));
+ Profiler.instance().startTask(ProfilerTask.INFO, "Initializing executors");
+ Profiler.instance().completeTask(ProfilerTask.INFO);
+ }
+
+ /**
+ * This method is called after the end of the execution phase of each build
+ * request (even if there was an interrupt).
+ */
+ public void executionPhaseEnding() {
+ if (!inExecutionPhase.get()) {
+ return;
+ }
+
+ Profiler.instance().startTask(ProfilerTask.INFO, "Shutting down executors");
+ Profiler.instance().completeTask(ProfilerTask.INFO);
+ inExecutionPhase.set(false);
+ }
+
+ public static void shutdownHelperPool(EventHandler reporter, ExecutorService pool,
+ String name) {
+ pool.shutdownNow();
+
+ boolean interrupted = false;
+ while (true) {
+ try {
+ if (!pool.awaitTermination(10, TimeUnit.SECONDS)) {
+ reporter.handle(Event.warn(name + " threadpool shutdown took greater than ten seconds"));
+ }
+ break;
+ } catch (InterruptedException e) {
+ interrupted = true;
+ }
+ }
+
+ if (interrupted) {
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ @Override
+ public <T extends ActionContext> T getContext(Class<? extends T> type) {
+ Preconditions.checkArgument(type != SpawnActionContext.class,
+ "should use getSpawnActionContext instead");
+ return type.cast(contextMap.get(type));
+ }
+
+ /**
+ * Returns the {@link SpawnActionContext} to use for the given mnemonic. If no execution mode is
+ * set, then it returns the default strategy for spawn actions.
+ */
+ @Override
+ public SpawnActionContext getSpawnActionContext(String mnemonic) {
+ SpawnActionContext context = spawnActionContextMap.get(mnemonic);
+ return context == null ? spawnActionContextMap.get("") : context;
+ }
+
+ /** Returns true iff the --verbose_failures option was enabled. */
+ @Override
+ public boolean getVerboseFailures() {
+ return verboseFailures;
+ }
+
+ /** Returns the options associated with the execution. */
+ @Override
+ public OptionsClassProvider getOptions() {
+ return options;
+ }
+
+ public Path getOutputPath() {
+ return outputPath;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/BuildFailedException.java b/src/main/java/com/google/devtools/build/lib/actions/BuildFailedException.java
new file mode 100644
index 0000000..088ba60
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/BuildFailedException.java
@@ -0,0 +1,81 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.syntax.Label;
+
+/**
+ * This exception gets thrown if there were errors during the execution phase of
+ * the build.
+ *
+ * <p>The argument to the constructor may be null if the thrower has already
+ * printed an error message; in this case, no error message should be printed by
+ * the catcher. (Typically, this happens when the builder is unsuccessful and
+ * {@code --keep_going} was specified. This error corresponds to one or more
+ * actions failing, but since those actions' failures will be reported
+ * separately, the exception carries no message and is just used for control
+ * flow.)
+ */
+@ThreadSafe
+public class BuildFailedException extends Exception {
+ private final boolean catastrophic;
+ private final Action action;
+ private final Iterable<Label> rootCauses;
+ private final boolean errorAlreadyShown;
+
+ public BuildFailedException() {
+ this(null);
+ }
+
+ public BuildFailedException(String message) {
+ this(message, false, null, ImmutableList.<Label>of());
+ }
+
+ public BuildFailedException(String message, boolean catastrophic) {
+ this(message, catastrophic, null, ImmutableList.<Label>of());
+ }
+
+ public BuildFailedException(String message, boolean catastrophic,
+ Action action, Iterable<Label> rootCauses) {
+ this(message, catastrophic, action, rootCauses, false);
+ }
+
+ public BuildFailedException(String message, boolean catastrophic,
+ Action action, Iterable<Label> rootCauses, boolean errorAlreadyShown) {
+ super(message);
+ this.catastrophic = catastrophic;
+ this.rootCauses = ImmutableList.copyOf(rootCauses);
+ this.action = action;
+ this.errorAlreadyShown = errorAlreadyShown;
+ }
+
+ public boolean isCatastrophic() {
+ return catastrophic;
+ }
+
+ public Action getAction() {
+ return action;
+ }
+
+ public Iterable<Label> getRootCauses() {
+ return rootCauses;
+ }
+
+ public boolean isErrorAlreadyShown() {
+ return errorAlreadyShown || getMessage() == null;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/BuilderUtils.java b/src/main/java/com/google/devtools/build/lib/actions/BuilderUtils.java
new file mode 100644
index 0000000..72ce13335
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/BuilderUtils.java
@@ -0,0 +1,57 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+/**
+ * Methods needed by {@code SkyframeBuilder}.
+ */
+public final class BuilderUtils {
+
+ private BuilderUtils() {}
+
+ /**
+ * Figure out why an action's execution failed and rethrow the right kind of exception.
+ */
+ public static void rethrowCause(Exception e) throws BuildFailedException, TestExecException {
+ Throwable cause = e.getCause();
+ Throwable innerCause = cause.getCause();
+ if (innerCause instanceof TestExecException) {
+ throw (TestExecException) innerCause;
+ }
+ if (cause instanceof ActionExecutionException) {
+ ActionExecutionException actionExecutionCause = (ActionExecutionException) cause;
+ // Sometimes ActionExecutionExceptions are caused by Actions with no owner.
+ String message =
+ (actionExecutionCause.getLocation() != null) ?
+ (actionExecutionCause.getLocation().print() + " " + cause.getMessage()) :
+ e.getMessage();
+ throw new BuildFailedException(message, actionExecutionCause.isCatastrophe(),
+ actionExecutionCause.getAction(), actionExecutionCause.getRootCauses(),
+ /*errorAlreadyShown=*/ !actionExecutionCause.showError());
+ } else if (cause instanceof MissingInputFileException) {
+ throw new BuildFailedException(cause.getMessage());
+ } else if (cause instanceof RuntimeException) {
+ throw (RuntimeException) cause;
+ } else if (cause instanceof Error) {
+ throw (Error) cause;
+ } else {
+ /*
+ * This should never happen - we should only get exceptions listed in the exception
+ * specification for ExecuteBuildAction.call().
+ */
+ throw new IllegalArgumentException("action terminated with "
+ + "unexpected exception: " + cause.getMessage(), cause);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/CachedActionEvent.java b/src/main/java/com/google/devtools/build/lib/actions/CachedActionEvent.java
new file mode 100644
index 0000000..35db7d6
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/CachedActionEvent.java
@@ -0,0 +1,45 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+/**
+ * This event is fired during the build if an action was in the action cache.
+ */
+public class CachedActionEvent {
+
+ private final Action action;
+ private final long nanoTimeStart;
+
+ /**
+ * Create an event for an action that was cached.
+ *
+ * @param action the cached action
+ * @param nanoTimeStart the time when the action was started. This allow us to
+ * record more accurately the time spend by the action, since we execute some code before
+ * deciding if we execute the action or not.
+ */
+ public CachedActionEvent(Action action, long nanoTimeStart) {
+ this.action = action;
+ this.nanoTimeStart = nanoTimeStart;
+ }
+
+ public Action getAction() {
+ return action;
+ }
+
+ public long getNanoTimeStart() {
+ return nanoTimeStart;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/ChangedArtifactsMessage.java b/src/main/java/com/google/devtools/build/lib/actions/ChangedArtifactsMessage.java
new file mode 100644
index 0000000..9177cba
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/ChangedArtifactsMessage.java
@@ -0,0 +1,34 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+import com.google.common.collect.ImmutableSet;
+
+import java.util.Set;
+
+/**
+ * Used to signal when the incremental builder has found the set of changed artifacts.
+ */
+public class ChangedArtifactsMessage {
+
+ private final Set<Artifact> artifacts;
+
+ public ChangedArtifactsMessage(Set<Artifact> changedArtifacts) {
+ this.artifacts = ImmutableSet.copyOf(changedArtifacts);
+ }
+
+ public Set<Artifact> getChangedArtifacts() {
+ return artifacts;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/ChangedFilesMessage.java b/src/main/java/com/google/devtools/build/lib/actions/ChangedFilesMessage.java
new file mode 100644
index 0000000..56f6882
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/ChangedFilesMessage.java
@@ -0,0 +1,35 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.Set;
+
+/**
+ * A message sent conveying a set of changed files.
+ */
+public class ChangedFilesMessage {
+
+ private final Set<PathFragment> changedFiles;
+
+ public ChangedFilesMessage(Set<PathFragment> changedFiles) {
+ this.changedFiles = ImmutableSet.copyOf(changedFiles);
+ }
+
+ public Set<PathFragment> getChangedFiles() {
+ return changedFiles;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/ConcurrentMultimapWithHeadElement.java b/src/main/java/com/google/devtools/build/lib/actions/ConcurrentMultimapWithHeadElement.java
new file mode 100644
index 0000000..d9d875d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/ConcurrentMultimapWithHeadElement.java
@@ -0,0 +1,220 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadHostile;
+
+import java.util.Iterator;
+import java.util.Set;
+import java.util.concurrent.ConcurrentMap;
+
+import javax.annotation.Nullable;
+
+/**
+ * A Multimap-like object that is actually a {@link ConcurrentMap} of {@code SmallSet}s to avoid
+ * the memory penalties of a {@code Multimap} while preserving concurrency guarantees, and
+ * retrieving a consistent "head" element. Operations are guaranteed to reflect a consistent view of
+ * a {@code SetMultimap}, although most methods are not implemented.
+ */
+final class ConcurrentMultimapWithHeadElement<K, V> {
+ private final ConcurrentMap<K, SmallSet<V>> map = Maps.newConcurrentMap();
+
+ /**
+ * Remove (key, val) pair from the multimap. If this removes the current 'head' element
+ * for a key, then another randomly chosen element becomes the current head.
+ *
+ * <p>Until the next (possibly concurrent) {@link #putAndGet}(key, val) call, {@link #get}(key)
+ * will never return val.
+ */
+ void remove(K key, V val) {
+ SmallSet<V> entry = getEntry(key);
+ if (entry != null) {
+ entry.remove(val);
+ if (entry.get() == null) {
+ // Remove entry completely from map if dead.
+ map.remove(key, entry);
+ }
+ }
+ }
+
+ /**
+ * Return some value val such that (key, val) is in the multimap. If there is always at least one
+ * entry for key in the multimap during the lifetime of this method call, it will not return null.
+ */
+ @Nullable V get(K key) {
+ SmallSet<V> entry = getEntry(key);
+ return (entry != null) ? entry.get() : null;
+ }
+
+ /**
+ * Adds (key, val) to the multimap. Returns the head element for key, either val or another
+ * already-stored value.
+ */
+ V putAndGet(K key, V val) {
+ V result = null;
+ while (result == null) {
+ // If another thread concurrently removes the only remaining value from the entry, this
+ // putAndGet will return null, since the entry is about to be removed from the map. In that
+ // case, we obtain a fresh entry from the map and do the put on it.
+ result = getOrCreateEntry(key).putAndGet(val);
+ }
+ return result;
+ }
+
+ /**
+ * Obtain the entry for key, adding it to the underlying map if no entry was previously present.
+ */
+ private SmallSet<V> getOrCreateEntry(K key) {
+ SmallSet<V> entry = new SmallSet<V>();
+ SmallSet<V> oldEntry = map.putIfAbsent(key, entry);
+ if (oldEntry != null) {
+ return oldEntry;
+ }
+ return entry;
+ }
+
+ /**
+ * Obtain the entry for key, returning null if no entry was present in the underlying map.
+ */
+ private SmallSet<V> getEntry(K key) {
+ return map.get(key);
+ }
+
+ /**
+ * Clears the multimap. May not be called concurrently with any other methods.
+ */
+ @ThreadHostile
+ void clear() {
+ map.clear();
+ }
+
+ /**
+ * Wrapper for a {@code #Set} that will probably have at most one element. Keeps the first element
+ * in a separate variable for fast reading/writing and to save space if more than one element is
+ * never written to this set. We always have the invariant that {@link #first} is null only if
+ * {@link #rest} is null.
+ */
+ private static class SmallSet<T> {
+ /*
+ * What is this 'volatile' on first and where's the lock on the read path?
+ *
+ * Volatile is an alternative to locking that works only in very limited situations, such as
+ * simple field reads and writes. Writes from one thread to 'first' happen before reads from
+ * other threads. When used correctly, it can have the same correctness properties as a
+ * 'synchronized' but is much faster on most hardware.
+ *
+ * Here, volatile is used to eliminate locks on the read path. Since get() is merely fetching
+ * the contents of 'first', it meets the criteria for a safe volatile read. In the mutator
+ * methods, care is taken to write only correct values to 'first'; intermediate and incomplete
+ * values do not get written to the field. This means that whenever 'first' is replaced, it is
+ * immediately replaced with the next correct value. Therefore, it is a safe volatile write.
+ *
+ * Other more complex relationships that need to be maintained during the mutate are maintained
+ * with the Object monitor. Since they do not impact the read path (only 'first' matters), the
+ * lock is sufficient for writes and unnecessary for 'first' reads.
+ *
+ * Documentation on volatile:
+ * http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/package-summary.html#MemoryVisibility
+ * (java.util.concurrent package docs)
+ */
+
+ private volatile T first = null;
+ private Set<T> rest = null;
+
+ /*
+ * We may have a race where one thread tries to remove a small set from the map while another
+ * thread tries to add to it. If the second thread loses the race, it will add to a set that is
+ * no longer in the map. To prevent that, once a small set is ever empty, we mark it "dead" by
+ * setting {@code rest} to a {@code TOMBSTONE} value, and (and subsequently remove it from the
+ * map). No modifications to a set can happen after the {@code TOMBSTONE} value is set. Thus,
+ * the thread trying to add a new value to a set will fail, and knows to retrieve the entry anew
+ * from the map and try again.
+ */
+ private static final Set<Object> TOMBSTONE = ImmutableSet.of();
+
+ /**
+ * Return some value in the SmallSet.
+ *
+ * <p>If there is always at least one value in the SmallSet during the lifetime of this call,
+ * it will not return null, since by the invariant, {@link #first} must be non-null.
+ */
+ private T get() {
+ return first;
+ }
+
+ /**
+ * Adds val to the SmallSet. Returns some element of the SmallSet.
+ */
+ private synchronized T putAndGet(T elt) {
+ Preconditions.checkNotNull(elt);
+ if (isDead()) {
+ return null;
+ }
+ if (elt.equals(first)) {
+ return first;
+ }
+ if (first == null) {
+ Preconditions.checkState(rest == null, elt);
+ first = elt;
+ return first;
+ }
+ if (rest == null) {
+ rest = Sets.newHashSet();
+ }
+ rest.add(elt);
+ return first;
+ }
+
+ /**
+ * Remove val from the SmallSet, if it is present.
+ */
+ private synchronized void remove(T elt) {
+ Preconditions.checkNotNull(elt);
+ if (isDead()) {
+ return;
+ }
+ if (elt.equals(first)) {
+ // Normalize to enforce invariant "first is null only if rest is empty."
+ if (rest != null) {
+ Iterator<T> it = rest.iterator();
+ first = it.next();
+ it.remove();
+ if (!it.hasNext()) {
+ rest = null;
+ }
+ } else {
+ first = null;
+ markDead();
+ }
+ } else if ((rest != null) && rest.remove(elt) && rest.isEmpty()) { // side-effect: remove
+ rest = null;
+ }
+ }
+
+ private boolean isDead() {
+ Preconditions.checkState(rest != TOMBSTONE || first == null,
+ "%s present in tombstoned SmallSet, but tombstoned SmallSets should be empty", first);
+ return rest == TOMBSTONE;
+ }
+
+ @SuppressWarnings("unchecked") // Cast of TOMBSTONE. Ok since TOMBSTONE is empty immutable set.
+ private void markDead() {
+ rest = (Set<T>) TOMBSTONE;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/DelegateSpawn.java b/src/main/java/com/google/devtools/build/lib/actions/DelegateSpawn.java
new file mode 100644
index 0000000..4b4048b
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/DelegateSpawn.java
@@ -0,0 +1,106 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.actions.extra.SpawnInfo;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.Collection;
+
+/**
+ * A delegating spawn that allow us to overwrite certain methods while maintaining the original
+ * behavior for non-overwritten methods.
+ */
+public class DelegateSpawn implements Spawn {
+
+ private final Spawn spawn;
+
+ public DelegateSpawn(Spawn spawn){
+ this.spawn = spawn;
+ }
+
+ @Override
+ public final ImmutableMap<String, String> getExecutionInfo() {
+ return spawn.getExecutionInfo();
+ }
+
+ @Override
+ public boolean isRemotable() {
+ return spawn.isRemotable();
+ }
+
+ @Override
+ public ImmutableList<Artifact> getFilesetManifests() {
+ return spawn.getFilesetManifests();
+ }
+
+ @Override
+ public String asShellCommand(Path workingDir) {
+ return spawn.asShellCommand(workingDir);
+ }
+
+ @Override
+ public ImmutableMap<PathFragment, Artifact> getRunfilesManifests() {
+ return spawn.getRunfilesManifests();
+ }
+
+ @Override
+ public SpawnInfo getExtraActionInfo() {
+ return spawn.getExtraActionInfo();
+ }
+
+ @Override
+ public ImmutableList<String> getArguments() {
+ return spawn.getArguments();
+ }
+
+ @Override
+ public ImmutableMap<String, String> getEnvironment() {
+ return spawn.getEnvironment();
+ }
+
+ @Override
+ public Iterable<? extends ActionInput> getInputFiles() {
+ return spawn.getInputFiles();
+ }
+
+ @Override
+ public Collection<? extends ActionInput> getOutputFiles() {
+ return spawn.getOutputFiles();
+ }
+
+ @Override
+ public ActionMetadata getResourceOwner() {
+ return spawn.getResourceOwner();
+ }
+
+ @Override
+ public ResourceSet getLocalResources() {
+ return spawn.getLocalResources();
+ }
+
+ @Override
+ public ActionOwner getOwner() {
+ return spawn.getOwner();
+ }
+
+ @Override
+ public String getMnemonic() {
+ return spawn.getMnemonic();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/DigestOfDirectoryException.java b/src/main/java/com/google/devtools/build/lib/actions/DigestOfDirectoryException.java
new file mode 100644
index 0000000..92cc12b
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/DigestOfDirectoryException.java
@@ -0,0 +1,28 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+import java.io.IOException;
+
+/**
+ * Exception thrown when we try to digest a directory in {@code ActionInputFileCache}.
+ *
+ */
+public class DigestOfDirectoryException extends IOException {
+
+ public DigestOfDirectoryException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/EnvironmentalExecException.java b/src/main/java/com/google/devtools/build/lib/actions/EnvironmentalExecException.java
new file mode 100644
index 0000000..e2c493c
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/EnvironmentalExecException.java
@@ -0,0 +1,58 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+/**
+ * An ExecException which is results from an external problem on the user's
+ * local system.
+ *
+ * <p>Note that this is fundamentally different exception then the higher level
+ * LocalEnvironmentException, which is thrown from the BuildTool. That exception
+ * is thrown when the higher levels of Blaze decide to exit.
+ *
+ * <p>This exception is thrown when a low level error is encountered in the
+ * strategy or client protocol layers. This does not necessarily mean we will
+ * exit; we may just retry the action.
+ */
+public class EnvironmentalExecException extends ExecException {
+
+ public EnvironmentalExecException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public EnvironmentalExecException(String message) {
+ super(message);
+ }
+
+ public EnvironmentalExecException(String message, Throwable cause, boolean catastrophe) {
+ super(message, cause, catastrophe);
+ }
+
+ public EnvironmentalExecException(String message, boolean catastrophe) {
+ super(message, catastrophe);
+ }
+
+ @Override
+ public ActionExecutionException toActionExecutionException(String messagePrefix,
+ boolean verboseFailures, Action action) {
+ if (verboseFailures) {
+ return new ActionExecutionException(messagePrefix + " failed" + getMessage(), this, action,
+ isCatastrophic());
+ } else {
+ return new ActionExecutionException(messagePrefix + " failed" + getMessage(), action,
+ isCatastrophic());
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/ExecException.java b/src/main/java/com/google/devtools/build/lib/actions/ExecException.java
new file mode 100644
index 0000000..a2edc84
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/ExecException.java
@@ -0,0 +1,96 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+/**
+ * An exception indication that the execution of an action has failed OR could
+ * not be attempted OR could not be finished OR had something else wrong.
+ *
+ * <p>The four main kinds of failure are broadly defined as follows:
+ *
+ * <p>USER_INPUT which means it had something to do with what the user told us
+ * to do. This failure should satisfy the invariant that it would happen
+ * identically again if all other things are equal.
+ *
+ * <p>ENVIRONMENT which is loosely defined as anything which is generally out of
+ * scope for a blaze evaluation. As a rule of thumb, these are any errors would
+ * not necessarily happen again given constant input.
+ *
+ * <p>INTERRUPTION conditions arise from being unable to complete an evaluation
+ * for whatever reason.
+ *
+ * <p>INTERNAL_ERROR would happen because of anything which arises from within
+ * blaze itself but is generally unexpected to ever occur for any user input.
+ *
+ * <p>The class is a catch-all for both failures of actions and failures to
+ * evaluate actions properly.
+ *
+ * <p>Invariably, all low level ExecExceptions are caught by various specific
+ * ConfigurationAction classes and re-raised as ActionExecutionExceptions.
+ */
+public abstract class ExecException extends Exception {
+
+ private final boolean catastrophe;
+
+ public ExecException(String message, boolean catastrophe) {
+ super(message);
+ this.catastrophe = catastrophe;
+ }
+
+ public ExecException(String message) {
+ this(message, false);
+ }
+
+ public ExecException(String message, Throwable cause, boolean catastrophe) {
+ super(message + ": " + cause.getMessage(), cause);
+ this.catastrophe = catastrophe;
+ }
+
+ public ExecException(String message, Throwable cause) {
+ this(message, cause, false);
+ }
+
+ /**
+ * Catastrophic exceptions should stop the build, even if --keep_going.
+ */
+ public boolean isCatastrophic() {
+ return catastrophe;
+ }
+
+ /**
+ * Returns a new ActionExecutionException without a message prefix.
+ * @param action failed action
+ * @return ActionExecutionException object describing the action failure
+ */
+ public ActionExecutionException toActionExecutionException(Action action) {
+ // In all ExecException implementations verboseFailures argument used only to determine should
+ // we pass ExecException as cause of ActionExecutionException. So use this method only
+ // if you need this information inside of ActionExecutionexception.
+ return toActionExecutionException("", true, action);
+ }
+
+ /**
+ * Returns a new ActionExecutionException given a message prefix describing the action type as a
+ * noun. When appropriate (we use some heuristics to decide), produces an abbreviated message
+ * incorporating just the termination status if available.
+ *
+ * @param messagePrefix describes the action type as noun
+ * @param verboseFailures true if user requested verbose output with flag --verbose_failures
+ * @param action failed action
+ * @return ActionExecutionException object describing the action failure
+ */
+ public abstract ActionExecutionException toActionExecutionException(String messagePrefix,
+ boolean verboseFailures, Action action);
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/ExecutionStrategy.java b/src/main/java/com/google/devtools/build/lib/actions/ExecutionStrategy.java
new file mode 100644
index 0000000..d86ea6c
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/ExecutionStrategy.java
@@ -0,0 +1,37 @@
+// Copyright 2014 Google Inc. 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.actions;
+import com.google.devtools.build.lib.actions.Executor.ActionContext;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation that marks strategies that extend the execution phase behavior of Blaze.
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ExecutionStrategy {
+ /**
+ * The names this strategy is available under on the command line.
+ */
+ String[] name() default {};
+
+ /**
+ * Returns the action context this strategy implements.
+ */
+ Class<? extends ActionContext> contextType();
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/Executor.java b/src/main/java/com/google/devtools/build/lib/actions/Executor.java
new file mode 100644
index 0000000..f3904bb
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/Executor.java
@@ -0,0 +1,103 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+import com.google.common.eventbus.EventBus;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.util.Clock;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.common.options.OptionsClassProvider;
+
+/**
+ * The Executor provides the context for the execution of actions. It is only valid during the
+ * execution phase, and references should not be cached.
+ *
+ * <p>This class provides the actual logic to execute actions. The platonic ideal of this system
+ * is that {@link Action}s are immutable objects that tell Blaze <b>what</b> to do and
+ * <link>ActionContext</link>s tell Blaze <b>how</b> to do it (however, we do have an "execute"
+ * method on actions now).
+ *
+ * <p>In theory, most of the methods below would not exist and they would be methods on action
+ * contexts, but in practice, that would require some refactoring work so we are stuck with these
+ * for the time being.
+ *
+ * <p>In theory, we could also merge {@link Executor} with {@link ActionExecutionContext}, since
+ * they both provide services to actions being executed and are passed to almost the same places.
+ */
+public interface Executor {
+ /**
+ * A marker interface for classes that provide services for actions during execution.
+ *
+ * <p>Interfaces extending this one should also be annotated with {@link ActionContextMarker}.
+ */
+ public interface ActionContext {
+ }
+
+ /**
+ * Returns the execution root. This is the directory underneath which Blaze builds its entire
+ * output working tree, including the source symlink forest. All build actions are executed
+ * relative to this directory.
+ */
+ Path getExecRoot();
+
+ /**
+ * Returns a clock. This is not hermetic, and should only be used for build info actions or
+ * performance measurements / reporting.
+ */
+ Clock getClock();
+
+ /**
+ * The EventBus for the current build.
+ */
+ EventBus getEventBus();
+
+ /**
+ * Returns whether failures should have verbose error messages.
+ */
+ boolean getVerboseFailures();
+
+ /**
+ * Returns the command line options of the Blaze command being executed.
+ */
+ OptionsClassProvider getOptions();
+
+ /**
+ * Whether this Executor reports subcommands. If not, reportSubcommand has no effect.
+ * This is provided so the caller of reportSubcommand can avoid wastefully constructing the
+ * subcommand string.
+ */
+ boolean reportsSubcommands();
+
+ /**
+ * Report a subcommand event to this Executor's Reporter and, if action
+ * logging is enabled, post it on its EventBus.
+ */
+ void reportSubcommand(String reason, String message);
+
+ /**
+ * An event listener to report messages to. Errors that signal a action failure should
+ * use ActionExecutionException.
+ */
+ EventHandler getEventHandler();
+
+ /**
+ * Looks up and returns an action context implementation of the given interface type.
+ */
+ <T extends ActionContext> T getContext(Class<? extends T> type);
+
+ /**
+ * Returns the action context implementation for spawn actions with a given mnemonic.
+ */
+ SpawnActionContext getSpawnActionContext(String mnemonic);
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/ExecutorInitException.java b/src/main/java/com/google/devtools/build/lib/actions/ExecutorInitException.java
new file mode 100644
index 0000000..21369fb
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/ExecutorInitException.java
@@ -0,0 +1,38 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+import com.google.devtools.build.lib.util.AbruptExitException;
+import com.google.devtools.build.lib.util.ExitCode;
+
+/**
+ * An exception that is thrown when an executor can't be initialized.
+ */
+public class ExecutorInitException extends AbruptExitException {
+
+ public ExecutorInitException(String message) {
+ this(message, ExitCode.LOCAL_ENVIRONMENTAL_ERROR);
+ }
+
+ public ExecutorInitException(String message, ExitCode exitCode) {
+ super(message, exitCode);
+ }
+
+ public ExecutorInitException(String message, Throwable cause) {
+ super(message + ": " + cause.getMessage(),
+ ExitCode.LOCAL_ENVIRONMENTAL_ERROR,
+ cause);
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/FailAction.java b/src/main/java/com/google/devtools/build/lib/actions/FailAction.java
new file mode 100644
index 0000000..6c15b31
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/FailAction.java
@@ -0,0 +1,73 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+
+/**
+ * FailAction is an Action that always fails to execute. (Used as scaffolding
+ * for rules we haven't yet implemented. Also useful for testing.)
+ */
+@ThreadSafe
+public final class FailAction extends AbstractAction {
+
+ private static final String GUID = "626cb78a-810f-4af3-979c-ee194955f04c";
+
+ private final String errorMessage;
+
+ public FailAction(ActionOwner owner, Iterable<Artifact> outputs, String errorMessage) {
+ super(owner, ImmutableList.<Artifact>of(), outputs);
+ this.errorMessage = errorMessage;
+ }
+
+ @Override
+ public Artifact getPrimaryInput() {
+ return null;
+ }
+
+ @Override
+ public void execute(
+ ActionExecutionContext actionExecutionContext)
+ throws ActionExecutionException {
+ throw new ActionExecutionException(errorMessage, this, false);
+ }
+
+ @Override
+ public ResourceSet estimateResourceConsumption(Executor executor) {
+ return ResourceSet.ZERO;
+ }
+
+ @Override
+ protected String computeKey() {
+ return GUID;
+ }
+
+ @Override
+ protected String getRawProgressMessage() {
+ return "Building unsupported rule " + getOwner().getLabel()
+ + " located at " + getOwner().getLocation();
+ }
+
+ @Override
+ public String describeStrategy(Executor executor) {
+ return "";
+ }
+
+ @Override
+ public String getMnemonic() {
+ return "Fail";
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/FilesetOutputSymlink.java b/src/main/java/com/google/devtools/build/lib/actions/FilesetOutputSymlink.java
new file mode 100644
index 0000000..1ae15ba
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/FilesetOutputSymlink.java
@@ -0,0 +1,81 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Objects;
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+/** Definition of a symlink in the output tree of a Fileset rule. */
+public final class FilesetOutputSymlink {
+ private static final String STRIPPED_METADATA = "<stripped-for-testing>";
+
+ /** Final name of the symlink relative to the Fileset's output directory. */
+ public final PathFragment name;
+
+ /** Target of the symlink. Depending on FilesetEntry.symlinks it may be relative or absolute. */
+ public final PathFragment target;
+
+ /** Opaque metadata about the link and its target; should change if either of them changes. */
+ public final String metadata;
+
+ @VisibleForTesting
+ public FilesetOutputSymlink(PathFragment name, PathFragment target) {
+ this.name = name;
+ this.target = target;
+ this.metadata = STRIPPED_METADATA;
+ }
+
+ /**
+ * @param name relative path under the Fileset's output directory, including FilesetEntry.destdir
+ * with and FilesetEntry.strip_prefix applied (if applicable)
+ * @param target relative or absolute value of the link
+ * @param metadata opaque metadata about the link and its target; should change if either the link
+ * or its target changes
+ */
+ public FilesetOutputSymlink(PathFragment name, PathFragment target, String metadata) {
+ this.name = Preconditions.checkNotNull(name);
+ this.target = Preconditions.checkNotNull(target);
+ this.metadata = Preconditions.checkNotNull(metadata);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || !obj.getClass().equals(getClass())) {
+ return false;
+ }
+ FilesetOutputSymlink o = (FilesetOutputSymlink) obj;
+ return name.equals(o.name) && target.equals(o.target) && metadata.equals(o.metadata);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(name, target, metadata);
+ }
+
+ @Override
+ public String toString() {
+ if (metadata.equals(STRIPPED_METADATA)) {
+ return String.format("FilesetOutputSymlink(%s -> %s)",
+ name.getPathString(), target.getPathString());
+ } else {
+ return String.format("FilesetOutputSymlink(%s -> %s | metadata=%s)",
+ name.getPathString(), target.getPathString(), metadata);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/FilesetTraversalParams.java b/src/main/java/com/google/devtools/build/lib/actions/FilesetTraversalParams.java
new file mode 100644
index 0000000..1464f73
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/FilesetTraversalParams.java
@@ -0,0 +1,165 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+import com.google.common.base.Optional;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.util.Fingerprint;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.lib.vfs.RootedPath;
+
+import java.util.Set;
+
+/**
+ * Parameters of a filesystem traversal requested by a Fileset rule.
+ *
+ * <p>This object stores the details of the traversal request, e.g. whether it's a direct or nested
+ * traversal (see {@link #getDirectTraversal()} and {@link #getNestedTraversal()}) or who the owner
+ * of the traversal is.
+ */
+public interface FilesetTraversalParams {
+
+ /**
+ * Abstraction of the root directory of a {@link DirectTraversal}.
+ *
+ * <ul>
+ * <li>The root of package traversals is the package directory, i.e. the parent of the BUILD file.
+ * <li>The root of "recursive" directory traversals is the directory's path.
+ * <li>The root of "file" traversals is the path of the file (or directory, or symlink) itself.
+ * </ul>
+ *
+ * <p>For the meaning of "recursive" and "file" traversals see {@link DirectTraversal}.
+ */
+ interface DirectTraversalRoot {
+
+ /**
+ * Returns the root part of the full path.
+ *
+ * <p>This is typically the workspace root or some output tree's root (e.g. genfiles, binfiles).
+ */
+ Path getRootPart();
+
+ /**
+ * Returns the {@link #getRootPart() root}-relative part of the path.
+ *
+ * <p>This is typically the source directory under the workspace or the output file under an
+ * output directory.
+ */
+ PathFragment getRelativePart();
+
+ /** Returns a {@link RootedPath} composed of the root and relative parts. */
+ RootedPath asRootedPath();
+ }
+
+ /**
+ * Describes a request for a direct filesystem traversal.
+ *
+ * <p>"Direct" means this corresponds to an actual filesystem traversal as opposed to traversing
+ * another Fileset rule, which is called a "nested" traversal.
+ *
+ * <p>Direct traversals can further be divided into two categories, "file" traversals and
+ * "recursive" traversals.
+ *
+ * <p>File traversal requests are created when the FilesetEntry.files attribute is defined; one
+ * file traversal request is created for each entry.
+ *
+ * <p>Recursive traversal requests are created when the FilesetEntry.files attribute is
+ * unspecified; one recursive traversal request is created for the FilesetEntry.srcdir.
+ *
+ * <p>See {@link DirectTraversal#getRoot()} for more details.
+ */
+ interface DirectTraversal {
+
+ /** Returns the root of the traversal; see {@link DirectTraversalRoot}. */
+ DirectTraversalRoot getRoot();
+
+ /**
+ * Returns true if this traversal refers to a whole package.
+ *
+ * <p>In that case the root (see {@link #getRoot()}) refers to the path of the package.
+ *
+ * <p>Package traversals are always recursive (see {@link #isRecursive()}) and are never
+ * generated (see {@link #isGenerated()}).
+ */
+ boolean isPackage();
+
+ /**
+ * Returns true if this is a "recursive traversal", i.e. created from FilesetEntry.srcdir.
+ *
+ * <p>This type of traversal is created when the FilesetEntry doesn't define a "files" list.
+ * When it does, the traversal is referred to as a "file traversal". When it doesn't, but the
+ * srcdir points to another Fileset, it is called a "nested" traversal.
+ *
+ * <p>Recursive traversals got their name from recursively traversing a directory structure.
+ * These are usually whole-package traversals, i.e. when FilesetEntry.srcdir refers to a BUILD
+ * file (see {@link #isPackage()}), but sometimes the srcdir references a input or output
+ * directory (the latter being generated by a local genrule) or a symlink (which must point to a
+ * directory; enforced during action execution).
+ *
+ * <p>The files in the results of a recursive traversal are all under the {@link #getRoot()
+ * root}. The root's path is stripped from the results.
+ *
+ * <p>N.B.: "file traversals" can also be recursive if the entry in FilesetEntry.files, for
+ * which the traversal parameters were created, turned out to be a directory. The difference
+ * lies in how the output paths are computed (with recursive traversals, the directory's name
+ * is stripped; with file traversals it is not, modulo usage of strip_prefix and the excludes
+ * attributes), and how directory symlinks are handled (in "recursive traversals" they are
+ * expanded just like normal directories, subsequent directory symlinks under them are *not*
+ * expanded though; they are not expanded at all in "file traversals").
+ */
+ boolean isRecursive();
+
+ /** Returns true if the root points to a generated file, symlink or directory. */
+ boolean isGenerated();
+
+ /** Returns true if input symlinks should be dereferenced; false if copied. */
+ boolean isFollowingSymlinks();
+
+ /** Returns the desired behavior when the traversal hits a subpackage. */
+ boolean getCrossPackageBoundary();
+ }
+
+ /** Label of the Fileset rule that owns this traversal. */
+ Label getOwnerLabel();
+
+ /** Returns the directory under the output path where the files will be mapped. May be empty. */
+ PathFragment getDestPath();
+
+ /** Returns a list of file basenames to be excluded from the output. May be empty. */
+ Set<String> getExcludedFiles();
+
+ /**
+ * Returns the parameters of the direct traversal request, if any.
+ *
+ * <p>A direct traversal is anything that's not a nested traversal, e.g. traversal of a package or
+ * directory (when FilesetEntry.srcdir is specified) or traversal of a single file (when
+ * FilesetEntry.files is specified). See {@link DirectTraversal} for more detail.
+ *
+ * <p>The value is present if and only if {@link #getNestedTraversal} is absent.
+ */
+ Optional<DirectTraversal> getDirectTraversal();
+
+ /**
+ * Returns the parameters of the nested traversal request, if any.
+ *
+ * <p>A nested traversal is the traversal of another Fileset referenced by FilesetEntry.srcdir.
+ *
+ * <p>The value is present if and only if {@link #getDirectTraversal} is absent.
+ */
+ Optional<FilesetTraversalParams> getNestedTraversal();
+
+ /** Adds the fingerprint of this traversal object. */
+ void fingerprint(Fingerprint fp);
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/FilesetTraversalParamsFactory.java b/src/main/java/com/google/devtools/build/lib/actions/FilesetTraversalParamsFactory.java
new file mode 100644
index 0000000..acc0e86
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/FilesetTraversalParamsFactory.java
@@ -0,0 +1,314 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+import com.google.common.base.Optional;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Ordering;
+import com.google.devtools.build.lib.actions.FilesetTraversalParams.DirectTraversal;
+import com.google.devtools.build.lib.actions.FilesetTraversalParams.DirectTraversalRoot;
+import com.google.devtools.build.lib.syntax.FilesetEntry.SymlinkBehavior;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.util.Fingerprint;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.lib.vfs.RootedPath;
+
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+/** Factory of {@link FilesetTraversalParams}. */
+public final class FilesetTraversalParamsFactory {
+
+ /**
+ * Creates parameters for a recursive traversal request in a package.
+ *
+ * <p>"Recursive" means that a directory is traversed along with all of its subdirectories. Such
+ * a traversal is created when FilesetEntry.files is unspecified.
+ *
+ * @param ownerLabel the rule that created this object
+ * @param buildFile path of the BUILD file of the package to traverse
+ * @param destPath path in the Fileset's output directory that will be the root of files found
+ * in this directory
+ * @param excludes optional; set of files directly under this package's directory to exclude;
+ * files in subdirectories cannot be excluded
+ * @param symlinkBehaviorMode what to do with symlinks
+ * @param crossPkgBoundary whether to traverse a subdirectory if it's also a subpackage (contains
+ * a BUILD file)
+ */
+ public static FilesetTraversalParams recursiveTraversalOfPackage(Label ownerLabel,
+ Artifact buildFile, PathFragment destPath, @Nullable Set<String> excludes,
+ SymlinkBehavior symlinkBehaviorMode, boolean crossPkgBoundary) {
+ Preconditions.checkState(buildFile.isSourceArtifact(), "%s", buildFile);
+ return new DirectoryTraversalParams(ownerLabel, DirectTraversalRootImpl.forPackage(buildFile),
+ true, destPath, excludes, symlinkBehaviorMode, crossPkgBoundary, true, false);
+ }
+
+ /**
+ * Creates parameters for a recursive traversal request in a directory.
+ *
+ * <p>"Recursive" means that a directory is traversed along with all of its subdirectories. Such
+ * a traversal is created when FilesetEntry.files is unspecified.
+ *
+ * @param ownerLabel the rule that created this object
+ * @param directoryToTraverse path of the directory to traverse
+ * @param destPath path in the Fileset's output directory that will be the root of files found
+ * in this directory
+ * @param excludes optional; set of files directly below this directory to exclude; files in
+ * subdirectories cannot be excluded
+ * @param symlinkBehaviorMode what to do with symlinks
+ * @param crossPkgBoundary whether to traverse a subdirectory if it's also a subpackage (contains
+ * a BUILD file)
+ */
+ public static FilesetTraversalParams recursiveTraversalOfDirectory(Label ownerLabel,
+ Artifact directoryToTraverse, PathFragment destPath, @Nullable Set<String> excludes,
+ SymlinkBehavior symlinkBehaviorMode, boolean crossPkgBoundary) {
+ return new DirectoryTraversalParams(ownerLabel,
+ DirectTraversalRootImpl.forFileOrDirectory(directoryToTraverse), false, destPath,
+ excludes, symlinkBehaviorMode, crossPkgBoundary, true,
+ !directoryToTraverse.isSourceArtifact());
+ }
+
+ /**
+ * Creates parameters for a file traversal request.
+ *
+ * <p>Such a traversal is created for every entry in FilesetEntry.files, when it is specified.
+ *
+ * @param ownerLabel the rule that created this object
+ * @param fileToTraverse the file to traverse; "traversal" means that if this file is actually a
+ * directory or a symlink to one then it'll be traversed as one
+ * @param destPath path in the Fileset's output directory that will be the name of this file's
+ * respective symlink there, or the root of files found (in case this is a directory)
+ * @param symlinkBehaviorMode what to do with symlinks
+ * @param crossPkgBoundary whether to traverse a subdirectory if it's also a subpackage (contains
+ * a BUILD file)
+ */
+ public static FilesetTraversalParams fileTraversal(Label ownerLabel, Artifact fileToTraverse,
+ PathFragment destPath, SymlinkBehavior symlinkBehaviorMode, boolean crossPkgBoundary) {
+ return new DirectoryTraversalParams(ownerLabel,
+ DirectTraversalRootImpl.forFileOrDirectory(fileToTraverse), false, destPath, null,
+ symlinkBehaviorMode, crossPkgBoundary, false, !fileToTraverse.isSourceArtifact());
+ }
+
+ /**
+ * Creates traversal request parameters for a FilesetEntry wrapping another Fileset.
+ *
+ * @param ownerLabel the rule that created this object
+ * @param nested the traversal params that were used for the nested (inner) Fileset
+ * @param destDir path in the Fileset's output directory that will be the root of files coming
+ * from the nested Fileset
+ * @param excludes optional; set of files directly below (not in a subdirectory of) the nested
+ * Fileset that should be excluded from the outer Fileset
+ */
+ public static FilesetTraversalParams nestedTraversal(Label ownerLabel,
+ FilesetTraversalParams nested, PathFragment destDir, @Nullable Set<String> excludes) {
+ // When srcdir is another Fileset, then files must be null so strip_prefix must also be null.
+ return new NestedTraversalParams(ownerLabel, nested, destDir, excludes);
+ }
+
+ private abstract static class ParamsCommon implements FilesetTraversalParams {
+ private final Label ownerLabel;
+ private final PathFragment destDir;
+ private final ImmutableSet<String> excludes;
+
+ ParamsCommon(Label ownerLabel, PathFragment destDir, @Nullable Set<String> excludes) {
+ this.ownerLabel = ownerLabel;
+ this.destDir = destDir;
+ if (excludes == null) {
+ this.excludes = ImmutableSet.<String>of();
+ } else {
+ // Order the set for the sake of deterministic fingerprinting.
+ this.excludes = ImmutableSet.copyOf(Ordering.natural().immutableSortedCopy(excludes));
+ }
+ }
+
+ @Override
+ public Label getOwnerLabel() {
+ return ownerLabel;
+ }
+
+ @Override
+ public Set<String> getExcludedFiles() {
+ return excludes;
+ }
+
+ @Override
+ public PathFragment getDestPath() {
+ return destDir;
+ }
+
+ protected final void commonFingerprint(Fingerprint fp) {
+ fp.addPath(destDir);
+ if (!excludes.isEmpty()) {
+ fp.addStrings(excludes);
+ }
+ }
+ }
+
+ private static final class DirectTraversalImpl implements DirectTraversal {
+ private final DirectTraversalRoot root;
+ private final boolean isPackage;
+ private final boolean followSymlinks;
+ private final boolean crossPkgBoundary;
+ private final boolean isRecursive;
+ private final boolean isGenerated;
+
+ DirectTraversalImpl(DirectTraversalRoot root, boolean isPackage, boolean followSymlinks,
+ boolean crossPkgBoundary, boolean isRecursive, boolean isGenerated) {
+ this.root = root;
+ this.isPackage = isPackage;
+ this.followSymlinks = followSymlinks;
+ this.crossPkgBoundary = crossPkgBoundary;
+ this.isRecursive = isRecursive;
+ this.isGenerated = isGenerated;
+ }
+
+ @Override
+ public DirectTraversalRoot getRoot() {
+ return root;
+ }
+
+ @Override
+ public boolean isPackage() {
+ return isPackage;
+ }
+
+ @Override
+ public boolean isRecursive() {
+ return isRecursive;
+ }
+
+ @Override
+ public boolean isGenerated() {
+ return isGenerated;
+ }
+
+ @Override
+ public boolean isFollowingSymlinks() {
+ return followSymlinks;
+ }
+
+ @Override
+ public boolean getCrossPackageBoundary() {
+ return crossPkgBoundary;
+ }
+
+ void fingerprint(Fingerprint fp) {
+ fp.addPath(root.asRootedPath().asPath());
+ fp.addBoolean(isPackage);
+ fp.addBoolean(followSymlinks);
+ fp.addBoolean(isRecursive);
+ fp.addBoolean(isGenerated);
+ fp.addBoolean(crossPkgBoundary);
+ }
+ }
+
+ private static final class DirectoryTraversalParams extends ParamsCommon {
+ private final DirectTraversalImpl traversal;
+
+ DirectoryTraversalParams(Label ownerLabel,
+ DirectTraversalRoot root,
+ boolean isPackage,
+ PathFragment destPath,
+ @Nullable Set<String> excludes,
+ SymlinkBehavior symlinkBehaviorMode,
+ boolean crossPkgBoundary,
+ boolean isRecursive,
+ boolean isGenerated) {
+ super(ownerLabel, destPath, excludes);
+ traversal = new DirectTraversalImpl(root, isPackage,
+ symlinkBehaviorMode == SymlinkBehavior.DEREFERENCE, crossPkgBoundary, isRecursive,
+ isGenerated);
+ }
+
+ @Override
+ public Optional<DirectTraversal> getDirectTraversal() {
+ return Optional.<DirectTraversal>of(traversal);
+ }
+
+ @Override
+ public Optional<FilesetTraversalParams> getNestedTraversal() {
+ return Optional.absent();
+ }
+
+ @Override
+ public void fingerprint(Fingerprint fp) {
+ commonFingerprint(fp);
+ traversal.fingerprint(fp);
+ }
+ }
+
+ private static final class NestedTraversalParams extends ParamsCommon {
+ private final FilesetTraversalParams nested;
+
+ public NestedTraversalParams(Label ownerLabel, FilesetTraversalParams nested,
+ PathFragment destDir, @Nullable Set<String> excludes) {
+ super(ownerLabel, destDir, excludes);
+ this.nested = nested;
+ }
+
+ @Override
+ public Optional<DirectTraversal> getDirectTraversal() {
+ return Optional.absent();
+ }
+
+ @Override
+ public Optional<FilesetTraversalParams> getNestedTraversal() {
+ return Optional.of(nested);
+ }
+
+ @Override
+ public void fingerprint(Fingerprint fp) {
+ commonFingerprint(fp);
+ nested.fingerprint(fp);
+ }
+ }
+
+ private static final class DirectTraversalRootImpl implements DirectTraversalRoot {
+ private final Path rootDir;
+ private final PathFragment relativeDir;
+
+ static DirectTraversalRoot forPackage(Artifact buildFile) {
+ return new DirectTraversalRootImpl(buildFile.getRoot().getPath(),
+ buildFile.getRootRelativePath().getParentDirectory());
+ }
+
+ static DirectTraversalRoot forFileOrDirectory(Artifact fileOrDirectory) {
+ return new DirectTraversalRootImpl(fileOrDirectory.getRoot().getPath(),
+ fileOrDirectory.getRootRelativePath());
+ }
+
+ private DirectTraversalRootImpl(Path rootDir, PathFragment relativeDir) {
+ this.rootDir = rootDir;
+ this.relativeDir = relativeDir;
+ }
+
+ @Override
+ public Path getRootPart() {
+ return rootDir;
+ }
+
+ @Override
+ public PathFragment getRelativePart() {
+ return relativeDir;
+ }
+
+ @Override
+ public RootedPath asRootedPath() {
+ return RootedPath.toRootedPath(rootDir, relativeDir);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/LocalHostCapacity.java b/src/main/java/com/google/devtools/build/lib/actions/LocalHostCapacity.java
new file mode 100644
index 0000000..5fc6013
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/LocalHostCapacity.java
@@ -0,0 +1,302 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Splitter;
+import com.google.common.io.Files;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadCompatible;
+import com.google.devtools.build.lib.util.BlazeClock;
+import com.google.devtools.build.lib.util.Clock;
+import com.google.devtools.build.lib.util.LoggingUtil;
+import com.google.devtools.build.lib.util.ProcMeminfoParser;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * This class estimates the local host's resource capacity.
+ */
+@ThreadCompatible
+public final class LocalHostCapacity {
+
+ private static final Logger LOG = Logger.getLogger(LocalHostCapacity.class.getName());
+
+ /**
+ * Stores parsed /proc/stat CPU time counters.
+ * See {@link LocalHostCapacity#getCpuTimes(String)} for details.
+ */
+ @Immutable
+ private final static class CpuTimes {
+ private final long idleJiffies;
+ private final long totalJiffies;
+
+ CpuTimes(long idleJiffies, long totalJiffies) {
+ this.idleJiffies = idleJiffies;
+ this.totalJiffies = totalJiffies;
+ }
+
+ /**
+ * Return idle CPU ratio using current and previous CPU readings or 0 if
+ * ratio is undefined.
+ */
+ double getIdleRatio(CpuTimes prevTimes) {
+ if (prevTimes.totalJiffies == 0 || totalJiffies == prevTimes.totalJiffies) {
+ return 0;
+ }
+ return ((double)(idleJiffies - prevTimes.idleJiffies) /
+ (double)(totalJiffies - prevTimes.totalJiffies));
+ }
+ }
+
+ /**
+ * Used to store available local CPU and RAM resources information.
+ * See {@link LocalHostCapacity#getFreeResources(FreeResources)} for details.
+ */
+ public static final class FreeResources {
+
+ private final Clock clock;
+ private final CpuTimes cpuTimes;
+ private final long lastTimestamp;
+ private final double freeCpu;
+ private final double freeMb;
+ private final long interval;
+
+ private FreeResources(Clock localClock, ProcMeminfoParser memInfo, String statContent,
+ FreeResources prevStats) {
+ clock = localClock;
+ lastTimestamp = localClock.nanoTime();
+ freeMb = ProcMeminfoParser.kbToMb(memInfo.getFreeRamKb());
+ cpuTimes = getCpuTimes(statContent);
+ if (prevStats == null) {
+ interval = 0;
+ freeCpu = 0.0;
+ } else {
+ interval = lastTimestamp - prevStats.lastTimestamp;
+ freeCpu = getLocalHostCapacity().getCpuUsage() * cpuTimes.getIdleRatio(prevStats.cpuTimes);
+ }
+ }
+
+ /**
+ * Returns amount of available RAM in MB.
+ */
+ public double getFreeMb() { return freeMb; }
+
+ /**
+ * Returns average available CPU resources (as a fraction of the CPU core,
+ * so one fully CPU-bound thread should consume exactly 1.0 CPU resource).
+ */
+ public double getAvgFreeCpu() { return freeCpu; }
+
+ /**
+ * Returns interval in ms between CPU load measurements used to calculate
+ * average available CPU resources.
+ */
+ public long getInterval() { return interval / 1000000; }
+
+ /**
+ * Returns age of available resource data in ms.
+ */
+ public long getReadingAge() {
+ return (clock.nanoTime() - lastTimestamp) / 1000000;
+ }
+ }
+
+ // Disables getFreeResources() if error occured during reading or parsing
+ // /proc/* information.
+ @VisibleForTesting
+ static boolean isDisabled;
+
+ // If /proc/* information is not available, assume 3000 MB and 2 CPUs.
+ private static ResourceSet DEFAULT_RESOURCES = new ResourceSet(3000.0, 2.0, 1.0);
+
+ private LocalHostCapacity() {}
+
+ /**
+ * Estimates of the local host's resource capacity,
+ * obtained by reading /proc/cpuinfo and /proc/meminfo.
+ */
+ private static ResourceSet localHostCapacity;
+
+ /**
+ * Estimates of the local host's resource capacity,
+ * obtained by reading /proc/cpuinfo and /proc/meminfo.
+ */
+ public static ResourceSet getLocalHostCapacity() {
+ if (localHostCapacity == null) {
+ localHostCapacity = getLocalHostCapacity("/proc/cpuinfo", "/proc/meminfo");
+ }
+ return localHostCapacity;
+ }
+
+ /**
+ * Returns new FreeResources object populated with free RAM information from
+ * /proc/meminfo and CPU load information from the /proc/stat. First call
+ * should be made with null parameter to instantiate new FreeResources object.
+ * Subsequent calls will use information inside it to calculate average CPU
+ * load over the time between calls and to calculate amount of free CPU
+ * resources and generate new FreeResources() instance.
+ *
+ * If information is not available due to error, functionality will be disabled
+ * and method will always return null.
+ */
+ public static FreeResources getFreeResources(FreeResources stats) {
+ return getFreeResources(BlazeClock.instance(), "/proc/meminfo", "/proc/stat", stats);
+ }
+
+ private static final Splitter NEWLINE_SPLITTER = Splitter.on('\n').omitEmptyStrings();
+
+ @VisibleForTesting
+ static int getLogicalCpuCount(String cpuinfoContent) {
+ Iterable<String> lines = NEWLINE_SPLITTER.split(cpuinfoContent);
+ int count = 0;
+ for (String line : lines) {
+ if(line.startsWith("processor")) {
+ count++;
+ }
+ }
+ if (count == 0) {
+ throw new IllegalArgumentException("Can't locate processor in the /proc/cpuinfo");
+ }
+ return count;
+ }
+
+ @VisibleForTesting
+ static int getPhysicalCpuCount(String cpuinfoContent, int logicalCpuCount) {
+ Iterable<String> lines = NEWLINE_SPLITTER.split(cpuinfoContent);
+ Set<String> uniq = new HashSet<>();
+ for (String line : lines) {
+ if(line.startsWith("physical id")) {
+ uniq.add(line);
+ }
+ }
+ int physicalCpuCount = uniq.size();
+ if (physicalCpuCount == 0) {
+ physicalCpuCount = logicalCpuCount;
+ }
+ return physicalCpuCount;
+ }
+
+ @VisibleForTesting
+ static int getCoresPerCpu(String cpuinfoFileContent) {
+ Iterable<String> lines = NEWLINE_SPLITTER.split(cpuinfoFileContent);
+ Set<String> uniq = new HashSet<>();
+ for (String line : lines) {
+ if(line.startsWith("core id")) {
+ uniq.add(line);
+ }
+ }
+ int coresPerCpu = uniq.size();
+ if (coresPerCpu == 0) {
+ coresPerCpu = 1;
+ }
+ return coresPerCpu;
+ }
+
+ /**
+ * Parses cpu line of the /proc/stats, calculates number of idle and total
+ * CPU jiffies and returns CpuTimes instance with that information.
+ *
+ * Total CPU time includes <b>all</b> time reported to be spent by the CPUs,
+ * including so-called "stolen" time - time spent by other VMs on the same
+ * workstation.
+ */
+ private static CpuTimes getCpuTimes(String statContent) {
+ String[] cpuStats = statContent.substring(0, statContent.indexOf('\n')).trim().split(" +");
+ // Supported versions of /proc/stat (Linux kernel 2.6.x) must contain either
+ // 9 or 10 fields:
+ // "cpu" utime ultime stime idle iowait irq softirq steal(since 2.6.11) 0
+ // We are interested in total time (sum of all columns) and idle time.
+ if (cpuStats.length < 9 | cpuStats.length > 10) {
+ throw new IllegalArgumentException("Unrecognized /proc/stat format");
+ }
+ if (!cpuStats[0].equals("cpu")) {
+ throw new IllegalArgumentException("/proc/stat does not start with cpu keyword");
+ }
+ long idleCpuJiffies = Long.parseLong(cpuStats[4]); // "idle" column.
+ long totalJiffies = 0;
+ for (int i = 1; i < cpuStats.length; i++) {
+ totalJiffies += Long.parseLong(cpuStats[i]);
+ }
+ long totalCpuJiffies = totalJiffies;
+ return new CpuTimes(idleCpuJiffies, totalCpuJiffies);
+ }
+
+ @VisibleForTesting
+ static ResourceSet getLocalHostCapacity(String cpuinfoFile, String meminfoFile) {
+ try {
+ String cpuinfoContent = readContent(cpuinfoFile);
+ ProcMeminfoParser memInfo = new ProcMeminfoParser(meminfoFile);
+ int logicalCpuCount = getLogicalCpuCount(cpuinfoContent);
+ int physicalCpuCount = getPhysicalCpuCount(cpuinfoContent, logicalCpuCount);
+ int coresPerCpu = getCoresPerCpu(cpuinfoContent);
+ int totalCores = coresPerCpu * physicalCpuCount;
+ boolean hyperthreading = (logicalCpuCount != totalCores);
+ double ramMb = ProcMeminfoParser.kbToMb(memInfo.getTotalKb());
+ final double EFFECTIVE_CPUS_PER_HYPERTHREADED_CPU = 0.6;
+ return new ResourceSet(
+ ramMb,
+ logicalCpuCount * (hyperthreading ? EFFECTIVE_CPUS_PER_HYPERTHREADED_CPU
+ : 1.0),
+ 1.0);
+ } catch (IOException | IllegalArgumentException e) {
+ disableProcFsUse(e);
+ return DEFAULT_RESOURCES;
+ }
+ }
+
+ @VisibleForTesting
+ static FreeResources getFreeResources(Clock localClock, String meminfoFile, String statFile,
+ FreeResources prevStats) {
+ if (isDisabled) { return null; }
+ try {
+ String statContent = readContent(statFile);
+ return new FreeResources(localClock, new ProcMeminfoParser(meminfoFile),
+ statContent, prevStats);
+ } catch (IOException | IllegalArgumentException e) {
+ disableProcFsUse(e);
+ return null;
+ }
+ }
+
+ /**
+ * For testing purposes only. Do not use it.
+ */
+ @VisibleForTesting
+ static void setLocalHostCapacity(ResourceSet resources) {
+ localHostCapacity = resources;
+ isDisabled = false;
+ }
+
+ private static String readContent(String filename) throws IOException {
+ return Files.toString(new File(filename), Charset.defaultCharset());
+ }
+
+ /**
+ * Disables use of /proc filesystem. Called internally when unexpected
+ * exception is caught.
+ */
+ private static void disableProcFsUse(Throwable cause) {
+ LoggingUtil.logToRemote(Level.WARNING, "Unable to read system load or capacity", cause);
+ LOG.log(Level.WARNING, "Unable to read system load or capacity", cause);
+ isDisabled = true;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/MapBasedActionGraph.java b/src/main/java/com/google/devtools/build/lib/actions/MapBasedActionGraph.java
new file mode 100644
index 0000000..2788f2f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/MapBasedActionGraph.java
@@ -0,0 +1,64 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+import com.google.common.base.Preconditions;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * An action graph that resolves generating actions by looking them up in a map.
+ */
+@ThreadSafe
+public final class MapBasedActionGraph implements MutableActionGraph {
+
+ private final ConcurrentMultimapWithHeadElement<Artifact, Action> generatingActionMap =
+ new ConcurrentMultimapWithHeadElement<Artifact, Action>();
+
+ @Override
+ @Nullable
+ public Action getGeneratingAction(Artifact artifact) {
+ return generatingActionMap.get(artifact);
+ }
+
+ @Override
+ public void registerAction(Action action) throws ActionConflictException {
+ for (Artifact artifact : action.getOutputs()) {
+ Action previousAction = generatingActionMap.putAndGet(artifact, action);
+ if (previousAction != null && previousAction != action
+ && !Actions.canBeShared(action, previousAction)) {
+ generatingActionMap.remove(artifact, action);
+ throw new ActionConflictException(artifact, previousAction, action);
+ }
+ }
+ }
+
+ @Override
+ public void unregisterAction(Action action) {
+ for (Artifact artifact : action.getOutputs()) {
+ generatingActionMap.remove(artifact, action);
+ Action otherAction = generatingActionMap.get(artifact);
+ Preconditions.checkState(otherAction == null
+ || (otherAction != action && Actions.canBeShared(action, otherAction)),
+ "%s %s", action, otherAction);
+ }
+ }
+
+ @Override
+ public void clear() {
+ generatingActionMap.clear();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/MiddlemanAction.java b/src/main/java/com/google/devtools/build/lib/actions/MiddlemanAction.java
new file mode 100644
index 0000000..9d3a2b8
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/MiddlemanAction.java
@@ -0,0 +1,107 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.syntax.Label;
+
+/**
+ * An action that depends on a set of inputs and creates a single output file whenever it
+ * runs. This is useful for bundling up a bunch of dependencies that are shared
+ * between individual targets in the action graph; for example generated header files.
+ */
+public class MiddlemanAction extends AbstractAction {
+
+ public static final String MIDDLEMAN_MNEMONIC = "Middleman";
+ private final String description;
+ private final MiddlemanType middlemanType;
+
+ /**
+ * Constructs a new {@link MiddlemanAction}.
+ *
+ * @param owner the owner of the action, usually a {@code ConfiguredTarget}
+ * @param inputs inputs of the middleman, i.e. the files it acts as a placeholder for
+ * @param stampFile the output of the middleman expansion; must be a middleman artifact (see
+ * {@link Artifact#isMiddlemanArtifact()})
+ * @param description a short description for the action, for progress messages
+ * @param middlemanType the type of the middleman
+ * @throws IllegalArgumentException if {@code stampFile} is not a middleman artifact
+ */
+ public MiddlemanAction(ActionOwner owner, Iterable<Artifact> inputs, Artifact stampFile,
+ String description, MiddlemanType middlemanType) {
+ super(owner, inputs, ImmutableList.of(stampFile));
+ Preconditions.checkNotNull(middlemanType);
+ Preconditions.checkArgument(stampFile.isMiddlemanArtifact(), stampFile);
+ this.description = description;
+ this.middlemanType = middlemanType;
+ }
+
+ @Override
+ public final void execute(
+ ActionExecutionContext actionExecutionContext) {
+ throw new IllegalStateException("MiddlemanAction should never be executed");
+ }
+
+ @Override
+ public ResourceSet estimateResourceConsumption(Executor executor) {
+ return ResourceSet.ZERO;
+ }
+
+ @Override
+ protected String computeKey() {
+ // TODO(bazel-team): Need to take middlemanType into account here.
+ // Only the set of inputs matters, and the dependency checker is
+ // responsible for considering those.
+ return "";
+ }
+
+ /**
+ * Returns the type of the middleman.
+ */
+ @Override
+ public MiddlemanType getActionType() {
+ return middlemanType;
+ }
+
+ @Override
+ protected String getRawProgressMessage() {
+ return null; // users don't really want to know about Middlemen.
+ }
+
+ @Override
+ public String prettyPrint() {
+ return description + " for " + Label.print(getOwner().getLabel());
+ }
+
+ @Override
+ public String describeStrategy(Executor executor) {
+ return "";
+ }
+
+ @Override
+ public String getMnemonic() {
+ return MIDDLEMAN_MNEMONIC;
+ }
+
+ /**
+ * Creates a new middleman action.
+ */
+ public static Action create(ActionRegistry env, ActionOwner owner,
+ Iterable<Artifact> inputs, Artifact stampFile, String purpose, MiddlemanType middlemanType) {
+ MiddlemanAction action = new MiddlemanAction(owner, inputs, stampFile, purpose, middlemanType);
+ env.registerAction(action);
+ return action;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/MiddlemanFactory.java b/src/main/java/com/google/devtools/build/lib/actions/MiddlemanFactory.java
new file mode 100644
index 0000000..85a1450
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/MiddlemanFactory.java
@@ -0,0 +1,188 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.Action.MiddlemanType;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.util.Pair;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.Iterator;
+
+/**
+ * A factory to create middleman objects.
+ */
+@ThreadSafe
+public final class MiddlemanFactory {
+
+ private final ArtifactFactory artifactFactory;
+ private final ActionRegistry actionRegistry;
+
+ public MiddlemanFactory(
+ ArtifactFactory artifactFactory, ActionRegistry actionRegistry) {
+ this.artifactFactory = Preconditions.checkNotNull(artifactFactory);
+ this.actionRegistry = Preconditions.checkNotNull(actionRegistry);
+ }
+
+ /**
+ * Creates a {@link MiddlemanType#AGGREGATING_MIDDLEMAN aggregating} middleman.
+ *
+ * @param owner the owner of the action that will be created; must not be null
+ * @param purpose the purpose for which this middleman is created. This should be a string which
+ * is suitable for use as a filename. A single rule may have many middlemen with distinct
+ * purposes.
+ * @param inputs the set of artifacts for which the created artifact is to be the middleman.
+ * @param middlemanDir the directory in which to place the middleman.
+ * @return null iff {@code inputs} is empty; the single element of {@code inputs} if there's only
+ * one; a new aggregating middleman for the {@code inputs} otherwise
+ */
+ public Artifact createAggregatingMiddleman(
+ ActionOwner owner, String purpose, Iterable<Artifact> inputs, Root middlemanDir) {
+ if (hasExactlyOneInput(inputs)) { // Optimization: No middleman for just one input.
+ return Iterables.getOnlyElement(inputs);
+ }
+ Pair<Artifact, Action> result = createMiddleman(
+ owner, Label.print(owner.getLabel()), purpose, inputs, middlemanDir,
+ MiddlemanType.AGGREGATING_MIDDLEMAN);
+ return result == null ? null : result.getFirst();
+ }
+
+ /**
+ * Returns <code>null</code> iff inputs is empty. Returns the sole element
+ * of inputs iff <code>inputs.size()==1</code>. Otherwise, returns a
+ * middleman artifact and creates a middleman action that generates that
+ * artifact.
+ *
+ * @param owner the owner of the action that will be created.
+ * @param owningArtifact the artifact of the file for which the runfiles
+ * should be created. There may be at most one set of runfiles for
+ * an owning artifact, unless the owning artifact is null. There
+ * may be at most one set of runfiles per owner with a null
+ * owning artifact.
+ * Further, if the owning Artifact is non-null, the owning Artifacts'
+ * root-relative path must be unique and the artifact must be part
+ * of the runfiles tree for which this middleman is created. Usually
+ * this artifact will be an executable program.
+ * @param inputs the set of artifacts for which the created artifact is to be
+ * the middleman.
+ * @param middlemanDir the directory in which to place the middleman.
+ */
+ public Artifact createRunfilesMiddleman(
+ ActionOwner owner, Artifact owningArtifact, Iterable<Artifact> inputs, Root middlemanDir) {
+ if (hasExactlyOneInput(inputs)) { // Optimization: No middleman for just one input.
+ return Iterables.getOnlyElement(inputs);
+ }
+ String middlemanPath = owningArtifact == null
+ ? Label.print(owner.getLabel())
+ : owningArtifact.getRootRelativePath().getPathString();
+ return createMiddleman(owner, middlemanPath, "runfiles", inputs, middlemanDir,
+ MiddlemanType.RUNFILES_MIDDLEMAN).getFirst();
+ }
+
+ private <T> boolean hasExactlyOneInput(Iterable<T> iterable) {
+ Iterator<T> it = iterable.iterator();
+ if (!it.hasNext()) {
+ return false;
+ }
+ it.next();
+ return !it.hasNext();
+ }
+
+ /**
+ * Creates a {@link MiddlemanType#ERROR_PROPAGATING_MIDDLEMAN error-propagating} middleman.
+ *
+ * @param owner the owner of the action that will be created. May not be null.
+ * @param middlemanName a unique file name for the middleman artifact in the {@code middlemanDir};
+ * in practice this is usually the owning rule's label (so it gets escaped as such)
+ * @param purpose the purpose for which this middleman is created. This should be a string which
+ * is suitable for use as a filename. A single rule may have many middlemen with distinct
+ * purposes.
+ * @param inputs the set of artifacts for which the created artifact is to be the middleman; must
+ * not be null or empty
+ * @param middlemanDir the directory in which to place the middleman.
+ * @return a middleman that enforces scheduling order (just like a scheduling middleman) and
+ * propagates errors, but is ignored by the dependency checker
+ * @throws IllegalArgumentException if {@code inputs} is null or empty
+ */
+ public Artifact createErrorPropagatingMiddleman(ActionOwner owner, String middlemanName,
+ String purpose, Iterable<Artifact> inputs, Root middlemanDir) {
+ Preconditions.checkArgument(inputs != null);
+ Preconditions.checkArgument(!Iterables.isEmpty(inputs));
+ // We must always create this middleman even if there is only one input.
+ return createMiddleman(owner, middlemanName, purpose, inputs, middlemanDir,
+ MiddlemanType.ERROR_PROPAGATING_MIDDLEMAN).getFirst();
+ }
+
+ /**
+ * Returns the same artifact as {@code createErrorPropagatingMiddleman} would return,
+ * but doesn't create any action.
+ */
+ public Artifact getErrorPropagatingMiddlemanArtifact(String middlemanName, String purpose,
+ Root middlemanDir) {
+ return getStampFileArtifact(middlemanName, purpose, middlemanDir);
+ }
+
+ /**
+ * Creates both normal and scheduling middlemen.
+ *
+ * <p>Note: there's no need to synchronize this method; the only use of a field is via a call to
+ * another synchronized method (getArtifact()).
+ *
+ * @return null iff {@code inputs} is null or empty; the middleman file and the middleman action
+ * otherwise
+ */
+ private Pair<Artifact, Action> createMiddleman(
+ ActionOwner owner, String middlemanName, String purpose, Iterable<Artifact> inputs,
+ Root middlemanDir, MiddlemanType middlemanType) {
+ if (inputs == null || Iterables.isEmpty(inputs)) {
+ return null;
+ }
+
+ Artifact stampFile = getStampFileArtifact(middlemanName, purpose, middlemanDir);
+ Action action = new MiddlemanAction(owner, inputs, stampFile, purpose, middlemanType);
+ actionRegistry.registerAction(action);
+ return Pair.of(stampFile, action);
+ }
+
+ /**
+ * Creates a normal middleman.
+ *
+ * <p>If called multiple times, it always returns the same object depending on the {@code
+ * purpose}. It does not check that the list of inputs is identical. In contrast to other
+ * middleman methods, this one also returns an object if the list of inputs is empty.
+ *
+ * <p>Note: there's no need to synchronize this method; the only use of a field is via a call to
+ * another synchronized method (getArtifact()).
+ */
+ public Artifact createMiddlemanAllowMultiple(ActionRegistry registry,
+ ActionOwner owner, String purpose, Iterable<Artifact> inputs, Root middlemanDir) {
+ PathFragment stampName = new PathFragment("_middlemen/" + purpose);
+ Artifact stampFile = artifactFactory.getDerivedArtifact(stampName, middlemanDir,
+ actionRegistry.getOwner());
+ MiddlemanAction.create(
+ registry, owner, inputs, stampFile, purpose, MiddlemanType.AGGREGATING_MIDDLEMAN);
+ return stampFile;
+ }
+
+ private Artifact getStampFileArtifact(String middlemanName, String purpose, Root middlemanDir) {
+ String escapedFilename = Actions.escapedPath(middlemanName);
+ PathFragment stampName = new PathFragment("_middlemen/" + escapedFilename + "-" + purpose);
+ Artifact stampFile = artifactFactory.getDerivedArtifact(stampName, middlemanDir,
+ actionRegistry.getOwner());
+ return stampFile;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/MissingInputFileException.java b/src/main/java/com/google/devtools/build/lib/actions/MissingInputFileException.java
new file mode 100644
index 0000000..52f9f27
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/MissingInputFileException.java
@@ -0,0 +1,42 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+import com.google.devtools.build.lib.events.Location;
+
+/**
+ * This exception is thrown during a build when an input file is missing, but the file
+ * is not the input to any action being executed.
+ *
+ * If a missing input file is an input
+ * to an action, an {@link ActionExecutionException} is thrown instead.
+ */
+public class MissingInputFileException extends BuildFailedException {
+ private final Location location;
+
+ public MissingInputFileException(String message, Location location) {
+ super(message);
+ this.location = location;
+ }
+
+ /**
+ * Return a location where this input file is referenced. If there
+ * are multiple such locations, one is chosen arbitrarily. If there
+ * are none, return null.
+ */
+ public Location getLocation() {
+ return location;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/MutableActionGraph.java b/src/main/java/com/google/devtools/build/lib/actions/MutableActionGraph.java
new file mode 100644
index 0000000..8b84c31
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/MutableActionGraph.java
@@ -0,0 +1,151 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+import com.google.common.collect.Sets.SetView;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.util.StringUtil;
+
+import java.util.Set;
+
+/**
+ * A mutable action graph. Implementations of this interface must be thread-safe.
+ */
+public interface MutableActionGraph extends ActionGraph {
+
+ /**
+ * Attempts to register the action. If any of the action's outputs already has a generating
+ * action, and the two actions are not compatible, then an {@link ActionConflictException} is
+ * thrown. The internal data structure may be partially modified when that happens; it is not
+ * guaranteed that all potential conflicts are detected, but at least one of them is.
+ *
+ * <p>For example, take three actions A, B, and C, where A creates outputs a and b, B creates just
+ * b, and C creates c and b. There are two potential conflicts in this case, between A and B, and
+ * between B and C. Depending on the ordering of calls to this method and the ordering of outputs
+ * in the action output lists, either one or two conflicts are detected: if B is registered first,
+ * then both conflicts are detected; if either A or C is registered first, then only one conflict
+ * is detected.
+ */
+ void registerAction(Action action) throws ActionConflictException;
+
+ /**
+ * Removes an action from this action graph if it is present.
+ *
+ * <p>Throws {@link IllegalStateException} if one of the outputs of the action is in fact
+ * generated by a different {@link Action} instance (even if they are sharable).
+ */
+ void unregisterAction(Action action);
+
+ /**
+ * Clear the action graph.
+ */
+ void clear();
+
+ /**
+ * This exception is thrown when a conflict between actions is detected. It contains information
+ * about the artifact for which the conflict is found, and data about the two conflicting actions
+ * and their owners.
+ */
+ public static final class ActionConflictException extends Exception {
+
+ private final Artifact artifact;
+ private final Action previousAction;
+ private final Action attemptedAction;
+
+ public ActionConflictException(Artifact artifact, Action previousAction,
+ Action attemptedAction) {
+ super("for " + artifact);
+ this.artifact = artifact;
+ this.previousAction = previousAction;
+ this.attemptedAction = attemptedAction;
+ }
+
+ public Artifact getArtifact() {
+ return artifact;
+ }
+
+ public void reportTo(EventHandler eventListener) {
+ String msg = "file '" + artifact.prettyPrint()
+ + "' is generated by these conflicting actions:\n" +
+ suffix(attemptedAction, previousAction);
+ eventListener.handle(Event.error(msg));
+ }
+
+ private void addStringDetail(StringBuilder sb, String key, String valueA, String valueB) {
+ valueA = valueA != null ? valueA : "(null)";
+ valueB = valueB != null ? valueB : "(null)";
+
+ sb.append(key).append(": ").append(valueA);
+ if (!valueA.equals(valueB)) {
+ sb.append(", ").append(valueB);
+ }
+ sb.append("\n");
+ }
+
+ private void addListDetail(StringBuilder sb, String key,
+ Iterable<Artifact> valueA, Iterable<Artifact> valueB) {
+ Set<Artifact> setA = ImmutableSet.copyOf(valueA);
+ Set<Artifact> setB = ImmutableSet.copyOf(valueB);
+ SetView<Artifact> diffA = Sets.difference(setA, setB);
+ SetView<Artifact> diffB = Sets.difference(setB, setA);
+
+ sb.append(key).append(": ");
+ if (diffA.isEmpty() && diffB.isEmpty()) {
+ sb.append("are equal");
+ } else {
+ if (!diffA.isEmpty() && !diffB.isEmpty()) {
+ sb.append("attempted action contains artifacts not in previous action and "
+ + "previous action contains artifacts not in attempted action.");
+ } else if (!diffA.isEmpty()) {
+ sb.append("attempted action contains artifacts not in previous action: ");
+ sb.append(StringUtil.joinEnglishList(diffA, "and"));
+ } else if (!diffB.isEmpty()) {
+ sb.append("previous action contains artifacts not in attempted action: ");
+ sb.append(StringUtil.joinEnglishList(diffB, "and"));
+ }
+ }
+ sb.append("\n");
+ }
+
+ // See also Actions.canBeShared()
+ private String suffix(Action a, Action b) {
+ // Note: the error message reveals to users the names of intermediate files that are not
+ // documented in the BUILD language. This error-reporting logic is rather elaborate but it
+ // does help to diagnose some tricky situations.
+ StringBuilder sb = new StringBuilder();
+ ActionOwner aOwner = a.getOwner();
+ ActionOwner bOwner = b.getOwner();
+ boolean aNull = aOwner == null;
+ boolean bNull = bOwner == null;
+
+ addStringDetail(sb, "Label", aNull ? null : Label.print(aOwner.getLabel()),
+ bNull ? null : Label.print(bOwner.getLabel()));
+ addStringDetail(sb, "RuleClass", aNull ? null : aOwner.getTargetKind(),
+ bNull ? null : bOwner.getTargetKind());
+ addStringDetail(sb, "Configuration", aNull ? null : aOwner.getConfigurationName(),
+ bNull ? null : bOwner.getConfigurationName());
+ addStringDetail(sb, "Mnemonic", a.getMnemonic(), b.getMnemonic());
+
+ addListDetail(sb, "MandatoryInputs", a.getMandatoryInputs(), b.getMandatoryInputs());
+ addListDetail(sb, "Outputs", a.getOutputs(), b.getOutputs());
+
+ return sb.toString();
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/NotifyOnActionCacheHit.java b/src/main/java/com/google/devtools/build/lib/actions/NotifyOnActionCacheHit.java
new file mode 100644
index 0000000..fa9b54e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/NotifyOnActionCacheHit.java
@@ -0,0 +1,30 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+/**
+ * An action which must know when it is skipped due to an action cache hit.
+ *
+ * Use should be rare, as the action graph is a functional model.
+ */
+public interface NotifyOnActionCacheHit extends Action {
+
+ /**
+ * Called when action has "cache hit", and therefore need not be executed.
+ *
+ * @param executor the executor
+ */
+ void actionCacheHit(Executor executor);
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/PackageRootResolver.java b/src/main/java/com/google/devtools/build/lib/actions/PackageRootResolver.java
new file mode 100644
index 0000000..90af136
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/PackageRootResolver.java
@@ -0,0 +1,34 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.Map;
+
+import javax.annotation.Nullable;
+
+/**
+ * Represents logic that evaluates the root of the package containing path.
+ */
+public interface PackageRootResolver {
+
+ /**
+ * Returns mapping from execPath to Root. Some roots can equal null if the corresponding
+ * package can't be found. Returns null if for some reason we can't evaluate it.
+ */
+ @Nullable
+ Map<PathFragment, Root> findPackageRoots(Iterable<PathFragment> execPaths);
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/ParameterFile.java b/src/main/java/com/google/devtools/build/lib/actions/ParameterFile.java
new file mode 100644
index 0000000..80df9e2
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/ParameterFile.java
@@ -0,0 +1,114 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.util.FileType;
+import com.google.devtools.build.lib.util.ShellEscaper;
+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.nio.charset.Charset;
+import java.util.List;
+
+/**
+ * Support for parameter file generation (as used by gcc and other tools, e.g.
+ * {@code gcc @param_file}. Note that the parameter file needs to be explicitly
+ * deleted after use. Different tools require different parameter file formats,
+ * which can be selected via the {@link ParameterFileType} enum.
+ *
+ * <p>The default charset is ISO-8859-1 (latin1). This also has to match the
+ * expectation of the tool.
+ *
+ * <p>Don't use this class for new code. Use the ParameterFileWriteAction
+ * instead!
+ */
+public class ParameterFile {
+
+ /**
+ * Different styles of parameter files.
+ */
+ public static enum ParameterFileType {
+ /**
+ * A parameter file with every parameter on a separate line. This format
+ * cannot handle newlines in parameters. It is currently used for most
+ * tools, but may not be interpreted correctly if parameters contain
+ * white space or other special characters. It should be avoided for new
+ * development.
+ */
+ UNQUOTED,
+
+ /**
+ * A parameter file where each parameter is correctly quoted for shell
+ * use, and separated by white space (space, tab, newline). This format is
+ * safe for all characters, but must be specially supported by the tool. In
+ * particular, it must not be used with gcc and related tools, which do not
+ * support this format as it is.
+ */
+ SHELL_QUOTED;
+ }
+
+ // Parameter file location.
+ private final Path execRoot;
+ private final PathFragment execPath;
+ private final Charset charset;
+ private final ParameterFileType type;
+
+ @VisibleForTesting
+ public static final FileType PARAMETER_FILE = FileType.of(".params");
+
+ /**
+ * Creates a parameter file with the given parameters.
+ */
+ public ParameterFile(Path execRoot, PathFragment execPath, Charset charset,
+ ParameterFileType type) {
+ Preconditions.checkNotNull(type);
+ this.execRoot = execRoot;
+ this.execPath = execPath;
+ this.charset = Preconditions.checkNotNull(charset);
+ this.type = Preconditions.checkNotNull(type);
+ }
+
+ /**
+ * Derives an exec path from a given exec path by appending <code>".params"</code>.
+ */
+ public static PathFragment derivePath(PathFragment original) {
+ return original.replaceName(original.getBaseName() + "-2.params");
+ }
+
+ /**
+ * Returns the path for the parameter file.
+ */
+ public Path getPath() {
+ return execRoot.getRelative(execPath);
+ }
+
+ /**
+ * Writes the arguments from the list into the parameter file according to
+ * the style selected in the constructor.
+ */
+ public void writeContent(List<String> arguments) throws ExecException {
+ Iterable<String> actualArgs = (type == ParameterFileType.SHELL_QUOTED) ?
+ ShellEscaper.escapeAll(arguments) : arguments;
+ Path file = getPath();
+ try {
+ FileSystemUtils.writeLinesAs(file, charset, actualArgs);
+ } catch (IOException e) {
+ throw new EnvironmentalExecException("could not write param file '" + file + "'", e);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/ResourceManager.java b/src/main/java/com/google/devtools/build/lib/actions/ResourceManager.java
new file mode 100644
index 0000000..929a106
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/ResourceManager.java
@@ -0,0 +1,472 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.eventbus.EventBus;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.profiler.Profiler;
+import com.google.devtools.build.lib.profiler.ProfilerTask;
+import com.google.devtools.build.lib.util.LoggingUtil;
+import com.google.devtools.build.lib.util.Pair;
+
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.concurrent.CountDownLatch;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * CPU/RAM resource manager. Used to keep track of resources consumed by the Blaze action execution
+ * threads and throttle them when necessary.
+ *
+ * <p>Threads which are known to consume a significant amount of the local CPU or RAM resources
+ * should call {@link #acquireResources} method. This method will check whether requested resources
+ * are available and will either mark them as used and allow thread to proceed or will block the
+ * thread until requested resources will become available. When thread completes it task, it must
+ * release allocated resources by calling {@link #releaseResources} method.
+ *
+ * <p>Available resources can be calculated using one of three ways:
+ * <ol>
+ * <li>They can be preset using {@link #setAvailableResources(ResourceSet)} method. This is used
+ * mainly by the unit tests (however it is possible to provide a future option that would
+ * artificially limit amount of CPU/RAM consumed by the Blaze).
+ * <li>They can be preset based on the /proc/cpuinfo and /proc/meminfo information. Blaze will
+ * calculate amount of available CPU cores (adjusting for hyperthreading logical cores) and
+ * amount of the total available memory and will limit itself to the number of effective cores
+ * and 2/3 of the available memory. For details, please look at the {@link
+ * LocalHostCapacity#getLocalHostCapacity} method.
+ * <li>Blaze will periodically (every 3 seconds) poll {@code /proc/meminfo} and {@code /proc/stat}
+ * information to obtain how much RAM and CPU resources are currently idle at that moment. For
+ * calculation details, please look at the {@link LocalHostCapacity#getFreeResources}
+ * implementation.
+ * </ol>
+ *
+ * <p>The resource manager also allows a slight overallocation of the resources to account for the
+ * fact that requested resources are usually estimated using a pessimistic approximation. It also
+ * guarantees that at least one thread will always be able to acquire any amount of requested
+ * resources (even if it is greater than amount of available resources). Therefore, assuming that
+ * threads correctly release acquired resources, Blaze will never be fully blocked.
+ */
+@ThreadSafe
+public class ResourceManager {
+
+ private static final Logger LOG = Logger.getLogger(ResourceManager.class.getName());
+ private final boolean FINE;
+
+ private EventBus eventBus;
+
+ private final ThreadLocal<Boolean> threadLocked = new ThreadLocal<Boolean>() {
+ @Override
+ protected Boolean initialValue() {
+ return false;
+ }
+ };
+
+ /**
+ * Singleton reference defined in a separate class to ensure thread-safe lazy
+ * initialization.
+ */
+ private static class Singleton {
+ static ResourceManager instance = new ResourceManager();
+ }
+
+ /**
+ * Returns singleton instance of the resource manager.
+ */
+ public static ResourceManager instance() {
+ return Singleton.instance;
+ }
+
+ // Allocated resources are allowed to go "negative", but at least
+ // MIN_AVAILABLE_CPU_RATIO portion of CPU and MIN_AVAILABLE_RAM_RATIO portion
+ // of RAM should be available.
+ // Please note that this value is purely empirical - we assume that generally
+ // requested resources are somewhat pessimistic and thread would end up
+ // using less than requested amount.
+ private final static double MIN_NECESSARY_CPU_RATIO = 0.6;
+ private final static double MIN_NECESSARY_RAM_RATIO = 1.0;
+ private final static double MIN_NECESSARY_IO_RATIO = 1.0;
+
+ // List of blocked threads. Associated CountDownLatch object will always
+ // be initialized to 1 during creation in the acquire() method.
+ private final List<Pair<ResourceSet, CountDownLatch>> requestList;
+
+ // The total amount of resources on the local host. Must be set by
+ // an explicit call to setAvailableResources(), often using
+ // LocalHostCapacity.getLocalHostCapacity() as an argument.
+ private ResourceSet staticResources = null;
+
+ private ResourceSet availableResources = null;
+ private LocalHostCapacity.FreeResources freeReading = null;
+
+ // Used amount of CPU capacity (where 1.0 corresponds to the one fully
+ // occupied CPU core. Corresponds to the CPU resource definition in the
+ // ResourceSet class.
+ private double usedCpu;
+
+ // Used amount of RAM capacity in MB. Corresponds to the RAM resource
+ // definition in the ResourceSet class.
+ private double usedRam;
+
+ // Used amount of I/O resources. Corresponds to the I/O resource
+ // definition in the ResourceSet class.
+ private double usedIo;
+
+ // Specifies how much of the RAM in staticResources we should allow to be used.
+ public static final int DEFAULT_RAM_UTILIZATION_PERCENTAGE = 67;
+ private int ramUtilizationPercentage = DEFAULT_RAM_UTILIZATION_PERCENTAGE;
+
+ // Timer responsible for the periodic polling of the current system load.
+ private Timer timer = null;
+
+ private ResourceManager() {
+ FINE = LOG.isLoggable(Level.FINE);
+ requestList = new LinkedList<Pair<ResourceSet, CountDownLatch>>();
+ }
+
+ @VisibleForTesting public static ResourceManager instanceForTestingOnly() {
+ return new ResourceManager();
+ }
+
+ /**
+ * Resets resource manager state and releases all thread locks.
+ * Note - it does not reset auto-sensing or available resources. Use
+ * separate call to setAvailableResoures() or to setAutoSensing().
+ */
+ public synchronized void resetResourceUsage() {
+ usedCpu = 0;
+ usedRam = 0;
+ usedIo = 0;
+ for (Pair<ResourceSet, CountDownLatch> request : requestList) {
+ // CountDownLatch can be set only to 0 or 1.
+ request.second.countDown();
+ }
+ requestList.clear();
+ }
+
+ /**
+ * Sets available resources using given resource set. Must be called
+ * at least once before using resource manager.
+ * <p>
+ * Method will also disable auto-sensing if it was enabled.
+ */
+ public synchronized void setAvailableResources(ResourceSet resources) {
+ Preconditions.checkNotNull(resources);
+ staticResources = resources;
+ setAutoSensing(false);
+ }
+
+ public synchronized boolean isAutoSensingEnabled() {
+ return timer != null;
+ }
+
+ /**
+ * Specify how much of the available RAM we should allow to be used.
+ * This has no effect if autosensing is enabled.
+ */
+ public synchronized void setRamUtilizationPercentage(int percentage) {
+ ramUtilizationPercentage = percentage;
+ }
+
+ /**
+ * Enables or disables secondary resource allocation algorithm that will
+ * periodically (when needed but at most once per 3 seconds) checks real
+ * amount of available memory (based on /proc/meminfo) and current CPU load
+ * (based on 1 second difference of /proc/stat) and allows additional resource
+ * acquisition if previous requests were overly pessimistic.
+ */
+ public synchronized void setAutoSensing(boolean enable) {
+ // Create new Timer instance only if it does not exist already.
+ if (enable && !isAutoSensingEnabled()) {
+ Profiler.instance().logEvent(ProfilerTask.INFO, "Enable auto sensing");
+ if(refreshFreeResources()) {
+ timer = new Timer("AutoSenseTimer", true);
+ timer.schedule(new TimerTask() {
+ @Override public void run() { refreshFreeResources(); }
+ }, 3000, 3000);
+ }
+ } else if (!enable) {
+ if (isAutoSensingEnabled()) {
+ Profiler.instance().logEvent(ProfilerTask.INFO, "Disable auto sensing");
+ timer.cancel();
+ timer = null;
+ }
+ if (staticResources != null) {
+ updateAvailableResources(false);
+ }
+ }
+ }
+
+ /**
+ * Acquires requested resource set. Will block if resource is not available.
+ * NB! This method must be thread-safe!
+ */
+ public void acquireResources(ActionMetadata owner, ResourceSet resources)
+ throws InterruptedException {
+ Preconditions.checkArgument(resources != null);
+ long startTime = Profiler.nanoTimeMaybe();
+ CountDownLatch latch = null;
+ try {
+ waiting(owner);
+ latch = acquire(resources);
+ if (latch != null) {
+ latch.await();
+ }
+ } finally {
+ threadLocked.set(resources.getCpuUsage() != 0 || resources.getMemoryMb() != 0
+ || resources.getIoUsage() != 0);
+ acquired(owner);
+
+ // Profile acquisition only if it waited for resource to become available.
+ if (latch != null) {
+ Profiler.instance().logSimpleTask(startTime, ProfilerTask.ACTION_LOCK, owner);
+ }
+ }
+ }
+
+ /**
+ * Acquires the given resources if available immediately. Does not block.
+ * @return true iff the given resources were locked (all or nothing).
+ */
+ public boolean tryAcquire(ActionMetadata owner, ResourceSet resources) {
+ boolean acquired = false;
+ synchronized (this) {
+ if (areResourcesAvailable(resources)) {
+ incrementResources(resources);
+ acquired = true;
+ }
+ }
+
+ if (acquired) {
+ threadLocked.set(resources.getCpuUsage() != 0 || resources.getMemoryMb() != 0);
+ acquired(owner);
+ }
+
+ return acquired;
+ }
+
+ private void incrementResources(ResourceSet resources) {
+ usedCpu += resources.getCpuUsage();
+ usedRam += resources.getMemoryMb();
+ usedIo += resources.getIoUsage();
+ }
+
+ /**
+ * Return true if any resources have been claimed through this manager.
+ */
+ public synchronized boolean inUse() {
+ return usedCpu != 0.0 || usedRam != 0.0 || usedIo != 0.0 || requestList.size() > 0;
+ }
+
+
+ /**
+ * Return true iff this thread has a lock on non-zero resources.
+ */
+ public boolean threadHasResources() {
+ return threadLocked.get();
+ }
+
+ public void setEventBus(EventBus eventBus) {
+ Preconditions.checkState(this.eventBus == null);
+ this.eventBus = Preconditions.checkNotNull(eventBus);
+ }
+
+ public void unsetEventBus() {
+ Preconditions.checkState(this.eventBus != null);
+ this.eventBus = null;
+ }
+
+ private void waiting(ActionMetadata owner) {
+ if (eventBus != null) {
+ // Null only in tests.
+ eventBus.post(ActionStatusMessage.schedulingStrategy(owner));
+ }
+ }
+
+ private void acquired(ActionMetadata owner) {
+ if (eventBus != null) {
+ // Null only in tests.
+ eventBus.post(ActionStatusMessage.runningStrategy(owner));
+ }
+ }
+
+ /**
+ * Releases previously requested resource set.
+ *
+ * <p>NB! This method must be thread-safe!
+ */
+ public void releaseResources(ActionMetadata owner, ResourceSet resources) {
+ boolean isConflict = false;
+ long startTime = Profiler.nanoTimeMaybe();
+ try {
+ isConflict = release(resources);
+ } finally {
+ threadLocked.set(false);
+
+ // Profile resource release only if it resolved at least one allocation request.
+ if (isConflict) {
+ Profiler.instance().logSimpleTask(startTime, ProfilerTask.ACTION_RELEASE, owner);
+ }
+ }
+ }
+
+ private synchronized CountDownLatch acquire(ResourceSet resources) {
+ if (areResourcesAvailable(resources)) {
+ incrementResources(resources);
+ return null;
+ }
+ Pair<ResourceSet, CountDownLatch> request =
+ new Pair<>(resources, new CountDownLatch(1));
+ requestList.add(request);
+
+ // If we use auto sensing and there has not been an update within last
+ // 30 seconds, something has gone really wrong - disable it.
+ if (isAutoSensingEnabled() && freeReading.getReadingAge() > 30000) {
+ LoggingUtil.logToRemote(Level.WARNING, "Free resource readings were " +
+ "not updated for 30 seconds - auto-sensing is disabled",
+ new IllegalStateException());
+ LOG.warning("Free resource readings were not updated for 30 seconds - "
+ + "auto-sensing is disabled");
+ setAutoSensing(false);
+ }
+ return request.second;
+ }
+
+ private synchronized boolean release(ResourceSet resources) {
+ usedCpu -= resources.getCpuUsage();
+ usedRam -= resources.getMemoryMb();
+ usedIo -= resources.getIoUsage();
+
+ // TODO(bazel-team): (2010) rounding error can accumulate and value below can end up being
+ // e.g. 1E-15. So if it is small enough, we set it to 0. But maybe there is a better solution.
+ if (usedCpu < 0.0001) {
+ usedCpu = 0;
+ }
+ if (usedRam < 0.0001) {
+ usedRam = 0;
+ }
+ if (usedIo < 0.0001) {
+ usedIo = 0;
+ }
+ if (requestList.size() > 0) {
+ processWaitingThreads();
+ return true;
+ }
+ return false;
+ }
+
+
+ /**
+ * Tries to unblock one or more waiting threads if there are sufficient resources available.
+ */
+ private synchronized void processWaitingThreads() {
+ Iterator<Pair<ResourceSet, CountDownLatch>> iterator = requestList.iterator();
+ while (iterator.hasNext()) {
+ Pair<ResourceSet, CountDownLatch> request = iterator.next();
+ if (areResourcesAvailable(request.first)) {
+ incrementResources(request.first);
+ request.second.countDown();
+ iterator.remove();
+ }
+ }
+ }
+
+ // Method will return true if all requested resources are considered to be available.
+ private boolean areResourcesAvailable(ResourceSet resources) {
+ Preconditions.checkNotNull(availableResources);
+ // Comparison below is robust, since any calculation errors will be fixed
+ // by the release() method.
+ if (usedCpu == 0.0 && usedRam == 0.0 && usedIo == 0.0) {
+ return true;
+ }
+ // Use only MIN_NECESSARY_???_RATIO of the resource value to check for
+ // allocation. This is necessary to account for the fact that most of the
+ // requested resource sets use pessimistic estimations. Note that this
+ // ratio is used only during comparison - for tracking we will actually
+ // mark whole requested amount as used.
+ double cpu = resources.getCpuUsage() * MIN_NECESSARY_CPU_RATIO;
+ double ram = resources.getMemoryMb() * MIN_NECESSARY_RAM_RATIO;
+ double io = resources.getIoUsage() * MIN_NECESSARY_IO_RATIO;
+
+ double availableCpu = availableResources.getCpuUsage();
+ double availableRam = availableResources.getMemoryMb();
+ double availableIo = availableResources.getIoUsage();
+
+ // Resources are considered available if any one of the conditions below is true:
+ // 1) If resource is not requested at all, it is available.
+ // 2) If resource is not used at the moment, it is considered to be
+ // available regardless of how much is requested. This is necessary to
+ // ensure that at any given time, at least one thread is able to acquire
+ // resources even if it requests more than available.
+ // 3) If used resource amount is less than total available resource amount.
+ return (cpu == 0.0 || usedCpu == 0.0 || usedCpu + cpu <= availableCpu) &&
+ (ram == 0.0 || usedRam == 0.0 || usedRam + ram <= availableRam) &&
+ (io == 0.0 || usedIo == 0.0 || usedIo + io <= availableIo);
+ }
+
+ private synchronized void updateAvailableResources(boolean useFreeReading) {
+ Preconditions.checkNotNull(staticResources);
+ if (useFreeReading && isAutoSensingEnabled()) {
+ availableResources = new ResourceSet(
+ usedRam + freeReading.getFreeMb(),
+ usedCpu + freeReading.getAvgFreeCpu(),
+ staticResources.getIoUsage());
+ if(FINE) {
+ LOG.fine("Free resources: " + Math.round(freeReading.getFreeMb()) + " MB,"
+ + Math.round(freeReading.getAvgFreeCpu() * 100) + "% CPU");
+ }
+ processWaitingThreads();
+ } else {
+ availableResources = new ResourceSet(
+ staticResources.getMemoryMb() * this.ramUtilizationPercentage / 100.0,
+ staticResources.getCpuUsage(),
+ staticResources.getIoUsage());
+ processWaitingThreads();
+ }
+ }
+
+ /**
+ * Called by the timer thread to update system load information.
+ *
+ * @return true if update was successful and false if error was detected and
+ * autosensing was disabled.
+ */
+ private boolean refreshFreeResources() {
+ freeReading = LocalHostCapacity.getFreeResources(freeReading);
+ if (freeReading == null) { // Unable to read or parse /proc/* information.
+ LOG.warning("Unable to obtain system load - autosensing is disabled");
+ setAutoSensing(false);
+ return false;
+ }
+ updateAvailableResources(
+ freeReading.getInterval() >= 1000 && freeReading.getInterval() <= 10000);
+ return true;
+ }
+
+ @VisibleForTesting
+ synchronized int getWaitCount() {
+ return requestList.size();
+ }
+
+ @VisibleForTesting
+ synchronized boolean isAvailable(double ram, double cpu, double io) {
+ return areResourcesAvailable(new ResourceSet(ram, cpu, io));
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/ResourceSet.java b/src/main/java/com/google/devtools/build/lib/actions/ResourceSet.java
new file mode 100644
index 0000000..e7ab98f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/ResourceSet.java
@@ -0,0 +1,113 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+import com.google.common.base.Splitter;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.common.options.Converter;
+import com.google.devtools.common.options.OptionsParsingException;
+
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+/**
+ * Instances of this class represent an estimate of the resource consumption
+ * for a particular Action, or the total available resources. We plan to
+ * use this to do smarter scheduling of actions, for example making sure
+ * that we don't schedule jobs concurrently if they would use so much
+ * memory as to cause the machine to thrash.
+ */
+@Immutable
+public class ResourceSet {
+
+ /** For actions that consume negligible resources. */
+ public static final ResourceSet ZERO = new ResourceSet(0.0, 0.0, 0.0);
+
+ /** The amount of real memory (resident set size). */
+ private final double memoryMb;
+
+ /** The number of CPUs, or fractions thereof. */
+ private final double cpuUsage;
+
+ /**
+ * Relative amount of used I/O resources (with 1.0 being total available amount on an "average"
+ * workstation.
+ */
+ private final double ioUsage;
+
+ public ResourceSet(double memoryMb, double cpuUsage, double ioUsage) {
+ this.memoryMb = memoryMb;
+ this.cpuUsage = cpuUsage;
+ this.ioUsage = ioUsage;
+ }
+
+ /** Returns the amount of real memory (resident set size) used in MB. */
+ public double getMemoryMb() {
+ return memoryMb;
+ }
+
+ /**
+ * Returns the number of CPUs (or fractions thereof) used.
+ * For a CPU-bound single-threaded process, this will be 1.0.
+ * For a single-threaded process which spends part of its
+ * time waiting for I/O, this will be somewhere between 0.0 and 1.0.
+ * For a multi-threaded or multi-process application,
+ * this may be more than 1.0.
+ */
+ public double getCpuUsage() {
+ return cpuUsage;
+ }
+
+ /**
+ * Returns the amount of I/O used.
+ * Full amount of available I/O resources on the "average" workstation is
+ * considered to be 1.0.
+ */
+ public double getIoUsage() {
+ return ioUsage;
+ }
+
+ public static class ResourceSetConverter implements Converter<ResourceSet> {
+ private static final Splitter SPLITTER = Splitter.on(',');
+
+ @Override
+ public ResourceSet convert(String input) throws OptionsParsingException {
+ Iterator<String> values = SPLITTER.split(input).iterator();
+ try {
+ double memoryMb = Double.parseDouble(values.next());
+ double cpuUsage = Double.parseDouble(values.next());
+ double ioUsage = Double.parseDouble(values.next());
+ if (values.hasNext()) {
+ throw new OptionsParsingException("Expected exactly 3 comma-separated float values");
+ }
+ if (memoryMb <= 0.0 || cpuUsage <= 0.0 || ioUsage <= 0.0) {
+ throw new OptionsParsingException("All resource values must be positive");
+ }
+ return new ResourceSet(memoryMb, cpuUsage, ioUsage);
+ } catch (NumberFormatException nfe) {
+ throw new OptionsParsingException("Expected exactly 3 comma-separated float values", nfe);
+ } catch (NoSuchElementException nsee) {
+ throw new OptionsParsingException("Expected exactly 3 comma-separated float values", nsee);
+ }
+ }
+
+ @Override
+ public String getTypeDescription() {
+ return "comma-separated available amount of RAM (in MB), CPU (in cores) and "
+ + "available I/O (1.0 being average workstation)";
+ }
+
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/Root.java b/src/main/java/com/google/devtools/build/lib/actions/Root.java
new file mode 100644
index 0000000..284b85f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/Root.java
@@ -0,0 +1,163 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.syntax.SkylarkCallable;
+import com.google.devtools.build.lib.syntax.SkylarkModule;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.io.Serializable;
+import java.util.Objects;
+
+import javax.annotation.Nullable;
+
+/**
+ * A root for an artifact. The roots are the directories containing artifacts, and they are mapped
+ * together into a single directory tree to form the execution environment. There are two kinds of
+ * roots, source roots and derived roots. Source roots correspond to entries of the package path,
+ * and they can be anywhere on disk. Derived roots correspond to output directories; there are
+ * generally different output directories for different configurations, and different types of
+ * output (bin, genfiles, includes, etc.).
+ *
+ * <p>When mapping the roots into a single directory tree, the source roots are merged, such that
+ * each package is accessed in its entirety from a single source root. The package cache is
+ * responsible for determining that mapping. The derived roots, on the other hand, have to be
+ * distinct. (It is currently allowed to have a derived root that is the prefix of another one.)
+ *
+ * <p>The derived roots must have paths that point inside the exec root, i.e. below the directory
+ * that is the root of the merged directory tree.
+ */
+@SkylarkModule(name = "root",
+ doc = "A root for files. The roots are the directories containing files, and they are mapped "
+ + "together into a single directory tree to form the execution environment.")
+public final class Root implements Comparable<Root>, Serializable {
+
+ /**
+ * Returns the given path as a source root. The path may not be {@code null}.
+ */
+ public static Root asSourceRoot(Path path) {
+ return new Root(null, path);
+ }
+
+ /**
+ * DO NOT USE IN PRODUCTION CODE!
+ *
+ * <p>Returns the given path as a derived root. This method only exists as a convenience for
+ * tests, which don't need a proper Root object.
+ */
+ @VisibleForTesting
+ public static Root asDerivedRoot(Path path) {
+ return new Root(path, path);
+ }
+
+ /**
+ * Returns the given path as a derived root, relative to the given exec root. The root must be a
+ * proper sub-directory of the exec root (i.e. not equal). Neither may be {@code null}.
+ *
+ * <p>Be careful with this method - all derived roots must be registered with the artifact factory
+ * before the analysis phase.
+ */
+ public static Root asDerivedRoot(Path execRoot, Path root) {
+ Preconditions.checkArgument(root.startsWith(execRoot));
+ Preconditions.checkArgument(!root.equals(execRoot));
+ return new Root(execRoot, root);
+ }
+
+ public static Root middlemanRoot(Path execRoot, Path outputDir) {
+ Path root = outputDir.getRelative("internal");
+ Preconditions.checkArgument(root.startsWith(execRoot));
+ Preconditions.checkArgument(!root.equals(execRoot));
+ return new Root(execRoot, root, true);
+ }
+
+ /**
+ * Returns the exec root as a derived root. The exec root should never be treated as a derived
+ * root, but this is currently allowed. Do not add any further uses besides the ones that already
+ * exist!
+ */
+ static Root execRootAsDerivedRoot(Path execRoot) {
+ return new Root(execRoot, execRoot);
+ }
+
+ @Nullable private final Path execRoot;
+ private final Path path;
+ private final boolean isMiddlemanRoot;
+
+ private Root(@Nullable Path execRoot, Path path, boolean isMiddlemanRoot) {
+ this.execRoot = execRoot;
+ this.path = Preconditions.checkNotNull(path);
+ this.isMiddlemanRoot = isMiddlemanRoot;
+ }
+
+ private Root(@Nullable Path execRoot, Path path) {
+ this(execRoot, path, false);
+ }
+
+ public Path getPath() {
+ return path;
+ }
+
+ /**
+ * Returns the path fragment from the exec root to the actual root. For source roots, this returns
+ * the empty fragment.
+ */
+ public PathFragment getExecPath() {
+ return isSourceRoot() ? PathFragment.EMPTY_FRAGMENT : path.relativeTo(execRoot);
+ }
+
+ @SkylarkCallable(name = "path", structField = true,
+ doc = "Returns the relative path from the exec root to the actual root.")
+ public String getExecPathString() {
+ return getExecPath().getPathString();
+ }
+
+ public boolean isSourceRoot() {
+ return execRoot == null;
+ }
+
+ public boolean isMiddlemanRoot() {
+ return isMiddlemanRoot;
+ }
+
+ @Override
+ public int compareTo(Root o) {
+ return path.compareTo(o.path);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(execRoot, path.hashCode());
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) {
+ return true;
+ }
+ if (!(o instanceof Root)) {
+ return false;
+ }
+ Root r = (Root) o;
+ return path.equals(r.path) && Objects.equals(execRoot, r.execRoot);
+ }
+
+ @Override
+ public String toString() {
+ return path.toString() + (isSourceRoot() ? "[source]" : "[derived]");
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/Spawn.java b/src/main/java/com/google/devtools/build/lib/actions/Spawn.java
new file mode 100644
index 0000000..2f905d0
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/Spawn.java
@@ -0,0 +1,122 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.actions.extra.SpawnInfo;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.Collection;
+
+/**
+ * An object representing a subprocess to be invoked, including its command and
+ * arguments, its working directory, its environment, a boolean indicating
+ * whether remote execution is appropriate for this command, and if so, the set
+ * of files it is expected to read and write.
+ */
+public interface Spawn {
+
+ /**
+ * Returns true iff this command may be executed remotely.
+ */
+ boolean isRemotable();
+
+ /**
+ * Out-of-band data for this spawn. This can be used to signal hints (hardware requirements,
+ * local vs. remote) to the execution subsystem.
+ *
+ * <p>String tags from {@link
+ * com.google.devtools.build.lib.rules.test.TestTargetProperties#getExecutionInfo()} can be added
+ * as keys with arbitrary values to this map too.
+ */
+ ImmutableMap<String, String> getExecutionInfo();
+
+ /**
+ * Returns this Spawn as a Bourne shell command.
+ *
+ * @param workingDir the initial working directory of the command
+ */
+ String asShellCommand(Path workingDir);
+
+ /**
+ * Returns the runfiles data for remote execution. Format is (directory, manifest file).
+ */
+ ImmutableMap<PathFragment, Artifact> getRunfilesManifests();
+
+ /**
+ * Returns artifacts for filesets, so they can be scheduled on remote execution.
+ */
+ ImmutableList<Artifact> getFilesetManifests();
+
+ /**
+ * Returns a protocol buffer describing this spawn for use by the extra_action functionality.
+ */
+ SpawnInfo getExtraActionInfo();
+
+ /**
+ * Returns the command (the first element) and its arguments.
+ */
+ ImmutableList<String> getArguments();
+
+ /**
+ * Returns the initial environment of the process.
+ * If null, the environment is inherited from the parent process.
+ */
+ ImmutableMap<String, String> getEnvironment();
+
+ /**
+ * Returns the list of files that this command may read.
+ *
+ * <p>This method explicitly does not expand middleman artifacts. Pass the result
+ * to an appropriate utility method on {@link com.google.devtools.build.lib.actions.Artifact} to
+ * expand the middlemen.
+ *
+ * <p>This is for use with remote execution, so we can ship inputs before starting the
+ * command. Order stability across multiple calls should be upheld for performance reasons.
+ */
+ Iterable<? extends ActionInput> getInputFiles();
+
+ /**
+ * Returns the collection of files that this command must write. Callers should not mutate
+ * the result.
+ *
+ * <p>This is for use with remote execution, so remote execution does not have to guess what
+ * outputs the process writes. While the order does not affect the semantics, it should be
+ * stable so it can be cached.
+ */
+ Collection<? extends ActionInput> getOutputFiles();
+
+ /**
+ * Returns the resource owner for local fallback.
+ */
+ ActionMetadata getResourceOwner();
+
+ /**
+ * Returns the amount of resources needed for local fallback.
+ */
+ ResourceSet getLocalResources();
+
+ /**
+ * Returns the owner for this action. Production code should supply a non-null owner.
+ */
+ ActionOwner getOwner();
+
+ /**
+ * Returns a mnemonic (string constant) for this kind of spawn.
+ */
+ String getMnemonic();
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/SpawnActionContext.java b/src/main/java/com/google/devtools/build/lib/actions/SpawnActionContext.java
new file mode 100644
index 0000000..c2ea1b0
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/SpawnActionContext.java
@@ -0,0 +1,42 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+
+/**
+ * A context that allows execution of {@link Spawn} instances.
+ */
+@ActionContextMarker(name = "spawn")
+public interface SpawnActionContext extends Executor.ActionContext {
+
+ /**
+ * Executes the given spawn.
+ */
+ void exec(Spawn spawn, ActionExecutionContext actionExecutionContext)
+ throws ExecException, InterruptedException;
+
+ /** Returns the locality of running the spawn, i.e., "local". */
+ String strategyLocality(String mnemonic, boolean remotable);
+
+ /**
+ * This implements a tri-state mode. There are three possible cases: (1) implementations of this
+ * class can unconditionally execute spawns locally, (2) they can follow whatever is set for the
+ * corresponding spawn (see {@link Spawn#isRemotable}), or (3) they can unconditionally execute
+ * spawns remotely, i.e., force remote execution.
+ *
+ * <p>Passing the spawns remotable flag to this method returns whether the spawn will actually be
+ * executed remotely.
+ */
+ boolean isRemotable(String mnemonic, boolean remotable);
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/TargetOutOfDateException.java b/src/main/java/com/google/devtools/build/lib/actions/TargetOutOfDateException.java
new file mode 100644
index 0000000..9092ab2
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/TargetOutOfDateException.java
@@ -0,0 +1,25 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+/**
+ * An exception indicating that a target is out of date.
+ */
+public class TargetOutOfDateException extends ActionExecutionException {
+
+ public TargetOutOfDateException(Action action) {
+ super (action.prettyPrint() + " is not up-to-date", action, false);
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/TestExecException.java b/src/main/java/com/google/devtools/build/lib/actions/TestExecException.java
new file mode 100644
index 0000000..c861338
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/TestExecException.java
@@ -0,0 +1,35 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+/**
+ * An TestExecException that is related to the failure of a TestAction.
+ */
+public final class TestExecException extends ExecException {
+
+ public TestExecException(String message) {
+ super(message);
+ }
+
+ @Override
+ public ActionExecutionException toActionExecutionException(String messagePrefix,
+ boolean verboseFailures, Action action) {
+ String message = messagePrefix + " failed" + getMessage();
+ if (verboseFailures) {
+ return new ActionExecutionException(message, this, action, isCatastrophic());
+ } else {
+ return new ActionExecutionException(message, action, isCatastrophic());
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/TestMiddlemanObserver.java b/src/main/java/com/google/devtools/build/lib/actions/TestMiddlemanObserver.java
new file mode 100644
index 0000000..128ac20
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/TestMiddlemanObserver.java
@@ -0,0 +1,30 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+/**
+ * Used as a notification mechanism for the scheduling middleman mutations
+ * related to scheduling exclusive tests.
+ */
+public interface TestMiddlemanObserver {
+
+ /**
+ * Called when the test removes the stale middleman.
+ *
+ * @param action the test action.
+ * @param middleman the scheduling middleman.
+ * @param middlemanAction the action generating the scheduling middleman
+ */
+ void remove(Action action, Artifact middleman, Action middlemanAction);
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/UserExecException.java b/src/main/java/com/google/devtools/build/lib/actions/UserExecException.java
new file mode 100644
index 0000000..86a6eb0
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/UserExecException.java
@@ -0,0 +1,41 @@
+// Copyright 2014 Google Inc. 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.actions;
+
+/**
+ * An ExecException that is related to the failure of an Action and therefore
+ * very likely the user's fault.
+ */
+public class UserExecException extends ExecException {
+
+ public UserExecException(String message) {
+ super(message);
+ }
+
+ public UserExecException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ @Override
+ public ActionExecutionException toActionExecutionException(String messagePrefix,
+ boolean verboseFailures, Action action) {
+ String message = messagePrefix + " failed: " + getMessage();
+ if (verboseFailures) {
+ return new ActionExecutionException(message, this, action, isCatastrophic());
+ } else {
+ return new ActionExecutionException(message, action, isCatastrophic());
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/cache/ActionCache.java b/src/main/java/com/google/devtools/build/lib/actions/cache/ActionCache.java
new file mode 100644
index 0000000..2ac0ab8
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/cache/ActionCache.java
@@ -0,0 +1,173 @@
+// Copyright 2014 Google Inc. 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.actions.cache;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Lists;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadCompatible;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.io.IOException;
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * An interface defining a cache of already-executed Actions.
+ *
+ * <p>This class' naming is misleading; it doesn't cache the actual actions, but it stores a
+ * fingerprint of the action state (ie. a hash of the input and output files on disk), so
+ * we can tell if we need to rerun an action given the state of the file system.
+ *
+ * <p>Each action entry uses one of its output paths as a key (after conversion
+ * to the string).
+ */
+@ThreadCompatible
+public interface ActionCache {
+
+ /**
+ * Updates the cache entry for the specified key.
+ */
+ void put(String key, ActionCache.Entry entry);
+
+ /**
+ * Returns the corresponding cache entry for the specified key, if any, or
+ * null if not found.
+ */
+ ActionCache.Entry get(String key);
+
+ /**
+ * Removes entry from cache
+ */
+ void remove(String key);
+
+ /**
+ * Returns a new Entry instance. This method allows ActionCache subclasses to
+ * define their own Entry implementation.
+ */
+ ActionCache.Entry createEntry(String key);
+
+ /**
+ * An entry in the ActionCache that contains all action input and output
+ * artifact paths and their metadata plus action key itself.
+ *
+ * Cache entry operates under assumption that once it is fully initialized
+ * and getFileDigest() method is called, it becomes logically immutable (all methods
+ * will continue to return same result regardless of internal data transformations).
+ */
+ public final class Entry {
+ private final String actionKey;
+ private final List<String> files;
+ // If null, digest is non-null and the entry is immutable.
+ private Map<String, Metadata> mdMap;
+ private Digest digest;
+
+ public Entry(String key) {
+ actionKey = key;
+ files = new ArrayList<>();
+ mdMap = new HashMap<>();
+ }
+
+ public Entry(String key, List<String> files, Digest digest) {
+ actionKey = key;
+ this.files = files;
+ this.digest = digest;
+ mdMap = null;
+ }
+
+ /**
+ * Adds the artifact, specified by the executable relative path and its
+ * metadata into the cache entry.
+ */
+ public void addFile(PathFragment relativePath, Metadata md) {
+ Preconditions.checkState(mdMap != null);
+ Preconditions.checkState(!isCorrupted());
+ Preconditions.checkState(digest == null);
+
+ String execPath = relativePath.getPathString();
+ files.add(execPath);
+ mdMap.put(execPath, md);
+ }
+
+ /**
+ * @return action key string.
+ */
+ public String getActionKey() {
+ return actionKey;
+ }
+
+ /**
+ * Returns the combined digest of the action's inputs and outputs.
+ *
+ * This may compresses the data into a more compact representation, and
+ * makes the object immutable.
+ */
+ public Digest getFileDigest() {
+ if (digest == null) {
+ digest = Digest.fromMetadata(mdMap);
+ mdMap = null;
+ }
+ return digest;
+ }
+
+ /**
+ * Returns true if this cache entry is corrupted and should be ignored.
+ */
+ public boolean isCorrupted() {
+ return actionKey == null;
+ }
+
+ /**
+ * @return stored path strings.
+ */
+ public Collection<String> getPaths() {
+ return files;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append(" actionKey = ").append(actionKey).append("\n");
+ builder.append(" digestKey = ");
+ if (digest == null) {
+ builder.append(Digest.fromMetadata(mdMap)).append(" (from mdMap)\n");
+ } else {
+ builder.append(digest).append("\n");
+ }
+ List<String> fileInfo = Lists.newArrayListWithCapacity(files.size());
+ fileInfo.addAll(files);
+ Collections.sort(fileInfo);
+ for (String info : fileInfo) {
+ builder.append(" ").append(info).append("\n");
+ }
+ return builder.toString();
+ }
+ }
+
+ /**
+ * Give persistent cache implementations a notification to write to disk.
+ * @return size in bytes of the serialized cache.
+ */
+ long save() throws IOException;
+
+ /**
+ * Dumps action cache content into the given PrintStream.
+ */
+ void dump(PrintStream out);
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/cache/CompactPersistentActionCache.java b/src/main/java/com/google/devtools/build/lib/actions/cache/CompactPersistentActionCache.java
new file mode 100644
index 0000000..24eb42e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/cache/CompactPersistentActionCache.java
@@ -0,0 +1,389 @@
+// Copyright 2014 Google Inc. 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.actions.cache;
+
+import static java.nio.charset.StandardCharsets.ISO_8859_1;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ConditionallyThreadSafe;
+import com.google.devtools.build.lib.util.Clock;
+import com.google.devtools.build.lib.util.CompactStringIndexer;
+import com.google.devtools.build.lib.util.PersistentMap;
+import com.google.devtools.build.lib.util.StringIndexer;
+import com.google.devtools.build.lib.util.VarInt;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.UnixGlob;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * An implementation of the ActionCache interface that uses
+ * {@link CompactStringIndexer} to reduce memory footprint and saves
+ * cached actions using the {@link PersistentMap}.
+ *
+ * <p>This cache is not fully correct: as hashes are xor'd together, a permutation of input
+ * file contents will erroneously be considered up to date.
+ */
+@ConditionallyThreadSafe // condition: each instance must instantiated with
+ // different cache root
+public class CompactPersistentActionCache implements ActionCache {
+ private static final int SAVE_INTERVAL_SECONDS = 3;
+ private static final long NANOS_PER_SECOND = 1000 * 1000 * 1000;
+
+ // Key of the action cache record that holds information used to verify referential integrity
+ // between action cache and string indexer. Must be < 0 to avoid conflict with real action
+ // cache records.
+ private static final int VALIDATION_KEY = -10;
+
+ private static final int VERSION = 10;
+
+ private final class ActionMap extends PersistentMap<Integer, byte[]> {
+ private final Clock clock;
+ private long nextUpdate;
+
+ public ActionMap(Map<Integer, byte[]> map, Clock clock, Path mapFile, Path journalFile)
+ throws IOException {
+ super(VERSION, map, mapFile, journalFile);
+ this.clock = clock;
+ // Using nanoTime. currentTimeMillis may not provide enough granularity.
+ nextUpdate = clock.nanoTime() / NANOS_PER_SECOND + SAVE_INTERVAL_SECONDS;
+ load();
+ }
+
+ @Override
+ protected boolean updateJournal() {
+ // Using nanoTime. currentTimeMillis may not provide enough granularity.
+ long time = clock.nanoTime() / NANOS_PER_SECOND;
+ if (SAVE_INTERVAL_SECONDS == 0 || time > nextUpdate) {
+ nextUpdate = time + SAVE_INTERVAL_SECONDS;
+ // Force flushing of the PersistentStringIndexer instance. This is needed to ensure
+ // that filename index data on disk is always up-to-date when we save action cache
+ // data.
+ indexer.flush();
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ protected boolean keepJournal() {
+ // We must first flush the journal to get an accurate measure of its size.
+ forceFlush();
+ try {
+ return journalSize() * 100 < cacheSize();
+ } catch (IOException e) {
+ return false;
+ }
+ }
+
+ @Override
+ protected Integer readKey(DataInputStream in) throws IOException {
+ return in.readInt();
+ }
+
+ @Override
+ protected byte[] readValue(DataInputStream in)
+ throws IOException {
+ int size = in.readInt();
+ if (size < 0) {
+ throw new IOException("found negative array size: " + size);
+ }
+ byte[] data = new byte[size];
+ in.readFully(data);
+ return data;
+ }
+
+ @Override
+ protected void writeKey(Integer key, DataOutputStream out)
+ throws IOException {
+ out.writeInt(key);
+ }
+
+ @Override
+ // TODO(bazel-team): (2010) This method, writeKey() and related Metadata methods
+ // should really use protocol messages. Doing so would allow easy inspection
+ // of the action cache content and, more importantly, would cut down on the
+ // need to change VERSION to different number every time we touch those
+ // methods. Especially when we'll start to add stuff like statistics for
+ // each action.
+ protected void writeValue(byte[] value, DataOutputStream out)
+ throws IOException {
+ out.writeInt(value.length);
+ out.write(value);
+ }
+ }
+
+ private final PersistentMap<Integer, byte[]> map;
+ private final PersistentStringIndexer indexer;
+ static final ActionCache.Entry CORRUPTED = new ActionCache.Entry(null);
+
+ public CompactPersistentActionCache(Path cacheRoot, Clock clock) throws IOException {
+ Path cacheFile = cacheFile(cacheRoot);
+ Path journalFile = journalFile(cacheRoot);
+ Path indexFile = cacheRoot.getChild("filename_index_v" + VERSION + ".blaze");
+ // we can now use normal hash map as backing map, since dependency checker
+ // will manually purge records from the action cache.
+ Map<Integer, byte[]> backingMap = new HashMap<>();
+
+ try {
+ indexer = PersistentStringIndexer.newPersistentStringIndexer(indexFile, clock);
+ } catch (IOException e) {
+ renameCorruptedFiles(cacheRoot);
+ throw new IOException("Failed to load filename index data", e);
+ }
+
+ try {
+ map = new ActionMap(backingMap, clock, cacheFile, journalFile);
+ } catch (IOException e) {
+ renameCorruptedFiles(cacheRoot);
+ throw new IOException("Failed to load action cache data", e);
+ }
+
+ // Validate referential integrity between two collections.
+ if (!map.isEmpty()) {
+ String integrityError = validateIntegrity(indexer.size(), map.get(VALIDATION_KEY));
+ if (integrityError != null) {
+ renameCorruptedFiles(cacheRoot);
+ throw new IOException("Failed action cache referential integrity check: " + integrityError);
+ }
+ }
+ }
+
+ /**
+ * Rename corrupted files so they could be analyzed later. This would also ensure
+ * that next initialization attempt will create empty cache.
+ */
+ private static void renameCorruptedFiles(Path cacheRoot) {
+ try {
+ for (Path path : UnixGlob.forPath(cacheRoot).addPattern("action_*_v" + VERSION + ".*")
+ .glob()) {
+ path.renameTo(path.getParentDirectory().getChild(path.getBaseName() + ".bad"));
+ }
+ for (Path path : UnixGlob.forPath(cacheRoot).addPattern("filename_*_v" + VERSION + ".*")
+ .glob()) {
+ path.renameTo(path.getParentDirectory().getChild(path.getBaseName() + ".bad"));
+ }
+ } catch (IOException e) {
+ // do nothing
+ }
+ }
+
+ /**
+ * @return false iff indexer contains no data or integrity check has failed.
+ */
+ private static String validateIntegrity(int indexerSize, byte[] validationRecord) {
+ if (indexerSize == 0) {
+ return "empty index";
+ }
+ if (validationRecord == null) {
+ return "no validation record";
+ }
+ try {
+ int validationSize = ByteBuffer.wrap(validationRecord).asIntBuffer().get();
+ if (validationSize <= indexerSize) {
+ return null;
+ } else {
+ return String.format("Validation mismatch: validation entry %d is too large " +
+ "compared to index size %d", validationSize, indexerSize);
+ }
+ } catch (BufferUnderflowException e) {
+ return e.getMessage();
+ }
+
+ }
+
+ public static Path cacheFile(Path cacheRoot) {
+ return cacheRoot.getChild("action_cache_v" + VERSION + ".blaze");
+ }
+
+ public static Path journalFile(Path cacheRoot) {
+ return cacheRoot.getChild("action_journal_v" + VERSION + ".blaze");
+ }
+
+ @Override
+ public ActionCache.Entry createEntry(String key) {
+ return new ActionCache.Entry(key);
+ }
+
+ @Override
+ public ActionCache.Entry get(String key) {
+ int index = indexer.getIndex(key);
+ if (index < 0) {
+ return null;
+ }
+ byte[] data;
+ synchronized (this) {
+ data = map.get(index);
+ }
+ try {
+ return data != null ? CompactPersistentActionCache.decode(indexer, data) : null;
+ } catch (IOException e) {
+ // return entry marked as corrupted.
+ return CORRUPTED;
+ }
+ }
+
+ @Override
+ public void put(String key, ActionCache.Entry entry) {
+ // Encode record. Note that both methods may create new mappings in the indexer.
+ int index = indexer.getOrCreateIndex(key);
+ byte[] content = encode(indexer, entry);
+
+ // Update validation record.
+ ByteBuffer buffer = ByteBuffer.allocate(4); // size of int in bytes
+ int indexSize = indexer.size();
+ buffer.asIntBuffer().put(indexSize);
+
+ // Note the benign race condition here in which two threads might race on
+ // updating the VALIDATION_KEY. If the most recent update loses the race,
+ // a value lower than the indexer size will remain in the validation record.
+ // This will still pass the integrity check.
+ synchronized (this) {
+ map.put(VALIDATION_KEY, buffer.array());
+ // Now update record itself.
+ map.put(index, content);
+ }
+ }
+
+ @Override
+ public synchronized void remove(String key) {
+ map.remove(indexer.getIndex(key));
+ }
+
+ @Override
+ public synchronized long save() throws IOException {
+ long indexSize = indexer.save();
+ long mapSize = map.save();
+ return indexSize + mapSize;
+ }
+
+ @Override
+ public synchronized String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("Action cache (" + map.size() + " records):\n");
+ for (Map.Entry<Integer, byte[]> entry: map.entrySet()) {
+ if (entry.getKey() == VALIDATION_KEY) { continue; }
+ String content;
+ try {
+ content = decode(indexer, entry.getValue()).toString();
+ } catch (IOException e) {
+ content = e.toString() + "\n";
+ }
+ builder.append("-> ").append(indexer.getStringForIndex(entry.getKey())).append("\n")
+ .append(content).append(" packed_len = ").append(entry.getValue().length).append("\n");
+ }
+ return builder.toString();
+ }
+
+ /**
+ * Dumps action cache content.
+ */
+ @Override
+ public synchronized void dump(PrintStream out) {
+ out.println("String indexer content:\n");
+ out.println(indexer.toString());
+ out.println("Action cache (" + map.size() + " records):\n");
+ for (Map.Entry<Integer, byte[]> entry: map.entrySet()) {
+ if (entry.getKey() == VALIDATION_KEY) { continue; }
+ String content;
+ try {
+ content = CompactPersistentActionCache.decode(indexer, entry.getValue()).toString();
+ } catch (IOException e) {
+ content = e.toString() + "\n";
+ }
+ out.println(entry.getKey() + ", " + indexer.getStringForIndex(entry.getKey()) + ":\n"
+ + content + "\n packed_len = " + entry.getValue().length + "\n");
+ }
+ }
+
+ /**
+ * @return action data encoded as a byte[] array.
+ */
+ private static byte[] encode(StringIndexer indexer, ActionCache.Entry entry) {
+ Preconditions.checkState(!entry.isCorrupted());
+
+ try {
+ byte[] actionKeyBytes = entry.getActionKey().getBytes(ISO_8859_1);
+ Collection<String> files = entry.getPaths();
+
+ // Estimate the size of the buffer:
+ // 5 bytes max for the actionKey length
+ // + the actionKey itself
+ // + 16 bytes for the digest
+ // + 5 bytes max for the file list length
+ // + 5 bytes max for each file id
+ int maxSize = VarInt.MAX_VARINT_SIZE + actionKeyBytes.length + Digest.MD5_SIZE
+ + VarInt.MAX_VARINT_SIZE + files.size() * VarInt.MAX_VARINT_SIZE;
+ ByteArrayOutputStream sink = new ByteArrayOutputStream(maxSize);
+
+ VarInt.putVarInt(actionKeyBytes.length, sink);
+ sink.write(actionKeyBytes);
+
+ entry.getFileDigest().write(sink);
+
+ VarInt.putVarInt(files.size(), sink);
+ for (String file : files) {
+ VarInt.putVarInt(indexer.getOrCreateIndex(file), sink);
+ }
+ return sink.toByteArray();
+ } catch (IOException e) {
+ // This Exception can never be thrown by ByteArrayOutputStream.
+ throw new AssertionError(e);
+ }
+ }
+
+ /**
+ * Creates new action cache entry using given compressed entry data. Data
+ * will stay in the compressed format until entry is actually used by the
+ * dependency checker.
+ */
+ private static ActionCache.Entry decode(StringIndexer indexer, byte[] data) throws IOException {
+ try {
+ ByteBuffer source = ByteBuffer.wrap(data);
+
+ byte[] actionKeyBytes = new byte[VarInt.getVarInt(source)];
+ source.get(actionKeyBytes);
+ String actionKey = new String(actionKeyBytes, ISO_8859_1);
+
+ Digest digest = Digest.read(source);
+
+ int count = VarInt.getVarInt(source);
+ ImmutableList.Builder<String> builder = new ImmutableList.Builder<>();
+ for (int i = 0; i < count; i++) {
+ int id = VarInt.getVarInt(source);
+ String filename = (id >= 0 ? indexer.getStringForIndex(id) : null);
+ if (filename == null) {
+ throw new IOException("Corrupted file index");
+ }
+ builder.add(filename);
+ }
+ if (source.remaining() > 0) {
+ throw new IOException("serialized entry data has not been fully decoded");
+ }
+ return new Entry(actionKey, builder.build(), digest);
+ } catch (BufferUnderflowException e) {
+ throw new IOException("encoded entry data is incomplete", e);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/cache/Digest.java b/src/main/java/com/google/devtools/build/lib/actions/cache/Digest.java
new file mode 100644
index 0000000..f278507
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/cache/Digest.java
@@ -0,0 +1,142 @@
+// Copyright 2014 Google Inc. 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.actions.cache;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.util.Fingerprint;
+import com.google.devtools.build.lib.util.VarInt;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.Map;
+
+/**
+ * A value class for capturing and comparing MD5-based digests.
+ *
+ * <p>Note that this class is responsible for digesting file metadata in an
+ * order-independent manner. Care must be taken to do this properly. The
+ * digest must be a function of the set of (path, metadata) tuples. While the
+ * order of these pairs must not matter, it would <b>not</b> be safe to make
+ * the digest be a function of the set of paths and the set of metadata.
+ *
+ * <p>Note that the (path, metadata) tuples must be unique, otherwise the
+ * XOR-based approach will fail.
+ */
+public class Digest {
+
+ static final int MD5_SIZE = 16;
+
+ private final byte[] digest;
+
+ /**
+ * Construct the digest from the given bytes.
+ * @param digest an MD5 digest. Must be sized properly.
+ */
+ @VisibleForTesting
+ Digest(byte[] digest) {
+ Preconditions.checkState(digest.length == MD5_SIZE);
+ this.digest = Arrays.copyOf(digest, digest.length);
+ }
+
+ /**
+ * @param source the byte buffer source.
+ * @return the digest from the given buffer.
+ * @throws IOException if the byte buffer is incorrectly formatted.
+ */
+ public static Digest read(ByteBuffer source) throws IOException {
+ int size = VarInt.getVarInt(source);
+ if (size != MD5_SIZE) {
+ throw new IOException("Unexpected digest length: " + size);
+ }
+ byte[] bytes = new byte[size];
+ source.get(bytes);
+ return new Digest(bytes);
+ }
+
+ /**
+ * Write the digest to the output stream.
+ */
+ public void write(OutputStream sink) throws IOException {
+ VarInt.putVarInt(digest.length, sink);
+ sink.write(digest);
+ }
+
+ /**
+ * @param mdMap A collection of (execPath, Metadata) pairs.
+ * Values may be null.
+ * @return an <b>order-independent</b> digest from the given "set" of
+ * (path, metadata) pairs.
+ */
+ public static Digest fromMetadata(Map<String, Metadata> mdMap) {
+ byte[] result = new byte[MD5_SIZE];
+ // Profiling showed that MD5 engine instantiation was a hotspot, so create one instance for
+ // this computation to amortize its cost.
+ Fingerprint fp = new Fingerprint();
+ for (Map.Entry<String, Metadata> entry : mdMap.entrySet()) {
+ xorWith(result, getDigest(fp, entry.getKey(), entry.getValue()));
+ fp.reset();
+ }
+ return new Digest(result);
+ }
+
+ /**
+ * @return this Digest as a Metadata with no mtime.
+ */
+ public Metadata asMetadata() {
+ return new Metadata(digest);
+ }
+
+ @Override
+ public int hashCode() {
+ return Arrays.hashCode(digest);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return (obj instanceof Digest) && Arrays.equals(digest, ((Digest) obj).digest);
+ }
+
+ @Override
+ public String toString() {
+ return Fingerprint.hexDigest(digest);
+ }
+
+ private static byte[] getDigest(Fingerprint fp, String execPath, Metadata md) {
+ fp.addString(execPath);
+
+ if (md == null) {
+ // Move along, nothing to see here.
+ } else if (md.digest == null) {
+ // Use the timestamp if the digest is not present, but not both.
+ // Modifying a timestamp while keeping the contents of a file the
+ // same should not cause rebuilds.
+ fp.addLong(md.mtime);
+ } else {
+ fp.addBytes(md.digest);
+ }
+ return fp.digestAndReset();
+ }
+
+ /**
+ * Compute lhs ^= rhs bitwise operation of the arrays.
+ */
+ private static void xorWith(byte[] lhs, byte[] rhs) {
+ for (int i = 0; i < lhs.length; i++) {
+ lhs[i] ^= rhs[i];
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/cache/DigestUtils.java b/src/main/java/com/google/devtools/build/lib/actions/cache/DigestUtils.java
new file mode 100644
index 0000000..7295fb5
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/cache/DigestUtils.java
@@ -0,0 +1,130 @@
+// Copyright 2014 Google Inc. 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.actions.cache;
+
+import com.google.common.base.Preconditions;
+import com.google.common.io.BaseEncoding;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.profiler.Profiler;
+import com.google.devtools.build.lib.profiler.ProfilerTask;
+import com.google.devtools.build.lib.util.BlazeClock;
+import com.google.devtools.build.lib.util.LoggingUtil;
+import com.google.devtools.build.lib.vfs.Path;
+
+import java.io.IOException;
+import java.util.Objects;
+import java.util.logging.Level;
+
+import javax.annotation.Nullable;
+
+/**
+ * Utility class for getting md5 digests of files.
+ */
+public class DigestUtils {
+ // Object to synchronize on when serializing large file reads.
+ private static final Object MD5_LOCK = new Object();
+
+ /** Private constructor to prevent instantiation of utility class. */
+ private DigestUtils() {}
+
+ /**
+ * Returns true iff using MD5 digests is appropriate for an artifact.
+ *
+ * @param artifact Artifact in question.
+ * @param isFile whether or not Artifact is a file versus a directory, isFile() on its stat.
+ * @param size size of Artifact on filesystem in bytes, getSize() on its stat.
+ */
+ public static boolean useFileDigest(Artifact artifact, boolean isFile, long size) {
+ // Use timestamps for directories. Use digests for everything else.
+ return isFile && size != 0;
+ }
+
+ /**
+ * Obtain file's MD5 metadata using synchronized method, ensuring that system
+ * is not overloaded in case when multiple threads are requesting MD5
+ * calculations and underlying file system cannot provide it via extended
+ * attribute.
+ */
+ private static byte[] getDigestInExclusiveMode(Path path) throws IOException {
+ long startTime = BlazeClock.nanoTime();
+ synchronized (MD5_LOCK) {
+ Profiler.instance().logSimpleTask(startTime, ProfilerTask.WAIT, path.getPathString());
+ return getDigestInternal(path);
+ }
+ }
+
+ private static byte[] getDigestInternal(Path path) throws IOException {
+ long startTime = BlazeClock.nanoTime();
+ byte[] md5bin = path.getMD5Digest();
+
+ long millis = (BlazeClock.nanoTime() - startTime) / 1000000;
+ if (millis > 5000L) {
+ System.err.println("Slow read: a " + path.getFileSize() + "-byte read from " + path
+ + " took " + millis + "ms.");
+ }
+ return md5bin;
+ }
+
+ private static boolean binaryDigestWellFormed(byte[] digest) {
+ Preconditions.checkNotNull(digest);
+ return digest.length == 16;
+ }
+
+ /**
+ * Returns the the fast md5 digest of the file, or null if not available.
+ */
+ @Nullable
+ public static byte[] getFastDigest(Path path) throws IOException {
+ return path.getFastDigestFunctionType().equals("MD5") ? path.getFastDigest() : null;
+ }
+
+ /**
+ * Get the md5 digest of {@code path}, using a constant-time xattr call if the filesystem supports
+ * it, and calculating the digest manually otherwise.
+ *
+ * @param path Path of the file.
+ * @param fileSize size of the file. Used to determine if digest calculation should be done
+ * serially or in parallel. Files larger than a certain threshold will be read serially, in order
+ * to avoid excessive disk seeks.
+ */
+ public static byte[] getDigestOrFail(Path path, long fileSize) throws IOException {
+ // TODO(bazel-team): the action cache currently only works with md5 digests but it ought to
+ // work with any opaque digest.
+ byte[] md5bin = null;
+ if (Objects.equals(path.getFastDigestFunctionType(), "MD5")) {
+ md5bin = getFastDigest(path);
+ }
+ if (md5bin != null && !binaryDigestWellFormed(md5bin)) {
+ // Fail-soft in cases where md5bin is non-null, but not a valid digest.
+ String msg = String.format("Malformed digest '%s' for file %s",
+ BaseEncoding.base16().lowerCase().encode(md5bin),
+ path);
+ LoggingUtil.logToRemote(Level.SEVERE, msg, new IllegalStateException(msg));
+ md5bin = null;
+ }
+ if (md5bin != null) {
+ return md5bin;
+ } else if (fileSize > 4096) {
+ // We'll have to read file content in order to calculate the digest. In that case
+ // it would be beneficial to serialize those calculations since there is a high
+ // probability that MD5 will be requested for multiple output files simultaneously.
+ // Exception is made for small (<=4K) files since they will not likely to introduce
+ // significant delays (at worst they will result in two extra disk seeks by
+ // interrupting other reads).
+ return getDigestInExclusiveMode(path);
+ } else {
+ return getDigestInternal(path);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/cache/InjectedStat.java b/src/main/java/com/google/devtools/build/lib/actions/cache/InjectedStat.java
new file mode 100644
index 0000000..9764cd8
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/cache/InjectedStat.java
@@ -0,0 +1,67 @@
+// Copyright 2014 Google Inc. 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.actions.cache;
+
+import com.google.devtools.build.lib.vfs.FileStatus;
+
+/**
+ * A FileStatus corresponding to a file that is not determined by querying the file system.
+ */
+public class InjectedStat implements FileStatus {
+
+ private final long mtime;
+ private final long size;
+ private final long nodeId;
+
+ public InjectedStat(long mtime, long size, long nodeId) {
+ this.mtime = mtime;
+ this.size = size;
+ this.nodeId = nodeId;
+ }
+
+ @Override
+ public boolean isFile() {
+ return true;
+ }
+
+ @Override
+ public boolean isDirectory() {
+ return false;
+ }
+
+ @Override
+ public boolean isSymbolicLink() {
+ return false;
+ }
+
+ @Override
+ public long getSize() {
+ return size;
+ }
+
+ @Override
+ public long getLastModifiedTime() {
+ return mtime;
+ }
+
+ @Override
+ public long getLastChangeTime() {
+ return getLastModifiedTime();
+ }
+
+ @Override
+ public long getNodeId() {
+ return nodeId;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/cache/Metadata.java b/src/main/java/com/google/devtools/build/lib/actions/cache/Metadata.java
new file mode 100644
index 0000000..36c52b9
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/cache/Metadata.java
@@ -0,0 +1,92 @@
+// Copyright 2014 Google Inc. 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.actions.cache;
+
+import com.google.common.base.Preconditions;
+import com.google.common.io.BaseEncoding;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+
+import java.util.Arrays;
+import java.util.Date;
+
+/**
+ * A class to represent file metadata.
+ * ActionCacheChecker may assume that, for a given file, equal
+ * metadata at different moments implies equal file-contents,
+ * where metadata equality is computed using Metadata.equals().
+ * <p>
+ * NB! Several other parts of Blaze are relying on the fact that metadata
+ * uses mtime and not ctime. If metadata is ever changed
+ * to use ctime, all uses of Metadata must be carefully examined.
+ */
+@Immutable @ThreadSafe
+public final class Metadata {
+ public final long mtime;
+ public final byte[] digest;
+
+ // Convenience object for use with volatile files that we do not want checked
+ // (e.g. the build-changelist.txt)
+ public static final Metadata CONSTANT_METADATA = new Metadata(-1);
+
+ public Metadata(long mtime) {
+ this.mtime = mtime;
+ this.digest = null;
+ }
+
+ public Metadata(byte[] digest) {
+ this.mtime = 0L;
+ this.digest = Preconditions.checkNotNull(digest);
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = 0;
+ if (digest != null) {
+ // We are already dealing with the digest so we can just use portion of it
+ // as a hash code.
+ hash += digest[0] + (digest[1] << 8) + (digest[2] << 16) + (digest[3] << 24);
+ } else {
+ // Inlined hashCode for Long, so we don't
+ // have to construct an Object, just to compute
+ // a 32-bit hash out of a 64 bit value.
+ hash = (int) (mtime ^ (mtime >>> 32));
+ }
+ return hash;
+ }
+
+ @Override
+ public boolean equals(Object that) {
+ if (this == that) {
+ return true;
+ }
+ if (!(that instanceof Metadata)) {
+ return false;
+ }
+ // Do a strict comparison - both digest and mtime should match
+ return Arrays.equals(this.digest, ((Metadata) that).digest)
+ && this.mtime == ((Metadata) that).mtime;
+ }
+
+ @Override
+ public String toString() {
+ if (digest != null) {
+ return "MD5 " + BaseEncoding.base16().lowerCase().encode(digest);
+ } else if (mtime > 0) {
+ return "timestamp " + new Date(mtime);
+ }
+ return "no metadata";
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/cache/MetadataHandler.java b/src/main/java/com/google/devtools/build/lib/actions/cache/MetadataHandler.java
new file mode 100644
index 0000000..9d288db
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/cache/MetadataHandler.java
@@ -0,0 +1,69 @@
+// Copyright 2014 Google Inc. 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.actions.cache;
+
+import com.google.devtools.build.lib.actions.ActionInput;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.vfs.FileStatus;
+
+import java.io.IOException;
+import java.util.Collection;
+
+/** Retrieves {@link Metadata} of {@link Artifact}s, and inserts virtual metadata as well. */
+public interface MetadataHandler {
+ /**
+ * Returns metadata for the given artifact or null if it does not exist.
+ *
+ * @param artifact artifact
+ *
+ * @return metadata instance or null if metadata cannot be obtained.
+ */
+ Metadata getMetadataMaybe(Artifact artifact);
+ /**
+ * Returns metadata for the given artifact or throws an exception if the
+ * metadata could not be obtained.
+ *
+ * @return metadata instance
+ *
+ * @throws IOException if metadata could not be obtained.
+ */
+ Metadata getMetadata(Artifact artifact) throws IOException;
+
+ /** Sets digest for virtual artifacts (e.g. middlemen). {@code digest} must not be null. */
+ void setDigestForVirtualArtifact(Artifact artifact, Digest digest);
+
+ /**
+ * Injects provided digest into the metadata handler, simultaneously caching lstat() data as well.
+ */
+ void injectDigest(ActionInput output, FileStatus statNoFollow, byte[] digest);
+
+ /** Returns true iff artifact exists. */
+ boolean artifactExists(Artifact artifact);
+ /** Returns true iff artifact is a regular file. */
+ boolean isRegularFile(Artifact artifact);
+
+ /**
+ * @return Whether the artifact's data was injected.
+ * @throws IOException if implementation tried to stat artifact which threw an exception.
+ * Technically, this means that the artifact could not have been injected, but by throwing
+ * here we save the caller trying to stat this file on their own and throwing the same
+ * exception. Implementations are not guaranteed to throw in this case if they are able to
+ * determine that the artifact is not injected without statting it.
+ */
+ boolean isInjected(Artifact artifact) throws IOException;
+
+ /** Discards all metadata for the given artifacts, presumably because they will be modified. */
+ void discardMetadata(Collection<Artifact> artifactList);
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/cache/NullActionCache.java b/src/main/java/com/google/devtools/build/lib/actions/cache/NullActionCache.java
new file mode 100644
index 0000000..0975150
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/cache/NullActionCache.java
@@ -0,0 +1,51 @@
+// Copyright 2014 Google Inc. 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.actions.cache;
+
+import java.io.IOException;
+import java.io.PrintStream;
+
+/**
+ * A no-op action cache that never caches anything.
+ */
+public final class NullActionCache implements ActionCache {
+
+ @Override
+ public void put(String key, Entry entry) {
+ }
+
+ @Override
+ public Entry get(String key) {
+ return null;
+ }
+
+ @Override
+ public void remove(String key) {
+ }
+
+ @Override
+ public Entry createEntry(String key) {
+ return new ActionCache.Entry(key);
+ }
+
+ @Override
+ public long save() throws IOException {
+ return 0;
+ }
+
+ @Override
+ public void dump(PrintStream out) {
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/cache/PersistentStringIndexer.java b/src/main/java/com/google/devtools/build/lib/actions/cache/PersistentStringIndexer.java
new file mode 100644
index 0000000..bd98b2b
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/cache/PersistentStringIndexer.java
@@ -0,0 +1,161 @@
+// Copyright 2014 Google Inc. 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.actions.cache;
+
+import com.google.common.collect.MapMaker;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ConditionallyThreadSafe;
+import com.google.devtools.build.lib.util.CanonicalStringIndexer;
+import com.google.devtools.build.lib.util.Clock;
+import com.google.devtools.build.lib.util.PersistentMap;
+import com.google.devtools.build.lib.util.StringCanonicalizer;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
+import com.google.devtools.build.lib.vfs.Path;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.util.Map;
+import java.util.concurrent.ConcurrentMap;
+
+/**
+ * Persistent version of the CanonicalStringIndexer.
+ *
+ * <p>This class is backed by a PersistentMap that holds one direction of the
+ * canonicalization mapping. The other direction is handled purely in memory
+ * and reconstituted at load-time.
+ *
+ * <p>Thread-safety is ensured by locking on all mutating operations from the
+ * superclass. Read-only operations are not locked, but rather backed by
+ * ConcurrentMaps.
+ */
+@ConditionallyThreadSafe // condition: each instance must instantiated with
+ // different dataFile.
+final class PersistentStringIndexer extends CanonicalStringIndexer {
+
+ /**
+ * Persistent metadata map. Used as a backing map to provide a persistent
+ * implementation of the metadata cache.
+ */
+ private static final class PersistentIndexMap extends PersistentMap<String, Integer> {
+ private static final int VERSION = 0x01;
+ private static final long SAVE_INTERVAL_NS = 3L * 1000 * 1000 * 1000;
+
+ private final Clock clock;
+ private long nextUpdate;
+
+ public PersistentIndexMap(Path mapFile, Path journalFile, Clock clock) throws IOException {
+ super(VERSION, PersistentStringIndexer.<String, Integer>newConcurrentMap(INITIAL_ENTRIES),
+ mapFile, journalFile);
+ this.clock = clock;
+ nextUpdate = clock.nanoTime();
+ load(/*throwOnLoadFailure=*/true);
+ }
+
+ @Override
+ protected boolean updateJournal() {
+ long time = clock.nanoTime();
+ if (SAVE_INTERVAL_NS == 0 || time > nextUpdate) {
+ nextUpdate = time + SAVE_INTERVAL_NS;
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public Integer remove(Object object) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void clear() {
+ throw new UnsupportedOperationException();
+ }
+
+ public void flush() {
+ super.forceFlush();
+ }
+
+ @Override
+ protected String readKey(DataInputStream in) throws IOException {
+ int length = in.readInt();
+ if (length < 0) {
+ throw new IOException("corrupt key length: " + length);
+ }
+ byte[] content = new byte[length];
+ in.readFully(content);
+ return StringCanonicalizer.intern(bytes2string(content));
+ }
+
+ @Override
+ protected Integer readValue(DataInputStream in) throws IOException {
+ return in.readInt();
+ }
+
+ @Override
+ protected void writeKey(String key, DataOutputStream out) throws IOException {
+ byte[] content = string2bytes(key);
+ out.writeInt(content.length);
+ out.write(content);
+ }
+
+ @Override
+ protected void writeValue(Integer value, DataOutputStream out) throws IOException {
+ out.writeInt(value);
+ }
+ }
+
+ private final PersistentIndexMap persistentIndexMap;
+ private static final int INITIAL_ENTRIES = 10000;
+
+ /**
+ * Instantiates and loads instance of the persistent string indexer.
+ */
+ static PersistentStringIndexer newPersistentStringIndexer(Path dataPath,
+ Clock clock) throws IOException {
+ PersistentIndexMap persistentIndexMap = new PersistentIndexMap(dataPath,
+ FileSystemUtils.replaceExtension(dataPath, ".journal"), clock);
+ Map<Integer, String> reverseMapping = newConcurrentMap(INITIAL_ENTRIES);
+ for (Map.Entry<String, Integer> entry : persistentIndexMap.entrySet()) {
+ if (reverseMapping.put(entry.getValue(), entry.getKey()) != null) {
+ throw new IOException("Corrupted filename index has duplicate entry: " + entry.getKey());
+ }
+ }
+ return new PersistentStringIndexer(persistentIndexMap, reverseMapping);
+ }
+
+ private PersistentStringIndexer(PersistentIndexMap stringToInt,
+ Map<Integer, String> intToString) {
+ super(stringToInt, intToString);
+ this.persistentIndexMap = stringToInt;
+ }
+
+ /**
+ * Saves index data to the file.
+ */
+ synchronized long save() throws IOException {
+ return persistentIndexMap.save();
+ }
+
+ /**
+ * Flushes the journal.
+ */
+ synchronized void flush() {
+ persistentIndexMap.flush();
+ }
+
+ private static <K, V> ConcurrentMap<K, V> newConcurrentMap(int expectedCapacity) {
+ return new MapMaker().initialCapacity(expectedCapacity).makeMap();
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/actions/cache/VirtualActionInput.java b/src/main/java/com/google/devtools/build/lib/actions/cache/VirtualActionInput.java
new file mode 100644
index 0000000..debb8ea
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/actions/cache/VirtualActionInput.java
@@ -0,0 +1,31 @@
+// Copyright 2014 Google Inc. 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.actions.cache;
+
+import com.google.devtools.build.lib.actions.ActionInput;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * An ActionInput that does not actually exist on the filesystem, but can still be written to an
+ * OutputStream.
+ */
+public interface VirtualActionInput extends ActionInput {
+ /**
+ * Writes the the fake file to an OutputStream. MUST be deterministic, in that multiple calls
+ * to write the same VirtualActionInput must write identical bytes.
+ */
+ void writeTo(OutputStream out) throws IOException;
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/AbstractConfiguredTarget.java b/src/main/java/com/google/devtools/build/lib/analysis/AbstractConfiguredTarget.java
new file mode 100644
index 0000000..e574978
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/AbstractConfiguredTarget.java
@@ -0,0 +1,111 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.packages.PackageSpecification;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.syntax.ClassObject;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.syntax.SkylarkNestedSet;
+
+/**
+ * An abstract implementation of ConfiguredTarget in which all properties are
+ * assigned trivial default values.
+ */
+public abstract class AbstractConfiguredTarget
+ implements ConfiguredTarget, VisibilityProvider, ClassObject {
+ private final Target target;
+ private final BuildConfiguration configuration;
+
+ private final NestedSet<PackageSpecification> visibility;
+
+ AbstractConfiguredTarget(Target target,
+ BuildConfiguration configuration) {
+ this.target = target;
+ this.configuration = configuration;
+ this.visibility = NestedSetBuilder.emptySet(Order.STABLE_ORDER);
+ }
+
+ AbstractConfiguredTarget(TargetContext targetContext) {
+ this.target = targetContext.getTarget();
+ this.configuration = targetContext.getConfiguration();
+ this.visibility = targetContext.getVisibility();
+ }
+
+ @Override
+ public final NestedSet<PackageSpecification> getVisibility() {
+ return visibility;
+ }
+
+ @Override
+ public Target getTarget() {
+ return target;
+ }
+
+ @Override
+ public BuildConfiguration getConfiguration() {
+ return configuration;
+ }
+
+ @Override
+ public Label getLabel() {
+ return getTarget().getLabel();
+ }
+
+ @Override
+ public String toString() {
+ return "ConfiguredTarget(" + getTarget().getLabel() + ", " + getConfiguration() + ")";
+ }
+
+ @Override
+ public <P extends TransitiveInfoProvider> P getProvider(Class<P> provider) {
+ AnalysisUtils.checkProvider(provider);
+ if (provider.isAssignableFrom(getClass())) {
+ return provider.cast(this);
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public Object getValue(String name) {
+ if (name.equals("label")) {
+ return getLabel();
+ } else if (name.equals("files")) {
+ // A shortcut for files to build in Skylark. FileConfiguredTarget and RunleConfiguredTarget
+ // always has FileProvider and Error- and PackageGroupConfiguredTarget-s shouldn't be
+ // accessible in Skylark.
+ return SkylarkNestedSet.of(Artifact.class, getProvider(FileProvider.class).getFilesToBuild());
+ }
+ return get(name);
+ }
+
+ @Override
+ public String errorMessage(String name) {
+ return null;
+ }
+
+ @Override
+ public ImmutableCollection<String> getKeys() {
+ return ImmutableList.<String>builder().add("label").add("files").build();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/AlwaysBuiltArtifactsProvider.java b/src/main/java/com/google/devtools/build/lib/analysis/AlwaysBuiltArtifactsProvider.java
new file mode 100644
index 0000000..e4d40fc
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/AlwaysBuiltArtifactsProvider.java
@@ -0,0 +1,46 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+
+/**
+ * Artifacts that should be built when a target is mentioned in the command line, but are neither in
+ * the {@code filesToBuild} nor in the runfiles.
+ *
+ * <p>
+ * Link actions, may not run a link for their transitive dependencies, so it does not force the
+ * source files in the transitive closure to be built by default. However, users expect builds to
+ * fail when there is an error in a dependent library, so we use this mechanism to force their
+ * compilation.
+ */
+@Immutable
+public final class AlwaysBuiltArtifactsProvider implements TransitiveInfoProvider {
+
+ private final NestedSet<Artifact> artifactsToAlwaysBuild;
+
+ public AlwaysBuiltArtifactsProvider(NestedSet<Artifact> artifactsToAlwaysBuild) {
+ this.artifactsToAlwaysBuild = artifactsToAlwaysBuild;
+ }
+
+ /**
+ * Returns the collection of artifacts to be built.
+ */
+ public NestedSet<Artifact> getArtifactsToAlwaysBuild() {
+ return artifactsToAlwaysBuild;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/AnalysisEnvironment.java b/src/main/java/com/google/devtools/build/lib/analysis/AnalysisEnvironment.java
new file mode 100644
index 0000000..0bccc72
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/AnalysisEnvironment.java
@@ -0,0 +1,128 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.actions.ActionRegistry;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.MiddlemanFactory;
+import com.google.devtools.build.lib.actions.Root;
+import com.google.devtools.build.lib.analysis.buildinfo.BuildInfoFactory.BuildInfoKey;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.skyframe.SkyFunction;
+
+/**
+ * The set of services that are provided to {@link ConfiguredTarget} objects
+ * during initialization.
+ */
+public interface AnalysisEnvironment extends ActionRegistry {
+ /**
+ * Returns a callback to be used in this build for reporting analysis errors.
+ */
+ EventHandler getEventHandler();
+
+ /**
+ * Returns whether any errors were reported to this instance.
+ */
+ boolean hasErrors();
+
+ /**
+ * Returns the artifact for the derived file {@code rootRelativePath}.
+ *
+ * <p>Creates the artifact if necessary and sets the root of that artifact to {@code root}.
+ */
+ Artifact getDerivedArtifact(PathFragment rootRelativePath, Root root);
+
+ /**
+ * Returns an artifact for the derived file {@code rootRelativePath} whose changes do not cause
+ * a rebuild.
+ *
+ * <p>Creates the artifact if necessary and sets the root of that artifact to {@code root}.
+ *
+ * <p>This is useful for files that store data that changes very frequently (e.g. current time)
+ * but does not substantially affect the result of the build.
+ */
+ Artifact getConstantMetadataArtifact(PathFragment rootRelativePath,
+ Root root);
+
+ /**
+ * Returns the artifact for the derived file {@code rootRelativePath},
+ * creating it if necessary, and setting the root of that artifact to
+ * {@code root}. The artifact will represent the output directory of a {@code Fileset}.
+ */
+ Artifact getFilesetArtifact(PathFragment rootRelativePath, Root root);
+
+ /**
+ * Returns the artifact for the specified tool.
+ */
+ Artifact getEmbeddedToolArtifact(String embeddedPath);
+
+ /**
+ * Returns the middleman factory associated with the build.
+ */
+ // TODO(bazel-team): remove this method and replace it with delegate methods.
+ MiddlemanFactory getMiddlemanFactory();
+
+ /**
+ * Returns the generating action for the given local artifact.
+ *
+ * If the artifact was created in another analysis environment (e.g. by a different configured
+ * target instance) or the artifact is a source artifact, it returns null.
+ */
+ Action getLocalGeneratingAction(Artifact artifact);
+
+ /**
+ * Returns the actions that were registered so far with this analysis environment, that is, all
+ * the actions that were created by the current target being analyzed.
+ */
+ Iterable<Action> getRegisteredActions();
+
+ /**
+ * Returns the Skyframe SkyFunction.Environment if available. Otherwise, null.
+ *
+ * <p>If you need to use this for something other than genquery, please think long and hard
+ * about that.
+ */
+ SkyFunction.Environment getSkyframeEnv();
+
+ /**
+ * Returns the Artifact that is used to hold the non-volatile workspace status for the current
+ * build request.
+ */
+ Artifact getStableWorkspaceStatusArtifact();
+
+ /**
+ * Returns the Artifact that is used to hold the volatile workspace status (e.g. build
+ * changelist) for the current build request.
+ */
+ Artifact getVolatileWorkspaceStatusArtifact();
+
+ /**
+ * Returns the Artifacts that contain the workspace status for the current build request.
+ *
+ * @param ruleContext the rule to use for error reporting and to determine the
+ * configuration
+ */
+ ImmutableList<Artifact> getBuildInfo(RuleContext ruleContext, BuildInfoKey key);
+
+ /**
+ * Returns the set of orphan Artifacts (i.e. Artifacts without generating action). Should only be
+ * called after the ConfiguredTarget is created.
+ */
+ ImmutableSet<Artifact> getOrphanArtifacts();
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/AnalysisFailureEvent.java b/src/main/java/com/google/devtools/build/lib/analysis/AnalysisFailureEvent.java
new file mode 100644
index 0000000..5064163
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/AnalysisFailureEvent.java
@@ -0,0 +1,40 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.devtools.build.lib.syntax.Label;
+
+/**
+ * This event is fired during the build, when it becomes known that the analysis
+ * of a target cannot be completed because of an error in one of its
+ * dependencies.
+ */
+public class AnalysisFailureEvent {
+ private final LabelAndConfiguration failedTarget;
+ private final Label failureReason;
+
+ public AnalysisFailureEvent(LabelAndConfiguration failedTarget, Label failureReason) {
+ this.failedTarget = failedTarget;
+ this.failureReason = failureReason;
+ }
+
+ public LabelAndConfiguration getFailedTarget() {
+ return failedTarget;
+ }
+
+ public Label getFailureReason() {
+ return failureReason;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/AnalysisHooks.java b/src/main/java/com/google/devtools/build/lib/analysis/AnalysisHooks.java
new file mode 100644
index 0000000..82c4485
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/AnalysisHooks.java
@@ -0,0 +1,36 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.pkgcache.PackageManager;
+
+/**
+ * This interface resolves target - configuration pairs to {@link ConfiguredTarget} instances.
+ *
+ * <p>This interface is used to provide analysis phase functionality to actions that need it in
+ * the execution phase.
+ */
+public interface AnalysisHooks {
+ /**
+ * Returns the package manager used during the analysis phase.
+ */
+ PackageManager getPackageManager();
+
+ /**
+ * Resolves an existing configured target. Returns null if it is not in the cache.
+ */
+ ConfiguredTarget getExistingConfiguredTarget(Target target, BuildConfiguration configuration);
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/AnalysisPhaseCompleteEvent.java b/src/main/java/com/google/devtools/build/lib/analysis/AnalysisPhaseCompleteEvent.java
new file mode 100644
index 0000000..0d1e565
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/AnalysisPhaseCompleteEvent.java
@@ -0,0 +1,59 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.Collection;
+
+/**
+ * This event is fired after the analysis phase is complete.
+ */
+public class AnalysisPhaseCompleteEvent {
+
+ private final Collection<ConfiguredTarget> targets;
+ private final long timeInMs;
+ private int targetsVisited;
+
+ /**
+ * Construct the event.
+ * @param targets The set of active targets that remain.
+ */
+ public AnalysisPhaseCompleteEvent(Collection<? extends ConfiguredTarget> targets,
+ int targetsVisited, long timeInMs) {
+ this.timeInMs = timeInMs;
+ this.targets = ImmutableList.copyOf(targets);
+ this.targetsVisited = targetsVisited;
+ }
+
+ /**
+ * @return The set of active targets remaining, which is a subset
+ * of the targets we attempted to analyze.
+ */
+ public Collection<ConfiguredTarget> getTargets() {
+ return targets;
+ }
+
+ /**
+ * @return The number of targets freshly visited during analysis
+ */
+ public int getTargetsVisited() {
+ return targetsVisited;
+ }
+
+ public long getTimeInMs() {
+ return timeInMs;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/AnalysisPhaseStartedEvent.java b/src/main/java/com/google/devtools/build/lib/analysis/AnalysisPhaseStartedEvent.java
new file mode 100644
index 0000000..fc97c60
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/AnalysisPhaseStartedEvent.java
@@ -0,0 +1,51 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.syntax.Label;
+
+import java.util.Collection;
+
+/**
+ * This event is fired before the analysis phase is started.
+ */
+public class AnalysisPhaseStartedEvent {
+
+ private final Iterable<Label> labels;
+
+ /**
+ * Construct the event.
+ * @param targets The set of active targets that remain.
+ */
+ public AnalysisPhaseStartedEvent(Collection<Target> targets) {
+ this.labels = Iterables.transform(targets, new Function<Target, Label>() {
+ @Override
+ public Label apply(Target input) {
+ return input.getLabel();
+ }
+ });
+ }
+
+ /**
+ * @return The set of active targets remaining, which is a subset
+ * of the targets we attempted to load.
+ */
+ public Iterable<Label> getLabels() {
+ return labels;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/AnalysisUtils.java b/src/main/java/com/google/devtools/build/lib/analysis/AnalysisUtils.java
new file mode 100644
index 0000000..2e4c251
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/AnalysisUtils.java
@@ -0,0 +1,146 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.packages.TriState;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Collection;
+
+/**
+ * Utility functions for use during analysis.
+ */
+public final class AnalysisUtils {
+
+ private AnalysisUtils() {
+ throw new IllegalStateException(); // utility class
+ }
+
+ /**
+ * Returns whether link stamping is enabled for a rule.
+ *
+ * <p>This returns false for unstampable rule classes and for rules in the
+ * host configuration. Otherwise it returns the value of the stamp attribute,
+ * or of the stamp option if the attribute value is -1.
+ */
+ public static boolean isStampingEnabled(RuleContext ruleContext) {
+ BuildConfiguration config = ruleContext.getConfiguration();
+ Rule rule = ruleContext.getRule();
+ if (config.isHostConfiguration()
+ || !rule.getRuleClassObject().hasAttr("stamp", Type.TRISTATE)) {
+ return false;
+ }
+ TriState stamp = ruleContext.attributes().get("stamp", Type.TRISTATE);
+ return stamp == TriState.YES || (stamp == TriState.AUTO && config.stampBinaries());
+ }
+
+ // TODO(bazel-team): These need Iterable<? extends TransitiveInfoCollection> because they need to
+ // be called with Iterable<ConfiguredTarget>. Once the configured target lockdown is complete, we
+ // can eliminate the "extends" clauses.
+ /**
+ * Returns the list of providers of the specified type from a set of transitive info
+ * collections.
+ */
+ public static <C extends TransitiveInfoProvider> Iterable<C> getProviders(
+ Iterable<? extends TransitiveInfoCollection> prerequisites, Class<C> provider) {
+ Collection<C> result = new ArrayList<>();
+ for (TransitiveInfoCollection prerequisite : prerequisites) {
+ C prerequisiteProvider = prerequisite.getProvider(provider);
+ if (prerequisiteProvider != null) {
+ result.add(prerequisiteProvider);
+ }
+ }
+ return ImmutableList.copyOf(result);
+ }
+
+ /**
+ * Returns the iterable of collections that have the specified provider.
+ */
+ public static <S extends TransitiveInfoCollection, C extends TransitiveInfoProvider> Iterable<S>
+ filterByProvider(Iterable<S> prerequisites, final Class<C> provider) {
+ return Iterables.filter(prerequisites, new Predicate<S>() {
+ @Override
+ public boolean apply(S target) {
+ return target.getProvider(provider) != null;
+ }
+ });
+ }
+
+ /**
+ * Returns the path of the associated manifest file for the path of a Fileset. Works for both
+ * exec paths and root relative paths.
+ */
+ public static PathFragment getManifestPathFromFilesetPath(PathFragment filesetDir) {
+ PathFragment manifestDir = filesetDir.replaceName("_" + filesetDir.getBaseName());
+ PathFragment outputManifestFrag = manifestDir.getRelative("MANIFEST");
+ return outputManifestFrag;
+ }
+
+ /**
+ * Returns the middleman artifact on the specified attribute of the specified rule, or an empty
+ * set if it does not exist.
+ */
+ public static NestedSet<Artifact> getMiddlemanFor(RuleContext rule, String attribute) {
+ TransitiveInfoCollection prereq = rule.getPrerequisite(attribute, Mode.HOST);
+ if (prereq == null) {
+ return NestedSetBuilder.emptySet(Order.STABLE_ORDER);
+ }
+ MiddlemanProvider provider = prereq.getProvider(MiddlemanProvider.class);
+ if (provider == null) {
+ return NestedSetBuilder.emptySet(Order.STABLE_ORDER);
+ }
+ return provider.getMiddlemanArtifact();
+ }
+
+ /**
+ * Returns a path fragment qualified by the rule name and unique fragment to
+ * disambiguate artifacts produced from the source file appearing in
+ * multiple rules.
+ *
+ * <p>For example "//pkg:target" -> "pkg/<fragment>/target.
+ */
+ public static PathFragment getUniqueDirectory(Label label, PathFragment fragment) {
+ return label.getPackageFragment().getRelative(fragment)
+ .getRelative(label.getName());
+ }
+
+ /**
+ * Checks that the given provider class either refers to an interface or to a value class.
+ */
+ public static <T extends TransitiveInfoProvider> void checkProvider(Class<T> clazz) {
+ if (!clazz.isInterface()) {
+ Preconditions.checkArgument(Modifier.isFinal(clazz.getModifiers()),
+ clazz.getName() + " has to be final");
+ Preconditions.checkArgument(clazz.isAnnotationPresent(Immutable.class),
+ clazz.getName() + " has to be tagged with @Immutable");
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/Aspect.java b/src/main/java/com/google/devtools/build/lib/analysis/Aspect.java
new file mode 100644
index 0000000..3f4a06e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/Aspect.java
@@ -0,0 +1,82 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.UnmodifiableIterator;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * Extra information about a configured target computed on request of a dependent.
+ *
+ * <p>Analogous to {@link ConfiguredTarget}: contains a bunch of transitive info providers, which
+ * are merged with the providers of the associated configured target before they are passed to
+ * the configured target factories that depend on the configured target to which this aspect is
+ * added.
+ *
+ * <p>Aspects are created alongside configured targets on request from dependents.
+ */
+@Immutable
+public final class Aspect implements Iterable<TransitiveInfoProvider> {
+ private final
+ ImmutableMap<Class<? extends TransitiveInfoProvider>, TransitiveInfoProvider> providers;
+
+ private Aspect(
+ ImmutableMap<Class<? extends TransitiveInfoProvider>, TransitiveInfoProvider> providers) {
+ this.providers = providers;
+ }
+
+ /**
+ * Returns the providers created by the aspect.
+ */
+ public ImmutableMap<Class<? extends TransitiveInfoProvider>, TransitiveInfoProvider>
+ getProviders() {
+ return providers;
+ }
+
+ @Override
+ public UnmodifiableIterator<TransitiveInfoProvider> iterator() {
+ return providers.values().iterator();
+ }
+
+ /**
+ * Builder for {@link Aspect}.
+ */
+ public static class Builder {
+ private final Map<Class<? extends TransitiveInfoProvider>, TransitiveInfoProvider>
+ providers = new LinkedHashMap<>();
+
+ /**
+ * Adds a provider to the aspect.
+ */
+ public Builder addProvider(
+ Class<? extends TransitiveInfoProvider> key, TransitiveInfoProvider value) {
+ Preconditions.checkNotNull(key);
+ Preconditions.checkNotNull(value);
+ AnalysisUtils.checkProvider(key);
+ Preconditions.checkState(!providers.containsKey(key));
+ providers.put(key, value);
+ return this;
+ }
+
+ public Aspect build() {
+ return new Aspect(ImmutableMap.copyOf(providers));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/BaseRuleClasses.java b/src/main/java/com/google/devtools/build/lib/analysis/BaseRuleClasses.java
new file mode 100644
index 0000000..ad5756e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/BaseRuleClasses.java
@@ -0,0 +1,265 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import static com.google.devtools.build.lib.packages.Attribute.ConfigurationTransition.DATA;
+import static com.google.devtools.build.lib.packages.Attribute.ConfigurationTransition.HOST;
+import static com.google.devtools.build.lib.packages.Attribute.attr;
+import static com.google.devtools.build.lib.packages.Type.BOOLEAN;
+import static com.google.devtools.build.lib.packages.Type.DISTRIBUTIONS;
+import static com.google.devtools.build.lib.packages.Type.INTEGER;
+import static com.google.devtools.build.lib.packages.Type.LABEL;
+import static com.google.devtools.build.lib.packages.Type.LABEL_LIST;
+import static com.google.devtools.build.lib.packages.Type.LICENSE;
+import static com.google.devtools.build.lib.packages.Type.NODEP_LABEL_LIST;
+import static com.google.devtools.build.lib.packages.Type.STRING;
+import static com.google.devtools.build.lib.packages.Type.STRING_LIST;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.analysis.config.RunUnder;
+import com.google.devtools.build.lib.analysis.constraints.EnvironmentRule;
+import com.google.devtools.build.lib.packages.Attribute;
+import com.google.devtools.build.lib.packages.Attribute.LateBoundLabel;
+import com.google.devtools.build.lib.packages.Attribute.LateBoundLabelList;
+import com.google.devtools.build.lib.packages.AttributeMap;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.RuleClass.Builder;
+import com.google.devtools.build.lib.packages.RuleClass.Builder.RuleClassType;
+import com.google.devtools.build.lib.packages.TestSize;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.util.FileTypeSet;
+
+import java.util.List;
+
+/**
+ * Rule class definitions used by (almost) every rule.
+ */
+public class BaseRuleClasses {
+ /**
+ * Label of the pseudo-filegroup that contains all the targets that are needed
+ * for running tests in coverage mode.
+ */
+ private static final Label COVERAGE_SUPPORT_LABEL =
+ Label.parseAbsoluteUnchecked("//tools/defaults:coverage");
+
+ private static final Attribute.ComputedDefault obsoleteDefault =
+ new Attribute.ComputedDefault() {
+ @Override
+ public Object getDefault(AttributeMap rule) {
+ return rule.getPackageDefaultObsolete();
+ }
+ };
+
+ private static final Attribute.ComputedDefault testonlyDefault =
+ new Attribute.ComputedDefault() {
+ @Override
+ public Object getDefault(AttributeMap rule) {
+ return rule.getPackageDefaultTestOnly();
+ }
+ };
+
+ private static final Attribute.ComputedDefault deprecationDefault =
+ new Attribute.ComputedDefault() {
+ @Override
+ public Object getDefault(AttributeMap rule) {
+ return rule.getPackageDefaultDeprecation();
+ }
+ };
+
+ /**
+ * Implementation for the :action_listener attribute.
+ */
+ private static final LateBoundLabelList<BuildConfiguration> ACTION_LISTENER =
+ new LateBoundLabelList<BuildConfiguration>() {
+ @Override
+ public List<Label> getDefault(Rule rule, BuildConfiguration configuration) {
+ // action_listeners are special rules; they tell the build system to add extra_actions to
+ // existing rules. As such they need an edge to every ConfiguredTarget with the limitation
+ // that they only run on the target configuration and should not operate on action_listeners
+ // and extra_actions themselves (to avoid cycles).
+ return configuration.getActionListeners();
+ }
+ };
+
+ private static final LateBoundLabelList<BuildConfiguration> COVERAGE_SUPPORT =
+ new LateBoundLabelList<BuildConfiguration>(ImmutableList.of(COVERAGE_SUPPORT_LABEL)) {
+ @Override
+ public List<Label> getDefault(Rule rule, BuildConfiguration configuration) {
+ return configuration.isCodeCoverageEnabled()
+ ? ImmutableList.<Label>copyOf(configuration.getCoverageLabels())
+ : ImmutableList.<Label>of();
+ }
+ };
+
+ private static final LateBoundLabelList<BuildConfiguration> COVERAGE_REPORT_GENERATOR =
+ new LateBoundLabelList<BuildConfiguration>(ImmutableList.of(COVERAGE_SUPPORT_LABEL)) {
+ @Override
+ public List<Label> getDefault(Rule rule, BuildConfiguration configuration) {
+ return configuration.isCodeCoverageEnabled()
+ ? ImmutableList.<Label>copyOf(configuration.getCoverageReportGeneratorLabels())
+ : ImmutableList.<Label>of();
+ }
+ };
+
+ /**
+ * Implementation for the :run_under attribute.
+ */
+ private static final LateBoundLabel<BuildConfiguration> RUN_UNDER =
+ new LateBoundLabel<BuildConfiguration>() {
+ @Override
+ public Label getDefault(Rule rule, BuildConfiguration configuration) {
+ RunUnder runUnder = configuration.getRunUnder();
+ return runUnder == null ? null : runUnder.getLabel();
+ }
+ };
+
+ /**
+ * A base rule for all test rules.
+ */
+ @BlazeRule(name = "$test_base_rule",
+ type = RuleClassType.ABSTRACT)
+ public static final class TestBaseRule implements RuleDefinition {
+ @Override
+ public RuleClass build(Builder builder, RuleDefinitionEnvironment env) {
+ return builder
+ .add(attr("size", STRING).value("medium").taggable()
+ .nonconfigurable("policy decision: should be consistent across configurations"))
+ .add(attr("timeout", STRING).taggable()
+ .nonconfigurable("policy decision: should be consistent across configurations")
+ .value(new Attribute.ComputedDefault() {
+ @Override
+ public Object getDefault(AttributeMap rule) {
+ TestSize size = TestSize.getTestSize(rule.get("size", Type.STRING));
+ if (size != null) {
+ String timeout = size.getDefaultTimeout().toString();
+ if (timeout != null) {
+ return timeout;
+ }
+ }
+ return "illegal";
+ }
+ }))
+ .add(attr("flaky", BOOLEAN).value(false).taggable()
+ .nonconfigurable("policy decision: should be consistent across configurations"))
+ .add(attr("shard_count", INTEGER).value(-1))
+ .add(attr("local", BOOLEAN).value(false).taggable()
+ .nonconfigurable("policy decision: should be consistent across configurations"))
+ .add(attr("args", STRING_LIST)
+ .nonconfigurable("policy decision: should be consistent across configurations"))
+ .add(attr("$test_runtime", LABEL_LIST).cfg(HOST).value(ImmutableList.of(
+ env.getLabel("//tools/test:runtime"))))
+
+ // TODO(bazel-team): TestActions may need to be run with coverage, so all tests
+ // implicitly depend on crosstool, which provides gcov. We could add gcov to
+ // InstrumentedFilesProvider.getInstrumentationMetadataFiles() (or a new method) for
+ // all the test rules that have C++ in their transitive closure. Then this could go.
+ .add(attr(":coverage_support", LABEL_LIST).cfg(HOST).value(COVERAGE_SUPPORT))
+ .add(attr(":coverage_report_generator", LABEL_LIST).cfg(HOST)
+ .value(COVERAGE_REPORT_GENERATOR))
+
+ // The target itself and run_under both run on the same machine. We use the DATA config
+ // here because the run_under acts like a data dependency (e.g. no LIPO optimization).
+ .add(attr(":run_under", LABEL).cfg(DATA).value(RUN_UNDER))
+ .build();
+ }
+ }
+
+ /**
+ * Share common attributes across both base and Skylark base rules.
+ */
+ public static RuleClass.Builder commonCoreAndSkylarkAttributes(RuleClass.Builder builder) {
+ return builder
+ // The visibility attribute is special: it is a nodep label, and loading the
+ // necessary package groups is handled by {@link LabelVisitor#visitTargetVisibility}.
+ // Package groups always have the null configuration so that they are not duplicated
+ // needlessly.
+ .add(attr("visibility", NODEP_LABEL_LIST).orderIndependent().cfg(HOST)
+ .nonconfigurable("special attribute integrated more deeply into Bazel's core logic"))
+ .add(attr("deprecation", STRING).value(deprecationDefault)
+ .nonconfigurable("Used in core loading phase logic with no access to configs"))
+ .add(attr("tags", STRING_LIST).orderIndependent().taggable()
+ .nonconfigurable("low-level attribute, used in TargetUtils without configurations"))
+ .add(attr("generator_name", STRING).undocumented("internal"))
+ .add(attr("generator_function", STRING).undocumented("internal"))
+ .add(attr("testonly", BOOLEAN).value(testonlyDefault)
+ .nonconfigurable("policy decision: rules testability should be consistent"))
+ .add(attr(RuleClass.COMPATIBLE_ENVIRONMENT_ATTR, LABEL_LIST)
+ .allowedRuleClasses(EnvironmentRule.RULE_NAME)
+ .cfg(Attribute.ConfigurationTransition.HOST)
+ .allowedFileTypes(FileTypeSet.NO_FILE)
+ .undocumented("not yet released"))
+ .add(attr(RuleClass.RESTRICTED_ENVIRONMENT_ATTR, LABEL_LIST)
+ .allowedRuleClasses(EnvironmentRule.RULE_NAME)
+ .cfg(Attribute.ConfigurationTransition.HOST)
+ .allowedFileTypes(FileTypeSet.NO_FILE)
+ .undocumented("not yet released"));
+ }
+
+ /**
+ * Common parts of rules.
+ */
+ @BlazeRule(name = "$base_rule",
+ type = RuleClassType.ABSTRACT)
+ public static final class BaseRule implements RuleDefinition {
+ @Override
+ public RuleClass build(RuleClass.Builder builder, RuleDefinitionEnvironment env) {
+ return commonCoreAndSkylarkAttributes(builder)
+ // The name attribute is handled specially, so it does not appear here.
+ //
+ // Aggregates the labels of all {@link ConfigRuleClasses} rules this rule uses (e.g.
+ // keys for configurable attributes). This is specially populated in
+ // {@RuleClass#populateRuleAttributeValues}.
+ //
+ // This attribute is not needed for actual builds. Its main purpose is so query's
+ // proto/XML output includes the labels of config dependencies, so, e.g., depserver
+ // reverse dependency lookups remain accurate. These can't just be added to the
+ // attribute definitions proto/XML queries already output because not all attributes
+ // contain labels.
+ //
+ // Builds and Blaze-interactive queries don't need this because they find dependencies
+ // through direct Rule label visitation, which already factors these in.
+ .add(attr("$config_dependencies", LABEL_LIST)
+ .nonconfigurable("not intended for actual builds"))
+ .add(attr("licenses", LICENSE)
+ .nonconfigurable("Used in core loading phase logic with no access to configs"))
+ .add(attr("distribs", DISTRIBUTIONS)
+ .nonconfigurable("Used in core loading phase logic with no access to configs"))
+ .add(attr("obsolete", BOOLEAN).value(obsoleteDefault)
+ .nonconfigurable("Used in core loading phase logic with no access to configs"))
+ .add(attr(":action_listener", LABEL_LIST).cfg(HOST).value(ACTION_LISTENER))
+ .build();
+ }
+ }
+
+ /**
+ * Common ancestor class for all rules.
+ */
+ @BlazeRule(name = "$rule",
+ type = RuleClassType.ABSTRACT,
+ ancestors = { BaseRule.class })
+ public static final class RuleBase implements RuleDefinition {
+ @Override
+ public RuleClass build(Builder builder, RuleDefinitionEnvironment env) {
+ return builder
+ .add(attr("deps", LABEL_LIST).legacyAllowAnyFileType())
+ .add(attr("data", LABEL_LIST).cfg(DATA).allowedFileTypes(FileTypeSet.ANY_FILE))
+ .build();
+ }
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/BaselineCoverageArtifactsProvider.java b/src/main/java/com/google/devtools/build/lib/analysis/BaselineCoverageArtifactsProvider.java
new file mode 100644
index 0000000..ab74581
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/BaselineCoverageArtifactsProvider.java
@@ -0,0 +1,42 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+
+/**
+ * A {@link TransitiveInfoProvider} that has baseline coverage artifacts.
+ */
+@Immutable
+public final class BaselineCoverageArtifactsProvider implements TransitiveInfoProvider {
+ private final ImmutableList<Artifact> baselineCoverageArtifacts;
+
+ public BaselineCoverageArtifactsProvider(ImmutableList<Artifact> baselineCoverageArtifacts) {
+ this.baselineCoverageArtifacts = baselineCoverageArtifacts;
+ }
+
+ /**
+ * Returns a set of baseline coverage artifacts for a given set of configured targets.
+ *
+ * <p>These artifacts represent "empty" code coverage data for non-test libraries and binaries and
+ * used to establish correct baseline when calculating code coverage ratios since they would cover
+ * completely non-tested code as well.
+ */
+ public ImmutableList<Artifact> getBaselineCoverageArtifacts() {
+ return baselineCoverageArtifacts;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/BlazeDirectories.java b/src/main/java/com/google/devtools/build/lib/analysis/BlazeDirectories.java
new file mode 100644
index 0000000..38a8d41
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/BlazeDirectories.java
@@ -0,0 +1,183 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.Constants;
+import com.google.devtools.build.lib.actions.Root;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.util.StringCanonicalizer;
+import com.google.devtools.build.lib.vfs.FileSystem;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.io.Serializable;
+
+import javax.annotation.Nullable;
+
+/**
+ * Encapsulation of all of the interesting top-level directories in any Blaze application.
+ *
+ * <p>The <code>installBase</code> is the directory where the Blaze binary has been installed.The
+ * <code>workspace</code> is the top-level directory in the user's client (possibly read-only).The
+ * <code>outputBase</code> is the directory below which Blaze puts all its state. The
+ * <code>execRoot</code> is the working directory for all spawned tools, which is generally below
+ * <code>outputBase</code>.
+ *
+ * <p>There is a 1:1 correspondence between a running Blaze instance and an output base directory;
+ * however, multiple Blaze instances may compile code that's in the same workspace, even on the same
+ * machine. If the user does not qualify an output base directory, the startup code will derive it
+ * deterministically from the workspace. Note also that while the Blaze server process runs with the
+ * workspace directory as its working directory, the client process may have a different working
+ * directory, typically a subdirectory.
+ *
+ * <p>Do not put shortcuts to specific files here!
+ */
+@Immutable
+public final class BlazeDirectories implements Serializable {
+
+ // Output directory name, relative to the execRoot.
+ // TODO(bazel-team): (2011) make this private?
+ public static final String RELATIVE_OUTPUT_PATH = StringCanonicalizer.intern(
+ Constants.PRODUCT_NAME + "-out");
+
+ // Include directory name, relative to execRoot/blaze-out/configuration.
+ public static final String RELATIVE_INCLUDE_DIR = StringCanonicalizer.intern("include");
+
+ private final Path installBase; // Where Blaze gets unpacked
+ private final Path workspace; // Workspace root and server CWD
+ private final Path outputBase; // The root of the temp and output trees
+ private final Path execRoot; // the root of all build actions
+
+ // These two are kept to avoid creating new objects every time they are accessed. This showed up
+ // in a profiler.
+ private final Path outputPath;
+ private final Path localOutputPath;
+
+ public BlazeDirectories(Path installBase, Path outputBase, @Nullable Path workspace) {
+ this.installBase = installBase;
+ this.workspace = workspace;
+ this.outputBase = outputBase;
+ if (this.workspace == null) {
+ // TODO(bazel-team): this should be null, but at the moment there is a lot of code that
+ // depends on it being non-null.
+ this.execRoot = outputBase.getChild("default-exec-root");
+ } else {
+ this.execRoot = outputBase.getChild(workspace.getBaseName());
+ }
+ this.outputPath = execRoot.getRelative(RELATIVE_OUTPUT_PATH);
+ Preconditions.checkState(this.workspace == null || outputPath.asFragment().equals(
+ outputPathFromOutputBase(outputBase.asFragment(), workspace.asFragment())));
+ this.localOutputPath = outputBase.getRelative(BlazeDirectories.RELATIVE_OUTPUT_PATH);
+ }
+
+ /**
+ * Returns the Filesystem that all of our directories belong to. Handy for
+ * resolving absolute paths.
+ */
+ public FileSystem getFileSystem() {
+ return installBase.getFileSystem();
+ }
+
+ /**
+ * Returns the installation base directory. Currently used by info command only.
+ */
+ public Path getInstallBase() {
+ return installBase;
+ }
+
+ /**
+ * Returns the workspace directory, which is also the working dir of the server.
+ */
+ public Path getWorkspace() {
+ return workspace;
+ }
+
+ /**
+ * Returns if the workspace directory is a valid workspace.
+ */
+ public boolean inWorkspace() {
+ return this.workspace != null;
+ }
+
+ /**
+ * Returns the base of the output tree, which hosts all build and scratch
+ * output for a user and workspace.
+ */
+ public Path getOutputBase() {
+ return outputBase;
+ }
+
+ /**
+ * Returns the execution root. This is the directory underneath which Blaze builds the source
+ * symlink forest, to represent the merged view of different workspaces specified
+ * with --package_path.
+ */
+ public Path getExecRoot() {
+ return execRoot;
+ }
+
+ /**
+ * Returns the output path used by this Blaze instance.
+ */
+ public Path getOutputPath() {
+ return outputPath;
+ }
+
+ /**
+ * @param outputBase the outputBase as a path fragment.
+ * @param workspace the workspace as a path fragment.
+ * @return the outputPath as a path fragment, given the outputBase.
+ */
+ public static PathFragment outputPathFromOutputBase(
+ PathFragment outputBase, PathFragment workspace) {
+ if (workspace.equals(PathFragment.EMPTY_FRAGMENT)) {
+ return outputBase;
+ }
+ return outputBase.getRelative(workspace.getBaseName() + "/" + RELATIVE_OUTPUT_PATH);
+ }
+
+ /**
+ * Returns the local output path used by this Blaze instance.
+ */
+ public Path getLocalOutputPath() {
+ return localOutputPath;
+ }
+
+ /**
+ * Returns the directory where the stdout/stderr for actions can be stored
+ * temporarily for a build. If the directory already exists, the directory
+ * is cleaned.
+ */
+ public Path getActionConsoleOutputDirectory() {
+ return getOutputBase().getRelative("action_outs");
+ }
+
+ /**
+ * Returns the installed embedded binaries directory, under the shared
+ * installBase location.
+ */
+ public Path getEmbeddedBinariesRoot() {
+ return installBase.getChild("_embedded_binaries");
+ }
+
+ /**
+ * Returns the configuration-independent root where the build-data should be placed, given the
+ * {@link BlazeDirectories} of this server instance. Nothing else should be placed here.
+ */
+ public Root getBuildDataDirectory() {
+ return Root.asDerivedRoot(getExecRoot(), getOutputPath());
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/BlazeRule.java b/src/main/java/com/google/devtools/build/lib/analysis/BlazeRule.java
new file mode 100644
index 0000000..c349e65
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/BlazeRule.java
@@ -0,0 +1,55 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.devtools.build.lib.packages.RuleClass.Builder.RuleClassType;
+import com.google.devtools.build.lib.rules.RuleConfiguredTargetFactory;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * An annotation for rule classes.
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface BlazeRule {
+ /**
+ * The name of the rule, as it appears in the BUILD file. If it starts with
+ * '$', the rule will be hidden from users and will only be usable from
+ * inside Blaze.
+ */
+ String name();
+
+ /**
+ * The type of the rule. It can be an abstract rule, a normal rule or a test
+ * rule. If the rule type is abstract, the configured class must not be set.
+ */
+ RuleClassType type() default RuleClassType.NORMAL;
+
+ /**
+ * The {@link RuleConfiguredTargetFactory} class that implements this rule. If the rule is
+ * abstract, this must not be set.
+ */
+ Class<? extends RuleConfiguredTargetFactory> factoryClass()
+ default RuleConfiguredTargetFactory.class;
+
+ /**
+ * The list of other rule classes this rule inherits from.
+ */
+ Class<? extends RuleDefinition>[] ancestors() default {};
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/BlazeVersionInfo.java b/src/main/java/com/google/devtools/build/lib/analysis/BlazeVersionInfo.java
new file mode 100644
index 0000000..d5b5f94
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/BlazeVersionInfo.java
@@ -0,0 +1,113 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import com.google.devtools.build.lib.util.StringUtilities;
+
+import java.util.Map;
+import java.util.logging.Logger;
+
+/**
+ * Determines the version information of the current process.
+ *
+ * <p>The version information is a dictionary mapping from string keys to string values. For
+ * build stamping, it should have the key "Build label", which contains among others a
+ * XXXXXXX-YYYY.MM.DD string to indicate the version of the release. If no data is available
+ * (eg. when running non-released version), {@link #isAvailable()} returns false.
+ */
+public class BlazeVersionInfo {
+ private final Map<String, String> buildData = Maps.newTreeMap();
+ private static BlazeVersionInfo instance = null;
+ private static final String BUILD_LABEL = "Build label";
+
+ private static final Logger LOG = Logger.getLogger(BlazeVersionInfo.class.getName());
+
+ public BlazeVersionInfo(Map<String, String> info) {
+ buildData.putAll(info);
+ }
+
+ /**
+ * Accessor method for BlazeVersionInfo singleton.
+ *
+ * <p>If setBuildInfo was not called, returns an empty BlazeVersionInfo instance, which should
+ * not be persisted.
+ */
+ public static synchronized BlazeVersionInfo instance() {
+ if (instance == null) {
+ return new BlazeVersionInfo(ImmutableMap.<String, String>of());
+ }
+ return instance;
+ }
+
+ private static void logVersionInfo(BlazeVersionInfo info) {
+ if (info.getSummary() == null) {
+ LOG.warning("Blaze release version information not available");
+ } else {
+ LOG.info("Blaze version info: " + info.getSummary());
+ }
+ }
+
+ /**
+ * Sets build info.
+ *
+ * <p>This should be called once in the program execution, as early soon as possible, so we
+ * can have the version information even before modules are initialized.
+ */
+ public static synchronized void setBuildInfo(Map<String, String> info) {
+ if (instance != null) {
+ throw new IllegalStateException("setBuildInfo called twice.");
+ }
+ instance = new BlazeVersionInfo(info);
+ logVersionInfo(instance);
+ }
+
+ /**
+ * Indicates whether version information is available.
+ */
+ public boolean isAvailable() {
+ return !buildData.isEmpty();
+ }
+
+ /**
+ * Returns the summary which gets displayed in the 'version' command.
+ * The summary is a list of formatted key / value pairs.
+ */
+ public String getSummary() {
+ if (buildData.isEmpty()) {
+ return null;
+ }
+ return StringUtilities.layoutTable(buildData);
+ }
+
+ /**
+ * Returns true iff this binary is released--that is, a
+ * binary built with a release label.
+ */
+ public boolean isReleasedBlaze() {
+ String buildLabel = buildData.get(BUILD_LABEL);
+ return buildLabel != null && buildLabel.length() > 0;
+ }
+
+ /**
+ * Returns the release label, if any, or "development version".
+ */
+ public String getReleaseName() {
+ String buildLabel = buildData.get(BUILD_LABEL);
+ return (buildLabel != null && buildLabel.length() > 0)
+ ? "release " + buildLabel
+ : "development version";
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/BuildInfoEvent.java b/src/main/java/com/google/devtools/build/lib/analysis/BuildInfoEvent.java
new file mode 100644
index 0000000..59d1514
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/BuildInfoEvent.java
@@ -0,0 +1,40 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.common.collect.ImmutableMap;
+
+import java.util.Map;
+
+/**
+ * This event is fired once build info data is available.
+ */
+public final class BuildInfoEvent {
+ private final Map<String, String> buildInfoMap;
+
+ /**
+ * Construct the event from a map.
+ */
+ public BuildInfoEvent(Map<String, String> buildInfo) {
+ buildInfoMap = ImmutableMap.copyOf(buildInfo);
+ }
+
+ /**
+ * Return immutable map populated with build info key/value pairs.
+ */
+ public Map<String, String> getBuildInfoMap() {
+ return buildInfoMap;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/BuildInfoHelper.java b/src/main/java/com/google/devtools/build/lib/analysis/BuildInfoHelper.java
new file mode 100644
index 0000000..755df9f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/BuildInfoHelper.java
@@ -0,0 +1,31 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.devtools.build.lib.actions.AbstractActionOwner;
+import com.google.devtools.build.lib.actions.ActionOwner;
+
+// TODO(bazel-team): move BUILD_INFO_ACTION_OWNER somewhere else and remove this class.
+/**
+ * Helper class for the CompatibleWriteBuildInfoAction, which holds the
+ * methods for generating build information.
+ * Abstracted away to allow non-action code to also generate build info under
+ * --nobuild or --check_up_to_date.
+ */
+public abstract class BuildInfoHelper {
+ /** ActionOwner for BuildInfoActions. */
+ public static final ActionOwner BUILD_INFO_ACTION_OWNER =
+ AbstractActionOwner.SYSTEM_ACTION_OWNER;
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/BuildView.java b/src/main/java/com/google/devtools/build/lib/analysis/BuildView.java
new file mode 100644
index 0000000..052d0a2
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/BuildView.java
@@ -0,0 +1,1056 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Multimaps;
+import com.google.common.collect.Sets;
+import com.google.common.eventbus.EventBus;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.actions.ActionGraph;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.ArtifactFactory;
+import com.google.devtools.build.lib.actions.PackageRootResolver;
+import com.google.devtools.build.lib.actions.Root;
+import com.google.devtools.build.lib.analysis.DependencyResolver.Dependency;
+import com.google.devtools.build.lib.analysis.ExtraActionArtifactsProvider.ExtraArtifactSet;
+import com.google.devtools.build.lib.analysis.config.BinTools;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.analysis.config.BuildConfigurationCollection;
+import com.google.devtools.build.lib.analysis.config.ConfigMatchingProvider;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadCompatible;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.events.DelegatingEventHandler;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.events.EventKind;
+import com.google.devtools.build.lib.events.StoredEventHandler;
+import com.google.devtools.build.lib.events.WarningsAsErrorsEventHandler;
+import com.google.devtools.build.lib.packages.Attribute;
+import com.google.devtools.build.lib.packages.NoSuchPackageException;
+import com.google.devtools.build.lib.packages.NoSuchTargetException;
+import com.google.devtools.build.lib.packages.NoSuchThingException;
+import com.google.devtools.build.lib.packages.PackageIdentifier;
+import com.google.devtools.build.lib.packages.PackageSpecification;
+import com.google.devtools.build.lib.packages.RawAttributeMapper;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.packages.TargetUtils;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.pkgcache.LoadingPhaseRunner.LoadingResult;
+import com.google.devtools.build.lib.pkgcache.PackageManager;
+import com.google.devtools.build.lib.rules.test.CoverageReportActionFactory;
+import com.google.devtools.build.lib.skyframe.ConfiguredTargetKey;
+import com.google.devtools.build.lib.skyframe.CoverageReportValue;
+import com.google.devtools.build.lib.skyframe.SkyframeBuildView;
+import com.google.devtools.build.lib.skyframe.SkyframeExecutor;
+import com.google.devtools.build.lib.syntax.EvalException;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.util.RegexFilter;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.common.options.Option;
+import com.google.devtools.common.options.OptionsBase;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.logging.Logger;
+
+import javax.annotation.Nullable;
+
+/**
+ * <p>The BuildView presents a semantically-consistent and transitively-closed
+ * dependency graph for some set of packages.
+ *
+ * <h2>Package design</h2>
+ *
+ * <p>This package contains the Blaze dependency analysis framework (aka
+ * "analysis phase"). The goal of this code is to perform semantic analysis of
+ * all of the build targets required for a given build, to report
+ * errors/warnings for any problems in the input, and to construct an "action
+ * graph" (see {@code lib.actions} package) correctly representing the work to
+ * be done during the execution phase of the build.
+ *
+ * <p><b>Configurations</b> the inputs to a build come from two sources: the
+ * intrinsic inputs, specified in the BUILD file, are called <em>targets</em>.
+ * The environmental inputs, coming from the build tool, the command-line, or
+ * configuration files, are called the <em>configuration</em>. Only when a
+ * target and a configuration are combined is there sufficient information to
+ * perform a build. </p>
+ *
+ * <p>Targets are implemented by the {@link Target} hierarchy in the {@code
+ * lib.packages} code. Configurations are implemented by {@link
+ * BuildConfiguration}. The pair of these together is represented by an
+ * instance of class {@link ConfiguredTarget}; this is the root of a hierarchy
+ * with different implementations for each kind of target: source file, derived
+ * file, rules, etc.
+ *
+ * <p>The framework code in this package (as opposed to its subpackages) is
+ * responsible for constructing the {@code ConfiguredTarget} graph for a given
+ * target and configuration, taking care of such issues as:
+ * <ul>
+ * <li>caching common subgraphs.
+ * <li>detecting and reporting cycles.
+ * <li>correct propagation of errors through the graph.
+ * <li>reporting universal errors, such as dependencies from production code
+ * to tests, or to experimental branches.
+ * <li>capturing and replaying errors.
+ * <li>maintaining the graph from one build to the next to
+ * avoid unnecessary recomputation.
+ * <li>checking software licenses.
+ * </ul>
+ *
+ * <p>See also {@link ConfiguredTarget} which documents some important
+ * invariants.
+ */
+public class BuildView {
+
+ /**
+ * Options that affect the <i>mechanism</i> of analysis. These are distinct from {@link
+ * com.google.devtools.build.lib.analysis.config.BuildOptions}, which affect the <i>value</i>
+ * of a BuildConfiguration.
+ */
+ public static class Options extends OptionsBase {
+
+ @Option(name = "keep_going",
+ abbrev = 'k',
+ defaultValue = "false",
+ category = "strategy",
+ help = "Continue as much as possible after an error. While the "
+ + "target that failed, and those that depend on it, cannot be "
+ + "analyzed (or built), the other prerequisites of these "
+ + "targets can be analyzed (or built) all the same.")
+ public boolean keepGoing;
+
+ @Option(name = "analysis_warnings_as_errors",
+ defaultValue = "false",
+ category = "strategy",
+ help = "Treat visible analysis warnings as errors.")
+ public boolean analysisWarningsAsErrors;
+
+ @Option(name = "discard_analysis_cache",
+ defaultValue = "false",
+ category = "strategy",
+ help = "Discard the analysis cache immediately after the analysis phase completes. "
+ + "Reduces memory usage by ~10%, but makes further incremental builds slower.")
+ public boolean discardAnalysisCache;
+
+ @Option(name = "keep_forward_graph",
+ deprecationWarning = "keep_forward_graph is now a no-op and will be removed in an "
+ + "upcoming Blaze release",
+ defaultValue = "false",
+ category = "undocumented",
+ help = "Cache the forward action graph across builds for faster "
+ + "incremental rebuilds. May slightly increase memory while Blaze "
+ + "server is idle."
+ )
+ public boolean keepForwardGraph;
+
+ @Option(name = "experimental_extra_action_filter",
+ defaultValue = "",
+ category = "experimental",
+ converter = RegexFilter.RegexFilterConverter.class,
+ help = "Filters set of targets to schedule extra_actions for.")
+ public RegexFilter extraActionFilter;
+
+ @Option(name = "experimental_extra_action_top_level_only",
+ defaultValue = "false",
+ category = "experimental",
+ help = "Only schedules extra_actions for top level targets.")
+ public boolean extraActionTopLevelOnly;
+
+ @Option(name = "version_window_for_dirty_node_gc",
+ defaultValue = "0",
+ category = "undocumented",
+ help = "Nodes that have been dirty for more than this many versions will be deleted"
+ + " from the graph upon the next update. Values must be non-negative long integers,"
+ + " or -1 indicating the maximum possible window.")
+ public long versionWindowForDirtyNodeGc;
+ }
+
+ private static Logger LOG = Logger.getLogger(BuildView.class.getName());
+
+ private final BlazeDirectories directories;
+
+ private final SkyframeExecutor skyframeExecutor;
+ private final SkyframeBuildView skyframeBuildView;
+
+ private final PackageManager packageManager;
+
+ private final BinTools binTools;
+
+ private BuildConfigurationCollection configurations;
+
+ private final ConfiguredRuleClassProvider ruleClassProvider;
+
+ private final ArtifactFactory artifactFactory;
+
+ /**
+ * A factory class to create the coverage report action. May be null.
+ */
+ @Nullable private final CoverageReportActionFactory coverageReportActionFactory;
+
+ /**
+ * A union of package roots of all previous incremental analysis results. This is used to detect
+ * changes of package roots between incremental analysis instances.
+ */
+ private final Map<PackageIdentifier, Path> cumulativePackageRoots = new HashMap<>();
+
+ /**
+ * Used only for testing that we clear Skyframe caches correctly.
+ * TODO(bazel-team): Remove this once we get rid of legacy Skyframe synchronization.
+ */
+ private boolean skyframeCacheWasInvalidated = false;
+
+ /**
+ * If the last build was executed with {@code Options#discard_analysis_cache} and we are not
+ * running Skyframe full, we should clear the legacy data since it is out-of-sync.
+ */
+ private boolean skyframeAnalysisWasDiscarded = false;
+
+ @VisibleForTesting
+ public Set<SkyKey> getSkyframeEvaluatedTargetKeysForTesting() {
+ return skyframeBuildView.getEvaluatedTargetKeys();
+ }
+
+ /** The number of targets freshly evaluated in the last analysis run. */
+ public int getTargetsVisited() {
+ return skyframeBuildView.getEvaluatedTargetKeys().size();
+ }
+
+ /**
+ * Returns true iff Skyframe was invalidated during the analysis phase.
+ * TODO(bazel-team): Remove this once we do not need to keep legacy in sync with Skyframe.
+ */
+ @VisibleForTesting
+ boolean wasSkyframeCacheInvalidatedDuringAnalysis() {
+ return skyframeCacheWasInvalidated;
+ }
+
+ public BuildView(BlazeDirectories directories, PackageManager packageManager,
+ ConfiguredRuleClassProvider ruleClassProvider,
+ SkyframeExecutor skyframeExecutor,
+ BinTools binTools, CoverageReportActionFactory coverageReportActionFactory) {
+ this.directories = directories;
+ this.packageManager = packageManager;
+ this.binTools = binTools;
+ this.coverageReportActionFactory = coverageReportActionFactory;
+ this.artifactFactory = new ArtifactFactory(directories.getExecRoot());
+ this.ruleClassProvider = ruleClassProvider;
+ this.skyframeExecutor = Preconditions.checkNotNull(skyframeExecutor);
+ this.skyframeBuildView =
+ new SkyframeBuildView(
+ new ConfiguredTargetFactory(ruleClassProvider),
+ artifactFactory,
+ skyframeExecutor,
+ new Runnable() {
+ @Override
+ public void run() {
+ clear();
+ }
+ },
+ binTools);
+ skyframeExecutor.setSkyframeBuildView(skyframeBuildView);
+ }
+
+ /** Returns the action graph. */
+ public ActionGraph getActionGraph() {
+ return new ActionGraph() {
+ @Override
+ public Action getGeneratingAction(Artifact artifact) {
+ return skyframeExecutor.getGeneratingAction(artifact);
+ }
+ };
+ }
+
+ /**
+ * Returns whether the given configured target has errors.
+ */
+ @VisibleForTesting
+ public boolean hasErrors(ConfiguredTarget configuredTarget) {
+ return configuredTarget == null;
+ }
+
+ /**
+ * Sets the configurations. Not thread-safe. DO NOT CALL except from tests!
+ */
+ @VisibleForTesting
+ void setConfigurationsForTesting(BuildConfigurationCollection configurations) {
+ this.configurations = configurations;
+ }
+
+ public BuildConfigurationCollection getConfigurationCollection() {
+ return configurations;
+ }
+
+ /**
+ * Clear the graphs of ConfiguredTargets and Artifacts.
+ */
+ @VisibleForTesting
+ public void clear() {
+ cumulativePackageRoots.clear();
+ artifactFactory.clear();
+ }
+
+ public ArtifactFactory getArtifactFactory() {
+ return artifactFactory;
+ }
+
+ @VisibleForTesting
+ WorkspaceStatusAction getLastWorkspaceBuildInfoActionForTesting() {
+ return skyframeExecutor.getLastWorkspaceStatusActionForTesting();
+ }
+
+ /**
+ * Returns a corresponding ConfiguredTarget, if one exists; otherwise throws an {@link
+ * NoSuchConfiguredTargetException}.
+ */
+ @ThreadSafe
+ private ConfiguredTarget getConfiguredTarget(Target target, BuildConfiguration config)
+ throws NoSuchConfiguredTargetException {
+ ConfiguredTarget result =
+ getExistingConfiguredTarget(target.getLabel(), config);
+ if (result == null) {
+ throw new NoSuchConfiguredTargetException(target.getLabel(), config);
+ }
+ return result;
+ }
+
+ /**
+ * Obtains a {@link ConfiguredTarget} given a {@code label}, by delegating
+ * to the package cache and
+ * {@link #getConfiguredTarget(Target, BuildConfiguration)}.
+ */
+ public ConfiguredTarget getConfiguredTarget(Label label, BuildConfiguration config)
+ throws NoSuchPackageException, NoSuchTargetException, NoSuchConfiguredTargetException {
+ return getConfiguredTarget(packageManager.getLoadedTarget(label), config);
+ }
+
+ public Iterable<ConfiguredTarget> getDirectPrerequisites(ConfiguredTarget ct) {
+ return getDirectPrerequisites(ct, null);
+ }
+
+ public Iterable<ConfiguredTarget> getDirectPrerequisites(ConfiguredTarget ct,
+ @Nullable final LoadingCache<Label, Target> targetCache) {
+ if (!(ct.getTarget() instanceof Rule)) {
+ return ImmutableList.of();
+ }
+
+ class SilentDependencyResolver extends DependencyResolver {
+ @Override
+ protected void invalidVisibilityReferenceHook(TargetAndConfiguration node, Label label) {
+ // The error must have been reported already during analysis.
+ }
+
+ @Override
+ protected void invalidPackageGroupReferenceHook(TargetAndConfiguration node, Label label) {
+ // The error must have been reported already during analysis.
+ }
+
+ @Override
+ protected Target getTarget(Label label) throws NoSuchThingException {
+ if (targetCache == null) {
+ return packageManager.getLoadedTarget(label);
+ }
+
+ try {
+ return targetCache.get(label);
+ } catch (ExecutionException e) {
+ // All lookups should succeed because we should not be looking up any targets in error.
+ throw new IllegalStateException(e);
+ }
+ }
+ }
+
+ DependencyResolver dependencyResolver = new SilentDependencyResolver();
+ TargetAndConfiguration ctgNode =
+ new TargetAndConfiguration(ct.getTarget(), ct.getConfiguration());
+ return skyframeExecutor.getConfiguredTargets(
+ dependencyResolver.dependentNodes(ctgNode, getConfigurableAttributeKeys(ctgNode)));
+ }
+
+ /**
+ * Returns ConfigMatchingProvider instances corresponding to the configurable attribute keys
+ * present in this rule's attributes.
+ */
+ private Set<ConfigMatchingProvider> getConfigurableAttributeKeys(TargetAndConfiguration ctg) {
+ if (!(ctg.getTarget() instanceof Rule)) {
+ return ImmutableSet.of();
+ }
+ Rule rule = (Rule) ctg.getTarget();
+ ImmutableSet.Builder<ConfigMatchingProvider> keys = ImmutableSet.builder();
+ RawAttributeMapper mapper = RawAttributeMapper.of(rule);
+ for (Attribute attribute : rule.getAttributes()) {
+ for (Label label : mapper.getConfigurabilityKeys(attribute.getName(), attribute.getType())) {
+ if (Type.Selector.isReservedLabel(label)) {
+ continue;
+ }
+ try {
+ ConfiguredTarget ct = getConfiguredTarget(label, ctg.getConfiguration());
+ keys.add(Preconditions.checkNotNull(ct.getProvider(ConfigMatchingProvider.class)));
+ } catch (NoSuchPackageException e) {
+ // All lookups should succeed because we should not be looking up any targets in error.
+ throw new IllegalStateException(e);
+ } catch (NoSuchTargetException e) {
+ // All lookups should succeed because we should not be looking up any targets in error.
+ throw new IllegalStateException(e);
+ } catch (NoSuchConfiguredTargetException e) {
+ // All lookups should succeed because we should not be looking up any targets in error.
+ throw new IllegalStateException(e);
+ }
+ }
+ }
+ return keys.build();
+ }
+
+ public TransitiveInfoCollection getGeneratingRule(OutputFileConfiguredTarget target) {
+ return target.getGeneratingRule();
+ }
+
+ @Override
+ public int hashCode() {
+ throw new UnsupportedOperationException(); // avoid nondeterminism
+ }
+
+ /**
+ * Return value for {@link BuildView#update} and {@code BuildTool.prepareToBuild}.
+ */
+ public static final class AnalysisResult {
+
+ public static final AnalysisResult EMPTY = new AnalysisResult(
+ ImmutableList.<ConfiguredTarget>of(), null, null, null,
+ ImmutableList.<Artifact>of(),
+ ImmutableList.<ConfiguredTarget>of(),
+ ImmutableList.<ConfiguredTarget>of(),
+ null);
+
+ private final ImmutableList<ConfiguredTarget> targetsToBuild;
+ @Nullable private final ImmutableList<ConfiguredTarget> targetsToTest;
+ @Nullable private final String error;
+ private final ActionGraph actionGraph;
+ private final ImmutableSet<Artifact> artifactsToBuild;
+ private final ImmutableSet<ConfiguredTarget> parallelTests;
+ private final ImmutableSet<ConfiguredTarget> exclusiveTests;
+ @Nullable private final TopLevelArtifactContext topLevelContext;
+
+ private AnalysisResult(
+ Collection<ConfiguredTarget> targetsToBuild, Collection<ConfiguredTarget> targetsToTest,
+ @Nullable String error, ActionGraph actionGraph,
+ Collection<Artifact> artifactsToBuild, Collection<ConfiguredTarget> parallelTests,
+ Collection<ConfiguredTarget> exclusiveTests, TopLevelArtifactContext topLevelContext) {
+ this.targetsToBuild = ImmutableList.copyOf(targetsToBuild);
+ this.targetsToTest = targetsToTest == null ? null : ImmutableList.copyOf(targetsToTest);
+ this.error = error;
+ this.actionGraph = actionGraph;
+ this.artifactsToBuild = ImmutableSet.copyOf(artifactsToBuild);
+ this.parallelTests = ImmutableSet.copyOf(parallelTests);
+ this.exclusiveTests = ImmutableSet.copyOf(exclusiveTests);
+ this.topLevelContext = topLevelContext;
+ }
+
+ /**
+ * Returns configured targets to build.
+ */
+ public Collection<ConfiguredTarget> getTargetsToBuild() {
+ return targetsToBuild;
+ }
+
+ /**
+ * Returns the configured targets to run as tests, or {@code null} if testing was not
+ * requested (e.g. "build" command rather than "test" command).
+ */
+ @Nullable
+ public Collection<ConfiguredTarget> getTargetsToTest() {
+ return targetsToTest;
+ }
+
+ public ImmutableSet<Artifact> getAdditionalArtifactsToBuild() {
+ return artifactsToBuild;
+ }
+
+ public ImmutableSet<ConfiguredTarget> getExclusiveTests() {
+ return exclusiveTests;
+ }
+
+ public ImmutableSet<ConfiguredTarget> getParallelTests() {
+ return parallelTests;
+ }
+
+ /**
+ * Returns an error description (if any).
+ */
+ @Nullable public String getError() {
+ return error;
+ }
+
+ /**
+ * Returns the action graph.
+ */
+ public ActionGraph getActionGraph() {
+ return actionGraph;
+ }
+
+ public TopLevelArtifactContext getTopLevelContext() {
+ return topLevelContext;
+ }
+ }
+
+
+ /**
+ * Returns the collection of configured targets corresponding to any of the provided targets.
+ */
+ @VisibleForTesting
+ static Iterable<? extends ConfiguredTarget> filterTestsByTargets(
+ Collection<? extends ConfiguredTarget> targets,
+ final Set<? extends Target> allowedTargets) {
+ return Iterables.filter(targets,
+ new Predicate<ConfiguredTarget>() {
+ @Override
+ public boolean apply(ConfiguredTarget rule) {
+ return allowedTargets.contains(rule.getTarget());
+ }
+ });
+ }
+
+ private void prepareToBuild(PackageRootResolver resolver) throws ViewCreationFailedException {
+ for (BuildConfiguration config : configurations.getTargetConfigurations()) {
+ config.prepareToBuild(directories.getExecRoot(), getArtifactFactory(), resolver);
+ }
+ }
+
+ @ThreadCompatible
+ public AnalysisResult update(LoadingResult loadingResult,
+ BuildConfigurationCollection configurations, Options viewOptions,
+ TopLevelArtifactContext topLevelOptions, EventHandler eventHandler, EventBus eventBus)
+ throws ViewCreationFailedException, InterruptedException {
+
+ // Detect errors during analysis and don't attempt a build.
+ //
+ // (Errors reported during the previous step, package loading, that do
+ // not cause the visitation of the transitive closure to abort, are
+ // recoverable. For example, an error encountered while evaluating an
+ // irrelevant rule in a visited package causes an error to be reported,
+ // but visitation still succeeds.)
+ ErrorCollector errorCollector = null;
+ if (!viewOptions.keepGoing) {
+ eventHandler = errorCollector = new ErrorCollector(eventHandler);
+ }
+
+ // Treat analysis warnings as errors, to enable strict builds.
+ //
+ // Warnings reported during analysis are converted to errors, ultimately
+ // triggering failure. This check needs to be added after the keep-going check
+ // above so that it is invoked first (FIFO eventHandler chain). This way, detected
+ // warnings are converted to errors first, and then the proper error handling
+ // logic is invoked.
+ WarningsAsErrorsEventHandler warningsHandler = null;
+ if (viewOptions.analysisWarningsAsErrors) {
+ eventHandler = warningsHandler = new WarningsAsErrorsEventHandler(eventHandler);
+ }
+
+ skyframeBuildView.setWarningListener(eventHandler);
+ skyframeExecutor.setErrorEventListener(eventHandler);
+
+ LOG.info("Starting analysis");
+ pollInterruptedStatus();
+
+ skyframeBuildView.resetEvaluatedConfiguredTargetKeysSet();
+
+ Collection<Target> targets = loadingResult.getTargets();
+ eventBus.post(new AnalysisPhaseStartedEvent(targets));
+
+ skyframeCacheWasInvalidated = false;
+ // Clear all cached ConfiguredTargets on configuration change. We need to do this explicitly
+ // because we need to make sure that the legacy action graph does not contain multiple actions
+ // with different versions of the same (target/host/etc.) configuration.
+ // In the future the action graph will be probably be keyed by configurations, which should
+ // obviate the need for this workaround.
+ //
+ // Also if --discard_analysis_cache was used in the last build we want to clear the legacy
+ // data.
+ if ((this.configurations != null && !configurations.equals(this.configurations))
+ || skyframeAnalysisWasDiscarded) {
+ skyframeExecutor.dropConfiguredTargets();
+ skyframeCacheWasInvalidated = true;
+ clear();
+ }
+ skyframeAnalysisWasDiscarded = false;
+ ImmutableMap<PackageIdentifier, Path> packageRoots = loadingResult.getPackageRoots();
+
+ if (buildHasIncompatiblePackageRoots(packageRoots)) {
+ // When a package root changes source artifacts with the new root will be created, but we
+ // cannot be sure that there are no references remaining to the corresponding artifacts
+ // with the old root. To avoid that scenario, the analysis cache is simply dropped when
+ // a package root change is detected.
+ LOG.info("Discarding analysis cache: package roots have changed.");
+
+ skyframeExecutor.dropConfiguredTargets();
+ skyframeCacheWasInvalidated = true;
+ clear();
+ }
+ cumulativePackageRoots.putAll(packageRoots);
+ this.configurations = configurations;
+ setArtifactRoots(packageRoots);
+
+ // Determine the configurations.
+ List<TargetAndConfiguration> nodes = nodesForTargets(targets);
+
+ List<ConfiguredTargetKey> targetSpecs =
+ Lists.transform(nodes, new Function<TargetAndConfiguration, ConfiguredTargetKey>() {
+ @Override
+ public ConfiguredTargetKey apply(TargetAndConfiguration node) {
+ return new ConfiguredTargetKey(node.getLabel(), node.getConfiguration());
+ }
+ });
+
+ prepareToBuild(new SkyframePackageRootResolver(skyframeExecutor));
+ skyframeBuildView.setWarningListener(warningsHandler);
+ skyframeExecutor.injectWorkspaceStatusData();
+ Collection<ConfiguredTarget> configuredTargets;
+ try {
+ configuredTargets = skyframeBuildView.configureTargets(
+ targetSpecs, eventBus, viewOptions.keepGoing);
+ } finally {
+ skyframeBuildView.clearInvalidatedConfiguredTargets();
+ }
+
+ int numTargetsToAnalyze = nodes.size();
+ int numSuccessful = configuredTargets.size();
+ boolean analysisSuccessful = (numSuccessful == numTargetsToAnalyze);
+ if (0 < numSuccessful && numSuccessful < numTargetsToAnalyze) {
+ String msg = String.format("Analysis succeeded for only %d of %d top-level targets",
+ numSuccessful, numTargetsToAnalyze);
+ eventHandler.handle(Event.info(msg));
+ LOG.info(msg);
+ }
+
+ postUpdateValidation(errorCollector, warningsHandler);
+
+ AnalysisResult result = createResult(loadingResult, topLevelOptions,
+ viewOptions, configuredTargets, analysisSuccessful);
+ LOG.info("Finished analysis");
+ return result;
+ }
+
+ // Validates that the update has been done correctly
+ private void postUpdateValidation(ErrorCollector errorCollector,
+ WarningsAsErrorsEventHandler warningsHandler) throws ViewCreationFailedException {
+ if (warningsHandler != null && warningsHandler.warningsEncountered()) {
+ throw new ViewCreationFailedException("Warnings being treated as errors");
+ }
+
+ if (errorCollector != null && !errorCollector.getEvents().isEmpty()) {
+ // This assertion ensures that if any errors were reported during the
+ // initialization phase, the call to configureTargets will fail with a
+ // ViewCreationFailedException. Violation of this invariant leads to
+ // incorrect builds, because the fact that errors were encountered is not
+ // properly recorded in the view (i.e. the graph of configured targets).
+ // Rule errors must be reported via RuleConfiguredTarget.reportError,
+ // which causes the rule's hasErrors() flag to be set, and thus the
+ // hasErrors() flag of anything that depends on it transitively. If the
+ // toplevel rule hasErrors, then analysis is aborted and we do not
+ // proceed to the execution phase of a build.
+ //
+ // Reporting errors directly through the Reporter does not set the error
+ // flag, so analysis may succeed spuriously, allowing the execution
+ // phase to begin with unpredictable consequences.
+ //
+ // The use of errorCollector (rather than an ErrorSensor) makes the
+ // assertion failure messages more informative.
+ // Note we tolerate errors iff --keep-going, because some of the
+ // requested targets may have had problems during analysis, but that's ok.
+ StringBuilder message = new StringBuilder("Unexpected errors reported during analysis:");
+ for (Event event : errorCollector.getEvents()) {
+ message.append('\n').append(event);
+ }
+ throw new IllegalStateException(message.toString());
+ }
+ }
+
+ /**
+ * Skyframe implementation of {@link PackageRootResolver}.
+ *
+ * <p> Note: you should not use this class inside of any SkyFunction.
+ */
+ @VisibleForTesting
+ public static final class SkyframePackageRootResolver implements PackageRootResolver {
+ private final SkyframeExecutor executor;
+
+ public SkyframePackageRootResolver(SkyframeExecutor executor) {
+ this.executor = executor;
+ }
+
+ @Override
+ public Map<PathFragment, Root> findPackageRoots(Iterable<PathFragment> execPaths) {
+ return executor.getArtifactRoots(execPaths);
+ }
+ }
+
+ private AnalysisResult createResult(LoadingResult loadingResult,
+ TopLevelArtifactContext topLevelOptions, BuildView.Options viewOptions,
+ Collection<ConfiguredTarget> configuredTargets, boolean analysisSuccessful)
+ throws InterruptedException {
+ Collection<Target> testsToRun = loadingResult.getTestsToRun();
+ Collection<ConfiguredTarget> allTargetsToTest = null;
+ if (testsToRun != null) {
+ // Determine the subset of configured targets that are meant to be run as tests.
+ allTargetsToTest = Lists.newArrayList(
+ filterTestsByTargets(configuredTargets, Sets.newHashSet(testsToRun)));
+ }
+
+ skyframeExecutor.injectTopLevelContext(topLevelOptions);
+
+ Set<Artifact> artifactsToBuild = new HashSet<>();
+ Set<ConfiguredTarget> parallelTests = new HashSet<>();
+ Set<ConfiguredTarget> exclusiveTests = new HashSet<>();
+ Collection<Artifact> buildInfoArtifacts;
+ buildInfoArtifacts = skyframeExecutor.getWorkspaceStatusArtifacts();
+ // build-info and build-changelist.
+ Preconditions.checkState(buildInfoArtifacts.size() == 2, buildInfoArtifacts);
+ artifactsToBuild.addAll(buildInfoArtifacts);
+ addExtraActionsIfRequested(viewOptions, artifactsToBuild, configuredTargets);
+ if (coverageReportActionFactory != null) {
+ Action action = coverageReportActionFactory.createCoverageReportAction(
+ allTargetsToTest,
+ getBaselineCoverageArtifacts(configuredTargets),
+ artifactFactory,
+ CoverageReportValue.ARTIFACT_OWNER);
+ if (action != null) {
+ skyframeExecutor.injectCoverageReportData(action);
+ artifactsToBuild.addAll(action.getOutputs());
+ }
+ }
+
+ // Note that this must come last, so that the tests are scheduled after all artifacts are built.
+ scheduleTestsIfRequested(parallelTests, exclusiveTests, topLevelOptions, allTargetsToTest);
+
+ String error = !loadingResult.hasLoadingError()
+ ? (analysisSuccessful
+ ? null
+ : "execution phase succeeded, but not all targets were analyzed")
+ : "execution phase succeeded, but there were loading phase errors";
+ return new AnalysisResult(configuredTargets, allTargetsToTest, error, getActionGraph(),
+ artifactsToBuild, parallelTests, exclusiveTests, topLevelOptions);
+ }
+
+ private static ImmutableSet<Artifact> getBaselineCoverageArtifacts(
+ Collection<ConfiguredTarget> configuredTargets) {
+ Set<Artifact> baselineCoverageArtifacts = Sets.newHashSet();
+ for (ConfiguredTarget target : configuredTargets) {
+ BaselineCoverageArtifactsProvider provider =
+ target.getProvider(BaselineCoverageArtifactsProvider.class);
+ if (provider != null) {
+ baselineCoverageArtifacts.addAll(provider.getBaselineCoverageArtifacts());
+ }
+ }
+ return ImmutableSet.copyOf(baselineCoverageArtifacts);
+ }
+
+ private void addExtraActionsIfRequested(BuildView.Options viewOptions,
+ Set<Artifact> artifactsToBuild, Iterable<ConfiguredTarget> topLevelTargets) {
+ NestedSetBuilder<ExtraArtifactSet> builder = NestedSetBuilder.stableOrder();
+ for (ConfiguredTarget topLevel : topLevelTargets) {
+ ExtraActionArtifactsProvider provider = topLevel.getProvider(
+ ExtraActionArtifactsProvider.class);
+ if (provider != null) {
+ if (viewOptions.extraActionTopLevelOnly) {
+ builder.add(ExtraArtifactSet.of(topLevel.getLabel(), provider.getExtraActionArtifacts()));
+ } else {
+ builder.addTransitive(provider.getTransitiveExtraActionArtifacts());
+ }
+ }
+ }
+
+ RegexFilter filter = viewOptions.extraActionFilter;
+ for (ExtraArtifactSet set : builder.build()) {
+ boolean filterMatches = filter == null || filter.isIncluded(set.getLabel().toString());
+ if (filterMatches) {
+ Iterables.addAll(artifactsToBuild, set.getArtifacts());
+ }
+ }
+ }
+
+ private static void scheduleTestsIfRequested(Collection<ConfiguredTarget> targetsToTest,
+ Collection<ConfiguredTarget> targetsToTestExclusive, TopLevelArtifactContext topLevelOptions,
+ Collection<ConfiguredTarget> allTestTargets) {
+ if (!topLevelOptions.compileOnly() && !topLevelOptions.compilationPrerequisitesOnly()
+ && allTestTargets != null) {
+ scheduleTests(targetsToTest, targetsToTestExclusive, allTestTargets,
+ topLevelOptions.runTestsExclusively());
+ }
+ }
+
+
+ /**
+ * Returns set of artifacts representing test results, writing into targetsToTest and
+ * targetsToTestExclusive.
+ */
+ private static void scheduleTests(Collection<ConfiguredTarget> targetsToTest,
+ Collection<ConfiguredTarget> targetsToTestExclusive,
+ Collection<ConfiguredTarget> allTestTargets,
+ boolean isExclusive) {
+ for (ConfiguredTarget target : allTestTargets) {
+ if (target.getTarget() instanceof Rule) {
+ boolean exclusive =
+ isExclusive || TargetUtils.isExclusiveTestRule((Rule) target.getTarget());
+ Collection<ConfiguredTarget> testCollection = exclusive
+ ? targetsToTestExclusive
+ : targetsToTest;
+ testCollection.add(target);
+ }
+ }
+ }
+
+ @VisibleForTesting
+ List<TargetAndConfiguration> nodesForTargets(Collection<Target> targets) {
+ // We use a hash set here to remove duplicate nodes; this can happen for input files and package
+ // groups.
+ LinkedHashSet<TargetAndConfiguration> nodes = new LinkedHashSet<>(targets.size());
+ for (BuildConfiguration config : configurations.getTargetConfigurations()) {
+ for (Target target : targets) {
+ nodes.add(new TargetAndConfiguration(target,
+ BuildConfigurationCollection.configureTopLevelTarget(config, target)));
+ }
+ }
+ return ImmutableList.copyOf(nodes);
+ }
+
+ /**
+ * Detects when a package root changes between instances of incremental analysis.
+ *
+ * <p>This case is currently problematic for incremental analysis because when a package root
+ * changes, source artifacts with the new root will be created, but we can not be sure that there
+ * are no references remaining to the corresponding artifacts with the old root.
+ */
+ private boolean buildHasIncompatiblePackageRoots(Map<PackageIdentifier, Path> packageRoots) {
+ for (Map.Entry<PackageIdentifier, Path> entry : packageRoots.entrySet()) {
+ Path prevRoot = cumulativePackageRoots.get(entry.getKey());
+ if (prevRoot != null && !entry.getValue().equals(prevRoot)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns an existing ConfiguredTarget for the specified target and
+ * configuration, or null if none exists. No validity check is done.
+ */
+ @ThreadSafe
+ public ConfiguredTarget getExistingConfiguredTarget(Target target, BuildConfiguration config) {
+ return getExistingConfiguredTarget(target.getLabel(), config);
+ }
+
+ /**
+ * Returns an existing ConfiguredTarget for the specified node, or null if none exists. No
+ * validity check is done.
+ */
+ @ThreadSafe
+ private ConfiguredTarget getExistingConfiguredTarget(
+ Label label, BuildConfiguration configuration) {
+ return Iterables.getFirst(
+ skyframeExecutor.getConfiguredTargets(
+ ImmutableList.of(new Dependency(label, configuration))),
+ null);
+ }
+
+ @VisibleForTesting
+ ListMultimap<Attribute, ConfiguredTarget> getPrerequisiteMapForTesting(ConfiguredTarget target) {
+ DependencyResolver resolver = new DependencyResolver() {
+ @Override
+ protected void invalidVisibilityReferenceHook(TargetAndConfiguration node, Label label) {
+ throw new RuntimeException("bad visibility on " + label + " during testing unexpected");
+ }
+
+ @Override
+ protected void invalidPackageGroupReferenceHook(TargetAndConfiguration node, Label label) {
+ throw new RuntimeException("bad package group on " + label + " during testing unexpected");
+ }
+
+ @Override
+ protected Target getTarget(Label label) throws NoSuchThingException {
+ return packageManager.getLoadedTarget(label);
+ }
+ };
+ TargetAndConfiguration ctNode = new TargetAndConfiguration(target);
+ ListMultimap<Attribute, Dependency> depNodeNames;
+ try {
+ depNodeNames = resolver.dependentNodeMap(ctNode, null, getConfigurableAttributeKeys(ctNode));
+ } catch (EvalException e) {
+ throw new IllegalStateException(e);
+ }
+
+ final Map<LabelAndConfiguration, ConfiguredTarget> depMap = new HashMap<>();
+ for (ConfiguredTarget dep : skyframeExecutor.getConfiguredTargets(depNodeNames.values())) {
+ depMap.put(LabelAndConfiguration.of(dep.getLabel(), dep.getConfiguration()), dep);
+ }
+
+ return Multimaps.transformValues(depNodeNames, new Function<Dependency, ConfiguredTarget>() {
+ @Override
+ public ConfiguredTarget apply(Dependency depName) {
+ return depMap.get(LabelAndConfiguration.of(depName.getLabel(),
+ depName.getConfiguration()));
+ }
+ });
+ }
+
+ /**
+ * Sets the possible artifact roots in the artifact factory. This allows the
+ * factory to resolve paths with unknown roots to artifacts.
+ * <p>
+ * <em>Note: This must be called before any call to
+ * {@link #getConfiguredTarget(Label, BuildConfiguration)}
+ * </em>
+ */
+ @VisibleForTesting // for BuildViewTestCase
+ void setArtifactRoots(ImmutableMap<PackageIdentifier, Path> packageRoots) {
+ Map<Path, Root> rootMap = new HashMap<>();
+ Map<PackageIdentifier, Root> realPackageRoots = new HashMap<>();
+ for (Map.Entry<PackageIdentifier, Path> entry : packageRoots.entrySet()) {
+ Root root = rootMap.get(entry.getValue());
+ if (root == null) {
+ root = Root.asSourceRoot(entry.getValue());
+ rootMap.put(entry.getValue(), root);
+ }
+ realPackageRoots.put(entry.getKey(), root);
+ }
+ // Source Artifact roots:
+ artifactFactory.setPackageRoots(realPackageRoots);
+
+ // Derived Artifact roots:
+ ImmutableList.Builder<Root> roots = ImmutableList.builder();
+
+ // build-info.txt and friends; this root is not configuration specific.
+ roots.add(directories.getBuildDataDirectory());
+
+ // The roots for each configuration - duplicates are automatically removed in the call below.
+ for (BuildConfiguration cfg : configurations.getAllConfigurations()) {
+ roots.addAll(cfg.getRoots());
+ }
+
+ artifactFactory.setDerivedArtifactRoots(roots.build());
+ }
+
+ /**
+ * Returns a configured target for the specified target and configuration.
+ * This should only be called from test cases, and is needed, because
+ * plain {@link #getConfiguredTarget(Target, BuildConfiguration)} does not
+ * construct the configured target graph, and would thus fail if called from
+ * outside an update.
+ */
+ @VisibleForTesting
+ public ConfiguredTarget getConfiguredTargetForTesting(Label label, BuildConfiguration config)
+ throws NoSuchPackageException, NoSuchTargetException {
+ return getConfiguredTargetForTesting(packageManager.getLoadedTarget(label), config);
+ }
+
+ @VisibleForTesting
+ public ConfiguredTarget getConfiguredTargetForTesting(Target target, BuildConfiguration config) {
+ return skyframeExecutor.getConfiguredTargetForTesting(target.getLabel(), config);
+ }
+
+ /**
+ * Returns a RuleContext which is the same as the original RuleContext of the target parameter.
+ */
+ @VisibleForTesting
+ public RuleContext getRuleContextForTesting(ConfiguredTarget target,
+ StoredEventHandler eventHandler) {
+ BuildConfiguration config = target.getConfiguration();
+ CachingAnalysisEnvironment analysisEnvironment =
+ new CachingAnalysisEnvironment(artifactFactory,
+ new ConfiguredTargetKey(target.getLabel(), config),
+ /*isSystemEnv=*/false, config.extendedSanityChecks(), eventHandler,
+ /*skyframeEnv=*/null, config.isActionsEnabled(), binTools);
+ RuleContext ruleContext = new RuleContext.Builder(analysisEnvironment,
+ (Rule) target.getTarget(), config, ruleClassProvider.getPrerequisiteValidator())
+ .setVisibility(NestedSetBuilder.<PackageSpecification>create(
+ Order.STABLE_ORDER, PackageSpecification.EVERYTHING))
+ .setPrerequisites(getPrerequisiteMapForTesting(target))
+ .setConfigConditions(ImmutableSet.<ConfigMatchingProvider>of())
+ .build();
+ return ruleContext;
+ }
+
+ /**
+ * Tests and clears the current thread's pending "interrupted" status, and
+ * throws InterruptedException iff it was set.
+ */
+ protected final void pollInterruptedStatus() throws InterruptedException {
+ if (Thread.interrupted()) {
+ throw new InterruptedException();
+ }
+ }
+
+ /**
+ * Drops the analysis cache. If building with Skyframe, targets in {@code topLevelTargets} may
+ * remain in the cache for use during the execution phase.
+ *
+ * @see BuildView.Options#discardAnalysisCache
+ */
+ public void clearAnalysisCache(Collection<ConfiguredTarget> topLevelTargets) {
+ // TODO(bazel-team): Consider clearing packages too to save more memory.
+ skyframeAnalysisWasDiscarded = true;
+ skyframeExecutor.clearAnalysisCache(topLevelTargets);
+ }
+
+ /********************************************************************
+ * *
+ * 'blaze dump' related functions *
+ * *
+ ********************************************************************/
+
+ /**
+ * Collects and stores error events while also forwarding them to another eventHandler.
+ */
+ public static class ErrorCollector extends DelegatingEventHandler {
+ private final List<Event> events;
+
+ public ErrorCollector(EventHandler delegate) {
+ super(delegate);
+ this.events = Lists.newArrayList();
+ }
+
+ public List<Event> getEvents() {
+ return events;
+ }
+
+ @Override
+ public void handle(Event e) {
+ super.handle(e);
+ if (e.getKind() == EventKind.ERROR) {
+ events.add(e);
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/CachingAnalysisEnvironment.java b/src/main/java/com/google/devtools/build/lib/analysis/CachingAnalysisEnvironment.java
new file mode 100644
index 0000000..bc45ba3
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/CachingAnalysisEnvironment.java
@@ -0,0 +1,303 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.ArtifactFactory;
+import com.google.devtools.build.lib.actions.ArtifactOwner;
+import com.google.devtools.build.lib.actions.MiddlemanFactory;
+import com.google.devtools.build.lib.actions.Root;
+import com.google.devtools.build.lib.analysis.buildinfo.BuildInfoCollection;
+import com.google.devtools.build.lib.analysis.buildinfo.BuildInfoFactory.BuildInfoKey;
+import com.google.devtools.build.lib.analysis.config.BinTools;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.events.StoredEventHandler;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.skyframe.BuildInfoCollectionValue;
+import com.google.devtools.build.lib.skyframe.WorkspaceStatusValue;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.skyframe.SkyFunction;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+
+import javax.annotation.Nullable;
+
+/**
+ * The implementation of AnalysisEnvironment used for analysis. It tracks metadata for each
+ * configured target, such as the errors and warnings emitted by that target. It is intended that
+ * a separate instance is used for each configured target, so that these don't mix up.
+ */
+public class CachingAnalysisEnvironment implements AnalysisEnvironment {
+ private final ArtifactFactory artifactFactory;
+
+ private final ArtifactOwner owner;
+ /**
+ * If this is the system analysis environment, then errors and warnings are directly reported
+ * to the global reporter, rather than stored, i.e., we don't track here whether there are any
+ * errors.
+ */
+ private final boolean isSystemEnv;
+ private final boolean extendedSanityChecks;
+
+ /**
+ * If false, no actions will be registered, they'll all be just dropped.
+ *
+ * <p>Usually, an analysis environment should register all actions. However, in some scenarios we
+ * analyze some targets twice, but the first one only serves the purpose of collecting information
+ * for the second analysis. In this case we don't register actions created by the first pass in
+ * order to avoid action conflicts.
+ */
+ private final boolean allowRegisteringActions;
+
+ private boolean enabled = true;
+ private MiddlemanFactory middlemanFactory;
+ private EventHandler errorEventListener;
+ private SkyFunction.Environment skyframeEnv;
+ private Map<Artifact, String> artifacts;
+ private final BinTools binTools;
+
+ /**
+ * The list of actions registered by the configured target this analysis environment is
+ * responsible for. May get cleared out at the end of the analysis of said target.
+ */
+ final List<Action> actions = new ArrayList<>();
+
+ public CachingAnalysisEnvironment(ArtifactFactory artifactFactory,
+ ArtifactOwner owner, boolean isSystemEnv, boolean extendedSanityChecks,
+ EventHandler errorEventListener, SkyFunction.Environment env, boolean allowRegisteringActions,
+ BinTools binTools) {
+ this.artifactFactory = artifactFactory;
+ this.owner = Preconditions.checkNotNull(owner);
+ this.isSystemEnv = isSystemEnv;
+ this.extendedSanityChecks = extendedSanityChecks;
+ this.errorEventListener = errorEventListener;
+ this.skyframeEnv = env;
+ this.allowRegisteringActions = allowRegisteringActions;
+ this.binTools = binTools;
+ middlemanFactory = new MiddlemanFactory(artifactFactory, this);
+ artifacts = new HashMap<>();
+ }
+
+ public void disable(Target target) {
+ if (!hasErrors() && allowRegisteringActions) {
+ verifyGeneratedArtifactHaveActions(target);
+ }
+ artifacts = null;
+ middlemanFactory = null;
+ enabled = false;
+ errorEventListener = null;
+ skyframeEnv = null;
+ }
+
+ private static StringBuilder shortDescription(Action action) {
+ if (action == null) {
+ return new StringBuilder("null Action");
+ }
+ return new StringBuilder()
+ .append(action.getClass().getName())
+ .append(' ')
+ .append(action.getMnemonic());
+ }
+
+ /**
+ * Sanity checks that all generated artifacts have a generating action.
+ * @param target for error reporting
+ */
+ public void verifyGeneratedArtifactHaveActions(Target target) {
+ Collection<String> orphanArtifacts = getOrphanArtifactMap().values();
+ List<String> checkedActions = null;
+ if (!orphanArtifacts.isEmpty()) {
+ checkedActions = Lists.newArrayListWithCapacity(actions.size());
+ for (Action action : actions) {
+ StringBuilder sb = shortDescription(action);
+ for (Artifact o : action.getOutputs()) {
+ sb.append("\n ");
+ sb.append(o.getExecPathString());
+ }
+ checkedActions.add(sb.toString());
+ }
+ throw new IllegalStateException(
+ String.format(
+ "%s %s : These artifacts miss a generating action:\n%s\n"
+ + "These actions we checked:\n%s\n",
+ target.getTargetKind(), target.getLabel(),
+ Joiner.on('\n').join(orphanArtifacts), Joiner.on('\n').join(checkedActions)));
+ }
+ }
+
+ @Override
+ public ImmutableSet<Artifact> getOrphanArtifacts() {
+ return ImmutableSet.copyOf(getOrphanArtifactMap().keySet());
+ }
+
+ private Map<Artifact, String> getOrphanArtifactMap() {
+ // Construct this set to avoid poor performance under large --runs_per_test.
+ Set<Artifact> artifactsWithActions = new HashSet<>();
+ for (Action action : actions) {
+ // Don't bother checking that every Artifact only appears once; that test is performed
+ // elsewhere (see #testNonUniqueOutputs in ActionListenerIntegrationTest).
+ artifactsWithActions.addAll(action.getOutputs());
+ }
+ // The order of the artifacts.entrySet iteration is unspecified - we use a TreeMap here to
+ // guarantee that the return value of this method is deterministic.
+ Map<Artifact, String> orphanArtifacts = new TreeMap<>();
+ for (Map.Entry<Artifact, String> entry : artifacts.entrySet()) {
+ Artifact a = entry.getKey();
+ if (!a.isSourceArtifact() && !artifactsWithActions.contains(a)) {
+ orphanArtifacts.put(a, String.format("%s\n%s",
+ a.getExecPathString(), // uncovered artifact
+ entry.getValue())); // origin of creation
+ }
+ }
+ return orphanArtifacts;
+ }
+
+ @Override
+ public EventHandler getEventHandler() {
+ return errorEventListener;
+ }
+
+ @Override
+ public boolean hasErrors() {
+ // The system analysis environment never has errors.
+ if (isSystemEnv) {
+ return false;
+ }
+ Preconditions.checkState(enabled);
+ return ((StoredEventHandler) errorEventListener).hasErrors();
+ }
+
+ @Override
+ public MiddlemanFactory getMiddlemanFactory() {
+ Preconditions.checkState(enabled);
+ return middlemanFactory;
+ }
+
+ /**
+ * Keeps track of artifacts. We check that all of them have an owner when the environment is
+ * sealed (disable()). For performance reasons we only track the originating stacktrace when
+ * running with --experimental_extended_sanity_checks.
+ */
+ private Artifact trackArtifactAndOrigin(Artifact a, @Nullable Throwable e) {
+ if ((e != null) && !artifacts.containsKey(a)) {
+ StringWriter sw = new StringWriter();
+ e.printStackTrace(new PrintWriter(sw));
+ artifacts.put(a, sw.toString());
+ } else {
+ artifacts.put(a, "No origin, run with --experimental_extended_sanity_checks");
+ }
+ return a;
+ }
+
+ @Override
+ public Artifact getDerivedArtifact(PathFragment rootRelativePath, Root root) {
+ Preconditions.checkState(enabled);
+ return trackArtifactAndOrigin(
+ artifactFactory.getDerivedArtifact(rootRelativePath, root, getOwner()),
+ extendedSanityChecks ? new Throwable() : null);
+ }
+
+ @Override
+ public Artifact getFilesetArtifact(PathFragment rootRelativePath, Root root) {
+ Preconditions.checkState(enabled);
+ return trackArtifactAndOrigin(
+ artifactFactory.getFilesetArtifact(rootRelativePath, root, getOwner()),
+ extendedSanityChecks ? new Throwable() : null);
+ }
+
+ @Override
+ public Artifact getConstantMetadataArtifact(PathFragment rootRelativePath, Root root) {
+ return artifactFactory.getConstantMetadataArtifact(rootRelativePath, root, getOwner());
+ }
+
+ @Override
+ public Artifact getEmbeddedToolArtifact(String embeddedPath) {
+ Preconditions.checkState(enabled);
+ return binTools.getEmbeddedArtifact(embeddedPath, artifactFactory);
+ }
+
+ @Override
+ public void registerAction(Action... actions) {
+ Preconditions.checkState(enabled);
+ if (allowRegisteringActions) {
+ for (Action action : actions) {
+ this.actions.add(action);
+ }
+ }
+ }
+
+ @Override
+ public Action getLocalGeneratingAction(Artifact artifact) {
+ Preconditions.checkState(allowRegisteringActions);
+ for (Action action : actions) {
+ if (action.getOutputs().contains(artifact)) {
+ return action;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public Collection<Action> getRegisteredActions() {
+ return Collections.unmodifiableCollection(actions);
+ }
+
+ @Override
+ public SkyFunction.Environment getSkyframeEnv() {
+ return skyframeEnv;
+ }
+
+ @Override
+ public Artifact getStableWorkspaceStatusArtifact() {
+ return ((WorkspaceStatusValue) skyframeEnv.getValue(WorkspaceStatusValue.SKY_KEY))
+ .getStableArtifact();
+ }
+
+ @Override
+ public Artifact getVolatileWorkspaceStatusArtifact() {
+ return ((WorkspaceStatusValue) skyframeEnv.getValue(WorkspaceStatusValue.SKY_KEY))
+ .getVolatileArtifact();
+ }
+
+ @Override
+ public ImmutableList<Artifact> getBuildInfo(RuleContext ruleContext, BuildInfoKey key) {
+ boolean stamp = AnalysisUtils.isStampingEnabled(ruleContext);
+ BuildInfoCollection collection =
+ ((BuildInfoCollectionValue) skyframeEnv.getValue(BuildInfoCollectionValue.key(
+ new BuildInfoCollectionValue.BuildInfoKeyAndConfig(key, ruleContext.getConfiguration()))))
+ .getCollection();
+ return stamp ? collection.getStampedBuildInfo() : collection.getRedactedBuildInfo();
+ }
+
+ @Override
+ public ArtifactOwner getOwner() {
+ return owner;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/CommandHelper.java b/src/main/java/com/google/devtools/build/lib/analysis/CommandHelper.java
new file mode 100644
index 0000000..2c05f0f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/CommandHelper.java
@@ -0,0 +1,307 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.BaseSpawn;
+import com.google.devtools.build.lib.analysis.actions.FileWriteAction;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.syntax.SkylarkCallable;
+import com.google.devtools.build.lib.syntax.SkylarkModule;
+import com.google.devtools.build.lib.util.Pair;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * Provides shared functionality for parameterized command-line launching
+ * e.g. {@link com.google.devtools.build.lib.view.genrule.GenRule}
+ * Also used by {@link com.google.devtools.build.lib.rules.extra.ExtraActionFactory}.
+ *
+ * Two largely independent separate sets of functionality are provided:
+ * 1- string interpolation for {@code $(location[s] ...)} and {@code $(MakeVariable)}
+ * 2- a utility to build potentially large command lines (presumably made of multiple commands),
+ * that if presumed too large for the kernel's taste can be dumped into a shell script
+ * that will contain the same commands,
+ * at which point the shell script is added to the list of inputs.
+ */
+@SkylarkModule(name = "command_helper", doc = "A helper class to create shell commands.")
+public final class CommandHelper {
+
+ /**
+ * Maximum total command-line length, in bytes, not counting "/bin/bash -c ".
+ * If the command is very long, then we write the command to a script file,
+ * to avoid overflowing any limits on command-line length.
+ * For short commands, we just use /bin/bash -c command.
+ */
+ @VisibleForTesting
+ public static int maxCommandLength = 64000;
+
+ /**
+ * A map of remote path prefixes and corresponding runfiles manifests for tools
+ * used by this rule.
+ */
+ private final ImmutableMap<PathFragment, Artifact> remoteRunfileManifestMap;
+
+ /**
+ * Use labelMap for heuristically expanding labels (does not include "outs")
+ * This is similar to heuristic location expansion in LocationExpander
+ * and should be kept in sync.
+ */
+ private final ImmutableMap<Label, ImmutableCollection<Artifact>> labelMap;
+
+ /**
+ * The ruleContext this helper works on
+ */
+ private final RuleContext ruleContext;
+
+ /**
+ * Output executable files from the 'tools' attribute.
+ */
+ private final ImmutableList<Artifact> resolvedTools;
+
+ /**
+ * Creates an {@link CommandHelper}.
+ *
+ * @param tools - Resolves set of tools into set of executable binaries. Populates manifests,
+ * remoteRunfiles and label map where required.
+ * @param labelMap - Adds files to set of known files of label. Used for resolving $(location)
+ * variables.
+ */
+ public CommandHelper(RuleContext ruleContext,
+ Iterable<FilesToRunProvider> tools,
+ ImmutableMap<Label, Iterable<Artifact>> labelMap) {
+ this.ruleContext = ruleContext;
+
+ ImmutableList.Builder<Artifact> resolvedToolsBuilder = ImmutableList.builder();
+ ImmutableMap.Builder<PathFragment, Artifact> remoteRunfileManifestBuilder =
+ ImmutableMap.builder();
+ Map<Label, Collection<Artifact>> tempLabelMap = new HashMap<>();
+
+ for (Map.Entry<Label, Iterable<Artifact>> entry : labelMap.entrySet()) {
+ Iterables.addAll(mapGet(tempLabelMap, entry.getKey()), entry.getValue());
+ }
+
+ for (FilesToRunProvider tool : tools) { // (Note: host configuration)
+ Label label = tool.getLabel();
+ Collection<Artifact> files = tool.getFilesToRun();
+ resolvedToolsBuilder.addAll(files);
+ Artifact executableArtifact = tool.getExecutable();
+ // If the label has an executable artifact add that to the multimaps.
+ if (executableArtifact != null) {
+ mapGet(tempLabelMap, label).add(executableArtifact);
+ // Also send the runfiles when running remotely.
+ Artifact runfilesManifest = tool.getRunfilesManifest();
+ if (runfilesManifest != null) {
+ remoteRunfileManifestBuilder.put(
+ BaseSpawn.runfilesForFragment(executableArtifact.getExecPath()), runfilesManifest);
+ }
+ } else {
+ // Map all depArtifacts to the respective label using the multimaps.
+ Iterables.addAll(mapGet(tempLabelMap, label), files);
+ }
+ }
+
+ this.resolvedTools = resolvedToolsBuilder.build();
+ this.remoteRunfileManifestMap = remoteRunfileManifestBuilder.build();
+ ImmutableMap.Builder<Label, ImmutableCollection<Artifact>> labelMapBuilder =
+ ImmutableMap.builder();
+ for (Entry<Label, Collection<Artifact>> entry : tempLabelMap.entrySet()) {
+ labelMapBuilder.put(entry.getKey(), ImmutableList.copyOf(entry.getValue()));
+ }
+ this.labelMap = labelMapBuilder.build();
+ }
+
+ @SkylarkCallable(name = "resolved_tools", doc = "", structField = true)
+ public List<Artifact> getResolvedTools() {
+ return resolvedTools;
+ }
+
+ @SkylarkCallable(name = "runfiles_manifests", doc = "", structField = true)
+ public ImmutableMap<PathFragment, Artifact> getRemoteRunfileManifestMap() {
+ return remoteRunfileManifestMap;
+ }
+
+ // Returns the value in the specified corresponding to 'key', creating and
+ // inserting an empty container if absent. We use Map not Multimap because
+ // we need to distinguish the cases of "empty value" and "absent key".
+ private static Collection<Artifact> mapGet(Map<Label, Collection<Artifact>> map, Label key) {
+ Collection<Artifact> values = map.get(key);
+ if (values == null) {
+ // We use sets not lists, because it's conceivable that the same artifact
+ // could appear twice, e.g. in "srcs" and "deps".
+ values = Sets.newHashSet();
+ map.put(key, values);
+ }
+ return values;
+ }
+
+ /**
+ * Resolves the 'cmd' attribute, and expands known locations for $(location)
+ * variables.
+ */
+ @SkylarkCallable(doc = "")
+ public String resolveCommandAndExpandLabels(Boolean supportLegacyExpansion,
+ Boolean allowDataInLabel) {
+ String command = ruleContext.attributes().get("cmd", Type.STRING);
+ command = new LocationExpander(ruleContext, allowDataInLabel).expand("cmd", command);
+
+ if (supportLegacyExpansion) {
+ command = expandLabels(command, labelMap);
+ }
+ return command;
+ }
+
+ /**
+ * Expands labels occurring in the string "expr" in the rule 'cmd'.
+ * Each label must be valid, be a declared prerequisite, and expand to a
+ * unique path.
+ *
+ * <p>If the expansion fails, an attribute error is reported and the original
+ * expression is returned.
+ */
+ private <T extends Iterable<Artifact>> String expandLabels(String expr, Map<Label, T> labelMap) {
+ try {
+ return LabelExpander.expand(expr, labelMap, ruleContext.getLabel());
+ } catch (LabelExpander.NotUniqueExpansionException nuee) {
+ ruleContext.attributeError("cmd", nuee.getMessage());
+ return expr;
+ }
+ }
+
+ private static Pair<List<String>, Artifact> buildCommandLineMaybeWithScriptFile(
+ RuleContext ruleContext, String command, String scriptPostFix) {
+ List<String> argv;
+ Artifact scriptFileArtifact = null;
+ if (command.length() <= maxCommandLength) {
+ argv = buildCommandLineSimpleArgv(ruleContext, command);
+ } else {
+ // Use script file.
+ scriptFileArtifact = buildCommandLineArtifact(ruleContext, command, scriptPostFix);
+ argv = buildCommandLineArgvWithArtifact(ruleContext, scriptFileArtifact);
+ }
+ return Pair.of(argv, scriptFileArtifact);
+
+ }
+
+ /**
+ * Builds the set of command-line arguments. Creates a bash script if the
+ * command line is longer than the allowed maximum {@link
+ * #maxCommandLength}. Fixes up the input artifact list with the
+ * created bash script when required.
+ * TODO(bazel-team): do away with the side effect on inputs (ugh).
+ */
+ public static List<String> buildCommandLine(RuleContext ruleContext,
+ String command, NestedSetBuilder<Artifact> inputs, String scriptPostFix) {
+ Pair<List<String>, Artifact> argvAndScriptFile =
+ buildCommandLineMaybeWithScriptFile(ruleContext, command, scriptPostFix);
+ if (argvAndScriptFile.second != null) {
+ inputs.add(argvAndScriptFile.second);
+ }
+ return argvAndScriptFile.first;
+ }
+
+ /**
+ * Builds the set of command-line arguments. Creates a bash script if the
+ * command line is longer than the allowed maximum {@link
+ * #maxCommandLength}. Fixes up the input artifact list with the
+ * created bash script when required.
+ * TODO(bazel-team): do away with the side effect on inputs (ugh).
+ */
+ public static List<String> buildCommandLine(RuleContext ruleContext,
+ String command, List<Artifact> inputs, String scriptPostFix) {
+ Pair<List<String>, Artifact> argvAndScriptFile =
+ buildCommandLineMaybeWithScriptFile(ruleContext, command, scriptPostFix);
+ if (argvAndScriptFile.second != null) {
+ inputs.add(argvAndScriptFile.second);
+ }
+ return argvAndScriptFile.first;
+ }
+
+ /**
+ * Builds the set of command-line arguments. Creates a bash script if the
+ * command line is longer than the allowed maximum {@link
+ * #maxCommandLength}. Fixes up the input artifact list with the
+ * created bash script when required.
+ * TODO(bazel-team): do away with the side effect on inputs (ugh).
+ */
+ public static List<String> buildCommandLine(RuleContext ruleContext,
+ String command, ImmutableSet.Builder<Artifact> inputs, String scriptPostFix) {
+ Pair<List<String>, Artifact> argvAndScriptFile =
+ buildCommandLineMaybeWithScriptFile(ruleContext, command, scriptPostFix);
+ if (argvAndScriptFile.second != null) {
+ inputs.add(argvAndScriptFile.second);
+ }
+ return argvAndScriptFile.first;
+ }
+
+ private static ImmutableList<String> buildCommandLineArgvWithArtifact(RuleContext ruleContext,
+ Artifact scriptFileArtifact) {
+ return ImmutableList.of(
+ ruleContext.getConfiguration().getShExecutable().getPathString(),
+ scriptFileArtifact.getExecPathString());
+ }
+
+ private static Artifact buildCommandLineArtifact(RuleContext ruleContext, String command,
+ String scriptPostFix) {
+ String scriptFileName = ruleContext.getTarget().getName() + scriptPostFix;
+ String scriptFileContents = "#!/bin/bash\n" + command;
+ Artifact scriptFileArtifact = FileWriteAction.createFile(
+ ruleContext, scriptFileName, scriptFileContents, /*executable=*/true);
+ return scriptFileArtifact;
+ }
+
+ private static ImmutableList<String> buildCommandLineSimpleArgv(RuleContext ruleContext,
+ String command) {
+ return ImmutableList.of(
+ ruleContext.getConfiguration().getShExecutable().getPathString(), "-c", command);
+ }
+
+ /**
+ * Builds the set of command-line arguments. Creates a bash script if the
+ * command line is longer than the allowed maximum {@link
+ * #maxCommandLength}. Fixes up the input artifact list with the
+ * created bash script when required.
+ */
+ public List<String> buildCommandLine(
+ String command, NestedSetBuilder<Artifact> inputs, String scriptPostFix) {
+ return buildCommandLine(ruleContext, command, inputs, scriptPostFix);
+ }
+
+ /**
+ * Builds the set of command-line arguments. Creates a bash script if the
+ * command line is longer than the allowed maximum {@link
+ * #maxCommandLength}. Fixes up the input artifact list with the
+ * created bash script when required.
+ */
+ @SkylarkCallable(doc = "")
+ public List<String> buildCommandLine(
+ String command, List<Artifact> inputs, String scriptPostFix) {
+ return buildCommandLine(ruleContext, command, inputs, scriptPostFix);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/CompilationHelper.java b/src/main/java/com/google/devtools/build/lib/analysis/CompilationHelper.java
new file mode 100644
index 0000000..23dd8d4
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/CompilationHelper.java
@@ -0,0 +1,92 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.ActionOwner;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.MiddlemanFactory;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+
+import java.util.List;
+
+/**
+ * A helper class for compilation helpers.
+ */
+public final class CompilationHelper {
+ /**
+ * Returns a middleman for all files to build for the given configured target.
+ * If multiple calls are made, then it returns the same artifact for
+ * configurations with the same internal directory.
+ *
+ * <p>The resulting middleman only aggregates the inputs and must be expanded
+ * before an action using it can be sent to a distributed execution-system.
+ */
+ public static NestedSet<Artifact> getAggregatingMiddleman(
+ RuleContext ruleContext, String purpose, NestedSet<Artifact> filesToBuild) {
+ return NestedSetBuilder.wrap(Order.STABLE_ORDER, getMiddlemanInternal(
+ ruleContext.getAnalysisEnvironment(), ruleContext, ruleContext.getActionOwner(), purpose,
+ filesToBuild));
+ }
+
+ /**
+ * Internal implementation for getAggregatingMiddleman / getAggregatingMiddlemanWithSolibSymlinks.
+ */
+ private static List<Artifact> getMiddlemanInternal(AnalysisEnvironment env,
+ RuleContext ruleContext, ActionOwner actionOwner, String purpose,
+ NestedSet<Artifact> filesToBuild) {
+ if (filesToBuild == null) {
+ return ImmutableList.of();
+ }
+ MiddlemanFactory factory = env.getMiddlemanFactory();
+ return ImmutableList.of(factory.createMiddlemanAllowMultiple(
+ env, actionOwner, purpose, filesToBuild,
+ ruleContext.getConfiguration().getMiddlemanDirectory()));
+ }
+
+ // TODO(bazel-team): remove this duplicated code after the ConfiguredTarget migration
+ /**
+ * Returns a middleman for all files to build for the given configured target.
+ * If multiple calls are made, then it returns the same artifact for
+ * configurations with the same internal directory.
+ *
+ * <p>The resulting middleman only aggregates the inputs and must be expanded
+ * before an action using it can be sent to a distributed execution-system.
+ */
+ public static NestedSet<Artifact> getAggregatingMiddleman(
+ RuleContext ruleContext, String purpose, TransitiveInfoCollection dep) {
+ return NestedSetBuilder.wrap(Order.STABLE_ORDER, getMiddlemanInternal(
+ ruleContext.getAnalysisEnvironment(), ruleContext, ruleContext.getActionOwner(), purpose,
+ dep));
+ }
+
+ /**
+ * Internal implementation for getAggregatingMiddleman / getAggregatingMiddlemanWithSolibSymlinks.
+ */
+ private static List<Artifact> getMiddlemanInternal(AnalysisEnvironment env,
+ RuleContext ruleContext, ActionOwner actionOwner, String purpose,
+ TransitiveInfoCollection dep) {
+ if (dep == null) {
+ return ImmutableList.of();
+ }
+ MiddlemanFactory factory = env.getMiddlemanFactory();
+ Iterable<Artifact> artifacts = dep.getProvider(FileProvider.class).getFilesToBuild();
+ return ImmutableList.of(factory.createMiddlemanAllowMultiple(
+ env, actionOwner, purpose, artifacts,
+ ruleContext.getConfiguration().getMiddlemanDirectory()));
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/CompilationPrerequisitesProvider.java b/src/main/java/com/google/devtools/build/lib/analysis/CompilationPrerequisitesProvider.java
new file mode 100644
index 0000000..8c3a6ce
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/CompilationPrerequisitesProvider.java
@@ -0,0 +1,38 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+
+/**
+ * A provider for compilation prerequisites of a given target.
+ */
+@Immutable
+public final class CompilationPrerequisitesProvider implements TransitiveInfoProvider {
+
+ private final NestedSet<Artifact> compilationPrerequisites;
+
+ public CompilationPrerequisitesProvider(NestedSet<Artifact> compilationPrerequisites) {
+ this.compilationPrerequisites = compilationPrerequisites;
+ }
+
+ /**
+ * Returns compilation prerequisites (e.g., generated sources and headers) for a rule.
+ */
+ public NestedSet<Artifact> getCompilationPrerequisites() {
+ return compilationPrerequisites;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/ConfigurationCollectionFactory.java b/src/main/java/com/google/devtools/build/lib/analysis/ConfigurationCollectionFactory.java
new file mode 100644
index 0000000..8ffac43
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/ConfigurationCollectionFactory.java
@@ -0,0 +1,54 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.analysis.config.BuildOptions;
+import com.google.devtools.build.lib.analysis.config.ConfigurationFactory;
+import com.google.devtools.build.lib.analysis.config.InvalidConfigurationException;
+import com.google.devtools.build.lib.analysis.config.PackageProviderForConfigurations;
+import com.google.devtools.build.lib.events.EventHandler;
+
+import java.util.Map;
+
+import javax.annotation.Nullable;
+
+/**
+ * A factory for configuration collection creation.
+ */
+public interface ConfigurationCollectionFactory {
+ /**
+ * Creates the top-level configuration for a build.
+ *
+ * <p>Also it may create a set of BuildConfigurations and define a transition table over them.
+ * All configurations during a build should be accessible from this top-level configuration
+ * via configuration transitions.
+ * @param loadedPackageProvider the package provider
+ * @param buildOptions top-level build options representing the command-line
+ * @param clientEnv the system environment
+ * @param errorEventListener the event listener for errors
+ * @param performSanityCheck flag to signal about performing sanity check. Can be false only for
+ * tests in skyframe. Legacy mode uses loadedPackageProvider == null condition for this.
+ * @return the top-level configuration
+ * @throws InvalidConfigurationException
+ */
+ @Nullable
+ public BuildConfiguration createConfigurations(
+ ConfigurationFactory configurationFactory,
+ PackageProviderForConfigurations loadedPackageProvider,
+ BuildOptions buildOptions,
+ Map<String, String> clientEnv,
+ EventHandler errorEventListener,
+ boolean performSanityCheck) throws InvalidConfigurationException;
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/ConfigurationMakeVariableContext.java b/src/main/java/com/google/devtools/build/lib/analysis/ConfigurationMakeVariableContext.java
new file mode 100644
index 0000000..8c98f5f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/ConfigurationMakeVariableContext.java
@@ -0,0 +1,56 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.devtools.build.lib.analysis.MakeVariableExpander.ExpansionException;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.packages.Package;
+
+import java.util.Map;
+
+/**
+ * Implements make variable expansion for make variables that depend on the
+ * configuration and the target (not on behavior of the
+ * {@link ConfiguredTarget} implementation)
+ */
+public class ConfigurationMakeVariableContext implements MakeVariableExpander.Context {
+ private final Package pkg;
+ private final Map<String, String> commandLineEnv;
+ private final Map<String, String> globalEnv;
+ private final String platform;
+
+ public ConfigurationMakeVariableContext(Package pkg, BuildConfiguration configuration) {
+ this.pkg = pkg;
+ commandLineEnv = configuration.getCommandLineDefines();
+ globalEnv = configuration.getGlobalMakeEnvironment();
+ platform = configuration.getPlatformName();
+ }
+
+ @Override
+ public String lookupMakeVariable(String var) throws ExpansionException {
+ String value = commandLineEnv.get(var);
+ if (value == null) {
+ value = pkg.lookupMakeVariable(var, platform);
+ }
+ if (value == null) {
+ value = globalEnv.get(var);
+ }
+ if (value == null) {
+ throw new MakeVariableExpander.ExpansionException("$(" + var + ") not defined");
+ }
+
+ return value;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/ConfigurationsCreatedEvent.java b/src/main/java/com/google/devtools/build/lib/analysis/ConfigurationsCreatedEvent.java
new file mode 100644
index 0000000..8b0621a
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/ConfigurationsCreatedEvent.java
@@ -0,0 +1,38 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.devtools.build.lib.analysis.config.BuildConfigurationCollection;
+
+/**
+ * This event is fired when the build configurations are created.
+ */
+public class ConfigurationsCreatedEvent {
+
+ private final BuildConfigurationCollection configurations;
+
+ /**
+ * Construct the ConfigurationsCreatedEvent.
+ *
+ * @param configurations the build configuration collection
+ */
+ public ConfigurationsCreatedEvent(BuildConfigurationCollection configurations) {
+ this.configurations = configurations;
+ }
+
+ public BuildConfigurationCollection getBuildConfigurationCollection() {
+ return configurations;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/ConfiguredAspectFactory.java b/src/main/java/com/google/devtools/build/lib/analysis/ConfiguredAspectFactory.java
new file mode 100644
index 0000000..955241a
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/ConfiguredAspectFactory.java
@@ -0,0 +1,28 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.devtools.build.lib.packages.AspectFactory;
+
+/**
+ * Instantiation of {@link AspectFactory} with the actual types.
+ *
+ * <p>This is needed because {@link AspectFactory} is needed in the {@code packages} package to
+ * do loading phase things properly and to be able to specify them on attributes, but the actual
+ * classes are in the {@code view} package, which is not available there.
+ */
+public interface ConfiguredAspectFactory
+ extends AspectFactory<ConfiguredTarget, RuleContext, Aspect> {
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/ConfiguredAttributeMapper.java b/src/main/java/com/google/devtools/build/lib/analysis/ConfiguredAttributeMapper.java
new file mode 100644
index 0000000..84029b2
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/ConfiguredAttributeMapper.java
@@ -0,0 +1,158 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.analysis.config.ConfigMatchingProvider;
+import com.google.devtools.build.lib.packages.AbstractAttributeMapper;
+import com.google.devtools.build.lib.packages.AttributeMap;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.syntax.EvalException;
+import com.google.devtools.build.lib.syntax.Label;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * {@link AttributeMap} implementation that binds a rule's attribute as follows:
+ *
+ * <ol>
+ * <li>If the attribute is selectable (i.e. its BUILD declaration is of the form
+ * "attr = { config1: "value1", "config2: "value2", ... }", returns the subset of values
+ * chosen by the current configuration in accordance with Bazel's documented policy on
+ * configurable attribute selection.
+ * <li>If the attribute is not selectable (i.e. its value is static), returns that value with
+ * no additional processing.
+ * </ol>
+ *
+ * <p>Example usage:
+ * <pre>
+ * Label fooLabel = ConfiguredAttributeMapper.of(ruleConfiguredTarget).get("foo", Type.LABEL);
+ * </pre>
+ */
+public class ConfiguredAttributeMapper extends AbstractAttributeMapper {
+
+ private final Map<Label, ConfigMatchingProvider> configConditions;
+ private Rule rule;
+
+ private ConfiguredAttributeMapper(Rule rule, Set<ConfigMatchingProvider> configConditions) {
+ super(Preconditions.checkNotNull(rule).getPackage(), rule.getRuleClassObject(), rule.getLabel(),
+ rule.getAttributeContainer());
+ ImmutableMap.Builder<Label, ConfigMatchingProvider> builder = ImmutableMap.builder();
+ for (ConfigMatchingProvider configCondition : configConditions) {
+ builder.put(configCondition.label(), configCondition);
+ }
+ this.configConditions = builder.build();
+ this.rule = rule;
+ }
+
+ /**
+ * "Do-it-all" constructor that just needs a {@link RuleConfiguredTarget}.
+ */
+ public static ConfiguredAttributeMapper of(RuleConfiguredTarget ct) {
+ return new ConfiguredAttributeMapper(ct.getTarget(), ct.getConfigConditions());
+ }
+
+ /**
+ * "Manual" constructor that requires the caller to pass the set of configurability conditions
+ * that trigger this rule's configurable attributes.
+ *
+ * <p>If you don't know how to do this, you really want to use one of the "do-it-all"
+ * constructors.
+ */
+ static ConfiguredAttributeMapper of(Rule rule, Set<ConfigMatchingProvider> configConditions) {
+ return new ConfiguredAttributeMapper(rule, configConditions);
+ }
+
+ /**
+ * Checks that all attributes can be mapped to their configured values. This is
+ * useful for checking that the configuration space in a configured attribute doesn't
+ * contain unresolvable contradictions.
+ *
+ * @throws EvalException if any attribute's value can't be resolved under this mapper
+ */
+ public void validateAttributes() throws EvalException {
+ for (String attrName : getAttributeNames()) {
+ getAndValidate(attrName, getAttributeType(attrName));
+ }
+ }
+
+ /**
+ * Variation of {@link #get} that throws an informative exception if the attribute
+ * can't be resolved due to intrinsic contradictions in the configuration.
+ */
+ private <T> T getAndValidate(String attributeName, Type<T> type) throws EvalException {
+ Type.Selector<T> selector = getSelector(attributeName, type);
+ if (selector == null) {
+ // This is a normal attribute.
+ return super.get(attributeName, type);
+ }
+
+ // We expect exactly one of this attribute's conditions to match (including the default
+ // condition, if specified). Throw an exception if our expectations aren't met.
+ Label matchingCondition = null;
+ T matchingValue = null;
+
+ // Find the matching condition and record its value (checking for duplicates).
+ for (Map.Entry<Label, T> entry : selector.getEntries().entrySet()) {
+ Label curCondition = entry.getKey();
+ if (Type.Selector.isReservedLabel(curCondition)) {
+ continue;
+ } else if (Preconditions.checkNotNull(configConditions.get(curCondition)).matches()) {
+ if (matchingCondition != null) {
+ throw new EvalException(rule.getAttributeLocation(attributeName),
+ "Both " + matchingCondition.toString() + " and " + curCondition.toString()
+ + " match configurable attribute \"" + attributeName + "\" in " + getLabel()
+ + ". At most one match is allowed");
+ }
+ matchingCondition = curCondition;
+ matchingValue = entry.getValue();
+ }
+ }
+
+ // If nothing matched, choose the default condition.
+ if (matchingCondition == null) {
+ if (!selector.hasDefault()) {
+ throw new EvalException(rule.getAttributeLocation(attributeName),
+ "Configurable attribute \"" + attributeName + "\" doesn't match this "
+ + "configuration (would a default condition help?)");
+ }
+ matchingValue = selector.getDefault();
+ }
+
+ return matchingValue;
+ }
+
+ @Override
+ public <T> T get(String attributeName, Type<T> type) {
+ try {
+ return getAndValidate(attributeName, type);
+ } catch (EvalException e) {
+ // Callers that reach this branch should explicitly validate the attribute through an
+ // appropriate call and handle the exception directly. This method assumes
+ // pre-validated attributes.
+ throw new IllegalStateException(
+ "lookup failed on attribute " + attributeName + ": " + e.getMessage());
+ }
+ }
+
+ @Override
+ protected <T> Iterable<T> visitAttribute(String attributeName, Type<T> type) {
+ T value = get(attributeName, type);
+ return value == null ? ImmutableList.<T>of() : ImmutableList.of(value);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/ConfiguredRuleClassProvider.java b/src/main/java/com/google/devtools/build/lib/analysis/ConfiguredRuleClassProvider.java
new file mode 100644
index 0000000..e84f9aa
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/ConfiguredRuleClassProvider.java
@@ -0,0 +1,376 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import static com.google.devtools.build.lib.packages.RuleClass.Builder.RuleClassType.ABSTRACT;
+import static com.google.devtools.build.lib.packages.RuleClass.Builder.RuleClassType.TEST;
+
+import com.google.common.base.Preconditions;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.analysis.buildinfo.BuildInfoFactory;
+import com.google.devtools.build.lib.analysis.config.BuildOptions;
+import com.google.devtools.build.lib.analysis.config.ConfigurationFragmentFactory;
+import com.google.devtools.build.lib.analysis.config.DefaultsPackage;
+import com.google.devtools.build.lib.analysis.config.FragmentOptions;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.graph.Digraph;
+import com.google.devtools.build.lib.graph.Node;
+import com.google.devtools.build.lib.packages.Attribute;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.RuleClassProvider;
+import com.google.devtools.build.lib.rules.RuleConfiguredTargetFactory;
+import com.google.devtools.build.lib.rules.SkylarkModules;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.syntax.SkylarkEnvironment;
+import com.google.devtools.build.lib.syntax.SkylarkModule;
+import com.google.devtools.build.lib.syntax.SkylarkType;
+import com.google.devtools.build.lib.syntax.ValidationEnvironment;
+import com.google.devtools.common.options.OptionsClassProvider;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Knows about every rule Blaze supports and the associated configuration options.
+ *
+ * <p>This class is initialized on server startup and the set of rules, build info factories
+ * and configuration options is guarantees not to change over the life time of the Blaze server.
+ */
+public class ConfiguredRuleClassProvider implements RuleClassProvider {
+ /**
+ * Custom dependency validation logic.
+ */
+ public static interface PrerequisiteValidator {
+ /**
+ * Checks whether the rule in {@code contextBuilder} is allowed to depend on
+ * {@code prerequisite} through the attribute {@code attribute}.
+ *
+ * <p>Can be used for enforcing any organization-specific policies about the layout of the
+ * workspace.
+ */
+ void validate(
+ RuleContext.Builder contextBuilder, ConfiguredTarget prerequisite, Attribute attribute);
+ }
+
+ /**
+ * Builder for {@link ConfiguredRuleClassProvider}.
+ */
+ public static class Builder implements RuleDefinitionEnvironment {
+ private final List<ConfigurationFragmentFactory> configurationFragments = new ArrayList<>();
+ private final List<BuildInfoFactory> buildInfoFactories = new ArrayList<>();
+ private final List<Class<? extends FragmentOptions>> configurationOptions = new ArrayList<>();
+
+ private final Map<String, RuleClass> ruleClassMap = new HashMap<>();
+ private final Map<String, Class<? extends RuleDefinition>> ruleDefinitionMap =
+ new HashMap<>();
+ private final Map<Class<? extends RuleDefinition>, RuleClass> ruleMap = new HashMap<>();
+ private final Digraph<Class<? extends RuleDefinition>> dependencyGraph =
+ new Digraph<>();
+ private ConfigurationCollectionFactory configurationCollectionFactory;
+ private PrerequisiteValidator prerequisiteValidator;
+ private ImmutableMap<String, SkylarkType> skylarkAccessibleJavaClasses = ImmutableMap.of();
+
+ public Builder setPrerequisiteValidator(PrerequisiteValidator prerequisiteValidator) {
+ this.prerequisiteValidator = prerequisiteValidator;
+ return this;
+ }
+
+ public Builder addBuildInfoFactory(BuildInfoFactory factory) {
+ buildInfoFactories.add(factory);
+ return this;
+ }
+
+ public Builder addRuleDefinition(Class<? extends RuleDefinition> ruleDefinition) {
+ dependencyGraph.createNode(ruleDefinition);
+ BlazeRule annotation = ruleDefinition.getAnnotation(BlazeRule.class);
+ for (Class<? extends RuleDefinition> ancestor : annotation.ancestors()) {
+ dependencyGraph.addEdge(ancestor, ruleDefinition);
+ }
+
+ return this;
+ }
+
+ public Builder addConfigurationOptions(Class<? extends FragmentOptions> configurationOptions) {
+ this.configurationOptions.add(configurationOptions);
+ return this;
+ }
+
+ public Builder addConfigurationFragment(ConfigurationFragmentFactory factory) {
+ configurationFragments.add(factory);
+ return this;
+ }
+
+ public Builder setConfigurationCollectionFactory(ConfigurationCollectionFactory factory) {
+ this.configurationCollectionFactory = factory;
+ return this;
+ }
+
+ public Builder setSkylarkAccessibleJavaClasses(ImmutableMap<String, SkylarkType> objects) {
+ this.skylarkAccessibleJavaClasses = objects;
+ return this;
+ }
+
+ private RuleConfiguredTargetFactory createFactory(
+ Class<? extends RuleConfiguredTargetFactory> factoryClass) {
+ try {
+ Constructor<? extends RuleConfiguredTargetFactory> ctor = factoryClass.getConstructor();
+ return ctor.newInstance();
+ } catch (NoSuchMethodException | IllegalAccessException | InstantiationException
+ | InvocationTargetException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ private RuleClass commitRuleDefinition(Class<? extends RuleDefinition> definitionClass) {
+ BlazeRule annotation = definitionClass.getAnnotation(BlazeRule.class);
+ Preconditions.checkArgument(ruleClassMap.get(annotation.name()) == null, annotation.name());
+
+ Preconditions.checkArgument(
+ annotation.type() == ABSTRACT ^
+ annotation.factoryClass() != RuleConfiguredTargetFactory.class);
+ Preconditions.checkArgument(
+ (annotation.type() != TEST) ||
+ Arrays.asList(annotation.ancestors()).contains(
+ BaseRuleClasses.TestBaseRule.class));
+
+ RuleDefinition instance;
+ try {
+ instance = definitionClass.newInstance();
+ } catch (IllegalAccessException | InstantiationException e) {
+ throw new IllegalStateException(e);
+ }
+ RuleClass[] ancestorClasses = new RuleClass[annotation.ancestors().length];
+ for (int i = 0; i < annotation.ancestors().length; i++) {
+ ancestorClasses[i] = ruleMap.get(annotation.ancestors()[i]);
+ if (ancestorClasses[i] == null) {
+ // Ancestors should have been initialized by now
+ throw new IllegalStateException("Ancestor " + annotation.ancestors()[i] + " of "
+ + annotation.name() + " is not initialized");
+ }
+ }
+
+ RuleConfiguredTargetFactory factory = null;
+ if (annotation.type() != ABSTRACT) {
+ factory = createFactory(annotation.factoryClass());
+ }
+
+ RuleClass.Builder builder = new RuleClass.Builder(
+ annotation.name(), annotation.type(), false, ancestorClasses);
+ builder.factory(factory);
+ RuleClass ruleClass = instance.build(builder, this);
+ ruleMap.put(definitionClass, ruleClass);
+ ruleClassMap.put(ruleClass.getName(), ruleClass);
+ ruleDefinitionMap.put(ruleClass.getName(), definitionClass);
+
+ return ruleClass;
+ }
+
+ public ConfiguredRuleClassProvider build() {
+ for (Node<Class<? extends RuleDefinition>> ruleDefinition :
+ dependencyGraph.getTopologicalOrder()) {
+ commitRuleDefinition(ruleDefinition.getLabel());
+ }
+
+ return new ConfiguredRuleClassProvider(
+ ImmutableMap.copyOf(ruleClassMap),
+ ImmutableMap.copyOf(ruleDefinitionMap),
+ ImmutableList.copyOf(buildInfoFactories),
+ ImmutableList.copyOf(configurationOptions),
+ ImmutableList.copyOf(configurationFragments),
+ configurationCollectionFactory,
+ prerequisiteValidator,
+ skylarkAccessibleJavaClasses);
+ }
+
+ @Override
+ public Label getLabel(String labelValue) {
+ return LABELS.getUnchecked(labelValue);
+ }
+ }
+
+ /**
+ * Used to make the label instances unique, so that we don't create a new
+ * instance for every rule.
+ */
+ private static LoadingCache<String, Label> LABELS = CacheBuilder.newBuilder().build(
+ new CacheLoader<String, Label>() {
+ @Override
+ public Label load(String from) {
+ try {
+ return Label.parseAbsolute(from);
+ } catch (Label.SyntaxException e) {
+ throw new IllegalArgumentException(from);
+ }
+ }
+ });
+
+ /**
+ * Maps rule class name to the metaclass instance for that rule.
+ */
+ private final ImmutableMap<String, RuleClass> ruleClassMap;
+
+ /**
+ * Maps rule class name to the rule definition metaclasses.
+ */
+ private final ImmutableMap<String, Class<? extends RuleDefinition>> ruleDefinitionMap;
+
+ /**
+ * The configuration options that affect the behavior of the rules.
+ */
+ private final ImmutableList<Class<? extends FragmentOptions>> configurationOptions;
+
+ /**
+ * The set of configuration fragment factories.
+ */
+ private final ImmutableList<ConfigurationFragmentFactory> configurationFragments;
+
+ /**
+ * The factory that creates the configuration collection.
+ */
+ private final ConfigurationCollectionFactory configurationCollectionFactory;
+
+ private final ImmutableList<BuildInfoFactory> buildInfoFactories;
+
+ private final PrerequisiteValidator prerequisiteValidator;
+
+ private final ImmutableMap<String, SkylarkType> skylarkAccessibleJavaClasses;
+
+ private final ValidationEnvironment skylarkValidationEnvironment;
+
+ public ConfiguredRuleClassProvider(
+ ImmutableMap<String, RuleClass> ruleClassMap,
+ ImmutableMap<String, Class<? extends RuleDefinition>> ruleDefinitionMap,
+ ImmutableList<BuildInfoFactory> buildInfoFactories,
+ ImmutableList<Class<? extends FragmentOptions>> configurationOptions,
+ ImmutableList<ConfigurationFragmentFactory> configurationFragments,
+ ConfigurationCollectionFactory configurationCollectionFactory,
+ PrerequisiteValidator prerequisiteValidator,
+ ImmutableMap<String, SkylarkType> skylarkAccessibleJavaClasses) {
+
+ this.ruleClassMap = ruleClassMap;
+ this.ruleDefinitionMap = ruleDefinitionMap;
+ this.buildInfoFactories = buildInfoFactories;
+ this.configurationOptions = configurationOptions;
+ this.configurationFragments = configurationFragments;
+ this.configurationCollectionFactory = configurationCollectionFactory;
+ this.prerequisiteValidator = prerequisiteValidator;
+ this.skylarkAccessibleJavaClasses = skylarkAccessibleJavaClasses;
+ this.skylarkValidationEnvironment = SkylarkModules.getValidationEnvironment(
+ ImmutableMap.<String, SkylarkType>builder()
+ .putAll(skylarkAccessibleJavaClasses)
+ .put("native", SkylarkType.of(NativeModule.class))
+ .build());
+ }
+
+ public PrerequisiteValidator getPrerequisiteValidator() {
+ return prerequisiteValidator;
+ }
+
+ @Override
+ public Map<String, RuleClass> getRuleClassMap() {
+ return ruleClassMap;
+ }
+
+ /**
+ * Returns a list of build info factories that are needed for the supported languages.
+ */
+ public ImmutableList<BuildInfoFactory> getBuildInfoFactories() {
+ return buildInfoFactories;
+ }
+
+ /**
+ * Returns the set of configuration fragments provided by this module.
+ */
+ public ImmutableList<ConfigurationFragmentFactory> getConfigurationFragments() {
+ return configurationFragments;
+ }
+
+ /**
+ * Returns the set of configuration options that are supported in this module.
+ */
+ public ImmutableList<Class<? extends FragmentOptions>> getConfigurationOptions() {
+ return configurationOptions;
+ }
+
+ /**
+ * Returns the definition of the rule class definition with the specified name.
+ */
+ public Class<? extends RuleDefinition> getRuleClassDefinition(String ruleClassName) {
+ return ruleDefinitionMap.get(ruleClassName);
+ }
+
+ /**
+ * Returns the configuration collection creator.
+ */
+ public ConfigurationCollectionFactory getConfigurationCollectionFactory() {
+ return configurationCollectionFactory;
+ }
+
+ /**
+ * Returns the defaults package for the default settings.
+ */
+ public String getDefaultsPackageContent() {
+ return DefaultsPackage.getDefaultsPackageContent(configurationOptions);
+ }
+
+ /**
+ * Returns the defaults package for the given options taken from an optionsProvider.
+ */
+ public String getDefaultsPackageContent(OptionsClassProvider optionsProvider) {
+ return DefaultsPackage.getDefaultsPackageContent(
+ BuildOptions.of(configurationOptions, optionsProvider));
+ }
+
+ /**
+ * Creates a BuildOptions class for the given options taken from an optionsProvider.
+ */
+ public BuildOptions createBuildOptions(OptionsClassProvider optionsProvider) {
+ return BuildOptions.of(configurationOptions, optionsProvider);
+ }
+
+ @SkylarkModule(name = "native", namespace = true, onlyLoadingPhase = true,
+ doc = "Module for native rules.")
+ private static final class NativeModule {}
+
+ public static final NativeModule nativeModule = new NativeModule();
+
+ @Override
+ public SkylarkEnvironment createSkylarkRuleClassEnvironment(
+ EventHandler eventHandler, String astFileContentHashCode) {
+ SkylarkEnvironment env = SkylarkModules.getNewEnvironment(eventHandler, astFileContentHashCode);
+ for (Map.Entry<String, SkylarkType> entry : skylarkAccessibleJavaClasses.entrySet()) {
+ env.update(entry.getKey(), entry.getValue().getType());
+ }
+ return env;
+ }
+
+ @Override
+ public ValidationEnvironment getSkylarkValidationEnvironment() {
+ return skylarkValidationEnvironment;
+ }
+
+ @Override
+ public Object getNativeModule() {
+ return nativeModule;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/ConfiguredTarget.java b/src/main/java/com/google/devtools/build/lib/analysis/ConfiguredTarget.java
new file mode 100644
index 0000000..7aad367
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/ConfiguredTarget.java
@@ -0,0 +1,47 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.packages.Target;
+
+import javax.annotation.Nullable;
+
+/**
+ * A {@link ConfiguredTarget} is conceptually a {@link TransitiveInfoCollection} coupled
+ * with the {@link Target} and {@link BuildConfiguration} objects it was created from.
+ *
+ * <p>This interface is supposed to only be used in {@link BuildView} and above. In particular,
+ * rule implementations should not be able to access the {@link ConfiguredTarget} objects
+ * associated with their direct dependencies, only the corresponding
+ * {@link TransitiveInfoCollection}s. Also, {@link ConfiguredTarget} objects should not be
+ * accessible from the action graph.
+ */
+public interface ConfiguredTarget extends TransitiveInfoCollection {
+ /**
+ * Returns the Target with which this {@link ConfiguredTarget} is associated.
+ */
+ Target getTarget();
+
+ /**
+ * <p>Returns the {@link BuildConfiguration} for which this {@link ConfiguredTarget} is
+ * defined. Configuration is defined for all configured targets with exception
+ * of the {@link InputFileConfiguredTarget} for which it is always
+ * <b>null</b>.</p>
+ */
+ @Override
+ @Nullable
+ BuildConfiguration getConfiguration();
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/ConfiguredTargetFactory.java b/src/main/java/com/google/devtools/build/lib/analysis/ConfiguredTargetFactory.java
new file mode 100644
index 0000000..d265533
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/ConfiguredTargetFactory.java
@@ -0,0 +1,302 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ListMultimap;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.ArtifactFactory;
+import com.google.devtools.build.lib.actions.ArtifactOwner;
+import com.google.devtools.build.lib.actions.FailAction;
+import com.google.devtools.build.lib.actions.Root;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration.Fragment;
+import com.google.devtools.build.lib.analysis.config.ConfigMatchingProvider;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.packages.Attribute;
+import com.google.devtools.build.lib.packages.ConstantRuleVisibility;
+import com.google.devtools.build.lib.packages.InputFile;
+import com.google.devtools.build.lib.packages.OutputFile;
+import com.google.devtools.build.lib.packages.PackageGroup;
+import com.google.devtools.build.lib.packages.PackageGroupsRuleVisibility;
+import com.google.devtools.build.lib.packages.PackageSpecification;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.RuleVisibility;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.rules.SkylarkRuleConfiguredTargetBuilder;
+import com.google.devtools.build.lib.skyframe.ConfiguredTargetKey;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+/**
+ * This class creates {@link ConfiguredTarget} instances using a given {@link
+ * ConfiguredRuleClassProvider}.
+ */
+@ThreadSafe
+public final class ConfiguredTargetFactory {
+ // This class is not meant to be outside of the analysis phase machinery and is only public
+ // in order to be accessible from the .view.skyframe package.
+
+ private final ConfiguredRuleClassProvider ruleClassProvider;
+
+ public ConfiguredTargetFactory(ConfiguredRuleClassProvider ruleClassProvider) {
+ this.ruleClassProvider = ruleClassProvider;
+ }
+
+ /**
+ * Returns the visibility of the given target. Errors during package group resolution are reported
+ * to the {@code AnalysisEnvironment}.
+ */
+ private NestedSet<PackageSpecification> convertVisibility(
+ ListMultimap<Attribute, ConfiguredTarget> prerequisiteMap, EventHandler reporter,
+ Target target, BuildConfiguration packageGroupConfiguration) {
+ RuleVisibility ruleVisibility = target.getVisibility();
+ if (ruleVisibility instanceof ConstantRuleVisibility) {
+ return ((ConstantRuleVisibility) ruleVisibility).isPubliclyVisible()
+ ? NestedSetBuilder.<PackageSpecification>create(
+ Order.STABLE_ORDER, PackageSpecification.EVERYTHING)
+ : NestedSetBuilder.<PackageSpecification>emptySet(Order.STABLE_ORDER);
+ } else if (ruleVisibility instanceof PackageGroupsRuleVisibility) {
+ PackageGroupsRuleVisibility packageGroupsVisibility =
+ (PackageGroupsRuleVisibility) ruleVisibility;
+
+ NestedSetBuilder<PackageSpecification> packageSpecifications =
+ NestedSetBuilder.stableOrder();
+ for (Label groupLabel : packageGroupsVisibility.getPackageGroups()) {
+ // PackageGroupsConfiguredTargets are always in the package-group configuration.
+ ConfiguredTarget group =
+ findPrerequisite(prerequisiteMap, groupLabel, packageGroupConfiguration);
+ PackageSpecificationProvider provider = null;
+ // group == null can only happen if the package group list comes
+ // from a default_visibility attribute, because in every other case,
+ // this missing link is caught during transitive closure visitation or
+ // if the RuleConfiguredTargetGraph threw out a visibility edge
+ // because if would have caused a cycle. The filtering should be done
+ // in a single place, ConfiguredTargetGraph, but for now, this is the
+ // minimally invasive way of providing a sane error message in case a
+ // cycle is created by a visibility attribute.
+ if (group != null) {
+ provider = group.getProvider(PackageSpecificationProvider.class);
+ }
+ if (provider != null) {
+ packageSpecifications.addTransitive(provider.getPackageSpecifications());
+ } else {
+ reporter.handle(Event.error(target.getLocation(),
+ String.format("Label '%s' does not refer to a package group", groupLabel)));
+ }
+ }
+
+ packageSpecifications.addAll(packageGroupsVisibility.getDirectPackages());
+ return packageSpecifications.build();
+ } else {
+ throw new IllegalStateException("unknown visibility");
+ }
+ }
+
+ private ConfiguredTarget findPrerequisite(
+ ListMultimap<Attribute, ConfiguredTarget> prerequisiteMap, Label label,
+ BuildConfiguration config) {
+ for (ConfiguredTarget prerequisite : prerequisiteMap.get(null)) {
+ if (prerequisite.getLabel().equals(label) && (prerequisite.getConfiguration() == config)) {
+ return prerequisite;
+ }
+ }
+ return null;
+ }
+
+ private Artifact getOutputArtifact(OutputFile outputFile, BuildConfiguration configuration,
+ boolean isFileset, ArtifactFactory artifactFactory) {
+ Rule rule = outputFile.getAssociatedRule();
+ Root root = rule.hasBinaryOutput()
+ ? configuration.getBinDirectory()
+ : configuration.getGenfilesDirectory();
+ ArtifactOwner owner =
+ new ConfiguredTargetKey(rule.getLabel(), configuration.getArtifactOwnerConfiguration());
+ PathFragment rootRelativePath = Util.getWorkspaceRelativePath(outputFile);
+ Artifact result = isFileset
+ ? artifactFactory.getFilesetArtifact(rootRelativePath, root, owner)
+ : artifactFactory.getDerivedArtifact(rootRelativePath, root, owner);
+ // The associated rule should have created the artifact.
+ Preconditions.checkNotNull(result, "no artifact for %s", rootRelativePath);
+ return result;
+ }
+
+ /**
+ * Invokes the appropriate constructor to create a {@link ConfiguredTarget} instance.
+ * <p>For use in {@code ConfiguredTargetFunction}.
+ *
+ * <p>Returns null if Skyframe deps are missing or upon certain errors.
+ */
+ @Nullable
+ public final ConfiguredTarget createConfiguredTarget(AnalysisEnvironment analysisEnvironment,
+ ArtifactFactory artifactFactory, Target target, BuildConfiguration config,
+ ListMultimap<Attribute, ConfiguredTarget> prerequisiteMap,
+ Set<ConfigMatchingProvider> configConditions)
+ throws InterruptedException {
+ if (target instanceof Rule) {
+ return createRule(
+ analysisEnvironment, (Rule) target, config, prerequisiteMap, configConditions);
+ }
+
+ // Visibility, like all package groups, doesn't have a configuration
+ NestedSet<PackageSpecification> visibility = convertVisibility(
+ prerequisiteMap, analysisEnvironment.getEventHandler(), target, null);
+ TargetContext targetContext = new TargetContext(analysisEnvironment, target, config,
+ prerequisiteMap.get(null), visibility);
+ if (target instanceof OutputFile) {
+ OutputFile outputFile = (OutputFile) target;
+ boolean isFileset = outputFile.getGeneratingRule().getRuleClass().equals("Fileset");
+ Artifact artifact = getOutputArtifact(outputFile, config, isFileset, artifactFactory);
+ TransitiveInfoCollection rule = targetContext.findDirectPrerequisite(
+ outputFile.getGeneratingRule().getLabel(), config);
+ if (isFileset) {
+ return new FilesetOutputConfiguredTarget(targetContext, outputFile, rule, artifact);
+ } else {
+ return new OutputFileConfiguredTarget(targetContext, outputFile, rule, artifact);
+ }
+ } else if (target instanceof InputFile) {
+ InputFile inputFile = (InputFile) target;
+ Artifact artifact = artifactFactory.getSourceArtifact(
+ inputFile.getExecPath(),
+ Root.asSourceRoot(inputFile.getPackage().getSourceRoot()),
+ new ConfiguredTargetKey(target.getLabel(), config));
+
+ return new InputFileConfiguredTarget(targetContext, inputFile, artifact);
+ } else if (target instanceof PackageGroup) {
+ PackageGroup packageGroup = (PackageGroup) target;
+ return new PackageGroupConfiguredTarget(targetContext, packageGroup);
+ } else {
+ throw new AssertionError("Unexpected target class: " + target.getClass().getName());
+ }
+ }
+
+ /**
+ * Factory method: constructs a RuleConfiguredTarget of the appropriate class, based on the rule
+ * class. May return null if an error occurred.
+ */
+ @Nullable
+ private ConfiguredTarget createRule(
+ AnalysisEnvironment env, Rule rule, BuildConfiguration configuration,
+ ListMultimap<Attribute, ConfiguredTarget> prerequisiteMap,
+ Set<ConfigMatchingProvider> configConditions) throws InterruptedException {
+ // Visibility computation and checking is done for every rule.
+ RuleContext ruleContext = new RuleContext.Builder(env, rule, configuration,
+ ruleClassProvider.getPrerequisiteValidator())
+ .setVisibility(convertVisibility(prerequisiteMap, env.getEventHandler(), rule, null))
+ .setPrerequisites(prerequisiteMap)
+ .setConfigConditions(configConditions)
+ .build();
+ if (ruleContext.hasErrors()) {
+ return null;
+ }
+ if (!rule.getRuleClassObject().getRequiredConfigurationFragments().isEmpty()) {
+ if (!configuration.hasAllFragments(
+ rule.getRuleClassObject().getRequiredConfigurationFragments())) {
+ if (rule.getRuleClassObject().failIfMissingConfigurationFragment()) {
+ ruleContext.ruleError(missingFragmentError(ruleContext));
+ return null;
+ }
+ return createFailConfiguredTarget(ruleContext);
+ }
+ }
+ if (rule.getRuleClassObject().isSkylarkExecutable()) {
+ // TODO(bazel-team): maybe merge with RuleConfiguredTargetBuilder?
+ return SkylarkRuleConfiguredTargetBuilder.buildRule(
+ ruleContext, rule.getRuleClassObject().getConfiguredTargetFunction());
+ } else {
+ RuleClass.ConfiguredTargetFactory<ConfiguredTarget, RuleContext> factory =
+ rule.getRuleClassObject().<ConfiguredTarget, RuleContext>getConfiguredTargetFactory();
+ Preconditions.checkNotNull(factory, rule.getRuleClassObject());
+ return factory.create(ruleContext);
+ }
+ }
+
+ private String missingFragmentError(RuleContext ruleContext) {
+ RuleClass ruleClass = ruleContext.getRule().getRuleClassObject();
+ Set<Class<?>> missingFragments = new LinkedHashSet<>();
+ for (Class<?> fragment : ruleClass.getRequiredConfigurationFragments()) {
+ if (!ruleContext.getConfiguration().hasFragment(fragment.asSubclass(Fragment.class))) {
+ missingFragments.add(fragment);
+ }
+ }
+ Preconditions.checkState(!missingFragments.isEmpty());
+ StringBuilder result = new StringBuilder();
+ result.append("all rules of type " + ruleClass.getName() + " require the presence of ");
+ List<String> names = new ArrayList<>();
+ for (Class<?> fragment : missingFragments) {
+ // TODO(bazel-team): Using getSimpleName here is sub-optimal, but we don't have anything
+ // better right now.
+ names.add(fragment.getSimpleName());
+ }
+ result.append("all of [");
+ result.append(Joiner.on(",").join(names));
+ result.append("], but these were all disabled");
+ return result.toString();
+ }
+
+ /**
+ * Constructs an {@link Aspect}. Returns null if an error occurs; in that case,
+ * {@code aspectFactory} should call one of the error reporting methods of {@link RuleContext}.
+ */
+ public Aspect createAspect(
+ AnalysisEnvironment env, RuleConfiguredTarget associatedTarget,
+ ConfiguredAspectFactory aspectFactory,
+ ListMultimap<Attribute, ConfiguredTarget> prerequisiteMap,
+ Set<ConfigMatchingProvider> configConditions) {
+ RuleContext.Builder builder = new RuleContext.Builder(env,
+ associatedTarget.getTarget(),
+ associatedTarget.getConfiguration(),
+ ruleClassProvider.getPrerequisiteValidator());
+ RuleContext ruleContext = builder
+ .setVisibility(convertVisibility(
+ prerequisiteMap, env.getEventHandler(), associatedTarget.getTarget(), null))
+ .setPrerequisites(prerequisiteMap)
+ .setConfigConditions(configConditions)
+ .build();
+ if (ruleContext.hasErrors()) {
+ return null;
+ }
+
+ return aspectFactory.create(associatedTarget, ruleContext);
+ }
+
+ /**
+ * A pseudo-implementation for configured targets that creates fail actions for all declared
+ * outputs, both implicit and explicit.
+ */
+ private static ConfiguredTarget createFailConfiguredTarget(RuleContext ruleContext) {
+ RuleConfiguredTargetBuilder builder = new RuleConfiguredTargetBuilder(ruleContext);
+ if (!ruleContext.getOutputArtifacts().isEmpty()) {
+ ruleContext.registerAction(new FailAction(ruleContext.getActionOwner(),
+ ruleContext.getOutputArtifacts(), "Can't build this"));
+ }
+ builder.add(RunfilesProvider.class, RunfilesProvider.simple(Runfiles.EMPTY));
+ return builder.build();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/DependencyResolver.java b/src/main/java/com/google/devtools/build/lib/analysis/DependencyResolver.java
new file mode 100644
index 0000000..64cddb1
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/DependencyResolver.java
@@ -0,0 +1,573 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.common.base.Function;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Sets;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.analysis.config.ConfigMatchingProvider;
+import com.google.devtools.build.lib.collect.ImmutableSortedKeyListMultimap;
+import com.google.devtools.build.lib.packages.AspectDefinition;
+import com.google.devtools.build.lib.packages.AspectFactory;
+import com.google.devtools.build.lib.packages.Attribute;
+import com.google.devtools.build.lib.packages.Attribute.ConfigurationTransition;
+import com.google.devtools.build.lib.packages.Attribute.LateBoundDefault;
+import com.google.devtools.build.lib.packages.Attribute.SplitTransition;
+import com.google.devtools.build.lib.packages.AttributeMap;
+import com.google.devtools.build.lib.packages.EnvironmentGroup;
+import com.google.devtools.build.lib.packages.InputFile;
+import com.google.devtools.build.lib.packages.NoSuchThingException;
+import com.google.devtools.build.lib.packages.OutputFile;
+import com.google.devtools.build.lib.packages.PackageGroup;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.skyframe.ConfiguredTargetKey;
+import com.google.devtools.build.lib.syntax.EvalException;
+import com.google.devtools.build.lib.syntax.Label;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+import javax.annotation.Nullable;
+
+/**
+ * Resolver for dependencies between configured targets.
+ *
+ * <p>Includes logic to derive the right configurations depending on transition type.
+ */
+public abstract class DependencyResolver {
+ /**
+ * A dependency of a configured target through a label.
+ *
+ * <p>Includes the target and the configuration of the dependency configured target and any
+ * aspects that may be required.
+ *
+ * <p>Note that the presence of an aspect here does not necessarily mean that it will be created.
+ * They will be filtered based on the {@link TransitiveInfoProvider} instances their associated
+ * configured targets have (we cannot do that here because the configured targets are not
+ * available yet). No error or warning is reported in this case, because it is expected that rules
+ * sometimes over-approximate the providers they supply in their definitions.
+ */
+ public static final class Dependency {
+
+ /**
+ * Returns the {@link ConfiguredTargetKey} for a direct dependency.
+ *
+ * <p>Essentially the same information as {@link Dependency} minus the aspects.
+ */
+ public static final Function<Dependency, ConfiguredTargetKey>
+ TO_CONFIGURED_TARGET_KEY = new Function<Dependency, ConfiguredTargetKey>() {
+ @Override
+ public ConfiguredTargetKey apply(Dependency input) {
+ return new ConfiguredTargetKey(input.getLabel(), input.getConfiguration());
+ }
+ };
+
+ private final Label label;
+ private final BuildConfiguration configuration;
+ private final ImmutableSet<Class<? extends ConfiguredAspectFactory>> aspects;
+
+ public Dependency(Label label, BuildConfiguration configuration,
+ ImmutableSet<Class<? extends ConfiguredAspectFactory>> aspects) {
+ this.label = label;
+ this.configuration = configuration;
+ this.aspects = aspects;
+ }
+
+ public Dependency(Label label, BuildConfiguration configuration) {
+ this(label, configuration, ImmutableSet.<Class<? extends ConfiguredAspectFactory>>of());
+ }
+
+ public Label getLabel() {
+ return label;
+ }
+
+ public BuildConfiguration getConfiguration() {
+ return configuration;
+ }
+
+ public ImmutableSet<Class<? extends ConfiguredAspectFactory>> getAspects() {
+ return aspects;
+ }
+ }
+
+ protected DependencyResolver() {
+ }
+
+ /**
+ * Returns ids for dependent nodes of a given node, sorted by attribute. Note that some
+ * dependencies do not have a corresponding attribute here, and we use the null attribute to
+ * represent those edges. Visibility attributes are only visited if {@code visitVisibility} is
+ * {@code true}.
+ *
+ * <p>If {@code aspect} is null, returns the dependent nodes of the configured target node
+ * representing the given target and configuration, otherwise that of the aspect node accompanying
+ * the aforementioned configured target node for the specified aspect.
+ *
+ * <p>The values are not simply labels because this also implements the first step of applying
+ * configuration transitions, namely, split transitions. This needs to be done before the labels
+ * are resolved because late bound attributes depend on the configuration. A good example for this
+ * is @{code :cc_toolchain}.
+ *
+ * <p>The long-term goal is that most configuration transitions be applied here. However, in order
+ * to do that, we first have to eliminate transitions that depend on the rule class of the
+ * dependency.
+ */
+ public final ListMultimap<Attribute, Dependency> dependentNodeMap(
+ TargetAndConfiguration node, AspectDefinition aspect,
+ Set<ConfigMatchingProvider> configConditions)
+ throws EvalException {
+ Target target = node.getTarget();
+ BuildConfiguration config = node.getConfiguration();
+ ListMultimap<Attribute, Dependency> outgoingEdges = ArrayListMultimap.create();
+ if (target instanceof OutputFile) {
+ Preconditions.checkNotNull(config);
+ visitTargetVisibility(node, outgoingEdges.get(null));
+ Rule rule = ((OutputFile) target).getGeneratingRule();
+ outgoingEdges.get(null).add(new Dependency(rule.getLabel(), config));
+ } else if (target instanceof InputFile) {
+ visitTargetVisibility(node, outgoingEdges.get(null));
+ } else if (target instanceof EnvironmentGroup) {
+ visitTargetVisibility(node, outgoingEdges.get(null));
+ } else if (target instanceof Rule) {
+ Preconditions.checkNotNull(config);
+ visitTargetVisibility(node, outgoingEdges.get(null));
+ Rule rule = (Rule) target;
+ ListMultimap<Attribute, LabelAndConfiguration> labelMap =
+ resolveAttributes(rule, aspect, config, configConditions);
+ visitRule(rule, aspect, labelMap, outgoingEdges);
+ } else if (target instanceof PackageGroup) {
+ visitPackageGroup(node, (PackageGroup) target, outgoingEdges.get(null));
+ } else {
+ throw new IllegalStateException(target.getLabel().toString());
+ }
+ return outgoingEdges;
+ }
+
+ private ListMultimap<Attribute, LabelAndConfiguration> resolveAttributes(
+ Rule rule, AspectDefinition aspect, BuildConfiguration configuration,
+ Set<ConfigMatchingProvider> configConditions)
+ throws EvalException {
+ ConfiguredAttributeMapper attributeMap = ConfiguredAttributeMapper.of(rule, configConditions);
+ attributeMap.validateAttributes();
+ List<Attribute> attributes;
+ if (aspect == null) {
+ attributes = rule.getRuleClassObject().getAttributes();
+ } else {
+ attributes = new ArrayList<>();
+ attributes.addAll(rule.getRuleClassObject().getAttributes());
+ if (aspect != null) {
+ attributes.addAll(aspect.getAttributes().values());
+ }
+ }
+
+ ImmutableSortedKeyListMultimap.Builder<Attribute, LabelAndConfiguration> result =
+ ImmutableSortedKeyListMultimap.builder();
+
+ resolveExplicitAttributes(rule, configuration, attributeMap, result);
+ resolveImplicitAttributes(rule, configuration, attributeMap, attributes, result);
+ resolveLateBoundAttributes(rule, configuration, attributeMap, attributes, result);
+
+ // Add the rule's visibility labels (which may come from the rule or from package defaults).
+ addExplicitDeps(result, rule, "visibility", rule.getVisibility().getDependencyLabels(),
+ configuration);
+
+ // Add package default constraints when the rule doesn't explicitly declare them.
+ //
+ // Note that this can have subtle implications for constraint semantics. For example: say that
+ // package defaults declare compatibility with ':foo' and rule R declares compatibility with
+ // ':bar'. Does that mean that R is compatible with [':foo', ':bar'] or just [':bar']? In other
+ // words, did R's author intend to add additional compatibility to the package defaults or to
+ // override them? More severely, what if package defaults "restrict" support to just [':baz']?
+ // Should R's declaration signify [':baz'] + ['bar'], [ORIGINAL_DEFAULTS] + ['bar'], or
+ // something else?
+ //
+ // Rather than try to answer these questions with possibly confusing logic, we take the
+ // simple approach of assigning the rule's "restriction" attribute to the rule-declared value if
+ // it exists, else the package defaults value (and likewise for "compatibility"). This may not
+ // always provide what users want, but it makes it easy for them to understand how rule
+ // declarations and package defaults intermix (and how to refactor them to get what they want).
+ //
+ // An alternative model would be to apply the "rule declaration" / "rule class defaults"
+ // relationship, i.e. the rule class' "compatibility" and "restriction" declarations are merged
+ // to generate a set of default environments, then the rule's declarations are independently
+ // processed on top of that. This protects against obscure coupling behavior between
+ // declarations from wildly different places (e.g. it offers clear answers to the examples posed
+ // above). But within the scope of a single package it seems better to keep the model simple and
+ // make the user responsible for resolving ambiguities.
+ if (!rule.isAttributeValueExplicitlySpecified(RuleClass.COMPATIBLE_ENVIRONMENT_ATTR)) {
+ addExplicitDeps(result, rule, RuleClass.COMPATIBLE_ENVIRONMENT_ATTR,
+ rule.getPackage().getDefaultCompatibleWith(), configuration);
+ }
+ if (!rule.isAttributeValueExplicitlySpecified(RuleClass.RESTRICTED_ENVIRONMENT_ATTR)) {
+ addExplicitDeps(result, rule, RuleClass.RESTRICTED_ENVIRONMENT_ATTR,
+ rule.getPackage().getDefaultRestrictedTo(), configuration);
+ }
+
+ return result.build();
+ }
+
+ /**
+ * Adds new dependencies to the given rule under the given attribute name
+ *
+ * @param result the builder for the attribute --> dependency labels map
+ * @param rule the rule being evaluated
+ * @param attrName the name of the attribute to add dependency labels to
+ * @param labels the dependencies to add
+ * @param configuration the configuration to apply to those dependencies
+ */
+ private void addExplicitDeps(
+ ImmutableSortedKeyListMultimap.Builder<Attribute, LabelAndConfiguration> result, Rule rule,
+ String attrName, Iterable<Label> labels, BuildConfiguration configuration) {
+ if (!rule.isAttrDefined(attrName, Type.LABEL_LIST)
+ && !rule.isAttrDefined(attrName, Type.NODEP_LABEL_LIST)) {
+ return;
+ }
+ Attribute attribute = rule.getRuleClassObject().getAttributeByName(attrName);
+ for (Label label : labels) {
+ // The configuration must be the configuration after the first transition step (applying
+ // split configurations). The proper configuration (null) for package groups will be set
+ // later.
+ result.put(attribute, LabelAndConfiguration.of(label, configuration));
+ }
+ }
+
+ private void resolveExplicitAttributes(Rule rule, final BuildConfiguration configuration,
+ AttributeMap attributes,
+ final ImmutableSortedKeyListMultimap.Builder<Attribute, LabelAndConfiguration> builder) {
+ attributes.visitLabels(
+ new AttributeMap.AcceptsLabelAttribute() {
+ @Override
+ public void acceptLabelAttribute(Label label, Attribute attribute) {
+ String attributeName = attribute.getName();
+ if (attributeName.equals("abi_deps")) {
+ // abi_deps is handled specially: we visit only the branch that
+ // needs to be taken based on the configuration.
+ return;
+ }
+
+ if (attribute.getType() == Type.NODEP_LABEL) {
+ return;
+ }
+
+ if (attribute.isImplicit() || attribute.isLateBound()) {
+ return;
+ }
+
+ builder.put(attribute, LabelAndConfiguration.of(label, configuration));
+ }
+ });
+
+ // TODO(bazel-team): Remove this in favor of the new configurable attributes.
+ if (attributes.getAttributeDefinition("abi_deps") != null) {
+ Attribute depsAttribute = attributes.getAttributeDefinition("deps");
+ MakeVariableExpander.Context context = new ConfigurationMakeVariableContext(
+ rule.getPackage(), configuration);
+ String abi = null;
+ try {
+ abi = MakeVariableExpander.expand(attributes.get("abi", Type.STRING), context);
+ } catch (MakeVariableExpander.ExpansionException e) {
+ // Ignore this. It will be handled during the analysis phase.
+ }
+
+ if (abi != null) {
+ for (Map.Entry<String, List<Label>> entry
+ : attributes.get("abi_deps", Type.LABEL_LIST_DICT).entrySet()) {
+ try {
+ if (Pattern.matches(entry.getKey(), abi)) {
+ for (Label label : entry.getValue()) {
+ builder.put(depsAttribute, LabelAndConfiguration.of(label, configuration));
+ }
+ }
+ } catch (PatternSyntaxException e) {
+ // Ignore this. It will be handled during the analysis phase.
+ }
+ }
+ }
+ }
+ }
+
+ private void resolveImplicitAttributes(Rule rule, BuildConfiguration configuration,
+ AttributeMap attributeMap, Iterable<Attribute> attributes,
+ ImmutableSortedKeyListMultimap.Builder<Attribute, LabelAndConfiguration> builder) {
+ // Since the attributes that come from aspects do not appear in attributeMap, we have to get
+ // their values from somewhere else. This incidentally means that aspects attributes are not
+ // configurable. It would be nice if that wasn't the case, but we'd have to revamp how
+ // attribute mapping works, which is a large chunk of work.
+ ImmutableSet<String> mappedAttributes = ImmutableSet.copyOf(attributeMap.getAttributeNames());
+ for (Attribute attribute : attributes) {
+ if (!attribute.isImplicit() || !attribute.getCondition().apply(attributeMap)) {
+ continue;
+ }
+
+ if (attribute.getType() == Type.LABEL) {
+ Label label = mappedAttributes.contains(attribute.getName())
+ ? attributeMap.get(attribute.getName(), Type.LABEL)
+ : Type.LABEL.cast(attribute.getDefaultValue(rule));
+
+ if (label != null) {
+ builder.put(attribute, LabelAndConfiguration.of(label, configuration));
+ }
+ } else if (attribute.getType() == Type.LABEL_LIST) {
+ List<Label> labelList = mappedAttributes.contains(attribute.getName())
+ ? attributeMap.get(attribute.getName(), Type.LABEL_LIST)
+ : Type.LABEL_LIST.cast(attribute.getDefaultValue(rule));
+
+ for (Label label : labelList) {
+ builder.put(attribute, LabelAndConfiguration.of(label, configuration));
+ }
+ }
+ }
+ }
+
+ private void resolveLateBoundAttributes(Rule rule, BuildConfiguration configuration,
+ AttributeMap attributeMap, Iterable<Attribute> attributes,
+ ImmutableSortedKeyListMultimap.Builder<Attribute, LabelAndConfiguration> builder)
+ throws EvalException {
+ for (Attribute attribute : attributes) {
+ if (!attribute.isLateBound() || !attribute.getCondition().apply(attributeMap)) {
+ continue;
+ }
+
+ List<BuildConfiguration> actualConfigurations = ImmutableList.of(configuration);
+ if (attribute.getConfigurationTransition() instanceof SplitTransition<?>) {
+ Preconditions.checkState(attribute.getConfigurator() == null);
+ // TODO(bazel-team): This ends up applying the split transition twice, both here and in the
+ // visitRule method below - this is not currently a problem, because the configuration graph
+ // never contains nested split transitions, so the second application is idempotent.
+ actualConfigurations = configuration.getSplitConfigurations(
+ (SplitTransition<?>) attribute.getConfigurationTransition());
+ }
+
+ for (BuildConfiguration actualConfig : actualConfigurations) {
+ @SuppressWarnings("unchecked")
+ LateBoundDefault<BuildConfiguration> lateBoundDefault =
+ (LateBoundDefault<BuildConfiguration>) attribute.getLateBoundDefault();
+ if (lateBoundDefault.useHostConfiguration()) {
+ actualConfig =
+ actualConfig.getConfiguration(ConfigurationTransition.HOST);
+ }
+ // TODO(bazel-team): This might be too expensive - can we cache this somehow?
+ if (!lateBoundDefault.getRequiredConfigurationFragments().isEmpty()) {
+ if (!actualConfig.hasAllFragments(lateBoundDefault.getRequiredConfigurationFragments())) {
+ continue;
+ }
+ }
+
+ // TODO(bazel-team): We should check if the implementation tries to access an undeclared
+ // fragment.
+ Object actualValue = lateBoundDefault.getDefault(rule, actualConfig);
+ if (attribute.getType() == Type.LABEL) {
+ Label label;
+ label = Type.LABEL.cast(actualValue);
+ if (label != null) {
+ builder.put(attribute, LabelAndConfiguration.of(label, actualConfig));
+ }
+ } else if (attribute.getType() == Type.LABEL_LIST) {
+ for (Label label : Type.LABEL_LIST.cast(actualValue)) {
+ builder.put(attribute, LabelAndConfiguration.of(label, actualConfig));
+ }
+ } else {
+ throw new IllegalStateException(String.format(
+ "Late bound attribute '%s' is not a label or a label list", attribute.getName()));
+ }
+ }
+ }
+ }
+
+ /**
+ * A variant of {@link #dependentNodeMap} that only returns the values of the resulting map, and
+ * also converts any internally thrown {@link EvalException} instances into {@link
+ * IllegalStateException}.
+ */
+ public final Collection<Dependency> dependentNodes(
+ TargetAndConfiguration node, Set<ConfigMatchingProvider> configConditions) {
+ try {
+ return dependentNodeMap(node, null, configConditions).values();
+ } catch (EvalException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ /**
+ * Converts the given multimap of attributes to labels into a multi map of attributes to
+ * {@link Dependency} objects using the proper configuration transition for each attribute.
+ *
+ * @throws IllegalArgumentException if the {@code node} does not refer to a {@link Rule} instance
+ */
+ public final Collection<Dependency> resolveRuleLabels(
+ TargetAndConfiguration node, AspectDefinition aspect, ListMultimap<Attribute,
+ LabelAndConfiguration> labelMap) {
+ Preconditions.checkArgument(node.getTarget() instanceof Rule);
+ Rule rule = (Rule) node.getTarget();
+ ListMultimap<Attribute, Dependency> outgoingEdges = ArrayListMultimap.create();
+ visitRule(rule, aspect, labelMap, outgoingEdges);
+ return outgoingEdges.values();
+ }
+
+ private void visitPackageGroup(TargetAndConfiguration node, PackageGroup packageGroup,
+ Collection<Dependency> outgoingEdges) {
+ for (Label label : packageGroup.getIncludes()) {
+ try {
+ Target target = getTarget(label);
+ if (target == null) {
+ return;
+ }
+ if (!(target instanceof PackageGroup)) {
+ // Note that this error could also be caught in PackageGroupConfiguredTarget, but since
+ // these have the null configuration, visiting the corresponding target would trigger an
+ // analysis of a rule with a null configuration, which doesn't work.
+ invalidPackageGroupReferenceHook(node, label);
+ continue;
+ }
+
+ outgoingEdges.add(new Dependency(label, node.getConfiguration()));
+ } catch (NoSuchThingException e) {
+ // Don't visit targets that don't exist (--keep_going)
+ }
+ }
+ }
+
+ private ImmutableSet<Class<? extends ConfiguredAspectFactory>> requiredAspects(
+ AspectDefinition aspect, Attribute attribute, Target target) {
+ if (!(target instanceof Rule)) {
+ return ImmutableSet.of();
+ }
+
+ RuleClass ruleClass = ((Rule) target).getRuleClassObject();
+
+ // The order of this set will be deterministic. This is necessary because this order eventually
+ // influences the order in which aspects are merged into the main configured target, which in
+ // turn influences which aspect takes precedence if two emit the same provider (maybe this
+ // should be an error)
+ Set<Class<? extends AspectFactory<?, ?, ?>>> aspectCandidates = new LinkedHashSet<>();
+ aspectCandidates.addAll(attribute.getAspects());
+ if (aspect != null) {
+ aspectCandidates.addAll(aspect.getAttributeAspects().get(attribute.getName()));
+ }
+
+ ImmutableSet.Builder<Class<? extends ConfiguredAspectFactory>> result = ImmutableSet.builder();
+ for (Class<? extends AspectFactory<?, ?, ?>> candidateClass : aspectCandidates) {
+ ConfiguredAspectFactory candidate =
+ (ConfiguredAspectFactory) AspectFactory.Util.create(candidateClass);
+ if (Sets.difference(
+ candidate.getDefinition().getRequiredProviders(),
+ ruleClass.getAdvertisedProviders()).isEmpty()) {
+ result.add(candidateClass.asSubclass(ConfiguredAspectFactory.class));
+ }
+ }
+
+ return result.build();
+ }
+
+ private void visitRule(Rule rule, AspectDefinition aspect,
+ ListMultimap<Attribute, LabelAndConfiguration> labelMap,
+ ListMultimap<Attribute, Dependency> outgoingEdges) {
+ Preconditions.checkNotNull(labelMap);
+ for (Map.Entry<Attribute, Collection<LabelAndConfiguration>> entry :
+ labelMap.asMap().entrySet()) {
+ Attribute attribute = entry.getKey();
+ for (LabelAndConfiguration dep : entry.getValue()) {
+ Label label = dep.getLabel();
+ BuildConfiguration config = dep.getConfiguration();
+
+ Target toTarget;
+ try {
+ toTarget = getTarget(label);
+ } catch (NoSuchThingException e) {
+ throw new IllegalStateException("not found: " + label + " from " + rule + " in "
+ + attribute.getName());
+ }
+ if (toTarget == null) {
+ continue;
+ }
+ Iterable<BuildConfiguration> toConfigurations = config.evaluateTransition(
+ rule, attribute, toTarget);
+ for (BuildConfiguration toConfiguration : toConfigurations) {
+ outgoingEdges.get(entry.getKey()).add(new Dependency(
+ label, toConfiguration, requiredAspects(aspect, attribute, toTarget)));
+ }
+ }
+ }
+ }
+
+ private void visitTargetVisibility(TargetAndConfiguration node,
+ Collection<Dependency> outgoingEdges) {
+ for (Label label : node.getTarget().getVisibility().getDependencyLabels()) {
+ try {
+ Target visibilityTarget = getTarget(label);
+ if (visibilityTarget == null) {
+ return;
+ }
+ if (!(visibilityTarget instanceof PackageGroup)) {
+ // Note that this error could also be caught in
+ // AbstractConfiguredTarget.convertVisibility(), but we have an
+ // opportunity here to avoid dependency cycles that result from
+ // the visibility attribute of a rule referring to a rule that
+ // depends on it (instead of its package)
+ invalidVisibilityReferenceHook(node, label);
+ continue;
+ }
+
+ // Visibility always has null configuration
+ outgoingEdges.add(new Dependency(label, null));
+ } catch (NoSuchThingException e) {
+ // Don't visit targets that don't exist (--keep_going)
+ }
+ }
+ }
+
+ /**
+ * Hook for the error case when an invalid visibility reference is found.
+ *
+ * @param node the node with the visibility attribute
+ * @param label the invalid visibility reference
+ */
+ protected abstract void invalidVisibilityReferenceHook(TargetAndConfiguration node, Label label);
+
+ /**
+ * Hook for the error case when an invalid package group reference is found.
+ *
+ * @param node the package group node with the includes attribute
+ * @param label the invalid reference
+ */
+ protected abstract void invalidPackageGroupReferenceHook(TargetAndConfiguration node,
+ Label label);
+
+ /**
+ * Returns the target by the given label.
+ *
+ * <p>Throws {@link NoSuchThingException} if the target is known not to exist.
+ *
+ * <p>Returns null if the target is not ready to be returned at this moment. If getTarget returns
+ * null once or more during a {@link #dependentNodeMap} call, the results of that call will be
+ * incomplete. For use within Skyframe, where several iterations may be needed to discover
+ * all dependencies.
+ */
+ @Nullable
+ protected abstract Target getTarget(Label label) throws NoSuchThingException;
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/ErrorConfiguredTarget.java b/src/main/java/com/google/devtools/build/lib/analysis/ErrorConfiguredTarget.java
new file mode 100644
index 0000000..aa07a7d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/ErrorConfiguredTarget.java
@@ -0,0 +1,41 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.common.collect.UnmodifiableIterator;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.packages.Target;
+
+/**
+ * A configured target that is used instead of a real configured target if there
+ * are cyclic dependencies or if any of the prerequisites has errors. This
+ * avoids accessing state that shouldn't be accessed.
+ */
+final class ErrorConfiguredTarget extends AbstractConfiguredTarget {
+ ErrorConfiguredTarget(Target target, BuildConfiguration configuration) {
+ super(target, configuration);
+ }
+
+ @Override
+ public Object get(String providerKey) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public UnmodifiableIterator<TransitiveInfoProvider> iterator() {
+ throw new IllegalStateException();
+ }
+}
+
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/ExtraActionArtifactsProvider.java b/src/main/java/com/google/devtools/build/lib/analysis/ExtraActionArtifactsProvider.java
new file mode 100644
index 0000000..05b5f37
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/ExtraActionArtifactsProvider.java
@@ -0,0 +1,103 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.syntax.Label;
+
+/**
+ * A {@link TransitiveInfoProvider} that creates extra actions.
+ */
+@Immutable
+public final class ExtraActionArtifactsProvider implements TransitiveInfoProvider {
+ public static final ExtraActionArtifactsProvider EMPTY =
+ new ExtraActionArtifactsProvider(
+ ImmutableList.<Artifact>of(),
+ NestedSetBuilder.<ExtraArtifactSet>emptySet(Order.STABLE_ORDER));
+
+ /**
+ * The set of extra artifacts provided by a single configured target.
+ */
+ @Immutable
+ public static final class ExtraArtifactSet {
+ private final Label label;
+ private final ImmutableList<Artifact> artifacts;
+
+ private ExtraArtifactSet(Label label, Iterable<Artifact> artifacts) {
+ this.label = label;
+ this.artifacts = ImmutableList.copyOf(artifacts);
+ }
+
+ public Label getLabel() {
+ return label;
+ }
+
+ public ImmutableList<Artifact> getArtifacts() {
+ return artifacts;
+ }
+
+ public static ExtraArtifactSet of(Label label, Iterable<Artifact> artifacts) {
+ return new ExtraArtifactSet(label, artifacts);
+ }
+
+ @Override
+ public int hashCode() {
+ return label.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+
+ if (!(other instanceof ExtraArtifactSet)) {
+ return false;
+ }
+
+ return label.equals(((ExtraArtifactSet) other).getLabel());
+ }
+ }
+
+ /** The outputs of the extra actions associated with this target. */
+ private ImmutableList<Artifact> extraActionArtifacts = ImmutableList.of();
+ private NestedSet<ExtraArtifactSet> transitiveExtraActionArtifacts =
+ NestedSetBuilder.emptySet(Order.STABLE_ORDER);
+
+ public ExtraActionArtifactsProvider(ImmutableList<Artifact> extraActionArtifacts,
+ NestedSet<ExtraArtifactSet> transitiveExtraActionArtifacts) {
+ this.extraActionArtifacts = extraActionArtifacts;
+ this.transitiveExtraActionArtifacts = transitiveExtraActionArtifacts;
+ }
+
+ /**
+ * The outputs of the extra actions associated with this target.
+ */
+ public ImmutableList<Artifact> getExtraActionArtifacts() {
+ return extraActionArtifacts;
+ }
+
+ /**
+ * The outputs of the extra actions in the whole transitive closure.
+ */
+ public NestedSet<ExtraArtifactSet> getTransitiveExtraActionArtifacts() {
+ return transitiveExtraActionArtifacts;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/ExtraActionsVisitor.java b/src/main/java/com/google/devtools/build/lib/analysis/ExtraActionsVisitor.java
new file mode 100644
index 0000000..79ffe80
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/ExtraActionsVisitor.java
@@ -0,0 +1,84 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Sets;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.actions.ActionGraph;
+import com.google.devtools.build.lib.actions.ActionGraphVisitor;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.rules.extra.ExtraActionSpec;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+/**
+ * A bipartite graph visitor which accumulates extra actions for a target.
+ */
+final class ExtraActionsVisitor extends ActionGraphVisitor {
+ private final RuleContext ruleContext;
+ private final Multimap<String, ExtraActionSpec> mnemonicToExtraActionMap;
+ private final List<Artifact> extraArtifacts;
+ public final Set<Action> actions = Sets.newHashSet();
+
+ /** Creates a new visitor for the extra actions associated with the given target. */
+ public ExtraActionsVisitor(RuleContext ruleContext,
+ Multimap<String, ExtraActionSpec> mnemonicToExtraActionMap) {
+ super(getActionGraph(ruleContext));
+ this.ruleContext = ruleContext;
+ this.mnemonicToExtraActionMap = mnemonicToExtraActionMap;
+ extraArtifacts = Lists.newArrayList();
+ }
+
+ public void addExtraAction(Action original) {
+ Collection<ExtraActionSpec> extraActions = mnemonicToExtraActionMap.get(
+ original.getMnemonic());
+ if (extraActions != null) {
+ for (ExtraActionSpec extraAction : extraActions) {
+ extraArtifacts.addAll(extraAction.addExtraAction(ruleContext, original));
+ }
+ }
+ }
+
+ @Override
+ protected void visitAction(Action action) {
+ actions.add(action);
+ addExtraAction(action);
+ }
+
+ /** Retrieves the collected artifacts since this method was last called and clears the list. */
+ public ImmutableList<Artifact> getAndResetExtraArtifacts() {
+ ImmutableList<Artifact> collected = ImmutableList.copyOf(extraArtifacts);
+ extraArtifacts.clear();
+ return collected;
+ }
+
+ /** Gets an action graph wrapper for the given target through its analysis environment. */
+ private static ActionGraph getActionGraph(final RuleContext ruleContext) {
+ return new ActionGraph() {
+ @Override
+ @Nullable
+ public Action getGeneratingAction(Artifact artifact) {
+ return ruleContext.getAnalysisEnvironment().getLocalGeneratingAction(artifact);
+ }
+ };
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/FileConfiguredTarget.java b/src/main/java/com/google/devtools/build/lib/analysis/FileConfiguredTarget.java
new file mode 100644
index 0000000..815eea7
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/FileConfiguredTarget.java
@@ -0,0 +1,93 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableMap.Builder;
+import com.google.common.collect.UnmodifiableIterator;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.packages.FileTarget;
+import com.google.devtools.build.lib.rules.fileset.FilesetProvider;
+import com.google.devtools.build.lib.rules.test.InstrumentedFilesProvider;
+import com.google.devtools.build.lib.util.FileType;
+
+/**
+ * A ConfiguredTarget for a source FileTarget. (Generated files use a
+ * subclass, OutputFileConfiguredTarget.)
+ */
+public abstract class FileConfiguredTarget extends AbstractConfiguredTarget
+ implements FileType.HasFilename, LicensesProvider {
+
+ private final Artifact artifact;
+ private final ImmutableMap<Class<? extends TransitiveInfoProvider>, TransitiveInfoProvider>
+ providers;
+
+ FileConfiguredTarget(TargetContext targetContext, Artifact artifact) {
+ super(targetContext);
+ NestedSet<Artifact> filesToBuild = NestedSetBuilder.create(Order.STABLE_ORDER, artifact);
+ this.artifact = artifact;
+ Builder<Class<? extends TransitiveInfoProvider>, TransitiveInfoProvider> builder = ImmutableMap
+ .<Class<? extends TransitiveInfoProvider>, TransitiveInfoProvider>builder()
+ .put(VisibilityProvider.class, this)
+ .put(LicensesProvider.class, this)
+ .put(FileProvider.class, new FileProvider(targetContext.getLabel(), filesToBuild))
+ .put(FilesToRunProvider.class, new FilesToRunProvider(targetContext.getLabel(),
+ ImmutableList.copyOf(filesToBuild), null, artifact));
+ if (this instanceof FilesetProvider) {
+ builder.put(FilesetProvider.class, this);
+ }
+ if (this instanceof InstrumentedFilesProvider) {
+ builder.put(InstrumentedFilesProvider.class, this);
+ }
+ this.providers = builder.build();
+ }
+
+ @Override
+ public FileTarget getTarget() {
+ return (FileTarget) super.getTarget();
+ }
+
+ public Artifact getArtifact() {
+ return artifact;
+ }
+
+ /**
+ * Returns the file type of this file target.
+ */
+ @Override
+ public String getFilename() {
+ return getTarget().getFilename();
+ }
+
+ @Override
+ public <P extends TransitiveInfoProvider> P getProvider(Class<P> provider) {
+ AnalysisUtils.checkProvider(provider);
+ return provider.cast(providers.get(provider));
+ }
+
+ @Override
+ public Object get(String providerKey) {
+ return null;
+ }
+
+ @Override
+ public UnmodifiableIterator<TransitiveInfoProvider> iterator() {
+ return providers.values().iterator();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/FileProvider.java b/src/main/java/com/google/devtools/build/lib/analysis/FileProvider.java
new file mode 100644
index 0000000..893f211
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/FileProvider.java
@@ -0,0 +1,76 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.syntax.SkylarkCallable;
+import com.google.devtools.build.lib.syntax.SkylarkModule;
+
+import javax.annotation.Nullable;
+
+/**
+ * A representation of the concept "this transitive info provider builds these files".
+ *
+ * <p>Every transitive info collection contains at least this provider.
+ */
+@Immutable
+@SkylarkModule(name = "file_provider", doc = "An interface for rules that provide files.")
+public final class FileProvider implements TransitiveInfoProvider {
+
+ @Nullable private final Label label;
+ private final NestedSet<Artifact> filesToBuild;
+
+ public FileProvider(@Nullable Label label, NestedSet<Artifact> filesToBuild) {
+ this.label = label;
+ this.filesToBuild = filesToBuild;
+ }
+
+ /**
+ * Returns the label that is associated with this piece of information.
+ *
+ * <p>This is usually the label of the target that provides the information.
+ */
+ @SkylarkCallable(name = "label", doc = "", structField = true)
+ public Label getLabel() {
+ if (label == null) {
+ throw new UnsupportedOperationException();
+ }
+ return label;
+ }
+
+ /**
+ * Returns the set of artifacts that are the "output" of this rule.
+ *
+ * <p>The term "output" is somewhat hazily defined; it is vaguely the set of files that are
+ * passed on to dependent rules that list the rule in their {@code srcs} attribute and the
+ * set of files that are built when a rule is mentioned on the command line. It does
+ * <b>not</b> include the runfiles; that is the bailiwick of {@code FilesToRunProvider}.
+ *
+ * <p>Note that the above definition is somewhat imprecise; in particular, when a rule is
+ * mentioned on the command line, some other files are also built
+ * {@code TopLevelArtifactHelper} and dependent rules are free to filter this set of artifacts
+ * e.g. based on their extension.
+ *
+ * <p>Also, some rules may generate artifacts that are not listed here by way of defining other
+ * implicit targets, for example, deploy jars.
+ */
+ @SkylarkCallable(name = "files_to_build", doc = "", structField = true)
+ public NestedSet<Artifact> getFilesToBuild() {
+ return filesToBuild;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/FilesToCompileProvider.java b/src/main/java/com/google/devtools/build/lib/analysis/FilesToCompileProvider.java
new file mode 100644
index 0000000..025392c
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/FilesToCompileProvider.java
@@ -0,0 +1,42 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+
+/**
+ * A {@link TransitiveInfoProvider} that provides files to be built when the {@code --compile_only}
+ * command line option is in effect. This is to avoid expensive build steps when the user only
+ * wants a quick syntax check.
+ */
+@Immutable
+public final class FilesToCompileProvider implements TransitiveInfoProvider {
+
+ private final ImmutableList<Artifact> filesToCompile;
+
+ public FilesToCompileProvider(ImmutableList<Artifact> filesToCompile) {
+ this.filesToCompile = filesToCompile;
+ }
+
+ /**
+ * Returns the list of artifacts to be built when the {@code --compile_only} command line option
+ * is in effect.
+ */
+ public ImmutableList<Artifact> getFilesToCompile() {
+ return filesToCompile;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/FilesToRunProvider.java b/src/main/java/com/google/devtools/build/lib/analysis/FilesToRunProvider.java
new file mode 100644
index 0000000..0e024b1
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/FilesToRunProvider.java
@@ -0,0 +1,80 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.syntax.Label;
+
+import javax.annotation.Nullable;
+
+/**
+ * Returns information about executables produced by a target and the files needed to run it.
+ */
+@Immutable
+public final class FilesToRunProvider implements TransitiveInfoProvider {
+
+ private final Label label;
+ private final ImmutableList<Artifact> filesToRun;
+ @Nullable private final RunfilesSupport runfilesSupport;
+ @Nullable private final Artifact executable;
+
+ public FilesToRunProvider(Label label, ImmutableList<Artifact> filesToRun,
+ @Nullable RunfilesSupport runfilesSupport, @Nullable Artifact executable) {
+ this.label = label;
+ this.filesToRun = filesToRun;
+ this.runfilesSupport = runfilesSupport;
+ this.executable = executable;
+ }
+
+ /**
+ * Returns the label that is associated with this piece of information.
+ *
+ * <p>This is usually the label of the target that provides the information.
+ */
+ public Label getLabel() {
+ return label;
+ }
+
+ /**
+ * Returns artifacts needed to run the executable for this target.
+ */
+ public ImmutableList<Artifact> getFilesToRun() {
+ return filesToRun;
+ }
+
+ /**
+ * Returns the {@RunfilesSupport} object associated with the target or null if it does not exist.
+ */
+ @Nullable public RunfilesSupport getRunfilesSupport() {
+ return runfilesSupport;
+ }
+
+ /**
+ * Returns the Executable or null if it does not exist.
+ */
+ @Nullable public Artifact getExecutable() {
+ return executable;
+ }
+
+ /**
+ * Returns the RunfilesManifest or null if it does not exist. It is a shortcut to
+ * getRunfilesSupport().getRunfilesManifest().
+ */
+ @Nullable public Artifact getRunfilesManifest() {
+ return runfilesSupport != null ? runfilesSupport.getRunfilesManifest() : null;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/FilesetOutputConfiguredTarget.java b/src/main/java/com/google/devtools/build/lib/analysis/FilesetOutputConfiguredTarget.java
new file mode 100644
index 0000000..860024d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/FilesetOutputConfiguredTarget.java
@@ -0,0 +1,55 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.packages.OutputFile;
+import com.google.devtools.build.lib.rules.fileset.FilesetProvider;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+/**
+ * A configured target for output files generated by {@code Fileset} rules. They are almost the
+ * same thing as output files except that they implement {@link FilesetProvider} so that
+ * {@code Fileset} can figure out the link tree behind them.
+ *
+ * <p>In an ideal world, this would not be needed: Filesets would depend on other Filesets and not
+ * their output directories. However, sometimes a Fileset depends on the output directory of
+ * another Fileset. Thus, we need this hack.
+ */
+public final class FilesetOutputConfiguredTarget extends OutputFileConfiguredTarget
+ implements FilesetProvider {
+ private final Artifact filesetInputManifest;
+ private final PathFragment filesetLinkDir;
+
+ FilesetOutputConfiguredTarget(TargetContext targetContext, OutputFile outputFile,
+ TransitiveInfoCollection generatingRule, Artifact outputArtifact) {
+ super(targetContext, outputFile, generatingRule, outputArtifact);
+ FilesetProvider provider = generatingRule.getProvider(FilesetProvider.class);
+ Preconditions.checkArgument(provider != null);
+ filesetInputManifest = provider.getFilesetInputManifest();
+ filesetLinkDir = provider.getFilesetLinkDir();
+ }
+
+ @Override
+ public Artifact getFilesetInputManifest() {
+ return filesetInputManifest;
+ }
+
+ @Override
+ public PathFragment getFilesetLinkDir() {
+ return filesetLinkDir;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/InputFileConfiguredTarget.java b/src/main/java/com/google/devtools/build/lib/analysis/InputFileConfiguredTarget.java
new file mode 100644
index 0000000..9e56033
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/InputFileConfiguredTarget.java
@@ -0,0 +1,68 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.packages.InputFile;
+import com.google.devtools.build.lib.packages.License;
+
+/**
+ * A ConfiguredTarget for an InputFile.
+ *
+ * All InputFiles for the same target are equivalent, so configuration does not
+ * play any role here and is always set to <b>null</b>.
+ */
+public final class InputFileConfiguredTarget extends FileConfiguredTarget {
+ private final Artifact artifact;
+ private final NestedSet<TargetLicense> licenses;
+
+ InputFileConfiguredTarget(TargetContext targetContext, InputFile inputFile, Artifact artifact) {
+ super(targetContext, artifact);
+ Preconditions.checkArgument(targetContext.getTarget() == inputFile, getLabel());
+ Preconditions.checkArgument(getConfiguration() == null, getLabel());
+ this.artifact = artifact;
+
+ if (inputFile.getLicense() != License.NO_LICENSE) {
+ licenses = NestedSetBuilder.create(Order.LINK_ORDER,
+ new TargetLicense(getLabel(), inputFile.getLicense()));
+ } else {
+ licenses = NestedSetBuilder.emptySet(Order.LINK_ORDER);
+ }
+ }
+
+ @Override
+ public InputFile getTarget() {
+ return (InputFile) super.getTarget();
+ }
+
+ @Override
+ public Artifact getArtifact() {
+ return artifact;
+ }
+
+ @Override
+ public String toString() {
+ return "InputFileConfiguredTarget(" + getTarget().getLabel() + ")";
+ }
+
+ @Override
+ public final NestedSet<TargetLicense> getTransitiveLicenses() {
+ return licenses;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/LabelAndConfiguration.java b/src/main/java/com/google/devtools/build/lib/analysis/LabelAndConfiguration.java
new file mode 100644
index 0000000..66efba3
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/LabelAndConfiguration.java
@@ -0,0 +1,76 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.syntax.Label;
+
+import java.util.Objects;
+
+import javax.annotation.Nullable;
+
+/**
+* A (label,configuration) pair.
+*/
+public final class LabelAndConfiguration {
+ private final Label label;
+ @Nullable
+ private final BuildConfiguration configuration;
+
+ private LabelAndConfiguration(Label label, @Nullable BuildConfiguration configuration) {
+ this.label = Preconditions.checkNotNull(label);
+ this.configuration = configuration;
+ }
+
+ public LabelAndConfiguration(ConfiguredTarget rule) {
+ this(rule.getTarget().getLabel(), rule.getConfiguration());
+ }
+
+ public Label getLabel() {
+ return label;
+ }
+
+ @Nullable
+ public BuildConfiguration getConfiguration() {
+ return configuration;
+ }
+
+ @Override
+ public int hashCode() {
+ int configVal = configuration == null ? 79 : configuration.hashCode();
+ return 31 * label.hashCode() + configVal;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (!(obj instanceof LabelAndConfiguration)) {
+ return false;
+ }
+ LabelAndConfiguration other = (LabelAndConfiguration) obj;
+ return Objects.equals(label, other.label) && Objects.equals(configuration, other.configuration);
+ }
+
+ public static LabelAndConfiguration of(
+ Label label, @Nullable BuildConfiguration configuration) {
+ return new LabelAndConfiguration(label, configuration);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/LabelExpander.java b/src/main/java/com/google/devtools/build/lib/analysis/LabelExpander.java
new file mode 100644
index 0000000..89ce2e7
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/LabelExpander.java
@@ -0,0 +1,181 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nullable;
+
+/**
+ * Helper class encapsulating string scanning state used during "heuristic"
+ * expansion of labels embedded within rules.
+ */
+public final class LabelExpander {
+ /**
+ * An exception that is thrown when a label is expanded to zero or multiple
+ * files during expansion.
+ */
+ public static class NotUniqueExpansionException extends Exception {
+ public NotUniqueExpansionException(int sizeOfResultSet, String labelText) {
+ super("heuristic label expansion found '" + labelText + "', which expands to "
+ + sizeOfResultSet + " files"
+ + (sizeOfResultSet > 1
+ ? ", please use $(locations " + labelText + ") instead"
+ : ""));
+ }
+ }
+
+ // This is a utility class, no need to instantiate.
+ private LabelExpander() {}
+
+ /**
+ * CharMatcher to determine if a given character is valid for labels.
+ *
+ * <p>The Build Concept Reference additionally allows '=' and ',' to appear in labels,
+ * but for the purposes of the heuristic, this function does not, as it would cause
+ * "--foo=:rule1,:rule2" to scan as a single possible label, instead of three
+ * ("--foo", ":rule1", ":rule2").
+ */
+ private static final CharMatcher LABEL_CHAR_MATCHER =
+ CharMatcher.inRange('a', 'z')
+ .or(CharMatcher.inRange('A', 'Z'))
+ .or(CharMatcher.inRange('0', '9'))
+ .or(CharMatcher.anyOf(":/_.-+" + PathFragment.SEPARATOR_CHAR));
+
+ /**
+ * Expands all references to labels embedded within a string using the
+ * provided expansion mapping from labels to artifacts.
+ *
+ * <p>Since this pass is heuristic, references to non-existent labels (such
+ * as arbitrary words) or invalid labels are simply ignored and are unchanged
+ * in the output. However, if the heuristic discovers a label, which
+ * identifies an existing target producing zero or multiple files, an error
+ * is reported.
+ *
+ * @param expression the expression to expand.
+ * @param labelMap the mapping from labels to artifacts, whose relative path
+ * is to be used as the expansion.
+ * @param labelResolver the {@code Label} that can resolve label strings
+ * to {@code Label} objects. The resolved label is either relative to
+ * {@code labelResolver} or is a global label (i.e. starts with "//").
+ * @return the expansion of the string.
+ * @throws NotUniqueExpansionException if a label that is present in the
+ * mapping expands to zero or multiple files.
+ */
+ public static <T extends Iterable<Artifact>> String expand(@Nullable String expression,
+ Map<Label, T> labelMap, Label labelResolver) throws NotUniqueExpansionException {
+ if (Strings.isNullOrEmpty(expression)) {
+ return "";
+ }
+ Preconditions.checkNotNull(labelMap);
+ Preconditions.checkNotNull(labelResolver);
+
+ int offset = 0;
+ StringBuilder result = new StringBuilder();
+ while (offset < expression.length()) {
+ String labelText = scanLabel(expression, offset);
+ if (labelText != null) {
+ offset += labelText.length();
+ result.append(tryResolvingLabelTextToArtifactPath(labelText, labelMap, labelResolver));
+ } else {
+ result.append(expression.charAt(offset));
+ offset++;
+ }
+ }
+ return result.toString();
+ }
+
+ /**
+ * Tries resolving a label text to a full label for the associated {@code
+ * Artifact}, using the provided mapping.
+ *
+ * <p>The method succeeds if the label text can be resolved to a {@code
+ * Label} object, which is present in the {@code labelMap} and maps to
+ * exactly one {@code Artifact}.
+ *
+ * @param labelText the text to resolve.
+ * @param labelMap the mapping from labels to artifacts, whose relative path
+ * is to be used as the expansion.
+ * @param labelResolver the {@code Label} that can resolve label strings
+ * to {@code Label} objects. The resolved label is either relative to
+ * {@code labelResolver} or is a global label (i.e. starts with "//").
+ * @return an absolute label to an {@code Artifact} if the resolving was
+ * successful or the original label text.
+ * @throws NotUniqueExpansionException if a label that is present in the
+ * mapping expands to zero or multiple files.
+ */
+ private static <T extends Iterable<Artifact>> String tryResolvingLabelTextToArtifactPath(
+ String labelText, Map<Label, T> labelMap, Label labelResolver)
+ throws NotUniqueExpansionException {
+ Label resolvedLabel = resolveLabelText(labelText, labelResolver);
+ if (resolvedLabel != null) {
+ Iterable<Artifact> artifacts = labelMap.get(resolvedLabel);
+ if (artifacts != null) { // resolvedLabel identifies an existing target
+ List<String> locations = new ArrayList<>();
+ Artifact.addExecPaths(artifacts, locations);
+ int resultSetSize = locations.size();
+ if (resultSetSize == 1) {
+ return Iterables.getOnlyElement(locations); // success!
+ } else {
+ throw new NotUniqueExpansionException(resultSetSize, labelText);
+ }
+ }
+ }
+ return labelText;
+ }
+
+ /**
+ * Resolves a string to a label text. Uses {@code labelResolver} to do so.
+ * The result is either relative to {@code labelResolver} or is an absolute
+ * label. In case of an invalid label text, the return value is null.
+ */
+ private static Label resolveLabelText(String labelText, Label labelResolver) {
+ try {
+ return labelResolver.getRelative(labelText);
+ } catch (Label.SyntaxException e) {
+ // It's a heuristic, so quietly ignore "errors". Because Label.getRelative never
+ // returns null, we can use null to indicate an error.
+ return null;
+ }
+ }
+
+ /**
+ * Scans the argument string from a given start position until the name of a
+ * potential label has been consumed, then returns the label text. If
+ * the expression contains no possible label starting at the start position,
+ * the return value is null.
+ */
+ private static String scanLabel(String expression, int start) {
+ int offset = start;
+ while (offset < expression.length() && LABEL_CHAR_MATCHER.matches(expression.charAt(offset))) {
+ ++offset;
+ }
+ if (offset > start) {
+ return expression.substring(start, offset);
+ } else {
+ return null;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/LanguageDependentFragment.java b/src/main/java/com/google/devtools/build/lib/analysis/LanguageDependentFragment.java
new file mode 100644
index 0000000..d42d625
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/LanguageDependentFragment.java
@@ -0,0 +1,109 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.syntax.Label;
+
+import java.util.Set;
+
+/**
+ * Transitive info provider for rules that behave differently when used from
+ * different languages.
+ *
+ * <p>Most rules generate code for a particular language or are totally language independent.
+ * Some rules, however, behave differently when depended upon from different languages.
+ * They might generate different libraries when used from different languages (and with
+ * different API versions). This interface allows code sharing between implementations.
+ *
+ * <p>This provider is not really a roll-up of transitive information.
+ */
+@Immutable
+public final class LanguageDependentFragment implements TransitiveInfoProvider {
+ /**
+ * A language that can be supported by a multi-language configured target.
+ *
+ * <p>Note that no {@code hashCode}/{@code equals} methods are provided, because these
+ * objects are expected to be compared for object identity, which is the default.
+ */
+ public static final class LibraryLanguage {
+ private final String displayName;
+
+ public LibraryLanguage(String displayName) {
+ this.displayName = displayName;
+ }
+
+ @Override
+ public String toString() {
+ return displayName;
+ }
+ }
+
+ private final Label label;
+ private final ImmutableSet<LibraryLanguage> languages;
+
+ public LanguageDependentFragment(Label label, Set<LibraryLanguage> languages) {
+ this.label = label;
+ this.languages = ImmutableSet.copyOf(languages);
+ }
+
+ /**
+ * Returns the label that is associated with this piece of information.
+ *
+ * <p>This is usually the label of the target that provides the information.
+ */
+ public Label getLabel() {
+ return label;
+ }
+
+ /**
+ * Returns a set of the languages the ConfiguredTarget generates output for.
+ * For use only by rules that directly depend on this library via a "deps" attribute.
+ */
+ public ImmutableSet<LibraryLanguage> getSupportedLanguages() {
+ return languages;
+ }
+
+ /**
+ * Routines for verifying that dependency provide the right output.
+ */
+ public static final class Checker {
+ /**
+ * Checks that given dep supports the given language.
+ */
+ public static boolean depSupportsLanguage(
+ RuleContext context, LanguageDependentFragment dep, LibraryLanguage language) {
+ if (dep.getSupportedLanguages().contains(language)) {
+ return true;
+ } else {
+ context.attributeError(
+ "deps", String.format("'%s' does not produce output for %s", dep.getLabel(), language));
+ return false;
+ }
+ }
+
+ /**
+ * Checks that all LanguageDependentFragment support the given language.
+ */
+ public static void depsSupportsLanguage(RuleContext context, LibraryLanguage language) {
+ for (LanguageDependentFragment dep :
+ context.getPrerequisites("deps", Mode.TARGET, LanguageDependentFragment.class)) {
+ depSupportsLanguage(context, dep, language);
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/LicensesProvider.java b/src/main/java/com/google/devtools/build/lib/analysis/LicensesProvider.java
new file mode 100644
index 0000000..548a1f2
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/LicensesProvider.java
@@ -0,0 +1,88 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.packages.License;
+import com.google.devtools.build.lib.syntax.Label;
+
+import java.util.Objects;
+
+/**
+ * A {@link ConfiguredTarget} that has licensed targets in its transitive closure.
+ */
+public interface LicensesProvider extends TransitiveInfoProvider {
+
+ /**
+ * The set of label - license associations in the transitive closure.
+ *
+ * <p>Always returns an empty set if {@link BuildConfiguration#checkLicenses()} is false.
+ */
+ NestedSet<TargetLicense> getTransitiveLicenses();
+
+ /**
+ * License association for a particular target.
+ */
+ public static final class TargetLicense {
+
+ private final Label label;
+ private final License license;
+
+ public TargetLicense(Label label, License license) {
+ Preconditions.checkNotNull(label);
+ Preconditions.checkNotNull(license);
+ this.label = label;
+ this.license = license;
+ }
+
+ /**
+ * Returns the label of the associated target.
+ */
+ public Label getLabel() {
+ return label;
+ }
+
+ /**
+ * Returns the license for the target.
+ */
+ public License getLicense() {
+ return license;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(label, license);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof TargetLicense)) {
+ return false;
+ }
+ TargetLicense other = (TargetLicense) obj;
+ return label.equals(other.label) && license.equals(other.license);
+ }
+
+ @Override
+ public String toString() {
+ return label + " => " + license;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/LicensesProviderImpl.java b/src/main/java/com/google/devtools/build/lib/analysis/LicensesProviderImpl.java
new file mode 100644
index 0000000..ffdf9fd
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/LicensesProviderImpl.java
@@ -0,0 +1,40 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+
+/**
+ * A {@link ConfiguredTarget} that has licensed targets in its transitive closure.
+ */
+@Immutable
+public final class LicensesProviderImpl implements LicensesProvider {
+ public static final LicensesProvider EMPTY =
+ new LicensesProviderImpl(NestedSetBuilder.<TargetLicense>emptySet(Order.LINK_ORDER));
+
+ private final NestedSet<TargetLicense> transitiveLicenses;
+
+ public LicensesProviderImpl(NestedSet<TargetLicense> transitiveLicenses) {
+ this.transitiveLicenses = transitiveLicenses;
+ }
+
+ @Override
+ public NestedSet<TargetLicense> getTransitiveLicenses() {
+ return transitiveLicenses;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/LocationExpander.java b/src/main/java/com/google/devtools/build/lib/analysis/LocationExpander.java
new file mode 100644
index 0000000..8feb28e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/LocationExpander.java
@@ -0,0 +1,260 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.packages.OutputFile;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Expands $(location) tags inside target attributes.
+ * You can specify something like this in the BUILD file:
+ *
+ * somerule(name='some name',
+ * someopt = [ '$(location //mypackage:myhelper)' ],
+ * ...)
+ *
+ * and location will be substituted with //mypackage:myhelper executable output.
+ * Note that //mypackage:myhelper should have just one output.
+ */
+public class LocationExpander {
+ private static final int MAX_PATHS_SHOWN = 5;
+ private static final String LOCATION = "$(location";
+ private final RuleContext ruleContext;
+ private Map<Label, Collection<Artifact>> locationMap;
+ private boolean allowDataAttributeEntriesInLabel = false;
+
+ /**
+ * Creates location expander helper bound to specific target and with default
+ * location map.
+ *
+ * @param ruleContext BUILD rule
+ */
+ public LocationExpander(RuleContext ruleContext) {
+ this(ruleContext, false);
+ }
+
+ public LocationExpander(RuleContext ruleContext,
+ boolean allowDataAttributeEntriesInLabel) {
+ this.ruleContext = ruleContext;
+ this.allowDataAttributeEntriesInLabel = allowDataAttributeEntriesInLabel;
+ }
+
+ public Map<Label, Collection<Artifact>> getLocationMap() {
+ if (locationMap == null) {
+ locationMap = buildLocationMap(ruleContext, allowDataAttributeEntriesInLabel);
+ }
+ return locationMap;
+ }
+
+ /**
+ * Expands attribute's location and locations tags based on the target and
+ * location map.
+ *
+ * @param attrName name of the attribute
+ * @param attrValue initial value of the attribute
+ * @return attribute value with expanded location tags or original value in
+ * case of errors
+ */
+ public String expand(String attrName, String attrValue) {
+ int restart = 0;
+
+ int attrLength = attrValue.length();
+ StringBuilder result = new StringBuilder(attrValue.length());
+
+ while (true) {
+ // (1) find '$(location ' or '$(locations '
+ String message = "$(location)";
+ boolean multiple = false;
+ int start = attrValue.indexOf(LOCATION, restart);
+ int scannedLength = LOCATION.length();
+ if (start == -1 || start + scannedLength == attrLength) {
+ result.append(attrValue.substring(restart));
+ break;
+ }
+
+ if (attrValue.charAt(start + scannedLength) == 's') {
+ scannedLength++;
+ if (start + scannedLength == attrLength) {
+ result.append(attrValue.substring(restart));
+ break;
+ }
+ message = "$(locations)";
+ multiple = true;
+ }
+
+ if (attrValue.charAt(start + scannedLength) != ' ') {
+ result.append(attrValue.substring(restart, start + scannedLength));
+ restart = start + scannedLength;
+ continue;
+ }
+ scannedLength++;
+
+ int end = attrValue.indexOf(')', start + scannedLength);
+ if (end == -1) {
+ ruleContext.attributeError(attrName, "unterminated " + message + " expression");
+ return attrValue;
+ }
+
+ // (2) parse label
+ String labelText = attrValue.substring(start + scannedLength, end);
+ Label label;
+ try {
+ label = ruleContext.getLabel().getRelative(labelText);
+ } catch (Label.SyntaxException e) {
+ ruleContext.attributeError(attrName,
+ "invalid label in " + message + " expression: " + e.getMessage());
+ return attrValue;
+ }
+
+ // (3) replace with singleton artifact, iff unique.
+ Collection<Artifact> artifacts = getLocationMap().get(label);
+ if (artifacts == null) {
+ ruleContext.attributeError(attrName,
+ "label '" + label + "' in " + message + " expression is not a "
+ + "declared prerequisite of this rule");
+ return attrValue;
+ }
+ List<String> paths = getPaths(artifacts);
+ if (paths.isEmpty()) {
+ ruleContext.attributeError(attrName,
+ "label '" + label + "' in " + message + " expression expands to no "
+ + "files");
+ return attrValue;
+ }
+
+ result.append(attrValue.substring(restart, start));
+ if (multiple) {
+ Collections.sort(paths);
+ Joiner.on(' ').appendTo(result, paths);
+ } else {
+ if (paths.size() > 1) {
+ ruleContext.attributeError(attrName,
+ String.format(
+ "label '%s' in %s expression expands to more than one file, "
+ + "please use $(locations %s) instead. Files (at most %d shown) are: %s",
+ label, message, label,
+ MAX_PATHS_SHOWN, Iterables.limit(paths, MAX_PATHS_SHOWN)));
+ return attrValue;
+ }
+ result.append(Iterables.getOnlyElement(paths));
+ }
+ restart = end + 1;
+ }
+ return result.toString();
+ }
+
+ /**
+ * Extracts all possible target locations from target specification.
+ *
+ * @param ruleContext BUILD target object
+ * @return map of all possible target locations
+ */
+ private static Map<Label, Collection<Artifact>> buildLocationMap(RuleContext ruleContext,
+ boolean allowDataAttributeEntriesInLabel) {
+ Map<Label, Collection<Artifact>> locationMap = new HashMap<>();
+
+ // Add all destination locations.
+ for (OutputFile out : ruleContext.getRule().getOutputFiles()) {
+ mapGet(locationMap, out.getLabel()).add(ruleContext.createOutputArtifact(out));
+ }
+
+ if (ruleContext.getRule().isAttrDefined("srcs", Type.LABEL_LIST)) {
+ for (FileProvider src : ruleContext
+ .getPrerequisites("srcs", Mode.TARGET, FileProvider.class)) {
+ Iterables.addAll(mapGet(locationMap, src.getLabel()), src.getFilesToBuild());
+ }
+ }
+
+ // Add all locations associated with dependencies and tools
+ List<FilesToRunProvider> depsDataAndTools = new ArrayList<>();
+ if (ruleContext.getRule().isAttrDefined("deps", Type.LABEL_LIST)) {
+ Iterables.addAll(depsDataAndTools,
+ ruleContext.getPrerequisites("deps", Mode.DONT_CHECK, FilesToRunProvider.class));
+ }
+ if (allowDataAttributeEntriesInLabel
+ && ruleContext.getRule().isAttrDefined("data", Type.LABEL_LIST)) {
+ Iterables.addAll(depsDataAndTools,
+ ruleContext.getPrerequisites("data", Mode.DATA, FilesToRunProvider.class));
+ }
+ if (ruleContext.getRule().isAttrDefined("tools", Type.LABEL_LIST)) {
+ Iterables.addAll(depsDataAndTools,
+ ruleContext.getPrerequisites("tools", Mode.HOST, FilesToRunProvider.class));
+ }
+
+ for (FilesToRunProvider dep : depsDataAndTools) {
+ Label label = dep.getLabel();
+ Artifact executableArtifact = dep.getExecutable();
+
+ // If the label has an executable artifact add that to the multimaps.
+ if (executableArtifact != null) {
+ mapGet(locationMap, label).add(executableArtifact);
+ } else {
+ mapGet(locationMap, label).addAll(dep.getFilesToRun());
+ }
+ }
+ return locationMap;
+ }
+
+ /**
+ * Extracts list of all executables associated with given collection of label
+ * artifacts.
+ *
+ * @param artifacts to get the paths of
+ * @return all associated executable paths
+ */
+ private static List<String> getPaths(Collection<Artifact> artifacts) {
+ List<String> paths = Lists.newArrayListWithCapacity(artifacts.size());
+ for (Artifact artifact : artifacts) {
+ PathFragment execPath = artifact.getExecPath();
+ if (execPath != null) { // omit middlemen etc
+ paths.add(execPath.getPathString());
+ }
+ }
+ return paths;
+ }
+
+ /**
+ * Returns the value in the specified map corresponding to 'key', creating and
+ * inserting an empty container if absent. We use Map not Multimap because
+ * we need to distinguish the cases of "empty value" and "absent key".
+ *
+ * @return the value in the specified map corresponding to 'key'
+ */
+ private static <K, V> Collection<V> mapGet(Map<K, Collection<V>> map, K key) {
+ Collection<V> values = map.get(key);
+ if (values == null) {
+ // We use sets not lists, because it's conceivable that the same label
+ // could appear twice, in "srcs" and "deps".
+ values = Sets.newHashSet();
+ map.put(key, values);
+ }
+ return values;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/MakeEnvironmentEvent.java b/src/main/java/com/google/devtools/build/lib/analysis/MakeEnvironmentEvent.java
new file mode 100644
index 0000000..f4b9ca8
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/MakeEnvironmentEvent.java
@@ -0,0 +1,40 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.common.collect.ImmutableMap;
+
+import java.util.Map;
+
+/**
+ * This event is fired once the global make environment is available.
+ */
+public final class MakeEnvironmentEvent {
+
+ private final Map<String, String> makeEnvMap;
+
+ /**
+ * Construct the event.
+ */
+ public MakeEnvironmentEvent(Map<String, String> makeEnv) {
+ makeEnvMap = ImmutableMap.copyOf(makeEnv);
+ }
+
+ /**
+ * Returns make environment variable names and values as a map.
+ */
+ public Map<String, String> getMakeEnvMap() {
+ return makeEnvMap;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/MakeVariableExpander.java b/src/main/java/com/google/devtools/build/lib/analysis/MakeVariableExpander.java
new file mode 100644
index 0000000..55366da
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/MakeVariableExpander.java
@@ -0,0 +1,201 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+/**
+ * MakeVariableExpander defines a utility method, <code>expand</code>, for
+ * expanding references to "Make" variables embedded within a string. The
+ * caller provides a Context instance which defines the expansion of each
+ * variable.
+ *
+ * <p>Note that neither <code>$(location x)</code> nor Make-isms are treated
+ * specially in any way by this class.
+ */
+public class MakeVariableExpander {
+
+ private final char[] buffer;
+ private final int length;
+ private int offset;
+
+ private MakeVariableExpander(String expression) {
+ buffer = expression.toCharArray();
+ length = buffer.length;
+ offset = 0;
+ }
+
+ /**
+ * Interface to be implemented by callers of MakeVariableExpander which
+ * defines the expansion of each "Make" variable.
+ */
+ public interface Context {
+
+ /**
+ * Returns the expansion of the specified "Make" variable.
+ *
+ * @param var the variable to expand.
+ * @return the expansion of the variable.
+ * @throws ExpansionException if the variable "var" was not defined or
+ * there was any other error while expanding "var".
+ */
+ String lookupMakeVariable(String var) throws ExpansionException;
+ }
+
+ /**
+ * Exception thrown by MakeVariableExpander.Context.expandVariable when an
+ * unknown variable is passed.
+ */
+ public static class ExpansionException extends Exception {
+ public ExpansionException(String message) {
+ super(message);
+ }
+ }
+
+ /**
+ * Expands all references to "Make" variables embedded within string "expr",
+ * using the provided Context instance to expand individual variables.
+ *
+ * @param expression the string to expand.
+ * @param context the context which defines the expansion of each individual
+ * variable.
+ * @return the expansion of "expr".
+ * @throws ExpansionException if "expr" contained undefined or ill-formed
+ * variables references.
+ */
+ public static String expand(String expression, Context context) throws ExpansionException {
+ if (expression.indexOf('$') < 0) {
+ return expression;
+ }
+ return expand(expression, context, 0);
+ }
+
+ /**
+ * If the string contains a single variable, return the expansion of that variable.
+ * Otherwise, return null.
+ */
+ public static String expandSingleVariable(String expression, Context context)
+ throws ExpansionException {
+ String var = new MakeVariableExpander(expression).getSingleVariable();
+ return (var != null) ? context.lookupMakeVariable(var) : null;
+ }
+
+ // Helper method for counting recursion depth.
+ private static String expand(String expression, Context context, int depth)
+ throws ExpansionException {
+ if (depth > 10) { // plenty!
+ throw new ExpansionException("potentially unbounded recursion during "
+ + "expansion of '" + expression + "'");
+ }
+ return new MakeVariableExpander(expression).expand(context, depth);
+ }
+
+ private String expand(Context context, int depth) throws ExpansionException {
+ StringBuilder result = new StringBuilder();
+ while (offset < length) {
+ char c = buffer[offset];
+ if (c == '$') { // variable
+ offset++;
+ if (offset >= length) {
+ throw new ExpansionException("unterminated $");
+ }
+ if (buffer[offset] == '$') {
+ result.append('$');
+ } else {
+ String var = scanVariable();
+ String value = context.lookupMakeVariable(var);
+ // To prevent infinite recursion for the ignored shell variables
+ if (!value.equals(var)) {
+ // recursively expand using Make's ":=" semantics:
+ value = expand(value, context, depth + 1);
+ }
+ result.append(value);
+ }
+ } else {
+ result.append(c);
+ }
+ offset++;
+ }
+ return result.toString();
+ }
+
+ /**
+ * Starting at the current position, scans forward until the name of a Make
+ * variable has been consumed. Returns the variable name and advances the
+ * position. If the variable is a potential shell variable returns the shell
+ * variable expression itself, so that we can let the shell handle the
+ * expansion.
+ *
+ * @return the name of the variable found at the current point.
+ * @throws ExpansionException if the variable reference was ill-formed.
+ */
+ private String scanVariable() throws ExpansionException {
+ char c = buffer[offset];
+ switch (c) {
+ case '(': { // $(SRCS)
+ offset++;
+ int start = offset;
+ while (offset < length && buffer[offset] != ')') {
+ offset++;
+ }
+ if (offset >= length) {
+ throw new ExpansionException("unterminated variable reference");
+ }
+ return new String(buffer, start, offset - start);
+ }
+ case '{': { // ${SRCS}
+ offset++;
+ int start = offset;
+ while (offset < length && buffer[offset] != '}') {
+ offset++;
+ }
+ if (offset >= length) {
+ throw new ExpansionException("unterminated variable reference");
+ }
+ String expr = new String(buffer, start, offset - start);
+ throw new ExpansionException("'${" + expr + "}' syntax is not supported; use '$(" + expr
+ + ")' instead for \"Make\" variables, or escape the '$' as "
+ + "'$$' if you intended this for the shell");
+ }
+ case '@':
+ case '<':
+ case '^':
+ return String.valueOf(c);
+ default: {
+ int start = offset;
+ while (offset + 1 < length && Character.isJavaIdentifierPart(buffer[offset + 1])) {
+ offset++;
+ }
+ String expr = new String(buffer, start, offset + 1 - start);
+ throw new ExpansionException("'$" + expr + "' syntax is not supported; use '$(" + expr
+ + ")' instead for \"Make\" variables, or escape the '$' as "
+ + "'$$' if you intended this for the shell");
+ }
+ }
+ }
+
+ /**
+ * @return the variable name if the variable spans from offset to the end of
+ * the buffer, otherwise return null.
+ * @throws ExpansionException if the variable reference was ill-formed.
+ */
+ public String getSingleVariable() throws ExpansionException {
+ if (buffer[offset] == '$') {
+ offset++;
+ String result = scanVariable();
+ if (offset + 1 == length) {
+ return result;
+ }
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/MiddlemanProvider.java b/src/main/java/com/google/devtools/build/lib/analysis/MiddlemanProvider.java
new file mode 100644
index 0000000..d8425f2
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/MiddlemanProvider.java
@@ -0,0 +1,38 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+
+/**
+ * A provider class that supplies an aggregating middleman to the targets that depend on it.
+ */
+@Immutable
+public final class MiddlemanProvider implements TransitiveInfoProvider {
+
+ private final NestedSet<Artifact> middlemanArtifact;
+
+ public MiddlemanProvider(NestedSet<Artifact> middlemanArtifact) {
+ this.middlemanArtifact = middlemanArtifact;
+ }
+
+ /**
+ * Returns the middleman for the files produced by the transitive info collection.
+ */
+ public NestedSet<Artifact> getMiddlemanArtifact() {
+ return middlemanArtifact;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/NoSuchConfiguredTargetException.java b/src/main/java/com/google/devtools/build/lib/analysis/NoSuchConfiguredTargetException.java
new file mode 100644
index 0000000..2e9bf8c
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/NoSuchConfiguredTargetException.java
@@ -0,0 +1,29 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.packages.NoSuchThingException;
+import com.google.devtools.build.lib.syntax.Label;
+
+/**
+ * Exception indicating that the required configured target is not in the
+ * analysis cache.
+ */
+public class NoSuchConfiguredTargetException extends NoSuchThingException {
+ public NoSuchConfiguredTargetException(Label label, BuildConfiguration configuration) {
+ super("not in cache: " + label + " " + configuration);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/OutputFileConfiguredTarget.java b/src/main/java/com/google/devtools/build/lib/analysis/OutputFileConfiguredTarget.java
new file mode 100644
index 0000000..51122e2
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/OutputFileConfiguredTarget.java
@@ -0,0 +1,80 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.packages.OutputFile;
+import com.google.devtools.build.lib.rules.test.InstrumentedFilesProvider;
+import com.google.devtools.build.lib.rules.test.InstrumentedFilesProviderImpl;
+
+/**
+ * A ConfiguredTarget for an OutputFile.
+ */
+public class OutputFileConfiguredTarget extends FileConfiguredTarget
+ implements InstrumentedFilesProvider {
+
+ private final TransitiveInfoCollection generatingRule;
+
+ OutputFileConfiguredTarget(
+ TargetContext targetContext, OutputFile outputFile,
+ TransitiveInfoCollection generatingRule, Artifact outputArtifact) {
+ super(targetContext, outputArtifact);
+ Preconditions.checkArgument(targetContext.getTarget() == outputFile);
+ this.generatingRule = generatingRule;
+ }
+
+ @Override
+ public OutputFile getTarget() {
+ return (OutputFile) super.getTarget();
+ }
+
+ public TransitiveInfoCollection getGeneratingRule() {
+ return generatingRule;
+ }
+
+ @Override
+ public NestedSet<TargetLicense> getTransitiveLicenses() {
+ return getProvider(LicensesProvider.class, LicensesProviderImpl.EMPTY)
+ .getTransitiveLicenses();
+ }
+
+ @Override
+ public NestedSet<Artifact> getInstrumentedFiles() {
+ return getProvider(InstrumentedFilesProvider.class, InstrumentedFilesProviderImpl.EMPTY)
+ .getInstrumentedFiles();
+ }
+
+ @Override
+ public NestedSet<Artifact> getInstrumentationMetadataFiles() {
+ return getProvider(InstrumentedFilesProvider.class, InstrumentedFilesProviderImpl.EMPTY)
+ .getInstrumentationMetadataFiles();
+ }
+
+ /**
+ * Returns the corresponding provider from the generating rule, if it is non-null, or {@code
+ * defaultValue} otherwise.
+ */
+ private <T extends TransitiveInfoProvider> T getProvider(Class<T> clazz, T defaultValue) {
+ if (generatingRule != null) {
+ T result = generatingRule.getProvider(clazz);
+ if (result != null) {
+ return result;
+ }
+ }
+ return defaultValue;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/PackageGroupConfiguredTarget.java b/src/main/java/com/google/devtools/build/lib/analysis/PackageGroupConfiguredTarget.java
new file mode 100644
index 0000000..75e2981
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/PackageGroupConfiguredTarget.java
@@ -0,0 +1,77 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.UnmodifiableIterator;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.packages.PackageGroup;
+import com.google.devtools.build.lib.packages.PackageSpecification;
+import com.google.devtools.build.lib.syntax.Label;
+
+/**
+ * Dummy ConfiguredTarget for package groups. Contains no functionality, since
+ * package groups are not really first-class Targets.
+ */
+public final class PackageGroupConfiguredTarget extends AbstractConfiguredTarget
+ implements PackageSpecificationProvider {
+ private final NestedSet<PackageSpecification> packageSpecifications;
+
+ PackageGroupConfiguredTarget(TargetContext targetContext, PackageGroup packageGroup) {
+ super(targetContext);
+ Preconditions.checkArgument(targetContext.getConfiguration() == null);
+
+ NestedSetBuilder<PackageSpecification> builder =
+ NestedSetBuilder.stableOrder();
+ for (Label label : packageGroup.getIncludes()) {
+ TransitiveInfoCollection include = targetContext.findDirectPrerequisite(
+ label, targetContext.getConfiguration());
+ PackageSpecificationProvider provider = include == null ? null :
+ include.getProvider(PackageSpecificationProvider.class);
+ if (provider == null) {
+ targetContext.getAnalysisEnvironment().getEventHandler().handle(Event.error(getTarget().getLocation(),
+ String.format("label '%s' does not refer to a package group", label)));
+ continue;
+ }
+
+ builder.addTransitive(provider.getPackageSpecifications());
+ }
+
+ builder.addAll(packageGroup.getPackageSpecifications());
+ packageSpecifications = builder.build();
+ }
+
+ @Override
+ public PackageGroup getTarget() {
+ return (PackageGroup) super.getTarget();
+ }
+
+ @Override
+ public NestedSet<PackageSpecification> getPackageSpecifications() {
+ return packageSpecifications;
+ }
+
+ @Override
+ public Object get(String providerKey) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public UnmodifiableIterator<TransitiveInfoProvider> iterator() {
+ throw new IllegalStateException();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/PackageSpecificationProvider.java b/src/main/java/com/google/devtools/build/lib/analysis/PackageSpecificationProvider.java
new file mode 100644
index 0000000..3f852c7
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/PackageSpecificationProvider.java
@@ -0,0 +1,26 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.packages.PackageSpecification;
+
+/**
+ * A {@link TransitiveInfoProvider} that describes a set of transitive package specifications
+ * used in package groups.
+ */
+public interface PackageSpecificationProvider extends TransitiveInfoProvider {
+ NestedSet<PackageSpecification> getPackageSpecifications();
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/PrerequisiteArtifacts.java b/src/main/java/com/google/devtools/build/lib/analysis/PrerequisiteArtifacts.java
new file mode 100644
index 0000000..8932d5c
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/PrerequisiteArtifacts.java
@@ -0,0 +1,106 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.util.FileType;
+import com.google.devtools.build.lib.util.FileTypeSet;
+
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+/**
+ * Contains a sequence of prerequisite artifacts and supplies methods for filtering and reporting
+ * errors on those artifacts.
+ */
+public final class PrerequisiteArtifacts {
+ private final RuleContext ruleContext;
+ private final String attributeName;
+ private final ImmutableList<Artifact> artifacts;
+
+ private PrerequisiteArtifacts(
+ RuleContext ruleContext, String attributeName, ImmutableList<Artifact> artifacts) {
+ this.ruleContext = Preconditions.checkNotNull(ruleContext);
+ this.attributeName = Preconditions.checkNotNull(attributeName);
+ this.artifacts = Preconditions.checkNotNull(artifacts);
+ }
+
+ static PrerequisiteArtifacts get(RuleContext ruleContext, String attributeName, Mode mode) {
+ Set<Artifact> result = new LinkedHashSet<>();
+ for (FileProvider target :
+ ruleContext.getPrerequisites(attributeName, mode, FileProvider.class)) {
+ Iterables.addAll(result, target.getFilesToBuild());
+ }
+ return new PrerequisiteArtifacts(ruleContext, attributeName, ImmutableList.copyOf(result));
+ }
+
+ /**
+ * Returns the artifacts this instance contains in an {@link ImmutableList}.
+ */
+ public ImmutableList<Artifact> list() {
+ return artifacts;
+ }
+
+ private PrerequisiteArtifacts filter(Predicate<String> fileType, boolean errorsForNonMatching) {
+ ImmutableList.Builder<Artifact> filtered = new ImmutableList.Builder<Artifact>();
+
+ for (Artifact artifact : artifacts) {
+ if (fileType.apply(artifact.getFilename())) {
+ filtered.add(artifact);
+ } else if (errorsForNonMatching) {
+ ruleContext.attributeError(
+ attributeName,
+ String.format("%s does not match expected type: %s", artifact, fileType));
+ }
+ }
+
+ return new PrerequisiteArtifacts(ruleContext, attributeName, filtered.build());
+ }
+
+ /**
+ * Returns an equivalent instance but only containing artifacts of the given type, reporting
+ * errors for non-matching artifacts.
+ */
+ public PrerequisiteArtifacts errorsForNonMatching(FileType fileType) {
+ return filter(fileType, /*errorsForNonMatching=*/true);
+ }
+
+ /**
+ * Returns an equivalent instance but only containing artifacts of the given types, reporting
+ * errors for non-matching artifacts.
+ */
+ public PrerequisiteArtifacts errorsForNonMatching(FileTypeSet fileTypeSet) {
+ return filter(fileTypeSet, /*errorsForNonMatching=*/true);
+ }
+
+ /**
+ * Returns an equivalent instance but only containing artifacts of the given type.
+ */
+ public PrerequisiteArtifacts filter(FileType fileType) {
+ return filter(fileType, /*errorsForNonMatching=*/false);
+ }
+
+ /**
+ * Returns an equivalent instance but only containing artifacts of the given types.
+ */
+ public PrerequisiteArtifacts filter(FileTypeSet fileTypeSet) {
+ return filter(fileTypeSet, /*errorsForNonMatching=*/false);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/PrintActionVisitor.java b/src/main/java/com/google/devtools/build/lib/analysis/PrintActionVisitor.java
new file mode 100644
index 0000000..c852734
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/PrintActionVisitor.java
@@ -0,0 +1,66 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.actions.ActionGraph;
+import com.google.devtools.build.lib.actions.ActionGraphVisitor;
+import com.google.devtools.build.lib.actions.ActionOwner;
+
+import java.util.List;
+
+/**
+ * A bipartite graph visitor which accumulates actions with matching mnemonics for a target.
+ */
+public final class PrintActionVisitor extends ActionGraphVisitor {
+ private final ConfiguredTarget target;
+ private final List<Action> actions;
+ private final Predicate<Action> actionMnemonicMatcher;
+ private final String targetConfigurationKey;
+
+ /**
+ * Creates a new visitor for the actions associated with the given target that have a matching
+ * mnemonic.
+ */
+ public PrintActionVisitor(ActionGraph actionGraph, ConfiguredTarget target,
+ Predicate<Action> actionMnemonicMatcher) {
+ super(actionGraph);
+ this.target = target;
+ this.actionMnemonicMatcher = actionMnemonicMatcher;
+ actions = Lists.newArrayList();
+ targetConfigurationKey = target.getConfiguration().shortCacheKey();
+ }
+
+ @Override
+ protected boolean shouldVisit(Action action) {
+ ActionOwner owner = action.getOwner();
+ return owner != null && target.getLabel().equals(owner.getLabel())
+ && targetConfigurationKey.equals(owner.getConfigurationShortCacheKey());
+ }
+
+ @Override
+ protected void visitAction(Action action) {
+ if (actionMnemonicMatcher.apply(action)) {
+ actions.add(action);
+ }
+ }
+
+ /** Retrieves the collected actions since this method was last called and clears the list. */
+ public ImmutableList<Action> getActions() {
+ return ImmutableList.copyOf(actions);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/PseudoAction.java b/src/main/java/com/google/devtools/build/lib/analysis/PseudoAction.java
new file mode 100644
index 0000000..00d43a3
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/PseudoAction.java
@@ -0,0 +1,95 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.devtools.build.lib.actions.AbstractAction;
+import com.google.devtools.build.lib.actions.ActionExecutionContext;
+import com.google.devtools.build.lib.actions.ActionExecutionException;
+import com.google.devtools.build.lib.actions.ActionOwner;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.Executor;
+import com.google.devtools.build.lib.actions.ResourceSet;
+import com.google.devtools.build.lib.actions.extra.ExtraActionInfo;
+import com.google.devtools.build.lib.util.Fingerprint;
+import com.google.protobuf.GeneratedMessage.GeneratedExtension;
+import com.google.protobuf.MessageLite;
+
+import java.util.Collection;
+import java.util.UUID;
+
+/**
+ * An action that is inserted into the build graph only to provide info
+ * about rules to extra_actions.
+ */
+public class PseudoAction<InfoType extends MessageLite> extends AbstractAction {
+
+ private final UUID uuid;
+ private final String mnemonic;
+ private final GeneratedExtension<ExtraActionInfo, InfoType> infoExtension;
+ private final InfoType info;
+
+ public PseudoAction(UUID uuid, ActionOwner owner,
+ Collection<Artifact> inputs, Collection<Artifact> outputs,
+ String mnemonic,
+ GeneratedExtension<ExtraActionInfo, InfoType> infoExtension, InfoType info) {
+ super(owner, inputs, outputs);
+ this.uuid = uuid;
+ this.mnemonic = mnemonic;
+ this.infoExtension = infoExtension;
+ this.info = info;
+ }
+
+ @Override
+ public String describeStrategy(Executor executor) {
+ return null;
+ }
+
+ @Override
+ public void execute(ActionExecutionContext actionExecutionContext)
+ throws ActionExecutionException {
+ throw new ActionExecutionException(
+ mnemonic + "ExtraAction should not be executed.", this, false);
+ }
+
+ @Override
+ public String getMnemonic() {
+ return mnemonic;
+ }
+
+ @Override
+ protected String computeKey() {
+ return new Fingerprint()
+ .addUUID(uuid)
+ .addBytes(info.toByteArray())
+ .hexDigestAndReset();
+ }
+
+ @Override
+ public ResourceSet estimateResourceConsumption(Executor executor) {
+ return ResourceSet.ZERO;
+ }
+
+ @Override
+ public ExtraActionInfo.Builder getExtraActionInfo() {
+ return super.getExtraActionInfo().setExtension(infoExtension, info);
+ }
+
+ public static Artifact getDummyOutput(RuleContext ruleContext) {
+ return ruleContext.getAnalysisEnvironment().getDerivedArtifact(
+ ruleContext.getLabel().toPathFragment().replaceName(
+ ruleContext.getLabel().getName() + ".extra_action_dummy"),
+ ruleContext.getConfiguration().getGenfilesDirectory());
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/RedirectChaser.java b/src/main/java/com/google/devtools/build/lib/analysis/RedirectChaser.java
new file mode 100644
index 0000000..108a577
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/RedirectChaser.java
@@ -0,0 +1,114 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.devtools.build.lib.analysis.config.ConfigurationEnvironment;
+import com.google.devtools.build.lib.analysis.config.InvalidConfigurationException;
+import com.google.devtools.build.lib.packages.AbstractAttributeMapper;
+import com.google.devtools.build.lib.packages.NoSuchPackageException;
+import com.google.devtools.build.lib.packages.NoSuchTargetException;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.syntax.Label;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+/**
+ * Tool for chasing filegroup redirects. This is mainly intended to be used during
+ * BuildConfiguration creation.
+ */
+public final class RedirectChaser {
+
+ /**
+ * Custom attribute mapper that throws an exception if an attribute's value depends on the
+ * build configuration.
+ */
+ private static class StaticValuedAttributeMapper extends AbstractAttributeMapper {
+ public StaticValuedAttributeMapper(Rule rule) {
+ super(rule.getPackage(), rule.getRuleClassObject(), rule.getLabel(),
+ rule.getAttributeContainer());
+ }
+
+ /**
+ * Returns the value of the given attribute.
+ *
+ * @throws InvalidConfigurationException if the value is configuration-dependent
+ */
+ public <T> T getAndValidate(String attributeName, Type<T> type)
+ throws InvalidConfigurationException {
+ if (getSelector(attributeName, type) != null) {
+ throw new InvalidConfigurationException
+ ("The value of '" + attributeName + "' cannot be configuration-dependent");
+ }
+ return super.get(attributeName, type);
+ }
+
+ @Override
+ protected <T> Iterable<T> visitAttribute(String attributeName, Type<T> type) {
+ throw new IllegalStateException("Attribute visitation not supported redirect resolution");
+ }
+ }
+
+ /**
+ * Follows the 'srcs' attribute of the given label recursively. Keeps repeating as long as the
+ * labels are filegroups with a single srcs entry.
+ *
+ * @param env for loading the packages
+ * @param label the label to start at
+ * @param name user-meaningful description of the content being resolved
+ * @return the label which cannot be further resolved
+ * @throws InvalidConfigurationException if something goes wrong
+ */
+ @Nullable
+ public static Label followRedirects(ConfigurationEnvironment env, Label label, String name)
+ throws InvalidConfigurationException {
+ Set<Label> visitedLabels = new HashSet<>();
+ visitedLabels.add(label);
+ try {
+ while (true) {
+ Target possibleRedirect = env.getTarget(label);
+ if (possibleRedirect == null) {
+ return null;
+ }
+ if ((possibleRedirect instanceof Rule) &&
+ "filegroup".equals(((Rule) possibleRedirect).getRuleClass())) {
+ List<Label> labels = new StaticValuedAttributeMapper((Rule) possibleRedirect)
+ .getAndValidate("srcs", Type.LABEL_LIST);
+ if (labels.size() != 1) {
+ // We can't distinguish redirects from the final filegroup, so we assume this must be
+ // the final one.
+ return label;
+ }
+ label = labels.get(0);
+ if (!visitedLabels.add(label)) {
+ throw new InvalidConfigurationException("The " + name + " points to a filegroup which "
+ + "recursively includes itself. The label " + label + " is part of the loop");
+ }
+ } else {
+ return label;
+ }
+ }
+ } catch (NoSuchPackageException e) {
+ throw new InvalidConfigurationException(e.getMessage(), e);
+ } catch (NoSuchTargetException e) {
+ return label;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/RuleConfiguredTarget.java b/src/main/java/com/google/devtools/build/lib/analysis/RuleConfiguredTarget.java
new file mode 100644
index 0000000..602e949
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/RuleConfiguredTarget.java
@@ -0,0 +1,226 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.UnmodifiableIterator;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.config.ConfigMatchingProvider;
+import com.google.devtools.build.lib.analysis.config.RunUnder;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.packages.OutputFile;
+import com.google.devtools.build.lib.packages.Rule;
+
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A generic implementation of RuleConfiguredTarget. Do not use directly. Use {@link
+ * RuleConfiguredTargetBuilder} instead.
+ */
+public final class RuleConfiguredTarget extends AbstractConfiguredTarget {
+ /**
+ * The configuration transition for an attribute through which a prerequisite
+ * is requested.
+ */
+ public enum Mode {
+ TARGET,
+ HOST,
+ DATA,
+ SPLIT,
+ DONT_CHECK
+ }
+
+ private final ImmutableMap<Class<? extends TransitiveInfoProvider>, Object> providers;
+ private final ImmutableList<Artifact> mandatoryStampFiles;
+ private final Set<ConfigMatchingProvider> configConditions;
+ private final ImmutableList<Aspect> aspects;
+
+ RuleConfiguredTarget(RuleContext ruleContext,
+ ImmutableList<Artifact> mandatoryStampFiles,
+ ImmutableMap<String, Object> skylarkProviders,
+ Map<Class<? extends TransitiveInfoProvider>, TransitiveInfoProvider> providers) {
+ super(ruleContext);
+ // We don't use ImmutableMap.Builder here to allow augmenting the initial list of 'default'
+ // providers by passing them in.
+ Map<Class<? extends TransitiveInfoProvider>, Object> providerBuilder = new LinkedHashMap<>();
+ providerBuilder.putAll(providers);
+ Preconditions.checkState(providerBuilder.containsKey(RunfilesProvider.class));
+ Preconditions.checkState(providerBuilder.containsKey(FileProvider.class));
+ Preconditions.checkState(providerBuilder.containsKey(FilesToRunProvider.class));
+
+ providerBuilder.put(SkylarkProviders.class, new SkylarkProviders(skylarkProviders));
+
+ this.providers = ImmutableMap.copyOf(providerBuilder);
+ this.mandatoryStampFiles = mandatoryStampFiles;
+ this.configConditions = ruleContext.getConfigConditions();
+ this.aspects = ImmutableList.of();
+
+ // If this rule is the run_under target, then check that we have an executable; note that
+ // run_under is only set in the target configuration, and the target must also be analyzed for
+ // the target configuration.
+ RunUnder runUnder = getConfiguration().getRunUnder();
+ if (runUnder != null && getLabel().equals(runUnder.getLabel())) {
+ if (getProvider(FilesToRunProvider.class).getExecutable() == null) {
+ ruleContext.ruleError("run_under target " + runUnder.getLabel() + " is not executable");
+ }
+ }
+
+ // Make sure that all declared output files are also created as artifacts. The
+ // CachingAnalysisEnvironment makes sure that they all have generating actions.
+ if (!ruleContext.hasErrors()) {
+ for (OutputFile out : ruleContext.getRule().getOutputFiles()) {
+ ruleContext.createOutputArtifact(out);
+ }
+ }
+ }
+
+ /**
+ * Merge a configured target with its associated aspects.
+ *
+ * <p>If aspects are present, the configured target must be created from a rule (instead of e.g.
+ * an input or an output file).
+ */
+ public static ConfiguredTarget mergeAspects(
+ ConfiguredTarget base, Iterable<Aspect> aspects) {
+ if (Iterables.isEmpty(aspects)) {
+ // If there are no aspects, don't bother with creating a proxy object
+ return base;
+ } else {
+ // Aspects can only be attached to rules for now. This invariant is upheld by
+ // DependencyResolver#requiredAspects()
+ return new RuleConfiguredTarget((RuleConfiguredTarget) base, aspects);
+ }
+ }
+
+ /**
+ * Creates an instance based on a configured target and a set of aspects.
+ */
+ private RuleConfiguredTarget(RuleConfiguredTarget base, Iterable<Aspect> aspects) {
+ super(base.getTarget(), base.getConfiguration());
+
+ Set<Class<? extends TransitiveInfoProvider>> providers = new HashSet<>();
+
+ providers.addAll(base.providers.keySet());
+ for (Aspect aspect : aspects) {
+ for (TransitiveInfoProvider aspectProvider : aspect) {
+ if (!providers.add(aspectProvider.getClass())) {
+ throw new IllegalStateException(
+ "Provider " + aspectProvider.getClass() + " provided twice");
+ }
+ }
+ }
+ this.providers = base.providers;
+ this.mandatoryStampFiles = base.mandatoryStampFiles;
+ this.configConditions = base.configConditions;
+ this.aspects = ImmutableList.copyOf(aspects);
+ }
+
+ /**
+ * The configuration conditions that trigger this rule's configurable attributes.
+ */
+ Set<ConfigMatchingProvider> getConfigConditions() {
+ return configConditions;
+ }
+
+ @Override
+ public <P extends TransitiveInfoProvider> P getProvider(Class<P> providerClass) {
+ AnalysisUtils.checkProvider(providerClass);
+ // TODO(bazel-team): Should aspects be allowed to override providers on the configured target
+ // class?
+ Object provider = providers.get(providerClass);
+ if (provider == null) {
+ for (Aspect aspect : aspects) {
+ provider = aspect.getProviders().get(providerClass);
+ if (provider != null) {
+ break;
+ }
+ }
+ }
+
+ return providerClass.cast(provider);
+ }
+
+ /**
+ * Returns a value provided by this target. Only meant to use from Skylark.
+ */
+ @Override
+ public Object get(String providerKey) {
+ return getProvider(SkylarkProviders.class).skylarkProviders.get(providerKey);
+ }
+
+ public ImmutableList<Artifact> getMandatoryStampFiles() {
+ return mandatoryStampFiles;
+ }
+
+ @Override
+ public final Rule getTarget() {
+ return (Rule) super.getTarget();
+ }
+
+ /**
+ * A helper class for transitive infos provided by Skylark rule implementations.
+ */
+ @Immutable
+ public static final class SkylarkProviders implements TransitiveInfoProvider {
+ private final ImmutableMap<String, Object> skylarkProviders;
+
+ private SkylarkProviders(ImmutableMap<String, Object> skylarkProviders) {
+ Preconditions.checkNotNull(skylarkProviders);
+ this.skylarkProviders = skylarkProviders;
+ }
+
+ /**
+ * Returns the keys for the Skylark providers.
+ */
+ public ImmutableCollection<String> getKeys() {
+ return skylarkProviders.keySet();
+ }
+ }
+
+ @Override
+ public UnmodifiableIterator<TransitiveInfoProvider> iterator() {
+ Map<Class<? extends TransitiveInfoProvider>, TransitiveInfoProvider> allProviders =
+ new LinkedHashMap<>();
+ for (int i = aspects.size() - 1; i >= 0; i++) {
+ for (TransitiveInfoProvider tip : aspects.get(i)) {
+ allProviders.put(tip.getClass(), tip);
+ }
+ }
+
+ for (Map.Entry<Class<? extends TransitiveInfoProvider>, Object> entry : providers.entrySet()) {
+ allProviders.put(entry.getKey(), entry.getKey().cast(entry.getValue()));
+ }
+
+ return ImmutableList.copyOf(allProviders.values()).iterator();
+ }
+
+ @Override
+ public String errorMessage(String name) {
+ return String.format("target (rule class of '%s') doesn't have provider '%s'.",
+ getTarget().getRuleClass(), name);
+ }
+
+ @Override
+ public ImmutableCollection<String> getKeys() {
+ return ImmutableList.<String>builder().addAll(super.getKeys())
+ .addAll(getProvider(SkylarkProviders.class).skylarkProviders.keySet()).build();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/RuleConfiguredTargetBuilder.java b/src/main/java/com/google/devtools/build/lib/analysis/RuleConfiguredTargetBuilder.java
new file mode 100644
index 0000000..b82713f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/RuleConfiguredTargetBuilder.java
@@ -0,0 +1,423 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Multimap;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.ExtraActionArtifactsProvider.ExtraArtifactSet;
+import com.google.devtools.build.lib.analysis.LicensesProvider.TargetLicense;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.analysis.constraints.ConstraintSemantics;
+import com.google.devtools.build.lib.analysis.constraints.EnvironmentCollection;
+import com.google.devtools.build.lib.analysis.constraints.SupportedEnvironments;
+import com.google.devtools.build.lib.analysis.constraints.SupportedEnvironmentsProvider;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.packages.License;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.packages.TargetUtils;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.rules.extra.ExtraActionMapProvider;
+import com.google.devtools.build.lib.rules.extra.ExtraActionSpec;
+import com.google.devtools.build.lib.rules.test.ExecutionInfoProvider;
+import com.google.devtools.build.lib.rules.test.InstrumentedFilesProvider;
+import com.google.devtools.build.lib.rules.test.TestActionBuilder;
+import com.google.devtools.build.lib.rules.test.TestProvider;
+import com.google.devtools.build.lib.rules.test.TestProvider.TestParams;
+import com.google.devtools.build.lib.syntax.ClassObject;
+import com.google.devtools.build.lib.syntax.Environment;
+import com.google.devtools.build.lib.syntax.EvalException;
+import com.google.devtools.build.lib.syntax.EvalUtils;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.syntax.SkylarkList;
+import com.google.devtools.build.lib.syntax.SkylarkNestedSet;
+
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * Builder class for analyzed rule instances (i.e., instances of {@link ConfiguredTarget}).
+ */
+public final class RuleConfiguredTargetBuilder {
+ private final RuleContext ruleContext;
+ private final Map<Class<? extends TransitiveInfoProvider>, TransitiveInfoProvider> providers =
+ new LinkedHashMap<>();
+ private final ImmutableMap.Builder<String, Object> skylarkProviders = ImmutableMap.builder();
+
+ /** These are supported by all configured targets and need to be specially handled. */
+ private NestedSet<Artifact> filesToBuild = NestedSetBuilder.emptySet(Order.STABLE_ORDER);
+ private RunfilesSupport runfilesSupport;
+ private Artifact executable;
+ private ImmutableList<Artifact> mandatoryStampFiles;
+ private ImmutableSet<Action> actionsWithoutExtraAction = ImmutableSet.of();
+
+ public RuleConfiguredTargetBuilder(RuleContext ruleContext) {
+ this.ruleContext = ruleContext;
+ add(LicensesProvider.class, initializeLicensesProvider());
+ add(VisibilityProvider.class, new VisibilityProviderImpl(ruleContext.getVisibility()));
+ }
+
+ /**
+ * Constructs the RuleConfiguredTarget instance based on the values set for this Builder.
+ */
+ public ConfiguredTarget build() {
+ if (ruleContext.getConfiguration().enforceConstraints()) {
+ checkConstraints();
+ }
+ if (ruleContext.hasErrors()) {
+ return null;
+ }
+
+ FilesToRunProvider filesToRunProvider = new FilesToRunProvider(ruleContext.getLabel(),
+ RuleContext.getFilesToRun(runfilesSupport, filesToBuild), runfilesSupport, executable);
+ add(FileProvider.class, new FileProvider(ruleContext.getLabel(), filesToBuild));
+ add(FilesToRunProvider.class, filesToRunProvider);
+
+ // Create test action and artifacts if target was successfully initialized
+ // and is a test.
+ if (TargetUtils.isTestRule(ruleContext.getTarget())) {
+ Preconditions.checkState(runfilesSupport != null);
+ add(TestProvider.class, initializeTestProvider(filesToRunProvider));
+ }
+ add(ExtraActionArtifactsProvider.class, initializeExtraActions());
+ return new RuleConfiguredTarget(
+ ruleContext, mandatoryStampFiles, skylarkProviders.build(), providers);
+ }
+
+ /**
+ * Invokes Blaze's constraint enforcement system: checks that this rule's dependencies
+ * support its environments and reports appropriate errors if violations are found. Also
+ * publishes this rule's supported environments for the rules that depend on it.
+ */
+ private void checkConstraints() {
+ if (providers.get(SupportedEnvironmentsProvider.class) == null) {
+ // Note the "environment" rule sets its own SupportedEnvironmentProvider instance, so this
+ // logic is for "normal" rules that just want to apply default semantics.
+ EnvironmentCollection supportedEnvironments =
+ ConstraintSemantics.getSupportedEnvironments(ruleContext);
+ if (supportedEnvironments != null) {
+ add(SupportedEnvironmentsProvider.class, new SupportedEnvironments(supportedEnvironments));
+ ConstraintSemantics.checkConstraints(ruleContext, supportedEnvironments);
+ }
+ }
+ }
+
+ private TestProvider initializeTestProvider(FilesToRunProvider filesToRunProvider) {
+ int explicitShardCount = ruleContext.attributes().get("shard_count", Type.INTEGER);
+ if (explicitShardCount < 0
+ && ruleContext.getRule().isAttributeValueExplicitlySpecified("shard_count")) {
+ ruleContext.attributeError("shard_count", "Must not be negative.");
+ }
+ if (explicitShardCount > 50) {
+ ruleContext.attributeError("shard_count",
+ "Having more than 50 shards is indicative of poor test organization. "
+ + "Please reduce the number of shards.");
+ }
+ final TestParams testParams = new TestActionBuilder(ruleContext)
+ .setFilesToRunProvider(filesToRunProvider)
+ .setInstrumentedFiles(findProvider(InstrumentedFilesProvider.class))
+ .setExecutionRequirements(findProvider(ExecutionInfoProvider.class))
+ .setShardCount(explicitShardCount)
+ .build();
+ final ImmutableList<String> testTags =
+ ImmutableList.copyOf(ruleContext.getRule().getRuleTags());
+ return new TestProvider(testParams, testTags);
+ }
+
+ private LicensesProvider initializeLicensesProvider() {
+ if (!ruleContext.getConfiguration().checkLicenses()) {
+ return LicensesProviderImpl.EMPTY;
+ }
+
+ NestedSetBuilder<TargetLicense> builder = NestedSetBuilder.linkOrder();
+ BuildConfiguration configuration = ruleContext.getConfiguration();
+ Rule rule = ruleContext.getRule();
+ License toolOutputLicense = rule.getToolOutputLicense(ruleContext.attributes());
+ if (configuration.isHostConfiguration() && toolOutputLicense != null) {
+ if (toolOutputLicense != License.NO_LICENSE) {
+ builder.add(new TargetLicense(rule.getLabel(), toolOutputLicense));
+ }
+ } else {
+ if (rule.getLicense() != License.NO_LICENSE) {
+ builder.add(new TargetLicense(rule.getLabel(), rule.getLicense()));
+ }
+
+ for (TransitiveInfoCollection dep : ruleContext.getConfiguredTargetMap().values()) {
+ LicensesProvider provider = dep.getProvider(LicensesProvider.class);
+ if (provider != null) {
+ builder.addTransitive(provider.getTransitiveLicenses());
+ }
+ }
+ }
+
+ return new LicensesProviderImpl(builder.build());
+ }
+
+ /**
+ * Scans {@code action_listeners} associated with this build to see if any
+ * {@code extra_actions} should be added to this configured target. If any
+ * action_listeners are present, a partial visit of the artifact/action graph
+ * is performed (for as long as actions found are owned by this {@link
+ * ConfiguredTarget}). Any actions that match the {@code action_listener}
+ * get an {@code extra_action} associated. The output artifacts of the
+ * extra_action are reported to the {@link AnalysisEnvironment} for
+ * bookkeeping.
+ */
+ private ExtraActionArtifactsProvider initializeExtraActions() {
+ BuildConfiguration configuration = ruleContext.getConfiguration();
+ if (configuration.isHostConfiguration()) {
+ return ExtraActionArtifactsProvider.EMPTY;
+ }
+
+ ImmutableList<Artifact> extraActionArtifacts = ImmutableList.of();
+ NestedSetBuilder<ExtraArtifactSet> builder = NestedSetBuilder.stableOrder();
+
+ List<Label> actionListenerLabels = configuration.getActionListeners();
+ if (!actionListenerLabels.isEmpty()
+ && ruleContext.getRule().getAttributeDefinition(":action_listener") != null) {
+ ExtraActionsVisitor visitor = new ExtraActionsVisitor(ruleContext,
+ computeMnemonicsToExtraActionMap());
+
+ // The action list is modified within the body of the loop by the addExtraAction() call,
+ // thus the copy
+ for (Action action : ImmutableList.copyOf(
+ ruleContext.getAnalysisEnvironment().getRegisteredActions())) {
+ if (!actionsWithoutExtraAction.contains(action)) {
+ visitor.addExtraAction(action);
+ }
+ }
+
+ extraActionArtifacts = visitor.getAndResetExtraArtifacts();
+ if (!extraActionArtifacts.isEmpty()) {
+ builder.add(ExtraArtifactSet.of(ruleContext.getLabel(), extraActionArtifacts));
+ }
+ }
+
+ // Add extra action artifacts from dependencies
+ for (TransitiveInfoCollection dep : ruleContext.getConfiguredTargetMap().values()) {
+ ExtraActionArtifactsProvider provider =
+ dep.getProvider(ExtraActionArtifactsProvider.class);
+ if (provider != null) {
+ builder.addTransitive(provider.getTransitiveExtraActionArtifacts());
+ }
+ }
+
+ if (mandatoryStampFiles != null && !mandatoryStampFiles.isEmpty()) {
+ builder.add(ExtraArtifactSet.of(ruleContext.getLabel(), mandatoryStampFiles));
+ }
+
+ if (extraActionArtifacts.isEmpty() && builder.isEmpty()) {
+ return ExtraActionArtifactsProvider.EMPTY;
+ }
+ return new ExtraActionArtifactsProvider(extraActionArtifacts, builder.build());
+ }
+
+ /**
+ * Populates the configuration specific mnemonicToExtraActionMap
+ * based on all action_listers selected by the user (via the blaze option
+ * --experimental_action_listener=<target>).
+ */
+ private Multimap<String, ExtraActionSpec> computeMnemonicsToExtraActionMap() {
+ // We copy the multimap here every time. This could be expensive.
+ Multimap<String, ExtraActionSpec> mnemonicToExtraActionMap = HashMultimap.create();
+ for (TransitiveInfoCollection actionListener :
+ ruleContext.getPrerequisites(":action_listener", Mode.HOST)) {
+ ExtraActionMapProvider provider = actionListener.getProvider(ExtraActionMapProvider.class);
+ if (provider == null) {
+ ruleContext.ruleError(String.format(
+ "Unable to match experimental_action_listeners to this rule. "
+ + "Specified target %s is not an action_listener rule",
+ actionListener.getLabel().toString()));
+ } else {
+ mnemonicToExtraActionMap.putAll(provider.getExtraActionMap());
+ }
+ }
+ return mnemonicToExtraActionMap;
+ }
+
+ private <T extends TransitiveInfoProvider> T findProvider(Class<T> clazz) {
+ return clazz.cast(providers.get(clazz));
+ }
+
+ /**
+ * Add a specific provider with a given value.
+ */
+ public <T extends TransitiveInfoProvider> RuleConfiguredTargetBuilder add(Class<T> key, T value) {
+ return addProvider(key, value);
+ }
+
+ /**
+ * Add a specific provider with a given value.
+ */
+ public RuleConfiguredTargetBuilder addProvider(
+ Class<? extends TransitiveInfoProvider> key, TransitiveInfoProvider value) {
+ Preconditions.checkNotNull(key);
+ Preconditions.checkNotNull(value);
+ AnalysisUtils.checkProvider(key);
+ providers.put(key, value);
+ return this;
+ }
+
+ /**
+ * Add multiple providers with given values.
+ */
+ public RuleConfiguredTargetBuilder addProviders(
+ Map<Class<? extends TransitiveInfoProvider>, TransitiveInfoProvider> providers) {
+ for (Entry<Class<? extends TransitiveInfoProvider>, TransitiveInfoProvider> provider :
+ providers.entrySet()) {
+ addProvider(provider.getKey(), provider.getValue());
+ }
+ return this;
+ }
+
+ /**
+ * Add a Skylark transitive info. The provider value must be safe (i.e. a String, a Boolean,
+ * an Integer, an Artifact, a Label, None, a Java TransitiveInfoProvider or something composed
+ * from these in Skylark using lists, sets, structs or dicts). Otherwise an EvalException is
+ * thrown.
+ */
+ public RuleConfiguredTargetBuilder addSkylarkTransitiveInfo(
+ String name, Object value, Location loc) throws EvalException {
+ try {
+ checkSkylarkObjectSafe(value);
+ } catch (IllegalArgumentException e) {
+ throw new EvalException(loc, String.format("Value of provider '%s' is of an illegal type: %s",
+ name, e.getMessage()));
+ }
+ skylarkProviders.put(name, value);
+ return this;
+ }
+
+ /**
+ * Add a Skylark transitive info. The provider value must be safe.
+ */
+ public RuleConfiguredTargetBuilder addSkylarkTransitiveInfo(
+ String name, Object value) {
+ checkSkylarkObjectSafe(value);
+ skylarkProviders.put(name, value);
+ return this;
+ }
+
+ /**
+ * Check if the value provided by a Skylark provider is safe (i.e. can be a
+ * TransitiveInfoProvider value).
+ */
+ private void checkSkylarkObjectSafe(Object value) {
+ if (!isSimpleSkylarkObjectSafe(value.getClass())
+ // Java transitive Info Providers are accessible from Skylark.
+ || value instanceof TransitiveInfoProvider) {
+ checkCompositeSkylarkObjectSafe(value);
+ }
+ }
+
+ private void checkCompositeSkylarkObjectSafe(Object object) {
+ if (object instanceof SkylarkList) {
+ SkylarkList list = (SkylarkList) object;
+ if (list == SkylarkList.EMPTY_LIST || isSimpleSkylarkObjectSafe(list.getGenericType())) {
+ // Try not to iterate over the list if avoidable.
+ return;
+ }
+ // The list can be a tuple or a list of composite items.
+ for (Object listItem : list) {
+ checkSkylarkObjectSafe(listItem);
+ }
+ return;
+ } else if (object instanceof SkylarkNestedSet) {
+ // SkylarkNestedSets cannot have composite items.
+ Class<?> genericType = ((SkylarkNestedSet) object).getGenericType();
+ if (!genericType.equals(Object.class) && !isSimpleSkylarkObjectSafe(genericType)) {
+ throw new IllegalArgumentException(EvalUtils.getDatatypeName(genericType));
+ }
+ return;
+ } else if (object instanceof Map<?, ?>) {
+ for (Map.Entry<?, ?> entry : ((Map<?, ?>) object).entrySet()) {
+ checkSkylarkObjectSafe(entry.getKey());
+ checkSkylarkObjectSafe(entry.getValue());
+ }
+ return;
+ } else if (object instanceof ClassObject) {
+ ClassObject struct = (ClassObject) object;
+ for (String key : struct.getKeys()) {
+ checkSkylarkObjectSafe(struct.getValue(key));
+ }
+ return;
+ }
+ throw new IllegalArgumentException(EvalUtils.getDatatypeName(object));
+ }
+
+ private boolean isSimpleSkylarkObjectSafe(Class<?> type) {
+ return type.equals(String.class)
+ || type.equals(Integer.class)
+ || type.equals(Boolean.class)
+ || Artifact.class.isAssignableFrom(type)
+ || type.equals(Label.class)
+ || type.equals(Environment.NoneType.class);
+ }
+
+ /**
+ * Set the runfiles support for executable targets.
+ */
+ public RuleConfiguredTargetBuilder setRunfilesSupport(
+ RunfilesSupport runfilesSupport, Artifact executable) {
+ this.runfilesSupport = runfilesSupport;
+ this.executable = executable;
+ return this;
+ }
+
+ /**
+ * Set the files to build.
+ */
+ public RuleConfiguredTargetBuilder setFilesToBuild(NestedSet<Artifact> filesToBuild) {
+ this.filesToBuild = filesToBuild;
+ return this;
+ }
+
+ /**
+ * Set the baseline coverage Artifacts.
+ */
+ public RuleConfiguredTargetBuilder setBaselineCoverageArtifacts(
+ Collection<Artifact> artifacts) {
+ return add(BaselineCoverageArtifactsProvider.class,
+ new BaselineCoverageArtifactsProvider(ImmutableList.copyOf(artifacts)));
+ }
+
+ /**
+ * Set the mandatory stamp files.
+ */
+ public RuleConfiguredTargetBuilder setMandatoryStampFiles(ImmutableList<Artifact> files) {
+ this.mandatoryStampFiles = files;
+ return this;
+ }
+
+ /**
+ * Set the extra action pseudo actions.
+ */
+ public RuleConfiguredTargetBuilder setActionsWithoutExtraAction(
+ ImmutableSet<Action> actions) {
+ this.actionsWithoutExtraAction = actions;
+ return this;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/RuleContext.java b/src/main/java/com/google/devtools/build/lib/analysis/RuleContext.java
new file mode 100644
index 0000000..9ad7c70
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/RuleContext.java
@@ -0,0 +1,1391 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Multimaps;
+import com.google.common.collect.Sets;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.actions.ActionOwner;
+import com.google.devtools.build.lib.actions.ActionRegistry;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.ArtifactOwner;
+import com.google.devtools.build.lib.actions.Root;
+import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider.PrerequisiteValidator;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.analysis.actions.ActionConstructionContext;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration.Fragment;
+import com.google.devtools.build.lib.analysis.config.ConfigMatchingProvider;
+import com.google.devtools.build.lib.collect.ImmutableSortedKeyListMultimap;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.packages.Attribute;
+import com.google.devtools.build.lib.packages.Attribute.ConfigurationTransition;
+import com.google.devtools.build.lib.packages.Attribute.SplitTransition;
+import com.google.devtools.build.lib.packages.AttributeMap;
+import com.google.devtools.build.lib.packages.FileTarget;
+import com.google.devtools.build.lib.packages.ImplicitOutputsFunction;
+import com.google.devtools.build.lib.packages.InputFile;
+import com.google.devtools.build.lib.packages.OutputFile;
+import com.google.devtools.build.lib.packages.PackageSpecification;
+import com.google.devtools.build.lib.packages.RawAttributeMapper;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.RuleErrorConsumer;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.packages.TargetUtils;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.rules.fileset.FilesetProvider;
+import com.google.devtools.build.lib.shell.ShellUtils;
+import com.google.devtools.build.lib.syntax.EvalException;
+import com.google.devtools.build.lib.syntax.FilesetEntry;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.util.FileTypeSet;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+/**
+ * A helper class for rule implementations building and initialization. Objects of this
+ * class are intended to be passed to the builder for the configured target, which then creates the
+ * configured target.
+ */
+public final class RuleContext extends TargetContext
+ implements ActionConstructionContext, ActionRegistry, RuleErrorConsumer {
+
+ /**
+ * The configured version of FilesetEntry.
+ */
+ @Immutable
+ public static final class ConfiguredFilesetEntry {
+ private final FilesetEntry entry;
+ private final TransitiveInfoCollection src;
+ private final ImmutableList<TransitiveInfoCollection> files;
+
+ ConfiguredFilesetEntry(FilesetEntry entry, TransitiveInfoCollection src) {
+ this.entry = entry;
+ this.src = src;
+ this.files = null;
+ }
+
+ ConfiguredFilesetEntry(FilesetEntry entry, ImmutableList<TransitiveInfoCollection> files) {
+ this.entry = entry;
+ this.src = null;
+ this.files = files;
+ }
+
+ public FilesetEntry getEntry() {
+ return entry;
+ }
+
+ public TransitiveInfoCollection getSrc() {
+ return src;
+ }
+
+ /**
+ * Targets from FilesetEntry.files, or null if the user omitted it.
+ */
+ @Nullable
+ public List<TransitiveInfoCollection> getFiles() {
+ return files;
+ }
+ }
+
+ static final String HOST_CONFIGURATION_PROGRESS_TAG = "for host";
+
+ private final Rule rule;
+ private final ListMultimap<String, ConfiguredTarget> targetMap;
+ private final ListMultimap<String, ConfiguredFilesetEntry> filesetEntryMap;
+ private final Set<ConfigMatchingProvider> configConditions;
+ private final AttributeMap attributes;
+ private final ImmutableSet<String> features;
+
+ private ActionOwner actionOwner;
+
+ /* lazily computed cache for Make variables, computed from the above. See get... method */
+ private transient ConfigurationMakeVariableContext configurationMakeVariableContext = null;
+
+ private RuleContext(Builder builder, ListMultimap<String, ConfiguredTarget> targetMap,
+ ListMultimap<String, ConfiguredFilesetEntry> filesetEntryMap,
+ Set<ConfigMatchingProvider> configConditions, ImmutableSet<String> features) {
+ super(builder.env, builder.rule, builder.configuration, builder.prerequisiteMap.get(null),
+ builder.visibility);
+ this.rule = builder.rule;
+ this.targetMap = targetMap;
+ this.filesetEntryMap = filesetEntryMap;
+ this.configConditions = configConditions;
+ this.attributes =
+ ConfiguredAttributeMapper.of(builder.rule, configConditions);
+ this.features = features;
+ }
+
+ @Override
+ public Rule getRule() {
+ return rule;
+ }
+
+ /**
+ * The configuration conditions that trigger this rule's configurable attributes.
+ */
+ Set<ConfigMatchingProvider> getConfigConditions() {
+ return configConditions;
+ }
+
+ /**
+ * Returns the host configuration for this rule; keep in mind that there may be multiple different
+ * host configurations, even during a single build.
+ */
+ public BuildConfiguration getHostConfiguration() {
+ BuildConfiguration configuration = getConfiguration();
+ // Note: the Builder checks that the configuration is non-null.
+ return configuration.getConfiguration(ConfigurationTransition.HOST);
+ }
+
+ /**
+ * Accessor for the Rule's attribute values.
+ */
+ public AttributeMap attributes() {
+ return attributes;
+ }
+
+ /**
+ * Returns whether this instance is known to have errors at this point during analysis. Do not
+ * call this method after the initializationHook has returned.
+ */
+ public boolean hasErrors() {
+ return getAnalysisEnvironment().hasErrors();
+ }
+
+ /**
+ * Returns an immutable map from attribute name to list of configured targets for that attribute.
+ */
+ public ListMultimap<String, ? extends TransitiveInfoCollection> getConfiguredTargetMap() {
+ return targetMap;
+ }
+
+ /**
+ * Returns an immutable map from attribute name to list of fileset entries.
+ */
+ public ListMultimap<String, ConfiguredFilesetEntry> getFilesetEntryMap() {
+ return filesetEntryMap;
+ }
+
+ @Override
+ public ActionOwner getActionOwner() {
+ if (actionOwner == null) {
+ actionOwner = new RuleActionOwner(rule, getConfiguration());
+ }
+ return actionOwner;
+ }
+
+ /**
+ * Returns a configuration fragment for this this target.
+ */
+ @Nullable
+ public <T extends Fragment> T getFragment(Class<T> fragment) {
+ // TODO(bazel-team): The fragments can also be accessed directly through BuildConfiguration.
+ // Can we lock that down somehow?
+ Preconditions.checkArgument(
+ rule.getRuleClassObject().isLegalConfigurationFragment(fragment),
+ "%s does not have access to %s", rule.getRuleClass(), fragment);
+ return getConfiguration().getFragment(fragment);
+ }
+
+ @Override
+ public ArtifactOwner getOwner() {
+ return getAnalysisEnvironment().getOwner();
+ }
+
+ // TODO(bazel-team): This class could be simpler if Rule and BuildConfiguration classes
+ // were immutable. Then we would need to store only references those two.
+ @Immutable
+ private static final class RuleActionOwner implements ActionOwner {
+ private final Label label;
+ private final Location location;
+ private final String configurationName;
+ private final String mnemonic;
+ private final String targetKind;
+ private final String shortCacheKey;
+ private final boolean hostConfiguration;
+
+ private RuleActionOwner(Rule rule, BuildConfiguration configuration) {
+ this.label = rule.getLabel();
+ this.location = rule.getLocation();
+ this.targetKind = rule.getTargetKind();
+ this.configurationName = configuration.getShortName();
+ this.mnemonic = configuration.getMnemonic();
+ this.shortCacheKey = configuration.shortCacheKey();
+ this.hostConfiguration = configuration.isHostConfiguration();
+ }
+
+ @Override
+ public Location getLocation() {
+ return location;
+ }
+
+ @Override
+ public Label getLabel() {
+ return label;
+ }
+
+ @Override
+ public String getConfigurationName() {
+ return configurationName;
+ }
+
+ @Override
+ public String getConfigurationMnemonic() {
+ return mnemonic;
+ }
+
+ @Override
+ public String getConfigurationShortCacheKey() {
+ return shortCacheKey;
+ }
+
+ @Override
+ public String getTargetKind() {
+ return targetKind;
+ }
+
+ @Override
+ public String getAdditionalProgressInfo() {
+ return hostConfiguration ? HOST_CONFIGURATION_PROGRESS_TAG : null;
+ }
+ }
+
+ @Override
+ public void registerAction(Action... action) {
+ getAnalysisEnvironment().registerAction(action);
+ }
+
+ /**
+ * Convenience function for subclasses to report non-attribute-specific
+ * errors in the current rule.
+ */
+ @Override
+ public void ruleError(String message) {
+ reportError(rule.getLocation(), prefixRuleMessage(message));
+ }
+
+ /**
+ * Convenience function for subclasses to report non-attribute-specific
+ * warnings in the current rule.
+ */
+ @Override
+ public void ruleWarning(String message) {
+ reportWarning(rule.getLocation(), prefixRuleMessage(message));
+ }
+
+ /**
+ * Convenience function for subclasses to report attribute-specific errors in
+ * the current rule.
+ *
+ * <p>If the name of the attribute starts with <code>$</code>
+ * it is replaced with a string <code>(an implicit dependency)</code>.
+ */
+ @Override
+ public void attributeError(String attrName, String message) {
+ reportError(rule.getAttributeLocation(attrName),
+ prefixAttributeMessage(Attribute.isImplicit(attrName)
+ ? "(an implicit dependency)"
+ : attrName,
+ message));
+ }
+
+ /**
+ * Like attributeError, but does not mark the configured target as errored.
+ *
+ * <p>If the name of the attribute starts with <code>$</code>
+ * it is replaced with a string <code>(an implicit dependency)</code>.
+ */
+ @Override
+ public void attributeWarning(String attrName, String message) {
+ reportWarning(rule.getAttributeLocation(attrName),
+ prefixAttributeMessage(Attribute.isImplicit(attrName)
+ ? "(an implicit dependency)"
+ : attrName,
+ message));
+ }
+
+ private String prefixAttributeMessage(String attrName, String message) {
+ return "in " + attrName + " attribute of "
+ + rule.getRuleClass() + " rule "
+ + getLabel() + ": " + message;
+ }
+
+ private String prefixRuleMessage(String message) {
+ return "in " + rule.getRuleClass() + " rule "
+ + getLabel() + ": " + message;
+ }
+
+ private void reportError(Location location, String message) {
+ getAnalysisEnvironment().getEventHandler().handle(Event.error(location, message));
+ }
+
+ private void reportWarning(Location location, String message) {
+ getAnalysisEnvironment().getEventHandler().handle(Event.warn(location, message));
+ }
+
+ /**
+ * Returns an artifact beneath the root of either the "bin" or "genfiles"
+ * tree, whose path is based on the name of this target and the current
+ * configuration. The choice of which tree to use is based on the rule with
+ * which this target (which must be an OutputFile or a Rule) is associated.
+ */
+ public Artifact createOutputArtifact() {
+ return internalCreateOutputArtifact(getTarget());
+ }
+
+ /**
+ * Returns the output artifact of an {@link OutputFile} of this target.
+ *
+ * @see #createOutputArtifact()
+ */
+ public Artifact createOutputArtifact(OutputFile out) {
+ return internalCreateOutputArtifact(out);
+ }
+
+ /**
+ * Implementation for {@link #createOutputArtifact()} and
+ * {@link #createOutputArtifact(OutputFile)}. This is private so that
+ * {@link #createOutputArtifact(OutputFile)} can have a more specific
+ * signature.
+ */
+ private Artifact internalCreateOutputArtifact(Target target) {
+ Root root = getBinOrGenfilesDirectory();
+ return getAnalysisEnvironment().getDerivedArtifact(Util.getWorkspaceRelativePath(target), root);
+ }
+
+ /**
+ * Returns the root of either the "bin" or "genfiles"
+ * tree, based on this target and the current configuration.
+ * The choice of which tree to use is based on the rule with
+ * which this target (which must be an OutputFile or a Rule) is associated.
+ */
+ public Root getBinOrGenfilesDirectory() {
+ return rule.hasBinaryOutput()
+ ? getConfiguration().getBinDirectory()
+ : getConfiguration().getGenfilesDirectory();
+ }
+
+ /**
+ * Returns the list of transitive info collections that feed into this target through the
+ * specified attribute. Note that you need to specify the correct mode for the attribute,
+ * otherwise an assertion will be raised.
+ */
+ public List<? extends TransitiveInfoCollection> getPrerequisites(String attributeName,
+ Mode mode) {
+ Attribute attributeDefinition = getRule().getAttributeDefinition(attributeName);
+ if ((mode == Mode.TARGET)
+ && (attributeDefinition.getConfigurationTransition() instanceof SplitTransition)) {
+ // TODO(bazel-team): If you request a split-configured attribute in the target configuration,
+ // we return only the list of configured targets for the first architecture; this is for
+ // backwards compatibility with existing code in cases where the call to getPrerequisites is
+ // deeply nested and we can't easily inject the behavior we want. However, we should fix all
+ // such call sites.
+ checkAttribute(attributeName, Mode.SPLIT);
+ Map<String, ? extends List<? extends TransitiveInfoCollection>> map =
+ getSplitPrerequisites(attributeName, /*requireSplit=*/false);
+ return map.isEmpty()
+ ? ImmutableList.<TransitiveInfoCollection>of()
+ : map.entrySet().iterator().next().getValue();
+ }
+
+ checkAttribute(attributeName, mode);
+ return targetMap.get(attributeName);
+ }
+
+ /**
+ * Returns the a prerequisites keyed by the CPU of their configurations; this method throws an
+ * exception if the split transition is not active.
+ */
+ public Map<String, ? extends List<? extends TransitiveInfoCollection>>
+ getSplitPrerequisites(String attributeName) {
+ return getSplitPrerequisites(attributeName, /*requireSplit*/true);
+ }
+
+ private Map<String, ? extends List<? extends TransitiveInfoCollection>>
+ getSplitPrerequisites(String attributeName, boolean requireSplit) {
+ checkAttribute(attributeName, Mode.SPLIT);
+
+ Attribute attributeDefinition = getRule().getAttributeDefinition(attributeName);
+ SplitTransition<?> transition =
+ (SplitTransition<?>) attributeDefinition.getConfigurationTransition();
+ List<BuildConfiguration> configurations =
+ getConfiguration().getTransitions().getSplitConfigurations(transition);
+ if (configurations.size() == 1) {
+ // There are two cases here:
+ // 1. Splitting is enabled, but only one target cpu.
+ // 2. Splitting is disabled, and no --cpu value was provided on the command line.
+ // In the first case, the cpu value is non-null, but in the second case it is null. We only
+ // allow that to proceed if the caller specified that he is going to ignore the cpu value
+ // anyway.
+ String cpu = configurations.get(0).getCpu();
+ if (cpu == null) {
+ Preconditions.checkState(!requireSplit);
+ cpu = "DO_NOT_USE";
+ }
+ return ImmutableMap.of(cpu, targetMap.get(attributeName));
+ }
+
+ Set<String> cpus = new HashSet<>();
+ for (BuildConfiguration config : configurations) {
+ // This method should only be called when the split config is enabled on the command line, in
+ // which case this cpu can't be null.
+ Preconditions.checkNotNull(config.getCpu());
+ cpus.add(config.getCpu());
+ }
+
+ // Use an ImmutableListMultimap.Builder here to preserve ordering.
+ ImmutableListMultimap.Builder<String, TransitiveInfoCollection> result =
+ ImmutableListMultimap.builder();
+ for (TransitiveInfoCollection t : targetMap.get(attributeName)) {
+ if (t.getConfiguration() != null) {
+ result.put(t.getConfiguration().getCpu(), t);
+ } else {
+ // Source files don't have a configuration, so we add them to all architecture entries.
+ for (String cpu : cpus) {
+ result.put(cpu, t);
+ }
+ }
+ }
+ return Multimaps.asMap(result.build());
+ }
+
+ /**
+ * Returns the specified provider of the prerequisite referenced by the attribute in the
+ * argument. Note that you need to specify the correct mode for the attribute, otherwise an
+ * assertion will be raised. If the attribute is empty of it does not support the specified
+ * provider, returns null.
+ */
+ public <C extends TransitiveInfoProvider> C getPrerequisite(
+ String attributeName, Mode mode, Class<C> provider) {
+ TransitiveInfoCollection prerequisite = getPrerequisite(attributeName, mode);
+ return prerequisite == null ? null : prerequisite.getProvider(provider);
+ }
+
+ /**
+ * Returns the transitive info collection that feeds into this target through the specified
+ * attribute. Note that you need to specify the correct mode for the attribute, otherwise an
+ * assertion will be raised. Returns null if the attribute is empty.
+ */
+ public TransitiveInfoCollection getPrerequisite(String attributeName, Mode mode) {
+ checkAttribute(attributeName, mode);
+ List<? extends TransitiveInfoCollection> elements = targetMap.get(attributeName);
+ if (elements.size() > 1) {
+ throw new IllegalStateException(rule.getRuleClass() + " attribute " + attributeName
+ + " produces more then one prerequisites");
+ }
+ return elements.isEmpty() ? null : elements.get(0);
+ }
+
+ /**
+ * Returns all the providers of the specified type that are listed under the specified attribute
+ * of this target in the BUILD file.
+ */
+ public <C extends TransitiveInfoProvider> Iterable<C> getPrerequisites(String attributeName,
+ Mode mode, final Class<C> classType) {
+ AnalysisUtils.checkProvider(classType);
+ return AnalysisUtils.getProviders(getPrerequisites(attributeName, mode), classType);
+ }
+
+ /**
+ * Returns all the providers of the specified type that are listed under the specified attribute
+ * of this target in the BUILD file, and that contain the specified provider.
+ */
+ public <C extends TransitiveInfoProvider> Iterable<? extends TransitiveInfoCollection>
+ getPrerequisitesIf(String attributeName, Mode mode, final Class<C> classType) {
+ AnalysisUtils.checkProvider(classType);
+ return AnalysisUtils.filterByProvider(getPrerequisites(attributeName, mode), classType);
+ }
+
+ /**
+ * Returns the prerequisite referred to by the specified attribute. Also checks whether
+ * the attribute is marked as executable and that the target referred to can actually be
+ * executed.
+ *
+ * <p>The {@code mode} argument must match the configuration transition specified in the
+ * definition of the attribute.
+ *
+ * @param attributeName the name of the attribute
+ * @param mode the configuration transition of the attribute
+ *
+ * @return the {@link FilesToRunProvider} interface of the prerequisite.
+ */
+ public FilesToRunProvider getExecutablePrerequisite(String attributeName, Mode mode) {
+ Attribute ruleDefinition = getRule().getAttributeDefinition(attributeName);
+
+ if (ruleDefinition == null) {
+ throw new IllegalStateException(getRule().getRuleClass() + " attribute " + attributeName
+ + " is not defined");
+ }
+ if (!ruleDefinition.isExecutable()) {
+ throw new IllegalStateException(getRule().getRuleClass() + " attribute " + attributeName
+ + " is not configured to be executable");
+ }
+
+ TransitiveInfoCollection prerequisite = getPrerequisite(attributeName, mode);
+ if (prerequisite == null) {
+ return null;
+ }
+
+ FilesToRunProvider result = prerequisite.getProvider(FilesToRunProvider.class);
+ if (result == null || result.getExecutable() == null) {
+ attributeError(
+ attributeName, prerequisite.getLabel() + " does not refer to a valid executable target");
+ }
+ return result;
+ }
+
+ /**
+ * Gets an attribute of type STRING_LIST expanding Make variables and
+ * tokenizes the result.
+ *
+ * @param attributeName the name of the attribute to process
+ * @return a list of strings containing the expanded and tokenized values for the
+ * attribute
+ */
+ public List<String> getTokenizedStringListAttr(String attributeName) {
+ if (!getRule().isAttrDefined(attributeName, Type.STRING_LIST)) {
+ // TODO(bazel-team): This should be an error.
+ return ImmutableList.of();
+ }
+ List<String> original = attributes().get(attributeName, Type.STRING_LIST);
+ if (original.isEmpty()) {
+ return ImmutableList.of();
+ }
+ List<String> tokens = new ArrayList<>();
+ for (String token : original) {
+ tokenizeAndExpandMakeVars(tokens, attributeName, token);
+ }
+ return ImmutableList.copyOf(tokens);
+ }
+
+ /**
+ * Expands make variables in value and tokenizes the result into tokens.
+ *
+ * <p>This methods should be called only during initialization.
+ */
+ public void tokenizeAndExpandMakeVars(List<String> tokens, String attributeName,
+ String value) {
+ try {
+ ShellUtils.tokenize(tokens, expandMakeVariables(attributeName, value));
+ } catch (ShellUtils.TokenizationException e) {
+ attributeError(attributeName, e.getMessage());
+ }
+ }
+
+ /**
+ * Return a context that maps Make variable names (string) to values (string).
+ *
+ * @return a ConfigurationMakeVariableContext.
+ **/
+ public ConfigurationMakeVariableContext getConfigurationMakeVariableContext() {
+ if (configurationMakeVariableContext == null) {
+ configurationMakeVariableContext = new ConfigurationMakeVariableContext(
+ getRule().getPackage(), getConfiguration());
+ }
+ return configurationMakeVariableContext;
+ }
+
+ /**
+ * Returns the string "expression" after expanding all embedded references to
+ * "Make" variables. If any errors are encountered, they are reported, and
+ * "expression" is returned unchanged.
+ *
+ * @param attributeName the name of the attribute from which "expression" comes;
+ * used for error reporting.
+ * @param expression the string to expand.
+ * @return the expansion of "expression".
+ */
+ public String expandMakeVariables(String attributeName, String expression) {
+ return expandMakeVariables(attributeName, expression, getConfigurationMakeVariableContext());
+ }
+
+ /**
+ * Returns the string "expression" after expanding all embedded references to
+ * "Make" variables. If any errors are encountered, they are reported, and
+ * "expression" is returned unchanged.
+ *
+ * @param attributeName the name of the attribute from which "expression" comes;
+ * used for error reporting.
+ * @param expression the string to expand.
+ * @param context the ConfigurationMakeVariableContext which can have a customized
+ * lookupMakeVariable(String) method.
+ * @return the expansion of "expression".
+ */
+ public String expandMakeVariables(String attributeName, String expression,
+ ConfigurationMakeVariableContext context) {
+ try {
+ return MakeVariableExpander.expand(expression, context);
+ } catch (MakeVariableExpander.ExpansionException e) {
+ attributeError(attributeName, e.getMessage());
+ return expression;
+ }
+ }
+
+ /**
+ * Gets the value of the STRING_LIST attribute expanding all make variables.
+ */
+ public List<String> expandedMakeVariablesList(String attrName) {
+ List<String> variables = new ArrayList<>();
+ for (String variable : attributes().get(attrName, Type.STRING_LIST)) {
+ variables.add(expandMakeVariables(attrName, variable));
+ }
+ return variables;
+ }
+
+ /**
+ * If the string consists of a single variable, returns the expansion of
+ * that variable. Otherwise, returns null. Syntax errors are reported.
+ *
+ * @param attrName the name of the attribute from which "expression" comes;
+ * used for error reporting.
+ * @param expression the string to expand.
+ * @return the expansion of "expression", or null.
+ */
+ public String expandSingleMakeVariable(String attrName, String expression) {
+ try {
+ return MakeVariableExpander.expandSingleVariable(expression,
+ new ConfigurationMakeVariableContext(getRule().getPackage(), getConfiguration()));
+ } catch (MakeVariableExpander.ExpansionException e) {
+ attributeError(attrName, e.getMessage());
+ return expression;
+ }
+ }
+
+ private void checkAttribute(String attributeName, Mode mode) {
+ Attribute attributeDefinition = getRule().getAttributeDefinition(attributeName);
+ if (attributeDefinition == null) {
+ throw new IllegalStateException(getRule().getLocation() + ": " + getRule().getRuleClass()
+ + " attribute " + attributeName + " is not defined");
+ }
+ if (!(attributeDefinition.getType() == Type.LABEL
+ || attributeDefinition.getType() == Type.LABEL_LIST)) {
+ throw new IllegalStateException(rule.getRuleClass() + " attribute " + attributeName
+ + " is not a label type attribute");
+ }
+ if (mode == Mode.HOST) {
+ if (attributeDefinition.getConfigurationTransition() != ConfigurationTransition.HOST) {
+ throw new IllegalStateException(getRule().getLocation() + ": "
+ + getRule().getRuleClass() + " attribute " + attributeName
+ + " is not configured for the host configuration");
+ }
+ } else if (mode == Mode.TARGET) {
+ if (attributeDefinition.getConfigurationTransition() != ConfigurationTransition.NONE) {
+ throw new IllegalStateException(getRule().getLocation() + ": "
+ + getRule().getRuleClass() + " attribute " + attributeName
+ + " is not configured for the target configuration");
+ }
+ } else if (mode == Mode.DATA) {
+ if (attributeDefinition.getConfigurationTransition() != ConfigurationTransition.DATA) {
+ throw new IllegalStateException(getRule().getLocation() + ": "
+ + getRule().getRuleClass() + " attribute " + attributeName
+ + " is not configured for the data configuration");
+ }
+ } else if (mode == Mode.SPLIT) {
+ if (!(attributeDefinition.getConfigurationTransition() instanceof SplitTransition)) {
+ throw new IllegalStateException(getRule().getLocation() + ": "
+ + getRule().getRuleClass() + " attribute " + attributeName
+ + " is not configured for a split transition");
+ }
+ }
+ }
+
+ /**
+ * Returns the Mode for which the attribute is configured.
+ * This is intended for Skylark, where the Mode is implicitly chosen.
+ */
+ public Mode getAttributeMode(String attributeName) {
+ Attribute attributeDefinition = getRule().getAttributeDefinition(attributeName);
+ if (attributeDefinition == null) {
+ throw new IllegalStateException(getRule().getLocation() + ": " + getRule().getRuleClass()
+ + " attribute " + attributeName + " is not defined");
+ }
+ if (!(attributeDefinition.getType() == Type.LABEL
+ || attributeDefinition.getType() == Type.LABEL_LIST)) {
+ throw new IllegalStateException(rule.getRuleClass() + " attribute " + attributeName
+ + " is not a label type attribute");
+ }
+ if (attributeDefinition.getConfigurationTransition() == ConfigurationTransition.HOST) {
+ return Mode.HOST;
+ } else if (attributeDefinition.getConfigurationTransition() == ConfigurationTransition.NONE) {
+ return Mode.TARGET;
+ } else if (attributeDefinition.getConfigurationTransition() == ConfigurationTransition.DATA) {
+ return Mode.DATA;
+ } else if (attributeDefinition.getConfigurationTransition() instanceof SplitTransition) {
+ return Mode.SPLIT;
+ }
+ throw new IllegalStateException(getRule().getLocation() + ": "
+ + getRule().getRuleClass() + " attribute " + attributeName + " is not configured");
+ }
+
+ /**
+ * For the specified attribute "attributeName" (which must be of type
+ * list(label)), resolve all the labels into ConfiguredTargets (for the
+ * configuration appropriate to the attribute) and return their build
+ * artifacts as a {@link PrerequisiteArtifacts} instance.
+ *
+ * @param attributeName the name of the attribute to traverse
+ */
+ public PrerequisiteArtifacts getPrerequisiteArtifacts(String attributeName, Mode mode) {
+ return PrerequisiteArtifacts.get(this, attributeName, mode);
+ }
+
+ /**
+ * For the specified attribute "attributeName" (which must be of type label),
+ * resolves the ConfiguredTarget and returns its single build artifact.
+ *
+ * <p>If the attribute is optional, has no default and was not specified, then
+ * null will be returned. Note also that null is returned (and an attribute
+ * error is raised) if there wasn't exactly one build artifact for the target.
+ */
+ public Artifact getPrerequisiteArtifact(String attributeName, Mode mode) {
+ TransitiveInfoCollection target = getPrerequisite(attributeName, mode);
+ return transitiveInfoCollectionToArtifact(attributeName, target);
+ }
+
+ /**
+ * Equivalent to getPrerequisiteArtifact(), but also asserts that
+ * host-configuration is appropriate for the specified attribute.
+ */
+ public Artifact getHostPrerequisiteArtifact(String attributeName) {
+ TransitiveInfoCollection target = getPrerequisite(attributeName, Mode.HOST);
+ return transitiveInfoCollectionToArtifact(attributeName, target);
+ }
+
+ private Artifact transitiveInfoCollectionToArtifact(
+ String attributeName, TransitiveInfoCollection target) {
+ if (target != null) {
+ Iterable<Artifact> artifacts = target.getProvider(FileProvider.class).getFilesToBuild();
+ if (Iterables.size(artifacts) == 1) {
+ return Iterables.getOnlyElement(artifacts);
+ } else {
+ attributeError(attributeName, target.getLabel() + " expected a single artifact");
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the sole file in the "srcs" attribute. Reports an error and
+ * (possibly) returns null if "srcs" does not identify a single file of the
+ * expected type.
+ */
+ public Artifact getSingleSource(String fileTypeName) {
+ List<Artifact> srcs = PrerequisiteArtifacts.get(this, "srcs", Mode.TARGET).list();
+ switch (srcs.size()) {
+ case 0 : // error already issued by getSrc()
+ return null;
+ case 1 : // ok
+ return Iterables.getOnlyElement(srcs);
+ default :
+ attributeError("srcs", "only a single " + fileTypeName + " is allowed here");
+ return srcs.get(0);
+ }
+ }
+
+ public Artifact getSingleSource() {
+ return getSingleSource(getRule().getRuleClass() + " source file");
+ }
+
+ /**
+ * Returns a path fragment qualified by the rule name and unique fragment to
+ * disambiguate artifacts produced from the source file appearing in
+ * multiple rules.
+ *
+ * <p>For example "pkg/dir/name" -> "pkg/<fragment>/rule/dir/name.
+ */
+ public final PathFragment getUniqueDirectory(String fragment) {
+ return AnalysisUtils.getUniqueDirectory(getLabel(), new PathFragment(fragment));
+ }
+
+ /**
+ * Check that all targets that were specified as sources are from the same
+ * package as this rule. Output a warning or an error for every target that is
+ * imported from a different package.
+ */
+ public void checkSrcsSamePackage(boolean onlyWarn) {
+ PathFragment packageName = getLabel().getPackageFragment();
+ for (Artifact srcItem : PrerequisiteArtifacts.get(this, "srcs", Mode.TARGET).list()) {
+ if (!srcItem.isSourceArtifact()) {
+ // In theory, we should not do this check. However, in practice, we
+ // have a couple of rules that do not obey the "srcs must contain
+ // files and only files" rule. Thus, we are stuck with this hack here :(
+ continue;
+ }
+ Label associatedLabel = srcItem.getOwner();
+ PathFragment itemPackageName = associatedLabel.getPackageFragment();
+ if (!itemPackageName.equals(packageName)) {
+ String message = "please do not import '" + associatedLabel + "' directly. "
+ + "You should either move the file to this package or depend on "
+ + "an appropriate rule there";
+ if (onlyWarn) {
+ attributeWarning("srcs", message);
+ } else {
+ attributeError("srcs", message);
+ }
+ }
+ }
+ }
+
+
+ /**
+ * Returns the label to which the {@code NODEP_LABEL} attribute
+ * {@code attrName} refers, checking that it is a valid label, and that it is
+ * referring to a local target. Reports a warning otherwise.
+ */
+ public Label getLocalNodepLabelAttribute(String attrName) {
+ Label label = attributes().get(attrName, Type.NODEP_LABEL);
+ if (label == null) {
+ return null;
+ }
+
+ if (!getTarget().getLabel().getPackageFragment().equals(label.getPackageFragment())) {
+ attributeWarning(attrName, "does not reference a local rule");
+ }
+
+ return label;
+ }
+
+ /**
+ * Returns the implicit output artifact for a given template function. If multiple or no artifacts
+ * can be found as a result of the template, an exception is thrown.
+ */
+ public Artifact getImplicitOutputArtifact(ImplicitOutputsFunction function) {
+ Iterable<String> result;
+ try {
+ result = function.getImplicitOutputs(RawAttributeMapper.of(rule));
+ } catch (EvalException e) {
+ // It's ok as long as we don't use this method from Skylark.
+ throw new IllegalStateException(e);
+ }
+ return getImplicitOutputArtifact(Iterables.getOnlyElement(result));
+ }
+
+ /**
+ * Only use from Skylark. Returns the implicit output artifact for a given output path.
+ */
+ public Artifact getImplicitOutputArtifact(String path) {
+ Root root = getBinOrGenfilesDirectory();
+ PathFragment packageFragment = getLabel().getPackageFragment();
+ return getAnalysisEnvironment().getDerivedArtifact(packageFragment.getRelative(path), root);
+ }
+
+ /**
+ * Convenience method to return a host configured target for the "compiler"
+ * attribute. Allows caller to decide whether a warning should be printed if
+ * the "compiler" attribute is not set to the default value.
+ *
+ * @param warnIfNotDefault if true, print a warning if the value for the
+ * "compiler" attribute is set to something other than the default
+ * @return a ConfiguredTarget using the host configuration for the "compiler"
+ * attribute
+ */
+ public final FilesToRunProvider getCompiler(boolean warnIfNotDefault) {
+ Label label = attributes().get("compiler", Type.LABEL);
+ if (warnIfNotDefault && !label.equals(getRule().getAttrDefaultValue("compiler"))) {
+ attributeWarning("compiler", "setting the compiler is strongly discouraged");
+ }
+ return getExecutablePrerequisite("compiler", Mode.HOST);
+ }
+
+ /**
+ * Returns the (unmodifiable, ordered) list of artifacts which are the outputs
+ * of this target.
+ *
+ * <p>Each element in this list is associated with a single output, either
+ * declared implicitly (via setImplicitOutputsFunction()) or explicitly
+ * (listed in the 'outs' attribute of our rule).
+ */
+ public final ImmutableList<Artifact> getOutputArtifacts() {
+ ImmutableList.Builder<Artifact> artifacts = ImmutableList.builder();
+ for (OutputFile out : getRule().getOutputFiles()) {
+ artifacts.add(createOutputArtifact(out));
+ }
+ return artifacts.build();
+ }
+
+ /**
+ * Like getFilesToBuild(), except that it also includes the runfiles middleman, if any.
+ * Middlemen are expanded in the SpawnStrategy or by the Distributor.
+ */
+ public static ImmutableList<Artifact> getFilesToRun(
+ RunfilesSupport runfilesSupport, NestedSet<Artifact> filesToBuild) {
+ if (runfilesSupport == null) {
+ return ImmutableList.copyOf(filesToBuild);
+ } else {
+ ImmutableList.Builder<Artifact> allFilesToBuild = ImmutableList.builder();
+ allFilesToBuild.addAll(filesToBuild);
+ allFilesToBuild.add(runfilesSupport.getRunfilesMiddleman());
+ return allFilesToBuild.build();
+ }
+ }
+
+ /**
+ * Like {@link #getOutputArtifacts()} but for a singular output item.
+ * Reports an error if the "out" attribute is not a singleton.
+ *
+ * @return null if the output list is empty, the artifact for the first item
+ * of the output list otherwise
+ */
+ public Artifact getOutputArtifact() {
+ List<Artifact> outs = getOutputArtifacts();
+ if (outs.size() != 1) {
+ attributeError("out", "exactly one output file required");
+ if (outs.isEmpty()) {
+ return null;
+ }
+ }
+ return outs.get(0);
+ }
+
+ /**
+ * Returns an artifact with a given file extension. All other path components
+ * are the same as in {@code pathFragment}.
+ */
+ public final Artifact getRelatedArtifact(PathFragment pathFragment, String extension) {
+ PathFragment file = FileSystemUtils.replaceExtension(pathFragment, extension);
+ return getAnalysisEnvironment().getDerivedArtifact(file, getConfiguration().getBinDirectory());
+ }
+
+ /**
+ * Returns true if runfiles support should create the runfiles tree, or
+ * false if it should just create the manifest.
+ */
+ public boolean shouldCreateRunfilesSymlinks() {
+ // TODO(bazel-team): Ideally we wouldn't need such logic, and we'd
+ // always use the BuildConfiguration#buildRunfiles() to determine
+ // whether to build the runfiles. The problem is that certain build
+ // steps actually consume their runfiles. These include:
+ // a. par files consumes the runfiles directory
+ // We should modify autopar to take a list of files instead.
+ // of the runfiles directory.
+ // b. host tools could potentially use data files, but currently don't
+ // (they're run from the execution root, not a runfiles tree).
+ // Currently hostConfiguration.buildRunfiles() returns true.
+ if (TargetUtils.isTestRule(getTarget())) {
+ // Tests are only executed during testing (duh),
+ // and their runfiles are generated lazily on local
+ // execution (see LocalTestStrategy). Therefore, it
+ // is safe not to build their runfiles.
+ return getConfiguration().buildRunfiles();
+ } else {
+ return true;
+ }
+ }
+
+ /**
+ * @return true if {@code rule} is visible from {@code prerequisite}.
+ *
+ * <p>This only computes the logic as implemented by the visibility system. The final decision
+ * whether a dependency is allowed is made by
+ * {@link ConfiguredRuleClassProvider.PrerequisiteValidator}.
+ */
+ public static boolean isVisible(Rule rule, TransitiveInfoCollection prerequisite) {
+ // Check visibility attribute
+ for (PackageSpecification specification :
+ prerequisite.getProvider(VisibilityProvider.class).getVisibility()) {
+ if (specification.containsPackage(rule.getLabel().getPackageFragment())) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * @return the set of features applicable for the current rule's package.
+ */
+ public ImmutableSet<String> getFeatures() {
+ return features;
+ }
+
+ /**
+ * Builder class for a RuleContext.
+ */
+ public static final class Builder {
+ private final AnalysisEnvironment env;
+ private final Rule rule;
+ private final BuildConfiguration configuration;
+ private final PrerequisiteValidator prerequisiteValidator;
+ private ListMultimap<Attribute, ConfiguredTarget> prerequisiteMap;
+ private Set<ConfigMatchingProvider> configConditions;
+ private NestedSet<PackageSpecification> visibility;
+
+ Builder(AnalysisEnvironment env, Rule rule, BuildConfiguration configuration,
+ PrerequisiteValidator prerequisiteValidator) {
+ this.env = Preconditions.checkNotNull(env);
+ this.rule = Preconditions.checkNotNull(rule);
+ this.configuration = Preconditions.checkNotNull(configuration);
+ this.prerequisiteValidator = prerequisiteValidator;
+ }
+
+ RuleContext build() {
+ Preconditions.checkNotNull(prerequisiteMap);
+ Preconditions.checkNotNull(configConditions);
+ Preconditions.checkNotNull(visibility);
+ ListMultimap<String, ConfiguredTarget> targetMap = createTargetMap();
+ ListMultimap<String, ConfiguredFilesetEntry> filesetEntryMap =
+ createFilesetEntryMap(rule, configConditions);
+ return new RuleContext(this, targetMap, filesetEntryMap, configConditions,
+ getEnabledFeatures());
+ }
+
+ private ImmutableSet<String> getEnabledFeatures() {
+ Set<String> enabled = new HashSet<>();
+ Set<String> disabled = new HashSet<>();
+ for (String feature : Iterables.concat(getConfiguration().getDefaultFeatures(),
+ getRule().getPackage().getFeatures())) {
+ if (feature.startsWith("-")) {
+ disabled.add(feature.substring(1));
+ } else if (feature.equals("no_layering_check")) {
+ // TODO(bazel-team): Remove once we do not have BUILD files left that contain
+ // 'no_layering_check'.
+ disabled.add(feature.substring(3));
+ } else {
+ enabled.add(feature);
+ }
+ }
+ return Sets.difference(enabled, disabled).immutableCopy();
+ }
+
+ Builder setVisibility(NestedSet<PackageSpecification> visibility) {
+ this.visibility = visibility;
+ return this;
+ }
+
+ /**
+ * Sets the prerequisites and checks their visibility. It also generates appropriate error or
+ * warning messages and sets the error flag as appropriate.
+ */
+ Builder setPrerequisites(ListMultimap<Attribute, ConfiguredTarget> prerequisiteMap) {
+ this.prerequisiteMap = Preconditions.checkNotNull(prerequisiteMap);
+ return this;
+ }
+
+ /**
+ * Sets the configuration conditions needed to determine which paths to follow for this
+ * rule's configurable attributes.
+ */
+ Builder setConfigConditions(Set<ConfigMatchingProvider> configConditions) {
+ this.configConditions = Preconditions.checkNotNull(configConditions);
+ return this;
+ }
+
+ private boolean validateFilesetEntry(FilesetEntry filesetEntry, ConfiguredTarget src) {
+ if (src.getProvider(FilesetProvider.class) != null) {
+ return true;
+ }
+ if (filesetEntry.isSourceFileset()) {
+ return true;
+ }
+
+ Target srcTarget = src.getTarget();
+ if (!(srcTarget instanceof FileTarget)) {
+ attributeError("entries", String.format(
+ "Invalid 'srcdir' target '%s'. Must be another Fileset or package",
+ srcTarget.getLabel()));
+ return false;
+ }
+
+ if (srcTarget instanceof OutputFile) {
+ attributeWarning("entries", String.format("'srcdir' target '%s' is not an input file. "
+ + "This forces the Fileset to be executed unconditionally",
+ srcTarget.getLabel()));
+ }
+
+ return true;
+ }
+
+ /**
+ * Determines and returns a map from attribute name to list of configured fileset entries, based
+ * on a PrerequisiteMap instance.
+ */
+ private ListMultimap<String, ConfiguredFilesetEntry> createFilesetEntryMap(
+ final Rule rule, Set<ConfigMatchingProvider> configConditions) {
+ final ImmutableSortedKeyListMultimap.Builder<String, ConfiguredFilesetEntry> mapBuilder =
+ ImmutableSortedKeyListMultimap.builder();
+ for (Attribute attr : rule.getAttributes()) {
+ if (attr.getType() != Type.FILESET_ENTRY_LIST) {
+ continue;
+ }
+ String attributeName = attr.getName();
+ Map<Label, ConfiguredTarget> ctMap = new HashMap<>();
+ for (ConfiguredTarget prerequisite : prerequisiteMap.get(attr)) {
+ ctMap.put(prerequisite.getLabel(), prerequisite);
+ }
+ List<FilesetEntry> entries = ConfiguredAttributeMapper.of(rule, configConditions)
+ .get(attributeName, Type.FILESET_ENTRY_LIST);
+ for (FilesetEntry entry : entries) {
+ if (entry.getFiles() == null) {
+ Label label = entry.getSrcLabel();
+ ConfiguredTarget src = ctMap.get(label);
+ if (!validateFilesetEntry(entry, src)) {
+ continue;
+ }
+
+ mapBuilder.put(attributeName, new ConfiguredFilesetEntry(entry, src));
+ } else {
+ ImmutableList.Builder<TransitiveInfoCollection> files = ImmutableList.builder();
+ for (Label file : entry.getFiles()) {
+ files.add(ctMap.get(file));
+ }
+ mapBuilder.put(attributeName, new ConfiguredFilesetEntry(entry, files.build()));
+ }
+ }
+ }
+ return mapBuilder.build();
+ }
+
+ /**
+ * Determines and returns a map from attribute name to list of configured targets.
+ */
+ private ImmutableSortedKeyListMultimap<String, ConfiguredTarget> createTargetMap() {
+ ImmutableSortedKeyListMultimap.Builder<String, ConfiguredTarget> mapBuilder =
+ ImmutableSortedKeyListMultimap.builder();
+
+ for (Map.Entry<Attribute, Collection<ConfiguredTarget>> entry :
+ prerequisiteMap.asMap().entrySet()) {
+ Attribute attribute = entry.getKey();
+ if (attribute == null) {
+ continue;
+ }
+ if (attribute.isSilentRuleClassFilter()) {
+ Predicate<RuleClass> filter = attribute.getAllowedRuleClassesPredicate();
+ for (ConfiguredTarget configuredTarget : entry.getValue()) {
+ Target prerequisiteTarget = configuredTarget.getTarget();
+ if ((prerequisiteTarget instanceof Rule)
+ && filter.apply(((Rule) prerequisiteTarget).getRuleClassObject())) {
+ validateDirectPrerequisite(attribute, configuredTarget);
+ mapBuilder.put(attribute.getName(), configuredTarget);
+ }
+ }
+ } else {
+ for (ConfiguredTarget configuredTarget : entry.getValue()) {
+ validateDirectPrerequisite(attribute, configuredTarget);
+ mapBuilder.put(attribute.getName(), configuredTarget);
+ }
+ }
+ }
+
+ // Handle abi_deps+deps error.
+ Attribute abiDepsAttr = rule.getAttributeDefinition("abi_deps");
+ if ((abiDepsAttr != null) && rule.isAttributeValueExplicitlySpecified("abi_deps")
+ && rule.isAttributeValueExplicitlySpecified("deps")) {
+ attributeError("deps", "Only one of deps and abi_deps should be provided");
+ }
+ return mapBuilder.build();
+ }
+
+ private String prefixRuleMessage(String message) {
+ return String.format("in %s rule %s: %s", rule.getRuleClass(), rule.getLabel(), message);
+ }
+
+ private String maskInternalAttributeNames(String name) {
+ return Attribute.isImplicit(name) ? "(an implicit dependency)" : name;
+ }
+
+ private String prefixAttributeMessage(String attrName, String message) {
+ return String.format("in %s attribute of %s rule %s: %s",
+ maskInternalAttributeNames(attrName), rule.getRuleClass(), rule.getLabel(), message);
+ }
+
+ public void reportError(Location location, String message) {
+ env.getEventHandler().handle(Event.error(location, message));
+ }
+
+ public void ruleError(String message) {
+ reportError(rule.getLocation(), prefixRuleMessage(message));
+ }
+
+ public void attributeError(String attrName, String message) {
+ reportError(rule.getAttributeLocation(attrName), prefixAttributeMessage(attrName, message));
+ }
+
+ public void reportWarning(Location location, String message) {
+ env.getEventHandler().handle(Event.warn(location, message));
+ }
+
+ public void ruleWarning(String message) {
+ env.getEventHandler().handle(Event.warn(rule.getLocation(), prefixRuleMessage(message)));
+ }
+
+ public void attributeWarning(String attrName, String message) {
+ reportWarning(rule.getAttributeLocation(attrName), prefixAttributeMessage(attrName, message));
+ }
+
+ private void reportBadPrerequisite(Attribute attribute, String targetKind,
+ Label prerequisiteLabel, String reason, boolean isWarning) {
+ String msgPrefix = targetKind != null ? targetKind + " " : "";
+ String msgReason = reason != null ? " (" + reason + ")" : "";
+ if (isWarning) {
+ attributeWarning(attribute.getName(), String.format(
+ "%s'%s' is unexpected here%s; continuing anyway",
+ msgPrefix, prerequisiteLabel, msgReason));
+ } else {
+ attributeError(attribute.getName(), String.format(
+ "%s'%s' is misplaced here%s", msgPrefix, prerequisiteLabel, msgReason));
+ }
+ }
+
+ private void validateDirectPrerequisiteType(ConfiguredTarget prerequisite,
+ Attribute attribute) {
+ Target prerequisiteTarget = prerequisite.getTarget();
+ Label prerequisiteLabel = prerequisiteTarget.getLabel();
+
+ if (prerequisiteTarget instanceof Rule) {
+ Rule prerequisiteRule = (Rule) prerequisiteTarget;
+
+ String reason = attribute.getValidityPredicate().checkValid(rule, prerequisiteRule);
+ if (reason != null) {
+ reportBadPrerequisite(attribute, prerequisiteTarget.getTargetKind(),
+ prerequisiteLabel, reason, false);
+ }
+ }
+
+ if (attribute.isStrictLabelCheckingEnabled()) {
+ if (prerequisiteTarget instanceof Rule) {
+ RuleClass ruleClass = ((Rule) prerequisiteTarget).getRuleClassObject();
+ if (!attribute.getAllowedRuleClassesPredicate().apply(ruleClass)) {
+ boolean allowedWithWarning = attribute.getAllowedRuleClassesWarningPredicate()
+ .apply(ruleClass);
+ reportBadPrerequisite(attribute, prerequisiteTarget.getTargetKind(), prerequisiteLabel,
+ "expected " + attribute.getAllowedRuleClassesPredicate().toString(),
+ allowedWithWarning);
+ }
+ } else if (prerequisiteTarget instanceof FileTarget) {
+ if (!attribute.getAllowedFileTypesPredicate()
+ .apply(((FileTarget) prerequisiteTarget).getFilename())) {
+ if (prerequisiteTarget instanceof InputFile
+ && !((InputFile) prerequisiteTarget).getPath().exists()) {
+ // Misplaced labels, no corresponding target exists
+ if (attribute.getAllowedFileTypesPredicate().isNone()
+ && !((InputFile) prerequisiteTarget).getFilename().contains(".")) {
+ // There are no allowed files in the attribute but it's not a valid rule,
+ // and the filename doesn't contain a dot --> probably a misspelled rule
+ attributeError(attribute.getName(),
+ "rule '" + prerequisiteLabel + "' does not exist");
+ } else {
+ attributeError(attribute.getName(),
+ "target '" + prerequisiteLabel + "' does not exist");
+ }
+ } else {
+ // The file exists but has a bad extension
+ reportBadPrerequisite(attribute, "file", prerequisiteLabel,
+ "expected " + attribute.getAllowedFileTypesPredicate().toString(), false);
+ }
+ }
+ }
+ }
+ }
+
+ public Rule getRule() {
+ return rule;
+ }
+
+ public BuildConfiguration getConfiguration() {
+ return configuration;
+ }
+
+ /**
+ * @return true if {@code rule} is visible from {@code prerequisite}.
+ *
+ * <p>This only computes the logic as implemented by the visibility system. The final decision
+ * whether a dependency is allowed is made by
+ * {@link ConfiguredRuleClassProvider.PrerequisiteValidator}, who is supposed to call this
+ * method to determine whether a dependency is allowed as per visibility rules.
+ */
+ public boolean isVisible(TransitiveInfoCollection prerequisite) {
+ return RuleContext.isVisible(rule, prerequisite);
+ }
+
+ private void validateDirectPrerequisiteFileTypes(ConfiguredTarget prerequisite,
+ Attribute attribute) {
+ if (attribute.isSkipAnalysisTimeFileTypeCheck()) {
+ return;
+ }
+ FileTypeSet allowedFileTypes = attribute.getAllowedFileTypesPredicate();
+ if (allowedFileTypes == FileTypeSet.ANY_FILE && !attribute.isNonEmpty()
+ && !attribute.isSingleArtifact()) {
+ return;
+ }
+
+ // If we allow any file we still need to check if there are actually files generated
+ // Note that this check only runs for ANY_FILE predicates if the attribute is NON_EMPTY
+ // or SINGLE_ARTIFACT
+ // If we performed this check when allowedFileTypes == NO_FILE this would
+ // always throw an error in those cases
+ if (allowedFileTypes != FileTypeSet.NO_FILE) {
+ Iterable<Artifact> artifacts = prerequisite.getProvider(FileProvider.class)
+ .getFilesToBuild();
+ if (attribute.isSingleArtifact() && Iterables.size(artifacts) != 1) {
+ attributeError(attribute.getName(),
+ "'" + prerequisite.getLabel() + "' must produce a single file");
+ return;
+ }
+ for (Artifact sourceArtifact : artifacts) {
+ if (allowedFileTypes.apply(sourceArtifact.getFilename())) {
+ return;
+ }
+ }
+ attributeError(attribute.getName(), "'" + prerequisite.getLabel()
+ + "' does not produce any " + rule.getRuleClass() + " " + attribute.getName()
+ + " files (expected " + allowedFileTypes + ")");
+ }
+ }
+
+ private void validateMandatoryProviders(ConfiguredTarget prerequisite, Attribute attribute) {
+ for (String provider : attribute.getMandatoryProviders()) {
+ if (prerequisite.get(provider) == null) {
+ attributeError(attribute.getName(), "'" + prerequisite.getLabel()
+ + "' does not have mandatory provider '" + provider + "'");
+ }
+ }
+ }
+
+ private void validateDirectPrerequisite(Attribute attribute, ConfiguredTarget prerequisite) {
+ validateDirectPrerequisiteType(prerequisite, attribute);
+ validateDirectPrerequisiteFileTypes(prerequisite, attribute);
+ validateMandatoryProviders(prerequisite, attribute);
+ prerequisiteValidator.validate(this, prerequisite, attribute);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "RuleContext(" + getLabel() + ", " + getConfiguration() + ")";
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/RuleDefinition.java b/src/main/java/com/google/devtools/build/lib/analysis/RuleDefinition.java
new file mode 100644
index 0000000..c5e32e3
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/RuleDefinition.java
@@ -0,0 +1,39 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.devtools.build.lib.packages.RuleClass;
+
+/**
+ * This class is a common ancestor for every rule object.
+ *
+ * <p>Implementors are also required to have the {@link BlazeRule} annotation
+ * set.
+ */
+public interface RuleDefinition {
+ /**
+ * This method should return a RuleClass object that represents the rule. The usual pattern is
+ * that various setter methods are called on the builder object passed in as the argument, then
+ * the object that is built by the builder is returned.
+ *
+ * @param builder A {@link com.google.devtools.build.lib.packages.RuleClass.Builder} object
+ * already preloaded with the attributes of the ancestors specified in the {@link
+ * BlazeRule} annotation.
+ * @param environment The services Blaze provides to rule definitions.
+ *
+ * @return the {@link RuleClass} representing the rule.
+ */
+ RuleClass build(RuleClass.Builder builder, RuleDefinitionEnvironment environment);
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/RuleDefinitionEnvironment.java b/src/main/java/com/google/devtools/build/lib/analysis/RuleDefinitionEnvironment.java
new file mode 100644
index 0000000..4dcd080
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/RuleDefinitionEnvironment.java
@@ -0,0 +1,31 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.devtools.build.lib.syntax.Label;
+
+/**
+ * Encapsulates the services available for implementors of the {@link RuleDefinition}
+ * interface.
+ */
+public interface RuleDefinitionEnvironment {
+ /**
+ * Parses the given string as a label and returns the label, by calling {@link
+ * Label#parseAbsolute}. Instead of throwing a {@link
+ * com.google.devtools.build.lib.syntax.Label.SyntaxException}, it throws an {@link
+ * IllegalArgumentException}, if the parsing fails.
+ */
+ Label getLabel(String labelValue);
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/Runfiles.java b/src/main/java/com/google/devtools/build/lib/analysis/Runfiles.java
new file mode 100644
index 0000000..a4da4b4
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/Runfiles.java
@@ -0,0 +1,756 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicates;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Maps;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.util.Pair;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * An object that encapsulates runfiles. Conceptually, the runfiles are a map of paths to files,
+ * forming a symlink tree.
+ *
+ * <p>In order to reduce memory consumption, this map is not explicitly stored here, but instead as
+ * a combination of four parts: artifacts placed at their root-relative paths, source tree symlinks,
+ * root symlinks (outside of the source tree), and artifacts included as parts of "pruning
+ * manifests" (see {@link PruningManifest}).
+ */
+@Immutable
+public final class Runfiles {
+ private static final Function<Map.Entry<PathFragment, Artifact>, Artifact> TO_ARTIFACT =
+ new Function<Map.Entry<PathFragment, Artifact>, Artifact>() {
+ @Override
+ public Artifact apply(Map.Entry<PathFragment, Artifact> input) {
+ return input.getValue();
+ }
+ };
+
+ private static final Function<Map<PathFragment, Artifact>, Map<PathFragment, Artifact>>
+ DUMMY_SYMLINK_EXPANDER =
+ new Function<Map<PathFragment, Artifact>, Map<PathFragment, Artifact>>() {
+ @Override
+ public Map<PathFragment, Artifact> apply(Map<PathFragment, Artifact> input) {
+ return ImmutableMap.of();
+ }
+ };
+
+ // It is important to declare this *after* the DUMMY_SYMLINK_EXPANDER to avoid NPEs
+ public static final Runfiles EMPTY = new Builder().build();
+
+
+ /**
+ * The artifacts that should *always* be present in the runfiles directory. These are
+ * differentiated from the artifacts that may or may not be included by a pruning manifest
+ * (see {@link PruningManifest} below).
+ *
+ * <p>This collection may not include any middlemen. These artifacts will be placed at a location
+ * that corresponds to the root-relative path of each artifact. It's possible for several
+ * artifacts to have the same root-relative path, in which case the last one will win.
+ */
+ private final NestedSet<Artifact> unconditionalArtifacts;
+
+ /**
+ * A map of symlinks that should be present in the runfiles directory. In general, the symlink can
+ * be determined from the artifact by using the root-relative path, so this should only be used
+ * for cases where that isn't possible.
+ *
+ * <p>This may include runfiles symlinks from the root of the runfiles tree.
+ */
+ private final NestedSet<Map.Entry<PathFragment, Artifact>> symlinks;
+
+ /**
+ * A map of symlinks that should be present above the runfiles directory. These are useful for
+ * certain rule types like AppEngine apps which have root level config files outside of the
+ * regular source tree.
+ */
+ private final NestedSet<Map.Entry<PathFragment, Artifact>> rootSymlinks;
+
+ /**
+ * A function to generate extra manifest entries.
+ */
+ private final Function<Map<PathFragment, Artifact>, Map<PathFragment, Artifact>>
+ manifestExpander;
+
+ /**
+ * Defines a set of artifacts that may or may not be included in the runfiles directory and
+ * a manifest file that makes that determination. These are applied on top of any artifacts
+ * specified in {@link #unconditionalArtifacts}.
+ *
+ * <p>The incentive behind this is to enable execution-phase "pruning" of runfiles. Anything
+ * set in unconditionalArtifacts is hard-set in Blaze's analysis phase, and thus unchangeable in
+ * response to execution phase results. This isn't always convenient. For example, say we have an
+ * action that consumes a set of "possible" runtime dependencies for a source file, parses that
+ * file for "import a.b.c" statements, and outputs a manifest of the actual dependencies that are
+ * referenced and thus really needed. This can reduce the size of the runfiles set, but we can't
+ * use this information until the manifest output is available.
+ *
+ * <p>Only artifacts present in the candidate set AND the manifest output make it into the
+ * runfiles tree. The candidate set requirement guarantees that analysis-time dependencies are a
+ * superset of the pruned dependencies, so undeclared inclusions (which can break build
+ * correctness) aren't possible.
+ */
+ public static class PruningManifest {
+ private final NestedSet<Artifact> candidateRunfiles;
+ private final Artifact manifestFile;
+
+ /**
+ * Creates a new pruning manifest.
+ *
+ * @param candidateRunfiles set of possible artifacts that the manifest file may reference
+ * @param manifestFile the manifest file, expected to be a newline-separated list of
+ * source tree root-relative paths (i.e. "my/package/myfile.txt"). Anything that can't be
+ * resolved back to an entry in candidateRunfiles is ignored and will *not* make it into
+ * the runfiles tree.
+ */
+ public PruningManifest(NestedSet<Artifact> candidateRunfiles, Artifact manifestFile) {
+ this.candidateRunfiles = candidateRunfiles;
+ this.manifestFile = manifestFile;
+ }
+
+ public NestedSet<Artifact> getCandidateRunfiles() {
+ return candidateRunfiles;
+ }
+
+ public Artifact getManifestFile() {
+ return manifestFile;
+ }
+ }
+
+ /**
+ * The pruning manifests that should be applied to these runfiles.
+ */
+ private final NestedSet<PruningManifest> pruningManifests;
+
+ private Runfiles(NestedSet<Artifact> artifacts,
+ NestedSet<Map.Entry<PathFragment, Artifact>> symlinks,
+ NestedSet<Map.Entry<PathFragment, Artifact>> rootSymlinks,
+ NestedSet<PruningManifest> pruningManifests,
+ Function<Map<PathFragment, Artifact>, Map<PathFragment, Artifact>> expander) {
+ this.unconditionalArtifacts = Preconditions.checkNotNull(artifacts);
+ this.symlinks = Preconditions.checkNotNull(symlinks);
+ this.rootSymlinks = Preconditions.checkNotNull(rootSymlinks);
+ this.pruningManifests = Preconditions.checkNotNull(pruningManifests);
+ this.manifestExpander = Preconditions.checkNotNull(expander);
+ }
+
+ /**
+ * Returns the artifacts that are unconditionally included in the runfiles (as opposed to
+ * pruning manifest candidates, which may or may not be included).
+ */
+ @VisibleForTesting
+ public NestedSet<Artifact> getUnconditionalArtifacts() {
+ return unconditionalArtifacts;
+ }
+
+ /**
+ * Returns the artifacts that are unconditionally included in the runfiles (as opposed to
+ * pruning manifest candidates, which may or may not be included). Middleman artifacts are
+ * excluded.
+ */
+ public Iterable<Artifact> getUnconditionalArtifactsWithoutMiddlemen() {
+ return Iterables.filter(unconditionalArtifacts, Artifact.MIDDLEMAN_FILTER);
+ }
+
+ /**
+ * Returns the collection of runfiles as artifacts, including both unconditional artifacts
+ * and pruning manifest candidates.
+ */
+ @VisibleForTesting
+ public NestedSet<Artifact> getArtifacts() {
+ NestedSetBuilder<Artifact> allArtifacts = NestedSetBuilder.stableOrder();
+ allArtifacts.addAll(unconditionalArtifacts.toCollection());
+ for (PruningManifest manifest : getPruningManifests()) {
+ allArtifacts.addTransitive(manifest.getCandidateRunfiles());
+ }
+ return allArtifacts.build();
+ }
+
+ /**
+ * Returns the collection of runfiles as artifacts, including both unconditional artifacts
+ * and pruning manifest candidates. Middleman artifacts are excluded.
+ */
+ public Iterable<Artifact> getArtifactsWithoutMiddlemen() {
+ return Iterables.filter(getArtifacts(), Artifact.MIDDLEMAN_FILTER);
+ }
+
+ /**
+ * Returns the symlinks.
+ */
+ public NestedSet<Map.Entry<PathFragment, Artifact>> getSymlinks() {
+ return symlinks;
+ }
+
+ /**
+ * Returns the symlinks as a map from path fragment to artifact.
+ */
+ public Map<PathFragment, Artifact> getSymlinksAsMap() {
+ return entriesToMap(symlinks);
+ }
+
+ /**
+ * @param eventHandler Used for throwing an error if we have an obscuring runlink.
+ * May be null, in which case obscuring symlinks are silently discarded.
+ * @param location Location for reporter. Ignored if reporter is null.
+ * @param workingManifest Manifest to be checked for obscuring symlinks.
+ * @return map of source file names mapped to their location on disk.
+ */
+ public static Map<PathFragment, Artifact> filterListForObscuringSymlinks(
+ EventHandler eventHandler, Location location, Map<PathFragment, Artifact> workingManifest) {
+ Map<PathFragment, Artifact> newManifest = new HashMap<>();
+
+ outer:
+ for (Iterator<Entry<PathFragment, Artifact>> i = workingManifest.entrySet().iterator();
+ i.hasNext(); ) {
+ Entry<PathFragment, Artifact> entry = i.next();
+ PathFragment source = entry.getKey();
+ Artifact symlink = entry.getValue();
+ // drop nested entries; warn if this changes anything
+ int n = source.segmentCount();
+ for (int j = 1; j < n; ++j) {
+ PathFragment prefix = source.subFragment(0, n - j);
+ Artifact ancestor = workingManifest.get(prefix);
+ if (ancestor != null) {
+ // This is an obscuring symlink, so just drop it and move on if there's no reporter.
+ if (eventHandler == null) {
+ continue outer;
+ }
+ PathFragment suffix = source.subFragment(n - j, n);
+ Path viaAncestor = ancestor.getPath().getRelative(suffix);
+ Path expected = symlink.getPath();
+ if (!viaAncestor.equals(expected)) {
+ eventHandler.handle(Event.warn(location, "runfiles symlink " + source + " -> "
+ + expected + " obscured by " + prefix + " -> " + ancestor.getPath()));
+ }
+ continue outer;
+ }
+ }
+ newManifest.put(entry.getKey(), entry.getValue());
+ }
+ return newManifest;
+ }
+
+ /**
+ * Returns the symlinks as a map from PathFragment to Artifact, with PathFragments relativized
+ * and rooted at the specified points.
+ * @param root The root the PathFragment is computed relative to (before it is
+ * rooted again). May be null.
+ * @param eventHandler Used for throwing an error if we have an obscuring runlink.
+ * May be null, in which case obscuring symlinks are silently discarded.
+ * @param location Location for eventHandler warnings. Ignored if eventHandler is null.
+ * @return Pair of Maps from remote path fragment to artifact, the first of normal source tree
+ * entries, the second of any elements that live outside the source tree.
+ */
+ public Pair<Map<PathFragment, Artifact>, Map<PathFragment, Artifact>> getRunfilesInputs(
+ PathFragment root, String workspaceSuffix, EventHandler eventHandler, Location location)
+ throws IOException {
+ Map<PathFragment, Artifact> manifest = getSymlinksAsMap();
+ // Add unconditional artifacts (committed to inclusion on construction of runfiles).
+ for (Artifact artifact : getUnconditionalArtifactsWithoutMiddlemen()) {
+ addToManifest(manifest, artifact, root);
+ }
+
+ // Add conditional artifacts (only included if they appear in a pruning manifest).
+ for (Runfiles.PruningManifest pruningManifest : getPruningManifests()) {
+ // This map helps us convert from source tree root-relative paths back to artifacts.
+ Map<String, Artifact> allowedRunfiles = new HashMap<>();
+ for (Artifact artifact : pruningManifest.getCandidateRunfiles()) {
+ allowedRunfiles.put(artifact.getRootRelativePath().getPathString(), artifact);
+ }
+ BufferedReader reader = new BufferedReader(
+ new InputStreamReader(pruningManifest.getManifestFile().getPath().getInputStream()));
+ String line;
+ while ((line = reader.readLine()) != null) {
+ Artifact artifact = allowedRunfiles.get(line);
+ if (artifact != null) {
+ addToManifest(manifest, artifact, root);
+ }
+ }
+ }
+
+ manifest = filterListForObscuringSymlinks(eventHandler, location, manifest);
+ manifest.putAll(manifestExpander.apply(manifest));
+ PathFragment path = new PathFragment(workspaceSuffix);
+ Map<PathFragment, Artifact> result = new HashMap<>();
+ for (Map.Entry<PathFragment, Artifact> entry : manifest.entrySet()) {
+ result.put(path.getRelative(entry.getKey()), entry.getValue());
+ }
+ return Pair.of(result, (Map<PathFragment, Artifact>) new HashMap<>(getRootSymlinksAsMap()));
+ }
+
+ @VisibleForTesting
+ protected static void addToManifest(Map<PathFragment, Artifact> manifest, Artifact artifact,
+ PathFragment root) {
+ PathFragment rootRelativePath = root != null
+ ? artifact.getRootRelativePath().relativeTo(root)
+ : artifact.getRootRelativePath();
+ manifest.put(rootRelativePath, artifact);
+ }
+
+ /**
+ * Returns the root symlinks.
+ */
+ public NestedSet<Map.Entry<PathFragment, Artifact>> getRootSymlinks() {
+ return rootSymlinks;
+ }
+
+ /**
+ * Returns the root symlinks.
+ */
+ public Map<PathFragment, Artifact> getRootSymlinksAsMap() {
+ return entriesToMap(rootSymlinks);
+ }
+
+ /**
+ * Returns the unified map of path fragments to artifacts, taking both artifacts and symlinks into
+ * account.
+ */
+ public Map<PathFragment, Artifact> asMapWithoutRootSymlinks() {
+ Map<PathFragment, Artifact> result = entriesToMap(symlinks);
+ // If multiple artifacts have the same root-relative path, the last one in the list will win.
+ // That is because the runfiles tree cannot contain the same artifact for different
+ // configurations, because it only uses root-relative paths.
+ for (Artifact artifact : Iterables.filter(unconditionalArtifacts, Artifact.MIDDLEMAN_FILTER)) {
+ result.put(artifact.getRootRelativePath(), artifact);
+ }
+ return result;
+ }
+
+ /**
+ * Returns the pruning manifests specified for this runfiles tree.
+ */
+ public NestedSet<PruningManifest> getPruningManifests() {
+ return pruningManifests;
+ }
+
+ /**
+ * Returns the symlinks expander specified for this runfiles tree.
+ */
+ public Function<Map<PathFragment, Artifact>, Map<PathFragment, Artifact>> getSymlinkExpander() {
+ return manifestExpander;
+ }
+
+ /**
+ * Returns the unified map of path fragments to artifacts, taking into account artifacts,
+ * symlinks, and pruning manifest candidates. The returned set is guaranteed to be a (not
+ * necessarily strict) superset of the actual runfiles tree created at execution time.
+ */
+ public NestedSet<Artifact> getAllArtifacts() {
+ if (isEmpty()) {
+ return NestedSetBuilder.emptySet(Order.STABLE_ORDER);
+ }
+ NestedSetBuilder<Artifact> allArtifacts = NestedSetBuilder.stableOrder();
+ allArtifacts
+ .addTransitive(unconditionalArtifacts)
+ .addAll(Iterables.transform(symlinks, TO_ARTIFACT))
+ .addAll(Iterables.transform(rootSymlinks, TO_ARTIFACT));
+ for (PruningManifest manifest : getPruningManifests()) {
+ allArtifacts.addTransitive(manifest.getCandidateRunfiles());
+ }
+ return allArtifacts.build();
+ }
+
+ /**
+ * Returns if there are no runfiles.
+ */
+ public boolean isEmpty() {
+ return unconditionalArtifacts.isEmpty() && symlinks.isEmpty() && rootSymlinks.isEmpty() &&
+ pruningManifests.isEmpty();
+ }
+
+ private static <K, V> Map<K, V> entriesToMap(Iterable<Map.Entry<K, V>> entrySet) {
+ Map<K, V> map = new LinkedHashMap<>();
+ for (Map.Entry<K, V> entry : entrySet) {
+ map.put(entry.getKey(), entry.getValue());
+ }
+ return map;
+ }
+
+ /**
+ * Builder for Runfiles objects.
+ */
+ public static final class Builder {
+ /**
+ * This must be COMPILE_ORDER because {@link #asMapWithoutRootSymlinks} overwrites earlier
+ * entries with later ones, so we want a post-order iteration.
+ */
+ private NestedSetBuilder<Artifact> artifactsBuilder =
+ NestedSetBuilder.compileOrder();
+ private NestedSetBuilder<Map.Entry<PathFragment, Artifact>> symlinksBuilder =
+ NestedSetBuilder.stableOrder();
+ private NestedSetBuilder<Map.Entry<PathFragment, Artifact>> rootSymlinksBuilder =
+ NestedSetBuilder.stableOrder();
+ private NestedSetBuilder<PruningManifest> pruningManifestsBuilder =
+ NestedSetBuilder.stableOrder();
+ private Function<Map<PathFragment, Artifact>, Map<PathFragment, Artifact>>
+ manifestExpander = DUMMY_SYMLINK_EXPANDER;
+
+ /**
+ * Builds a new Runfiles object.
+ */
+ public Runfiles build() {
+ return new Runfiles(artifactsBuilder.build(), symlinksBuilder.build(),
+ rootSymlinksBuilder.build(), pruningManifestsBuilder.build(),
+ manifestExpander);
+ }
+
+ /**
+ * Adds an artifact to the internal collection of artifacts.
+ */
+ public Builder addArtifact(Artifact artifact) {
+ Preconditions.checkNotNull(artifact);
+ artifactsBuilder.add(artifact);
+ return this;
+ }
+
+ /**
+ * Adds several artifacts to the internal collection.
+ */
+ public Builder addArtifacts(Iterable<Artifact> artifacts) {
+ for (Artifact artifact : artifacts) {
+ addArtifact(artifact);
+ }
+ return this;
+ }
+
+
+ /**
+ * Use {@link #addTransitiveArtifacts} instead, to prevent increased memory use.
+ */
+ @Deprecated
+ public Builder addArtifacts(NestedSet<Artifact> artifacts) {
+ // Do not delete this method, or else addArtifacts(Iterable) calls with a NestedSet argument
+ // will not be flagged.
+ Iterable<Artifact> it = artifacts;
+ addArtifacts(it);
+ return this;
+ }
+ /**
+ * Adds a nested set to the internal collection.
+ */
+ public Builder addTransitiveArtifacts(NestedSet<Artifact> artifacts) {
+ artifactsBuilder.addTransitive(artifacts);
+ return this;
+ }
+
+ /**
+ * Adds a symlink.
+ */
+ public Builder addSymlink(PathFragment link, Artifact target) {
+ Preconditions.checkNotNull(link);
+ Preconditions.checkNotNull(target);
+ symlinksBuilder.add(Maps.immutableEntry(link, target));
+ return this;
+ }
+
+ /**
+ * Adds several symlinks.
+ */
+ public Builder addSymlinks(Map<PathFragment, Artifact> symlinks) {
+ symlinksBuilder.addAll(symlinks.entrySet());
+ return this;
+ }
+
+ /**
+ * Adds several symlinks as a NestedSet.
+ */
+ public Builder addSymlinks(NestedSet<Map.Entry<PathFragment, Artifact>> symlinks) {
+ symlinksBuilder.addTransitive(symlinks);
+ return this;
+ }
+
+ /**
+ * Adds several root symlinks.
+ */
+ public Builder addRootSymlinks(Map<PathFragment, Artifact> symlinks) {
+ rootSymlinksBuilder.addAll(symlinks.entrySet());
+ return this;
+ }
+
+ /**
+ * Adds several root symlinks as a NestedSet.
+ */
+ public Builder addRootSymlinks(NestedSet<Map.Entry<PathFragment, Artifact>> symlinks) {
+ rootSymlinksBuilder.addTransitive(symlinks);
+ return this;
+ }
+
+ /**
+ * Adds a pruning manifest. See {@link PruningManifest} for an explanation.
+ */
+ public Builder addPruningManifest(PruningManifest manifest) {
+ pruningManifestsBuilder.add(manifest);
+ return this;
+ }
+
+ /**
+ * Adds several pruning manifests as a NestedSet. See {@link PruningManifest} for an
+ * explanation.
+ */
+ public Builder addPruningManifests(NestedSet<PruningManifest> manifests) {
+ pruningManifestsBuilder.addTransitive(manifests);
+ return this;
+ }
+
+ /**
+ * Specify a function that can create additional manifest entries based on the input entries.
+ */
+ public Builder setManifestExpander(
+ Function<Map<PathFragment, Artifact>, Map<PathFragment, Artifact>> expander) {
+ manifestExpander = Preconditions.checkNotNull(expander);
+ return this;
+ }
+
+ /**
+ * Merges runfiles from a given runfiles support.
+ *
+ * @param runfilesSupport the runfiles support to be merged in
+ */
+ public Builder merge(@Nullable RunfilesSupport runfilesSupport) {
+ return merge(runfilesSupport, null);
+ }
+
+ /**
+ * Merges runfiles from a given runfiles support.
+ *
+ * <p>Sometimes a particular symlink from the runfiles support must not be included in runfiles.
+ * In such cases the path fragment denoting the symlink should be passed in as {@code
+ * ommittedAdditionalSymlink}. The symlink will then be filtered away from the set of additional
+ * symlinks of the target.
+ *
+ * @param runfilesSupport the runfiles support to be merged in
+ * @param omittedAdditionalSymlink the symlink to be omitted, or null if no filtering is needed
+ */
+ public Builder merge(@Nullable RunfilesSupport runfilesSupport,
+ @Nullable final PathFragment omittedAdditionalSymlink) {
+ if (runfilesSupport == null) {
+ return this;
+ }
+ // TODO(bazel-team): We may be able to remove this now.
+ addArtifact(runfilesSupport.getRunfilesMiddleman());
+ Runfiles runfiles = runfilesSupport.getRunfiles();
+ if (omittedAdditionalSymlink == null) {
+ merge(runfiles);
+ } else {
+ artifactsBuilder.addTransitive(runfiles.getUnconditionalArtifacts());
+ symlinksBuilder.addAll(Maps.filterKeys(entriesToMap(runfiles.getSymlinks()),
+ Predicates.not(Predicates.equalTo(omittedAdditionalSymlink))).entrySet());
+ rootSymlinksBuilder.addTransitive(runfiles.getRootSymlinks());
+ pruningManifestsBuilder.addTransitive(runfiles.getPruningManifests());
+ if (manifestExpander == DUMMY_SYMLINK_EXPANDER) {
+ manifestExpander = runfiles.getSymlinkExpander();
+ } else {
+ Function<Map<PathFragment, Artifact>, Map<PathFragment, Artifact>> otherExpander =
+ runfiles.getSymlinkExpander();
+ Preconditions.checkState((otherExpander == DUMMY_SYMLINK_EXPANDER)
+ || manifestExpander.equals(otherExpander));
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Adds the runfiles for a particular target and visits the transitive closure of "srcs",
+ * "deps" and "data", collecting all of their respective runfiles.
+ */
+ public Builder addRunfiles(RuleContext ruleContext,
+ Function<TransitiveInfoCollection, Runfiles> mapping) {
+ Preconditions.checkNotNull(mapping);
+ Preconditions.checkNotNull(ruleContext);
+ addDataDeps(ruleContext);
+ addNonDataDeps(ruleContext, mapping);
+ return this;
+ }
+
+ /**
+ * Adds the files specified by a mapping from the transitive info collection to the runfiles.
+ *
+ * <p>Dependencies in {@code srcs} and {@code deps} are considered.
+ */
+ public Builder add(RuleContext ruleContext,
+ Function<TransitiveInfoCollection, Runfiles> mapping) {
+ Preconditions.checkNotNull(ruleContext);
+ Preconditions.checkNotNull(mapping);
+ for (TransitiveInfoCollection dep : getNonDataDeps(ruleContext)) {
+ Runfiles runfiles = mapping.apply(dep);
+ if (runfiles != null) {
+ merge(runfiles);
+ }
+ }
+
+ return this;
+ }
+
+ /**
+ * Collects runfiles from data dependencies of a target.
+ */
+ public Builder addDataDeps(RuleContext ruleContext) {
+ addTargets(getPrerequisites(ruleContext, "data", Mode.DATA),
+ RunfilesProvider.DATA_RUNFILES);
+ return this;
+ }
+
+ /**
+ * Collects runfiles from "srcs" and "deps" of a target.
+ */
+ public Builder addNonDataDeps(RuleContext ruleContext,
+ Function<TransitiveInfoCollection, Runfiles> mapping) {
+ for (TransitiveInfoCollection target : getNonDataDeps(ruleContext)) {
+ addTargetExceptFileTargets(target, mapping);
+ }
+ return this;
+ }
+
+ public Builder addTargets(Iterable<? extends TransitiveInfoCollection> targets,
+ Function<TransitiveInfoCollection, Runfiles> mapping) {
+ for (TransitiveInfoCollection target : targets) {
+ addTarget(target, mapping);
+ }
+ return this;
+ }
+
+ public Builder addTarget(TransitiveInfoCollection target,
+ Function<TransitiveInfoCollection, Runfiles> mapping) {
+ return addTargetIncludingFileTargets(target, mapping);
+ }
+
+ private Builder addTargetExceptFileTargets(TransitiveInfoCollection target,
+ Function<TransitiveInfoCollection, Runfiles> mapping) {
+ Runfiles runfiles = mapping.apply(target);
+ if (runfiles != null) {
+ merge(runfiles);
+ }
+
+ return this;
+ }
+
+ private Builder addTargetIncludingFileTargets(TransitiveInfoCollection target,
+ Function<TransitiveInfoCollection, Runfiles> mapping) {
+ if (target.getProvider(RunfilesProvider.class) == null
+ && mapping == RunfilesProvider.DATA_RUNFILES) {
+ // RuleConfiguredTarget implements RunfilesProvider, so this will only be called on
+ // FileConfiguredTarget instances.
+ // TODO(bazel-team): This is a terrible hack. We should be able to make this go away
+ // by implementing RunfilesProvider on FileConfiguredTarget. We'd need to be mindful
+ // of the memory use, though, since we have a whole lot of FileConfiguredTarget instances.
+ addTransitiveArtifacts(target.getProvider(FileProvider.class).getFilesToBuild());
+ return this;
+ }
+
+ return addTargetExceptFileTargets(target, mapping);
+ }
+
+ /**
+ * Adds symlinks to given artifacts at their exec paths.
+ */
+ public Builder addSymlinksToArtifacts(Iterable<Artifact> artifacts) {
+ for (Artifact artifact : artifacts) {
+ addSymlink(artifact.getExecPath(), artifact);
+ }
+ return this;
+ }
+
+ /**
+ * Add the other {@link Runfiles} object transitively.
+ */
+ public Builder merge(Runfiles runfiles) {
+ return merge(runfiles, true);
+ }
+
+ /**
+ * Add the other {@link Runfiles} object transitively, but don't merge
+ * pruning manifests.
+ */
+ public Builder mergeExceptPruningManifests(Runfiles runfiles) {
+ return merge(runfiles, false);
+ }
+
+ /**
+ * Add the other {@link Runfiles} object transitively, with the option to include or exclude
+ * pruning manifests in the merge.
+ */
+ private Builder merge(Runfiles runfiles, boolean includePruningManifests) {
+ artifactsBuilder.addTransitive(runfiles.getUnconditionalArtifacts());
+ symlinksBuilder.addTransitive(runfiles.getSymlinks());
+ rootSymlinksBuilder.addTransitive(runfiles.getRootSymlinks());
+ if (includePruningManifests) {
+ pruningManifestsBuilder.addTransitive(runfiles.getPruningManifests());
+ }
+ if (manifestExpander == DUMMY_SYMLINK_EXPANDER) {
+ manifestExpander = runfiles.getSymlinkExpander();
+ } else {
+ Function<Map<PathFragment, Artifact>, Map<PathFragment, Artifact>> otherExpander =
+ runfiles.getSymlinkExpander();
+ Preconditions.checkState((otherExpander == DUMMY_SYMLINK_EXPANDER)
+ || manifestExpander.equals(otherExpander));
+ }
+ return this;
+ }
+
+ private static Iterable<TransitiveInfoCollection> getNonDataDeps(RuleContext ruleContext) {
+ return Iterables.concat(
+ // TODO(bazel-team): This line shouldn't be here. Removing it requires that no rules have
+ // dependent rules in srcs (except for filegroups and such), but always in deps.
+ // TODO(bazel-team): DONT_CHECK is not optimal here. Rules that use split configs need to
+ // be changed not to call into here.
+ getPrerequisites(ruleContext, "srcs", Mode.DONT_CHECK),
+ getPrerequisites(ruleContext, "deps", Mode.DONT_CHECK));
+ }
+
+ /**
+ * For the specified attribute "attributeName" (which must be of type list(label)), resolves all
+ * the labels into ConfiguredTargets (for the same configuration as this one) and returns them
+ * as a list.
+ *
+ * <p>If the rule does not have the specified attribute, returns the empty list.
+ */
+ private static Iterable<? extends TransitiveInfoCollection> getPrerequisites(
+ RuleContext ruleContext, String attributeName, Mode mode) {
+ if (ruleContext.getRule().isAttrDefined(attributeName, Type.LABEL_LIST)) {
+ return ruleContext.getPrerequisites(attributeName, mode);
+ } else {
+ return Collections.emptyList();
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/RunfilesProvider.java b/src/main/java/com/google/devtools/build/lib/analysis/RunfilesProvider.java
new file mode 100644
index 0000000..2e26bbc
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/RunfilesProvider.java
@@ -0,0 +1,91 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.common.base.Function;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+
+/**
+ * Runfiles a target contributes to targets that depend on it.
+ *
+ * <p>The set of runfiles contributed can be different if the dependency is through a
+ * <code>data</code> attribute (note that this is just a rough approximation of the reality --
+ * rule implementations are free to request the data runfiles at any time)
+ */
+@Immutable
+public final class RunfilesProvider implements TransitiveInfoProvider {
+ private final Runfiles defaultRunfiles;
+ private final Runfiles dataRunfiles;
+
+ private RunfilesProvider(Runfiles defaultRunfiles, Runfiles dataRunfiles) {
+ this.defaultRunfiles = defaultRunfiles;
+ this.dataRunfiles = dataRunfiles;
+ }
+
+ public Runfiles getDefaultRunfiles() {
+ return defaultRunfiles;
+ }
+
+ public Runfiles getDataRunfiles() {
+ return dataRunfiles;
+ }
+
+ /**
+ * Returns a function that gets the default runfiles from a {@link TransitiveInfoCollection} or
+ * the empty runfiles instance if it does not contain that provider.
+ */
+ public static final Function<TransitiveInfoCollection, Runfiles> DEFAULT_RUNFILES =
+ new Function<TransitiveInfoCollection, Runfiles>() {
+ @Override
+ public Runfiles apply(TransitiveInfoCollection input) {
+ RunfilesProvider provider = input.getProvider(RunfilesProvider.class);
+ if (provider != null) {
+ return provider.getDefaultRunfiles();
+ }
+
+ return Runfiles.EMPTY;
+ }
+ };
+
+ /**
+ * Returns a function that gets the data runfiles from a {@link TransitiveInfoCollection} or the
+ * empty runfiles instance if it does not contain that provider.
+ *
+ * <p>These are usually used if the target is depended on through a {@code data} attribute.
+ */
+ public static final Function<TransitiveInfoCollection, Runfiles> DATA_RUNFILES =
+ new Function<TransitiveInfoCollection, Runfiles>() {
+ @Override
+ public Runfiles apply(TransitiveInfoCollection input) {
+ RunfilesProvider provider = input.getProvider(RunfilesProvider.class);
+ if (provider != null) {
+ return provider.getDataRunfiles();
+ }
+
+ return Runfiles.EMPTY;
+ }
+ };
+
+ public static RunfilesProvider simple(Runfiles defaultRunfiles) {
+ return new RunfilesProvider(defaultRunfiles, defaultRunfiles);
+ }
+
+ public static RunfilesProvider withData(
+ Runfiles defaultRunfiles, Runfiles dataRunfiles) {
+ return new RunfilesProvider(defaultRunfiles, dataRunfiles);
+ }
+
+ public static final RunfilesProvider EMPTY = new RunfilesProvider(
+ Runfiles.EMPTY, Runfiles.EMPTY);
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/RunfilesSupport.java b/src/main/java/com/google/devtools/build/lib/analysis/RunfilesSupport.java
new file mode 100644
index 0000000..aa7f429
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/RunfilesSupport.java
@@ -0,0 +1,382 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.analysis.SourceManifestAction.ManifestType;
+import com.google.devtools.build.lib.analysis.actions.ActionConstructionContext;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.analysis.config.RunUnder;
+import com.google.devtools.build.lib.collect.IterablesChain;
+import com.google.devtools.build.lib.packages.TargetUtils;
+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.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * This class manages the creation of the runfiles symlink farms.
+ *
+ * <p>For executables that might depend on the existence of files at run-time, we create a symlink
+ * farm: a directory which contains symlinks to the right locations for those runfiles.
+ *
+ * <p>The runfiles symlink farm serves two purposes. The first is to allow programs (and
+ * programmers) to refer to files using their workspace-relative paths, regardless of whether the
+ * files were source files or generated files, and regardless of which part of the package path they
+ * came from. The second purpose is to ensure that all run-time dependencies are explicitly declared
+ * in the BUILD files; programs may only use files which the build system knows that they depend on.
+ *
+ * <p>The symlink farm contains a MANIFEST file which describes its contents. The MANIFEST file
+ * lists the names and contents of all of the symlinks in the symlink farm. For efficiency, Blaze's
+ * dependency analysis ignores the actual symlinks and just looks at the MANIFEST file. It is an
+ * invariant that the MANIFEST file should accurately represent the contents of the symlinks
+ * whenever the MANIFEST file is present. build_runfile_links.py preserves this invariant (modulo
+ * bugs - currently it has a bug where it may fail to preserve that invariant if it gets
+ * interrupted). So the Blaze dependency analysis looks only at the MANIFEST file, rather than at
+ * the individual symlinks.
+ *
+ * <p>We create an Artifact for the MANIFEST file and a RunfilesAction Action to create it. This
+ * action does not depend on any other Artifacts.
+ *
+ * <p>When building an executable and running it, there are three things which must be built: the
+ * executable itself, the runfiles symlink farm (represented in the action graph by the Artifact for
+ * its MANIFEST), and the files pointed to by the symlinks in the symlink farm. To avoid redundancy
+ * in the dependency analysis, we create a Middleman Artifact which depends on all of these. Actions
+ * which will run an executable should depend on this Middleman Artifact.
+ */
+public class RunfilesSupport {
+ private static final String RUNFILES_DIR_EXT = ".runfiles";
+
+ private final Runfiles runfiles;
+
+ private final Artifact runfilesInputManifest;
+ private final Artifact runfilesManifest;
+ private final Artifact runfilesMiddleman;
+ private final Artifact sourcesManifest;
+ private final Artifact owningExecutable;
+ private final boolean createSymlinks;
+ private final ImmutableList<String> args;
+
+ /**
+ * Creates the RunfilesSupport helper with the given executable and runfiles.
+ *
+ * @param ruleContext the rule context to create the runfiles support for
+ * @param executable the executable for whose runfiles this runfiles support is responsible, may
+ * be null
+ * @param runfiles the runfiles
+ * @param appendingArgs to be added after the rule's args
+ */
+ private RunfilesSupport(RuleContext ruleContext, Artifact executable, Runfiles runfiles,
+ List<String> appendingArgs, boolean createSymlinks) {
+ owningExecutable = executable;
+ this.createSymlinks = createSymlinks;
+
+ // Adding run_under target to the runfiles manifest so it would become part
+ // of runfiles tree and would be executable everywhere.
+ RunUnder runUnder = ruleContext.getConfiguration().getRunUnder();
+ if (runUnder != null && runUnder.getLabel() != null
+ && TargetUtils.isTestRule(ruleContext.getRule())) {
+ TransitiveInfoCollection runUnderTarget =
+ ruleContext.getPrerequisite(":run_under", Mode.DATA);
+ runfiles = new Runfiles.Builder()
+ .merge(getRunfiles(runUnderTarget))
+ .merge(runfiles)
+ .build();
+ }
+ this.runfiles = runfiles;
+
+ Preconditions.checkState(!runfiles.isEmpty());
+
+ Map<PathFragment, Artifact> symlinks = getRunfilesSymlinks();
+ if (executable != null && !symlinks.values().contains(executable)) {
+ throw new IllegalStateException("main program " + executable + " not included in runfiles");
+ }
+
+ runfilesInputManifest = createRunfilesInputManifestArtifact(ruleContext);
+ this.runfilesManifest = createRunfilesAction(ruleContext, runfiles);
+ this.runfilesMiddleman = createRunfilesMiddleman(ruleContext, runfiles.getAllArtifacts());
+ sourcesManifest = createSourceManifest(ruleContext, runfiles);
+ args = ImmutableList.<String>builder()
+ .addAll(ruleContext.getTokenizedStringListAttr("args"))
+ .addAll(appendingArgs)
+ .build();
+ }
+
+ private RunfilesSupport(Runfiles runfiles, Artifact runfilesInputManifest,
+ Artifact runfilesManifest, Artifact runfilesMiddleman, Artifact sourcesManifest,
+ Artifact owningExecutable, boolean createSymlinks, ImmutableList<String> args) {
+ this.runfiles = runfiles;
+ this.runfilesInputManifest = runfilesInputManifest;
+ this.runfilesManifest = runfilesManifest;
+ this.runfilesMiddleman = runfilesMiddleman;
+ this.sourcesManifest = sourcesManifest;
+ this.owningExecutable = owningExecutable;
+ this.createSymlinks = createSymlinks;
+ this.args = args;
+ }
+
+ /**
+ * Returns the executable owning this RunfilesSupport. Only use from Skylark.
+ */
+ public Artifact getExecutable() {
+ return owningExecutable;
+ }
+
+ /**
+ * Returns the exec path of the directory where the runfiles contained in this
+ * RunfilesSupport are generated. When the owning rule has no executable,
+ * returns null.
+ */
+ public PathFragment getRunfilesDirectoryExecPath() {
+ if (owningExecutable == null) {
+ return null;
+ }
+
+ PathFragment executablePath = owningExecutable.getExecPath();
+ return executablePath.getParentDirectory().getChild(
+ executablePath.getBaseName() + RUNFILES_DIR_EXT);
+ }
+
+ public Runfiles getRunfiles() {
+ return runfiles;
+ }
+
+ /**
+ * For executable programs, the .runfiles_manifest file outside of the
+ * runfiles symlink farm; otherwise, returns null.
+ *
+ * <p>The MANIFEST file represents the contents of all of the symlinks in the
+ * symlink farm. For efficiency, Blaze's dependency analysis ignores the
+ * actual symlinks and just looks at the MANIFEST file. It is an invariant
+ * that the MANIFEST file should accurately represent the contents of the
+ * symlinks whenever the MANIFEST file is present.
+ */
+ public Artifact getRunfilesInputManifest() {
+ return runfilesInputManifest;
+ }
+
+ private Artifact createRunfilesInputManifestArtifact(ActionConstructionContext context) {
+ // The executable may be null for emptyRunfiles
+ PathFragment relativePath = (owningExecutable != null)
+ ? owningExecutable.getRootRelativePath()
+ : Util.getWorkspaceRelativePath(context.getRule());
+ String basename = relativePath.getBaseName();
+ PathFragment inputManifestPath = relativePath.replaceName(basename + ".runfiles_manifest");
+ return context.getAnalysisEnvironment().getDerivedArtifact(inputManifestPath,
+ context.getConfiguration().getBinDirectory());
+ }
+
+ /**
+ * For executable programs, returns the MANIFEST file in the runfiles
+ * symlink farm, if blaze is run with --build_runfile_links; returns
+ * the .runfiles_manifest file outside of the symlink farm, if blaze
+ * is run with --nobuild_runfile_links.
+ * <p>
+ * Beware: In most cases {@link #getRunfilesInputManifest} is the more
+ * appropriate function.
+ */
+ public Artifact getRunfilesManifest() {
+ return runfilesManifest;
+ }
+
+ /**
+ * For executable programs, the root directory of the runfiles symlink farm;
+ * otherwise, returns null.
+ */
+ public Path getRunfilesDirectory() {
+ return FileSystemUtils.replaceExtension(getRunfilesInputManifest().getPath(), RUNFILES_DIR_EXT);
+ }
+
+ /**
+ * Returns the files pointed to by the symlinks in the runfiles symlink farm. This method is slow.
+ */
+ @VisibleForTesting
+ public Collection<Artifact> getRunfilesSymlinkTargets() {
+ return getRunfilesSymlinks().values();
+ }
+
+ /**
+ * Returns the names of the symlinks in the runfiles symlink farm as a Set of PathFragments. This
+ * method is slow.
+ */
+ // We should make this VisibleForTesting, but it is still used by TestHelper
+ public Set<PathFragment> getRunfilesSymlinkNames() {
+ return getRunfilesSymlinks().keySet();
+ }
+
+ /**
+ * Returns the names of the symlinks in the runfiles symlink farm as a Set of PathFragments. This
+ * method is slow.
+ */
+ @VisibleForTesting
+ public Map<PathFragment, Artifact> getRunfilesSymlinks() {
+ return runfiles.asMapWithoutRootSymlinks();
+ }
+
+ /**
+ * Returns both runfiles artifacts and "conditional" artifacts that may be part of a
+ * Runfiles PruningManifest. This means the returned set may be an overapproximation of the
+ * actual set of runfiles (see {@link Runfiles.PruningManifest}).
+ */
+ public Iterable<Artifact> getRunfilesArtifactsWithoutMiddlemen() {
+ return runfiles.getArtifactsWithoutMiddlemen();
+ }
+
+ /**
+ * Returns the middleman artifact that depends on getExecutable(),
+ * getRunfilesManifest(), and getRunfilesSymlinkTargets(). Anything which
+ * needs to actually run the executable should depend on this.
+ */
+ public Artifact getRunfilesMiddleman() {
+ return runfilesMiddleman;
+ }
+
+ /**
+ * Returns the Sources manifest.
+ * This may be null if the owningRule has no executable.
+ */
+ public Artifact getSourceManifest() {
+ return sourcesManifest;
+ }
+
+ private Artifact createRunfilesMiddleman(ActionConstructionContext context,
+ Iterable<Artifact> allRunfilesArtifacts) {
+ Iterable<Artifact> inputs = IterablesChain.<Artifact>builder()
+ .add(allRunfilesArtifacts)
+ .addElement(runfilesManifest)
+ .build();
+ return context.getAnalysisEnvironment().getMiddlemanFactory().createRunfilesMiddleman(
+ context.getActionOwner(), owningExecutable, inputs,
+ context.getConfiguration().getMiddlemanDirectory());
+ }
+
+ /**
+ * Creates a runfiles action for all of the specified files, and returns the
+ * output artifact (the artifact for the MANIFEST file).
+ *
+ * <p>The "runfiles" action creates a symlink farm that links all the runfiles
+ * (which may come from different places, e.g. different package paths,
+ * generated files, etc.) into a single tree, so that programs can access them
+ * using the workspace-relative name.
+ */
+ private Artifact createRunfilesAction(ActionConstructionContext context, Runfiles runfiles) {
+ // Compute the names of the runfiles directory and its MANIFEST file.
+ Artifact inputManifest = getRunfilesInputManifest();
+ context.getAnalysisEnvironment().registerAction(
+ SourceManifestAction.forRunfiles(
+ ManifestType.SOURCE_SYMLINKS, context.getActionOwner(), inputManifest, runfiles));
+
+ if (!createSymlinks) {
+ // Just return the manifest if that's all the build calls for.
+ return inputManifest;
+ }
+
+ PathFragment runfilesDir = FileSystemUtils.replaceExtension(inputManifest.getRootRelativePath(),
+ RUNFILES_DIR_EXT);
+ PathFragment outputManifestPath = runfilesDir.getRelative("MANIFEST");
+
+ BuildConfiguration config = context.getConfiguration();
+ Artifact outputManifest = context.getAnalysisEnvironment().getDerivedArtifact(
+ outputManifestPath, config.getBinDirectory());
+ context.getAnalysisEnvironment().registerAction(new SymlinkTreeAction(
+ context.getActionOwner(), inputManifest, outputManifest, /*filesetTree=*/false));
+ return outputManifest;
+ }
+
+ /**
+ * Creates an Artifact which writes the "sources only" manifest file.
+ *
+ * @param context the owner for the manifest action
+ * @param runfiles the runfiles
+ * @return the Artifact representing the file write action.
+ */
+ private Artifact createSourceManifest(ActionConstructionContext context, Runfiles runfiles) {
+ // Put the sources only manifest next to the MANIFEST file but call it SOURCES.
+ PathFragment runfilesDir = getRunfilesDirectoryExecPath();
+ if (runfilesDir != null) {
+ PathFragment sourcesManifestPath = runfilesDir.getRelative("SOURCES");
+ Artifact sourceOnlyManifest = context.getAnalysisEnvironment().getDerivedArtifact(
+ sourcesManifestPath, context.getConfiguration().getBinDirectory());
+ context.getAnalysisEnvironment().registerAction(
+ SourceManifestAction.forRunfiles(
+ ManifestType.SOURCES_ONLY, context.getActionOwner(), sourceOnlyManifest, runfiles));
+ return sourceOnlyManifest;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Helper method that returns a collection of artifacts that are necessary for the runfiles of the
+ * given target. Note that the runfile symlink tree is never built, so this may include artifacts
+ * that end up not being used (see {@link Runfiles}).
+ *
+ * @return the Runfiles object
+ */
+
+ private static Runfiles getRunfiles(TransitiveInfoCollection target) {
+ RunfilesProvider runfilesProvider = target.getProvider(RunfilesProvider.class);
+ if (runfilesProvider != null) {
+ return runfilesProvider.getDefaultRunfiles();
+ } else {
+ return Runfiles.EMPTY;
+ }
+ }
+
+ /**
+ * Returns the unmodifiable list of expanded and tokenized 'args' attribute
+ * values.
+ */
+ public List<String> getArgs() {
+ return args;
+ }
+
+ /**
+ * Creates and returns a RunfilesSupport object for the given rule and executable. Note that this
+ * method calls back into the passed in rule to obtain the runfiles.
+ */
+ public static RunfilesSupport withExecutable(RuleContext ruleContext, Runfiles runfiles,
+ Artifact executable) {
+ return new RunfilesSupport(ruleContext, executable, runfiles, ImmutableList.<String>of(),
+ ruleContext.shouldCreateRunfilesSymlinks());
+ }
+
+ /**
+ * Creates and returns a RunfilesSupport object for the given rule and executable. Note that this
+ * method calls back into the passed in rule to obtain the runfiles.
+ */
+ public static RunfilesSupport withExecutable(RuleContext ruleContext, Runfiles runfiles,
+ Artifact executable, boolean createSymlinks) {
+ return new RunfilesSupport(ruleContext, executable, runfiles, ImmutableList.<String>of(),
+ createSymlinks);
+ }
+
+ /**
+ * Creates and returns a RunfilesSupport object for the given rule, executable, runfiles and args.
+ */
+ public static RunfilesSupport withExecutable(RuleContext ruleContext, Runfiles runfiles,
+ Artifact executable, List<String> appendingArgs) {
+ return new RunfilesSupport(ruleContext, executable, runfiles,
+ ImmutableList.copyOf(appendingArgs), ruleContext.shouldCreateRunfilesSymlinks());
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/SourceManifestAction.java b/src/main/java/com/google/devtools/build/lib/analysis/SourceManifestAction.java
new file mode 100644
index 0000000..5aa3bdc
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/SourceManifestAction.java
@@ -0,0 +1,404 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import static java.nio.charset.StandardCharsets.ISO_8859_1;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.ActionOwner;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.Executor;
+import com.google.devtools.build.lib.actions.Executor.ActionContext;
+import com.google.devtools.build.lib.analysis.actions.AbstractFileWriteAction;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.util.Fingerprint;
+import com.google.devtools.build.lib.util.Pair;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nullable;
+
+/**
+ * Action to create a manifest of input files for processing by a subsequent
+ * build step (e.g. runfiles symlinking or archive building).
+ *
+ * <p>The manifest's format is specifiable by {@link ManifestType}, in
+ * accordance with the needs of the calling functionality.
+ *
+ * <p>Note that this action carefully avoids building the manifest content in
+ * memory.
+ */
+public class SourceManifestAction extends AbstractFileWriteAction {
+ /**
+ * Action context that tells what workspace suffix we should use.
+ */
+ public interface Context extends ActionContext {
+ PathFragment getRunfilesPrefix();
+ }
+
+ private static final String GUID = "07459553-a3d0-4d37-9d78-18ed942470f4";
+
+ /**
+ * Interface for defining manifest formatting and reporting specifics.
+ */
+ @VisibleForTesting
+ interface ManifestWriter {
+
+ /**
+ * Writes a single line of manifest output.
+ *
+ * @param manifestWriter the output stream
+ * @param rootRelativePath path of an entry relative to the manifest's root
+ * @param symlink (optional) symlink that resolves the above path
+ */
+ void writeEntry(Writer manifestWriter, PathFragment rootRelativePath,
+ @Nullable Artifact symlink) throws IOException;
+
+ /**
+ * Fulfills {@link #ActionMetadata.getMnemonic()}
+ */
+ String getMnemonic();
+
+ /**
+ * Fulfills {@link #AbstractAction.getRawProgressMessage()}
+ */
+ String getRawProgressMessage();
+ }
+
+ /**
+ * The strategy we use to write manifest entries.
+ */
+ private final ManifestWriter manifestWriter;
+
+ /**
+ * The runfiles for which to create the symlink tree.
+ */
+ private final Runfiles runfiles;
+
+ /**
+ * If non-null, the paths should be computed relative to this path fragment.
+ */
+ private final PathFragment root;
+
+ /**
+ * Creates a new AbstractSourceManifestAction instance using latin1 encoding
+ * to write the manifest file and with a specified root path for manifest entries.
+ *
+ * @param manifestWriter the strategy to use to write manifest entries
+ * @param owner the action owner
+ * @param output the file to which to write the manifest
+ * @param runfiles runfiles
+ * @param root the artifacts' root-relative path is relativized to this before writing it out
+ */
+ private SourceManifestAction(ManifestWriter manifestWriter, ActionOwner owner, Artifact output,
+ Runfiles runfiles, PathFragment root) {
+ super(owner, getDependencies(runfiles), output, false);
+ this.manifestWriter = manifestWriter;
+ this.runfiles = runfiles;
+ this.root = root;
+ }
+
+ @VisibleForTesting
+ public void writeOutputFile(OutputStream out, EventHandler eventHandler, String workspaceSuffix)
+ throws IOException {
+ writeFile(out, runfiles.getRunfilesInputs(
+ root, workspaceSuffix, eventHandler, getOwner().getLocation()));
+ }
+
+ @Override
+ public DeterministicWriter newDeterministicWriter(EventHandler eventHandler, Executor executor)
+ throws IOException {
+ final Pair<Map<PathFragment, Artifact>, Map<PathFragment, Artifact>> runfilesInputs =
+ runfiles.getRunfilesInputs(root,
+ executor.getContext(Context.class).getRunfilesPrefix().toString(), eventHandler,
+ getOwner().getLocation());
+ return new DeterministicWriter() {
+ @Override
+ public void writeOutputFile(OutputStream out) throws IOException {
+ writeFile(out, runfilesInputs);
+ }
+ };
+ }
+
+ /**
+ * Returns the input dependencies for this action. Note we don't need to create the symlink
+ * target Artifacts before we write the output manifest, so this Action does not have to
+ * depend on them. The only necessary dependencies are pruning manifests, which must be read
+ * to properly prune the tree.
+ */
+ private static Collection<Artifact> getDependencies(Runfiles runfiles) {
+ ImmutableList.Builder<Artifact> builder = ImmutableList.builder();
+ for (Runfiles.PruningManifest manifest : runfiles.getPruningManifests()) {
+ builder.add(manifest.getManifestFile());
+ }
+ return builder.build();
+ }
+
+ /**
+ * Sort the entries in both the normal and root manifests and write the output
+ * file.
+ *
+ * @param out is the message stream to write errors to.
+ * @param output The actual mapping of the output manifest.
+ * @throws IOException
+ */
+ private void writeFile(OutputStream out,
+ Pair<Map<PathFragment, Artifact>, Map<PathFragment, Artifact>> output)
+ throws IOException {
+ Writer manifestFile = new BufferedWriter(new OutputStreamWriter(out, ISO_8859_1));
+
+ Comparator<Map.Entry<PathFragment, Artifact>> fragmentComparator =
+ new Comparator<Map.Entry<PathFragment, Artifact>>() {
+ @Override
+ public int compare(Map.Entry<PathFragment, Artifact> path1,
+ Map.Entry<PathFragment, Artifact> path2) {
+ return path1.getKey().compareTo(path2.getKey());
+ }
+ };
+
+ List<Map.Entry<PathFragment, Artifact>> sortedRootLinks =
+ new ArrayList<>(output.second.entrySet());
+ Collections.sort(sortedRootLinks, fragmentComparator);
+
+ List<Map.Entry<PathFragment, Artifact>> sortedManifest =
+ new ArrayList<>(output.first.entrySet());
+ Collections.sort(sortedManifest, fragmentComparator);
+
+ for (Map.Entry<PathFragment, Artifact> line : sortedRootLinks) {
+ manifestWriter.writeEntry(manifestFile, line.getKey(), line.getValue());
+ }
+
+ for (Map.Entry<PathFragment, Artifact> line : sortedManifest) {
+ manifestWriter.writeEntry(manifestFile, line.getKey(), line.getValue());
+ }
+ manifestFile.flush();
+ }
+
+ @Override
+ public String getMnemonic() {
+ return manifestWriter.getMnemonic();
+ }
+
+ @Override
+ protected String getRawProgressMessage() {
+ return manifestWriter.getRawProgressMessage() + " for " + getOwner().getLabel();
+ }
+
+ @Override
+ protected String computeKey() {
+ Fingerprint f = new Fingerprint();
+ f.addString(GUID);
+ Map<PathFragment, Artifact> symlinks = runfiles.getSymlinksAsMap();
+ f.addInt(symlinks.size());
+ for (Map.Entry<PathFragment, Artifact> symlink : symlinks.entrySet()) {
+ f.addPath(symlink.getKey());
+ f.addPath(symlink.getValue().getPath());
+ }
+ Map<PathFragment, Artifact> rootSymlinks = runfiles.getRootSymlinksAsMap();
+ f.addInt(rootSymlinks.size());
+ for (Map.Entry<PathFragment, Artifact> rootSymlink : rootSymlinks.entrySet()) {
+ f.addPath(rootSymlink.getKey());
+ f.addPath(rootSymlink.getValue().getPath());
+ }
+
+ if (root != null) {
+ for (Artifact artifact : runfiles.getArtifactsWithoutMiddlemen()) {
+ f.addPath(artifact.getRootRelativePath().relativeTo(root));
+ f.addPath(artifact.getPath());
+ }
+ } else {
+ for (Artifact artifact : runfiles.getArtifactsWithoutMiddlemen()) {
+ f.addPath(artifact.getRootRelativePath());
+ f.addPath(artifact.getPath());
+ }
+ }
+ return f.hexDigestAndReset();
+ }
+
+ /**
+ * Supported manifest writing strategies.
+ */
+ public static enum ManifestType implements ManifestWriter {
+
+ /**
+ * Writes each line as:
+ *
+ * [rootRelativePath] [resolvingSymlink]
+ *
+ * <p>This strategy is suitable for creating an input manifest to a source view tree. Its
+ * output is a valid input to {@link com.google.devtools.build.lib.analysis.SymlinkTreeAction}.
+ */
+ SOURCE_SYMLINKS {
+ @Override
+ public void writeEntry(Writer manifestWriter, PathFragment rootRelativePath, Artifact symlink)
+ throws IOException {
+ manifestWriter.append(rootRelativePath.getPathString());
+ // This trailing whitespace is REQUIRED to process the single entry line correctly.
+ manifestWriter.append(' ');
+ if (symlink != null) {
+ manifestWriter.append(symlink.getPath().getPathString());
+ }
+ manifestWriter.append('\n');
+ }
+
+ @Override
+ public String getMnemonic() {
+ return "SourceSymlinkManifest";
+ }
+
+ @Override
+ public String getRawProgressMessage() {
+ return "Creating source manifest";
+ }
+ },
+
+ /**
+ * Writes each line as:
+ *
+ * [rootRelativePath]
+ *
+ * <p>This strategy is suitable for an input into a packaging system (notably .par) that
+ * consumes a list of all source files but needs that list to be constant with respect to
+ * how the user has their client laid out on local disk.
+ */
+ SOURCES_ONLY {
+ @Override
+ public void writeEntry(Writer manifestWriter, PathFragment rootRelativePath, Artifact symlink)
+ throws IOException {
+ manifestWriter.append(rootRelativePath.getPathString());
+ manifestWriter.append('\n');
+ manifestWriter.flush();
+ }
+
+ @Override
+ public String getMnemonic() {
+ return "PackagingSourcesManifest";
+ }
+
+ @Override
+ public String getRawProgressMessage() {
+ return "Creating file sources list";
+ }
+ }
+ }
+
+ /** Creates an action for the given runfiles. */
+ public static SourceManifestAction forRunfiles(ManifestType manifestType, ActionOwner owner,
+ Artifact output, Runfiles runfiles) {
+ return new SourceManifestAction(manifestType, owner, output, runfiles, null);
+ }
+
+ /**
+ * Builder class to construct {@link SourceManifestAction} instances.
+ */
+ public static final class Builder {
+ private final ManifestWriter manifestWriter;
+ private final ActionOwner owner;
+ private final Artifact output;
+ private PathFragment top;
+ private final Runfiles.Builder runfilesBuilder = new Runfiles.Builder();
+
+ public Builder(ManifestType manifestType, ActionOwner owner, Artifact output) {
+ manifestWriter = manifestType;
+ this.owner = owner;
+ this.output = output;
+ }
+
+ @VisibleForTesting
+ Builder(ManifestWriter manifestWriter, ActionOwner owner, Artifact output) {
+ this.manifestWriter = manifestWriter;
+ this.owner = owner;
+ this.output = output;
+ }
+
+ public SourceManifestAction build() {
+ return new SourceManifestAction(manifestWriter, owner, output, runfilesBuilder.build(), top);
+ }
+
+ /**
+ * Sets the path fragment which is used to relativize the artifacts' root
+ * relative paths further. Most likely, you don't need this.
+ */
+ public Builder setTopLevel(PathFragment top) {
+ this.top = top;
+ return this;
+ }
+
+ /**
+ * Adds a set of symlinks from the artifacts' root-relative paths to the
+ * artifacts themselves.
+ */
+ public Builder addSymlinks(Iterable<Artifact> artifacts) {
+ runfilesBuilder.addArtifacts(artifacts);
+ return this;
+ }
+
+ /**
+ * Adds a map of symlinks.
+ */
+ public Builder addSymlinks(Map<PathFragment, Artifact> symlinks) {
+ runfilesBuilder.addSymlinks(symlinks);
+ return this;
+ }
+
+ /**
+ * Adds a single symlink.
+ */
+ public Builder addSymlink(PathFragment link, Artifact target) {
+ runfilesBuilder.addSymlink(link, target);
+ return this;
+ }
+
+ /**
+ * <p>Adds a mapping of Artifacts to the directory above the normal symlink
+ * forest base.
+ */
+ public Builder addRootSymlinks(Map<PathFragment, Artifact> rootSymlinks) {
+ runfilesBuilder.addRootSymlinks(rootSymlinks);
+ return this;
+ }
+
+ /**
+ * Set an expander function for the symlinks.
+ */
+ @VisibleForTesting
+ Builder setSymlinksExpander(
+ Function<Map<PathFragment, Artifact>, Map<PathFragment, Artifact>> expander) {
+ runfilesBuilder.setManifestExpander(expander);
+ return this;
+ }
+
+ /**
+ * Adds a runfiles pruning manifest.
+ */
+ @VisibleForTesting
+ Builder addPruningManifest(Runfiles.PruningManifest manifest) {
+ runfilesBuilder.addPruningManifest(manifest);
+ return this;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/SymlinkTreeAction.java b/src/main/java/com/google/devtools/build/lib/analysis/SymlinkTreeAction.java
new file mode 100644
index 0000000..2dc0d4a
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/SymlinkTreeAction.java
@@ -0,0 +1,109 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.AbstractAction;
+import com.google.devtools.build.lib.actions.ActionExecutionContext;
+import com.google.devtools.build.lib.actions.ActionExecutionException;
+import com.google.devtools.build.lib.actions.ActionOwner;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.Executor;
+import com.google.devtools.build.lib.actions.ResourceSet;
+import com.google.devtools.build.lib.util.Fingerprint;
+
+/**
+ * Action responsible for the symlink tree creation.
+ * Used to generate runfiles and fileset symlink farms.
+ */
+public class SymlinkTreeAction extends AbstractAction {
+
+ private static final String GUID = "63412bda-4026-4c8e-a3ad-7deb397728d4";
+
+ private final Artifact inputManifest;
+ private final Artifact outputManifest;
+ private final boolean filesetTree;
+
+ /**
+ * Creates SymlinkTreeAction instance.
+ *
+ * @param owner action owner
+ * @param inputManifest exec path to the input runfiles manifest
+ * @param outputManifest exec path to the generated symlink tree manifest
+ * (must have "MANIFEST" base name). Symlink tree root
+ * will be set to the artifact's parent directory.
+ * @param filesetTree true if this is fileset symlink tree,
+ * false if this is a runfiles symlink tree.
+ */
+ public SymlinkTreeAction(ActionOwner owner, Artifact inputManifest, Artifact outputManifest,
+ boolean filesetTree) {
+ super(owner, ImmutableList.of(inputManifest), ImmutableList.of(outputManifest));
+ Preconditions.checkArgument(outputManifest.getPath().getBaseName().equals("MANIFEST"));
+ this.inputManifest = inputManifest;
+ this.outputManifest = outputManifest;
+ this.filesetTree = filesetTree;
+ }
+
+ public Artifact getInputManifest() {
+ return inputManifest;
+ }
+
+ public Artifact getOutputManifest() {
+ return outputManifest;
+ }
+
+ public boolean isFilesetTree() {
+ return filesetTree;
+ }
+
+ @Override
+ public String getMnemonic() {
+ return "SymlinkTree";
+ }
+
+ @Override
+ protected String getRawProgressMessage() {
+ return (filesetTree ? "Creating Fileset tree " : "Creating runfiles tree ")
+ + outputManifest.getExecPath().getParentDirectory().getPathString();
+ }
+
+ @Override
+ protected String computeKey() {
+ Fingerprint f = new Fingerprint();
+ f.addString(GUID);
+ f.addInt(filesetTree ? 1 : 0);
+ return f.hexDigestAndReset();
+ }
+
+ @Override
+ public ResourceSet estimateResourceConsumption(Executor executor) {
+ // Return null here to indicate that resources would be managed manually
+ // during action execution.
+ return null;
+ }
+
+ @Override
+ public String describeStrategy(Executor executor) {
+ return "local"; // Symlink tree is always generated locally.
+ }
+
+ @Override
+ public void execute(
+ ActionExecutionContext actionExecutionContext)
+ throws ActionExecutionException, InterruptedException {
+ actionExecutionContext.getExecutor().getContext(SymlinkTreeActionContext.class)
+ .createSymlinks(this, actionExecutionContext);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/SymlinkTreeActionContext.java b/src/main/java/com/google/devtools/build/lib/analysis/SymlinkTreeActionContext.java
new file mode 100644
index 0000000..fe61056
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/SymlinkTreeActionContext.java
@@ -0,0 +1,31 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.devtools.build.lib.actions.ActionExecutionContext;
+import com.google.devtools.build.lib.actions.ActionExecutionException;
+import com.google.devtools.build.lib.actions.Executor.ActionContext;
+
+/**
+ * Action context for symlink tree actions (an action that creates a tree of symlinks).
+ */
+public interface SymlinkTreeActionContext extends ActionContext {
+
+ /**
+ * Creates the symlink tree.
+ */
+ void createSymlinks(SymlinkTreeAction action,
+ ActionExecutionContext actionExecutionContext)
+ throws ActionExecutionException, InterruptedException;
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/TargetAndConfiguration.java b/src/main/java/com/google/devtools/build/lib/analysis/TargetAndConfiguration.java
new file mode 100644
index 0000000..cc0feb6
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/TargetAndConfiguration.java
@@ -0,0 +1,104 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.common.base.Function;
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.skyframe.ConfiguredTargetKey;
+import com.google.devtools.build.lib.syntax.Label;
+
+import java.util.Objects;
+
+import javax.annotation.Nullable;
+
+/**
+ * Refers to the pair of a target and a configuration and certain additional information. Not the
+ * same as {@link ConfiguredTarget} -- that also contains the result of the analysis phase.
+ */
+public class TargetAndConfiguration {
+ private final Target target;
+ @Nullable private final BuildConfiguration configuration;
+
+ public TargetAndConfiguration(Target target, @Nullable BuildConfiguration configuration) {
+ this.target = Preconditions.checkNotNull(target);
+ this.configuration = configuration;
+ }
+
+ public TargetAndConfiguration(ConfiguredTarget configuredTarget) {
+ this.target = Preconditions.checkNotNull(configuredTarget).getTarget();
+ this.configuration = configuredTarget.getConfiguration();
+ }
+
+ // The node name in the graph. The name should be unique.
+ // It is not suitable for user display.
+ public String getName() {
+ return target.getLabel() + " "
+ + (configuration == null ? "null" : configuration.shortCacheKey());
+ }
+
+ public static final Function<TargetAndConfiguration, String> NAME_FUNCTION =
+ new Function<TargetAndConfiguration, String>() {
+ @Override
+ public String apply(TargetAndConfiguration node) {
+ return node.getName();
+ }
+ };
+
+ public static final Function<TargetAndConfiguration, ConfiguredTargetKey>
+ TO_LABEL_AND_CONFIGURATION = new Function<TargetAndConfiguration, ConfiguredTargetKey>() {
+ @Override
+ public ConfiguredTargetKey apply(TargetAndConfiguration input) {
+ return new ConfiguredTargetKey(input.getLabel(), input.getConfiguration());
+ }
+ };
+
+ @Override
+ public boolean equals(Object that) {
+ if (this == that) {
+ return true;
+ }
+ if (!(that instanceof TargetAndConfiguration)) {
+ return false;
+ }
+
+ TargetAndConfiguration thatNode = (TargetAndConfiguration) that;
+ return thatNode.target.getLabel().equals(this.target.getLabel()) &&
+ thatNode.configuration == this.configuration;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(target.getLabel(), configuration);
+ }
+
+ @Override
+ public String toString() {
+ return target.getLabel() + " (" + configuration + ")";
+ }
+
+ public Target getTarget() {
+ return target;
+ }
+
+ public Label getLabel() {
+ return target.getLabel();
+ }
+
+ @Nullable
+ public BuildConfiguration getConfiguration() {
+ return configuration;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/TargetCompleteEvent.java b/src/main/java/com/google/devtools/build/lib/analysis/TargetCompleteEvent.java
new file mode 100644
index 0000000..fcfec55
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/TargetCompleteEvent.java
@@ -0,0 +1,75 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.skyframe.SkyValue;
+
+/**
+ * This event is fired as soon as a target is either built or fails.
+ */
+public final class TargetCompleteEvent implements SkyValue {
+
+ private final ConfiguredTarget target;
+ private final NestedSet<Label> rootCauses;
+
+ private TargetCompleteEvent(ConfiguredTarget target, NestedSet<Label> rootCauses) {
+ this.target = target;
+ this.rootCauses = (rootCauses == null)
+ ? NestedSetBuilder.<Label>emptySet(Order.STABLE_ORDER)
+ : rootCauses;
+ }
+
+ /**
+ * Construct a successful target completion event.
+ */
+ public static TargetCompleteEvent createSuccessful(ConfiguredTarget ct) {
+ return new TargetCompleteEvent(ct, null);
+ }
+
+ /**
+ * Construct a target completion event for a failed target, with the given non-empty root causes.
+ */
+ public static TargetCompleteEvent createFailed(ConfiguredTarget ct, NestedSet<Label> rootCauses) {
+ Preconditions.checkArgument(!Iterables.isEmpty(rootCauses));
+ return new TargetCompleteEvent(ct, rootCauses);
+ }
+
+ /**
+ * Returns the target associated with the event.
+ */
+ public ConfiguredTarget getTarget() {
+ return target;
+ }
+
+ /**
+ * Determines whether the target has failed or succeeded.
+ */
+ public boolean failed() {
+ return !rootCauses.isEmpty();
+ }
+
+ /**
+ * Get the root causes of the target. May be empty.
+ */
+ public Iterable<Label> getRootCauses() {
+ return rootCauses;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/TargetContext.java b/src/main/java/com/google/devtools/build/lib/analysis/TargetContext.java
new file mode 100644
index 0000000..9c95db3
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/TargetContext.java
@@ -0,0 +1,96 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.packages.PackageSpecification;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.syntax.Label;
+
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+/**
+ * A helper class for building {@link ConfiguredTarget} instances, in particular for non-rule ones.
+ * For {@link RuleConfiguredTarget} instances, use {@link RuleContext} instead,
+ * which is a subclass of this class.
+ *
+ * <p>The class is intended to be sub-classed by RuleContext, in order to share the code. However,
+ * it's not intended for sub-classing beyond that, and the constructor is intentionally package
+ * private to enforce that.
+ */
+public class TargetContext {
+
+ private final AnalysisEnvironment env;
+ private final Target target;
+ private final BuildConfiguration configuration;
+ /**
+ * This list only contains prerequisites that are not declared in rule attributes, with the
+ * exception of visibility (i.e., visibility is represented here, even though it is a rule
+ * attribute in case of a rule). Rule attributes are handled by the {@link RuleContext} subclass.
+ */
+ private final List<ConfiguredTarget> directPrerequisites;
+ private final NestedSet<PackageSpecification> visibility;
+
+ /**
+ * The constructor is intentionally package private.
+ */
+ TargetContext(AnalysisEnvironment env, Target target, BuildConfiguration configuration,
+ List<ConfiguredTarget> directPrerequisites,
+ NestedSet<PackageSpecification> visibility) {
+ this.env = env;
+ this.target = target;
+ this.configuration = configuration;
+ this.directPrerequisites = directPrerequisites;
+ this.visibility = visibility;
+ }
+
+ public AnalysisEnvironment getAnalysisEnvironment() {
+ return env;
+ }
+
+ public Target getTarget() {
+ return target;
+ }
+
+ public Label getLabel() {
+ return target.getLabel();
+ }
+
+ /**
+ * Returns the configuration for this target. This may return null if the target is supposed to be
+ * configuration-independent (like an input file, or a visibility rule). However, this is
+ * guaranteed to be non-null for rules and for output files.
+ */
+ @Nullable
+ public BuildConfiguration getConfiguration() {
+ return configuration;
+ }
+
+ public NestedSet<PackageSpecification> getVisibility() {
+ return visibility;
+ }
+
+ TransitiveInfoCollection findDirectPrerequisite(Label label, BuildConfiguration config) {
+ for (ConfiguredTarget prerequisite : directPrerequisites) {
+ if (prerequisite.getLabel().equals(label) && (prerequisite.getConfiguration() == config)) {
+ return prerequisite;
+ }
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/TempsProvider.java b/src/main/java/com/google/devtools/build/lib/analysis/TempsProvider.java
new file mode 100644
index 0000000..109992e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/TempsProvider.java
@@ -0,0 +1,42 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+
+import java.util.Collection;
+
+/**
+ * A {@link TransitiveInfoProvider} for rule classes that save extra files when
+ * {@code --save_temps} is in effect.
+ */
+@Immutable
+public final class TempsProvider implements TransitiveInfoProvider {
+
+ private final ImmutableList<Artifact> temps;
+
+ public TempsProvider(ImmutableList<Artifact> temps) {
+ this.temps = temps;
+ }
+
+ /**
+ * Return the extra artifacts to save when {@code --save_temps} is in effect.
+ */
+ public Collection<Artifact> getTemps() {
+ return temps;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/TopLevelArtifactContext.java b/src/main/java/com/google/devtools/build/lib/analysis/TopLevelArtifactContext.java
new file mode 100644
index 0000000..c9b8e51
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/TopLevelArtifactContext.java
@@ -0,0 +1,104 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Contains options which control the set of artifacts to build for top-level targets.
+ */
+@Immutable
+public final class TopLevelArtifactContext {
+
+ public static final TopLevelArtifactContext DEFAULT = new TopLevelArtifactContext(
+ "", /*compileOnly=*/false, /*compilationPrerequisitesOnly*/false,
+ /*runTestsExclusively=*/false, /*outputGroups=*/ImmutableSet.<String>of(),
+ /*shouldRunTests=*/false);
+
+ private final String buildCommand;
+ private final boolean compileOnly;
+ private final boolean compilationPrerequisitesOnly;
+ private final boolean runTestsExclusively;
+ private final ImmutableSet<String> outputGroups;
+ private final boolean shouldRunTests;
+
+ public TopLevelArtifactContext(String buildCommand, boolean compileOnly,
+ boolean compilationPrerequisitesOnly, boolean runTestsExclusively,
+ ImmutableSet<String> outputGroups, boolean shouldRunTests) {
+ this.buildCommand = buildCommand;
+ this.compileOnly = compileOnly;
+ this.compilationPrerequisitesOnly = compilationPrerequisitesOnly;
+ this.runTestsExclusively = runTestsExclusively;
+ this.outputGroups = outputGroups;
+ this.shouldRunTests = shouldRunTests;
+ }
+
+ /** Returns the build command as a string. */
+ public String buildCommand() {
+ return buildCommand;
+ }
+
+ /** Returns the value of the --compile_only flag. */
+ public boolean compileOnly() {
+ return compileOnly;
+ }
+
+ /** Returns the value of the --compilation_prerequisites_only flag. */
+ public boolean compilationPrerequisitesOnly() {
+ return compilationPrerequisitesOnly;
+ }
+
+ /** Whether to run tests in exclusive mode. */
+ public boolean runTestsExclusively() {
+ return runTestsExclusively;
+ }
+
+ /** Returns the value of the --output_groups flag. */
+ public Set<String> outputGroups() {
+ return outputGroups;
+ }
+
+ /** Whether the top-level request command may run tests. */
+ public boolean shouldRunTests() {
+ return shouldRunTests;
+ }
+
+ // TopLevelArtifactContexts are stored in maps in BuildView,
+ // so equals() and hashCode() need to work.
+ @Override
+ public boolean equals(Object other) {
+ if (other instanceof TopLevelArtifactContext) {
+ TopLevelArtifactContext otherContext = (TopLevelArtifactContext) other;
+ return buildCommand.equals(otherContext.buildCommand)
+ && compileOnly == otherContext.compileOnly
+ && compilationPrerequisitesOnly == otherContext.compilationPrerequisitesOnly
+ && runTestsExclusively == otherContext.runTestsExclusively
+ && outputGroups.equals(otherContext.outputGroups)
+ && shouldRunTests == otherContext.shouldRunTests;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(buildCommand, compileOnly, compilationPrerequisitesOnly,
+ runTestsExclusively, outputGroups, shouldRunTests);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/TopLevelArtifactHelper.java b/src/main/java/com/google/devtools/build/lib/analysis/TopLevelArtifactHelper.java
new file mode 100644
index 0000000..3c025f4
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/TopLevelArtifactHelper.java
@@ -0,0 +1,158 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.rules.test.TestProvider;
+
+/**
+ * A small static class containing utility methods for handling the inclusion of
+ * extra top-level artifacts into the build.
+ */
+public final class TopLevelArtifactHelper {
+
+ private TopLevelArtifactHelper() {
+ // Prevent instantiation.
+ }
+
+ /** Returns command-specific artifacts which may exist for a given target and build command. */
+ public static final Iterable<Artifact> getCommandArtifacts(TransitiveInfoCollection target,
+ String buildCommand) {
+ TopLevelArtifactProvider provider = target.getProvider(TopLevelArtifactProvider.class);
+ if (provider != null
+ && provider.getCommandsForExtraArtifacts().contains(buildCommand.toLowerCase())) {
+ return provider.getArtifactsForCommand();
+ } else {
+ return ImmutableList.of();
+ }
+ }
+
+ /**
+ * Utility function to form a list of all test output Artifacts of the given targets to test.
+ */
+ public static ImmutableCollection<Artifact> getAllArtifactsToTest(
+ Iterable<? extends TransitiveInfoCollection> targets) {
+ if (targets == null) {
+ return ImmutableList.of();
+ }
+ ImmutableList.Builder<Artifact> allTestArtifacts = ImmutableList.builder();
+ for (TransitiveInfoCollection target : targets) {
+ allTestArtifacts.addAll(TestProvider.getTestStatusArtifacts(target));
+ }
+ return allTestArtifacts.build();
+ }
+
+ /**
+ * Utility function to form a NestedSet of all top-level Artifacts of the given targets.
+ */
+ public static NestedSet<Artifact> getAllArtifactsToBuild(
+ Iterable<? extends TransitiveInfoCollection> targets, TopLevelArtifactContext options) {
+ NestedSetBuilder<Artifact> allArtifacts = NestedSetBuilder.stableOrder();
+ for (TransitiveInfoCollection target : targets) {
+ allArtifacts.addTransitive(getAllArtifactsToBuild(target, options));
+ }
+ return allArtifacts.build();
+ }
+
+ /**
+ * Returns all artifacts to build if this target is requested as a top-level target. The resulting
+ * set includes the temps and either the files to compile, if
+ * {@code options.compileOnly() == true}, or the files to run.
+ *
+ * <p>Calls to this method should generally return quickly; however, the runfiles computation can
+ * be lazy, in which case it can be expensive on the first call. Subsequent calls may or may not
+ * return the same {@code Iterable} instance.
+ */
+ public static NestedSet<Artifact> getAllArtifactsToBuild(TransitiveInfoCollection target,
+ TopLevelArtifactContext options) {
+ NestedSetBuilder<Artifact> allArtifacts = NestedSetBuilder.stableOrder();
+ TempsProvider tempsProvider = target.getProvider(TempsProvider.class);
+ if (tempsProvider != null) {
+ allArtifacts.addAll(tempsProvider.getTemps());
+ }
+
+ TopLevelArtifactProvider topLevelArtifactProvider =
+ target.getProvider(TopLevelArtifactProvider.class);
+ if (topLevelArtifactProvider != null) {
+ for (String outputGroup : options.outputGroups()) {
+ NestedSet<Artifact> results = topLevelArtifactProvider.getOutputGroup(outputGroup);
+ if (results != null) {
+ allArtifacts.addTransitive(results);
+ }
+ }
+ }
+
+ if (options.compileOnly()) {
+ FilesToCompileProvider provider = target.getProvider(FilesToCompileProvider.class);
+ if (provider != null) {
+ allArtifacts.addAll(provider.getFilesToCompile());
+ }
+ } else if (options.compilationPrerequisitesOnly()) {
+ CompilationPrerequisitesProvider provider =
+ target.getProvider(CompilationPrerequisitesProvider.class);
+ if (provider != null) {
+ allArtifacts.addTransitive(provider.getCompilationPrerequisites());
+ }
+ } else {
+ FilesToRunProvider filesToRunProvider = target.getProvider(FilesToRunProvider.class);
+ boolean hasRunfilesSupport = false;
+ if (filesToRunProvider != null) {
+ allArtifacts.addAll(filesToRunProvider.getFilesToRun());
+ hasRunfilesSupport = filesToRunProvider.getRunfilesSupport() != null;
+ }
+
+ if (!hasRunfilesSupport) {
+ RunfilesProvider runfilesProvider =
+ target.getProvider(RunfilesProvider.class);
+ if (runfilesProvider != null) {
+ allArtifacts.addTransitive(runfilesProvider.getDefaultRunfiles().getAllArtifacts());
+ }
+ }
+
+ AlwaysBuiltArtifactsProvider forcedArtifacts = target.getProvider(
+ AlwaysBuiltArtifactsProvider.class);
+ if (forcedArtifacts != null) {
+ allArtifacts.addTransitive(forcedArtifacts.getArtifactsToAlwaysBuild());
+ }
+ }
+
+ allArtifacts.addAll(getCommandArtifacts(target, options.buildCommand()));
+ allArtifacts.addAll(getCoverageArtifacts(target, options));
+ return allArtifacts.build();
+ }
+
+ private static Iterable<Artifact> getCoverageArtifacts(TransitiveInfoCollection target,
+ TopLevelArtifactContext topLevelOptions) {
+ if (!topLevelOptions.compileOnly() && !topLevelOptions.compilationPrerequisitesOnly()
+ && topLevelOptions.shouldRunTests()) {
+ // Add baseline code coverage artifacts if we are collecting code coverage. We do that only
+ // when running tests.
+ // It might be slightly faster to first check if any configuration has coverage enabled.
+ if (target.getConfiguration() != null
+ && target.getConfiguration().isCodeCoverageEnabled()) {
+ BaselineCoverageArtifactsProvider provider =
+ target.getProvider(BaselineCoverageArtifactsProvider.class);
+ if (provider != null) {
+ return provider.getBaselineCoverageArtifacts();
+ }
+ }
+ }
+ return ImmutableList.of();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/TopLevelArtifactProvider.java b/src/main/java/com/google/devtools/build/lib/analysis/TopLevelArtifactProvider.java
new file mode 100644
index 0000000..e2a2d57
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/TopLevelArtifactProvider.java
@@ -0,0 +1,61 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+
+/**
+ * ConfiguredTargets implementing this interface can provide command-specific
+ * and unconditional extra artifacts to the build.
+ */
+@Immutable
+public final class TopLevelArtifactProvider implements TransitiveInfoProvider {
+
+ private final ImmutableList<String> commandsForExtraArtifacts;
+ private final ImmutableList<Artifact> artifactsForCommand;
+ private final ImmutableMap<String, NestedSet<Artifact>> outputGroups;
+
+ public TopLevelArtifactProvider(ImmutableList<String> commandsForExtraArtifacts,
+ ImmutableList<Artifact> artifactsForCommand) {
+ this.commandsForExtraArtifacts = commandsForExtraArtifacts;
+ this.artifactsForCommand = artifactsForCommand;
+ this.outputGroups = ImmutableMap.<String, NestedSet<Artifact>>of();
+ }
+
+ public TopLevelArtifactProvider(String key, NestedSet<Artifact> artifactsToBuild) {
+ this.commandsForExtraArtifacts = ImmutableList.of();
+ this.artifactsForCommand = ImmutableList.of();
+ this.outputGroups = ImmutableMap.<String, NestedSet<Artifact>>of(key, artifactsToBuild);
+ }
+
+ /** Returns the commands (in lowercase) that this provider should provide artifacts for. */
+ public ImmutableList<String> getCommandsForExtraArtifacts() {
+ return commandsForExtraArtifacts;
+ }
+
+ /** Returns the extra artifacts for the commands. */
+ public ImmutableList<Artifact> getArtifactsForCommand() {
+ return artifactsForCommand;
+ }
+
+ /** Returns artifacts that are to be built for every command. */
+ public NestedSet<Artifact> getOutputGroup(String outputGroupName) {
+ return outputGroups.get(outputGroupName);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/TransitiveInfoCollection.java b/src/main/java/com/google/devtools/build/lib/analysis/TransitiveInfoCollection.java
new file mode 100644
index 0000000..82396ee
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/TransitiveInfoCollection.java
@@ -0,0 +1,118 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.common.collect.UnmodifiableIterator;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.syntax.SkylarkModule;
+
+import javax.annotation.Nullable;
+
+/**
+ * Objects that implement this interface bundle multiple {@link TransitiveInfoProvider} interfaces.
+ *
+ * <p>This interface (together with {@link TransitiveInfoProvider} is the cornerstone of the data
+ * model of the analysis phase.
+ *
+ * <p>The computation a configured target does is allowed to depend on the following things:
+ * <ul>
+ * <li>The associated Target (which will usually be a Rule)
+ * <li>Its own configuration (the configured target does not have access to other configurations,
+ * e.g. the host configuration, though)
+ * <li>The transitive info providers and labels of its direct dependencies.
+ * </ul>
+ *
+ * <p>And these are the only inputs. Notably, a configured target is not supposed to access
+ * other configured targets, the transitive info collections of configured targets it does not
+ * directly depend on, the actions created by anyone else or the contents of any input file. We
+ * strive to make it impossible for configured targets to do these things.
+ *
+ * <p>A configured target is expected to produce the following data during its analysis:
+ * <ul>
+ * <li>A number of Artifacts and Actions generating them
+ * <li>A set of {@link TransitiveInfoProvider}s that it passes on to the targets directly dependent
+ * on it
+ * </ul>
+ *
+ * <p>The information that can be passed on to dependent targets by way of
+ * {@link TransitiveInfoProvider} is subject to constraints (which are detailed in the
+ * documentation of that class).
+ *
+ * <p>Configured targets are currently allowed to create artifacts at any exec path. It would be
+ * better if they could be constrained to a subtree based on the label of the configured target,
+ * but this is currently not feasible because multiple rules violate this constraint and the
+ * output format is part of its interface.
+ *
+ * <p>In principle, multiple configured targets should not create actions with conflicting
+ * outputs. There are still a few exceptions to this rule that are slated to be eventually
+ * removed, we have provisions to handle this case (Action instances that share at least one
+ * output file are required to be exactly the same), but this does put some pressure on the design
+ * and we are eventually planning to eliminate this option.
+ *
+ * <p>These restrictions together make it possible to:
+ * <ul>
+ * <li>Correctly cache the analysis phase; by tightly constraining what a configured target is
+ * allowed to access and what it is not, we can know when it needs to invalidate a particular
+ * one and when it can reuse an already existing one.
+ * <li>Serialize / deserialize individual configured targets at will, making it possible for
+ * example to swap out part of the analysis state if there is memory pressure or to move them in
+ * persistent storage so that the state can be reconstructed at a different time or in a
+ * different process. The stretch goal is to eventually facilitate cross-uses caching of this
+ * information.
+ * </ul>
+ *
+ * <p>Implementations of build rules should <b>not</b> hold on to references to the
+ * {@link TransitiveInfoCollection}s representing their direct prerequisites in order to reduce
+ * their memory footprint (otherwise, the referenced object could refer one of its direct
+ * dependencies in turn, thereby making the size of the objects reachable from a single instance
+ * unbounded).
+ *
+ * @see TransitiveInfoProvider
+ */
+@SkylarkModule(name = "target", doc = "A BUILD target.")
+public interface TransitiveInfoCollection extends Iterable<TransitiveInfoProvider> {
+
+ /**
+ * Returns the transitive information provider requested, or null if the provider is not found.
+ * The provider has to be a TransitiveInfoProvider Java class.
+ */
+ @Nullable <P extends TransitiveInfoProvider> P getProvider(Class<P> provider);
+
+ /**
+ * Returns the label associated with this prerequisite.
+ */
+ Label getLabel();
+
+ /**
+ * <p>Returns the {@link BuildConfiguration} for which this transitive info collection is defined.
+ * Configuration is defined for all configured targets with exception of {@link
+ * InputFileConfiguredTarget} and {@link PackageGroupConfiguredTarget} for which it is always
+ * <b>null</b>.</p>
+ */
+ @Nullable BuildConfiguration getConfiguration();
+
+ /**
+ * Returns the transitive information requested or null, if the information is not found.
+ * The transitive information has to have been added using the Skylark framework.
+ */
+ @Nullable Object get(String providerKey);
+
+ /**
+ * Returns an unmodifiable iterator over the transitive info providers in the collections.
+ */
+ @Override
+ UnmodifiableIterator<TransitiveInfoProvider> iterator();
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/TransitiveInfoProvider.java b/src/main/java/com/google/devtools/build/lib/analysis/TransitiveInfoProvider.java
new file mode 100644
index 0000000..37ec191
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/TransitiveInfoProvider.java
@@ -0,0 +1,60 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+/**
+ * This marker interface must be extended by every interface that represents
+ * rolled-up data about the transitive closure of a configured target.
+ *
+ * TransitiveInfoProviders need to be serializable, and for that reason they must conform to
+ * the following restrictions:
+ *
+ * <ul>
+ * <li>The provider interface must directly extend {@code TransitiveInfoProvider}.
+ * <li>Every method must return immutable data.</li>
+ * <li>Every method must return the same object if called multiple times with the same
+ * arguments.</li>
+ * <li>Overloading a method name multiple times is forbidden.</li>
+ * <li>The return type of a method must satisfy one of the following conditions:
+ * <ul>
+ * <li>It must be from the set of {String, Integer, int, Boolean, bool, Label, PathFragment,
+ * Artifact}, OR</li>
+ * <li>it must be an ImmutableList/List/Collection/Iterable of T, where T is either
+ * one of the types above with a default serializer or T implements ValueSerializer), OR</li>
+ * <li>it must be serializable (TBD)</li>
+ * </ul>
+ * <li>If the method takes arguments, it must declare a custom serializer (TBD).</li>
+ * </ul>
+ *
+ * <p>Some typical uses of this interface are:
+ * <ul>
+ * <li>The set of Python source files in the transitive closure of this rule
+ * <li>The set of declared C++ header files in the transitive closure
+ * <li>The files that need to be built when the target is mentioned on the command line
+ * </ul>
+ *
+ * <p>Note that if implemented naively, this would result in the memory requirements
+ * being O(n^2): in a long dependency chain, if every target adds one single artifact, storing the
+ * transitive closures of every rule would take 1+2+3+...+n-1+n = O(n^2) memory.
+ *
+ * <p>In order to avoid this, we introduce the concept of nested sets. A nested set is an immutable
+ * data structure that can contain direct members and other nested sets (recursively). Nested sets
+ * are iterable and can be flattened into ordered sets, where the order depends on which
+ * implementation of NestedSet you pick.
+ *
+ * @see TransitiveInfoCollection
+ */
+public interface TransitiveInfoProvider {
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/Util.java b/src/main/java/com/google/devtools/build/lib/analysis/Util.java
new file mode 100644
index 0000000..ee10bf0
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/Util.java
@@ -0,0 +1,64 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+/**
+ * Utility methods for use by ConfiguredTarget implementations.
+ */
+public abstract class Util {
+
+ private Util() {}
+
+ //---------- Label and Target related methods
+
+ /**
+ * Returns the workspace-relative path of the specified target (file or rule).
+ *
+ * <p>For example, "//foo/bar:wiz" and "//foo:bar/wiz" both result in "foo/bar/wiz".
+ */
+ public static PathFragment getWorkspaceRelativePath(Target target) {
+ return getWorkspaceRelativePath(target.getLabel());
+ }
+
+ /**
+ * Returns the workspace-relative path of the specified target (file or rule).
+ *
+ * <p>For example, "//foo/bar:wiz" and "//foo:bar/wiz" both result in "foo/bar/wiz".
+ */
+ public static PathFragment getWorkspaceRelativePath(Label label) {
+ return label.getPackageFragment().getRelative(label.getName());
+ }
+
+ /**
+ * Returns the workspace-relative path of the specified target (file or rule),
+ * prepending a prefix and appending a suffix.
+ *
+ * <p>For example, "//foo/bar:wiz" and "//foo:bar/wiz" both result in "foo/bar/wiz".
+ */
+ public static PathFragment getWorkspaceRelativePath(Target target, String prefix, String suffix) {
+ return target.getLabel().getPackageFragment().getRelative(prefix + target.getName() + suffix);
+ }
+
+ /**
+ * Checks if a PathFragment contains a '-'.
+ */
+ public static boolean containsHyphen(PathFragment path) {
+ return path.getPathString().indexOf('-') >= 0;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/ViewCreationFailedException.java b/src/main/java/com/google/devtools/build/lib/analysis/ViewCreationFailedException.java
new file mode 100644
index 0000000..ae7dfc5
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/ViewCreationFailedException.java
@@ -0,0 +1,31 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+/**
+ * An exception indicating that there was a problem during the view
+ * construction (loading and analysis phases) for one or more targets, that the
+ * configured target graph could not be successfully constructed, and that
+ * a build cannot be started.
+ */
+public class ViewCreationFailedException extends Exception {
+
+ public ViewCreationFailedException(String message) {
+ super(message);
+ }
+
+ public ViewCreationFailedException(String message, Throwable cause) {
+ super(message + ": " + cause.getMessage(), cause);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/VisibilityProvider.java b/src/main/java/com/google/devtools/build/lib/analysis/VisibilityProvider.java
new file mode 100644
index 0000000..a438145
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/VisibilityProvider.java
@@ -0,0 +1,29 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.packages.PackageSpecification;
+
+/**
+ * Provider class for configured targets that have a visibility.
+ */
+public interface VisibilityProvider extends TransitiveInfoProvider {
+
+ /**
+ * Returns the visibility specification.
+ */
+ NestedSet<PackageSpecification> getVisibility();
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/VisibilityProviderImpl.java b/src/main/java/com/google/devtools/build/lib/analysis/VisibilityProviderImpl.java
new file mode 100644
index 0000000..01dd06a
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/VisibilityProviderImpl.java
@@ -0,0 +1,36 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.packages.PackageSpecification;
+
+/**
+ * Visibility provider implementation.
+ */
+@Immutable
+public final class VisibilityProviderImpl implements VisibilityProvider {
+ private final NestedSet<PackageSpecification> visibility;
+
+ public VisibilityProviderImpl(NestedSet<PackageSpecification> visibility) {
+ this.visibility = visibility;
+ }
+
+ @Override
+ public NestedSet<PackageSpecification> getVisibility() {
+ return visibility;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/WorkspaceStatusAction.java b/src/main/java/com/google/devtools/build/lib/analysis/WorkspaceStatusAction.java
new file mode 100644
index 0000000..5b3bd27
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/WorkspaceStatusAction.java
@@ -0,0 +1,185 @@
+// Copyright 2014 Google Inc. 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.analysis;
+
+import com.google.common.base.Splitter;
+import com.google.common.base.Supplier;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.actions.AbstractAction;
+import com.google.devtools.build.lib.actions.ActionOwner;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.ArtifactFactory;
+import com.google.devtools.build.lib.actions.ArtifactOwner;
+import com.google.devtools.build.lib.actions.Executor.ActionContext;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
+import com.google.devtools.build.lib.vfs.Path;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * An action writing the workspace status files.
+ *
+ * <p>These files represent information about the environment the build was run in. They are used
+ * by language-specific build info factories to make the data in them available for individual
+ * languages (e.g. by turning them into .h files for C++)
+ *
+ * <p>The format of these files a list of key-value pairs, one for each line. The key and the value
+ * are separated by a space.
+ *
+ * <p>There are two of these files: volatile and stable. Changes in the volatile file do not
+ * cause rebuilds if no other file is changed. This is useful for frequently-changing information
+ * that does not significantly affect the build, e.g. the current time.
+ */
+public abstract class WorkspaceStatusAction extends AbstractAction {
+
+ /**
+ * The type of a workspace status action key.
+ */
+ public enum KeyType {
+ INTEGER,
+ STRING,
+ VERBATIM,
+ }
+
+ /**
+ * Language for keys that should be present in the build info for every language.
+ */
+ // TODO(bazel-team): Once this is released, migrate the only place in the depot to use
+ // the BUILD_USERNAME, BUILD_HOSTNAME and BUILD_DIRECTORY keys instead of BUILD_INFO. Then
+ // language-specific build info keys can be removed.
+ public static final String ALL_LANGUAGES = "*";
+
+ /**
+ * Action context required by the actions that write language-specific workspace status artifacts.
+ */
+ public static interface Context extends ActionContext {
+ ImmutableMap<String, Key> getStableKeys();
+ ImmutableMap<String, Key> getVolatileKeys();
+ }
+
+ /**
+ * A key in the workspace status info file.
+ */
+ public static class Key {
+ private final KeyType type;
+
+ /**
+ * Should be set to ALL_LANGUAGES if the key should be present in the build info of every
+ * language.
+ */
+ private final String language;
+ private final String defaultValue;
+ private final String redactedValue;
+
+ private Key(KeyType type, String language, String defaultValue, String redactedValue) {
+ this.type = type;
+ this.language = language;
+ this.defaultValue = defaultValue;
+ this.redactedValue = redactedValue;
+ }
+
+ public KeyType getType() {
+ return type;
+ }
+
+ public boolean isInLanguage(String language) {
+ return this.language.equals(ALL_LANGUAGES) || this.language.equals(language);
+ }
+
+ public String getDefaultValue() {
+ return defaultValue;
+ }
+
+ public String getRedactedValue() {
+ return redactedValue;
+ }
+
+ public static Key forLanguage(
+ String language, KeyType type, String defaultValue, String redactedValue) {
+ return new Key(type, language, defaultValue, redactedValue);
+ }
+
+ public static Key of(KeyType type, String defaultValue, String redactedValue) {
+ return new Key(type, ALL_LANGUAGES, defaultValue, redactedValue);
+ }
+ }
+
+ /**
+ * Parses the output of the workspace status action.
+ *
+ * <p>The output is a text file with each line representing a workspace status info key.
+ * The key is the part of the line before the first space and should consist of the characters
+ * [A-Z_] (although this is not checked). Everything after the first space is the value.
+ */
+ public static Map<String, String> parseValues(Path file) throws IOException {
+ HashMap<String, String> result = new HashMap<>();
+ Splitter lineSplitter = Splitter.on(" ").limit(2);
+ for (String line : Splitter.on("\n").split(
+ new String(FileSystemUtils.readContentAsLatin1(file)))) {
+ List<String> items = ImmutableList.copyOf(lineSplitter.split(line));
+ if (items.size() != 2) {
+ continue;
+ }
+
+ result.put(items.get(0), items.get(1));
+ }
+
+ return ImmutableMap.copyOf(result);
+ }
+
+ /**
+ * Factory for {@link WorkspaceStatusAction}.
+ */
+ public interface Factory {
+ /**
+ * Creates the workspace status action.
+ *
+ * <p>If the objects returned for two builds are equals, the workspace status action can be
+ * be reused between them. Note that this only applies to the action object itself (the action
+ * will be unconditionally re-executed on every build)
+ */
+ WorkspaceStatusAction createWorkspaceStatusAction(
+ ArtifactFactory artifactFactory, ArtifactOwner artifactOwner, Supplier<UUID> buildId);
+
+ /**
+ * Creates a dummy workspace status map. Used in cases where the build failed, so that part of
+ * the workspace status is nevertheless available.
+ */
+ Map<String, String> createDummyWorkspaceStatus();
+ }
+
+ protected WorkspaceStatusAction(ActionOwner owner,
+ Iterable<Artifact> inputs,
+ Iterable<Artifact> outputs) {
+ super(owner, inputs, outputs);
+ }
+
+ /**
+ * The volatile status artifact containing items that may change even if nothing changed
+ * between the two builds, e.g. current time.
+ */
+ public abstract Artifact getVolatileStatus();
+
+ /**
+ * The stable status artifact containing items that change only if information relevant to the
+ * build changes, e.g. the name of the user running the build or the hostname.
+ */
+ public abstract Artifact getStableStatus();
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/actions/AbstractFileWriteAction.java b/src/main/java/com/google/devtools/build/lib/analysis/actions/AbstractFileWriteAction.java
new file mode 100644
index 0000000..187fc48
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/actions/AbstractFileWriteAction.java
@@ -0,0 +1,142 @@
+// Copyright 2014 Google Inc. 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.analysis.actions;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.AbstractAction;
+import com.google.devtools.build.lib.actions.ActionExecutionContext;
+import com.google.devtools.build.lib.actions.ActionExecutionException;
+import com.google.devtools.build.lib.actions.ActionOwner;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.ExecException;
+import com.google.devtools.build.lib.actions.Executor;
+import com.google.devtools.build.lib.actions.ResourceSet;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.syntax.Label;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * Abstract Action to write to a file.
+ */
+public abstract class AbstractFileWriteAction extends AbstractAction {
+
+ protected final boolean makeExecutable;
+
+ /**
+ * Creates a new AbstractFileWriteAction instance.
+ *
+ * @param owner the action owner.
+ * @param inputs the Artifacts that this Action depends on
+ * @param output the Artifact that will be created by executing this Action.
+ * @param makeExecutable iff true will change the output file to be
+ * executable.
+ */
+ public AbstractFileWriteAction(ActionOwner owner,
+ Iterable<Artifact> inputs, Artifact output, boolean makeExecutable) {
+ // There is only one output, and it is primary.
+ super(owner, inputs, ImmutableList.of(output));
+ this.makeExecutable = makeExecutable;
+ }
+
+ public boolean makeExecutable() {
+ return makeExecutable;
+ }
+
+ @Override
+ public final void execute(ActionExecutionContext actionExecutionContext)
+ throws ActionExecutionException, InterruptedException {
+ try {
+ getStrategy(actionExecutionContext.getExecutor()).exec(actionExecutionContext.getExecutor(),
+ this, actionExecutionContext.getFileOutErr(), actionExecutionContext);
+ } catch (ExecException e) {
+ throw e.toActionExecutionException(
+ "Writing file for rule '" + Label.print(getOwner().getLabel()) + "'",
+ actionExecutionContext.getExecutor().getVerboseFailures(), this);
+ }
+ afterWrite(actionExecutionContext.getExecutor());
+ }
+
+ /**
+ * Produce a DeterministicWriter that can write the file to an OutputStream deterministically.
+ *
+ * @param eventHandler destination for warning messages. (Note that errors should
+ * still be indicated by throwing an exception; reporter.error() will
+ * not cause action execution to fail.)
+ * @param executor the Executor.
+ * @throws IOException if the content cannot be written to the output stream
+ */
+ public abstract DeterministicWriter newDeterministicWriter(EventHandler eventHandler,
+ Executor executor) throws IOException, InterruptedException, ExecException;
+
+ /**
+ * This hook is called after the File has been successfully written to disk.
+ *
+ * @param executor the Executor.
+ */
+ protected void afterWrite(Executor executor) {
+ }
+
+ // We're mainly doing I/O, so estimate very low CPU usage, e.g. 1%. Just a guess.
+ private static final ResourceSet DEFAULT_FILEWRITE_LOCAL_ACTION_RESOURCE_SET =
+ new ResourceSet(/*memoryMb=*/0.0, /*cpuUsage=*/0.01, /*ioUsage=*/0.2);
+
+ @Override
+ public ResourceSet estimateResourceConsumption(Executor executor) {
+ return executor.getContext(FileWriteActionContext.class).estimateResourceConsumption(this);
+ }
+
+ public ResourceSet estimateResourceConsumptionLocal() {
+ return DEFAULT_FILEWRITE_LOCAL_ACTION_RESOURCE_SET;
+ }
+
+ @Override
+ public String getMnemonic() {
+ return "FileWrite";
+ }
+
+ @Override
+ protected String getRawProgressMessage() {
+ return "Writing " + (makeExecutable ? "script " : "file ")
+ + Iterables.getOnlyElement(getOutputs()).prettyPrint();
+ }
+
+ /**
+ * Whether the file write can be generated remotely. If the file is consumed in Blaze
+ * unconditionally, it doesn't make sense to run remotely.
+ */
+ public boolean isRemotable() {
+ return true;
+ }
+
+ @Override
+ public final String describeStrategy(Executor executor) {
+ return executor.getContext(FileWriteActionContext.class).strategyLocality(this);
+ }
+
+ private FileWriteActionContext getStrategy(Executor executor) {
+ return executor.getContext(FileWriteActionContext.class);
+ }
+
+ /**
+ * A deterministic writer writes bytes to an output stream. The same byte stream is written
+ * on every invocation of writeOutputFile().
+ */
+ public interface DeterministicWriter {
+ public void writeOutputFile(OutputStream out) throws IOException;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/actions/ActionConstructionContext.java b/src/main/java/com/google/devtools/build/lib/analysis/actions/ActionConstructionContext.java
new file mode 100644
index 0000000..b7461e5
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/actions/ActionConstructionContext.java
@@ -0,0 +1,37 @@
+// Copyright 2014 Google Inc. 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.analysis.actions;
+
+import com.google.devtools.build.lib.actions.ActionOwner;
+import com.google.devtools.build.lib.analysis.AnalysisEnvironment;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.packages.Rule;
+
+/**
+ * A temporary interface to allow migration from RuleConfiguredTarget to RuleContext. It bundles
+ * the items commonly needed to construct action instances.
+ */
+public interface ActionConstructionContext {
+ /** The rule for which the actions are constructed. */
+ Rule getRule();
+
+ /** Returns the action owner that should be used for actions. */
+ ActionOwner getActionOwner();
+
+ /** Returns the {@link BuildConfiguration} for which the given rule is analyzed. */
+ BuildConfiguration getConfiguration();
+
+ /** The current analysis environment. */
+ AnalysisEnvironment getAnalysisEnvironment();
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/actions/BinaryFileWriteAction.java b/src/main/java/com/google/devtools/build/lib/analysis/actions/BinaryFileWriteAction.java
new file mode 100644
index 0000000..b9a2ea5
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/actions/BinaryFileWriteAction.java
@@ -0,0 +1,90 @@
+// Copyright 2014 Google Inc. 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.analysis.actions;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.io.ByteSource;
+import com.google.common.io.ByteStreams;
+import com.google.devtools.build.lib.actions.ActionOwner;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.Executor;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.util.Fingerprint;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * Action to write a binary file.
+ */
+public final class BinaryFileWriteAction extends AbstractFileWriteAction {
+
+ private static final String GUID = "eeee07fe-4b40-11e4-82d6-eba0b4f713e2";
+
+ private final ByteSource source;
+
+ /**
+ * Creates a new BinaryFileWriteAction instance without inputs.
+ *
+ * @param owner the action owner.
+ * @param output the Artifact that will be created by executing this Action.
+ * @param source a source of bytes that will be written to the file.
+ * @param makeExecutable iff true will change the output file to be executable.
+ */
+ public BinaryFileWriteAction(
+ ActionOwner owner, Artifact output, ByteSource source, boolean makeExecutable) {
+ super(owner, /*inputs=*/Artifact.NO_ARTIFACTS, output, makeExecutable);
+ this.source = Preconditions.checkNotNull(source);
+ }
+
+ @VisibleForTesting
+ public ByteSource getSource() {
+ return source;
+ }
+
+ @Override
+ public DeterministicWriter newDeterministicWriter(EventHandler eventHandler, Executor executor) {
+ return new DeterministicWriter() {
+ @Override
+ public void writeOutputFile(OutputStream out) throws IOException {
+ try (InputStream in = source.openStream()) {
+ ByteStreams.copy(in, out);
+ }
+ out.flush();
+ }
+ };
+ }
+
+ @Override
+ protected String computeKey() {
+ Fingerprint f = new Fingerprint();
+ f.addString(GUID);
+ f.addString(String.valueOf(makeExecutable));
+
+ try (InputStream in = source.openStream()) {
+ byte[] buffer = new byte[512];
+ int amountRead;
+ while ((amountRead = in.read(buffer)) != -1) {
+ f.addBytes(buffer, 0, amountRead);
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ return f.hexDigestAndReset();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/actions/CommandLine.java b/src/main/java/com/google/devtools/build/lib/analysis/actions/CommandLine.java
new file mode 100644
index 0000000..ddadc25
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/actions/CommandLine.java
@@ -0,0 +1,123 @@
+// Copyright 2014 Google Inc. 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.analysis.actions;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.collect.CollectionUtils;
+
+/**
+ * A representation of a command line to be executed by a SpawnAction.
+ */
+public abstract class CommandLine {
+ /**
+ * Returns the command line.
+ */
+ public abstract Iterable<String> arguments();
+
+ /**
+ * Returns whether the command line represents a shell command with the given shell executable.
+ * This is used to give better error messages.
+ *
+ * <p>By default, this method returns false.
+ */
+ public boolean isShellCommand() {
+ return false;
+ }
+
+ /**
+ * A default implementation of a command line backed by a copy of the given list of arguments.
+ */
+ static CommandLine ofInternal(Iterable<String> arguments, final boolean isShellCommand) {
+ final Iterable<String> immutableArguments = CollectionUtils.makeImmutable(arguments);
+ return new CommandLine() {
+ @Override
+ public Iterable<String> arguments() {
+ return immutableArguments;
+ }
+
+ @Override
+ public boolean isShellCommand() {
+ return isShellCommand;
+ }
+ };
+ }
+
+ /**
+ * Returns a {@link CommandLine} backed by a copy of the given list of arguments.
+ */
+ public static CommandLine of(Iterable<String> arguments, final boolean isShellCommand) {
+ final Iterable<String> immutableArguments = CollectionUtils.makeImmutable(arguments);
+ return new CommandLine() {
+ @Override
+ public Iterable<String> arguments() {
+ return immutableArguments;
+ }
+
+ @Override
+ public boolean isShellCommand() {
+ return isShellCommand;
+ }
+ };
+ }
+
+ /**
+ * Returns a {@link CommandLine} that is constructed by prepending the {@code executableArgs} to
+ * {@code commandLine}.
+ */
+ static CommandLine ofMixed(final ImmutableList<String> executableArgs,
+ final CommandLine commandLine, final boolean isShellCommand) {
+ Preconditions.checkState(!executableArgs.isEmpty());
+ return new CommandLine() {
+ @Override
+ public Iterable<String> arguments() {
+ return Iterables.concat(executableArgs, commandLine.arguments());
+ }
+
+ @Override
+ public boolean isShellCommand() {
+ return isShellCommand;
+ }
+ };
+ }
+
+ /**
+ * Returns a {@link CommandLine} with {@link CharSequence} arguments. This can be useful to create
+ * memory efficient command lines with {@link com.google.devtools.build.lib.util.LazyString}s.
+ */
+ public static CommandLine ofCharSequences(final ImmutableList<CharSequence> arguments) {
+ return new CommandLine() {
+ @Override
+ public Iterable<String> arguments() {
+ ImmutableList.Builder<String> builder = ImmutableList.builder();
+ for (CharSequence arg : arguments) {
+ builder.add(arg.toString());
+ }
+ return builder.build();
+ }
+ };
+ }
+
+ /**
+ * This helps when debugging Blaze code that uses {@link CommandLine}s, as you can see their
+ * content directly in the variable inspector.
+ */
+ @Override
+ public String toString() {
+ return Joiner.on(' ').join(arguments());
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/actions/CustomCommandLine.java b/src/main/java/com/google/devtools/build/lib/analysis/actions/CustomCommandLine.java
new file mode 100644
index 0000000..d358f0b
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/actions/CustomCommandLine.java
@@ -0,0 +1,358 @@
+// Copyright 2014 Google Inc. 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.analysis.actions;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.collect.CollectionUtils;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A customizable, serializable class for building memory efficient command lines.
+ */
+@Immutable
+public final class CustomCommandLine extends CommandLine {
+
+ private abstract static class ArgvFragment {
+ abstract void eval(ImmutableList.Builder<String> builder);
+ }
+
+ // It's better to avoid anonymous classes if we want to serialize command lines
+
+ private static final class ObjectArg extends ArgvFragment {
+ private final Object arg;
+
+ private ObjectArg(Object arg) {
+ this.arg = arg;
+ }
+
+ @Override
+ void eval(ImmutableList.Builder<String> builder) {
+ builder.add(arg.toString());
+ }
+ }
+
+ private static final class JoinExecPathsArg extends ArgvFragment {
+
+ private final String delimiter;
+ private final Iterable<Artifact> artifacts;
+
+ private JoinExecPathsArg(String delimiter, Iterable<Artifact> artifacts) {
+ this.delimiter = delimiter;
+ this.artifacts = CollectionUtils.makeImmutable(artifacts);
+ }
+
+ @Override
+ void eval(ImmutableList.Builder<String> builder) {
+ builder.add(Artifact.joinExecPaths(delimiter, artifacts));
+ }
+ }
+
+ private static final class PathWithTemplateArg extends ArgvFragment {
+
+ private final String template;
+ private final PathFragment[] paths;
+
+ private PathWithTemplateArg(String template, PathFragment... paths) {
+ this.template = template;
+ this.paths = paths;
+ }
+
+ @Override
+ void eval(ImmutableList.Builder<String> builder) {
+ // PathFragment.toString() uses getPathString()
+ builder.add(String.format(template, (Object[]) paths));
+ }
+ }
+
+ // TODO(bazel-team): CustomArgv and CustomMultiArgv is going to be difficult to expose
+ // in Skylark. Maybe we can get rid of them by refactoring JavaCompileAction. It also
+ // raises immutability / serialization issues.
+ /**
+ * Custom Java code producing a String argument. Usage of this class is discouraged.
+ */
+ public abstract static class CustomArgv extends ArgvFragment {
+
+ @Override
+ void eval(ImmutableList.Builder<String> builder) {
+ builder.add(argv());
+ }
+
+ public abstract String argv();
+ }
+
+ /**
+ * Custom Java code producing a List of String arguments. Usage of this class is discouraged.
+ */
+ public abstract static class CustomMultiArgv extends ArgvFragment {
+
+ @Override
+ void eval(ImmutableList.Builder<String> builder) {
+ builder.addAll(argv());
+ }
+
+ public abstract Iterable<String> argv();
+ }
+
+ private static final class JoinPathsArg extends ArgvFragment {
+
+ private final String delimiter;
+ private final Iterable<PathFragment> paths;
+
+ private JoinPathsArg(String delimiter, Iterable<PathFragment> paths) {
+ this.delimiter = delimiter;
+ this.paths = CollectionUtils.makeImmutable(paths);
+ }
+
+ @Override
+ void eval(ImmutableList.Builder<String> builder) {
+ builder.add(Joiner.on(delimiter).join(paths));
+ }
+ }
+
+ /**
+ * Arguments that intersperse strings between the items in a sequence. There are two forms of
+ * interspersing, and either may be used by this implementation:
+ * <ul>
+ * <li>before each - a string is added before each item in a sequence. e.g.
+ * {@code -f foo -f bar -f baz}
+ * <li>format each - a format string is used to format each item in a sequence. e.g.
+ * {@code -I/foo -I/bar -I/baz} for the format {@code "-I%s"}
+ * </ul>
+ *
+ * <p>This class could be used both with both the "before" and "format" features at the same
+ * time, but this is probably more confusion than it is worth. If you need this functionality,
+ * consider using "before" only but storing the strings pre-formated in a {@link NestedSet}.
+ */
+ private static final class InterspersingArgs extends ArgvFragment {
+ private final Iterable<?> sequence;
+ private final String beforeEach;
+ private final String formatEach;
+
+ /**
+ * Do not call from outside this class because this does not guarantee that {@code sequence} is
+ * immutable.
+ */
+ private InterspersingArgs(Iterable<?> sequence, String beforeEach, String formatEach) {
+ this.sequence = sequence;
+ this.beforeEach = beforeEach;
+ this.formatEach = formatEach;
+ }
+
+ static InterspersingArgs fromStrings(
+ Iterable<?> sequence, String beforeEach, String formatEach) {
+ return new InterspersingArgs(
+ CollectionUtils.makeImmutable(sequence), beforeEach, formatEach);
+ }
+
+ static InterspersingArgs fromExecPaths(
+ Iterable<Artifact> sequence, String beforeEach, String formatEach) {
+ return new InterspersingArgs(
+ Artifact.toExecPaths(CollectionUtils.makeImmutable(sequence)), beforeEach, formatEach);
+ }
+
+ @Override
+ void eval(ImmutableList.Builder<String> builder) {
+ for (Object item : sequence) {
+ if (item == null) {
+ continue;
+ }
+
+ if (beforeEach != null) {
+ builder.add(beforeEach);
+ }
+ String arg = item.toString();
+ if (formatEach != null) {
+ arg = String.format(formatEach, arg);
+ }
+ builder.add(arg);
+ }
+ }
+ }
+
+ /**
+ * A Builder class for CustomCommandLine with the appropriate methods.
+ *
+ * <p>{@link Iterable} instances passed to {@code add*} methods will be stored internally as
+ * collections that are known to be immutable copies. This means that any {@link Iterable} that is
+ * not a {@link NestedSet} or {@link ImmutableList} may be copied.
+ *
+ * <p>{@code addFormatEach*} methods take an {@link Iterable} but use these as arguments to
+ * {@link String#format(String, Object...)} with a certain constant format string. For instance,
+ * if {@code format} is {@code "-I%s"}, then the final arguments may be
+ * {@code -Ifoo -Ibar -Ibaz}
+ *
+ * <p>{@code addBeforeEach*} methods take an {@link Iterable} but insert a certain {@link String}
+ * once before each element in the string, meaning the total number of elements added is twice the
+ * length of the {@link Iterable}. For instance: {@code -f foo -f bar -f baz}
+ */
+ public static final class Builder {
+
+ private final List<ArgvFragment> arguments = new ArrayList<>();
+
+ public Builder add(CharSequence arg) {
+ if (arg != null) {
+ arguments.add(new ObjectArg(arg));
+ }
+ return this;
+ }
+
+ public Builder add(Label arg) {
+ if (arg != null) {
+ arguments.add(new ObjectArg(arg));
+ }
+ return this;
+ }
+
+ public Builder add(String arg, Iterable<String> args) {
+ if (arg != null && args != null) {
+ arguments.add(new ObjectArg(arg));
+ arguments.add(InterspersingArgs.fromStrings(args, /*beforeEach=*/null, "%s"));
+ }
+ return this;
+ }
+
+ public Builder add(Iterable<String> args) {
+ if (args != null) {
+ arguments.add(InterspersingArgs.fromStrings(args, /*beforeEach=*/null, "%s"));
+ }
+ return this;
+ }
+
+ public Builder addExecPath(String arg, Artifact artifact) {
+ if (arg != null && artifact != null) {
+ arguments.add(new ObjectArg(arg));
+ arguments.add(new ObjectArg(artifact.getExecPath()));
+ }
+ return this;
+ }
+
+ public Builder addExecPaths(String arg, Iterable<Artifact> artifacts) {
+ if (arg != null && artifacts != null) {
+ arguments.add(new ObjectArg(arg));
+ arguments.add(InterspersingArgs.fromExecPaths(artifacts, /*beforeEach=*/null, "%s"));
+ }
+ return this;
+ }
+
+ public Builder addExecPaths(Iterable<Artifact> artifacts) {
+ if (artifacts != null) {
+ arguments.add(InterspersingArgs.fromExecPaths(artifacts, /*beforeEach=*/null, "%s"));
+ }
+ return this;
+ }
+
+ public Builder addJoinExecPaths(String arg, String delimiter, Iterable<Artifact> artifacts) {
+ if (arg != null && artifacts != null) {
+ arguments.add(new ObjectArg(arg));
+ arguments.add(new JoinExecPathsArg(delimiter, artifacts));
+ }
+ return this;
+ }
+
+ public Builder addPath(PathFragment path) {
+ if (path != null) {
+ arguments.add(new ObjectArg(path));
+ }
+ return this;
+ }
+
+ public Builder addPaths(String template, PathFragment... path) {
+ if (template != null && path != null) {
+ arguments.add(new PathWithTemplateArg(template, path));
+ }
+ return this;
+ }
+
+ public Builder addJoinPaths(String delimiter, Iterable<PathFragment> paths) {
+ if (delimiter != null && paths != null) {
+ arguments.add(new JoinPathsArg(delimiter, paths));
+ }
+ return this;
+ }
+
+ public Builder addBeforeEachPath(String repeated, Iterable<PathFragment> paths) {
+ if (repeated != null && paths != null) {
+ arguments.add(InterspersingArgs.fromStrings(paths, repeated, "%s"));
+ }
+ return this;
+ }
+
+ public Builder addBeforeEach(String repeated, Iterable<String> strings) {
+ if (repeated != null && strings != null) {
+ arguments.add(InterspersingArgs.fromStrings(strings, repeated, "%s"));
+ }
+ return this;
+ }
+
+ public Builder addBeforeEachExecPath(String repeated, Iterable<Artifact> artifacts) {
+ if (repeated != null && artifacts != null) {
+ arguments.add(InterspersingArgs.fromExecPaths(artifacts, repeated, "%s"));
+ }
+ return this;
+ }
+
+ public Builder addFormatEach(String format, Iterable<String> strings) {
+ if (format != null && strings != null) {
+ arguments.add(InterspersingArgs.fromStrings(strings, /*beforeEach=*/null, format));
+ }
+ return this;
+ }
+
+ public Builder add(CustomArgv arg) {
+ if (arg != null) {
+ arguments.add(arg);
+ }
+ return this;
+ }
+
+ public Builder add(CustomMultiArgv arg) {
+ if (arg != null) {
+ arguments.add(arg);
+ }
+ return this;
+ }
+
+ public CustomCommandLine build() {
+ return new CustomCommandLine(arguments);
+ }
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ private final ImmutableList<ArgvFragment> arguments;
+
+ private CustomCommandLine(List<ArgvFragment> arguments) {
+ this.arguments = ImmutableList.copyOf(arguments);
+ }
+
+ @Override
+ public Iterable<String> arguments() {
+ ImmutableList.Builder<String> builder = ImmutableList.builder();
+ for (ArgvFragment arg : arguments) {
+ arg.eval(builder);
+ }
+ return builder.build();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/actions/ExecutableSymlinkAction.java b/src/main/java/com/google/devtools/build/lib/analysis/actions/ExecutableSymlinkAction.java
new file mode 100644
index 0000000..376d9b8
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/actions/ExecutableSymlinkAction.java
@@ -0,0 +1,66 @@
+// Copyright 2014 Google Inc. 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.analysis.actions;
+
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.ActionExecutionContext;
+import com.google.devtools.build.lib.actions.ActionExecutionException;
+import com.google.devtools.build.lib.actions.ActionOwner;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.vfs.Path;
+
+import java.io.IOException;
+
+/**
+ * Action to create an executable symbolic link. It includes additional
+ * validation that symlink target is indeed an executable file.
+ */
+public final class ExecutableSymlinkAction extends SymlinkAction {
+
+ public ExecutableSymlinkAction(ActionOwner owner, Artifact input, Artifact output) {
+ super(owner, input, output, "Symlinking " + owner.getLabel());
+ }
+
+ @Override
+ public void execute(ActionExecutionContext actionExecutionContext)
+ throws ActionExecutionException {
+ Path inputPath = actionExecutionContext.getExecutor().getExecRoot().getRelative(
+ getInputPath());
+ try {
+ // Validate that input path is a file with the executable bit is set.
+ if (!inputPath.isFile()) {
+ throw new ActionExecutionException(
+ "'" + Iterables.getOnlyElement(getInputs()).prettyPrint() + "' is not a file", this,
+ false);
+ }
+ if (!inputPath.isExecutable()) {
+ throw new ActionExecutionException("failed to create symbolic link '"
+ + Iterables.getOnlyElement(getOutputs()).prettyPrint()
+ + "': file '" + Iterables.getOnlyElement(getInputs()).prettyPrint()
+ + "' is not executable", this, false);
+ }
+ } catch (IOException e) {
+ throw new ActionExecutionException("failed to create symbolic link '"
+ + Iterables.getOnlyElement(getOutputs()).prettyPrint()
+ + "' to the '" + Iterables.getOnlyElement(getInputs()).prettyPrint()
+ + "' due to I/O error: " + e.getMessage(), e, this, false);
+ }
+
+ super.execute(actionExecutionContext);
+ }
+
+ @Override
+ public String getMnemonic() { return "ExecutableSymlink"; }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/actions/FileWriteAction.java b/src/main/java/com/google/devtools/build/lib/analysis/actions/FileWriteAction.java
new file mode 100644
index 0000000..1128617
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/actions/FileWriteAction.java
@@ -0,0 +1,145 @@
+// Copyright 2014 Google Inc. 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.analysis.actions;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.devtools.build.lib.actions.ActionOwner;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.Executor;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.util.Fingerprint;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Collection;
+
+/**
+ * Action to write to a file.
+ * <p>TODO(bazel-team): Choose a better name to distinguish this class from
+ * {@link BinaryFileWriteAction}.
+ */
+public class FileWriteAction extends AbstractFileWriteAction {
+
+ private static final String GUID = "332877c7-ca9f-4731-b387-54f620408522";
+
+ /**
+ * We keep it as a CharSequence for memory-efficiency reasons. The toString()
+ * method of the object represents the content of the file.
+ *
+ * <p>For example, this allows us to keep a {@code List<Artifact>} wrapped
+ * in a {@code LazyString} instead of the string representation of the concatenation.
+ * This saves memory because the Artifacts are shared objects but the
+ * resulting String is not.
+ */
+ private final CharSequence fileContents;
+
+ /**
+ * Creates a new FileWriteAction instance without inputs using UTF8 encoding.
+ *
+ * @param owner the action owner.
+ * @param output the Artifact that will be created by executing this Action.
+ * @param fileContents the contents to be written to the file.
+ * @param makeExecutable iff true will change the output file to be
+ * executable.
+ */
+ public FileWriteAction(ActionOwner owner, Artifact output, CharSequence fileContents,
+ boolean makeExecutable) {
+ this(owner, Artifact.NO_ARTIFACTS, output, fileContents, makeExecutable);
+ }
+
+ /**
+ * Creates a new FileWriteAction instance using UTF8 encoding.
+ *
+ * @param owner the action owner.
+ * @param inputs the Artifacts that this Action depends on
+ * @param output the Artifact that will be created by executing this Action.
+ * @param fileContents the contents to be written to the file.
+ * @param makeExecutable iff true will change the output file to be
+ * executable.
+ */
+ public FileWriteAction(ActionOwner owner, Collection<Artifact> inputs,
+ Artifact output, CharSequence fileContents, boolean makeExecutable) {
+ super(owner, inputs, output, makeExecutable);
+ this.fileContents = fileContents;
+ }
+
+ /**
+ * Creates a new FileWriteAction instance using UTF8 encoding.
+ *
+ * @param owner the action owner.
+ * @param inputs the Artifacts that this Action depends on
+ * @param output the Artifact that will be created by executing this Action.
+ * @param makeExecutable iff true will change the output file to be
+ * executable.
+ */
+ protected FileWriteAction(ActionOwner owner, Collection<Artifact> inputs,
+ Artifact output, boolean makeExecutable) {
+ this(owner, inputs, output, "", makeExecutable);
+ }
+
+ public String getFileContents() {
+ return fileContents.toString();
+ }
+
+ /**
+ * Create a DeterministicWriter for the content of the output file as provided by
+ * {@link #getFileContents()}.
+ */
+ @Override
+ public DeterministicWriter newDeterministicWriter(EventHandler eventHandler,
+ Executor executor) {
+ return new DeterministicWriter() {
+ @Override
+ public void writeOutputFile(OutputStream out) throws IOException {
+ out.write(getFileContents().getBytes(UTF_8));
+ }
+ };
+ }
+
+ /**
+ * Computes the Action key for this action by computing the fingerprint for
+ * the file contents.
+ */
+ @Override
+ protected String computeKey() {
+ Fingerprint f = new Fingerprint();
+ f.addString(GUID);
+ f.addString(String.valueOf(makeExecutable));
+ f.addString(getFileContents());
+ return f.hexDigestAndReset();
+ }
+
+ /**
+ * Creates a FileWriteAction to write contents to the resulting artifact
+ * fileName in the genfiles root underneath the package path.
+ *
+ * @param ruleContext the ruleContext that will own the action of creating this file.
+ * @param fileName name of the file to create.
+ * @param contents data to write to file.
+ * @param executable flags that file should be marked executable.
+ * @return Artifact describing the file to create.
+ */
+ public static Artifact createFile(RuleContext ruleContext,
+ String fileName, CharSequence contents, boolean executable) {
+ Artifact scriptFileArtifact = ruleContext.getAnalysisEnvironment().getDerivedArtifact(
+ ruleContext.getTarget().getLabel().getPackageFragment().getRelative(fileName),
+ ruleContext.getConfiguration().getGenfilesDirectory());
+ ruleContext.registerAction(new FileWriteAction(
+ ruleContext.getActionOwner(), scriptFileArtifact, contents, executable));
+ return scriptFileArtifact;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/actions/FileWriteActionContext.java b/src/main/java/com/google/devtools/build/lib/analysis/actions/FileWriteActionContext.java
new file mode 100644
index 0000000..7e10331
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/actions/FileWriteActionContext.java
@@ -0,0 +1,44 @@
+// Copyright 2014 Google Inc. 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.analysis.actions;
+
+import com.google.devtools.build.lib.actions.ActionExecutionContext;
+import com.google.devtools.build.lib.actions.ExecException;
+import com.google.devtools.build.lib.actions.Executor;
+import com.google.devtools.build.lib.actions.Executor.ActionContext;
+import com.google.devtools.build.lib.actions.ResourceSet;
+import com.google.devtools.build.lib.util.io.FileOutErr;
+
+/**
+ * The action context for {@link AbstractFileWriteAction} instances (technically instances of
+ * subclasses).
+ */
+public interface FileWriteActionContext extends ActionContext {
+
+ /**
+ * Performs all the setup and then calls back into the action to write the data.
+ */
+ void exec(Executor executor, AbstractFileWriteAction action, FileOutErr outErr,
+ ActionExecutionContext actionExecutionContext) throws ExecException, InterruptedException;
+
+ /**
+ * Returns the estimated resource consumption of the action.
+ */
+ ResourceSet estimateResourceConsumption(AbstractFileWriteAction action);
+
+ /**
+ * Returns where the action actually runs.
+ */
+ String strategyLocality(AbstractFileWriteAction action);
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/actions/ParamFileHelper.java b/src/main/java/com/google/devtools/build/lib/analysis/actions/ParamFileHelper.java
new file mode 100644
index 0000000..e7869e4
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/actions/ParamFileHelper.java
@@ -0,0 +1,158 @@
+// Copyright 2014 Google Inc. 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.analysis.actions;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.actions.ActionOwner;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.ParameterFile;
+import com.google.devtools.build.lib.analysis.AnalysisEnvironment;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.List;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * A command-line implementation that wraps another command line and puts the arguments in a
+ * parameter file if necessary
+ *
+ * <p>The Linux kernel has a limit for the command line length, and that can be easily reached
+ * if, for example, a command is listing all its inputs on the command line.
+ */
+@Immutable
+public final class ParamFileHelper {
+
+ /**
+ * Returns a params file artifact or null for a given command description.
+ *
+ * <p>Returns null if parameter files are not to be used according to paramFileInfo, or if the
+ * command line is short enough that a parameter file is not needed.
+ *
+ * <p>Make sure to add the returned artifact (if not null) as an input of the corresponding
+ * action.
+ *
+ * @param executableArgs leading arguments that should never be wrapped in a parameter file
+ * @param arguments arguments to the command (in addition to executableArgs), OR
+ * @param commandLine a {@link CommandLine} that provides the arguments (in addition to
+ * executableArgs)
+ * @param paramFileInfo parameter file information
+ * @param configuration the configuration
+ * @param analysisEnvironment the analysis environment
+ * @param outputs outputs of the action (used to construct a filename for the params file)
+ */
+ public static Artifact getParamsFile(
+ List<String> executableArgs,
+ @Nullable Iterable<String> arguments,
+ @Nullable CommandLine commandLine,
+ @Nullable ParamFileInfo paramFileInfo,
+ BuildConfiguration configuration,
+ AnalysisEnvironment analysisEnvironment,
+ Iterable<Artifact> outputs) {
+ if (paramFileInfo == null ||
+ getParamFileSize(executableArgs, arguments, commandLine)
+ < configuration.getMinParamFileSize()) {
+ return null;
+ }
+
+ PathFragment paramFilePath = ParameterFile.derivePath(
+ Iterables.getFirst(outputs, null).getRootRelativePath());
+ return analysisEnvironment.getDerivedArtifact(paramFilePath, configuration.getBinDirectory());
+ }
+
+ /**
+ * Creates a command line using an external params file.
+ *
+ * <p>Call this with the result of {@link #getParamsFile} if it is not null.
+ *
+ * @param executableArgs leading arguments that should never be wrapped in a parameter file
+ * @param arguments arguments to the command (in addition to executableArgs), OR
+ * @param commandLine a {@link CommandLine} that provides the arguments (in addition to
+ * executableArgs)
+ * @param isShellCommand true if this is a shell command
+ * @param owner owner of the action
+ * @param paramFileInfo parameter file information
+ */
+ public static CommandLine createWithParamsFile(
+ List<String> executableArgs,
+ @Nullable Iterable<String> arguments,
+ @Nullable CommandLine commandLine,
+ boolean isShellCommand,
+ ActionOwner owner,
+ List<Action> requiredActions,
+ ParamFileInfo paramFileInfo,
+ Artifact parameterFile) {
+ Preconditions.checkNotNull(parameterFile);
+ if (commandLine != null && arguments != null && !Iterables.isEmpty(arguments)) {
+ throw new IllegalStateException("must provide either commandLine or arguments: " + arguments);
+ }
+
+ CommandLine paramFileContents =
+ (commandLine != null) ? commandLine : CommandLine.ofInternal(arguments, false);
+ requiredActions.add(new ParameterFileWriteAction(owner, parameterFile, paramFileContents,
+ paramFileInfo.getFileType(), paramFileInfo.getCharset()));
+
+ String pathWithFlag = paramFileInfo.getFlag() + parameterFile.getExecPathString();
+ Iterable<String> commandArgv = Iterables.concat(executableArgs, ImmutableList.of(pathWithFlag));
+ return CommandLine.ofInternal(commandArgv, isShellCommand);
+ }
+
+ /**
+ * Creates a command line without using a params file.
+ *
+ * <p>Call this if {@link #getParamsFile} returns null.
+ *
+ * @param executableArgs leading arguments that should never be wrapped in a parameter file
+ * @param arguments arguments to the command (in addition to executableArgs), OR
+ * @param commandLine a {@link CommandLine} that provides the arguments (in addition to
+ * executableArgs)
+ * @param isShellCommand true if this is a shell command
+ */
+ public static CommandLine createWithoutParamsFile(List<String> executableArgs,
+ Iterable<String> arguments, CommandLine commandLine, boolean isShellCommand) {
+ if (commandLine == null) {
+ Iterable<String> commandArgv = Iterables.concat(executableArgs, arguments);
+ return CommandLine.ofInternal(commandArgv, isShellCommand);
+ }
+
+ if (executableArgs.isEmpty()) {
+ return commandLine;
+ }
+
+ return CommandLine.ofMixed(ImmutableList.copyOf(executableArgs), commandLine, isShellCommand);
+ }
+
+ /**
+ * Estimates the params file size for the given arguments.
+ */
+ private static int getParamFileSize(
+ List<String> executableArgs, Iterable<String> arguments, CommandLine commandLine) {
+ Iterable<String> actualArguments = (commandLine != null) ? commandLine.arguments() : arguments;
+ return getParamFileSize(executableArgs) + getParamFileSize(actualArguments);
+ }
+
+ private static int getParamFileSize(Iterable<String> args) {
+ int size = 0;
+ for (String s : args) {
+ size += s.length() + 1;
+ }
+ return size;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/actions/ParamFileInfo.java b/src/main/java/com/google/devtools/build/lib/analysis/actions/ParamFileInfo.java
new file mode 100644
index 0000000..ae00181
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/actions/ParamFileInfo.java
@@ -0,0 +1,79 @@
+// Copyright 2014 Google Inc. 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.analysis.actions;
+
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.actions.ParameterFile.ParameterFileType;
+
+import java.nio.charset.Charset;
+import java.util.Objects;
+
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * An object that encapsulates how a params file should be constructed: what is the filetype,
+ * what charset to use and what prefix (typically "@") to use.
+ */
+@Immutable
+public final class ParamFileInfo {
+ private final ParameterFileType fileType;
+ private final Charset charset;
+ private final String flag;
+
+ public ParamFileInfo(ParameterFileType fileType, Charset charset, String flag) {
+ this.fileType = Preconditions.checkNotNull(fileType);
+ this.charset = Preconditions.checkNotNull(charset);
+ this.flag = Preconditions.checkNotNull(flag);
+ }
+
+ /**
+ * Returns the file type.
+ */
+ public ParameterFileType getFileType() {
+ return fileType;
+ }
+
+ /**
+ * Returns the charset.
+ */
+ public Charset getCharset() {
+ return charset;
+ }
+
+ /**
+ * Returns the prefix for the params filename on the command line (typically "@").
+ */
+ public String getFlag() {
+ return flag;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(charset, flag, fileType);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof ParamFileInfo)) {
+ return false;
+ }
+ ParamFileInfo other = (ParamFileInfo) obj;
+ return fileType.equals(other.fileType) && charset.equals(other.charset)
+ && flag.equals(other.flag);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/actions/ParameterFileWriteAction.java b/src/main/java/com/google/devtools/build/lib/analysis/actions/ParameterFileWriteAction.java
new file mode 100644
index 0000000..fd10e2b
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/actions/ParameterFileWriteAction.java
@@ -0,0 +1,122 @@
+// Copyright 2014 Google Inc. 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.analysis.actions;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.ActionOwner;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.Executor;
+import com.google.devtools.build.lib.actions.ParameterFile.ParameterFileType;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.util.Fingerprint;
+import com.google.devtools.build.lib.util.ShellEscaper;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.nio.charset.Charset;
+
+/**
+ * Action to write a parameter file for a {@link CommandLine}.
+ */
+public final class ParameterFileWriteAction extends AbstractFileWriteAction {
+
+ private static final String GUID = "45f678d8-e395-401e-8446-e795ccc6361f";
+
+ private final CommandLine commandLine;
+ private final ParameterFileType type;
+ private final Charset charset;
+
+ /**
+ * Creates a new instance.
+ *
+ * @param owner the action owner
+ * @param output the Artifact that will be created by executing this Action
+ * @param commandLine the contents to be written to the file
+ * @param type the type of the file
+ * @param charset the charset of the file
+ */
+ public ParameterFileWriteAction(ActionOwner owner, Artifact output, CommandLine commandLine,
+ ParameterFileType type, Charset charset) {
+ super(owner, ImmutableList.<Artifact>of(), output, false);
+ this.commandLine = commandLine;
+ this.type = type;
+ this.charset = charset;
+ }
+
+ /**
+ * Returns the list of options written to the parameter file. Don't use this
+ * method outside tests - the list is often huge, resulting in significant
+ * garbage collection overhead.
+ */
+ @VisibleForTesting
+ public Iterable<String> getContents() {
+ return commandLine.arguments();
+ }
+
+ @Override
+ public DeterministicWriter newDeterministicWriter(EventHandler eventHandler, Executor executor) {
+ return new DeterministicWriter() {
+ @Override
+ public void writeOutputFile(OutputStream out) throws IOException {
+ switch (type) {
+ case SHELL_QUOTED :
+ writeContentQuoted(out);
+ break;
+ case UNQUOTED :
+ writeContentUnquoted(out);
+ break;
+ default :
+ throw new AssertionError();
+ }
+ }
+ };
+ }
+
+ /**
+ * Writes the arguments from the list into the parameter file.
+ */
+ private void writeContentUnquoted(OutputStream outputStream) throws IOException {
+ OutputStreamWriter out = new OutputStreamWriter(outputStream, charset);
+ for (String line : commandLine.arguments()) {
+ out.write(line);
+ out.write('\n');
+ }
+ out.flush();
+ }
+
+ /**
+ * Writes the arguments from the list into the parameter file with shell
+ * quoting (if required).
+ */
+ private void writeContentQuoted(OutputStream outputStream) throws IOException {
+ OutputStreamWriter out = new OutputStreamWriter(outputStream, charset);
+ for (String line : ShellEscaper.escapeAll(commandLine.arguments())) {
+ out.write(line);
+ out.write('\n');
+ }
+ out.flush();
+ }
+
+ @Override
+ protected String computeKey() {
+ Fingerprint f = new Fingerprint();
+ f.addString(GUID);
+ f.addString(String.valueOf(makeExecutable));
+ f.addStrings(commandLine.arguments());
+ return f.hexDigestAndReset();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/actions/SpawnAction.java b/src/main/java/com/google/devtools/build/lib/analysis/actions/SpawnAction.java
new file mode 100644
index 0000000..f51c917
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/actions/SpawnAction.java
@@ -0,0 +1,874 @@
+// Copyright 2014 Google Inc. 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.analysis.actions;
+
+import static java.nio.charset.StandardCharsets.ISO_8859_1;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.devtools.build.lib.actions.AbstractAction;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.actions.ActionExecutionContext;
+import com.google.devtools.build.lib.actions.ActionExecutionException;
+import com.google.devtools.build.lib.actions.ActionInput;
+import com.google.devtools.build.lib.actions.ActionOwner;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.BaseSpawn;
+import com.google.devtools.build.lib.actions.ExecException;
+import com.google.devtools.build.lib.actions.Executor;
+import com.google.devtools.build.lib.actions.ParameterFile.ParameterFileType;
+import com.google.devtools.build.lib.actions.ResourceSet;
+import com.google.devtools.build.lib.actions.Spawn;
+import com.google.devtools.build.lib.actions.SpawnActionContext;
+import com.google.devtools.build.lib.actions.extra.ExtraActionInfo;
+import com.google.devtools.build.lib.actions.extra.SpawnInfo;
+import com.google.devtools.build.lib.analysis.AnalysisEnvironment;
+import com.google.devtools.build.lib.analysis.FilesToRunProvider;
+import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.collect.CollectionUtils;
+import com.google.devtools.build.lib.collect.IterablesChain;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.util.Fingerprint;
+import com.google.devtools.build.lib.util.ShellEscaper;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.protobuf.GeneratedMessage.GeneratedExtension;
+
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.CheckReturnValue;
+
+/**
+ * An Action representing an arbitrary subprocess to be forked and exec'd.
+ */
+public class SpawnAction extends AbstractAction {
+ private static class ExtraActionInfoSupplier<T> {
+ private final GeneratedExtension<ExtraActionInfo, T> extension;
+ private final T value;
+
+ private ExtraActionInfoSupplier(
+ GeneratedExtension<ExtraActionInfo, T> extension,
+ T value) {
+ this.extension = extension;
+ this.value = value;
+ }
+
+ void extend(ExtraActionInfo.Builder builder) {
+ builder.setExtension(extension, value);
+ }
+ }
+
+ private static final String GUID = "ebd6fce3-093e-45ee-adb6-bf513b602f0d";
+
+ private final CommandLine argv;
+
+ private final String progressMessage;
+ private final String mnemonic;
+ // entries are (directory for remote execution, Artifact)
+ private final Map<PathFragment, Artifact> inputManifests;
+
+ private final ResourceSet resourceSet;
+ private final ImmutableMap<String, String> environment;
+ private final ImmutableMap<String, String> executionInfo;
+
+ private final ExtraActionInfoSupplier<?> extraActionInfoSupplier;
+
+ /**
+ * Constructs a SpawnAction using direct initialization arguments.
+ * <p>
+ * All collections provided must not be subsequently modified.
+ *
+ * @param owner the owner of the Action.
+ * @param inputs the set of all files potentially read by this action; must
+ * not be subsequently modified.
+ * @param outputs the set of all files written by this action; must not be
+ * subsequently modified.
+ * @param resourceSet the resources consumed by executing this Action
+ * @param environment the map of environment variables.
+ * @param argv the command line to execute. This is merely a list of options
+ * to the executable, and is uninterpreted by the build tool for the
+ * purposes of dependency checking; typically it may include the names
+ * of input and output files, but this is not necessary.
+ * @param progressMessage the message printed during the progression of the build
+ * @param mnemonic the mnemonic that is reported in the master log.
+ */
+ public SpawnAction(ActionOwner owner,
+ Iterable<Artifact> inputs, Iterable<Artifact> outputs,
+ ResourceSet resourceSet,
+ CommandLine argv,
+ Map<String, String> environment,
+ String progressMessage,
+ String mnemonic) {
+ this(owner, inputs, outputs,
+ resourceSet, argv, ImmutableMap.copyOf(environment),
+ ImmutableMap.<String, String>of(), progressMessage,
+ ImmutableMap.<PathFragment, Artifact>of(), mnemonic, null);
+ }
+
+ /**
+ * Constructs a SpawnAction using direct initialization arguments.
+ *
+ * <p>All collections provided must not be subsequently modified.
+ *
+ * @param owner the owner of the Action.
+ * @param inputs the set of all files potentially read by this action; must
+ * not be subsequently modified.
+ * @param outputs the set of all files written by this action; must not be
+ * subsequently modified.
+ * @param resourceSet the resources consumed by executing this Action
+ * @param environment the map of environment variables.
+ * @param executionInfo out-of-band information for scheduling the spawn.
+ * @param argv the argv array (including argv[0]) of arguments to pass. This
+ * is merely a list of options to the executable, and is uninterpreted
+ * by the build tool for the purposes of dependency checking; typically
+ * it may include the names of input and output files, but this is not
+ * necessary.
+ * @param progressMessage the message printed during the progression of the build
+ * @param inputManifests entries in inputs that are symlink manifest files.
+ * These are passed to remote execution in the environment rather than as inputs.
+ * @param mnemonic the mnemonic that is reported in the master log.
+ */
+ public SpawnAction(ActionOwner owner,
+ Iterable<Artifact> inputs, Iterable<Artifact> outputs,
+ ResourceSet resourceSet,
+ CommandLine argv,
+ ImmutableMap<String, String> environment,
+ ImmutableMap<String, String> executionInfo,
+ String progressMessage,
+ ImmutableMap<PathFragment, Artifact> inputManifests,
+ String mnemonic,
+ ExtraActionInfoSupplier<?> extraActionInfoSupplier) {
+ super(owner, inputs, outputs);
+ this.resourceSet = resourceSet;
+ this.executionInfo = executionInfo;
+ this.environment = environment;
+ this.argv = argv;
+ this.progressMessage = progressMessage;
+ this.inputManifests = inputManifests;
+ this.mnemonic = mnemonic;
+ this.extraActionInfoSupplier = extraActionInfoSupplier;
+ }
+
+ /**
+ * Returns the (immutable) list of all arguments, including the command name, argv[0].
+ */
+ @VisibleForTesting
+ public List<String> getArguments() {
+ return ImmutableList.copyOf(argv.arguments());
+ }
+
+ /**
+ * Returns command argument, argv[0].
+ */
+ @VisibleForTesting
+ public String getCommandFilename() {
+ return Iterables.getFirst(argv.arguments(), null);
+ }
+
+ /**
+ * Returns the (immutable) list of arguments, excluding the command name,
+ * argv[0].
+ */
+ @VisibleForTesting
+ public List<String> getRemainingArguments() {
+ return ImmutableList.copyOf(Iterables.skip(argv.arguments(), 1));
+ }
+
+ @VisibleForTesting
+ public boolean isShellCommand() {
+ return argv.isShellCommand();
+ }
+
+ /**
+ * Executes the action without handling ExecException errors.
+ *
+ * <p>Called by {@link #execute}.
+ */
+ protected void internalExecute(
+ ActionExecutionContext actionExecutionContext) throws ExecException, InterruptedException {
+ getContext(actionExecutionContext.getExecutor()).exec(getSpawn(), actionExecutionContext);
+ }
+
+ @Override
+ public void execute(ActionExecutionContext actionExecutionContext)
+ throws ActionExecutionException, InterruptedException {
+ Executor executor = actionExecutionContext.getExecutor();
+ try {
+ internalExecute(actionExecutionContext);
+ } catch (ExecException e) {
+ String failMessage = progressMessage;
+ if (isShellCommand()) {
+ // The possible reasons it could fail are: shell executable not found, shell
+ // exited non-zero, or shell died from signal. The first is impossible
+ // and the second two aren't very interesting, so in the interests of
+ // keeping the noise-level down, we don't print a reason why, just the
+ // command that failed.
+ //
+ // 0=shell executable, 1=shell command switch, 2=command
+ failMessage = "error executing shell command: " + "'"
+ + truncate(Iterables.get(argv.arguments(), 2), 200) + "'";
+ }
+ throw e.toActionExecutionException(failMessage, executor.getVerboseFailures(), this);
+ }
+ }
+
+ /**
+ * Returns s, truncated to no more than maxLen characters, appending an
+ * ellipsis if truncation occurred.
+ */
+ private static String truncate(String s, int maxLen) {
+ return s.length() > maxLen
+ ? s.substring(0, maxLen - "...".length()) + "..."
+ : s;
+ }
+
+ /**
+ * Returns a Spawn that is representative of the command that this Action
+ * will execute. This function must not modify any state.
+ */
+ public Spawn getSpawn() {
+ return new ActionSpawn();
+ }
+
+ @Override
+ protected String computeKey() {
+ Fingerprint f = new Fingerprint();
+ f.addString(GUID);
+ f.addStrings(argv.arguments());
+ f.addString(getMnemonic());
+ f.addInt(inputManifests.size());
+ for (Map.Entry<PathFragment, Artifact> input : inputManifests.entrySet()) {
+ f.addString(input.getKey().getPathString() + "/");
+ f.addPath(input.getValue().getExecPath());
+ }
+ f.addStringMap(getEnvironment());
+ f.addStringMap(getExecutionInfo());
+ return f.hexDigestAndReset();
+ }
+
+ @Override
+ public String describeKey() {
+ StringBuilder message = new StringBuilder();
+ message.append(getProgressMessage());
+ message.append('\n');
+ for (Map.Entry<String, String> entry : getEnvironment().entrySet()) {
+ message.append(" Environment variable: ");
+ message.append(ShellEscaper.escapeString(entry.getKey()));
+ message.append('=');
+ message.append(ShellEscaper.escapeString(entry.getValue()));
+ message.append('\n');
+ }
+ for (String argument : ShellEscaper.escapeAll(argv.arguments())) {
+ message.append(" Argument: ");
+ message.append(argument);
+ message.append('\n');
+ }
+ return message.toString();
+ }
+
+ @Override
+ public final String getMnemonic() {
+ return mnemonic;
+ }
+
+ @Override
+ protected String getRawProgressMessage() {
+ return progressMessage;
+ }
+
+ @Override
+ public ExtraActionInfo.Builder getExtraActionInfo() {
+ ExtraActionInfo.Builder builder = super.getExtraActionInfo();
+ if (extraActionInfoSupplier == null) {
+ Spawn spawn = getSpawn();
+ SpawnInfo spawnInfo = spawn.getExtraActionInfo();
+
+ return builder
+ .setExtension(SpawnInfo.spawnInfo, spawnInfo);
+ } else {
+ extraActionInfoSupplier.extend(builder);
+ return builder;
+ }
+ }
+
+ /**
+ * Returns the environment in which to run this action.
+ */
+ public Map<String, String> getEnvironment() {
+ return environment;
+ }
+
+ /**
+ * Returns the out-of-band execution data for this action.
+ */
+ public Map<String, String> getExecutionInfo() {
+ return executionInfo;
+ }
+
+ @Override
+ public String describeStrategy(Executor executor) {
+ return getContext(executor).strategyLocality(getMnemonic(), isRemotable());
+ }
+
+ protected SpawnActionContext getContext(Executor executor) {
+ return executor.getSpawnActionContext(getMnemonic());
+ }
+
+ @Override
+ public ResourceSet estimateResourceConsumption(Executor executor) {
+ SpawnActionContext context = getContext(executor);
+ if (context.isRemotable(getMnemonic(), isRemotable())) {
+ return ResourceSet.ZERO;
+ }
+ return resourceSet;
+ }
+
+ /**
+ * Returns true if this can be run remotely.
+ */
+ public final boolean isRemotable() {
+ // TODO(bazel-team): get rid of this method.
+ return !executionInfo.containsKey("local");
+ }
+
+ /**
+ * The Spawn which this SpawnAction will execute.
+ */
+ private class ActionSpawn extends BaseSpawn {
+
+ private final List<Artifact> filesets = new ArrayList<>();
+
+ public ActionSpawn() {
+ super(ImmutableList.copyOf(argv.arguments()),
+ ImmutableMap.<String, String>of(),
+ executionInfo,
+ inputManifests,
+ SpawnAction.this,
+ resourceSet);
+ for (Artifact input : getInputs()) {
+ if (input.isFileset()) {
+ filesets.add(input);
+ }
+ }
+ }
+
+ @Override
+ public ImmutableMap<String, String> getEnvironment() {
+ return ImmutableMap.copyOf(SpawnAction.this.getEnvironment());
+ }
+
+ @Override
+ public ImmutableList<Artifact> getFilesetManifests() {
+ return ImmutableList.copyOf(filesets);
+ }
+
+ @Override
+ public Iterable<? extends ActionInput> getInputFiles() {
+ // Remove Fileset directories in inputs list. Instead, these are
+ // included as manifests in getEnvironment().
+ List<Artifact> inputs = Lists.newArrayList(getInputs());
+ inputs.removeAll(filesets);
+ inputs.removeAll(inputManifests.values());
+ return inputs;
+ // Also expand middleman artifacts.
+ }
+ }
+
+ /**
+ * Builder class to construct {@link SpawnAction} instances.
+ */
+ public static class Builder {
+
+ private final NestedSetBuilder<Artifact> inputsBuilder =
+ NestedSetBuilder.stableOrder();
+ private final List<Artifact> outputs = new ArrayList<>();
+ private final Map<PathFragment, Artifact> inputManifests = new LinkedHashMap<>();
+ private ResourceSet resourceSet = AbstractAction.DEFAULT_RESOURCE_SET;
+ private ImmutableMap<String, String> environment = ImmutableMap.of();
+ private ImmutableMap<String, String> executionInfo = ImmutableMap.of();
+ private boolean isShellCommand = false;
+ private boolean useDefaultShellEnvironment = false;
+ private PathFragment executable;
+ // executableArgs does not include the executable itself.
+ private List<String> executableArgs;
+ private final IterablesChain.Builder<String> argumentsBuilder = IterablesChain.builder();
+ private CommandLine commandLine;
+
+ private String progressMessage;
+ private ParamFileInfo paramFileInfo = null;
+ private String mnemonic = "Unknown";
+ private ExtraActionInfoSupplier<?> extraActionInfoSupplier = null;
+
+ /**
+ * Creates a SpawnAction builder.
+ */
+ public Builder() {}
+
+ /**
+ * Creates a builder that is a copy of another builder.
+ */
+ public Builder(Builder other) {
+ this.inputsBuilder.addTransitive(other.inputsBuilder.build());
+ this.outputs.addAll(other.outputs);
+ this.inputManifests.putAll(other.inputManifests);
+ this.resourceSet = other.resourceSet;
+ this.environment = other.environment;
+ this.executionInfo = other.executionInfo;
+ this.isShellCommand = other.isShellCommand;
+ this.useDefaultShellEnvironment = other.useDefaultShellEnvironment;
+ this.executable = other.executable;
+ this.executableArgs = (other.executableArgs != null)
+ ? Lists.newArrayList(other.executableArgs)
+ : null;
+ this.argumentsBuilder.add(other.argumentsBuilder.build());
+ this.commandLine = other.commandLine;
+ this.progressMessage = other.progressMessage;
+ this.paramFileInfo = other.paramFileInfo;
+ this.mnemonic = other.mnemonic;
+ }
+
+ /**
+ * Builds the SpawnAction using the passed in action configuration and returns it and all
+ * dependent actions. The first item of the returned array is always the SpawnAction itself.
+ *
+ * <p>This method makes a copy of all the collections, so it is safe to reuse the builder after
+ * this method returns.
+ *
+ * <p>This is annotated with @CheckReturnValue, which causes a compiler error when you call this
+ * method and ignore its return value. This is because some time ago, calling .build() had the
+ * side-effect of registering it with the RuleContext that was passed in to the constructor.
+ * This logic was removed, but if people don't notice and still rely on the side-effect, things
+ * may break.
+ *
+ * @return the SpawnAction and any actions required by it, with the first item always being the
+ * SpawnAction itself.
+ */
+ @CheckReturnValue
+ public Action[] build(ActionConstructionContext context) {
+ return build(context.getActionOwner(), context.getAnalysisEnvironment(),
+ context.getConfiguration());
+ }
+
+ @VisibleForTesting @CheckReturnValue
+ public Action[] build(ActionOwner owner, AnalysisEnvironment analysisEnvironment,
+ BuildConfiguration configuration) {
+ if (isShellCommand && executable == null) {
+ executable = configuration.getShExecutable();
+ }
+ Preconditions.checkNotNull(executable);
+ Preconditions.checkNotNull(executableArgs);
+
+ if (useDefaultShellEnvironment) {
+ this.environment = configuration.getDefaultShellEnvironment();
+ }
+
+ ImmutableList<String> argv = ImmutableList.<String>builder()
+ .add(executable.getPathString())
+ .addAll(executableArgs)
+ .build();
+
+ Iterable<String> arguments = argumentsBuilder.build();
+
+ Artifact paramsFile = ParamFileHelper.getParamsFile(argv, arguments, commandLine,
+ paramFileInfo, configuration, analysisEnvironment, outputs);
+
+ List<Action> actions = new ArrayList<>();
+ CommandLine actualCommandLine;
+ if (paramsFile != null) {
+ actualCommandLine = ParamFileHelper.createWithParamsFile(argv, arguments, commandLine,
+ isShellCommand, owner, actions, paramFileInfo, paramsFile);
+ } else {
+ actualCommandLine = ParamFileHelper.createWithoutParamsFile(argv, arguments, commandLine,
+ isShellCommand);
+ }
+
+ Iterable<Artifact> actualInputs = collectActualInputs(paramsFile);
+
+ actions.add(0, new SpawnAction(owner, actualInputs, ImmutableList.copyOf(outputs),
+ resourceSet, actualCommandLine, environment, executionInfo, progressMessage,
+ ImmutableMap.copyOf(inputManifests), mnemonic, extraActionInfoSupplier));
+ return actions.toArray(new Action[actions.size()]);
+ }
+
+ private Iterable<Artifact> collectActualInputs(Artifact parameterFile) {
+ if (parameterFile != null) {
+ inputsBuilder.add(parameterFile);
+ }
+ return inputsBuilder.build();
+ }
+
+ /**
+ * Adds an input to this action.
+ */
+ public Builder addInput(Artifact artifact) {
+ inputsBuilder.add(artifact);
+ return this;
+ }
+
+ /**
+ * Adds inputs to this action.
+ */
+ public Builder addInputs(Iterable<Artifact> artifacts) {
+ inputsBuilder.addAll(artifacts);
+ return this;
+ }
+
+ /**
+ * Adds transitive inputs to this action.
+ */
+ public Builder addTransitiveInputs(NestedSet<Artifact> artifacts) {
+ inputsBuilder.addTransitive(artifacts);
+ return this;
+ }
+
+ public Builder addInputManifest(Artifact artifact, PathFragment remote) {
+ inputManifests.put(remote, artifact);
+ return this;
+ }
+
+ public Builder addOutput(Artifact artifact) {
+ outputs.add(artifact);
+ return this;
+ }
+
+ public Builder addOutputs(Iterable<Artifact> artifacts) {
+ Iterables.addAll(outputs, artifacts);
+ return this;
+ }
+
+ public Builder setResources(ResourceSet resourceSet) {
+ this.resourceSet = resourceSet;
+ return this;
+ }
+
+ /**
+ * Sets the map of environment variables.
+ */
+ public Builder setEnvironment(Map<String, String> environment) {
+ this.environment = ImmutableMap.copyOf(environment);
+ this.useDefaultShellEnvironment = false;
+ return this;
+ }
+
+ /**
+ * Sets the map of execution info.
+ */
+ public Builder setExecutionInfo(Map<String, String> info) {
+ this.executionInfo = ImmutableMap.copyOf(info);
+ return this;
+ }
+
+ /**
+ * Sets the environment to the configurations default shell environment,
+ * see {@link BuildConfiguration#getDefaultShellEnvironment}.
+ */
+ public Builder useDefaultShellEnvironment() {
+ this.environment = null;
+ this.useDefaultShellEnvironment = true;
+ return this;
+ }
+
+ /**
+ * Sets the executable path; the path is interpreted relative to the
+ * execution root.
+ *
+ * <p>Calling this method overrides any previous values set via calls to
+ * {@link #setExecutable(Artifact)}, {@link #setJavaExecutable}, or
+ * {@link #setShellCommand(String)}.
+ */
+ public Builder setExecutable(PathFragment executable) {
+ this.executable = executable;
+ this.executableArgs = Lists.newArrayList();
+ this.isShellCommand = false;
+ return this;
+ }
+
+ /**
+ * Sets the executable as an artifact.
+ *
+ * <p>Calling this method overrides any previous values set via calls to
+ * {@link #setExecutable(Artifact)}, {@link #setJavaExecutable}, or
+ * {@link #setShellCommand(String)}.
+ */
+ public Builder setExecutable(Artifact executable) {
+ return setExecutable(executable.getExecPath());
+ }
+
+ /**
+ * Sets the executable as a configured target. Automatically adds the files
+ * to run to the inputs and uses the executable of the target as the
+ * executable.
+ *
+ * <p>Calling this method overrides any previous values set via calls to
+ * {@link #setExecutable(Artifact)}, {@link #setJavaExecutable}, or
+ * {@link #setShellCommand(String)}.
+ */
+ public Builder setExecutable(TransitiveInfoCollection executable) {
+ FilesToRunProvider provider = executable.getProvider(FilesToRunProvider.class);
+ Preconditions.checkArgument(provider != null);
+ return setExecutable(provider);
+ }
+
+ /**
+ * Sets the executable as a configured target. Automatically adds the files
+ * to run to the inputs and uses the executable of the target as the
+ * executable.
+ *
+ * <p>Calling this method overrides any previous values set via calls to
+ * {@link #setExecutable}, {@link #setJavaExecutable}, or
+ * {@link #setShellCommand(String)}.
+ */
+ public Builder setExecutable(FilesToRunProvider executableProvider) {
+ Preconditions.checkArgument(executableProvider.getExecutable() != null,
+ "The target does not have an executable");
+ setExecutable(executableProvider.getExecutable().getExecPath());
+ return addTool(executableProvider);
+ }
+
+ private Builder setJavaExecutable(PathFragment javaExecutable, Artifact deployJar,
+ List<String> jvmArgs, String... launchArgs) {
+ this.executable = javaExecutable;
+ this.executableArgs = Lists.newArrayList();
+ executableArgs.add("-Xverify:none");
+ executableArgs.addAll(jvmArgs);
+ for (String arg : launchArgs) {
+ executableArgs.add(arg);
+ }
+ inputsBuilder.add(deployJar);
+ this.isShellCommand = false;
+ return this;
+ }
+
+ /**
+ * Sets the executable to be a java class executed from the given deploy
+ * jar. The deploy jar is automatically added to the action inputs.
+ *
+ * <p>Calling this method overrides any previous values set via calls to
+ * {@link #setExecutable}, {@link #setJavaExecutable}, or
+ * {@link #setShellCommand(String)}.
+ */
+ public Builder setJavaExecutable(PathFragment javaExecutable,
+ Artifact deployJar, String javaMainClass, List<String> jvmArgs) {
+ return setJavaExecutable(javaExecutable, deployJar, jvmArgs, "-cp",
+ deployJar.getExecPathString(), javaMainClass);
+ }
+
+ /**
+ * Sets the executable to be a jar executed from the given deploy jar. The deploy jar is
+ * automatically added to the action inputs.
+ *
+ * <p>This method is similar to {@link #setJavaExecutable} but it assumes that the Jar artifact
+ * declares a main class.
+ *
+ * <p>Calling this method overrides any previous values set via calls to {@link #setExecutable},
+ * {@link #setJavaExecutable}, or {@link #setShellCommand(String)}.
+ */
+ public Builder setJarExecutable(PathFragment javaExecutable,
+ Artifact deployJar, List<String> jvmArgs) {
+ return setJavaExecutable(javaExecutable, deployJar, jvmArgs, "-jar",
+ deployJar.getExecPathString());
+ }
+
+ /**
+ * Sets the executable to be the shell and adds the given command as the
+ * command to be executed.
+ *
+ * <p>Note that this will not clear the arguments, so any arguments will
+ * be passed in addition to the command given here.
+ *
+ * <p>Calling this method overrides any previous values set via calls to
+ * {@link #setExecutable(Artifact)}, {@link #setJavaExecutable}, or
+ * {@link #setShellCommand(String)}.
+ */
+ public Builder setShellCommand(String command) {
+ this.executable = null;
+ // 0=shell command switch, 1=command
+ this.executableArgs = Lists.newArrayList("-c", command);
+ this.isShellCommand = true;
+ return this;
+ }
+
+ /**
+ * Sets the executable to be the shell and adds the given interned commands as the
+ * commands to be executed.
+ */
+ public Builder setShellCommand(Iterable<String> command) {
+ this.executable = new PathFragment(Iterables.getFirst(command, null));
+ // The first item of the commands is the shell executable that should be used.
+ this.executableArgs = ImmutableList.copyOf(Iterables.skip(command, 1));
+ this.isShellCommand = true;
+ return this;
+ }
+
+ /**
+ * Adds an executable and its runfiles, so it can be called from a shell command.
+ */
+ public Builder addTool(FilesToRunProvider tool) {
+ addInputs(tool.getFilesToRun());
+ if (tool.getRunfilesManifest() != null) {
+ addInputManifest(tool.getRunfilesManifest(),
+ BaseSpawn.runfilesForFragment(tool.getExecutable().getExecPath()));
+ }
+ return this;
+ }
+
+ /**
+ * Appends the arguments to the list of executable arguments.
+ */
+ public Builder addExecutableArguments(String... arguments) {
+ Preconditions.checkState(executableArgs != null);
+ executableArgs.addAll(Arrays.asList(arguments));
+ return this;
+ }
+
+ /**
+ * Add multiple arguments in the order they are returned by the collection
+ * to the list of executable arguments.
+ */
+ public Builder addExecutableArguments(Iterable<String> arguments) {
+ Preconditions.checkState(executableArgs != null);
+ Iterables.addAll(executableArgs, arguments);
+ return this;
+ }
+
+ /**
+ * Appends the argument to the list of command-line arguments.
+ */
+ public Builder addArgument(String argument) {
+ Preconditions.checkState(commandLine == null);
+ argumentsBuilder.addElement(argument);
+ return this;
+ }
+
+ /**
+ * Appends the arguments to the list of command-line arguments.
+ */
+ public Builder addArguments(String... arguments) {
+ Preconditions.checkState(commandLine == null);
+ argumentsBuilder.add(ImmutableList.copyOf(arguments));
+ return this;
+ }
+
+ /**
+ * Add multiple arguments in the order they are returned by the collection.
+ */
+ public Builder addArguments(Iterable<String> arguments) {
+ Preconditions.checkState(commandLine == null);
+ argumentsBuilder.add(CollectionUtils.makeImmutable(arguments));
+ return this;
+ }
+
+ /**
+ * Appends the argument both to the inputs and to the list of command-line
+ * arguments.
+ */
+ public Builder addInputArgument(Artifact argument) {
+ Preconditions.checkState(commandLine == null);
+ addInput(argument);
+ addArgument(argument.getExecPathString());
+ return this;
+ }
+
+ /**
+ * Appends the arguments both to the inputs and to the list of command-line
+ * arguments.
+ */
+ public Builder addInputArguments(Iterable<Artifact> arguments) {
+ for (Artifact argument : arguments) {
+ addInputArgument(argument);
+ }
+ return this;
+ }
+
+ /**
+ * Appends the argument both to the ouputs and to the list of command-line
+ * arguments.
+ */
+ public Builder addOutputArgument(Artifact argument) {
+ Preconditions.checkState(commandLine == null);
+ outputs.add(argument);
+ argumentsBuilder.addElement(argument.getExecPathString());
+ return this;
+ }
+
+ /**
+ * Sets a delegate to compute the command line at a later time. This method
+ * cannot be used in conjunction with the {@link #addArgument} or {@link
+ * #addArguments} methods.
+ *
+ * <p>The main intention of this method is to save memory by allowing
+ * client-controlled sharing between actions and configured targets.
+ * Objects passed to this method MUST be immutable.
+ */
+ public Builder setCommandLine(CommandLine commandLine) {
+ Preconditions.checkState(argumentsBuilder.isEmpty());
+ this.commandLine = commandLine;
+ return this;
+ }
+
+ public Builder setProgressMessage(String progressMessage) {
+ this.progressMessage = progressMessage;
+ return this;
+ }
+
+ public Builder setMnemonic(String mnemonic) {
+ Preconditions.checkArgument(
+ !mnemonic.isEmpty() && CharMatcher.JAVA_LETTER_OR_DIGIT.matchesAllOf(mnemonic),
+ "mnemonic must only contain letters and/or digits, and have non-zero length, was: \"%s\"",
+ mnemonic);
+ this.mnemonic = mnemonic;
+ return this;
+ }
+
+ public <T> Builder setExtraActionInfo(
+ GeneratedExtension<ExtraActionInfo, T> extension, T value) {
+ this.extraActionInfoSupplier = new ExtraActionInfoSupplier<T>(extension, value);
+ return this;
+ }
+
+ /**
+ * Enable use of a parameter file and set the encoding to ISO-8859-1 (latin1).
+ *
+ * <p>In order to use parameter files, at least one output artifact must be specified.
+ */
+ public Builder useParameterFile(ParameterFileType parameterFileType) {
+ return useParameterFile(parameterFileType, ISO_8859_1, "@");
+ }
+
+ /**
+ * Enable or disable the use of a parameter file, set the encoding to the given value, and
+ * specify the argument prefix to use in passing the parameter file name to the tool.
+ *
+ * <p>The default argument prefix is "@". In order to use parameter files, at least one output
+ * artifact must be specified.
+ */
+ public Builder useParameterFile(
+ ParameterFileType parameterFileType, Charset charset, String flagPrefix) {
+ paramFileInfo = new ParamFileInfo(parameterFileType, charset, flagPrefix);
+ return this;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/actions/SymlinkAction.java b/src/main/java/com/google/devtools/build/lib/analysis/actions/SymlinkAction.java
new file mode 100644
index 0000000..2bbb3e2
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/actions/SymlinkAction.java
@@ -0,0 +1,130 @@
+// Copyright 2014 Google Inc. 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.analysis.actions;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.AbstractAction;
+import com.google.devtools.build.lib.actions.ActionExecutionContext;
+import com.google.devtools.build.lib.actions.ActionExecutionException;
+import com.google.devtools.build.lib.actions.ActionOwner;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.Executor;
+import com.google.devtools.build.lib.actions.ResourceSet;
+import com.google.devtools.build.lib.util.Fingerprint;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.io.IOException;
+
+/**
+ * Action to create a symbolic link.
+ */
+public class SymlinkAction extends AbstractAction {
+
+ private static final String GUID = "349675b5-437c-4da8-891a-7fb98fba6ab5";
+
+ private final PathFragment inputPath;
+ private final Artifact output;
+ private final String progressMessage;
+
+ /**
+ * Creates a new SymlinkAction instance.
+ *
+ * @param owner the action owner.
+ * @param input the Artifact that will be the src of the symbolic link.
+ * @param output the Artifact that will be created by executing this Action.
+ * @param progressMessage the progress message.
+ */
+ public SymlinkAction(ActionOwner owner, Artifact input, Artifact output,
+ String progressMessage) {
+ // These actions typically have only one input and one output, which
+ // become the sole and primary in their respective lists.
+ this(owner, input.getExecPath(), input, output, progressMessage);
+ }
+
+ /**
+ * Creates a new SymlinkAction instance, where the inputPath
+ * may be different than that input artifact's path. This is
+ * only useful when dealing with runfiles trees where
+ * link target is a directory.
+ *
+ * @param owner the action owner.
+ * @param inputPath the Path that will be the src of the symbolic link.
+ * @param input the Artifact that is required to build the inputPath.
+ * @param output the Artifact that will be created by executing this Action.
+ * @param progressMessage the progress message.
+ */
+ public SymlinkAction(ActionOwner owner, PathFragment inputPath, Artifact input,
+ Artifact output, String progressMessage) {
+ super(owner, ImmutableList.of(input), ImmutableList.of(output));
+ this.inputPath = Preconditions.checkNotNull(inputPath);
+ this.output = Preconditions.checkNotNull(output);
+ this.progressMessage = progressMessage;
+ }
+
+ public PathFragment getInputPath() {
+ return inputPath;
+ }
+
+ public Path getOutputPath() {
+ return output.getPath();
+ }
+
+ @Override
+ public void execute(ActionExecutionContext actionExecutionContext)
+ throws ActionExecutionException {
+ try {
+ getOutputPath().createSymbolicLink(
+ actionExecutionContext.getExecutor().getExecRoot().getRelative(inputPath));
+ } catch (IOException e) {
+ throw new ActionExecutionException("failed to create symbolic link '"
+ + Iterables.getOnlyElement(getOutputs()).prettyPrint()
+ + "' to the '" + Iterables.getOnlyElement(getInputs()).prettyPrint()
+ + "' due to I/O error: " + e.getMessage(), e, this, false);
+ }
+ }
+
+ @Override
+ public ResourceSet estimateResourceConsumption(Executor executor) {
+ return new ResourceSet(/*memoryMb=*/0, /*cpuUsage=*/0, /*ioUsage=*/0.0);
+ }
+
+ @Override
+ protected String computeKey() {
+ Fingerprint f = new Fingerprint();
+ f.addString(GUID);
+ // We don't normally need to add inputs to the key. In this case, however, the inputPath can be
+ // different from the actual input artifact.
+ f.addPath(inputPath);
+ return f.hexDigestAndReset();
+ }
+
+ @Override
+ public String getMnemonic() {
+ return "Symlink";
+ }
+
+ @Override
+ protected String getRawProgressMessage() {
+ return progressMessage;
+ }
+
+ @Override
+ public String describeStrategy(Executor executor) {
+ return "local";
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/actions/TemplateExpansionAction.java b/src/main/java/com/google/devtools/build/lib/analysis/actions/TemplateExpansionAction.java
new file mode 100644
index 0000000..b2c83fb
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/actions/TemplateExpansionAction.java
@@ -0,0 +1,335 @@
+// Copyright 2014 Google Inc. 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.analysis.actions;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.ActionOwner;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.Executor;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.util.Fingerprint;
+import com.google.devtools.build.lib.util.ResourceFileLoader;
+import com.google.devtools.build.lib.util.StringUtilities;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
+import com.google.devtools.build.lib.vfs.Path;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Action to expand a template and write the expanded content to a file.
+ */
+public class TemplateExpansionAction extends AbstractFileWriteAction {
+
+ private static final String GUID = "786c1fe0-dca8-407a-b108-e1ecd6d1bc7f";
+
+ /**
+ * A pair of a string to be substituted and a string to substitute it with.
+ * For simplicity, these are called key and value. All implementations must
+ * be immutable, and always return the identical key. The returned values
+ * must be the same, though they need not be the same object.
+ *
+ * <p>It should be assumed that the {@link #getKey} invocation is cheap, and
+ * that the {@link #getValue} invocation is expensive.
+ */
+ public abstract static class Substitution {
+ private Substitution() {
+ }
+
+ public abstract String getKey();
+ public abstract String getValue();
+
+ /**
+ * Returns an immutable Substitution instance for the given key and value.
+ */
+ public static Substitution of(final String key, final String value) {
+ return new Substitution() {
+ @Override
+ public String getKey() {
+ return key;
+ }
+
+ @Override
+ public String getValue() {
+ return value;
+ }
+ };
+ }
+
+ /**
+ * Returns an immutable Substitution instance for the key and list of values. The
+ * values will be joined by spaces before substitution.
+ */
+ public static Substitution ofSpaceSeparatedList(final String key, final List<?> value) {
+ return new Substitution() {
+ @Override
+ public String getKey() {
+ return key;
+ }
+
+ @Override
+ public String getValue() {
+ return Joiner.on(" ").join(value);
+ }
+ };
+ }
+ }
+
+ /**
+ * A substitution with a fixed key, and a computed value. The computed value
+ * must not change over the lifetime of an instance, though the {@link
+ * #getValue} method may return different String objects.
+ *
+ * <p>It should be assumed that the {@link #getKey} invocation is cheap, and
+ * that the {@link #getValue} invocation is expensive.
+ */
+ public abstract static class ComputedSubstitution extends Substitution {
+ private final String key;
+
+ public ComputedSubstitution(String key) {
+ this.key = key;
+ }
+
+ @Override
+ public String getKey() {
+ return key;
+ }
+ }
+
+ /**
+ * A template that contains text content, or alternatively throws an {@link
+ * IOException}.
+ */
+ public abstract static class Template {
+
+ /**
+ * We only allow subclasses in this file.
+ */
+ private Template() {
+ }
+
+ /**
+ * Returns the text content of the template.
+ */
+ protected abstract String getContent() throws IOException;
+
+ /**
+ * Returns a string that is used for the action key. This must change if
+ * the getContent method returns something different, but is not allowed to
+ * throw an exception.
+ */
+ protected abstract String getKey();
+
+ /**
+ * Loads a template from the given resource. The resource is looked up
+ * relative to the given class. If the resource cannot be loaded, the returned
+ * template throws an {@link IOException} when {@link #getContent} is
+ * called. This makes it safe to use this method in a constant initializer.
+ */
+ public static Template forResource(final Class<?> relativeToClass, final String templateName) {
+ try {
+ String content = ResourceFileLoader.loadResource(relativeToClass, templateName);
+ return forString(content);
+ } catch (final IOException e) {
+ return new Template() {
+ @Override
+ protected String getContent() throws IOException {
+ throw new IOException("failed to load resource file '" + templateName
+ + "' due to I/O error: " + e.getMessage(), e);
+ }
+
+ @Override
+ protected String getKey() {
+ return "ERROR: " + e.getMessage();
+ }
+ };
+ }
+ }
+
+ /**
+ * Returns a template for the given text string.
+ */
+ public static Template forString(final String templateText) {
+ return new Template() {
+ @Override
+ protected String getContent() {
+ return templateText;
+ }
+
+ @Override
+ protected String getKey() {
+ return templateText;
+ }
+ };
+ }
+
+ /**
+ * Returns a template that loads the given artifact. It is important that
+ * the artifact is also an input for the action, or this won't work.
+ * Therefore this method is private, and you should use the corresponding
+ * {@link TemplateExpansionAction} constructor.
+ */
+ private static Template forArtifact(final Artifact templateArtifact) {
+ return new Template() {
+ @Override
+ protected String getContent() throws IOException {
+ Path templatePath = templateArtifact.getPath();
+ try {
+ return new String(FileSystemUtils.readContentAsLatin1(templatePath));
+ } catch (IOException e) {
+ throw new IOException("failed to load template file '" + templatePath.getPathString()
+ + "' due to I/O error: " + e.getMessage(), e);
+ }
+ }
+
+ @Override
+ protected String getKey() {
+ // This isn't strictly necessary, because the action inputs are automatically considered.
+ return "ARTIFACT: " + templateArtifact.getExecPathString();
+ }
+ };
+ }
+ }
+
+ private final Template template;
+ private final List<Substitution> substitutions;
+
+ /**
+ * Creates a new TemplateExpansionAction instance.
+ *
+ * @param owner the action owner.
+ * @param inputs the Artifacts that this Action depends on
+ * @param output the Artifact that will be created by executing this Action.
+ * @param template the template that will be expanded by this Action.
+ * @param substitutions the substitutions that will be applied to the
+ * template. All substitutions will be applied in order.
+ * @param makeExecutable iff true will change the output file to be
+ * executable.
+ */
+ private TemplateExpansionAction(ActionOwner owner,
+ Collection<Artifact> inputs,
+ Artifact output,
+ Template template,
+ List<Substitution> substitutions,
+ boolean makeExecutable) {
+ super(owner, inputs, output, makeExecutable);
+ this.template = template;
+ this.substitutions = ImmutableList.copyOf(substitutions);
+ }
+
+ /**
+ * Creates a new TemplateExpansionAction instance for an artifact template.
+ *
+ * @param owner the action owner.
+ * @param templateArtifact the Artifact that will be read as the text template
+ * file
+ * @param output the Artifact that will be created by executing this Action.
+ * @param substitutions the substitutions that will be applied to the
+ * template. All substitutions will be applied in order.
+ * @param makeExecutable iff true will change the output file to be
+ * executable.
+ */
+ public TemplateExpansionAction(ActionOwner owner,
+ Artifact templateArtifact,
+ Artifact output,
+ List<Substitution> substitutions,
+ boolean makeExecutable) {
+ this(owner, ImmutableList.of(templateArtifact), output, Template.forArtifact(templateArtifact),
+ substitutions, makeExecutable);
+ }
+
+ /**
+ * Creates a new TemplateExpansionAction instance without inputs.
+ *
+ * @param owner the action owner.
+ * @param output the Artifact that will be created by executing this Action.
+ * @param template the template
+ * @param substitutions the substitutions that will be applied to the
+ * template. All substitutions will be applied in order.
+ * @param makeExecutable iff true will change the output file to be
+ * executable.
+ */
+ public TemplateExpansionAction(ActionOwner owner,
+ Artifact output,
+ Template template,
+ List<Substitution> substitutions,
+ boolean makeExecutable) {
+ this(owner, Artifact.NO_ARTIFACTS, output, template, substitutions, makeExecutable);
+ }
+
+ /**
+ * Expands the template by applying all substitutions.
+ * @param template
+ * @return the expanded text.
+ */
+ private String expandTemplate(String template) {
+ for (Substitution entry : substitutions) {
+ template = StringUtilities.replaceAllLiteral(template, entry.getKey(), entry.getValue());
+ }
+ return template;
+ }
+
+ @VisibleForTesting
+ public String getFileContents() throws IOException {
+ return expandTemplate(template.getContent());
+ }
+
+ @Override
+ public DeterministicWriter newDeterministicWriter(EventHandler eventHandler,
+ Executor executor) throws IOException {
+ final byte[] bytes = getFileContents().getBytes(UTF_8);
+ return new DeterministicWriter() {
+ @Override
+ public void writeOutputFile(OutputStream out) throws IOException {
+ out.write(bytes);
+ }
+ };
+ }
+
+ @Override
+ protected String computeKey() {
+ Fingerprint f = new Fingerprint();
+ f.addString(GUID);
+ f.addString(String.valueOf(makeExecutable));
+ f.addString(template.getKey());
+ f.addInt(substitutions.size());
+ for (Substitution entry : substitutions) {
+ f.addString(entry.getKey());
+ f.addString(entry.getValue());
+ }
+ return f.hexDigestAndReset();
+ }
+
+ @Override
+ public String getMnemonic() {
+ return "TemplateExpand";
+ }
+
+ @Override
+ protected String getRawProgressMessage() {
+ return "Expanding template " + Iterables.getOnlyElement(getOutputs()).prettyPrint();
+ }
+
+ public List<Substitution> getSubstitutions() {
+ return substitutions;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/buildinfo/BuildInfoCollection.java b/src/main/java/com/google/devtools/build/lib/analysis/buildinfo/BuildInfoCollection.java
new file mode 100644
index 0000000..54067a0
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/buildinfo/BuildInfoCollection.java
@@ -0,0 +1,48 @@
+// Copyright 2014 Google Inc. 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.analysis.buildinfo;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.actions.Artifact;
+
+import java.util.List;
+
+/**
+ * A collection of build-info files for both stamped and unstamped modes.
+ */
+public final class BuildInfoCollection {
+ private final ImmutableList<Action> actions;
+ private final ImmutableList<Artifact> stampedBuildInfo;
+ private final ImmutableList<Artifact> redactedBuildInfo;
+
+ public BuildInfoCollection(List<? extends Action> actions, List<Artifact> stampedBuildInfo,
+ List<Artifact> redactedBuildInfo) {
+ this.actions = ImmutableList.copyOf(actions);
+ this.stampedBuildInfo = ImmutableList.copyOf(stampedBuildInfo);
+ this.redactedBuildInfo = ImmutableList.copyOf(redactedBuildInfo);
+ }
+
+ public ImmutableList<Action> getActions() {
+ return actions;
+ }
+
+ public ImmutableList<Artifact> getStampedBuildInfo() {
+ return stampedBuildInfo;
+ }
+
+ public ImmutableList<Artifact> getRedactedBuildInfo() {
+ return redactedBuildInfo;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/buildinfo/BuildInfoFactory.java b/src/main/java/com/google/devtools/build/lib/analysis/buildinfo/BuildInfoFactory.java
new file mode 100644
index 0000000..c6ec4d7
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/buildinfo/BuildInfoFactory.java
@@ -0,0 +1,99 @@
+// Copyright 2014 Google Inc. 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.analysis.buildinfo;
+
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.Root;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.io.Serializable;
+
+/**
+ * A factory for language-specific build-info files. Use this to translate the build-info into
+ * target-independent language-specific files. The generated actions are registered into the action
+ * graph on every build, but only executed if anything depends on them.
+ */
+public interface BuildInfoFactory extends Serializable {
+ /**
+ * Type of the build-data artifact.
+ */
+ public enum BuildInfoType {
+ /**
+ * Ignore changes to this file for the purposes of determining whether an action needs to be
+ * re-executed. I.e., the action is only re-executed if at least one other input has changed.
+ */
+ NO_REBUILD,
+
+ /**
+ * Changes to this file trigger re-execution of actions, similar to source file changes.
+ */
+ FORCE_REBUILD_IF_CHANGED;
+ }
+
+ /**
+ * Context for the creation of build-info artifacts.
+ */
+ public interface BuildInfoContext {
+ Artifact getBuildInfoArtifact(PathFragment rootRelativePath, Root root, BuildInfoType type);
+ Root getBuildDataDirectory();
+ }
+
+ /**
+ * Build-info key for lookup from the {@link
+ * com.google.devtools.build.lib.analysis.AnalysisEnvironment}.
+ */
+ public static final class BuildInfoKey implements Serializable {
+ private final String name;
+
+ public BuildInfoKey(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof BuildInfoKey)) {
+ return false;
+ }
+ return name.equals(((BuildInfoKey) o).name);
+ }
+
+ @Override
+ public int hashCode() {
+ return name.hashCode();
+ }
+ }
+
+ /**
+ * Create actions and artifacts for language-specific build-info files.
+ */
+ BuildInfoCollection create(BuildInfoContext context, BuildConfiguration config,
+ Artifact buildInfo, Artifact buildChangelist);
+
+ /**
+ * Returns the key for the information created by this factory.
+ */
+ BuildInfoKey getKey();
+
+ /**
+ * Returns false if this build info factory is disabled based on the configuration (usually by
+ * checking if all required configuration fragments are present).
+ */
+ boolean isEnabled(BuildConfiguration config);
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/config/BinTools.java b/src/main/java/com/google/devtools/build/lib/analysis/config/BinTools.java
new file mode 100644
index 0000000..6d2477d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/config/BinTools.java
@@ -0,0 +1,191 @@
+// Copyright 2014 Google Inc. 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.analysis.config;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.ArtifactFactory;
+import com.google.devtools.build.lib.actions.EnvironmentalExecException;
+import com.google.devtools.build.lib.actions.ExecException;
+import com.google.devtools.build.lib.analysis.BlazeDirectories;
+import com.google.devtools.build.lib.vfs.Dirent;
+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 com.google.devtools.build.lib.vfs.Symlinks;
+
+import java.io.IOException;
+
+/**
+ * Initializes the <execRoot>/_bin/ directory that contains auxiliary tools used during action
+ * execution (alarm, etc). The main purpose of this is to make sure that those tools are accessible
+ * using relative paths from the execution root.
+ */
+public final class BinTools {
+ private final BlazeDirectories directories;
+ private final Path binDir; // the working bin directory under execRoot
+ private final ImmutableList<String> embeddedTools;
+
+ private BinTools(BlazeDirectories directories, ImmutableList<String> tools) {
+ this.directories = directories;
+ this.binDir = directories.getExecRoot().getRelative("_bin");
+ this.embeddedTools = tools;
+ }
+
+ /**
+ * Creates an instance with the list of embedded tools obtained from scanning the directory
+ * into which said binaries were extracted by the launcher.
+ */
+ public static BinTools forProduction(BlazeDirectories directories) throws IOException {
+ ImmutableList.Builder<String> builder = ImmutableList.builder();
+ scanDirectoryRecursively(builder, directories.getEmbeddedBinariesRoot(), "");
+ return new BinTools(directories, builder.build());
+ }
+
+ /**
+ * Creates an empty instance for testing.
+ */
+ @VisibleForTesting
+ public static BinTools empty(BlazeDirectories directories) {
+ return new BinTools(directories, ImmutableList.<String>of());
+ }
+
+ /**
+ * Creates an instance for testing without actually symlinking the tools.
+ *
+ * <p>Used for tests that need a set of embedded tools to be present, but not the actual files.
+ */
+ @VisibleForTesting
+ public static BinTools forUnitTesting(BlazeDirectories directories, Iterable<String> tools) {
+ return new BinTools(directories, ImmutableList.copyOf(tools));
+ }
+
+ /**
+ * Populates the _bin directory by symlinking the necessary files from the given
+ * srcDir, and returns the corresponding BinTools.
+ */
+ @VisibleForTesting
+ public static BinTools forIntegrationTesting(
+ BlazeDirectories directories, String srcDir, Iterable<String> tools)
+ throws IOException {
+ Path srcPath = directories.getOutputBase().getFileSystem().getPath(srcDir);
+ for (String embedded : tools) {
+ Path runfilesPath = srcPath.getRelative(embedded);
+ if (!runfilesPath.isFile()) {
+ // The file isn't there - nothing to symlink!
+ //
+ // Note: This path is usually taken by the tests using the in-memory
+ // file system. They can't run the embedded scripts anyhow, so there isn't
+ // much point in creating a symlink to a non-existent binary here.
+ continue;
+ }
+ Path outputPath = directories.getExecRoot().getChild("_bin").getChild(embedded);
+ if (outputPath.exists()) {
+ outputPath.delete();
+ }
+ FileSystemUtils.createDirectoryAndParents(outputPath.getParentDirectory());
+ outputPath.createSymbolicLink(runfilesPath);
+ }
+
+ return new BinTools(directories, ImmutableList.copyOf(tools));
+ }
+
+ private static void scanDirectoryRecursively(
+ ImmutableList.Builder<String> result, Path root, String relative) throws IOException {
+ for (Dirent dirent : root.readdir(Symlinks.NOFOLLOW)) {
+ String childRelative = relative.isEmpty()
+ ? dirent.getName()
+ : relative + "/" + dirent.getName();
+ switch (dirent.getType()) {
+ case FILE:
+ result.add(childRelative);
+ break;
+
+ case DIRECTORY:
+ scanDirectoryRecursively(result, root.getChild(dirent.getName()), childRelative);
+ break;
+
+ default:
+ // Nothing to do here -- we ignore symlinks, since they should not be present in the
+ // embedded binaries tree.
+ break;
+ }
+ }
+ }
+
+ public PathFragment getExecPath(String embedPath) {
+ Preconditions.checkState(embeddedTools.contains(embedPath), "%s not in %s", embedPath,
+ embeddedTools);
+ return new PathFragment("_bin").getRelative(new PathFragment(embedPath).getBaseName());
+ }
+
+ public Artifact getEmbeddedArtifact(String embedPath, ArtifactFactory artifactFactory) {
+ return artifactFactory.getDerivedArtifact(getExecPath(embedPath));
+ }
+
+ public ImmutableList<Artifact> getAllEmbeddedArtifacts(ArtifactFactory artifactFactory) {
+ ImmutableList.Builder<Artifact> builder = ImmutableList.builder();
+ for (String embeddedTool : embeddedTools) {
+ builder.add(getEmbeddedArtifact(embeddedTool, artifactFactory));
+ }
+ return builder.build();
+ }
+
+ /**
+ * Initializes the build tools not available at absolute paths. Note that
+ * these must be constant across all configurations.
+ */
+ public void setupBuildTools() throws ExecException {
+ try {
+ FileSystemUtils.createDirectoryAndParents(binDir);
+ } catch (IOException e) {
+ throw new EnvironmentalExecException("could not create directory '" + binDir + "'", e);
+ }
+
+ for (String embeddedPath : embeddedTools) {
+ setupTool(embeddedPath);
+ }
+ }
+
+ private void setupTool(String embeddedPath) throws ExecException {
+ Path sourcePath = directories.getEmbeddedBinariesRoot().getRelative(embeddedPath);
+ Path linkPath = binDir.getRelative(new PathFragment(embeddedPath).getBaseName());
+ linkTool(sourcePath, linkPath);
+ }
+
+ private void linkTool(Path sourcePath, Path linkPath) throws ExecException {
+ if (linkPath.getFileSystem().supportsSymbolicLinks()) {
+ try {
+ if (!linkPath.isSymbolicLink()) {
+ // ensureSymbolicLink() does not handle the case where there is already
+ // a file with the same name, so we need to handle it here.
+ linkPath.delete();
+ }
+ FileSystemUtils.ensureSymbolicLink(linkPath, sourcePath);
+ } catch (IOException e) {
+ throw new EnvironmentalExecException("failed to link '" + sourcePath + "'", e);
+ }
+ } else {
+ // For file systems that do not support linking, copy.
+ try {
+ FileSystemUtils.copyTool(sourcePath, linkPath);
+ } catch (IOException e) {
+ throw new EnvironmentalExecException("failed to copy '" + sourcePath + "'" , e);
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/config/BuildConfiguration.java b/src/main/java/com/google/devtools/build/lib/analysis/config/BuildConfiguration.java
new file mode 100644
index 0000000..8e80211
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/config/BuildConfiguration.java
@@ -0,0 +1,1944 @@
+// Copyright 2014 Google Inc. 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.analysis.config;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicates;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+import com.google.devtools.build.lib.actions.ArtifactFactory;
+import com.google.devtools.build.lib.actions.PackageRootResolver;
+import com.google.devtools.build.lib.actions.Root;
+import com.google.devtools.build.lib.analysis.BlazeDirectories;
+import com.google.devtools.build.lib.analysis.ViewCreationFailedException;
+import com.google.devtools.build.lib.analysis.config.BuildConfigurationCollection.Transitions;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.packages.Attribute;
+import com.google.devtools.build.lib.packages.Attribute.Configurator;
+import com.google.devtools.build.lib.packages.Attribute.SplitTransition;
+import com.google.devtools.build.lib.packages.Attribute.Transition;
+import com.google.devtools.build.lib.packages.InputFile;
+import com.google.devtools.build.lib.packages.PackageGroup;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.rules.test.TestActionBuilder;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.syntax.Label.SyntaxException;
+import com.google.devtools.build.lib.syntax.SkylarkCallable;
+import com.google.devtools.build.lib.syntax.SkylarkModule;
+import com.google.devtools.build.lib.util.Fingerprint;
+import com.google.devtools.build.lib.util.OS;
+import com.google.devtools.build.lib.util.RegexFilter;
+import com.google.devtools.build.lib.util.StringUtilities;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyFunction.Environment;
+import com.google.devtools.common.options.Converter;
+import com.google.devtools.common.options.Converters;
+import com.google.devtools.common.options.EnumConverter;
+import com.google.devtools.common.options.Option;
+import com.google.devtools.common.options.OptionsBase;
+import com.google.devtools.common.options.OptionsParsingException;
+import com.google.devtools.common.options.TriState;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Queue;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+/**
+ * Instances of BuildConfiguration represent a collection of context
+ * information which may affect a build (for example: the target platform for
+ * compilation, or whether or not debug tables are required). In fact, all
+ * "environmental" information (e.g. from the tool's command-line, as opposed
+ * to the BUILD file) that can affect the output of any build tool should be
+ * explicitly represented in the BuildConfiguration instance.
+ *
+ * <p>A single build may require building tools to run on a variety of
+ * platforms: when compiling a server application for production, we must build
+ * the build tools (like compilers) to run on the host platform, but cross-compile
+ * the application for the production environment.
+ *
+ * <p>There is always at least one BuildConfiguration instance in any build:
+ * the one representing the host platform. Additional instances may be created,
+ * in a cross-compilation build, for example.
+ *
+ * <p>Instances of BuildConfiguration are canonical:
+ * <pre>c1.equals(c2) <=> c1==c2.</pre>
+ */
+@SkylarkModule(name = "configuration",
+ doc = "Data required for the analysis of a target that comes from targets that "
+ + "depend on it and not targets that it depends on.")
+public final class BuildConfiguration implements Serializable {
+
+ /**
+ * An interface for language-specific configurations.
+ */
+ public abstract static class Fragment implements Serializable {
+ /**
+ * Returns a human-readable name of the configuration fragment.
+ */
+ public abstract String getName();
+
+ /**
+ * Validates the options for this Fragment. Issues warnings for the
+ * use of deprecated options, and warnings or errors for any option settings
+ * that conflict.
+ */
+ @SuppressWarnings("unused")
+ public void reportInvalidOptions(EventHandler reporter, BuildOptions buildOptions) {
+ }
+
+ /**
+ * Adds mapping of names to values of "Make" variables defined by this configuration.
+ */
+ @SuppressWarnings("unused")
+ public void addGlobalMakeVariables(ImmutableMap.Builder<String, String> globalMakeEnvBuilder) {
+ }
+
+ /**
+ * Collects all labels that should be implicitly loaded from labels that were specified as
+ * options, keyed by the name to be displayed to the user if something goes wrong.
+ * The resulting set only contains labels that were derived from command-line options; the
+ * intention is that it can be used to sanity-check that the command-line options actually
+ * contain these in their transitive closure.
+ */
+ @SuppressWarnings("unused")
+ public void addImplicitLabels(Multimap<String, Label> implicitLabels) {
+ }
+
+ /**
+ * Returns a string that identifies the configuration fragment.
+ */
+ public abstract String cacheKey();
+
+ /**
+ * The fragment may use this hook to perform I/O and read data into memory that is used during
+ * analysis. During the analysis phase disk I/O operations are disallowed.
+ *
+ * <p>This hook is only called for the top-level configuration after the loading phase is
+ * complete.
+ */
+ @SuppressWarnings("unused")
+ public void prepareHook(Path execPath, ArtifactFactory artifactFactory,
+ PathFragment genfilesPath, PackageRootResolver resolver)
+ throws ViewCreationFailedException {
+ }
+
+ /**
+ * Adds all the roots from this fragment.
+ */
+ @SuppressWarnings("unused")
+ public void addRoots(List<Root> roots) {
+ }
+
+ /**
+ * Returns a (key, value) mapping to insert into the subcommand environment for coverage.
+ */
+ public Map<String, String> getCoverageEnvironment() {
+ return ImmutableMap.<String, String>of();
+ }
+
+ /*
+ * Returns the command-line "Make" variable overrides.
+ */
+ public ImmutableMap<String, String> getCommandLineDefines() {
+ return ImmutableMap.of();
+ }
+
+ /**
+ * Returns all the coverage labels for the fragment.
+ */
+ public ImmutableList<Label> getCoverageLabels() {
+ return ImmutableList.of();
+ }
+
+ /**
+ * Returns the coverage report generator tool labels.
+ */
+ public ImmutableList<Label> getCoverageReportGeneratorLabels() {
+ return ImmutableList.of();
+ }
+
+ /**
+ * Returns a fragment of the output directory name for this configuration. The output
+ * directory for the whole configuration contains all the short names by all fragments.
+ */
+ @Nullable
+ public String getOutputDirectoryName() {
+ return null;
+ }
+
+ /**
+ * This will be added to the name of the configuration, but not to the output directory name.
+ */
+ @Nullable
+ public String getConfigurationNameSuffix() {
+ return null;
+ }
+
+ /**
+ * The platform name is a concatenation of fragment platform names.
+ */
+ public String getPlatformName() {
+ return "";
+ }
+
+ /**
+ * Return false if incremental build is not possible for some reason.
+ */
+ public boolean supportsIncrementalBuild() {
+ return true;
+ }
+
+ /**
+ * Return true if the fragment performs static linking. This information is needed for
+ * lincence checking.
+ */
+ public boolean performsStaticLink() {
+ return false;
+ }
+
+ /**
+ * Fragments should delete temporary directories they create for their inner mechanisms.
+ * This is only called for target configuration.
+ */
+ @SuppressWarnings("unused")
+ public void prepareForExecutionPhase() throws IOException {
+ }
+
+ /**
+ * Add items to the shell environment.
+ */
+ @SuppressWarnings("unused")
+ public void setupShellEnvironment(ImmutableMap.Builder<String, String> builder) {
+ }
+
+ /**
+ * Add mappings from generally available tool names (like "sh") to their paths
+ * that actions can access.
+ */
+ @SuppressWarnings("unused")
+ public void defineExecutables(ImmutableMap.Builder<String, PathFragment> builder) {
+ }
+
+ /**
+ * Returns { 'option name': 'alternative default' } entries for options where the
+ * "real default" should be something besides the default specified in the {@link Option}
+ * declaration.
+ */
+ public Map<String, Object> lateBoundOptionDefaults() {
+ return ImmutableMap.of();
+ }
+
+ /**
+ * Declares dependencies on any relevant Skyframe values (for example, relevant FileValues).
+ *
+ * @param env the skyframe environment
+ */
+ public void declareSkyframeDependencies(Environment env) {
+ }
+ }
+
+ /**
+ * A converter from strings to Labels.
+ */
+ public static class LabelConverter implements Converter<Label> {
+ @Override
+ public Label convert(String input) throws OptionsParsingException {
+ try {
+ // Check if the input starts with '/'. We don't check for "//" so that
+ // we get a better error message if the user accidentally tries to use
+ // an absolute path (starting with '/') for a label.
+ if (!input.startsWith("/")) {
+ input = "//" + input;
+ }
+ return Label.parseAbsolute(input);
+ } catch (SyntaxException e) {
+ throw new OptionsParsingException(e.getMessage());
+ }
+ }
+
+ @Override
+ public String getTypeDescription() {
+ return "a build target label";
+ }
+ }
+
+ public static class PluginOptionConverter implements Converter<Map.Entry<String, String>> {
+ @Override
+ public Map.Entry<String, String> convert(String input) throws OptionsParsingException {
+ int index = input.indexOf('=');
+ if (index == -1) {
+ throw new OptionsParsingException("Plugin option not in the plugin=option format");
+ }
+ String option = input.substring(0, index);
+ String value = input.substring(index + 1);
+ return Maps.immutableEntry(option, value);
+ }
+
+ @Override
+ public String getTypeDescription() {
+ return "An option for a plugin";
+ }
+ }
+
+ public static class RunsPerTestConverter extends PerLabelOptions.PerLabelOptionsConverter {
+ @Override
+ public PerLabelOptions convert(String input) throws OptionsParsingException {
+ try {
+ return parseAsInteger(input);
+ } catch (NumberFormatException ignored) {
+ return parseAsRegex(input);
+ }
+ }
+
+ private PerLabelOptions parseAsInteger(String input)
+ throws NumberFormatException, OptionsParsingException {
+ int numericValue = Integer.parseInt(input);
+ if (numericValue <= 0) {
+ throw new OptionsParsingException("'" + input + "' should be >= 1");
+ } else {
+ RegexFilter catchAll = new RegexFilter(Collections.singletonList(".*"),
+ Collections.<String>emptyList());
+ return new PerLabelOptions(catchAll, Collections.singletonList(input));
+ }
+ }
+
+ private PerLabelOptions parseAsRegex(String input) throws OptionsParsingException {
+ PerLabelOptions testRegexps = super.convert(input);
+ if (testRegexps.getOptions().size() != 1) {
+ throw new OptionsParsingException(
+ "'" + input + "' has multiple runs for a single pattern");
+ }
+ String runsPerTest = Iterables.getOnlyElement(testRegexps.getOptions());
+ try {
+ int numericRunsPerTest = Integer.parseInt(runsPerTest);
+ if (numericRunsPerTest <= 0) {
+ throw new OptionsParsingException("'" + input + "' has a value < 1");
+ }
+ } catch (NumberFormatException e) {
+ throw new OptionsParsingException("'" + input + "' has a non-numeric value", e);
+ }
+ return testRegexps;
+ }
+
+ @Override
+ public String getTypeDescription() {
+ return "a positive integer or test_regex@runs. This flag may be passed more than once";
+ }
+ }
+
+ /**
+ * Values for the --strict_*_deps option
+ */
+ public static enum StrictDepsMode {
+ /** Silently allow referencing transitive dependencies. */
+ OFF,
+ /** Warn about transitive dependencies being used directly. */
+ WARN,
+ /** Fail the build when transitive dependencies are used directly. */
+ ERROR,
+ /** Transition to strict by default. */
+ STRICT,
+ /** When no flag value is specified on the command line. */
+ DEFAULT
+ }
+
+ /**
+ * Converter for the --strict_*_deps option.
+ */
+ public static class StrictDepsConverter extends EnumConverter<StrictDepsMode> {
+ public StrictDepsConverter() {
+ super(StrictDepsMode.class, "strict dependency checking level");
+ }
+ }
+
+ /**
+ * Options that affect the value of a BuildConfiguration instance.
+ *
+ * <p>(Note: any client that creates a view will also need to declare
+ * BuildView.Options, which affect the <i>mechanism</i> of view construction,
+ * even if they don't affect the value of the BuildConfiguration instances.)
+ *
+ * <p>IMPORTANT: when adding new options, be sure to consider whether those
+ * values should be propagated to the host configuration or not (see
+ * {@link ConfigurationFactory#getConfiguration}.
+ *
+ * <p>ALSO IMPORTANT: all option types MUST define a toString method that
+ * gives identical results for semantically identical option values. The
+ * simplest way to ensure that is to return the input string.
+ */
+ public static class Options extends FragmentOptions implements Cloneable {
+ public String getCpu() {
+ return cpu;
+ }
+
+ @Option(name = "cpu",
+ defaultValue = "null",
+ category = "semantics",
+ help = "The target CPU.")
+ public String cpu;
+
+ @Option(name = "min_param_file_size",
+ defaultValue = "32768",
+ category = "undocumented",
+ help = "Minimum command line length before creating a parameter file.")
+ public int minParamFileSize;
+
+ @Option(name = "experimental_extended_sanity_checks",
+ defaultValue = "false",
+ category = "undocumented",
+ help = "Enables internal validation checks to make sure that configured target "
+ + "implementations only access things they should. Causes a performance hit.")
+ public boolean extendedSanityChecks;
+
+ @Option(name = "experimental_allow_runtime_deps_on_neverlink",
+ defaultValue = "true",
+ category = "undocumented",
+ help = "Flag to help transition from allowing to disallowing runtime_deps on neverlink"
+ + " Java archives. The depot needs to be cleaned up to roll this out by default.")
+ public boolean allowRuntimeDepsOnNeverLink;
+
+ @Option(name = "strict_filesets",
+ defaultValue = "false",
+ category = "semantics",
+ help = "If this option is enabled, filesets crossing package boundaries are reported "
+ + "as errors. It does not work when check_fileset_dependencies_recursively is "
+ + "disabled.")
+ public boolean strictFilesets;
+
+ // Plugins are build using the host config. To avoid cycles we just don't propagate
+ // this option to the host config. If one day we decide to use plugins when building
+ // host tools, we can improve this by (for example) creating a compiler configuration that is
+ // used only for building plugins.
+ @Option(name = "plugin",
+ converter = LabelConverter.class,
+ allowMultiple = true,
+ defaultValue = "",
+ category = "flags",
+ help = "Plugins to use in the build. Currently works with java_plugin.")
+ public List<Label> pluginList;
+
+ @Option(name = "plugin_copt",
+ converter = PluginOptionConverter.class,
+ allowMultiple = true,
+ category = "flags",
+ defaultValue = ":",
+ help = "Plugin options")
+ public List<Map.Entry<String, String>> pluginCoptList;
+
+ @Option(name = "stamp",
+ defaultValue = "true",
+ category = "semantics",
+ help = "Stamp binaries with the date, username, hostname, workspace information, etc.")
+ public boolean stampBinaries;
+
+ // TODO(bazel-team): delete from OSS tree
+ @Option(name = "instrumentation_filter",
+ converter = RegexFilter.RegexFilterConverter.class,
+ defaultValue = "-javatests,-_test$",
+ category = "semantics",
+ help = "When coverage is enabled, only rules with names included by the "
+ + "specified regex-based filter will be instrumented. Rules prefixed "
+ + "with '-' are excluded instead. By default, rules containing "
+ + "'javatests' or ending with '_test' will not be instrumented.")
+ public RegexFilter instrumentationFilter;
+
+ @Option(name = "show_cached_analysis_results",
+ defaultValue = "true",
+ category = "undocumented",
+ help = "Bazel reruns a static analysis only if it detects changes in the analysis "
+ + "or its dependencies. If this option is enabled, Bazel will show the analysis' "
+ + "results, even if it did not rerun the analysis. If this option is disabled, "
+ + "Bazel will show analysis results only if it reran the analysis.")
+ public boolean showCachedAnalysisResults;
+
+ @Option(name = "host_cpu",
+ defaultValue = "null",
+ category = "semantics",
+ help = "The host CPU.")
+ public String hostCpu;
+
+ @Option(name = "compilation_mode",
+ abbrev = 'c',
+ converter = CompilationMode.Converter.class,
+ defaultValue = "fastbuild",
+ category = "semantics", // Should this be "flags"?
+ help = "Specify the mode the binary will be built in. "
+ + "Values: 'fastbuild', 'dbg', 'opt'.")
+ public CompilationMode compilationMode;
+
+ /**
+ * This option is used internally to set the short name (see {@link
+ * #getShortName()}) of the <i>host</i> configuration to a constant, so
+ * that the output files for the host are completely independent of those
+ * for the target, no matter what options are in force (k8/piii, opt/dbg,
+ * etc).
+ */
+ @Option(name = "configuration short name", // (Spaces => can't be specified on command line.)
+ defaultValue = "null",
+ category = "undocumented")
+ public String shortName;
+
+ @Option(name = "platform_suffix",
+ defaultValue = "null",
+ category = "misc",
+ help = "Specifies a suffix to be added to the configuration directory.")
+ public String platformSuffix;
+
+ @Option(name = "test_env",
+ converter = Converters.OptionalAssignmentConverter.class,
+ allowMultiple = true,
+ defaultValue = "",
+ category = "testing",
+ help = "Specifies additional environment variables to be injected into the test runner "
+ + "environment. Variables can be either specified by name, in which case its value "
+ + "will be read from the Bazel client environment, or by the name=value pair. "
+ + "This option can be used multiple times to specify several variables. "
+ + "Used only by the 'bazel test' command."
+ )
+ public List<Map.Entry<String, String>> testEnvironment;
+
+ @Option(name = "collect_code_coverage",
+ defaultValue = "false",
+ category = "testing",
+ help = "If specified, Bazel will instrument code (using offline instrumentation where "
+ + "possible) and will collect coverage information during tests. Only targets that "
+ + " match --instrumentation_filter will be affected. Usually this option should "
+ + " not be specified directly - 'bazel coverage' command should be used instead."
+ )
+ public boolean collectCodeCoverage;
+
+ @Option(name = "microcoverage",
+ defaultValue = "false",
+ category = "testing",
+ help = "If specified with coverage, Blaze will collect microcoverage (per test method "
+ + "coverage) information during tests. Only targets that match "
+ + "--instrumentation_filter will be affected. Usually this option should not be "
+ + "specified directly - 'blaze coverage --microcoverage' command should be used "
+ + "instead."
+ )
+ public boolean collectMicroCoverage;
+
+ @Option(name = "cache_test_results",
+ defaultValue = "auto",
+ category = "testing",
+ abbrev = 't', // it's useful to toggle this on/off quickly
+ help = "If 'auto', Bazel will only rerun a test if any of the following conditions apply: "
+ + "(1) Bazel detects changes in the test or its dependencies "
+ + "(2) the test is marked as external "
+ + "(3) multiple test runs were requested with --runs_per_test"
+ + "(4) the test failed"
+ + "If 'yes', the caching behavior will be the same as 'auto' except that "
+ + "it may cache test failures and test runs with --runs_per_test."
+ + "If 'no', all tests will be always executed.")
+ public TriState cacheTestResults;
+
+ @Deprecated
+ @Option(name = "test_result_expiration",
+ defaultValue = "-1", // No expiration by defualt.
+ category = "testing",
+ help = "This option is deprecated and has no effect.")
+ public int testResultExpiration;
+
+ @Option(name = "test_sharding_strategy",
+ defaultValue = "explicit",
+ category = "testing",
+ converter = TestActionBuilder.ShardingStrategyConverter.class,
+ help = "Specify strategy for test sharding: "
+ + "'explicit' to only use sharding if the 'shard_count' BUILD attribute is present. "
+ + "'disabled' to never use test sharding. "
+ + "'experimental_heuristic' to enable sharding on remotely executed tests without an "
+ + "explicit 'shard_count' attribute which link in a supported framework. Considered "
+ + "experimental.")
+ public TestActionBuilder.TestShardingStrategy testShardingStrategy;
+
+ @Option(name = "runs_per_test",
+ allowMultiple = true,
+ defaultValue = "1",
+ category = "testing",
+ converter = RunsPerTestConverter.class,
+ help = "Specifies number of times to run each test. If any of those attempts "
+ + "fail for any reason, the whole test would be considered failed. "
+ + "Normally the value specified is just an integer. Example: --runs_per_test=3 "
+ + "will run all tests 3 times. "
+ + "Alternate syntax: regex_filter@runs_per_test. Where runs_per_test stands for "
+ + "an integer value and regex_filter stands "
+ + "for a list of include and exclude regular expression patterns (Also see "
+ + "--instrumentation_filter). Example: "
+ + "--runs_per_test=//foo/.*,-//foo/bar/.*@3 runs all tests in //foo/ "
+ + "except those under foo/bar three times. "
+ + "This option can be passed multiple times. ")
+ public List<PerLabelOptions> runsPerTest;
+
+ @Option(name = "build_runfile_links",
+ defaultValue = "true",
+ category = "strategy",
+ help = "If true, build runfiles symlink forests for all targets. "
+ + "If false, write only manifests when possible.")
+ public boolean buildRunfiles;
+
+ @Option(name = "test_arg",
+ allowMultiple = true,
+ defaultValue = "",
+ category = "testing",
+ help = "Specifies additional options and arguments that should be passed to the test "
+ + "executable. Can be used multiple times to specify several arguments. "
+ + "If multiple tests are executed, each of them will receive identical arguments. "
+ + "Used only by the 'bazel test' command."
+ )
+ public List<String> testArguments;
+
+ @Option(name = "test_filter",
+ allowMultiple = false,
+ defaultValue = "null",
+ category = "testing",
+ help = "Specifies a filter to forward to the test framework. Used to limit "
+ + "the tests run. Note that this does not affect which targets are built.")
+ public String testFilter;
+
+ @Option(name = "check_fileset_dependencies_recursively",
+ defaultValue = "true",
+ category = "semantics",
+ help = "If false, fileset targets will, whenever possible, create "
+ + "symlinks to directories instead of creating one symlink for each "
+ + "file inside the directory. Disabling this will significantly "
+ + "speed up fileset builds, but targets that depend on filesets will "
+ + "not be rebuilt if files are added, removed or modified in a "
+ + "subdirectory which has not been traversed.")
+ public boolean checkFilesetDependenciesRecursively;
+
+ @Option(name = "run_under",
+ category = "run",
+ defaultValue = "null",
+ converter = RunUnderConverter.class,
+ help = "Prefix to insert in front of command before running. "
+ + "Examples:\n"
+ + "\t--run_under=valgrind\n"
+ + "\t--run_under=strace\n"
+ + "\t--run_under='strace -c'\n"
+ + "\t--run_under='valgrind --quiet --num-callers=20'\n"
+ + "\t--run_under=//package:target\n"
+ + "\t--run_under='//package:target --options'\n")
+ public RunUnder runUnder;
+
+ @Option(name = "distinct_host_configuration",
+ defaultValue = "true",
+ category = "strategy",
+ help = "Build all the tools used during the build for a distinct configuration from "
+ + "that used for the target program. By default, the same configuration is used "
+ + "for host and target programs, but this may cause undesirable rebuilds of tool "
+ + "such as the protocol compiler (and then everything downstream) whenever a minor "
+ + "change is made to the target configuration, such as setting the linker options. "
+ + "When this flag is specified, a distinct configuration will be used to build the "
+ + "tools, preventing undesired rebuilds. However, certain libraries will then "
+ + "need to be compiled twice, once for each configuration, which may cause some "
+ + "builds to be slower. As a rule of thumb, this option is likely to benefit "
+ + "users that make frequent changes in configuration (e.g. opt/dbg). "
+ + "Please read the user manual for the full explanation.")
+ public boolean useDistinctHostConfiguration;
+
+ @Option(name = "check_visibility",
+ defaultValue = "true",
+ category = "checking",
+ help = "If disabled, visibility errors are demoted to warnings.")
+ public boolean checkVisibility;
+
+ // Moved from viewOptions to here because license information is very expensive to serialize.
+ // Having it here allows us to skip computation of transitive license information completely
+ // when the setting is disabled.
+ @Option(name = "check_licenses",
+ defaultValue = "false",
+ category = "checking",
+ help = "Check that licensing constraints imposed by dependent packages "
+ + "do not conflict with distribution modes of the targets being built. "
+ + "By default, licenses are not checked.")
+ public boolean checkLicenses;
+
+ @Option(name = "experimental_enforce_constraints",
+ defaultValue = "true",
+ category = "undocumented",
+ help = "Checks the environments each target is compatible with and reports errors if any "
+ + "target has dependencies that don't support the same environments")
+ public boolean enforceConstraints;
+
+ @Option(name = "experimental_action_listener",
+ allowMultiple = true,
+ defaultValue = "",
+ category = "experimental",
+ converter = LabelConverter.class,
+ help = "Use action_listener to attach an extra_action to existing build actions.")
+ public List<Label> actionListeners;
+
+ @Option(name = "is host configuration",
+ defaultValue = "false",
+ category = "undocumented",
+ help = "Shows whether these options are set for host configuration.")
+ public boolean isHost;
+
+ @Option(name = "experimental_proto_header_modules",
+ defaultValue = "false",
+ category = "undocumented",
+ help = "Enables compilation of C++ header modules for proto libraries.")
+ public boolean protoHeaderModules;
+
+ @Option(name = "features",
+ allowMultiple = true,
+ defaultValue = "",
+ category = "flags",
+ help = "The given features will be enabled or disabled by default for all packages. "
+ + "Specifying -<feature> will disable the feature globally. "
+ + "Negative features always override positive ones. "
+ + "This flag is used to enable rolling out default feature changes without a "
+ + "Blaze release.")
+ public List<String> defaultFeatures;
+
+ @Override
+ public FragmentOptions getHost(boolean fallback) {
+ Options host = (Options) getDefault();
+
+ host.shortName = "host";
+ host.compilationMode = CompilationMode.OPT;
+ host.isHost = true;
+
+ if (fallback) {
+ // In the fallback case, we have already tried the target options and they didn't work, so
+ // now we try the default options; the hostCpu field has the default value, because we use
+ // getDefault() above.
+ host.cpu = computeHostCpu(host.hostCpu);
+ } else {
+ host.cpu = computeHostCpu(hostCpu);
+ }
+
+ // === Runfiles ===
+ // Ideally we could force this the other way, and skip runfiles construction
+ // for host tools which are never run locally, but that's probably a very
+ // small optimization.
+ host.buildRunfiles = true;
+
+ // === Linkstamping ===
+ // Disable all link stamping for the host configuration, to improve action
+ // cache hit rates for tools.
+ host.stampBinaries = false;
+
+ // === Visibility ===
+ host.checkVisibility = checkVisibility;
+
+ // === Licenses ===
+ host.checkLicenses = checkLicenses;
+
+ // === Allow runtime_deps to depend on neverlink Java libraries.
+ host.allowRuntimeDepsOnNeverLink = allowRuntimeDepsOnNeverLink;
+
+ // === Pass on C++ compiler features.
+ host.defaultFeatures = ImmutableList.copyOf(defaultFeatures);
+
+ return host;
+ }
+
+ private static String computeHostCpu(String explicitHostCpu) {
+ if (explicitHostCpu != null) {
+ return explicitHostCpu;
+ }
+ switch (OS.getCurrent()) {
+ case DARWIN:
+ return "darwin";
+ default:
+ return "k8";
+ }
+ }
+
+ @Override
+ public void addAllLabels(Multimap<String, Label> labelMap) {
+ labelMap.putAll("action_listener", actionListeners);
+ labelMap.putAll("plugins", pluginList);
+ if ((runUnder != null) && (runUnder.getLabel() != null)) {
+ labelMap.put("RunUnder", runUnder.getLabel());
+ }
+ }
+ }
+
+ /**
+ * A list of build configurations that only contains the null element.
+ */
+ private static final List<BuildConfiguration> NULL_LIST =
+ Collections.unmodifiableList(Arrays.asList(new BuildConfiguration[] { null }));
+
+ private final String cacheKey;
+ private final String shortCacheKey;
+
+ private Transitions transitions;
+ private Set<BuildConfiguration> allReachableConfigurations;
+
+ private final ImmutableMap<Class<? extends Fragment>, Fragment> fragments;
+
+ // Directories in the output tree
+ private final Root outputDirectory; // the configuration-specific output directory.
+ private final Root binDirectory;
+ private final Root genfilesDirectory;
+ private final Root coverageMetadataDirectory; // for coverage-related metadata, artifacts, etc.
+ private final Root testLogsDirectory;
+ private final Root includeDirectory;
+ private final Root middlemanDirectory;
+
+ private final PathFragment binFragment;
+ private final PathFragment genfilesFragment;
+
+ // If false, AnalysisEnviroment doesn't register any actions created by the ConfiguredTarget.
+ private final boolean actionsEnabled;
+
+ private final ImmutableSet<Label> coverageLabels;
+ private final ImmutableSet<Label> coverageReportGeneratorLabels;
+
+ // Executables like "perl" or "sh"
+ private final ImmutableMap<String, PathFragment> executables;
+
+ // All the "defglobals" in //tools:GLOBALS for this platform/configuration:
+ private final ImmutableMap<String, String> globalMakeEnv;
+
+ private final ImmutableMap<String, String> defaultShellEnvironment;
+ private final BuildOptions buildOptions;
+ private final Options options;
+
+ private final String shortName;
+ private final String mnemonic;
+ private final String platformName;
+
+ /**
+ * It is not fingerprinted because it should only be used to access
+ * variables that do not break the hermetism of build rules.
+ */
+ private final ImmutableMap<String, String> clientEnvironment;
+
+ /**
+ * Helper container for {@link #transitiveOptionsMap} below.
+ */
+ private static class OptionDetails implements Serializable {
+ private OptionDetails(Class<? extends OptionsBase> optionsClass, Object value,
+ boolean allowsMultiple) {
+ this.optionsClass = optionsClass;
+ this.value = value;
+ this.allowsMultiple = allowsMultiple;
+ }
+
+ /** The {@link FragmentOptions} class that defines this option. */
+ private final Class<? extends OptionsBase> optionsClass;
+
+ /**
+ * The value of the given option (either explicitly defined or default). May be null.
+ */
+ private final Object value;
+
+ /** Whether or not this option supports multiple values. */
+ private final boolean allowsMultiple;
+ }
+
+ /**
+ * Maps option names to the {@link OptionDetails} the option takes for this configuration.
+ *
+ * <p>This can be used to:
+ * <ol>
+ * <li>Find an option's (parsed) value given its command-line name</li>
+ * <li>Parse alternative values for the option.</li>
+ * </ol>
+ *
+ * <p>This map is "transitive" in that it includes *all* options recognizable by this
+ * configuration, including those defined in child fragments.
+ */
+ private final Map<String, OptionDetails> transitiveOptionsMap;
+
+
+ /**
+ * Validates the options for this BuildConfiguration. Issues warnings for the
+ * use of deprecated options, and warnings or errors for any option settings
+ * that conflict.
+ */
+ public void reportInvalidOptions(EventHandler reporter) {
+ for (Fragment fragment : fragments.values()) {
+ fragment.reportInvalidOptions(reporter, this.buildOptions);
+ }
+
+ Set<String> plugins = new HashSet<>();
+ for (Label plugin : options.pluginList) {
+ String name = plugin.getName();
+ if (plugins.contains(name)) {
+ reporter.handle(Event.error("A build cannot have two plugins with the same name"));
+ }
+ plugins.add(name);
+ }
+ for (Map.Entry<String, String> opt : options.pluginCoptList) {
+ if (!plugins.contains(opt.getKey())) {
+ reporter.handle(Event.error("A plugin_copt must refer to an existing plugin"));
+ }
+ }
+
+ if (options.shortName != null) {
+ reporter.handle(Event.error(
+ "The internal '--configuration short name' option cannot be used on the command line"));
+ }
+
+ if (options.testShardingStrategy
+ == TestActionBuilder.TestShardingStrategy.EXPERIMENTAL_HEURISTIC) {
+ reporter.handle(Event.warn(
+ "Heuristic sharding is intended as a one-off experimentation tool for determing the "
+ + "benefit from sharding certain tests. Please don't keep this option in your "
+ + ".blazerc or continuous build"));
+ }
+ }
+
+ private ImmutableMap<String, String> setupShellEnvironment() {
+ ImmutableMap.Builder<String, String> builder = new ImmutableMap.Builder<>();
+ for (Fragment fragment : fragments.values()) {
+ fragment.setupShellEnvironment(builder);
+ }
+ return builder.build();
+ }
+
+ BuildConfiguration(BlazeDirectories directories,
+ Map<Class<? extends Fragment>, Fragment> fragmentsMap,
+ BuildOptions buildOptions,
+ Map<String, String> clientEnv,
+ boolean actionsDisabled) {
+ this.actionsEnabled = !actionsDisabled;
+ fragments = ImmutableMap.copyOf(fragmentsMap);
+
+ // This is a view that will be updated upon each client command.
+ this.clientEnvironment = ImmutableMap.copyOf(clientEnv);
+
+ this.buildOptions = buildOptions;
+ this.options = buildOptions.get(Options.class);
+
+ this.mnemonic = buildMnemonic();
+ String outputDirName = (options.shortName != null) ? options.shortName : mnemonic;
+ this.shortName = buildShortName(outputDirName);
+
+ this.executables = collectExecutables();
+
+ Path execRoot = directories.getExecRoot();
+ // configuration-specific output tree
+ Path outputDir = directories.getOutputPath().getRelative(outputDirName);
+ this.outputDirectory = Root.asDerivedRoot(execRoot, outputDir);
+
+ // specific subdirs under outputDirectory
+ this.binDirectory = Root.asDerivedRoot(execRoot, outputDir.getRelative("bin"));
+ this.genfilesDirectory = Root.asDerivedRoot(execRoot, outputDir.getRelative("genfiles"));
+ this.coverageMetadataDirectory = Root.asDerivedRoot(execRoot,
+ outputDir.getRelative("coverage-metadata"));
+ this.testLogsDirectory = Root.asDerivedRoot(execRoot, outputDir.getRelative("testlogs"));
+ this.includeDirectory = Root.asDerivedRoot(execRoot,
+ outputDir.getRelative(BlazeDirectories.RELATIVE_INCLUDE_DIR));
+ this.middlemanDirectory = Root.middlemanRoot(execRoot, outputDir);
+
+ // precompute some frequently-used relative paths
+ this.binFragment = getBinDirectory().getExecPath();
+ this.genfilesFragment = getGenfilesDirectory().getExecPath();
+
+ ImmutableSet.Builder<Label> coverageLabelsBuilder = ImmutableSet.builder();
+ ImmutableSet.Builder<Label> coverageReportGeneratorLabelsBuilder = ImmutableSet.builder();
+ for (Fragment fragment : fragments.values()) {
+ coverageLabelsBuilder.addAll(fragment.getCoverageLabels());
+ coverageReportGeneratorLabelsBuilder.addAll(fragment.getCoverageReportGeneratorLabels());
+ }
+ this.coverageLabels = coverageLabelsBuilder.build();
+ this.coverageReportGeneratorLabels = coverageReportGeneratorLabelsBuilder.build();
+
+ // Platform name
+ StringBuilder platformNameBuilder = new StringBuilder();
+ for (Fragment fragment : fragments.values()) {
+ platformNameBuilder.append(fragment.getPlatformName());
+ }
+ this.platformName = platformNameBuilder.toString();
+
+ this.defaultShellEnvironment = setupShellEnvironment();
+
+ this.transitiveOptionsMap = computeOptionsMap(buildOptions, fragments.values());
+
+ ImmutableMap.Builder<String, String> globalMakeEnvBuilder = ImmutableMap.builder();
+ for (Fragment fragment : fragments.values()) {
+ fragment.addGlobalMakeVariables(globalMakeEnvBuilder);
+ }
+
+ // Lots of packages in third_party assume that BINMODE expands to either "-dbg", or "-opt". So
+ // for backwards compatibility we preserve that invariant, setting BINMODE to "-dbg" rather than
+ // "-fastbuild" if the compilation mode is "fastbuild".
+ // We put the real compilation mode in a new variable COMPILATION_MODE.
+ globalMakeEnvBuilder.put("COMPILATION_MODE", options.compilationMode.toString());
+ globalMakeEnvBuilder.put("BINMODE", "-"
+ + ((options.compilationMode == CompilationMode.FASTBUILD)
+ ? "dbg"
+ : options.compilationMode.toString()));
+ /*
+ * Attention! Document these in the build-encyclopedia
+ */
+ // the bin directory and the genfiles directory
+ // These variables will be used on Windows as well, so we need to make sure
+ // that paths use the correct system file-separator.
+ globalMakeEnvBuilder.put("BINDIR", binFragment.getPathString());
+ globalMakeEnvBuilder.put("INCDIR",
+ getIncludeDirectory().getExecPath().getPathString());
+ globalMakeEnvBuilder.put("GENDIR", genfilesFragment.getPathString());
+ globalMakeEnv = globalMakeEnvBuilder.build();
+
+ cacheKey = computeCacheKey(
+ directories, fragmentsMap, this.buildOptions, this.clientEnvironment);
+ shortCacheKey = shortName + "-" + Fingerprint.md5Digest(cacheKey);
+ }
+
+
+ /**
+ * Computes and returns the transitive optionName -> "option info" map for
+ * this configuration.
+ */
+ private static Map<String, OptionDetails> computeOptionsMap(BuildOptions buildOptions,
+ Iterable<Fragment> fragments) {
+ // Collect from our fragments "alternative defaults" for options where the default
+ // should be something other than what's specified in Option.defaultValue.
+ Map<String, Object> lateBoundDefaults = Maps.newHashMap();
+ for (Fragment fragment : fragments) {
+ lateBoundDefaults.putAll(fragment.lateBoundOptionDefaults());
+ }
+
+ ImmutableMap.Builder<String, OptionDetails> map = ImmutableMap.builder();
+ try {
+ for (FragmentOptions options : buildOptions.getOptions()) {
+ for (Field field : options.getClass().getFields()) {
+ if (field.isAnnotationPresent(Option.class)) {
+ Option option = field.getAnnotation(Option.class);
+ Object value = field.get(options);
+ if (value == null) {
+ if (lateBoundDefaults.containsKey(option.name())) {
+ value = lateBoundDefaults.get(option.name());
+ } else if (!option.defaultValue().equals("null")) {
+ // See {@link Option#defaultValue} for an explanation of default "null" strings.
+ value = option.defaultValue();
+ }
+ }
+ map.put(option.name(),
+ new OptionDetails(options.getClass(), value, option.allowMultiple()));
+ }
+ }
+ }
+ } catch (IllegalAccessException e) {
+ throw new IllegalStateException(
+ "Unexpected illegal access trying to create this configuration's options map: ", e);
+ }
+ return map.build();
+ }
+
+ private String buildShortName(String outputDirName) {
+ ArrayList<String> nameParts = new ArrayList<>(ImmutableList.of(outputDirName));
+ for (Fragment fragment : fragments.values()) {
+ nameParts.add(fragment.getConfigurationNameSuffix());
+ }
+ return Joiner.on('-').skipNulls().join(nameParts);
+ }
+
+ private String buildMnemonic() {
+ // See explanation at getShortName().
+ String platformSuffix = (options.platformSuffix != null) ? options.platformSuffix : "";
+ ArrayList<String> nameParts = new ArrayList<String>();
+ for (Fragment fragment : fragments.values()) {
+ nameParts.add(fragment.getOutputDirectoryName());
+ }
+ nameParts.add(getCompilationMode() + platformSuffix);
+ return Joiner.on('-').join(Iterables.filter(nameParts, Predicates.notNull()));
+ }
+
+ /**
+ * Set the outgoing configuration transitions. During the lifetime of a given build configuration,
+ * this must happen exactly once, shortly after the configuration is created.
+ * TODO(bazel-team): this makes the object mutable, get rid of it.
+ */
+ public void setConfigurationTransitions(Transitions transitions) {
+ Preconditions.checkNotNull(transitions);
+ Preconditions.checkState(this.transitions == null);
+ this.transitions = transitions;
+ }
+
+ public Transitions getTransitions() {
+ Preconditions.checkState(this.transitions != null || isHostConfiguration());
+ return transitions;
+ }
+
+ /**
+ * Returns all configurations that can be reached from this configuration through any kind of
+ * configuration transition.
+ */
+ public synchronized Collection<BuildConfiguration> getAllReachableConfigurations() {
+ if (allReachableConfigurations == null) {
+ // This is needed for every configured target in skyframe m2, so we cache it.
+ // We could alternatively make the corresponding dependencies into a skyframe node.
+ this.allReachableConfigurations = computeAllReachableConfigurations();
+ }
+ return allReachableConfigurations;
+ }
+
+ /**
+ * Returns all configurations that can be reached from this configuration through any kind of
+ * configuration transition.
+ */
+ private Set<BuildConfiguration> computeAllReachableConfigurations() {
+ Set<BuildConfiguration> result = new LinkedHashSet<>();
+ Queue<BuildConfiguration> queue = new LinkedList<>();
+ queue.add(this);
+ while (!queue.isEmpty()) {
+ BuildConfiguration config = queue.remove();
+ if (!result.add(config)) {
+ continue;
+ }
+ config.getTransitions().addDirectlyReachableConfigurations(queue);
+ }
+ return result;
+ }
+
+ /**
+ * Returns the new configuration after traversing a dependency edge with a given configuration
+ * transition.
+ *
+ * @param transition the configuration transition
+ * @return the new configuration
+ * @throws IllegalArgumentException if the transition is a {@link SplitTransition}
+ */
+ public BuildConfiguration getConfiguration(Transition transition) {
+ Preconditions.checkArgument(!(transition instanceof SplitTransition));
+ return transitions.getConfiguration(transition);
+ }
+
+ /**
+ * Returns the new configurations after traversing a dependency edge with a given split
+ * transition.
+ *
+ * @param transition the split configuration transition
+ * @return the new configurations
+ */
+ public List<BuildConfiguration> getSplitConfigurations(SplitTransition<?> transition) {
+ return transitions.getSplitConfigurations(transition);
+ }
+
+ /**
+ * Calculates the configurations of a direct dependency. If a rule in some BUILD file refers
+ * to a target (like another rule or a source file) using a label attribute, that target needs
+ * to have a configuration, too. This method figures out the proper configuration for the
+ * dependency.
+ *
+ * @param fromRule the rule that's depending on some target
+ * @param attribute the attribute using which the rule depends on that target (eg. "srcs")
+ * @param toTarget the target that's dependeded on
+ * @return the configuration that should be associated to {@code toTarget}
+ */
+ public Iterable<BuildConfiguration> evaluateTransition(final Rule fromRule,
+ final Attribute attribute, final Target toTarget) {
+ // Fantastic configurations and where to find them:
+
+ // I. Input files and package groups have no configurations. We don't want to duplicate them.
+ if (toTarget instanceof InputFile || toTarget instanceof PackageGroup) {
+ return NULL_LIST;
+ }
+
+ // II. Host configurations never switch to another. All prerequisites of host targets have the
+ // same host configuration.
+ if (isHostConfiguration()) {
+ return ImmutableList.of(this);
+ }
+
+ // Make sure config_setting dependencies are resolved in the referencing rule's configuration,
+ // unconditionally. For example, given:
+ //
+ // genrule(
+ // name = 'myrule',
+ // tools = select({ '//a:condition': [':sometool'] })
+ //
+ // all labels in "tools" get resolved in the host configuration (since the "tools" attribute
+ // declares a host configuration transition). We want to explicitly exclude configuration labels
+ // from these transitions, since their *purpose* is to do computation on the owning
+ // rule's configuration.
+ // TODO(bazel-team): implement this more elegantly. This is far too hackish. Specifically:
+ // don't reference the rule name explicitly and don't require special-casing here.
+ if (toTarget instanceof Rule && ((Rule) toTarget).getRuleClass().equals("config_setting")) {
+ return ImmutableList.of(this);
+ }
+
+ List<BuildConfiguration> toConfigurations;
+ if (attribute.getConfigurationTransition() instanceof SplitTransition) {
+ Preconditions.checkState(attribute.getConfigurator() == null);
+ toConfigurations = getSplitConfigurations(
+ (SplitTransition<?>) attribute.getConfigurationTransition());
+ } else {
+ // III. Attributes determine configurations. The configuration of a prerequisite is determined
+ // by the attribute.
+ @SuppressWarnings("unchecked")
+ Configurator<BuildConfiguration, Rule> configurator =
+ (Configurator<BuildConfiguration, Rule>) attribute.getConfigurator();
+ toConfigurations = ImmutableList.of((configurator != null)
+ ? configurator.apply(fromRule, this, attribute, toTarget)
+ : getConfiguration(attribute.getConfigurationTransition()));
+ }
+
+ return Iterables.transform(toConfigurations,
+ new Function<BuildConfiguration, BuildConfiguration>() {
+ @Override
+ public BuildConfiguration apply(BuildConfiguration input) {
+ // IV. Allow the transition object to perform an arbitrary switch. Blaze modules can inject
+ // configuration transition logic by extending the Transitions class.
+ BuildConfiguration actual = getTransitions().configurationHook(
+ fromRule, attribute, toTarget, input);
+
+ // V. Allow rule classes to override their own configurations.
+ Rule associatedRule = toTarget.getAssociatedRule();
+ if (associatedRule != null) {
+ @SuppressWarnings("unchecked")
+ RuleClass.Configurator<BuildConfiguration, Rule> func =
+ associatedRule.getRuleClassObject().<BuildConfiguration, Rule>getConfigurator();
+ actual = func.apply(associatedRule, actual);
+ }
+
+ return actual;
+ }
+ });
+ }
+
+ /**
+ * Returns a multimap of all labels that should be implicitly loaded from labels that were
+ * specified as options, keyed by the name to be displayed to the user if something goes wrong.
+ * The returned set only contains labels that were derived from command-line options; the
+ * intention is that it can be used to sanity-check that the command-line options actually contain
+ * these in their transitive closure.
+ */
+ public ListMultimap<String, Label> getImplicitLabels() {
+ ListMultimap<String, Label> implicitLabels = ArrayListMultimap.create();
+ for (Fragment fragment : fragments.values()) {
+ fragment.addImplicitLabels(implicitLabels);
+ }
+ return implicitLabels;
+ }
+
+ /**
+ * For an given environment, returns a subset containing all
+ * variables in the given list if they are defined in the given
+ * environment.
+ */
+ @VisibleForTesting
+ static Map<String, String> getMapping(List<String> variables,
+ Map<String, String> environment) {
+ Map<String, String> result = new HashMap<>();
+ for (String var : variables) {
+ if (environment.containsKey(var)) {
+ result.put(var, environment.get(var));
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Avoid this method. The client environment is not part of the configuration's signature, so
+ * calls to this method introduce a non-hermetic access to data that is not visible to Skyframe.
+ *
+ * @return an unmodifiable view of the bazel client's environment
+ * upon its most recent request.
+ */
+ // TODO(bazel-team): Remove this.
+ public Map<String, String> getClientEnv() {
+ return clientEnvironment;
+ }
+
+ /**
+ * Returns the {@link Option} class the defines the given option, null if the
+ * option isn't recognized.
+ *
+ * <p>optionName is the name of the option as it appears on the command line
+ * e.g. {@link Option#name}).
+ */
+ Class<? extends OptionsBase> getOptionClass(String optionName) {
+ OptionDetails optionData = transitiveOptionsMap.get(optionName);
+ return optionData == null ? null : optionData.optionsClass;
+ }
+
+ /**
+ * Returns the value of the specified option for this configuration or null if the
+ * option isn't recognized. Since an option's legitimate value could be null, use
+ * {@link #getOptionClass} to distinguish between that and an unknown option.
+ *
+ * <p>optionName is the name of the option as it appears on the command line
+ * e.g. {@link Option#name}).
+ */
+ Object getOptionValue(String optionName) {
+ OptionDetails optionData = transitiveOptionsMap.get(optionName);
+ return (optionData == null) ? null : optionData.value;
+ }
+
+ /**
+ * Returns whether or not the given option supports multiple values at the command line (e.g.
+ * "--myoption value1 --myOption value2 ..."). Returns false for unrecognized options. Use
+ * {@link #getOptionClass} to distinguish between those and legitimate single-value options.
+ *
+ * <p>As declared in {@link Option#allowMultiple}, multi-value options are expected to be
+ * of type {@code List<T>}.
+ */
+ boolean allowsMultipleValues(String optionName) {
+ OptionDetails optionData = transitiveOptionsMap.get(optionName);
+ return (optionData == null) ? false : optionData.allowsMultiple;
+ }
+
+ /**
+ * The platform string, suitable for use as a key into a MakeEnvironment.
+ */
+ public String getPlatformName() {
+ return platformName;
+ }
+
+ /**
+ * Returns the output directory for this build configuration.
+ */
+ public Root getOutputDirectory() {
+ return outputDirectory;
+ }
+
+ /**
+ * Returns the bin directory for this build configuration.
+ */
+ @SkylarkCallable(name = "bin_dir", structField = true,
+ doc = "The root corresponding to bin directory.")
+ public Root getBinDirectory() {
+ return binDirectory;
+ }
+
+ /**
+ * Returns a relative path to the bin directory at execution time.
+ */
+ public PathFragment getBinFragment() {
+ return binFragment;
+ }
+
+ /**
+ * Returns the include directory for this build configuration.
+ */
+ public Root getIncludeDirectory() {
+ return includeDirectory;
+ }
+
+ /**
+ * Returns the genfiles directory for this build configuration.
+ */
+ @SkylarkCallable(name = "genfiles_dir", structField = true,
+ doc = "The root corresponding to genfiles directory.")
+ public Root getGenfilesDirectory() {
+ return genfilesDirectory;
+ }
+
+ /**
+ * Returns the directory where coverage-related artifacts and metadata files
+ * should be stored. This includes for example uninstrumented class files
+ * needed for Jacoco's coverage reporting tools.
+ */
+ public Root getCoverageMetadataDirectory() {
+ return coverageMetadataDirectory;
+ }
+
+ /**
+ * Returns the testlogs directory for this build configuration.
+ */
+ public Root getTestLogsDirectory() {
+ return testLogsDirectory;
+ }
+
+ /**
+ * Returns a relative path to the genfiles directory at execution time.
+ */
+ public PathFragment getGenfilesFragment() {
+ return genfilesFragment;
+ }
+
+ /**
+ * Returns the path separator for the host platform. This is basically the same as {@link
+ * java.io.File#pathSeparator}, except that that returns the value for this JVM, which may or may
+ * not match the host platform. You should only use this when invoking tools that are known to use
+ * the native path separator, i.e., the path separator for the machine that they run on.
+ */
+ @SkylarkCallable(name = "host_path_separator", structField = true,
+ doc = "Returns the separator for PATH variable, which is ':' on Unix.")
+ public String getHostPathSeparator() {
+ // TODO(bazel-team): This needs to change when we support Windows.
+ return ":";
+ }
+
+ /**
+ * Returns the internal directory (used for middlemen) for this build configuration.
+ */
+ public Root getMiddlemanDirectory() {
+ return middlemanDirectory;
+ }
+
+ public boolean getAllowRuntimeDepsOnNeverLink() {
+ return options.allowRuntimeDepsOnNeverLink;
+ }
+
+ public boolean isStrictFilesets() {
+ return options.strictFilesets;
+ }
+
+ public List<Label> getPlugins() {
+ return options.pluginList;
+ }
+
+ public List<Map.Entry<String, String>> getPluginCopts() {
+ return options.pluginCoptList;
+ }
+
+ /**
+ * Implements a non-injective mapping from BuildConfiguration instances to
+ * strings. The result should identify the aspects of the configuration
+ * that should be reflected in the output file names. Furthermore the
+ * returned string must not contain shell metacharacters.
+ *
+ * <p>The intention here is that we use this string as the directory name
+ * for artifacts of this build.
+ *
+ * <p>For configuration settings which are NOT part of the short name,
+ * rebuilding with a different value of such a setting will build in
+ * the same output directory. This means that any actions whose
+ * keys (see Action.getKey()) have changed will be rerun. That
+ * may result in a lot of recompilation.
+ *
+ * <p>For configuration settings which ARE part of the short name,
+ * rebuilding with a different value of such a setting will rebuild
+ * in a different output directory; this will result in higher disk
+ * usage and more work the _first_ time you rebuild with a different
+ * setting, but will result in less work if you regularly switch
+ * back and forth between different settings.
+ *
+ * <p>With one important exception, it's sound to choose any subset of the
+ * config's components for this string, it just alters the dimensionality
+ * of the cache. In other words, it's a trade-off on the "injectiveness"
+ * scale: at one extreme (shortName is in fact a complete fingerprint, and
+ * thus injective) you get extremely precise caching (no competition for the
+ * same output-file locations) but you have to rebuild for even the
+ * slightest change in configuration. At the other extreme
+ * (PartialFingerprint is a constant) you have very high competition for
+ * output-file locations, but if a slight change in configuration doesn't
+ * affect a particular build step, you're guaranteed not to have to
+ * rebuild it. The important exception has to do with cross-compilation:
+ * the host and target configurations must not map to the same output
+ * directory, because then files would need to get built for the host
+ * and then rebuilt for the target even within a single build, and that
+ * wouldn't work.
+ *
+ * <p>Just to re-iterate: cross-compilation builds (i.e. hostConfig !=
+ * targetConfig) will not work if the two configurations' short names are
+ * equal. This is an important practical case: the mere addition of
+ * a compile flag to the target configuration would cause the build to
+ * fail. In other words, it would break if the host and target
+ * configurations are not identical but are "too close". The current
+ * solution is to set the host configuration equal to the target
+ * configuration if they are "too close"; this may cause the tools to get
+ * rebuild for the new host configuration though.
+ */
+ public String getShortName() {
+ return shortName;
+ }
+
+ /**
+ * Like getShortName(), but always returns a configuration-dependent string even for
+ * the host configuration.
+ */
+ public String getMnemonic() {
+ return mnemonic;
+ }
+
+ @Override
+ public String toString() {
+ return getShortName();
+ }
+
+ /**
+ * Returns the default shell environment
+ */
+ @SkylarkCallable(name = "default_shell_env", structField = true,
+ doc = "A dictionary representing the default environment. It maps variables "
+ + "to their values (strings).")
+ public ImmutableMap<String, String> getDefaultShellEnvironment() {
+ return defaultShellEnvironment;
+ }
+
+ /**
+ * Returns the path to sh.
+ */
+ public PathFragment getShExecutable() {
+ return executables.get("sh");
+ }
+
+ /**
+ * Returns a regex-based instrumentation filter instance that used to match label
+ * names to identify targets to be instrumented in the coverage mode.
+ */
+ public RegexFilter getInstrumentationFilter() {
+ return options.instrumentationFilter;
+ }
+
+ /**
+ * Returns the set of labels for coverage.
+ */
+ public Set<Label> getCoverageLabels() {
+ return coverageLabels;
+ }
+
+ /**
+ * Returns the set of labels for the coverage report generator.
+ */
+ public Set<Label> getCoverageReportGeneratorLabels() {
+ return coverageReportGeneratorLabels;
+ }
+
+ /**
+ * Returns true if bazel should show analyses results, even if it did not
+ * re-run the analysis.
+ */
+ public boolean showCachedAnalysisResults() {
+ return options.showCachedAnalysisResults;
+ }
+
+ /**
+ * Returns a new, unordered mapping of names to values of "Make" variables defined by this
+ * configuration.
+ *
+ * <p>This does *not* include package-defined overrides (e.g. vardef)
+ * and so should not be used by the build logic. This is used only for
+ * the 'info' command.
+ *
+ * <p>Command-line definitions of make enviroments override variables defined by
+ * {@code Fragment.addGlobalMakeVariables()}.
+ */
+ public Map<String, String> getMakeEnvironment() {
+ Map<String, String> makeEnvironment = new HashMap<>();
+ makeEnvironment.putAll(globalMakeEnv);
+ for (Fragment fragment : fragments.values()) {
+ makeEnvironment.putAll(fragment.getCommandLineDefines());
+ }
+ return ImmutableMap.copyOf(makeEnvironment);
+ }
+
+ /**
+ * Returns a new, unordered mapping of names that are set through the command lines.
+ * (Fragments, in particular the Google C++ support, can set variables through the
+ * command line.)
+ */
+ public Map<String, String> getCommandLineDefines() {
+ ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
+ for (Fragment fragment : fragments.values()) {
+ builder.putAll(fragment.getCommandLineDefines());
+ }
+ return builder.build();
+ }
+
+ /**
+ * Returns the global defaults for this configuration for the Make environment.
+ */
+ public Map<String, String> getGlobalMakeEnvironment() {
+ return globalMakeEnv;
+ }
+
+ /**
+ * Returns a (key, value) mapping to insert into the subcommand environment for coverage
+ * actions.
+ */
+ public Map<String, String> getCoverageEnvironment() {
+ Map<String, String> env = new HashMap<>();
+ for (Fragment fragment : fragments.values()) {
+ env.putAll(fragment.getCoverageEnvironment());
+ }
+ return env;
+ }
+
+ /**
+ * Returns the default value for the specified "Make" variable for this
+ * configuration. Returns null if no value was found.
+ */
+ public String getMakeVariableDefault(String var) {
+ return globalMakeEnv.get(var);
+ }
+
+ /**
+ * Returns a configuration fragment instances of the given class.
+ */
+ @SkylarkCallable(name = "fragment", doc = "Returns a configuration fragment using the key.")
+ public <T extends Fragment> T getFragment(Class<T> clazz) {
+ return clazz.cast(fragments.get(clazz));
+ }
+
+ /**
+ * Returns true if the requested configuration fragment is present.
+ */
+ public <T extends Fragment> boolean hasFragment(Class<T> clazz) {
+ return getFragment(clazz) != null;
+ }
+
+ /**
+ * Returns true if all requested configuration fragment are present (this may be slow).
+ */
+ public boolean hasAllFragments(Set<Class<?>> fragmentClasses) {
+ for (Class<?> fragmentClass : fragmentClasses) {
+ if (!hasFragment(fragmentClass.asSubclass(Fragment.class))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Returns true if non-functional build stamps are enabled.
+ */
+ public boolean stampBinaries() {
+ return options.stampBinaries;
+ }
+
+ /**
+ * Returns true if extended sanity checks should be enabled.
+ */
+ public boolean extendedSanityChecks() {
+ return options.extendedSanityChecks;
+ }
+
+ /**
+ * Returns true if we are building runfiles symlinks for this configuration.
+ */
+ public boolean buildRunfiles() {
+ return options.buildRunfiles;
+ }
+
+ public boolean getCheckFilesetDependenciesRecursively() {
+ return options.checkFilesetDependenciesRecursively;
+ }
+
+ public List<String> getTestArguments() {
+ return options.testArguments;
+ }
+
+ public String getTestFilter() {
+ return options.testFilter;
+ }
+
+ /**
+ * Returns user-specified test environment variables and their values, as
+ * set by the --test_env options.
+ */
+ public Map<String, String> getTestEnv() {
+ return getTestEnv(options.testEnvironment, clientEnvironment);
+ }
+
+ /**
+ * Returns user-specified test environment variables and their values, as
+ * set by the --test_env options.
+ *
+ * @param envOverrides The --test_env flag values.
+ * @param clientEnvironment The full client environment.
+ */
+ public static Map<String, String> getTestEnv(List<Map.Entry<String, String>> envOverrides,
+ Map<String, String> clientEnvironment) {
+ Map<String, String> testEnv = new HashMap<>();
+ for (Map.Entry<String, String> var : envOverrides) {
+ if (var.getValue() != null) {
+ testEnv.put(var.getKey(), var.getValue());
+ } else {
+ String value = clientEnvironment.get(var.getKey());
+ if (value != null) {
+ testEnv.put(var.getKey(), value);
+ }
+ }
+ }
+ return testEnv;
+ }
+
+ public TriState cacheTestResults() {
+ return options.cacheTestResults;
+ }
+
+ public int getMinParamFileSize() {
+ return options.minParamFileSize;
+ }
+
+ @SkylarkCallable(name = "coverage_enabled", structField = true,
+ doc = "A boolean that tells whether code coverage is enabled.")
+ public boolean isCodeCoverageEnabled() {
+ return options.collectCodeCoverage;
+ }
+
+ public boolean isMicroCoverageEnabled() {
+ return options.collectMicroCoverage;
+ }
+
+ public boolean isActionsEnabled() {
+ return actionsEnabled;
+ }
+
+ public TestActionBuilder.TestShardingStrategy testShardingStrategy() {
+ return options.testShardingStrategy;
+ }
+
+ /**
+ * @return number of times the given test should run.
+ * If the test doesn't match any of the filters, runs it once.
+ */
+ public int getRunsPerTestForLabel(Label label) {
+ for (PerLabelOptions perLabelRuns : options.runsPerTest) {
+ if (perLabelRuns.isIncluded(label)) {
+ return Integer.parseInt(Iterables.getOnlyElement(perLabelRuns.getOptions()));
+ }
+ }
+ return 1;
+ }
+
+ public RunUnder getRunUnder() {
+ return options.runUnder;
+ }
+
+ /**
+ * Returns true if this is a host configuration.
+ */
+ public boolean isHostConfiguration() {
+ return options.isHost;
+ }
+
+ public boolean checkVisibility() {
+ return options.checkVisibility;
+ }
+
+ public boolean checkLicenses() {
+ return options.checkLicenses;
+ }
+
+ public boolean enforceConstraints() {
+ return options.enforceConstraints;
+ }
+
+ public List<Label> getActionListeners() {
+ return actionsEnabled ? options.actionListeners : ImmutableList.<Label>of();
+ }
+
+ /**
+ * Returns compilation mode.
+ */
+ public CompilationMode getCompilationMode() {
+ return options.compilationMode;
+ }
+
+ /**
+ * Helper method to create a key from the BuildConfiguration initialization
+ * parameters and any additional component suppliers.
+ */
+ static String computeCacheKey(BlazeDirectories directories,
+ Map<Class<? extends Fragment>, Fragment> fragments,
+ BuildOptions buildOptions, Map<String, String> clientEnv) {
+
+ // Creates a full fingerprint of all constructor parameters, used for
+ // canonicalization.
+ //
+ // Note the use of each Path's FileSystem field; the test suite creates
+ // many paths of equal name but belonging to distinct filesystems, so we
+ // have to detect this. (Note however that we're relying on the
+ // injectiveness of identityHashCode for FileSystem, which is inelegant,
+ // but only affects the tests, since the production code uses only one
+ // instance.)
+
+ ImmutableList.Builder<String> keys = ImmutableList.builder();
+
+ // NOTE: identityHashCode isn't sound; may cause tests to fail.
+ keys.add(String.valueOf(System.identityHashCode(directories.getOutputBase().getFileSystem())));
+ keys.add(directories.getOutputBase().toString());
+ keys.add(buildOptions.computeCacheKey());
+ // This is needed so that if we have --test_env=VAR, the configuration key is updated if the
+ // environment variable VAR is updated.
+ keys.add(BuildConfiguration.getTestEnv(
+ buildOptions.get(Options.class).testEnvironment, clientEnv).toString());
+ keys.add(directories.getWorkspace().toString());
+
+ for (Fragment fragment : fragments.values()) {
+ keys.add(fragment.cacheKey());
+ }
+
+ // TODO(bazel-team): add hash of the FDO/LIPO profile file to config cache key
+
+ return StringUtilities.combineKeys(keys.build());
+ }
+
+ /**
+ * Returns a string that identifies the configuration.
+ *
+ * <p>The string uniquely identifies the configuration. As a result, it can be rather long and
+ * include spaces and other non-alphanumeric characters. If you need a shorter key, use
+ * {@link #shortCacheKey()}.
+ *
+ * @see #computeCacheKey
+ */
+ public final String cacheKey() {
+ return cacheKey;
+ }
+
+ /**
+ * Returns a (relatively) short key that identifies the configuration.
+ *
+ * <p>The short key is the short name of the configuration concatenated with a hash of the
+ * {@link #cacheKey()}.
+ */
+ public final String shortCacheKey() {
+ return shortCacheKey;
+ }
+
+ /** Returns a copy of the build configuration options for this configuration. */
+ public BuildOptions cloneOptions() {
+ return buildOptions.clone();
+ }
+
+ /**
+ * Prepare the fdo support. It reads data into memory that is used during analysis. The analysis
+ * phase is generally not allowed to perform disk I/O. This code is here because it is
+ * conceptually part of the analysis phase, and it needs to happen when the loading phase is
+ * complete.
+ */
+ public void prepareToBuild(Path execRoot, ArtifactFactory artifactFactory,
+ PackageRootResolver resolver) throws ViewCreationFailedException {
+ for (Fragment fragment : fragments.values()) {
+ fragment.prepareHook(execRoot, artifactFactory, getGenfilesFragment(), resolver);
+ }
+ }
+
+ /**
+ * Declares dependencies on any relevant Skyframe values (for example, relevant FileValues).
+ */
+ public void declareSkyframeDependencies(SkyFunction.Environment env) {
+ for (Fragment fragment : fragments.values()) {
+ fragment.declareSkyframeDependencies(env);
+ }
+ }
+
+ /**
+ * Returns all the roots for this configuration.
+ */
+ public List<Root> getRoots() {
+ List<Root> roots = new ArrayList<>();
+
+ // Configuration-specific roots.
+ roots.add(getBinDirectory());
+ roots.add(getGenfilesDirectory());
+ roots.add(getIncludeDirectory());
+ roots.add(getMiddlemanDirectory());
+ roots.add(getTestLogsDirectory());
+
+ // Fragment-defined roots
+ for (Fragment fragment : fragments.values()) {
+ fragment.addRoots(roots);
+ }
+
+ return ImmutableList.copyOf(roots);
+ }
+
+ public ListMultimap<String, Label> getAllLabels() {
+ return buildOptions.getAllLabels();
+ }
+
+ public String getCpu() {
+ return options.cpu;
+ }
+
+ /**
+ * Returns true is incremental builds are supported with this configuration.
+ */
+ public boolean supportsIncrementalBuild() {
+ for (Fragment fragment : fragments.values()) {
+ if (!fragment.supportsIncrementalBuild()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Returns true if the configuration performs static linking.
+ */
+ public boolean performsStaticLink() {
+ for (Fragment fragment : fragments.values()) {
+ if (fragment.performsStaticLink()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Deletes temporary directories before execution phase. This is only called for
+ * target configuration.
+ */
+ public void prepareForExecutionPhase() throws IOException {
+ for (Fragment fragment : fragments.values()) {
+ fragment.prepareForExecutionPhase();
+ }
+ }
+
+ /**
+ * Collects executables defined by fragments.
+ */
+ private ImmutableMap<String, PathFragment> collectExecutables() {
+ ImmutableMap.Builder<String, PathFragment> builder = new ImmutableMap.Builder<>();
+ for (Fragment fragment : fragments.values()) {
+ fragment.defineExecutables(builder);
+ }
+ return builder.build();
+ }
+
+ /**
+ * See {@code BuildConfigurationCollection.Transitions.getArtifactOwnerConfiguration()}.
+ */
+ public BuildConfiguration getArtifactOwnerConfiguration() {
+ return transitions.getArtifactOwnerConfiguration();
+ }
+
+ /**
+ * @return whether proto header modules should be built.
+ */
+ public boolean getProtoHeaderModules() {
+ return options.protoHeaderModules;
+ }
+
+ /**
+ * @return the list of default features used for all packages.
+ */
+ public List<String> getDefaultFeatures() {
+ return options.defaultFeatures;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/config/BuildConfigurationCollection.java b/src/main/java/com/google/devtools/build/lib/analysis/config/BuildConfigurationCollection.java
new file mode 100644
index 0000000..e36a681
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/config/BuildConfigurationCollection.java
@@ -0,0 +1,276 @@
+// Copyright 2014 Google Inc. 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.analysis.config;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ListMultimap;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.packages.Attribute;
+import com.google.devtools.build.lib.packages.Attribute.SplitTransition;
+import com.google.devtools.build.lib.packages.Attribute.Transition;
+import com.google.devtools.build.lib.packages.InputFile;
+import com.google.devtools.build.lib.packages.PackageGroup;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.packages.Target;
+
+import java.io.PrintStream;
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * The primary container for all main {@link BuildConfiguration} instances,
+ * currently "target", "data", and "host".
+ *
+ * <p>The target configuration is used for all targets specified on the command
+ * line. Data dependencies of targets in the target configuration use the data
+ * configuration instead.
+ *
+ * <p>The host configuration is used for tools that are executed during the
+ * build, e. g, compilers.
+ *
+ * <p>The "related" configurations are also contained in this class.
+ */
+@ThreadSafe
+public final class BuildConfigurationCollection implements Serializable {
+ private final ImmutableList<BuildConfiguration> targetConfigurations;
+
+ public BuildConfigurationCollection(List<BuildConfiguration> targetConfigurations)
+ throws InvalidConfigurationException {
+ this.targetConfigurations = ImmutableList.copyOf(targetConfigurations);
+
+ // Except for the host configuration (which may be identical across target configs), the other
+ // configurations must all have different cache keys or we will end up with problems.
+ HashMap<String, BuildConfiguration> cacheKeyConflictDetector = new HashMap<>();
+ for (BuildConfiguration config : getAllConfigurations()) {
+ if (cacheKeyConflictDetector.containsKey(config.cacheKey())) {
+ throw new InvalidConfigurationException("Conflicting configurations: " + config + " & "
+ + cacheKeyConflictDetector.get(config.cacheKey()));
+ }
+ cacheKeyConflictDetector.put(config.cacheKey(), config);
+ }
+ }
+
+ /**
+ * Creates an empty configuration collection which will return null for everything.
+ */
+ public BuildConfigurationCollection() {
+ this.targetConfigurations = ImmutableList.of();
+ }
+
+ public static BuildConfiguration configureTopLevelTarget(BuildConfiguration topLevelConfiguration,
+ Target toTarget) {
+ if (toTarget instanceof InputFile || toTarget instanceof PackageGroup) {
+ return null;
+ }
+ return topLevelConfiguration.getTransitions().toplevelConfigurationHook(toTarget);
+ }
+
+ public ImmutableList<BuildConfiguration> getTargetConfigurations() {
+ return targetConfigurations;
+ }
+
+ /**
+ * Returns all configurations that can be reached from the target configuration through any kind
+ * of configuration transition.
+ */
+ public Collection<BuildConfiguration> getAllConfigurations() {
+ Set<BuildConfiguration> result = new LinkedHashSet<>();
+ for (BuildConfiguration config : targetConfigurations) {
+ result.addAll(config.getAllReachableConfigurations());
+ }
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof BuildConfigurationCollection)) {
+ return false;
+ }
+ BuildConfigurationCollection that = (BuildConfigurationCollection) obj;
+ return this.targetConfigurations.equals(that.targetConfigurations);
+ }
+
+ @Override
+ public int hashCode() {
+ return targetConfigurations.hashCode();
+ }
+
+ /**
+ * Prints the configuration graph in dot format to the given print stream. This is only intended
+ * for debugging.
+ */
+ public void dumpAsDotGraph(PrintStream out) {
+ out.println("digraph g {");
+ out.println(" ratio = 0.3;");
+ for (BuildConfiguration config : getAllConfigurations()) {
+ String from = config.shortCacheKey();
+ for (Map.Entry<? extends Transition, ConfigurationHolder> entry :
+ config.getTransitions().getTransitionTable().entrySet()) {
+ BuildConfiguration toConfig = entry.getValue().getConfiguration();
+ if (toConfig == config) {
+ continue;
+ }
+ String to = toConfig == null ? "ERROR" : toConfig.shortCacheKey();
+ out.println(" \"" + from + "\" -> \"" + to + "\" [label=\"" + entry.getKey() + "\"]");
+ }
+ }
+ out.println("}");
+ }
+
+ /**
+ * The outgoing transitions for a build configuration.
+ */
+ public static class Transitions implements Serializable {
+ protected final BuildConfiguration configuration;
+
+ /**
+ * Look up table for the configuration transitions, i.e., HOST, DATA, etc.
+ */
+ private final Map<? extends Transition, ConfigurationHolder> transitionTable;
+
+ // TODO(bazel-team): Consider merging transitionTable into this.
+ private final ListMultimap<? super SplitTransition<?>, BuildConfiguration> splitTransitionTable;
+
+ public Transitions(BuildConfiguration configuration,
+ Map<? extends Transition, ConfigurationHolder> transitionTable,
+ ListMultimap<? extends SplitTransition<?>, BuildConfiguration> splitTransitionTable) {
+ this.configuration = configuration;
+ this.transitionTable = ImmutableMap.copyOf(transitionTable);
+ this.splitTransitionTable = ImmutableListMultimap.copyOf(splitTransitionTable);
+ }
+
+ public Transitions(BuildConfiguration configuration,
+ Map<? extends Transition, ConfigurationHolder> transitionTable) {
+ this(configuration, transitionTable,
+ ImmutableListMultimap.<SplitTransition<?>, BuildConfiguration>of());
+ }
+
+ public Map<? extends Transition, ConfigurationHolder> getTransitionTable() {
+ return transitionTable;
+ }
+
+ public ListMultimap<? super SplitTransition<?>, BuildConfiguration> getSplitTransitionTable() {
+ return splitTransitionTable;
+ }
+
+ public List<BuildConfiguration> getSplitConfigurations(SplitTransition<?> transition) {
+ if (splitTransitionTable.containsKey(transition)) {
+ return splitTransitionTable.get(transition);
+ } else {
+ Preconditions.checkState(transition.defaultsToSelf());
+ return ImmutableList.of(configuration);
+ }
+ }
+
+ /**
+ * Adds all configurations that are directly reachable from this configuration through
+ * any kind of configuration transition.
+ */
+ public void addDirectlyReachableConfigurations(Collection<BuildConfiguration> queue) {
+ for (ConfigurationHolder holder : transitionTable.values()) {
+ if (holder.configuration != null) {
+ queue.add(holder.configuration);
+ }
+ }
+ queue.addAll(splitTransitionTable.values());
+ }
+
+ /**
+ * Artifacts need an owner in Skyframe. By default it's the same configuration as what
+ * the configured target has, but it can be overridden if necessary.
+ *
+ * @return the artifact owner configuration
+ */
+ public BuildConfiguration getArtifactOwnerConfiguration() {
+ return configuration;
+ }
+
+ /**
+ * Returns the new configuration after traversing a dependency edge with a
+ * given configuration transition.
+ *
+ * @param configurationTransition the configuration transition
+ * @return the new configuration
+ */
+ public BuildConfiguration getConfiguration(Transition configurationTransition) {
+ ConfigurationHolder holder = transitionTable.get(configurationTransition);
+ if (holder == null && configurationTransition.defaultsToSelf()) {
+ return configuration;
+ }
+ return holder.configuration;
+ }
+
+ /**
+ * Arbitrary configuration transitions can be implemented by overriding this hook.
+ */
+ @SuppressWarnings("unused")
+ public BuildConfiguration configurationHook(Rule fromTarget,
+ Attribute attribute, Target toTarget, BuildConfiguration toConfiguration) {
+ return toConfiguration;
+ }
+
+ /**
+ * Associating configurations to top-level targets can be implemented by overriding this hook.
+ */
+ @SuppressWarnings("unused")
+ public BuildConfiguration toplevelConfigurationHook(Target toTarget) {
+ return configuration;
+ }
+ }
+
+ /**
+ * A holder class for {@link BuildConfiguration} instances that allows {@code null} values,
+ * because none of the Table implementations allow them.
+ */
+ public static final class ConfigurationHolder implements Serializable {
+ private final BuildConfiguration configuration;
+
+ public ConfigurationHolder(BuildConfiguration configuration) {
+ this.configuration = configuration;
+ }
+
+ public BuildConfiguration getConfiguration() {
+ return configuration;
+ }
+
+ @Override
+ public int hashCode() {
+ return configuration == null ? 0 : configuration.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) {
+ return true;
+ }
+ if (!(o instanceof ConfigurationHolder)) {
+ return false;
+ }
+ return Objects.equals(configuration, ((ConfigurationHolder) o).configuration);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/config/BuildConfigurationKey.java b/src/main/java/com/google/devtools/build/lib/analysis/config/BuildConfigurationKey.java
new file mode 100644
index 0000000..e8fcf34
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/config/BuildConfigurationKey.java
@@ -0,0 +1,92 @@
+// Copyright 2014 Google Inc. 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.analysis.config;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSortedSet;
+import com.google.common.collect.ListMultimap;
+import com.google.devtools.build.lib.analysis.BlazeDirectories;
+import com.google.devtools.build.lib.syntax.Label;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * A key for the creation of {@link BuildConfigurationCollection} instances.
+ */
+public final class BuildConfigurationKey {
+
+ private final BuildOptions buildOptions;
+ private final BlazeDirectories directories;
+ private final Map<String, String> clientEnv;
+ private final ImmutableSortedSet<String> multiCpu;
+
+ /**
+ * Creates a key for the creation of {@link BuildConfigurationCollection} instances.
+ *
+ * Note that the BuildConfiguration.Options instance must not contain unresolved relative paths.
+ */
+ public BuildConfigurationKey(BuildOptions buildOptions, BlazeDirectories directories,
+ Map<String, String> clientEnv, Set<String> multiCpu) {
+ this.buildOptions = Preconditions.checkNotNull(buildOptions);
+ this.directories = Preconditions.checkNotNull(directories);
+ this.clientEnv = ImmutableMap.copyOf(clientEnv);
+ this.multiCpu = ImmutableSortedSet.copyOf(multiCpu);
+ }
+
+ public BuildConfigurationKey(BuildOptions buildOptions, BlazeDirectories directories,
+ Map<String, String> clientEnv) {
+ this(buildOptions, directories, clientEnv, ImmutableSet.<String>of());
+ }
+
+ public BuildOptions getBuildOptions() {
+ return buildOptions;
+ }
+
+ public BlazeDirectories getDirectories() {
+ return directories;
+ }
+
+ public Map<String, String> getClientEnv() {
+ return clientEnv;
+ }
+
+ public ImmutableSortedSet<String> getMultiCpu() {
+ return multiCpu;
+ }
+
+ public ListMultimap<String, Label> getLabelsToLoadUnconditionally() {
+ return buildOptions.getAllLabels();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof BuildConfigurationKey)) {
+ return false;
+ }
+ BuildConfigurationKey k = (BuildConfigurationKey) o;
+ return buildOptions.equals(k.buildOptions)
+ && directories.equals(k.directories)
+ && clientEnv.equals(k.clientEnv)
+ && multiCpu.equals(k.multiCpu);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(buildOptions, directories, clientEnv, multiCpu);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/config/BuildOptions.java b/src/main/java/com/google/devtools/build/lib/analysis/config/BuildOptions.java
new file mode 100644
index 0000000..afe408f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/config/BuildOptions.java
@@ -0,0 +1,254 @@
+// Copyright 2014 Google Inc. 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.analysis.config;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ListMultimap;
+import com.google.devtools.build.lib.packages.Attribute.SplitTransition;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.common.options.Options;
+import com.google.devtools.common.options.OptionsBase;
+import com.google.devtools.common.options.OptionsClassProvider;
+import com.google.devtools.common.options.OptionsParser;
+import com.google.devtools.common.options.OptionsParsingException;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+/**
+ * This is a collection of command-line options from all configuration fragments. Contains
+ * a single instance for all FragmentOptions classes provided by Blaze language modules.
+ */
+public final class BuildOptions implements Cloneable, Serializable {
+ /**
+ * Creates a BuildOptions object with all options set to its default value.
+ */
+ public static BuildOptions createDefaults(Iterable<Class<? extends FragmentOptions>> options) {
+ Builder builder = builder();
+ for (Class<? extends FragmentOptions> optionsClass : options) {
+ builder.add(Options.getDefaults(optionsClass));
+ }
+ return builder.build();
+ }
+
+ /**
+ * This function creates a new BuildOptions instance for host.
+ *
+ * @param fallback if true, we have already tried the user specified hostCpu options
+ * and it didn't work, so now we try the default options instead.
+ */
+ public BuildOptions createHostOptions(boolean fallback) {
+ Builder builder = builder();
+ for (FragmentOptions options : fragmentOptionsMap.values()) {
+ builder.add(options.getHost(fallback));
+ }
+ return builder.build();
+ }
+
+ /**
+ * Returns a list of potential split configuration transitions by calling {@link
+ * FragmentOptions#getPotentialSplitTransitions} on all the fragments.
+ */
+ public List<SplitTransition<BuildOptions>> getPotentialSplitTransitions() {
+ List<SplitTransition<BuildOptions>> result = new ArrayList<>();
+ for (FragmentOptions options : fragmentOptionsMap.values()) {
+ result.addAll(options.getPotentialSplitTransitions());
+ }
+ return result;
+ }
+
+ /**
+ * Creates an BuildOptions class by taking the option values from an options provider
+ * (eg. an OptionsParser).
+ */
+ public static BuildOptions of(List<Class<? extends FragmentOptions>> optionsList,
+ OptionsClassProvider provider) {
+ Builder builder = builder();
+ for (Class<? extends FragmentOptions> optionsClass : optionsList) {
+ builder.add(provider.getOptions(optionsClass));
+ }
+ return builder.build();
+ }
+
+ /**
+ * Creates an BuildOptions class by taking the option values from command-line arguments
+ */
+ @VisibleForTesting
+ public static BuildOptions of(List<Class<? extends FragmentOptions>> optionsList, String... args)
+ throws OptionsParsingException {
+ Builder builder = builder();
+ OptionsParser parser = OptionsParser.newOptionsParser(
+ ImmutableList.<Class<? extends OptionsBase>>copyOf(optionsList));
+ parser.parse(args);
+ for (Class<? extends FragmentOptions> optionsClass : optionsList) {
+ builder.add(parser.getOptions(optionsClass));
+ }
+ return builder.build();
+ }
+
+ /**
+ * Returns the actual instance of a FragmentOptions class.
+ */
+ @SuppressWarnings("unchecked")
+ public <T extends FragmentOptions> T get(Class<T> optionsClass) {
+ FragmentOptions options = fragmentOptionsMap.get(optionsClass);
+ Preconditions.checkNotNull(options);
+ Preconditions.checkArgument(optionsClass.isAssignableFrom(options.getClass()));
+ return (T) options;
+ }
+
+ /**
+ * Returns a multimap of all labels that were specified as options, keyed by the name to be
+ * displayed to the user if something goes wrong. This should be the set of all labels
+ * mentioned in explicit command line options that are not already covered by the
+ * tools/defaults package (see the DefaultsPackage class), and nothing else.
+ */
+ public ListMultimap<String, Label> getAllLabels() {
+ ListMultimap<String, Label> labels = ArrayListMultimap.create();
+ for (FragmentOptions optionsBase : fragmentOptionsMap.values()) {
+ optionsBase.addAllLabels(labels);
+ }
+ return labels;
+ }
+
+ // It would be very convenient to use a Multimap here, but we cannot do that because we need to
+ // support defaults labels that have zero elements.
+ ImmutableMap<String, ImmutableSet<Label>> getDefaultsLabels() {
+ BuildConfiguration.Options opts = get(BuildConfiguration.Options.class);
+ Map<String, Set<Label>> collector = new TreeMap<>();
+ for (FragmentOptions fragment : fragmentOptionsMap.values()) {
+ for (Map.Entry<String, Set<Label>> entry : fragment.getDefaultsLabels(opts).entrySet()) {
+ if (!collector.containsKey(entry.getKey())) {
+ collector.put(entry.getKey(), new TreeSet<Label>());
+ }
+ collector.get(entry.getKey()).addAll(entry.getValue());
+ }
+ }
+
+ ImmutableMap.Builder<String, ImmutableSet<Label>> result = new ImmutableMap.Builder<>();
+ for (Map.Entry<String, Set<Label>> entry : collector.entrySet()) {
+ result.put(entry.getKey(), ImmutableSet.copyOf(entry.getValue()));
+ }
+
+ return result.build();
+ }
+
+ /**
+ * The cache key for the options collection. Recomputes cache key every time it's called.
+ */
+ public String computeCacheKey() {
+ StringBuilder keyBuilder = new StringBuilder();
+ for (FragmentOptions options : fragmentOptionsMap.values()) {
+ keyBuilder.append(options.cacheKey());
+ }
+ return keyBuilder.toString();
+ }
+
+ /**
+ * String representation of build options.
+ */
+ @Override
+ public String toString() {
+ StringBuilder stringBuilder = new StringBuilder();
+ for (FragmentOptions options : fragmentOptionsMap.values()) {
+ stringBuilder.append(options.toString());
+ }
+ return stringBuilder.toString();
+ }
+
+ /**
+ * Returns the options contained in this collection.
+ */
+ public Iterable<FragmentOptions> getOptions() {
+ return fragmentOptionsMap.values();
+ }
+
+ /**
+ * Creates a copy of the BuildOptions object that contains copies of the FragmentOptions.
+ */
+ @Override
+ public BuildOptions clone() {
+ ImmutableMap.Builder<Class<? extends FragmentOptions>, FragmentOptions> builder =
+ ImmutableMap.builder();
+ for (Map.Entry<Class<? extends FragmentOptions>, FragmentOptions> entry :
+ fragmentOptionsMap.entrySet()) {
+ builder.put(entry.getKey(), entry.getValue().clone());
+ }
+ return new BuildOptions(builder.build());
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ return (this == other) || (other instanceof BuildOptions &&
+ fragmentOptionsMap.equals(((BuildOptions) other).fragmentOptionsMap));
+ }
+
+ @Override
+ public int hashCode() {
+ return fragmentOptionsMap.hashCode();
+ }
+
+ /**
+ * Maps options class definitions to FragmentOptions objects
+ */
+ private final ImmutableMap<Class<? extends FragmentOptions>, FragmentOptions> fragmentOptionsMap;
+
+ private BuildOptions(
+ ImmutableMap<Class<? extends FragmentOptions>, FragmentOptions> fragmentOptionsMap) {
+ this.fragmentOptionsMap = fragmentOptionsMap;
+ }
+
+ /**
+ * Creates a builder object for BuildOptions
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * Builder class for BuildOptions.
+ */
+ public static class Builder {
+ /**
+ * Adds a new FragmentOptions instance to the builder. Overrides previous instances of the
+ * exact same subclass of FragmentOptions.
+ */
+ public <T extends FragmentOptions> Builder add(T options) {
+ builderMap.put(options.getClass(), options);
+ return this;
+ }
+
+ public BuildOptions build() {
+ return new BuildOptions(ImmutableMap.copyOf(builderMap));
+ }
+
+ private Map<Class<? extends FragmentOptions>, FragmentOptions> builderMap;
+
+ private Builder() {
+ builderMap = new HashMap<>();
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/config/CompilationMode.java b/src/main/java/com/google/devtools/build/lib/analysis/config/CompilationMode.java
new file mode 100644
index 0000000..7f24351
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/config/CompilationMode.java
@@ -0,0 +1,50 @@
+// Copyright 2014 Google Inc. 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.analysis.config;
+
+import com.google.devtools.common.options.EnumConverter;
+
+/**
+ * This class represents the debug/optimization mode the binaries will be
+ * built for.
+ */
+public enum CompilationMode {
+
+ // Fast build mode (-g0).
+ FASTBUILD("fastbuild"),
+ // Debug mode (-g).
+ DBG("dbg"),
+ // Release mode (-g0 -O2 -DNDEBUG).
+ OPT("opt");
+
+ private final String mode;
+
+ private CompilationMode(String mode) {
+ this.mode = mode;
+ }
+
+ @Override
+ public String toString() {
+ return mode;
+ }
+
+ /**
+ * Converts to {@link CompilationMode}.
+ */
+ public static class Converter extends EnumConverter<CompilationMode> {
+ public Converter() {
+ super(CompilationMode.class, "compilation mode");
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/config/ConfigMatchingProvider.java b/src/main/java/com/google/devtools/build/lib/analysis/config/ConfigMatchingProvider.java
new file mode 100644
index 0000000..c719191
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/config/ConfigMatchingProvider.java
@@ -0,0 +1,54 @@
+// Copyright 2014 Google Inc. 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.analysis.config;
+
+import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.syntax.Label;
+
+/**
+ * A "configuration target" that asserts whether or not it matches the
+ * configuration it's bound to.
+ *
+ * <p>This can be used, e.g., to declare a BUILD target that defines the
+ * conditions which trigger a configurable attribute branch. In general,
+ * this can be used to trigger for any user-configurable build behavior.
+ */
+@Immutable
+public final class ConfigMatchingProvider implements TransitiveInfoProvider {
+
+ private final Label label;
+ private final boolean matches;
+
+ public ConfigMatchingProvider(Label label, boolean matches) {
+ this.label = label;
+ this.matches = matches;
+ }
+
+ /**
+ * The target's label.
+ */
+ public Label label() {
+ return label;
+ }
+
+ /**
+ * Whether or not the configuration criteria defined by this target match
+ * its actual configuration.
+ */
+ public boolean matches() {
+ return matches;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/config/ConfigRuleClasses.java b/src/main/java/com/google/devtools/build/lib/analysis/config/ConfigRuleClasses.java
new file mode 100644
index 0000000..6ee4cce
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/config/ConfigRuleClasses.java
@@ -0,0 +1,204 @@
+// Copyright 2014 Google Inc. 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.analysis.config;
+
+import static com.google.devtools.build.lib.packages.Attribute.attr;
+import static com.google.devtools.build.lib.packages.Type.STRING_DICT;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.analysis.BaseRuleClasses;
+import com.google.devtools.build.lib.analysis.BlazeRule;
+import com.google.devtools.build.lib.analysis.RuleDefinition;
+import com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.Type;
+
+/**
+ * Definitions for rule classes that specify or manipulate configuration settings.
+ *
+ * <p>These are not "traditional" rule classes in that they can't be requested as top-level
+ * targets and don't translate input artifacts into output artifacts. Instead, they affect
+ * how *other* rules work. See individual class comments for details.
+ */
+public class ConfigRuleClasses {
+
+ private static final String NONCONFIGURABLE_ATTRIBUTE_REASON =
+ "part of a rule class that *triggers* configurable behavior";
+
+ /**
+ * Common settings for all configurability rules.
+ */
+ @BlazeRule(name = "$config_base_rule",
+ type = RuleClass.Builder.RuleClassType.ABSTRACT,
+ ancestors = { BaseRuleClasses.BaseRule.class })
+ public static final class ConfigBaseRule implements RuleDefinition {
+ @Override
+ public RuleClass build(RuleClass.Builder builder, RuleDefinitionEnvironment env) {
+ return builder
+ .override(attr("tags", Type.STRING_LIST)
+ // No need to show up in ":all", etc. target patterns.
+ .value(ImmutableList.of("manual"))
+ .nonconfigurable(NONCONFIGURABLE_ATTRIBUTE_REASON))
+ .build();
+ }
+ }
+
+ /**
+ * A named "partial configuration setting" that specifies a set of command-line
+ * "flag=value" bindings.
+ *
+ * <p>For example:
+ * <pre>
+ * config_setting(
+ * name = 'foo',
+ * values = {
+ * 'flag1': 'aValue'
+ * 'flag2': 'bValue'
+ * })
+ * </pre>
+ *
+ * <p>declares a setting that binds command-line flag <pre>flag1</pre> to value
+ * <pre>aValue</pre> and <pre>flag2</pre> to <pre>bValue</pre>.
+ *
+ * <p>This is used by configurable attributes to determine which branch to
+ * follow based on which <pre>config_setting</pre> instance matches all its
+ * flag values in the configurable attribute owner's configuration.
+ *
+ * <p>This rule isn't accessed through the standard {@link RuleContext#getPrerequisites}
+ * interface. This is because Bazel constructs a rule's configured attribute map *before*
+ * its {@link RuleContext} is created (in fact, the map is an input to the context's
+ * constructor). And the config_settings referenced by the rule's configurable attributes are
+ * themselves inputs to that map. So Bazel has special logic to read and properly apply
+ * config_setting instances. See {@link ConfiguredTargetFunction#getConfigConditions} for details.
+ */
+ @BlazeRule(name = "config_setting",
+ type = RuleClass.Builder.RuleClassType.NORMAL,
+ ancestors = { ConfigBaseRule.class },
+ factoryClass = ConfigSetting.class)
+ public static final class ConfigSettingRule implements RuleDefinition {
+ /**
+ * The name of the attribute that declares flag bindings.
+ */
+ public static final String SETTINGS_ATTRIBUTE = "values";
+
+ @Override
+ public RuleClass build(RuleClass.Builder builder, RuleDefinitionEnvironment env) {
+ return builder
+ /* <!-- #BLAZE_RULE(config_setting).ATTRIBUTE(values) -->
+ The set of configuration values that match this rule (expressed as Blaze flags)
+
+ <i>(Dictionary mapping flags to expected values, both expressed as strings;
+ mandatory)</i>
+
+ <p>This rule inherits the configuration of the configured target that
+ references it in a <code>select</code> statement. It is considered to
+ "match" a Blaze invocation if, for every entry in the dictionary, its
+ configuration matches the entry's expected value. For example
+ <code>values = {"compilation_mode": "opt"}</code> matches the invocations
+ <code>blaze build --compilation_mode=opt ...</code> and
+ <code>blaze build -c opt ...</code> on target-configured rules.
+ </p>
+
+ <p>For convenience's sake, configuration values are specified as Blaze flags (without
+ the preceding <code>"--"</code>). But keep in mind that the two are not the same. This
+ is because targets can be built in multiple configurations within the same
+ build. For example, a host configuration's "cpu" matches the value of
+ <code>--host_cpu</code>, not <code>--cpu</code>. So different instances of the
+ same <code>config_setting</code> may match the same invocation differently
+ depending on the configuration of the rule using them.
+ </p>
+
+ <p>If a flag is not explicitly set at the command line, its default value is used.
+ If a key appears multiple times in the dictionary, only the last instance is used.
+ If a key references a flag that can be set multiple times on the command line (e.g.
+ <code>blaze build --copt=foo --copt=bar --copt=baz ...</code>), a match occurs if
+ *any* of those settings match.
+ <p>
+
+ <p>This attribute cannot be empty.
+ </p>
+ <!-- #END_BLAZE_RULE.ATTRIBUTE --> */
+ .add(attr(SETTINGS_ATTRIBUTE, STRING_DICT).mandatory()
+ .nonconfigurable(NONCONFIGURABLE_ATTRIBUTE_REASON))
+ .build();
+ }
+ }
+
+/*<!-- #BLAZE_RULE (NAME = config_setting, TYPE = OTHER, FAMILY = General)[GENERIC_RULE] -->
+
+${ATTRIBUTE_SIGNATURE}
+
+<p>
+ Matches an expected configuration state (expressed as Blaze flags) for the purpose of triggering
+ configurable attributes. See <a href="#select">select</a> for how to consume this rule and
+ <a href="#configurable-attributes">Configurable attributes</a> for an overview of
+ the general feature.
+
+${ATTRIBUTE_DEFINITION}
+
+<h4 id="config_setting_examples">Examples</h4>
+
+<p>The following matches any Blaze invocation that specifies <code>--compilation_mode=opt</code>
+ or <code>-c opt</code> (either explicitly at the command line or implicitly from .blazerc
+ files, etc.), when applied to a target configuration rule:
+</p>
+
+<pre class="code">
+config_setting(
+ name = "simple",
+ values = {"compilation_mode": "opt"}
+)
+</pre>
+
+<p>The following matches any Blaze invocation that builds for ARM and applies a custom define
+ (e.g. <code>blaze build --cpu=armeabi --define FOO=bar ...</code>), , when applied to a target
+ configuration rule:
+</p>
+
+<pre class="code">
+config_setting(
+ name = "two_conditions",
+ values = {
+ "cpu": "armeabi",
+ "define": "FOO=bar"
+ }
+)
+</pre>
+
+<h4 id="config_setting_notes">Notes</h4>
+
+<p>See <a href="#select">select</a> for policies on what happens depending on how many rules match
+ an invocation.
+</p>
+
+<p>For flags that support shorthand forms (e.g. <code>--compilation_mode</code> vs.
+ <code>-c</code>), <code>values</code> definitions must use the full form. These automatically
+ match invocations using either form.
+</p>
+
+<p>The currently endorsed method for creating custom conditions that can't be expressed through
+ dedicated build flags is through the --define flag. Use this flag with caution: it's not ideal
+ and only endorsed for lack of a currently better workaround. See the
+ <a href="#configurable-attributes">Configurable attributes</a> section for more discussion.
+</p>
+
+<p>Try to consolidate <code>config_setting</code> definitions as much as possible. In other words,
+ define <code>//common/conditions:foo</code> in one common package instead of repeating separate
+ instances in <code>//project1:foo</code>, <code>//project2:foo</code>, etc. that all mean the
+ same thing.
+</p>
+
+<!-- #END_BLAZE_RULE -->*/
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/config/ConfigSetting.java b/src/main/java/com/google/devtools/build/lib/analysis/config/ConfigSetting.java
new file mode 100644
index 0000000..97ad4ff
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/config/ConfigSetting.java
@@ -0,0 +1,170 @@
+// Copyright 2014 Google Inc. 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.analysis.config;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.FileProvider;
+import com.google.devtools.build.lib.analysis.FilesToRunProvider;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTargetBuilder;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.RunfilesProvider;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.packages.NonconfigurableAttributeMapper;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.rules.RuleConfiguredTargetFactory;
+import com.google.devtools.common.options.OptionsBase;
+import com.google.devtools.common.options.OptionsParser;
+import com.google.devtools.common.options.OptionsParsingException;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Implementation for the config_setting rule.
+ *
+ * <p>This is a "pseudo-rule" in that its purpose isn't to generate output artifacts
+ * from input artifacts. Rather, it provides configuration context to rules that
+ * depend on it.
+ */
+public class ConfigSetting implements RuleConfiguredTargetFactory {
+
+ @Override
+ public ConfiguredTarget create(RuleContext ruleContext) throws InterruptedException {
+ // Get the required flag=value settings for this rule.
+ Map<String, String> settings = NonconfigurableAttributeMapper.of(ruleContext.getRule())
+ .get(ConfigRuleClasses.ConfigSettingRule.SETTINGS_ATTRIBUTE, Type.STRING_DICT);
+ if (settings.isEmpty()) {
+ ruleContext.attributeError(ConfigRuleClasses.ConfigSettingRule.SETTINGS_ATTRIBUTE,
+ "no settings specified");
+ return null;
+ }
+
+ ConfigMatchingProvider configMatcher;
+ try {
+ configMatcher = new ConfigMatchingProvider(ruleContext.getLabel(),
+ matchesConfig(settings, ruleContext.getConfiguration()));
+ } catch (OptionsParsingException e) {
+ ruleContext.attributeError(ConfigRuleClasses.ConfigSettingRule.SETTINGS_ATTRIBUTE,
+ "error while parsing configuration settings: " + e.getMessage());
+ return null;
+ }
+
+ return new RuleConfiguredTargetBuilder(ruleContext)
+ .add(RunfilesProvider.class, RunfilesProvider.EMPTY)
+ .add(FileProvider.class, new FileProvider(ruleContext.getLabel(),
+ NestedSetBuilder.<Artifact>emptySet(Order.STABLE_ORDER)))
+ .add(FilesToRunProvider.class, new FilesToRunProvider(ruleContext.getLabel(),
+ ImmutableList.<Artifact>of(), null, null))
+ .add(ConfigMatchingProvider.class, configMatcher)
+ .build();
+ }
+
+ /**
+ * Given a list of [flagName, flagValue] pairs, returns true if flagName == flagValue for
+ * every item in the list under this configuration, false otherwise.
+ */
+ private boolean matchesConfig(Map<String, String> expectedSettings, BuildConfiguration config)
+ throws OptionsParsingException {
+ // Rather than returning fast when we find a mismatch, continue looking at the other flags
+ // to check that they're indeed valid flag specifications.
+ boolean foundMismatch = false;
+
+ // Since OptionsParser instantiation involves reflection, let's try to minimize that happening.
+ Map<Class<? extends OptionsBase>, OptionsParser> parserCache = new HashMap<>();
+
+ for (Map.Entry<String, String> setting : expectedSettings.entrySet()) {
+ String optionName = setting.getKey();
+ String expectedRawValue = setting.getValue();
+
+ Class<? extends OptionsBase> optionClass = config.getOptionClass(optionName);
+ if (optionClass == null) {
+ throw new OptionsParsingException("unknown option: '" + optionName + "'");
+ }
+
+ OptionsParser parser = parserCache.get(optionClass);
+ if (parser == null) {
+ parser = OptionsParser.newOptionsParser(optionClass);
+ parserCache.put(optionClass, parser);
+ }
+ parser.parse("--" + optionName + "=" + expectedRawValue);
+ Object expectedParsedValue = parser.getOptions(optionClass).asMap().get(optionName);
+
+ if (!optionMatches(config, optionName, expectedParsedValue)) {
+ foundMismatch = true;
+ }
+ }
+ return !foundMismatch;
+ }
+
+ /**
+ * For single-value options, returns true iff the option's value matches the expected value.
+ *
+ * <p>For multi-value List options, returns true iff any of the option's values matches
+ * the expected value. This means, e.g. "--tool_tag=foo --tool_tag=bar" would match the
+ * expected condition { 'tool_tag': 'bar' }.
+ *
+ * <p>For multi-value Map options, returns true iff the last instance with the same key as the
+ * expected key has the same value. This means, e.g. "--define foo=1 --define bar=2" would
+ * match { 'define': 'foo=1' }, but "--define foo=1 --define bar=2 --define foo=3" would not
+ * match. Note that the definition of --define states that the last instance takes precedence.
+ */
+ private static boolean optionMatches(BuildConfiguration config, String optionName,
+ Object expectedValue) {
+ Object actualValue = config.getOptionValue(optionName);
+ if (actualValue == null) {
+ return expectedValue == null;
+
+ // Single-value case:
+ } else if (!config.allowsMultipleValues(optionName)) {
+ return actualValue.equals(expectedValue);
+ }
+
+ // Multi-value case:
+ Preconditions.checkState(actualValue instanceof List);
+ Preconditions.checkState(expectedValue instanceof List);
+ List<?> actualList = (List<?>) actualValue;
+ List<?> expectedList = (List<?>) expectedValue;
+
+ if (actualList.isEmpty() || expectedList.isEmpty()) {
+ return actualList.isEmpty() && expectedList.isEmpty();
+ }
+
+ // We're expecting a single value of a multi-value type: the options parser still embeds
+ // that single value within a List container. Retrieve it here.
+ Object expectedSingleValue = Iterables.getOnlyElement(expectedList);
+
+ // Multi-value map:
+ if (actualList.get(0) instanceof Map.Entry) {
+ Map.Entry<?, ?> expectedEntry = (Map.Entry<?, ?>) expectedSingleValue;
+ for (Map.Entry<?, ?> actualEntry : Lists.reverse((List<Map.Entry<?, ?>>) actualList)) {
+ if (actualEntry.getKey().equals(expectedEntry.getKey())) {
+ // Found a key match!
+ return actualEntry.getValue().equals(expectedEntry.getValue());
+ }
+ }
+ return false; // Never found any matching key.
+ }
+
+ // Multi-value list:
+ return actualList.contains(expectedSingleValue);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/config/ConfigurationEnvironment.java b/src/main/java/com/google/devtools/build/lib/analysis/config/ConfigurationEnvironment.java
new file mode 100644
index 0000000..89722b5
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/config/ConfigurationEnvironment.java
@@ -0,0 +1,96 @@
+// Copyright 2014 Google Inc. 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.analysis.config;
+
+import com.google.devtools.build.lib.analysis.BlazeDirectories;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration.Fragment;
+import com.google.devtools.build.lib.packages.NoSuchPackageException;
+import com.google.devtools.build.lib.packages.NoSuchTargetException;
+import com.google.devtools.build.lib.packages.Package;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.pkgcache.LoadedPackageProvider;
+import com.google.devtools.build.lib.pkgcache.PackageProvider;
+import com.google.devtools.build.lib.pkgcache.TargetProvider;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.vfs.Path;
+
+import javax.annotation.Nullable;
+
+/**
+ * An environment to support creating BuildConfiguration instances in a hermetic fashion; all
+ * accesses to packages or the file system <b>must</b> go through this interface, so that they can
+ * be recorded for correct caching.
+ */
+public interface ConfigurationEnvironment {
+
+ /**
+ * Returns a target for the given label, loading it if necessary, and throwing an exception if it
+ * does not exist.
+ *
+ * @see TargetProvider#getTarget
+ */
+ Target getTarget(Label label) throws NoSuchPackageException, NoSuchTargetException;
+
+ /** Returns a path for the given file within the given package. */
+ Path getPath(Package pkg, String fileName);
+
+ /** Returns fragment based on fragment class and build options. */
+ <T extends Fragment> T getFragment(BuildOptions buildOptions, Class<T> fragmentType)
+ throws InvalidConfigurationException;
+
+ /** Returns global value of BlazeDirectories. */
+ @Nullable
+ BlazeDirectories getBlazeDirectories();
+
+ /**
+ * An implementation backed by a {@link PackageProvider} instance.
+ */
+ public static final class TargetProviderEnvironment implements ConfigurationEnvironment {
+
+ private final LoadedPackageProvider loadedPackageProvider;
+ private final BlazeDirectories blazeDirectories;
+
+ public TargetProviderEnvironment(LoadedPackageProvider loadedPackageProvider,
+ BlazeDirectories blazeDirectories) {
+ this.loadedPackageProvider = loadedPackageProvider;
+ this.blazeDirectories = blazeDirectories;
+ }
+
+ public TargetProviderEnvironment(LoadedPackageProvider loadedPackageProvider) {
+ this.loadedPackageProvider = loadedPackageProvider;
+ this.blazeDirectories = null;
+ }
+
+ @Override
+ public Target getTarget(Label label) throws NoSuchPackageException, NoSuchTargetException {
+ return loadedPackageProvider.getLoadedTarget(label);
+ }
+
+ @Override
+ public Path getPath(Package pkg, String fileName) {
+ return pkg.getPackageDirectory().getRelative(fileName);
+ }
+
+ @Override
+ public <T extends Fragment> T getFragment(BuildOptions buildOptions, Class<T> fragmentType) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public BlazeDirectories getBlazeDirectories() {
+ return blazeDirectories;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/config/ConfigurationFactory.java b/src/main/java/com/google/devtools/build/lib/analysis/config/ConfigurationFactory.java
new file mode 100644
index 0000000..13afb2a
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/config/ConfigurationFactory.java
@@ -0,0 +1,145 @@
+// Copyright 2014 Google Inc. 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.analysis.config;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSortedMap;
+import com.google.devtools.build.lib.analysis.BlazeDirectories;
+import com.google.devtools.build.lib.analysis.ConfigurationCollectionFactory;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration.Fragment;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadCompatible;
+import com.google.devtools.build.lib.events.EventHandler;
+
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nullable;
+
+/**
+ * A factory class for {@link BuildConfiguration} instances. This is unfortunately more complex,
+ * and should be simplified in the future, if
+ * possible. Right now, creating a {@link BuildConfiguration} instance involves
+ * creating the instance itself and the related configurations; the main method
+ * is {@link #createConfiguration}.
+ *
+ * <p>Avoid calling into this class, and instead use the skyframe infrastructure to obtain
+ * configuration instances.
+ *
+ * <p>Blaze currently relies on the fact that all {@link BuildConfiguration}
+ * instances used in a build can be constructed ahead of time by this class.
+ */
+@ThreadCompatible // safe as long as separate instances are used
+public final class ConfigurationFactory {
+ private final List<ConfigurationFragmentFactory> configurationFragmentFactories;
+ private final ConfigurationCollectionFactory configurationCollectionFactory;
+
+ // A cache of key to configuration instances.
+ private final Cache<String, BuildConfiguration> hostConfigCache =
+ CacheBuilder.newBuilder().softValues().build();
+
+ private boolean performSanityCheck = true;
+
+ public ConfigurationFactory(
+ ConfigurationCollectionFactory configurationCollectionFactory,
+ List<ConfigurationFragmentFactory> fragmentFactories) {
+ this.configurationCollectionFactory =
+ Preconditions.checkNotNull(configurationCollectionFactory);
+ this.configurationFragmentFactories = fragmentFactories;
+ }
+
+ @VisibleForTesting
+ public void forbidSanityCheck() {
+ performSanityCheck = false;
+ }
+
+ /** Create the build configurations with the given options. */
+ @Nullable
+ public BuildConfiguration createConfiguration(
+ PackageProviderForConfigurations loadedPackageProvider, BuildOptions buildOptions,
+ BuildConfigurationKey key, EventHandler errorEventListener)
+ throws InvalidConfigurationException {
+ return configurationCollectionFactory.createConfigurations(this,
+ loadedPackageProvider, buildOptions, key.getClientEnv(),
+ errorEventListener, performSanityCheck);
+ }
+
+ /**
+ * Returns a (possibly new) canonical host BuildConfiguration instance based
+ * upon a given request configuration
+ */
+ @Nullable
+ public BuildConfiguration getHostConfiguration(
+ PackageProviderForConfigurations loadedPackageProvider, Map<String, String> clientEnv,
+ BuildOptions buildOptions, boolean fallback) throws InvalidConfigurationException {
+ return getConfiguration(loadedPackageProvider, buildOptions.createHostOptions(fallback),
+ clientEnv, false, hostConfigCache);
+ }
+
+ /**
+ * The core of BuildConfiguration creation. All host and target instances are
+ * constructed and cached here.
+ */
+ @Nullable
+ public BuildConfiguration getConfiguration(PackageProviderForConfigurations loadedPackageProvider,
+ BuildOptions buildOptions, Map<String, String> clientEnv,
+ boolean actionsDisabled, Cache<String, BuildConfiguration> cache)
+ throws InvalidConfigurationException {
+
+ Map<Class<? extends Fragment>, Fragment> fragments = new HashMap<>();
+ // Create configuration fragments
+ for (ConfigurationFragmentFactory factory : configurationFragmentFactories) {
+ Class<? extends Fragment> fragmentType = factory.creates();
+ Fragment fragment = loadedPackageProvider.getFragment(buildOptions, fragmentType);
+ if (fragment != null && fragments.get(fragment) == null) {
+ fragments.put(fragment.getClass(), fragment);
+ }
+ }
+ BlazeDirectories directories = loadedPackageProvider.getDirectories();
+ if (loadedPackageProvider.valuesMissing()) {
+ return null;
+ }
+
+ // Sort the fragments by class name to make sure that the order is stable. Afterwards, copy to
+ // an ImmutableMap, which keeps the order stable, but uses hashing, and drops the reference to
+ // the Comparator object.
+ fragments = ImmutableSortedMap.copyOf(fragments, new Comparator<Class<? extends Fragment>>() {
+ @Override
+ public int compare(Class<? extends Fragment> o1, Class<? extends Fragment> o2) {
+ return o1.getName().compareTo(o2.getName());
+ }
+ });
+ fragments = ImmutableMap.copyOf(fragments);
+
+ String key = BuildConfiguration.computeCacheKey(
+ directories, fragments, buildOptions, clientEnv);
+ BuildConfiguration configuration = cache.getIfPresent(key);
+ if (configuration == null) {
+ configuration = new BuildConfiguration(directories, fragments, buildOptions,
+ clientEnv, actionsDisabled);
+ cache.put(key, configuration);
+ }
+ return configuration;
+ }
+
+ public List<ConfigurationFragmentFactory> getFactories() {
+ return configurationFragmentFactories;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/config/ConfigurationFragmentFactory.java b/src/main/java/com/google/devtools/build/lib/analysis/config/ConfigurationFragmentFactory.java
new file mode 100644
index 0000000..8ca8f1c
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/config/ConfigurationFragmentFactory.java
@@ -0,0 +1,39 @@
+// Copyright 2014 Google Inc. 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.analysis.config;
+
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration.Fragment;
+
+import javax.annotation.Nullable;
+
+/**
+ * A factory that creates configuration fragments.
+ */
+public interface ConfigurationFragmentFactory {
+ /**
+ * Creates a configuration fragment.
+ *
+ * @param env the ConfigurationEnvironment for querying targets and paths
+ * @param buildOptions command-line options (see {@link FragmentOptions})
+ * @return the configuration fragment or null if some required dependencies are missing.
+ */
+ @Nullable
+ BuildConfiguration.Fragment create(ConfigurationEnvironment env, BuildOptions buildOptions)
+ throws InvalidConfigurationException;
+
+ /**
+ * @return the exact type of the fragment this factory creates.
+ */
+ Class<? extends Fragment> creates();
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/config/DefaultsPackage.java b/src/main/java/com/google/devtools/build/lib/analysis/config/DefaultsPackage.java
new file mode 100644
index 0000000..207d49a
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/config/DefaultsPackage.java
@@ -0,0 +1,164 @@
+// Copyright 2014 Google Inc. 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.analysis.config;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.syntax.Label.SyntaxException;
+
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A helper class to compute and inject a defaults package into the package cache.
+ *
+ * <p>The <code>//tools/defaults</code> package provides a mechanism let tool locations be
+ * specified over the commandline, without requiring any special support in the rule code.
+ * As such, it can be used in genrule <code>$(location)</code> substitutions.
+ *
+ * <p>It works as follows:
+ * <ul>
+ *
+ * <li> SomeLanguage.createCompileAction will refer to a host-configured target for the
+ * compiler by looking for
+ * <code>env.getHostPrerequisiteArtifact("$somelanguage_compiler")</code>.
+ *
+ * <li> the attribute <code>$somelanguage_compiler</code> is defined in the
+ * {@link RuleDefinition} subclass for that language.
+ *
+ * <li> if the attribute cannot be set on the command-line, its value may be a normal label.
+ *
+ * <li> if the attribute can be set on the command-line, its value will be
+ * <code>//tools/defaults:somelanguage_compiler</code>.
+ *
+ * <li> in the latter case, the {@link BuildConfiguration.Fragment} subclass will define the
+ * option (with an existing target, eg. <code>//third_party/somelanguage:compiler</code>), and
+ * return the name in its implementation of {@link FragmentOptions#getDefaultsLabels}.
+ *
+ * <li> On startup, the rule is wired up with <code>//tools/defaults:somelanguage_compiler</code>.
+ *
+ * <li> On starting a build, the <code>//tools/defaults</code> package is synthesized, using
+ * the values as specified on the command-line. The contents of
+ * <code>tools/defaults/BUILD</code> is ignored.
+ *
+ * <li> Hence, changes in the command line values for tools are now handled exactly as if they
+ * were changes in a BUILD file.
+ *
+ * <li> The file <code>tools/defaults/BUILD</code> must exist, so we create a package in that
+ * location.
+ *
+ * <li> The code in {@link DefaultsPackage} can dump the synthesized package as a BUILD file,
+ * so external tooling does not need to understand the intricacies of handling command-line
+ * options.
+ *
+ * </ul>
+ *
+ * <p>For built-in rules (as opposed to genrules), late-bound labels provide an alternative
+ * method of depending on command-line values. These work by declaring attribute default values
+ * to be {@link LateBoundLabel} instances, whose <code>getDefault(Rule rule, T
+ * configuration)</code> method will have access to {@link BuildConfiguration}, which in turn
+ * may depend on command line flag values.
+ */
+public final class DefaultsPackage {
+
+ // The template contents are broken into lines such that the resulting file has no more than 80
+ // characters per line.
+ private static final String HEADER = ""
+ + "# DO NOT EDIT THIS FILE!\n"
+ + "#\n"
+ + "# Bazel does not read this file. Instead, it internally replaces the targets in\n"
+ + "# this package with the correct packages as given on the command line.\n"
+ + "#\n"
+ + "# If these options are not given on the command line, Bazel will use the exact\n"
+ + "# same targets as given here."
+ + "\n"
+ + "package(default_visibility = ['//visibility:public'])\n";
+
+ /**
+ * The map from entries to their values.
+ */
+ private ImmutableMap<String, ImmutableSet<Label>> values;
+
+ private DefaultsPackage(BuildOptions buildOptions) {
+ values = buildOptions.getDefaultsLabels();
+ }
+
+ private String labelsToString(Set<Label> labels) {
+ StringBuffer result = new StringBuffer();
+ for (Label label : labels) {
+ if (result.length() != 0) {
+ result.append(", ");
+ }
+ result.append("'").append(label).append("'");
+ }
+ return result.toString();
+ }
+
+ /**
+ * Returns a string of the defaults package with the given settings.
+ */
+ private String getContent() {
+ Preconditions.checkState(!values.isEmpty());
+ StringBuilder result = new StringBuilder(HEADER);
+ for (Map.Entry<String, ImmutableSet<Label>> entry : values.entrySet()) {
+ result
+ .append("filegroup(name = '")
+ .append(entry.getKey().toLowerCase(Locale.US)).append("',\n")
+ .append(" srcs = [")
+ .append(labelsToString(entry.getValue())).append("])\n");
+ }
+ return result.toString();
+ }
+
+ /**
+ * Returns the defaults package for the default settings.
+ */
+ public static String getDefaultsPackageContent(
+ Iterable<Class<? extends FragmentOptions>> options) {
+ return getDefaultsPackageContent(BuildOptions.createDefaults(options));
+ }
+
+ /**
+ * Returns the defaults package for the given options.
+ */
+ public static String getDefaultsPackageContent(BuildOptions buildOptions) {
+ return new DefaultsPackage(buildOptions).getContent();
+ }
+
+ public static void parseAndAdd(Set<Label> labels, String optionalLabel) {
+ if (optionalLabel != null) {
+ Label label = parseOptionalLabel(optionalLabel);
+ if (label != null) {
+ labels.add(label);
+ }
+ }
+ }
+
+ public static Label parseOptionalLabel(String value) {
+ if (value.startsWith("//")) {
+ try {
+ return Label.parseAbsolute(value);
+ } catch (SyntaxException e) {
+ // We ignore this exception here - it will cause an error message at a later time.
+ return null;
+ }
+ } else {
+ return null;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/config/FragmentOptions.java b/src/main/java/com/google/devtools/build/lib/analysis/config/FragmentOptions.java
new file mode 100644
index 0000000..ce4b2d2
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/config/FragmentOptions.java
@@ -0,0 +1,115 @@
+// Copyright 2014 Google Inc. 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.analysis.config;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Multimap;
+import com.google.devtools.build.lib.packages.Attribute.SplitTransition;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.syntax.Label.SyntaxException;
+import com.google.devtools.common.options.Options;
+import com.google.devtools.common.options.OptionsBase;
+
+import java.io.Serializable;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Command-line build options for a Blaze module.
+ */
+public abstract class FragmentOptions extends OptionsBase implements Cloneable, Serializable {
+
+ /**
+ * Adds all labels defined by the options to a multimap. See {@code BuildOptions.getAllLabels()}.
+ *
+ * <p>There should generally be no code duplication between this code and DefaultsPackage. Either
+ * the labels are loaded unconditionally using this method, or they are added as magic labels
+ * using the tools/defaults package, but not both.
+ *
+ * @param labelMap a mutable multimap to which the labels of this fragment should be added
+ */
+ public void addAllLabels(Multimap<String, Label> labelMap) {
+ }
+
+ /**
+ * Returns the labels contributed to the defaults package by this fragment.
+ *
+ * <p>The set of keys returned by this function should be constant, however, the values are
+ * allowed to change depending on the value of the options.
+ */
+ @SuppressWarnings("unused")
+ public Map<String, Set<Label>> getDefaultsLabels(BuildConfiguration.Options commonOptions) {
+ return ImmutableMap.of();
+ }
+
+ /**
+ * Returns a list of potential split configuration transitions for this fragment. Split
+ * configurations usually need to be explicitly enabled by passing in an option.
+ */
+ public List<SplitTransition<BuildOptions>> getPotentialSplitTransitions() {
+ return ImmutableList.of();
+ }
+
+ @Override
+ public FragmentOptions clone() {
+ try {
+ return (FragmentOptions) super.clone();
+ } catch (CloneNotSupportedException e) {
+ // This can't happen.
+ throw new IllegalStateException(e);
+ }
+ }
+
+ /**
+ * Creates a new FragmentOptions instance with all flags set to default.
+ */
+ public FragmentOptions getDefault() {
+ return Options.getDefaults(getClass());
+ }
+
+ /**
+ * Creates a new FragmentOptions instance with flags adjusted to host platform.
+ *
+ * @param fallback see {@code BuildOptions.createHostOptions}
+ */
+ @SuppressWarnings("unused")
+ public FragmentOptions getHost(boolean fallback) {
+ return getDefault();
+ }
+
+ protected void addOptionalLabel(Multimap<String, Label> map, String key, String value) {
+ Label label = parseOptionalLabel(value);
+ if (label != null) {
+ map.put(key, label);
+ }
+ }
+
+ private static Label parseOptionalLabel(String value) {
+ if ((value != null) && value.startsWith("//")) {
+ try {
+ return Label.parseAbsolute(value);
+ } catch (SyntaxException e) {
+ // We ignore this exception here - it will cause an error message at a later time.
+ // TODO(bazel-team): We can use a Converter to check the validity of the crosstoolTop
+ // earlier.
+ return null;
+ }
+ } else {
+ return null;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/config/InvalidConfigurationException.java b/src/main/java/com/google/devtools/build/lib/analysis/config/InvalidConfigurationException.java
new file mode 100644
index 0000000..c39325d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/config/InvalidConfigurationException.java
@@ -0,0 +1,33 @@
+// Copyright 2014 Google Inc. 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.analysis.config;
+
+/**
+ * Thrown if the configuration options lead to an invalid configuration, or if any of the
+ * configuration labels cannot be loaded.
+ */
+public class InvalidConfigurationException extends Exception {
+
+ public InvalidConfigurationException(String message) {
+ super(message);
+ }
+
+ public InvalidConfigurationException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public InvalidConfigurationException(Throwable cause) {
+ this(cause.getMessage(), cause);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/config/PackageProviderForConfigurations.java b/src/main/java/com/google/devtools/build/lib/analysis/config/PackageProviderForConfigurations.java
new file mode 100644
index 0000000..83b4715
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/config/PackageProviderForConfigurations.java
@@ -0,0 +1,49 @@
+// Copyright 2014 Google Inc. 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.analysis.config;
+
+import com.google.devtools.build.lib.analysis.BlazeDirectories;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration.Fragment;
+import com.google.devtools.build.lib.packages.Package;
+import com.google.devtools.build.lib.pkgcache.LoadedPackageProvider;
+import com.google.devtools.build.lib.syntax.Label.SyntaxException;
+
+import java.io.IOException;
+
+/**
+ * Extended LoadedPackageProvider which is used during a creation of BuildConfiguration.Fragments.
+ */
+public interface PackageProviderForConfigurations extends LoadedPackageProvider {
+ /**
+ * Adds dependency to fileName if needed. Used only in skyframe, for creating correct dependencies
+ * for {@link com.google.devtools.build.lib.skyframe.ConfigurationCollectionValue}.
+ */
+ void addDependency(Package pkg, String fileName) throws SyntaxException, IOException;
+
+ /**
+ * Returns fragment based on fragment type and build options.
+ */
+ <T extends Fragment> T getFragment(BuildOptions buildOptions, Class<T> fragmentType)
+ throws InvalidConfigurationException;
+
+ /**
+ * Returns blaze directories and adds dependency to that value.
+ */
+ BlazeDirectories getDirectories();
+
+ /**
+ * Returns true if any dependency is missing (value of some node hasn't been evaluated yet).
+ */
+ boolean valuesMissing();
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/config/PerLabelOptions.java b/src/main/java/com/google/devtools/build/lib/analysis/config/PerLabelOptions.java
new file mode 100644
index 0000000..1e921e5
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/config/PerLabelOptions.java
@@ -0,0 +1,128 @@
+// Copyright 2014 Google Inc. 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.analysis.config;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.util.RegexFilter;
+import com.google.devtools.build.lib.util.RegexFilter.RegexFilterConverter;
+import com.google.devtools.common.options.Converter;
+import com.google.devtools.common.options.OptionsParsingException;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Models options that can be added to a command line when a label matches a
+ * given {@link RegexFilter}.
+ */
+public class PerLabelOptions implements Serializable {
+
+ /** The filter used to match labels */
+ private final RegexFilter regexFilter;
+
+ /** The list of options to add when the filter matches a label */
+ private final List<String> optionsList;
+
+ /**
+ * Converts a String to a {@link PerLabelOptions} object. The syntax of the
+ * string is {@code regex_filter@option_1,option_2,...,option_n}. Where
+ * regex_filter stands for the String representation of a {@link RegexFilter},
+ * and {@code option_1} to {@code option_n} stand for arbitrary command line
+ * options. If an option contains a comma it has to be quoted with a
+ * backslash. Options can contain @. Only the first @ is used to split the
+ * string.
+ */
+ public static class PerLabelOptionsConverter implements Converter<PerLabelOptions> {
+
+ @Override
+ public PerLabelOptions convert(String input) throws OptionsParsingException {
+ int atIndex = input.indexOf('@');
+ RegexFilterConverter converter = new RegexFilter.RegexFilterConverter();
+ if (atIndex < 0) {
+ return new PerLabelOptions(converter.convert(input), ImmutableList.<String> of());
+ } else {
+ String filterPiece = input.substring(0, atIndex);
+ String optionsPiece = input.substring(atIndex + 1);
+ List<String> optionsList = new ArrayList<>();
+ for (String option : optionsPiece.split("(?<!\\\\),")) { // Split on ',' but not on '\,'
+ if (option != null && !option.trim().equals("")) {
+ optionsList.add(option.replace("\\,", ","));
+ }
+ }
+ return new PerLabelOptions(converter.convert(filterPiece), optionsList);
+ }
+ }
+
+ @Override
+ public String getTypeDescription() {
+ return "a comma-separated list of regex expressions with prefix '-' specifying"
+ + " excluded paths followed by an @ and a comma separated list of options";
+ }
+ }
+
+ public PerLabelOptions(RegexFilter regexFilter, List<String> optionsList) {
+ this.regexFilter = regexFilter;
+ this.optionsList = optionsList;
+ }
+
+ /**
+ * @return true if the given label is matched by the {@link RegexFilter}.
+ */
+ public boolean isIncluded(Label label) {
+ return regexFilter.isIncluded(label.toString());
+ }
+
+ /**
+ * @return true if the execution path (which includes the base name of the file)
+ * of the given file is matched by the {@link RegexFilter}.
+ */
+ public boolean isIncluded(Artifact artifact) {
+ return regexFilter.isIncluded(artifact.getExecPathString());
+ }
+
+ /**
+ * Returns the list of options to add to a command line.
+ */
+ public List<String> getOptions() {
+ return optionsList;
+ }
+
+ RegexFilter getRegexFilter() {
+ return regexFilter;
+ }
+
+ @Override
+ public String toString() {
+ return regexFilter + " Options: " + optionsList;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ PerLabelOptions otherOptions =
+ other instanceof PerLabelOptions ? (PerLabelOptions) other : null;
+ return this == other || (otherOptions != null &&
+ this.regexFilter.equals(otherOptions.regexFilter) &&
+ this.optionsList.equals(otherOptions.optionsList));
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(regexFilter, optionsList);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/config/RunUnder.java b/src/main/java/com/google/devtools/build/lib/analysis/config/RunUnder.java
new file mode 100644
index 0000000..a51ea25
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/config/RunUnder.java
@@ -0,0 +1,52 @@
+// Copyright 2014 Google Inc. 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.analysis.config;
+
+import com.google.devtools.build.lib.syntax.Label;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * Components of --run_under option.
+ */
+public interface RunUnder extends Serializable {
+ /**
+ * @return the whole value passed to --run_under option.
+ */
+ String getValue();
+
+ /**
+ * Returns label corresponding to the first word (according to shell
+ * tokenization) passed to --run_under.
+ *
+ * @return if the first word (according to shell tokenization) passed to
+ * --run_under starts with {@code "//"} returns the label
+ * corresponding to that word otherwise {@code null}
+ */
+ Label getLabel();
+
+ /**
+ * @return if the first word (according to shell tokenization) passed to
+ * --run_under starts with {@code "//"} returns {@code null}
+ * otherwise the first word
+ */
+ String getCommand();
+
+ /**
+ * @return everything except the first word (according to shell
+ * tokenization) passed to --run_under.
+ */
+ List<String> getOptions();
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/config/RunUnderConverter.java b/src/main/java/com/google/devtools/build/lib/analysis/config/RunUnderConverter.java
new file mode 100644
index 0000000..1f7b660
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/config/RunUnderConverter.java
@@ -0,0 +1,133 @@
+// Copyright 2014 Google Inc. 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.analysis.config;
+
+import com.google.devtools.build.lib.shell.ShellUtils;
+import com.google.devtools.build.lib.shell.ShellUtils.TokenizationException;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.syntax.Label.SyntaxException;
+import com.google.devtools.common.options.Converter;
+import com.google.devtools.common.options.OptionsParsingException;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * --run_under options converter.
+ */
+public class RunUnderConverter implements Converter<RunUnder> {
+ @Override
+ public RunUnder convert(final String input) throws OptionsParsingException {
+ final List<String> runUnderList = new ArrayList<>();
+ try {
+ ShellUtils.tokenize(runUnderList, input);
+ } catch (TokenizationException e) {
+ throw new OptionsParsingException("Not a valid command prefix " + e.getMessage());
+ }
+ if (runUnderList.isEmpty()) {
+ throw new OptionsParsingException("Empty command");
+ }
+ final String runUnderCommand = runUnderList.get(0);
+ if (runUnderCommand.startsWith("//")) {
+ try {
+ final Label runUnderLabel = Label.parseAbsolute(runUnderCommand);
+ return new RunUnderLabel(input, runUnderLabel, runUnderList);
+ } catch (SyntaxException e) {
+ throw new OptionsParsingException("Not a valid label " + e.getMessage());
+ }
+ } else {
+ return new RunUnderCommand(input, runUnderCommand, runUnderList);
+ }
+ }
+
+ private static final class RunUnderLabel implements RunUnder {
+ private final String input;
+ private final Label runUnderLabel;
+ private final List<String> runUnderList;
+
+ public RunUnderLabel(String input, Label runUnderLabel, List<String> runUnderList) {
+ this.input = input;
+ this.runUnderLabel = runUnderLabel;
+ this.runUnderList = new ArrayList<String>(runUnderList.subList(1, runUnderList.size()));
+ }
+
+ @Override public String getValue() { return input; }
+ @Override public Label getLabel() { return runUnderLabel; }
+ @Override public String getCommand() { return null; }
+ @Override public List<String> getOptions() { return runUnderList; }
+ @Override public String toString() { return input; }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ } else if (other instanceof RunUnderLabel) {
+ RunUnderLabel otherRunUnderLabel = (RunUnderLabel) other;
+ return Objects.equals(input, otherRunUnderLabel.input)
+ && Objects.equals(runUnderLabel, otherRunUnderLabel.runUnderLabel)
+ && Objects.equals(runUnderList, otherRunUnderLabel.runUnderList);
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(input, runUnderLabel, runUnderList);
+ }
+ }
+
+ private static final class RunUnderCommand implements RunUnder {
+ private final String input;
+ private final String runUnderCommand;
+ private final List<String> runUnderList;
+
+ public RunUnderCommand(String input, String runUnderCommand, List<String> runUnderList) {
+ this.input = input;
+ this.runUnderCommand = runUnderCommand;
+ this.runUnderList = new ArrayList<String>(runUnderList.subList(1, runUnderList.size()));
+ }
+
+ @Override public String getValue() { return input; }
+ @Override public Label getLabel() { return null; }
+ @Override public String getCommand() { return runUnderCommand; }
+ @Override public List<String> getOptions() { return runUnderList; }
+ @Override public String toString() { return input; }
+
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ } else if (other instanceof RunUnderCommand) {
+ RunUnderCommand otherRunUnderCommand = (RunUnderCommand) other;
+ return Objects.equals(input, otherRunUnderCommand.input)
+ && Objects.equals(runUnderCommand, otherRunUnderCommand.runUnderCommand)
+ && Objects.equals(runUnderList, otherRunUnderCommand.runUnderList);
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(input, runUnderCommand, runUnderList);
+ }
+ }
+ @Override
+ public String getTypeDescription() {
+ return "a prefix in front of command";
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/constraints/ConstraintSemantics.java b/src/main/java/com/google/devtools/build/lib/analysis/constraints/ConstraintSemantics.java
new file mode 100644
index 0000000..14ac2bc
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/constraints/ConstraintSemantics.java
@@ -0,0 +1,473 @@
+// Copyright 2015 Google Inc. 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.analysis.constraints;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
+import com.google.devtools.build.lib.analysis.constraints.EnvironmentCollection.EnvironmentWithGroup;
+import com.google.devtools.build.lib.packages.AttributeMap;
+import com.google.devtools.build.lib.packages.EnvironmentGroup;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.syntax.Label;
+
+import java.util.Collection;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+/**
+ * Implementation of the semantics of Bazel's constraint specification and enforcement system.
+ *
+ * <p>This is how the system works:
+ *
+ * <p>All build rules can declare which "environments" they can be built for, where an "environment"
+ * is a label instance of an {@link EnvironmentRule} rule declared in a BUILD file. There are
+ * various ways to do this:
+ *
+ * <ul>
+ * <li>Through a "restricted to" attribute setting
+ * ({@link RuleClass#RESTRICTED_ENVIRONMENT_ATTR}). This is the most direct form of
+ * specification - it declares the exact set of environments the rule supports (for its group -
+ * see precise details below).
+ * <li>Through a "compatible with" attribute setting
+ * ({@link RuleClass#COMPATIBLE_ENVIRONMENT_ATTR}. This declares <b>additional</b>
+ * environments a rule supports in addition to "standard" environments that are supported by
+ * default (see below).
+ * <li>Through "default" specifications in {@link EnvironmentGroup} rules. Every environment
+ * belongs to a group of thematically related peers (e.g. "target architectures", "JDK versions",
+ * or "mobile devices"). An environment group's definition includes which of these
+ * environments should be supported "by default" if not otherwise specified by one of the above
+ * mechanisms. In particular, a rule with no environment-related attributes automatically
+ * inherits all defaults.
+ * <li>Through a rule class default ({@link RuleClass.Builder#restrictedTo} and
+ * {@link RuleClass.Builder#compatibleWith}). This overrides global defaults for all instances
+ * of the given rule class. This can be used, for example, to make all *_test rules "testable"
+ * without each instance having to explicitly declare this capability.
+ * </ul>
+ *
+ * <p>Groups exist to model the idea that some environments are related while others have nothing
+ * to do with each other. Say, for example, we want to say a rule works for PowerPC platforms but
+ * not x86. We can do so by setting its "restricted to" attribute to
+ * {@code ['//sample/path:powerpc']}. Because both PowerPC and x86 are in the same
+ * "target architectures" group, this setting removes x86 from the set of supported environments.
+ * But since JDK support belongs to its own group ("JDK versions") it says nothing about which JDK
+ * the rule supports.
+ *
+ * <p>More precisely, if a rule has a "restricted to" value of [A, B, C], this removes support
+ * for all default environments D such that group(D) is in [group(A), group(B), group(C)] AND
+ * D is not in [A, B, C] (in other words, D isn't explicitly opted back in). The rule's full
+ * set of supported environments thus becomes [A, B, C] + all defaults that belong to unrelated
+ * groups.
+ *
+ * <p>If the rule has a "compatible with" value of [E, F, G], these are unconditionally
+ * added to its set of supported environments (in addition to the results from above).
+ *
+ * <p>An environment may not appear in both a rule's "restricted to" and "compatible with" values.
+ * If two environments belong to the same group, they must either both be in "restricted to",
+ * both be in "compatible with", or not explicitly specified.
+ *
+ * <p>Given all the above, constraint enforcement is this: rule A can depend on rule B if, for
+ * every environment A supports, B also supports that environment.
+ */
+public class ConstraintSemantics {
+ private ConstraintSemantics() {
+ }
+
+ /**
+ * Provides a set of default environments for a given environment group.
+ */
+ private interface DefaultsProvider {
+ Collection<Label> getDefaults(EnvironmentGroup group);
+ }
+
+ /**
+ * Provides a group's defaults as specified in the environment group's BUILD declaration.
+ */
+ private static class GroupDefaultsProvider implements DefaultsProvider {
+ @Override
+ public Collection<Label> getDefaults(EnvironmentGroup group) {
+ return group.getDefaults();
+ }
+ }
+
+ /**
+ * Provides a group's defaults, factoring in rule class defaults as specified by
+ * {@link com.google.devtools.build.lib.packages.RuleClass.Builder#compatibleWith}
+ * and {@link com.google.devtools.build.lib.packages.RuleClass.Builder#restrictedTo}.
+ */
+ private static class RuleClassDefaultsProvider implements DefaultsProvider {
+ private final EnvironmentCollection ruleClassDefaults;
+ private final GroupDefaultsProvider groupDefaults;
+
+ RuleClassDefaultsProvider(EnvironmentCollection ruleClassDefaults) {
+ this.ruleClassDefaults = ruleClassDefaults;
+ this.groupDefaults = new GroupDefaultsProvider();
+ }
+
+ @Override
+ public Collection<Label> getDefaults(EnvironmentGroup group) {
+ if (ruleClassDefaults.getGroups().contains(group)) {
+ return ruleClassDefaults.getEnvironments(group);
+ } else {
+ // If there are no rule class defaults for this group, just inherit global defaults.
+ return groupDefaults.getDefaults(group);
+ }
+ }
+ }
+
+ /**
+ * Collects the set of supported environments for a given rule by merging its
+ * restriction-style and compatibility-style environment declarations as specified by
+ * the given attributes. Only includes environments from "known" groups, i.e. the groups
+ * owning the environments explicitly referenced from these attributes.
+ */
+ private static class EnvironmentCollector {
+ private final RuleContext ruleContext;
+ private final String restrictionAttr;
+ private final String compatibilityAttr;
+ private final DefaultsProvider defaultsProvider;
+
+ private final EnvironmentCollection restrictionEnvironments;
+ private final EnvironmentCollection compatibilityEnvironments;
+ private final EnvironmentCollection supportedEnvironments;
+
+ /**
+ * Constructs a new collector on the given attributes.
+ *
+ * @param ruleContext analysis context for the rule
+ * @param restrictionAttr the name of the attribute that declares "restricted to"-style
+ * environments. If the rule doesn't have this attribute, this is considered an
+ * empty declaration.
+ * @param compatibilityAttr the name of the attribute that declares "compatible with"-style
+ * environments. If the rule doesn't have this attribute, this is considered an
+ * empty declaration.
+ * @param defaultsProvider provider for the default environments within a group if not
+ * otherwise overriden by the above attributes
+ */
+ EnvironmentCollector(RuleContext ruleContext, String restrictionAttr, String compatibilityAttr,
+ DefaultsProvider defaultsProvider) {
+ this.ruleContext = ruleContext;
+ this.restrictionAttr = restrictionAttr;
+ this.compatibilityAttr = compatibilityAttr;
+ this.defaultsProvider = defaultsProvider;
+
+ EnvironmentCollection.Builder environmentsBuilder = new EnvironmentCollection.Builder();
+ restrictionEnvironments = collectRestrictionEnvironments(environmentsBuilder);
+ compatibilityEnvironments = collectCompatibilityEnvironments(environmentsBuilder);
+ supportedEnvironments = environmentsBuilder.build();
+ }
+
+ /**
+ * Returns the set of environments supported by this rule, as determined by the
+ * restriction-style attribute, compatibility-style attribute, and group defaults
+ * provider instantiated with this class.
+ */
+ EnvironmentCollection getEnvironments() {
+ return supportedEnvironments;
+ }
+
+ /**
+ * Validity-checks that no group has its environment referenced in both the "compatible with"
+ * and restricted to" attributes. Returns true if all is good, returns false and reports
+ * appropriate errors if there are any problems.
+ */
+ boolean validateEnvironmentSpecifications() {
+ ImmutableCollection<EnvironmentGroup> restrictionGroups = restrictionEnvironments.getGroups();
+ boolean hasErrors = false;
+
+ for (EnvironmentGroup group : compatibilityEnvironments.getGroups()) {
+ if (restrictionGroups.contains(group)) {
+ // To avoid error-spamming the user, when we find a conflict we only report one example
+ // environment from each attribute for that group.
+ Label compatibilityEnv =
+ compatibilityEnvironments.getEnvironments(group).iterator().next();
+ Label restrictionEnv = restrictionEnvironments.getEnvironments(group).iterator().next();
+
+ if (compatibilityEnv.equals(restrictionEnv)) {
+ ruleContext.attributeError(compatibilityAttr, compatibilityEnv
+ + " cannot appear both here and in " + restrictionAttr);
+ } else {
+ ruleContext.attributeError(compatibilityAttr, compatibilityEnv + " and "
+ + restrictionEnv + " belong to the same environment group. They should be declared "
+ + "together either here or in " + restrictionAttr);
+ }
+ hasErrors = true;
+ }
+ }
+
+ return !hasErrors;
+ }
+
+ /**
+ * Adds environments specified in the "restricted to" attribute to the set of supported
+ * environments and returns the environments added.
+ */
+ private EnvironmentCollection collectRestrictionEnvironments(
+ EnvironmentCollection.Builder supportedEnvironments) {
+ return collectEnvironments(restrictionAttr, supportedEnvironments);
+ }
+
+ /**
+ * Adds environments specified in the "compatible with" attribute to the set of supported
+ * environments, along with all defaults from the groups they belong to. Returns these
+ * environments, not including the defaults.
+ */
+ private EnvironmentCollection collectCompatibilityEnvironments(
+ EnvironmentCollection.Builder supportedEnvironments) {
+ EnvironmentCollection compatibilityEnvironments =
+ collectEnvironments(compatibilityAttr, supportedEnvironments);
+ for (EnvironmentGroup group : compatibilityEnvironments.getGroups()) {
+ supportedEnvironments.putAll(group, defaultsProvider.getDefaults(group));
+ }
+ return compatibilityEnvironments;
+ }
+
+ /**
+ * Adds environments specified by the given attribute to the set of supported environments
+ * and returns the environments added.
+ *
+ * <p>If this rule doesn't have the given attributes, returns an empty set.
+ */
+ private EnvironmentCollection collectEnvironments(String attrName,
+ EnvironmentCollection.Builder supportedEnvironments) {
+ if (!ruleContext.getRule().isAttrDefined(attrName, Type.LABEL_LIST)) {
+ return EnvironmentCollection.EMPTY;
+ }
+ EnvironmentCollection.Builder environments = new EnvironmentCollection.Builder();
+ for (TransitiveInfoCollection envTarget :
+ ruleContext.getPrerequisites(attrName, RuleConfiguredTarget.Mode.DONT_CHECK)) {
+ EnvironmentWithGroup envInfo = resolveEnvironment(envTarget);
+ environments.put(envInfo.group(), envInfo.environment());
+ supportedEnvironments.put(envInfo.group(), envInfo.environment());
+ }
+ return environments.build();
+ }
+
+ /**
+ * Returns the environment and its group. An {@link Environment} rule only "supports" one
+ * environment: itself. Extract that from its more generic provider interface and sanity
+ * check that that's in fact what we see.
+ */
+ private static EnvironmentWithGroup resolveEnvironment(TransitiveInfoCollection envRule) {
+ SupportedEnvironmentsProvider prereq =
+ Preconditions.checkNotNull(envRule.getProvider(SupportedEnvironmentsProvider.class));
+ return Iterables.getOnlyElement(prereq.getEnvironments().getGroupedEnvironments());
+ }
+ }
+
+ /**
+ * Returns the set of environments this rule supports, applying the logic described in
+ * {@link ConstraintSemantics}.
+ *
+ * <p>Note this set is <b>not complete</b> - it doesn't include environments from groups we don't
+ * "know about". Environments and groups can be declared in any package. If the rule includes
+ * no references to that package, then it simply doesn't know anything about them. But the
+ * constraint semantics say the rule should support the defaults for that group. We encode this
+ * implicitly: given the returned set, for any group that's not in the set the rule is also
+ * considered to support that group's defaults.
+ *
+ * @param ruleContext analysis context for the rule. A rule error is triggered here if
+ * invalid constraint settings are discovered.
+ * @return the environments this rule supports, not counting defaults "unknown" to this rule
+ * as described above. Returns null if any errors are encountered.
+ */
+ @Nullable
+ public static EnvironmentCollection getSupportedEnvironments(RuleContext ruleContext) {
+ if (!validateAttributes(ruleContext)) {
+ return null;
+ }
+
+ // This rule's rule class defaults (or null if the rule class has no defaults).
+ EnvironmentCollector ruleClassCollector = maybeGetRuleClassDefaults(ruleContext);
+ // Default environments for this rule. If the rule has rule class defaults, this is
+ // those defaults. Otherwise it's the global defaults specified by environment_group
+ // declarations.
+ DefaultsProvider ruleDefaults;
+
+ if (ruleClassCollector != null) {
+ if (!ruleClassCollector.validateEnvironmentSpecifications()) {
+ return null;
+ }
+ ruleDefaults = new RuleClassDefaultsProvider(ruleClassCollector.getEnvironments());
+ } else {
+ ruleDefaults = new GroupDefaultsProvider();
+ }
+
+ EnvironmentCollector ruleCollector = new EnvironmentCollector(ruleContext,
+ RuleClass.RESTRICTED_ENVIRONMENT_ATTR, RuleClass.COMPATIBLE_ENVIRONMENT_ATTR, ruleDefaults);
+ if (!ruleCollector.validateEnvironmentSpecifications()) {
+ return null;
+ }
+
+ EnvironmentCollection supportedEnvironments = ruleCollector.getEnvironments();
+ if (ruleClassCollector != null) {
+ // If we have rule class defaults from groups that aren't referenced from the rule itself,
+ // we need to add them in too to override the global defaults.
+ supportedEnvironments =
+ addUnknownGroupsToCollection(supportedEnvironments, ruleClassCollector.getEnvironments());
+ }
+ return supportedEnvironments;
+ }
+
+ /**
+ * Returns the rule class defaults specified for this rule, or null if there are
+ * no such defaults.
+ */
+ @Nullable
+ private static EnvironmentCollector maybeGetRuleClassDefaults(RuleContext ruleContext) {
+ Rule rule = ruleContext.getRule();
+ String restrictionAttr = RuleClass.DEFAULT_RESTRICTED_ENVIRONMENT_ATTR;
+ String compatibilityAttr = RuleClass.DEFAULT_COMPATIBLE_ENVIRONMENT_ATTR;
+
+ if (rule.isAttrDefined(restrictionAttr, Type.LABEL_LIST)
+ || rule.isAttrDefined(compatibilityAttr, Type.LABEL_LIST)) {
+ return new EnvironmentCollector(ruleContext, restrictionAttr, compatibilityAttr,
+ new GroupDefaultsProvider());
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Adds environments to an {@link EnvironmentCollection} from groups that aren't already
+ * a part of that collection.
+ *
+ * @param environments the collection to add to
+ * @param toAdd the collection to add. All environments in this collection in groups
+ * that aren't represented in {@code environments} are added to {@code environments}.
+ * @return the expanded collection.
+ */
+ private static EnvironmentCollection addUnknownGroupsToCollection(
+ EnvironmentCollection environments, EnvironmentCollection toAdd) {
+ EnvironmentCollection.Builder builder = new EnvironmentCollection.Builder();
+ builder.putAll(environments);
+ for (EnvironmentGroup candidateGroup : toAdd.getGroups()) {
+ if (!environments.getGroups().contains(candidateGroup)) {
+ builder.putAll(candidateGroup, toAdd.getEnvironments(candidateGroup));
+ }
+ }
+ return builder.build();
+ }
+
+ /**
+ * Validity-checks this rule's constraint-related attributes. Returns true if all is good,
+ * returns false and reports appropriate errors if there are any problems.
+ */
+ private static boolean validateAttributes(RuleContext ruleContext) {
+ AttributeMap attributes = ruleContext.attributes();
+
+ // Report an error if "restricted to" is explicitly set to nothing. Even if this made
+ // conceptual sense, we don't know which groups we should apply that to.
+ String restrictionAttr = RuleClass.RESTRICTED_ENVIRONMENT_ATTR;
+ List<? extends TransitiveInfoCollection> restrictionEnvironments = ruleContext
+ .getPrerequisites(restrictionAttr, RuleConfiguredTarget.Mode.DONT_CHECK);
+ if (restrictionEnvironments.isEmpty()
+ && attributes.isAttributeValueExplicitlySpecified(restrictionAttr)) {
+ ruleContext.attributeError(restrictionAttr, "attribute cannot be empty");
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Performs constraint checking on the given rule's dependencies and reports any errors.
+ *
+ * @param ruleContext the rule to analyze
+ * @param supportedEnvironments the rule's supported environments, as defined by the return
+ * value of {@link #getSupportedEnvironments}. In particular, for any environment group that's
+ * not in this collection, the rule is assumed to support the defaults for that group.
+ */
+ public static void checkConstraints(RuleContext ruleContext,
+ EnvironmentCollection supportedEnvironments) {
+
+ Set<EnvironmentGroup> knownGroups = supportedEnvironments.getGroups();
+
+ for (TransitiveInfoCollection dependency : getAllPrerequisites(ruleContext)) {
+ SupportedEnvironmentsProvider depProvider =
+ dependency.getProvider(SupportedEnvironmentsProvider.class);
+ if (depProvider == null) {
+ // Input files (InputFileConfiguredTarget) don't support environments. We may subsequently
+ // opt them into constraint checking, but for now just pass them by.
+ continue;
+ }
+ Collection<Label> depEnvironments = depProvider.getEnvironments().getEnvironments();
+ Set<EnvironmentGroup> groupsKnownToDep = depProvider.getEnvironments().getGroups();
+
+ // Environments we support that the dependency does not support.
+ Set<Label> disallowedEnvironments = new LinkedHashSet<>();
+
+ // For every environment we support, either the dependency must also support it OR it must be
+ // a default for a group the dependency doesn't know about.
+ for (EnvironmentWithGroup supportedEnv : supportedEnvironments.getGroupedEnvironments()) {
+ EnvironmentGroup group = supportedEnv.group();
+ Label environment = supportedEnv.environment();
+ if (!depEnvironments.contains(environment)
+ && (groupsKnownToDep.contains(group) || !group.isDefault(environment))) {
+ disallowedEnvironments.add(environment);
+ }
+ }
+
+ // For any environment group we don't know about, we implicitly support its defaults. Check
+ // that the dep does, too.
+ for (EnvironmentGroup depGroup : groupsKnownToDep) {
+ if (!knownGroups.contains(depGroup)) {
+ for (Label defaultEnv : depGroup.getDefaults()) {
+ if (!depEnvironments.contains(defaultEnv)) {
+ disallowedEnvironments.add(defaultEnv);
+ }
+ }
+ }
+ }
+
+ // Report errors on bad environments.
+ if (!disallowedEnvironments.isEmpty()) {
+ ruleContext.ruleError("dependency " + dependency.getLabel()
+ + " doesn't support expected environment"
+ + (disallowedEnvironments.size() == 1 ? "" : "s")
+ + ": " + Joiner.on(", ").join(disallowedEnvironments));
+ }
+ }
+ }
+
+ /**
+ * Returns all dependencies that should be constraint-checked against the current rule.
+ */
+ private static Iterable<TransitiveInfoCollection> getAllPrerequisites(RuleContext ruleContext) {
+ Set<TransitiveInfoCollection> prerequisites = new LinkedHashSet<>();
+ AttributeMap attributes = ruleContext.attributes();
+
+ for (String attr : attributes.getAttributeNames()) {
+ Type<?> attrType = attributes.getAttributeType(attr);
+ // TODO(bazel-team): support specifying which attributes are subject to constraint checking
+ if ((attrType == Type.LABEL || attrType == Type.LABEL_LIST)
+ && !RuleClass.isConstraintAttribute(attr)
+ && !attr.equals("visibility")) {
+ prerequisites.addAll(
+ ruleContext.getPrerequisites(attr, RuleConfiguredTarget.Mode.DONT_CHECK));
+ }
+ }
+ return prerequisites;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/constraints/Environment.java b/src/main/java/com/google/devtools/build/lib/analysis/constraints/Environment.java
new file mode 100644
index 0000000..912ed72
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/constraints/Environment.java
@@ -0,0 +1,71 @@
+// Copyright 2015 Google Inc. 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.analysis.constraints;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.FileProvider;
+import com.google.devtools.build.lib.analysis.FilesToRunProvider;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTargetBuilder;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.RunfilesProvider;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.packages.EnvironmentGroup;
+import com.google.devtools.build.lib.packages.Package;
+import com.google.devtools.build.lib.rules.RuleConfiguredTargetFactory;
+import com.google.devtools.build.lib.syntax.Label;
+
+/**
+ * Implementation for the environment rule.
+ */
+public class Environment implements RuleConfiguredTargetFactory {
+
+ @Override
+ public ConfiguredTarget create(RuleContext ruleContext) {
+
+ // The main analysis work to do here is to simply fill in SupportedEnvironmentsProvider to
+ // pass the environment itself to depending rules.
+ //
+ // This will likely expand when we add support for environments fulfilling other environments.
+ Label label = ruleContext.getLabel();
+ Package pkg = ruleContext.getRule().getPackage();
+
+ EnvironmentGroup group = null;
+ for (EnvironmentGroup pkgGroup : pkg.getTargets(EnvironmentGroup.class)) {
+ if (pkgGroup.getEnvironments().contains(label)) {
+ group = pkgGroup;
+ break;
+ }
+ }
+
+ if (group == null) {
+ ruleContext.ruleError("no matching environment group from the same package");
+ return null;
+ }
+
+ return new RuleConfiguredTargetBuilder(ruleContext)
+ .addProvider(SupportedEnvironmentsProvider.class,
+ new SupportedEnvironments(
+ new EnvironmentCollection.Builder().put(group, label).build()))
+ .addProvider(RunfilesProvider.class, RunfilesProvider.EMPTY)
+ .add(FileProvider.class, new FileProvider(ruleContext.getLabel(),
+ NestedSetBuilder.<Artifact>emptySet(Order.STABLE_ORDER)))
+ .add(FilesToRunProvider.class, new FilesToRunProvider(ruleContext.getLabel(),
+ ImmutableList.<Artifact>of(), null, null))
+ .build();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/constraints/EnvironmentCollection.java b/src/main/java/com/google/devtools/build/lib/analysis/constraints/EnvironmentCollection.java
new file mode 100644
index 0000000..1ce5f1c
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/constraints/EnvironmentCollection.java
@@ -0,0 +1,126 @@
+// Copyright 2015 Google Inc. 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.analysis.constraints;
+
+import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.packages.EnvironmentGroup;
+import com.google.devtools.build.lib.syntax.Label;
+
+import java.util.Map;
+
+/**
+ * Contains a set of {@link Environment} labels and their associated groups.
+ */
+@Immutable
+public class EnvironmentCollection {
+ private final ImmutableMultimap<EnvironmentGroup, Label> map;
+
+ private EnvironmentCollection(ImmutableMultimap<EnvironmentGroup, Label> map) {
+ this.map = map;
+ }
+
+ /**
+ * Stores an environment's build label along with the group it belongs to.
+ */
+ static class EnvironmentWithGroup {
+ private final Label environment;
+ private final EnvironmentGroup group;
+ EnvironmentWithGroup(Label environment, EnvironmentGroup group) {
+ this.environment = environment;
+ this.group = group;
+ }
+ Label environment() { return environment; }
+ EnvironmentGroup group() { return group; }
+ }
+
+ /**
+ * Returns the build labels of each environment in this collection, ordered by
+ * their insertion order in {@link Builder}.
+ */
+ ImmutableCollection<Label> getEnvironments() {
+ return map.values();
+ }
+
+ /**
+ * Returns the set of groups the environments in this collection belong to, ordered by
+ * their insertion order in {@link Builder}
+ */
+ ImmutableSet<EnvironmentGroup> getGroups() {
+ return map.keySet();
+ }
+
+ /**
+ * Returns the build labels of each environment in this collection paired with the
+ * group each environment belongs to, ordered by their insertion order in {@link Builder}.
+ */
+ ImmutableCollection<EnvironmentWithGroup> getGroupedEnvironments() {
+ ImmutableSet.Builder<EnvironmentWithGroup> builder = ImmutableSet.builder();
+ for (Map.Entry<EnvironmentGroup, Label> entry : map.entries()) {
+ builder.add(new EnvironmentWithGroup(entry.getValue(), entry.getKey()));
+ }
+ return builder.build();
+ }
+
+ /**
+ * Returns the environments in this collection that belong to the given group, ordered by
+ * their insertion order in {@link Builder}. If no environments belong to the given group,
+ * returns an empty collection.
+ */
+ ImmutableCollection<Label> getEnvironments(EnvironmentGroup group) {
+ return map.get(group);
+ }
+
+ /**
+ * An empty collection.
+ */
+ static final EnvironmentCollection EMPTY =
+ new EnvironmentCollection(ImmutableMultimap.<EnvironmentGroup, Label>of());
+
+ static class Builder {
+ private final ImmutableMultimap.Builder<EnvironmentGroup, Label> mapBuilder =
+ ImmutableMultimap.builder();
+
+ /**
+ * Inserts the given environment / owning group pair.
+ */
+ Builder put(EnvironmentGroup group, Label environment) {
+ mapBuilder.put(group, environment);
+ return this;
+ }
+
+ /**
+ * Inserts the given set of environments, all belonging to the specified group.
+ */
+ Builder putAll(EnvironmentGroup group, Iterable<Label> environments) {
+ mapBuilder.putAll(group, environments);
+ return this;
+ }
+
+ /**
+ * Inserts the contents of another {@link EnvironmentCollection} into this one.
+ */
+ Builder putAll(EnvironmentCollection other) {
+ mapBuilder.putAll(other.map);
+ return this;
+ }
+
+ EnvironmentCollection build() {
+ return new EnvironmentCollection(mapBuilder.build());
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/constraints/EnvironmentRule.java b/src/main/java/com/google/devtools/build/lib/analysis/constraints/EnvironmentRule.java
new file mode 100644
index 0000000..0553af0
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/constraints/EnvironmentRule.java
@@ -0,0 +1,48 @@
+// Copyright 2015 Google Inc. 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.analysis.constraints;
+
+import static com.google.devtools.build.lib.packages.Attribute.attr;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.analysis.BaseRuleClasses;
+import com.google.devtools.build.lib.analysis.BlazeRule;
+import com.google.devtools.build.lib.analysis.RuleDefinition;
+import com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.Type;
+
+/**
+ * Rule definition for environment rules (for Bazel's constraint enforcement system).
+ */
+@BlazeRule(name = EnvironmentRule.RULE_NAME,
+ ancestors = { BaseRuleClasses.BaseRule.class },
+ factoryClass = Environment.class)
+public final class EnvironmentRule implements RuleDefinition {
+ public static final String RULE_NAME = "environment";
+
+ @Override
+ public RuleClass build(RuleClass.Builder builder, RuleDefinitionEnvironment env) {
+ return builder
+ .override(attr("tags", Type.STRING_LIST)
+ // No need to show up in ":all", etc. target patterns.
+ .value(ImmutableList.of("manual"))
+ .nonconfigurable("low-level attribute, used in TargetUtils without configurations"))
+ .removeAttribute(RuleClass.COMPATIBLE_ENVIRONMENT_ATTR)
+ .removeAttribute(RuleClass.RESTRICTED_ENVIRONMENT_ATTR)
+ .setUndocumented()
+ .build();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/constraints/SupportedEnvironments.java b/src/main/java/com/google/devtools/build/lib/analysis/constraints/SupportedEnvironments.java
new file mode 100644
index 0000000..78c835e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/constraints/SupportedEnvironments.java
@@ -0,0 +1,31 @@
+// Copyright 2015 Google Inc. 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.analysis.constraints;
+
+/**
+ * Standard {@link SupportedEnvironmentsProvider} implementation.
+ */
+public class SupportedEnvironments implements SupportedEnvironmentsProvider {
+ private final EnvironmentCollection supportedEnvironments;
+
+ public SupportedEnvironments(EnvironmentCollection supportedEnvironments) {
+ this.supportedEnvironments = supportedEnvironments;
+ }
+
+ @Override
+ public EnvironmentCollection getEnvironments() {
+ return supportedEnvironments;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/constraints/SupportedEnvironmentsProvider.java b/src/main/java/com/google/devtools/build/lib/analysis/constraints/SupportedEnvironmentsProvider.java
new file mode 100644
index 0000000..8200386
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/constraints/SupportedEnvironmentsProvider.java
@@ -0,0 +1,29 @@
+// Copyright 2015 Google Inc. 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.analysis.constraints;
+
+import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
+
+/**
+ * A provider that advertises which environments the associated target is compatible with
+ * (from the point of view of the constraint enforcement system).
+ */
+public interface SupportedEnvironmentsProvider extends TransitiveInfoProvider {
+
+ /**
+ * Returns the environments the associated target is compatible with.
+ */
+ EnvironmentCollection getEnvironments();
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/BazelDiffAwarenessModule.java b/src/main/java/com/google/devtools/build/lib/bazel/BazelDiffAwarenessModule.java
new file mode 100644
index 0000000..1dad1f5
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/BazelDiffAwarenessModule.java
@@ -0,0 +1,34 @@
+// Copyright 2014 Google Inc. 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.bazel;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.runtime.BlazeModule;
+import com.google.devtools.build.lib.skyframe.DiffAwareness;
+import com.google.devtools.build.lib.skyframe.LocalDiffAwareness;
+
+/**
+ * Provides the {@link DiffAwareness} implementation that uses the Java watch service.
+ */
+public class BazelDiffAwarenessModule extends BlazeModule {
+
+ @Override
+ public Iterable<DiffAwareness.Factory> getDiffAwarenessFactories(boolean watchFS) {
+ ImmutableList.Builder<DiffAwareness.Factory> builder = ImmutableList.builder();
+ if (watchFS) {
+ builder.add(new LocalDiffAwareness.Factory());
+ }
+ return builder.build();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/BazelMain.java b/src/main/java/com/google/devtools/build/lib/bazel/BazelMain.java
new file mode 100644
index 0000000..fd3d000
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/BazelMain.java
@@ -0,0 +1,40 @@
+// Copyright 2014 Google Inc. 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.bazel;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.runtime.BlazeModule;
+import com.google.devtools.build.lib.runtime.BlazeRuntime;
+
+import java.util.List;
+
+/**
+ * The main class.
+ */
+public final class BazelMain {
+ private static final List<Class<? extends BlazeModule>> BAZEL_MODULES = ImmutableList.of(
+ com.google.devtools.build.lib.bazel.BazelShutdownLoggerModule.class,
+ com.google.devtools.build.lib.bazel.BazelWorkspaceStatusModule.class,
+ com.google.devtools.build.lib.bazel.BazelDiffAwarenessModule.class,
+ com.google.devtools.build.lib.bazel.BazelRepositoryModule.class,
+ com.google.devtools.build.lib.bazel.rules.BazelRulesModule.class,
+ com.google.devtools.build.lib.standalone.StandaloneModule.class,
+ com.google.devtools.build.lib.runtime.BuildSummaryStatsModule.class,
+ com.google.devtools.build.lib.webstatusserver.WebStatusServerModule.class
+ );
+
+ public static void main(String[] args) {
+ BlazeRuntime.main(BAZEL_MODULES, args);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/BazelRepositoryModule.java b/src/main/java/com/google/devtools/build/lib/bazel/BazelRepositoryModule.java
new file mode 100644
index 0000000..483103f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/BazelRepositoryModule.java
@@ -0,0 +1,100 @@
+// Copyright 2014 Google Inc. 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.bazel;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.analysis.BlazeDirectories;
+import com.google.devtools.build.lib.analysis.BlazeVersionInfo;
+import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider;
+import com.google.devtools.build.lib.bazel.repository.HttpArchiveFunction;
+import com.google.devtools.build.lib.bazel.repository.HttpJarFunction;
+import com.google.devtools.build.lib.bazel.repository.LocalRepositoryFunction;
+import com.google.devtools.build.lib.bazel.repository.MavenJarFunction;
+import com.google.devtools.build.lib.bazel.repository.NewLocalRepositoryFunction;
+import com.google.devtools.build.lib.bazel.repository.RepositoryDelegatorFunction;
+import com.google.devtools.build.lib.bazel.repository.RepositoryFunction;
+import com.google.devtools.build.lib.bazel.rules.workspace.HttpArchiveRule;
+import com.google.devtools.build.lib.bazel.rules.workspace.HttpJarRule;
+import com.google.devtools.build.lib.bazel.rules.workspace.LocalRepositoryRule;
+import com.google.devtools.build.lib.bazel.rules.workspace.MavenJarRule;
+import com.google.devtools.build.lib.bazel.rules.workspace.NewLocalRepositoryRule;
+import com.google.devtools.build.lib.runtime.BlazeModule;
+import com.google.devtools.build.lib.skyframe.SkyFunctions;
+import com.google.devtools.build.lib.util.Clock;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyFunctionName;
+import com.google.devtools.common.options.OptionsProvider;
+
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.UUID;
+
+/**
+ * Adds support for fetching external code.
+ */
+public class BazelRepositoryModule extends BlazeModule {
+
+ private BlazeDirectories directories;
+ // A map of repository handlers that can be looked up by rule class name.
+ private final ImmutableMap<String, RepositoryFunction> repositoryHandlers;
+
+ public BazelRepositoryModule() {
+ repositoryHandlers = ImmutableMap.of(
+ LocalRepositoryRule.NAME, new LocalRepositoryFunction(),
+ HttpArchiveRule.NAME, new HttpArchiveFunction(),
+ HttpJarRule.NAME, new HttpJarFunction(),
+ MavenJarRule.NAME, new MavenJarFunction(),
+ NewLocalRepositoryRule.NAME, new NewLocalRepositoryFunction());
+ }
+
+ @Override
+ public void blazeStartup(OptionsProvider startupOptions,
+ BlazeVersionInfo versionInfo, UUID instanceId, BlazeDirectories directories,
+ Clock clock) {
+ this.directories = directories;
+ for (RepositoryFunction handler : repositoryHandlers.values()) {
+ handler.setDirectories(directories);
+ }
+ }
+
+ @Override
+ public Set<Path> getImmutableDirectories() {
+ return ImmutableSet.of(RepositoryFunction.getExternalRepositoryDirectory(directories));
+ }
+
+ @Override
+ public void initializeRuleClasses(ConfiguredRuleClassProvider.Builder builder) {
+ for (Entry<String, RepositoryFunction> handler : repositoryHandlers.entrySet()) {
+ builder.addRuleDefinition(handler.getValue().getRuleDefinition());
+ }
+ }
+
+ @Override
+ public ImmutableMap<SkyFunctionName, SkyFunction> getSkyFunctions(BlazeDirectories directories) {
+ ImmutableMap.Builder<SkyFunctionName, SkyFunction> builder = ImmutableMap.builder();
+
+ // Bazel-specific repository downloaders.
+ for (RepositoryFunction handler : repositoryHandlers.values()) {
+ builder.put(handler.getSkyFunctionName(), handler);
+ }
+
+ // Create the delegator everything flows through.
+ builder.put(SkyFunctions.REPOSITORY,
+ new RepositoryDelegatorFunction(repositoryHandlers));
+ return builder.build();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/BazelShutdownLoggerModule.java b/src/main/java/com/google/devtools/build/lib/bazel/BazelShutdownLoggerModule.java
new file mode 100644
index 0000000..3c32611
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/BazelShutdownLoggerModule.java
@@ -0,0 +1,40 @@
+// Copyright 2014 Google Inc. 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.bazel;
+
+import com.google.devtools.build.lib.analysis.BlazeDirectories;
+import com.google.devtools.build.lib.analysis.BlazeVersionInfo;
+import com.google.devtools.build.lib.runtime.BlazeModule;
+import com.google.devtools.build.lib.util.Clock;
+import com.google.devtools.common.options.OptionsProvider;
+
+import java.util.UUID;
+import java.util.logging.LogManager;
+import java.util.logging.Logger;
+
+/**
+ * Shutdown log output when Bazel runs in batch mode
+ */
+public class BazelShutdownLoggerModule extends BlazeModule {
+
+ private Logger globalLogger;
+
+ @Override
+ public void blazeStartup(OptionsProvider startupOptions, BlazeVersionInfo versionInfo,
+ UUID instanceId, BlazeDirectories directories, Clock clock) {
+ LogManager.getLogManager().reset();
+ globalLogger = Logger.getGlobal();
+ globalLogger.setLevel(java.util.logging.Level.OFF);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/BazelWorkspaceStatusModule.java b/src/main/java/com/google/devtools/build/lib/bazel/BazelWorkspaceStatusModule.java
new file mode 100644
index 0000000..dda983d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/BazelWorkspaceStatusModule.java
@@ -0,0 +1,196 @@
+// Copyright 2014 Google Inc. 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.bazel;
+
+import com.google.common.base.Supplier;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.actions.ActionContextProvider;
+import com.google.devtools.build.lib.actions.ActionExecutionContext;
+import com.google.devtools.build.lib.actions.ActionExecutionException;
+import com.google.devtools.build.lib.actions.ActionGraph;
+import com.google.devtools.build.lib.actions.ActionInputFileCache;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.ArtifactFactory;
+import com.google.devtools.build.lib.actions.ArtifactOwner;
+import com.google.devtools.build.lib.actions.ExecutionStrategy;
+import com.google.devtools.build.lib.actions.Executor;
+import com.google.devtools.build.lib.actions.Executor.ActionContext;
+import com.google.devtools.build.lib.actions.ExecutorInitException;
+import com.google.devtools.build.lib.actions.ResourceSet;
+import com.google.devtools.build.lib.actions.Root;
+import com.google.devtools.build.lib.analysis.BuildInfoHelper;
+import com.google.devtools.build.lib.analysis.WorkspaceStatusAction;
+import com.google.devtools.build.lib.analysis.WorkspaceStatusAction.Key;
+import com.google.devtools.build.lib.runtime.BlazeModule;
+import com.google.devtools.build.lib.runtime.BlazeRuntime;
+import com.google.devtools.build.lib.runtime.Command;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.Objects;
+import java.util.UUID;
+
+/**
+ * Workspace status information for Bazel.
+ *
+ * <p>Currently only a stub.
+ */
+public class BazelWorkspaceStatusModule extends BlazeModule {
+ private static class BazelWorkspaceStatusAction extends WorkspaceStatusAction {
+ private final Artifact stableStatus;
+ private final Artifact volatileStatus;
+
+ private BazelWorkspaceStatusAction(
+ Artifact stableStatus, Artifact volatileStatus) {
+ super(BuildInfoHelper.BUILD_INFO_ACTION_OWNER, Artifact.NO_ARTIFACTS,
+ ImmutableList.of(stableStatus, volatileStatus));
+ this.stableStatus = stableStatus;
+ this.volatileStatus = volatileStatus;
+ }
+
+ @Override
+ public String describeStrategy(Executor executor) {
+ return "";
+ }
+
+ @Override
+ public void execute(ActionExecutionContext actionExecutionContext)
+ throws ActionExecutionException {
+ try {
+ FileSystemUtils.writeContent(stableStatus.getPath(), new byte[] {});
+ FileSystemUtils.writeContent(volatileStatus.getPath(), new byte[] {});
+ } catch (IOException e) {
+ throw new ActionExecutionException(e, this, true);
+ }
+ }
+
+ // TODO(bazel-team): Add test for equals, add hashCode.
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof BazelWorkspaceStatusAction)) {
+ return false;
+ }
+
+ BazelWorkspaceStatusAction that = (BazelWorkspaceStatusAction) o;
+ return this.stableStatus.equals(that.stableStatus)
+ && this.volatileStatus.equals(that.volatileStatus);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(stableStatus, volatileStatus);
+ }
+
+ @Override
+ public String getMnemonic() {
+ return "BazelWorkspaceStatusAction";
+ }
+
+ @Override
+ public ResourceSet estimateResourceConsumption(Executor executor) {
+ return ResourceSet.ZERO;
+ }
+
+ @Override
+ protected String computeKey() {
+ return "";
+ }
+
+ @Override
+ public Artifact getVolatileStatus() {
+ return volatileStatus;
+ }
+
+ @Override
+ public Artifact getStableStatus() {
+ return stableStatus;
+ }
+ }
+
+ private class BazelStatusActionFactory implements WorkspaceStatusAction.Factory {
+ @Override
+ public Map<String, String> createDummyWorkspaceStatus() {
+ return ImmutableMap.of();
+ }
+
+ @Override
+ public WorkspaceStatusAction createWorkspaceStatusAction(
+ ArtifactFactory factory, ArtifactOwner artifactOwner, Supplier<UUID> buildId) {
+ Root root = runtime.getDirectories().getBuildDataDirectory();
+
+ Artifact stableArtifact = factory.getDerivedArtifact(
+ new PathFragment("stable-status.txt"), root, artifactOwner);
+ Artifact volatileArtifact = factory.getConstantMetadataArtifact(
+ new PathFragment("volatile-status.txt"), root, artifactOwner);
+
+ return new BazelWorkspaceStatusAction(stableArtifact, volatileArtifact);
+ }
+ }
+
+ @ExecutionStrategy(contextType = WorkspaceStatusAction.Context.class)
+ private class BazelWorkspaceStatusActionContext implements WorkspaceStatusAction.Context {
+ @Override
+ public ImmutableMap<String, Key> getStableKeys() {
+ return ImmutableMap.of();
+ }
+
+ @Override
+ public ImmutableMap<String, Key> getVolatileKeys() {
+ return ImmutableMap.of();
+ }
+ }
+
+
+ private class WorkspaceActionContextProvider implements ActionContextProvider {
+ @Override
+ public Iterable<ActionContext> getActionContexts() {
+ return ImmutableList.<ActionContext>of(new BazelWorkspaceStatusActionContext());
+ }
+
+ @Override
+ public void executorCreated(Iterable<ActionContext> usedContexts)
+ throws ExecutorInitException {
+ }
+
+ @Override
+ public void executionPhaseEnding() {
+ }
+
+ @Override
+ public void executionPhaseStarting(ActionInputFileCache actionInputFileCache,
+ ActionGraph actionGraph, Iterable<Artifact> topLevelArtifacts) throws ExecutorInitException,
+ InterruptedException {
+ }
+ }
+
+ private BlazeRuntime runtime;
+
+ @Override
+ public void beforeCommand(BlazeRuntime runtime, Command command) {
+ this.runtime = runtime;
+ }
+
+ @Override
+ public ActionContextProvider getActionContextProvider() {
+ return new WorkspaceActionContextProvider();
+ }
+
+ @Override
+ public WorkspaceStatusAction.Factory getWorkspaceStatusActionFactory() {
+ return new BazelStatusActionFactory();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/repository/DecompressorFactory.java b/src/main/java/com/google/devtools/build/lib/bazel/repository/DecompressorFactory.java
new file mode 100644
index 0000000..1076f24
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/repository/DecompressorFactory.java
@@ -0,0 +1,218 @@
+// Copyright 2014 Google Inc. 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.bazel.repository;
+
+import com.google.devtools.build.lib.bazel.rules.workspace.HttpArchiveRule;
+import com.google.devtools.build.lib.bazel.rules.workspace.HttpJarRule;
+import com.google.devtools.build.lib.packages.Target;
+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 org.apache.commons.compress.archivers.ArchiveException;
+import org.apache.commons.compress.archivers.ArchiveInputStream;
+import org.apache.commons.compress.archivers.ArchiveStreamFactory;
+import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
+import org.apache.commons.compress.utils.IOUtils;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.Charset;
+
+/**
+ * Creates decompressors to use on archive. Use {@link DecompressorFactory#create} to get the
+ * correct type of decompressor for the input archive, then call
+ * {@link Decompressor#decompress} to decompress it.
+ */
+public abstract class DecompressorFactory {
+
+
+ public static Decompressor create(Target target, Path archivePath)
+ throws DecompressorException {
+ String baseName = archivePath.getBaseName();
+
+ if (target.getTargetKind().startsWith(HttpJarRule.NAME + " ")) {
+ if (baseName.endsWith(".jar")) {
+ return new JarDecompressor(target, archivePath);
+ } else {
+ throw new DecompressorException(
+ "Expected " + HttpJarRule.NAME + " " + target.getName()
+ + " to create file with a .jar suffix (got " + archivePath + ")");
+ }
+ }
+
+ if (target.getTargetKind().startsWith(HttpArchiveRule.NAME + " ")) {
+ if (baseName.endsWith(".zip") || baseName.endsWith(".jar")) {
+ return new ZipDecompressor(archivePath);
+ } else {
+ throw new DecompressorException(
+ "Expected " + HttpArchiveRule.NAME + " " + target.getName()
+ + " to create file with a .zip or .jar suffix (got " + archivePath + ")");
+ }
+ }
+
+ throw new DecompressorException(
+ "No decompressor found for " + target.getTargetKind() + " rule " + target.getName()
+ + " (got " + archivePath + ")");
+ }
+
+ /**
+ * General decompressor for an archive. Should be overridden for each specific archive type.
+ */
+ public abstract static class Decompressor {
+ protected final Path archiveFile;
+
+ private Decompressor(Path archiveFile) {
+ this.archiveFile = archiveFile;
+ }
+
+ /**
+ * This is overridden by archive-specific decompression logic. Often this logic will create
+ * files and directories under the {@link Decompressor#archiveFile}'s parent directory.
+ *
+ * @return the path to the repository directory. That is, the returned path will be a directory
+ * containing a WORKSPACE file.
+ */
+ public abstract Path decompress() throws DecompressorException;
+ }
+
+ static class JarDecompressor extends Decompressor {
+ private final Target target;
+
+ public JarDecompressor(Target target, Path archiveFile) {
+ super(archiveFile);
+ this.target = target;
+ }
+
+ /**
+ * The .jar can be used compressed, so this just exposes it in a way Bazel can use.
+ *
+ * <p>It moves the jar from some-name/foo.jar to some-name/repository/jar/foo.jar and creates a
+ * BUILD file containing one entry: a .jar.
+ */
+ @Override
+ public Path decompress() throws DecompressorException {
+ Path destinationDirectory = archiveFile.getParentDirectory().getRelative("repository");
+ // Example: archiveFile is .external-repository/some-name/foo.jar.
+ String baseName = archiveFile.getBaseName();
+
+ try {
+ FileSystemUtils.createDirectoryAndParents(destinationDirectory);
+ // .external-repository/some-name/repository/WORKSPACE.
+ Path workspaceFile = destinationDirectory.getRelative("WORKSPACE");
+ FileSystemUtils.writeContent(workspaceFile, Charset.forName("UTF-8"),
+ "# DO NOT EDIT: automatically generated WORKSPACE file for " + target.getTargetKind()
+ + " rule " + target.getName());
+ // .external-repository/some-name/repository/jar.
+ Path jarDirectory = destinationDirectory.getRelative("jar");
+ FileSystemUtils.createDirectoryAndParents(jarDirectory);
+ // .external-repository/some-name/repository/jar/foo.jar is a symbolic link to the jar in
+ // .external-repository/some-name.
+ Path jarSymlink = jarDirectory.getRelative(baseName);
+ if (!jarSymlink.exists()) {
+ jarSymlink.createSymbolicLink(archiveFile);
+ }
+ // .external-repository/some-name/repository/jar/BUILD defines the //jar target.
+ Path buildFile = jarDirectory.getRelative("BUILD");
+ FileSystemUtils.writeLinesAs(buildFile, Charset.forName("UTF-8"),
+ "# DO NOT EDIT: automatically generated BUILD file for " + target.getTargetKind()
+ + " rule " + target.getName(),
+ "java_import(",
+ " name = 'jar',",
+ " jars = ['" + baseName + "'],",
+ " visibility = ['//visibility:public']",
+ ")");
+ } catch (IOException e) {
+ throw new DecompressorException(e.getMessage());
+ }
+ return destinationDirectory;
+ }
+ }
+
+ private static class ZipDecompressor extends Decompressor {
+ public ZipDecompressor(Path archiveFile) {
+ super(archiveFile);
+ }
+
+ /**
+ * This unzips the zip file to a sibling directory of {@link Decompressor#archiveFile}. The
+ * zip file is expected to have the WORKSPACE file at the top level, e.g.:
+ *
+ * <pre>
+ * $ unzip -lf some-repo.zip
+ * Archive: ../repo.zip
+ * Length Date Time Name
+ * --------- ---------- ----- ----
+ * 0 2014-11-20 15:50 WORKSPACE
+ * 0 2014-11-20 16:10 foo/
+ * 236 2014-11-20 15:52 foo/BUILD
+ * ...
+ * </pre>
+ */
+ @Override
+ public Path decompress() throws DecompressorException {
+ Path destinationDirectory = archiveFile.getParentDirectory().getRelative("repository");
+ try (InputStream is = new FileInputStream(archiveFile.getPathString())) {
+ ArchiveInputStream in = new ArchiveStreamFactory().createArchiveInputStream(
+ ArchiveStreamFactory.ZIP, is);
+ ZipArchiveEntry entry = (ZipArchiveEntry) in.getNextEntry();
+ while (entry != null) {
+ extractZipEntry(in, entry, destinationDirectory);
+ entry = (ZipArchiveEntry) in.getNextEntry();
+ }
+ } catch (IOException | ArchiveException e) {
+ throw new DecompressorException(
+ "Error extracting " + archiveFile + " to " + destinationDirectory + ": "
+ + e.getMessage());
+ }
+ return destinationDirectory;
+ }
+
+ private void extractZipEntry(
+ ArchiveInputStream in, ZipArchiveEntry entry, Path destinationDirectory)
+ throws IOException, DecompressorException {
+ PathFragment relativePath = new PathFragment(entry.getName());
+ if (relativePath.isAbsolute()) {
+ throw new DecompressorException("Failed to extract " + relativePath
+ + ", zipped paths cannot be absolute");
+ }
+ Path outputPath = destinationDirectory.getRelative(relativePath);
+ FileSystemUtils.createDirectoryAndParents(outputPath.getParentDirectory());
+ if (entry.isDirectory()) {
+ FileSystemUtils.createDirectoryAndParents(outputPath);
+ } else {
+ try (OutputStream out = new FileOutputStream(new File(outputPath.getPathString()))) {
+ IOUtils.copy(in, out);
+ } catch (IOException e) {
+ throw new DecompressorException("Error writing " + outputPath + " from "
+ + archiveFile);
+ }
+ }
+ }
+ }
+
+ /**
+ * Exceptions thrown when something goes wrong decompressing an archive.
+ */
+ public static class DecompressorException extends Exception {
+ public DecompressorException(String message) {
+ super(message);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/repository/HttpArchiveFunction.java b/src/main/java/com/google/devtools/build/lib/bazel/repository/HttpArchiveFunction.java
new file mode 100644
index 0000000..1cd6db8
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/repository/HttpArchiveFunction.java
@@ -0,0 +1,112 @@
+// Copyright 2014 Google Inc. 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.bazel.repository;
+
+import com.google.devtools.build.lib.analysis.RuleDefinition;
+import com.google.devtools.build.lib.bazel.repository.DecompressorFactory.DecompressorException;
+import com.google.devtools.build.lib.bazel.rules.workspace.HttpArchiveRule;
+import com.google.devtools.build.lib.packages.AggregatingAttributeMapper;
+import com.google.devtools.build.lib.packages.PackageIdentifier.RepositoryName;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.skyframe.FileValue;
+import com.google.devtools.build.lib.skyframe.RepositoryValue;
+import com.google.devtools.build.lib.syntax.EvalException;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.skyframe.SkyFunctionException;
+import com.google.devtools.build.skyframe.SkyFunctionException.Transience;
+import com.google.devtools.build.skyframe.SkyFunctionName;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+/**
+ * Downloads a file over HTTP.
+ */
+public class HttpArchiveFunction extends RepositoryFunction {
+
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env) throws SkyFunctionException {
+ RepositoryName repositoryName = (RepositoryName) skyKey.argument();
+ Rule rule = RepositoryFunction.getRule(repositoryName, HttpArchiveRule.NAME, env);
+ if (rule == null) {
+ return null;
+ }
+
+ return compute(env, rule);
+ }
+
+ protected FileValue createOutputDirectory(Environment env, String repositoryName)
+ throws RepositoryFunctionException {
+ // The output directory is always under .external-repository (to stay out of the way of
+ // artifacts from this repository) and uses the rule's name to avoid conflicts with other
+ // remote repository rules. For example, suppose you had the following WORKSPACE file:
+ //
+ // http_archive(name = "png", url = "http://example.com/downloads/png.tar.gz", sha256 = "...")
+ //
+ // This would download png.tar.gz to .external-repository/png/png.tar.gz.
+ Path outputDirectory = getExternalRepositoryDirectory().getRelative(repositoryName);
+ try {
+ FileSystemUtils.createDirectoryAndParents(outputDirectory);
+ } catch (IOException e) {
+ throw new RepositoryFunctionException(e, Transience.TRANSIENT);
+ }
+ return getRepositoryDirectory(outputDirectory, env);
+ }
+
+ protected SkyValue compute(Environment env, Rule rule)
+ throws RepositoryFunctionException {
+ FileValue directoryValue = createOutputDirectory(env, rule.getName());
+ if (directoryValue == null) {
+ return null;
+ }
+ Path outputDirectory = directoryValue.realRootedPath().asPath();
+ AggregatingAttributeMapper mapper = AggregatingAttributeMapper.of(rule);
+ URL url = null;
+ try {
+ url = new URL(mapper.get("url", Type.STRING));
+ } catch (MalformedURLException e) {
+ throw new RepositoryFunctionException(
+ new EvalException(rule.getLocation(), "Error parsing URL: " + e.getMessage()),
+ Transience.PERSISTENT);
+ }
+ String sha256 = mapper.get("sha256", Type.STRING);
+ HttpDownloader downloader = new HttpDownloader(url, sha256, outputDirectory);
+ try {
+ Path archiveFile = downloader.download();
+ outputDirectory = DecompressorFactory.create(rule, archiveFile).decompress();
+ } catch (IOException e) {
+ // Assumes all IO errors transient.
+ throw new RepositoryFunctionException(e, Transience.TRANSIENT);
+ } catch (DecompressorException e) {
+ throw new RepositoryFunctionException(new IOException(e.getMessage()), Transience.TRANSIENT);
+ }
+ return new RepositoryValue(outputDirectory, directoryValue);
+ }
+
+ @Override
+ public SkyFunctionName getSkyFunctionName() {
+ return SkyFunctionName.computed(HttpArchiveRule.NAME.toUpperCase());
+ }
+
+ @Override
+ public Class<? extends RuleDefinition> getRuleDefinition() {
+ return HttpArchiveRule.class;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/repository/HttpDownloader.java b/src/main/java/com/google/devtools/build/lib/bazel/repository/HttpDownloader.java
new file mode 100644
index 0000000..0f9ff44
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/repository/HttpDownloader.java
@@ -0,0 +1,107 @@
+// Copyright 2014 Google Inc. 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.bazel.repository;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.hash.Hasher;
+import com.google.common.hash.Hashing;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URL;
+import java.nio.ByteBuffer;
+import java.nio.channels.Channels;
+import java.nio.channels.ReadableByteChannel;
+
+/**
+ * Helper class for downloading a file from a URL.
+ */
+public class HttpDownloader {
+ private static final int BUFFER_SIZE = 2048;
+
+ private final URL url;
+ private final String sha256;
+ private final Path outputDirectory;
+
+ HttpDownloader(URL url, String sha256, Path outputDirectory) {
+ this.url = url;
+ this.sha256 = sha256;
+ this.outputDirectory = outputDirectory;
+ }
+
+ /**
+ * Attempt to download a file from the repository's URL. Returns the path to the file downloaded.
+ */
+ public Path download() throws IOException {
+ String filename = new PathFragment(url.getPath()).getBaseName();
+ if (filename.isEmpty()) {
+ filename = "temp";
+ }
+ Path destination = outputDirectory.getRelative(filename);
+
+ try (OutputStream outputStream = destination.getOutputStream()) {
+ ReadableByteChannel rbc = getChannel(url);
+ ByteBuffer byteBuffer = ByteBuffer.allocate(BUFFER_SIZE);
+ while (rbc.read(byteBuffer) > 0) {
+ byteBuffer.flip();
+ while (byteBuffer.hasRemaining()) {
+ outputStream.write(byteBuffer.get());
+ }
+ }
+ } catch (IOException e) {
+ throw new IOException(
+ "Error downloading " + url + " to " + destination + ": " + e.getMessage());
+ }
+
+ try {
+ String downloadedSha256 = getSha256(destination);
+ if (!downloadedSha256.equals(sha256)) {
+ throw new IOException(
+ "Downloaded file at " + destination + " has SHA-256 of " + downloadedSha256
+ + ", does not match expected SHA-256 (" + sha256 + ")");
+ }
+ } catch (IOException e) {
+ throw new IOException(
+ "Could not hash file " + destination + ": " + e.getMessage() + ", expected SHA-256 of "
+ + sha256 + ")");
+ }
+ return destination;
+ }
+
+ @VisibleForTesting
+ protected ReadableByteChannel getChannel(URL url) throws IOException {
+ return Channels.newChannel(url.openStream());
+ }
+
+ private String getSha256(Path path) throws IOException {
+ Hasher hasher = Hashing.sha256().newHasher();
+
+ byte byteBuffer[] = new byte[BUFFER_SIZE];
+ try (InputStream stream = path.getInputStream()) {
+ int numBytesRead = stream.read(byteBuffer);
+ while (numBytesRead != -1) {
+ if (numBytesRead != 0) {
+ // If more than 0 bytes were read, add them to the hash.
+ hasher.putBytes(byteBuffer, 0, numBytesRead);
+ }
+ numBytesRead = stream.read(byteBuffer);
+ }
+ }
+ return hasher.hash().toString();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/repository/HttpJarFunction.java b/src/main/java/com/google/devtools/build/lib/bazel/repository/HttpJarFunction.java
new file mode 100644
index 0000000..56e5e55
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/repository/HttpJarFunction.java
@@ -0,0 +1,50 @@
+// Copyright 2014 Google Inc. 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.bazel.repository;
+
+import com.google.devtools.build.lib.analysis.RuleDefinition;
+import com.google.devtools.build.lib.bazel.rules.workspace.HttpJarRule;
+import com.google.devtools.build.lib.packages.PackageIdentifier.RepositoryName;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.skyframe.SkyFunctionException;
+import com.google.devtools.build.skyframe.SkyFunctionName;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+/**
+ * Downloads a jar file from a URL.
+ */
+public class HttpJarFunction extends HttpArchiveFunction {
+
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env) throws SkyFunctionException {
+ RepositoryName repositoryName = (RepositoryName) skyKey.argument();
+ Rule rule = RepositoryFunction.getRule(repositoryName, HttpJarRule.NAME, env);
+ if (rule == null) {
+ return null;
+ }
+ return compute(env, rule);
+ }
+
+ @Override
+ public SkyFunctionName getSkyFunctionName() {
+ return SkyFunctionName.computed(HttpJarRule.NAME.toUpperCase());
+ }
+
+ @Override
+ public Class<? extends RuleDefinition> getRuleDefinition() {
+ return HttpJarRule.class;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/repository/LocalRepositoryFunction.java b/src/main/java/com/google/devtools/build/lib/bazel/repository/LocalRepositoryFunction.java
new file mode 100644
index 0000000..1a72dad
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/repository/LocalRepositoryFunction.java
@@ -0,0 +1,82 @@
+// Copyright 2014 Google Inc. 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.bazel.repository;
+
+import com.google.devtools.build.lib.analysis.RuleDefinition;
+import com.google.devtools.build.lib.bazel.rules.workspace.LocalRepositoryRule;
+import com.google.devtools.build.lib.packages.AggregatingAttributeMapper;
+import com.google.devtools.build.lib.packages.PackageIdentifier.RepositoryName;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.skyframe.FileValue;
+import com.google.devtools.build.lib.skyframe.RepositoryValue;
+import com.google.devtools.build.lib.syntax.EvalException;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.skyframe.SkyFunctionException;
+import com.google.devtools.build.skyframe.SkyFunctionException.Transience;
+import com.google.devtools.build.skyframe.SkyFunctionName;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.io.IOException;
+
+/**
+ * Access a repository on the local filesystem.
+ */
+public class LocalRepositoryFunction extends RepositoryFunction {
+
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env) throws SkyFunctionException {
+ RepositoryName repositoryName = (RepositoryName) skyKey.argument();
+ Rule rule = RepositoryFunction.getRule(repositoryName, LocalRepositoryRule.NAME, env);
+ if (rule == null) {
+ return null;
+ }
+
+ AggregatingAttributeMapper mapper = AggregatingAttributeMapper.of(rule);
+ String path = mapper.get("path", Type.STRING);
+ PathFragment pathFragment = new PathFragment(path);
+ if (!pathFragment.isAbsolute()) {
+ throw new RepositoryFunctionException(
+ new EvalException(
+ rule.getLocation(),
+ "In " + rule + " the 'path' attribute must specify an absolute path"),
+ Transience.PERSISTENT);
+ }
+ Path repositoryPath = getOutputBase().getFileSystem().getPath(pathFragment);
+ FileValue repositoryValue = getRepositoryDirectory(repositoryPath, env);
+ if (repositoryValue == null) {
+ return null;
+ }
+
+ if (!repositoryValue.isDirectory()) {
+ throw new RepositoryFunctionException(
+ new IOException(rule + " must specify an existing directory"), Transience.TRANSIENT);
+ }
+
+ return new RepositoryValue(repositoryPath, repositoryValue);
+ }
+
+ @Override
+ public SkyFunctionName getSkyFunctionName() {
+ return SkyFunctionName.computed(LocalRepositoryRule.NAME.toUpperCase());
+ }
+
+ @Override
+ public Class<? extends RuleDefinition> getRuleDefinition() {
+ return LocalRepositoryRule.class;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/repository/MavenJarFunction.java b/src/main/java/com/google/devtools/build/lib/bazel/repository/MavenJarFunction.java
new file mode 100644
index 0000000..4f83de6
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/repository/MavenJarFunction.java
@@ -0,0 +1,189 @@
+// Copyright 2014 Google Inc. 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.bazel.repository;
+
+import com.google.common.base.Ascii;
+import com.google.common.collect.Lists;
+import com.google.devtools.build.lib.analysis.RuleDefinition;
+import com.google.devtools.build.lib.bazel.repository.DecompressorFactory.DecompressorException;
+import com.google.devtools.build.lib.bazel.repository.DecompressorFactory.JarDecompressor;
+import com.google.devtools.build.lib.bazel.rules.workspace.MavenJarRule;
+import com.google.devtools.build.lib.packages.AggregatingAttributeMapper;
+import com.google.devtools.build.lib.packages.PackageIdentifier.RepositoryName;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.skyframe.FileValue;
+import com.google.devtools.build.lib.skyframe.RepositoryValue;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.skyframe.SkyFunctionException.Transience;
+import com.google.devtools.build.skyframe.SkyFunctionName;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import org.apache.maven.repository.internal.MavenRepositorySystemUtils;
+import org.eclipse.aether.AbstractRepositoryListener;
+import org.eclipse.aether.DefaultRepositorySystemSession;
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory;
+import org.eclipse.aether.impl.DefaultServiceLocator;
+import org.eclipse.aether.repository.LocalRepository;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.resolution.ArtifactRequest;
+import org.eclipse.aether.resolution.ArtifactResolutionException;
+import org.eclipse.aether.resolution.ArtifactResult;
+import org.eclipse.aether.spi.connector.RepositoryConnectorFactory;
+import org.eclipse.aether.spi.connector.transport.TransporterFactory;
+import org.eclipse.aether.transfer.AbstractTransferListener;
+import org.eclipse.aether.transport.file.FileTransporterFactory;
+import org.eclipse.aether.transport.http.HttpTransporterFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Implementation of maven_jar.
+ */
+public class MavenJarFunction extends HttpJarFunction {
+
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env) throws RepositoryFunctionException {
+ RepositoryName repositoryName = (RepositoryName) skyKey.argument();
+ Rule rule = RepositoryFunction.getRule(repositoryName, MavenJarRule.NAME, env);
+ if (rule == null) {
+ return null;
+ }
+
+ AggregatingAttributeMapper mapper = AggregatingAttributeMapper.of(rule);
+ FileValue outputDirectoryValue = createOutputDirectory(env, rule.getName());
+ if (outputDirectoryValue == null) {
+ return null;
+ }
+ Path outputDirectory = outputDirectoryValue.realRootedPath().asPath();
+ MavenDownloader downloader = new MavenDownloader(
+ mapper.get("group_id", Type.STRING),
+ mapper.get("artifact_id", Type.STRING),
+ mapper.get("version", Type.STRING),
+ outputDirectory);
+
+ List<String> repositories = mapper.get("repositories", Type.STRING_LIST);
+ if (repositories != null && !repositories.isEmpty()) {
+ downloader.setRepositories(repositories);
+ }
+
+ Path repositoryJar = null;
+ try {
+ repositoryJar = downloader.download();
+ } catch (IOException e) {
+ throw new RepositoryFunctionException(e, Transience.TRANSIENT);
+ }
+
+ // Add a WORKSPACE file & BUILD file to the Maven jar.
+ JarDecompressor decompressor = new JarDecompressor(rule, repositoryJar);
+ Path repositoryDirectory = null;
+ try {
+ repositoryDirectory = decompressor.decompress();
+ } catch (DecompressorException e) {
+ throw new RepositoryFunctionException(new IOException(e.getMessage()), Transience.TRANSIENT);
+ }
+ FileValue repositoryFileValue = getRepositoryDirectory(repositoryDirectory, env);
+ if (repositoryFileValue == null) {
+ return null;
+ }
+ return new RepositoryValue(repositoryDirectory, repositoryFileValue);
+ }
+
+ @Override
+ public SkyFunctionName getSkyFunctionName() {
+ return SkyFunctionName.computed(Ascii.toUpperCase(MavenJarRule.NAME));
+ }
+
+ @Override
+ public Class<? extends RuleDefinition> getRuleDefinition() {
+ return MavenJarRule.class;
+ }
+
+ private static class MavenDownloader {
+ private static final String MAVEN_CENTRAL_URL = "http://central.maven.org/maven2/";
+
+ private final String groupId;
+ private final String artifactId;
+ private final String version;
+ private final Path outputDirectory;
+ private List<RemoteRepository> repositories;
+
+ MavenDownloader(String groupId, String artifactId, String version, Path outputDirectory) {
+ this.groupId = groupId;
+ this.artifactId = artifactId;
+ this.version = version;
+ this.outputDirectory = outputDirectory;
+
+ this.repositories = new ArrayList<>(Arrays.asList(
+ new RemoteRepository.Builder("central", "default", MAVEN_CENTRAL_URL)
+ .build()));
+ }
+
+ /**
+ * Customizes the set of Maven repositories to check. Takes a list of repository addresses.
+ */
+ public void setRepositories(List<String> repositoryUrls) {
+ repositories = Lists.newArrayList();
+ for (String repositoryUrl : repositoryUrls) {
+ repositories.add(new RemoteRepository.Builder(
+ "user-defined repository " + repositories.size(), "default", repositoryUrl).build());
+ }
+ }
+
+ public Path download() throws IOException {
+ RepositorySystem system = newRepositorySystem();
+ RepositorySystemSession session = newRepositorySystemSession(system);
+
+ ArtifactRequest artifactRequest = new ArtifactRequest();
+ Artifact artifact = new DefaultArtifact(groupId + ":" + artifactId + ":" + version);
+ artifactRequest.setArtifact(artifact);
+ artifactRequest.setRepositories(repositories);
+
+ try {
+ ArtifactResult artifactResult = system.resolveArtifact(session, artifactRequest);
+ artifact = artifactResult.getArtifact();
+ } catch (ArtifactResolutionException e) {
+ throw new IOException("Failed to fetch Maven dependency: " + e.getMessage());
+ }
+ return outputDirectory.getRelative(artifact.getFile().getAbsolutePath());
+ }
+
+ private RepositorySystemSession newRepositorySystemSession(RepositorySystem system) {
+ DefaultRepositorySystemSession session = MavenRepositorySystemUtils.newSession();
+ LocalRepository localRepo = new LocalRepository(outputDirectory.getPathString());
+ session.setLocalRepositoryManager(system.newLocalRepositoryManager(session, localRepo));
+ session.setTransferListener(new AbstractTransferListener() {});
+ session.setRepositoryListener(new AbstractRepositoryListener() {});
+ return session;
+ }
+
+ private RepositorySystem newRepositorySystem() {
+ DefaultServiceLocator locator = MavenRepositorySystemUtils.newServiceLocator();
+ locator.addService(RepositoryConnectorFactory.class, BasicRepositoryConnectorFactory.class);
+ locator.addService(TransporterFactory.class, FileTransporterFactory.class);
+ locator.addService(TransporterFactory.class, HttpTransporterFactory.class);
+ return locator.getService(RepositorySystem.class);
+ }
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/repository/NewLocalRepositoryFunction.java b/src/main/java/com/google/devtools/build/lib/bazel/repository/NewLocalRepositoryFunction.java
new file mode 100644
index 0000000..b2d9f74
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/repository/NewLocalRepositoryFunction.java
@@ -0,0 +1,145 @@
+// Copyright 2014 Google Inc. 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.bazel.repository;
+
+import com.google.devtools.build.lib.analysis.RuleDefinition;
+import com.google.devtools.build.lib.bazel.rules.workspace.NewLocalRepositoryRule;
+import com.google.devtools.build.lib.packages.AggregatingAttributeMapper;
+import com.google.devtools.build.lib.packages.PackageIdentifier.RepositoryName;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.skyframe.FileValue;
+import com.google.devtools.build.lib.skyframe.RepositoryValue;
+import com.google.devtools.build.lib.syntax.EvalException;
+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 com.google.devtools.build.skyframe.SkyFunctionException;
+import com.google.devtools.build.skyframe.SkyFunctionException.Transience;
+import com.google.devtools.build.skyframe.SkyFunctionName;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.io.IOException;
+import java.nio.charset.Charset;
+
+/**
+ * Create a repository from a directory on the local filesystem.
+ */
+public class NewLocalRepositoryFunction extends RepositoryFunction {
+
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env) throws SkyFunctionException {
+ RepositoryName repositoryName = (RepositoryName) skyKey.argument();
+ Rule rule = RepositoryFunction.getRule(repositoryName, NewLocalRepositoryRule.NAME, env);
+ if (rule == null) {
+ return null;
+ }
+
+ // Given a rule that looks like this:
+ // new_local_repository(
+ // name = 'x',
+ // path = '/some/path/to/y',
+ // build_file = 'x.BUILD'
+ // )
+ // This creates the following directory structure:
+ // .external-repository/
+ // x/
+ // WORKSPACE
+ // x/
+ // BUILD -> <build_root>/x.BUILD
+ // y -> /some/path/to/y
+ //
+ // In the structure above, .external-repository/x is the repository directory and
+ // .external-repository/x/x is the package directory.
+ Path repositoryDirectory = getExternalRepositoryDirectory().getRelative(rule.getName());
+ Path outputDirectory = repositoryDirectory.getRelative(rule.getName());
+ try {
+ FileSystemUtils.createDirectoryAndParents(outputDirectory);
+ } catch (IOException e) {
+ throw new RepositoryFunctionException(e, Transience.TRANSIENT);
+ }
+ FileValue directoryValue = getRepositoryDirectory(outputDirectory, env);
+ if (directoryValue == null) {
+ return null;
+ }
+
+ // Add x/WORKSPACE.
+ try {
+ Path workspaceFile = repositoryDirectory.getRelative("WORKSPACE");
+ FileSystemUtils.writeContent(workspaceFile, Charset.forName("UTF-8"),
+ "# DO NOT EDIT: automatically generated WORKSPACE file for " + rule + "\n");
+ } catch (IOException e) {
+ throw new RepositoryFunctionException(e, Transience.TRANSIENT);
+ }
+
+ AggregatingAttributeMapper mapper = AggregatingAttributeMapper.of(rule);
+ // Link x/x/y to /some/path/to/y.
+ String path = mapper.get("path", Type.STRING);
+ PathFragment pathFragment = new PathFragment(path);
+ if (!pathFragment.isAbsolute()) {
+ throw new RepositoryFunctionException(
+ new EvalException(
+ rule.getLocation(),
+ "In " + rule + " the 'path' attribute must specify an absolute path"),
+ Transience.PERSISTENT);
+ }
+ Path pathTarget = getOutputBase().getFileSystem().getPath(pathFragment);
+ Path symlinkPath = outputDirectory.getRelative(pathTarget.getBaseName());
+ if (createSymbolicLink(symlinkPath, pathTarget, env) == null) {
+ return null;
+ }
+
+ // Link x/x/BUILD to <build_root>/x.BUILD.
+ PathFragment buildFile = new PathFragment(mapper.get("build_file", Type.STRING));
+ Path buildFileTarget = getWorkspace().getRelative(buildFile);
+ if (buildFile.equals(PathFragment.EMPTY_FRAGMENT) || buildFile.isAbsolute()
+ || !buildFileTarget.exists()) {
+ throw new RepositoryFunctionException(
+ new EvalException(rule.getLocation(), "In " + rule
+ + " the 'build_file' attribute must specify a relative path to an existing file"),
+ Transience.PERSISTENT);
+ }
+ Path buildFilePath = outputDirectory.getRelative("BUILD");
+ if (createSymbolicLink(buildFilePath, buildFileTarget, env) == null) {
+ return null;
+ }
+
+ return new RepositoryValue(repositoryDirectory, directoryValue);
+ }
+
+ private FileValue createSymbolicLink(Path from, Path to, Environment env)
+ throws RepositoryFunctionException {
+ try {
+ if (!from.exists()) {
+ from.createSymbolicLink(to);
+ }
+ } catch (IOException e) {
+ throw new RepositoryFunctionException(e, Transience.TRANSIENT);
+ }
+ FileValue fromValue = getRepositoryDirectory(from, env);
+ return fromValue;
+ }
+
+ @Override
+ public SkyFunctionName getSkyFunctionName() {
+ return SkyFunctionName.computed(NewLocalRepositoryRule.NAME.toUpperCase());
+ }
+
+ @Override
+ public Class<? extends RuleDefinition> getRuleDefinition() {
+ return NewLocalRepositoryRule.class;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/repository/RepositoryDelegatorFunction.java b/src/main/java/com/google/devtools/build/lib/bazel/repository/RepositoryDelegatorFunction.java
new file mode 100644
index 0000000..f0af0c6
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/repository/RepositoryDelegatorFunction.java
@@ -0,0 +1,72 @@
+// Copyright 2014 Google Inc. 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.bazel.repository;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.packages.NoSuchPackageException;
+import com.google.devtools.build.lib.packages.PackageIdentifier.RepositoryName;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.syntax.EvalException;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyFunctionException;
+import com.google.devtools.build.skyframe.SkyFunctionException.Transience;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.io.IOException;
+
+/**
+ * Implements delegation to the correct repository fetcher.
+ */
+public class RepositoryDelegatorFunction implements SkyFunction {
+
+ // Mapping of rule class name to SkyFunction.
+ private final ImmutableMap<String, RepositoryFunction> handlers;
+
+ public RepositoryDelegatorFunction(
+ ImmutableMap<String, RepositoryFunction> handlers) {
+ this.handlers = handlers;
+ }
+
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env) throws SkyFunctionException {
+ RepositoryName repositoryName = (RepositoryName) skyKey.argument();
+ Rule rule = RepositoryFunction.getRule(repositoryName, null, env);
+ if (rule == null) {
+ return null;
+ }
+ RepositoryFunction handler = handlers.get(rule.getRuleClass());
+ if (handler == null) {
+ throw new IllegalStateException("Could not find handler for " + rule);
+ }
+ SkyKey key = new SkyKey(handler.getSkyFunctionName(), repositoryName);
+
+ try {
+ return env.getValueOrThrow(
+ key, NoSuchPackageException.class, IOException.class, EvalException.class);
+ } catch (NoSuchPackageException e) {
+ throw new RepositoryFunction.RepositoryFunctionException(e, Transience.PERSISTENT);
+ } catch (IOException e) {
+ throw new RepositoryFunction.RepositoryFunctionException(e, Transience.PERSISTENT);
+ } catch (EvalException e) {
+ throw new RepositoryFunction.RepositoryFunctionException(e, Transience.PERSISTENT);
+ }
+ }
+
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return null;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/repository/RepositoryFunction.java b/src/main/java/com/google/devtools/build/lib/bazel/repository/RepositoryFunction.java
new file mode 100644
index 0000000..906c38b
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/repository/RepositoryFunction.java
@@ -0,0 +1,181 @@
+// Copyright 2014 Google Inc. 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.bazel.repository;
+
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.analysis.BlazeDirectories;
+import com.google.devtools.build.lib.analysis.RuleDefinition;
+import com.google.devtools.build.lib.packages.BuildFileContainsErrorsException;
+import com.google.devtools.build.lib.packages.BuildFileNotFoundException;
+import com.google.devtools.build.lib.packages.ExternalPackage;
+import com.google.devtools.build.lib.packages.NoSuchPackageException;
+import com.google.devtools.build.lib.packages.PackageIdentifier;
+import com.google.devtools.build.lib.packages.PackageIdentifier.RepositoryName;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.skyframe.FileSymlinkCycleException;
+import com.google.devtools.build.lib.skyframe.FileValue;
+import com.google.devtools.build.lib.skyframe.InconsistentFilesystemException;
+import com.google.devtools.build.lib.skyframe.PackageFunction;
+import com.google.devtools.build.lib.skyframe.PackageValue;
+import com.google.devtools.build.lib.syntax.EvalException;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.lib.vfs.RootedPath;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyFunctionException;
+import com.google.devtools.build.skyframe.SkyFunctionException.Transience;
+import com.google.devtools.build.skyframe.SkyFunctionName;
+import com.google.devtools.build.skyframe.SkyKey;
+
+import java.io.IOException;
+
+import javax.annotation.Nullable;
+
+/**
+ * Parent class for repository-related Skyframe functions.
+ */
+public abstract class RepositoryFunction implements SkyFunction {
+ private static final String EXTERNAL_REPOSITORY_DIRECTORY = ".external-repository";
+ private BlazeDirectories directories;
+
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return null;
+ }
+
+ /**
+ * Gets Skyframe's name for this.
+ */
+ public abstract SkyFunctionName getSkyFunctionName();
+
+ /**
+ * Sets up output path information.
+ */
+ public void setDirectories(BlazeDirectories directories) {
+ this.directories = directories;
+ }
+
+ protected Path getExternalRepositoryDirectory() {
+ return RepositoryFunction.getExternalRepositoryDirectory(directories);
+ }
+
+ public static Path getExternalRepositoryDirectory(BlazeDirectories directories) {
+ return directories.getOutputBase().getRelative(EXTERNAL_REPOSITORY_DIRECTORY);
+ }
+
+ /**
+ * Gets the base directory repositories should be stored in locally.
+ */
+ protected Path getOutputBase() {
+ return directories.getOutputBase();
+ }
+
+ /**
+ * Gets the directory the WORKSPACE file for the build is in.
+ */
+ protected Path getWorkspace() {
+ return directories.getWorkspace();
+ }
+
+
+ /**
+ * Returns the RuleDefinition class for this type of repository.
+ */
+ public abstract Class<? extends RuleDefinition> getRuleDefinition();
+
+ /**
+ * Uses a remote repository name to fetch the corresponding Rule describing how to get it.
+ * This should be called from {@link SkyFunction#compute} functions, which should return null if
+ * this returns null. If {@code ruleClassName} is set, the rule found must have a matching rule
+ * class name.
+ */
+ @Nullable
+ public static Rule getRule(
+ RepositoryName repositoryName, @Nullable String ruleClassName, Environment env)
+ throws RepositoryFunctionException {
+ SkyKey packageKey = PackageValue.key(
+ PackageIdentifier.createInDefaultRepo(PackageFunction.EXTERNAL_PACKAGE_NAME));
+ PackageValue packageValue;
+ try {
+ packageValue = (PackageValue) env.getValueOrThrow(packageKey,
+ NoSuchPackageException.class);
+ } catch (NoSuchPackageException e) {
+ throw new RepositoryFunctionException(
+ new BuildFileNotFoundException(
+ PackageFunction.EXTERNAL_PACKAGE_NAME, "Could not load //external package"),
+ Transience.PERSISTENT);
+ }
+ if (packageValue == null) {
+ return null;
+ }
+ ExternalPackage externalPackage = (ExternalPackage) packageValue.getPackage();
+ Rule rule = externalPackage.getRepositoryInfo(repositoryName);
+ if (rule == null) {
+ throw new RepositoryFunctionException(
+ new BuildFileContainsErrorsException(
+ PackageFunction.EXTERNAL_PACKAGE_NAME,
+ "The repository named '" + repositoryName + "' could not be resolved"),
+ Transience.PERSISTENT);
+ }
+ Preconditions.checkState(ruleClassName == null || rule.getRuleClass().equals(ruleClassName),
+ "Got " + rule + ", was expecting a " + ruleClassName);
+ return rule;
+ }
+
+ /**
+ * Adds the repository's directory to the graph and, if it's a symlink, resolves it to an
+ * actual directory.
+ */
+ @Nullable
+ protected static FileValue getRepositoryDirectory(Path repositoryDirectory, Environment env)
+ throws RepositoryFunctionException {
+ SkyKey outputDirectoryKey = FileValue.key(RootedPath.toRootedPath(
+ repositoryDirectory, PathFragment.EMPTY_FRAGMENT));
+ try {
+ return (FileValue) env.getValueOrThrow(outputDirectoryKey, IOException.class,
+ FileSymlinkCycleException.class, InconsistentFilesystemException.class);
+ } catch (IOException | FileSymlinkCycleException | InconsistentFilesystemException e) {
+ throw new RepositoryFunctionException(
+ new IOException("Could not access " + repositoryDirectory + ": " + e.getMessage()),
+ Transience.PERSISTENT);
+ }
+ }
+
+ /**
+ * Exception thrown when something goes wrong accessing a remote repository.
+ *
+ * This exception should be used by child classes to limit the types of exceptions
+ * {@link RepositoryDelegatorFunction} has to know how to catch.
+ */
+ static final class RepositoryFunctionException extends SkyFunctionException {
+ public RepositoryFunctionException(NoSuchPackageException cause, Transience transience) {
+ super(cause, transience);
+ }
+
+ /**
+ * Error reading or writing to the filesystem.
+ */
+ public RepositoryFunctionException(IOException cause, Transience transience) {
+ super(cause, transience);
+ }
+
+ /**
+ * For errors in WORKSPACE file rules (e.g., malformed paths or URLs).
+ */
+ public RepositoryFunctionException(EvalException cause, Transience transience) {
+ super(cause, transience);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/BazelBaseRuleClasses.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/BazelBaseRuleClasses.java
new file mode 100644
index 0000000..a1b27fe
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/BazelBaseRuleClasses.java
@@ -0,0 +1,73 @@
+// Copyright 2014 Google Inc. 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.bazel.rules;
+
+import static com.google.devtools.build.lib.packages.Attribute.attr;
+import static com.google.devtools.build.lib.packages.Type.BOOLEAN;
+import static com.google.devtools.build.lib.packages.Type.LICENSE;
+import static com.google.devtools.build.lib.packages.Type.STRING_LIST;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.analysis.BaseRuleClasses;
+import com.google.devtools.build.lib.analysis.BlazeRule;
+import com.google.devtools.build.lib.analysis.RuleDefinition;
+import com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.RuleClass.Builder;
+import com.google.devtools.build.lib.packages.RuleClass.Builder.RuleClassType;
+
+/**
+ * The foundational rule templates to help in real rule construction. Only attributes truly common
+ * to all rules go in here. Attributes such as "out", "outs", "src" and "srcs" exhibit enough
+ * variation that we declare them explicitly for each rule. This leads to stricter error checking
+ * and prevents users from inadvertently using an attribute that doesn't actually do anything.
+ */
+public class BazelBaseRuleClasses {
+ public static final ImmutableSet<String> ALLOWED_RULE_CLASSES =
+ ImmutableSet.of("filegroup", "genrule", "Fileset");
+
+ /**
+ * A base rule for all binary rules.
+ */
+ @BlazeRule(name = "$binary_base_rule",
+ type = RuleClassType.ABSTRACT)
+ public static final class BinaryBaseRule implements RuleDefinition {
+ @Override
+ public RuleClass build(Builder builder, RuleDefinitionEnvironment env) {
+ return builder
+ .add(attr("args", STRING_LIST)
+ .nonconfigurable("policy decision: should be consistent across configurations"))
+ .add(attr("output_licenses", LICENSE))
+ .add(attr("$is_executable", BOOLEAN).value(true)
+ .nonconfigurable("Called from RunCommand.isExecutable, which takes a Target"))
+ .build();
+ }
+ }
+
+ /**
+ * Rule class for rules in error.
+ */
+ @BlazeRule(name = "$error_rule",
+ type = RuleClassType.ABSTRACT,
+ ancestors = { BaseRuleClasses.BaseRule.class })
+ public static final class ErrorRule implements RuleDefinition {
+ @Override
+ public RuleClass build(Builder builder, RuleDefinitionEnvironment env) {
+ return builder
+ .publicByDefault()
+ .build();
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/BazelConfiguration.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/BazelConfiguration.java
new file mode 100644
index 0000000..63aa4e3
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/BazelConfiguration.java
@@ -0,0 +1,70 @@
+// Copyright 2014 Google Inc. 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.bazel.rules;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration.Fragment;
+import com.google.devtools.build.lib.analysis.config.BuildOptions;
+import com.google.devtools.build.lib.analysis.config.ConfigurationEnvironment;
+import com.google.devtools.build.lib.analysis.config.ConfigurationFragmentFactory;
+import com.google.devtools.build.lib.analysis.config.InvalidConfigurationException;
+import com.google.devtools.build.lib.util.OS;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+/**
+ * Bazel-specific configuration fragment.
+ */
+public class BazelConfiguration extends Fragment {
+ /**
+ * Loader for Google-specific settings.
+ */
+ public static class Loader implements ConfigurationFragmentFactory {
+ @Override
+ public Fragment create(ConfigurationEnvironment env, BuildOptions buildOptions)
+ throws InvalidConfigurationException {
+ return new BazelConfiguration();
+ }
+
+ @Override
+ public Class<? extends Fragment> creates() {
+ return BazelConfiguration.class;
+ }
+ }
+
+ public BazelConfiguration() {
+ }
+
+ @Override
+ public String getName() {
+ return "Bazel";
+ }
+
+ @Override
+ public String cacheKey() {
+ return "";
+ }
+
+ @Override
+ public void defineExecutables(ImmutableMap.Builder<String, PathFragment> builder) {
+ if (OS.getCurrent() == OS.WINDOWS) {
+ String path = System.getenv("BAZEL_SH");
+ if (path != null) {
+ builder.put("sh", new PathFragment(path));
+ return;
+ }
+ }
+ builder.put("sh", new PathFragment("/bin/bash"));
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/BazelConfigurationCollection.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/BazelConfigurationCollection.java
new file mode 100644
index 0000000..1472b43
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/BazelConfigurationCollection.java
@@ -0,0 +1,235 @@
+// Copyright 2014 Google Inc. 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.bazel.rules;
+
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Table;
+import com.google.devtools.build.lib.analysis.ConfigurationCollectionFactory;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.analysis.config.BuildConfigurationCollection;
+import com.google.devtools.build.lib.analysis.config.BuildConfigurationCollection.ConfigurationHolder;
+import com.google.devtools.build.lib.analysis.config.BuildConfigurationCollection.Transitions;
+import com.google.devtools.build.lib.analysis.config.BuildConfigurationKey;
+import com.google.devtools.build.lib.analysis.config.BuildOptions;
+import com.google.devtools.build.lib.analysis.config.ConfigurationFactory;
+import com.google.devtools.build.lib.analysis.config.InvalidConfigurationException;
+import com.google.devtools.build.lib.analysis.config.PackageProviderForConfigurations;
+import com.google.devtools.build.lib.bazel.rules.cpp.BazelCppRuleClasses.CppTransition;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.packages.AggregatingAttributeMapper;
+import com.google.devtools.build.lib.packages.Attribute.ConfigurationTransition;
+import com.google.devtools.build.lib.packages.Attribute.Transition;
+import com.google.devtools.build.lib.packages.NoSuchThingException;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.syntax.Label;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+/**
+ * Configuration collection used by the rules Bazel knows.
+ */
+public class BazelConfigurationCollection implements ConfigurationCollectionFactory {
+ @Override
+ @Nullable
+ public BuildConfiguration createConfigurations(
+ ConfigurationFactory configurationFactory,
+ PackageProviderForConfigurations loadedPackageProvider,
+ BuildOptions buildOptions,
+ Map<String, String> clientEnv,
+ EventHandler errorEventListener,
+ boolean performSanityCheck) throws InvalidConfigurationException {
+
+ // We cache all the related configurations for this target configuration in a cache that is
+ // dropped at the end of this method call. We instead rely on the cache for entire collections
+ // for caching the target and related configurations, and on a dedicated host configuration
+ // cache for the host configuration.
+ Cache<String, BuildConfiguration> cache =
+ CacheBuilder.newBuilder().<String, BuildConfiguration>build();
+
+ // Target configuration
+ BuildConfiguration targetConfiguration = configurationFactory.getConfiguration(
+ loadedPackageProvider, buildOptions, clientEnv, false, cache);
+ if (targetConfiguration == null) {
+ return null;
+ }
+
+ BuildConfiguration dataConfiguration = targetConfiguration;
+
+ // Host configuration
+ // Note that this passes in the dataConfiguration, not the target
+ // configuration. This is intentional.
+ BuildConfiguration hostConfiguration = getHostConfigurationFromRequest(configurationFactory,
+ loadedPackageProvider, clientEnv, dataConfiguration, buildOptions);
+ if (hostConfiguration == null) {
+ return null;
+ }
+
+ // Sanity check that the implicit labels are all in the transitive closure of explicit ones.
+ // This also registers all targets in the cache entry and validates them on subsequent requests.
+ Set<Label> reachableLabels = new HashSet<>();
+ if (performSanityCheck) {
+ // We allow the package provider to be null for testing.
+ for (Label label : buildOptions.getAllLabels().values()) {
+ try {
+ collectTransitiveClosure(loadedPackageProvider, reachableLabels, label);
+ } catch (NoSuchThingException e) {
+ // We've loaded the transitive closure of the labels-to-load above, and made sure that
+ // there are no errors loading it, so this can't happen.
+ throw new IllegalStateException(e);
+ }
+ }
+ sanityCheckImplicitLabels(reachableLabels, targetConfiguration);
+ sanityCheckImplicitLabels(reachableLabels, hostConfiguration);
+ }
+
+ BuildConfiguration result = setupTransitions(
+ targetConfiguration, dataConfiguration, hostConfiguration);
+ result.reportInvalidOptions(errorEventListener);
+ return result;
+ }
+
+ /**
+ * Gets the correct host configuration for this build. The behavior
+ * depends on the value of the --distinct_host_configuration flag.
+ *
+ * <p>With --distinct_host_configuration=false, we use identical configurations
+ * for the host and target, and you can ignore everything below. But please
+ * note: if you're cross-compiling for k8 on a piii machine, your build will
+ * fail. This is a stopgap measure.
+ *
+ * <p>Currently, every build is (in effect) a cross-compile, in the strict
+ * sense that host and target configurations are unequal, thus we do not
+ * issue a "cross-compiling" warning. (Perhaps we should?)
+ * *
+ * @param requestConfig the requested target (not host!) configuration for
+ * this build.
+ * @param buildOptions the configuration options used for the target configuration
+ */
+ @Nullable
+ private BuildConfiguration getHostConfigurationFromRequest(
+ ConfigurationFactory configurationFactory,
+ PackageProviderForConfigurations loadedPackageProvider, Map<String, String> clientEnv,
+ BuildConfiguration requestConfig, BuildOptions buildOptions)
+ throws InvalidConfigurationException {
+ BuildConfiguration.Options commonOptions = buildOptions.get(BuildConfiguration.Options.class);
+ if (!commonOptions.useDistinctHostConfiguration) {
+ return requestConfig;
+ } else {
+ BuildConfiguration hostConfig = configurationFactory.getHostConfiguration(
+ loadedPackageProvider, clientEnv, buildOptions, /*fallback=*/false);
+ if (hostConfig == null) {
+ return null;
+ }
+ return hostConfig;
+ }
+ }
+
+ static BuildConfiguration setupTransitions(BuildConfiguration targetConfiguration,
+ BuildConfiguration dataConfiguration, BuildConfiguration hostConfiguration) {
+ Set<BuildConfiguration> allConfigurations = ImmutableSet.of(targetConfiguration,
+ dataConfiguration, hostConfiguration);
+
+ Table<BuildConfiguration, Transition, ConfigurationHolder> transitionBuilder =
+ HashBasedTable.create();
+ for (BuildConfiguration from : allConfigurations) {
+ for (ConfigurationTransition transition : ConfigurationTransition.values()) {
+ BuildConfiguration to;
+ if (transition == ConfigurationTransition.HOST) {
+ to = hostConfiguration;
+ } else if (transition == ConfigurationTransition.DATA && from == targetConfiguration) {
+ to = dataConfiguration;
+ } else {
+ to = from;
+ }
+ transitionBuilder.put(from, transition, new ConfigurationHolder(to));
+ }
+ }
+
+ // TODO(bazel-team): This makes LIPO totally not work. Just a band-aid until we get around to
+ // implementing a way for the C++ rules to contribute this transition to the configuration
+ // collection.
+ for (BuildConfiguration config : allConfigurations) {
+ transitionBuilder.put(config, CppTransition.LIPO_COLLECTOR, new ConfigurationHolder(config));
+ transitionBuilder.put(config, CppTransition.TARGET_CONFIG_FOR_LIPO,
+ new ConfigurationHolder(config.isHostConfiguration() ? null : config));
+ }
+
+ for (BuildConfiguration config : allConfigurations) {
+ Transitions outgoingTransitions =
+ new BuildConfigurationCollection.Transitions(config, transitionBuilder.row(config));
+ // We allow host configurations to be shared between target configurations. In that case, the
+ // transitions may already be set.
+ // TODO(bazel-team): Check that the transitions are identical, or even better, change the
+ // code to set the host configuration transitions before we even create the target
+ // configuration.
+ if (config.isHostConfiguration() && config.getTransitions() != null) {
+ continue;
+ }
+ config.setConfigurationTransitions(outgoingTransitions);
+ }
+
+ return targetConfiguration;
+ }
+
+ /**
+ * Checks that the implicit labels are reachable from the loaded labels. The loaded labels are
+ * those returned from {@link BuildConfigurationKey#getLabelsToLoadUnconditionally()}, and the
+ * implicit ones are those that need to be available for late-bound attributes.
+ */
+ private void sanityCheckImplicitLabels(Collection<Label> reachableLabels,
+ BuildConfiguration config) throws InvalidConfigurationException {
+ for (Map.Entry<String, Label> entry : config.getImplicitLabels().entries()) {
+ if (!reachableLabels.contains(entry.getValue())) {
+ throw new InvalidConfigurationException("The required " + entry.getKey()
+ + " target is not transitively reachable from a command-line option: '"
+ + entry.getValue() + "'");
+ }
+ }
+ }
+
+ private void collectTransitiveClosure(PackageProviderForConfigurations loadedPackageProvider,
+ Set<Label> reachableLabels, Label from) throws NoSuchThingException {
+ if (!reachableLabels.add(from)) {
+ return;
+ }
+ Target fromTarget = loadedPackageProvider.getLoadedTarget(from);
+ if (fromTarget instanceof Rule) {
+ Rule rule = (Rule) fromTarget;
+ if (rule.getRuleClassObject().hasAttr("srcs", Type.LABEL_LIST)) {
+ // TODO(bazel-team): refine this. This visits "srcs" reachable under *any* configuration,
+ // not necessarily the configuration actually applied to the rule. We should correlate the
+ // two. However, doing so requires faithfully reflecting the configuration transitions that
+ // might happen as we traverse the dependency chain.
+ for (List<Label> labelsForConfiguration :
+ AggregatingAttributeMapper.of(rule).visitAttribute("srcs", Type.LABEL_LIST)) {
+ for (Label label : labelsForConfiguration) {
+ collectTransitiveClosure(loadedPackageProvider, reachableLabels, label);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/BazelRuleClassProvider.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/BazelRuleClassProvider.java
new file mode 100644
index 0000000..1280bdb
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/BazelRuleClassProvider.java
@@ -0,0 +1,272 @@
+// Copyright 2014 Google Inc. 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.bazel.rules;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Functions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.analysis.BaseRuleClasses;
+import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider;
+import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider.PrerequisiteValidator;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.analysis.config.BuildOptions;
+import com.google.devtools.build.lib.analysis.config.ConfigRuleClasses;
+import com.google.devtools.build.lib.analysis.config.ConfigurationEnvironment;
+import com.google.devtools.build.lib.analysis.config.FragmentOptions;
+import com.google.devtools.build.lib.analysis.config.InvalidConfigurationException;
+import com.google.devtools.build.lib.analysis.constraints.EnvironmentRule;
+import com.google.devtools.build.lib.bazel.rules.common.BazelActionListenerRule;
+import com.google.devtools.build.lib.bazel.rules.common.BazelExtraActionRule;
+import com.google.devtools.build.lib.bazel.rules.common.BazelFilegroupRule;
+import com.google.devtools.build.lib.bazel.rules.common.BazelTestSuiteRule;
+import com.google.devtools.build.lib.bazel.rules.cpp.BazelCppRuleClasses;
+import com.google.devtools.build.lib.bazel.rules.genrule.BazelGenRuleRule;
+import com.google.devtools.build.lib.bazel.rules.java.BazelJavaBinaryRule;
+import com.google.devtools.build.lib.bazel.rules.java.BazelJavaBuildInfoFactory;
+import com.google.devtools.build.lib.bazel.rules.java.BazelJavaImportRule;
+import com.google.devtools.build.lib.bazel.rules.java.BazelJavaLibraryRule;
+import com.google.devtools.build.lib.bazel.rules.java.BazelJavaPluginRule;
+import com.google.devtools.build.lib.bazel.rules.java.BazelJavaRuleClasses;
+import com.google.devtools.build.lib.bazel.rules.java.BazelJavaTestRule;
+import com.google.devtools.build.lib.bazel.rules.objc.BazelIosTestRule;
+import com.google.devtools.build.lib.bazel.rules.sh.BazelShBinaryRule;
+import com.google.devtools.build.lib.bazel.rules.sh.BazelShLibraryRule;
+import com.google.devtools.build.lib.bazel.rules.sh.BazelShRuleClasses;
+import com.google.devtools.build.lib.bazel.rules.sh.BazelShTestRule;
+import com.google.devtools.build.lib.bazel.rules.workspace.HttpArchiveRule;
+import com.google.devtools.build.lib.bazel.rules.workspace.HttpJarRule;
+import com.google.devtools.build.lib.bazel.rules.workspace.LocalRepositoryRule;
+import com.google.devtools.build.lib.bazel.rules.workspace.MavenJarRule;
+import com.google.devtools.build.lib.bazel.rules.workspace.NewLocalRepositoryRule;
+import com.google.devtools.build.lib.packages.Attribute;
+import com.google.devtools.build.lib.packages.PackageGroup;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.rules.cpp.CcToolchainRule;
+import com.google.devtools.build.lib.rules.cpp.CppConfiguration;
+import com.google.devtools.build.lib.rules.cpp.CppConfigurationLoader;
+import com.google.devtools.build.lib.rules.cpp.CppOptions;
+import com.google.devtools.build.lib.rules.java.JavaConfiguration;
+import com.google.devtools.build.lib.rules.java.JavaConfigurationLoader;
+import com.google.devtools.build.lib.rules.java.JavaCpuSupplier;
+import com.google.devtools.build.lib.rules.java.JavaImportBaseRule;
+import com.google.devtools.build.lib.rules.java.JavaOptions;
+import com.google.devtools.build.lib.rules.java.JavaToolchainRule;
+import com.google.devtools.build.lib.rules.java.Jvm;
+import com.google.devtools.build.lib.rules.java.JvmConfigurationLoader;
+import com.google.devtools.build.lib.rules.objc.IosApplicationRule;
+import com.google.devtools.build.lib.rules.objc.IosDeviceRule;
+import com.google.devtools.build.lib.rules.objc.ObjcBinaryRule;
+import com.google.devtools.build.lib.rules.objc.ObjcBundleLibraryRule;
+import com.google.devtools.build.lib.rules.objc.ObjcBundleRule;
+import com.google.devtools.build.lib.rules.objc.ObjcCommandLineOptions;
+import com.google.devtools.build.lib.rules.objc.ObjcConfigurationLoader;
+import com.google.devtools.build.lib.rules.objc.ObjcFrameworkRule;
+import com.google.devtools.build.lib.rules.objc.ObjcImportRule;
+import com.google.devtools.build.lib.rules.objc.ObjcLibraryRule;
+import com.google.devtools.build.lib.rules.objc.ObjcOptionsRule;
+import com.google.devtools.build.lib.rules.objc.ObjcProtoLibraryRule;
+import com.google.devtools.build.lib.rules.objc.ObjcRuleClasses;
+import com.google.devtools.build.lib.rules.objc.ObjcXcodeprojRule;
+import com.google.devtools.build.lib.rules.workspace.BindRule;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.syntax.SkylarkType;
+
+/**
+ * A rule class provider implementing the rules Bazel knows.
+ */
+public class BazelRuleClassProvider {
+
+ /**
+ * Used by the build encyclopedia generator.
+ */
+ public static ConfiguredRuleClassProvider create() {
+ ConfiguredRuleClassProvider.Builder builder =
+ new ConfiguredRuleClassProvider.Builder();
+ setup(builder);
+ return builder.build();
+ }
+
+ public static final JavaCpuSupplier JAVA_CPU_SUPPLIER = new JavaCpuSupplier() {
+ @Override
+ public String getJavaCpu(BuildOptions buildOptions, ConfigurationEnvironment env)
+ throws InvalidConfigurationException {
+ JavaOptions javaOptions = buildOptions.get(JavaOptions.class);
+ return javaOptions.javaCpu == null ? "default" : javaOptions.javaCpu;
+ }
+ };
+
+ private static class BazelPrerequisiteValidator implements PrerequisiteValidator {
+ @Override
+ public void validate(RuleContext.Builder context,
+ ConfiguredTarget prerequisite, Attribute attribute) {
+ validateDirectPrerequisiteVisibility(context, prerequisite, attribute.getName());
+ }
+
+ private void validateDirectPrerequisiteVisibility(
+ RuleContext.Builder context, ConfiguredTarget prerequisite, String attrName) {
+ Rule rule = context.getRule();
+ Target prerequisiteTarget = prerequisite.getTarget();
+ Label prerequisiteLabel = prerequisiteTarget.getLabel();
+ // We don't check the visibility of late-bound attributes, because it would break some
+ // features.
+ if (!context.getRule().getLabel().getPackageName().equals(
+ prerequisite.getTarget().getLabel().getPackageName())
+ && !context.isVisible(prerequisite)) {
+ if (!context.getConfiguration().checkVisibility()) {
+ context.ruleWarning(String.format("Target '%s' violates visibility of target "
+ + "'%s'. Continuing because --nocheck_visibility is active",
+ rule.getLabel(), prerequisiteLabel));
+ } else {
+ // Oddly enough, we use reportError rather than ruleError here.
+ context.reportError(rule.getLocation(),
+ String.format("Target '%s' is not visible from target '%s'. Check "
+ + "the visibility declaration of the former target if you think "
+ + "the dependency is legitimate",
+ prerequisiteLabel, rule.getLabel()));
+ }
+ }
+
+ if (prerequisiteTarget instanceof PackageGroup) {
+ if (!attrName.equals("visibility")) {
+ context.reportError(rule.getAttributeLocation(attrName),
+ "in " + attrName + " attribute of " + rule.getRuleClass()
+ + " rule " + rule.getLabel() + ": package group '"
+ + prerequisiteLabel + "' is misplaced here "
+ + "(they are only allowed in the visibility attribute)");
+ }
+ }
+ }
+ }
+
+ /**
+ * List of all build option classes in Blaze.
+ */
+ // TODO(bazel-team): make this private, remove from tests, then BuildOptions.of can be merged
+ // into RuleClassProvider.
+ @VisibleForTesting
+ @SuppressWarnings("unchecked")
+ public static final ImmutableList<Class<? extends FragmentOptions>> BUILD_OPTIONS =
+ ImmutableList.of(
+ BuildConfiguration.Options.class,
+ CppOptions.class,
+ JavaOptions.class,
+ ObjcCommandLineOptions.class
+ );
+
+ /**
+ * Java objects accessible from Skylark rule implementations using this module.
+ */
+ private static final ImmutableMap<String, SkylarkType> skylarkBuiltinJavaObects =
+ ImmutableMap.of(
+ "jvm", SkylarkType.of(Jvm.class),
+ "java_configuration", SkylarkType.of(JavaConfiguration.class),
+ "cpp", SkylarkType.of(CppConfiguration.class));
+
+ public static void setup(ConfiguredRuleClassProvider.Builder builder) {
+ builder
+ .addBuildInfoFactory(new BazelJavaBuildInfoFactory())
+ .setConfigurationCollectionFactory(new BazelConfigurationCollection())
+ .setPrerequisiteValidator(new BazelPrerequisiteValidator())
+ .setSkylarkAccessibleJavaClasses(skylarkBuiltinJavaObects);
+
+ for (Class<? extends FragmentOptions> fragmentOptions : BUILD_OPTIONS) {
+ builder.addConfigurationOptions(fragmentOptions);
+ }
+
+ builder.addRuleDefinition(BaseRuleClasses.BaseRule.class);
+ builder.addRuleDefinition(BaseRuleClasses.RuleBase.class);
+ builder.addRuleDefinition(BazelBaseRuleClasses.BinaryBaseRule.class);
+ builder.addRuleDefinition(BaseRuleClasses.TestBaseRule.class);
+ builder.addRuleDefinition(BazelBaseRuleClasses.ErrorRule.class);
+
+ builder.addRuleDefinition(EnvironmentRule.class);
+
+ builder.addRuleDefinition(ConfigRuleClasses.ConfigBaseRule.class);
+ builder.addRuleDefinition(ConfigRuleClasses.ConfigSettingRule.class);
+
+ builder.addRuleDefinition(BazelFilegroupRule.class);
+ builder.addRuleDefinition(BazelTestSuiteRule.class);
+ builder.addRuleDefinition(BazelGenRuleRule.class);
+
+ builder.addRuleDefinition(BazelShRuleClasses.ShRule.class);
+ builder.addRuleDefinition(BazelShLibraryRule.class);
+ builder.addRuleDefinition(BazelShBinaryRule.class);
+ builder.addRuleDefinition(BazelShTestRule.class);
+
+ builder.addRuleDefinition(CcToolchainRule.class);
+ builder.addRuleDefinition(BazelCppRuleClasses.CcLinkingRule.class);
+ builder.addRuleDefinition(BazelCppRuleClasses.CcDeclRule.class);
+ builder.addRuleDefinition(BazelCppRuleClasses.CcBaseRule.class);
+ builder.addRuleDefinition(BazelCppRuleClasses.CcRule.class);
+ builder.addRuleDefinition(BazelCppRuleClasses.CcBinaryBaseRule.class);
+ builder.addRuleDefinition(BazelCppRuleClasses.CcBinaryRule.class);
+ builder.addRuleDefinition(BazelCppRuleClasses.CcTestRule.class);
+
+ builder.addRuleDefinition(BazelCppRuleClasses.CcLibraryBaseRule.class);
+ builder.addRuleDefinition(BazelCppRuleClasses.CcLibraryRule.class);
+
+
+ builder.addRuleDefinition(BazelJavaRuleClasses.BaseJavaBinaryRule.class);
+ builder.addRuleDefinition(BazelJavaRuleClasses.IjarBaseRule.class);
+ builder.addRuleDefinition(BazelJavaRuleClasses.JavaBaseRule.class);
+ builder.addRuleDefinition(JavaImportBaseRule.class);
+ builder.addRuleDefinition(BazelJavaRuleClasses.JavaRule.class);
+ builder.addRuleDefinition(BazelJavaBinaryRule.class);
+ builder.addRuleDefinition(BazelJavaLibraryRule.class);
+ builder.addRuleDefinition(BazelJavaImportRule.class);
+ builder.addRuleDefinition(BazelJavaTestRule.class);
+ builder.addRuleDefinition(BazelJavaPluginRule.class);
+ builder.addRuleDefinition(JavaToolchainRule.class);
+
+ builder.addRuleDefinition(BazelIosTestRule.class);
+ builder.addRuleDefinition(IosDeviceRule.class);
+ builder.addRuleDefinition(ObjcBinaryRule.class);
+ builder.addRuleDefinition(ObjcBundleRule.class);
+ builder.addRuleDefinition(ObjcBundleLibraryRule.class);
+ builder.addRuleDefinition(ObjcFrameworkRule.class);
+ builder.addRuleDefinition(ObjcImportRule.class);
+ builder.addRuleDefinition(ObjcLibraryRule.class);
+ builder.addRuleDefinition(ObjcOptionsRule.class);
+ builder.addRuleDefinition(ObjcProtoLibraryRule.class);
+ builder.addRuleDefinition(ObjcXcodeprojRule.class);
+ builder.addRuleDefinition(ObjcRuleClasses.IosTestBaseRule.class);
+ builder.addRuleDefinition(ObjcRuleClasses.ObjcHasInfoplistRule.class);
+ builder.addRuleDefinition(ObjcRuleClasses.ObjcHasEntitlementsRule.class);
+ builder.addRuleDefinition(ObjcRuleClasses.ObjcCompilationRule.class);
+ builder.addRuleDefinition(ObjcRuleClasses.ObjcBaseResourcesRule.class);
+ builder.addRuleDefinition(IosApplicationRule.class);
+
+ builder.addRuleDefinition(BazelExtraActionRule.class);
+ builder.addRuleDefinition(BazelActionListenerRule.class);
+
+ builder.addRuleDefinition(BindRule.class);
+ builder.addRuleDefinition(HttpArchiveRule.class);
+ builder.addRuleDefinition(HttpJarRule.class);
+ builder.addRuleDefinition(LocalRepositoryRule.class);
+ builder.addRuleDefinition(MavenJarRule.class);
+ builder.addRuleDefinition(NewLocalRepositoryRule.class);
+
+ builder.addConfigurationFragment(new BazelConfiguration.Loader());
+ builder.addConfigurationFragment(new CppConfigurationLoader(
+ Functions.<String>identity()));
+ builder.addConfigurationFragment(new JvmConfigurationLoader(JAVA_CPU_SUPPLIER));
+ builder.addConfigurationFragment(new JavaConfigurationLoader(JAVA_CPU_SUPPLIER));
+ builder.addConfigurationFragment(new ObjcConfigurationLoader());
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/BazelRulesModule.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/BazelRulesModule.java
new file mode 100644
index 0000000..214b367
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/BazelRulesModule.java
@@ -0,0 +1,159 @@
+// Copyright 2014 Google Inc. 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.bazel.rules;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.eventbus.Subscribe;
+import com.google.devtools.build.lib.actions.ActionContextConsumer;
+import com.google.devtools.build.lib.actions.ActionContextProvider;
+import com.google.devtools.build.lib.actions.ActionGraph;
+import com.google.devtools.build.lib.actions.ActionInputFileCache;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.Executor.ActionContext;
+import com.google.devtools.build.lib.actions.ExecutorInitException;
+import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider;
+import com.google.devtools.build.lib.rules.cpp.CppCompileActionContext;
+import com.google.devtools.build.lib.rules.cpp.CppLinkActionContext;
+import com.google.devtools.build.lib.rules.cpp.LocalGccStrategy;
+import com.google.devtools.build.lib.rules.cpp.LocalLinkStrategy;
+import com.google.devtools.build.lib.runtime.BlazeModule;
+import com.google.devtools.build.lib.runtime.BlazeRuntime;
+import com.google.devtools.build.lib.runtime.Command;
+import com.google.devtools.build.lib.runtime.GotOptionsEvent;
+import com.google.devtools.common.options.Option;
+import com.google.devtools.common.options.OptionsBase;
+import com.google.devtools.common.options.OptionsProvider;
+
+import java.util.Map;
+
+/**
+ * Module implementing the rule set of Bazel.
+ */
+public class BazelRulesModule extends BlazeModule {
+ /**
+ * Execution options affecting how we execute the build actions (but not their semantics).
+ */
+ public static class BazelExecutionOptions extends OptionsBase {
+ @Option(
+ name = "spawn_strategy",
+ defaultValue = "standalone",
+ category = "strategy",
+ help = "Specify how spawn actions are executed by default."
+ + "'standalone' means run all of them locally."
+ + "'sandboxed' means run them in namespaces based sandbox (available only on Linux)")
+ public String spawnStrategy;
+
+ @Option(
+ name = "genrule_strategy",
+ defaultValue = "standalone",
+ category = "strategy",
+ help = "Specify how to execute genrules."
+ + "'standalone' means run all of them locally."
+ + "'sandboxed' means run them in namespaces based sandbox (available only on Linux)")
+
+ public String genruleStrategy;
+ }
+
+ private static class BazelActionContextConsumer implements ActionContextConsumer {
+ BazelExecutionOptions options;
+
+ private BazelActionContextConsumer(BazelExecutionOptions options) {
+ this.options = options;
+
+ }
+ @Override
+ public Map<String, String> getSpawnActionContexts() {
+ ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
+
+ builder.put("Genrule", options.genruleStrategy);
+
+ // TODO(bazel-team): put this in getActionContexts (key=SpawnActionContext.class) instead
+ builder.put("", options.spawnStrategy);
+
+ return builder.build();
+ }
+
+ @Override
+ public Map<Class<? extends ActionContext>, String> getActionContexts() {
+ ImmutableMap.Builder<Class<? extends ActionContext>, String> builder =
+ ImmutableMap.builder();
+ builder.put(CppCompileActionContext.class, "");
+ builder.put(CppLinkActionContext.class, "");
+ return builder.build();
+ }
+ }
+
+ private class BazelActionContextProvider implements ActionContextProvider {
+ @Override
+ public Iterable<ActionContext> getActionContexts() {
+ return ImmutableList.of(
+ new LocalGccStrategy(optionsProvider),
+ new LocalLinkStrategy());
+ }
+
+ @Override
+ public void executorCreated(Iterable<ActionContext> usedContexts)
+ throws ExecutorInitException {
+ }
+
+ @Override
+ public void executionPhaseStarting(ActionInputFileCache actionInputFileCache,
+ ActionGraph actionGraph, Iterable<Artifact> topLevelArtifacts)
+ throws ExecutorInitException, InterruptedException {
+ }
+
+ @Override
+ public void executionPhaseEnding() {
+ }
+ }
+
+ private BlazeRuntime runtime;
+ private OptionsProvider optionsProvider;
+
+ @Override
+ public void beforeCommand(BlazeRuntime blazeRuntime, Command command) {
+ this.runtime = blazeRuntime;
+ runtime.getEventBus().register(this);
+ }
+
+ @Override
+ public Iterable<Class<? extends OptionsBase>> getCommandOptions(Command command) {
+ return command.builds()
+ ? ImmutableList.<Class<? extends OptionsBase>>of(BazelExecutionOptions.class)
+ : ImmutableList.<Class<? extends OptionsBase>>of();
+ }
+
+ @Override
+ public ActionContextConsumer getActionContextConsumer() {
+ return new BazelActionContextConsumer(
+ optionsProvider.getOptions(BazelExecutionOptions.class));
+ }
+
+ @Override
+ public ActionContextProvider getActionContextProvider() {
+ return new BazelActionContextProvider();
+ }
+
+ @Subscribe
+ public void gotOptions(GotOptionsEvent event) {
+ optionsProvider = event.getOptions();
+ }
+
+ @Override
+ public void initializeRuleClasses(ConfiguredRuleClassProvider.Builder builder) {
+ BazelRuleClassProvider.setup(builder);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/common/BazelActionListenerRule.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/common/BazelActionListenerRule.java
new file mode 100644
index 0000000..eba1553
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/common/BazelActionListenerRule.java
@@ -0,0 +1,47 @@
+// Copyright 2014 Google Inc. 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.bazel.rules.common;
+
+import static com.google.devtools.build.lib.packages.Attribute.attr;
+import static com.google.devtools.build.lib.packages.Type.LABEL_LIST;
+import static com.google.devtools.build.lib.packages.Type.STRING_LIST;
+
+import com.google.devtools.build.lib.analysis.BaseRuleClasses;
+import com.google.devtools.build.lib.analysis.BlazeRule;
+import com.google.devtools.build.lib.analysis.RuleDefinition;
+import com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.RuleClass.Builder;
+import com.google.devtools.build.lib.rules.extra.ActionListener;
+
+/**
+ * Rule definition for action_listener rule.
+ */
+@BlazeRule(name = "action_listener",
+ ancestors = { BaseRuleClasses.RuleBase.class },
+ factoryClass = ActionListener.class)
+public final class BazelActionListenerRule implements RuleDefinition {
+ @Override
+ public RuleClass build(Builder builder, RuleDefinitionEnvironment environment) {
+ return builder
+ .add(attr("mnemonics", STRING_LIST).mandatory())
+ .add(attr("extra_actions", LABEL_LIST).mandatory()
+ .allowedRuleClasses("extra_action")
+ .allowedFileTypes())
+ .removeAttribute("deps")
+ .removeAttribute("data")
+ .removeAttribute(":action_listener")
+ .build();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/common/BazelExtraActionRule.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/common/BazelExtraActionRule.java
new file mode 100644
index 0000000..fa73f93
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/common/BazelExtraActionRule.java
@@ -0,0 +1,49 @@
+// Copyright 2014 Google Inc. 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.bazel.rules.common;
+
+import static com.google.devtools.build.lib.packages.Attribute.ConfigurationTransition.HOST;
+import static com.google.devtools.build.lib.packages.Attribute.attr;
+import static com.google.devtools.build.lib.packages.Type.BOOLEAN;
+import static com.google.devtools.build.lib.packages.Type.LABEL_LIST;
+import static com.google.devtools.build.lib.packages.Type.STRING;
+import static com.google.devtools.build.lib.packages.Type.STRING_LIST;
+
+import com.google.devtools.build.lib.analysis.BaseRuleClasses;
+import com.google.devtools.build.lib.analysis.BlazeRule;
+import com.google.devtools.build.lib.analysis.RuleDefinition;
+import com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.RuleClass.Builder;
+import com.google.devtools.build.lib.rules.extra.ExtraActionFactory;
+
+/**
+ * Rule definition for extra_action rule.
+ */
+@BlazeRule(name = "extra_action",
+ ancestors = { BaseRuleClasses.RuleBase.class },
+ factoryClass = ExtraActionFactory.class)
+public final class BazelExtraActionRule implements RuleDefinition {
+ @Override
+ public RuleClass build(Builder builder, RuleDefinitionEnvironment environment) {
+ return builder
+ .add(attr("tools", LABEL_LIST).cfg(HOST).allowedFileTypes().exec())
+ .add(attr("out_templates", STRING_LIST))
+ .add(attr("cmd", STRING).mandatory())
+ .add(attr("requires_action_output", BOOLEAN))
+ .removeAttribute("deps")
+ .removeAttribute(":action_listener")
+ .build();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/common/BazelFilegroupRule.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/common/BazelFilegroupRule.java
new file mode 100644
index 0000000..0ff5cd0
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/common/BazelFilegroupRule.java
@@ -0,0 +1,48 @@
+// Copyright 2014 Google Inc. 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.bazel.rules.common;
+
+import static com.google.devtools.build.lib.packages.Attribute.ConfigurationTransition.DATA;
+import static com.google.devtools.build.lib.packages.Attribute.attr;
+import static com.google.devtools.build.lib.packages.Type.LABEL_LIST;
+import static com.google.devtools.build.lib.packages.Type.LICENSE;
+import static com.google.devtools.build.lib.packages.Type.STRING;
+
+import com.google.devtools.build.lib.analysis.BaseRuleClasses;
+import com.google.devtools.build.lib.analysis.BlazeRule;
+import com.google.devtools.build.lib.analysis.RuleDefinition;
+import com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.RuleClass.Builder;
+import com.google.devtools.build.lib.rules.filegroup.Filegroup;
+import com.google.devtools.build.lib.util.FileTypeSet;
+
+/**
+ * Rule object implementing "filegroup".
+ */
+@BlazeRule(name = "filegroup",
+ ancestors = { BaseRuleClasses.BaseRule.class },
+ factoryClass = Filegroup.class)
+public final class BazelFilegroupRule implements RuleDefinition {
+ @Override
+ public RuleClass build(Builder builder, RuleDefinitionEnvironment env) {
+ // filegroup ignores any filtering set with setSrcsAllowedFiles.
+ return builder
+ .add(attr("srcs", LABEL_LIST).allowedFileTypes(FileTypeSet.ANY_FILE))
+ .add(attr("data", LABEL_LIST).cfg(DATA).allowedFileTypes(FileTypeSet.ANY_FILE))
+ .add(attr("output_licenses", LICENSE))
+ .add(attr("path", STRING))
+ .build();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/common/BazelTestSuiteRule.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/common/BazelTestSuiteRule.java
new file mode 100644
index 0000000..54db469
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/common/BazelTestSuiteRule.java
@@ -0,0 +1,50 @@
+// Copyright 2014 Google Inc. 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.bazel.rules.common;
+
+import static com.google.devtools.build.lib.packages.Attribute.attr;
+import static com.google.devtools.build.lib.packages.Type.BOOLEAN;
+import static com.google.devtools.build.lib.packages.Type.LABEL_LIST;
+
+import com.google.devtools.build.lib.analysis.BaseRuleClasses;
+import com.google.devtools.build.lib.analysis.BlazeRule;
+import com.google.devtools.build.lib.analysis.RuleDefinition;
+import com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.RuleClass.Builder;
+import com.google.devtools.build.lib.rules.test.TestSuite;
+
+/**
+ * Rule object implementing "test_suite".
+ */
+@BlazeRule(name = "test_suite",
+ ancestors = { BaseRuleClasses.BaseRule.class },
+ factoryClass = TestSuite.class)
+public final class BazelTestSuiteRule implements RuleDefinition {
+ @Override
+ public RuleClass build(Builder builder, RuleDefinitionEnvironment env) {
+ return builder
+ .override(attr("testonly", BOOLEAN).value(true)
+ .nonconfigurable("policy decision: should be consistent across configurations"))
+ .add(attr("tests", LABEL_LIST).orderIndependent().allowedFileTypes()
+ .nonconfigurable("policy decision: should be consistent across configurations"))
+ .add(attr("suites", LABEL_LIST).orderIndependent().allowedFileTypes()
+ .nonconfigurable("policy decision: should be consistent across configurations"))
+ // This magic attribute contains all *test rules in the package, iff
+ // tests=[] and suites=[]:
+ .add(attr("$implicit_tests", LABEL_LIST)
+ .nonconfigurable("Accessed in TestTargetUtils without config context"))
+ .build();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/cpp/BazelCcBinary.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/cpp/BazelCcBinary.java
new file mode 100644
index 0000000..e3f62a1
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/cpp/BazelCcBinary.java
@@ -0,0 +1,26 @@
+// Copyright 2014 Google Inc. 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.bazel.rules.cpp;
+
+import com.google.devtools.build.lib.rules.cpp.CcBinary;
+
+/**
+ * Factory class for the {@code cc_binary} rule.
+ */
+public class BazelCcBinary extends CcBinary {
+ public BazelCcBinary() {
+ super(BazelCppSemantics.INSTANCE);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/cpp/BazelCcLibrary.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/cpp/BazelCcLibrary.java
new file mode 100644
index 0000000..ae38806
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/cpp/BazelCcLibrary.java
@@ -0,0 +1,26 @@
+// Copyright 2014 Google Inc. 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.bazel.rules.cpp;
+
+import com.google.devtools.build.lib.rules.cpp.CcLibrary;
+
+/**
+ * Factory class for the {@code cc_library} rule.
+ */
+public class BazelCcLibrary extends CcLibrary {
+ public BazelCcLibrary() {
+ super(BazelCppSemantics.INSTANCE);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/cpp/BazelCcTest.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/cpp/BazelCcTest.java
new file mode 100644
index 0000000..f42b1dc
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/cpp/BazelCcTest.java
@@ -0,0 +1,26 @@
+// Copyright 2014 Google Inc. 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.bazel.rules.cpp;
+
+import com.google.devtools.build.lib.rules.cpp.CcTest;
+
+/**
+ * Factory class for the {@code cc_test} rule.
+ */
+public class BazelCcTest extends CcTest {
+ public BazelCcTest() {
+ super(BazelCppSemantics.INSTANCE);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/cpp/BazelCppRuleClasses.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/cpp/BazelCppRuleClasses.java
new file mode 100644
index 0000000..4a1f3b6
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/cpp/BazelCppRuleClasses.java
@@ -0,0 +1,422 @@
+// Copyright 2014 Google Inc. 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.bazel.rules.cpp;
+
+import static com.google.devtools.build.lib.packages.Attribute.attr;
+import static com.google.devtools.build.lib.packages.ImplicitOutputsFunction.fromFunctions;
+import static com.google.devtools.build.lib.packages.ImplicitOutputsFunction.fromTemplates;
+import static com.google.devtools.build.lib.packages.Type.BOOLEAN;
+import static com.google.devtools.build.lib.packages.Type.LABEL;
+import static com.google.devtools.build.lib.packages.Type.LABEL_LIST;
+import static com.google.devtools.build.lib.packages.Type.LABEL_LIST_DICT;
+import static com.google.devtools.build.lib.packages.Type.STRING;
+import static com.google.devtools.build.lib.packages.Type.STRING_LIST;
+import static com.google.devtools.build.lib.packages.Type.TRISTATE;
+import static com.google.devtools.build.lib.rules.cpp.CppFileTypes.ALWAYS_LINK_LIBRARY;
+import static com.google.devtools.build.lib.rules.cpp.CppFileTypes.ALWAYS_LINK_PIC_LIBRARY;
+import static com.google.devtools.build.lib.rules.cpp.CppFileTypes.ARCHIVE;
+import static com.google.devtools.build.lib.rules.cpp.CppFileTypes.ASSEMBLER_WITH_C_PREPROCESSOR;
+import static com.google.devtools.build.lib.rules.cpp.CppFileTypes.CPP_HEADER;
+import static com.google.devtools.build.lib.rules.cpp.CppFileTypes.CPP_SOURCE;
+import static com.google.devtools.build.lib.rules.cpp.CppFileTypes.C_SOURCE;
+import static com.google.devtools.build.lib.rules.cpp.CppFileTypes.OBJECT_FILE;
+import static com.google.devtools.build.lib.rules.cpp.CppFileTypes.PIC_ARCHIVE;
+import static com.google.devtools.build.lib.rules.cpp.CppFileTypes.PIC_OBJECT_FILE;
+import static com.google.devtools.build.lib.rules.cpp.CppFileTypes.SHARED_LIBRARY;
+import static com.google.devtools.build.lib.rules.cpp.CppFileTypes.VERSIONED_SHARED_LIBRARY;
+
+import com.google.common.base.Predicates;
+import com.google.devtools.build.lib.analysis.BaseRuleClasses;
+import com.google.devtools.build.lib.analysis.BlazeRule;
+import com.google.devtools.build.lib.analysis.RuleDefinition;
+import com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.bazel.rules.BazelBaseRuleClasses;
+import com.google.devtools.build.lib.packages.Attribute;
+import com.google.devtools.build.lib.packages.Attribute.ConfigurationTransition;
+import com.google.devtools.build.lib.packages.Attribute.LateBoundLabel;
+import com.google.devtools.build.lib.packages.Attribute.Transition;
+import com.google.devtools.build.lib.packages.AttributeMap;
+import com.google.devtools.build.lib.packages.ImplicitOutputsFunction.SafeImplicitOutputsFunction;
+import com.google.devtools.build.lib.packages.RawAttributeMapper;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.RuleClass.Builder;
+import com.google.devtools.build.lib.packages.RuleClass.Builder.RuleClassType;
+import com.google.devtools.build.lib.packages.TriState;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.rules.cpp.CcLibrary;
+import com.google.devtools.build.lib.rules.cpp.CppConfiguration;
+import com.google.devtools.build.lib.rules.cpp.CppRuleClasses;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.util.FileTypeSet;
+import com.google.devtools.build.lib.view.config.crosstool.CrosstoolConfig.LipoMode;
+
+/**
+ * Rule class definitions for C++ rules.
+ */
+public class BazelCppRuleClasses {
+ static final SafeImplicitOutputsFunction CC_LIBRARY_DYNAMIC_LIB =
+ fromTemplates("%{dirname}lib%{basename}.so");
+
+ static final SafeImplicitOutputsFunction CC_BINARY_IMPLICIT_OUTPUTS =
+ fromFunctions(CppRuleClasses.CC_BINARY_STRIPPED, CppRuleClasses.CC_BINARY_DEBUG_PACKAGE);
+
+ static final FileTypeSet ALLOWED_SRC_FILES = FileTypeSet.of(
+ CPP_SOURCE,
+ C_SOURCE,
+ CPP_HEADER,
+ ASSEMBLER_WITH_C_PREPROCESSOR,
+ ARCHIVE,
+ PIC_ARCHIVE,
+ ALWAYS_LINK_LIBRARY,
+ ALWAYS_LINK_PIC_LIBRARY,
+ SHARED_LIBRARY,
+ VERSIONED_SHARED_LIBRARY,
+ OBJECT_FILE,
+ PIC_OBJECT_FILE);
+
+ static final String[] DEPS_ALLOWED_RULES = new String[] {
+ "cc_library",
+ };
+
+ /**
+ * Miscellaneous configuration transitions. It would be better not to have this - please don't add
+ * to it.
+ */
+ public static enum CppTransition implements Transition {
+ /**
+ * The configuration for LIPO information collection. Requesting this from a configuration that
+ * does not have lipo optimization enabled may result in an exception.
+ */
+ LIPO_COLLECTOR,
+
+ /**
+ * The corresponding (target) configuration.
+ */
+ TARGET_CONFIG_FOR_LIPO;
+
+ @Override
+ public boolean defaultsToSelf() {
+ return false;
+ }
+ }
+
+ private static final RuleClass.Configurator<BuildConfiguration, Rule> LIPO_ON_DEMAND =
+ new RuleClass.Configurator<BuildConfiguration, Rule>() {
+ @Override
+ public BuildConfiguration apply(Rule rule, BuildConfiguration configuration) {
+ BuildConfiguration toplevelConfig =
+ configuration.getConfiguration(CppTransition.TARGET_CONFIG_FOR_LIPO);
+ // If LIPO is enabled, override the default configuration.
+ if (toplevelConfig != null
+ && toplevelConfig.getFragment(CppConfiguration.class).isLipoOptimization()
+ && !configuration.isHostConfiguration()
+ && !configuration.getFragment(CppConfiguration.class).isLipoContextCollector()) {
+ // Switch back to data when the cc_binary is not the LIPO context.
+ return (rule.getLabel().equals(
+ toplevelConfig.getFragment(CppConfiguration.class).getLipoContextLabel()))
+ ? toplevelConfig
+ : configuration.getTransitions().getConfiguration(ConfigurationTransition.DATA);
+ }
+ return configuration;
+ }
+ };
+
+ /**
+ * Label of a pseudo-filegroup that contains all crosstool and libcfiles for
+ * all configurations, as specified on the command-line.
+ */
+ public static final String CROSSTOOL_LABEL = "//tools/defaults:crosstool";
+
+ public static final LateBoundLabel<BuildConfiguration> CC_TOOLCHAIN =
+ new LateBoundLabel<BuildConfiguration>(CROSSTOOL_LABEL) {
+ @Override
+ public Label getDefault(Rule rule, BuildConfiguration configuration) {
+ return configuration.getFragment(CppConfiguration.class).getCcToolchainRuleLabel();
+ }
+ };
+
+ public static final LateBoundLabel<BuildConfiguration> DEFAULT_MALLOC =
+ new LateBoundLabel<BuildConfiguration>() {
+ @Override
+ public Label getDefault(Rule rule, BuildConfiguration configuration) {
+ return configuration.getFragment(CppConfiguration.class).customMalloc();
+ }
+ };
+
+ public static final LateBoundLabel<BuildConfiguration> STL =
+ new LateBoundLabel<BuildConfiguration>() {
+ @Override
+ public Label getDefault(Rule rule, BuildConfiguration configuration) {
+ return getStl(rule, configuration);
+ }
+ };
+
+ /**
+ * Implementation for the :lipo_context_collector attribute.
+ */
+ public static final LateBoundLabel<BuildConfiguration> LIPO_CONTEXT_COLLECTOR =
+ new LateBoundLabel<BuildConfiguration>() {
+ @Override
+ public Label getDefault(Rule rule, BuildConfiguration configuration) {
+ // This attribute connects a target to the LIPO context target configured with the
+ // lipo input collector configuration.
+ CppConfiguration cppConfiguration = configuration.getFragment(CppConfiguration.class);
+ return !cppConfiguration.isLipoContextCollector()
+ && (cppConfiguration.getLipoMode() == LipoMode.BINARY)
+ ? cppConfiguration.getLipoContextLabel()
+ : null;
+ }
+ };
+
+ /**
+ * Returns the STL prerequisite of the rule.
+ *
+ * <p>If rule has an implicit $stl attribute returns STL version set on the
+ * command line or if not set, the value of the $stl attribute. Returns
+ * {@code null} otherwise.
+ */
+ private static Label getStl(Rule rule, BuildConfiguration original) {
+ Label stl = null;
+ if (rule.getRuleClassObject().hasAttr("$stl", Type.LABEL)) {
+ Label stlConfigLabel = original.getFragment(CppConfiguration.class).getStl();
+ Label stlRuleLabel = RawAttributeMapper.of(rule).get("$stl", Type.LABEL);
+ if (stlConfigLabel == null) {
+ stl = stlRuleLabel;
+ } else if (!stlConfigLabel.equals(rule.getLabel()) && stlRuleLabel != null) {
+ // prevents self-reference and a cycle through standard STL in the dependency graph
+ stl = stlConfigLabel;
+ }
+ }
+ return stl;
+ }
+
+ /**
+ * Common attributes for all rules that create C++ links. This may
+ * include non-cc_* rules (e.g. py_binary).
+ */
+ @BlazeRule(name = "$cc_linking_rule",
+ type = RuleClassType.ABSTRACT)
+ public static final class CcLinkingRule implements RuleDefinition {
+ @Override
+ @SuppressWarnings("unchecked")
+ public RuleClass build(Builder builder, RuleDefinitionEnvironment env) {
+ return builder
+ .add(attr(":cc_toolchain", LABEL).value(CC_TOOLCHAIN))
+ .setPreferredDependencyPredicate(Predicates.<String>or(CPP_SOURCE, C_SOURCE, CPP_HEADER))
+ .build();
+ }
+ }
+
+ /**
+ * Common attributes for C++ rules.
+ */
+ @BlazeRule(name = "$cc_base_rule",
+ type = RuleClassType.ABSTRACT,
+ ancestors = { CcLinkingRule.class })
+ public static final class CcBaseRule implements RuleDefinition {
+ @Override
+ public RuleClass build(Builder builder, RuleDefinitionEnvironment env) {
+ return builder
+ .add(attr("copts", STRING_LIST))
+ .add(attr("$stl", LABEL).value(env.getLabel("//tools/cpp:stl")))
+ .add(attr(":stl", LABEL).value(STL))
+ .build();
+ }
+ }
+
+ /**
+ * Helper rule class.
+ */
+ @BlazeRule(name = "$cc_decl_rule",
+ type = RuleClassType.ABSTRACT,
+ ancestors = { BaseRuleClasses.RuleBase.class })
+ public static final class CcDeclRule implements RuleDefinition {
+ @Override
+ public RuleClass build(Builder builder, RuleDefinitionEnvironment env) {
+ return builder
+ .add(attr("abi", STRING).value("$(ABI)"))
+ .add(attr("abi_deps", LABEL_LIST_DICT))
+ .add(attr("defines", STRING_LIST))
+ .add(attr("includes", STRING_LIST))
+ .add(attr(":lipo_context_collector", LABEL)
+ .cfg(CppTransition.LIPO_COLLECTOR)
+ .value(LIPO_CONTEXT_COLLECTOR))
+ .build();
+ }
+ }
+
+ /**
+ * Helper rule class.
+ */
+ @BlazeRule(name = "$cc_rule",
+ type = RuleClassType.ABSTRACT,
+ ancestors = { CcDeclRule.class, CcBaseRule.class })
+ public static final class CcRule implements RuleDefinition {
+ @Override
+ public RuleClass build(Builder builder, final RuleDefinitionEnvironment env) {
+ return builder
+ .add(attr("srcs", LABEL_LIST)
+ .direct_compile_time_input()
+ .allowedFileTypes(ALLOWED_SRC_FILES))
+ .override(attr("deps", LABEL_LIST)
+ .allowedRuleClasses(DEPS_ALLOWED_RULES)
+ .allowedFileTypes()
+ .skipAnalysisTimeFileTypeCheck())
+ .add(attr("linkopts", STRING_LIST))
+ .add(attr("nocopts", STRING))
+ .add(attr("hdrs_check", STRING).value("strict"))
+ .add(attr("linkstatic", BOOLEAN).value(true))
+ .override(attr("$stl", LABEL).value(new Attribute.ComputedDefault() {
+ @Override
+ public Object getDefault(AttributeMap rule) {
+ // Every cc_rule depends implicitly on STL to make
+ // sure that the correct headers are used for inclusion. The only exception is
+ // STL itself to avoid cycles in the dependency graph.
+ Label stl = env.getLabel("//tools/cpp:stl");
+ return rule.getLabel().equals(stl) ? null : stl;
+ }
+ }))
+ .build();
+ }
+ }
+
+ /**
+ * Helper rule class.
+ */
+ @BlazeRule(name = "$cc_binary_base",
+ type = RuleClassType.ABSTRACT,
+ ancestors = CcRule.class)
+ public static final class CcBinaryBaseRule implements RuleDefinition {
+ @Override
+ public RuleClass build(Builder builder, RuleDefinitionEnvironment env) {
+ return builder
+ .add(attr("malloc", LABEL)
+ .value(env.getLabel("//tools/cpp:malloc"))
+ .allowedFileTypes()
+ .allowedRuleClasses("cc_library"))
+ .add(attr(":default_malloc", LABEL).value(DEFAULT_MALLOC))
+ .add(attr("stamp", TRISTATE).value(TriState.AUTO))
+ .build();
+ }
+ }
+
+ /**
+ * Rule definition for cc_binary rules.
+ */
+ @BlazeRule(name = "cc_binary",
+ ancestors = { CcBinaryBaseRule.class,
+ BazelBaseRuleClasses.BinaryBaseRule.class },
+ factoryClass = BazelCcBinary.class)
+ public static final class CcBinaryRule implements RuleDefinition {
+ @Override
+ public RuleClass build(Builder builder, RuleDefinitionEnvironment env) {
+ return builder
+ .setImplicitOutputsFunction(CC_BINARY_IMPLICIT_OUTPUTS)
+ .add(attr("linkshared", BOOLEAN).value(false)
+ .nonconfigurable("used to *determine* the rule's configuration"))
+ .cfg(LIPO_ON_DEMAND)
+ .build();
+ }
+ }
+
+ /**
+ * Implementation for the :lipo_context attribute.
+ */
+ private static final LateBoundLabel<BuildConfiguration> LIPO_CONTEXT =
+ new LateBoundLabel<BuildConfiguration>() {
+ @Override
+ public Label getDefault(Rule rule, BuildConfiguration configuration) {
+ Label result = configuration.getFragment(CppConfiguration.class).getLipoContextLabel();
+ return (rule == null || rule.getLabel().equals(result)) ? null : result;
+ }
+ };
+
+ /**
+ * Rule definition for cc_test rules.
+ */
+ @BlazeRule(name = "cc_test",
+ type = RuleClassType.TEST,
+ ancestors = { CcBinaryBaseRule.class, BaseRuleClasses.TestBaseRule.class },
+ factoryClass = BazelCcTest.class)
+ public static final class CcTestRule implements RuleDefinition {
+ @Override
+ public RuleClass build(Builder builder, RuleDefinitionEnvironment env) {
+ return builder
+ .setImplicitOutputsFunction(CppRuleClasses.CC_BINARY_DEBUG_PACKAGE)
+ .override(attr("linkstatic", BOOLEAN).value(false))
+ .override(attr("stamp", TRISTATE).value(TriState.NO))
+ .add(attr(":lipo_context", LABEL).value(LIPO_CONTEXT))
+ .build();
+ }
+ }
+
+ /**
+ * Helper rule class.
+ */
+ @BlazeRule(name = "$cc_library",
+ type = RuleClassType.ABSTRACT,
+ ancestors = { CcRule.class })
+ public static final class CcLibraryBaseRule implements RuleDefinition {
+ @Override
+ public RuleClass build(Builder builder, RuleDefinitionEnvironment env) {
+ return builder
+ .add(attr("hdrs", LABEL_LIST).orderIndependent().direct_compile_time_input()
+ .allowedFileTypes(CPP_HEADER))
+ .add(attr("linkstamp", LABEL).allowedFileTypes(CPP_SOURCE, C_SOURCE))
+ .build();
+ }
+ }
+
+ /**
+ * Rule definition for the cc_library rule.
+ */
+ @BlazeRule(name = "cc_library",
+ ancestors = { CcLibraryBaseRule.class},
+ factoryClass = BazelCcLibrary.class)
+ public static final class CcLibraryRule implements RuleDefinition {
+ @Override
+ public RuleClass build(Builder builder, RuleDefinitionEnvironment env) {
+ SafeImplicitOutputsFunction implicitOutputsFunction = new SafeImplicitOutputsFunction() {
+ @Override
+ public Iterable<String> getImplicitOutputs(AttributeMap rule) {
+ boolean alwaysLink = rule.get("alwayslink", Type.BOOLEAN);
+ boolean linkstatic = rule.get("linkstatic", Type.BOOLEAN);
+ SafeImplicitOutputsFunction staticLib = fromTemplates(
+ alwaysLink
+ ? "%{dirname}lib%{basename}.lo"
+ : "%{dirname}lib%{basename}.a");
+ SafeImplicitOutputsFunction allLibs =
+ linkstatic || CcLibrary.appearsToHaveNoObjectFiles(rule)
+ ? staticLib
+ : fromFunctions(staticLib, CC_LIBRARY_DYNAMIC_LIB);
+ return allLibs.getImplicitOutputs(rule);
+ }
+ };
+
+ return builder
+ .setImplicitOutputsFunction(implicitOutputsFunction)
+ .add(attr("alwayslink", BOOLEAN).
+ nonconfigurable("value is referenced in an ImplicitOutputsFunction"))
+ .add(attr("implements", LABEL_LIST)
+ .allowedFileTypes()
+ .allowedRuleClasses("cc_public_library$headers"))
+ .override(attr("linkstatic", BOOLEAN).value(false)
+ .nonconfigurable("value is referenced in an ImplicitOutputsFunction"))
+ .build();
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/cpp/BazelCppSemantics.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/cpp/BazelCppSemantics.java
new file mode 100644
index 0000000..3771e6c
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/cpp/BazelCppSemantics.java
@@ -0,0 +1,52 @@
+// Copyright 2014 Google Inc. 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.bazel.rules.cpp;
+
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.rules.cpp.CppCompilationContext.Builder;
+import com.google.devtools.build.lib.rules.cpp.CppCompileActionBuilder;
+import com.google.devtools.build.lib.rules.cpp.CppCompileActionContext;
+import com.google.devtools.build.lib.rules.cpp.CppConfiguration;
+import com.google.devtools.build.lib.rules.cpp.CppHelper;
+import com.google.devtools.build.lib.rules.cpp.CppSemantics;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+/**
+ * C++ compilation semantics.
+ */
+public class BazelCppSemantics implements CppSemantics {
+ public static final CppSemantics INSTANCE = new BazelCppSemantics();
+
+ private BazelCppSemantics() {
+ }
+
+ @Override
+ public PathFragment getEffectiveSourcePath(Artifact source) {
+ return source.getRootRelativePath();
+ }
+
+ @Override
+ public void finalizeCompileActionBuilder(
+ RuleContext ruleContext, CppCompileActionBuilder actionBuilder) {
+ actionBuilder.setCppConfiguration(ruleContext.getFragment(CppConfiguration.class));
+ actionBuilder.setActionContext(CppCompileActionContext.class);
+ actionBuilder.addTransitiveMandatoryInputs(CppHelper.getToolchain(ruleContext).getCompile());
+ }
+
+ @Override
+ public void setupCompilationContext(RuleContext ruleContext, Builder contextBuilder) {
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/genrule/BazelGenRuleRule.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/genrule/BazelGenRuleRule.java
new file mode 100644
index 0000000..eabb4e9
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/genrule/BazelGenRuleRule.java
@@ -0,0 +1,77 @@
+// Copyright 2014 Google Inc. 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.bazel.rules.genrule;
+
+import static com.google.devtools.build.lib.packages.Attribute.ConfigurationTransition.HOST;
+import static com.google.devtools.build.lib.packages.Attribute.attr;
+import static com.google.devtools.build.lib.packages.Type.BOOLEAN;
+import static com.google.devtools.build.lib.packages.Type.LABEL;
+import static com.google.devtools.build.lib.packages.Type.LABEL_LIST;
+import static com.google.devtools.build.lib.packages.Type.LICENSE;
+import static com.google.devtools.build.lib.packages.Type.OUTPUT_LIST;
+import static com.google.devtools.build.lib.packages.Type.STRING;
+
+import com.google.devtools.build.lib.analysis.BaseRuleClasses;
+import com.google.devtools.build.lib.analysis.BlazeRule;
+import com.google.devtools.build.lib.analysis.RuleDefinition;
+import com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment;
+import com.google.devtools.build.lib.packages.Attribute;
+import com.google.devtools.build.lib.packages.AttributeMap;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.RuleClass.Builder;
+import com.google.devtools.build.lib.packages.Type;
+
+/**
+ * Rule definition for the genrule rule.
+ */
+@BlazeRule(name = "genrule",
+ ancestors = { BaseRuleClasses.RuleBase.class },
+ factoryClass = GenRule.class)
+public final class BazelGenRuleRule implements RuleDefinition {
+ public static final String GENRULE_SETUP_LABEL = "//tools/genrule:genrule-setup.sh";
+
+ @Override
+ public RuleClass build(Builder builder, RuleDefinitionEnvironment env) {
+ return builder
+ .setOutputToGenfiles()
+ .add(attr("srcs", LABEL_LIST)
+ .direct_compile_time_input()
+ .legacyAllowAnyFileType())
+ .add(attr("tools", LABEL_LIST).cfg(HOST).legacyAllowAnyFileType())
+ .add(attr("$genrule_setup", LABEL).cfg(HOST).value(env.getLabel(GENRULE_SETUP_LABEL)))
+ .add(attr("outs", OUTPUT_LIST).mandatory())
+ .add(attr("cmd", STRING).mandatory())
+ .add(attr("output_to_bindir", BOOLEAN).value(false)
+ .nonconfigurable("policy decision: no reason for this to depend on the configuration"))
+ .add(attr("local", BOOLEAN).value(false))
+ .add(attr("message", STRING))
+ .add(attr("output_licenses", LICENSE))
+ .add(attr("executable", BOOLEAN).value(false))
+ .add(attr("stamp", BOOLEAN).value(false))
+ .add(attr("heuristic_label_expansion", BOOLEAN).value(true))
+ .add(attr("$is_executable", BOOLEAN)
+ .nonconfigurable("Called from RunCommand.isExecutable, which takes a Target")
+ .value(
+ new Attribute.ComputedDefault("outs", "executable") {
+ @Override
+ public Object getDefault(AttributeMap rule) {
+ return (rule.get("outs", Type.OUTPUT_LIST).size() == 1)
+ && rule.get("executable", BOOLEAN);
+ }
+ }))
+ .removeAttribute("data")
+ .removeAttribute("deps")
+ .build();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/genrule/GenRule.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/genrule/GenRule.java
new file mode 100644
index 0000000..d70f9a7
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/genrule/GenRule.java
@@ -0,0 +1,219 @@
+// Copyright 2014 Google Inc. 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.bazel.rules.genrule;
+
+import static com.google.devtools.build.lib.analysis.RunfilesProvider.withData;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.CommandHelper;
+import com.google.devtools.build.lib.analysis.ConfigurationMakeVariableContext;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.FileProvider;
+import com.google.devtools.build.lib.analysis.FilesToRunProvider;
+import com.google.devtools.build.lib.analysis.MakeVariableExpander.ExpansionException;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTargetBuilder;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.Runfiles;
+import com.google.devtools.build.lib.analysis.RunfilesProvider;
+import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.packages.TargetUtils;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.rules.RuleConfiguredTargetFactory;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * An implementation of genrule.
+ */
+public class GenRule implements RuleConfiguredTargetFactory {
+
+ public static final String GENRULE_SETUP_CMD =
+ "source tools/genrule/genrule-setup.sh; ";
+
+ private Artifact getExecutable(RuleContext ruleContext, NestedSet<Artifact> filesToBuild) {
+ if (Iterables.size(filesToBuild) == 1) {
+ Artifact out = Iterables.getOnlyElement(filesToBuild);
+ if (ruleContext.attributes().get("executable", Type.BOOLEAN)) {
+ return out;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public ConfiguredTarget create(RuleContext ruleContext) {
+ final List<Artifact> resolvedSrcs = Lists.newArrayList();
+
+ final NestedSet<Artifact> filesToBuild =
+ NestedSetBuilder.wrap(Order.STABLE_ORDER, ruleContext.getOutputArtifacts());
+ if (filesToBuild.isEmpty()) {
+ ruleContext.attributeError("outs", "Genrules without outputs don't make sense");
+ }
+ if (ruleContext.attributes().get("executable", Type.BOOLEAN)
+ && Iterables.size(filesToBuild) > 1) {
+ ruleContext.attributeError("executable",
+ "if genrules produce executables, they are allowed only one output. "
+ + "If you need the executable=1 argument, then you should split this genrule into "
+ + "genrules producing single outputs");
+ }
+
+ ImmutableMap.Builder<Label, Iterable<Artifact>> labelMap = ImmutableMap.builder();
+ for (TransitiveInfoCollection dep : ruleContext.getPrerequisites("srcs", Mode.TARGET)) {
+ Iterable<Artifact> files = dep.getProvider(FileProvider.class).getFilesToBuild();
+ Iterables.addAll(resolvedSrcs, files);
+ labelMap.put(dep.getLabel(), files);
+ }
+
+ CommandHelper commandHelper = new CommandHelper(ruleContext, ruleContext
+ .getPrerequisites("tools", Mode.HOST, FilesToRunProvider.class), labelMap.build());
+
+ if (ruleContext.hasErrors()) {
+ return null;
+ }
+
+ String baseCommand = commandHelper.resolveCommandAndExpandLabels(
+ ruleContext.attributes().get("heuristic_label_expansion", Type.BOOLEAN), false);
+
+ // Adds the genrule environment setup script before the actual shell command
+ String command = GENRULE_SETUP_CMD + baseCommand;
+
+ command = resolveCommand(ruleContext, command, resolvedSrcs, filesToBuild);
+
+ String message = ruleContext.attributes().get("message", Type.STRING);
+ if (message.isEmpty()) {
+ message = "Executing genrule";
+ }
+
+ ImmutableMap<String, String> env =
+ ruleContext.getConfiguration().getDefaultShellEnvironment();
+
+ Map<String, String> executionInfo = Maps.newLinkedHashMap();
+ executionInfo.putAll(TargetUtils.getExecutionInfo(ruleContext.getRule()));
+
+ if (ruleContext.attributes().get("local", Type.BOOLEAN)) {
+ executionInfo.put("local", "");
+ }
+
+ NestedSetBuilder<Artifact> inputs = NestedSetBuilder.stableOrder();
+ inputs.addAll(resolvedSrcs);
+ inputs.addAll(commandHelper.getResolvedTools());
+ FilesToRunProvider genruleSetup =
+ ruleContext.getPrerequisite("$genrule_setup", Mode.HOST, FilesToRunProvider.class);
+ inputs.addAll(genruleSetup.getFilesToRun());
+ List<String> argv = commandHelper.buildCommandLine(command, inputs, ".genrule_script.sh");
+
+ if (ruleContext.attributes().get("stamp", Type.BOOLEAN)) {
+ inputs.add(ruleContext.getAnalysisEnvironment().getStableWorkspaceStatusArtifact());
+ inputs.add(ruleContext.getAnalysisEnvironment().getVolatileWorkspaceStatusArtifact());
+ }
+
+ ruleContext.registerAction(new GenRuleAction(
+ ruleContext.getActionOwner(), inputs.build(), filesToBuild, argv, env,
+ ImmutableMap.copyOf(executionInfo), commandHelper.getRemoteRunfileManifestMap(),
+ message + ' ' + ruleContext.getLabel()));
+
+ RunfilesProvider runfilesProvider = withData(
+ // No runfiles provided if not a data dependency.
+ Runfiles.EMPTY,
+ // We only need to consider the outputs of a genrule
+ // No need to visit the dependencies of a genrule. They cross from the target into the host
+ // configuration, because the dependencies of a genrule are always built for the host
+ // configuration.
+ new Runfiles.Builder().addTransitiveArtifacts(filesToBuild).build());
+
+ return new RuleConfiguredTargetBuilder(ruleContext)
+ .setFilesToBuild(filesToBuild)
+ .setRunfilesSupport(null, getExecutable(ruleContext, filesToBuild))
+ .addProvider(RunfilesProvider.class, runfilesProvider)
+ .build();
+ }
+
+ private String resolveCommand(final RuleContext ruleContext, final String command,
+ final List<Artifact> resolvedSrcs, final NestedSet<Artifact> filesToBuild) {
+ return ruleContext.expandMakeVariables("cmd", command, new ConfigurationMakeVariableContext(
+ ruleContext.getRule().getPackage(), ruleContext.getConfiguration()) {
+ @Override
+ public String lookupMakeVariable(String name) throws ExpansionException {
+ if (name.equals("SRCS")) {
+ return Artifact.joinExecPaths(" ", resolvedSrcs);
+ } else if (name.equals("<")) {
+ return expandSingletonArtifact(resolvedSrcs, "$<", "input file");
+ } else if (name.equals("OUTS")) {
+ return Artifact.joinExecPaths(" ", filesToBuild);
+ } else if (name.equals("@")) {
+ return expandSingletonArtifact(filesToBuild, "$@", "output file");
+ } else if (name.equals("@D")) {
+ // The output directory. If there is only one filename in outs,
+ // this expands to the directory containing that file. If there are
+ // multiple filenames, this variable instead expands to the
+ // package's root directory in the genfiles tree, even if all the
+ // generated files belong to the same subdirectory!
+ if (Iterables.size(filesToBuild) == 1) {
+ Artifact outputFile = Iterables.getOnlyElement(filesToBuild);
+ PathFragment relativeOutputFile = outputFile.getExecPath();
+ if (relativeOutputFile.segmentCount() <= 1) {
+ // This should never happen, since the path should contain at
+ // least a package name and a file name.
+ throw new IllegalStateException("$(@D) for genrule " + ruleContext.getLabel()
+ + " has less than one segment");
+ }
+ return relativeOutputFile.getParentDirectory().getPathString();
+ } else {
+ PathFragment dir;
+ if (ruleContext.getRule().hasBinaryOutput()) {
+ dir = ruleContext.getConfiguration().getBinFragment();
+ } else {
+ dir = ruleContext.getConfiguration().getGenfilesFragment();
+ }
+ PathFragment relPath = ruleContext.getRule().getLabel().getPackageFragment();
+ return dir.getRelative(relPath).getPathString();
+ }
+ } else {
+ return super.lookupMakeVariable(name);
+ }
+ }
+ }
+ );
+ }
+
+ // Returns the path of the sole element "artifacts", generating an exception
+ // with an informative error message iff the set is not a singleton.
+ //
+ // Used to expand "$<", "$@"
+ private String expandSingletonArtifact(Iterable<Artifact> artifacts,
+ String variable,
+ String artifactName)
+ throws ExpansionException {
+ if (Iterables.isEmpty(artifacts)) {
+ throw new ExpansionException("variable '" + variable
+ + "' : no " + artifactName);
+ } else if (Iterables.size(artifacts) > 1) {
+ throw new ExpansionException("variable '" + variable
+ + "' : more than one " + artifactName);
+ }
+ return Iterables.getOnlyElement(artifacts).getExecPathString();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/genrule/GenRuleAction.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/genrule/GenRuleAction.java
new file mode 100644
index 0000000..0a9b3e7
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/genrule/GenRuleAction.java
@@ -0,0 +1,62 @@
+// Copyright 2014 Google Inc. 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.bazel.rules.genrule;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.actions.ActionExecutionContext;
+import com.google.devtools.build.lib.actions.ActionOwner;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.ExecException;
+import com.google.devtools.build.lib.actions.ResourceSet;
+import com.google.devtools.build.lib.analysis.actions.CommandLine;
+import com.google.devtools.build.lib.analysis.actions.SpawnAction;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.List;
+
+/**
+ * A spawn action for genrules. Genrules are handled specially in that inputs and outputs are
+ * checked for directories.
+ */
+public final class GenRuleAction extends SpawnAction {
+
+ private static final ResourceSet GENRULE_RESOURCES =
+ // Not chosen scientifically/carefully. 300MB memory, 100% CPU, 20% of total I/O.
+ new ResourceSet(300, 1.0, 0.0);
+
+ public GenRuleAction(ActionOwner owner,
+ Iterable<Artifact> inputs,
+ Iterable<Artifact> outputs,
+ List<String> argv,
+ ImmutableMap<String, String> environment,
+ ImmutableMap<String, String> executionInfo,
+ ImmutableMap<PathFragment, Artifact> runfilesManifests,
+ String progressMessage) {
+ super(owner, inputs, outputs, GENRULE_RESOURCES,
+ CommandLine.of(argv, false), environment, executionInfo, progressMessage,
+ runfilesManifests,
+ "Genrule", null);
+ }
+
+ @Override
+ protected void internalExecute(
+ ActionExecutionContext actionExecutionContext) throws ExecException, InterruptedException {
+ EventHandler reporter = actionExecutionContext.getExecutor().getEventHandler();
+ checkInputsForDirectories(reporter, actionExecutionContext.getMetadataHandler());
+ super.internalExecute(actionExecutionContext);
+ checkOutputsForDirectories(reporter);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaBinary.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaBinary.java
new file mode 100644
index 0000000..04713c2
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaBinary.java
@@ -0,0 +1,26 @@
+// Copyright 2014 Google Inc. 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.bazel.rules.java;
+
+import com.google.devtools.build.lib.rules.java.JavaBinary;
+
+/**
+ * Implementation of {@code java_binary} with Bazel semantics.
+ */
+public class BazelJavaBinary extends JavaBinary {
+ public BazelJavaBinary() {
+ super(BazelJavaSemantics.INSTANCE);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaBinaryRule.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaBinaryRule.java
new file mode 100644
index 0000000..279cbdb
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaBinaryRule.java
@@ -0,0 +1,51 @@
+// Copyright 2014 Google Inc. 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.bazel.rules.java;
+
+import static com.google.devtools.build.lib.packages.Attribute.attr;
+import static com.google.devtools.build.lib.packages.Type.BOOLEAN;
+
+import com.google.devtools.build.lib.analysis.BlazeRule;
+import com.google.devtools.build.lib.analysis.RuleDefinition;
+import com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment;
+import com.google.devtools.build.lib.bazel.rules.BazelBaseRuleClasses;
+import com.google.devtools.build.lib.bazel.rules.java.BazelJavaRuleClasses.BaseJavaBinaryRule;
+import com.google.devtools.build.lib.packages.Attribute;
+import com.google.devtools.build.lib.packages.AttributeMap;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.RuleClass.Builder;
+
+/**
+ * Rule definition for the java_binary rule.
+ */
+@BlazeRule(name = "java_binary",
+ ancestors = { BaseJavaBinaryRule.class,
+ BazelBaseRuleClasses.BinaryBaseRule.class },
+ factoryClass = BazelJavaBinary.class)
+public final class BazelJavaBinaryRule implements RuleDefinition {
+ @Override
+ public RuleClass build(Builder builder, RuleDefinitionEnvironment env) {
+ return builder
+ .setImplicitOutputsFunction(BazelJavaRuleClasses.JAVA_BINARY_IMPLICIT_OUTPUTS)
+ .override(attr("$is_executable", BOOLEAN).nonconfigurable("automatic").value(
+ new Attribute.ComputedDefault() {
+ @Override
+ public Object getDefault(AttributeMap rule) {
+ return rule.get("create_executable", BOOLEAN);
+ }
+ }))
+ .build();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaBuildInfoFactory.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaBuildInfoFactory.java
new file mode 100644
index 0000000..db33897
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaBuildInfoFactory.java
@@ -0,0 +1,61 @@
+// Copyright 2014 Google Inc. 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.bazel.rules.java;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.rules.java.BuildInfoPropertiesTranslator;
+import com.google.devtools.build.lib.rules.java.GenericBuildInfoPropertiesTranslator;
+import com.google.devtools.build.lib.rules.java.JavaBuildInfoFactory;
+
+import java.util.Map;
+
+/**
+ * BuildInfoFactory for Java.
+ */
+public class BazelJavaBuildInfoFactory extends JavaBuildInfoFactory {
+ private static final Map<String, String> VOLATILE_KEYS = ImmutableMap
+ .<String, String>builder()
+ .put("build.time", "%BUILD_TIME%")
+ .put("build.timestamp.as.int", "%BUILD_TIMESTAMP%")
+ .put("build.timestamp", "%BUILD_TIMESTAMP%")
+ .build();
+
+ private static final Map<String, String> NONVOLATILE_KEYS = ImmutableMap
+ .<String, String>builder()
+ .build();
+
+ private static final Map<String, String> REDACTED_KEYS = ImmutableMap
+ .<String, String>builder()
+ .put("build.time", "Thu Jan 01 00:00:00 1970 (0)")
+ .put("build.timestamp.as.int", "0")
+ .put("build.timestamp", "Thu Jan 01 00:00:00 1970 (0)")
+ .build();
+
+ @Override
+ protected BuildInfoPropertiesTranslator createVolatileTranslator() {
+ return new GenericBuildInfoPropertiesTranslator(VOLATILE_KEYS);
+ }
+
+ @Override
+ protected BuildInfoPropertiesTranslator createNonVolatileTranslator() {
+ return new GenericBuildInfoPropertiesTranslator(NONVOLATILE_KEYS);
+ }
+
+ @Override
+ protected BuildInfoPropertiesTranslator createRedactedTranslator() {
+ return new GenericBuildInfoPropertiesTranslator(REDACTED_KEYS);
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaImport.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaImport.java
new file mode 100644
index 0000000..6c7dcd4
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaImport.java
@@ -0,0 +1,26 @@
+// Copyright 2014 Google Inc. 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.bazel.rules.java;
+
+import com.google.devtools.build.lib.rules.java.JavaImport;
+
+/**
+ * Implementation of {@code java_import} with Bazel semantics.
+ */
+public class BazelJavaImport extends JavaImport {
+ public BazelJavaImport() {
+ super(BazelJavaSemantics.INSTANCE);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaImportRule.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaImportRule.java
new file mode 100644
index 0000000..132df23
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaImportRule.java
@@ -0,0 +1,53 @@
+// Copyright 2014 Google Inc. 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.bazel.rules.java;
+
+import static com.google.devtools.build.lib.packages.Attribute.ANY_EDGE;
+import static com.google.devtools.build.lib.packages.Attribute.attr;
+import static com.google.devtools.build.lib.packages.Type.LABEL_LIST;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.analysis.BlazeRule;
+import com.google.devtools.build.lib.analysis.RuleDefinition;
+import com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment;
+import com.google.devtools.build.lib.bazel.rules.java.BazelJavaRuleClasses.IjarBaseRule;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.RuleClass.Builder;
+import com.google.devtools.build.lib.rules.java.JavaImportBaseRule;
+
+/**
+ * Rule definition for the java_import rule.
+ */
+@BlazeRule(name = "java_import",
+ ancestors = { JavaImportBaseRule.class, IjarBaseRule.class },
+ factoryClass = BazelJavaImport.class)
+public final class BazelJavaImportRule implements RuleDefinition {
+ @Override
+ public RuleClass build(Builder builder, RuleDefinitionEnvironment env) {
+ return builder
+ /* <!-- #BLAZE_RULE(java_import).ATTRIBUTE(exports) -->
+ Targets to make available to users of this rule.
+ ${SYNOPSIS}
+ See <a href="#java_library.exports">java_library.exports</a>.
+ <!-- #END_BLAZE_RULE.ATTRIBUTE --> */
+ .add(attr("exports", LABEL_LIST)
+ .allowedRuleClasses(ImmutableSet.of(
+ "java_library", "java_import", "cc_library", "cc_binary"))
+ .allowedFileTypes() // none allowed
+ .validityPredicate(ANY_EDGE))
+ .build();
+
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaLibrary.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaLibrary.java
new file mode 100644
index 0000000..6af9450
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaLibrary.java
@@ -0,0 +1,26 @@
+// Copyright 2014 Google Inc. 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.bazel.rules.java;
+
+import com.google.devtools.build.lib.rules.java.JavaLibrary;
+
+/**
+ * Implementation of {@code java_library} with Bazel semantics.
+ */
+public class BazelJavaLibrary extends JavaLibrary {
+ public BazelJavaLibrary() {
+ super(BazelJavaSemantics.INSTANCE);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaLibraryRule.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaLibraryRule.java
new file mode 100644
index 0000000..04c8a0f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaLibraryRule.java
@@ -0,0 +1,51 @@
+// Copyright 2014 Google Inc. 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.bazel.rules.java;
+
+import static com.google.devtools.build.lib.packages.Attribute.ConfigurationTransition.HOST;
+import static com.google.devtools.build.lib.packages.Attribute.attr;
+import static com.google.devtools.build.lib.packages.Type.BOOLEAN;
+import static com.google.devtools.build.lib.packages.Type.LABEL_LIST;
+import static com.google.devtools.build.lib.packages.Type.STRING_LIST;
+
+import com.google.devtools.build.lib.analysis.BlazeRule;
+import com.google.devtools.build.lib.analysis.RuleDefinition;
+import com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment;
+import com.google.devtools.build.lib.bazel.rules.java.BazelJavaRuleClasses.JavaRule;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.RuleClass.Builder;
+
+/**
+ * Common attributes for Java rules.
+ */
+@BlazeRule(name = "java_library",
+ ancestors = { JavaRule.class },
+ factoryClass = BazelJavaLibrary.class)
+public final class BazelJavaLibraryRule implements RuleDefinition {
+ @Override
+ public RuleClass build(Builder builder, final RuleDefinitionEnvironment env) {
+
+ return builder
+ .setImplicitOutputsFunction(BazelJavaRuleClasses.JAVA_LIBRARY_IMPLICIT_OUTPUTS)
+ .add(attr("exports", LABEL_LIST)
+ .allowedRuleClasses(BazelJavaRuleClasses.ALLOWED_RULES_IN_DEPS)
+ .allowedFileTypes(/*May not have files in exports!*/))
+ .add(attr("neverlink", BOOLEAN).value(false))
+ .override(attr("javacopts", STRING_LIST))
+ .add(attr("exported_plugins", LABEL_LIST).cfg(HOST).allowedRuleClasses("java_plugin")
+ .legacyAllowAnyFileType())
+ .build();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaPlugin.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaPlugin.java
new file mode 100644
index 0000000..e6d3478
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaPlugin.java
@@ -0,0 +1,27 @@
+// Copyright 2014 Google Inc. 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.bazel.rules.java;
+
+import com.google.devtools.build.lib.rules.java.JavaPlugin;
+
+/**
+ * Implementation of the {@code java_plugin} rule for bazel.
+ */
+public class BazelJavaPlugin extends JavaPlugin {
+
+ public BazelJavaPlugin() {
+ super(BazelJavaSemantics.INSTANCE);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaPluginRule.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaPluginRule.java
new file mode 100644
index 0000000..cbb9411
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaPluginRule.java
@@ -0,0 +1,47 @@
+// Copyright 2014 Google Inc. 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.bazel.rules.java;
+
+import static com.google.devtools.build.lib.packages.Attribute.attr;
+import static com.google.devtools.build.lib.packages.Type.STRING;
+
+import com.google.devtools.build.lib.analysis.BlazeRule;
+import com.google.devtools.build.lib.analysis.RuleDefinition;
+import com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment;
+import com.google.devtools.build.lib.packages.Attribute;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.RuleClass.Builder;
+
+/**
+ * Rule definition for the java_plugin rule.
+ */
+@BlazeRule(name = "java_plugin",
+ ancestors = { BazelJavaLibraryRule.class },
+ factoryClass = BazelJavaPlugin.class)
+public final class BazelJavaPluginRule implements RuleDefinition {
+ @Override
+ public RuleClass build(Builder builder, RuleDefinitionEnvironment env) {
+ return builder
+ .setImplicitOutputsFunction(BazelJavaRuleClasses.JAVA_LIBRARY_IMPLICIT_OUTPUTS)
+ .override(builder.copy("deps").validityPredicate(Attribute.ANY_EDGE))
+ .override(builder.copy("srcs").validityPredicate(Attribute.ANY_EDGE))
+ .add(attr("processor_class", STRING))
+ .removeAttribute("runtime_deps")
+ .removeAttribute("exports")
+ .removeAttribute("exported_plugins")
+ .build();
+ }
+}
+
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaRuleClasses.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaRuleClasses.java
new file mode 100644
index 0000000..663b82a
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaRuleClasses.java
@@ -0,0 +1,173 @@
+// Copyright 2014 Google Inc. 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.bazel.rules.java;
+
+import static com.google.devtools.build.lib.packages.Attribute.ConfigurationTransition.HOST;
+import static com.google.devtools.build.lib.packages.Attribute.attr;
+import static com.google.devtools.build.lib.packages.ImplicitOutputsFunction.fromFunctions;
+import static com.google.devtools.build.lib.packages.Type.BOOLEAN;
+import static com.google.devtools.build.lib.packages.Type.LABEL;
+import static com.google.devtools.build.lib.packages.Type.LABEL_LIST;
+import static com.google.devtools.build.lib.packages.Type.STRING;
+import static com.google.devtools.build.lib.packages.Type.STRING_LIST;
+import static com.google.devtools.build.lib.packages.Type.TRISTATE;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.analysis.BaseRuleClasses;
+import com.google.devtools.build.lib.analysis.BlazeRule;
+import com.google.devtools.build.lib.analysis.RuleDefinition;
+import com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment;
+import com.google.devtools.build.lib.bazel.rules.cpp.BazelCppRuleClasses;
+import com.google.devtools.build.lib.packages.ImplicitOutputsFunction;
+import com.google.devtools.build.lib.packages.PredicateWithMessage;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.RuleClass.Builder;
+import com.google.devtools.build.lib.packages.RuleClass.Builder.RuleClassType;
+import com.google.devtools.build.lib.packages.RuleClass.PackageNameConstraint;
+import com.google.devtools.build.lib.packages.TriState;
+import com.google.devtools.build.lib.rules.java.JavaSemantics;
+import com.google.devtools.build.lib.util.FileTypeSet;
+
+import java.util.Set;
+
+/**
+ * Rule class definitions for Java rules.
+ */
+public class BazelJavaRuleClasses {
+
+ public static final PredicateWithMessage<Rule> JAVA_PACKAGE_NAMES = new PackageNameConstraint(
+ PackageNameConstraint.ANY_SEGMENT, "java", "javatests");
+
+ public static final ImplicitOutputsFunction JAVA_BINARY_IMPLICIT_OUTPUTS =
+ fromFunctions(JavaSemantics.JAVA_BINARY_CLASS_JAR, JavaSemantics.JAVA_BINARY_SOURCE_JAR,
+ JavaSemantics.JAVA_BINARY_DEPLOY_JAR, JavaSemantics.JAVA_BINARY_DEPLOY_SOURCE_JAR);
+
+ static final ImplicitOutputsFunction JAVA_LIBRARY_IMPLICIT_OUTPUTS =
+ fromFunctions(JavaSemantics.JAVA_LIBRARY_CLASS_JAR, JavaSemantics.JAVA_LIBRARY_SOURCE_JAR);
+
+ /**
+ * Common attributes for rules that depend on ijar.
+ */
+ @BlazeRule(name = "$ijar_base_rule",
+ type = RuleClassType.ABSTRACT)
+ public static final class IjarBaseRule implements RuleDefinition {
+ @Override
+ public RuleClass build(Builder builder, RuleDefinitionEnvironment env) {
+ return builder
+ .add(attr("$ijar", LABEL).cfg(HOST).exec().value(env.getLabel("//tools/defaults:ijar")))
+ .setPreferredDependencyPredicate(JavaSemantics.JAVA_SOURCE)
+ .build();
+ }
+ }
+
+
+ /**
+ * Common attributes for Java rules.
+ */
+ @BlazeRule(name = "$java_base_rule",
+ type = RuleClassType.ABSTRACT,
+ ancestors = { IjarBaseRule.class })
+ public static final class JavaBaseRule implements RuleDefinition {
+ @Override
+ public RuleClass build(Builder builder, RuleDefinitionEnvironment env) {
+ return builder
+ .add(attr(":jvm", LABEL).cfg(HOST).value(JavaSemantics.JVM))
+ .add(attr(":host_jdk", LABEL).cfg(HOST).value(JavaSemantics.HOST_JDK))
+ .add(attr(":java_toolchain", LABEL).value(JavaSemantics.JAVA_TOOLCHAIN))
+ .add(attr("$java_langtools", LABEL).cfg(HOST)
+ .value(env.getLabel("//tools/defaults:java_langtools")))
+ .add(attr("$javac_bootclasspath", LABEL).cfg(HOST)
+ .value(env.getLabel(JavaSemantics.JAVAC_BOOTCLASSPATH_LABEL)))
+ .add(attr("$javabuilder", LABEL).cfg(HOST)
+ .value(env.getLabel(JavaSemantics.JAVABUILDER_LABEL)))
+ .add(attr("$singlejar", LABEL).cfg(HOST)
+ .value(env.getLabel(JavaSemantics.SINGLEJAR_LABEL)))
+ .build();
+ }
+ }
+
+ static final Set<String> ALLOWED_RULES_IN_DEPS = ImmutableSet.of(
+ "cc_binary", // NB: linkshared=1
+ "cc_library",
+ "genrule",
+ "genproto", // TODO(bazel-team): we should filter using providers instead (skylark rule).
+ "java_import",
+ "java_library",
+ "sh_binary",
+ "sh_library");
+
+ /**
+ * Common attributes for Java rules.
+ */
+ @BlazeRule(name = "$java_rule",
+ type = RuleClassType.ABSTRACT,
+ ancestors = { BaseRuleClasses.RuleBase.class, JavaBaseRule.class })
+ public static final class JavaRule implements RuleDefinition {
+ @Override
+ public RuleClass build(Builder builder, RuleDefinitionEnvironment env) {
+ return builder
+ .override(builder.copy("deps")
+ .allowedFileTypes(JavaSemantics.JAR)
+ .allowedRuleClasses(ALLOWED_RULES_IN_DEPS)
+ .skipAnalysisTimeFileTypeCheck())
+ .add(attr("runtime_deps", LABEL_LIST)
+ .allowedFileTypes(JavaSemantics.JAR)
+ .allowedRuleClasses(ALLOWED_RULES_IN_DEPS)
+ .skipAnalysisTimeFileTypeCheck())
+ .add(attr("srcs", LABEL_LIST)
+ .orderIndependent()
+ .direct_compile_time_input()
+ .allowedFileTypes(JavaSemantics.JAVA_SOURCE, JavaSemantics.JAR,
+ JavaSemantics.SOURCE_JAR, JavaSemantics.PROPERTIES))
+ .add(attr("resources", LABEL_LIST).orderIndependent()
+ .allowedFileTypes(FileTypeSet.ANY_FILE))
+ .add(attr("plugins", LABEL_LIST).cfg(HOST).allowedRuleClasses("java_plugin")
+ .legacyAllowAnyFileType())
+ .add(attr(":java_plugins", LABEL_LIST)
+ .cfg(HOST)
+ .allowedRuleClasses("java_plugin")
+ .silentRuleClassFilter()
+ .value(JavaSemantics.JAVA_PLUGINS))
+ .add(attr("javacopts", STRING_LIST))
+ .build();
+ }
+ }
+
+ /**
+ * Base class for rule definitions producing Java binaries.
+ */
+ @BlazeRule(name = "$base_java_binary",
+ type = RuleClassType.ABSTRACT,
+ ancestors = { JavaRule.class,
+ // java_binary and java_test require the crosstool C++ runtime
+ // libraries (libstdc++.so, libgcc_s.so).
+ // TODO(bazel-team): Add tests for Java+dynamic runtime.
+ BazelCppRuleClasses.CcLinkingRule.class })
+ public static final class BaseJavaBinaryRule implements RuleDefinition {
+ @Override
+ public RuleClass build(Builder builder, final RuleDefinitionEnvironment env) {
+ return builder
+ .add(attr("classpath_resources", LABEL_LIST).legacyAllowAnyFileType())
+ .add(attr("jvm_flags", STRING_LIST))
+ .add(attr("main_class", STRING))
+ .add(attr("create_executable", BOOLEAN).nonconfigurable("internal").value(true))
+ .add(attr("deploy_manifest_lines", STRING_LIST))
+ .add(attr("stamp", TRISTATE).value(TriState.AUTO))
+ .add(attr(":java_launcher", LABEL).value(JavaSemantics.JAVA_LAUNCHER)) // blaze flag
+ .build();
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaSemantics.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaSemantics.java
new file mode 100644
index 0000000..b301161
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaSemantics.java
@@ -0,0 +1,341 @@
+// Copyright 2014 Google Inc. 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.bazel.rules.java;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.actions.ActionInput;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.AnalysisEnvironment;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTargetBuilder;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.Runfiles;
+import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
+import com.google.devtools.build.lib.analysis.actions.CustomCommandLine;
+import com.google.devtools.build.lib.analysis.actions.TemplateExpansionAction;
+import com.google.devtools.build.lib.analysis.actions.TemplateExpansionAction.ComputedSubstitution;
+import com.google.devtools.build.lib.analysis.actions.TemplateExpansionAction.Substitution;
+import com.google.devtools.build.lib.analysis.actions.TemplateExpansionAction.Template;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration.StrictDepsMode;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.rules.java.DeployArchiveBuilder;
+import com.google.devtools.build.lib.rules.java.DeployArchiveBuilder.Compression;
+import com.google.devtools.build.lib.rules.java.DirectDependencyProvider;
+import com.google.devtools.build.lib.rules.java.DirectDependencyProvider.Dependency;
+import com.google.devtools.build.lib.rules.java.JavaCommon;
+import com.google.devtools.build.lib.rules.java.JavaCompilationArtifacts;
+import com.google.devtools.build.lib.rules.java.JavaCompilationHelper;
+import com.google.devtools.build.lib.rules.java.JavaConfiguration;
+import com.google.devtools.build.lib.rules.java.JavaHelper;
+import com.google.devtools.build.lib.rules.java.JavaPrimaryClassProvider;
+import com.google.devtools.build.lib.rules.java.JavaSemantics;
+import com.google.devtools.build.lib.rules.java.JavaTargetAttributes;
+import com.google.devtools.build.lib.rules.java.JavaUtil;
+import com.google.devtools.build.lib.rules.java.Jvm;
+import com.google.devtools.build.lib.rules.test.InstrumentedFilesCollector.InstrumentationSpec;
+import com.google.devtools.build.lib.util.FileType;
+import com.google.devtools.build.lib.util.FileTypeSet;
+import com.google.devtools.build.lib.util.ShellEscaper;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Semantics for Bazel Java rules
+ */
+public class BazelJavaSemantics implements JavaSemantics {
+
+ public static final BazelJavaSemantics INSTANCE = new BazelJavaSemantics();
+
+ private static final Template STUB_SCRIPT =
+ Template.forResource(BazelJavaSemantics.class, "java_stub_template.txt");
+
+ public static final InstrumentationSpec GREEDY_COLLECTION_SPEC = new InstrumentationSpec(
+ FileTypeSet.of(FileType.of(".sh"), JavaSemantics.JAVA_SOURCE),
+ "srcs", "deps", "data");
+
+ private BazelJavaSemantics() {
+ }
+
+ private boolean isJavaBinaryOrJavaTest(RuleContext ruleContext) {
+ String ruleClass = ruleContext.getRule().getRuleClass();
+ return ruleClass.equals("java_binary") || ruleClass.equals("java_test");
+ }
+
+ @Override
+ public void checkRule(RuleContext ruleContext, JavaCommon javaCommon) {
+ if (isJavaBinaryOrJavaTest(ruleContext)) {
+ checkMainClass(ruleContext, javaCommon);
+ }
+ }
+
+ private String getMainClassInternal(RuleContext ruleContext) {
+ return ruleContext.getRule().isAttrDefined("main_class", Type.STRING)
+ ? ruleContext.attributes().get("main_class", Type.STRING) : "";
+ }
+
+ private void checkMainClass(RuleContext ruleContext, JavaCommon javaCommon) {
+ boolean createExecutable = ruleContext.attributes().get("create_executable", Type.BOOLEAN);
+ String mainClass = getMainClassInternal(ruleContext);
+
+ if (!createExecutable && !mainClass.isEmpty()) {
+ ruleContext.ruleError("main class must not be specified when executable is not created");
+ }
+
+ if (createExecutable && mainClass.isEmpty()) {
+ if (javaCommon.getSrcsArtifacts().isEmpty()) {
+ ruleContext.ruleError(
+ "need at least one of 'main_class', 'use_testrunner' or Java source files");
+ }
+ mainClass = javaCommon.determinePrimaryClass(javaCommon.getSrcsArtifacts());
+ if (mainClass == null) {
+ ruleContext.ruleError("cannot determine main class for launching "
+ + "(found neither a source file '" + ruleContext.getTarget().getName()
+ + ".java', nor a main_class attribute, and package name "
+ + "doesn't include 'java' or 'javatests')");
+ }
+ }
+ }
+
+ @Override
+ public String getMainClass(RuleContext ruleContext, JavaCommon javaCommon) {
+ checkMainClass(ruleContext, javaCommon);
+ return getMainClassInternal(ruleContext);
+ }
+
+ @Override
+ public ImmutableList<Artifact> collectResources(RuleContext ruleContext) {
+ if (!ruleContext.getRule().isAttrDefined("resources", Type.LABEL_LIST)) {
+ return ImmutableList.of();
+ }
+
+ return ruleContext.getPrerequisiteArtifacts("resources", Mode.TARGET).list();
+ }
+
+ @Override
+ public Artifact createInstrumentationMetadataArtifact(
+ AnalysisEnvironment analysisEnvironment, Artifact outputJar) {
+ return null;
+ }
+
+ @Override
+ public void buildJavaCommandLine(Collection<Artifact> outputs, BuildConfiguration configuration,
+ CustomCommandLine.Builder result) {
+ }
+
+ @Override
+ public void createStubAction(RuleContext ruleContext, final JavaCommon javaCommon,
+ List<String> jvmFlags, Artifact executable, String javaStartClass,
+ String javaExecutable) {
+
+ Preconditions.checkNotNull(jvmFlags);
+ Preconditions.checkNotNull(executable);
+ Preconditions.checkNotNull(javaStartClass);
+ Preconditions.checkNotNull(javaExecutable);
+ BuildConfiguration config = ruleContext.getConfiguration();
+
+ List<Substitution> arguments = new ArrayList<>();
+ arguments.add(Substitution.of("%javabin%", javaExecutable));
+ arguments.add(Substitution.of("%needs_runfiles%",
+ config.getFragment(Jvm.class).getJavaExecutable().isAbsolute() ? "0" : "1"));
+ arguments.add(new ComputedSubstitution("%classpath%") {
+ @Override
+ public String getValue() {
+ StringBuilder buffer = new StringBuilder();
+ Iterable<Artifact> jars = javaCommon.getRuntimeClasspath();
+ appendRunfilesRelativeEntries(buffer, jars, ':');
+ return buffer.toString();
+ }
+ });
+
+ arguments.add(Substitution.of("%java_start_class%",
+ ShellEscaper.escapeString(javaStartClass)));
+ arguments.add(Substitution.ofSpaceSeparatedList("%jvm_flags%", jvmFlags));
+
+ ruleContext.registerAction(new TemplateExpansionAction(
+ ruleContext.getActionOwner(), executable, STUB_SCRIPT, arguments, true));
+ }
+
+ /**
+ * Builds a class path by concatenating the root relative paths of the artifacts separated by the
+ * delimiter. Each relative path entry is prepended with "${RUNPATH}" which will be expanded by
+ * the stub script at runtime, to either "${JAVA_RUNFILES}/" or if we are lucky, the empty
+ * string.
+ *
+ * @param buffer the buffer to use for concatenating the entries
+ * @param artifacts the entries to concatenate in the buffer
+ * @param delimiter the delimiter character to separate the entries
+ */
+ private static void appendRunfilesRelativeEntries(StringBuilder buffer,
+ Iterable<Artifact> artifacts, char delimiter) {
+ for (Artifact artifact : artifacts) {
+ if (buffer.length() > 0) {
+ buffer.append(delimiter);
+ }
+ buffer.append("${RUNPATH}");
+ buffer.append(artifact.getRootRelativePath().getPathString());
+ }
+ }
+
+ @Override
+ public void addRunfilesForBinary(RuleContext ruleContext, Artifact launcher,
+ Runfiles.Builder runfilesBuilder) {
+ }
+
+ @Override
+ public void addRunfilesForLibrary(RuleContext ruleContext, Runfiles.Builder runfilesBuilder) {
+ }
+
+ @Override
+ public void collectTargetsTreatedAsDeps(
+ RuleContext ruleContext, ImmutableList.Builder<TransitiveInfoCollection> builder) {
+ }
+
+ @Override
+ public InstrumentationSpec getCoverageInstrumentationSpec() {
+ return GREEDY_COLLECTION_SPEC.withAttributes("srcs", "deps", "data", "exports", "runtime_deps");
+ }
+
+ @Override
+ public Iterable<String> getExtraJavacOpts(RuleContext ruleContext) {
+ return ImmutableList.<String>of();
+ }
+
+ @Override
+ public void addProviders(RuleContext ruleContext,
+ JavaCommon javaCommon,
+ List<String> jvmFlags,
+ Artifact classJar,
+ Artifact srcJar,
+ Artifact gensrcJar,
+ ImmutableMap<Artifact, Artifact> compilationToRuntimeJarMap,
+ JavaCompilationHelper helper,
+ NestedSetBuilder<Artifact> filesBuilder,
+ RuleConfiguredTargetBuilder ruleBuilder) {
+ if (!isJavaBinaryOrJavaTest(ruleContext)) {
+ Artifact outputDepsProto = helper.getOutputDepsProtoArtifact();
+ if (outputDepsProto != null && helper.getStrictJavaDeps() != StrictDepsMode.OFF) {
+ ImmutableList<Dependency> strictDependencies =
+ javaCommon.computeStrictDepsFromJavaAttributes(helper.getAttributes());
+ ruleBuilder.add(DirectDependencyProvider.class,
+ new DirectDependencyProvider(strictDependencies));
+ }
+ } else {
+ boolean createExec = ruleContext.attributes().get("create_executable", Type.BOOLEAN);
+ ruleBuilder.add(JavaPrimaryClassProvider.class,
+ new JavaPrimaryClassProvider(createExec ? getMainClassInternal(ruleContext) : null));
+ }
+ }
+
+
+ @Override
+ public Iterable<String> getJvmFlags(RuleContext ruleContext, JavaCommon javaCommon,
+ Artifact launcher, List<String> userJvmFlags) {
+ return userJvmFlags;
+ }
+
+ @Override
+ public String addCoverageSupport(JavaCompilationHelper helper,
+ JavaTargetAttributes.Builder attributes,
+ Artifact executable, Artifact instrumentationMetadata,
+ JavaCompilationArtifacts.Builder javaArtifactsBuilder, String mainClass) {
+ return mainClass;
+ }
+
+ @Override
+ public boolean useStrictJavaDeps(BuildConfiguration configuration) {
+ return true;
+ }
+
+ @Override
+ public CustomCommandLine buildSingleJarCommandLine(BuildConfiguration configuration,
+ Artifact output, String mainClass, ImmutableList<String> manifestLines,
+ Iterable<Artifact> buildInfoFiles, ImmutableList<Artifact> resources,
+ Iterable<Artifact> classpath, boolean includeBuildData,
+ Compression compression, Artifact launcher) {
+ return DeployArchiveBuilder.defaultSingleJarCommandLine(output, mainClass, manifestLines,
+ buildInfoFiles, resources, classpath, includeBuildData, compression, launcher).build();
+ }
+
+ @Override
+ public Collection<Artifact> translate(RuleContext ruleContext, JavaConfiguration javaConfig,
+ List<Artifact> messages) {
+ return ImmutableList.<Artifact>of();
+ }
+
+ @Override
+ public Artifact getLauncher(RuleContext ruleContext, JavaCommon common,
+ DeployArchiveBuilder deployArchiveBuilder, Runfiles.Builder runfilesBuilder,
+ List<String> jvmFlags, JavaTargetAttributes.Builder attributesBuilder) {
+ return JavaHelper.launcherArtifactForTarget(this, ruleContext);
+ }
+
+ @Override
+ public void addDependenciesForRunfiles(RuleContext ruleContext, Runfiles.Builder builder) {
+ }
+
+ @Override
+ public boolean forceUseJavaLauncherTarget(RuleContext ruleContext) {
+ return false;
+ }
+
+ @Override
+ public void addArtifactToJavaTargetAttribute(JavaTargetAttributes.Builder builder,
+ Artifact srcArtifact) {
+ }
+
+ @Override
+ public void commonDependencyProcessing(RuleContext ruleContext,
+ JavaTargetAttributes.Builder attributes,
+ Collection<? extends TransitiveInfoCollection> deps) {
+ }
+
+ @Override
+ public Collection<ActionInput> getExtraJavaCompileOutputs(PathFragment classDirectory) {
+ return ImmutableList.of();
+ }
+
+ @Override
+ public PathFragment getJavaResourcePath(PathFragment path) {
+ PathFragment javaPath = JavaUtil.getJavaPath(path);
+ return javaPath == null ? path : javaPath;
+ }
+
+ @Override
+ public List<String> getExtraArguments(RuleContext ruleContext, JavaCommon javaCommon) {
+ if (ruleContext.getRule().getRuleClass().equals("java_test")) {
+ if (ruleContext.getConfiguration().getTestArguments().isEmpty()
+ && !ruleContext.attributes().isAttributeValueExplicitlySpecified("args")) {
+ ImmutableList.Builder<String> builder = ImmutableList.builder();
+ for (Artifact artifact : javaCommon.getSrcsArtifacts()) {
+ PathFragment path = artifact.getRootRelativePath();
+ String className = JavaUtil.getJavaFullClassname(FileSystemUtils.removeExtension(path));
+ if (className != null) {
+ builder.add(className);
+ }
+ }
+ return builder.build();
+ }
+ }
+ return ImmutableList.<String>of();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaTest.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaTest.java
new file mode 100644
index 0000000..ca94814
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaTest.java
@@ -0,0 +1,27 @@
+// Copyright 2014 Google Inc. 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.bazel.rules.java;
+
+import com.google.devtools.build.lib.rules.RuleConfiguredTargetFactory;
+import com.google.devtools.build.lib.rules.java.JavaBinary;
+
+/**
+ * An implementation of {@code java_test} rules.
+ */
+public class BazelJavaTest extends JavaBinary implements RuleConfiguredTargetFactory {
+ public BazelJavaTest() {
+ super(BazelJavaSemantics.INSTANCE);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaTestRule.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaTestRule.java
new file mode 100644
index 0000000..fe881b7
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaTestRule.java
@@ -0,0 +1,54 @@
+// Copyright 2014 Google Inc. 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.bazel.rules.java;
+
+import static com.google.devtools.build.lib.packages.Attribute.attr;
+import static com.google.devtools.build.lib.packages.Type.LABEL;
+import static com.google.devtools.build.lib.packages.Type.STRING;
+import static com.google.devtools.build.lib.packages.Type.TRISTATE;
+
+import com.google.devtools.build.lib.analysis.BaseRuleClasses;
+import com.google.devtools.build.lib.analysis.BlazeRule;
+import com.google.devtools.build.lib.analysis.RuleDefinition;
+import com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment;
+import com.google.devtools.build.lib.bazel.rules.java.BazelJavaRuleClasses.BaseJavaBinaryRule;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.RuleClass.Builder;
+import com.google.devtools.build.lib.packages.RuleClass.Builder.RuleClassType;
+import com.google.devtools.build.lib.packages.TriState;
+import com.google.devtools.build.lib.rules.java.JavaSemantics;
+
+/**
+ * Rule definition for the java_test rule.
+ */
+@BlazeRule(name = "java_test",
+ type = RuleClassType.TEST,
+ ancestors = { BaseJavaBinaryRule.class,
+ BaseRuleClasses.TestBaseRule.class },
+ factoryClass = BazelJavaTest.class)
+public final class BazelJavaTestRule implements RuleDefinition {
+
+ private static final String JUNIT4_RUNNER = "org.junit.runner.JUnitCore";
+
+ @Override
+ public RuleClass build(Builder builder, RuleDefinitionEnvironment env) {
+ return builder
+ .setImplicitOutputsFunction(BazelJavaRuleClasses.JAVA_BINARY_IMPLICIT_OUTPUTS)
+ .override(attr("main_class", STRING).value(JUNIT4_RUNNER))
+ .override(attr("stamp", TRISTATE).value(TriState.NO))
+ .override(attr(":java_launcher", LABEL).value(JavaSemantics.JAVA_LAUNCHER))
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/java/java_stub_template.txt b/src/main/java/com/google/devtools/build/lib/bazel/rules/java/java_stub_template.txt
new file mode 100644
index 0000000..a17246f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/java/java_stub_template.txt
@@ -0,0 +1,195 @@
+#!/bin/bash --posix
+# Copyright 2014 Google Inc. 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.
+#
+# This script was generated from java_stub_template.txt. Please
+# don't edit it directly.
+#
+# If present, these flags should either be at the beginning of the command
+# line, or they should be wrapped in a --wrapper_script_flag=FLAG argument.
+#
+# --debug Launch the JVM in remote debugging mode listening
+# --debug=<port> to the specified port or the port set in the
+# DEFAULT_JVM_DEBUG_PORT environment variable (e.g.
+# 'export DEFAULT_JVM_DEBUG_PORT=8000') or else the
+# default port of 5005. The JVM starts suspended
+# unless the DEFAULT_JVM_DEBUG_SUSPEND environment
+# variable is set to 'n'.
+# --main_advice=<class> Run an alternate main class with the usual main
+# program and arguments appended as arguments.
+# --main_advice_classpath=<classpath>
+# Prepend additional class path entries.
+# --jvm_flag=<flag> Pass <flag> to the "java" command itself.
+# <flag> may contain spaces. Can be used multiple times.
+# --jvm_flags=<flags> Pass space-separated flags to the "java" command
+# itself. Can be used multiple times.
+# --singlejar Start the program from the packed-up deployment
+# jar rather than from the classpath.
+# --print_javabin Print the location of java executable binary and exit.
+#
+# The remainder of the command line is passed to the program.
+
+# Make it easy to insert 'set -x' or similar commands when debugging problems with this script.
+eval "$JAVA_STUB_DEBUG"
+
+# Prevent problems where the caller has exported CLASSPATH, causing our
+# computed value to be copied into the environment and double-counted
+# against the argv limit.
+unset CLASSPATH
+
+JVM_FLAGS_CMDLINE=()
+
+# Processes an argument for the wrapper. Returns 0 if the given argument
+# was recognized as an argument for this wrapper, and 1 if it was not.
+function process_wrapper_argument() {
+ case "$1" in
+ --debug) JVM_DEBUG_PORT="${DEFAULT_JVM_DEBUG_PORT:-5005}" ;;
+ --debug=*) JVM_DEBUG_PORT="${1#--debug=}" ;;
+ --main_advice=*) MAIN_ADVICE="${1#--main_advice=}" ;;
+ --main_advice_classpath=*) MAIN_ADVICE_CLASSPATH="${1#--main_advice_classpath=}" ;;
+ --jvm_flag=*) JVM_FLAGS_CMDLINE+=( "${1#--jvm_flag=}" ) ;;
+ --jvm_flags=*) JVM_FLAGS_CMDLINE+=( ${1#--jvm_flags=} ) ;;
+ --singlejar) SINGLEJAR=1 ;;
+ --print_javabin) PRINT_JAVABIN=1 ;;
+ *)
+ return 1 ;;
+ esac
+ return 0
+}
+
+die() {
+ printf "%s: $1\n" "$0" "${@:2}" >&2
+ exit 1
+}
+
+# Parse arguments sequentially until the first unrecognized arg is encountered.
+# Scan the remaining args for --wrapper_script_flag=X options and process them.
+ARGS=()
+for ARG in "$@"; do
+ if [[ "$ARG" == --wrapper_script_flag=* ]]; then
+ process_wrapper_argument "${ARG#--wrapper_script_flag=}" \
+ || die "invalid wrapper argument '%s'" "$ARG"
+ elif [[ "${#ARGS}" > 0 ]] || ! process_wrapper_argument "$ARG"; then
+ ARGS+=( "$ARG" )
+ fi
+done
+
+# Find our runfiles tree. We need this to construct the classpath
+# (unless --singlejar was passed).
+#
+# Call this program X. X was generated by a java_binary or java_test rule.
+# X may be invoked in many ways:
+# 1a) directly by a user, with $0 in the output tree
+# 1b) via 'bazel run' (similar to case 1a)
+# 2) directly by a user, with $0 in X's runfiles tree
+# 3) by another program Y which has a data dependency on X, with $0 in Y's runfiles tree
+# 4) via 'bazel test'
+# 5) by a genrule cmd, with $0 in the output tree
+# 6) case 3 in the context of a genrule
+#
+# For case 1, $0 will be a regular file, and the runfiles tree will be
+# at $0.runfiles.
+# For case 2 or 3, $0 will be a symlink to the file seen in case 1.
+# For case 4, $JAVA_RUNFILES and $TEST_SRCDIR should already be set.
+# Case 5 is handled like case 1.
+# Case 6 is handled like case 3.
+
+case "$0" in
+ /*) self="$0" ;;
+ *) self="$PWD/$0" ;;
+esac
+
+if [[ "$SINGLEJAR" != 1 || "%needs_runfiles%" == 1 ]]; then
+ if [[ -z "$JAVA_RUNFILES" ]]; then
+ while true; do
+ if [[ -e "$self.runfiles" ]]; then
+ JAVA_RUNFILES="$self.runfiles"
+ break
+ fi
+ if [[ $self == *.runfiles/* ]]; then
+ JAVA_RUNFILES="${self%.runfiles/*}.runfiles"
+ # don't break; this value is only a last resort for case 6b
+ fi
+ if [[ ! -L "$self" ]]; then
+ break
+ fi
+ readlink="$(readlink "$self")"
+ if [[ "$readlink" = /* ]]; then
+ self="$readlink"
+ else
+ # resolve relative symlink
+ self="${self%/*}/$readlink"
+ fi
+ done
+ if [[ -n "$JAVA_RUNFILES" ]]; then
+ export TEST_SRCDIR=${TEST_SRCDIR:-$JAVA_RUNFILES}
+ elif [[ -f "${self}_deploy.jar" && "%needs_runfiles%" == 0 ]]; then
+ SINGLEJAR=1;
+ else
+ die 'Cannot locate runfiles directory. (Set $JAVA_RUNFILES to inhibit searching.)'
+ fi
+ fi
+fi
+
+# Set JAVABIN to the path to the JVM launcher.
+%javabin%
+
+if [[ "$PRINT_JAVABIN" == 1 || "%java_start_class%" == "--print_javabin" ]]; then
+ echo -n "$JAVABIN"
+ exit 0
+fi
+
+if [[ "$SINGLEJAR" == 1 ]]; then
+ CLASSPATH="${self}_deploy.jar"
+ # Check for the deploy jar now. If it doesn't exist, we can print a
+ # more helpful error message than the JVM.
+ [[ -r "$CLASSPATH" ]] \
+ || die "Option --singlejar was passed, but %s does not exist.\n (You may need to build it explicitly.)" "$CLASSPATH"
+else
+ # Create the shortest classpath we can, by making it relative if possible.
+ RUNPATH="${JAVA_RUNFILES}/"
+ RUNPATH="${RUNPATH#$PWD/}"
+ CLASSPATH=%classpath%
+fi
+
+if [[ -n "$JVM_DEBUG_PORT" ]]; then
+ JVM_DEBUG_SUSPEND=${DEFAULT_JVM_DEBUG_SUSPEND:-"y"}
+ JVM_DEBUG_FLAGS="-agentlib:jdwp=transport=dt_socket,server=y,suspend=${JVM_DEBUG_SUSPEND},address=${JVM_DEBUG_PORT}"
+fi
+
+if [[ -n "$MAIN_ADVICE_CLASSPATH" ]]; then
+ CLASSPATH="${MAIN_ADVICE_CLASSPATH}:${CLASSPATH}"
+fi
+
+# Check if TEST_TMPDIR is available to use for scratch.
+if [[ -n "$TEST_TMPDIR" && -d "$TEST_TMPDIR" ]]; then
+ JVM_FLAGS+=" -Djava.io.tmpdir=$TEST_TMPDIR"
+fi
+
+ARGS=(
+ ${JVM_DEBUG_FLAGS}
+ ${JVM_FLAGS}
+ %jvm_flags%
+ "${JVM_FLAGS_CMDLINE[@]}"
+ ${MAIN_ADVICE}
+ %java_start_class%
+ "${ARGS[@]}")
+
+# Linux per-arg limit MAX_ARG_STRLEN == 128k!
+if (("${#CLASSPATH}" > 120000)); then
+ set +o posix # Enable process substitution.
+ exec $JAVABIN -classpath @<(echo $CLASSPATH) "${ARGS[@]}"
+else
+ exec $JAVABIN -classpath $CLASSPATH "${ARGS[@]}"
+fi
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/objc/BazelIosTest.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/objc/BazelIosTest.java
new file mode 100644
index 0000000..f45ca08
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/objc/BazelIosTest.java
@@ -0,0 +1,57 @@
+// Copyright 2014 Google Inc. 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.bazel.rules.objc;
+
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTargetBuilder;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.Runfiles;
+import com.google.devtools.build.lib.analysis.RunfilesProvider;
+import com.google.devtools.build.lib.analysis.RunfilesSupport;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.rules.objc.IosTest;
+import com.google.devtools.build.lib.rules.objc.ObjcCommon;
+import com.google.devtools.build.lib.rules.objc.XcodeProvider;
+
+/**
+ * Implementation for ios_test rule in Bazel.
+ */
+public final class BazelIosTest extends IosTest {
+ static final String IOS_TEST_ON_BAZEL_ATTR = "$ios_test_on_bazel";
+
+ @Override
+ public ConfiguredTarget create(RuleContext ruleContext, ObjcCommon common,
+ XcodeProvider xcodeProvider, NestedSet<Artifact> filesToBuild) throws InterruptedException {
+ Artifact testRunner = ruleContext.getPrerequisiteArtifact(IOS_TEST_ON_BAZEL_ATTR, Mode.TARGET);
+ Runfiles runfiles = new Runfiles.Builder()
+ .addArtifact(testRunner)
+ .build();
+ RunfilesSupport runfilesSupport =
+ RunfilesSupport.withExecutable(ruleContext, runfiles, testRunner);
+
+ return new RuleConfiguredTargetBuilder(ruleContext)
+ .setFilesToBuild(NestedSetBuilder.<Artifact>stableOrder()
+ .addTransitive(filesToBuild)
+ .add(testRunner)
+ .build())
+ .add(XcodeProvider.class, xcodeProvider)
+ .add(RunfilesProvider.class, RunfilesProvider.simple(runfiles))
+ .setRunfilesSupport(runfilesSupport, testRunner)
+ .build();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/objc/BazelIosTestRule.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/objc/BazelIosTestRule.java
new file mode 100644
index 0000000..114d454
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/objc/BazelIosTestRule.java
@@ -0,0 +1,69 @@
+// Copyright 2014 Google Inc. 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.bazel.rules.objc;
+
+import static com.google.devtools.build.lib.packages.Attribute.attr;
+import static com.google.devtools.build.lib.packages.Type.LABEL;
+
+import com.google.devtools.build.lib.analysis.BaseRuleClasses;
+import com.google.devtools.build.lib.analysis.BlazeRule;
+import com.google.devtools.build.lib.analysis.RuleDefinition;
+import com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment;
+import com.google.devtools.build.lib.packages.ImplicitOutputsFunction;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.RuleClass.Builder.RuleClassType;
+import com.google.devtools.build.lib.rules.objc.ApplicationSupport;
+import com.google.devtools.build.lib.rules.objc.ObjcRuleClasses;
+import com.google.devtools.build.lib.rules.objc.XcodeSupport;
+
+/**
+ * Rule definition for the ios_test rule.
+ */
+@BlazeRule(name = "ios_test",
+ type = RuleClassType.TEST,
+ ancestors = { ObjcRuleClasses.IosTestBaseRule.class,
+ BaseRuleClasses.TestBaseRule.class },
+ factoryClass = BazelIosTest.class)
+public final class BazelIosTestRule implements RuleDefinition {
+ @Override
+ public RuleClass build(RuleClass.Builder builder, final RuleDefinitionEnvironment env) {
+ return builder
+ /*<!-- #BLAZE_RULE(ios_test).IMPLICIT_OUTPUTS -->
+ <ul>
+ <li><code><var>name</var>.ipa</code>: the test bundle as an
+ <code>.ipa</code> file
+ <li><code><var>name</var>.xcodeproj/project.pbxproj: An Xcode project file which can be
+ used to develop or build on a Mac.</li>
+ </ul>
+ <!-- #END_BLAZE_RULE.IMPLICIT_OUTPUTS -->*/
+ .setImplicitOutputsFunction(
+ ImplicitOutputsFunction.fromFunctions(ApplicationSupport.IPA, XcodeSupport.PBXPROJ))
+ .add(attr(BazelIosTest.IOS_TEST_ON_BAZEL_ATTR, LABEL)
+ .value(env.getLabel("//tools/objc:ios_test_on_bazel")).exec())
+ .build();
+ }
+}
+
+/*<!-- #BLAZE_RULE (NAME = ios_test, TYPE = TEST, FAMILY = Objective-C) -->
+
+${ATTRIBUTE_SIGNATURE}
+
+<p>This rule provides a way to build iOS unit tests written in KIF, GTM and XCTest test frameworks
+on both iOS simulator and real devices.
+</p>
+
+${ATTRIBUTE_DEFINITION}
+
+<!-- #END_BLAZE_RULE -->*/
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/sh/BazelShBinaryRule.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/sh/BazelShBinaryRule.java
new file mode 100644
index 0000000..39d4a0c
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/sh/BazelShBinaryRule.java
@@ -0,0 +1,41 @@
+// Copyright 2014 Google Inc. 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.bazel.rules.sh;
+
+import static com.google.devtools.build.lib.packages.Attribute.attr;
+import static com.google.devtools.build.lib.packages.Type.STRING;
+
+import com.google.devtools.build.lib.analysis.BlazeRule;
+import com.google.devtools.build.lib.analysis.RuleDefinition;
+import com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment;
+import com.google.devtools.build.lib.bazel.rules.BazelBaseRuleClasses;
+import com.google.devtools.build.lib.bazel.rules.sh.BazelShRuleClasses.ShRule;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.RuleClass.Builder;
+
+/**
+ * Rule definition for the sh_binary rule.
+ */
+@BlazeRule(name = "sh_binary",
+ ancestors = { ShRule.class, BazelBaseRuleClasses.BinaryBaseRule.class },
+ factoryClass = ShBinary.class)
+public final class BazelShBinaryRule implements RuleDefinition {
+ @Override
+ public RuleClass build(Builder builder, RuleDefinitionEnvironment environment) {
+ return builder.add(
+ attr("bash_version", STRING)
+ .value(BazelShRuleClasses.DEFAULT_BASH_VERSION)
+ .allowedValues(BazelShRuleClasses.BASH_VERSION_ALLOWED_VALUES)).build();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/sh/BazelShLibraryRule.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/sh/BazelShLibraryRule.java
new file mode 100644
index 0000000..9d9640b
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/sh/BazelShLibraryRule.java
@@ -0,0 +1,113 @@
+// Copyright 2014 Google Inc. 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.bazel.rules.sh;
+
+import static com.google.devtools.build.lib.packages.Attribute.attr;
+import static com.google.devtools.build.lib.packages.Type.LABEL_LIST;
+
+import com.google.devtools.build.lib.analysis.BlazeRule;
+import com.google.devtools.build.lib.analysis.RuleDefinition;
+import com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment;
+import com.google.devtools.build.lib.bazel.rules.sh.BazelShRuleClasses.ShRule;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.RuleClass.Builder;
+
+/**
+ * Rule definition for the sh_library rule.
+ */
+@BlazeRule(name = "sh_library",
+ ancestors = { ShRule.class },
+ factoryClass = ShLibrary.class)
+public final class BazelShLibraryRule implements RuleDefinition {
+ @Override
+ public RuleClass build(Builder builder, RuleDefinitionEnvironment environment) {
+ return builder
+ /* <!-- #BLAZE_RULE(sh_library).ATTRIBUTE(deps) -->
+ The list of other targets to be aggregated in to this "library" target.
+ <i>(List of <a href="build-ref.html#labels">labels</a>; optional)</i><br/>
+ See general comments about <code>deps</code>
+ at <a href="#common-attributes">Attributes common to all build rules</a>.
+ You should use this attribute to list other
+ <code>sh_library</code> or <code>proto_library</code> rules that provide
+ interpreted program source code depended on by the code in
+ <code>srcs</code>. If you depend on a <code>proto_library</code> target,
+ the proto sources in that target will be included in this library, but
+ no generated files will be built.
+ <!-- #END_BLAZE_RULE.ATTRIBUTE --> */
+
+ /* <!-- #BLAZE_RULE(sh_library).ATTRIBUTE(srcs) -->
+ The list of input files.
+ <i>(List of <a href="build-ref.html#labels">labels</a>,
+ optional)</i><br/>
+ You should use this attribute to list interpreted program
+ source files that belong to this package, such as additional
+ files containing Bourne shell subroutines, loaded via the shell's
+ <code>source</code> or <code>.</code> command.
+ <!-- #END_BLAZE_RULE.ATTRIBUTE -->*/
+ .override(attr("srcs", LABEL_LIST).legacyAllowAnyFileType())
+ .build();
+ }
+}
+
+/*<!-- #BLAZE_RULE (NAME = sh_library, TYPE = LIBRARY, FAMILY = Shell) -->
+
+${ATTRIBUTE_SIGNATURE}
+
+<p>
+ The main use for this rule is to aggregate together a logical
+ "library" consisting of related scripts—programs in an
+ interpreted language that does not require compilation or linking,
+ such as the Bourne shell—and any data those programs need at
+ run-time. Such "libraries" can then be used from
+ the <code>data</code> attribute of one or
+ more <code>sh_binary</code> rules.
+</p>
+
+<p>
+ Historically, a second use was to aggregate a collection of data files
+ together, to ensure that they are available at runtime in
+ the <code>.runfiles</code> area of one or more <code>*_binary</code>
+ rules (not necessarily <code>sh_binary</code>).
+ However, the <a href="#filegroup"><code>filegroup()</code></a> rule
+ should be used now; it is intended to replace this use of
+ <code>sh_library</code>.
+</p>
+
+<p>
+ In interpreted programming languages, there's not always a clear
+ distinction between "code" and "data": after all, the program is
+ just "data" from the interpreter's point of view. For this reason
+ (and historical accident) this rule has three attributes which are
+ all essentially equivalent: <code>srcs</code>, <code>deps</code>
+ and <code>data</code>.
+ The recommended usage of each attribute is mentioned below. The
+ current implementation does not distinguish the elements of these lists.
+ All three attributes accept rules, source files and derived files.
+</p>
+
+${ATTRIBUTE_DEFINITION}
+
+<h4 id="sh_library_examples">Examples</h4>
+
+<pre class="code">
+sh_library(
+ name = "foo",
+ data = [
+ ":foo_service_script", # a sh_binary with srcs
+ ":deploy_foo", # another sh_binary with srcs
+ ],
+)
+</pre>
+
+<!-- #END_BLAZE_RULE -->*/
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/sh/BazelShRuleClasses.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/sh/BazelShRuleClasses.java
new file mode 100644
index 0000000..6f66465
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/sh/BazelShRuleClasses.java
@@ -0,0 +1,101 @@
+// Copyright 2014 Google Inc. 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.bazel.rules.sh;
+
+import static com.google.devtools.build.lib.packages.Attribute.attr;
+import static com.google.devtools.build.lib.packages.ImplicitOutputsFunction.fromTemplates;
+import static com.google.devtools.build.lib.packages.Type.LABEL_LIST;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.analysis.BaseRuleClasses;
+import com.google.devtools.build.lib.analysis.BlazeRule;
+import com.google.devtools.build.lib.analysis.RuleDefinition;
+import com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment;
+import com.google.devtools.build.lib.packages.Attribute.AllowedValueSet;
+import com.google.devtools.build.lib.packages.ImplicitOutputsFunction;
+import com.google.devtools.build.lib.packages.PredicateWithMessage;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.RuleClass.Builder;
+import com.google.devtools.build.lib.packages.RuleClass.Builder.RuleClassType;
+
+import java.util.Collection;
+import java.util.Map;
+
+import javax.annotation.Nullable;
+
+/**
+ * Rule definitions for rule classes implementing shell support.
+ */
+public final class BazelShRuleClasses {
+
+ static final Collection<String> ALLOWED_RULES_IN_DEPS_WITH_WARNING = ImmutableSet.of(
+ "filegroup", "Fileset", "genrule", "sh_binary", "sh_test", "test_suite");
+
+ /**
+ * Common attributes for shell rules.
+ */
+ @BlazeRule(name = "$sh_target",
+ type = RuleClassType.ABSTRACT,
+ ancestors = { BaseRuleClasses.RuleBase.class })
+ public static final class ShRule implements RuleDefinition {
+ @Override
+ public RuleClass build(Builder builder, RuleDefinitionEnvironment environment) {
+ return builder
+ .add(attr("srcs", LABEL_LIST).mandatory().legacyAllowAnyFileType())
+ .override(builder.copy("deps")
+ .allowedRuleClasses("sh_library", "proto_library")
+ .allowedRuleClassesWithWarning(ALLOWED_RULES_IN_DEPS_WITH_WARNING)
+ .allowedFileTypes())
+ .build();
+ }
+ }
+
+ /**
+ * Defines the file name of an sh_binary's implicit .sar (script package) output.
+ */
+ static final ImplicitOutputsFunction SAR_PACKAGE_FILENAME =
+ fromTemplates("%{name}.sar");
+
+ /**
+ * Convenience structure for the bash dependency combinations defined
+ * by BASH_BINARY_BINDINGS.
+ */
+ static class BashBinaryBinding {
+ public final String execPath;
+ public BashBinaryBinding(@Nullable String execPath) {
+ this.execPath = execPath;
+ }
+ }
+
+ /**
+ * Attribute value specifying the local system's bash version.
+ */
+ static final String SYSTEM_BASH_VERSION = "system";
+
+ static final Map<String, BashBinaryBinding> BASH_BINARY_BINDINGS =
+ ImmutableMap.of(
+ // "system": don't package any bash with the target, but rather use whatever is
+ // available on the system the script is run on.
+ SYSTEM_BASH_VERSION, new BashBinaryBinding("/bin/bash")
+ );
+
+ static final String DEFAULT_BASH_VERSION = SYSTEM_BASH_VERSION;
+
+ // TODO(bazel-team): refactor sh_binary and sh_base to have a common root
+ // with srcs and bash_version attributes
+ static final PredicateWithMessage<Object> BASH_VERSION_ALLOWED_VALUES =
+ new AllowedValueSet(BASH_BINARY_BINDINGS.keySet());
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/sh/BazelShTestRule.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/sh/BazelShTestRule.java
new file mode 100644
index 0000000..4a1e51f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/sh/BazelShTestRule.java
@@ -0,0 +1,44 @@
+// Copyright 2014 Google Inc. 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.bazel.rules.sh;
+
+import static com.google.devtools.build.lib.packages.Attribute.attr;
+import static com.google.devtools.build.lib.packages.Type.STRING;
+
+import com.google.devtools.build.lib.analysis.BaseRuleClasses;
+import com.google.devtools.build.lib.analysis.BlazeRule;
+import com.google.devtools.build.lib.analysis.RuleDefinition;
+import com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment;
+import com.google.devtools.build.lib.bazel.rules.sh.BazelShRuleClasses.ShRule;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.RuleClass.Builder;
+import com.google.devtools.build.lib.packages.RuleClass.Builder.RuleClassType;
+
+/**
+ * Rule definition for the sh_test rule.
+ */
+@BlazeRule(name = "sh_test",
+ type = RuleClassType.TEST,
+ ancestors = { ShRule.class, BaseRuleClasses.TestBaseRule.class },
+ factoryClass = ShTest.class)
+public final class BazelShTestRule implements RuleDefinition {
+ @Override
+ public RuleClass build(Builder builder, RuleDefinitionEnvironment environment) {
+ return builder
+ .add(attr("bash_version", STRING)
+ .value(BazelShRuleClasses.DEFAULT_BASH_VERSION)
+ .allowedValues(BazelShRuleClasses.BASH_VERSION_ALLOWED_VALUES))
+ .build();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/sh/ShBinary.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/sh/ShBinary.java
new file mode 100644
index 0000000..4e6ba81
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/sh/ShBinary.java
@@ -0,0 +1,82 @@
+// Copyright 2014 Google Inc. 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.bazel.rules.sh;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTargetBuilder;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.Runfiles;
+import com.google.devtools.build.lib.analysis.RunfilesProvider;
+import com.google.devtools.build.lib.analysis.RunfilesSupport;
+import com.google.devtools.build.lib.analysis.actions.ExecutableSymlinkAction;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.rules.RuleConfiguredTargetFactory;
+
+/**
+ * Implementation for the sh_binary rule.
+ */
+public class ShBinary implements RuleConfiguredTargetFactory {
+
+ @Override
+ public ConfiguredTarget create(RuleContext ruleContext) {
+ ImmutableList<Artifact> srcs = ruleContext.getPrerequisiteArtifacts("srcs", Mode.TARGET).list();
+ if (srcs.size() != 1) {
+ ruleContext.attributeError("srcs", "you must specify exactly one file in 'srcs'");
+ return null;
+ }
+
+ Artifact symlink = ruleContext.createOutputArtifact();
+ Artifact src = srcs.get(0);
+ Artifact executableScript = getExecutableScript(ruleContext, src);
+ // The interpretation of this deceptively simple yet incredibly generic rule is complicated
+ // by the distinction between targets and (not properly encapsulated) artifacts. It depends
+ // on the notion of other rule's "files-to-build" sets, which are undocumented, making it
+ // impossible to give a precise definition of what this rule does in all cases (e.g. what
+ // happens when srcs = ['x', 'y'] but 'x' is an empty filegroup?). This is a pervasive
+ // problem in Blaze.
+ ruleContext.registerAction(
+ new ExecutableSymlinkAction(ruleContext.getActionOwner(), executableScript, symlink));
+
+ NestedSet<Artifact> filesToBuild = NestedSetBuilder.<Artifact>stableOrder()
+ .add(src)
+ .add(executableScript) // May be the same as src, in which case set semantics apply.
+ .add(symlink)
+ .build();
+ Runfiles runfiles = new Runfiles.Builder()
+ .addTransitiveArtifacts(filesToBuild)
+ .addRunfiles(ruleContext, RunfilesProvider.DEFAULT_RUNFILES)
+ .build();
+ RunfilesSupport runfilesSupport = RunfilesSupport.withExecutable(
+ ruleContext, runfiles, symlink);
+ return new RuleConfiguredTargetBuilder(ruleContext)
+ .setFilesToBuild(filesToBuild)
+ .setRunfilesSupport(runfilesSupport, symlink)
+ .addProvider(RunfilesProvider.class, RunfilesProvider.simple(runfiles))
+ .build();
+ }
+
+ /**
+ * Hook for sh_test to provide the executable.
+ *
+ * @param ruleContext
+ * @param src
+ */
+ protected Artifact getExecutableScript(RuleContext ruleContext, Artifact src) {
+ return src;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/sh/ShLibrary.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/sh/ShLibrary.java
new file mode 100644
index 0000000..e2744ea
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/sh/ShLibrary.java
@@ -0,0 +1,47 @@
+// Copyright 2014 Google Inc. 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.bazel.rules.sh;
+
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTargetBuilder;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.Runfiles;
+import com.google.devtools.build.lib.analysis.RunfilesProvider;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.rules.RuleConfiguredTargetFactory;
+
+/**
+ * Implementation for the sh_library rule.
+ */
+public class ShLibrary implements RuleConfiguredTargetFactory {
+
+ @Override
+ public ConfiguredTarget create(RuleContext ruleContext) {
+ NestedSet<Artifact> filesToBuild = NestedSetBuilder.<Artifact>stableOrder()
+ .addAll(ruleContext.getPrerequisiteArtifacts("srcs", Mode.TARGET).list())
+ .addAll(ruleContext.getPrerequisiteArtifacts("deps", Mode.TARGET).list())
+ .addAll(ruleContext.getPrerequisiteArtifacts("data", Mode.DATA).list())
+ .build();
+ Runfiles runfiles = new Runfiles.Builder()
+ .addTransitiveArtifacts(filesToBuild)
+ .build();
+ return new RuleConfiguredTargetBuilder(ruleContext)
+ .setFilesToBuild(filesToBuild)
+ .addProvider(RunfilesProvider.class, RunfilesProvider.simple(runfiles))
+ .build();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/sh/ShTest.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/sh/ShTest.java
new file mode 100644
index 0000000..cc965aa
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/sh/ShTest.java
@@ -0,0 +1,53 @@
+// Copyright 2014 Google Inc. 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.bazel.rules.sh;
+
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.actions.FileWriteAction;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.rules.RuleConfiguredTargetFactory;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+/**
+ * Implementation for sh_test rules.
+ */
+public class ShTest extends ShBinary implements RuleConfiguredTargetFactory {
+
+ @Override
+ protected Artifact getExecutableScript(RuleContext ruleContext, Artifact src) {
+ if (ruleContext.attributes().get("bash_version", Type.STRING)
+ .equals(BazelShRuleClasses.SYSTEM_BASH_VERSION)) {
+ return src;
+ }
+
+ // What *will* this script run with the wrapper?
+ PathFragment newOutput = src.getRootRelativePath().getParentDirectory().getRelative(
+ ruleContext.getLabel().getName() + "_runner.sh");
+ Artifact testRunner = ruleContext.getAnalysisEnvironment().getDerivedArtifact(
+ newOutput, ruleContext.getConfiguration().getBinDirectory());
+
+ String bashPath = BazelShRuleClasses.BASH_BINARY_BINDINGS
+ .get(BazelShRuleClasses.SYSTEM_BASH_VERSION).execPath;
+
+ // Generate the runner contents.
+ String runnerContents =
+ "#!/bin/bash\n"
+ + bashPath + " \"" + src.getRootRelativePath().getPathString() + "\" \"$@\"\n";
+
+ ruleContext.registerAction(
+ new FileWriteAction(ruleContext.getActionOwner(), testRunner, runnerContents, true));
+ return testRunner;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/workspace/HttpArchiveRule.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/workspace/HttpArchiveRule.java
new file mode 100644
index 0000000..c7f3677
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/workspace/HttpArchiveRule.java
@@ -0,0 +1,113 @@
+// Copyright 2014 Google Inc. 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.bazel.rules.workspace;
+
+import static com.google.devtools.build.lib.packages.Attribute.attr;
+import static com.google.devtools.build.lib.packages.Type.STRING;
+
+import com.google.devtools.build.lib.analysis.BlazeRule;
+import com.google.devtools.build.lib.analysis.RuleDefinition;
+import com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.RuleClass.Builder;
+import com.google.devtools.build.lib.packages.RuleClass.Builder.RuleClassType;
+
+/**
+ * Rule definition for the http_archive rule.
+ */
+@BlazeRule(name = HttpArchiveRule.NAME,
+ type = RuleClassType.WORKSPACE,
+ ancestors = { WorkspaceBaseRule.class },
+ factoryClass = WorkspaceConfiguredTargetFactory.class)
+public class HttpArchiveRule implements RuleDefinition {
+
+ public static final String NAME = "http_archive";
+
+ @Override
+ public RuleClass build(Builder builder, RuleDefinitionEnvironment environment) {
+ return builder
+ /* <!-- #BLAZE_RULE(http_archive).ATTRIBUTE(url) -->
+ A URL to an archive file containing a Bazel repository
+
+ <p>This must be an http URL that ends with .zip. There is no support for authentication or
+ redirection.</p>
+ <!-- #END_BLAZE_RULE.ATTRIBUTE --> */
+ .add(attr("url", STRING).mandatory())
+ /* <!-- #BLAZE_RULE(http_archive).ATTRIBUTE(sha256) -->
+ The expected SHA-256 hash of the file downloaded
+
+ <p>This must match the SHA-256 hash of the file downloaded.</p>
+ <!-- #END_BLAZE_RULE.ATTRIBUTE --> */
+ .add(attr("sha256", STRING).mandatory())
+ .setWorkspaceOnly()
+ .build();
+ }
+}
+
+/*<!-- #BLAZE_RULE (NAME = http_archive, TYPE = OTHER, FAMILY = General)[GENERIC_RULE] -->
+
+${ATTRIBUTE_SIGNATURE}
+
+<p>Downloads a Bazel repository as a compressed archive file, decompresses it, and makes its
+ targets available for binding.</p>
+
+<p>Only Zip-formatted archives with the .zip extension are supported.</p>
+
+${ATTRIBUTE_DEFINITION}
+
+<h4 id="http_archive_examples">Examples</h4>
+
+<p>Suppose the current repository contains the source code for a chat program, rooted at the
+ directory <i>~/chat-app</i>. It needs to depend on an SSL library which is available from
+ <i>http://example.com/openssl.zip</i>. This .zip file contains the following directory
+ structure:</p>
+
+<pre class="code">
+WORKSPACE
+src/
+ BUILD
+ openssl.cc
+ openssl.h
+</pre>
+
+<p><i>src/BUILD</i> contains the following target definition:</p>
+
+<pre class="code">
+cc_library(
+ name = "openssl-lib",
+ srcs = ["openssl.cc"],
+ hdrs = ["openssl.h"],
+)
+</pre>
+
+<p>Targets in the <i>~/chat-app</i> repository can depend on this target if the following lines are
+ added to <i>~/chat-app/WORKSPACE</i>:</p>
+
+<pre class="code">
+http_archive(
+ name = "my-ssl",
+ url = "http://example.com/openssl.zip",
+ sha256 = "03a58ac630e59778f328af4bcc4acb4f80208ed4",
+)
+
+bind(
+ name = "openssl",
+ actual = "@my-ssl//src:openssl-lib",
+)
+</pre>
+
+<p>See <a href="#bind_examples">Bind</a> for how to use bound targets.</p>
+
+<!-- #END_BLAZE_RULE -->*/
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/workspace/HttpJarRule.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/workspace/HttpJarRule.java
new file mode 100644
index 0000000..862c6d7
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/workspace/HttpJarRule.java
@@ -0,0 +1,91 @@
+// Copyright 2014 Google Inc. 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.bazel.rules.workspace;
+
+import static com.google.devtools.build.lib.packages.Attribute.attr;
+import static com.google.devtools.build.lib.packages.Type.STRING;
+
+import com.google.devtools.build.lib.analysis.BlazeRule;
+import com.google.devtools.build.lib.analysis.RuleDefinition;
+import com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.RuleClass.Builder;
+import com.google.devtools.build.lib.packages.RuleClass.Builder.RuleClassType;
+
+/**
+ * Rule definition for the http_jar rule.
+ */
+@BlazeRule(name = HttpJarRule.NAME,
+ type = RuleClassType.WORKSPACE,
+ ancestors = { WorkspaceBaseRule.class },
+ factoryClass = WorkspaceConfiguredTargetFactory.class)
+public class HttpJarRule implements RuleDefinition {
+
+ public static final String NAME = "http_jar";
+
+ @Override
+ public RuleClass build(Builder builder, RuleDefinitionEnvironment environment) {
+ return builder
+ /* <!-- #BLAZE_RULE(http_jar).ATTRIBUTE(url) -->
+ A URL to an archive file containing a Bazel repository
+
+ <p>This must be an http or https URL that ends with .jar. Redirects are not followed.</p>
+ <!-- #END_BLAZE_RULE.ATTRIBUTE --> */
+ .add(attr("url", STRING).mandatory())
+ /* <!-- #BLAZE_RULE(http_jar).ATTRIBUTE(sha256) -->
+ The expected SHA-256 of the file downloaded
+
+ <p>This must match the SHA-256 of the file downloaded.</p>
+ <!-- #END_BLAZE_RULE.ATTRIBUTE --> */
+ .add(attr("sha256", STRING).mandatory())
+ .setWorkspaceOnly()
+ .build();
+ }
+}
+/*<!-- #BLAZE_RULE (NAME = http_jar, TYPE = OTHER, FAMILY = General)[GENERIC_RULE] -->
+
+${ATTRIBUTE_SIGNATURE}
+
+<p>Downloads a jar from a URL and makes it available to be used as a Java dependency.</p>
+
+<p>Downloaded files must have a .jar extension.</p>
+
+${ATTRIBUTE_DEFINITION}
+
+<h4 id="http_jar_examples">Examples</h4>
+
+<p>Suppose the current repository contains the source code for a chat program, rooted at the
+ directory <i>~/chat-app</i>. It needs to depend on an SSL library which is available from
+ <i>http://example.com/openssl-0.2.jar</i>.</p>
+
+<p>Targets in the <i>~/chat-app</i> repository can depend on this target if the following lines are
+ added to <i>~/chat-app/WORKSPACE</i>:</p>
+
+<pre class="code">
+http_jar(
+ name = "my-ssl",
+ url = "http://example.com/openssl-0.2.jar",
+ sha256 = "03a58ac630e59778f328af4bcc4acb4f80208ed4",
+)
+
+bind(
+ name = "openssl",
+ actual = "@my-ssl//jar:openssl-0.2.jar",
+)
+</pre>
+
+<p>See <a href="#bind_examples">Bind</a> for how to use bound targets.</p>
+
+<!-- #END_BLAZE_RULE -->*/
\ No newline at end of file
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/workspace/LocalRepositoryRule.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/workspace/LocalRepositoryRule.java
new file mode 100644
index 0000000..b904bc5
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/workspace/LocalRepositoryRule.java
@@ -0,0 +1,85 @@
+// Copyright 2014 Google Inc. 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.bazel.rules.workspace;
+
+import static com.google.devtools.build.lib.packages.Attribute.attr;
+import static com.google.devtools.build.lib.packages.Type.STRING;
+
+import com.google.devtools.build.lib.analysis.BlazeRule;
+import com.google.devtools.build.lib.analysis.RuleDefinition;
+import com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.RuleClass.Builder;
+import com.google.devtools.build.lib.packages.RuleClass.Builder.RuleClassType;
+
+/**
+ * Rule definition for the local_repository rule.
+ */
+@BlazeRule(name = LocalRepositoryRule.NAME,
+ type = RuleClassType.WORKSPACE,
+ ancestors = { WorkspaceBaseRule.class },
+ factoryClass = WorkspaceConfiguredTargetFactory.class)
+public class LocalRepositoryRule implements RuleDefinition {
+
+ public static final String NAME = "local_repository";
+
+ @Override
+ public RuleClass build(Builder builder, RuleDefinitionEnvironment environment) {
+ return builder
+ /* <!-- #BLAZE_RULE(local_repository).ATTRIBUTE(path) -->
+ The path to the local repository's directory.
+
+ <p>This must be an absolute path to the directory containing the repository's
+ <i>WORKSPACE</i> file.</p>
+ <!-- #END_BLAZE_RULE.ATTRIBUTE --> */
+ .add(attr("path", STRING).mandatory())
+ .setWorkspaceOnly()
+ .build();
+ }
+}
+/*<!-- #BLAZE_RULE (NAME = local_repository, TYPE = OTHER, FAMILY = General)[GENERIC_RULE] -->
+
+${ATTRIBUTE_SIGNATURE}
+
+<p>Allows targets from a local directory to be bound. This means that the current repository can
+ use targets defined in this other directory. See the <a href="#bind_examples">bind section</a>
+ for more details.</p>
+
+${ATTRIBUTE_DEFINITION}
+
+<h4 id="local_repository_examples">Examples</h4>
+
+<p>Suppose the current repository is a chat client, rooted at the directory <i>~/chat-app</i>. It
+ would like to use an SSL library which is defined in a different repository: <i>~/ssl</i>. The
+ SSL library has a target <code>//src:openssl-lib</code>.</p>
+
+<p>The user can add a dependency on this target by adding the following lines to
+ <i>~/chat-app/WORKSPACE</i>:</p>
+
+<pre class="code">
+local_repository(
+ name = "my-ssl",
+ path = "/home/user/ssl",
+)
+
+bind(
+ name = "openssl",
+ actual = "@my-ssl//src:openssl-lib",
+)
+</pre>
+
+<p>See <a href="#bind_examples">Bind</a> for how to use bound targets.</p>
+
+<!-- #END_BLAZE_RULE -->*/
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/workspace/MavenJarRule.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/workspace/MavenJarRule.java
new file mode 100644
index 0000000..f4496dd
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/workspace/MavenJarRule.java
@@ -0,0 +1,127 @@
+// Copyright 2014 Google Inc. 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.bazel.rules.workspace;
+
+import static com.google.devtools.build.lib.packages.Attribute.attr;
+
+import com.google.devtools.build.lib.analysis.BlazeRule;
+import com.google.devtools.build.lib.analysis.RuleDefinition;
+import com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.RuleClass.Builder;
+import com.google.devtools.build.lib.packages.RuleClass.Builder.RuleClassType;
+import com.google.devtools.build.lib.packages.Type;
+
+/**
+ * Rule definition for the maven_jar rule.
+ */
+@BlazeRule(name = MavenJarRule.NAME,
+ type = RuleClassType.WORKSPACE,
+ ancestors = { WorkspaceBaseRule.class },
+ factoryClass = WorkspaceConfiguredTargetFactory.class)
+public class MavenJarRule implements RuleDefinition {
+
+ public static final String NAME = "maven_jar";
+
+ @Override
+ public RuleClass build(Builder builder, RuleDefinitionEnvironment environment) {
+ return builder
+ /* <!-- #BLAZE_RULE(maven_jar).ATTRIBUTE(artifact_id) -->
+ The artifactId of the Maven dependency.
+
+ <p>Required.</p>
+ <!-- #END_BLAZE_RULE.ATTRIBUTE --> */
+ .add(attr("artifact_id", Type.STRING).mandatory())
+ /* <!-- #BLAZE_RULE(maven_jar).ATTRIBUTE(group_id) -->
+ The groupId of the Maven dependency.
+
+ <p>Required.</p>
+ <!-- #END_BLAZE_RULE.ATTRIBUTE --> */
+ .add(attr("group_id", Type.STRING).mandatory())
+ /* <!-- #BLAZE_RULE(maven_jar).ATTRIBUTE(version) -->
+ The version of the Maven dependency.
+
+ <p>Required.</p>
+ <!-- #END_BLAZE_RULE.ATTRIBUTE --> */
+ .add(attr("version", Type.STRING).mandatory())
+ /* <!-- #BLAZE_RULE(maven_jar).ATTRIBUTE(repositories) -->
+ A list of repositories to use to attempt to fetch the jar.
+
+ <p>Defaults to Maven Central ("repo1.maven.org"). If repositories are specified, they will
+ be checked in the order listed here (Maven Central will not be checked in this case,
+ unless it is on the list).</p>
+
+ <p><b>To be implemented: add a maven_repositories rule that allows a list of repositories
+ to be labeled.</b></p>
+ <!-- #END_BLAZE_RULE.ATTRIBUTE --> */
+ .add(attr("repositories", Type.STRING_LIST))
+ /* <!-- #BLAZE_RULE(maven_jar).ATTRIBUTE(exclusions) -->
+ Transitive dependencies of this dependency that should not be downloaded.
+
+ <p>Defaults to None: Bazel will download all of the dependencies requested by the Maven
+ dependency. If exclusions are specified, they will not be downloaded.</p>
+
+ <p>Exclusions are specified in the format "<group_id>:<artifact_id>", for example,
+ "com.google.guava:guava".</p>
+
+ <p><b>Not yet implemented.</b></p>
+ <!-- #END_BLAZE_RULE.ATTRIBUTE --> */
+ .add(attr("exclusions", Type.STRING_LIST))
+ .setWorkspaceOnly()
+ .build();
+ }
+}
+/*<!-- #BLAZE_RULE (NAME = maven_jar, TYPE = OTHER, FAMILY = General)[GENERIC_RULE] -->
+
+${ATTRIBUTE_SIGNATURE}
+
+<p>Downloads a jar from Maven and makes it available to be used as a Java dependency.</p>
+
+${ATTRIBUTE_DEFINITION}
+
+<h4 id="http_jar_examples">Examples</h4>
+
+Suppose that the current repostory contains a java_library target that needs to depend on Guava.
+Using Maven, this dependency would be defined in the pom.xml file as:
+
+<pre>
+<dependency>
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava</artifactId>
+ <version>18.0</version>
+</dependency>
+</pre>
+
+In Bazel, the following lines can be added to the WORKSPACE file:
+
+<pre>
+maven_jar(
+ name = "guava",
+ group_id = "com.google.guava",
+ artifact_id = "guava",
+ version = "18.0",
+)
+
+bind(
+ name = "guava-jar",
+ actual = "@guava//jar"
+)
+</pre>
+
+Then the java_library can depend on <code>//external:guava-jar</code>.
+
+<p>See <a href="#bind_examples">Bind</a> for how to use bound targets.</p>
+
+<!-- #END_BLAZE_RULE -->*/
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/workspace/NewLocalRepositoryRule.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/workspace/NewLocalRepositoryRule.java
new file mode 100644
index 0000000..0061d3d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/workspace/NewLocalRepositoryRule.java
@@ -0,0 +1,135 @@
+// Copyright 2015 Google Inc. 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.bazel.rules.workspace;
+
+import static com.google.devtools.build.lib.packages.Attribute.attr;
+import static com.google.devtools.build.lib.packages.Type.STRING;
+
+import com.google.devtools.build.lib.analysis.BlazeRule;
+import com.google.devtools.build.lib.analysis.RuleDefinition;
+import com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.RuleClass.Builder;
+import com.google.devtools.build.lib.packages.RuleClass.Builder.RuleClassType;
+
+/**
+ * Rule definition for the new_repository rule.
+ */
+@BlazeRule(name = NewLocalRepositoryRule.NAME,
+ type = RuleClassType.WORKSPACE,
+ ancestors = { WorkspaceBaseRule.class },
+ factoryClass = WorkspaceConfiguredTargetFactory.class)
+public class NewLocalRepositoryRule implements RuleDefinition {
+ public static final String NAME = "new_local_repository";
+
+ @Override
+ public RuleClass build(Builder builder, RuleDefinitionEnvironment environment) {
+ return builder
+ /* <!-- #BLAZE_RULE(new_local_repository).ATTRIBUTE(path) -->
+ A path on the local filesystem.
+
+ <p>This must be an absolute path to an existing file or a directory.</p>
+ <!-- #END_BLAZE_RULE.ATTRIBUTE --> */
+ .add(attr("path", STRING).mandatory())
+ /* <!-- #BLAZE_RULE(new_local_repository).ATTRIBUTE(build_file) -->
+ A file to use as a BUILD file for this directory.
+
+ <p>This path must be relative to the build's workspace. The file does not need to be named
+ BUILD, but can be (something like BUILD.new-repo-name may work well for distinguishing it
+ from the repository's actual BUILD files.</p>
+ <!-- #END_BLAZE_RULE.ATTRIBUTE --> */
+ .add(attr("build_file", STRING).mandatory())
+ .setWorkspaceOnly()
+ .build();
+ }
+}
+/*<!-- #BLAZE_RULE (NAME = new_local_repository, TYPE = OTHER, FAMILY = General)[GENERIC_RULE] -->
+
+${ATTRIBUTE_SIGNATURE}
+
+<p>Allows a local directory to be turned into a Bazel repository. This means that the current
+ repository can define and use targets from anywhere on the filesystem.</p>
+
+<p>This rule creates a Bazel repository by creating a WORKSPACE file and subdirectory containing
+symlinks to the BUILD file and path given. The build file should create targets relative to the
+path, which can then be bound and used by the current build.
+
+${ATTRIBUTE_DEFINITION}
+
+<h4 id="new_local_repository_examples">Examples</h4>
+
+<p>Suppose the current repository is a chat client, rooted at the directory <i>~/chat-app</i>. It
+ would like to use an SSL library which is defined in a different directory: <i>~/ssl</i>.</p>
+
+<p>The user can add a dependency by creating a BUILD file for the SSL library
+(~/chat-app/BUILD.my-ssl) containing:
+
+<pre class="code">
+java_library(
+ name = "openssl",
+ srcs = glob(['ssl/*.java'])
+)
+</pre>
+
+<p>Then they can add the following lines to <i>~/chat-app/WORKSPACE</i>:</p>
+
+<pre class="code">
+new_local_repository(
+ name = "my-ssl",
+ path = "/home/user/ssl",
+ build_file = "BUILD.my-ssl",
+)
+
+bind(
+ name = "openssl",
+ actual = "@my-ssl//my-ssl:openssl",
+)
+</pre>
+
+<p>This will create a @my-ssl repository containing a my-ssl package that contains a symlink to
+/home/user/ssl named ssl (so the BUILD file must refer to paths within /home/user/ssl relative to
+ssl).</p>
+
+<p>See <a href="#bind_examples">Bind</a> for how to use bound targets.</p>
+
+<p>You can also use <code>new_local_repository</code> to include single files, not just
+directories. For example, suppose you had a jar file at /home/username/Downloads/piano.jar. You
+could add just that file to your build by adding the following to your WORKSPACE file:
+
+<pre class="code">
+new_local_repository(
+ name = "piano",
+ path = "/home/username/Downloads/piano.jar",
+ build_file = "BUILD.piano",
+)
+
+bind(
+ name = "music",
+ actual = "@piano//piano:play-music",
+)
+</pre>
+
+<p>And creating the following BUILD file:</p>
+
+<pre class="code">
+java_import(
+ name = "play-music",
+ jars = ["piano.jar"],
+)
+</pre>
+
+Then targets can depend on //external:music to use piano.jar.
+
+<!-- #END_BLAZE_RULE -->*/
\ No newline at end of file
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/workspace/WorkspaceBaseRule.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/workspace/WorkspaceBaseRule.java
new file mode 100644
index 0000000..2719cb8
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/workspace/WorkspaceBaseRule.java
@@ -0,0 +1,34 @@
+// Copyright 2015 Google Inc. 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.bazel.rules.workspace;
+
+import com.google.devtools.build.lib.analysis.BlazeRule;
+import com.google.devtools.build.lib.analysis.RuleDefinition;
+import com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.RuleClass.Builder;
+import com.google.devtools.build.lib.packages.RuleClass.Builder.RuleClassType;
+
+/**
+ * Base rule for rules in the WORKSPACE file.
+ */
+@BlazeRule(name = "$workspace_base_rule",
+ type = RuleClassType.ABSTRACT)
+public class WorkspaceBaseRule implements RuleDefinition {
+ @Override
+ public RuleClass build(Builder builder, RuleDefinitionEnvironment environment) {
+ return builder.build();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/workspace/WorkspaceConfiguredTargetFactory.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/workspace/WorkspaceConfiguredTargetFactory.java
new file mode 100644
index 0000000..6162228
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/workspace/WorkspaceConfiguredTargetFactory.java
@@ -0,0 +1,37 @@
+// Copyright 2014 Google Inc. 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.bazel.rules.workspace;
+
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTargetBuilder;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.RunfilesProvider;
+import com.google.devtools.build.lib.rules.RuleConfiguredTargetFactory;
+
+/**
+ * Implementation of workspace rules. Generally, these don't have any providers, since they
+ * "forward" to the SkyFunctions which actually create the repositories and then are accessed via
+ * "normal" rules.
+ */
+public class WorkspaceConfiguredTargetFactory implements RuleConfiguredTargetFactory {
+
+ @Override
+ public ConfiguredTarget create(RuleContext ruleContext) throws InterruptedException {
+ return new RuleConfiguredTargetBuilder(ruleContext)
+ .addProvider(RunfilesProvider.class, RunfilesProvider.EMPTY)
+ .build();
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/buildtool/BuildRequest.java b/src/main/java/com/google/devtools/build/lib/buildtool/BuildRequest.java
new file mode 100644
index 0000000..95fbde5
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/buildtool/BuildRequest.java
@@ -0,0 +1,532 @@
+// Copyright 2014 Google Inc. 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.buildtool;
+
+import com.google.common.base.Optional;
+import com.google.common.base.Preconditions;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSortedSet;
+import com.google.devtools.build.lib.Constants;
+import com.google.devtools.build.lib.analysis.BuildView;
+import com.google.devtools.build.lib.analysis.TopLevelArtifactContext;
+import com.google.devtools.build.lib.analysis.config.InvalidConfigurationException;
+import com.google.devtools.build.lib.exec.ExecutionOptions;
+import com.google.devtools.build.lib.pkgcache.LoadingPhaseRunner;
+import com.google.devtools.build.lib.pkgcache.PackageCacheOptions;
+import com.google.devtools.build.lib.runtime.BlazeCommandEventHandler;
+import com.google.devtools.build.lib.util.OptionsUtils;
+import com.google.devtools.build.lib.util.io.OutErr;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.common.options.Converter;
+import com.google.devtools.common.options.Converters;
+import com.google.devtools.common.options.Converters.RangeConverter;
+import com.google.devtools.common.options.Option;
+import com.google.devtools.common.options.OptionsBase;
+import com.google.devtools.common.options.OptionsClassProvider;
+import com.google.devtools.common.options.OptionsParsingException;
+import com.google.devtools.common.options.OptionsProvider;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.ExecutionException;
+
+/**
+ * A BuildRequest represents a single invocation of the build tool by a user.
+ * A request specifies a list of targets to be built for a single
+ * configuration, a pair of output/error streams, and additional options such
+ * as --keep_going, --jobs, etc.
+ */
+public class BuildRequest implements OptionsClassProvider {
+ private static final String DEFAULT_SYMLINK_PREFIX_MARKER = "...---:::@@@DEFAULT@@@:::--...";
+
+ /**
+ * A converter for symlink prefixes that defaults to {@code Constants.PRODUCT_NAME} and a
+ * minus sign if the option is not given.
+ *
+ * <p>Required because you cannot specify a non-constant value in annotation attributes.
+ */
+ public static class SymlinkPrefixConverter implements Converter<String> {
+ @Override
+ public String convert(String input) throws OptionsParsingException {
+ return input.equals(DEFAULT_SYMLINK_PREFIX_MARKER)
+ ? Constants.PRODUCT_NAME + "-"
+ : input;
+ }
+
+ @Override
+ public String getTypeDescription() {
+ return "a string";
+ }
+ }
+
+ /**
+ * Options interface--can be used to parse command-line arguments.
+ *
+ * See also ExecutionOptions; from the user's point of view, there's no
+ * qualitative difference between these two sets of options.
+ */
+ public static class BuildRequestOptions extends OptionsBase {
+
+ /* "Execution": options related to the execution of a build: */
+
+ @Option(name = "jobs",
+ abbrev = 'j',
+ defaultValue = "200",
+ category = "strategy",
+ help = "The number of concurrent jobs to run. "
+ + "0 means build sequentially. Values above " + MAX_JOBS
+ + " are not allowed.")
+ public int jobs;
+
+ @Option(name = "progress_report_interval",
+ defaultValue = "0",
+ category = "verbosity",
+ converter = ProgressReportIntervalConverter.class,
+ help = "The number of seconds to wait between two reports on"
+ + " still running jobs. The default value 0 means to use"
+ + " the default 10:30:60 incremental algorithm.")
+ public int progressReportInterval;
+
+ @Option(name = "show_builder_stats",
+ defaultValue = "false",
+ category = "verbosity",
+ help = "If set, parallel builder will report worker-related statistics.")
+ public boolean useBuilderStatistics;
+
+ @Option(name = "explain",
+ defaultValue = "null",
+ category = "verbosity",
+ converter = OptionsUtils.PathFragmentConverter.class,
+ help = "Causes Blaze to explain each executed step of the build. "
+ + "The explanation is written to the specified log file.")
+ public PathFragment explanationPath;
+
+ @Option(name = "verbose_explanations",
+ defaultValue = "false",
+ category = "verbosity",
+ help = "Increases the verbosity of the explanations issued if --explain is enabled. "
+ + "Has no effect if --explain is not enabled.")
+ public boolean verboseExplanations;
+
+ @Deprecated
+ @Option(name = "dump_makefile",
+ defaultValue = "false",
+ category = "undocumented",
+ help = "this flag has no effect.")
+ public boolean dumpMakefile;
+
+ @Deprecated
+ @Option(name = "dump_action_graph",
+ defaultValue = "false",
+ category = "undocumented",
+ help = "this flag has no effect.")
+
+ public boolean dumpActionGraph;
+
+ @Deprecated
+ @Option(name = "dump_action_graph_for_package",
+ allowMultiple = true,
+ defaultValue = "",
+ category = "undocumented",
+ help = "this flag has no effect.")
+ public List<String> dumpActionGraphForPackage = new ArrayList<>();
+
+ @Deprecated
+ @Option(name = "dump_action_graph_with_middlemen",
+ defaultValue = "true",
+ category = "undocumented",
+ help = "this flag has no effect.")
+ public boolean dumpActionGraphWithMiddlemen;
+
+ @Deprecated
+ @Option(name = "dump_providers",
+ defaultValue = "false",
+ category = "undocumented",
+ help = "This is a no-op.")
+ public boolean dumpProviders;
+
+ @Option(name = "incremental_builder",
+ deprecationWarning = "incremental_builder is now a no-op and will be removed in an"
+ + " upcoming Blaze release",
+ defaultValue = "true",
+ category = "strategy",
+ help = "Enables an incremental builder aimed at faster "
+ + "incremental builds. Currently it has the greatest effect on null"
+ + "builds.")
+ public boolean useIncrementalDependencyChecker;
+
+ @Deprecated
+ @Option(name = "dump_targets",
+ defaultValue = "null",
+ category = "undocumented",
+ help = "this flag has no effect.")
+ public String dumpTargets;
+
+ @Deprecated
+ @Option(name = "dump_host_deps",
+ defaultValue = "true",
+ category = "undocumented",
+ help = "Deprecated")
+ public boolean dumpHostDeps;
+
+ @Deprecated
+ @Option(name = "dump_to_stdout",
+ defaultValue = "false",
+ category = "undocumented",
+ help = "Deprecated")
+ public boolean dumpToStdout;
+
+ @Option(name = "analyze",
+ defaultValue = "true",
+ category = "undocumented",
+ help = "Execute the analysis phase; this is the usual behaviour. "
+ + "Specifying --noanalyze causes the build to stop before starting the "
+ + "analysis phase, returning zero iff the package loading completed "
+ + "successfully; this mode is useful for testing.")
+ public boolean performAnalysisPhase;
+
+ @Option(name = "build",
+ defaultValue = "true",
+ category = "what",
+ help = "Execute the build; this is the usual behaviour. "
+ + "Specifying --nobuild causes the build to stop before executing the "
+ + "build actions, returning zero iff the package loading and analysis "
+ + "phases completed successfully; this mode is useful for testing "
+ + "those phases.")
+ public boolean performExecutionPhase;
+
+ @Option(name = "compile_only",
+ defaultValue = "false",
+ category = "what",
+ help = "If specified, Blaze will only build files that are generated by lightweight "
+ + "compilation actions, skipping more expensive build steps (such as linking).")
+ public boolean compileOnly;
+
+ @Option(name = "compilation_prerequisites_only",
+ defaultValue = "false",
+ category = "what",
+ help = "If specified, Blaze will only build files that are prerequisites to compilation "
+ + "of the given target (for example, generated source files and headers) without "
+ + "building the target itself. This flag is ignored if --compile_only is enabled.")
+ public boolean compilationPrerequisitesOnly;
+
+ @Option(name = "output_groups",
+ converter = Converters.CommaSeparatedOptionListConverter.class,
+ allowMultiple = true,
+ defaultValue = "",
+ category = "undocumented",
+ help = "Specifies, which output groups of the top-level target to build.")
+ public List<String> outputGroups;
+
+ @Option(name = "show_result",
+ defaultValue = "1",
+ category = "verbosity",
+ help = "Show the results of the build. For each "
+ + "target, state whether or not it was brought up-to-date, and if "
+ + "so, a list of output files that were built. The printed files "
+ + "are convenient strings for copy+pasting to the shell, to "
+ + "execute them.\n"
+ + "This option requires an integer argument, which "
+ + "is the threshold number of targets above which result "
+ + "information is not printed. "
+ + "Thus zero causes suppression of the message and MAX_INT "
+ + "causes printing of the result to occur always. The default is one.")
+ public int maxResultTargets;
+
+ @Option(name = "announce",
+ defaultValue = "false",
+ category = "verbosity",
+ help = "Deprecated. No-op.",
+ deprecationWarning = "This option is now deprecated and is a no-op")
+ public boolean announce;
+
+ @Option(name = "symlink_prefix",
+ defaultValue = DEFAULT_SYMLINK_PREFIX_MARKER,
+ converter = SymlinkPrefixConverter.class,
+ category = "misc",
+ help = "The prefix that is prepended to any of the convenience symlinks that are created "
+ + "after a build. If '/' is passed, then no symlinks are created and no warning is "
+ + "emitted."
+ )
+ public String symlinkPrefix;
+
+ @Option(name = "experimental_multi_cpu",
+ converter = Converters.CommaSeparatedOptionListConverter.class,
+ allowMultiple = true,
+ defaultValue = "",
+ category = "semantics",
+ help = "This flag allows specifying multiple target CPUs. If this is specified, "
+ + "the --cpu option is ignored.")
+ public List<String> multiCpus;
+
+ @Option(name = "experimental_check_output_files",
+ defaultValue = "true",
+ category = "undocumented",
+ help = "Check for modifications made to the output files of a build. Consider setting "
+ + "this flag to false to see the effect on incremental build times.")
+ public boolean checkOutputFiles;
+ }
+
+ /**
+ * Converter for progress_report_interval: [0, 3600].
+ */
+ public static class ProgressReportIntervalConverter extends RangeConverter {
+ public ProgressReportIntervalConverter() {
+ super(0, 3600);
+ }
+ }
+
+ private static final int MAX_JOBS = 2000;
+ private static final int JOBS_TOO_HIGH_WARNING = 1000;
+
+ private final UUID id;
+ private final LoadingCache<Class<? extends OptionsBase>, Optional<OptionsBase>> optionsCache;
+
+ /** A human-readable description of all the non-default option settings. */
+ private final String optionsDescription;
+
+ /**
+ * The name of the Blaze command that the user invoked.
+ * Used for --announce.
+ */
+ private final String commandName;
+
+ private final OutErr outErr;
+ private final List<String> targets;
+
+ private long startTimeMillis = 0; // milliseconds since UNIX epoch.
+
+ private boolean runningInEmacs = false;
+ private boolean runTests = false;
+
+ private static final List<Class<? extends OptionsBase>> MANDATORY_OPTIONS = ImmutableList.of(
+ BuildRequestOptions.class,
+ PackageCacheOptions.class,
+ LoadingPhaseRunner.Options.class,
+ BuildView.Options.class,
+ ExecutionOptions.class);
+
+ private BuildRequest(String commandName,
+ final OptionsProvider options,
+ final OptionsProvider startupOptions,
+ List<String> targets,
+ OutErr outErr,
+ UUID id,
+ long startTimeMillis) {
+ this.commandName = commandName;
+ this.optionsDescription = OptionsUtils.asShellEscapedString(options);
+ this.outErr = outErr;
+ this.targets = targets;
+ this.id = id;
+ this.startTimeMillis = startTimeMillis;
+ this.optionsCache = CacheBuilder.newBuilder()
+ .build(new CacheLoader<Class<? extends OptionsBase>, Optional<OptionsBase>>() {
+ @Override
+ public Optional<OptionsBase> load(Class<? extends OptionsBase> key) throws Exception {
+ OptionsBase result = options.getOptions(key);
+ if (result == null && startupOptions != null) {
+ result = startupOptions.getOptions(key);
+ }
+
+ return Optional.fromNullable(result);
+ }
+ });
+
+ for (Class<? extends OptionsBase> optionsClass : MANDATORY_OPTIONS) {
+ Preconditions.checkNotNull(getOptions(optionsClass));
+ }
+ }
+
+ /**
+ * Returns a unique identifier that universally identifies this build.
+ */
+ public UUID getId() {
+ return id;
+ }
+
+ /**
+ * Returns the name of the Blaze command that the user invoked.
+ */
+ public String getCommandName() {
+ return commandName;
+ }
+
+ /**
+ * Set to true if this build request was initiated by Emacs.
+ * (Certain output formatting may be necessary.)
+ */
+ public void setRunningInEmacs() {
+ runningInEmacs = true;
+ }
+
+ boolean isRunningInEmacs() {
+ return runningInEmacs;
+ }
+
+ /**
+ * Enables test execution for this build request.
+ */
+ public void setRunTests() {
+ runTests = true;
+ }
+
+ /**
+ * Returns true if tests should be run by the build tool.
+ */
+ public boolean shouldRunTests() {
+ return runTests;
+ }
+
+ /**
+ * Returns the (immutable) list of targets to build in commandline
+ * form.
+ */
+ public List<String> getTargets() {
+ return targets;
+ }
+
+ /**
+ * Returns the output/error streams to which errors and progress messages
+ * should be sent during the fulfillment of this request.
+ */
+ public OutErr getOutErr() {
+ return outErr;
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public <T extends OptionsBase> T getOptions(Class<T> clazz) {
+ try {
+ return (T) optionsCache.get(clazz).orNull();
+ } catch (ExecutionException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ /**
+ * Returns the set of command-line options specified for this request.
+ */
+ public BuildRequestOptions getBuildOptions() {
+ return getOptions(BuildRequestOptions.class);
+ }
+
+ /**
+ * Returns the set of options related to the loading phase.
+ */
+ public PackageCacheOptions getPackageCacheOptions() {
+ return getOptions(PackageCacheOptions.class);
+ }
+
+ /**
+ * Returns the set of options related to the loading phase.
+ */
+ public LoadingPhaseRunner.Options getLoadingOptions() {
+ return getOptions(LoadingPhaseRunner.Options.class);
+ }
+
+ /**
+ * Returns the set of command-line options related to the view specified for
+ * this request.
+ */
+ public BuildView.Options getViewOptions() {
+ return getOptions(BuildView.Options.class);
+ }
+
+ /**
+ * Returns the human-readable description of the non-default options
+ * for this build request.
+ */
+ public String getOptionsDescription() {
+ return optionsDescription;
+ }
+
+ /**
+ * Return the time (according to System.currentTimeMillis()) at which the
+ * service of this request was started.
+ */
+ public long getStartTime() {
+ return startTimeMillis;
+ }
+
+ /**
+ * Validates the options for this BuildRequest.
+ *
+ * <p>Issues warnings or throws {@code InvalidConfigurationException} for option settings that
+ * conflict.
+ *
+ * @return list of warnings
+ */
+ public List<String> validateOptions() throws InvalidConfigurationException {
+ List<String> warnings = new ArrayList<>();
+ // Validate "jobs".
+ int jobs = getBuildOptions().jobs;
+ if (jobs < 0 || jobs > MAX_JOBS) {
+ throw new InvalidConfigurationException(String.format(
+ "Invalid parameter for --jobs: %d. Only values 0 <= jobs <= %d are allowed.", jobs,
+ MAX_JOBS));
+ }
+ if (jobs > JOBS_TOO_HIGH_WARNING) {
+ warnings.add(
+ String.format("High value for --jobs: %d. You may run into memory issues", jobs));
+ }
+
+ // Validate other BuildRequest options.
+ if (getBuildOptions().verboseExplanations && getBuildOptions().explanationPath == null) {
+ warnings.add("--verbose_explanations has no effect when --explain=<file> is not enabled");
+ }
+ if (getBuildOptions().compileOnly && getBuildOptions().compilationPrerequisitesOnly) {
+ throw new InvalidConfigurationException(
+ "--compile_only and --compilation_prerequisites_only are not compatible");
+ }
+
+ return warnings;
+ }
+
+ /** Creates a new TopLevelArtifactContext from this build request. */
+ public TopLevelArtifactContext getTopLevelArtifactContext() {
+ return new TopLevelArtifactContext(getCommandName(),
+ getBuildOptions().compileOnly, getBuildOptions().compilationPrerequisitesOnly,
+ getOptions(ExecutionOptions.class).testStrategy.equals("exclusive"),
+ ImmutableSet.<String>copyOf(getBuildOptions().outputGroups), shouldRunTests());
+ }
+
+ public String getSymlinkPrefix() {
+ return getBuildOptions().symlinkPrefix;
+ }
+
+ public ImmutableSortedSet<String> getMultiCpus() {
+ return ImmutableSortedSet.copyOf(getBuildOptions().multiCpus);
+ }
+
+ public static BuildRequest create(String commandName, OptionsProvider options,
+ OptionsProvider startupOptions,
+ List<String> targets, OutErr outErr, UUID commandId, long commandStartTime) {
+
+ BuildRequest request = new BuildRequest(commandName, options, startupOptions, targets, outErr,
+ commandId, commandStartTime);
+
+ // All this, just to pass a global boolean from the client to the server. :(
+ if (options.getOptions(BlazeCommandEventHandler.Options.class).runningInEmacs) {
+ request.setRunningInEmacs();
+ }
+
+ return request;
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/buildtool/BuildResult.java b/src/main/java/com/google/devtools/build/lib/buildtool/BuildResult.java
new file mode 100644
index 0000000..22c36f8
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/buildtool/BuildResult.java
@@ -0,0 +1,196 @@
+// Copyright 2014 Google Inc. 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.buildtool;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.util.ExitCode;
+
+import java.util.Collection;
+import java.util.Collections;
+
+import javax.annotation.Nullable;
+
+/**
+ * Contains information about the result of a build. While BuildRequest is immutable, this class is
+ * mutable.
+ */
+public final class BuildResult {
+ private long startTimeMillis = 0; // milliseconds since UNIX epoch.
+ private long stopTimeMillis = 0;
+
+ private Throwable crash = null;
+ private boolean catastrophe = false;
+ private ExitCode exitCondition = ExitCode.BLAZE_INTERNAL_ERROR;
+ private Collection<ConfiguredTarget> actualTargets;
+ private Collection<ConfiguredTarget> testTargets;
+ private Collection<ConfiguredTarget> successfulTargets;
+
+ public BuildResult(long startTimeMillis) {
+ this.startTimeMillis = startTimeMillis;
+ }
+
+ /**
+ * Record the time (according to System.currentTimeMillis()) at which the
+ * service of this request was completed.
+ */
+ public void setStopTime(long stopTimeMillis) {
+ this.stopTimeMillis = stopTimeMillis;
+ }
+
+ /**
+ * Return the time (according to System.currentTimeMillis()) at which the
+ * service of this request was completed.
+ */
+ public long getStopTime() {
+ return stopTimeMillis;
+ }
+
+ /**
+ * Returns the elapsed time in seconds for the service of this request. Not
+ * defined for requests that have not been serviced.
+ */
+ public double getElapsedSeconds() {
+ if (startTimeMillis == 0 || stopTimeMillis == 0) {
+ throw new IllegalStateException("BuildRequest has not been serviced");
+ }
+ return (stopTimeMillis - startTimeMillis) / 1000.0;
+ }
+
+ public void setExitCondition(ExitCode exitCondition) {
+ this.exitCondition = exitCondition;
+ }
+
+ /**
+ * True iff the build request has been successfully completed.
+ */
+ public boolean getSuccess() {
+ return exitCondition == ExitCode.SUCCESS;
+ }
+
+ /**
+ * Gets the Blaze exit condition.
+ */
+ public ExitCode getExitCondition() {
+ return exitCondition;
+ }
+
+ /**
+ * Sets the RuntimeException / Error that induced a Blaze crash.
+ */
+ public void setUnhandledThrowable(Throwable crash) {
+ Preconditions.checkState(crash == null ||
+ ((crash instanceof RuntimeException) || (crash instanceof Error)));
+ this.crash = crash;
+ }
+
+ /**
+ * Sets a "catastrophe": A build failure severe enough to halt a keep_going build.
+ */
+ public void setCatastrophe() {
+ this.catastrophe = true;
+ }
+
+ /**
+ * Was the build a "catastrophe": A build failure severe enough to halt a keep_going build.
+ */
+ public boolean wasCatastrophe() {
+ return catastrophe;
+ }
+
+ /**
+ * Gets the Blaze crash Throwable. Null if Blaze did not crash.
+ */
+ public Throwable getUnhandledThrowable() {
+ return crash;
+ }
+
+ /**
+ * @see #getActualTargets
+ */
+ public void setActualTargets(Collection<ConfiguredTarget> actualTargets) {
+ this.actualTargets = actualTargets;
+ }
+
+ /**
+ * Returns the actual set of targets which we attempted to build. This value
+ * is set during the build, after the target patterns have been parsed and
+ * resolved. If --keep_going is specified, this set may exclude targets that
+ * could not be found or successfully analyzed. It may be examined after the
+ * build. May be null even after the build, if there were errors in the
+ * loading or analysis phases.
+ */
+ public Collection<ConfiguredTarget> getActualTargets() {
+ return actualTargets;
+ }
+
+ /**
+ * @see #getTestTargets
+ */
+ public void setTestTargets(@Nullable Collection<ConfiguredTarget> testTargets) {
+ this.testTargets = testTargets == null ? null : Collections.unmodifiableCollection(testTargets);
+ }
+
+ /**
+ * Returns the actual unmodifiable collection of targets which we attempted to
+ * test. This value is set at the end of the build analysis phase, after the
+ * test target patterns have been parsed and resolved. If --keep_going is
+ * specified, this collection may exclude targets that could not be found or
+ * successfully analyzed. It may be examined after the build. May be null even
+ * after the build, if there were errors in the loading or analysis phases or
+ * if testing was not requested.
+ */
+ public Collection<ConfiguredTarget> getTestTargets() {
+ return testTargets;
+ }
+
+ /**
+ * @see #getSuccessfulTargets
+ */
+ void setSuccessfulTargets(Collection<ConfiguredTarget> successfulTargets) {
+ this.successfulTargets = successfulTargets;
+ }
+
+ /**
+ * Returns the set of targets which successfully built. This value
+ * is set at the end of the build, after the target patterns have been parsed
+ * and resolved and after attempting to build the targets. If --keep_going
+ * is specified, this set may exclude targets that could not be found or
+ * successfully analyzed, or could not be built. It may be examined after
+ * the build. May be null if the execution phase was not attempted, as
+ * may happen if there are errors in the loading phase, for example.
+ */
+ public Collection<ConfiguredTarget> getSuccessfulTargets() {
+ return successfulTargets;
+ }
+
+ /** For debugging. */
+ @Override
+ @SuppressWarnings("deprecation")
+ public String toString() {
+ // We need to be compatible with Guava, so we use this, even though it is deprecated.
+ return Objects.toStringHelper(this)
+ .add("startTimeMillis", startTimeMillis)
+ .add("stopTimeMillis", stopTimeMillis)
+ .add("crash", crash)
+ .add("catastrophe", catastrophe)
+ .add("exitCondition", exitCondition)
+ .add("actualTargets", actualTargets)
+ .add("testTargets", testTargets)
+ .add("successfulTargets", successfulTargets)
+ .toString();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/buildtool/BuildTool.java b/src/main/java/com/google/devtools/build/lib/buildtool/BuildTool.java
new file mode 100644
index 0000000..a27cc50
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/buildtool/BuildTool.java
@@ -0,0 +1,540 @@
+// Copyright 2014 Google Inc. 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.buildtool;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Stopwatch;
+import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import com.google.common.eventbus.EventBus;
+import com.google.devtools.build.lib.actions.BuildFailedException;
+import com.google.devtools.build.lib.actions.ExecutorInitException;
+import com.google.devtools.build.lib.actions.TestExecException;
+import com.google.devtools.build.lib.analysis.AnalysisPhaseCompleteEvent;
+import com.google.devtools.build.lib.analysis.BuildInfoEvent;
+import com.google.devtools.build.lib.analysis.BuildView;
+import com.google.devtools.build.lib.analysis.BuildView.AnalysisResult;
+import com.google.devtools.build.lib.analysis.ConfigurationsCreatedEvent;
+import com.google.devtools.build.lib.analysis.ConfiguredAttributeMapper;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.LicensesProvider;
+import com.google.devtools.build.lib.analysis.LicensesProvider.TargetLicense;
+import com.google.devtools.build.lib.analysis.MakeEnvironmentEvent;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget;
+import com.google.devtools.build.lib.analysis.ViewCreationFailedException;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.analysis.config.BuildConfigurationCollection;
+import com.google.devtools.build.lib.analysis.config.BuildConfigurationKey;
+import com.google.devtools.build.lib.analysis.config.BuildOptions;
+import com.google.devtools.build.lib.analysis.config.DefaultsPackage;
+import com.google.devtools.build.lib.analysis.config.InvalidConfigurationException;
+import com.google.devtools.build.lib.buildtool.BuildRequest.BuildRequestOptions;
+import com.google.devtools.build.lib.buildtool.buildevent.BuildCompleteEvent;
+import com.google.devtools.build.lib.buildtool.buildevent.BuildInterruptedEvent;
+import com.google.devtools.build.lib.buildtool.buildevent.BuildStartingEvent;
+import com.google.devtools.build.lib.buildtool.buildevent.TestFilteringCompleteEvent;
+import com.google.devtools.build.lib.cmdline.TargetParsingException;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.Reporter;
+import com.google.devtools.build.lib.packages.InputFile;
+import com.google.devtools.build.lib.packages.License;
+import com.google.devtools.build.lib.packages.License.DistributionType;
+import com.google.devtools.build.lib.packages.PackageIdentifier;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.packages.TargetUtils;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.pkgcache.LoadingFailedException;
+import com.google.devtools.build.lib.pkgcache.LoadingPhaseRunner.Callback;
+import com.google.devtools.build.lib.pkgcache.LoadingPhaseRunner.LoadingResult;
+import com.google.devtools.build.lib.profiler.ProfilePhase;
+import com.google.devtools.build.lib.profiler.Profiler;
+import com.google.devtools.build.lib.runtime.BlazeRuntime;
+import com.google.devtools.build.lib.skyframe.SkyframeExecutor;
+import com.google.devtools.build.lib.util.AbruptExitException;
+import com.google.devtools.build.lib.util.ExitCode;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Logger;
+
+/**
+ * Provides the bulk of the implementation of the 'blaze build' command.
+ *
+ * <p>The various concrete build command classes handle the command options and request
+ * setup, then delegate the handling of the request (the building of targets) to this class.
+ *
+ * <p>The main entry point is {@link #buildTargets}.
+ *
+ * <p>This class is always instantiated and managed as a singleton, being constructed and held by
+ * {@link BlazeRuntime}. This is so multiple kinds of build commands can share this single
+ * instance.
+ *
+ * <p>Most of analysis is handled in {@link BuildView}, and execution in {@link ExecutionTool}.
+ */
+public class BuildTool {
+
+ private static final Logger LOG = Logger.getLogger(BuildTool.class.getName());
+
+ protected final BlazeRuntime runtime;
+
+ /**
+ * Constructs a BuildTool.
+ *
+ * @param runtime a reference to the blaze runtime.
+ */
+ public BuildTool(BlazeRuntime runtime) {
+ this.runtime = runtime;
+ }
+
+ /**
+ * The crux of the build system. Builds the targets specified in the request using the specified
+ * Executor.
+ *
+ * <p>Performs loading, analysis and execution for the specified set of targets, honoring the
+ * configuration options in the BuildRequest. Returns normally iff successful, throws an exception
+ * otherwise.
+ *
+ * <p>Callers must ensure that {@link #stopRequest} is called after this method, even if it
+ * throws.
+ *
+ * <p>The caller is responsible for setting up and syncing the package cache.
+ *
+ * <p>During this function's execution, the actualTargets and successfulTargets
+ * fields of the request object are set.
+ *
+ * @param request the build request that this build tool is servicing, which specifies various
+ * options; during this method's execution, the actualTargets and successfulTargets fields
+ * of the request object are populated
+ * @param result the build result that is the mutable result of this build
+ * @param validator target validator
+ */
+ public void buildTargets(BuildRequest request, BuildResult result, TargetValidator validator)
+ throws BuildFailedException, LocalEnvironmentException,
+ InterruptedException, ViewCreationFailedException,
+ TargetParsingException, LoadingFailedException, ExecutorInitException,
+ AbruptExitException, InvalidConfigurationException, TestExecException {
+ validateOptions(request);
+ BuildOptions buildOptions = runtime.createBuildOptions(request);
+ // Sync the package manager before sending the BuildStartingEvent in runLoadingPhase()
+ runtime.setupPackageCache(request.getPackageCacheOptions(),
+ DefaultsPackage.getDefaultsPackageContent(buildOptions));
+
+ ExecutionTool executionTool = null;
+ LoadingResult loadingResult = null;
+ BuildConfigurationCollection configurations = null;
+ try {
+ getEventBus().post(new BuildStartingEvent(runtime.getOutputFileSystem(), request));
+ LOG.info("Build identifier: " + request.getId());
+ executionTool = new ExecutionTool(runtime, request);
+ if (needsExecutionPhase(request.getBuildOptions())) {
+ // Initialize the execution tool early if we need it. This hides the latency of setting up
+ // the execution backends.
+ executionTool.init();
+ }
+
+ // Loading phase.
+ loadingResult = runLoadingPhase(request, validator);
+
+ // Create the build configurations.
+ if (!request.getMultiCpus().isEmpty()) {
+ getReporter().handle(Event.warn(
+ "The --experimental_multi_cpu option is _very_ experimental and only intended for "
+ + "internal testing at this time. If you do not work on the build tool, then you "
+ + "should stop now!"));
+ if (!"build".equals(request.getCommandName()) && !"test".equals(request.getCommandName())) {
+ throw new InvalidConfigurationException(
+ "The experimental setting to select multiple CPUs is only supported for 'build' and "
+ + "'test' right now!");
+ }
+ }
+ configurations = getConfigurations(
+ runtime.getBuildConfigurationKey(buildOptions, request.getMultiCpus()),
+ request.getViewOptions().keepGoing);
+
+ getEventBus().post(new ConfigurationsCreatedEvent(configurations));
+ runtime.throwPendingException();
+ if (configurations.getTargetConfigurations().size() == 1) {
+ // TODO(bazel-team): This is not optimal - we retain backwards compatibility in the case
+ // where there's only a single configuration, but we don't send an event in the multi-config
+ // case. Can we do better? [multi-config]
+ getEventBus().post(new MakeEnvironmentEvent(
+ configurations.getTargetConfigurations().get(0).getMakeEnvironment()));
+ }
+ LOG.info("Configurations created");
+
+ // Analysis phase.
+ AnalysisResult analysisResult = runAnalysisPhase(request, loadingResult, configurations);
+ result.setActualTargets(analysisResult.getTargetsToBuild());
+ result.setTestTargets(analysisResult.getTargetsToTest());
+
+ reportTargets(analysisResult);
+
+ // Execution phase.
+ if (needsExecutionPhase(request.getBuildOptions())) {
+ executionTool.executeBuild(analysisResult, result, runtime.getSkyframeExecutor(),
+ configurations, mergePackageRoots(loadingResult.getPackageRoots(),
+ runtime.getSkyframeExecutor().getPackageRoots()));
+ }
+
+ String delayedErrorMsg = analysisResult.getError();
+ if (delayedErrorMsg != null) {
+ throw new BuildFailedException(delayedErrorMsg);
+ }
+ } catch (RuntimeException e) {
+ // Print an error message for unchecked runtime exceptions. This does not concern Error
+ // subclasses such as OutOfMemoryError.
+ request.getOutErr().printErrLn("Unhandled exception thrown during build; message: " +
+ e.getMessage());
+ throw e;
+ } finally {
+ // Delete dirty nodes to ensure that they do not accumulate indefinitely.
+ long versionWindow = request.getViewOptions().versionWindowForDirtyNodeGc;
+ if (versionWindow != -1) {
+ runtime.getSkyframeExecutor().deleteOldNodes(versionWindow);
+ }
+
+ if (executionTool != null) {
+ executionTool.shutdown();
+ }
+ // The workspace status actions will not run with certain flags, or if an error
+ // occurs early in the build. Tell a lie so that the event is not missing.
+ // If multiple build_info events are sent, only the first is kept, so this does not harm
+ // successful runs (which use the workspace status action).
+ getEventBus().post(new BuildInfoEvent(
+ runtime.getworkspaceStatusActionFactory().createDummyWorkspaceStatus()));
+ }
+
+ if (loadingResult != null && loadingResult.hasTargetPatternError()) {
+ throw new BuildFailedException("execution phase successful, but there were errors " +
+ "parsing the target pattern");
+ }
+ }
+
+ private ImmutableMap<PathFragment, Path> mergePackageRoots(
+ ImmutableMap<PackageIdentifier, Path> first,
+ ImmutableMap<PackageIdentifier, Path> second) {
+ Map<PathFragment, Path> builder = Maps.newHashMap();
+ for (Map.Entry<PackageIdentifier, Path> entry : first.entrySet()) {
+ builder.put(entry.getKey().getPackageFragment(), entry.getValue());
+ }
+ for (Map.Entry<PackageIdentifier, Path> entry : second.entrySet()) {
+ if (first.containsKey(entry.getKey())) {
+ Preconditions.checkState(first.get(entry.getKey()).equals(entry.getValue()));
+ } else {
+ // This could overwrite entries from first in other repositories.
+ builder.put(entry.getKey().getPackageFragment(), entry.getValue());
+ }
+ }
+ return ImmutableMap.copyOf(builder);
+ }
+
+ private void reportExceptionError(Exception e) {
+ if (e.getMessage() != null) {
+ getReporter().handle(Event.error(e.getMessage()));
+ }
+ }
+ /**
+ * The crux of the build system. Builds the targets specified in the request using the specified
+ * Executor.
+ *
+ * <p>Performs loading, analysis and execution for the specified set of targets, honoring the
+ * configuration options in the BuildRequest. Returns normally iff successful, throws an exception
+ * otherwise.
+ *
+ * <p>The caller is responsible for setting up and syncing the package cache.
+ *
+ * <p>During this function's execution, the actualTargets and successfulTargets
+ * fields of the request object are set.
+ *
+ * @param request the build request that this build tool is servicing, which specifies various
+ * options; during this method's execution, the actualTargets and successfulTargets fields
+ * of the request object are populated
+ * @param validator target validator
+ * @return the result as a {@link BuildResult} object
+ */
+ public BuildResult processRequest(BuildRequest request, TargetValidator validator) {
+ BuildResult result = new BuildResult(request.getStartTime());
+ runtime.getEventBus().register(result);
+ Throwable catastrophe = null;
+ ExitCode exitCode = ExitCode.BLAZE_INTERNAL_ERROR;
+ try {
+ buildTargets(request, result, validator);
+ exitCode = ExitCode.SUCCESS;
+ } catch (BuildFailedException e) {
+ if (e.isErrorAlreadyShown()) {
+ // The actual error has already been reported by the Builder.
+ } else {
+ reportExceptionError(e);
+ }
+ if (e.isCatastrophic()) {
+ result.setCatastrophe();
+ }
+ exitCode = ExitCode.BUILD_FAILURE;
+ } catch (InterruptedException e) {
+ exitCode = ExitCode.INTERRUPTED;
+ getReporter().handle(Event.error("build interrupted"));
+ getEventBus().post(new BuildInterruptedEvent());
+ } catch (TargetParsingException | LoadingFailedException | ViewCreationFailedException e) {
+ exitCode = ExitCode.PARSING_FAILURE;
+ reportExceptionError(e);
+ } catch (TestExecException e) {
+ // ExitCode.SUCCESS means that build was successful. Real return code of program
+ // is going to be calculated in TestCommand.doTest().
+ exitCode = ExitCode.SUCCESS;
+ reportExceptionError(e);
+ } catch (InvalidConfigurationException e) {
+ exitCode = ExitCode.COMMAND_LINE_ERROR;
+ reportExceptionError(e);
+ } catch (AbruptExitException e) {
+ exitCode = e.getExitCode();
+ reportExceptionError(e);
+ result.setCatastrophe();
+ } catch (Throwable throwable) {
+ catastrophe = throwable;
+ Throwables.propagate(throwable);
+ } finally {
+ stopRequest(request, result, catastrophe, exitCode);
+ }
+
+ return result;
+ }
+
+ protected final BuildConfigurationCollection getConfigurations(BuildConfigurationKey key,
+ boolean keepGoing)
+ throws InvalidConfigurationException, InterruptedException {
+ SkyframeExecutor executor = runtime.getSkyframeExecutor();
+ // TODO(bazel-team): consider a possibility of moving ConfigurationFactory construction into
+ // skyframe.
+ return executor.createConfigurations(keepGoing, runtime.getConfigurationFactory(), key);
+ }
+
+ @VisibleForTesting
+ protected final LoadingResult runLoadingPhase(final BuildRequest request,
+ final TargetValidator validator)
+ throws LoadingFailedException, TargetParsingException, InterruptedException,
+ AbruptExitException {
+ Profiler.instance().markPhase(ProfilePhase.LOAD);
+ runtime.throwPendingException();
+
+ final boolean keepGoing = request.getViewOptions().keepGoing;
+
+ Callback callback = new Callback() {
+ @Override
+ public void notifyTargets(Collection<Target> targets) throws LoadingFailedException {
+ if (validator != null) {
+ validator.validateTargets(targets, keepGoing);
+ }
+ }
+
+ @Override
+ public void notifyVisitedPackages(Set<PackageIdentifier> visitedPackages) {
+ runtime.getSkyframeExecutor().updateLoadedPackageSet(visitedPackages);
+ }
+ };
+
+ LoadingResult result = runtime.getLoadingPhaseRunner().execute(getReporter(),
+ getEventBus(), request.getTargets(), request.getLoadingOptions(),
+ runtime.createBuildOptions(request).getAllLabels(), keepGoing,
+ request.shouldRunTests(), callback);
+ runtime.throwPendingException();
+ return result;
+ }
+
+ /**
+ * Performs the initial phases 0-2 of the build: Setup, Loading and Analysis.
+ * <p>
+ * Postcondition: On success, populates the BuildRequest's set of targets to
+ * build.
+ *
+ * @return null if loading / analysis phases were successful; a useful error
+ * message if loading or analysis phase errors were encountered and
+ * request.keepGoing.
+ * @throws InterruptedException if the current thread was interrupted.
+ * @throws ViewCreationFailedException if analysis failed for any reason.
+ */
+ private AnalysisResult runAnalysisPhase(BuildRequest request, LoadingResult loadingResult,
+ BuildConfigurationCollection configurations)
+ throws InterruptedException, ViewCreationFailedException {
+ Stopwatch timer = Stopwatch.createStarted();
+ if (!request.getBuildOptions().performAnalysisPhase) {
+ getReporter().handle(Event.progress("Loading complete."));
+ LOG.info("No analysis requested, so finished");
+ return AnalysisResult.EMPTY;
+ }
+
+ getReporter().handle(Event.progress("Loading complete. Analyzing..."));
+ Profiler.instance().markPhase(ProfilePhase.ANALYZE);
+
+ AnalysisResult analysisResult = getView().update(loadingResult, configurations,
+ request.getViewOptions(), request.getTopLevelArtifactContext(), getReporter(),
+ getEventBus());
+
+ // TODO(bazel-team): Merge these into one event.
+ getEventBus().post(new AnalysisPhaseCompleteEvent(analysisResult.getTargetsToBuild(),
+ getView().getTargetsVisited(), timer.stop().elapsed(TimeUnit.MILLISECONDS)));
+ getEventBus().post(new TestFilteringCompleteEvent(analysisResult.getTargetsToBuild(),
+ analysisResult.getTargetsToTest()));
+
+ // Check licenses.
+ // We check licenses if the first target configuration has license checking enabled. Right now,
+ // it is not possible to have multiple target configurations with different settings for this
+ // flag, which allows us to take this short cut.
+ boolean checkLicenses = configurations.getTargetConfigurations().get(0).checkLicenses();
+ if (checkLicenses) {
+ Profiler.instance().markPhase(ProfilePhase.LICENSE);
+ validateLicensingForTargets(analysisResult.getTargetsToBuild(),
+ request.getViewOptions().keepGoing);
+ }
+
+ return analysisResult;
+ }
+
+ private static boolean needsExecutionPhase(BuildRequestOptions options) {
+ return options.performAnalysisPhase && options.performExecutionPhase;
+ }
+
+ /**
+ * Stops processing the specified request.
+ *
+ * <p>This logs the build result, cleans up and stops the clock.
+ *
+ * @param request the build request that this build tool is servicing
+ * @param crash Any unexpected RuntimeException or Error. May be null
+ * @param exitCondition A suggested exit condition from either the build logic or
+ * a thrown exception somewhere along the way.
+ */
+ public void stopRequest(BuildRequest request, BuildResult result, Throwable crash,
+ ExitCode exitCondition) {
+ Preconditions.checkState((crash == null) || (exitCondition != ExitCode.SUCCESS));
+ result.setUnhandledThrowable(crash);
+ result.setExitCondition(exitCondition);
+ // The stop time has to be captured before we send the BuildCompleteEvent.
+ result.setStopTime(runtime.getClock().currentTimeMillis());
+ getEventBus().post(new BuildCompleteEvent(request, result));
+ }
+
+ private void reportTargets(AnalysisResult analysisResult) {
+ Collection<ConfiguredTarget> targetsToBuild = analysisResult.getTargetsToBuild();
+ Collection<ConfiguredTarget> targetsToTest = analysisResult.getTargetsToTest();
+ if (targetsToTest != null) {
+ int testCount = targetsToTest.size();
+ int targetCount = targetsToBuild.size() - testCount;
+ if (targetCount == 0) {
+ getReporter().handle(Event.info("Found "
+ + testCount + (testCount == 1 ? " test target..." : " test targets...")));
+ } else {
+ getReporter().handle(Event.info("Found "
+ + targetCount + (targetCount == 1 ? " target and " : " targets and ")
+ + testCount + (testCount == 1 ? " test target..." : " test targets...")));
+ }
+ } else {
+ int targetCount = targetsToBuild.size();
+ getReporter().handle(Event.info("Found "
+ + targetCount + (targetCount == 1 ? " target..." : " targets...")));
+ }
+ }
+
+ /**
+ * Validates the options for this BuildRequest.
+ *
+ * <p>Issues warnings for the use of deprecated options, and warnings or errors for any option
+ * settings that conflict.
+ */
+ @VisibleForTesting
+ public void validateOptions(BuildRequest request) throws InvalidConfigurationException {
+ for (String issue : request.validateOptions()) {
+ getReporter().handle(Event.warn(issue));
+ }
+ }
+
+ /**
+ * Takes a set of configured targets, and checks if the distribution methods
+ * declared for the targets are compatible with the constraints imposed by
+ * their prerequisites' licenses.
+ *
+ * @param configuredTargets the targets to check
+ * @param keepGoing if false, and a licensing error is encountered, both
+ * generates an error message on the reporter, <em>and</em> throws an
+ * exception. If true, then just generates a message on the reporter.
+ * @throws ViewCreationFailedException if the license checking failed (and not
+ * --keep_going)
+ */
+ private void validateLicensingForTargets(Iterable<ConfiguredTarget> configuredTargets,
+ boolean keepGoing) throws ViewCreationFailedException {
+ for (ConfiguredTarget configuredTarget : configuredTargets) {
+ final Target target = configuredTarget.getTarget();
+
+ if (TargetUtils.isTestRule(target)) {
+ continue; // Tests are exempt from license checking
+ }
+
+ final Set<DistributionType> distribs = target.getDistributions();
+ BuildConfiguration config = configuredTarget.getConfiguration();
+ boolean staticallyLinked = (config != null) && config.performsStaticLink();
+ staticallyLinked |= (config != null) && (target instanceof Rule)
+ && ((Rule) target).getRuleClassObject().hasAttr("linkopts", Type.STRING_LIST)
+ && ConfiguredAttributeMapper.of((RuleConfiguredTarget) configuredTarget)
+ .get("linkopts", Type.STRING_LIST).contains("-static");
+
+ LicensesProvider provider = configuredTarget.getProvider(LicensesProvider.class);
+ if (provider != null) {
+ NestedSet<TargetLicense> licenses = provider.getTransitiveLicenses();
+ for (TargetLicense targetLicense : licenses) {
+ if (!targetLicense.getLicense().checkCompatibility(
+ distribs, target, targetLicense.getLabel(), getReporter(), staticallyLinked)) {
+ if (!keepGoing) {
+ throw new ViewCreationFailedException("Build aborted due to licensing error");
+ }
+ }
+ }
+ } else if (configuredTarget.getTarget() instanceof InputFile) {
+ // Input file targets do not provide licenses because they do not
+ // depend on the rule where their license is taken from. This is usually
+ // not a problem, because the transitive collection of licenses always
+ // hits the rule they come from, except when the input file is a
+ // top-level target. Thus, we need to handle that case specially here.
+ //
+ // See FileTarget#getLicense for more information about the handling of
+ // license issues with File targets.
+ License license = configuredTarget.getTarget().getLicense();
+ if (!license.checkCompatibility(distribs, target, configuredTarget.getLabel(),
+ getReporter(), staticallyLinked)) {
+ if (!keepGoing) {
+ throw new ViewCreationFailedException("Build aborted due to licensing error");
+ }
+ }
+ }
+ }
+ }
+
+ public BuildView getView() {
+ return runtime.getView();
+ }
+
+ private Reporter getReporter() {
+ return runtime.getReporter();
+ }
+
+ private EventBus getEventBus() {
+ return runtime.getEventBus();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/buildtool/CachesSavedEvent.java b/src/main/java/com/google/devtools/build/lib/buildtool/CachesSavedEvent.java
new file mode 100644
index 0000000..5b3229c
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/buildtool/CachesSavedEvent.java
@@ -0,0 +1,39 @@
+// Copyright 2014 Google Inc. 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.buildtool;
+
+/**
+ * Event that is raised when the action and artifact metadata caches are saved at the end of the
+ * build. Contains statistics.
+ */
+public class CachesSavedEvent {
+ /** Cache serialization statistics. */
+ private final long actionCacheSaveTimeInMillis;
+ private final long actionCacheSizeInBytes;
+
+ public CachesSavedEvent(
+ long actionCacheSaveTimeInMillis,
+ long actionCacheSizeInBytes) {
+ this.actionCacheSaveTimeInMillis = actionCacheSaveTimeInMillis;
+ this.actionCacheSizeInBytes = actionCacheSizeInBytes;
+ }
+
+ public long getActionCacheSaveTimeInMillis() {
+ return actionCacheSaveTimeInMillis;
+ }
+
+ public long getActionCacheSizeInBytes() {
+ return actionCacheSizeInBytes;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/buildtool/ExecutionFinishedEvent.java b/src/main/java/com/google/devtools/build/lib/buildtool/ExecutionFinishedEvent.java
new file mode 100644
index 0000000..74143cc
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/buildtool/ExecutionFinishedEvent.java
@@ -0,0 +1,49 @@
+// Copyright 2014 Google Inc. 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.buildtool;
+
+import com.google.common.collect.ImmutableMap;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Event signaling the end of the execution phase. Contains statistics about the action cache,
+ * the metadata cache and about last file save times.
+ */
+public class ExecutionFinishedEvent {
+ /** The mtime of the most recently saved source file when the build starts. */
+ private long lastFileSaveTimeInMillis;
+
+ /**
+ * The (filename, mtime) pairs of all files saved between the last build's
+ * start time and the current build's start time. Only applies to builds
+ * running with existing Blaze servers. Currently disabled.
+ */
+ private Map<String, Long> changedFileSaveTimes = new HashMap<>();
+
+ public ExecutionFinishedEvent(Map<String, Long> changedFileSaveTimes,
+ long lastFileSaveTimeInMillis) {
+ this.changedFileSaveTimes = ImmutableMap.copyOf(changedFileSaveTimes);
+ this.lastFileSaveTimeInMillis = lastFileSaveTimeInMillis;
+ }
+
+ public long getLastFileSaveTimeInMillis() {
+ return lastFileSaveTimeInMillis;
+ }
+
+ public Map<String, Long> getChangedFileSaveTimes() {
+ return changedFileSaveTimes;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/buildtool/ExecutionTool.java b/src/main/java/com/google/devtools/build/lib/buildtool/ExecutionTool.java
new file mode 100644
index 0000000..771cfe6
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/buildtool/ExecutionTool.java
@@ -0,0 +1,875 @@
+// Copyright 2014 Google Inc. 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.buildtool;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.base.Stopwatch;
+import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Ordering;
+import com.google.common.collect.Table;
+import com.google.common.eventbus.EventBus;
+import com.google.devtools.build.lib.Constants;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.actions.ActionCacheChecker;
+import com.google.devtools.build.lib.actions.ActionContextConsumer;
+import com.google.devtools.build.lib.actions.ActionContextMarker;
+import com.google.devtools.build.lib.actions.ActionContextProvider;
+import com.google.devtools.build.lib.actions.ActionGraph;
+import com.google.devtools.build.lib.actions.ActionInputFileCache;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.BlazeExecutor;
+import com.google.devtools.build.lib.actions.BuildFailedException;
+import com.google.devtools.build.lib.actions.ExecException;
+import com.google.devtools.build.lib.actions.ExecutionStrategy;
+import com.google.devtools.build.lib.actions.Executor;
+import com.google.devtools.build.lib.actions.Executor.ActionContext;
+import com.google.devtools.build.lib.actions.ExecutorInitException;
+import com.google.devtools.build.lib.actions.LocalHostCapacity;
+import com.google.devtools.build.lib.actions.ResourceManager;
+import com.google.devtools.build.lib.actions.SpawnActionContext;
+import com.google.devtools.build.lib.actions.TestExecException;
+import com.google.devtools.build.lib.actions.cache.ActionCache;
+import com.google.devtools.build.lib.analysis.BuildView;
+import com.google.devtools.build.lib.analysis.BuildView.AnalysisResult;
+import com.google.devtools.build.lib.analysis.CompilationPrerequisitesProvider;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.FileProvider;
+import com.google.devtools.build.lib.analysis.FilesToCompileProvider;
+import com.google.devtools.build.lib.analysis.InputFileConfiguredTarget;
+import com.google.devtools.build.lib.analysis.OutputFileConfiguredTarget;
+import com.google.devtools.build.lib.analysis.TempsProvider;
+import com.google.devtools.build.lib.analysis.TopLevelArtifactHelper;
+import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
+import com.google.devtools.build.lib.analysis.ViewCreationFailedException;
+import com.google.devtools.build.lib.analysis.WorkspaceStatusAction;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.analysis.config.BuildConfigurationCollection;
+import com.google.devtools.build.lib.buildtool.buildevent.ExecutionPhaseCompleteEvent;
+import com.google.devtools.build.lib.buildtool.buildevent.ExecutionStartingEvent;
+import com.google.devtools.build.lib.collect.CollectionUtils;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.events.EventKind;
+import com.google.devtools.build.lib.events.Reporter;
+import com.google.devtools.build.lib.exec.CheckUpToDateFilter;
+import com.google.devtools.build.lib.exec.ExecutionOptions;
+import com.google.devtools.build.lib.exec.OutputService;
+import com.google.devtools.build.lib.exec.SingleBuildFileCache;
+import com.google.devtools.build.lib.exec.SourceManifestActionContextImpl;
+import com.google.devtools.build.lib.exec.SymlinkTreeStrategy;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.profiler.ProfilePhase;
+import com.google.devtools.build.lib.profiler.Profiler;
+import com.google.devtools.build.lib.profiler.ProfilerTask;
+import com.google.devtools.build.lib.rules.fileset.FilesetActionContext;
+import com.google.devtools.build.lib.rules.fileset.FilesetActionContextImpl;
+import com.google.devtools.build.lib.rules.test.TestActionContext;
+import com.google.devtools.build.lib.runtime.BlazeModule;
+import com.google.devtools.build.lib.runtime.BlazeRuntime;
+import com.google.devtools.build.lib.skyframe.Builder;
+import com.google.devtools.build.lib.skyframe.SkyframeExecutor;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.util.AbruptExitException;
+import com.google.devtools.build.lib.util.BlazeClock;
+import com.google.devtools.build.lib.util.ExitCode;
+import com.google.devtools.build.lib.util.LoggingUtil;
+import com.google.devtools.build.lib.util.io.OutErr;
+import com.google.devtools.build.lib.vfs.FileSystem;
+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.OutputStream;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.annotation.Nullable;
+
+/**
+ * This class manages the execution phase. The entry point is {@link #executeBuild}.
+ *
+ * <p>This is only intended for use by {@link BuildTool}.
+ *
+ * <p>This class contains an ActionCache, and refers to the BlazeRuntime's BuildView and
+ * PackageCache.
+ *
+ * @see BuildTool
+ * @see BuildView
+ */
+public class ExecutionTool {
+ private static class StrategyConverter {
+ private Table<Class<? extends ActionContext>, String, ActionContext> classMap =
+ HashBasedTable.create();
+ private Map<Class<? extends ActionContext>, ActionContext> defaultClassMap =
+ new HashMap<>();
+
+ /**
+ * Aggregates all {@link ActionContext}s that are in {@code contextProviders}.
+ */
+ @SuppressWarnings("unchecked")
+ private StrategyConverter(Iterable<ActionContextProvider> contextProviders) {
+ for (ActionContextProvider provider : contextProviders) {
+ for (ActionContext strategy : provider.getActionContexts()) {
+ ExecutionStrategy annotation =
+ strategy.getClass().getAnnotation(ExecutionStrategy.class);
+ if (annotation != null) {
+ defaultClassMap.put(annotation.contextType(), strategy);
+
+ for (String name : annotation.name()) {
+ classMap.put(annotation.contextType(), name, strategy);
+ }
+ }
+ }
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private <T extends ActionContext> T getStrategy(Class<T> clazz, String name) {
+ return (T) (name.isEmpty() ? defaultClassMap.get(clazz) : classMap.get(clazz, name));
+ }
+
+ private String getValidValues(Class<? extends ActionContext> context) {
+ return Joiner.on(", ").join(Ordering.natural().sortedCopy(classMap.row(context).keySet()));
+ }
+
+ private String getUserFriendlyName(Class<? extends ActionContext> context) {
+ ActionContextMarker marker = context.getAnnotation(ActionContextMarker.class);
+ return marker != null
+ ? marker.name()
+ : context.getSimpleName();
+ }
+ }
+
+ static final Logger LOG = Logger.getLogger(ExecutionTool.class.getName());
+
+ private final BlazeRuntime runtime;
+ private final BuildRequest request;
+ private BlazeExecutor executor;
+ private ActionInputFileCache fileCache;
+ private List<ActionContextProvider> actionContextProviders;
+
+ private Map<String, ActionContext> spawnStrategyMap = new HashMap<>();
+ private List<ActionContext> strategies = new ArrayList<>();
+
+ ExecutionTool(BlazeRuntime runtime, BuildRequest request) throws ExecutorInitException {
+ this.runtime = runtime;
+ this.request = request;
+
+ List<ActionContextConsumer> actionContextConsumers = new ArrayList<>();
+ actionContextProviders = new ArrayList<>();
+ for (BlazeModule module : runtime.getBlazeModules()) {
+ ActionContextProvider provider = module.getActionContextProvider();
+ if (provider != null) {
+ actionContextProviders.add(provider);
+ }
+
+ ActionContextConsumer consumer = module.getActionContextConsumer();
+ if (consumer != null) {
+ actionContextConsumers.add(consumer);
+ }
+ }
+
+ actionContextProviders.add(new FilesetActionContextImpl.Provider(
+ runtime.getReporter(), runtime.getWorkspaceName()));
+
+ strategies.add(new SourceManifestActionContextImpl(runtime.getRunfilesPrefix()));
+ strategies.add(new SymlinkTreeStrategy(runtime.getOutputService(), runtime.getBinTools()));
+
+ StrategyConverter strategyConverter = new StrategyConverter(actionContextProviders);
+ strategies.add(strategyConverter.getStrategy(FilesetActionContext.class, ""));
+ strategies.add(strategyConverter.getStrategy(WorkspaceStatusAction.Context.class, ""));
+
+ for (ActionContextConsumer consumer : actionContextConsumers) {
+ // There are many different SpawnActions, and we want to control the action context they use
+ // independently from each other, for example, to run genrules locally and Java compile action
+ // in prod. Thus, for SpawnActions, we decide the action context to use not only based on the
+ // context class, but also the mnemonic of the action.
+ for (Map.Entry<String, String> entry : consumer.getSpawnActionContexts().entrySet()) {
+ SpawnActionContext context =
+ strategyConverter.getStrategy(SpawnActionContext.class, entry.getValue());
+ if (context == null) {
+ throw makeExceptionForInvalidStrategyValue(entry.getValue(), "spawn",
+ strategyConverter.getValidValues(SpawnActionContext.class));
+ }
+
+ spawnStrategyMap.put(entry.getKey(), context);
+ }
+
+ for (Map.Entry<Class<? extends ActionContext>, String> entry :
+ consumer.getActionContexts().entrySet()) {
+ ActionContext context = strategyConverter.getStrategy(entry.getKey(), entry.getValue());
+ if (context != null) {
+ strategies.add(context);
+ } else if (!entry.getValue().isEmpty()) {
+ // If the action context consumer requested the default value (by passing in the empty
+ // string), we do not throw the exception, because we assume that whoever put together
+ // the modules in this Blaze binary knew what they were doing.
+ throw makeExceptionForInvalidStrategyValue(entry.getValue(),
+ strategyConverter.getUserFriendlyName(entry.getKey()),
+ strategyConverter.getValidValues(entry.getKey()));
+ }
+ }
+ }
+
+ // If tests are to be run during build, too, we have to explicitly load the test action context.
+ if (request.shouldRunTests()) {
+ String testStrategyValue = request.getOptions(ExecutionOptions.class).testStrategy;
+ ActionContext context = strategyConverter.getStrategy(TestActionContext.class,
+ testStrategyValue);
+ if (context == null) {
+ throw makeExceptionForInvalidStrategyValue(testStrategyValue, "test",
+ strategyConverter.getValidValues(TestActionContext.class));
+ }
+ strategies.add(context);
+ }
+ }
+
+ private static ExecutorInitException makeExceptionForInvalidStrategyValue(String value,
+ String strategy, String validValues) {
+ return new ExecutorInitException(String.format(
+ "'%s' is an invalid value for %s strategy. Valid values are: %s", value, strategy,
+ validValues), ExitCode.COMMAND_LINE_ERROR);
+ }
+
+ Executor getExecutor() throws ExecutorInitException {
+ if (executor == null) {
+ executor = createExecutor();
+ }
+ return executor;
+ }
+
+ /**
+ * Creates an executor for the current set of blaze runtime, execution options, and request.
+ */
+ private BlazeExecutor createExecutor()
+ throws ExecutorInitException {
+ return new BlazeExecutor(
+ runtime.getDirectories().getExecRoot(),
+ runtime.getDirectories().getOutputPath(),
+ getReporter(),
+ getEventBus(),
+ runtime.getClock(),
+ request,
+ request.getOptions(ExecutionOptions.class).verboseFailures,
+ request.getOptions(ExecutionOptions.class).showSubcommands,
+ strategies,
+ spawnStrategyMap,
+ actionContextProviders);
+ }
+
+ void init() throws ExecutorInitException {
+ createToolsSymlinks();
+ getExecutor();
+ }
+
+ void shutdown() {
+ for (ActionContextProvider actionContextProvider : actionContextProviders) {
+ actionContextProvider.executionPhaseEnding();
+ }
+ }
+
+ /**
+ * Performs the execution phase (phase 3) of the build, in which the Builder
+ * is applied to the action graph to bring the targets up to date. (This
+ * function will return prior to execution-proper if --nobuild was specified.)
+ *
+ * @param analysisResult the analysis phase output
+ * @param buildResult the mutable build result
+ * @param skyframeExecutor the skyframe executor (if any)
+ * @param packageRoots package roots collected from loading phase and BuildConfigutaionCollection
+ * creation
+ */
+ void executeBuild(AnalysisResult analysisResult,
+ BuildResult buildResult, @Nullable SkyframeExecutor skyframeExecutor,
+ BuildConfigurationCollection configurations,
+ ImmutableMap<PathFragment, Path> packageRoots)
+ throws BuildFailedException, InterruptedException, AbruptExitException, TestExecException,
+ ViewCreationFailedException {
+ Stopwatch timer = Stopwatch.createStarted();
+ prepare(packageRoots, configurations);
+
+ ActionGraph actionGraph = analysisResult.getActionGraph();
+
+ // Get top-level artifacts.
+ ImmutableSet<Artifact> additionalArtifacts = analysisResult.getAdditionalArtifactsToBuild();
+
+ // If --nobuild is specified, this request completes successfully without
+ // execution.
+ if (!request.getBuildOptions().performExecutionPhase) {
+ return;
+ }
+
+ // Create symlinks only after we've verified that we're actually
+ // supposed to build something.
+ if (getWorkspace().getFileSystem().supportsSymbolicLinks()) {
+ List<BuildConfiguration> targetConfigurations =
+ getView().getConfigurationCollection().getTargetConfigurations();
+ // TODO(bazel-team): This is not optimal - we retain backwards compatibility in the case where
+ // there's only a single configuration, but we don't create any symlinks in the multi-config
+ // case. Can we do better? [multi-config]
+ if (targetConfigurations.size() == 1) {
+ OutputDirectoryLinksUtils.createOutputDirectoryLinks(
+ runtime.getWorkspaceName(), getWorkspace(), getExecRoot(),
+ runtime.getOutputPath(), getReporter(), targetConfigurations.get(0),
+ request.getSymlinkPrefix());
+ }
+ }
+
+ OutputService outputService = runtime.getOutputService();
+ if (outputService != null) {
+ outputService.startBuild();
+ } else {
+ startLocalOutputBuild(); // TODO(bazel-team): this could be just another OutputService
+ }
+
+ ActionCache actionCache = getActionCache();
+ Builder builder = createBuilder(request, executor, actionCache, skyframeExecutor);
+
+ //
+ // Execution proper. All statements below are logically nested in
+ // begin/end pairs. No early returns or exceptions please!
+ //
+
+ Collection<ConfiguredTarget> configuredTargets = buildResult.getActualTargets();
+ getEventBus().post(new ExecutionStartingEvent(configuredTargets));
+
+ getReporter().handle(Event.progress("Building..."));
+
+ // Conditionally record dependency-checker log:
+ ExplanationHandler explanationHandler =
+ installExplanationHandler(request.getBuildOptions().explanationPath,
+ request.getOptionsDescription());
+
+ Set<ConfiguredTarget> builtTargets = new HashSet<>();
+ boolean interrupted = false;
+ try {
+ Iterable<Artifact> allArtifactsForProviders = Iterables.concat(additionalArtifacts,
+ TopLevelArtifactHelper.getAllArtifactsToBuild(
+ analysisResult.getTargetsToBuild(), analysisResult.getTopLevelContext()),
+ TopLevelArtifactHelper.getAllArtifactsToTest(analysisResult.getTargetsToTest()));
+ if (request.isRunningInEmacs()) {
+ // The syntax of this message is tightly constrained by lisp/progmodes/compile.el in emacs
+ request.getOutErr().printErrLn("blaze: Entering directory `" + getExecRoot() + "/'");
+ }
+ for (ActionContextProvider actionContextProvider : actionContextProviders) {
+ actionContextProvider.executionPhaseStarting(
+ fileCache,
+ actionGraph,
+ allArtifactsForProviders);
+ }
+ executor.executionPhaseStarting();
+ skyframeExecutor.drainChangedFiles();
+
+ if (request.getViewOptions().discardAnalysisCache) {
+ // Free memory by removing cache entries that aren't going to be needed. Note that in
+ // skyframe full, this destroys the action graph as well, so we can only do it after the
+ // action graph is no longer needed.
+ getView().clearAnalysisCache(analysisResult.getTargetsToBuild());
+ actionGraph = null;
+ }
+
+ configureResourceManager(request);
+
+ Profiler.instance().markPhase(ProfilePhase.EXECUTE);
+
+ builder.buildArtifacts(additionalArtifacts,
+ analysisResult.getParallelTests(),
+ analysisResult.getExclusiveTests(),
+ analysisResult.getTargetsToBuild(),
+ executor, builtTargets,
+ request.getBuildOptions().explanationPath != null);
+
+ } catch (InterruptedException e) {
+ interrupted = true;
+ throw e;
+ } finally {
+ if (request.isRunningInEmacs()) {
+ request.getOutErr().printErrLn("blaze: Leaving directory `" + getExecRoot() + "/'");
+ }
+ if (!interrupted) {
+ getReporter().handle(Event.progress("Building complete."));
+ }
+
+ // Transfer over source file "last save time" stats so the remote logger can find them.
+ runtime.getEventBus().post(new ExecutionFinishedEvent(ImmutableMap.<String, Long> of(), 0));
+
+ // Disable system load polling (noop if it was not enabled).
+ ResourceManager.instance().setAutoSensing(false);
+ executor.executionPhaseEnding();
+ for (ActionContextProvider actionContextProvider : actionContextProviders) {
+ actionContextProvider.executionPhaseEnding();
+ }
+
+ Profiler.instance().markPhase(ProfilePhase.FINISH);
+
+ if (!interrupted) {
+ saveCaches(actionCache);
+ }
+
+ long startTime = Profiler.nanoTimeMaybe();
+ determineSuccessfulTargets(buildResult, configuredTargets, builtTargets, timer);
+ showBuildResult(request, buildResult, configuredTargets);
+ Preconditions.checkNotNull(buildResult.getSuccessfulTargets());
+ Profiler.instance().logSimpleTask(startTime, ProfilerTask.INFO, "Show results");
+ if (explanationHandler != null) {
+ uninstallExplanationHandler(explanationHandler);
+ }
+ // Finalize output service last, so that if we do throw an exception, we know all the other
+ // code has already run.
+ if (runtime.getOutputService() != null) {
+ boolean isBuildSuccessful =
+ buildResult.getSuccessfulTargets().size() == configuredTargets.size();
+ runtime.getOutputService().finalizeBuild(isBuildSuccessful);
+ }
+ }
+ }
+
+ private void prepare(ImmutableMap<PathFragment, Path> packageRoots,
+ BuildConfigurationCollection configurations)
+ throws ViewCreationFailedException {
+ // Prepare for build.
+ Profiler.instance().markPhase(ProfilePhase.PREPARE);
+
+ // Create some tools symlinks / cleanup per-build state
+ createActionLogDirectory();
+
+ // Plant the symlink forest.
+ plantSymlinkForest(packageRoots, configurations);
+ }
+
+ private void createToolsSymlinks() throws ExecutorInitException {
+ try {
+ runtime.getBinTools().setupBuildTools();
+ } catch (ExecException e) {
+ throw new ExecutorInitException("Tools symlink creation failed: "
+ + e.getMessage() + "; build aborted", e);
+ }
+ }
+
+ private void plantSymlinkForest(ImmutableMap<PathFragment, Path> packageRoots,
+ BuildConfigurationCollection configurations) throws ViewCreationFailedException {
+ try {
+ FileSystemUtils.deleteTreesBelowNotPrefixed(getExecRoot(),
+ new String[] { ".", "_", Constants.PRODUCT_NAME + "-"});
+ // Delete the build configuration's temporary directories
+ for (BuildConfiguration configuration : configurations.getTargetConfigurations()) {
+ configuration.prepareForExecutionPhase();
+ }
+ FileSystemUtils.plantLinkForest(packageRoots, getExecRoot());
+ } catch (IOException e) {
+ throw new ViewCreationFailedException("Source forest creation failed: " + e.getMessage()
+ + "; build aborted", e);
+ }
+ }
+
+ private void createActionLogDirectory() throws ViewCreationFailedException {
+ Path directory = runtime.getDirectories().getActionConsoleOutputDirectory();
+ try {
+ if (directory.exists()) {
+ FileSystemUtils.deleteTree(directory);
+ }
+ directory.createDirectory();
+ } catch (IOException ex) {
+ throw new ViewCreationFailedException("couldn't delete action output directory: " +
+ ex.getMessage());
+ }
+ }
+
+ /**
+ * Prepare for a local output build.
+ */
+ private void startLocalOutputBuild() throws BuildFailedException {
+ long startTime = Profiler.nanoTimeMaybe();
+
+ try {
+ Path outputPath = runtime.getOutputPath();
+ Path localOutputPath = runtime.getDirectories().getLocalOutputPath();
+
+ if (outputPath.isSymbolicLink()) {
+ // Remove the existing symlink first.
+ outputPath.delete();
+ if (localOutputPath.exists()) {
+ // Pre-existing local output directory. Move to outputPath.
+ localOutputPath.renameTo(outputPath);
+ }
+ }
+ } catch (IOException e) {
+ throw new BuildFailedException(e.getMessage());
+ } finally {
+ Profiler.instance().logSimpleTask(startTime, ProfilerTask.INFO,
+ "Starting local output build");
+ }
+ }
+
+ /**
+ * If a path is supplied, creates and installs an ExplanationHandler. Returns
+ * an instance on success. Reports an error and returns null otherwise.
+ */
+ private ExplanationHandler installExplanationHandler(PathFragment explanationPath,
+ String allOptions) {
+ if (explanationPath == null) {
+ return null;
+ }
+ ExplanationHandler handler;
+ try {
+ handler = new ExplanationHandler(
+ getWorkspace().getRelative(explanationPath).getOutputStream(),
+ allOptions);
+ } catch (IOException e) {
+ getReporter().handle(Event.warn(String.format(
+ "Cannot write explanation of rebuilds to file '%s': %s",
+ explanationPath, e.getMessage())));
+ return null;
+ }
+ getReporter().handle(
+ Event.info("Writing explanation of rebuilds to '" + explanationPath + "'"));
+ getReporter().addHandler(handler);
+ return handler;
+ }
+
+ /**
+ * Uninstalls the specified ExplanationHandler (if any) and closes the log
+ * file.
+ */
+ private void uninstallExplanationHandler(ExplanationHandler handler) {
+ if (handler != null) {
+ getReporter().removeHandler(handler);
+ handler.log.close();
+ }
+ }
+
+ /**
+ * An ErrorEventListener implementation that records DEPCHECKER events into a log
+ * file, iff the --explain flag is specified during a build.
+ */
+ private static class ExplanationHandler implements EventHandler {
+
+ private final PrintWriter log;
+
+ private ExplanationHandler(OutputStream log, String optionsDescription) {
+ this.log = new PrintWriter(log);
+ this.log.println("Build options: " + optionsDescription);
+ }
+
+
+ @Override
+ public void handle(Event event) {
+ if (event.getKind() == EventKind.DEPCHECKER) {
+ log.println(event.getMessage());
+ }
+ }
+ }
+
+ /**
+ * Computes the result of the build. Sets the list of successful (up-to-date)
+ * targets in the request object.
+ *
+ * @param configuredTargets The configured targets whose artifacts are to be
+ * built.
+ * @param timer A timer that was started when the execution phase started.
+ */
+ private void determineSuccessfulTargets(BuildResult result,
+ Collection<ConfiguredTarget> configuredTargets, Set<ConfiguredTarget> builtTargets,
+ Stopwatch timer) {
+ // Maintain the ordering by copying builtTargets into a LinkedHashSet in the same iteration
+ // order as configuredTargets.
+ Collection<ConfiguredTarget> successfulTargets = new LinkedHashSet<>();
+ for (ConfiguredTarget target : configuredTargets) {
+ if (builtTargets.contains(target)) {
+ successfulTargets.add(target);
+ }
+ }
+ getEventBus().post(
+ new ExecutionPhaseCompleteEvent(timer.stop().elapsed(TimeUnit.MILLISECONDS)));
+ result.setSuccessfulTargets(successfulTargets);
+ }
+
+ /**
+ * Shows the result of the build. Information includes the list of up-to-date
+ * and failed targets and list of output artifacts for successful targets
+ *
+ * @param request The build request, which specifies various options.
+ * @param configuredTargets The configured targets whose artifacts are to be
+ * built.
+ *
+ * TODO(bazel-team): (2010) refactor into using Reporter and info/progress events
+ */
+ private void showBuildResult(BuildRequest request, BuildResult result,
+ Collection<ConfiguredTarget> configuredTargets) {
+ // NOTE: be careful what you print! We don't want to create a consistency
+ // problem where the summary message and the exit code disagree. The logic
+ // here is already complex.
+
+ // Filter the targets we care about into two buckets:
+ Collection<ConfiguredTarget> succeeded = new ArrayList<>();
+ Collection<ConfiguredTarget> failed = new ArrayList<>();
+ for (ConfiguredTarget target : configuredTargets) {
+ // TODO(bazel-team): this is quite ugly. Add a marker provider for this check.
+ if (target instanceof InputFileConfiguredTarget) {
+ // Suppress display of source files (because we do no work to build them).
+ continue;
+ }
+ if (target.getTarget() instanceof Rule) {
+ Rule rule = (Rule) target.getTarget();
+ if (rule.getRuleClass().contains("$")) {
+ // Suppress display of hidden rules
+ continue;
+ }
+ }
+ if (target instanceof OutputFileConfiguredTarget) {
+ // Suppress display of generated files (because they appear underneath
+ // their generating rule), EXCEPT those ones which are not part of the
+ // filesToBuild of their generating rule (e.g. .par, _deploy.jar
+ // files), OR when a user explicitly requests an output file but not
+ // its rule.
+ TransitiveInfoCollection generatingRule =
+ getView().getGeneratingRule((OutputFileConfiguredTarget) target);
+ if (CollectionUtils.containsAll(
+ generatingRule.getProvider(FileProvider.class).getFilesToBuild(),
+ target.getProvider(FileProvider.class).getFilesToBuild()) &&
+ configuredTargets.contains(generatingRule)) {
+ continue;
+ }
+ }
+
+ Collection<ConfiguredTarget> successfulTargets = result.getSuccessfulTargets();
+ (successfulTargets.contains(target) ? succeeded : failed).add(target);
+ }
+
+ // Suppress summary if --show_result value is exceeded:
+ if (succeeded.size() + failed.size() > request.getBuildOptions().maxResultTargets) {
+ return;
+ }
+
+ OutErr outErr = request.getOutErr();
+
+ for (ConfiguredTarget target : succeeded) {
+ Label label = target.getLabel();
+ // For up-to-date targets report generated artifacts, but only
+ // if they have associated action and not middleman artifacts.
+ boolean headerFlag = true;
+ for (Artifact artifact : getFilesToBuild(target, request)) {
+ if (!artifact.isSourceArtifact()) {
+ if (headerFlag) {
+ outErr.printErr("Target " + label + " up-to-date:\n");
+ headerFlag = false;
+ }
+ outErr.printErrLn(" " +
+ OutputDirectoryLinksUtils.getPrettyPath(artifact.getPath(),
+ runtime.getWorkspaceName(), getWorkspace(), request.getSymlinkPrefix()));
+ }
+ }
+ if (headerFlag) {
+ outErr.printErr(
+ "Target " + label + " up-to-date (nothing to build)\n");
+ }
+ }
+
+ for (ConfiguredTarget target : failed) {
+ outErr.printErr("Target " + target.getLabel() + " failed to build\n");
+
+ // For failed compilation, it is still useful to examine temp artifacts,
+ // (ie, preprocessed and assembler files).
+ TempsProvider tempsProvider = target.getProvider(TempsProvider.class);
+ if (tempsProvider != null) {
+ for (Artifact temp : tempsProvider.getTemps()) {
+ if (temp.getPath().exists()) {
+ outErr.printErrLn(" See temp at " +
+ OutputDirectoryLinksUtils.getPrettyPath(temp.getPath(),
+ runtime.getWorkspaceName(), getWorkspace(), request.getSymlinkPrefix()));
+ }
+ }
+ }
+ }
+ if (!failed.isEmpty() && !request.getOptions(ExecutionOptions.class).verboseFailures) {
+ outErr.printErr("Use --verbose_failures to see the command lines of failed build steps.\n");
+ }
+ }
+
+ /**
+ * Gets all the files to build for a given target and build request.
+ * There may be artifacts that should be built which are not represented in the
+ * configured target graph. Currently, this only occurs when "--save_temps" is on.
+ *
+ * @param target configured target
+ * @param request the build request
+ * @return artifacts to build
+ */
+ private static Collection<Artifact> getFilesToBuild(ConfiguredTarget target,
+ BuildRequest request) {
+ ImmutableSet.Builder<Artifact> result = ImmutableSet.builder();
+ if (request.getBuildOptions().compileOnly) {
+ FilesToCompileProvider provider = target.getProvider(FilesToCompileProvider.class);
+ if (provider != null) {
+ result.addAll(provider.getFilesToCompile());
+ }
+ } else if (request.getBuildOptions().compilationPrerequisitesOnly) {
+ CompilationPrerequisitesProvider provider =
+ target.getProvider(CompilationPrerequisitesProvider.class);
+ if (provider != null) {
+ result.addAll(provider.getCompilationPrerequisites());
+ }
+ } else {
+ FileProvider provider = target.getProvider(FileProvider.class);
+ if (provider != null) {
+ result.addAll(provider.getFilesToBuild());
+ }
+ }
+ TempsProvider tempsProvider = target.getProvider(TempsProvider.class);
+ if (tempsProvider != null) {
+ result.addAll(tempsProvider.getTemps());
+ }
+
+ return result.build();
+ }
+
+ private ActionCache getActionCache() throws LocalEnvironmentException {
+ try {
+ return runtime.getPersistentActionCache();
+ } catch (IOException e) {
+ // TODO(bazel-team): (2010) Ideally we should just remove all cache data and reinitialize
+ // caches.
+ LoggingUtil.logToRemote(Level.WARNING, "Failed to initialize action cache: "
+ + e.getMessage(), e);
+ throw new LocalEnvironmentException("couldn't create action cache: " + e.getMessage()
+ + ". If error persists, use 'blaze clean'");
+ }
+ }
+
+ private Builder createBuilder(BuildRequest request,
+ Executor executor,
+ ActionCache actionCache,
+ SkyframeExecutor skyframeExecutor) {
+ BuildRequest.BuildRequestOptions options = request.getBuildOptions();
+ boolean verboseExplanations = options.verboseExplanations;
+ boolean keepGoing = request.getViewOptions().keepGoing;
+
+ Path actionOutputRoot = runtime.getDirectories().getActionConsoleOutputDirectory();
+ Predicate<Action> executionFilter = CheckUpToDateFilter.fromOptions(
+ request.getOptions(ExecutionOptions.class));
+
+ // jobs should have been verified in BuildRequest#validateOptions().
+ Preconditions.checkState(options.jobs >= -1);
+ int actualJobs = options.jobs == 0 ? 1 : options.jobs; // Treat 0 jobs as a single task.
+
+ // Unfortunately, the exec root cache is not shared with caches in the remote execution
+ // client.
+ fileCache = createBuildSingleFileCache(executor.getExecRoot());
+ skyframeExecutor.setActionOutputRoot(actionOutputRoot);
+ return new SkyframeBuilder(skyframeExecutor,
+ new ActionCacheChecker(actionCache, getView().getArtifactFactory(), executionFilter,
+ verboseExplanations),
+ keepGoing, actualJobs, options.checkOutputFiles, fileCache,
+ request.getBuildOptions().progressReportInterval);
+ }
+
+ private void configureResourceManager(BuildRequest request) {
+ ResourceManager resourceMgr = ResourceManager.instance();
+ ExecutionOptions options = request.getOptions(ExecutionOptions.class);
+ if (options.availableResources != null) {
+ resourceMgr.setAvailableResources(options.availableResources);
+ resourceMgr.setRamUtilizationPercentage(100);
+ } else {
+ resourceMgr.setAvailableResources(LocalHostCapacity.getLocalHostCapacity());
+ resourceMgr.setRamUtilizationPercentage(options.ramUtilizationPercentage);
+ if (options.useResourceAutoSense) {
+ getReporter().handle(
+ Event.warn("Not using resource autosense due to known responsiveness issues"));
+ }
+ ResourceManager.instance().setAutoSensing(/*autosense=*/false);
+ }
+ }
+
+ /**
+ * Writes the cache files to disk, reporting any errors that occurred during
+ * writing.
+ */
+ private void saveCaches(ActionCache actionCache) {
+ long actionCacheSizeInBytes = 0;
+ long actionCacheSaveTime;
+
+ long startTime = BlazeClock.nanoTime();
+ try {
+ LOG.info("saving action cache...");
+ actionCacheSizeInBytes = actionCache.save();
+ LOG.info("action cache saved");
+ } catch (IOException e) {
+ getReporter().handle(Event.error("I/O error while writing action log: " + e.getMessage()));
+ } finally {
+ long stopTime = BlazeClock.nanoTime();
+ actionCacheSaveTime =
+ TimeUnit.MILLISECONDS.convert(stopTime - startTime, TimeUnit.NANOSECONDS);
+ Profiler.instance().logSimpleTask(startTime, stopTime,
+ ProfilerTask.INFO, "Saving action cache");
+ }
+
+ runtime.getEventBus().post(new CachesSavedEvent(
+ actionCacheSaveTime, actionCacheSizeInBytes));
+ }
+
+ private ActionInputFileCache createBuildSingleFileCache(Path execRoot) {
+ String cwd = execRoot.getPathString();
+ FileSystem fs = runtime.getDirectories().getFileSystem();
+
+ ActionInputFileCache cache = null;
+ for (BlazeModule module : runtime.getBlazeModules()) {
+ ActionInputFileCache pluggable = module.createActionInputCache(cwd, fs);
+ if (pluggable != null) {
+ Preconditions.checkState(cache == null);
+ cache = pluggable;
+ }
+ }
+
+ if (cache == null) {
+ cache = new SingleBuildFileCache(cwd, fs);
+ }
+ return cache;
+ }
+
+ private Reporter getReporter() {
+ return runtime.getReporter();
+ }
+
+ private EventBus getEventBus() {
+ return runtime.getEventBus();
+ }
+
+ private BuildView getView() {
+ return runtime.getView();
+ }
+
+ private Path getWorkspace() {
+ return runtime.getWorkspace();
+ }
+
+ private Path getExecRoot() {
+ return runtime.getExecRoot();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/buildtool/LocalEnvironmentException.java b/src/main/java/com/google/devtools/build/lib/buildtool/LocalEnvironmentException.java
new file mode 100644
index 0000000..7890b22
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/buildtool/LocalEnvironmentException.java
@@ -0,0 +1,45 @@
+// Copyright 2014 Google Inc. 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.buildtool;
+
+import com.google.devtools.build.lib.util.AbruptExitException;
+import com.google.devtools.build.lib.util.ExitCode;
+
+/**
+ * An exception that signals that something is wrong with the user's environment
+ * that he can fix. Used to report the problem of having no free space left in
+ * the blaze output directory.
+ *
+ * <p>Note that this is a much higher level exception then the similarly named
+ * EnvironmentExecException, which is thrown from the base Client and Strategy
+ * layers of Blaze.
+ *
+ * <p>This exception is only thrown when we've decided that the build has, in
+ * fact, failed and we should exit.
+ */
+public class LocalEnvironmentException extends AbruptExitException {
+
+ public LocalEnvironmentException(String message) {
+ super(message, ExitCode.LOCAL_ENVIRONMENTAL_ERROR);
+ }
+
+ public LocalEnvironmentException(Throwable cause) {
+ super(ExitCode.LOCAL_ENVIRONMENTAL_ERROR, cause);
+ }
+
+ public LocalEnvironmentException(String message, Throwable cause) {
+ super(message, ExitCode.LOCAL_ENVIRONMENTAL_ERROR, cause);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/buildtool/OutputDirectoryLinksUtils.java b/src/main/java/com/google/devtools/build/lib/buildtool/OutputDirectoryLinksUtils.java
new file mode 100644
index 0000000..094b7bc
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/buildtool/OutputDirectoryLinksUtils.java
@@ -0,0 +1,184 @@
+// Copyright 2014 Google Inc. 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.buildtool;
+
+import com.google.common.base.Joiner;
+import com.google.devtools.build.lib.Constants;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+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 com.google.devtools.build.lib.vfs.Symlinks;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Static utilities for managing output directory symlinks.
+ */
+public class OutputDirectoryLinksUtils {
+ public static final String OUTPUT_SYMLINK_NAME = Constants.PRODUCT_NAME + "-out";
+
+ // Used in getPrettyPath() method below.
+ private static final String[] LINKS = { "bin", "genfiles", "includes" };
+
+ private static final String NO_CREATE_SYMLINKS_PREFIX = "/";
+
+ private static String execRootSymlink(String workspaceName) {
+ return Constants.PRODUCT_NAME + "-" + workspaceName;
+ }
+ /**
+ * Attempts to create convenience symlinks in the workspaceDirectory and in
+ * execRoot to the output area and to the configuration-specific output
+ * directories. Issues a warning if it fails, e.g. because workspaceDirectory
+ * is readonly.
+ */
+ public static void createOutputDirectoryLinks(String workspaceName,
+ Path workspace, Path execRoot, Path outputPath,
+ EventHandler eventHandler, BuildConfiguration targetConfig, String symlinkPrefix) {
+ if (NO_CREATE_SYMLINKS_PREFIX.equals(symlinkPrefix)) {
+ return;
+ }
+ List<String> failures = new ArrayList<>();
+
+ // Make the two non-specific links from the workspace to the output area,
+ // and the configuration-specific links in both the workspace and the execution root dirs.
+ // NB! Keep in sync with removeOutputDirectoryLinks below.
+ createLink(workspace, OUTPUT_SYMLINK_NAME, outputPath, failures);
+
+ // Points to execroot
+ createLink(workspace, execRootSymlink(workspaceName), execRoot, failures);
+ createLink(workspace, symlinkPrefix + "bin", targetConfig.getBinDirectory().getPath(),
+ failures);
+ createLink(workspace, symlinkPrefix + "testlogs", targetConfig.getTestLogsDirectory().getPath(),
+ failures);
+ createLink(workspace, symlinkPrefix + "genfiles", targetConfig.getGenfilesDirectory().getPath(),
+ failures);
+ if (!failures.isEmpty()) {
+ eventHandler.handle(Event.warn(String.format(
+ "failed to create one or more convenience symlinks for prefix '%s':\n %s",
+ symlinkPrefix, Joiner.on("\n ").join(failures))));
+ }
+ }
+
+ /**
+ * Returns a convenient path to the specified file, relativizing it and using output-dir symlinks
+ * if possible. Otherwise, return a path relative to the workspace directory if possible.
+ * Otherwise, return the absolute path.
+ *
+ * <p>This method must be called after the symlinks are created at the end of a build. If called
+ * before, the pretty path may be incorrect if the symlinks end up pointing somewhere new.
+ */
+ public static PathFragment getPrettyPath(Path file, String workspaceName,
+ Path workspaceDirectory, String symlinkPrefix) {
+ for (String link : LINKS) {
+ PathFragment result = relativize(file, workspaceDirectory, symlinkPrefix + link);
+ if (result != null) {
+ return result;
+ }
+ }
+
+ PathFragment result = relativize(file, workspaceDirectory, execRootSymlink(workspaceName));
+ if (result != null) {
+ return result;
+ }
+
+ result = relativize(file, workspaceDirectory, OUTPUT_SYMLINK_NAME);
+ if (result != null) {
+ return result;
+ }
+
+ return file.asFragment();
+ }
+
+ // Helper to getPrettyPath. Returns file, relativized w.r.t. the referent of
+ // "linkname", or null if it was a not a child.
+ private static PathFragment relativize(Path file, Path workspaceDirectory, String linkname) {
+ PathFragment link = new PathFragment(linkname);
+ try {
+ Path dir = workspaceDirectory.getRelative(link);
+ PathFragment levelOneLinkTarget = dir.readSymbolicLink();
+ if (levelOneLinkTarget.isAbsolute() &&
+ file.startsWith(dir = file.getRelative(levelOneLinkTarget))) {
+ return link.getRelative(file.relativeTo(dir));
+ }
+ } catch (IOException e) {
+ /* ignore */
+ }
+ return null;
+ }
+
+ /**
+ * Attempts to remove the convenience symlinks in the workspace directory.
+ *
+ * <p>Issues a warning if it fails, e.g. because workspaceDirectory is readonly.
+ * Also cleans up any child directories created by a custom prefix.
+ *
+ * @param workspace the runtime's workspace
+ * @param eventHandler the error eventHandler
+ * @param symlinkPrefix the symlink prefix which should be removed
+ */
+ public static void removeOutputDirectoryLinks(String workspaceName, Path workspace,
+ EventHandler eventHandler, String symlinkPrefix) {
+ if (NO_CREATE_SYMLINKS_PREFIX.equals(symlinkPrefix)) {
+ return;
+ }
+ List<String> failures = new ArrayList<>();
+
+ removeLink(workspace, OUTPUT_SYMLINK_NAME, failures);
+ removeLink(workspace, execRootSymlink(workspaceName), failures);
+ removeLink(workspace, symlinkPrefix + "bin", failures);
+ removeLink(workspace, symlinkPrefix + "testlogs", failures);
+ removeLink(workspace, symlinkPrefix + "genfiles", failures);
+ FileSystemUtils.removeDirectoryAndParents(workspace, new PathFragment(symlinkPrefix));
+ if (!failures.isEmpty()) {
+ eventHandler.handle(Event.warn(String.format(
+ "failed to remove one or more convenience symlinks for prefix '%s':\n %s", symlinkPrefix,
+ Joiner.on("\n ").join(failures))));
+ }
+ }
+
+ /**
+ * Helper to createOutputDirectoryLinks that creates a symlink from base + name to target.
+ */
+ private static boolean createLink(Path base, String name, Path target, List<String> failures) {
+ try {
+ FileSystemUtils.ensureSymbolicLink(base.getRelative(name), target);
+ return true;
+ } catch (IOException e) {
+ failures.add(String.format("%s -> %s: %s", name, target.getPathString(), e.getMessage()));
+ return false;
+ }
+ }
+
+ /**
+ * Helper to removeOutputDirectoryLinks that removes one of the Blaze convenience symbolic links.
+ */
+ private static boolean removeLink(Path base, String name, List<String> failures) {
+ Path link = base.getRelative(name);
+ try {
+ if (link.exists(Symlinks.NOFOLLOW)) {
+ ExecutionTool.LOG.finest("Removing " + link);
+ link.delete();
+ }
+ return true;
+ } catch (IOException e) {
+ failures.add(String.format("%s: %s", name, e.getMessage()));
+ return false;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/buildtool/SkyframeBuilder.java b/src/main/java/com/google/devtools/build/lib/buildtool/SkyframeBuilder.java
new file mode 100644
index 0000000..779515a
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/buildtool/SkyframeBuilder.java
@@ -0,0 +1,355 @@
+// Copyright 2014 Google Inc. 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.buildtool;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
+import com.google.common.eventbus.EventBus;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.actions.ActionCacheChecker;
+import com.google.devtools.build.lib.actions.ActionExecutionStatusReporter;
+import com.google.devtools.build.lib.actions.ActionInputFileCache;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.BuildFailedException;
+import com.google.devtools.build.lib.actions.BuilderUtils;
+import com.google.devtools.build.lib.actions.Executor;
+import com.google.devtools.build.lib.actions.ResourceManager;
+import com.google.devtools.build.lib.actions.TestExecException;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.TargetCompleteEvent;
+import com.google.devtools.build.lib.rules.test.TestProvider;
+import com.google.devtools.build.lib.skyframe.ActionExecutionInactivityWatchdog;
+import com.google.devtools.build.lib.skyframe.ActionExecutionValue;
+import com.google.devtools.build.lib.skyframe.Builder;
+import com.google.devtools.build.lib.skyframe.SkyFunctions;
+import com.google.devtools.build.lib.skyframe.SkyframeActionExecutor;
+import com.google.devtools.build.lib.skyframe.SkyframeExecutor;
+import com.google.devtools.build.lib.skyframe.TargetCompletionValue;
+import com.google.devtools.build.lib.util.AbruptExitException;
+import com.google.devtools.build.lib.util.BlazeClock;
+import com.google.devtools.build.skyframe.CycleInfo;
+import com.google.devtools.build.skyframe.ErrorInfo;
+import com.google.devtools.build.skyframe.EvaluationProgressReceiver;
+import com.google.devtools.build.skyframe.EvaluationResult;
+import com.google.devtools.build.skyframe.SkyFunctionName;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.text.NumberFormat;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * A {@link Builder} implementation driven by Skyframe.
+ */
+@VisibleForTesting
+public class SkyframeBuilder implements Builder {
+
+ private final SkyframeExecutor skyframeExecutor;
+ private final boolean keepGoing;
+ private final int numJobs;
+ private final boolean checkOutputFiles;
+ private final ActionInputFileCache fileCache;
+ private final ActionCacheChecker actionCacheChecker;
+ private final int progressReportInterval;
+
+ @VisibleForTesting
+ public SkyframeBuilder(SkyframeExecutor skyframeExecutor, ActionCacheChecker actionCacheChecker,
+ boolean keepGoing, int numJobs, boolean checkOutputFiles,
+ ActionInputFileCache fileCache, int progressReportInterval) {
+ this.skyframeExecutor = skyframeExecutor;
+ this.actionCacheChecker = actionCacheChecker;
+ this.keepGoing = keepGoing;
+ this.numJobs = numJobs;
+ this.checkOutputFiles = checkOutputFiles;
+ this.fileCache = fileCache;
+ this.progressReportInterval = progressReportInterval;
+ }
+
+ @Override
+ public void buildArtifacts(Set<Artifact> artifacts,
+ Set<ConfiguredTarget> parallelTests,
+ Set<ConfiguredTarget> exclusiveTests,
+ Collection<ConfiguredTarget> targetsToBuild,
+ Executor executor,
+ Set<ConfiguredTarget> builtTargets,
+ boolean explain)
+ throws BuildFailedException, AbruptExitException, TestExecException, InterruptedException {
+ skyframeExecutor.prepareExecution(checkOutputFiles);
+ skyframeExecutor.setFileCache(fileCache);
+ // Note that executionProgressReceiver accesses builtTargets concurrently (after wrapping in a
+ // synchronized collection), so unsynchronized access to this variable is unsafe while it runs.
+ ExecutionProgressReceiver executionProgressReceiver =
+ new ExecutionProgressReceiver(Preconditions.checkNotNull(builtTargets),
+ countTestActions(exclusiveTests), skyframeExecutor.getEventBus());
+ ResourceManager.instance().setEventBus(skyframeExecutor.getEventBus());
+
+ boolean success = false;
+ EvaluationResult<?> result;
+
+ ActionExecutionStatusReporter statusReporter = ActionExecutionStatusReporter.create(
+ skyframeExecutor.getReporter(), executor, skyframeExecutor.getEventBus());
+
+ AtomicBoolean isBuildingExclusiveArtifacts = new AtomicBoolean(false);
+ ActionExecutionInactivityWatchdog watchdog = new ActionExecutionInactivityWatchdog(
+ executionProgressReceiver.createInactivityMonitor(statusReporter),
+ executionProgressReceiver.createInactivityReporter(statusReporter,
+ isBuildingExclusiveArtifacts), progressReportInterval);
+
+ skyframeExecutor.setActionExecutionProgressReportingObjects(executionProgressReceiver,
+ executionProgressReceiver, statusReporter);
+ watchdog.start();
+
+ try {
+ result = skyframeExecutor.buildArtifacts(executor, artifacts, targetsToBuild, parallelTests,
+ /*exclusiveTesting=*/false, keepGoing, explain, numJobs, actionCacheChecker,
+ executionProgressReceiver);
+ // progressReceiver is finished, so unsynchronized access to builtTargets is now safe.
+ success = processResult(result, keepGoing, skyframeExecutor);
+
+ Preconditions.checkState(
+ !success || result.keyNames().size()
+ == (artifacts.size() + targetsToBuild.size() + parallelTests.size()),
+ "Build reported as successful but not all artifacts and targets built: %s, %s",
+ result, artifacts);
+
+ // Run exclusive tests: either tagged as "exclusive" or is run in an invocation with
+ // --test_output=streamed.
+ isBuildingExclusiveArtifacts.set(true);
+ for (ConfiguredTarget exclusiveTest : exclusiveTests) {
+ // Since only one artifact is being built at a time, we don't worry about an artifact being
+ // built and then the build being interrupted.
+ result = skyframeExecutor.buildArtifacts(executor, ImmutableSet.<Artifact>of(),
+ targetsToBuild, ImmutableSet.of(exclusiveTest), /*exclusiveTesting=*/true, keepGoing,
+ explain, numJobs, actionCacheChecker, null);
+ boolean exclusiveSuccess = processResult(result, keepGoing, skyframeExecutor);
+ Preconditions.checkState(!exclusiveSuccess || !result.keyNames().isEmpty(),
+ "Build reported as successful but test %s not executed: %s",
+ exclusiveTest, result);
+ success &= exclusiveSuccess;
+ }
+ } finally {
+ watchdog.stop();
+ ResourceManager.instance().unsetEventBus();
+ skyframeExecutor.setActionExecutionProgressReportingObjects(null, null, null);
+ statusReporter.unregisterFromEventBus();
+ }
+
+ if (!success) {
+ throw new BuildFailedException();
+ }
+ }
+
+ private static boolean resultHasCatastrophicError(EvaluationResult<?> result) {
+ for (ErrorInfo errorInfo : result.errorMap().values()) {
+ if (errorInfo.isCatastrophic()) {
+ return true;
+ }
+ }
+ // An unreported catastrophe manifests with hasError() being true but no errors visible.
+ return result.hasError() && result.errorMap().isEmpty();
+ }
+
+ /**
+ * Process the Skyframe update, taking into account the keepGoing setting.
+ *
+ * Returns false if the update() failed, but we should continue. Returns true on success.
+ * Throws on fail-fast failures.
+ */
+ private static boolean processResult(EvaluationResult<?> result, boolean keepGoing,
+ SkyframeExecutor skyframeExecutor) throws BuildFailedException, TestExecException {
+ if (result.hasError()) {
+ boolean hasCycles = false;
+ for (Map.Entry<SkyKey, ErrorInfo> entry : result.errorMap().entrySet()) {
+ Iterable<CycleInfo> cycles = entry.getValue().getCycleInfo();
+ skyframeExecutor.reportCycles(cycles, entry.getKey());
+ hasCycles |= !Iterables.isEmpty(cycles);
+ }
+ if (keepGoing && !resultHasCatastrophicError(result)) {
+ return false;
+ }
+ if (hasCycles || result.errorMap().isEmpty()) {
+ // error map may be empty in the case of a catastrophe.
+ throw new BuildFailedException();
+ } else {
+ // Need to wrap exception for rethrowCause.
+ BuilderUtils.rethrowCause(
+ new Exception(Preconditions.checkNotNull(result.getError().getException())));
+ }
+ }
+ return true;
+ }
+
+ private static int countTestActions(Iterable<ConfiguredTarget> testTargets) {
+ int count = 0;
+ for (ConfiguredTarget testTarget : testTargets) {
+ count += TestProvider.getTestStatusArtifacts(testTarget).size();
+ }
+ return count;
+ }
+
+ /**
+ * Listener for executed actions and built artifacts. We use a listener so that we have an
+ * accurate set of successfully run actions and built artifacts, even if the build is interrupted.
+ */
+ private static final class ExecutionProgressReceiver implements EvaluationProgressReceiver,
+ SkyframeActionExecutor.ProgressSupplier, SkyframeActionExecutor.ActionCompletedReceiver {
+ private static final NumberFormat PROGRESS_MESSAGE_NUMBER_FORMATTER;
+
+ // Must be thread-safe!
+ private final Set<ConfiguredTarget> builtTargets;
+ private final Set<SkyKey> enqueuedActions = Sets.newConcurrentHashSet();
+ private final Set<Action> completedActions = Sets.newConcurrentHashSet();
+ private final Object activityIndicator = new Object();
+ /** Number of exclusive tests. To be accounted for in progress messages. */
+ private final int exclusiveTestsCount;
+ private final EventBus eventBus;
+
+ static {
+ PROGRESS_MESSAGE_NUMBER_FORMATTER = NumberFormat.getIntegerInstance(Locale.ENGLISH);
+ PROGRESS_MESSAGE_NUMBER_FORMATTER.setGroupingUsed(true);
+ }
+
+ /**
+ * {@code builtTargets} is accessed through a synchronized set, and so no other access to it
+ * is permitted while this receiver is active.
+ */
+ ExecutionProgressReceiver(Set<ConfiguredTarget> builtTargets, int exclusiveTestsCount,
+ EventBus eventBus) {
+ this.builtTargets = Collections.synchronizedSet(builtTargets);
+ this.exclusiveTestsCount = exclusiveTestsCount;
+ this.eventBus = eventBus;
+ }
+
+ @Override
+ public void invalidated(SkyValue node, InvalidationState state) {}
+
+ @Override
+ public void enqueueing(SkyKey skyKey) {
+ if (ActionExecutionValue.isReportWorthyAction(skyKey)) {
+ // Remember all enqueued actions for the benefit of progress reporting.
+ // We discover most actions early in the build, well before we start executing them.
+ // Some of these will be cache hits and won't be executed, so we'll need to account for them
+ // in the evaluated method too.
+ enqueuedActions.add(skyKey);
+ }
+ }
+
+ @Override
+ public void evaluated(SkyKey skyKey, SkyValue node, EvaluationState state) {
+ SkyFunctionName type = skyKey.functionName();
+ if (type == SkyFunctions.TARGET_COMPLETION) {
+ TargetCompletionValue val = (TargetCompletionValue) node;
+ ConfiguredTarget target = val.getConfiguredTarget();
+ builtTargets.add(target);
+ eventBus.post(TargetCompleteEvent.createSuccessful(target));
+ } else if (type == SkyFunctions.ACTION_EXECUTION) {
+ // Remember all completed actions, regardless of having been cached or really executed.
+ actionCompleted((Action) skyKey.argument());
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * <p>This method adds the action to {@link #completedActions} and notifies the
+ * {@link #activityIndicator}.
+ *
+ * <p>We could do this only in the {@link #evaluated} method too, but as it happens the action
+ * executor tells the reporter about the completed action before the node is inserted into the
+ * graph, so the reporter would find out about the completed action sooner than we could
+ * have updated {@link #completedActions}, which would result in incorrect numbers on the
+ * progress messages. However we have to store completed actions in {@link #evaluated} too,
+ * because that's the only place we get notified about completed cached actions.
+ */
+ @Override
+ public void actionCompleted(Action a) {
+ if (ActionExecutionValue.isReportWorthyAction(a)) {
+ completedActions.add(a);
+ synchronized (activityIndicator) {
+ activityIndicator.notifyAll();
+ }
+ }
+ }
+
+ @Override
+ public String getProgressString() {
+ return String.format("[%s / %s]",
+ PROGRESS_MESSAGE_NUMBER_FORMATTER.format(completedActions.size()),
+ PROGRESS_MESSAGE_NUMBER_FORMATTER.format(exclusiveTestsCount + enqueuedActions.size()));
+ }
+
+ ActionExecutionInactivityWatchdog.InactivityMonitor createInactivityMonitor(
+ final ActionExecutionStatusReporter statusReporter) {
+ return new ActionExecutionInactivityWatchdog.InactivityMonitor() {
+
+ @Override
+ public boolean hasStarted() {
+ return !enqueuedActions.isEmpty();
+ }
+
+ @Override
+ public int getPending() {
+ return statusReporter.getCount();
+ }
+
+ @Override
+ public int waitForNextCompletion(int timeoutMilliseconds) throws InterruptedException {
+ synchronized (activityIndicator) {
+ int before = completedActions.size();
+ long startTime = BlazeClock.instance().currentTimeMillis();
+ while (true) {
+ activityIndicator.wait(timeoutMilliseconds);
+
+ int completed = completedActions.size() - before;
+ long now = 0;
+ if (completed > 0 || (startTime + timeoutMilliseconds) <= (now = BlazeClock.instance()
+ .currentTimeMillis())) {
+ // Some actions completed, or timeout fully elapsed.
+ return completed;
+ } else {
+ // Spurious Wakeup -- no actions completed and there's still time to wait.
+ timeoutMilliseconds -= now - startTime; // account for elapsed wait time
+ startTime = now;
+ }
+ }
+ }
+ }
+ };
+ }
+
+ ActionExecutionInactivityWatchdog.InactivityReporter createInactivityReporter(
+ final ActionExecutionStatusReporter statusReporter,
+ final AtomicBoolean isBuildingExclusiveArtifacts) {
+ return new ActionExecutionInactivityWatchdog.InactivityReporter() {
+ @Override
+ public void maybeReportInactivity() {
+ // Do not report inactivity if we are currently running an exclusive test or a streaming
+ // action (in practice only tests can stream and it implicitly makes them exclusive).
+ if (!isBuildingExclusiveArtifacts.get()) {
+ statusReporter.showCurrentlyExecutingActions(
+ ExecutionProgressReceiver.this.getProgressString() + " ");
+ }
+ }
+ };
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/buildtool/TargetValidator.java b/src/main/java/com/google/devtools/build/lib/buildtool/TargetValidator.java
new file mode 100644
index 0000000..e6eed80
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/buildtool/TargetValidator.java
@@ -0,0 +1,37 @@
+// Copyright 2014 Google Inc. 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.buildtool;
+
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.pkgcache.LoadingFailedException;
+
+import java.util.Collection;
+
+/**
+ * Validator for targets.
+ *
+ * <p>Used in "blaze run" to make sure that we are building exactly one binary target.
+ */
+public interface TargetValidator {
+
+ /**
+ * Hook for subclasses to validate a build request before building begins.
+ * Implementors should print warnings for invalid targets iff keepGoing.
+ *
+ * @param targets The targets to build.
+ * @throws LoadingFailedException if the request is not valid for some reason.
+ */
+ void validateTargets(Collection<Target> targets, boolean keepGoing)
+ throws LoadingFailedException;
+}
diff --git a/src/main/java/com/google/devtools/build/lib/buildtool/buildevent/BuildCompleteEvent.java b/src/main/java/com/google/devtools/build/lib/buildtool/buildevent/BuildCompleteEvent.java
new file mode 100644
index 0000000..e9278e6
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/buildtool/buildevent/BuildCompleteEvent.java
@@ -0,0 +1,40 @@
+// Copyright 2014 Google Inc. 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.buildtool.buildevent;
+
+import com.google.devtools.build.lib.buildtool.BuildRequest;
+import com.google.devtools.build.lib.buildtool.BuildResult;
+
+/**
+ * This event is fired from BuildTool#stopRequest().
+ */
+public final class BuildCompleteEvent {
+ private final BuildResult result;
+
+ /**
+ * Construct the BuildStartingEvent.
+ * @param request the build request.
+ */
+ public BuildCompleteEvent(BuildRequest request, BuildResult result) {
+ this.result = result;
+ }
+
+ /**
+ * @return the build summary
+ */
+ public BuildResult getResult() {
+ return result;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/buildtool/buildevent/BuildInterruptedEvent.java b/src/main/java/com/google/devtools/build/lib/buildtool/buildevent/BuildInterruptedEvent.java
new file mode 100644
index 0000000..02a5d8b
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/buildtool/buildevent/BuildInterruptedEvent.java
@@ -0,0 +1,22 @@
+// Copyright 2014 Google Inc. 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.buildtool.buildevent;
+
+/**
+ * This event is fired from {@code AbstractBuildCommand#doBuild} to indicate
+ * that the user interrupted the build with control-C.
+ */
+public class BuildInterruptedEvent {
+}
diff --git a/src/main/java/com/google/devtools/build/lib/buildtool/buildevent/BuildStartingEvent.java b/src/main/java/com/google/devtools/build/lib/buildtool/buildevent/BuildStartingEvent.java
new file mode 100644
index 0000000..714534d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/buildtool/buildevent/BuildStartingEvent.java
@@ -0,0 +1,50 @@
+// Copyright 2014 Google Inc. 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.buildtool.buildevent;
+
+import com.google.devtools.build.lib.buildtool.BuildRequest;
+
+/**
+ * This event is fired from BuildTool#startRequest().
+ * At this point, the set of target patters are known, but have
+ * yet to be parsed.
+ */
+public class BuildStartingEvent {
+ private final String outputFileSystem;
+ private final BuildRequest request;
+
+ /**
+ * Construct the BuildStartingEvent.
+ * @param request the build request.
+ */
+ public BuildStartingEvent(String outputFileSystem, BuildRequest request) {
+ this.outputFileSystem = outputFileSystem;
+ this.request = request;
+ }
+
+ /**
+ * @return the output file system.
+ */
+ public String getOutputFileSystem() {
+ return outputFileSystem;
+ }
+
+ /**
+ * @return the active BuildRequest.
+ */
+ public BuildRequest getRequest() {
+ return request;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/buildtool/buildevent/ExecutionPhaseCompleteEvent.java b/src/main/java/com/google/devtools/build/lib/buildtool/buildevent/ExecutionPhaseCompleteEvent.java
new file mode 100644
index 0000000..cf57960
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/buildtool/buildevent/ExecutionPhaseCompleteEvent.java
@@ -0,0 +1,35 @@
+// Copyright 2014 Google Inc. 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.buildtool.buildevent;
+
+/**
+ * This event is fired after the execution phase is complete.
+ */
+public class ExecutionPhaseCompleteEvent {
+ private final long timeInMs;
+
+ /**
+ * Construct the event.
+ *
+ * @param timeInMs time for execution phase in milliseconds.
+ */
+ public ExecutionPhaseCompleteEvent(long timeInMs) {
+ this.timeInMs = timeInMs;
+ }
+
+ public long getTimeInMs() {
+ return timeInMs;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/buildtool/buildevent/ExecutionStartingEvent.java b/src/main/java/com/google/devtools/build/lib/buildtool/buildevent/ExecutionStartingEvent.java
new file mode 100644
index 0000000..c2b4f77
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/buildtool/buildevent/ExecutionStartingEvent.java
@@ -0,0 +1,44 @@
+// Copyright 2014 Google Inc. 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.buildtool.buildevent;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
+import com.google.devtools.build.lib.buildtool.ExecutionTool;
+
+import java.util.Collection;
+
+/**
+ * This event is fired from {@link ExecutionTool#executeBuild} to indicate that the execution phase
+ * of the build is starting.
+ */
+public class ExecutionStartingEvent {
+ private final Collection<TransitiveInfoCollection> targets;
+
+ /**
+ * Construct the event with a set of targets.
+ * @param targets Remaining active targets.
+ */
+ public ExecutionStartingEvent(Collection<? extends TransitiveInfoCollection> targets) {
+ this.targets = ImmutableList.copyOf(targets);
+ }
+
+ /**
+ * @return The set of active targets remaining, which is a subset
+ * of the targets in the user request.
+ */
+ public Collection<TransitiveInfoCollection> getTargets() {
+ return targets;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/buildtool/buildevent/TestFilteringCompleteEvent.java b/src/main/java/com/google/devtools/build/lib/buildtool/buildevent/TestFilteringCompleteEvent.java
new file mode 100644
index 0000000..c380456
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/buildtool/buildevent/TestFilteringCompleteEvent.java
@@ -0,0 +1,70 @@
+// Copyright 2014 Google Inc. 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.buildtool.buildevent;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.rules.test.TestProvider;
+
+import java.util.Collection;
+
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * This event is fired after test filtering.
+ *
+ * The test filtering phase always expands test_suite rules, so
+ * the set of active targets should never contain test_suites.
+ */
+@Immutable
+public class TestFilteringCompleteEvent {
+ private final Collection<ConfiguredTarget> targets;
+ private final Collection<ConfiguredTarget> testTargets;
+
+ /**
+ * Construct the event.
+ * @param targets The set of active targets that remain.
+ * @param testTargets The collection of tests to be run. May be null.
+ */
+ public TestFilteringCompleteEvent(
+ Collection<? extends ConfiguredTarget> targets,
+ Collection<? extends ConfiguredTarget> testTargets) {
+ this.targets = ImmutableList.copyOf(targets);
+ this.testTargets = testTargets == null ? null : ImmutableList.copyOf(testTargets);
+ if (testTargets == null) {
+ return;
+ }
+
+ for (ConfiguredTarget testTarget : testTargets) {
+ Preconditions.checkState(testTarget.getProvider(TestProvider.class) != null);
+ }
+ }
+
+ /**
+ * @return The set of active targets remaining. This is a subset of
+ * the targets that passed analysis, after test_suite expansion.
+ */
+ public Collection<ConfiguredTarget> getTargets() {
+ return targets;
+ }
+
+ /**
+ * @return The set of test targets to be run. May be null.
+ */
+ public Collection<ConfiguredTarget> getTestTargets() {
+ return testTargets;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/cmdline/LabelValidator.java b/src/main/java/com/google/devtools/build/lib/cmdline/LabelValidator.java
new file mode 100644
index 0000000..50b3379
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/cmdline/LabelValidator.java
@@ -0,0 +1,289 @@
+// Copyright 2014 Google Inc. 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.cmdline;
+
+import com.google.common.base.CharMatcher;
+
+import java.util.Objects;
+
+import javax.annotation.Nullable;
+
+/**
+ * The canonical place to parse and validate Blaze labels.
+ */
+public final class LabelValidator {
+
+ /**
+ * Matches punctuation in target names which requires quoting in a blaze query.
+ */
+ private static final CharMatcher PUNCTUATION_REQUIRING_QUOTING = CharMatcher.anyOf("+,=~");
+
+ /**
+ * Matches punctuation in target names which doesn't require quoting in a blaze query.
+ *
+ * Note that . is also allowed in target names, and doesn't require quoting, but has restrictions
+ * on its surrounding characters; see {@link #validateTargetName(String)}.
+ */
+ private static final CharMatcher PUNCTUATION_NOT_REQUIRING_QUOTING = CharMatcher.anyOf("_-@");
+
+ /**
+ * Matches characters allowed in target names regardless of context.
+ *
+ * Note that the only other characters allowed in target names are / and . but they have
+ * restrictions around surrounding characters; see {@link #validateTargetName(String)}.
+ */
+ private static final CharMatcher ALWAYS_ALLOWED_TARGET_CHARACTERS =
+ CharMatcher.JAVA_LETTER_OR_DIGIT
+ .or(PUNCTUATION_REQUIRING_QUOTING)
+ .or(PUNCTUATION_NOT_REQUIRING_QUOTING);
+
+ private static final String PACKAGE_NAME_ERROR =
+ "package names may contain only A-Z, a-z, 0-9, '/', '-' and '_'";
+
+ /**
+ * Performs validity checking of the specified package name. Returns null on success or an error
+ * message otherwise.
+ *
+ * @param packageName the name of the package
+ * @return null if {@code name} is valid or an error string if any part
+ * of the package name is invalid
+ */
+ @Nullable
+ public static String validatePackageName(String packageName) {
+ int len = packageName.length();
+ if (len == 0) {
+ return "empty package name";
+ }
+ char first = packageName.charAt(0);
+ if (first < 'a' || first > 'z') {
+ return "package names must start with a lowercase ASCII letter";
+ }
+
+ // Fast path for packages with '.' in their name
+ if (packageName.lastIndexOf('.') != -1) {
+ return PACKAGE_NAME_ERROR;
+ }
+
+ // Check for any character outside of [/0-9A-Z_a-z-]. Try to evaluate the
+ // conditional quickly (by looking in decreasing order of character class
+ // likelihood).
+ for (int i = len - 1; i >= 0; --i) {
+ char c = packageName.charAt(i);
+ if ((c < 'a' || c > 'z') && c != '/' && c != '_' && c != '-' &&
+ (c < '0' || c > '9') && (c < 'A' || c > 'Z')) {
+ return PACKAGE_NAME_ERROR;
+ }
+ }
+
+ if (packageName.contains("//")) {
+ return "package names may not contain '//' path separators";
+ }
+ if (packageName.endsWith("/")) {
+ return "package names may not end with '/'";
+ }
+ return null; // ok
+ }
+
+ /**
+ * Performs validity checking of the specified target name. Returns null on success or an error
+ * message otherwise.
+ */
+ @Nullable
+ public static String validateTargetName(String targetName) {
+ // TODO(bazel-team): (2011) allow labels equaling '.' or ending in '/.' for now. If we ever
+ // actually configure the target we will report an error, but they will be permitted for
+ // data directories.
+
+ // TODO(bazel-team): (2011) Get rid of this code once we have reached critical mass and can
+ // pressure developers to clean up their BUILD files.
+
+ // Code optimized for the common case: success.
+ int len = targetName.length();
+ if (len == 0) {
+ return "empty target name";
+ }
+ // Forbidden start chars:
+ char c = targetName.charAt(0);
+ if (c == '/') {
+ return "target names may not start with '/'";
+ } else if (c == '.') {
+ if (targetName.startsWith("../") || targetName.equals("..")) {
+ return "target names may not contain up-level references '..'";
+ } else if (targetName.equals(".")) {
+ return null; // See comment above; ideally should be an error.
+ } else if (targetName.startsWith("./")) {
+ return "target names may not contain '.' as a path segment";
+ }
+ }
+
+ // Give a friendly error message on CRs in target names
+ if (targetName.endsWith("\r")) {
+ return "target names may not end with carriage returns " +
+ "(perhaps the input source is CRLF-terminated)";
+ }
+
+ for (int ii = 0; ii < len; ++ii) {
+ c = targetName.charAt(ii);
+ if (ALWAYS_ALLOWED_TARGET_CHARACTERS.matches(c)) {
+ continue;
+ }
+ if (c == '.') {
+ continue;
+ }
+ if (c == '/') {
+ if (targetName.substring(ii).startsWith("/../")) {
+ return "target names may not contain up-level references '..'";
+ } else if (targetName.substring(ii).startsWith("/./")) {
+ return "target names may not contain '.' as a path segment";
+ } else if (targetName.substring(ii).startsWith("//")) {
+ return "target names may not contain '//' path separators";
+ }
+ continue;
+ }
+ if (CharMatcher.JAVA_ISO_CONTROL.matches(c)) {
+ return "target names may not contain non-printable characters: '" +
+ String.format("\\x%02X", (int) c) + "'";
+ }
+ return "target names may not contain '" + c + "'";
+ }
+ // Forbidden end chars:
+ if (c == '.' && targetName.endsWith("/..")) {
+ return "target names may not contain up-level references '..'";
+ } else if (c == '.' && targetName.endsWith("/.")) {
+ return null; // See comment above; ideally should be an error.
+ }
+ if (c == '/') {
+ return "target names may not end with '/'";
+ }
+ return null; // ok
+ }
+
+ /**
+ * Validate the label and parse it into a pair of package name and target name. If the label is
+ * not valid, it throws an {@link BadLabelException}.
+ *
+ * <p>It accepts these forms of labels:
+ * <pre>
+ * //foo/bar
+ * //foo/bar:quux
+ * //foo/bar: (undocumented, but accepted)
+ * </pre>
+ */
+ public static PackageAndTarget validateAbsoluteLabel(String absName) throws BadLabelException {
+ PackageAndTarget result = parseAbsoluteLabel(absName);
+ String packageName = result.getPackageName();
+ String targetName = result.getTargetName();
+ String error = validatePackageName(packageName);
+ if (error != null) {
+ error = "invalid package name '" + packageName + "': " + error;
+ // This check is just for a more helpful error message,
+ // i.e. valid target name, invalid package name, colon-free label form
+ // used => probably they meant "//foo:bar.c" not "//foo/bar.c".
+ if (packageName.endsWith("/" + targetName)) {
+ error += " (perhaps you meant \":" + targetName + "\"?)";
+ }
+ throw new BadLabelException(error);
+ }
+ error = validateTargetName(targetName);
+ if (error != null) {
+ error = "invalid target name '" + targetName + "': " + error;
+ throw new BadLabelException(error);
+ }
+ return result;
+ }
+
+ /**
+ * Parses the given absolute label by verifying that it starts with "//". If it contains a ':',
+ * then the part after that is the target name within the package, and the part before that (but
+ * without the leading "//") is the package name. However, it performs no validation on these two
+ * pieces.
+ *
+ * <p>Use of this method is generally not recommended.
+ *
+ * @throws NullPointerException if {@code absName} is {@code null}
+ * @throws BadLabelException if {@code absName} starts with "//"
+ */
+ public static PackageAndTarget parseAbsoluteLabel(String absName) throws BadLabelException {
+ if (!absName.startsWith("//")) {
+ throw new BadLabelException("invalid label: " + absName);
+ }
+ // Find the package/suffix separation:
+ int colonIndex = absName.indexOf(':');
+ int splitAt = colonIndex >= 0 ? colonIndex : absName.length();
+ String packageName = absName.substring("//".length(), splitAt);
+ String suffix = absName.substring(splitAt);
+ // ('suffix' is empty, or starts with a colon.)
+
+ // "If packagename and version are elided, the colon is not necessary."
+ String targetName = suffix.isEmpty()
+ // Target name is last package segment: (works in slash-free case too.)
+ ? packageName.substring(packageName.lastIndexOf('/') + 1)
+ // Target name is what's after colon:
+ : suffix.substring(1);
+
+ return new PackageAndTarget(packageName, targetName);
+ }
+
+ /**
+ * A pair of package and target names. Note that having an instance of this does not imply that
+ * the package or target names are actually valid.
+ */
+ public static class PackageAndTarget {
+ private final String packageName;
+ private final String targetName;
+
+ public PackageAndTarget(String packageName, String targetName) {
+ this.packageName = packageName;
+ this.targetName = targetName;
+ }
+
+ public String getPackageName() {
+ return packageName;
+ }
+
+ public String getTargetName() {
+ return targetName;
+ }
+
+ @Override
+ public String toString() {
+ return "//" + packageName + ":" + targetName;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(packageName, targetName);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == null || o.getClass() != getClass()) {
+ return false;
+ }
+ PackageAndTarget otherTarget = (PackageAndTarget) o;
+ return Objects.equals(otherTarget.targetName, targetName)
+ && Objects.equals(otherTarget.packageName, packageName);
+ }
+ }
+
+ /**
+ * An exception to notify the caller that a label could not be parsed.
+ */
+ public static class BadLabelException extends Exception {
+ public BadLabelException(String msg) {
+ super(msg);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/cmdline/ResolvedTargets.java b/src/main/java/com/google/devtools/build/lib/cmdline/ResolvedTargets.java
new file mode 100644
index 0000000..806cd61
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/cmdline/ResolvedTargets.java
@@ -0,0 +1,170 @@
+// Copyright 2014 Google Inc. 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.cmdline;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+
+import java.util.Collection;
+import java.util.Set;
+
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * Contains the result of the target pattern evaluation. This is a specialized container class for
+ * the result of target pattern resolution. There is no restriction on the element type, but it will
+ * usually be {@code Target}.
+ */
+@Immutable
+public final class ResolvedTargets<T> {
+ private static final ResolvedTargets<?> FAILED_RESULT =
+ new ResolvedTargets<>(ImmutableSet.of(), ImmutableSet.of(), true);
+
+ private static final ResolvedTargets<?> EMPTY_RESULT =
+ new ResolvedTargets<>(ImmutableSet.of(), ImmutableSet.of(), false);
+
+ @SuppressWarnings("unchecked")
+ public static <T> ResolvedTargets<T> failed() {
+ return (ResolvedTargets<T>) FAILED_RESULT;
+ }
+
+ @SuppressWarnings("unchecked")
+ public static <T> ResolvedTargets<T> empty() {
+ return (ResolvedTargets<T>) EMPTY_RESULT;
+ }
+
+ public static <T> ResolvedTargets<T> of(T target) {
+ return new ResolvedTargets<>(ImmutableSet.<T>of(target), false);
+ }
+
+ private final boolean hasError;
+ private final ImmutableSet<T> targets;
+ private final ImmutableSet<T> filteredTargets;
+
+ public ResolvedTargets(Set<T> targets, Set<T> filteredTargets, boolean hasError) {
+ this.targets = ImmutableSet.copyOf(targets);
+ this.filteredTargets = ImmutableSet.copyOf(filteredTargets);
+ this.hasError = hasError;
+ }
+
+ public ResolvedTargets(Set<T> targets, boolean hasError) {
+ this.targets = ImmutableSet.copyOf(targets);
+ this.filteredTargets = ImmutableSet.of();
+ this.hasError = hasError;
+ }
+
+ public boolean hasError() {
+ return hasError;
+ }
+
+ public ImmutableSet<T> getTargets() {
+ return targets;
+ }
+
+ public ImmutableSet<T> getFilteredTargets() {
+ return filteredTargets;
+ }
+
+ /**
+ * Returns a builder using concurrent sets, as long as you don't call filter.
+ */
+ public static <T> ResolvedTargets.Builder<T> concurrentBuilder() {
+ return new ResolvedTargets.Builder<>(
+ Sets.<T>newConcurrentHashSet(),
+ Sets.<T>newConcurrentHashSet());
+ }
+
+ public static <T> ResolvedTargets.Builder<T> builder() {
+ return new ResolvedTargets.Builder<>();
+ }
+
+ public static final class Builder<T> {
+ private Set<T> targets;
+ private Set<T> filteredTargets;
+ private volatile boolean hasError = false;
+
+ private Builder() {
+ this(Sets.<T>newLinkedHashSet(), Sets.<T>newLinkedHashSet());
+ }
+
+ private Builder(Set<T> targets, Set<T> filteredTargets) {
+ this.targets = targets;
+ this.filteredTargets = filteredTargets;
+ }
+
+ public ResolvedTargets<T> build() {
+ return new ResolvedTargets<>(targets, filteredTargets, hasError);
+ }
+
+ public Builder<T> merge(ResolvedTargets<T> other) {
+ removeAll(other.filteredTargets);
+ addAll(other.targets);
+ if (other.hasError) {
+ hasError = true;
+ }
+ return this;
+ }
+
+ public Builder<T> add(T target) {
+ targets.add(target);
+ filteredTargets.remove(target);
+ return this;
+ }
+
+ public Builder<T> addAll(Collection<T> targets) {
+ this.targets.addAll(targets);
+ this.filteredTargets.removeAll(targets);
+ return this;
+ }
+
+ public void remove(T target) {
+ targets.remove(target);
+ filteredTargets.add(target);
+ }
+
+ public Builder<T> removeAll(Collection<T> targets) {
+ this.filteredTargets.addAll(targets);
+ this.targets.removeAll(targets);
+ return this;
+ }
+
+ public Builder<T> filter(Predicate<T> predicate) {
+ Set<T> oldTargets = targets;
+ targets = Sets.newLinkedHashSet();
+ for (T target : oldTargets) {
+ if (predicate.apply(target)) {
+ add(target);
+ } else {
+ remove(target);
+ }
+ }
+ return this;
+ }
+
+ public Builder<T> setError() {
+ this.hasError = true;
+ return this;
+ }
+
+ public Builder<T> mergeError(boolean hasError) {
+ this.hasError |= hasError;
+ return this;
+ }
+
+ public boolean isEmpty() {
+ return targets.isEmpty();
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/cmdline/TargetParsingException.java b/src/main/java/com/google/devtools/build/lib/cmdline/TargetParsingException.java
new file mode 100644
index 0000000..044bcca
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/cmdline/TargetParsingException.java
@@ -0,0 +1,29 @@
+// Copyright 2014 Google Inc. 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.cmdline;
+
+import com.google.common.base.Preconditions;
+
+/**
+ * Indicates that a target label cannot be parsed.
+ */
+public class TargetParsingException extends Exception {
+ public TargetParsingException(String message) {
+ super(Preconditions.checkNotNull(message));
+ }
+
+ public TargetParsingException(String message, Throwable cause) {
+ super(Preconditions.checkNotNull(message), cause);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/cmdline/TargetPattern.java b/src/main/java/com/google/devtools/build/lib/cmdline/TargetPattern.java
new file mode 100644
index 0000000..6677970
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/cmdline/TargetPattern.java
@@ -0,0 +1,464 @@
+// Copyright 2014 Google Inc. 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.cmdline;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.cmdline.LabelValidator.BadLabelException;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * Represents a target pattern. Target patterns are a generalization of labels to include
+ * wildcards for finding all packages recursively beneath some root, and for finding all targets
+ * within a package.
+ *
+ * <p>Note that this class does not handle negative patterns ("-//foo/bar"); these must be handled
+ * one level up. In particular, the query language comes with built-in support for negative
+ * patterns.
+ *
+ * <p>In order to resolve target patterns, you need an implementation of {@link
+ * TargetPatternResolver}. This class is thread-safe if the corresponding instance is thread-safe.
+ *
+ * <p>See lib/blaze/commands/target-syntax.txt for details.
+ */
+public abstract class TargetPattern {
+
+ private static final Splitter SLASH_SPLITTER = Splitter.on('/');
+ private static final Joiner SLASH_JOINER = Joiner.on('/');
+
+ private static final Parser DEFAULT_PARSER = new Parser("");
+
+ private final Type type;
+
+ /**
+ * Returns a parser with no offset. Note that the Parser class is immutable, so this method may
+ * return the same instance on subsequent calls.
+ */
+ public static Parser defaultParser() {
+ return DEFAULT_PARSER;
+ }
+
+ private static String removeSuffix(String s, String suffix) {
+ if (s.endsWith(suffix)) {
+ return s.substring(0, s.length() - suffix.length());
+ } else {
+ throw new IllegalArgumentException(s + ", " + suffix);
+ }
+ }
+
+ /**
+ * Normalizes the given relative path by resolving {@code //}, {@code /./} and {@code x/../}
+ * pieces. Note that leading {@code ".."} segments are not removed, so the returned string can
+ * have leading {@code ".."} segments.
+ *
+ * @throws IllegalArgumentException if the path is absolute, i.e. starts with a @{code '/'}
+ */
+ @VisibleForTesting
+ static String normalize(String path) {
+ Preconditions.checkArgument(!path.startsWith("/"));
+ Iterator<String> it = SLASH_SPLITTER.split(path).iterator();
+ List<String> pieces = new ArrayList<>();
+ while (it.hasNext()) {
+ String piece = it.next();
+ if (".".equals(piece) || piece.isEmpty()) {
+ continue;
+ }
+ if ("..".equals(piece)) {
+ if (pieces.isEmpty()) {
+ pieces.add(piece);
+ continue;
+ }
+ String predecessor = pieces.remove(pieces.size() - 1);
+ if ("..".equals(predecessor)) {
+ pieces.add(piece);
+ pieces.add(piece);
+ }
+ continue;
+ }
+ pieces.add(piece);
+ }
+ return SLASH_JOINER.join(pieces);
+ }
+
+ private TargetPattern(Type type) {
+ // Don't allow inheritance outside this class.
+ this.type = type;
+ }
+
+ /**
+ * Return the type of the pattern. Examples include "below package" like "foo/..." and "single
+ * target" like "//x:y".
+ */
+ public Type getType() {
+ return type;
+ }
+
+ /**
+ * Evaluates the current target pattern and returns the result.
+ */
+ public abstract <T> ResolvedTargets<T> eval(TargetPatternResolver<T> resolver)
+ throws TargetParsingException, InterruptedException,
+ TargetPatternResolver.MissingDepException;
+
+ private static final class SingleTarget extends TargetPattern {
+
+ private final String targetName;
+
+ private SingleTarget(String targetName) {
+ super(Type.SINGLE_TARGET);
+ this.targetName = targetName;
+ }
+
+ @Override
+ public <T> ResolvedTargets<T> eval(TargetPatternResolver<T> resolver)
+ throws TargetParsingException, InterruptedException,
+ TargetPatternResolver.MissingDepException {
+ return resolver.getExplicitTarget(targetName);
+ }
+ }
+
+ private static final class InterpretPathAsTarget extends TargetPattern {
+
+ private final String path;
+
+ private InterpretPathAsTarget(String path) {
+ super(Type.PATH_AS_TARGET);
+ this.path = normalize(path);
+ }
+
+ @Override
+ public <T> ResolvedTargets<T> eval(TargetPatternResolver<T> resolver)
+ throws TargetParsingException, InterruptedException,
+ TargetPatternResolver.MissingDepException {
+ if (resolver.isPackage(path)) {
+ // User has specified a package name. Issue a helpful error message.
+ throw new TargetParsingException("ambiguous target pattern: '" + path + "' is "
+ + "the name of a package; use '" + path + ":all' to mean \"all "
+ + "rules in this package\", '" + path + "/...' to mean \"all rules recursively "
+ + "beneath this package\", or '//" + path + "' to mean \"the default rule in this "
+ + "package\"");
+ }
+
+ List<String> pieces = SLASH_SPLITTER.splitToList(path);
+
+ // Interprets the label as a file target. This loop stops as soon as the
+ // first BUILD file is found (i.e. longest prefix match).
+ for (int i = pieces.size() - 1; i > 0; i--) {
+ String packageName = SLASH_JOINER.join(pieces.subList(0, i));
+ if (resolver.isPackage(packageName)) {
+ String targetName = SLASH_JOINER.join(pieces.subList(i, pieces.size()));
+ return resolver.getExplicitTarget("//" + packageName + ":" + targetName);
+ }
+ }
+
+ throw new TargetParsingException(
+ "couldn't determine target from filename '" + path + "'");
+ }
+ }
+
+ private static final class TargetsInPackage extends TargetPattern {
+
+ private final String originalPattern;
+ private final String pattern;
+ private final String suffix;
+ private final boolean isAbsolute;
+ private final boolean rulesOnly;
+ private final boolean checkWildcardConflict;
+
+ private TargetsInPackage(String originalPattern, String pattern, String suffix,
+ boolean isAbsolute, boolean rulesOnly, boolean checkWildcardConflict) {
+ super(Type.TARGETS_IN_PACKAGE);
+ this.originalPattern = originalPattern;
+ this.pattern = pattern;
+ this.suffix = suffix;
+ this.isAbsolute = isAbsolute;
+ this.rulesOnly = rulesOnly;
+ this.checkWildcardConflict = checkWildcardConflict;
+ }
+
+ @Override
+ public <T> ResolvedTargets<T> eval(TargetPatternResolver<T> resolver)
+ throws TargetParsingException, InterruptedException,
+ TargetPatternResolver.MissingDepException {
+ if (checkWildcardConflict) {
+ ResolvedTargets<T> targets = getWildcardConflict(resolver);
+ if (targets != null) {
+ return targets;
+ }
+ }
+ return resolver.getTargetsInPackage(originalPattern, removeSuffix(pattern, suffix),
+ rulesOnly);
+ }
+
+ /**
+ * There's a potential ambiguity if '//foo/bar:all' refers to an actual target. In this case, we
+ * use the the target but print a warning.
+ *
+ * @return the Target corresponding to the given pattern, if the pattern is absolute and there
+ * is such a target. Otherwise, return null.
+ */
+ private <T> ResolvedTargets<T> getWildcardConflict(TargetPatternResolver<T> resolver)
+ throws InterruptedException, TargetPatternResolver.MissingDepException {
+ if (!isAbsolute) {
+ return null;
+ }
+
+ T target = resolver.getTargetOrNull("//" + pattern);
+ if (target != null) {
+ String name = pattern.lastIndexOf(':') != -1
+ ? pattern.substring(pattern.lastIndexOf(':') + 1)
+ : pattern.substring(pattern.lastIndexOf('/') + 1);
+ resolver.warn(String.format("The Blaze target pattern '%s' is ambiguous: '%s' is " +
+ "both a wildcard, and the name of an existing %s; " +
+ "using the latter interpretation",
+ "//" + pattern, ":" + name,
+ resolver.getTargetKind(target)));
+ try {
+ return resolver.getExplicitTarget("//" + pattern);
+ } catch (TargetParsingException e) {
+ throw new IllegalStateException(
+ "getTargetOrNull() returned non-null, so target should exist", e);
+ }
+ }
+ return null;
+ }
+ }
+
+ private static final class TargetsBelowPackage extends TargetPattern {
+
+ private final String originalPattern;
+ private final String pathPrefix;
+ private final boolean rulesOnly;
+
+ private TargetsBelowPackage(String originalPattern, String pathPrefix, boolean rulesOnly) {
+ super(Type.TARGETS_BELOW_PACKAGE);
+ this.originalPattern = originalPattern;
+ this.pathPrefix = pathPrefix;
+ this.rulesOnly = rulesOnly;
+ }
+
+ @Override
+ public <T> ResolvedTargets<T> eval(TargetPatternResolver<T> resolver)
+ throws TargetParsingException, InterruptedException,
+ TargetPatternResolver.MissingDepException {
+ return resolver.findTargetsBeneathDirectory(originalPattern, pathPrefix, rulesOnly);
+ }
+ }
+
+ @Immutable
+ public static final class Parser {
+ // TODO(bazel-team): Merge the Label functionality that requires similar constants into this
+ // class.
+ /**
+ * The set of target-pattern suffixes which indicate wildcards over all <em>rules</em> in a
+ * single package.
+ */
+ private static final List<String> ALL_RULES_IN_SUFFIXES = ImmutableList.of(
+ "all");
+
+ /**
+ * The set of target-pattern suffixes which indicate wildcards over all <em>targets</em> in a
+ * single package.
+ */
+ private static final List<String> ALL_TARGETS_IN_SUFFIXES = ImmutableList.of(
+ "*",
+ "all-targets");
+
+ private static final List<String> SUFFIXES;
+
+ static {
+ SUFFIXES = ImmutableList.<String>builder()
+ .addAll(ALL_RULES_IN_SUFFIXES)
+ .addAll(ALL_TARGETS_IN_SUFFIXES)
+ .add("/...")
+ .build();
+ }
+
+ /**
+ * Returns whether the given pattern is simple, i.e., not starting with '-' and using none of
+ * the target matching suffixes.
+ */
+ public static boolean isSimpleTargetPattern(String pattern) {
+ if (pattern.startsWith("-")) {
+ return false;
+ }
+
+ for (String suffix : SUFFIXES) {
+ if (pattern.endsWith(":" + suffix)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Directory prefix to use when resolving relative labels (rather than absolute ones). For
+ * example, if the working directory is "<workspace root>/foo", then this should be "foo",
+ * which will make patterns such as "bar:bar" be resolved as "//foo/bar:bar". This makes the
+ * command line a bit more convenient to use.
+ */
+ private final String relativeDirectory;
+
+ /**
+ * Creates a new parser with the given offset for relative patterns.
+ */
+ public Parser(String relativeDirectory) {
+ this.relativeDirectory = relativeDirectory;
+ }
+
+ /**
+ * Parses the given pattern, and throws an exception if the pattern is invalid.
+ *
+ * @return a target pattern corresponding to the pattern parsed
+ * @throws TargetParsingException if the pattern is invalid
+ */
+ public TargetPattern parse(String pattern) throws TargetParsingException {
+ // The structure of this method is by cases, according to the usage string
+ // constant (see lib/blaze/commands/target-syntax.txt).
+
+ String originalPattern = pattern;
+ final boolean isAbsolute = pattern.startsWith("//");
+
+ // We now absolutize non-absolute target patterns.
+ pattern = isAbsolute ? pattern.substring(2) : absolutize(pattern);
+ // Check for common errors.
+ if (pattern.startsWith("/")) {
+ throw new TargetParsingException("not a relative path or label: '" + pattern + "'");
+ }
+ if (pattern.isEmpty()) {
+ throw new TargetParsingException("the empty string is not a valid target");
+ }
+
+ // Transform "/BUILD" suffix into ":BUILD" to accept //foo/bar/BUILD
+ // syntax as a synonym to //foo/bar:BUILD.
+ if (pattern.endsWith("/BUILD")) {
+ pattern = pattern.substring(0, pattern.length() - 6) + ":BUILD";
+ }
+
+ int colonIndex = pattern.lastIndexOf(':');
+ String packagePart = colonIndex < 0 ? pattern : pattern.substring(0, colonIndex);
+ String targetPart = colonIndex < 0 ? "" : pattern.substring(colonIndex + 1);
+
+ if (packagePart.equals("...")) {
+ packagePart = "/..."; // special case this for easier parsing
+ }
+
+ if (packagePart.endsWith("/")) {
+ throw new TargetParsingException("The package part of '" + originalPattern
+ + "' should not end in a slash");
+ }
+
+ if (packagePart.endsWith("/...")) {
+ String realPackagePart = removeSuffix(packagePart, "/...");
+ if (targetPart.isEmpty() || ALL_RULES_IN_SUFFIXES.contains(targetPart)) {
+ return new TargetsBelowPackage(originalPattern, realPackagePart, true);
+ } else if (ALL_TARGETS_IN_SUFFIXES.contains(targetPart)) {
+ return new TargetsBelowPackage(originalPattern, realPackagePart, false);
+ }
+ }
+
+ if (ALL_RULES_IN_SUFFIXES.contains(targetPart)) {
+ return new TargetsInPackage(
+ originalPattern, pattern, ":" + targetPart, isAbsolute, true, true);
+ }
+
+ if (ALL_TARGETS_IN_SUFFIXES.contains(targetPart)) {
+ return new TargetsInPackage(
+ originalPattern, pattern, ":" + targetPart, isAbsolute, false, true);
+ }
+
+
+ if (isAbsolute || pattern.contains(":")) {
+ String fullLabel = "//" + pattern;
+ try {
+ LabelValidator.validateAbsoluteLabel(fullLabel);
+ } catch (BadLabelException e) {
+ String error = "invalid target format '" + originalPattern + "': " + e.getMessage();
+ throw new TargetParsingException(error);
+ }
+ return new SingleTarget(fullLabel);
+ }
+
+ // This is a stripped-down version of interpretPathAsTarget that does no I/O. We have a basic
+ // relative path. e.g. "foo/bar/Wiz.java". The strictest correct check we can do here (without
+ // I/O) is just to ensure that there is *some* prefix that is a valid package-name. It's
+ // sufficient to test the first segment. This is really a rather weak check; perhaps we should
+ // just eliminate it.
+ int slashIndex = pattern.indexOf('/');
+ if (slashIndex < 0) {
+ throw new TargetParsingException("ambiguous target pattern: '" + pattern + "' could "
+ + "potentially be the name of a package; use '" + pattern + ":all' to mean \"all "
+ + "rules in this package\", '" + pattern + "/...' to mean \"all rules recursively "
+ + "beneath this package\", or '//" + pattern + "' to mean \"the default rule in this "
+ + "package\"");
+ }
+ String errorMessage = LabelValidator.validatePackageName(pattern.substring(0, slashIndex));
+ if (errorMessage != null) {
+ throw new TargetParsingException("Bad target pattern '" + originalPattern + "': " +
+ errorMessage);
+ }
+ return new InterpretPathAsTarget(pattern);
+ }
+
+ /**
+ * Absolutizes the target pattern to the offset.
+ * Patterns starting with "/" are absolute and not modified.
+ *
+ * If the offset is "foo":
+ * absolutize(":bar") --> "foo:bar"
+ * absolutize("bar") --> "foo/bar"
+ * absolutize("/biz/bar") --> "biz/bar" (absolute)
+ * absolutize("biz:bar") --> "foo/biz:bar"
+ *
+ * @param pattern The target pattern to parse.
+ * @return the pattern, absolutized to the offset if approprate.
+ */
+ private String absolutize(String pattern) {
+ if (relativeDirectory.isEmpty() || pattern.startsWith("/")) {
+ return pattern;
+ }
+
+ // It seems natural to use {@link PathFragment#getRelative()} here,
+ // but it doesn't work when the pattern starts with ":".
+ // "foo".getRelative(":all") would return "foo/:all", where we
+ // really want "foo:all".
+ return pattern.startsWith(":")
+ ? relativeDirectory + pattern
+ : relativeDirectory + "/" + pattern;
+ }
+ }
+
+ /**
+ * The target pattern type (targets below package, in package, explicit target, etc.)
+ */
+ public enum Type {
+ /** A path interpreted as a target, eg "foo/bar/baz" */
+ PATH_AS_TARGET,
+ /** An explicit target, eg "//foo:bar." */
+ SINGLE_TARGET,
+ /** Targets below a package, eg "foo/...". */
+ TARGETS_BELOW_PACKAGE,
+ /** Target in a package, eg "foo:all". */
+ TARGETS_IN_PACKAGE;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/cmdline/TargetPatternResolver.java b/src/main/java/com/google/devtools/build/lib/cmdline/TargetPatternResolver.java
new file mode 100644
index 0000000..109179f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/cmdline/TargetPatternResolver.java
@@ -0,0 +1,94 @@
+// Copyright 2014 Google Inc. 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.cmdline;
+
+/**
+ * A callback interface that is used during the process of converting target patterns (such as
+ * <code>//foo:all</code>) into one or more lists of targets (such as <code>//foo:foo,
+ * //foo:bar</code>). During a call to {@link TargetPattern#eval}, the {@link TargetPattern} makes
+ * calls to this interface to implement the target pattern semantics. The generic type {@code T} is
+ * only for compile-time type safety; there are no requirements to the actual type.
+ */
+public interface TargetPatternResolver<T> {
+
+ /**
+ * Reports the given warning.
+ */
+ void warn(String msg);
+
+ /**
+ * Returns a single target corresponding to the given name, or null. This method may only throw an
+ * exception if the current thread was interrupted.
+ */
+ T getTargetOrNull(String targetName) throws InterruptedException, MissingDepException;
+
+ /**
+ * Returns a single target corresponding to the given name, or an empty or failed result.
+ */
+ ResolvedTargets<T> getExplicitTarget(String targetName)
+ throws TargetParsingException, InterruptedException, MissingDepException;
+
+ /**
+ * Returns the set containing the targets found in the given package. The specified directory is
+ * not necessarily a valid package name. If {@code rulesOnly} is true, then this method should
+ * only return rules in the given package.
+ *
+ * @param originalPattern the original target pattern for error reporting purposes
+ * @param packageName the name of the package
+ * @param rulesOnly whether to return rules only
+ */
+ ResolvedTargets<T> getTargetsInPackage(String originalPattern, String packageName,
+ boolean rulesOnly) throws TargetParsingException, InterruptedException, MissingDepException;
+
+ /**
+ * Returns the set containing the targets found below the given {@code pathPrefix}. Conceptually,
+ * this method should look for all packages that start with the {@code pathPrefix} (as a proper
+ * prefix directory, i.e., "foo/ba" is not a proper prefix of "foo/bar/"), and then collect all
+ * targets in each such package (subject to {@code rulesOnly}) as if calling {@link
+ * #getTargetsInPackage}. The specified directory is not necessarily a valid package name.
+ *
+ * <p>Note that the {@code pathPrefix} can be empty, which corresponds to the "//..." pattern.
+ * Implementations may choose not to support this case and throw an exception instead, or may
+ * restrict the set of directories that are considered by default.
+ *
+ * <p>If the {@code pathPrefix} points to a package, then that package should also be part of the
+ * result.
+ *
+ * @param originalPattern the original target pattern for error reporting purposes
+ * @param pathPrefix the directory in which to look for packages
+ * @param rulesOnly whether to return rules only
+ */
+ ResolvedTargets<T> findTargetsBeneathDirectory(String originalPattern, String pathPrefix,
+ boolean rulesOnly) throws TargetParsingException, InterruptedException, MissingDepException;
+
+ /**
+ * Returns true, if and only if the given name corresponds to a package, i.e., a file with the
+ * name {@code packageName/BUILD} exists.
+ */
+ boolean isPackage(String packageName) throws MissingDepException;
+
+ /**
+ * Returns the target kind of the given target, for example {@code cc_library rule}.
+ */
+ String getTargetKind(T target);
+
+ /**
+ * A missing dependency is needed before target parsing can proceed. Currently used only in
+ * skyframe to notify the framework of missing dependencies.
+ */
+ // TODO(bazel-team): Avoid this use of exception for expected control flow management.
+ public class MissingDepException extends Exception {
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/collect/CollectionUtils.java b/src/main/java/com/google/devtools/build/lib/collect/CollectionUtils.java
new file mode 100644
index 0000000..d7b04bb
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/collect/CollectionUtils.java
@@ -0,0 +1,225 @@
+// Copyright 2014 Google Inc. 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.collect;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Utilities for collection classes.
+ */
+public final class CollectionUtils {
+
+ private CollectionUtils() {}
+
+ /**
+ * Given a collection of elements and an equivalence relation, returns a new
+ * unordered collection of the disjoint subsets of those elements which are
+ * equivalent under the specified relation.
+ *
+ * <p>Note: the Comparator needs only to implement the less-strict contract
+ * of EquivalenceRelation (q.v.). (Hopefully this will one day be a
+ * superinterface of Comparator.)
+ *
+ * @param elements the collection of elements to be partitioned. May
+ * contain duplicates.
+ * @param equivalenceRelation an equivalence relation over the elements.
+ * @return a collection of sets of elements that are equivalent under the
+ * specified relation.
+ */
+ public static <T> Collection<Set<T>> partition(Collection<T> elements,
+ Comparator<T> equivalenceRelation) {
+ // TODO(bazel-team): (2009) O(n*m) where n=|elements| and m=|eqClasses|; i.e.,
+ // quadratic. Use Tarjan's algorithm instead.
+ List<Set<T>> eqClasses = new ArrayList<>();
+ for (T element : elements) {
+ boolean found = false;
+ for (Set<T> eqClass : eqClasses) {
+ if (equivalenceRelation.compare(eqClass.iterator().next(),
+ element) == 0) {
+ eqClass.add(element);
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ Set<T> eqClass = new HashSet<>();
+ eqClass.add(element);
+ eqClasses.add(eqClass);
+ }
+ }
+ return eqClasses;
+ }
+
+ /**
+ * See partition(Collection, Comparator).
+ */
+ public static <T> Collection<Set<T>> partition(Collection<T> elements,
+ final EquivalenceRelation<T> equivalenceRelation) {
+ return partition(elements, new Comparator<T>() {
+ @Override
+ public int compare(T o1, T o2) {
+ return equivalenceRelation.compare(o1, o2);
+ }
+ });
+ }
+
+ /**
+ * Returns the set of all elements in the given collection that appear more than once.
+ * @param input some collection.
+ * @return the set of repeated elements. May return an empty set, but never null.
+ */
+ public static <T> Set<T> duplicatedElementsOf(Collection<T> input) {
+ Set<T> duplicates = new HashSet<>();
+ Set<T> elementSet = new HashSet<>();
+ for (T el : input) {
+ if (!elementSet.add(el)) {
+ duplicates.add(el);
+ }
+ }
+ return duplicates;
+ }
+
+ /**
+ * Returns an immutable list of all non-null parameters in the order in which
+ * they are specified.
+ */
+ @SuppressWarnings("unchecked")
+ public static <T> ImmutableList<T> asListWithoutNulls(T... elements) {
+ ImmutableList.Builder<T> builder = ImmutableList.builder();
+ for (T element : elements) {
+ if (element != null) {
+ builder.add(element);
+ }
+ }
+ return builder.build();
+ }
+
+ /**
+ * Returns true if the given iterable can be verified to be immutable.
+ *
+ * <p>Note that if this method returns false, that does not mean that the iterable is mutable.
+ */
+ public static <T> boolean isImmutable(Iterable<T> iterable) {
+ return iterable instanceof ImmutableList<?>
+ || iterable instanceof ImmutableSet<?>
+ || iterable instanceof IterablesChain<?>
+ || iterable instanceof NestedSet<?>
+ || iterable instanceof ImmutableIterable<?>;
+ }
+
+ /**
+ * Throws a runtime exception if the given iterable can not be verified to be immutable.
+ */
+ public static <T> void checkImmutable(Iterable<T> iterable) {
+ Preconditions.checkState(isImmutable(iterable), iterable.getClass());
+ }
+
+ /**
+ * Given an iterable, returns an immutable iterable with the same contents.
+ */
+ public static <T> Iterable<T> makeImmutable(Iterable<T> iterable) {
+ if (isImmutable(iterable)) {
+ return iterable;
+ } else {
+ return ImmutableList.copyOf(iterable);
+ }
+ }
+
+ /**
+ * Converts a set of enum values to a bit field. Requires that the enum contains at most 32
+ * elements.
+ */
+ public static <T extends Enum<T>> int toBits(Set<T> values) {
+ int result = 0;
+ for (T value : values) {
+ // <p>Note that when the 32. bit is set, the integer becomes negative (because that is the
+ // sign bit). This does not affect the function of the bitwise operators, so it is fine.
+ Preconditions.checkArgument(value.ordinal() < 32);
+ result |= (1 << value.ordinal());
+ }
+
+ return result;
+ }
+
+ /**
+ * Converts a set of enum values to a bit field. Requires that the enum contains at most 32
+ * elements.
+ */
+ @SuppressWarnings("unchecked")
+ public static <T extends Enum<T>> int toBits(T... values) {
+ return toBits(ImmutableSet.copyOf(values));
+ }
+
+ /**
+ * Converts a bit field to a set of enum values. Requires that the enum contains at most 32
+ * elements.
+ */
+ public static <T extends Enum<T>> EnumSet<T> fromBits(int value, Class<T> clazz) {
+ T[] elements = clazz.getEnumConstants();
+ Preconditions.checkArgument(elements.length <= 32);
+ ArrayList<T> result = new ArrayList<>();
+ for (T element : elements) {
+ if ((value & (1 << element.ordinal())) != 0) {
+ result.add(element);
+ }
+ }
+
+ return result.isEmpty() ? EnumSet.noneOf(clazz) : EnumSet.copyOf(result);
+ }
+
+ /**
+ * Returns whether an {@link Iterable} is a superset of another one.
+ */
+ public static <T> boolean containsAll(Iterable<T> superset, Iterable<T> subset) {
+ return ImmutableSet.copyOf(superset).containsAll(ImmutableList.copyOf(subset));
+ }
+
+ /**
+ * Returns an ImmutableMap of ImmutableMaps created from the Map of Maps parameter.
+ */
+ public static <KEY_1, KEY_2, VALUE> ImmutableMap<KEY_1, ImmutableMap<KEY_2, VALUE>> toImmutable(
+ Map<KEY_1, Map<KEY_2, VALUE>> map) {
+ ImmutableMap.Builder<KEY_1, ImmutableMap<KEY_2, VALUE>> builder = ImmutableMap.builder();
+ for (Map.Entry<KEY_1, Map<KEY_2, VALUE>> entry : map.entrySet()) {
+ builder.put(entry.getKey(), ImmutableMap.copyOf(entry.getValue()));
+ }
+ return builder.build();
+ }
+
+ /**
+ * Returns a copy of the Map of Maps parameter.
+ */
+ public static <KEY_1, KEY_2, VALUE> Map<KEY_1, Map<KEY_2, VALUE>> copyOf(
+ Map<KEY_1, ? extends Map<KEY_2, VALUE>> map) {
+ Map<KEY_1, Map<KEY_2, VALUE>> result = new HashMap<>();
+ for (Map.Entry<KEY_1, ? extends Map<KEY_2, VALUE>> entry : map.entrySet()) {
+ result.put(entry.getKey(), new HashMap<>(entry.getValue()));
+ }
+ return result;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/collect/CompactHashSet.java b/src/main/java/com/google/devtools/build/lib/collect/CompactHashSet.java
new file mode 100644
index 0000000..5ee2417
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/collect/CompactHashSet.java
@@ -0,0 +1,604 @@
+// Copyright 2014 Google Inc. 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.
+/*
+ * Copyright (C) 2012 The Guava Authors
+ *
+ * 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.collect;
+
+import com.google.common.base.Preconditions;
+import com.google.common.primitives.Ints;
+
+import java.io.IOException;
+import java.io.InvalidObjectException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.lang.reflect.Array;
+import java.util.AbstractSet;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.ConcurrentModificationException;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+
+import javax.annotation.Nullable;
+
+/**
+ * CompactHashSet is an implementation of a Set. All optional operations (adding and
+ * removing) are supported. The elements can be any objects.
+ *
+ * <p>{@code contains(x)}, {@code add(x)} and {@code remove(x)}, are all (expected and amortized)
+ * constant time operations. Expected in the hashtable sense (depends on the hash function
+ * doing a good job of distributing the elements to the buckets to a distribution not far from
+ * uniform), and amortized since some operations can trigger a hash table resize.
+ *
+ * <p>Unlike {@code java.util.HashSet}, iteration is only proportional to the actual
+ * {@code size()}, which is optimal, and <i>not</i> the size of the internal hashtable,
+ * which could be much larger than {@code size()}. Furthermore, this structure only depends
+ * on a fixed number of arrays; {@code add(x)} operations <i>do not</i> create objects
+ * for the garbage collector to deal with, and for every element added, the garbage collector
+ * will have to traverse {@code 1.5} references on average, in the marking phase, not {@code 5.0}
+ * as in {@code java.util.HashSet}.
+ *
+ * <p>If there are no removals, then {@link #iterator iteration} order is the same as insertion
+ * order. Any removal invalidates any ordering guarantees.
+ */
+// TODO(bazel-team): This was branched of an internal version of guava. If the class is released, we
+// should remove this again.
+public class CompactHashSet<E> extends AbstractSet<E> implements Serializable {
+ // TODO(bazel-team): cache all field accesses in local vars
+
+ // A partial copy of com.google.common.collect.Hashing.
+ private static final int C1 = 0xcc9e2d51;
+ private static final int C2 = 0x1b873593;
+
+ /*
+ * This method was rewritten in Java from an intermediate step of the Murmur hash function in
+ * http://code.google.com/p/smhasher/source/browse/trunk/MurmurHash3.cpp, which contained the
+ * following header:
+ *
+ * MurmurHash3 was written by Austin Appleby, and is placed in the public domain. The author
+ * hereby disclaims copyright to this source code.
+ */
+ private static int smear(int hashCode) {
+ return C2 * Integer.rotateLeft(hashCode * C1, 15);
+ }
+
+ private static int smearedHash(@Nullable Object o) {
+ return smear((o == null) ? 0 : o.hashCode());
+ }
+
+ private static final int MAX_TABLE_SIZE = Ints.MAX_POWER_OF_TWO;
+
+ private static int closedTableSize(int expectedEntries, double loadFactor) {
+ // Get the recommended table size.
+ // Round down to the nearest power of 2.
+ expectedEntries = Math.max(expectedEntries, 2);
+ int tableSize = Integer.highestOneBit(expectedEntries);
+ // Check to make sure that we will not exceed the maximum load factor.
+ if (expectedEntries > (int) (loadFactor * tableSize)) {
+ tableSize <<= 1;
+ return (tableSize > 0) ? tableSize : MAX_TABLE_SIZE;
+ }
+ return tableSize;
+ }
+
+ /**
+ * Creates an empty {@code CompactHashSet} instance.
+ */
+ public static <E> CompactHashSet<E> create() {
+ return new CompactHashSet<E>();
+ }
+
+ /**
+ * Creates a <i>mutable</i> {@code CompactHashSet} instance containing the elements
+ * of the given collection in unspecified order.
+ *
+ * @param collection the elements that the set should contain
+ * @return a new {@code CompactHashSet} containing those elements (minus duplicates)
+ */
+ public static <E> CompactHashSet<E> create(Collection<? extends E> collection) {
+ CompactHashSet<E> set = createWithExpectedSize(collection.size());
+ set.addAll(collection);
+ return set;
+ }
+
+ /**
+ * Creates a <i>mutable</i> {@code CompactHashSet} instance containing the given
+ * elements in unspecified order.
+ *
+ * @param elements the elements that the set should contain
+ * @return a new {@code CompactHashSet} containing those elements (minus duplicates)
+ */
+ @SafeVarargs
+ public static <E> CompactHashSet<E> create(E... elements) {
+ CompactHashSet<E> set = createWithExpectedSize(elements.length);
+ Collections.addAll(set, elements);
+ return set;
+ }
+
+ /**
+ * Creates a {@code CompactHashSet} instance, with a high enough "initial capacity"
+ * that it <i>should</i> hold {@code expectedSize} elements without growth.
+ *
+ * @param expectedSize the number of elements you expect to add to the returned set
+ * @return a new, empty {@code CompactHashSet} with enough capacity to hold {@code
+ * expectedSize} elements without resizing
+ * @throws IllegalArgumentException if {@code expectedSize} is negative
+ */
+ public static <E> CompactHashSet<E> createWithExpectedSize(int expectedSize) {
+ return new CompactHashSet<E>(expectedSize);
+ }
+
+ private static final int MAXIMUM_CAPACITY = 1 << 30;
+
+ // TODO(bazel-team): decide, and inline, load factor. 0.75?
+ private static final float DEFAULT_LOAD_FACTOR = 1.0f;
+
+ /**
+ * Bitmask that selects the low 32 bits.
+ */
+ private static final long NEXT_MASK = (1L << 32) - 1;
+
+ /**
+ * Bitmask that selects the high 32 bits.
+ */
+ private static final long HASH_MASK = ~NEXT_MASK;
+
+ // TODO(bazel-team): decide default size
+ private static final int DEFAULT_SIZE = 3;
+
+ static final int UNSET = -1;
+
+ /**
+ * The hashtable. Its values are indexes to both the elements and entries arrays.
+ *
+ * Currently, the UNSET value means "null pointer", and any non negative value x is
+ * the actual index.
+ *
+ * Its size must be a power of two.
+ */
+ private transient int[] table;
+
+ /**
+ * Contains the logical entries, in the range of [0, size()). The high 32 bits of each
+ * long is the smeared hash of the element, whereas the low 32 bits is the "next" pointer
+ * (pointing to the next entry in the bucket chain). The pointers in [size(), entries.length)
+ * are all "null" (UNSET).
+ */
+ private transient long[] entries;
+
+ /**
+ * The elements contained in the set, in the range of [0, size()).
+ */
+ transient Object[] elements;
+
+ /**
+ * The load factor.
+ */
+ transient float loadFactor;
+
+ /**
+ * Keeps track of modifications of this set, to make it possible to throw
+ * ConcurrentModificationException in the iterator. Note that we choose not to
+ * make this volatile, so we do less of a "best effort" to track such errors,
+ * for better performance.
+ */
+ transient int modCount;
+
+ /**
+ * When we have this many elements, resize the hashtable.
+ */
+ private transient int threshold;
+
+ /**
+ * The number of elements contained in the set.
+ */
+ private transient int size;
+
+ /**
+ * Constructs a new empty instance of {@code CompactHashSet}.
+ */
+ CompactHashSet() {
+ init(DEFAULT_SIZE, DEFAULT_LOAD_FACTOR);
+ }
+
+ /**
+ * Constructs a new instance of {@code CompactHashSet} with the specified capacity.
+ *
+ * @param expectedSize the initial capacity of this {@code CompactHashSet}.
+ */
+ CompactHashSet(int expectedSize) {
+ init(expectedSize, DEFAULT_LOAD_FACTOR);
+ }
+
+ /**
+ * Pseudoconstructor for serialization support.
+ */
+ void init(int expectedSize, float loadFactor) {
+ Preconditions.checkArgument(expectedSize >= 0, "Initial capacity must be non-negative");
+ Preconditions.checkArgument(loadFactor > 0, "Illegal load factor");
+ int buckets = closedTableSize(expectedSize, loadFactor);
+ this.table = newTable(buckets);
+ this.loadFactor = loadFactor;
+ this.elements = new Object[expectedSize];
+ this.entries = newEntries(expectedSize);
+ this.threshold = Math.max(1, (int) (buckets * loadFactor));
+ }
+
+ private static int[] newTable(int size) {
+ int[] array = new int[size];
+ Arrays.fill(array, UNSET);
+ return array;
+ }
+
+ private static long[] newEntries(int size) {
+ long[] array = new long[size];
+ Arrays.fill(array, UNSET);
+ return array;
+ }
+
+ private static int getHash(long entry) {
+ return (int) (entry >>> 32);
+ }
+
+ /**
+ * Returns the index, or UNSET if the pointer is "null"
+ */
+ private static int getNext(long entry) {
+ return (int) entry;
+ }
+
+ /**
+ * Returns a new entry value by changing the "next" index of an existing entry
+ */
+ private static long swapNext(long entry, int newNext) {
+ return (HASH_MASK & entry) | (NEXT_MASK & newNext);
+ }
+
+ private int hashTableMask() {
+ return table.length - 1;
+ }
+
+ @Override
+ public boolean add(@Nullable E object) {
+ long[] entries = this.entries;
+ Object[] elements = this.elements;
+ int hash = smearedHash(object);
+ int tableIndex = hash & hashTableMask();
+ int newEntryIndex = this.size; // current size, and pointer to the entry to be appended
+ int next = table[tableIndex];
+ if (next == UNSET) { // uninitialized bucket
+ table[tableIndex] = newEntryIndex;
+ } else {
+ int last;
+ long entry;
+ do {
+ last = next;
+ entry = entries[next];
+ if (getHash(entry) == hash && Objects.equals(object, elements[next])) {
+ return false;
+ }
+ next = getNext(entry);
+ } while (next != UNSET);
+ entries[last] = swapNext(entry, newEntryIndex);
+ }
+ if (newEntryIndex == Integer.MAX_VALUE) {
+ throw new IllegalStateException("Cannot contain more than Integer.MAX_VALUE elements!");
+ }
+ int newSize = newEntryIndex + 1;
+ resizeMeMaybe(newSize);
+ insertEntry(newEntryIndex, object, hash);
+ this.size = newSize;
+ if (newEntryIndex >= threshold) {
+ resizeTable(2 * table.length);
+ }
+ modCount++;
+ return true;
+ }
+
+ /**
+ * Creates a fresh entry with the specified object at the specified position in the entry
+ * arrays.
+ */
+ void insertEntry(int entryIndex, E object, int hash) {
+ this.entries[entryIndex] = ((long) hash << 32) | (NEXT_MASK & UNSET);
+ this.elements[entryIndex] = object;
+ }
+
+ /**
+ * Returns currentSize + 1, after resizing the entries storage if necessary.
+ */
+ private void resizeMeMaybe(int newSize) {
+ int entriesSize = entries.length;
+ if (newSize > entriesSize) {
+ int newCapacity = entriesSize + Math.max(1, entriesSize >>> 1);
+ if (newCapacity < 0) {
+ newCapacity = Integer.MAX_VALUE;
+ }
+ if (newCapacity != entriesSize) {
+ resizeEntries(newCapacity);
+ }
+ }
+ }
+
+ /**
+ * Resizes the internal entries array to the specified capacity, which may be greater or less
+ * than the current capacity.
+ */
+ void resizeEntries(int newCapacity) {
+ this.elements = Arrays.copyOf(elements, newCapacity);
+ long[] entries = this.entries;
+ int oldSize = entries.length;
+ entries = Arrays.copyOf(entries, newCapacity);
+ if (newCapacity > oldSize) {
+ Arrays.fill(entries, oldSize, newCapacity, UNSET);
+ }
+ this.entries = entries;
+ }
+
+ private void resizeTable(int newCapacity) { // newCapacity always a power of two
+ int[] oldTable = table;
+ int oldCapacity = oldTable.length;
+ if (oldCapacity >= MAXIMUM_CAPACITY) {
+ threshold = Integer.MAX_VALUE;
+ return;
+ }
+ int newThreshold = 1 + (int) (newCapacity * loadFactor);
+ int[] newTable = newTable(newCapacity);
+ long[] entries = this.entries;
+
+ int mask = newTable.length - 1;
+ for (int i = 0; i < size; i++) {
+ long oldEntry = entries[i];
+ int hash = getHash(oldEntry);
+ int tableIndex = hash & mask;
+ int next = newTable[tableIndex];
+ newTable[tableIndex] = i;
+ entries[i] = ((long) hash << 32) | (NEXT_MASK & next);
+ }
+
+ this.threshold = newThreshold;
+ this.table = newTable;
+ }
+
+ @Override
+ public boolean contains(@Nullable Object object) {
+ int hash = smearedHash(object);
+ int next = table[hash & hashTableMask()];
+ while (next != UNSET) {
+ long entry = entries[next];
+ if (getHash(entry) == hash && Objects.equals(object, elements[next])) {
+ return true;
+ }
+ next = getNext(entry);
+ }
+ return false;
+ }
+
+ @Override
+ public boolean remove(@Nullable Object object) {
+ return remove(object, smearedHash(object));
+ }
+
+ private boolean remove(Object object, int hash) {
+ int tableIndex = hash & hashTableMask();
+ int next = table[tableIndex];
+ if (next == UNSET) {
+ return false;
+ }
+ int last = UNSET;
+ do {
+ if (getHash(entries[next]) == hash && Objects.equals(object, elements[next])) {
+ if (last == UNSET) {
+ // we need to update the root link from table[]
+ table[tableIndex] = getNext(entries[next]);
+ } else {
+ // we need to update the link from the chain
+ entries[last] = swapNext(entries[last], getNext(entries[next]));
+ }
+
+ moveEntry(next);
+ size--;
+ modCount++;
+ return true;
+ }
+ last = next;
+ next = getNext(entries[next]);
+ } while (next != UNSET);
+ return false;
+ }
+
+ /**
+ * Moves the last entry in the entry array into {@code dstIndex}, and nulls out its old position.
+ */
+ void moveEntry(int dstIndex) {
+ int srcIndex = size() - 1;
+ if (dstIndex < srcIndex) {
+ // move last entry to deleted spot
+ elements[dstIndex] = elements[srcIndex];
+ elements[srcIndex] = null;
+
+ // move the last entry to the removed spot, just like we moved the element
+ long lastEntry = entries[srcIndex];
+ entries[dstIndex] = lastEntry;
+ entries[srcIndex] = UNSET;
+
+ // also need to update whoever's "next" pointer was pointing to the last entry place
+ // reusing "tableIndex" and "next"; these variables were no longer needed
+ int tableIndex = getHash(lastEntry) & hashTableMask();
+ int lastNext = table[tableIndex];
+ if (lastNext == srcIndex) {
+ // we need to update the root pointer
+ table[tableIndex] = dstIndex;
+ } else {
+ // we need to update a pointer in an entry
+ int previous;
+ long entry;
+ do {
+ previous = lastNext;
+ lastNext = getNext(entry = entries[lastNext]);
+ } while (lastNext != srcIndex);
+ // here, entries[previous] points to the old entry location; update it
+ entries[previous] = swapNext(entry, dstIndex);
+ }
+ } else {
+ elements[dstIndex] = null;
+ entries[dstIndex] = UNSET;
+ }
+ }
+
+ @Override
+ public Iterator<E> iterator() {
+ return new Iterator<E>() {
+ int expectedModCount = modCount;
+ boolean nextCalled = false;
+ int index = 0;
+
+ @Override
+ public boolean hasNext() {
+ return index < size;
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public E next() {
+ checkForConcurrentModification();
+ if (!hasNext()) {
+ throw new NoSuchElementException();
+ }
+ nextCalled = true;
+ return (E) elements[index++];
+ }
+
+ @Override
+ public void remove() {
+ checkForConcurrentModification();
+ Preconditions.checkState(nextCalled, "no calls to next() since the last call to remove()");
+ expectedModCount++;
+ index--;
+ CompactHashSet.this.remove(elements[index], getHash(entries[index]));
+ nextCalled = false;
+ }
+
+ private void checkForConcurrentModification() {
+ if (modCount != expectedModCount) {
+ throw new ConcurrentModificationException();
+ }
+ }
+ };
+ }
+
+ @Override
+ public int size() {
+ return size;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return size == 0;
+ }
+
+ @Override
+ public Object[] toArray() {
+ return Arrays.copyOf(elements, size);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public <T> T[] toArray(T[] a) {
+ if (a.length < size) {
+ a = (T[]) Array.newInstance(a.getClass().getComponentType(), size);
+ }
+ System.arraycopy(elements, 0, a, 0, size);
+ return a;
+ }
+
+ /**
+ * Ensures that this {@code CompactHashSet} has the smallest representation in memory,
+ * given its current size.
+ */
+ public void trimToSize() {
+ int size = this.size;
+ if (size < entries.length) {
+ resizeEntries(size);
+ }
+ // size / loadFactor gives the table size of the appropriate load factor,
+ // but that may not be a power of two. We floor it to a power of two by
+ // keeping its highest bit. But the smaller table may have a load factor
+ // larger than what we want; then we want to go to the next power of 2 if we can
+ int minimumTableSize = Math.max(1, Integer.highestOneBit((int) (size / loadFactor)));
+ if (minimumTableSize < MAXIMUM_CAPACITY) {
+ double load = (double) size / minimumTableSize;
+ if (load > loadFactor) {
+ minimumTableSize <<= 1; // increase to next power if possible
+ }
+ }
+
+ if (minimumTableSize < table.length) {
+ resizeTable(minimumTableSize);
+ }
+ }
+
+ @Override
+ public void clear() {
+ modCount++;
+ Arrays.fill(elements, 0, size, null);
+ Arrays.fill(table, UNSET);
+ Arrays.fill(entries, UNSET);
+ this.size = 0;
+ }
+
+ private void writeObject(ObjectOutputStream stream) throws IOException {
+ stream.defaultWriteObject();
+ stream.writeInt(table.length);
+ stream.writeFloat(loadFactor);
+ stream.writeInt(size);
+ for (E e : this) {
+ stream.writeObject(e);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
+ stream.defaultReadObject();
+ int length = stream.readInt();
+ float loadFactor = stream.readFloat();
+ int elementCount = stream.readInt();
+ try {
+ init(length, loadFactor);
+ } catch (IllegalArgumentException e) {
+ throw new InvalidObjectException(e.getMessage());
+ }
+ for (int i = elementCount; --i >= 0;) {
+ E element = (E) stream.readObject();
+ add(element);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/collect/EquivalenceRelation.java b/src/main/java/com/google/devtools/build/lib/collect/EquivalenceRelation.java
new file mode 100644
index 0000000..1596523
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/collect/EquivalenceRelation.java
@@ -0,0 +1,93 @@
+// Copyright 2014 Google Inc. 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.collect;
+
+/**
+ * A comparison function, which imposes an equivalence relation on some
+ * collection of objects.
+ *
+ * <p>The ordering imposed by an EquivalenceRelation <tt>e</tt> on a set of
+ * elements <tt>S</tt> is said to be <i>consistent with equals</i> if and only
+ * if <tt>(compare((Object)e1, (Object)e2)==0)</tt> has the same boolean value
+ * as <tt>e1.equals((Object)e2)</tt> for every <tt>e1</tt> and <tt>e2</tt> in
+ * <tt>S</tt>.<p>
+ *
+ * <p>Unlike {@link java.util.Comparator}, whose implementations are often
+ * consistent with equals, the applications for which EquivalenceRelation
+ * instances are used means that its implementations rarely are. They may are
+ * usually more or less discriminative than the default equivalence relation
+ * for the type.
+ *
+ * <p>For example, consider possible equivalence relations for {@link
+ * java.lang.Integer}: the default equivalence defined by Integer.equals() is
+ * based on the integer value is represents, but two alternative equivalences
+ * would be {@link EquivalenceRelation#IDENTITY} (object identity—a more
+ * discriminative relation) or <i>parity</i> (under which all even numbers, odd
+ * numbers are considered equivalent to each other—a less discriminative
+ * relation).
+ */
+public interface EquivalenceRelation<T> {
+ // This should be a superinterface of Comparator.
+
+ /**
+ * Compares its two arguments for equivalence. Returns zero if they are
+ * considered equivalent, or non-zero otherwise.<p>
+ *
+ * The implementor must ensure that the relation is
+ *
+ * reflexive (<tt>compare(x,x)==0</tt> for all x),
+ *
+ * symmetric (<tt>compare(x,y)==compare(y,x)<tt> for all x, y),
+ *
+ * and transitive <tt>(compare(x, y)==0 && compare(y,
+ * z)==0</tt> implies <tt>compare(x, z)==0</tt>.<p>
+ *
+ * @param o1 the first object to be compared.
+ * @param o2 the second object to be compared.
+ * @return zero if the two objects are equivalent; some other integer value
+ * otherwise.
+ * @throws ClassCastException if the arguments' types prevent them from
+ * being compared by this EquivalenceRelation.
+ */
+ int compare(T o1, T o2);
+
+ /**
+ * The object-identity equivalence relation. This is the strictest possible
+ * equivalence relation for objects, and considers two values equal iff they
+ * are references to the same object instance.
+ */
+ public static final EquivalenceRelation<?> IDENTITY =
+ new EquivalenceRelation<Object>() {
+ @Override
+ public int compare(Object o1, Object o2) {
+ return o1 == o2 ? 0 : -1;
+ }
+ };
+
+ /**
+ * The default equivalence relation for type T, using T.equals(). This
+ * relation considers two values equivalent if either they are both null, or
+ * o1.equals(o2).
+ */
+ public static final EquivalenceRelation<?> DEFAULT =
+ new EquivalenceRelation<Object>() {
+ @Override
+ public int compare(Object o1, Object o2) {
+ return (o1 == null ? o2 == null : o1.equals(o2))
+ ? 0
+ : -1;
+ }
+ };
+}
diff --git a/src/main/java/com/google/devtools/build/lib/collect/ImmutableIterable.java b/src/main/java/com/google/devtools/build/lib/collect/ImmutableIterable.java
new file mode 100644
index 0000000..74fab83
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/collect/ImmutableIterable.java
@@ -0,0 +1,48 @@
+// Copyright 2014 Google Inc. 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.collect;
+
+import java.util.Iterator;
+
+/**
+ * A wrapper that signals the immutability of a certain iterable.
+ *
+ * <p>Intended for use in scenarios when you have an iterable that is de facto immutable,
+ * but is not recognized as such by {@link CollectionUtils#checkImmutable(Iterable)}.
+ *
+ * <p>Only use this when you know that the contents of the underlying iterable will never change,
+ * or you will be setting yourself up for aliasing bugs.
+ */
+public final class ImmutableIterable<T> implements Iterable<T> {
+
+ private final Iterable<T> iterable;
+
+ private ImmutableIterable(Iterable<T> iterable) {
+ this.iterable = iterable;
+ }
+
+ @Override
+ public Iterator<T> iterator() {
+ return iterable.iterator();
+ }
+
+ /**
+ * Creates an {@link ImmutableIterable} instance.
+ */
+ // Use a factory method in order to avoid having to specify generic arguments.
+ public static <T> ImmutableIterable<T> from(Iterable<T> iterable) {
+ return new ImmutableIterable<>(iterable);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/collect/ImmutableSortedKeyListMultimap.java b/src/main/java/com/google/devtools/build/lib/collect/ImmutableSortedKeyListMultimap.java
new file mode 100644
index 0000000..2163378
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/collect/ImmutableSortedKeyListMultimap.java
@@ -0,0 +1,394 @@
+// Copyright 2014 Google Inc. 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.collect;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.AbstractIterator;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMultiset;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Multiset;
+
+import java.util.AbstractCollection;
+import java.util.AbstractMap;
+import java.util.AbstractMap.SimpleImmutableEntry;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+/**
+ * A immutable multimap implementation for multimaps with comparable keys. It uses a sorted array
+ * and binary search to return the correct values. It's only purpose is to save memory - it consumes
+ * only about half the memory of the equivalent ImmutableListMultimap. Only a few methods are
+ * efficiently implemented: {@link #isEmpty} is O(1), {@link #get} and {@link #containsKey} are
+ * O(log(n)), and {@link #asMap} and {@link #values} refer to the parent instance. All other methods
+ * can take O(n) or even make a copy of the contents.
+ *
+ * <p>This implementation supports neither {@code null} keys nor {@code null} values.
+ */
+public final class ImmutableSortedKeyListMultimap<K extends Comparable<K>, V>
+ implements ListMultimap<K, V> {
+
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ private static final ImmutableSortedKeyListMultimap EMPTY_MULTIMAP =
+ new ImmutableSortedKeyListMultimap(new Comparable<?>[0], new List<?>[0]);
+
+ /** Returns the empty multimap. */
+ @SuppressWarnings("unchecked")
+ public static <K extends Comparable<K>, V> ImmutableSortedKeyListMultimap<K, V> of() {
+ // Safe because the multimap will never hold any elements.
+ return EMPTY_MULTIMAP;
+ }
+
+ @SuppressWarnings("unchecked")
+ public static <K extends Comparable<K>, V> ImmutableSortedKeyListMultimap<K, V> copyOf(
+ Multimap<K, V> data) {
+ if (data.isEmpty()) {
+ return EMPTY_MULTIMAP;
+ }
+ if (data instanceof ImmutableSortedKeyListMultimap) {
+ return (ImmutableSortedKeyListMultimap<K, V>) data;
+ }
+ Set<K> keySet = data.keySet();
+ int size = keySet.size();
+ K[] sortedKeys = (K[]) new Comparable<?>[size];
+ int index = 0;
+ for (K key : keySet) {
+ sortedKeys[index++] = Preconditions.checkNotNull(key);
+ }
+ Arrays.sort(sortedKeys);
+ List<V>[] values = (List<V>[]) new List<?>[size];
+ for (int i = 0; i < size; i++) {
+ values[i] = ImmutableList.copyOf(data.get(sortedKeys[i]));
+ }
+ return new ImmutableSortedKeyListMultimap<>(sortedKeys, values);
+ }
+
+ public static <K extends Comparable<K>, V> Builder<K, V> builder() {
+ return new Builder<>();
+ }
+
+ /**
+ * A builder class for ImmutableSortedKeyListMultimap<K, V> instances.
+ */
+ public static final class Builder<K extends Comparable<K>, V> {
+ private final Multimap<K, V> builderMultimap = ArrayListMultimap.create();
+
+ Builder() {
+ // Not public so you must call builder() instead.
+ }
+
+ public ImmutableSortedKeyListMultimap<K, V> build() {
+ return ImmutableSortedKeyListMultimap.copyOf(builderMultimap);
+ }
+
+ public Builder<K, V> put(K key, V value) {
+ builderMultimap.put(Preconditions.checkNotNull(key), Preconditions.checkNotNull(value));
+ return this;
+ }
+
+ public Builder<K, V> putAll(K key, Collection<? extends V> values) {
+ Collection<V> valueList = builderMultimap.get(Preconditions.checkNotNull(key));
+ for (V value : values) {
+ valueList.add(Preconditions.checkNotNull(value));
+ }
+ return this;
+ }
+
+ @SuppressWarnings("unchecked")
+ public Builder<K, V> putAll(K key, V... values) {
+ return putAll(Preconditions.checkNotNull(key), Arrays.asList(values));
+ }
+
+ public Builder<K, V> putAll(Multimap<? extends K, ? extends V> multimap) {
+ for (Map.Entry<? extends K, ? extends Collection<? extends V>> entry
+ : multimap.asMap().entrySet()) {
+ putAll(entry.getKey(), entry.getValue());
+ }
+ return this;
+ }
+ }
+
+ /**
+ * An implementation for the Multimap.asMap method. Note that AbstractMap already provides
+ * implementations for all methods except {@link #entrySet}, but we override a few here because we
+ * can do it much faster than the existing entrySet-based implementations. Also note that it
+ * inherits the type parameters K and V from the parent class.
+ */
+ private class AsMap extends AbstractMap<K, Collection<V>> {
+
+ AsMap() {
+ }
+
+ @Override
+ public int size() {
+ return sortedKeys.length;
+ }
+
+ @Override
+ public boolean containsKey(Object key) {
+ return ImmutableSortedKeyListMultimap.this.containsKey(key);
+ }
+
+ @Override
+ public Collection<V> get(Object key) {
+ int index = Arrays.binarySearch(sortedKeys, key);
+ // Note the different semantic between Map and Multimap.
+ return index >= 0 ? values[index] : null;
+ }
+
+ @Override
+ public Collection<V> remove(Object key) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void clear() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Set<Entry<K, Collection<V>>> entrySet() {
+ ImmutableSet.Builder<Entry<K, Collection<V>>> builder = ImmutableSet.builder();
+ for (int i = 0; i < sortedKeys.length; i++) {
+ builder.add(new SimpleImmutableEntry<K, Collection<V>>(sortedKeys[i], values[i]));
+ }
+ return builder.build();
+ }
+ }
+
+ private class ValuesCollection extends AbstractCollection<V> {
+
+ ValuesCollection() {
+ }
+
+ @Override
+ public int size() {
+ return ImmutableSortedKeyListMultimap.this.size();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return sortedKeys.length == 0;
+ }
+
+ @Override
+ public boolean contains(Object o) {
+ return ImmutableSortedKeyListMultimap.this.containsValue(o);
+ }
+
+ @Override
+ public Iterator<V> iterator() {
+ if (isEmpty()) {
+ return Collections.emptyIterator();
+ }
+ return new AbstractIterator<V>() {
+ private int currentList = 0;
+ private int currentIndex = 0;
+
+ @Override
+ protected V computeNext() {
+ if (currentList >= values.length) {
+ return endOfData();
+ }
+ V result = values[currentList].get(currentIndex);
+ // Find the next list/index pair.
+ currentIndex++;
+ if (currentIndex >= values[currentList].size()) {
+ currentIndex = 0;
+ currentList++;
+ }
+ return result;
+ }
+ };
+ }
+
+ @Override
+ public boolean remove(Object o) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean removeAll(Collection<?> c) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean retainAll(Collection<?> c) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void clear() {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ private final K[] sortedKeys;
+ private final List<V>[] values;
+
+ private ImmutableSortedKeyListMultimap(K[] sortedKeys, List<V>[] values) {
+ this.sortedKeys = sortedKeys;
+ this.values = values;
+ }
+
+ @Override
+ public int size() {
+ int result = 0;
+ for (List<V> list : values) {
+ result += list.size();
+ }
+ return result;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return sortedKeys.length == 0;
+ }
+
+ @Override
+ public boolean containsKey(Object key) {
+ int index = Arrays.binarySearch(sortedKeys, key);
+ return index >= 0;
+ }
+
+ @Override
+ public boolean containsValue(Object value) {
+ for (List<V> list : values) {
+ if (list.contains(value)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean containsEntry(Object key, Object value) {
+ int index = Arrays.binarySearch(sortedKeys, key);
+ if (index >= 0) {
+ return values[index].contains(value);
+ }
+ return false;
+ }
+
+ @Override
+ public boolean put(K key, V value) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean remove(Object key, Object value) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean putAll(K key, Iterable<? extends V> values) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean putAll(Multimap<? extends K, ? extends V> multimap) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public List<V> replaceValues(K key, Iterable<? extends V> values) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public List<V> removeAll(Object key) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void clear() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public List<V> get(K key) {
+ int index = Arrays.binarySearch(sortedKeys, key);
+ return index >= 0 ? values[index] : ImmutableList.<V>of();
+ }
+
+ @Override
+ public Set<K> keySet() {
+ return ImmutableSet.copyOf(sortedKeys);
+ }
+
+ @Override
+ public Multiset<K> keys() {
+ return ImmutableMultiset.copyOf(sortedKeys);
+ }
+
+ @Override
+ public Collection<V> values() {
+ return new ValuesCollection();
+ }
+
+ @Override
+ public Collection<Entry<K, V>> entries() {
+ ImmutableList.Builder<Entry<K, V>> builder = ImmutableList.builder();
+ for (int i = 0; i < sortedKeys.length; i++) {
+ for (V value : values[i]) {
+ builder.add(new SimpleImmutableEntry<K, V>(sortedKeys[i], value));
+ }
+ }
+ return builder.build();
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * <p>Note that only {@code get} and {@code containsKey} are implemented efficiently on the
+ * returned map.
+ */
+ @Override
+ public Map<K, Collection<V>> asMap() {
+ return new AsMap();
+ }
+
+ @Override
+ public String toString() {
+ return asMap().toString();
+ }
+
+ @Override
+ public int hashCode() {
+ return asMap().hashCode();
+ }
+
+ @Override
+ public boolean equals(@Nullable Object object) {
+ if (this == object) {
+ return true;
+ }
+ if (object instanceof Multimap) {
+ Multimap<?, ?> that = (Multimap<?, ?>) object;
+ return asMap().equals(that.asMap());
+ }
+ return false;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/collect/ImmutableSortedKeyMap.java b/src/main/java/com/google/devtools/build/lib/collect/ImmutableSortedKeyMap.java
new file mode 100644
index 0000000..e8f4621
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/collect/ImmutableSortedKeyMap.java
@@ -0,0 +1,310 @@
+// Copyright 2014 Google Inc. 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.collect;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.AbstractIterator;
+import com.google.common.collect.ImmutableSet;
+
+import java.util.AbstractCollection;
+import java.util.AbstractMap.SimpleImmutableEntry;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+/**
+ * A immutable map implementation for maps with comparable keys. It uses a sorted array
+ * and binary search to return the correct values. Its only purpose is to save memory - for n
+ * entries, it consumes 8n + 64 bytes, much less than a normal HashMap (43n + 128) or an
+ * ImmutableMap (35n + 81).
+ *
+ * <p>Only a few methods are efficiently implemented: {@link #isEmpty} is O(1), {@link #get} and
+ * {@link #containsKey} are O(log(n)), using binary search; {@link #keySet} and {@link #values}
+ * refer to the parent instance. All other methods can take O(n) or even make a copy of the
+ * contents.
+ *
+ * <p>This implementation supports neither {@code null} keys nor {@code null} values.
+ *
+ * @param <K> the type of keys maintained by this map; keys must be comparable
+ * @param <V> the type of mapped values
+ */
+public final class ImmutableSortedKeyMap<K extends Comparable<K>, V> implements Map<K, V> {
+
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ private static final ImmutableSortedKeyMap EMPTY_MAP =
+ new ImmutableSortedKeyMap(new Comparable<?>[0], new Object[0]);
+
+ /** Returns the empty multimap. */
+ @SuppressWarnings("unchecked")
+ public static <K extends Comparable<K>, V> ImmutableSortedKeyMap<K, V> of() {
+ // Safe because the multimap will never hold any elements.
+ return EMPTY_MAP;
+ }
+
+ public static <K extends Comparable<K>, V> ImmutableSortedKeyMap<K, V> of(K key0, V value0) {
+ return ImmutableSortedKeyMap.<K, V>builder()
+ .put(key0, value0)
+ .build();
+ }
+
+ public static <K extends Comparable<K>, V> ImmutableSortedKeyMap<K, V> of(
+ K key0, V value0, K key1, V value1) {
+ return ImmutableSortedKeyMap.<K, V>builder()
+ .put(key0, value0)
+ .put(key1, value1)
+ .build();
+ }
+
+ @SuppressWarnings("unchecked")
+ public static <K extends Comparable<K>, V> ImmutableSortedKeyMap<K, V> copyOf(Map<K, V> data) {
+ if (data.isEmpty()) {
+ return EMPTY_MAP;
+ }
+ if (data instanceof ImmutableSortedKeyMap) {
+ return (ImmutableSortedKeyMap<K, V>) data;
+ }
+ Set<K> keySet = data.keySet();
+ int size = keySet.size();
+ K[] sortedKeys = (K[]) new Comparable<?>[size];
+ int index = 0;
+ for (K key : keySet) {
+ sortedKeys[index] = Preconditions.checkNotNull(key);
+ index++;
+ }
+ Arrays.sort(sortedKeys);
+ V[] values = (V[]) new Object[size];
+ for (int i = 0; i < size; i++) {
+ values[i] = data.get(sortedKeys[i]);
+ }
+ return new ImmutableSortedKeyMap<>(sortedKeys, values);
+ }
+
+ public static <K extends Comparable<K>, V> Builder<K, V> builder() {
+ return new Builder<>();
+ }
+
+ /**
+ * A builder class for ImmutableSortedKeyListMultimap<K, V> instances.
+ */
+ public static final class Builder<K extends Comparable<K>, V> {
+ private final Map<K, V> builderMap = new HashMap<>();
+
+ Builder() {
+ // Not public so you must call builder() instead.
+ }
+
+ public ImmutableSortedKeyMap<K, V> build() {
+ return ImmutableSortedKeyMap.copyOf(builderMap);
+ }
+
+ public Builder<K, V> put(K key, V value) {
+ builderMap.put(Preconditions.checkNotNull(key), Preconditions.checkNotNull(value));
+ return this;
+ }
+
+ public Builder<K, V> putAll(Map<? extends K, ? extends V> map) {
+ for (Map.Entry<? extends K, ? extends V> entry : map.entrySet()) {
+ put(entry.getKey(), entry.getValue());
+ }
+ return this;
+ }
+ }
+
+ private class ValuesCollection extends AbstractCollection<V> {
+
+ ValuesCollection() {
+ }
+
+ @Override
+ public int size() {
+ return ImmutableSortedKeyMap.this.size();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return sortedKeys.length == 0;
+ }
+
+ @Override
+ public boolean contains(Object o) {
+ return ImmutableSortedKeyMap.this.containsValue(o);
+ }
+
+ @Override
+ public Iterator<V> iterator() {
+ if (isEmpty()) {
+ return Collections.emptyIterator();
+ }
+ return new AbstractIterator<V>() {
+ private int currentIndex = 0;
+
+ @Override
+ protected V computeNext() {
+ if (currentIndex >= values.length) {
+ return endOfData();
+ }
+ return values[currentIndex++];
+ }
+ };
+ }
+
+ @Override
+ public boolean remove(Object o) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean removeAll(Collection<?> c) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean retainAll(Collection<?> c) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void clear() {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ private final K[] sortedKeys;
+ private final V[] values;
+
+ private ImmutableSortedKeyMap(K[] sortedKeys, V[] values) {
+ this.sortedKeys = sortedKeys;
+ this.values = values;
+ }
+
+ @Override
+ public int size() {
+ return sortedKeys.length;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return sortedKeys.length == 0;
+ }
+
+ @Override
+ public boolean containsKey(@Nullable Object key) {
+ if (key == null) {
+ return false;
+ }
+ int index = Arrays.binarySearch(sortedKeys, key);
+ return index >= 0;
+ }
+
+ @Override
+ public boolean containsValue(@Nullable Object value) {
+ if (value == null) {
+ return false;
+ }
+ for (V v : values) {
+ if (v.equals(value)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public V put(K key, V value) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public V remove(Object key) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void putAll(Map<? extends K, ? extends V> map) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void clear() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public V get(@Nullable Object key) {
+ if (key == null) {
+ return null;
+ }
+ int index = Arrays.binarySearch(sortedKeys, key);
+ return index >= 0 ? values[index] : null;
+ }
+
+ @Override
+ public Set<K> keySet() {
+ return ImmutableSet.copyOf(sortedKeys);
+ }
+
+ @Override
+ public Collection<V> values() {
+ return new ValuesCollection();
+ }
+
+ @Override
+ public Set<Entry<K, V>> entrySet() {
+ ImmutableSet.Builder<Entry<K, V>> builder = ImmutableSet.builder();
+ for (int i = 0; i < sortedKeys.length; i++) {
+ builder.add(new SimpleImmutableEntry<K, V>(sortedKeys[i], values[i]));
+ }
+ return builder.build();
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder result = new StringBuilder();
+ result.append('{');
+ for (int i = 0; i < sortedKeys.length; i++) {
+ if (i != 0) {
+ result.append(", ");
+ }
+ result.append(sortedKeys[i]).append('=').append(values[i]);
+ }
+ result.append('}');
+ return result.toString();
+ }
+
+ @Override
+ public int hashCode() {
+ int h = 0;
+ for (Entry<K, V> entry : entrySet()) {
+ h += entry.hashCode();
+ }
+ return h;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object object) {
+ if (this == object) {
+ return true;
+ }
+ if (object instanceof Map) {
+ throw new UnsupportedOperationException();
+ }
+ return false;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/collect/IterablesChain.java b/src/main/java/com/google/devtools/build/lib/collect/IterablesChain.java
new file mode 100644
index 0000000..15182b6
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/collect/IterablesChain.java
@@ -0,0 +1,159 @@
+// Copyright 2014 Google Inc. 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.collect;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.AbstractIterator;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * An immutable chain of immutable Iterables.
+ *
+ * <p>This class is defined for the sole purpose of being able to check for immutability
+ * (using instanceof). Otherwise, we could use a plain Iterable (as returned by
+ * {@code Iterables.concat()}).
+ *
+ * @see CollectionUtils#checkImmutable(Iterable)
+ */
+public final class IterablesChain<T> implements Iterable<T> {
+
+ private final Iterable<T> chain;
+
+ private IterablesChain(Iterable<T> chain) {
+ this.chain = chain;
+ }
+
+ @Override
+ public Iterator<T> iterator() {
+ return chain.iterator();
+ }
+
+ public static <T> Builder<T> builder() {
+ return new Builder<>();
+ }
+
+ @Override
+ public String toString() {
+ return "[" + Joiner.on(", ").join(this) + "]";
+ }
+
+ /**
+ * Builder for IterablesChain.
+ *
+ *
+ */
+ public static class Builder<T> {
+ private List<Iterable<T>> iterables = new ArrayList<>();
+ private boolean deduplicate;
+
+ private Builder() {
+ }
+
+ /**
+ * Adds an immutable iterable to the end of the chain.
+ *
+ * <p>If the iterable can not be confirmed to be immutable, a runtime error is thrown.
+ */
+ public Builder<T> add(Iterable<T> iterable) {
+ CollectionUtils.checkImmutable(iterable);
+ if (!Iterables.isEmpty(iterable)) {
+ iterables.add(iterable);
+ }
+ return this;
+ }
+
+ /**
+ * Adds a single element to the chain.
+ */
+ public Builder<T> addElement(T element) {
+ iterables.add(ImmutableList.of(element));
+ return this;
+ }
+
+ /**
+ * Returns true if the chain is empty.
+ */
+ public boolean isEmpty() {
+ return iterables.isEmpty();
+ }
+
+ /**
+ * If this is called, the the resulting {@link IterablesChain} object uses a hash set to remove
+ * duplicate elements.
+ */
+ public Builder<T> deduplicate() {
+ this.deduplicate = true;
+ return this;
+ }
+
+ /**
+ * Builds an iterable that iterates through all elements in this chain.
+ */
+ public IterablesChain<T> build() {
+ if (isEmpty()) {
+ return new IterablesChain<>(ImmutableList.<T>of());
+ }
+ Iterable<T> concat = Iterables.concat(ImmutableList.copyOf(iterables));
+ return new IterablesChain<>(deduplicate ? new Deduper<>(concat) : concat);
+ }
+ }
+
+ /**
+ * An iterable implementation that removes duplicate elements (as determined by equals), using a
+ * hash set.
+ */
+ private static final class Deduper<T> implements Iterable<T> {
+ private final Iterable<T> iterable;
+
+ public Deduper(Iterable<T> iterable) {
+ this.iterable = iterable;
+ }
+
+ @Override
+ public Iterator<T> iterator() {
+ return new DedupingIterator<T>(iterable.iterator());
+ }
+ }
+
+ /**
+ * An iterator implementation that removes duplicate elements (as determined by equals), using a
+ * hash set.
+ */
+ private static final class DedupingIterator<T> extends AbstractIterator<T> {
+ private final HashSet<T> set = new HashSet<>();
+ private final Iterator<T> it;
+
+ public DedupingIterator(Iterator<T> it) {
+ this.it = it;
+ }
+
+ @Override
+ protected T computeNext() {
+ while (it.hasNext()) {
+ T next = it.next();
+ if (set.add(next)) {
+ return next;
+ }
+ }
+ return endOfData();
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/collect/nestedset/CompileOrderExpander.java b/src/main/java/com/google/devtools/build/lib/collect/nestedset/CompileOrderExpander.java
new file mode 100644
index 0000000..5ac8610
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/collect/nestedset/CompileOrderExpander.java
@@ -0,0 +1,48 @@
+// Copyright 2014 Google Inc. 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.collect.nestedset;
+
+import com.google.common.collect.ImmutableCollection;
+
+/**
+ * A nested set expander that implements left-to-right postordering.
+ *
+ * <p>For example, for the nested set {B, D, {A, C}}, the iteration order is "A C B D"
+ * (child-first).
+ *
+ * <p>This type of set would typically be used for artifacts where elements of nested sets go before
+ * the direct members of a set, for example in the case of constructing Java classpaths.
+ */
+final class CompileOrderExpander<E> implements NestedSetExpander<E> {
+
+ // We suppress unchecked warning so that we can access the internal raw structure of the
+ // NestedSet.
+ @SuppressWarnings("unchecked")
+ @Override
+ public void expandInto(NestedSet<E> set, Uniqueifier uniqueifier,
+ ImmutableCollection.Builder<E> builder) {
+ for (NestedSet<E> subset : set.transitiveSets()) {
+ if (!subset.isEmpty() && uniqueifier.isUnique(subset)) {
+ expandInto(subset, uniqueifier, builder);
+ }
+ }
+
+ // This switch is here to compress the memo used by the uniqueifier
+ for (Object e : set.directMembers()) {
+ if (uniqueifier.isUnique(e)) {
+ builder.add((E) e);
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/collect/nestedset/CompileOrderNestedSetFactory.java b/src/main/java/com/google/devtools/build/lib/collect/nestedset/CompileOrderNestedSetFactory.java
new file mode 100644
index 0000000..178f9a5
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/collect/nestedset/CompileOrderNestedSetFactory.java
@@ -0,0 +1,152 @@
+// Copyright 2014 Google Inc. 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.collect.nestedset;
+
+import com.google.common.collect.ImmutableList;
+
+/**
+ * Compile order {@code NestedSet} factory.
+ */
+final class CompileOrderNestedSetFactory implements NestedSetFactory {
+
+ @Override
+ public <E> NestedSet<E> onlyDirects(Object[] directs) {
+ return new CompileOnlyDirectsNestedSet<>(directs);
+ }
+
+ @Override
+ public <E> NestedSet<E> onlyDirects(ImmutableList<E> directs) {
+ return new CompileOrderImmutableListDirectsNestedSet<>(directs);
+ }
+
+ @Override
+ public <E> NestedSet<E> oneDirectOneTransitive(E direct, NestedSet<E> transitive) {
+ return new CompileOneDirectOneTransitiveNestedSet<>(direct, transitive);
+ }
+
+ @Override
+ public <E> NestedSet<E> manyDirectsOneTransitive(Object[] direct,
+ NestedSet<E> transitive) {
+ return new CompileManyDirectOneTransitiveNestedSet<>(direct, transitive);
+ }
+
+ @Override
+ public <E> NestedSet<E> onlyOneTransitive(NestedSet<E> transitive) {
+ return new CompileOnlyOneTransitiveNestedSet<>(transitive);
+ }
+
+ @Override
+ public <E> NestedSet<E> onlyManyTransitives(NestedSet[] transitives) {
+ return new CompileOnlyTransitivesNestedSet<>(transitives);
+ }
+
+ @Override
+ public <E> NestedSet<E> oneDirectManyTransitive(Object direct, NestedSet[] transitives) {
+ return new CompileOneDirectManyTransitive<>(direct, transitives);
+ }
+
+ @Override
+ public <E> NestedSet<E> manyDirectManyTransitive(Object[] directs, NestedSet[] transitives) {
+ return new CompileManyDirectManyTransitive<>(directs, transitives);
+ }
+
+ @Override
+ public <E> NestedSet<E> oneDirect(E element) {
+ return new CompileSingleDirectNestedSet<>(element);
+ }
+
+ private static class CompileOnlyDirectsNestedSet<E> extends OnlyDirectsNestedSet<E> {
+
+ CompileOnlyDirectsNestedSet(Object[] directs) { super(directs); }
+
+ @Override
+ public Order getOrder() { return Order.COMPILE_ORDER; }
+ }
+
+ private static class CompileOneDirectOneTransitiveNestedSet<E> extends
+ OneDirectOneTransitiveNestedSet<E> {
+
+ private CompileOneDirectOneTransitiveNestedSet(E direct, NestedSet<E> transitive) {
+ super(direct, transitive);
+ }
+
+ @Override
+ public Order getOrder() { return Order.COMPILE_ORDER; }
+ }
+
+ private static class CompileOneDirectManyTransitive<E> extends OneDirectManyTransitive<E> {
+
+ private CompileOneDirectManyTransitive(Object direct, NestedSet[] transitive) {
+ super(direct, transitive);
+ }
+
+ @Override
+ public Order getOrder() { return Order.COMPILE_ORDER; }
+ }
+
+ private static class CompileManyDirectManyTransitive<E> extends ManyDirectManyTransitive<E> {
+
+ private CompileManyDirectManyTransitive(Object[] directs, NestedSet[] transitives) {
+ super(directs, transitives);
+ }
+
+ @Override
+ public Order getOrder() { return Order.COMPILE_ORDER; }
+ }
+
+ private static class CompileOnlyOneTransitiveNestedSet<E> extends OnlyOneTransitiveNestedSet<E> {
+
+ private CompileOnlyOneTransitiveNestedSet(NestedSet<E> transitive) { super(transitive); }
+
+ @Override
+ public Order getOrder() { return Order.COMPILE_ORDER; }
+ }
+
+ private static class CompileManyDirectOneTransitiveNestedSet<E> extends
+ ManyDirectOneTransitiveNestedSet<E> {
+
+ private CompileManyDirectOneTransitiveNestedSet(Object[] direct,
+ NestedSet<E> transitive) { super(direct, transitive); }
+
+ @Override
+ public Order getOrder() { return Order.COMPILE_ORDER; }
+ }
+
+ private static class CompileOnlyTransitivesNestedSet<E> extends OnlyTransitivesNestedSet<E> {
+
+ private CompileOnlyTransitivesNestedSet(NestedSet[] transitives) { super(transitives); }
+
+ @Override
+ public Order getOrder() { return Order.COMPILE_ORDER; }
+ }
+
+ private static class CompileOrderImmutableListDirectsNestedSet<E> extends
+ ImmutableListDirectsNestedSet<E> {
+
+ private CompileOrderImmutableListDirectsNestedSet(ImmutableList<E> directs) { super(directs); }
+
+ @Override
+ public Order getOrder() {
+ return Order.COMPILE_ORDER;
+ }
+ }
+
+ private static class CompileSingleDirectNestedSet<E> extends SingleDirectNestedSet<E> {
+
+ private CompileSingleDirectNestedSet(E element) { super(element); }
+
+ @Override
+ public Order getOrder() { return Order.COMPILE_ORDER; }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/collect/nestedset/EmptyNestedSet.java b/src/main/java/com/google/devtools/build/lib/collect/nestedset/EmptyNestedSet.java
new file mode 100644
index 0000000..5889ba8
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/collect/nestedset/EmptyNestedSet.java
@@ -0,0 +1,87 @@
+// Copyright 2014 Google Inc. 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.collect.nestedset;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+/**
+ * An empty nested set.
+ */
+final class EmptyNestedSet<E> extends NestedSet<E> {
+ private static final NestedSet[] EMPTY_NESTED_SET = new NestedSet[0];
+ private static final Object[] EMPTY_ELEMENTS = new Object[0];
+ private final Order order;
+
+ EmptyNestedSet(Order type) {
+ this.order = type;
+ }
+
+ @Override
+ public Iterator<E> iterator() {
+ return ImmutableList.<E>of().iterator();
+ }
+
+ @Override
+ Object[] directMembers() {
+ return EMPTY_ELEMENTS;
+ }
+
+ @Override
+ NestedSet[] transitiveSets() {
+ return EMPTY_NESTED_SET;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return true;
+ }
+
+ @Override
+ public List<E> toList() {
+ return ImmutableList.of();
+ }
+
+ @Override
+ public Set<E> toSet() {
+ return ImmutableSet.of();
+ }
+
+ @Override
+ public String toString() {
+ return "{}";
+ }
+
+ @Override
+ public Order getOrder() {
+ return order;
+ }
+
+ @Override
+ public boolean shallowEquals(@Nullable NestedSet<? extends E> other) {
+ return other != null && getOrder() == other.getOrder() && other.isEmpty();
+ }
+
+ @Override
+ public int shallowHashCode() {
+ return Objects.hash(getOrder());
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/collect/nestedset/ImmutableListDirectsNestedSet.java b/src/main/java/com/google/devtools/build/lib/collect/nestedset/ImmutableListDirectsNestedSet.java
new file mode 100644
index 0000000..12bf222
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/collect/nestedset/ImmutableListDirectsNestedSet.java
@@ -0,0 +1,88 @@
+// Copyright 2014 Google Inc. 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.collect.nestedset;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+
+import java.util.List;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+/**
+ * Memory-optimized NestedSet implementation for NestedSets without transitive dependencies that
+ * allows us to share an ImmutableList.
+ */
+abstract class ImmutableListDirectsNestedSet<E> extends NestedSet<E> {
+
+ private static final NestedSet[] EMPTY = new NestedSet[0];
+ private final ImmutableList<E> directDeps;
+
+ public ImmutableListDirectsNestedSet(ImmutableList<E> directDeps) {
+ this.directDeps = directDeps;
+ }
+
+ @Override
+ public abstract Order getOrder();
+
+ @Override
+ Object[] directMembers() {
+ return directDeps.toArray();
+ }
+
+ @Override
+ NestedSet[] transitiveSets() {
+ return EMPTY;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return directDeps.isEmpty();
+ }
+
+ /**
+ * Currently all the Order implementations return the direct elements in the same order if they do
+ * not have transitive elements. So we skip calling order.getExpander().
+ */
+ @SuppressWarnings("unchecked")
+ @Override
+ public List<E> toList() {
+ return directDeps;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public Set<E> toSet() {
+ return ImmutableSet.copyOf(directDeps);
+ }
+
+ @Override
+ public boolean shallowEquals(@Nullable NestedSet<? extends E> other) {
+ if (this == other) {
+ return true;
+ }
+ if (other == null) {
+ return false;
+ }
+ return getOrder().equals(other.getOrder())
+ && other instanceof ImmutableListDirectsNestedSet
+ && directDeps.equals(((ImmutableListDirectsNestedSet) other).directDeps);
+ }
+
+ @Override
+ public int shallowHashCode() {
+ return directDeps.hashCode();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/collect/nestedset/LinkOrderExpander.java b/src/main/java/com/google/devtools/build/lib/collect/nestedset/LinkOrderExpander.java
new file mode 100644
index 0000000..603ac15
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/collect/nestedset/LinkOrderExpander.java
@@ -0,0 +1,105 @@
+// Copyright 2014 Google Inc. 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.collect.nestedset;
+
+import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.ImmutableList;
+
+/**
+ * A nested set expander that implements a variation of left-to-right preordering.
+ *
+ * <p>For example, for the nested set {A, C, {B, D}}, the iteration order is "A C B D"
+ * (parent-first).
+ *
+ * <p>This type of set would typically be used for artifacts where elements of
+ * nested sets go after the direct members of a set, for example when providing
+ * a list of libraries to the C++ compiler.
+ *
+ * <p>The custom ordering has the property that elements of nested sets always come
+ * before elements of descendant nested sets. Left-to-right order is preserved if
+ * possible, both for items and for references to nested sets.
+ *
+ * <p>The left-to-right pre-order-like ordering is implemented by running a
+ * right-to-left postorder traversal and then reversing the result.
+ *
+ * <p>The reason naive left-to left-to-right preordering is not used here is that
+ * it does not handle diamond-like structures properly. For example, take the
+ * following structure (nesting downwards):
+ *
+ * <pre>
+ * A
+ * / \
+ * B C
+ * \ /
+ * D
+ * </pre>
+ *
+ * <p>Naive preordering would produce "A B D C", which does not preserve the
+ * "parent before child" property: C is a parent of D, so C should come before
+ * D. Either "A B C D" or "A C B D" would be acceptable. This implementation
+ * returns the first option of the two so that left-to-right order is preserved.
+ *
+ * <p>In case the nested sets form a tree, the ordering algorithm is equivalent to
+ * standard left-to-right preorder.
+ *
+ * <p>Sometimes it may not be possible to preserve left-to-right order:
+ *
+ * <pre>
+ * A
+ * / \
+ * B C
+ * / \ / \
+ * \ E /
+ * \ /
+ * \ /
+ * D
+ * </pre>
+ *
+ * <p>The left branch (B) would indicate "D E" ordering and the right branch (C)
+ * dictates "E D". In such cases ordering is decided by the rightmost branch
+ * because of the list reversing behind the scenes, so the ordering in the final
+ * enumeration will be "E D".
+ */
+
+final class LinkOrderExpander<E> implements NestedSetExpander<E> {
+ @Override
+ public void expandInto(NestedSet<E> nestedSet, Uniqueifier uniqueifier,
+ ImmutableCollection.Builder<E> builder) {
+ ImmutableList.Builder<E> result = ImmutableList.builder();
+ internalEnumerate(nestedSet, uniqueifier, result);
+ builder.addAll(result.build().reverse());
+ }
+
+ // We suppress unchecked warning so that we can access the internal raw structure of the
+ // NestedSet.
+ @SuppressWarnings("unchecked")
+ private void internalEnumerate(NestedSet<E> set, Uniqueifier uniqueifier,
+ ImmutableCollection.Builder<E> builder) {
+ NestedSet[] transitiveSets = set.transitiveSets();
+ for (int i = transitiveSets.length - 1; i >= 0; i--) {
+ NestedSet<E> subset = transitiveSets[i];
+ if (!subset.isEmpty() && uniqueifier.isUnique(subset)) {
+ internalEnumerate(subset, uniqueifier, builder);
+ }
+ }
+
+ Object[] directMembers = set.directMembers();
+ for (int i = directMembers.length - 1; i >= 0; i--) {
+ Object e = directMembers[i];
+ if (uniqueifier.isUnique(e)) {
+ builder.add((E) e);
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/collect/nestedset/LinkOrderNestedSetFactory.java b/src/main/java/com/google/devtools/build/lib/collect/nestedset/LinkOrderNestedSetFactory.java
new file mode 100644
index 0000000..9e23793
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/collect/nestedset/LinkOrderNestedSetFactory.java
@@ -0,0 +1,152 @@
+// Copyright 2014 Google Inc. 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.collect.nestedset;
+
+import com.google.common.collect.ImmutableList;
+
+/**
+ * Link order {@code NestedSet} factory.
+ */
+final class LinkOrderNestedSetFactory implements NestedSetFactory {
+
+ @Override
+ public <E> NestedSet<E> onlyDirects(Object[] directs) {
+ return new LinkOnlyDirectsNestedSet<>(directs);
+ }
+
+ @Override
+ public <E> NestedSet<E> onlyDirects(ImmutableList<E> directs) {
+ return new LinkImmutableListDirectsNestedSet<>(directs);
+ }
+
+ @Override
+ public <E> NestedSet<E> oneDirectOneTransitive(E direct, NestedSet<E> transitive) {
+ return new LinkOneDirectOneTransitiveNestedSet<>(direct, transitive);
+ }
+
+ @Override
+ public <E> NestedSet<E> manyDirectsOneTransitive(Object[] direct,
+ NestedSet<E> transitive) {
+ return new LinkManyDirectOneTransitiveNestedSet<>(direct, transitive);
+ }
+
+ @Override
+ public <E> NestedSet<E> onlyOneTransitive(NestedSet<E> transitive) {
+ return new LinkOnlyOneTransitiveNestedSet<>(transitive);
+ }
+
+ @Override
+ public <E> NestedSet<E> onlyManyTransitives(NestedSet[] transitives) {
+ return new LinkOnlyTransitivesNestedSet<>(transitives);
+ }
+
+ @Override
+ public <E> NestedSet<E> oneDirectManyTransitive(Object direct, NestedSet[] transitives) {
+ return new LinkOneDirectManyTransitive<>(direct, transitives);
+ }
+
+ @Override
+ public <E> NestedSet<E> manyDirectManyTransitive(Object[] directs, NestedSet[] transitives) {
+ return new LinkManyDirectManyTransitive<>(directs, transitives);
+ }
+
+ @Override
+ public <E> NestedSet<E> oneDirect(E element) {
+ return new LinkSingleDirectNestedSet<>(element);
+ }
+
+ private static class LinkOnlyDirectsNestedSet<E> extends OnlyDirectsNestedSet<E> {
+
+ LinkOnlyDirectsNestedSet(Object[] directs) { super(directs); }
+
+ @Override
+ public Order getOrder() { return Order.LINK_ORDER; }
+ }
+
+ private static class LinkOneDirectOneTransitiveNestedSet<E> extends
+ OneDirectOneTransitiveNestedSet<E> {
+
+ private LinkOneDirectOneTransitiveNestedSet(E direct, NestedSet<E> transitive) {
+ super(direct, transitive);
+ }
+
+ @Override
+ public Order getOrder() { return Order.LINK_ORDER; }
+ }
+
+ private static class LinkOneDirectManyTransitive<E> extends OneDirectManyTransitive<E> {
+
+ private LinkOneDirectManyTransitive(Object direct, NestedSet[] transitive) {
+ super(direct, transitive);
+ }
+
+ @Override
+ public Order getOrder() { return Order.LINK_ORDER; }
+ }
+
+ private static class LinkManyDirectManyTransitive<E> extends ManyDirectManyTransitive<E> {
+
+ private LinkManyDirectManyTransitive(Object[] directs, NestedSet[] transitives) {
+ super(directs, transitives);
+ }
+
+ @Override
+ public Order getOrder() { return Order.LINK_ORDER; }
+ }
+
+ private static class LinkOnlyOneTransitiveNestedSet<E> extends OnlyOneTransitiveNestedSet<E> {
+
+ private LinkOnlyOneTransitiveNestedSet(NestedSet<E> transitive) { super(transitive); }
+
+ @Override
+ public Order getOrder() { return Order.LINK_ORDER; }
+ }
+
+ private static class LinkManyDirectOneTransitiveNestedSet<E> extends
+ ManyDirectOneTransitiveNestedSet<E> {
+
+ private LinkManyDirectOneTransitiveNestedSet(Object[] direct,
+ NestedSet<E> transitive) { super(direct, transitive); }
+
+ @Override
+ public Order getOrder() { return Order.LINK_ORDER; }
+ }
+
+ private static class LinkOnlyTransitivesNestedSet<E> extends OnlyTransitivesNestedSet<E> {
+
+ private LinkOnlyTransitivesNestedSet(NestedSet[] transitives) { super(transitives); }
+
+ @Override
+ public Order getOrder() { return Order.LINK_ORDER; }
+ }
+
+ private static class LinkImmutableListDirectsNestedSet<E> extends
+ ImmutableListDirectsNestedSet<E> {
+
+ private LinkImmutableListDirectsNestedSet(ImmutableList<E> directs) { super(directs); }
+
+ @Override
+ public Order getOrder() {
+ return Order.LINK_ORDER;
+ }
+ }
+
+ private static class LinkSingleDirectNestedSet<E> extends SingleDirectNestedSet<E> {
+
+ private LinkSingleDirectNestedSet(E element) { super(element); }
+
+ @Override
+ public Order getOrder() { return Order.LINK_ORDER; }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/collect/nestedset/ManyDirectManyTransitive.java b/src/main/java/com/google/devtools/build/lib/collect/nestedset/ManyDirectManyTransitive.java
new file mode 100644
index 0000000..05ba2e8
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/collect/nestedset/ManyDirectManyTransitive.java
@@ -0,0 +1,63 @@
+// Copyright 2014 Google Inc. 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.collect.nestedset;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+import javax.annotation.Nullable;
+
+/**
+ * NestedSet implementation that can have many direct elements and many transitive
+ * {@code NestedSet}s.
+ */
+abstract class ManyDirectManyTransitive<E> extends MemoizedUniquefierNestedSet<E> {
+
+ private final Object[] directs;
+ private final NestedSet[] transitives;
+ private Object memo;
+
+ ManyDirectManyTransitive(Object[] directs, NestedSet[] transitives) {
+ this.directs = directs;
+ this.transitives = transitives;
+ }
+
+ @Override
+ Object getMemo() { return memo; }
+
+ @Override
+ void setMemo(Object memo) { this.memo = memo; }
+
+ @Override
+ Object[] directMembers() { return directs; }
+
+ @Override
+ NestedSet[] transitiveSets() { return transitives; }
+
+ @Override
+ public boolean shallowEquals(@Nullable NestedSet<? extends E> other) {
+ if (this == other) {
+ return true;
+ }
+ return other != null
+ && getOrder().equals(other.getOrder())
+ && other instanceof ManyDirectManyTransitive
+ && Arrays.equals(directs, ((ManyDirectManyTransitive) other).directs)
+ && Arrays.equals(transitives, ((ManyDirectManyTransitive) other).transitives);
+ }
+
+ @Override
+ public int shallowHashCode() {
+ return Objects.hash(getOrder(), Arrays.hashCode(directs), Arrays.hashCode(transitives)); }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/collect/nestedset/ManyDirectOneTransitiveNestedSet.java b/src/main/java/com/google/devtools/build/lib/collect/nestedset/ManyDirectOneTransitiveNestedSet.java
new file mode 100644
index 0000000..cdb4f04
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/collect/nestedset/ManyDirectOneTransitiveNestedSet.java
@@ -0,0 +1,63 @@
+// Copyright 2014 Google Inc. 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.collect.nestedset;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+import javax.annotation.Nullable;
+
+/**
+ * Memory-efficient implementation for the case where we have many direct elements and one
+ * transitive NestedSet.
+ */
+abstract class ManyDirectOneTransitiveNestedSet<E> extends MemoizedUniquefierNestedSet<E> {
+
+ private final Object[] directs;
+ private final NestedSet<E> transitive;
+ private Object memo;
+
+ public ManyDirectOneTransitiveNestedSet(Object[] directs, NestedSet<E> transitive) {
+ this.directs = directs;
+ this.transitive = transitive;
+ }
+
+ @Override
+ Object getMemo() { return memo; }
+
+ @Override
+ void setMemo(Object memo) { this.memo = memo; }
+
+ @Override
+ Object[] directMembers() { return directs; }
+
+ @Override
+ NestedSet[] transitiveSets() { return new NestedSet[]{transitive}; }
+
+ @Override
+ public boolean shallowEquals(@Nullable NestedSet<? extends E> other) {
+ if (this == other) {
+ return true;
+ }
+ return other != null
+ && getOrder().equals(other.getOrder())
+ && other instanceof ManyDirectOneTransitiveNestedSet
+ && Arrays.equals(directs, ((ManyDirectOneTransitiveNestedSet) other).directs)
+ && transitive == ((ManyDirectOneTransitiveNestedSet) other).transitive;
+ }
+
+ @Override
+ public int shallowHashCode() {
+ return Objects.hash(getOrder(), Arrays.hashCode(directs), transitive); }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/collect/nestedset/MemoizedUniquefierNestedSet.java b/src/main/java/com/google/devtools/build/lib/collect/nestedset/MemoizedUniquefierNestedSet.java
new file mode 100644
index 0000000..2a7f1b6
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/collect/nestedset/MemoizedUniquefierNestedSet.java
@@ -0,0 +1,74 @@
+// Copyright 2014 Google Inc. 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.collect.nestedset;
+
+import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A NestedSet that keeps a memoized uniquifier so that it is faster to fill a set.
+ *
+ * <p>This class does not keep the memoized object itself so that we can take advantage of the
+ * memory field alignment (Memory alignment does not put in the same structure the fields of a
+ * class and its extensions).
+ */
+public abstract class MemoizedUniquefierNestedSet<E> extends NestedSet<E> {
+
+ @Override
+ public List<E> toList() {
+ ImmutableList.Builder<E> builder = new ImmutableList.Builder<>();
+ memoizedFill(builder);
+ return builder.build();
+ }
+
+ @Override
+ public Set<E> toSet() {
+ ImmutableSet.Builder<E> builder = new ImmutableSet.Builder<>();
+ memoizedFill(builder);
+ return builder.build();
+ }
+
+ /**
+ * It does not make sense to have a {@code MemoizedUniquefierNestedSet} if it is empty.
+ */
+ @Override
+ public boolean isEmpty() { return false; }
+
+ abstract Object getMemo();
+
+ abstract void setMemo(Object object);
+
+ /**
+ * Fill a collection builder by using a memoized {@code Uniqueifier} for faster uniqueness check.
+ */
+ final void memoizedFill(ImmutableCollection.Builder<E> builder) {
+ Uniqueifier memoed;
+ synchronized (this) {
+ Object memo = getMemo();
+ if (memo == null) {
+ RecordingUniqueifier uniqueifier = new RecordingUniqueifier();
+ getOrder().<E>expander().expandInto(this, uniqueifier, builder);
+ setMemo(uniqueifier.getMemo());
+ return;
+ } else {
+ memoed = RecordingUniqueifier.createReplayUniqueifier(memo);
+ }
+ }
+ getOrder().<E>expander().expandInto(this, memoed, builder);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/collect/nestedset/NaiveLinkOrderExpander.java b/src/main/java/com/google/devtools/build/lib/collect/nestedset/NaiveLinkOrderExpander.java
new file mode 100644
index 0000000..6c49103
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/collect/nestedset/NaiveLinkOrderExpander.java
@@ -0,0 +1,54 @@
+// Copyright 2014 Google Inc. 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.collect.nestedset;
+
+import com.google.common.collect.ImmutableCollection;
+
+/**
+ * A nested set expander that implements naive left-to-right preordering.
+ *
+ * <p>For example, for the nested set {B, D, {A, C}}, the iteration order is "B D A C".
+ *
+ * <p>This implementation is intended for backwards-compatible nested set replacements of code that
+ * uses naive preordering.
+ *
+ * <p>The implementation is called naive because it does no special treatment of dependency graphs
+ * that are not trees. For such graphs the property of parent-before-dependencies in the iteration
+ * order will not be upheld. For example, the diamond-shape graph A->{B, C}, B->{D}, C->{D} will be
+ * enumerated as "A B D C" rather than "A B C D" or "A C B D".
+ *
+ * <p>The difference from {@link LinkOrderNestedSet} is that this implementation gives priority to
+ * left-to-right order over dependencies-after-parent ordering. Note that the latter is usually more
+ * important, so please use {@link LinkOrderNestedSet} whenever possible.
+ */
+final class NaiveLinkOrderExpander<E> implements NestedSetExpander<E> {
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public void expandInto(NestedSet<E> set, Uniqueifier uniqueifier,
+ ImmutableCollection.Builder<E> builder) {
+
+ for (Object e : set.directMembers()) {
+ if (uniqueifier.isUnique(e)) {
+ builder.add((E) e);
+ }
+ }
+
+ for (NestedSet<E> subset : set.transitiveSets()) {
+ if (!subset.isEmpty() && uniqueifier.isUnique(subset)) {
+ expandInto(subset, uniqueifier, builder);
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/collect/nestedset/NaiveLinkOrderNestedSetFactory.java b/src/main/java/com/google/devtools/build/lib/collect/nestedset/NaiveLinkOrderNestedSetFactory.java
new file mode 100644
index 0000000..4677938
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/collect/nestedset/NaiveLinkOrderNestedSetFactory.java
@@ -0,0 +1,153 @@
+// Copyright 2014 Google Inc. 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.collect.nestedset;
+
+import com.google.common.collect.ImmutableList;
+
+/**
+ * NaiveLink order {@code NestedSet} factory.
+ */
+final class NaiveLinkOrderNestedSetFactory implements NestedSetFactory {
+
+ @Override
+ public <E> NestedSet<E> onlyDirects(Object[] directs) {
+ return new NaiveLinkOnlyDirectsNestedSet<>(directs);
+ }
+
+ @Override
+ public <E> NestedSet<E> onlyDirects(ImmutableList<E> directs) {
+ return new NaiveLinkImmutableListDirectsNestedSet<>(directs);
+ }
+
+ @Override
+ public <E> NestedSet<E> oneDirectOneTransitive(E direct, NestedSet<E> transitive) {
+ return new NaiveLinkOneDirectOneTransitiveNestedSet<>(direct, transitive);
+ }
+
+ @Override
+ public <E> NestedSet<E> manyDirectsOneTransitive(Object[] direct,
+ NestedSet<E> transitive) {
+ return new NaiveLinkManyDirectOneTransitiveNestedSet<>(direct, transitive);
+ }
+
+ @Override
+ public <E> NestedSet<E> onlyOneTransitive(NestedSet<E> transitive) {
+ return new NaiveLinkOnlyOneTransitiveNestedSet<>(transitive);
+ }
+
+ @Override
+ public <E> NestedSet<E> onlyManyTransitives(NestedSet[] transitives) {
+ return new NaiveLinkOnlyTransitivesNestedSet<>(transitives);
+ }
+
+ @Override
+ public <E> NestedSet<E> oneDirectManyTransitive(Object direct, NestedSet[] transitives) {
+ return new NaiveLinkOneDirectManyTransitive<>(direct, transitives);
+ }
+
+ @Override
+ public <E> NestedSet<E> manyDirectManyTransitive(Object[] directs, NestedSet[] transitives) {
+ return new NaiveLinkManyDirectManyTransitive<>(directs, transitives);
+ }
+
+ @Override
+ public <E> NestedSet<E> oneDirect(final E element) {
+ return new NaiveLinkSingleDirectNestedSet<>(element);
+ }
+
+ private static class NaiveLinkOnlyDirectsNestedSet<E> extends OnlyDirectsNestedSet<E> {
+
+ NaiveLinkOnlyDirectsNestedSet(Object[] directs) { super(directs); }
+
+ @Override
+ public Order getOrder() { return Order.NAIVE_LINK_ORDER; }
+ }
+
+ private static class NaiveLinkOneDirectOneTransitiveNestedSet<E> extends
+ OneDirectOneTransitiveNestedSet<E> {
+
+ private NaiveLinkOneDirectOneTransitiveNestedSet(E direct, NestedSet<E> transitive) {
+ super(direct, transitive);
+ }
+
+ @Override
+ public Order getOrder() { return Order.NAIVE_LINK_ORDER; }
+ }
+
+ private static class NaiveLinkOneDirectManyTransitive<E> extends OneDirectManyTransitive<E> {
+
+ private NaiveLinkOneDirectManyTransitive(Object direct, NestedSet[] transitive) {
+ super(direct, transitive);
+ }
+
+ @Override
+ public Order getOrder() { return Order.NAIVE_LINK_ORDER; }
+ }
+
+ private static class NaiveLinkManyDirectManyTransitive<E> extends ManyDirectManyTransitive<E> {
+
+ private NaiveLinkManyDirectManyTransitive(Object[] directs, NestedSet[] transitives) {
+ super(directs, transitives);
+ }
+
+ @Override
+ public Order getOrder() { return Order.NAIVE_LINK_ORDER; }
+ }
+
+ private static class NaiveLinkOnlyOneTransitiveNestedSet<E>
+ extends OnlyOneTransitiveNestedSet<E> {
+
+ private NaiveLinkOnlyOneTransitiveNestedSet(NestedSet<E> transitive) { super(transitive); }
+
+ @Override
+ public Order getOrder() { return Order.NAIVE_LINK_ORDER; }
+ }
+
+ private static class NaiveLinkManyDirectOneTransitiveNestedSet<E> extends
+ ManyDirectOneTransitiveNestedSet<E> {
+
+ private NaiveLinkManyDirectOneTransitiveNestedSet(Object[] direct,
+ NestedSet<E> transitive) { super(direct, transitive); }
+
+ @Override
+ public Order getOrder() { return Order.NAIVE_LINK_ORDER; }
+ }
+
+ private static class NaiveLinkOnlyTransitivesNestedSet<E> extends OnlyTransitivesNestedSet<E> {
+
+ private NaiveLinkOnlyTransitivesNestedSet(NestedSet[] transitives) { super(transitives); }
+
+ @Override
+ public Order getOrder() { return Order.NAIVE_LINK_ORDER; }
+ }
+
+ private static class NaiveLinkImmutableListDirectsNestedSet<E> extends
+ ImmutableListDirectsNestedSet<E> {
+
+ private NaiveLinkImmutableListDirectsNestedSet(ImmutableList<E> directs) { super(directs); }
+
+ @Override
+ public Order getOrder() {
+ return Order.NAIVE_LINK_ORDER;
+ }
+ }
+
+ private static class NaiveLinkSingleDirectNestedSet<E> extends SingleDirectNestedSet<E> {
+
+ private NaiveLinkSingleDirectNestedSet(E element) { super(element); }
+
+ @Override
+ public Order getOrder() { return Order.NAIVE_LINK_ORDER; }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/collect/nestedset/NestedSet.java b/src/main/java/com/google/devtools/build/lib/collect/nestedset/NestedSet.java
new file mode 100644
index 0000000..da074e0
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/collect/nestedset/NestedSet.java
@@ -0,0 +1,127 @@
+// Copyright 2014 Google Inc. 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.collect.nestedset;
+
+import com.google.common.base.Joiner;
+
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+/**
+ * A list-like iterable that supports efficient nesting.
+ *
+ * @see NestedSetBuilder
+ */
+public abstract class NestedSet<E> implements Iterable<E>, Serializable {
+
+ NestedSet() {}
+
+ /**
+ * Returns the ordering of this nested set.
+ */
+ public abstract Order getOrder();
+
+ /**
+ * Returns a collection of elements added to this specific set in an implementation-specified
+ * order.
+ *
+ * <p>Elements from subsets are not taken into account.
+ *
+ * <p>The reason for using Object[] instead of E[] is that when we build the NestedSet we
+ * would need to have access to the specific class that E represents in order to create an E
+ * array. Since this method is only designed to be used internally it is fine to keep it as
+ * Object[].
+ *
+ * <p>Callers of this method should only consume the objects and not modify the array.
+ */
+ abstract Object[] directMembers();
+
+ /**
+ * Returns the collection of sets included as subsets in this set.
+ *
+ * <p>Callers of this method should only consume the objects and not modify the array.
+ */
+ abstract NestedSet[] transitiveSets();
+
+ /**
+ * Returns true if the set is empty.
+ */
+ public abstract boolean isEmpty();
+
+ /**
+ * Returns a collection of all unique elements of this set (including subsets)
+ * in an implementation-specified order as a {@code Collection}.
+ *
+ * <p>If you do not need a Collection and an Iterable is enough, use the
+ * nested set itself as an Iterable.
+ */
+ public Collection<E> toCollection() {
+ return toList();
+ }
+
+ /**
+ * Returns a collection of all unique elements of this set (including subsets)
+ * in an implementation-specified order as a {code List}.
+ *
+ * <p>Use {@link #toCollection} when possible for better efficiency.
+ */
+ public abstract List<E> toList();
+
+ /**
+ * Returns a collection of all unique elements of this set (including subsets)
+ * in an implementation-specified order as a {@code Set}.
+ *
+ * <p>Use {@link #toCollection} when possible for better efficiency.
+ */
+ public abstract Set<E> toSet();
+
+ /**
+ * Returns true if this set is equal to {@code other} based on the top-level
+ * elements and object identity (==) of direct subsets. As such, this function
+ * can fail to equate {@code this} with another {@code NestedSet} that holds
+ * the same elements. It will never fail to detect that two {@code NestedSet}s
+ * are different, however.
+ *
+ * @param other the {@code NestedSet} to compare against.
+ */
+ public abstract boolean shallowEquals(@Nullable NestedSet<? extends E> other);
+
+ /**
+ * Returns a hash code that produces a notion of identity that is consistent with
+ * {@link #shallowEquals}. In other words, if two {@code NestedSet}s are equal according
+ * to {@code #shallowEquals}, then they return the same {@code shallowHashCode}.
+ *
+ * <p>The main reason for having these separate functions instead of reusing
+ * the standard equals/hashCode is to minimize accidental use, since they are
+ * different from both standard Java objects and collection-like objects.
+ */
+ public abstract int shallowHashCode();
+
+ @Override
+ public String toString() {
+ String members = Joiner.on(", ").join(directMembers());
+ String nestedSets = Joiner.on(", ").join(transitiveSets());
+ String separator = members.length() > 0 && nestedSets.length() > 0 ? ", " : "";
+ return "{" + members + separator + nestedSets + "}";
+ }
+
+ @Override
+ public Iterator<E> iterator() { return new NestedSetLazyIterator<>(this); }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/collect/nestedset/NestedSetBuilder.java b/src/main/java/com/google/devtools/build/lib/collect/nestedset/NestedSetBuilder.java
new file mode 100644
index 0000000..327fcdb
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/collect/nestedset/NestedSetBuilder.java
@@ -0,0 +1,253 @@
+// Copyright 2014 Google Inc. 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.collect.nestedset;
+
+import static com.google.common.collect.Iterables.getOnlyElement;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+
+import java.util.LinkedHashSet;
+
+/**
+ * A builder for nested sets.
+ *
+ * <p>The builder supports the standard builder interface (that is, {@code #add}, {@code #addAll}
+ * and {@code #addTransitive} followed by {@code build}), in addition to shortcut methods
+ * {@code #wrap} and {@code #of}.
+ */
+public final class NestedSetBuilder<E> {
+
+ private final Order order;
+ private final LinkedHashSet<E> items = new LinkedHashSet<>();
+ private final LinkedHashSet<NestedSet<? extends E>> transitiveSets = new LinkedHashSet<>();
+
+ public NestedSetBuilder(Order order) {
+ this.order = order;
+ }
+
+ /** Returns whether the set to be built is empty. */
+ public boolean isEmpty() {
+ return items.isEmpty() && transitiveSets.isEmpty();
+ }
+
+ /**
+ * Add an element.
+ *
+ * <p>Preserves ordering of added elements. Discards duplicate values.
+ * Throws an exception if a null value is passed in.
+ *
+ * <p>The collections of the direct members of the set and the nested sets are
+ * kept separate, so the order between multiple add/addAll calls matters,
+ * and the order between multiple addTransitive calls matters, but the order
+ * between add/addAll and addTransitive does not.
+ *
+ * @return the builder.
+ */
+ @SuppressWarnings("unchecked") // B is the type of the concrete subclass
+ public NestedSetBuilder<E> add(E element) {
+ Preconditions.checkNotNull(element);
+ items.add(element);
+ return this;
+ }
+
+ /**
+ * Adds a collection of elements to the set.
+ *
+ * <p>This is equivalent to invoking {@code add} for every item of the collection in iteration
+ * order.
+ *
+ * <p>The collections of the direct members of the set and the nested sets are kept separate, so
+ * the order between multiple add/addAll calls matters, and the order between multiple
+ * addTransitive calls matters, but the order between add/addAll and addTransitive does not.
+ *
+ * @return the builder.
+ */
+ @SuppressWarnings("unchecked") // B is the type of the concrete subclass
+ public NestedSetBuilder<E> addAll(Iterable<? extends E> elements) {
+ Preconditions.checkNotNull(elements);
+ Iterables.addAll(items, elements);
+ return this;
+ }
+
+ /**
+ * @deprecated Use {@link #addTransitive} to avoid excessive memory use.
+ */
+ @Deprecated
+ public NestedSetBuilder<E> addAll(NestedSet<E> elements) {
+ // Do not delete this method, or else addAll(Iterable) calls with a NestedSet argument
+ // will not be flagged.
+ Iterable<E> it = elements;
+ addAll(it);
+ return this;
+ }
+
+ /**
+ * Adds another nested set to this set.
+ *
+ * <p>Preserves ordering of added nested sets. Discards duplicate values. Throws an exception if
+ * a null value is passed in.
+ *
+ * <p>The collections of the direct members of the set and the nested sets are kept separate, so
+ * the order between multiple add/addAll calls matters, and the order between multiple
+ * addTransitive calls matters, but the order between add/addAll and addTransitive does not.
+ *
+ * <p>An error will be thrown if the ordering of {@code subset} is incompatible with the ordering
+ * of this set. Either they must match or this set must be a {@code STABLE_ORDER} set.
+ *
+ * @return the builder.
+ */
+ public NestedSetBuilder<E> addTransitive(NestedSet<? extends E> subset) {
+ Preconditions.checkNotNull(subset);
+ if (subset.getOrder() != order && order != Order.STABLE_ORDER
+ && subset.getOrder() != Order.STABLE_ORDER) {
+ // Note that this check is not strictly necessary, although keeping the nested set types
+ // consistent helps readability and protects against bugs. The polymorphism regarding
+ // STABLE_ORDER is allowed in order to be able to, e.g., include an arbitrary nested set in
+ // the inputs of an action, or include a nested set that is indifferent to its order in
+ // multiple nested sets.
+ throw new IllegalStateException(subset.getOrder() + " != " + order);
+ }
+ if (!subset.isEmpty()) {
+ transitiveSets.add(subset);
+ }
+ return this;
+ }
+
+ /**
+ * Builds the actual nested set.
+ *
+ * <p>This method may be called multiple times with interleaved {@link #add}, {@link #addAll} and
+ * {@link #addTransitive} calls.
+ */
+ // Casting from LinkedHashSet<NestedSet<? extends E>> to LinkedHashSet<NestedSet<E>> by way of
+ // LinkedHashSet<?>.
+ @SuppressWarnings("unchecked")
+ public NestedSet<E> build() {
+ if (isEmpty()) {
+ return order.emptySet();
+ }
+
+ // This cast is safe because NestedSets are immutable -- we will never try to add an element to
+ // these nested sets, only to retrieve elements from them. Thus, treating them as NestedSet<E>
+ // is safe.
+ LinkedHashSet<NestedSet<E>> transitiveSetsCast =
+ (LinkedHashSet<NestedSet<E>>) (LinkedHashSet<?>) transitiveSets;
+ if (items.isEmpty() && (transitiveSetsCast.size() == 1)) {
+ NestedSet<E> candidate = getOnlyElement(transitiveSetsCast);
+ if (candidate.getOrder().equals(order)) {
+ return candidate;
+ }
+ }
+ int transitiveSize = transitiveSets.size();
+ int directSize = items.size();
+
+ switch (transitiveSize) {
+ case 0:
+ switch (directSize) {
+ case 0:
+ return order.emptySet();
+ case 1:
+ return order.factory.oneDirect(getOnlyElement(items));
+ default:
+ return order.factory.onlyDirects(items.toArray());
+ }
+ case 1:
+ switch (directSize) {
+ case 0:
+ return order.factory.onlyOneTransitive(getOnlyElement(transitiveSetsCast));
+ case 1:
+ return order.factory.oneDirectOneTransitive(getOnlyElement(items),
+ getOnlyElement(transitiveSetsCast));
+ default:
+ return order.factory.manyDirectsOneTransitive(items.toArray(),
+ getOnlyElement(transitiveSetsCast));
+ }
+ default:
+ switch (directSize) {
+ case 0:
+ return order.factory.onlyManyTransitives(
+ transitiveSetsCast.toArray(new NestedSet[transitiveSize]));
+ case 1:
+ return order.factory.oneDirectManyTransitive(getOnlyElement(items), transitiveSetsCast
+ .toArray(new NestedSet[transitiveSize]));
+ default:
+ return order.factory.manyDirectManyTransitive(items.toArray(),
+ transitiveSetsCast.toArray(new NestedSet[transitiveSize]));
+ }
+ }
+ }
+
+ /**
+ * Creates a nested set from a given list of items.
+ *
+ * <p>If the list of items is an {@link ImmutableList}, reuses the list as the backing store for
+ * the nested set.
+ */
+ public static <E> NestedSet<E> wrap(Order order, Iterable<E> wrappedItems) {
+ ImmutableList<E> wrappedList = ImmutableList.copyOf(wrappedItems);
+ if (wrappedList.isEmpty()) {
+ return order.emptySet();
+ } else if (wrappedList.size() == 1) {
+ return order.factory.oneDirect(getOnlyElement(wrappedItems));
+ } else {
+ return order.factory.onlyDirects(wrappedList);
+ }
+ }
+
+
+ /**
+ * Creates a nested set with the given list of items as its elements.
+ */
+ @SuppressWarnings("unchecked")
+ public static <E> NestedSet<E> create(Order order, E... elems) {
+ return wrap(order, ImmutableList.copyOf(elems));
+ }
+
+ /**
+ * Creates an empty nested set.
+ */
+ public static <E> NestedSet<E> emptySet(Order order) {
+ return order.emptySet();
+ }
+
+ /**
+ * Creates a builder for stable order nested sets.
+ */
+ public static <E> NestedSetBuilder<E> stableOrder() {
+ return new NestedSetBuilder<>(Order.STABLE_ORDER);
+ }
+
+ /**
+ * Creates a builder for compile order nested sets.
+ */
+ public static <E> NestedSetBuilder<E> compileOrder() {
+ return new NestedSetBuilder<>(Order.COMPILE_ORDER);
+ }
+
+ /**
+ * Creates a builder for link order nested sets.
+ */
+ public static <E> NestedSetBuilder<E> linkOrder() {
+ return new NestedSetBuilder<>(Order.LINK_ORDER);
+ }
+
+ /**
+ * Creates a builder for naive link order nested sets.
+ */
+ public static <E> NestedSetBuilder<E> naiveLinkOrder() {
+ return new NestedSetBuilder<>(Order.NAIVE_LINK_ORDER);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/collect/nestedset/NestedSetExpander.java b/src/main/java/com/google/devtools/build/lib/collect/nestedset/NestedSetExpander.java
new file mode 100644
index 0000000..c04c39d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/collect/nestedset/NestedSetExpander.java
@@ -0,0 +1,30 @@
+// Copyright 2014 Google Inc. 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.collect.nestedset;
+
+import com.google.common.collect.ImmutableCollection;
+
+/**
+ * An expander that converts a nested set into a flattened collection.
+ *
+ * <p>Expanders are initialized statically (there is one for each order), so they should
+ * contain no state and all methods must be threadsafe.
+ */
+interface NestedSetExpander<E> {
+ /**
+ * Flattens the NestedSet into the builder.
+ */
+ void expandInto(NestedSet<E> nestedSet, Uniqueifier uniqueifier,
+ ImmutableCollection.Builder<E> builder);
+}
diff --git a/src/main/java/com/google/devtools/build/lib/collect/nestedset/NestedSetFactory.java b/src/main/java/com/google/devtools/build/lib/collect/nestedset/NestedSetFactory.java
new file mode 100644
index 0000000..99fb8bb
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/collect/nestedset/NestedSetFactory.java
@@ -0,0 +1,55 @@
+// Copyright 2014 Google Inc. 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.collect.nestedset;
+
+import com.google.common.collect.ImmutableList;
+
+/**
+ * Factory methods for creating {@link NestedSet}s of specific shapes. This allows the
+ * implementation to be memory efficient (e.g. a specialized implementation for the case where
+ * there are only direct elements, etc).
+ *
+ * <p>It's intended for each {@link Order} to have its own factory implementation. That way we can
+ * be even more efficient since the {@link NestedSet}s instances don't need to store their
+ * {@link Order}.
+ */
+interface NestedSetFactory {
+
+ /** Create a NestedSet with just one direct element and not transitive elements. */
+ <E> NestedSet<E> oneDirect(E element);
+
+ /** Create a NestedSet with only direct elements. */
+ <E> NestedSet<E> onlyDirects(Object[] directs);
+
+ /** Create a NestedSet with only direct elements potentially sharing the ImmutableList. */
+ <E> NestedSet<E> onlyDirects(ImmutableList<E> directs);
+
+ /** Create a NestedSet with one direct element and one transitive {@code NestedSet}. */
+ <E> NestedSet<E> oneDirectOneTransitive(E direct, NestedSet<E> transitive);
+
+ /** Create a NestedSet with many direct elements and one transitive {@code NestedSet}. */
+ <E> NestedSet<E> manyDirectsOneTransitive(Object[] direct, NestedSet<E> transitive);
+
+ /** Create a NestedSet with no direct elements and one transitive {@code NestedSet.} */
+ <E> NestedSet<E> onlyOneTransitive(NestedSet<E> transitive);
+
+ /** Create a NestedSet with no direct elements and many transitive {@code NestedSet}s. */
+ <E> NestedSet<E> onlyManyTransitives(NestedSet[] transitives);
+
+ /** Create a NestedSet with one direct elements and many transitive {@code NestedSet}s. */
+ <E> NestedSet<E> oneDirectManyTransitive(Object direct, NestedSet[] transitive);
+
+ /** Create a NestedSet with many direct elements and many transitive {@code NestedSet}s. */
+ <E> NestedSet<E> manyDirectManyTransitive(Object[] directs, NestedSet[] transitive);
+}
diff --git a/src/main/java/com/google/devtools/build/lib/collect/nestedset/NestedSetLazyIterator.java b/src/main/java/com/google/devtools/build/lib/collect/nestedset/NestedSetLazyIterator.java
new file mode 100644
index 0000000..c873d56
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/collect/nestedset/NestedSetLazyIterator.java
@@ -0,0 +1,53 @@
+// Copyright 2014 Google Inc. 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.collect.nestedset;
+
+import java.util.Iterator;
+
+/**
+ * A NestedSet iterator that only expands the NestedSet when the first element is requested. This
+ * allows code that calls unconditionally to {@code hasNext} to check if the iterator is empty
+ * to not expand the nested set.
+ */
+final class NestedSetLazyIterator<E> implements Iterator<E> {
+
+ private NestedSet<E> nestedSet;
+ private Iterator<E> delegate = null;
+
+ NestedSetLazyIterator(NestedSet<E> nestedSet) {
+ this.nestedSet = nestedSet;
+ }
+
+ @Override
+ public boolean hasNext() {
+ if (delegate == null) {
+ return !nestedSet.isEmpty();
+ }
+ return delegate.hasNext();
+ }
+
+ @Override
+ public E next() {
+ if (delegate == null) {
+ delegate = nestedSet.toCollection().iterator();
+ nestedSet = null;
+ }
+ return delegate.next();
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/collect/nestedset/NestedSetVisitor.java b/src/main/java/com/google/devtools/build/lib/collect/nestedset/NestedSetVisitor.java
new file mode 100644
index 0000000..ea8b810
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/collect/nestedset/NestedSetVisitor.java
@@ -0,0 +1,96 @@
+// Copyright 2014 Google Inc. 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.collect.nestedset;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Sets;
+
+import java.util.Set;
+
+/**
+ * NestedSetVisitor facilitates a transitive visitation over a NestedSet, which must be in STABLE
+ * order. The callback may be called from multiple threads, and must be thread-safe.
+ *
+ * <p>The visitation is iterative: The caller may invoke a NestedSet within the top-level NestedSet
+ * in any order.
+ *
+ * <p>Currently this class is only used in Skyframe to facilitate iterative replay of transitive
+ * warnings/errors.
+ *
+ * @param <E> the data type
+ */
+// @ThreadSafety.ThreadSafe
+public final class NestedSetVisitor<E> {
+
+ /**
+ * For each element of the NestedSet the {@code Reciver} will receive one element during the
+ * visitation.
+ */
+ public interface Receiver<E> {
+ void accept(E arg);
+ }
+
+ private final Receiver<E> callback;
+
+ private final VisitedState<E> visited;
+
+ public NestedSetVisitor(Receiver<E> callback, VisitedState<E> visited) {
+ this.callback = Preconditions.checkNotNull(callback);
+ this.visited = Preconditions.checkNotNull(visited);
+ }
+
+ /**
+ * Transitively visit a nested set.
+ *
+ * @param nestedSet the nested set to visit transitively.
+ *
+ */
+ @SuppressWarnings("unchecked")
+ public void visit(NestedSet<E> nestedSet) {
+ // This method suppresses the unchecked warning so that it can access the internal NestedSet
+ // raw structure.
+ Preconditions.checkArgument(nestedSet.getOrder() == Order.STABLE_ORDER);
+ if (!visited.add(nestedSet)) {
+ return;
+ }
+
+ for (NestedSet<E> subset : nestedSet.transitiveSets()) {
+ visit(subset);
+ }
+ for (Object member : nestedSet.directMembers()) {
+ if (visited.add((E) member)) {
+ callback.accept((E) member);
+ }
+ }
+ }
+
+ /** A class that allows us to keep track of the seen nodes and transitive sets. */
+ public static class VisitedState<E> {
+ private final Set<NestedSet<E>> seenSets = Sets.newConcurrentHashSet();
+ private final Set<E> seenNodes = Sets.newConcurrentHashSet();
+
+ public void clear() {
+ seenSets.clear();
+ seenNodes.clear();
+ }
+
+ private boolean add(E node) {
+ return seenNodes.add(node);
+ }
+
+ private boolean add(NestedSet<E> set) {
+ return seenSets.add(set);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/collect/nestedset/OneDirectManyTransitive.java b/src/main/java/com/google/devtools/build/lib/collect/nestedset/OneDirectManyTransitive.java
new file mode 100644
index 0000000..a99883c
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/collect/nestedset/OneDirectManyTransitive.java
@@ -0,0 +1,63 @@
+// Copyright 2014 Google Inc. 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.collect.nestedset;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+import javax.annotation.Nullable;
+
+/**
+ * Memory-optimized NestedSet implementation for NestedSets with one direct element and
+ * many transitive dependencies.
+ */
+abstract class OneDirectManyTransitive<E> extends MemoizedUniquefierNestedSet<E> {
+
+ private final Object direct;
+ private final NestedSet[] transitives;
+ private Object memo;
+
+ OneDirectManyTransitive(Object direct, NestedSet[] transitives) {
+ this.direct = direct;
+ this.transitives = transitives;
+ }
+
+ @Override
+ Object getMemo() { return memo; }
+
+ @Override
+ void setMemo(Object memo) { this.memo = memo; }
+
+ @Override
+ Object[] directMembers() { return new Object[]{direct}; }
+
+ @Override
+ NestedSet[] transitiveSets() { return transitives; }
+
+ @Override
+ public boolean shallowEquals(@Nullable NestedSet<? extends E> other) {
+ if (this == other) {
+ return true;
+ }
+ return other != null
+ && getOrder().equals(other.getOrder())
+ && other instanceof OneDirectManyTransitive
+ && direct.equals(((OneDirectManyTransitive) other).direct)
+ && Arrays.equals(transitives, ((OneDirectManyTransitive) other).transitives);
+ }
+
+ @Override
+ public int shallowHashCode() {
+ return Objects.hash(getOrder(), direct, Arrays.hashCode(transitives)); }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/collect/nestedset/OneDirectOneTransitiveNestedSet.java b/src/main/java/com/google/devtools/build/lib/collect/nestedset/OneDirectOneTransitiveNestedSet.java
new file mode 100644
index 0000000..9acdba1
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/collect/nestedset/OneDirectOneTransitiveNestedSet.java
@@ -0,0 +1,61 @@
+// Copyright 2014 Google Inc. 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.collect.nestedset;
+
+import java.util.Objects;
+
+import javax.annotation.Nullable;
+
+/**
+ * Memory-efficient implementation for the case where we have one direct element and one
+ * transitive NestedSet.
+ */
+abstract class OneDirectOneTransitiveNestedSet<E> extends MemoizedUniquefierNestedSet<E> {
+
+ private final E direct;
+ private final NestedSet<E> transitive;
+ private Object memo;
+
+ OneDirectOneTransitiveNestedSet(E direct, NestedSet<E> transitive) {
+ this.direct = direct;
+ this.transitive = transitive;
+ }
+
+ @Override
+ Object getMemo() { return memo; }
+
+ @Override
+ void setMemo(Object memo) { this.memo = memo; }
+
+ @Override
+ Object[] directMembers() { return new Object[]{direct}; }
+
+ @Override
+ NestedSet[] transitiveSets() { return new NestedSet[]{transitive}; }
+
+ @Override
+ public boolean shallowEquals(@Nullable NestedSet<? extends E> other) {
+ if (this == other) {
+ return true;
+ }
+ return other != null
+ && getOrder().equals(other.getOrder())
+ && other instanceof OneDirectOneTransitiveNestedSet
+ && direct.equals(((OneDirectOneTransitiveNestedSet) other).direct)
+ && transitive == ((OneDirectOneTransitiveNestedSet) other).transitive;
+ }
+
+ @Override
+ public int shallowHashCode() { return Objects.hash(getOrder(), direct, transitive); }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/collect/nestedset/OnlyDirectsNestedSet.java b/src/main/java/com/google/devtools/build/lib/collect/nestedset/OnlyDirectsNestedSet.java
new file mode 100644
index 0000000..9f7588d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/collect/nestedset/OnlyDirectsNestedSet.java
@@ -0,0 +1,88 @@
+// Copyright 2014 Google Inc. 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.collect.nestedset;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+/**
+ * Memory-optimized NestedSet implementation for NestedSets without transitive dependencies.
+ *
+ */
+abstract class OnlyDirectsNestedSet<E> extends NestedSet<E> {
+
+ private static final NestedSet[] EMPTY = new NestedSet[0];
+ private final Object[] directDeps;
+
+ public OnlyDirectsNestedSet(Object[] directDeps) { this.directDeps = directDeps; }
+
+ @Override
+ public abstract Order getOrder();
+
+ @Override
+ Object[] directMembers() {
+ return directDeps;
+ }
+
+ @Override
+ NestedSet[] transitiveSets() {
+ return EMPTY;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return false;
+ }
+
+ /**
+ * Currently all the Order implementations return the direct elements in the same order if they
+ * do not have transitive elements. So we skip calling order.getExpander()... and return a view
+ * of the array.
+ */
+ @SuppressWarnings("unchecked")
+ @Override
+ public List<E> toList() {
+ return (List<E>) ImmutableList.copyOf(directDeps);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public Set<E> toSet() {
+ return (Set<E>) ImmutableSet.copyOf(directDeps);
+ }
+
+ @Override
+ public boolean shallowEquals(@Nullable NestedSet<? extends E> other) {
+ if (this == other) {
+ return true;
+ }
+ if (other == null) {
+ return false;
+ }
+ return getOrder().equals(other.getOrder())
+ && other instanceof OnlyDirectsNestedSet
+ && Arrays.equals(directDeps, ((OnlyDirectsNestedSet) other).directDeps);
+ }
+
+ @Override
+ public int shallowHashCode() {
+ return Arrays.hashCode(directDeps);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/collect/nestedset/OnlyOneTransitiveNestedSet.java b/src/main/java/com/google/devtools/build/lib/collect/nestedset/OnlyOneTransitiveNestedSet.java
new file mode 100644
index 0000000..a3e2d1d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/collect/nestedset/OnlyOneTransitiveNestedSet.java
@@ -0,0 +1,68 @@
+// Copyright 2014 Google Inc. 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.collect.nestedset;
+
+import java.util.Objects;
+
+import javax.annotation.Nullable;
+
+/**
+ * Memory-efficient implementation for the case where we only have one transitive NestedSet.
+ *
+ * <p>Note that we cannot simply delegate to the inner NestedSet because the order could be
+ * different and the top-level order is the correct one.
+ */
+abstract class OnlyOneTransitiveNestedSet<E> extends MemoizedUniquefierNestedSet<E> {
+
+ private static final Object[] EMPTY = new Object[0];
+
+ private final NestedSet<E> transitive;
+ private Object memo;
+
+ public OnlyOneTransitiveNestedSet(NestedSet<E> transitive) {
+ this.transitive = transitive;
+ }
+
+ @Override
+ Object getMemo() { return memo; }
+
+ @Override
+ void setMemo(Object memo) { this.memo = memo; }
+
+ @Override
+ Object[] directMembers() {
+ return EMPTY;
+ }
+
+ @Override
+ NestedSet[] transitiveSets() {
+ return new NestedSet[]{transitive};
+ }
+
+ @Override
+ public boolean shallowEquals(@Nullable NestedSet<? extends E> other) {
+ if (this == other) {
+ return true;
+ }
+ return other != null
+ && getOrder().equals(other.getOrder())
+ && other instanceof OnlyOneTransitiveNestedSet
+ && transitive == ((OnlyOneTransitiveNestedSet) other).transitive;
+ }
+
+ @Override
+ public int shallowHashCode() {
+ return Objects.hash(getOrder(), transitive);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/collect/nestedset/OnlyTransitivesNestedSet.java b/src/main/java/com/google/devtools/build/lib/collect/nestedset/OnlyTransitivesNestedSet.java
new file mode 100644
index 0000000..5a57053
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/collect/nestedset/OnlyTransitivesNestedSet.java
@@ -0,0 +1,62 @@
+// Copyright 2014 Google Inc. 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.collect.nestedset;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+import javax.annotation.Nullable;
+
+/**
+ * Memory-efficient implementation for the case where we have one direct element and one
+ * transitive NestedSet.
+ */
+abstract class OnlyTransitivesNestedSet<E> extends MemoizedUniquefierNestedSet<E> {
+
+ private static final NestedSet[] EMPTY = new NestedSet[0];
+
+ private final NestedSet[] transitives;
+ private Object memo;
+
+ OnlyTransitivesNestedSet(NestedSet[] transitives) {
+ this.transitives = transitives;
+ }
+
+ @Override
+ Object getMemo() { return memo; }
+
+ @Override
+ void setMemo(Object memo) { this.memo = memo; }
+
+ @Override
+ Object[] directMembers() { return EMPTY; }
+
+ @Override
+ NestedSet[] transitiveSets() { return transitives; }
+
+ @Override
+ public boolean shallowEquals(@Nullable NestedSet<? extends E> other) {
+ if (this == other) {
+ return true;
+ }
+ return other != null
+ && getOrder().equals(other.getOrder())
+ && other instanceof OnlyTransitivesNestedSet
+ && Arrays.equals(transitives, ((OnlyTransitivesNestedSet) other).transitives);
+ }
+
+ @Override
+ public int shallowHashCode() {
+ return Objects.hash(getOrder(), Arrays.hashCode(transitives)); }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/collect/nestedset/Order.java b/src/main/java/com/google/devtools/build/lib/collect/nestedset/Order.java
new file mode 100644
index 0000000..38e6633
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/collect/nestedset/Order.java
@@ -0,0 +1,52 @@
+// Copyright 2014 Google Inc. 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.collect.nestedset;
+
+/**
+ * Type of a nested set (defines order).
+ */
+public enum Order {
+
+ STABLE_ORDER(new CompileOrderExpander<>(), new StableOrderNestedSetFactory()),
+ COMPILE_ORDER(new CompileOrderExpander<>(), new CompileOrderNestedSetFactory()),
+ LINK_ORDER(new LinkOrderExpander<>(), new LinkOrderNestedSetFactory()),
+ NAIVE_LINK_ORDER(new NaiveLinkOrderExpander<>(), new NaiveLinkOrderNestedSetFactory());
+
+ private final NestedSetExpander<?> expander;
+ final NestedSetFactory factory;
+ private final NestedSet<?> emptySet;
+
+ private Order(NestedSetExpander<?> expander, NestedSetFactory factory) {
+ this.expander = expander;
+ this.factory = factory;
+ this.emptySet = new EmptyNestedSet<Object>(this);
+ }
+
+ /**
+ * Returns an empty set of the given ordering.
+ */
+ @SuppressWarnings("unchecked") // Nested sets are immutable, so a downcast is fine.
+ <E> NestedSet<E> emptySet() {
+ return (NestedSet<E>) emptySet;
+ }
+
+ /**
+ * Returns an empty set of the given ordering.
+ */
+ @SuppressWarnings("unchecked") // Nested set expanders contain no data themselves.
+ <E> NestedSetExpander<E> expander() {
+ return (NestedSetExpander<E>) expander;
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/collect/nestedset/RecordingUniqueifier.java b/src/main/java/com/google/devtools/build/lib/collect/nestedset/RecordingUniqueifier.java
new file mode 100644
index 0000000..c71f200
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/collect/nestedset/RecordingUniqueifier.java
@@ -0,0 +1,140 @@
+// Copyright 2014 Google Inc. 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.collect.nestedset;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+
+import java.util.BitSet;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * A Uniqueifier that records a transcript of its interactions with the underlying Set. A memo can
+ * then be retrieved in the form of another Uniqueifier, and given the same sequence of isUnique
+ * calls, the Set interactions can be avoided.
+ */
+class RecordingUniqueifier implements Uniqueifier {
+ /**
+ * Unshared byte memos under this length are not constructed.
+ */
+ @VisibleForTesting static final int LENGTH_THRESHOLD = 4096 / 8; // bits -> bytes
+
+ /**
+ * Returned as a marker that memoization was not worthwhile.
+ */
+ private static final Object NO_MEMO = new Object();
+
+ /**
+ * Shared one-byte memos.
+ */
+ private static final byte[][] SHARED_SMALL_MEMOS_1;
+
+ /**
+ * Shared two-byte memos.
+ */
+ private static final byte[][] SHARED_SMALL_MEMOS_2;
+
+ static {
+ // Create interned arrays for one and two byte memos
+ // The memos always start with 0x3, so some one and two byte arrays can be skipped.
+
+ byte[][] memos1 = new byte[64][1];
+ for (int i = 0; i < 64; i++) {
+ memos1[i][0] = (byte) ((i << 2) | 0x3);
+ }
+ SHARED_SMALL_MEMOS_1 = memos1;
+
+ byte[][] memos2 = new byte[16384][2];
+ for (int i = 0; i < 64; i++) {
+ byte iAdj = (byte) (0x3 | (i << 2));
+ for (int j = 0; j < 256; j++) {
+ int idx = i | (j << 6);
+ memos2[idx][0] = iAdj;
+ memos2[idx][1] = (byte) j;
+ }
+ }
+ SHARED_SMALL_MEMOS_2 = memos2;
+ }
+
+ private final Set<Object> witnessed = new HashSet<>(256);
+ private final BitSet memo = new BitSet();
+ private int idx = 0;
+
+ static Uniqueifier createReplayUniqueifier(Object memo) {
+ if (memo == NO_MEMO) {
+ return new RecordingUniqueifier();
+ } else if (memo instanceof Integer) {
+ BitSet bs = new BitSet();
+ bs.set(0, (Integer) memo);
+ return new ReplayUniqueifier(bs);
+ }
+ return new ReplayUniqueifier(BitSet.valueOf((byte[]) memo));
+ }
+
+ @Override
+ public boolean isUnique(Object o) {
+ boolean firstInstance = witnessed.add(o);
+ memo.set(idx++, firstInstance);
+ return firstInstance;
+ }
+
+ /**
+ * Gets the memo of the set interactions. Do not call isUnique after this point.
+ */
+ Object getMemo() {
+ this.idx = -1; // will cause failures if isUnique is called after getMemo.
+
+ // If the bitset is just a contiguous block of ones, use a length memo
+ int length = memo.length();
+ if (memo.cardinality() == length) {
+ return length; // length-based memo
+ }
+
+ byte[] ba = memo.toByteArray();
+
+ Preconditions.checkState(
+ (length < 2) || ((ba[0] & 3) == 3),
+ "The memo machinery expects memos to always begin with two 1 bits, "
+ + "but instead, this memo starts with %X.", ba[0]);
+
+ // For short memos, use an interned array for the memo
+ if (ba.length == 1) {
+ return SHARED_SMALL_MEMOS_1[(0xFF & ba[0]) >>> 2]; // shared memo
+ } else if (ba.length == 2) {
+ return SHARED_SMALL_MEMOS_2[((ba[1] & 0xFF) << 6) | ((0xFF & ba[0]) >>> 2)];
+ }
+
+ // For mid-sized cases, skip the memo since it is not worthwhile
+ if (ba.length < LENGTH_THRESHOLD) {
+ return NO_MEMO; // skipped memo
+ }
+
+ return ba; // normal memo
+ }
+
+ private static final class ReplayUniqueifier implements Uniqueifier {
+ private final BitSet memo;
+ private int idx = 0;
+
+ ReplayUniqueifier(BitSet memo) {
+ this.memo = memo;
+ }
+
+ @Override
+ public boolean isUnique(Object o) {
+ return memo.get(idx++);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/collect/nestedset/SingleDirectNestedSet.java b/src/main/java/com/google/devtools/build/lib/collect/nestedset/SingleDirectNestedSet.java
new file mode 100644
index 0000000..dc4a5fb
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/collect/nestedset/SingleDirectNestedSet.java
@@ -0,0 +1,68 @@
+// Copyright 2014 Google Inc. 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.collect.nestedset;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterators;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+/**
+ * Memory-efficient implementation for nested sets with one element.
+ */
+public abstract class SingleDirectNestedSet<E> extends NestedSet<E> {
+
+ private static final NestedSet[] EMPTY = new NestedSet[0];
+ private final E e;
+
+ public SingleDirectNestedSet(E e) { this.e = Preconditions.checkNotNull(e); }
+
+ @Override
+ public Iterator<E> iterator() { return Iterators.singletonIterator(e); }
+
+ @Override
+ Object[] directMembers() { return new Object[]{e}; }
+
+ @Override
+ NestedSet[] transitiveSets() { return EMPTY; }
+
+ @Override
+ public boolean isEmpty() { return false; }
+
+ @Override
+ public List<E> toList() { return ImmutableList.of(e); }
+
+ @Override
+ public Set<E> toSet() { return ImmutableSet.of(e); }
+
+ @Override
+ public boolean shallowEquals(@Nullable NestedSet<? extends E> other) {
+ if (this == other) {
+ return true;
+ }
+ return other instanceof SingleDirectNestedSet
+ && e.equals(((SingleDirectNestedSet) other).e);
+ }
+
+ @Override
+ public int shallowHashCode() {
+ return e.hashCode();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/collect/nestedset/StableOrderNestedSetFactory.java b/src/main/java/com/google/devtools/build/lib/collect/nestedset/StableOrderNestedSetFactory.java
new file mode 100644
index 0000000..cd0b618
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/collect/nestedset/StableOrderNestedSetFactory.java
@@ -0,0 +1,152 @@
+// Copyright 2014 Google Inc. 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.collect.nestedset;
+
+import com.google.common.collect.ImmutableList;
+
+/**
+ * Stable order {@code NestedSet} factory.
+ */
+final class StableOrderNestedSetFactory implements NestedSetFactory {
+
+ @Override
+ public <E> NestedSet<E> onlyDirects(Object[] directs) {
+ return new StableOnlyDirectsNestedSet<>(directs);
+ }
+
+ @Override
+ public <E> NestedSet<E> onlyDirects(ImmutableList<E> directs) {
+ return new StableImmutableListDirectsNestedSet<>(directs);
+ }
+
+ @Override
+ public <E> NestedSet<E> oneDirectOneTransitive(E direct, NestedSet<E> transitive) {
+ return new StableOneDirectOneTransitiveNestedSet<>(direct, transitive);
+ }
+
+ @Override
+ public <E> NestedSet<E> manyDirectsOneTransitive(Object[] direct,
+ NestedSet<E> transitive) {
+ return new StableManyDirectOneTransitiveNestedSet<>(direct, transitive);
+ }
+
+ @Override
+ public <E> NestedSet<E> onlyOneTransitive(NestedSet<E> transitive) {
+ return new StableOnlyOneTransitiveNestedSet<>(transitive);
+ }
+
+ @Override
+ public <E> NestedSet<E> onlyManyTransitives(NestedSet[] transitives) {
+ return new StableOnlyTransitivesNestedSet<>(transitives);
+ }
+
+ @Override
+ public <E> NestedSet<E> oneDirectManyTransitive(Object direct, NestedSet[] transitives) {
+ return new StableOneDirectManyTransitive<>(direct, transitives);
+ }
+
+ @Override
+ public <E> NestedSet<E> manyDirectManyTransitive(Object[] directs, NestedSet[] transitives) {
+ return new StableManyDirectManyTransitive<>(directs, transitives);
+ }
+
+ @Override
+ public <E> NestedSet<E> oneDirect(final E element) {
+ return new StableSingleDirectNestedSet<>(element);
+ }
+
+ private static class StableOnlyDirectsNestedSet<E> extends OnlyDirectsNestedSet<E> {
+
+ StableOnlyDirectsNestedSet(Object[] directs) { super(directs); }
+
+ @Override
+ public Order getOrder() { return Order.STABLE_ORDER; }
+ }
+
+ private static class StableOneDirectOneTransitiveNestedSet<E> extends
+ OneDirectOneTransitiveNestedSet<E> {
+
+ private StableOneDirectOneTransitiveNestedSet(E direct, NestedSet<E> transitive) {
+ super(direct, transitive);
+ }
+
+ @Override
+ public Order getOrder() { return Order.STABLE_ORDER; }
+ }
+
+ private static class StableOneDirectManyTransitive<E> extends OneDirectManyTransitive<E> {
+
+ private StableOneDirectManyTransitive(Object direct, NestedSet[] transitive) {
+ super(direct, transitive);
+ }
+
+ @Override
+ public Order getOrder() { return Order.STABLE_ORDER; }
+ }
+
+ private static class StableManyDirectManyTransitive<E> extends ManyDirectManyTransitive<E> {
+
+ private StableManyDirectManyTransitive(Object[] directs, NestedSet[] transitives) {
+ super(directs, transitives);
+ }
+
+ @Override
+ public Order getOrder() { return Order.STABLE_ORDER; }
+ }
+
+ private static class StableOnlyOneTransitiveNestedSet<E> extends OnlyOneTransitiveNestedSet<E> {
+
+ private StableOnlyOneTransitiveNestedSet(NestedSet<E> transitive) { super(transitive); }
+
+ @Override
+ public Order getOrder() { return Order.STABLE_ORDER; }
+ }
+
+ private static class StableManyDirectOneTransitiveNestedSet<E> extends
+ ManyDirectOneTransitiveNestedSet<E> {
+
+ private StableManyDirectOneTransitiveNestedSet(Object[] direct,
+ NestedSet<E> transitive) { super(direct, transitive); }
+
+ @Override
+ public Order getOrder() { return Order.STABLE_ORDER; }
+ }
+
+ private static class StableOnlyTransitivesNestedSet<E> extends OnlyTransitivesNestedSet<E> {
+
+ private StableOnlyTransitivesNestedSet(NestedSet[] transitives) { super(transitives); }
+
+ @Override
+ public Order getOrder() { return Order.STABLE_ORDER; }
+ }
+
+ private static class StableImmutableListDirectsNestedSet<E> extends
+ ImmutableListDirectsNestedSet<E> {
+
+ private StableImmutableListDirectsNestedSet(ImmutableList<E> directs) { super(directs); }
+
+ @Override
+ public Order getOrder() {
+ return Order.STABLE_ORDER;
+ }
+ }
+
+ private static class StableSingleDirectNestedSet<E> extends SingleDirectNestedSet<E> {
+
+ private StableSingleDirectNestedSet(E element) { super(element); }
+
+ @Override
+ public Order getOrder() { return Order.STABLE_ORDER; }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/collect/nestedset/Uniqueifier.java b/src/main/java/com/google/devtools/build/lib/collect/nestedset/Uniqueifier.java
new file mode 100644
index 0000000..9421a57
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/collect/nestedset/Uniqueifier.java
@@ -0,0 +1,26 @@
+// Copyright 2014 Google Inc. 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.collect.nestedset;
+
+/**
+ * Helps reduce a sequence of potentially duplicated elements to a sequence of unique elements.
+ */
+interface Uniqueifier {
+
+ /**
+ * Returns true if-and-only-if this is the first time that this {@link Uniqueifier}'s method has
+ * been called with this Object. This uses Object.equals-type equality for the comparison.
+ */
+ public boolean isUnique(Object o);
+}
diff --git a/src/main/java/com/google/devtools/build/lib/concurrent/AbstractQueueVisitor.java b/src/main/java/com/google/devtools/build/lib/concurrent/AbstractQueueVisitor.java
new file mode 100644
index 0000000..a937d17
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/concurrent/AbstractQueueVisitor.java
@@ -0,0 +1,578 @@
+// Copyright 2014 Google Inc. 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.concurrent;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Throwables;
+import com.google.common.collect.Maps;
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+
+import java.util.Map;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.RejectedExecutionHandler;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * AbstractQueueVisitor is a wrapper around {@link ThreadPoolExecutor} which
+ * delays thread pool shutdown until entire visitation is complete.
+ * This is useful for cases in which worker tasks may submit additional tasks.
+ *
+ * <p>Consider the following example:
+ * <pre>
+ * ThreadPoolExecutor executor = <...>
+ * executor.submit(myRunnableTask);
+ * executor.shutdown();
+ * executor.awaitTermination();
+ * </pre>
+ *
+ * <p>This won't work properly if {@code myRunnableTask} submits additional
+ * tasks to the executor, because it may already have shut down
+ * by that point.
+ *
+ * <p>AbstractQueueVisitor supports interruption. If the main thread is
+ * interrupted, tasks will no longer be added to the queue, and the
+ * {@link #work(boolean)} method will throw {@link InterruptedException}.
+ */
+public class AbstractQueueVisitor {
+
+ /**
+ * The first unhandled exception thrown by a worker thread. We save it
+ * and re-throw it from the main thread to detect bugs faster;
+ * otherwise worker threads just quietly die.
+ *
+ * Field updates are synchronized; it's
+ * important to save the first one as it may be more informative than a
+ * subsequent one, and this is not a performance-critical path.
+ */
+ private Throwable unhandled = null;
+
+ /**
+ * An uncaught exception when submitting a job to the ThreadPool is catastrophic, and usually
+ * indicates a lack of stack space on which to allocate a native thread. The JDK
+ * ThreadPoolExecutor may reach an inconsistent state in such circumstances, so we avoid blocking
+ * on its termination when this field is non-null.
+ */
+ private volatile Throwable catastrophe;
+
+ /**
+ * Enables concurrency. For debugging or testing, set this to false
+ * to avoid thread creation and concurrency. Any deviation in observed
+ * behaviour is a bug.
+ */
+ private final boolean concurrent;
+
+ // Condition variable for remainingTasks==0, and a lock for it.
+ private final Object zeroRemainingTasks = new Object();
+ private long remainingTasks = 0;
+
+ // Map of thread ==> number of jobs executing in the thread.
+ // Currently used only for interrupt handling.
+ private final Map<Thread, Long> jobs = Maps.newConcurrentMap();
+
+ /**
+ * The thread pool. If !concurrent, always null. Created lazily on first
+ * call to {@link #enqueue(Runnable)}, and removed after call to
+ * {@link #work(boolean)}.
+ */
+ private final ThreadPoolExecutor pool;
+
+ /**
+ * Flag used to record when the main thread (the thread which called
+ * {@link #work(boolean)}) is interrupted.
+ *
+ * When this is true, adding tasks to the thread pool will
+ * fail quietly as a part of the process of shutting down the
+ * worker threads.
+ */
+ private volatile boolean threadInterrupted = false;
+
+ /**
+ * Latches used to signal when the visitor has been interrupted or
+ * seen an exception. Used only for testing.
+ */
+ private final CountDownLatch interruptedLatch = new CountDownLatch(1);
+ private final CountDownLatch exceptionLatch = new CountDownLatch(1);
+
+ /**
+ * If true, don't run new actions after an uncaught exception.
+ */
+ private final boolean failFastOnException;
+
+ /**
+ * If true, don't run new actions after an interrupt.
+ */
+ private final boolean failFastOnInterrupt;
+
+ /**
+ * If true, we must shut down the thread pool on completion.
+ */
+ private final boolean ownThreadPool;
+
+ /**
+ * Flag used to record when all threads were killed by failed action execution
+ */
+ private boolean jobsMustBeStopped = false;
+
+ /**
+ * Create the AbstractQueueVisitor.
+ *
+ * @param concurrent true if concurrency should be enabled. Only set to
+ * false for debugging.
+ * @param corePoolSize the core pool size of the thread pool. See
+ * {@link ThreadPoolExecutor#ThreadPoolExecutor(int, int, long, TimeUnit, java.util.concurrent.BlockingQueue)}
+ * @param maxPoolSize the max number of threads in the pool.
+ * @param keepAliveTime the keep-alive time for the thread pool.
+ * @param units the time units of keepAliveTime.
+ * @param failFastOnException if true, don't run new actions after
+ * an uncaught exception.
+ * @param failFastOnInterrupt if true, don't run new actions after interrupt.
+ * @param poolName sets the name of threads spawn by this thread pool. If {@code null}, default
+ * thread naming will be used.
+ */
+ public AbstractQueueVisitor(boolean concurrent, int corePoolSize, int maxPoolSize,
+ long keepAliveTime, TimeUnit units, boolean failFastOnException,
+ boolean failFastOnInterrupt, String poolName) {
+ Preconditions.checkNotNull(poolName);
+
+ this.concurrent = concurrent;
+ this.failFastOnException = failFastOnException;
+ this.failFastOnInterrupt = failFastOnInterrupt;
+ this.ownThreadPool = true;
+ this.pool = concurrent
+ ? new ThreadPoolExecutor(corePoolSize, maxPoolSize, keepAliveTime, units, getWorkQueue(),
+ new ThreadFactoryBuilder().setNameFormat(poolName + " %d").build())
+ : null;
+ }
+
+ /**
+ * Create the AbstractQueueVisitor.
+ *
+ * @param concurrent true if concurrency should be enabled. Only set to
+ * false for debugging.
+ * @param corePoolSize the core pool size of the thread pool. See
+ * {@link ThreadPoolExecutor#ThreadPoolExecutor(int, int, long, TimeUnit, java.util.concurrent.BlockingQueue)}
+ * @param maxPoolSize the max number of threads in the pool.
+ * @param keepAliveTime the keep-alive time for the thread pool.
+ * @param units the time units of keepAliveTime.
+ * @param failFastOnException if true, don't run new actions after
+ * an uncaught exception.
+ * @param poolName sets the name of threads spawn by this thread pool. If {@code null}, default
+ * thread naming will be used.
+ */
+ public AbstractQueueVisitor(boolean concurrent, int corePoolSize, int maxPoolSize,
+ long keepAliveTime, TimeUnit units, boolean failFastOnException, String poolName) {
+ this(concurrent, corePoolSize, maxPoolSize, keepAliveTime, units, failFastOnException, true,
+ poolName);
+ }
+
+ /**
+ * Create the AbstractQueueVisitor.
+ *
+ * @param executor The ThreadPool to use.
+ * @param shutdownOnCompletion If true, pass ownership of the Threadpool to
+ * this class. The pool will be shut down after a
+ * call to work(). Callers must not shutdown the
+ * threadpool while queue visitors use it.
+ * @param failFastOnException if true, don't run new actions after
+ * an uncaught exception.
+ * @param failFastOnInterrupt if true, don't run new actions after interrupt.
+ */
+ public AbstractQueueVisitor(ThreadPoolExecutor executor, boolean shutdownOnCompletion,
+ boolean failFastOnException, boolean failFastOnInterrupt) {
+ this(/*concurrent=*/true, executor, shutdownOnCompletion, failFastOnException,
+ failFastOnInterrupt);
+ }
+
+ /**
+ * Create the AbstractQueueVisitor.
+ *
+ * @param concurrent if false, run tasks inline instead of using the thread pool.
+ * @param executor The ThreadPool to use.
+ * @param shutdownOnCompletion If true, pass ownership of the Threadpool to
+ * this class. The pool will be shut down after a
+ * call to work(). Callers must not shut down the
+ * threadpool while queue visitors use it.
+ * @param failFastOnException if true, don't run new actions after
+ * an uncaught exception.
+ * @param failFastOnInterrupt if true, don't run new actions after interrupt.
+ */
+ public AbstractQueueVisitor(boolean concurrent, ThreadPoolExecutor executor,
+ boolean shutdownOnCompletion, boolean failFastOnException,
+ boolean failFastOnInterrupt) {
+ this.concurrent = concurrent;
+ this.failFastOnException = failFastOnException;
+ this.failFastOnInterrupt = failFastOnInterrupt;
+ this.pool = executor;
+ this.ownThreadPool = shutdownOnCompletion;
+ }
+
+ public AbstractQueueVisitor(ThreadPoolExecutor executor, boolean failFastOnException) {
+ this(executor, true, failFastOnException, true);
+ }
+
+ /**
+ * Create the AbstractQueueVisitor.
+ *
+ * @param concurrent true if concurrency should be enabled. Only set to
+ * false for debugging.
+ * @param corePoolSize the core pool size of the thread pool. See
+ * {@link ThreadPoolExecutor#ThreadPoolExecutor(int, int, long, TimeUnit, java.util.concurrent.BlockingQueue)}
+ * @param maxPoolSize the max number of threads in the pool.
+ * @param keepAliveTime the keep-alive time for the thread pool.
+ * @param units the time units of keepAliveTime.
+ * @param poolName sets the name of threads spawn by this thread pool. If {@code null}, default
+ * thread naming will be used.
+ */
+ public AbstractQueueVisitor(boolean concurrent, int corePoolSize, int maxPoolSize,
+ long keepAliveTime, TimeUnit units, String poolName) {
+ this(concurrent, corePoolSize, maxPoolSize, keepAliveTime, units, false, poolName);
+ }
+
+ /**
+ * Create the AbstractQueueVisitor with concurrency enabled.
+ *
+ * @param corePoolSize the core pool size of the thread pool. See
+ * {@link ThreadPoolExecutor#ThreadPoolExecutor(int, int, long, TimeUnit, java.util.concurrent.BlockingQueue)}
+ * @param maxPoolSize the max number of threads in the pool.
+ * @param keepAlive the keep-alive time for the thread pool.
+ * @param units the time units of keepAliveTime.
+ * @param poolName sets the name of threads spawn by this thread pool. If {@code null}, default
+ * thread naming will be used.
+ */
+ public AbstractQueueVisitor(int corePoolSize, int maxPoolSize, long keepAlive, TimeUnit units,
+ String poolName) {
+ this(true, corePoolSize, maxPoolSize, keepAlive, units, poolName);
+ }
+
+ protected BlockingQueue<Runnable> getWorkQueue() {
+ return new LinkedBlockingQueue<>();
+ }
+
+ /**
+ * Executes all tasks on the queue, and optionally shuts the pool down and deletes it.
+ *
+ * <p>Throws (the same) unchecked exception if any worker thread failed unexpectedly. If the pool
+ * is interrupted and a worker also throws an unchecked exception, the unchecked exception is
+ * rethrown, since it may indicate a programming bug. If callers handle the unchecked exception,
+ * they may check the interrupted bit to see if the pool was interrupted.
+ *
+ * @param interruptWorkers if true, interrupt worker threads when main thread gets an interrupt.
+ * If false, just wait for them to terminate normally.
+ */
+ protected void work(boolean interruptWorkers) throws InterruptedException {
+ if (concurrent) {
+ awaitTermination(interruptWorkers);
+ } else {
+ if (Thread.currentThread().isInterrupted()) {
+ throw new InterruptedException();
+ }
+ }
+ }
+
+ /**
+ * Schedules a call.
+ * Called in a worker thread if concurrent.
+ */
+ protected void enqueue(Runnable runnable) {
+ if (concurrent) {
+ AtomicBoolean ranTask = new AtomicBoolean(false);
+ try {
+ pool.execute(wrapRunnable(runnable, ranTask));
+ } catch (Throwable e) {
+ if (!ranTask.get()) {
+ // Note that keeping track of ranTask is necessary to disambiguate the case where
+ // execute() itself failed, vs. a caller-runs policy on pool exhaustion, where the
+ // runnable threw. To be extra cautious, we decrement the task count in a finally
+ // block, even though the CountDownLatch is unlikely to throw.
+ recordError(e);
+ }
+ }
+ } else {
+ runnable.run();
+ }
+ }
+
+ private void recordError(Throwable e) {
+ catastrophe = e;
+ try {
+ synchronized (this) {
+ if (unhandled == null) { // save only the first one.
+ unhandled = e;
+ exceptionLatch.countDown();
+ }
+ }
+ } finally {
+ decrementRemainingTasks();
+ }
+ }
+
+ private Runnable wrapRunnable(final Runnable runnable, final AtomicBoolean ranTask) {
+ synchronized (zeroRemainingTasks) {
+ remainingTasks++;
+ }
+ return new Runnable() {
+ @Override
+ public void run() {
+ Thread thread = null;
+ boolean addedJob = false;
+ try {
+ ranTask.set(true);
+ thread = Thread.currentThread();
+ addJob(thread);
+ addedJob = true;
+ if (blockNewActions()) {
+ // Make any newly enqueued tasks quickly die. We check after adding to the jobs map so
+ // that if another thread is racing to kill this thread and didn't make it before this
+ // conditional, it will be able to find and kill this thread anyway.
+ return;
+ }
+ runnable.run();
+ } catch (Throwable e) {
+ synchronized (AbstractQueueVisitor.this) {
+ if (unhandled == null) { // save only the first one.
+ unhandled = e;
+ exceptionLatch.countDown();
+ }
+ markToStopAllJobsIfNeeded(e);
+ }
+ } finally {
+ try {
+ if (thread != null && addedJob) {
+ removeJob(thread);
+ }
+ } finally {
+ decrementRemainingTasks();
+ }
+ }
+ }
+ };
+ }
+
+ private final void addJob(Thread thread) {
+ // Note: this looks like a check-then-act race but it isn't, because each
+ // key implies thread-locality.
+ long count = jobs.containsKey(thread) ? jobs.get(thread) + 1 : 1;
+ jobs.put(thread, count);
+ }
+
+ private final void removeJob(Thread thread) {
+ Long boxedCount = Preconditions.checkNotNull(jobs.get(thread),
+ "Can't retrieve job after successfully adding it");
+ long count = boxedCount - 1;
+ if (count == 0) {
+ jobs.remove(thread);
+ } else {
+ jobs.put(thread, count);
+ }
+ }
+
+ /**
+ * Set an internal flag to show that an interrupt was detected.
+ */
+ private void setInterrupted() {
+ threadInterrupted = true;
+ setRejectedExecutionHandler();
+ }
+
+ private final void decrementRemainingTasks() {
+ synchronized (zeroRemainingTasks) {
+ if (--remainingTasks == 0) {
+ zeroRemainingTasks.notify();
+ }
+ }
+ }
+
+ /**
+ * If this returns true, don't enqueue new actions.
+ */
+ protected boolean blockNewActions() {
+ return (failFastOnInterrupt && isInterrupted()) || (unhandled != null && failFastOnException);
+ }
+
+ /**
+ * Await interruption. Used only in tests.
+ */
+ @VisibleForTesting
+ public boolean awaitInterruptionForTestingOnly(long timeout, TimeUnit units)
+ throws InterruptedException {
+ return interruptedLatch.await(timeout, units);
+ }
+
+ /** Get latch that is released when exception is received by visitor. Used only in tests. */
+ @VisibleForTesting
+ public CountDownLatch getExceptionLatchForTestingOnly() {
+ return exceptionLatch;
+ }
+
+ /** Get latch that is released when interruption is received by visitor. Used only in tests. */
+ @VisibleForTesting
+ public CountDownLatch getInterruptionLatchForTestingOnly() {
+ return interruptedLatch;
+ }
+
+ /**
+ * Get the value of the interrupted flag.
+ */
+ @ThreadSafety.ThreadSafe
+ protected boolean isInterrupted() {
+ return threadInterrupted;
+ }
+
+ /**
+ * Get number of jobs remaining. Note that this can increase in value
+ * if running tasks submit further jobs.
+ */
+ @VisibleForTesting
+ protected long getTaskCount() {
+ synchronized (zeroRemainingTasks) {
+ return remainingTasks;
+ }
+ }
+
+ /**
+ * Waits for the task queue to drain, then shuts down the thread pool and
+ * waits for it to terminate. Throws (the same) unchecked exception if any
+ * worker thread failed unexpectedly.
+ */
+ private void awaitTermination(boolean interruptWorkers) throws InterruptedException {
+ Preconditions.checkState(failFastOnInterrupt || !interruptWorkers);
+ Throwables.propagateIfPossible(catastrophe);
+ try {
+ synchronized (zeroRemainingTasks) {
+ while (remainingTasks != 0 && !jobsMustBeStopped) {
+ zeroRemainingTasks.wait();
+ }
+ }
+ } catch (InterruptedException e) {
+ // Mark the visitor, so that it's known to be interrupted, and
+ // then break out of here, stop the worker threads and return ASAP,
+ // sending the interruption to the parent thread.
+ setInterrupted();
+ }
+
+ reallyAwaitTermination(interruptWorkers);
+
+ if (isInterrupted()) {
+ // Set interrupted bit on current thread so that callers can see that it was interrupted. Note
+ // that if the thread was interrupted while awaiting termination, we might not hit this
+ // codepath, but then the current thread's interrupt bit is already set, so we are fine.
+ Thread.currentThread().interrupt();
+ }
+ // Throw the first unhandled (worker thread) exception in the main thread. We throw an unchecked
+ // exception instead of InterruptedException if both are present because an unchecked exception
+ // may indicate a catastrophic failure that should shut down the program. The caller can
+ // check the interrupted bit if they will handle the unchecked exception without crashing.
+ Throwables.propagateIfPossible(unhandled);
+
+ if (Thread.interrupted()) {
+ throw new InterruptedException();
+ }
+ }
+
+ private void reallyAwaitTermination(boolean interruptWorkers) {
+ // TODO(bazel-team): verify that interrupt() is safe for every use of
+ // AbstractQueueVisitor and remove the interruptWorkers flag.
+ if (interruptWorkers && !jobs.isEmpty()) {
+ interruptInFlightTasks();
+ }
+
+ if (isInterrupted()) {
+ interruptedLatch.countDown();
+ }
+
+ Throwables.propagateIfPossible(catastrophe);
+ synchronized (zeroRemainingTasks) {
+ while (remainingTasks != 0) {
+ try {
+ zeroRemainingTasks.wait();
+ } catch (InterruptedException e) {
+ setInterrupted();
+ }
+ }
+ }
+
+ if (ownThreadPool) {
+ pool.shutdown();
+ for (;;) {
+ try {
+ Throwables.propagateIfPossible(catastrophe);
+ pool.awaitTermination(Integer.MAX_VALUE, TimeUnit.SECONDS);
+ break;
+ } catch (InterruptedException e) {
+ setInterrupted();
+ }
+ }
+ }
+ }
+
+ private void interruptInFlightTasks() {
+ Thread thisThread = Thread.currentThread();
+ for (Thread thread : jobs.keySet()) {
+ if (thisThread != thread) {
+ thread.interrupt();
+ }
+ }
+ }
+
+ /**
+ * Makes the visitation terminate prematurely.
+ */
+ public void interrupt() {
+ setInterrupted();
+ reallyAwaitTermination(true);
+ }
+
+ /**
+ * If this returns true, that means the exception {@code e} is critical
+ * and all running actions should be stopped.
+ *
+ * <p>Default value - always false. If different behavior is needed
+ * then we should override this method in subclasses.
+ *
+ * @param e the exception object to check
+ */
+ protected boolean isCriticalError(Throwable e) {
+ return false;
+ }
+
+ private void setRejectedExecutionHandler() {
+ if (ownThreadPool) {
+ pool.setRejectedExecutionHandler(new RejectedExecutionHandler() {
+ @Override
+ public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
+ decrementRemainingTasks();
+ }
+ });
+ }
+ }
+
+ /**
+ * If exception is critical then set a flag which signals
+ * to stop all jobs inside {@link #awaitTermination(boolean)}.
+ */
+ private synchronized void markToStopAllJobsIfNeeded(Throwable e) {
+ if (isCriticalError(e) && !jobsMustBeStopped) {
+ jobsMustBeStopped = true;
+ synchronized (zeroRemainingTasks) {
+ zeroRemainingTasks.notify();
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/concurrent/ExecutorShutdownUtil.java b/src/main/java/com/google/devtools/build/lib/concurrent/ExecutorShutdownUtil.java
new file mode 100644
index 0000000..95962b3
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/concurrent/ExecutorShutdownUtil.java
@@ -0,0 +1,103 @@
+// Copyright 2014 Google Inc. 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.concurrent;
+
+import com.google.common.base.Preconditions;
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.RejectedExecutionHandler;
+import java.util.concurrent.SynchronousQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Utilities for safely shutting down executors.
+ * TODO(bazel-team): Rename this class to something like "ExecutorUtil".
+ */
+public class ExecutorShutdownUtil {
+
+ private ExecutorShutdownUtil() {
+ }
+
+ /**
+ * Shutdown the executor. If an interrupt occurs, invoke shutdownNow(), but
+ * still block on the eventual termination of the pool.
+ *
+ * @param executor the executor service.
+ * @return true iff interrupted.
+ */
+ public static boolean interruptibleShutdown(ExecutorService executor) {
+ return shutdownImpl(executor, /*interruptible=*/true);
+ }
+
+ /**
+ * Shutdown the executor. If an interrupt occurs, ignore it and still block on the eventual
+ * termination of the pool. This way, all tasks are guaranteed to have completed normally.
+ *
+ * @param executor the executor service.
+ * @return true iff interrupted.
+ */
+ public static boolean uninterruptibleShutdown(ExecutorService executor) {
+ return shutdownImpl(executor, /*interruptible=*/false);
+ }
+
+ private static boolean shutdownImpl(ExecutorService executor, boolean interruptible) {
+ Preconditions.checkState(!executor.isShutdown());
+ executor.shutdown();
+
+ // Common pattern: check for interrupt, but don't return until all threads
+ // are finished.
+ boolean interrupted = false;
+ while (true) {
+ try {
+ executor.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
+ break;
+ } catch (InterruptedException e) {
+ if (interruptible) {
+ executor.shutdownNow();
+ }
+ interrupted = true;
+ }
+ }
+ return interrupted;
+ }
+
+ /**
+ * Create a "slack" thread pool which has the following properties:
+ * 1. the worker count shrinks as the threads go unused
+ * 2. the rejection policy is caller-runs
+ *
+ * @param threads maximum number of threads in the pool
+ * @param name name of the pool
+ * @return the new ThreadPoolExecutor
+ */
+ public static ThreadPoolExecutor newSlackPool(int threads, String name) {
+ // Using a synchronous queue with a bounded thread pool means we'll reject
+ // tasks after the pool size. The CallerRunsPolicy, however, implies that
+ // saturation is handled in the calling thread.
+ ThreadPoolExecutor pool = new ThreadPoolExecutor(threads, threads, 3L, TimeUnit.SECONDS,
+ new SynchronousQueue<Runnable>());
+ // Do not consume threads when not in use.
+ pool.allowCoreThreadTimeOut(true);
+ pool.setThreadFactory(new ThreadFactoryBuilder().setNameFormat(name + " %d").build());
+ pool.setRejectedExecutionHandler(new RejectedExecutionHandler() {
+ @Override
+ public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
+ r.run();
+ }
+ });
+ return pool;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/concurrent/MoreFutures.java b/src/main/java/com/google/devtools/build/lib/concurrent/MoreFutures.java
new file mode 100644
index 0000000..ab84f99
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/concurrent/MoreFutures.java
@@ -0,0 +1,59 @@
+// Copyright 2014 Google Inc. 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.concurrent;
+
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+/**
+ * Utility class for working with futures.
+ */
+public class MoreFutures {
+
+ private MoreFutures() {}
+
+ /**
+ * Creates a new {@code ListenableFuture} whose value is a list containing the
+ * values of all its input futures, if all succeed. If any input fails, the
+ * returned future fails. If any of the futures fails, it cancels all the other futures.
+ *
+ * <p> This method is similar to {@code Futures.allAsList} but additionally it cancels all the
+ * futures in case any of them fails.
+ */
+ public static <V> ListenableFuture<List<V>> allAsListOrCancelAll(
+ final Iterable<? extends ListenableFuture<? extends V>> futures) {
+ ListenableFuture<List<V>> combinedFuture = Futures.allAsList(futures);
+ Futures.addCallback(combinedFuture, new FutureCallback<List<V>>() {
+ @Override
+ public void onSuccess(@Nullable List<V> vs) {}
+
+ /**
+ * In case of a failure of any of the futures (that gets propagated to combinedFuture) we
+ * cancel all the futures in the list.
+ */
+ @Override
+ public void onFailure(Throwable ignore) {
+ for (ListenableFuture<? extends V> future : futures) {
+ future.cancel(true);
+ }
+ }
+ });
+ return combinedFuture;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/concurrent/Sharder.java b/src/main/java/com/google/devtools/build/lib/concurrent/Sharder.java
new file mode 100644
index 0000000..67a63e0
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/concurrent/Sharder.java
@@ -0,0 +1,71 @@
+// Copyright 2014 Google Inc. 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.concurrent;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * A class to build shards (work queues) for a given task.
+ *
+ * <p>{@link #add}ed elements will be equally distributed among the shards.
+ *
+ * @param <T> the type of collection over which we're sharding
+ */
+public final class Sharder<T> implements Iterable<List<T>> {
+ private final List<List<T>> shards;
+ private int nextShard = 0;
+
+ public Sharder(int maxNumShards, int expectedTotalSize) {
+ Preconditions.checkArgument(maxNumShards > 0);
+ Preconditions.checkArgument(expectedTotalSize >= 0);
+ this.shards = immutableListOfLists(maxNumShards, expectedTotalSize / maxNumShards);
+ }
+
+ public void add(T item) {
+ shards.get(nextShard).add(item);
+ nextShard = (nextShard + 1) % shards.size();
+ }
+
+ /**
+ * Returns an immutable list of mutable lists.
+ *
+ * @param numLists the number of top-level lists.
+ * @param expectedSize the exepected size of each mutable list.
+ * @return a list of lists.
+ */
+ private static <T> List<List<T>> immutableListOfLists(int numLists, int expectedSize) {
+ List<List<T>> list = Lists.newArrayListWithCapacity(numLists);
+ for (int i = 0; i < numLists; i++) {
+ list.add(Lists.<T>newArrayListWithExpectedSize(expectedSize));
+ }
+ return Collections.unmodifiableList(list);
+ }
+
+ @Override
+ public Iterator<List<T>> iterator() {
+ return Iterables.filter(shards, new Predicate<List<T>>() {
+ @Override
+ public boolean apply(List<T> list) {
+ return !list.isEmpty();
+ }
+ }).iterator();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/concurrent/ThreadSafety.java b/src/main/java/com/google/devtools/build/lib/concurrent/ThreadSafety.java
new file mode 100644
index 0000000..0c67fd9
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/concurrent/ThreadSafety.java
@@ -0,0 +1,135 @@
+// Copyright 2014 Google Inc. 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.concurrent;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Define some standard attributes for documenting thread safety properties.
+ *<p>
+ * The names used here are adapted from those used in Joshua Bloch's book
+ * "Effective Java", which are also described at
+ * <http://www-128.ibm.com/developerworks/java/library/j-jtp09263.html>.
+ *<p>
+ * These attributes are just documentation. They don't have any run-time
+ * effect. The main aim is mainly just to standardize the terminology.
+ * (However, if this catches on, I can also imagine in the future having
+ * a presubmit check that checks that all new classes have thread safety
+ * annotations :)
+ *<p>
+ * See ThreadSafetyTest for examples of how these attributes should be used.
+ */
+public class ThreadSafety {
+ /**
+ * The Immutable attribute indicates that instances of the class are
+ * immutable, or at least appear that way are far as their external API
+ * is concerned. Immutable classes are usually also ThreadSafe,
+ * but can be ThreadHostile if they perform unsynchronized access to
+ * mutable static data. (We deviate from Bloch's nomenclature by
+ * not assuming that Immutable implies ThreadSafe; developers should
+ * explicitly annotate classes as both Immutable and ThreadSafe when
+ * appropriate.)
+ */
+ @Documented
+ @Target(value = {ElementType.TYPE})
+ @Retention(RetentionPolicy.RUNTIME)
+ public @interface Immutable {}
+
+ /**
+ * The ThreadSafe attribute marks a class or method which can safely be used
+ * from multiple threads without any need for external synchronization.
+ *
+ * When applied to a class, this attribute indicates that instances
+ * of the class can safely be used concurrently from multiple threads
+ * without any need for external synchronization, i.e. that all non-static methods
+ * are thread-safe (except any private methods that are explicitly
+ * annotated with a different thread safety annotation). In addition it
+ * also indicates that all non-static nested classes are thread-safe (except any private
+ * nested classes that are explicitly annotated with a different thread
+ * safety annotation). Note that no guarantees are made about static class methods or static
+ * nested classes - they should be annotated separately.
+ *
+ * When applied to a method, this attribute indicates that the
+ * method can safely be called concurrently from multiple threads.
+ * The implementation must synchronize any accesses to mutable data.
+ */
+ @Documented
+ @Target(value = {ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.TYPE})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ThreadSafe {}
+
+ /**
+ * The ThreadCompatible attribute marks a class or method that
+ * is thread-safe provided that only one thread attempts to
+ * access each object at a time.
+ *
+ * The implementation of such a class must synchronize accesses
+ * to mutable static data, but can assume that each instance will
+ * only be accessed from one thread at a time.
+ *
+ * The client must obtain an appropriate lock before calling ThreadCompatible
+ * methods, or must otherwise ensure that only one thread calls such methods.
+ * Unless otherwise specified, an appropriate lock means synchronizing on the
+ * instance.
+ *
+ * A ThreadCompatible class may contain private methods or private nested
+ * classes that are not ThreadCompatible provided that they are explicitly
+ * annotated with a different thread safety annotation.
+ */
+ @Documented
+ @Target(value = {ElementType.METHOD, ElementType.TYPE})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ThreadCompatible {}
+
+ /**
+ * The ThreadHostile attribute marks a class or method that
+ * can't safely be used by multiple threads, for example because
+ * it performs unsynchronized access to mutable static objects.
+ */
+ @Documented
+ @Target(value = {ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.TYPE})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ThreadHostile {}
+
+ /**
+ * The ConditionallyThreadSafe attribute marks a class that contains
+ * some methods (or nested classes) which are ThreadSafe but others which are
+ * only ThreadCompatible or ThreadHostile.
+ *
+ * The methods (and nested classes) of a ConditionallyThreadSafe class should
+ * each have their thread safety marked.
+ */
+ @Documented
+ @Target(value = {ElementType.METHOD, ElementType.TYPE})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ConditionallyThreadSafe {}
+
+ /**
+ * The ConditionallyThreadCompatible attribute marks a class that contains
+ * some methods (or nested classes) which are ThreadCompatible but others
+ * which are ThreadHostile.
+ *
+ * The methods (and nested classes) of a ConditionallyThreadCompatible class
+ * should each have their thread safety marked.
+ */
+ @Documented
+ @Target(value = {ElementType.METHOD, ElementType.TYPE})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ConditionallyThreadCompatible {}
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/concurrent/ThrowableRecordingRunnableWrapper.java b/src/main/java/com/google/devtools/build/lib/concurrent/ThrowableRecordingRunnableWrapper.java
new file mode 100644
index 0000000..3f67d55
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/concurrent/ThrowableRecordingRunnableWrapper.java
@@ -0,0 +1,58 @@
+// Copyright 2014 Google Inc. 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.concurrent;
+
+import com.google.common.base.Preconditions;
+
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.annotation.Nullable;
+
+/**
+ * A class that wraps Runnables and records the first Throwable thrown by the wrapped Runnables
+ * when they are run.
+ */
+public class ThrowableRecordingRunnableWrapper {
+
+ private final String name;
+ private AtomicReference<Throwable> errorRef = new AtomicReference<>();
+
+ private static final Logger LOG =
+ Logger.getLogger(ThrowableRecordingRunnableWrapper.class.getName());
+
+ public ThrowableRecordingRunnableWrapper(String name) {
+ this.name = Preconditions.checkNotNull(name);
+ }
+
+ @Nullable
+ public Throwable getFirstThrownError() {
+ return errorRef.get();
+ }
+
+ public Runnable wrap(final Runnable runnable) {
+ return new Runnable() {
+ @Override
+ public void run() {
+ try {
+ runnable.run();
+ } catch (Throwable error) {
+ errorRef.compareAndSet(null, error);
+ LOG.log(Level.SEVERE, "Error thrown by runnable in " + name, error);
+ }
+ }
+ };
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/events/AbstractEventHandler.java b/src/main/java/com/google/devtools/build/lib/events/AbstractEventHandler.java
new file mode 100644
index 0000000..39faf14
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/events/AbstractEventHandler.java
@@ -0,0 +1,36 @@
+// Copyright 2014 Google Inc. 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.events;
+
+import java.util.Set;
+
+/**
+ * An abstract event handler that keeps track of the event mask. Events
+ * matching the mask will be handled.
+ */
+public abstract class AbstractEventHandler implements EventHandler {
+
+ private final Set<EventKind> mask;
+
+ /**
+ * Events matching the mask will be handled.
+ */
+ public AbstractEventHandler(Set<EventKind> mask) {
+ this.mask = mask;
+ }
+
+ public Set<EventKind> getEventMask() {
+ return mask;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/events/DelegatingEventHandler.java b/src/main/java/com/google/devtools/build/lib/events/DelegatingEventHandler.java
new file mode 100644
index 0000000..d26d70c
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/events/DelegatingEventHandler.java
@@ -0,0 +1,35 @@
+// Copyright 2014 Google Inc. 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.events;
+
+import com.google.common.base.Preconditions;
+
+/**
+ * An ErrorEventListener which delegates to another ErrorEventListener.
+ * Primarily useful as a base class for extending behavior.
+ */
+public class DelegatingEventHandler implements EventHandler {
+ protected final EventHandler delegate;
+
+ public DelegatingEventHandler(EventHandler delegate) {
+ super();
+ this.delegate = Preconditions.checkNotNull(delegate);
+ }
+
+ @Override
+ public void handle(Event e) {
+ delegate.handle(e);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/events/DelegatingOnlyErrorsEventHandler.java b/src/main/java/com/google/devtools/build/lib/events/DelegatingOnlyErrorsEventHandler.java
new file mode 100644
index 0000000..dec220d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/events/DelegatingOnlyErrorsEventHandler.java
@@ -0,0 +1,32 @@
+// Copyright 2014 Google Inc. 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.events;
+
+/**
+ * An {@link EventHandler} implementation that only
+ * passes through error messages.
+ */
+public class DelegatingOnlyErrorsEventHandler extends DelegatingEventHandler {
+
+ public DelegatingOnlyErrorsEventHandler(EventHandler eventHandler) {
+ super(eventHandler);
+ }
+
+ @Override
+ public void handle(Event e) {
+ if (e.getKind() == EventKind.ERROR) {
+ super.handle(e);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/events/ErrorSensingEventHandler.java b/src/main/java/com/google/devtools/build/lib/events/ErrorSensingEventHandler.java
new file mode 100644
index 0000000..705f7f4
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/events/ErrorSensingEventHandler.java
@@ -0,0 +1,48 @@
+// Copyright 2014 Google Inc. 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.events;
+
+/**
+ * Passes through any events, and keeps a flag if any of them were errors. It is thread-safe as long
+ * as the target eventHandler is thread-safe.
+ */
+public final class ErrorSensingEventHandler extends DelegatingEventHandler {
+
+ private volatile boolean hasErrors;
+
+ public ErrorSensingEventHandler(EventHandler eventHandler) {
+ super(eventHandler);
+ }
+
+ @Override
+ public void handle(Event e) {
+ hasErrors |= e.getKind() == EventKind.ERROR;
+ super.handle(e);
+ }
+
+ /**
+ * Returns whether any of the events on this objects were errors.
+ */
+ public boolean hasErrors() {
+ return hasErrors;
+ }
+
+ /**
+ * Reset the error flag. Don't call this while other threads are accessing the same object.
+ */
+ public void resetErrors() {
+ hasErrors = false;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/events/Event.java b/src/main/java/com/google/devtools/build/lib/events/Event.java
new file mode 100644
index 0000000..db5bc5f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/events/Event.java
@@ -0,0 +1,183 @@
+// Copyright 2014 Google Inc. 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.events;
+
+import static java.nio.charset.StandardCharsets.ISO_8859_1;
+
+import com.google.common.base.Preconditions;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * An event is a situation encountered by the build system that's worth
+ * reporting: A 3-tuple of ({@link EventKind}, {@link Location}, message).
+ */
+@Immutable
+public final class Event {
+
+ private final EventKind kind;
+ private final Location location;
+ private final String message;
+ /**
+ * An alternative representation for message.
+ * Exactly one of message or messageBytes will be non-null.
+ * If messageBytes is non-null, then it contains the bytes
+ * of the message, encoded using the platform's default charset.
+ * We do this to avoid converting back and forth between Strings
+ * and bytes.
+ */
+ private final byte[] messageBytes;
+
+ @Nullable
+ private final String tag;
+
+ public Event withTag(String tag) {
+ if (this.message != null) {
+ return new Event(this.kind, this.location, this.message, tag);
+ } else {
+ return new Event(this.kind, this.location, this.messageBytes, tag);
+ }
+ }
+
+ public Event(EventKind kind, @Nullable Location location, String message) {
+ this(kind, location, message, null);
+ }
+
+ public Event(EventKind kind, @Nullable Location location, String message, @Nullable String tag) {
+ this.kind = kind;
+ this.location = location;
+ this.message = Preconditions.checkNotNull(message);
+ this.messageBytes = null;
+ this.tag = tag;
+ }
+
+ public Event(EventKind kind, @Nullable Location location, byte[] messageBytes) {
+ this(kind, location, messageBytes, null);
+ }
+
+ public Event(
+ EventKind kind, @Nullable Location location, byte[] messageBytes, @Nullable String tag) {
+ this.kind = kind;
+ this.location = location;
+ this.message = null;
+ this.messageBytes = Preconditions.checkNotNull(messageBytes);
+ this.tag = tag;
+ }
+
+ public String getMessage() {
+ return message != null ? message : new String(messageBytes);
+ }
+
+ public byte[] getMessageBytes() {
+ return messageBytes != null ? messageBytes : message.getBytes(ISO_8859_1);
+ }
+
+ public EventKind getKind() {
+ return kind;
+ }
+
+ /**
+ * the tag is typically the action that generated the event.
+ */
+ @Nullable
+ public String getTag() {
+ return tag;
+ }
+
+ /**
+ * Returns the location of this event, if any. Returns null iff the event
+ * wasn't associated with any particular location, for example, a progress
+ * message.
+ */
+ @Nullable public Location getLocation() {
+ return location;
+ }
+
+ /**
+ * Returns <i>some</i> moderately sane representation of the event. Should never be used in
+ * user-visible places, only for debugging and testing.
+ */
+ @Override
+ public String toString() {
+ return kind + " " + (location != null ? location.print() : "<no location>") + ": "
+ + getMessage();
+ }
+
+ /**
+ * Replay a sequence of events on an {@link EventHandler}.
+ */
+ public static void replayEventsOn(EventHandler eventHandler, Iterable<Event> events) {
+ for (Event event : events) {
+ eventHandler.handle(event);
+ }
+ }
+
+ /**
+ * Reports a warning.
+ */
+ public static Event warn(Location location, String message) {
+ return new Event(EventKind.WARNING, location, message);
+ }
+
+ /**
+ * Reports an error.
+ */
+ public static Event error(Location location, String message){
+ return new Event(EventKind.ERROR, location, message);
+ }
+
+ /**
+ * Reports atemporal statements about the build, i.e. they're true for the duration of execution.
+ */
+ public static Event info(Location location, String message) {
+ return new Event(EventKind.INFO, location, message);
+ }
+
+ /**
+ * Reports a temporal statement about the build.
+ */
+ public static Event progress(Location location, String message) {
+ return new Event(EventKind.PROGRESS, location, message);
+ }
+
+ /**
+ * Reports a warning.
+ */
+ public static Event warn(String message) {
+ return warn(null, message);
+ }
+
+ /**
+ * Reports an error.
+ */
+ public static Event error(String message){
+ return error(null, message);
+ }
+
+ /**
+ * Reports atemporal statements about the build, i.e. they're true for the duration of execution.
+ */
+ public static Event info(String message) {
+ return info(null, message);
+ }
+
+ /**
+ * Reports a temporal statement about the build.
+ */
+ public static Event progress(String message) {
+ return progress(null, message);
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/events/EventCollector.java b/src/main/java/com/google/devtools/build/lib/events/EventCollector.java
new file mode 100644
index 0000000..774b323
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/events/EventCollector.java
@@ -0,0 +1,78 @@
+// Copyright 2014 Google Inc. 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.events;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Set;
+
+/**
+ * An {@link EventHandler} that collects all events it encounters, and makes
+ * them available via the {@link Iterable} interface. The collected events
+ * contain not just the original event information but also the location
+ * context.
+ */
+public class EventCollector extends AbstractEventHandler implements Iterable<Event> {
+
+ private final Collection<Event> collected;
+
+ /**
+ * This collector will collect all events that match the event mask.
+ */
+ public EventCollector(Set<EventKind> mask) {
+ this(mask, new ArrayList<Event>());
+ }
+
+ /**
+ * This collector will save the Event instances in the provided
+ * collection.
+ */
+ public EventCollector(Set<EventKind> mask, Collection<Event> collected) {
+ super(mask);
+ this.collected = collected;
+ }
+
+ /**
+ * Implements {@link EventHandler#handle(Event)}.
+ */
+ @Override
+ public void handle(Event event) {
+ if (getEventMask().contains(event.getKind())) {
+ collected.add(event);
+ }
+ }
+
+ /**
+ * Returns an iterator over the collected events.
+ */
+ @Override
+ public Iterator<Event> iterator() {
+ return collected.iterator();
+ }
+
+ /**
+ * Returns the number of events collected.
+ */
+ public int count() {
+ return collected.size();
+ }
+
+ /*
+ * Clears the collected events
+ */
+ public void clear() {
+ collected.clear();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/events/EventHandler.java b/src/main/java/com/google/devtools/build/lib/events/EventHandler.java
new file mode 100644
index 0000000..28b6265
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/events/EventHandler.java
@@ -0,0 +1,27 @@
+// Copyright 2014 Google Inc. 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.events;
+
+/**
+ * The ErrorEventListener is the primary means of reporting error and warning events. It is a subset
+ * of the functionality of the {@link Reporter}. In most cases, you should use this interface
+ * instead of the final {@code Reporter} class.
+ */
+public interface EventHandler {
+ /**
+ * Handles an event.
+ */
+ public void handle(Event event);
+}
diff --git a/src/main/java/com/google/devtools/build/lib/events/EventKind.java b/src/main/java/com/google/devtools/build/lib/events/EventKind.java
new file mode 100644
index 0000000..eb58873
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/events/EventKind.java
@@ -0,0 +1,146 @@
+// Copyright 2014 Google Inc. 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.events;
+
+import java.util.EnumSet;
+import java.util.Set;
+
+/**
+ * Indicates the kind of an {@link Event}.
+ */
+public enum EventKind {
+
+ /**
+ * For errors that will prevent a successful, correct build. In general, the
+ * build tool will not attempt to start or continue a build if an error is
+ * encountered (though the behaviour specified by --keep-going flag is a
+ * counterexample).
+ *
+ * Errors of a more severe nature in the input, such as those which might
+ * cause later passes of the analysis to fail catastrophically, should be
+ * handled by throwing an exception.
+ */
+ ERROR,
+
+ /**
+ * For warnings of minor problems that do not affect the integrity of a
+ * build.
+ */
+ WARNING,
+
+ /**
+ * For atemporal information that is true throughout the entire duration
+ * of a build. (e.g. the number of targets found)
+ */
+ INFO,
+
+ /**
+ * For temporal information that changes during the duration of a build.
+ * (e.g. what action is executing now)
+ */
+ PROGRESS,
+
+ /**
+ * For progress messages (temporal information) relating to the start
+ * and end of particular tasks.
+ * (e.g. "Loading package foo", "Compiling bar", etc.)
+ */
+ START,
+ FINISH,
+
+ /**
+ * For command lines of subcommands executed by the build tool (like make-dbg
+ * "-v").
+ */
+ SUBCOMMAND,
+
+ /**
+ * Output to stdout/stderr from subprocesses.
+ */
+ STDOUT,
+ STDERR,
+
+ /**
+ * Test result messages (similar to the INFO and ERROR, but test-specific).
+ */
+ PASS,
+ FAIL,
+ TIMEOUT,
+
+ /**
+ * For the reasoning of the dependency checker (like GNU Make "-d").
+ */
+ DEPCHECKER;
+
+ // Convenient predefined EnumSets. Clients should not mutate them!
+
+ public static final Set<EventKind> ALL_EVENTS =
+ EnumSet.allOf(EventKind.class);
+
+ public static final Set<EventKind> OUTPUT = EnumSet.of(
+ EventKind.STDOUT,
+ EventKind.STDERR
+ );
+
+ public static final Set<EventKind> ERRORS = EnumSet.of(
+ EventKind.ERROR,
+ EventKind.FAIL,
+ EventKind.TIMEOUT
+ );
+
+ public static final Set<EventKind> ERRORS_AND_WARNINGS = EnumSet.of(
+ EventKind.ERROR,
+ EventKind.WARNING,
+ EventKind.FAIL,
+ EventKind.TIMEOUT
+ );
+
+ public static final Set<EventKind> ERRORS_WARNINGS_AND_INFO = EnumSet.of(
+ EventKind.ERROR,
+ EventKind.WARNING,
+ EventKind.PASS,
+ EventKind.FAIL,
+ EventKind.TIMEOUT,
+ EventKind.INFO
+ );
+
+ public static final Set<EventKind> ERRORS_AND_OUTPUT = EnumSet.of(
+ EventKind.ERROR,
+ EventKind.FAIL,
+ EventKind.TIMEOUT,
+ EventKind.STDOUT,
+ EventKind.STDERR
+ );
+
+ public static final Set<EventKind> ERRORS_AND_WARNINGS_AND_OUTPUT = EnumSet.of(
+ EventKind.ERROR,
+ EventKind.WARNING,
+ EventKind.FAIL,
+ EventKind.TIMEOUT,
+ EventKind.STDOUT,
+ EventKind.STDERR
+ );
+
+ public static final Set<EventKind> ERRORS_WARNINGS_AND_INFO_AND_OUTPUT = EnumSet.of(
+ EventKind.ERROR,
+ EventKind.WARNING,
+ EventKind.PASS,
+ EventKind.FAIL,
+ EventKind.TIMEOUT,
+ EventKind.INFO,
+ EventKind.STDOUT,
+ EventKind.STDERR
+ );
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/events/EventSensor.java b/src/main/java/com/google/devtools/build/lib/events/EventSensor.java
new file mode 100644
index 0000000..5a31f13
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/events/EventSensor.java
@@ -0,0 +1,73 @@
+// Copyright 2014 Google Inc. 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.events;
+
+import java.util.Set;
+
+/**
+ * A "latch" that just detects whether or not a particular type of event has happened, based on its
+ * kind.
+ *
+ * <p>Be careful when using this class to track errors reported during some operation. Namely, this
+ * pattern is not thread-safe:
+ *
+ * <pre><code>
+ * EventSensor sensor = new EventSensor(EventKind.ERRORS);
+ * reporter.addHandler(sensor);
+ * someActionThatMightCreateErrors(reporter)
+ * reporter.removeHandler(sensor);
+ * boolean containsErrors = sensor.wasTriggered();
+ * </code></pre>
+ *
+ * <p>If other threads generate errors on the reporter, then containsErrors may be true even if
+ * someActionThatMightCreateErrors() did not cause any errors.
+ *
+ * <p>As a workaround, run someActionThatMightCreateErrors() with a local reporter, merging its
+ * events with those of the shared reporter.
+ */
+public class EventSensor extends AbstractEventHandler {
+
+ private int triggerCount;
+
+ /**
+ * Constructs a sensor that will register all events matching the mask.
+ */
+ public EventSensor(Set<EventKind> mask) {
+ super(mask);
+ }
+
+ /**
+ * Implements {@link EventHandler#handle(Event)}.
+ */
+ @Override
+ public void handle(Event event) {
+ if (getEventMask().contains(event.getKind())) {
+ triggerCount++;
+ }
+ }
+
+ /**
+ * Returns true iff a qualifying event was handled.
+ */
+ public boolean wasTriggered() {
+ return triggerCount > 0;
+ }
+
+ /**
+ * Returns the number of times the qualifying event was handled.
+ */
+ public int getTriggerCount() {
+ return triggerCount;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/events/ExceptionListener.java b/src/main/java/com/google/devtools/build/lib/events/ExceptionListener.java
new file mode 100644
index 0000000..174a5ca
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/events/ExceptionListener.java
@@ -0,0 +1,25 @@
+// Copyright 2014 Google Inc. 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.events;
+
+/**
+ * The ExceptionListener is the primary means of reporting exceptions. It is a subset of the
+ * functionality of the {@link Reporter}.
+ */
+public interface ExceptionListener {
+ /**
+ * Reports an error.
+ */
+ void error(Location location, String message, Throwable error);
+}
diff --git a/src/main/java/com/google/devtools/build/lib/events/Location.java b/src/main/java/com/google/devtools/build/lib/events/Location.java
new file mode 100644
index 0000000..39508d1
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/events/Location.java
@@ -0,0 +1,215 @@
+// Copyright 2014 Google Inc. 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.events;
+
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.io.Serializable;
+
+/**
+ * A Location is a range of characters within a file.
+ *
+ * The start and end locations may be the same, in which case the Location
+ * denotes a point in the file, not a range. The path may be null, indicating
+ * an unknown file.
+ *
+ * Implementations of Location should be optimised for speed of construction,
+ * not speed of attribute access, as far more Locations are created during
+ * parsing than are ever used to display error messages.
+ */
+public abstract class Location implements Serializable {
+
+ @Immutable
+ private static final class LocationWithPathAndStartColumn extends Location {
+ private final PathFragment path;
+ private final LineAndColumn startLineAndColumn;
+
+ private LocationWithPathAndStartColumn(Path path, int startOffSet, int endOffSet,
+ LineAndColumn startLineAndColumn) {
+ super(startOffSet, endOffSet);
+ this.path = path != null ? path.asFragment() : null;
+ this.startLineAndColumn = startLineAndColumn;
+ }
+
+ @Override
+ public PathFragment getPath() { return path; }
+
+ @Override
+ public LineAndColumn getStartLineAndColumn() {
+ return startLineAndColumn;
+ }
+ }
+
+ protected final int startOffset;
+ protected final int endOffset;
+
+ /**
+ * Returns a Location with a given Path, start and end offset and start line and column info.
+ */
+ public static Location fromPathAndStartColumn(Path path, int startOffSet, int endOffSet,
+ LineAndColumn startLineAndColumn) {
+ return new LocationWithPathAndStartColumn(path, startOffSet, endOffSet, startLineAndColumn);
+ }
+
+ /**
+ * Returns a Location relating to file 'path', but not to any specific part
+ * region within the file. Try to use a more specific location if possible.
+ */
+ public static Location fromFile(Path path) {
+ return fromFileAndOffsets(path, 0, 0);
+ }
+
+ /**
+ * Returns a Location relating to the subset of file 'path', starting at
+ * 'startOffset' and ending at 'endOffset'.
+ */
+ public static Location fromFileAndOffsets(final Path path,
+ int startOffset,
+ int endOffset) {
+ return new LocationWithPathAndStartColumn(path, startOffset, endOffset, null);
+ }
+
+ protected Location(int startOffset, int endOffset) {
+ this.startOffset = startOffset;
+ this.endOffset = endOffset;
+ }
+
+ /**
+ * Returns the start offset relative to the beginning of the file the object
+ * resides in.
+ */
+ public final int getStartOffset() {
+ return startOffset;
+ }
+
+ /**
+ * Returns the end offset relative to the beginning of the file the object
+ * resides in.
+ *
+ * <p>The end offset is one position past the actual end position, making this method
+ * behave in a compatible fashion with {@link String#substring(int, int)}.
+ *
+ * <p>To compute the length of this location, use {@code getEndOffset() - getStartOffset()}.
+ */
+ public final int getEndOffset() {
+ return endOffset;
+ }
+
+ /**
+ * Returns the path of the file to which the start/end offsets refer. May be
+ * null if the file name information is not available.
+ *
+ * This method is intentionally abstract, as a space optimisation. Some
+ * subclass instances implement sharing of common data (e.g. tables for
+ * convering offsets into line numbers) and this enables them to share the
+ * Path value in the same way.
+ */
+ public abstract PathFragment getPath();
+
+ /**
+ * Returns a (line, column) pair corresponding to the position denoted by
+ * getStartOffset. Returns null if this information is not available.
+ */
+ public LineAndColumn getStartLineAndColumn() {
+ return null;
+ }
+
+ /**
+ * Returns a (line, column) pair corresponding to the position denoted by
+ * getEndOffset. Returns null if this information is not available.
+ */
+ public LineAndColumn getEndLineAndColumn() {
+ return null;
+ }
+
+ /**
+ * A default implementation of toString() that formats the location in the
+ * following ways based on the amount of information available:
+ * <pre>
+ * "foo.cc:23:2"
+ * "23:2"
+ * "foo.cc:char offsets 123--456"
+ * "char offsets 123--456"
+ * </pre>
+ */
+ public String print() {
+ StringBuilder buf = new StringBuilder();
+ if (getPath() != null) {
+ buf.append(getPath()).append(':');
+ }
+ LineAndColumn start = getStartLineAndColumn();
+ if (start == null) {
+ if (getStartOffset() == 0 && getEndOffset() == 0) {
+ buf.append("1"); // i.e. line 1 (special case: no information at all)
+ } else {
+ buf.append("char offsets ").
+ append(getStartOffset()).append("--").append(getEndOffset());
+ }
+ } else {
+ buf.append(start.getLine()).append(':').append(start.getColumn());
+ }
+ return buf.toString();
+ }
+
+ /**
+ * Prints the object in a sort of reasonable way. This should never be used in user-visible
+ * places, only for debugging and testing.
+ */
+ @Override
+ public String toString() {
+ return print();
+ }
+
+ /**
+ * A value class that describes the line and column of an offset in a file.
+ */
+ @Immutable
+ public static final class LineAndColumn {
+ private final int line;
+ private final int column;
+
+ public LineAndColumn(int line, int column) {
+ this.line = line;
+ this.column = column;
+ }
+
+ public int getLine() {
+ return line;
+ }
+
+ public int getColumn() {
+ return column;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) {
+ return true;
+ }
+ if (!(o instanceof LineAndColumn)) {
+ return false;
+ }
+ LineAndColumn lac = (LineAndColumn) o;
+ return lac.line == line && lac.column == column;
+ }
+
+ @Override
+ public int hashCode() {
+ return line * 81 + column;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/events/NullEventHandler.java b/src/main/java/com/google/devtools/build/lib/events/NullEventHandler.java
new file mode 100644
index 0000000..8bee1eb
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/events/NullEventHandler.java
@@ -0,0 +1,28 @@
+// Copyright 2014 Google Inc. 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.events;
+
+/**
+ * An ErrorEventListener which does nothing.
+ */
+public final class NullEventHandler implements EventHandler {
+ public static final EventHandler INSTANCE = new NullEventHandler();
+
+ private NullEventHandler() {} // Prevent instantiation
+
+ @Override
+ public void handle(Event e) {
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/events/OutputFilter.java b/src/main/java/com/google/devtools/build/lib/events/OutputFilter.java
new file mode 100644
index 0000000..b5ca34d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/events/OutputFilter.java
@@ -0,0 +1,75 @@
+// Copyright 2014 Google Inc. 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.events;
+
+import java.util.regex.Pattern;
+
+/**
+ * An output filter for warnings.
+ */
+public interface OutputFilter {
+
+ /** An output filter that matches everything. */
+ public static final OutputFilter OUTPUT_EVERYTHING = new OutputFilter() {
+ @Override
+ public boolean showOutput(String tag) {
+ return true;
+ }
+ };
+
+ /** An output filter that matches nothing. */
+ public static final OutputFilter OUTPUT_NOTHING = new OutputFilter() {
+ @Override
+ public boolean showOutput(String tag) {
+ return false;
+ }
+ };
+
+ /**
+ * Returns true iff the given tag matches the output filter.
+ */
+ boolean showOutput(String tag);
+
+ /**
+ * An output filter using regular expression matching.
+ */
+ public static final class RegexOutputFilter implements OutputFilter {
+ /** Returns an output filter for the given regex (by compiling it). */
+ public static OutputFilter forRegex(String regex) {
+ return new RegexOutputFilter(Pattern.compile(regex));
+ }
+
+ /** Returns an output filter for the given pattern. */
+ public static OutputFilter forPattern(Pattern pattern) {
+ return new RegexOutputFilter(pattern);
+ }
+
+ private final Pattern pattern;
+
+ private RegexOutputFilter(Pattern pattern) {
+ this.pattern = pattern;
+ }
+
+ @Override
+ public boolean showOutput(String tag) {
+ return pattern.matcher(tag).find();
+ }
+
+ @Override
+ public String toString() {
+ return pattern.toString();
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/events/PrintingEventHandler.java b/src/main/java/com/google/devtools/build/lib/events/PrintingEventHandler.java
new file mode 100644
index 0000000..fa94d95
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/events/PrintingEventHandler.java
@@ -0,0 +1,119 @@
+// Copyright 2014 Google Inc. 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.events;
+
+import com.google.devtools.build.lib.util.io.OutErr;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.Set;
+
+/**
+ * An event handler that prints to an OutErr stream pair in a
+ * canonical format, for example:
+ * <pre>
+ * ERROR: /home/jrluser/src/workspace/x/BUILD:23:1: syntax error.
+ * </pre>
+ * This syntax is parseable by Emacs's compile.el.
+ *
+ * <p>
+ * By default, the output will go to SYSTEM_OUT_ERR,
+ * but this can be changed by calling the setOut() method.
+ *
+ * <p>
+ * This class is used only for tests.
+ */
+public class PrintingEventHandler extends AbstractEventHandler
+ implements EventHandler {
+
+ /**
+ * A convenient event-handler for terminal applications that prints all
+ * errors and warnings it encounters to the error stream.
+ * STDOUT and STDERR events pass their output directly
+ * through to the corresponding streams.
+ */
+ public static final PrintingEventHandler ERRORS_AND_WARNINGS_TO_STDERR =
+ new PrintingEventHandler(EventKind.ERRORS_AND_WARNINGS_AND_OUTPUT);
+
+ /**
+ * A convenient event-handler for terminal applications that prints all
+ * errors it encounters to the error stream.
+ * STDOUT and STDERR events pass their output directly
+ * through to the corresponding streams.
+ */
+ public static final PrintingEventHandler ERRORS_TO_STDERR =
+ new PrintingEventHandler(EventKind.ERRORS_AND_OUTPUT);
+
+ private OutErr outErr = OutErr.SYSTEM_OUT_ERR;
+
+ /**
+ * Setup a printing event handler that will handle events matching the mask.
+ * Events will be printed to the original System.out and System.err
+ * unless/until redirected by a call to setOutErr().
+ */
+ public PrintingEventHandler(Set<EventKind> mask) {
+ super(mask);
+ }
+
+ /**
+ * Redirect all output to the specified OutErr stream pair.
+ * Returns the previous OutErr.
+ */
+ public OutErr setOutErr(OutErr outErr) {
+ OutErr prev = this.outErr;
+ this.outErr = outErr;
+ return prev;
+ }
+
+ /**
+ * Print a description of the specified event to the appropriate
+ * output or error stream.
+ */
+ @Override
+ public void handle(Event event) {
+ if (!getEventMask().contains(event.getKind())) {
+ return;
+ }
+ try {
+ switch (event.getKind()) {
+ case STDOUT:
+ outErr.getOutputStream().write(event.getMessageBytes());
+ outErr.getOutputStream().flush();
+ break;
+ case STDERR:
+ outErr.getErrorStream().write(event.getMessageBytes());
+ outErr.getErrorStream().flush();
+ break;
+ default:
+ PrintWriter err = new PrintWriter(outErr.getErrorStream());
+ err.print(event.getKind());
+ err.print(": ");
+ if (event.getLocation() != null) {
+ err.print(event.getLocation().print());
+ err.print(": ");
+ }
+ err.println(event.getMessage());
+ err.flush();
+ }
+ } catch (IOException e) {
+ /*
+ * Note: we can't print to System.out or System.err here,
+ * because those will normally be set to streams which
+ * translate I/O to STDOUT and STDERR events,
+ * which would result in infinite recursion.
+ */
+ outErr.printErrLn(e.getMessage());
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/events/Reporter.java b/src/main/java/com/google/devtools/build/lib/events/Reporter.java
new file mode 100644
index 0000000..e0c3925
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/events/Reporter.java
@@ -0,0 +1,146 @@
+// Copyright 2014 Google Inc. 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.events;
+
+import com.google.devtools.build.lib.util.io.OutErr;
+
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * The reporter is the primary means of reporting events such as errors,
+ * warnings, progress information and diagnostic information to the user. It
+ * is not intended as a logging mechanism for developer-only messages; use a
+ * Logger for that.
+ *
+ * The reporter instance is consumed by the build system, and passes events to
+ * {@link EventHandler} instances. These handlers are registered via {@link
+ * #addHandler(EventHandler)}.
+ *
+ * <p>Thread-safe: calls to {@code #report} may be made on any thread.
+ * Handlers may be run in an arbitary thread (but right now, they will not be
+ * run concurrently).
+ */
+public final class Reporter implements EventHandler, ExceptionListener {
+
+ private final List<EventHandler> handlers = new ArrayList<>();
+
+ /** An OutErr that sends all of its output to this Reporter.
+ * Each write will (when flushed) get mapped to an EventKind.STDOUT or EventKind.STDERR event.
+ */
+ private final OutErr outErrToReporter = outErrForReporter(this);
+ private volatile OutputFilter outputFilter = OutputFilter.OUTPUT_EVERYTHING;
+
+ public Reporter() {}
+
+ public static OutErr outErrForReporter(EventHandler rep) {
+ return OutErr.create(
+ // We don't use BufferedOutputStream here, because in general the Blaze
+ // code base assumes that the output streams are not buffered.
+ new ReporterStream(rep, EventKind.STDOUT),
+ new ReporterStream(rep, EventKind.STDERR));
+ }
+
+ /**
+ * A copy constructor, to make it convenient to replicate a reporter
+ * config for temporary configuration changes.
+ */
+ public Reporter(Reporter template) {
+ handlers.addAll(template.handlers);
+ }
+
+ /**
+ * Constructor which configures a reporter with the specified handlers.
+ */
+ public Reporter(EventHandler... handlers) {
+ for (EventHandler handler: handlers) {
+ addHandler(handler);
+ }
+ }
+
+ /**
+ * Returns an OutErr that sends all of its output to this Reporter.
+ * Each write to the OutErr will cause an EventKind.STDOUT or EventKind.STDERR event.
+ */
+ public OutErr getOutErr() {
+ return outErrToReporter;
+ }
+
+ /**
+ * Adds a handler to this reporter.
+ */
+ public synchronized void addHandler(EventHandler handler) {
+ handlers.add(handler);
+ }
+
+ /**
+ * Removes handler from this reporter.
+ */
+ public synchronized void removeHandler(EventHandler handler) {
+ handlers.remove(handler);
+ }
+
+ /**
+ * This method is called by the build system to report an event.
+ */
+ @Override
+ public synchronized void handle(Event e) {
+ if (e.getKind() != EventKind.ERROR && e.getTag() != null && !showOutput(e.getTag())) {
+ return;
+ }
+ for (EventHandler handler : handlers) {
+ handler.handle(e);
+ }
+ }
+
+ /**
+ * Reports the start of a particular task.
+ * Is a wrapper around report() with event kind START.
+ * Should always be matched by a corresponding call to finishTask()
+ * with the same message, except that the leading percentage
+ * progress indicator (if any) in the message may differ.
+ */
+ public void startTask(Location location, String message) {
+ handle(new Event(EventKind.START, location, message));
+ }
+
+ /**
+ * Reports the start of a particular task.
+ * Is a wrapper around report() with event kind FINISH.
+ * Should always be matched by a corresponding call to startTask()
+ * with the same message, except that the leading percentage
+ * progress indicator (if any) in the message may differ.
+ */
+ public void finishTask(Location location, String message) {
+ handle(new Event(EventKind.FINISH, location, message));
+ }
+
+ @Override
+ public void error(Location location, String message, Throwable error) {
+ handle(new Event(EventKind.ERROR, location, message));
+ error.printStackTrace(new PrintStream(getOutErr().getErrorStream()));
+ }
+
+ /**
+ * Returns true iff the given tag matches the output filter.
+ */
+ public boolean showOutput(String tag) {
+ return outputFilter.showOutput(tag);
+ }
+
+ public void setOutputFilter(OutputFilter outputFilter) {
+ this.outputFilter = outputFilter;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/events/ReporterStream.java b/src/main/java/com/google/devtools/build/lib/events/ReporterStream.java
new file mode 100644
index 0000000..9c625c8
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/events/ReporterStream.java
@@ -0,0 +1,58 @@
+// Copyright 2014 Google Inc. 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.events;
+
+import static java.nio.charset.StandardCharsets.ISO_8859_1;
+
+import java.io.OutputStream;
+
+/**
+ * An OutputStream that delegates all writes to a Reporter.
+ */
+public final class ReporterStream extends OutputStream {
+
+ private final EventHandler reporter;
+ private final EventKind eventKind;
+
+ public ReporterStream(EventHandler reporter, EventKind eventKind) {
+ this.reporter = reporter;
+ this.eventKind = eventKind;
+ }
+
+ @Override
+ public void close() {
+ // NOP.
+ }
+
+ @Override
+ public void flush() {
+ // NOP.
+ }
+
+ @Override
+ public void write(int b) {
+ reporter.handle(new Event(eventKind, null, new byte[] { (byte) b }));
+ }
+
+ @Override
+ public void write(byte[] bytes) {
+ reporter.handle(new Event(eventKind, null, bytes));
+ }
+
+ @Override
+ public void write(byte[] bytes, int offset, int len) {
+ reporter.handle(new Event(eventKind, null, new String(bytes, offset, len, ISO_8859_1)));
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/events/StoredEventHandler.java b/src/main/java/com/google/devtools/build/lib/events/StoredEventHandler.java
new file mode 100644
index 0000000..be8a627
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/events/StoredEventHandler.java
@@ -0,0 +1,63 @@
+// Copyright 2014 Google Inc. 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.events;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Stores error and warning events, and later replays them. Thread-safe.
+ */
+public class StoredEventHandler implements EventHandler {
+
+ private final List<Event> events = new ArrayList<>();
+ private boolean hasErrors;
+
+ public synchronized ImmutableList<Event> getEvents() {
+ return ImmutableList.copyOf(events);
+ }
+
+ /** Returns true if there are no stored events. */
+ public synchronized boolean isEmpty() {
+ return events.isEmpty();
+ }
+
+
+ @Override
+ public synchronized void handle(Event e) {
+ hasErrors |= e.getKind() == EventKind.ERROR;
+ events.add(e);
+ }
+
+ /**
+ * Replay all events stored in this object on the given eventHandler, in the same order.
+ */
+ public synchronized void replayOn(EventHandler eventHandler) {
+ Event.replayEventsOn(eventHandler, events);
+ }
+
+ /**
+ * Returns whether any of the events on this objects were errors.
+ */
+ public synchronized boolean hasErrors() {
+ return hasErrors;
+ }
+
+ public synchronized void clear() {
+ events.clear();
+ hasErrors = false;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/events/WarningsAsErrorsEventHandler.java b/src/main/java/com/google/devtools/build/lib/events/WarningsAsErrorsEventHandler.java
new file mode 100644
index 0000000..91a2150
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/events/WarningsAsErrorsEventHandler.java
@@ -0,0 +1,41 @@
+// Copyright 2014 Google Inc. 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.events;
+
+/**
+ * Passes through any events, and converts any warnings to errors.
+ */
+public final class WarningsAsErrorsEventHandler extends DelegatingEventHandler {
+
+ boolean warningsEncountered = false;
+
+ public WarningsAsErrorsEventHandler(EventHandler eventHandler) {
+ super(eventHandler);
+ }
+
+ @Override
+ public synchronized void handle(Event e) {
+ if (e.getKind() == EventKind.WARNING) {
+ warningsEncountered = true;
+ super.handle(new Event(EventKind.ERROR, e.getLocation(), e.getMessage()));
+ } else {
+ super.handle(e);
+ }
+ }
+
+ public boolean warningsEncountered() {
+ return warningsEncountered;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/exec/AlwaysOutOfDateAction.java b/src/main/java/com/google/devtools/build/lib/exec/AlwaysOutOfDateAction.java
new file mode 100644
index 0000000..0e484f7
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/exec/AlwaysOutOfDateAction.java
@@ -0,0 +1,21 @@
+// Copyright 2014 Google Inc. 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.exec;
+
+/**
+ * Marker interface for actions that must be run unconditionally.
+ */
+public interface AlwaysOutOfDateAction {
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/exec/CheckUpToDateFilter.java b/src/main/java/com/google/devtools/build/lib/exec/CheckUpToDateFilter.java
new file mode 100644
index 0000000..84f3aef
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/exec/CheckUpToDateFilter.java
@@ -0,0 +1,73 @@
+// Copyright 2014 Google Inc. 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.exec;
+
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.rules.test.TestRunnerAction;
+
+/**
+ * Class implements --check_???_up_to_date execution filter predicate
+ * that prevents certain actions from being executed (thus aborting
+ * the build if action is not up-to-date).
+ */
+public final class CheckUpToDateFilter implements Predicate<Action> {
+
+ /**
+ * Determines an execution filter based on the --check_up_to_date and
+ * --check_tests_up_to_date options. Returns a singleton if possible.
+ */
+ public static Predicate<Action> fromOptions(ExecutionOptions options) {
+ if (!options.testCheckUpToDate && !options.checkUpToDate) {
+ return Predicates.alwaysTrue();
+ }
+ return new CheckUpToDateFilter(options);
+ }
+
+ private final boolean allowBuildActionExecution;
+ private final boolean allowTestActionExecution;
+
+ /**
+ * Creates new execution filter based on --check_up_to_date and
+ * --check_tests_up_to_date options.
+ */
+ private CheckUpToDateFilter(ExecutionOptions options) {
+ // If we want to check whether test is up-to-date, we should disallow
+ // test execution.
+ this.allowTestActionExecution = !options.testCheckUpToDate;
+
+ // Build action execution should be prohibited in two cases - if we are
+ // checking whether build is up-to-date or if we are checking that tests
+ // are up-to-date (and test execution is not allowed).
+ this.allowBuildActionExecution = allowTestActionExecution && !options.checkUpToDate;
+ }
+
+ /**
+ * @return true if actions' execution is allowed, false - otherwise
+ */
+ @Override
+ public boolean apply(Action action) {
+ if (action instanceof AlwaysOutOfDateAction) {
+ // Always allow fileset manifest action to execute because it identifies files included
+ // in the fileset during execution time.
+ return true;
+ } else if (action instanceof TestRunnerAction) {
+ return allowTestActionExecution;
+ } else {
+ return allowBuildActionExecution;
+ }
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/exec/Digest.java b/src/main/java/com/google/devtools/build/lib/exec/Digest.java
new file mode 100644
index 0000000..4262711
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/exec/Digest.java
@@ -0,0 +1,182 @@
+// Copyright 2014 Google Inc. 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.exec;
+
+import static java.nio.charset.StandardCharsets.US_ASCII;
+
+import com.google.common.io.BaseEncoding;
+import com.google.devtools.build.lib.actions.cache.VirtualActionInput;
+import com.google.devtools.build.lib.util.Pair;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.MessageLite;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * A utility class for obtaining MD5 digests.
+ * Digests are represented as 32 characters in lowercase ASCII.
+ */
+public class Digest {
+
+ public static final ByteString EMPTY_DIGEST = fromContent(new byte[]{});
+
+ private Digest() {
+ }
+
+ /**
+ * Get the digest from the given byte array.
+ * @param bytes the byte array.
+ * @return a digest.
+ */
+ public static ByteString fromContent(byte[] bytes) {
+ MessageDigest md = newBuilder();
+ md.update(bytes, 0, bytes.length);
+ return toByteString(BaseEncoding.base16().lowerCase().encode(md.digest()));
+ }
+
+ /**
+ * Get the digest from the given ByteBuffer.
+ * @param buffer the ByteBuffer.
+ * @return a digest.
+ */
+ public static ByteString fromBuffer(ByteBuffer buffer) {
+ MessageDigest md = newBuilder();
+ md.update(buffer);
+ return toByteString(BaseEncoding.base16().lowerCase().encode(md.digest()));
+ }
+
+ /**
+ * Gets the digest of the given proto.
+ *
+ * @param proto a protocol buffer.
+ * @return the digest.
+ */
+ public static ByteString fromProto(MessageLite proto) {
+ MD5OutputStream md5Stream = new MD5OutputStream();
+ try {
+ proto.writeTo(md5Stream);
+ } catch (IOException e) {
+ throw new IllegalStateException("Unexpected IOException: ", e);
+ }
+ return toByteString(md5Stream.getDigest());
+ }
+
+ /**
+ * Gets the digest and size of a given VirtualActionInput.
+ *
+ * @param input the VirtualActionInput.
+ * @return the digest and size.
+ */
+ public static Pair<ByteString, Long> fromVirtualActionInput(VirtualActionInput input)
+ throws IOException {
+ CountingMD5OutputStream md5Stream = new CountingMD5OutputStream();
+ input.writeTo(md5Stream);
+ ByteString digest = toByteString(md5Stream.getDigest());
+ return Pair.of(digest, md5Stream.getSize());
+ }
+
+ /**
+ * A Sink that does an online MD5 calculation, which avoids forcing us to keep the entire
+ * proto message in memory.
+ */
+ private static class MD5OutputStream extends OutputStream {
+ private final MessageDigest md = newBuilder();
+
+ @Override
+ public void write(int b) {
+ md.update((byte) b);
+ }
+
+ @Override
+ public void write(byte[] b, int off, int len) {
+ md.update(b, off, len);
+ }
+
+ public String getDigest() {
+ return BaseEncoding.base16().lowerCase().encode(md.digest());
+ }
+ }
+
+ private static final class CountingMD5OutputStream extends MD5OutputStream {
+ private long size;
+
+ @Override
+ public void write(int b) {
+ super.write(b);
+ size++;
+ }
+
+ @Override
+ public void write(byte[] b, int off, int len) {
+ super.write(b, off, len);
+ size += len;
+ }
+
+ public long getSize() {
+ return size;
+ }
+ }
+
+ /**
+ * @param digest the digest to check.
+ * @return true iff digest is a syntactically legal digest. It must be 32
+ * characters of hex with lowercase letters.
+ */
+ public static boolean isDigest(ByteString digest) {
+ if (digest == null || digest.size() != 32) {
+ return false;
+ }
+
+ for (byte b : digest) {
+ char c = (char) b;
+ if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) {
+ continue;
+ }
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * @param digest the digest.
+ * @return true iff the digest is that of an empty file.
+ */
+ public static boolean isEmpty(ByteString digest) {
+ return digest.equals(EMPTY_DIGEST);
+ }
+
+ /**
+ * @return a new MD5 digest builder.
+ */
+ public static MessageDigest newBuilder() {
+ try {
+ return MessageDigest.getInstance("md5");
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalStateException("MD5 not available");
+ }
+ }
+
+ /**
+ * Convert a String digest into a ByteString using ascii.
+ * @param digest the digest in ascii.
+ * @return the digest as a ByteString.
+ */
+ public static ByteString toByteString(String digest) {
+ return ByteString.copyFrom(digest.getBytes(US_ASCII));
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/exec/ExecutionOptions.java b/src/main/java/com/google/devtools/build/lib/exec/ExecutionOptions.java
new file mode 100644
index 0000000..58e360b
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/exec/ExecutionOptions.java
@@ -0,0 +1,195 @@
+// Copyright 2014 Google Inc. 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.exec;
+
+import com.google.devtools.build.lib.actions.ResourceSet;
+import com.google.devtools.build.lib.packages.TestTimeout;
+import com.google.devtools.build.lib.rules.test.TestStrategy;
+import com.google.devtools.build.lib.rules.test.TestStrategy.TestOutputFormat;
+import com.google.devtools.build.lib.rules.test.TestStrategy.TestSummaryFormat;
+import com.google.devtools.build.lib.util.OptionsUtils;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.common.options.Option;
+import com.google.devtools.common.options.Options;
+import com.google.devtools.common.options.OptionsBase;
+
+import java.util.Map;
+
+/**
+ * Options affecting the execution phase of a build.
+ *
+ * These options are interpreted by the BuildTool to choose an Executor to
+ * be used for the build.
+ *
+ * Note: from the user's point of view, the characteristic function of this
+ * set of options is indistinguishable from that of the BuildRequestOptions:
+ * they are all per-request. The difference is only apparent in the
+ * implementation: these options are used only by the lib.exec machinery, which
+ * affects how C++ and Java compilation occur. (The BuildRequestOptions
+ * contain a mixture of "semantic" options affecting the choice of targets to
+ * build, and "non-semantic" options affecting the lib.actions machinery.)
+ * Ideally, the user would be unaware of the difference. For now, the usage
+ * strings are identical modulo "part 1", "part 2".
+ */
+public class ExecutionOptions extends OptionsBase {
+
+ public static final ExecutionOptions DEFAULTS = Options.getDefaults(ExecutionOptions.class);
+
+ @Option(name = "verbose_failures",
+ defaultValue = "false",
+ category = "verbosity",
+ help = "If a command fails, print out the full command line.")
+ public boolean verboseFailures;
+
+ @Option(name = "subcommands",
+ abbrev = 's',
+ defaultValue = "false",
+ category = "verbosity",
+ help = "Display the subcommands executed during a build.")
+ public boolean showSubcommands;
+
+ @Option(name = "check_up_to_date",
+ defaultValue = "false",
+ category = "what",
+ help = "Don't perform the build, just check if it is up-to-date. If all targets are "
+ + "up-to-date, the build completes successfully. If any step needs to be executed "
+ + "an error is reported and the build fails.")
+ public boolean checkUpToDate;
+
+ @Option(name = "check_tests_up_to_date",
+ defaultValue = "false",
+ category = "testing",
+ implicitRequirements = { "--check_up_to_date" },
+ help = "Don't run tests, just check if they are up-to-date. If all tests results are "
+ + "up-to-date, the testing completes successfully. If any test needs to be built or "
+ + "executed, an error is reported and the testing fails. This option implies "
+ + "--check_up_to_date behavior."
+ )
+ public boolean testCheckUpToDate;
+
+ @Option(name = "test_strategy",
+ defaultValue = "",
+ category = "testing",
+ help = "Specifies which strategy to use when running tests.")
+ public String testStrategy;
+
+ @Option(name = "test_keep_going",
+ defaultValue = "true",
+ category = "testing",
+ help = "When disabled, any non-passing test will cause the entire build to stop. By default "
+ + "all tests are run, even if some do not pass.")
+ public boolean testKeepGoing;
+
+ @Option(name = "runs_per_test_detects_flakes",
+ defaultValue = "false",
+ category = "testing",
+ help = "If true, any shard in which at least one run/attempt passes and at least one "
+ + "run/attempt fails gets a FLAKY status.")
+ public boolean runsPerTestDetectsFlakes;
+
+ @Option(name = "flaky_test_attempts",
+ defaultValue = "default",
+ category = "testing",
+ converter = TestStrategy.TestAttemptsConverter.class,
+ help = "Each test will be retried up to the specified number of times in case of any test "
+ + "failure. Tests that required more than one attempt to pass would be marked as "
+ + "'FLAKY' in the test summary. If this option is set, it should specify an int N or the "
+ + "string 'default'. If it's an int, then all tests will be run up to N times. If it is "
+ + "not specified or its value is 'default', then only a single test attempt will be made "
+ + "for regular tests and three for tests marked explicitly as flaky by their rule "
+ + "(flaky=1 attribute).")
+ public int testAttempts;
+
+ @Option(name = "test_tmpdir",
+ defaultValue = "null",
+ category = "testing",
+ converter = OptionsUtils.PathFragmentConverter.class,
+ help = "Specifies the base temporary directory for 'blaze test' to use.")
+ public PathFragment testTmpDir;
+
+ @Option(name = "test_output",
+ defaultValue = "summary",
+ category = "testing",
+ converter = TestStrategy.TestOutputFormat.Converter.class,
+ help = "Specifies desired output mode. Valid values are 'summary' to "
+ + "output only test status summary, 'errors' to also print test logs "
+ + "for failed tests, 'all' to print logs for all tests and 'streamed' "
+ + "to output logs for all tests in real time (this will force tests "
+ + "to be executed locally one at a time regardless of --test_strategy "
+ + "value).")
+ public TestOutputFormat testOutput;
+
+ @Option(name = "test_summary",
+ defaultValue = "short",
+ category = "testing",
+ converter = TestStrategy.TestSummaryFormat.Converter.class,
+ help = "Specifies the desired format ot the test summary. Valid values "
+ + "are 'short' to print information only about tests executed, "
+ + "'terse', to print information only about unsuccessful tests,"
+ + "'detailed' to print detailed information about failed test cases, "
+ + "and 'none' to omit the summary.")
+ public TestSummaryFormat testSummary;
+
+ @Option(name = "test_timeout",
+ defaultValue = "-1",
+ category = "testing",
+ converter = TestTimeout.TestTimeoutConverter.class,
+ help = "Override the default test timeout values for test timeouts (in secs). If a single "
+ + "positive integer value is specified it will override all categories. If 4 comma-"
+ + "separated integers are specified, they will override the timeouts for short, "
+ + "moderate, long and eternal (in that order). In either form, a value of -1 tells blaze "
+ + "to use its default timeouts for that category.")
+ public Map<TestTimeout, Integer> testTimeout;
+
+
+ @Option(name = "resource_autosense",
+ defaultValue = "false",
+ category = "strategy",
+ help = "Periodically (every 3 seconds) poll system CPU load and available memory "
+ + "and allow execution of build commands if system has sufficient idle CPU and "
+ + "free RAM resources. By default this option is disabled, and Blaze will rely on "
+ + "approximation algorithms based on the total amount of available memory and number "
+ + "of CPU cores.")
+ public boolean useResourceAutoSense;
+
+ @Option(name = "ram_utilization_factor",
+ defaultValue = "67",
+ category = "strategy",
+ help = "Specify what percentage of the system's RAM Blaze should try to use for its "
+ + "subprocesses. "
+ + "This option affects how many processes Blaze will try to run in parallel. "
+ + "If you run several Blaze builds in parallel, using a lower value for "
+ + "this option may avoid thrashing and thus improve overall throughput. "
+ + "Using a value higher than the default is NOT recommended. "
+ + "Note that Blaze's estimates are very coarse, so the actual RAM usage may be much "
+ + "higher or much lower than specified. "
+ + "Note also that this option does not affect the amount of memory that the Blaze "
+ + "server itself will use. "
+ + "Also, this option has no effect if --resource_autosense is enabled."
+ )
+ public int ramUtilizationPercentage;
+
+ @Option(name = "local_resources",
+ defaultValue = "null",
+ category = "strategy",
+ help = "Explicitly set amount of local resources available to Blaze. "
+ + "By default, Blaze will query system configuration to estimate amount of RAM (in MB) "
+ + "and number of CPU cores available for the locally executed build actions. It would also "
+ + "assume default I/O capabilities of the local workstation (1.0). This options allows to "
+ + "explicitly set all 3 values. Note, that if this option is used, Blaze will ignore "
+ + "both --ram_utilization_factor and --resource_autosense values.",
+ converter = ResourceSet.ResourceSetConverter.class
+ )
+ public ResourceSet availableResources;
+}
diff --git a/src/main/java/com/google/devtools/build/lib/exec/FileWriteStrategy.java b/src/main/java/com/google/devtools/build/lib/exec/FileWriteStrategy.java
new file mode 100644
index 0000000..5dc9914
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/exec/FileWriteStrategy.java
@@ -0,0 +1,73 @@
+// Copyright 2014 Google Inc. 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.exec;
+
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.ActionExecutionContext;
+import com.google.devtools.build.lib.actions.EnvironmentalExecException;
+import com.google.devtools.build.lib.actions.ExecException;
+import com.google.devtools.build.lib.actions.ExecutionStrategy;
+import com.google.devtools.build.lib.actions.Executor;
+import com.google.devtools.build.lib.actions.ResourceSet;
+import com.google.devtools.build.lib.analysis.actions.AbstractFileWriteAction;
+import com.google.devtools.build.lib.analysis.actions.FileWriteActionContext;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.util.io.FileOutErr;
+import com.google.devtools.build.lib.vfs.Path;
+
+import java.io.BufferedOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * A strategy for executing an {@link AbstractFileWriteAction}.
+ */
+@ExecutionStrategy(name = { "local" }, contextType = FileWriteActionContext.class)
+public final class FileWriteStrategy implements FileWriteActionContext {
+
+ public static final Class<FileWriteStrategy> TYPE = FileWriteStrategy.class;
+
+ public FileWriteStrategy() {
+ }
+
+ @Override
+ public void exec(Executor executor, AbstractFileWriteAction action, FileOutErr outErr,
+ ActionExecutionContext actionExecutionContext) throws ExecException, InterruptedException {
+ EventHandler reporter = executor == null ? null : executor.getEventHandler();
+ try {
+ Path outputPath = Iterables.getOnlyElement(action.getOutputs()).getPath();
+ try (OutputStream out = new BufferedOutputStream(outputPath.getOutputStream())) {
+ action.newDeterministicWriter(reporter, executor).writeOutputFile(out);
+ }
+ if (action.makeExecutable()) {
+ outputPath.setExecutable(true);
+ }
+ } catch (IOException e) {
+ throw new EnvironmentalExecException("failed to create file '"
+ + Iterables.getOnlyElement(action.getOutputs()).prettyPrint()
+ + "' due to I/O error: " + e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public ResourceSet estimateResourceConsumption(AbstractFileWriteAction action) {
+ return action.estimateResourceConsumptionLocal();
+ }
+
+ @Override
+ public String strategyLocality(AbstractFileWriteAction action) {
+ return "local";
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/exec/OutputService.java b/src/main/java/com/google/devtools/build/lib/exec/OutputService.java
new file mode 100644
index 0000000..88d9b94
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/exec/OutputService.java
@@ -0,0 +1,122 @@
+// Copyright 2014 Google Inc. 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.exec;
+
+import com.google.devtools.build.lib.actions.BuildFailedException;
+import com.google.devtools.build.lib.actions.ExecException;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.util.AbruptExitException;
+import com.google.devtools.build.lib.vfs.BatchStat;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.io.IOException;
+
+/**
+ * An OutputService retains control over the Blaze output tree, and provides a higher level of
+ * abstraction compared to the VFS layer.
+ *
+ * <p>Higher-level facilities include batch statting, cleaning the output tree, creating symlink
+ * trees, and out-of-band insertion of metadata into the tree.
+ */
+public interface OutputService {
+
+ /**
+ * @return the name of filesystem, akin to what you might see in /proc/mounts
+ */
+ String getFilesSystemName();
+
+ /**
+ * @return true if the output service uses FUSE
+ */
+ boolean usesFuse();
+
+ /**
+ * @return a human-readable, one word name for the service
+ */
+ String getName();
+
+ /**
+ * Start the build.
+ *
+ * @throws BuildFailedException if build preparation failed
+ * @throws InterruptedException
+ */
+ void startBuild() throws BuildFailedException, AbruptExitException, InterruptedException;
+
+ /**
+ * Finish the build.
+ *
+ * @param buildSuccessful iff build was successful
+ * @throws BuildFailedException on failure
+ */
+ void finalizeBuild(boolean buildSuccessful) throws BuildFailedException, AbruptExitException;
+
+ /**
+ * Stages the given tool from the package path, possibly copying it to local disk.
+ *
+ * @param tool target representing the tool to stage
+ * @return a Path pointing to the staged target
+ */
+ Path stageTool(Target tool) throws IOException;
+
+ /**
+ * @return the name of the workspace this output service controls.
+ */
+ String getWorkspace();
+
+ /**
+ * @return the BatchStat instance or null.
+ */
+ BatchStat getBatchStatter();
+
+ /**
+ * @return true iff createSymlinkTree() is available.
+ */
+ boolean canCreateSymlinkTree();
+
+ /**
+ * Creates the symlink tree
+ *
+ * @param inputPath the input manifest
+ * @param outputPath the output manifest
+ * @param filesetTree is true iff we're constructing a Fileset
+ * @param symlinkTreeRoot the symlink tree root, relative to the execRoot
+ * @throws ExecException on failure
+ * @throws InterruptedException
+ */
+ void createSymlinkTree(Path inputPath, Path outputPath, boolean filesetTree,
+ PathFragment symlinkTreeRoot) throws ExecException, InterruptedException;
+
+ /**
+ * Cleans the entire output tree.
+ *
+ * @throws ExecException on failure
+ * @throws InterruptedException
+ */
+ void clean() throws ExecException, InterruptedException;
+
+ /**
+ * @param file the File
+ * @return true iff the file actually lives on a remote server
+ */
+ boolean isRemoteFile(Path file);
+
+ /**
+ * @param path a fully-resolved path
+ * @return true iff path is under this output service's control
+ */
+ boolean resolvedPathUnderTree(Path path);
+}
diff --git a/src/main/java/com/google/devtools/build/lib/exec/SingleBuildFileCache.java b/src/main/java/com/google/devtools/build/lib/exec/SingleBuildFileCache.java
new file mode 100644
index 0000000..8ec1e51
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/exec/SingleBuildFileCache.java
@@ -0,0 +1,143 @@
+// Copyright 2014 Google Inc. 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.exec;
+
+import static java.nio.charset.StandardCharsets.US_ASCII;
+
+import com.google.common.base.Preconditions;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.Maps;
+import com.google.common.io.BaseEncoding;
+import com.google.devtools.build.lib.actions.ActionInput;
+import com.google.devtools.build.lib.actions.ActionInputFileCache;
+import com.google.devtools.build.lib.actions.DigestOfDirectoryException;
+import com.google.devtools.build.lib.util.Pair;
+import com.google.devtools.build.lib.vfs.FileSystem;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.protobuf.ByteString;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Map;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * An in-memory cache to ensure we do I/O for source files only once during a single build.
+ *
+ * <p>Simply maintains a two-way cached mapping from digest <--> filename that may be populated
+ * only once.
+ */
+@ThreadSafe
+public class SingleBuildFileCache implements ActionInputFileCache {
+
+ private final String cwd;
+ private final FileSystem fs;
+
+ public SingleBuildFileCache(String cwd, FileSystem fs) {
+ this.fs = Preconditions.checkNotNull(fs);
+ this.cwd = Preconditions.checkNotNull(cwd);
+ }
+
+ // If we can't get the digest, we store the exception. This avoids extra file IO for files
+ // that are allowed to be missing, as we first check a likely non-existent content file
+ // first. Further we won't need to unwrap the exception in getDigest().
+ private final LoadingCache<ActionInput, Pair<ByteString, IOException>> pathToDigest =
+ CacheBuilder.newBuilder()
+ // We default to 10 disk read threads, but we don't expect them all to edit the map
+ // simultaneously.
+ .concurrencyLevel(8)
+ // Even small-ish builds, as of 11/21/2011 typically have over 10k artifacts, so it's
+ // unlikely that this default will adversely affect memory in most cases.
+ .initialCapacity(10000)
+ .build(new CacheLoader<ActionInput, Pair<ByteString, IOException>>() {
+ @Override
+ public Pair<ByteString, IOException> load(ActionInput input) {
+ Path path = null;
+ try {
+ path = fs.getPath(fullPath(input));
+ BaseEncoding hex = BaseEncoding.base16().lowerCase();
+ ByteString digest = ByteString.copyFrom(
+ hex.encode(path.getMD5Digest())
+ .getBytes(US_ASCII));
+ pathToBytes.put(input, path.getFileSize());
+ // Inject reverse mapping. Doing this unconditionally in getDigest() showed up
+ // as a hotspot in CPU profiling.
+ digestToPath.put(digest, input);
+ return Pair.of(digest, null);
+ } catch (IOException e) {
+ if (path != null && path.isDirectory()) {
+ pathToBytes.put(input, 0L);
+ return Pair.<ByteString, IOException>of(null, new DigestOfDirectoryException(
+ "Input is a directory: " + input.getExecPathString()));
+ }
+
+ // Put value into size map to avoid trying to read file again later.
+ pathToBytes.put(input, 0L);
+ return Pair.of(null, e);
+ }
+ }
+ });
+
+ private final Map<ByteString, ActionInput> digestToPath = Maps.newConcurrentMap();
+
+ private final Map<ActionInput, Long> pathToBytes = Maps.newConcurrentMap();
+
+ @Nullable
+ @Override
+ public File getFileFromDigest(ByteString digest) {
+ ActionInput relPath = digestToPath.get(digest);
+ return relPath == null ? null : new File(fullPath(relPath));
+ }
+
+ @Override
+ public long getSizeInBytes(ActionInput input) throws IOException {
+ // TODO(bazel-team): this only works if pathToDigest has already been called.
+ Long sz = pathToBytes.get(input);
+ if (sz != null) {
+ return sz;
+ }
+ Path path = fs.getPath(fullPath(input));
+ sz = path.getFileSize();
+ pathToBytes.put(input, sz);
+ return sz;
+ }
+
+ @Override
+ public ByteString getDigest(ActionInput input) throws IOException {
+ Pair<ByteString, IOException> result = pathToDigest.getUnchecked(input);
+ if (result.second != null) {
+ throw result.second;
+ }
+ return result.first;
+ }
+
+ @Override
+ public boolean contentsAvailableLocally(ByteString digest) {
+ return digestToPath.containsKey(digest);
+ }
+
+ /**
+ * Creates a File object that refers to fileName, if fileName is an absolute path. Otherwise,
+ * returns a File object that refers to the fileName appended to the (absolute) current working
+ * directory.
+ */
+ private String fullPath(ActionInput input) {
+ String relPath = input.getExecPathString();
+ return relPath.startsWith("/") ? relPath : new File(cwd, relPath).getPath();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/exec/SourceManifestActionContextImpl.java b/src/main/java/com/google/devtools/build/lib/exec/SourceManifestActionContextImpl.java
new file mode 100644
index 0000000..40fed77
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/exec/SourceManifestActionContextImpl.java
@@ -0,0 +1,37 @@
+// Copyright 2014 Google Inc. 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.exec;
+
+import com.google.devtools.build.lib.actions.ExecutionStrategy;
+import com.google.devtools.build.lib.analysis.SourceManifestAction;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+/**
+ * A context for {@link SourceManifestAction} that uses the runtime to determine
+ * the workspace suffix.
+ */
+@ExecutionStrategy(contextType = SourceManifestAction.Context.class)
+public class SourceManifestActionContextImpl implements SourceManifestAction.Context {
+ private final PathFragment runfilesPrefix;
+
+ public SourceManifestActionContextImpl(PathFragment runfilesPrefix) {
+ this.runfilesPrefix = runfilesPrefix;
+ }
+
+ @Override
+ public PathFragment getRunfilesPrefix() {
+ return runfilesPrefix;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/exec/SymlinkTreeHelper.java b/src/main/java/com/google/devtools/build/lib/exec/SymlinkTreeHelper.java
new file mode 100644
index 0000000..6127cee
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/exec/SymlinkTreeHelper.java
@@ -0,0 +1,137 @@
+// Copyright 2014 Google Inc. 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.exec;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.devtools.build.lib.actions.AbstractAction;
+import com.google.devtools.build.lib.actions.ActionExecutionContext;
+import com.google.devtools.build.lib.actions.BaseSpawn;
+import com.google.devtools.build.lib.actions.ExecException;
+import com.google.devtools.build.lib.actions.ResourceManager;
+import com.google.devtools.build.lib.actions.ResourceSet;
+import com.google.devtools.build.lib.analysis.config.BinTools;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.shell.CommandException;
+import com.google.devtools.build.lib.util.CommandBuilder;
+import com.google.devtools.build.lib.util.OsUtils;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.List;
+
+/**
+ * Helper class responsible for the symlink tree creation.
+ * Used to generate runfiles and fileset symlink farms.
+ */
+public final class SymlinkTreeHelper {
+
+ private static final String BUILD_RUNFILES = "build-runfiles" + OsUtils.executableExtension();
+
+ /**
+ * These actions run faster overall when serialized, because most of their
+ * cost is in the ext2 block allocator, and there's less seeking required if
+ * their directory creations get non-interleaved allocations. So we give them
+ * a huge resource cost.
+ */
+ public static final ResourceSet RESOURCE_SET = new ResourceSet(1000, 0.5, 0.75);
+
+ private final PathFragment inputManifest;
+ private final PathFragment symlinkTreeRoot;
+ private final boolean filesetTree;
+
+ /**
+ * Creates SymlinkTreeHelper instance. Can be used independently of
+ * SymlinkTreeAction.
+ *
+ * @param inputManifest exec path to the input runfiles manifest
+ * @param symlinkTreeRoot exec path to the symlink tree location
+ * @param filesetTree true if this is fileset symlink tree,
+ * false if this is a runfiles symlink tree.
+ */
+ public SymlinkTreeHelper(PathFragment inputManifest, PathFragment symlinkTreeRoot,
+ boolean filesetTree) {
+ this.inputManifest = inputManifest;
+ this.symlinkTreeRoot = symlinkTreeRoot;
+ this.filesetTree = filesetTree;
+ }
+
+ public PathFragment getSymlinkTreeRoot() { return symlinkTreeRoot; }
+
+ /**
+ * Creates a symlink tree using a CommandBuilder. This means that the symlink
+ * tree will always be present on the developer's workstation. Useful when
+ * running commands locally.
+ *
+ * Warning: this method REALLY executes the command on the box Blaze was
+ * run on, without any kind of synchronization, locking, or anything else.
+ *
+ * @param config the configuration that is used for creating the symlink tree.
+ * @throws CommandException
+ */
+ public void createSymlinksUsingCommand(Path execRoot,
+ BuildConfiguration config, BinTools binTools) throws CommandException {
+ List<String> argv = getSpawnArgumentList(execRoot, binTools);
+
+ CommandBuilder builder = new CommandBuilder();
+ builder.addArgs(argv);
+ builder.setWorkingDir(execRoot);
+ builder.build().execute();
+ }
+
+ /**
+ * Creates symlink tree using appropriate method. At this time tree
+ * always created using build-runfiles helper application.
+ *
+ * Note: method may try to acquire resources - meaning that it would
+ * block for undetermined period of time. If it is interrupted during
+ * that wait, ExecException will be thrown but interrupted bit will be
+ * preserved.
+ *
+ * @param action action instance that requested symlink tree creation
+ * @param actionExecutionContext Services that are in the scope of the action.
+ */
+ public void createSymlinks(AbstractAction action, ActionExecutionContext actionExecutionContext,
+ BinTools binTools) throws ExecException, InterruptedException {
+ List<String> args = getSpawnArgumentList(
+ actionExecutionContext.getExecutor().getExecRoot(), binTools);
+ try {
+ ResourceManager.instance().acquireResources(action, RESOURCE_SET);
+ actionExecutionContext.getExecutor().getSpawnActionContext(action.getMnemonic()).exec(
+ new BaseSpawn.Local(args, ImmutableMap.<String, String>of(), action),
+ actionExecutionContext);
+ } finally {
+ ResourceManager.instance().releaseResources(action, RESOURCE_SET);
+ }
+ }
+
+ /**
+ * Returns the complete argument list build-runfiles has to be called with.
+ */
+ private List<String> getSpawnArgumentList(Path execRoot, BinTools binTools) {
+ List<String> args = Lists.newArrayList(
+ execRoot.getRelative(binTools.getExecPath(BUILD_RUNFILES))
+ .getPathString());
+
+ if (filesetTree) {
+ args.add("--allow_relative");
+ args.add("--use_metadata");
+ }
+
+ args.add(inputManifest.getPathString());
+ args.add(symlinkTreeRoot.getPathString());
+
+ return args;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/exec/SymlinkTreeStrategy.java b/src/main/java/com/google/devtools/build/lib/exec/SymlinkTreeStrategy.java
new file mode 100644
index 0000000..d9470e4
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/exec/SymlinkTreeStrategy.java
@@ -0,0 +1,60 @@
+// Copyright 2014 Google Inc. 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.exec;
+
+import com.google.devtools.build.lib.actions.ActionExecutionContext;
+import com.google.devtools.build.lib.actions.ActionExecutionException;
+import com.google.devtools.build.lib.actions.ExecException;
+import com.google.devtools.build.lib.actions.ExecutionStrategy;
+import com.google.devtools.build.lib.actions.Executor;
+import com.google.devtools.build.lib.analysis.SymlinkTreeAction;
+import com.google.devtools.build.lib.analysis.SymlinkTreeActionContext;
+import com.google.devtools.build.lib.analysis.config.BinTools;
+
+/**
+ * Implements SymlinkTreeAction by using the output service or by running an embedded script to
+ * create the symlink tree.
+ */
+@ExecutionStrategy(contextType = SymlinkTreeActionContext.class)
+public final class SymlinkTreeStrategy implements SymlinkTreeActionContext {
+ private final OutputService outputService;
+ private final BinTools binTools;
+
+ public SymlinkTreeStrategy(OutputService outputService, BinTools binTools) {
+ this.outputService = outputService;
+ this.binTools = binTools;
+ }
+
+ @Override
+ public void createSymlinks(SymlinkTreeAction action,
+ ActionExecutionContext actionExecutionContext)
+ throws ActionExecutionException, InterruptedException {
+ Executor executor = actionExecutionContext.getExecutor();
+ try {
+ SymlinkTreeHelper helper = new SymlinkTreeHelper(
+ action.getInputManifest().getExecPath(),
+ action.getOutputManifest().getExecPath().getParentDirectory(), action.isFilesetTree());
+ if (outputService != null && outputService.canCreateSymlinkTree()) {
+ outputService.createSymlinkTree(action.getInputManifest().getPath(),
+ action.getOutputManifest().getPath(),
+ action.isFilesetTree(), helper.getSymlinkTreeRoot());
+ } else {
+ helper.createSymlinks(action, actionExecutionContext, binTools);
+ }
+ } catch (ExecException e) {
+ throw e.toActionExecutionException(
+ action.getProgressMessage(), executor.getVerboseFailures(), action);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/graph/AbstractGraphVisitor.java b/src/main/java/com/google/devtools/build/lib/graph/AbstractGraphVisitor.java
new file mode 100644
index 0000000..a08ed42
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/graph/AbstractGraphVisitor.java
@@ -0,0 +1,31 @@
+// Copyright 2014 Google Inc. 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.
+// All Rights Reserved.
+
+package com.google.devtools.build.lib.graph;
+
+/**
+ * <p> A stub implementation of GraphVisitor providing default behaviour (do
+ * nothing) for all its methods. </p>
+ */
+public class AbstractGraphVisitor<T> implements GraphVisitor<T> {
+ @Override
+ public void beginVisit() {}
+ @Override
+ public void endVisit() {}
+ @Override
+ public void visitEdge(Node<T> lhs, Node<T> rhs) {}
+ @Override
+ public void visitNode(Node<T> node) {}
+}
diff --git a/src/main/java/com/google/devtools/build/lib/graph/CollectingVisitor.java b/src/main/java/com/google/devtools/build/lib/graph/CollectingVisitor.java
new file mode 100644
index 0000000..caeb07b
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/graph/CollectingVisitor.java
@@ -0,0 +1,40 @@
+// Copyright 2014 Google Inc. 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.graph;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A graph visitor that collects the visited nodes in the order in which
+ * they were visited, and allows them to be accessed as a list.
+ */
+public class CollectingVisitor<T> extends AbstractGraphVisitor<T> {
+
+ private final List<Node<T>> order = new ArrayList<Node<T>>();
+
+ @Override
+ public void visitNode(Node<T> node) {
+ order.add(node);
+ }
+
+ /**
+ * Returns a reference to (not a copy of) the list of visited nodes in the
+ * order they were visited.
+ */
+ public List<Node<T>> getVisitedNodes() {
+ return order;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/graph/DFS.java b/src/main/java/com/google/devtools/build/lib/graph/DFS.java
new file mode 100644
index 0000000..37cd30e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/graph/DFS.java
@@ -0,0 +1,118 @@
+// Copyright 2014 Google Inc. 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.graph;
+
+import com.google.common.collect.Lists;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * <p> The DFS class encapsulates a depth-first search visitation, including
+ * the order in which nodes are to be visited relative to their successors
+ * (PREORDER/POSTORDER), whether the forward or transposed graph is to be
+ * used, and which nodes have been seen already. </p>
+ *
+ * <p> A variety of common uses of DFS are offered through methods of
+ * Digraph; however clients can use this class directly for maximum
+ * flexibility. See the implementation of
+ * Digraph.getStronglyConnectedComponents() for an example. </p>
+ *
+ * <p> Clients should not modify the enclosing Digraph instance of a DFS
+ * while a traversal is in progress. </p>
+ */
+public class DFS<T> {
+
+ // (Preferred over a boolean to avoid parameter confusion.)
+ public enum Order {
+ PREORDER,
+ POSTORDER
+ }
+
+ private final Order order; // = (PREORDER|POSTORDER)
+
+ private final Comparator<Node<T>> edgeOrder;
+
+ private final boolean transpose;
+
+ private final Set<Node<T>> marked = new HashSet<Node<T>>();
+
+ /**
+ * Constructs a DFS instance for searching over the enclosing Digraph
+ * instance, using the specified visitation parameters.
+ *
+ * @param order PREORDER or POSTORDER, determines node visitation order
+ * @param edgeOrder an ordering in which the edges originating from the same
+ * node should be visited (if null, the order is unspecified)
+ * @param transpose iff true, the graph is implicitly transposed during
+ * visitation.
+ */
+ public DFS(Order order, final Comparator<T> edgeOrder, boolean transpose) {
+ this.order = order;
+ this.transpose = transpose;
+
+ if (edgeOrder == null) {
+ this.edgeOrder = null;
+ } else {
+ this.edgeOrder = new Comparator<Node<T>>() {
+ @Override
+ public int compare(Node<T> o1, Node<T> o2) {
+ return edgeOrder.compare(o1.getLabel(), o2.getLabel());
+ }
+ };
+ }
+ }
+
+ public DFS(Order order, boolean transpose) {
+ this(order, null, transpose);
+ }
+
+ /**
+ * Returns the (immutable) set of nodes visited so far.
+ */
+ public Set<Node<T>> getMarked() {
+ return Collections.unmodifiableSet(marked);
+ }
+
+ public void visit(Node<T> node, GraphVisitor<T> visitor) {
+ if (!marked.add(node)) {
+ return;
+ }
+
+ if (order == Order.PREORDER) {
+ visitor.visitNode(node);
+ }
+
+ Collection<Node<T>> edgeTargets = transpose
+ ? node.getPredecessors() : node.getSuccessors();
+ if (edgeOrder != null) {
+ List<Node<T>> mutableNodeList = Lists.newArrayList(edgeTargets);
+ Collections.sort(mutableNodeList, edgeOrder);
+ edgeTargets = mutableNodeList;
+ }
+
+ for (Node<T> v: edgeTargets) {
+ visit(v, visitor);
+ }
+
+ if (order == Order.POSTORDER) {
+ visitor.visitNode(node);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/graph/Digraph.java b/src/main/java/com/google/devtools/build/lib/graph/Digraph.java
new file mode 100644
index 0000000..bea2f5a
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/graph/Digraph.java
@@ -0,0 +1,1063 @@
+// Copyright 2014 Google Inc. 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.graph;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Ordering;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.PriorityQueue;
+import java.util.Set;
+
+/**
+ * <p> {@code Digraph} a generic directed graph or "digraph", suitable for
+ * modeling asymmetric binary relations. </p>
+ *
+ * <p> An instance <code>G = <V,E></code> consists of a set of nodes or
+ * vertices <code>V</code>, and a set of directed edges <code>E</code>, which
+ * is a subset of <code>V × V</code>. This permits self-edges but does
+ * not represent multiple edges between the same pair of nodes. </p>
+ *
+ * <p> Nodes may be labeled with values of any type (type parameter
+ * T). All nodes within a graph have distinct labels. The null
+ * pointer is not a valid label.</p>
+ *
+ * <p> The package supports various operations for modeling partial order
+ * relations, and supports input/output in AT&T's 'dot' format. See
+ * http://www.research.att.com/sw/tools/graphviz/. </p>
+ *
+ * <p> Some invariants: </p>
+ * <ul>
+ *
+ * <li> Each graph instances "owns" the nodes is creates. The behaviour of
+ * operations on nodes a graph does not own is undefined.
+ *
+ * <li> {@code Digraph} assumes immutability of node labels, much like {@link
+ * HashMap} assumes it for keys.
+ *
+ * <li> Mutating the underlying graph invalidates any sets and iterators backed
+ * by it.
+ *
+ * </ul>
+ *
+ * <p>Each node stores successor and predecessor adjacency sets using a
+ * representation that dynamically changes with size: small sets are stored as
+ * arrays, large sets using hash tables. This representation provides
+ * significant space and time performance improvements upon two prior versions:
+ * the earliest used only HashSets; a later version used linked lists, as
+ * described in Cormen, Leiserson & Rivest.
+ */
+public final class Digraph<T> implements Cloneable {
+
+ /**
+ * Maps labels to nodes, which are in strict 1:1 correspondence.
+ */
+ private final HashMap<T, Node<T>> nodes = Maps.newHashMap();
+
+ /**
+ * A source of unique, deterministic hashCodes for {@link Node} instances.
+ */
+ private int nextHashCode = 0;
+
+ /**
+ * Construct an empty Digraph.
+ */
+ public Digraph() {}
+
+ /**
+ * Sanity-check: assert that a node is indeed a member of this graph and not
+ * another one. Perform this check whenever a function is supplied a node by
+ * the user.
+ */
+ private final void checkNode(Node<T> node) {
+ if (getNode(node.getLabel()) != node) {
+ throw new IllegalArgumentException("node " + node
+ + " is not a member of this graph");
+ }
+ }
+
+ /**
+ * Adds a directed edge between the nodes labelled 'from' and 'to', creating
+ * them if necessary.
+ *
+ * @return true iff the edge was not already present.
+ */
+ public boolean addEdge(T from, T to) {
+ Node<T> fromNode = createNode(from);
+ Node<T> toNode = createNode(to);
+ return addEdge(fromNode, toNode);
+ }
+
+ /**
+ * Adds a directed edge between the specified nodes, which must exist and
+ * belong to this graph.
+ *
+ * @return true iff the edge was not already present.
+ *
+ * Note: multi-edges are ignored. Self-edges are permitted.
+ */
+ public boolean addEdge(Node<T> fromNode, Node<T> toNode) {
+ checkNode(fromNode);
+ checkNode(toNode);
+ boolean isNewSuccessor = fromNode.addSuccessor(toNode);
+ boolean isNewPredecessor = toNode.addPredecessor(fromNode);
+ if (isNewPredecessor != isNewSuccessor) {
+ throw new IllegalStateException();
+ }
+ return isNewSuccessor;
+ }
+
+ /**
+ * Returns true iff the graph contains an edge between the
+ * specified nodes, which must exist and belong to this graph.
+ */
+ public boolean containsEdge(Node<T> fromNode, Node<T> toNode) {
+ checkNode(fromNode);
+ checkNode(toNode);
+ // TODO(bazel-team): (2009) iterate only over the shorter of from.succs, to.preds.
+ return fromNode.getSuccessors().contains(toNode);
+ }
+
+ /**
+ * Removes the edge between the specified nodes. Idempotent: attempts to
+ * remove non-existent edges have no effect.
+ *
+ * @return true iff graph changed.
+ */
+ public boolean removeEdge(Node<T> fromNode, Node<T> toNode) {
+ checkNode(fromNode);
+ checkNode(toNode);
+ boolean changed = fromNode.removeSuccessor(toNode);
+ if (changed) {
+ toNode.removePredecessor(fromNode);
+ }
+ return changed;
+ }
+
+ /**
+ * Remove all nodes and edges.
+ */
+ public void clear() {
+ nodes.clear();
+ }
+
+ @Override
+ public String toString() {
+ return "Digraph[" + getNodeCount() + " nodes]";
+ }
+
+ @Override
+ public int hashCode() {
+ throw new UnsupportedOperationException(); // avoid nondeterminism
+ }
+
+ /**
+ * Returns true iff the two graphs are equivalent, i.e. have the same set
+ * of node labels, with the same connectivity relation.
+ *
+ * O(n^2) in the worst case, i.e. equivalence. The algorithm could be speed up by
+ * close to a factor 2 in the worst case by a more direct implementation instead
+ * of using isSubgraph twice.
+ */
+ @Override
+ public boolean equals(Object thatObject) {
+ /* If this graph is a subgraph of thatObject, then we know that thatObject is of
+ * type Digraph<?> and thatObject can be cast to this type.
+ */
+ return isSubgraph(thatObject) && ((Digraph<?>) thatObject).isSubgraph(this);
+ }
+
+ /**
+ * Returns true iff this graph is a subgraph of the argument. This means that this graph's nodes
+ * are a subset of those of the argument; moreover, for each node of this graph the set of
+ * successors is a subset of those of the corresponding node in the argument graph.
+ *
+ * This algorithm is O(n^2), but linear in the total sizes of the graphs.
+ */
+ public boolean isSubgraph(Object thatObject) {
+ if (this == thatObject) {
+ return true;
+ }
+ if (!(thatObject instanceof Digraph)) {
+ return false;
+ }
+
+ @SuppressWarnings("unchecked")
+ Digraph<T> that = (Digraph<T>) thatObject;
+ if (this.getNodeCount() > that.getNodeCount()) {
+ return false;
+ }
+ for (Node<T> n1: nodes.values()) {
+ Node<T> n2 = that.getNodeMaybe(n1.getLabel());
+ if (n2 == null) {
+ return false; // 'that' is missing a node
+ }
+
+ // Now compare the successor relations.
+ // Careful:
+ // - We can't do simple equality on the succs-sets because the
+ // nodes belong to two different graphs!
+ // - There's no need to check both predecessor and successor
+ // relations, either one is sufficient.
+ Collection<Node<T>> n1succs = n1.getSuccessors();
+ Collection<Node<T>> n2succs = n2.getSuccessors();
+ if (n1succs.size() > n2succs.size()) {
+ return false;
+ }
+ // foreach successor of n1, ensure n2 has a similarly-labeled succ.
+ for (Node<T> succ1: n1succs) {
+ Node<T> succ2 = that.getNodeMaybe(succ1.getLabel());
+ if (succ2 == null) {
+ return false;
+ }
+ if (!n2succs.contains(succ2)) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Returns a duplicate graph with the same set of node labels and the same
+ * connectivity relation. The labels themselves are not cloned.
+ */
+ @Override
+ public Digraph<T> clone() {
+ final Digraph<T> that = new Digraph<T>();
+ visitNodesBeforeEdges(new AbstractGraphVisitor<T>() {
+ @Override
+ public void visitEdge(Node<T> lhs, Node<T> rhs) {
+ that.addEdge(lhs.getLabel(), rhs.getLabel());
+ }
+ @Override
+ public void visitNode(Node<T> node) {
+ that.createNode(node.getLabel());
+ }
+ });
+ return that;
+ }
+
+ /**
+ * Returns a deterministic immutable view of the nodes of this graph.
+ */
+ public Collection<Node<T>> getNodes(final Comparator<T> comparator) {
+ Ordering<Node<T>> ordering = new Ordering<Node<T>>() {
+ @Override
+ public int compare(Node<T> o1, Node<T> o2) {
+ return comparator.compare(o1.getLabel(), o2.getLabel());
+ }
+ };
+ return ordering.immutableSortedCopy(nodes.values());
+ }
+
+ /**
+ * Returns an immutable view of the nodes of this graph.
+ *
+ * Note: we have to return Collection and not Set because values() returns
+ * one: the 'nodes' HashMap doesn't know that it is injective. :-(
+ */
+ public Collection<Node<T>> getNodes() {
+ return Collections.unmodifiableCollection(nodes.values());
+ }
+
+ /**
+ * @return the set of root nodes: those with no predecessors.
+ *
+ * NOTE: in a cyclic graph, there may be nodes that are not reachable from
+ * any "root".
+ */
+ public Set<Node<T>> getRoots() {
+ Set<Node<T>> roots = new HashSet<Node<T>>();
+ for (Node<T> node: nodes.values()) {
+ if (!node.hasPredecessors()) {
+ roots.add(node);
+ }
+ }
+ return roots;
+ }
+
+ /**
+ * @return the set of leaf nodes: those with no successors.
+ */
+ public Set<Node<T>> getLeaves() {
+ Set<Node<T>> leaves = new HashSet<Node<T>>();
+ for (Node<T> node: nodes.values()) {
+ if (!node.hasSuccessors()) {
+ leaves.add(node);
+ }
+ }
+ return leaves;
+ }
+
+ /**
+ * @return an immutable view of the set of labels of this graph's nodes.
+ */
+ public Set<T> getLabels() {
+ return Collections.unmodifiableSet(nodes.keySet());
+ }
+
+ /**
+ * Finds and returns the node with the specified label. If there is no such
+ * node, an exception is thrown. The null pointer is not a valid label.
+ *
+ * @return the node whose label is "label".
+ * @throws IllegalArgumentException if no node was found with the specified
+ * label.
+ */
+ public Node<T> getNode(T label) {
+ if (label == null) {
+ throw new NullPointerException();
+ }
+ Node<T> node = nodes.get(label);
+ if (node == null) {
+ throw new IllegalArgumentException("No such node label: " + label);
+ }
+ return node;
+ }
+
+ /**
+ * Find the node with the specified label. Returns null if it doesn't exist.
+ * The null pointer is not a valid label.
+ *
+ * @return the node whose label is "label", or null if it was not found.
+ */
+ public Node<T> getNodeMaybe(T label) {
+ if (label == null) {
+ throw new NullPointerException();
+ }
+ return nodes.get(label);
+ }
+
+ /**
+ * @return the number of nodes in the graph.
+ */
+ public int getNodeCount() {
+ return nodes.size();
+ }
+
+ /**
+ * @return the number of edges in the graph.
+ *
+ * Note: expensive! Useful when asserting against mutations though.
+ */
+ public int getEdgeCount() {
+ int edges = 0;
+ for (Node<T> node: nodes.values()) {
+ edges += node.getSuccessors().size();
+ }
+ return edges;
+ }
+
+ /**
+ * Find or create a node with the specified label. This is the <i>only</i>
+ * factory of Nodes. The null pointer is not a valid label.
+ */
+ public Node<T> createNode(T label) {
+ if (label == null) {
+ throw new NullPointerException();
+ }
+ Node<T> n = nodes.get(label);
+ if (n == null) {
+ nodes.put(label, n = new Node<T>(label, nextHashCode++));
+ }
+ return n;
+ }
+
+ /******************************************************************
+ * *
+ * Graph Algorithms *
+ * *
+ ******************************************************************/
+
+ /**
+ * These only manipulate the graph through methods defined above.
+ */
+
+ /**
+ * Returns true iff the graph is cyclic. Time: O(n).
+ */
+ public boolean isCyclic() {
+
+ // To detect cycles, we use a colored depth-first search. All nodes are
+ // initially marked white. When a node is encountered, it is marked grey,
+ // and when its descendants are completely visited, it is marked black.
+ // If a grey node is ever encountered, then there is a cycle.
+ final Object WHITE = null; // i.e. not present in nodeToColor, the default.
+ final Object GREY = new Object();
+ final Object BLACK = new Object();
+ final Map<Node<T>, Object> nodeToColor =
+ new HashMap<Node<T>, Object>(); // empty => all white
+
+ class CycleDetector { /* defining a class gives us lexical scope */
+ boolean visit(Node<T> node) {
+ nodeToColor.put(node, GREY);
+ for (Node<T> succ: node.getSuccessors()) {
+ if (nodeToColor.get(succ) == GREY) {
+ return true;
+ } else if (nodeToColor.get(succ) == WHITE) {
+ if (visit(succ)) {
+ return true;
+ }
+ }
+ }
+ nodeToColor.put(node, BLACK);
+ return false;
+ }
+ }
+
+ CycleDetector detector = new CycleDetector();
+ for (Node<T> node: nodes.values()) {
+ if (nodeToColor.get(node) == WHITE) {
+ if (detector.visit(node)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns the strong component graph of "this". That is, returns a new
+ * acyclic graph in which all strongly-connected components in the original
+ * graph have been "fused" into a single node.
+ *
+ * @return a new graph, whose node labels are sets of nodes of the
+ * original graph. (Do not get confused as to which graph each
+ * set of Nodes belongs!)
+ */
+ public Digraph<Set<Node<T>>> getStrongComponentGraph() {
+ Collection<Set<Node<T>>> sccs = getStronglyConnectedComponents();
+ Digraph<Set<Node<T>>> scGraph = createImageUnderPartition(sccs);
+ scGraph.removeSelfEdges(); // scGraph should be acyclic: no self-edges
+ return scGraph;
+ }
+
+ /**
+ * Returns a partition of the nodes of this graph into sets, each set being
+ * one strongly-connected component of the graph.
+ */
+ public Collection<Set<Node<T>>> getStronglyConnectedComponents() {
+ final List<Set<Node<T>>> sccs = new ArrayList<Set<Node<T>>>();
+ NodeSetReceiver<T> r = new NodeSetReceiver<T>() {
+ @Override
+ public void accept(Set<Node<T>> scc) {
+ sccs.add(scc);
+ }
+ };
+ SccVisitor<T> v = new SccVisitor<T>();
+ for (Node<T> node : nodes.values()) {
+ v.visit(r, node);
+ }
+ return sccs;
+ }
+
+ /**
+ * <p> Given a partition of the graph into sets of nodes, returns the image
+ * of this graph under the function which maps each node to the
+ * partition-set in which it appears. The labels of the new graph are the
+ * (immutable) sets of the partition, and the edges of the new graph are the
+ * edges of the original graph, mapped via the same function. </p>
+ *
+ * <p> Note: the resulting graph may contain self-edges. If these are not
+ * wanted, call <code>removeSelfEdges()</code>> on the result. </p>
+ *
+ * <p> Interesting special case: if the partition is the set of
+ * strongly-connected components, the result of this function is the
+ * strong-component graph. </p>
+ */
+ public Digraph<Set<Node<T>>>
+ createImageUnderPartition(Collection<Set<Node<T>>> partition) {
+
+ // Build mapping function: each node label is mapped to its equiv class:
+ Map<T, Set<Node<T>>> labelToImage =
+ new HashMap<T, Set<Node<T>>>();
+ for (Set<Node<T>> set: partition) {
+ // It's important to use immutable sets of node labels when sets are keys
+ // in a map; see ImmutableSet class for explanation.
+ Set<Node<T>> imageSet = ImmutableSet.copyOf(set);
+ for (Node<T> node: imageSet) {
+ labelToImage.put(node.getLabel(), imageSet);
+ }
+ }
+
+ if (labelToImage.size() != getNodeCount()) {
+ throw new IllegalArgumentException(
+ "createImageUnderPartition(): argument is not a partition");
+ }
+
+ return createImageUnderMapping(labelToImage);
+ }
+
+ /**
+ * Returns the image of this graph in a given function, expressed as a
+ * mapping from labels to some other domain.
+ */
+ public <IMAGE> Digraph<IMAGE>
+ createImageUnderMapping(Map<T, IMAGE> map) {
+
+ Digraph<IMAGE> imageGraph = new Digraph<IMAGE>();
+
+ for (Node<T> fromNode: nodes.values()) {
+ T fromLabel = fromNode.getLabel();
+
+ IMAGE fromImage = map.get(fromLabel);
+ if (fromImage == null) {
+ throw new IllegalArgumentException(
+ "Incomplete function: undefined for " + fromLabel);
+ }
+ imageGraph.createNode(fromImage);
+
+ for (Node<T> toNode: fromNode.getSuccessors()) {
+ T toLabel = toNode.getLabel();
+
+ IMAGE toImage = map.get(toLabel);
+ if (toImage == null) {
+ throw new IllegalArgumentException(
+ "Incomplete function: undefined for " + toLabel);
+ }
+ imageGraph.addEdge(fromImage, toImage);
+ }
+ }
+
+ return imageGraph;
+ }
+
+ /**
+ * Removes any self-edges (x,x) in this graph.
+ */
+ public void removeSelfEdges() {
+ for (Node<T> node: nodes.values()) {
+ removeEdge(node, node);
+ }
+ }
+
+ /**
+ * Finds the shortest directed path from "fromNode" to "toNode". The path is
+ * returned as an ordered list of nodes, including both endpoints. Returns
+ * null if there is no path. Uses breadth-first search. Running time is
+ * O(n).
+ */
+ public List<Node<T>> getShortestPath(Node<T> fromNode,
+ Node<T> toNode) {
+ checkNode(fromNode);
+ checkNode(toNode);
+
+ if (fromNode == toNode) {
+ return Collections.singletonList(fromNode);
+ }
+
+ Map<Node<T>, Node<T>> pathPredecessor =
+ new HashMap<Node<T>, Node<T>>();
+
+ Set<Node<T>> marked = new HashSet<Node<T>>();
+
+ LinkedList<Node<T>> queue = new LinkedList<Node<T>>();
+ queue.addLast(fromNode);
+ marked.add(fromNode);
+
+ while (queue.size() > 0) {
+ Node<T> u = queue.removeFirst();
+ for (Node<T> v: u.getSuccessors()) {
+ if (marked.add(v)) {
+ pathPredecessor.put(v, u);
+ if (v == toNode) {
+ return getPathToTreeNode(pathPredecessor, v); // found a path
+ }
+ queue.addLast(v);
+ }
+ }
+ }
+ return null; // no path
+ }
+
+ /**
+ * Given a tree (expressed as a map from each node to its parent), and a
+ * starting node, returns the path from the root of the tree to 'node' as a
+ * list.
+ */
+ private static <X> List<X> getPathToTreeNode(Map<X, X> tree, X node) {
+ List<X> path = new ArrayList<X>();
+ while (node != null) {
+ path.add(node);
+ node = tree.get(node); // get parent
+ }
+ Collections.reverse(path);
+ return path;
+ }
+
+ /**
+ * Returns the nodes of an acyclic graph in topological order
+ * [a.k.a "reverse post-order" of depth-first search.]
+ *
+ * A topological order is one such that, if (u, v) is a path in
+ * acyclic graph G, then u is before v in the topological order.
+ * In other words "tails before heads" or "roots before leaves".
+ *
+ * @return The nodes of the graph, in a topological order
+ */
+ public List<Node<T>> getTopologicalOrder() {
+ List<Node<T>> order = getPostorder();
+ Collections.reverse(order);
+ return order;
+ }
+
+ /**
+ * Returns the nodes of an acyclic graph in topological order
+ * [a.k.a "reverse post-order" of depth-first search.]
+ *
+ * A topological order is one such that, if (u, v) is a path in
+ * acyclic graph G, then u is before v in the topological order.
+ * In other words "tails before heads" or "roots before leaves".
+ *
+ * If an ordering is given, returns a specific topological order from the set
+ * of all topological orders; if no ordering given, returns an arbitrary
+ * (nondeterministic) one, but is a bit faster because no sorting needs to be
+ * done for each node.
+ *
+ * @param edgeOrder the ordering in which edges originating from the same node
+ * are visited.
+ * @return The nodes of the graph, in a topological order
+ */
+ public List<Node<T>> getTopologicalOrder(
+ Comparator<T> edgeOrder) {
+ CollectingVisitor<T> visitor = new CollectingVisitor<T>();
+ DFS<T> visitation = new DFS<T>(DFS.Order.POSTORDER, edgeOrder, false);
+ visitor.beginVisit();
+ for (Node<T> node : getNodes(edgeOrder)) {
+ visitation.visit(node, visitor);
+ }
+ visitor.endVisit();
+
+ List<Node<T>> order = visitor.getVisitedNodes();
+ Collections.reverse(order);
+ return order;
+ }
+
+ /**
+ * Returns the nodes of an acyclic graph in post-order.
+ */
+ public List<Node<T>> getPostorder() {
+ CollectingVisitor<T> collectingVisitor = new CollectingVisitor<T>();
+ visitPostorder(collectingVisitor);
+ return collectingVisitor.getVisitedNodes();
+ }
+
+ /**
+ * Returns the (immutable) set of nodes reachable from node 'n' (reflexive
+ * transitive closure).
+ */
+ public Set<Node<T>> getFwdReachable(Node<T> n) {
+ return getFwdReachable(Collections.singleton(n));
+ }
+
+ /**
+ * Returns the (immutable) set of nodes reachable from any node in {@code
+ * startNodes} (reflexive transitive closure).
+ */
+ public Set<Node<T>> getFwdReachable(Collection<Node<T>> startNodes) {
+ // This method is intentionally not static, to permit future expansion.
+ DFS<T> dfs = new DFS<T>(DFS.Order.PREORDER, false);
+ for (Node<T> n : startNodes) {
+ dfs.visit(n, new AbstractGraphVisitor<T>());
+ }
+ return dfs.getMarked();
+ }
+
+ /**
+ * Returns the (immutable) set of nodes that reach node 'n' (reflexive
+ * transitive closure).
+ */
+ public Set<Node<T>> getBackReachable(Node<T> n) {
+ return getBackReachable(Collections.singleton(n));
+ }
+
+ /**
+ * Returns the (immutable) set of nodes that reach some node in {@code
+ * startNodes} (reflexive transitive closure).
+ */
+ public Set<Node<T>> getBackReachable(Collection<Node<T>> startNodes) {
+ // This method is intentionally not static, to permit future expansion.
+ DFS<T> dfs = new DFS<T>(DFS.Order.PREORDER, true);
+ for (Node<T> n : startNodes) {
+ dfs.visit(n, new AbstractGraphVisitor<T>());
+ }
+ return dfs.getMarked();
+ }
+
+ /**
+ * Removes the node in the graph specified by the given label. Optionally,
+ * preserves the graph order (by connecting up the broken edges) or drop them
+ * all. If the specified label is not the label of any node in the graph,
+ * does nothing.
+ *
+ * @param label the label of the node to remove.
+ * @param preserveOrder if true, adds edges between the neighbours
+ * of the removed node so as to maintain the graph ordering
+ * relation between all pairs of such nodes. If false, simply
+ * discards all edges from the deleted node to its neighbours.
+ * @return true iff 'label' identifies a node (i.e. the graph was changed).
+ */
+ public boolean removeNode(T label, boolean preserveOrder) {
+ Node<T> node = getNodeMaybe(label);
+ if (node != null) {
+ removeNode(node, preserveOrder);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Removes the specified node in the graph.
+ *
+ * @param n the node to remove (must be in the graph).
+ * @param preserveOrder see removeNode(T, boolean).
+ */
+ public void removeNode(Node<T> n, boolean preserveOrder) {
+ checkNode(n);
+ for (Node<T> b: n.getSuccessors()) { // edges from n
+ // exists: n -> b
+ if (preserveOrder) {
+ for (Node<T> a: n.getPredecessors()) { // edges to n
+ // exists: a -> n
+ // beware self edges: they prevent n's deletion!
+ if (a != n && b != n) {
+ addEdge(a, b); // concurrent mod?
+ }
+ }
+ }
+ b.removePredecessor(n); // remove edge n->b in b
+ }
+ for (Node<T> a: n.getPredecessors()) { // edges to n
+ a.removeSuccessor(n); // remove edge a->n in a
+ }
+
+ n.removeAllEdges(); // remove edges b->n and a->n in n
+ Object del = nodes.remove(n.getLabel());
+ if (del != n) {
+ throw new IllegalStateException(del + " " + n);
+ }
+ }
+
+ /**
+ * Extracts the subgraph G' of this graph G, containing exactly the nodes
+ * specified by the labels in V', and preserving the original
+ * <i>transitive</i> graph relation among those nodes. </p>
+ *
+ * @param subset a subset of the labels of this graph; the resulting graph
+ * will have only the nodes with these labels.
+ */
+ public Digraph<T> extractSubgraph(final Set<T> subset) {
+ Digraph<T> subgraph = this.clone();
+ subgraph.subgraph(subset);
+ return subgraph;
+ }
+
+ /**
+ * Removes all nodes from this graph except those whose label is an element of {@code keepLabels}.
+ * Edges are added so as to preserve the <i>transitive</i> closure relation.
+ *
+ * @param keepLabels a subset of the labels of this graph; the resulting graph
+ * will have only the nodes with these labels.
+ */
+ public void subgraph(final Set<T> keepLabels) {
+ // This algorithm does the following:
+ // Let keep = nodes that have labels in keepLabels.
+ // Let toRemove = nodes \ keep. reachables = successors and predecessors of keep in nodes.
+ // reachables is the subset of nodes of remove that are an immediate neighbor of some node in
+ // keep.
+ //
+ // Removes all nodes of reachables from keepLabels.
+ // Until reachables is empty:
+ // Takes n from reachables
+ // for all s in succ(n)
+ // for all p in pred(n)
+ // add the edge (p, s)
+ // add s to reachables
+ // for all p in pred(n)
+ // add p to reachables
+ // Remove n and its edges
+ //
+ // A few adjustments are needed to do the whole computation.
+
+ final Set<Node<T>> toRemove = new HashSet<>();
+ final Set<Node<T>> keepNeighbors = new HashSet<>();
+
+ // Look for all nodes if they are to be kept or removed
+ for (Node<T> node : nodes.values()) {
+ if (keepLabels.contains(node.getLabel())) {
+ // Node is to be kept
+ keepNeighbors.addAll(node.getPredecessors());
+ keepNeighbors.addAll(node.getSuccessors());
+ } else {
+ // node is to be removed.
+ toRemove.add(node);
+ }
+ }
+
+ if (toRemove.isEmpty()) {
+ // This premature return is needed to avoid 0-size priority queue creation.
+ return;
+ }
+
+ // We use a priority queue to look for low-order nodes first so we don't propagate the high
+ // number of paths of high-order nodes making the time consumption explode.
+ // For perfect results we should reorder the set each time we add a new edge but this would
+ // be too expensive, so this is a good enough approximation.
+ final PriorityQueue<Node<T>> reachables = new PriorityQueue<>(toRemove.size(),
+ new Comparator<Node<T>>() {
+ @Override
+ public int compare(Node<T> o1, Node<T> o2) {
+ return Long.compare((long) o1.numPredecessors() * (long) o1.numSuccessors(),
+ (long) o2.numPredecessors() * (long) o2.numSuccessors());
+ }
+ });
+
+ // Construct the reachables queue with the list of successors and predecessors of keep in
+ // toRemove.
+ keepNeighbors.retainAll(toRemove);
+ reachables.addAll(keepNeighbors);
+ toRemove.removeAll(reachables);
+
+ // Remove nodes, least connected first, preserving reachability.
+ while (!reachables.isEmpty()) {
+ Node<T> node = reachables.poll();
+ for (Node<T> s : node.getSuccessors()) {
+ if (s == node) { continue; } // ignore self-edge
+
+ for (Node<T> p : node.getPredecessors()) {
+ if (p == node) { continue; } // ignore self-edge
+ addEdge(p, s);
+ }
+
+ // removes n -> s
+ s.removePredecessor(node);
+ if (toRemove.remove(s)) {
+ reachables.add(s);
+ }
+ }
+
+ for (Node<T> p : node.getPredecessors()) {
+ if (p == node) { continue; } // ignore self-edge
+ p.removeSuccessor(node);
+ if (toRemove.remove(p)) {
+ reachables.add(p);
+ }
+ }
+
+ // After the node deletion, the graph is again well-formed and the original topological order
+ // is preserved.
+ nodes.remove(node.getLabel());
+ }
+
+ // Final cleanup for non-reachable nodes.
+ for (Node<T> node : toRemove) {
+ removeNode(node, false);
+ }
+ }
+
+ private interface NodeSetReceiver<T> {
+ void accept(Set<Node<T>> nodes);
+ }
+
+ /**
+ * Find strongly connected components using path-based strong component
+ * algorithm. This has the advantage over the default method of returning
+ * the components in postorder.
+ *
+ * We visit nodes depth-first, keeping track of the order that
+ * we visit them in (preorder). Our goal is to find the smallest node (in
+ * this preorder of visitation) reachable from a given node. We keep track of the
+ * smallest node pointed to so far at the top of a stack. If we ever find an
+ * already-visited node, then if it is not already part of a component, we
+ * pop nodes from that stack until we reach this already-visited node's number
+ * or an even smaller one.
+ *
+ * Once the depth-first visitation of a node is complete, if this node's
+ * number is at the top of the stack, then it is the "first" element visited
+ * in its strongly connected component. Hence we pop all elements that were
+ * pushed onto the visitation stack and put them in a strongly connected
+ * component with this one, then send a passed-in {@link Digraph.NodeSetReceiver} this component.
+ */
+ private class SccVisitor<T> {
+ // Nodes already assigned to a strongly connected component.
+ private final Set<Node<T>> assigned = new HashSet<Node<T>>();
+ // The order each node was visited in.
+ private final Map<Node<T>, Integer> preorder = new HashMap<Node<T>, Integer>();
+ // Stack of all nodes visited whose SCC has not yet been determined. When an SCC is found,
+ // that SCC is an initial segment of this stack, and is popped off. Every time a new node is
+ // visited, it is put on this stack.
+ private final List<Node<T>> stack = new ArrayList<Node<T>>();
+ // Stack of visited indices for the first-visited nodes in each of their known-so-far
+ // strongly connected components. A node pushes its index on when it is visited. If any of
+ // its successors have already been visited and are not in an already-found strongly connected
+ // component, then, since the successor was already visited, it and this node must be part of a
+ // cycle. So every node visited since the successor is actually in the same strongly connected
+ // component. In this case, preorderStack is popped until the top is at most the successor's
+ // index.
+ //
+ // After all descendants of a node have been visited, if the top element of preorderStack is
+ // still the current node's index, then it was the first element visited of the current strongly
+ // connected component. So all nodes on {@code stack} down to the current node are in its
+ // strongly connected component. And the node's index is popped from preorderStack.
+ private final List<Integer> preorderStack = new ArrayList<Integer>();
+ // Index of node being visited.
+ private int counter = 0;
+
+ private void visit(NodeSetReceiver<T> visitor, Node<T> node) {
+ if (preorder.containsKey(node)) {
+ // This can only happen if this was a non-recursive call, and a previous
+ // visit call had already visited node.
+ return;
+ }
+ preorder.put(node, counter);
+ stack.add(node);
+ preorderStack.add(counter++);
+ int preorderLength = preorderStack.size();
+ for (Node<T> succ : node.getSuccessors()) {
+ Integer succPreorder = preorder.get(succ);
+ if (succPreorder == null) {
+ visit(visitor, succ);
+ } else {
+ // Does succ not already belong to an SCC? If it doesn't, then it
+ // must be in the same SCC as node. The "starting node" of this SCC
+ // must have been visited before succ (or is succ itself).
+ if (!assigned.contains(succ)) {
+ while (preorderStack.get(preorderStack.size() - 1) > succPreorder) {
+ preorderStack.remove(preorderStack.size() - 1);
+ }
+ }
+ }
+ }
+ if (preorderLength == preorderStack.size()) {
+ // If the length of the preorderStack is unchanged, we did not find any earlier-visited
+ // nodes that were part of a cycle with this node. So this node is the first-visited
+ // element in its strongly connected component, and we collect the component.
+ preorderStack.remove(preorderStack.size() - 1);
+ Set<Node<T>> scc = new HashSet<Node<T>>();
+ Node<T> compNode;
+ do {
+ compNode = stack.remove(stack.size() - 1);
+ assigned.add(compNode);
+ scc.add(compNode);
+ } while (!node.equals(compNode));
+ visitor.accept(scc);
+ }
+ }
+ }
+
+ /********************************************************************
+ * *
+ * Orders, traversals and visitors *
+ * *
+ ********************************************************************/
+
+ /**
+ * A visitation over all the nodes in the graph that invokes
+ * <code>visitor.visitNode()</code> for each node in a depth-first
+ * post-order: each node is visited <i>after</i> each of its successors; the
+ * order in which edges are traversed is the order in which they were added
+ * to the graph. <code>visitor.visitEdge()</code> is not called.
+ *
+ * @param startNodes the set of nodes from which to begin the visitation.
+ */
+ public void visitPostorder(GraphVisitor<T> visitor,
+ Iterable<Node<T>> startNodes) {
+ visitDepthFirst(visitor, DFS.Order.POSTORDER, false, startNodes);
+ }
+
+ /**
+ * Equivalent to {@code visitPostorder(visitor, getNodes())}.
+ */
+ public void visitPostorder(GraphVisitor<T> visitor) {
+ visitPostorder(visitor, nodes.values());
+ }
+
+ /**
+ * A visitation over all the nodes in the graph that invokes
+ * <code>visitor.visitNode()</code> for each node in a depth-first
+ * pre-order: each node is visited <i>before</i> each of its successors; the
+ * order in which edges are traversed is the order in which they were added
+ * to the graph. <code>visitor.visitEdge()</code> is not called.
+ *
+ * @param startNodes the set of nodes from which to begin the visitation.
+ */
+ public void visitPreorder(GraphVisitor<T> visitor,
+ Iterable<Node<T>> startNodes) {
+ visitDepthFirst(visitor, DFS.Order.PREORDER, false, startNodes);
+ }
+
+ /**
+ * Equivalent to {@code visitPreorder(visitor, getNodes())}.
+ */
+ public void visitPreorder(GraphVisitor<T> visitor) {
+ visitPreorder(visitor, nodes.values());
+ }
+
+ /**
+ * A visitation over all the nodes in the graph in depth-first order. See
+ * DFS constructor for meaning of 'order' and 'transpose' parameters.
+ *
+ * @param startNodes the set of nodes from which to begin the visitation.
+ */
+ public void visitDepthFirst(GraphVisitor<T> visitor,
+ DFS.Order order,
+ boolean transpose,
+ Iterable<Node<T>> startNodes) {
+ DFS<T> visitation = new DFS<T>(order, transpose);
+ visitor.beginVisit();
+ for (Node<T> node: startNodes) {
+ visitation.visit(node, visitor);
+ }
+ visitor.endVisit();
+ }
+
+ /**
+ * A visitation over the graph that visits all nodes and edges in some order
+ * such that each node is visited before any edge coming out of that node;
+ * the order is otherwise unspecified.
+ *
+ * @param startNodes the set of nodes from which to begin the visitation.
+ */
+ public void visitNodesBeforeEdges(GraphVisitor<T> visitor,
+ Iterable<Node<T>> startNodes) {
+ visitor.beginVisit();
+ for (Node<T> fromNode: startNodes) {
+ visitor.visitNode(fromNode);
+ for (Node<T> toNode: fromNode.getSuccessors()) {
+ visitor.visitEdge(fromNode, toNode);
+ }
+ }
+ visitor.endVisit();
+ }
+
+ /**
+ * Equivalent to {@code visitNodesBeforeEdges(visitor, getNodes())}.
+ */
+ public void visitNodesBeforeEdges(GraphVisitor<T> visitor) {
+ visitNodesBeforeEdges(visitor, nodes.values());
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/graph/DotOutputVisitor.java b/src/main/java/com/google/devtools/build/lib/graph/DotOutputVisitor.java
new file mode 100644
index 0000000..2d18ac2
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/graph/DotOutputVisitor.java
@@ -0,0 +1,93 @@
+// Copyright 2014 Google Inc. 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.
+// All Rights Reserved.
+
+package com.google.devtools.build.lib.graph;
+
+import java.io.PrintWriter;
+
+/**
+ * <p> An implementation of GraphVisitor for displaying graphs in dot
+ * format. </p>
+ */
+public class DotOutputVisitor<T> implements GraphVisitor<T> {
+
+ /**
+ * Constructs a dot output visitor.
+ *
+ * The visitor writes to writer 'out', and rendering node labels as
+ * strings using the specified displayer, 'disp'.
+ */
+ public DotOutputVisitor(PrintWriter out, LabelSerializer<T> disp) {
+ // assert disp != null;
+ // assert out != null;
+ this.out = out;
+ this.disp = disp;
+ }
+
+ private final LabelSerializer<T> disp;
+ protected final PrintWriter out;
+ private boolean closeAtEnd = false;
+
+ @Override
+ public void beginVisit() {
+ out.println("digraph mygraph {");
+ }
+
+ @Override
+ public void endVisit() {
+ out.println("}");
+ out.flush();
+ if (closeAtEnd) {
+ out.close();
+ }
+ }
+
+ @Override
+ public void visitEdge(Node<T> lhs, Node<T> rhs) {
+ String s_lhs = disp.serialize(lhs);
+ String s_rhs = disp.serialize(rhs);
+ out.println("\"" + s_lhs + "\" -> \"" + s_rhs + "\"");
+ }
+
+ @Override
+ public void visitNode(Node<T> node) {
+ out.println("\"" + disp.serialize(node) + "\"");
+ }
+
+ /******************************************************************
+ * *
+ * Factories *
+ * *
+ ******************************************************************/
+
+ /**
+ * Create a DotOutputVisitor for output to a writer; uses default
+ * LabelSerializer.
+ */
+ public static <U> DotOutputVisitor<U> create(PrintWriter writer) {
+ return new DotOutputVisitor<U>(writer, new DefaultLabelSerializer<U>());
+ }
+
+ /**
+ * The default implementation of LabelSerializer simply serializes
+ * each node using its toString method.
+ */
+ private static class DefaultLabelSerializer<T> implements LabelSerializer<T> {
+ @Override
+ public String serialize(Node<T> node) {
+ return node.getLabel().toString();
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/graph/DotSyntaxException.java b/src/main/java/com/google/devtools/build/lib/graph/DotSyntaxException.java
new file mode 100644
index 0000000..adf70aa
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/graph/DotSyntaxException.java
@@ -0,0 +1,34 @@
+// Copyright 2014 Google Inc. 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.
+// All Rights Reserved.
+
+package com.google.devtools.build.lib.graph;
+
+import java.io.File;
+
+/**
+ * <p> A DotSyntaxException represents a syntax error encountered while
+ * parsing a dot-format fule. Thrown by createFromDotFile if syntax errors
+ * are encountered. May also be thrown by implementations of
+ * LabelDeserializer. </p>
+ *
+ * <p> The 'file' and 'lineNumber' fields indicate location of syntax error,
+ * and are populated externally by Digraph.createFromDotFile(). </p>
+ */
+public class DotSyntaxException extends Exception {
+
+ public DotSyntaxException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/graph/GraphVisitor.java b/src/main/java/com/google/devtools/build/lib/graph/GraphVisitor.java
new file mode 100644
index 0000000..f7e0f62
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/graph/GraphVisitor.java
@@ -0,0 +1,50 @@
+// Copyright 2014 Google Inc. 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.
+// All Rights Reserved.
+
+package com.google.devtools.build.lib.graph;
+
+/**
+ * <p> An graph visitor interface; particularly useful for allowing subclasses
+ * to specify how to output a graph. The order in which node and edge
+ * callbacks are made (DFS, BFS, etc) is defined by the choice of Digraph
+ * visitation method used. </p>
+ */
+public interface GraphVisitor<T> {
+
+ /**
+ * Called before visitation commences.
+ */
+ void beginVisit();
+
+ /**
+ * Called after visitation is complete.
+ */
+ void endVisit();
+
+ /**
+ * <p> Called for each edge. </p>
+ *
+ * TODO(bazel-team): This method is not essential, and in all known cases so
+ * far, the visitEdge code can always be placed within visitNode. Perhaps
+ * we should remove it, and the begin/end methods, and make this just a
+ * NodeVisitor? Are there any algorithms for which edge-visitation order is
+ * important?
+ */
+ void visitEdge(Node<T> lhs, Node<T> rhs);
+ /**
+ * Called for each node.
+ */
+ void visitNode(Node<T> node);
+}
diff --git a/src/main/java/com/google/devtools/build/lib/graph/LabelDeserializer.java b/src/main/java/com/google/devtools/build/lib/graph/LabelDeserializer.java
new file mode 100644
index 0000000..2ae9f96
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/graph/LabelDeserializer.java
@@ -0,0 +1,38 @@
+// Copyright 2014 Google Inc. 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.
+// All Rights Reserved.
+
+package com.google.devtools.build.lib.graph;
+
+/**
+ * <p> An interface for specifying a graph label de-serialization
+ * function. </p>
+ *
+ * <p> Implementations should provide a means of mapping from the string
+ * representation to an instance of the graph label type T. </p>
+ *
+ * <p> e.g. to construct Digraph{Integer} from a String representation, the
+ * LabelDeserializer{Integer} implementation would return
+ * Integer.parseInt(rep). </p>
+ */
+public interface LabelDeserializer<T> {
+
+ /**
+ * Returns an instance of the label object (of type T)
+ * corresponding to serialized representation 'rep'.
+ *
+ * @throws DotSyntaxException if 'rep' is invalid.
+ */
+ T deserialize(String rep) throws DotSyntaxException;
+}
diff --git a/src/main/java/com/google/devtools/build/lib/graph/LabelSerializer.java b/src/main/java/com/google/devtools/build/lib/graph/LabelSerializer.java
new file mode 100644
index 0000000..7386583
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/graph/LabelSerializer.java
@@ -0,0 +1,28 @@
+// Copyright 2014 Google Inc. 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.
+// All Rights Reserved.
+
+package com.google.devtools.build.lib.graph;
+
+/**
+ * <p> An interface for specifying a user-defined serialization of graph node
+ * labels as strings. </p>
+ */
+public interface LabelSerializer<T> {
+
+ /**
+ * Returns the serialized form of the label of the specified node.
+ */
+ String serialize(Node<T> node);
+}
diff --git a/src/main/java/com/google/devtools/build/lib/graph/Matrix.java b/src/main/java/com/google/devtools/build/lib/graph/Matrix.java
new file mode 100644
index 0000000..225d3d2
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/graph/Matrix.java
@@ -0,0 +1,105 @@
+// Copyright 2014 Google Inc. 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.graph;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * <p>A simple and inefficient directed graph with the adjacency
+ * relation represented as a 2-D bit-matrix. </p>
+ *
+ * <p> Used as an adjunct to Digraph for performing certain algorithms
+ * which are more naturally implemented on this representation,
+ * e.g. transitive closure and reduction. </p>
+ *
+ * <p> Not many operations are supported. </p>
+ */
+final class Matrix<T> {
+
+ /**
+ * Constructs a square bit-matrix, initially empty, with the ith row/column
+ * corresponding to the ith element of 'labels', in iteration order.
+ *
+ * Does not retain a references to 'labels'.
+ */
+ public Matrix(Set<T> labels) {
+ this.N = labels.size();
+ this.values = new ArrayList<T>(N);
+ this.indices = new HashMap<T, Integer>();
+ this.m = new boolean[N][N];
+
+ for (T label: labels) {
+ int idx = values.size();
+ values.add(label);
+ indices.put(label, idx);
+ }
+ }
+
+ /**
+ * Constructs a matrix from the set of logical values specified. There is
+ * one row/column for each node in the graph, and the entry matrix[i,j] is
+ * set iff there is an edge in 'graph' from the node labelled values[i] to
+ * the node labelled values[j].
+ */
+ public Matrix(Digraph<T> graph) {
+ this(graph.getLabels());
+
+ for (Node<T> nfrom: graph.getNodes()) {
+ Integer ifrom = indices.get(nfrom.getLabel());
+ for (Node<T> nto: nfrom.getSuccessors()) {
+ Integer ito = indices.get(nto.getLabel());
+ m[ifrom][ito] = true;
+ }
+ }
+ }
+
+ /**
+ * The size of one side of the matrix.
+ */
+ private final int N;
+
+ /**
+ * The logical values associated with each row/column.
+ */
+ private final List<T> values;
+
+ /**
+ * The mapping from logical values to row/column index.
+ */
+ private final Map<T, Integer> indices;
+
+ /**
+ * The bit-matrix itself.
+ * m[from][to] indicates an edge from-->to.
+ */
+ private final boolean[][] m;
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ for (int ii = 0; ii < N; ++ii) {
+ for (int jj = 0; jj < N; ++jj) {
+ sb.append(m[ii][jj] ? '1' : '0');
+ }
+ sb.append(' ').append(values.get(ii)).append('\n');
+ }
+ return sb.toString();
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/graph/Node.java b/src/main/java/com/google/devtools/build/lib/graph/Node.java
new file mode 100644
index 0000000..9db2a4c
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/graph/Node.java
@@ -0,0 +1,294 @@
+// Copyright 2014 Google Inc. 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.graph;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+
+/**
+ * <p>A generic directed-graph Node class. Type parameter T is the type
+ * of the node's label.
+ *
+ * <p>Each node is identified by a label, which is unique within the graph
+ * owning the node.
+ *
+ * <p>Nodes are immutable, that is, their labels cannot be changed. However,
+ * their predecessor/successor lists are mutable.
+ *
+ * <p>Nodes cannot be created directly by clients.
+ *
+ * <p>Clients should not confuse nodes belonging to two different graphs! (Use
+ * Digraph.checkNode() to catch such errors.) There is no way to find the
+ * graph to which a node belongs; it is intentionally not represented, to save
+ * space.
+ */
+public final class Node<T> {
+
+ private static final int ARRAYLIST_THRESHOLD = 6;
+ private static final int INITIAL_HASHSET_CAPACITY = 12;
+
+ // The succs and preds set representation changes depending on its size.
+ // It is implemented using the following collections:
+ // - null for size = 0.
+ // - Collections$SingletonList for size = 1.
+ // - ArrayList(6) for size = [2..6].
+ // - HashSet(12) for size > 6.
+ // These numbers were chosen based on profiling.
+
+ private final T label;
+
+ /**
+ * A duplicate-free collection of edges from this node. May be null,
+ * indicating the empty set.
+ */
+ private Collection<Node<T>> succs = null;
+
+ /**
+ * A duplicate-free collection of edges to this node. May be null,
+ * indicating the empty set.
+ */
+ private Collection<Node<T>> preds = null;
+
+ private final int hashCode;
+
+ /**
+ * Only Digraph.createNode() can call this!
+ */
+ Node(T label, int hashCode) {
+ if (label == null) { throw new NullPointerException("label"); }
+ this.label = label;
+ this.hashCode = hashCode;
+ }
+
+ /**
+ * Returns the label for this node.
+ */
+ public T getLabel() {
+ return label;
+ }
+
+ /**
+ * Returns a duplicate-free collection of the nodes that this node links to.
+ */
+ public Collection<Node<T>> getSuccessors() {
+ if (succs == null) {
+ return Collections.emptyList();
+ } else {
+ return Collections.unmodifiableCollection(succs);
+ }
+ }
+
+ /**
+ * Equivalent to {@code !getSuccessors().isEmpty()} but possibly more
+ * efficient.
+ */
+ public boolean hasSuccessors() {
+ return succs != null;
+ }
+
+ /**
+ * Equivalent to {@code getSuccessors().size()} but possibly more efficient.
+ */
+ public int numSuccessors() {
+ return succs == null ? 0 : succs.size();
+ }
+
+ /**
+ * Removes all edges to/from this node.
+ * Private: breaks graph invariant!
+ */
+ void removeAllEdges() {
+ this.succs = null;
+ this.preds = null;
+ }
+
+ /**
+ * Returns an (unordered, possibly immutable) set of the nodes that link to
+ * this node.
+ */
+ public Collection<Node<T>> getPredecessors() {
+ if (preds == null) {
+ return Collections.emptyList();
+ } else {
+ return Collections.unmodifiableCollection(preds);
+ }
+ }
+
+ /**
+ * Equivalent to {@code getPredecessors().size()} but possibly more
+ * efficient.
+ */
+ public int numPredecessors() {
+ return preds == null ? 0 : preds.size();
+ }
+
+ /**
+ * Equivalent to {@code !getPredecessors().isEmpty()} but possibly more
+ * efficient.
+ */
+ public boolean hasPredecessors() {
+ return preds != null;
+ }
+
+ /**
+ * Adds 'value' to either the predecessor or successor set, updating the
+ * appropriate field as necessary.
+ * @return {@code true} if the set was modified; {@code false} if the set
+ * was not modified
+ */
+ private boolean add(boolean predecessorSet, Node<T> value) {
+ final Collection<Node<T>> set = predecessorSet ? preds : succs;
+ if (set == null) {
+ // null -> SingletonList
+ return updateField(predecessorSet, Collections.singletonList(value));
+ }
+ if (set.contains(value)) {
+ // already exists in this set
+ return false;
+ }
+ int previousSize = set.size();
+ if (previousSize == 1) {
+ // SingletonList -> ArrayList
+ Collection<Node<T>> newSet =
+ new ArrayList<Node<T>>(ARRAYLIST_THRESHOLD);
+ newSet.addAll(set);
+ newSet.add(value);
+ return updateField(predecessorSet, newSet);
+ } else if (previousSize < ARRAYLIST_THRESHOLD) {
+ // ArrayList
+ set.add(value);
+ return true;
+ } else if (previousSize == ARRAYLIST_THRESHOLD) {
+ // ArrayList -> HashSet
+ Collection<Node<T>> newSet =
+ new HashSet<Node<T>>(INITIAL_HASHSET_CAPACITY);
+ newSet.addAll(set);
+ newSet.add(value);
+ return updateField(predecessorSet, newSet);
+ } else {
+ // HashSet
+ set.add(value);
+ return true;
+ }
+ }
+
+ /**
+ * Removes 'value' from either 'preds' or 'succs', updating the appropriate
+ * field as necessary.
+ * @return {@code true} if the set was modified; {@code false} if the set
+ * was not modified
+ */
+ private boolean remove(boolean predecessorSet, Node<T> value) {
+ final Collection<Node<T>> set = predecessorSet ? preds : succs;
+ if (set == null) {
+ // null
+ return false;
+ }
+
+ int previousSize = set.size();
+ if (previousSize == 1) {
+ if (set.contains(value)) {
+ // -> null
+ return updateField(predecessorSet, null);
+ } else {
+ return false;
+ }
+ }
+ // now remove the value
+ if (set.remove(value)) {
+ // may need to change representation
+ if (previousSize == 2) {
+ // -> SingletonList
+ List<Node<T>> list =
+ Collections.singletonList(set.iterator().next());
+ return updateField(predecessorSet, list);
+
+ } else if (previousSize == 1 + ARRAYLIST_THRESHOLD) {
+ // -> ArrayList
+ Collection<Node<T>> newSet =
+ new ArrayList<Node<T>>(ARRAYLIST_THRESHOLD);
+ newSet.addAll(set);
+ return updateField(predecessorSet, newSet);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Update either the {@link #preds} or {@link #succs} field to point to the
+ * new set.
+ * @return {@code true}, because the set must have been updated
+ */
+ private boolean updateField(boolean predecessorSet,
+ Collection<Node<T>> newSet) {
+ if (predecessorSet) {
+ preds = newSet;
+ } else {
+ succs = newSet;
+ }
+ return true;
+ }
+
+
+ /**
+ * Add 'to' as a successor of 'this' node. Returns true iff
+ * the graph changed. Private: breaks graph invariant!
+ */
+ boolean addSuccessor(Node<T> to) {
+ return add(false, to);
+ }
+
+ /**
+ * Add 'from' as a predecessor of 'this' node. Returns true iff
+ * the graph changed. Private: breaks graph invariant!
+ */
+ boolean addPredecessor(Node<T> from) {
+ return add(true, from);
+ }
+
+ /**
+ * Remove edge: fromNode.succs = {n | n in fromNode.succs && n != toNode}
+ * Private: breaks graph invariant!
+ */
+ boolean removeSuccessor(Node<T> to) {
+ return remove(false, to);
+ }
+
+ /**
+ * Remove edge: toNode.preds = {n | n in toNode.preds && n != fromNode}
+ * Private: breaks graph invariant!
+ */
+ boolean removePredecessor(Node<T> from) {
+ return remove(true, from);
+ }
+
+ @Override
+ public String toString() {
+ return "node:" + label;
+ }
+
+ @Override
+ public int hashCode() {
+ return hashCode; // Fast, deterministic.
+ }
+
+ @Override
+ public boolean equals(Object that) {
+ return this == that; // Nodes are unique for a given label
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/AbstractAttributeMapper.java b/src/main/java/com/google/devtools/build/lib/packages/AbstractAttributeMapper.java
new file mode 100644
index 0000000..20b9304
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/AbstractAttributeMapper.java
@@ -0,0 +1,212 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.syntax.Label;
+
+import javax.annotation.Nullable;
+
+/**
+ * Base {@link AttributeMap} implementation providing direct, unmanipulated access to
+ * underlying attribute data as stored within the Rule.
+ *
+ * <p>Any instantiable subclass should define a clear policy of what it does with this
+ * data before exposing it to consumers.
+ */
+public abstract class AbstractAttributeMapper implements AttributeMap {
+
+ private final Package pkg;
+ private final RuleClass ruleClass;
+ private final Label ruleLabel;
+ private final AttributeContainer attributes;
+
+ public AbstractAttributeMapper(Package pkg, RuleClass ruleClass, Label ruleLabel,
+ AttributeContainer attributes) {
+ this.pkg = pkg;
+ this.ruleClass = ruleClass;
+ this.ruleLabel = ruleLabel;
+ this.attributes = attributes;
+ }
+
+ @Override
+ public String getName() {
+ return ruleLabel.getName();
+ }
+
+ @Override
+ public Label getLabel() {
+ return ruleLabel;
+ }
+
+ @Nullable
+ @Override
+ public <T> T get(String attributeName, Type<T> type) {
+ int index = getIndexWithTypeCheck(attributeName, type);
+ Object value = attributes.getAttributeValue(index);
+ if (value instanceof Attribute.ComputedDefault) {
+ value = ((Attribute.ComputedDefault) value).getDefault(this);
+ }
+ return type.cast(value);
+ }
+
+ /**
+ * Returns the given attribute if it's a computed default, null otherwise.
+ *
+ * @throws IllegalArgumentException if the given attribute doesn't exist with the specified
+ * type. This happens whether or not it's a computed default.
+ */
+ protected <T> Attribute.ComputedDefault getComputedDefault(String attributeName, Type<T> type) {
+ int index = getIndexWithTypeCheck(attributeName, type);
+ Object value = attributes.getAttributeValue(index);
+ if (value instanceof Attribute.ComputedDefault) {
+ return (Attribute.ComputedDefault) value;
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public Iterable<String> getAttributeNames() {
+ ImmutableList.Builder<String> names = ImmutableList.builder();
+ for (Attribute a : ruleClass.getAttributes()) {
+ names.add(a.getName());
+ }
+ return names.build();
+ }
+
+ @Nullable
+ @Override
+ public Type<?> getAttributeType(String attrName) {
+ Attribute attr = getAttributeDefinition(attrName);
+ return attr == null ? null : attr.getType();
+ }
+
+ @Nullable
+ @Override
+ public Attribute getAttributeDefinition(String attrName) {
+ return ruleClass.getAttributeByNameMaybe(attrName);
+ }
+
+ @Override
+ public boolean isAttributeValueExplicitlySpecified(String attributeName) {
+ return attributes.isAttributeValueExplicitlySpecified(attributeName);
+ }
+
+ @Override
+ public String getPackageDefaultHdrsCheck() {
+ return pkg.getDefaultHdrsCheck();
+ }
+
+ @Override
+ public Boolean getPackageDefaultObsolete() {
+ return pkg.getDefaultObsolete();
+ }
+
+ @Override
+ public Boolean getPackageDefaultTestOnly() {
+ return pkg.getDefaultTestOnly();
+ }
+
+ @Override
+ public String getPackageDefaultDeprecation() {
+ return pkg.getDefaultDeprecation();
+ }
+
+ @Override
+ public ImmutableList<String> getPackageDefaultCopts() {
+ return pkg.getDefaultCopts();
+ }
+
+ @Override
+ public void visitLabels(AcceptsLabelAttribute observer) {
+ for (Attribute attribute : ruleClass.getAttributes()) {
+ Type<?> type = attribute.getType();
+ // TODO(bazel-team): This is incoherent: we shouldn't have to special-case these types
+ // for our visitation policy (e.g., why is Type.NODEP_LABEL_LIST excluded but not
+ // Type.NODEP_LABEL?). But this is the semantics the calling code requires. Audit
+ // exactly which calling code expects what and clean up this interface.
+ if (type == Type.OUTPUT || type == Type.OUTPUT_LIST || type == Type.NODEP_LABEL_LIST) {
+ continue;
+ }
+ for (Object value : visitAttribute(attribute.getName(), type)) {
+ if (value == null) {
+ // This is particularly possible for computed defaults.
+ continue;
+ }
+ for (Label label : type.getLabels(value)) {
+ observer.acceptLabelAttribute(label, attribute);
+ }
+ }
+ }
+ }
+
+ /**
+ * Implementations should provide policy-appropriate mappings when an attribute is requested in
+ * the context of a rule visitation.
+ */
+ protected abstract <T> Iterable<T> visitAttribute(String attributeName, Type<T> type);
+
+ /**
+ * Returns a {@link Type.Selector} for the given attribute if the attribute is configurable
+ * for this rule, null otherwise.
+ *
+ * @return a {@link Type.Selector} if the attribute takes the form
+ * "attrName = { 'a': value1_of_type_T, 'b': value2_of_type_T }") for this rule, null
+ * if it takes the form "attrName = value_of_type_T", null if it doesn't exist
+ * @throws IllegalArgumentException if the attribute is configurable but of the wrong type
+ */
+ @Nullable
+ protected <T> Type.Selector<T> getSelector(String attributeName, Type<T> type) {
+ Integer index = ruleClass.getAttributeIndex(attributeName);
+ if (index == null) {
+ return null;
+ }
+ Object attrValue = attributes.getAttributeValue(index);
+ if (!(attrValue instanceof Type.Selector<?>)) {
+ return null;
+ }
+ if (((Type.Selector<?>) attrValue).getOriginalType() != type) {
+ throw new IllegalArgumentException("Attribute " + attributeName
+ + " is not of type " + type + " in rule " + ruleLabel.getName());
+ }
+ return (Type.Selector<T>) attrValue;
+ }
+
+ /**
+ * Returns the index of the specified attribute, if its type is 'type'. Throws
+ * an exception otherwise.
+ */
+ private int getIndexWithTypeCheck(String attrName, Type<?> type) {
+ Integer index = ruleClass.getAttributeIndex(attrName);
+ if (index == null) {
+ throw new IllegalArgumentException("No such attribute " + attrName
+ + " in rule " + ruleLabel.getName());
+ }
+ Attribute attr = ruleClass.getAttribute(index);
+ if (attr.getType() != type) {
+ throw new IllegalArgumentException("Attribute " + attrName
+ + " is not of type " + type + " in rule " + ruleLabel.getName());
+ }
+ return index;
+ }
+
+ /**
+ * Helper routine that just checks the given attribute has the given type for this rule and
+ * throws an IllegalException if not.
+ */
+ protected void checkType(String attrName, Type<?> type) {
+ getIndexWithTypeCheck(attrName, type);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/AggregatingAttributeMapper.java b/src/main/java/com/google/devtools/build/lib/packages/AggregatingAttributeMapper.java
new file mode 100644
index 0000000..2aef224
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/AggregatingAttributeMapper.java
@@ -0,0 +1,218 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.syntax.Label;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nullable;
+
+/**
+ * {@link AttributeMap} implementation that provides the ability to retrieve *all possible*
+ * values an attribute might take.
+ */
+public class AggregatingAttributeMapper extends AbstractAttributeMapper {
+
+ /**
+ * Store for all of this rule's attributes that are non-configurable. These are
+ * unconditionally available to computed defaults no matter what dependencies
+ * they've declared.
+ */
+ private final List<String> nonconfigurableAttributes;
+
+ private AggregatingAttributeMapper(Rule rule) {
+ super(rule.getPackage(), rule.getRuleClassObject(), rule.getLabel(),
+ rule.getAttributeContainer());
+
+ ImmutableList.Builder<String> nonconfigurableAttributesBuilder = ImmutableList.builder();
+ for (Attribute attr : rule.getAttributes()) {
+ if (!attr.isConfigurable()) {
+ nonconfigurableAttributesBuilder.add(attr.getName());
+ }
+ }
+ nonconfigurableAttributes = nonconfigurableAttributesBuilder.build();
+ }
+
+ public static AggregatingAttributeMapper of(Rule rule) {
+ return new AggregatingAttributeMapper(rule);
+ }
+
+ /**
+ * Override that also visits the rule's configurable attribute keys (which are
+ * themselves labels).
+ */
+ @Override
+ public void visitLabels(AcceptsLabelAttribute observer) {
+ super.visitLabels(observer);
+ for (String attrName : getAttributeNames()) {
+ Attribute attribute = getAttributeDefinition(attrName);
+ Type.Selector<?> selector = getSelector(attrName, attribute.getType());
+ if (selector != null) {
+ for (Label configLabel : selector.getEntries().keySet()) {
+ if (!Type.Selector.isReservedLabel(configLabel)) {
+ observer.acceptLabelAttribute(configLabel, attribute);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns a list of all possible values an attribute can take for this rule.
+ */
+ @Override
+ public <T> Iterable<T> visitAttribute(String attributeName, Type<T> type) {
+ // If this attribute value is configurable, visit all possible values.
+ Type.Selector<T> selector = getSelector(attributeName, type);
+ if (selector != null) {
+ ImmutableList.Builder<T> builder = ImmutableList.builder();
+ for (Map.Entry<Label, T> entry : selector.getEntries().entrySet()) {
+ builder.add(entry.getValue());
+ }
+ return builder.build();
+ }
+
+ // If this attribute is a computed default, feed it all possible value combinations of
+ // its declared dependencies and return all computed results. For example, if this default
+ // uses attributes x and y, x can configurably be x1 or x2, and y can configurably be y1
+ // or y1, then compute default values for the (x1,y1), (x1,y2), (x2,y1), and (x2,y2) cases.
+ Attribute.ComputedDefault computedDefault = getComputedDefault(attributeName, type);
+ if (computedDefault != null) {
+ // This will hold every (value1, value2, ..) combination of the declared dependencies.
+ List<Map<String, Object>> depMaps = new LinkedList<>();
+ // Collect those combinations.
+ mapDepsForComputedDefault(computedDefault.dependencies(), depMaps,
+ ImmutableMap.<String, Object>of());
+ List<T> possibleValues = new ArrayList<>(); // Not ImmutableList.Builder: values may be null.
+ // For each combination, call getDefault on a specialized AttributeMap providing those values.
+ for (Map<String, Object> depMap : depMaps) {
+ possibleValues.add(type.cast(computedDefault.getDefault(mapBackedAttributeMap(depMap))));
+ }
+ return possibleValues;
+ }
+
+ // For any other attribute, just return its direct value.
+ T value = get(attributeName, type);
+ return value == null ? ImmutableList.<T>of() : ImmutableList.of(value);
+ }
+
+ /**
+ * Given (possibly configurable) attributes that a computed default depends on, creates an
+ * {attrName -> attrValue} map for every possible combination of those attribute values and
+ * returns a list of all the maps. This defines the complete dependency space that can affect
+ * the computed default's values.
+ *
+ * <p>For example, given dependencies x and y, which might respectively have values x1, x2 and
+ * y1, y2, this returns:
+ * <pre>
+ * [
+ * {x: x1, y: y1},
+ * {x: x1, y: y2},
+ * {x: x2, y: y1},
+ * {x: x2, y: y2}
+ * ]
+ * </pre>
+ *
+ * @param depAttributes the names of the attributes this computed default depends on
+ * @param mappings the list of {attrName --> attrValue} maps defining the computed default's
+ * dependency space. This is where this method's results are written.
+ * @param currentMap a (possibly non-empty) map to add {attrName --> attrValue}
+ * entries to. Outside callers can just pass in an empty map.
+ */
+ private void mapDepsForComputedDefault(List<String> depAttributes,
+ List<Map<String, Object>> mappings, Map<String, Object> currentMap) {
+ // Because this method uses exponential time/space on the number of inputs, keep the
+ // maximum number of inputs conservatively small.
+ Preconditions.checkState(depAttributes.size() <= 2);
+
+ if (depAttributes.isEmpty()) {
+ // Recursive base case: store whatever's already been populated in currentMap.
+ mappings.add(currentMap);
+ return;
+ }
+
+ // Take the first attribute in the dependency list and iterate over all its values. For each
+ // value x, copy currentMap with the additional entry { firstAttrName: x }, then feed
+ // this recursively into a subcall over all remaining dependencies. This recursively
+ // continues until we run out of values.
+ String firstAttribute = depAttributes.get(0);
+ for (Object value : visitAttribute(firstAttribute, getAttributeType(firstAttribute))) {
+ Map<String, Object> newMap = new HashMap<>();
+ newMap.putAll(currentMap);
+ newMap.put(firstAttribute, value);
+ mapDepsForComputedDefault(depAttributes.subList(1, depAttributes.size()), mappings, newMap);
+ }
+ }
+
+ /**
+ * A custom {@link AttributeMap} that reads attribute values from the given Map. All
+ * non-configurable attributes are also readable. Any attempt to read an attribute
+ * that's not in one of these two cases triggers an IllegalArgumentException.
+ */
+ private AttributeMap mapBackedAttributeMap(final Map<String, Object> directMap) {
+ final AggregatingAttributeMapper owner = AggregatingAttributeMapper.this;
+ return new AttributeMap() {
+
+ @Override
+ public <T> T get(String attributeName, Type<T> type) {
+ owner.checkType(attributeName, type);
+ if (nonconfigurableAttributes.contains(attributeName)) {
+ return owner.get(attributeName, type);
+ }
+ if (!directMap.containsKey(attributeName)) {
+ throw new IllegalArgumentException("attribute \"" + attributeName
+ + "\" isn't available in this computed default context");
+ }
+ return type.cast(directMap.get(attributeName));
+ }
+
+ @Override public String getName() { return owner.getName(); }
+ @Override public Label getLabel() { return owner.getLabel(); }
+ @Override public Iterable<String> getAttributeNames() {
+ return ImmutableList.<String>builder()
+ .addAll(directMap.keySet()).addAll(nonconfigurableAttributes).build();
+ }
+ @Override
+ public void visitLabels(AcceptsLabelAttribute observer) { owner.visitLabels(observer); }
+ @Override
+ public String getPackageDefaultHdrsCheck() { return owner.getPackageDefaultHdrsCheck(); }
+ @Override
+ public Boolean getPackageDefaultObsolete() { return owner.getPackageDefaultObsolete(); }
+ @Override
+ public Boolean getPackageDefaultTestOnly() { return owner.getPackageDefaultTestOnly(); }
+ @Override
+ public String getPackageDefaultDeprecation() { return owner.getPackageDefaultDeprecation(); }
+ @Override
+ public ImmutableList<String> getPackageDefaultCopts() {
+ return owner.getPackageDefaultCopts();
+ }
+ @Nullable @Override
+ public Type<?> getAttributeType(String attrName) { return owner.getAttributeType(attrName); }
+ @Nullable @Override public Attribute getAttributeDefinition(String attrName) {
+ return owner.getAttributeDefinition(attrName);
+ }
+ @Override public boolean isAttributeValueExplicitlySpecified(String attributeName) {
+ return owner.isAttributeValueExplicitlySpecified(attributeName);
+ }
+ };
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/AnalysisIssues.java b/src/main/java/com/google/devtools/build/lib/packages/AnalysisIssues.java
new file mode 100644
index 0000000..22c02b5
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/AnalysisIssues.java
@@ -0,0 +1,109 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+/**
+ * Checked exception for analysis-time errors, which can store the errors for later reporting.
+ *
+ * <p>It's more robust for a method to throw this exception than expecting a
+ * {@link RuleErrorConsumer} object (which may be null).
+ */
+public final class AnalysisIssues extends Exception {
+
+ /**
+ * An error entry.
+ *
+ * <p>{@link AnalysisIssues} can accumulate multiple of these, and report all of them at once.
+ */
+ public static final class Entry {
+ private final String attribute;
+ private final String messageTemplate;
+ private final Object[] arguments;
+
+ private Entry(@Nullable String attribute, String messageTemplate, Object... arguments) {
+ this.attribute = attribute;
+ this.messageTemplate = messageTemplate;
+ this.arguments = arguments;
+ }
+
+ private void reportTo(RuleErrorConsumer errors) {
+ String msg = String.format(messageTemplate, arguments);
+ if (attribute == null) {
+ errors.ruleError(msg);
+ } else {
+ errors.attributeError(attribute, msg);
+ }
+ }
+
+ @Override
+ public String toString() {
+ if (attribute == null) {
+ return String.format("ERROR: " + messageTemplate, arguments);
+ } else {
+ List<Object> args = new ArrayList<>();
+ args.add(attribute);
+ args.addAll(Arrays.asList(arguments));
+ return String.format("ERROR in '%s': " + messageTemplate, args.toArray());
+ }
+ }
+ }
+
+ private final ImmutableList<Entry> entries;
+
+ public AnalysisIssues(Entry entry) {
+ this.entries = ImmutableList.of(Preconditions.checkNotNull(entry));
+ }
+
+ public AnalysisIssues(Collection<Entry> entries) {
+ this.entries = ImmutableList.copyOf(Preconditions.checkNotNull(entries));
+ }
+
+ /**
+ * Creates a attribute error entry that will be added to a {@link AnalysisIssues} later.
+ */
+ public static Entry attributeError(String attribute, String messageTemplate,
+ Object... arguments) {
+ return new Entry(attribute, messageTemplate, arguments);
+ }
+
+ public static Entry ruleError(String messageTemplate, Object... arguments) {
+ return new Entry(null, messageTemplate, arguments);
+ }
+
+ /**
+ * Report all accumulated errors and warnings to the given consumer object.
+ */
+ public void reportTo(RuleErrorConsumer errors) {
+ Preconditions.checkNotNull(errors);
+ for (Entry e : entries) {
+ e.reportTo(errors);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "Errors during analysis:\n" + Joiner.on("\n").join(entries);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/AspectDefinition.java b/src/main/java/com/google/devtools/build/lib/packages/AspectDefinition.java
new file mode 100644
index 0000000..e1b5f05
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/AspectDefinition.java
@@ -0,0 +1,167 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.LinkedHashMultimap;
+import com.google.common.collect.Multimap;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * The definition of an aspect (see {@link com.google.devtools.build.lib.analysis.Aspect} for more
+ * information.)
+ *
+ * <p>Contains enough information to build up the configured target graph except for the actual way
+ * to build the Skyframe node (that is the territory of
+ * {@link com.google.devtools.build.lib.view AspectFactory}). In particular:
+ * <ul>
+ * <li>The condition that must be fulfilled for an aspect to be able to operate on a configured
+ * target
+ * <li>The (implicit or late-bound) attributes of the aspect that denote dependencies the aspect
+ * itself needs (e.g. runtime libraries for a new language for protocol buffers)
+ * <li>The aspects this aspect requires from its direct dependencies
+ * </ul>
+ *
+ * <p>The way to build the Skyframe node is not here because this data needs to be accessible from
+ * the {@code .packages} package and that one requires references to the {@code .view} package.
+ */
+@Immutable
+public final class AspectDefinition {
+
+ private final String name;
+ private final ImmutableSet<Class<?>> requiredProviders;
+ private final ImmutableMap<String, Attribute> attributes;
+ private final ImmutableMultimap<String, Class<? extends AspectFactory<?, ?, ?>>> attributeAspects;
+
+ private AspectDefinition(
+ String name,
+ ImmutableSet<Class<?>> requiredProviders,
+ ImmutableMap<String, Attribute> attributes,
+ ImmutableMultimap<String, Class<? extends AspectFactory<?, ?, ?>>> attributeAspects) {
+ this.name = name;
+ this.requiredProviders = requiredProviders;
+ this.attributes = attributes;
+ this.attributeAspects = attributeAspects;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns the attributes of the aspect in the form of a String -> {@link Attribute} map.
+ *
+ * <p>All attributes are either implicit or late-bound.
+ */
+ public ImmutableMap<String, Attribute> getAttributes() {
+ return attributes;
+ }
+
+ /**
+ * Returns the set of {@link com.google.devtools.build.lib.analysis.TransitiveInfoProvider} instances
+ * that must be present on a configured target so that this aspect can be applied to it.
+ *
+ * <p>We cannot refer to that class here due to our dependency structure, so this returns a set
+ * of unconstrained class objects.
+ *
+ * <p>If a configured target does not have a required provider, the aspect is silently not created
+ * for it.
+ */
+ public ImmutableSet<Class<?>> getRequiredProviders() {
+ return requiredProviders;
+ }
+
+ /**
+ * Returns the attribute -> set of required aspects map.
+ *
+ * <p>Note that the map actually contains {@link AspectFactory}
+ * instances, except that we cannot reference that class here.
+ */
+ public ImmutableMultimap<String, Class<? extends AspectFactory<?, ?, ?>>> getAttributeAspects() {
+ return attributeAspects;
+ }
+
+ /**
+ * Builder class for {@link AspectDefinition}.
+ */
+ public static final class Builder {
+ private final String name;
+ private final Map<String, Attribute> attributes = new LinkedHashMap<>();
+ private final Set<Class<?>> requiredProviders = new LinkedHashSet<>();
+ private final Multimap<String, Class<? extends AspectFactory<?, ?, ?>>> attributeAspects =
+ LinkedHashMultimap.create();
+
+ public Builder(String name) {
+ this.name = name;
+ }
+
+ /**
+ * Asserts that this aspect can only be evaluated for rules that supply the specified provider.
+ */
+ public Builder requireProvider(Class<?> requiredProvider) {
+ this.requiredProviders.add(requiredProvider);
+ return this;
+ }
+
+ /**
+ * Tells that in order for this aspect to work, the given aspect must be computed for the
+ * direct dependencies in the attribute with the specified name on the associated configured
+ * target.
+ *
+ * <p>Note that {@code AspectFactory} instances are expected in the second argument, but we
+ * cannot reference that interface here.
+ */
+ public Builder attributeAspect(
+ String attribute, Class<? extends AspectFactory<?, ?, ?>> aspectFactory) {
+ this.attributeAspects.put(
+ Preconditions.checkNotNull(attribute), Preconditions.checkNotNull(aspectFactory));
+ return this;
+ }
+
+ /**
+ * Adds an attribute to the aspect.
+ *
+ * <p>Since aspects do not appear in BUILD files, the attribute must be either implicit
+ * (not available in the BUILD file, starting with '$') or late-bound (determined after the
+ * configuration is available, starting with ':')
+ */
+ public <TYPE> Builder add(Attribute.Builder<TYPE> attr) {
+ Attribute attribute = attr.build();
+ Preconditions.checkState(attribute.isImplicit() || attribute.isLateBound());
+ Preconditions.checkState(!attributes.containsKey(attribute.getName()),
+ "An attribute with the name '%s' already exists.", attribute.getName());
+ attributes.put(attribute.getName(), attribute);
+ return this;
+ }
+
+ /**
+ * Builds the aspect definition.
+ *
+ * <p>The builder object is reusable afterwards.
+ */
+ public AspectDefinition build() {
+ return new AspectDefinition(name, ImmutableSet.copyOf(requiredProviders),
+ ImmutableMap.copyOf(attributes), ImmutableMultimap.copyOf(attributeAspects));
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/AspectFactory.java b/src/main/java/com/google/devtools/build/lib/packages/AspectFactory.java
new file mode 100644
index 0000000..2283f1b
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/AspectFactory.java
@@ -0,0 +1,54 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+/**
+ * Creates the Skyframe node of an aspect.
+ *
+ * <p>Also has a reference to the definition of the aspect.
+ */
+public interface AspectFactory<TConfiguredTarget, TRuleContext, TAspect> {
+ /**
+ * Creates the aspect based on the configured target of the associated rule.
+ *
+ * @param base the configured target of the associated rule
+ * @param context the context of the associated configured target plus all the attributes the
+ * aspect itself has defined
+ */
+ TAspect create(TConfiguredTarget base, TRuleContext context);
+
+ /**
+ * Returns the definition of the aspect.
+ */
+ AspectDefinition getDefinition();
+
+ /**
+ * Dummy wrapper class for utility methods because interfaces cannot even have static ones.
+ */
+ public static final class Util {
+ private Util() {
+ // Should never be instantiated
+ }
+
+ public static AspectFactory create(Class<? extends AspectFactory<?, ?, ?>> clazz) {
+ // TODO(bazel-team): This should be cached somehow, because this method is invoked quite often
+ try {
+ return clazz.newInstance();
+ } catch (Exception e) {
+ throw new IllegalStateException(e);
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/Attribute.java b/src/main/java/com/google/devtools/build/lib/packages/Attribute.java
new file mode 100644
index 0000000..9a8ae61
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/Attribute.java
@@ -0,0 +1,1343 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
+import com.google.devtools.build.lib.packages.Type.ConversionException;
+import com.google.devtools.build.lib.syntax.ClassObject;
+import com.google.devtools.build.lib.syntax.ClassObject.SkylarkClassObject;
+import com.google.devtools.build.lib.syntax.EvalException;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.syntax.SkylarkCallbackFunction;
+import com.google.devtools.build.lib.util.FileType;
+import com.google.devtools.build.lib.util.FileTypeSet;
+import com.google.devtools.build.lib.util.StringUtil;
+
+import java.util.Collection;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * Metadata of a rule attribute. Contains the attribute name and type, and an
+ * default value to be used if none is provided in a rule declaration in a BUILD
+ * file. Attributes are immutable, and may be shared by more than one rule (for
+ * example, <code>foo_binary</code> and <code>foo_library</code> may share many
+ * attributes in common).
+ */
+@Immutable
+public final class Attribute implements Comparable<Attribute> {
+
+ public static final Predicate<RuleClass> ANY_RULE = Predicates.alwaysTrue();
+
+ public static final Predicate<RuleClass> NO_RULE = Predicates.alwaysFalse();
+
+ /**
+ * A configuration transition.
+ */
+ public interface Transition {
+ /**
+ * Usually, a non-existent entry in the configuration transition table indicates an error.
+ * Unfortunately, that means that we need to always build the full table. This method allows a
+ * transition to indicate that a non-existent entry indicates a self transition, i.e., that the
+ * resulting configuration is the same as the current configuration. This can simplify the code
+ * needed to set up the transition table.
+ */
+ boolean defaultsToSelf();
+ }
+
+ /**
+ * A configuration split transition; this should be used to transition to multiple configurations
+ * simultaneously. Note that the corresponding rule implementations must have special support to
+ * handle this.
+ */
+ // TODO(bazel-team): Serializability constraints?
+ public interface SplitTransition<T> extends Transition {
+ /**
+ * Return the list of {@code BuildOptions} after splitting; empty if not applicable.
+ */
+ List<T> split(T buildOptions);
+ }
+
+ /**
+ * Declaration how the configuration should change when following a label or
+ * label list attribute.
+ */
+ public enum ConfigurationTransition implements Transition {
+ /** No transition, i.e., the same configuration as the current. */
+ NONE,
+
+ /** Transition to the host configuration. */
+ HOST,
+
+ /** Transition from the target configuration to the data configuration. */
+ // TODO(bazel-team): Move this elsewhere.
+ DATA;
+
+ @Override
+ public boolean defaultsToSelf() {
+ return false;
+ }
+ }
+
+ private enum PropertyFlag {
+ MANDATORY,
+ EXECUTABLE,
+ UNDOCUMENTED,
+ TAGGABLE,
+
+ /**
+ * Whether the list attribute is order-independent and can be sorted.
+ */
+ ORDER_INDEPENDENT,
+
+ /**
+ * Whether the allowedRuleClassesForLabels or allowedFileTypesForLabels are
+ * set to custom values. If so, and the attribute is called "deps", the
+ * legacy deps checking is skipped, and the new stricter checks are used
+ * instead. For non-"deps" attributes, this allows skipping the check if it
+ * would pass anyway, as the default setting allows any rule classes and
+ * file types.
+ */
+ STRICT_LABEL_CHECKING,
+
+ /**
+ * Set for things that would cause the a compile or lint-like action to
+ * be executed when the input changes. Used by compile_one_dependency.
+ * Set for attributes like hdrs and srcs on cc_ rules or srcs on java_
+ * or py_rules. Generally not set on data/resource attributes.
+ */
+ DIRECT_COMPILE_TIME_INPUT,
+
+ /**
+ * Whether the value of the list type attribute must not be an empty list.
+ */
+ NON_EMPTY,
+
+ /**
+ * Verifies that the referenced rule produces a single artifact. Note that this check happens
+ * on a per label basis, i.e. the check happens separately for every label in a label list.
+ */
+ SINGLE_ARTIFACT,
+
+ /**
+ * Whether we perform silent ruleclass filtering of the dependencies of the label type
+ * attribute according to their rule classes. I.e. elements of the list which don't match the
+ * allowedRuleClasses predicate or not rules will be filtered out without throwing any errors.
+ * This flag is introduced to handle plugins, do not use it in other cases.
+ */
+ SILENT_RULECLASS_FILTER,
+
+ // TODO(bazel-team): This is a hack introduced because of the bad design of the original rules.
+ // Depot cleanup would be too expensive, but don't migrate this to Skylark.
+ /**
+ * Whether to perform analysis time filetype check on this label-type attribute or not.
+ * If the flag is set, we skip the check that applies the allowedFileTypes filter
+ * to generated files. Do not use this if avoidable.
+ */
+ SKIP_ANALYSIS_TIME_FILETYPE_CHECK,
+
+ /**
+ * Whether the value of the attribute should come from a given set of values.
+ */
+ CHECK_ALLOWED_VALUES,
+
+ /**
+ * Whether this attribute is opted out of "configurability", i.e. the ability to determine
+ * its value based on properties of the build configuration.
+ */
+ NONCONFIGURABLE,
+ }
+
+ // TODO(bazel-team): modify this interface to extend Predicate and have an extra error
+ // message function like AllowedValues does
+ /**
+ * A predicate-like class that determines whether an edge between two rules is valid or not.
+ */
+ public interface ValidityPredicate {
+ /**
+ * This method should return null if the edge is valid, or a suitable error message
+ * if it is not. Note that warnings are not supported.
+ */
+ String checkValid(Rule from, Rule to);
+ }
+
+ public static final ValidityPredicate ANY_EDGE =
+ new ValidityPredicate() {
+ @Override
+ public String checkValid(Rule from, Rule to) {
+ return null;
+ }
+ };
+
+ /**
+ * Using this callback function, rules can set the configuration of their dependencies during the
+ * analysis phase.
+ */
+ public interface Configurator<TConfig, TRule> {
+ TConfig apply(TRule fromRule, TConfig fromConfiguration, Attribute attribute, Target toTarget);
+ }
+
+ /**
+ * A predicate class to check if the value of the attribute comes from a predefined set.
+ */
+ public static class AllowedValueSet implements PredicateWithMessage<Object> {
+
+ private final Set<Object> allowedValues;
+
+ public AllowedValueSet(Iterable<?> values) {
+ Preconditions.checkNotNull(values);
+ Preconditions.checkArgument(!Iterables.isEmpty(values));
+ allowedValues = ImmutableSet.copyOf(values);
+ }
+
+ @Override
+ public boolean apply(Object input) {
+ return allowedValues.contains(input);
+ }
+
+ @Override
+ public String getErrorReason(Object value) {
+ return String.format("has to be one of %s instead of '%s'",
+ StringUtil.joinEnglishList(allowedValues, "or", "'"), value);
+ }
+
+ @VisibleForTesting
+ public Collection<Object> getAllowedValues() {
+ return allowedValues;
+ }
+ }
+
+ /**
+ * Creates a new attribute builder.
+ *
+ * @param name attribute name
+ * @param type attribute type
+ * @return attribute builder
+ *
+ * @param <TYPE> attribute type class
+ */
+ public static <TYPE> Attribute.Builder<TYPE> attr(String name, Type<TYPE> type) {
+ return new Builder<>(name, type);
+ }
+
+ /**
+ * A fluent builder for the {@code Attribute} instances.
+ *
+ * <p>All methods could be called only once per builder. The attribute
+ * already undocumented based on its name cannot be marked as undocumented.
+ */
+ public static class Builder <TYPE> {
+ private String name;
+ private final Type<TYPE> type;
+ private Transition configTransition = ConfigurationTransition.NONE;
+ private Predicate<RuleClass> allowedRuleClassesForLabels = Predicates.alwaysTrue();
+ private Predicate<RuleClass> allowedRuleClassesForLabelsWarning = Predicates.alwaysFalse();
+ private Configurator<?, ?> configurator = null;
+ private boolean allowedFileTypesForLabelsSet;
+ private FileTypeSet allowedFileTypesForLabels = FileTypeSet.ANY_FILE;
+ private ValidityPredicate validityPredicate = ANY_EDGE;
+ private Object value;
+ private boolean valueSet;
+ private Predicate<AttributeMap> condition;
+ private Set<PropertyFlag> propertyFlags = EnumSet.noneOf(PropertyFlag.class);
+ private PredicateWithMessage<Object> allowedValues = null;
+ private ImmutableSet<String> mandatoryProviders = ImmutableSet.<String>of();
+ private Set<Class<? extends AspectFactory<?, ?, ?>>> aspects = new LinkedHashSet<>();
+
+ /**
+ * Creates an attribute builder with given name and type. This attribute is optional, uses
+ * target configuration and has a default value the same as its type default value. This
+ * attribute will be marked as undocumented if its name starts with the dollar sign ({@code $})
+ * or colon ({@code :}).
+ *
+ * @param name attribute name
+ * @param type attribute type
+ */
+ public Builder(String name, Type<TYPE> type) {
+ this.name = Preconditions.checkNotNull(name);
+ this.type = Preconditions.checkNotNull(type);
+ if (isImplicit(name) || isLateBound(name)) {
+ setPropertyFlag(PropertyFlag.UNDOCUMENTED, "undocumented");
+ }
+ }
+
+ private Builder<TYPE> setPropertyFlag(PropertyFlag flag, String propertyName) {
+ Preconditions.checkState(!propertyFlags.contains(flag),
+ propertyName + " flag is already set");
+ propertyFlags.add(flag);
+ return this;
+ }
+
+ /**
+ * Sets the property flag of the corresponding name if exists, otherwise throws an Exception.
+ * Only meant to use from Skylark, do not use from Java.
+ */
+ public Builder<TYPE> setPropertyFlag(String propertyName) {
+ PropertyFlag flag = null;
+ try {
+ flag = PropertyFlag.valueOf(propertyName);
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("unknown attribute flag " + propertyName);
+ }
+ setPropertyFlag(flag, propertyName);
+ return this;
+ }
+
+ /**
+ * Makes the built attribute mandatory.
+ */
+ public Builder<TYPE> mandatory() {
+ return setPropertyFlag(PropertyFlag.MANDATORY, "mandatory");
+ }
+
+ /**
+ * Makes the built attribute non empty, meaning the attribute cannot have an empty list value.
+ * Only applicable for list type attributes.
+ */
+ public Builder<TYPE> nonEmpty() {
+ Preconditions.checkNotNull(type.getListElementType(),
+ "attribute '" + name + "' must be a list");
+ return setPropertyFlag(PropertyFlag.NON_EMPTY, "non_empty");
+ }
+
+ /**
+ * Makes the built attribute producing a single artifact.
+ */
+ public Builder<TYPE> singleArtifact() {
+ Preconditions.checkState((type == Type.LABEL) || (type == Type.LABEL_LIST),
+ "attribute '" + name + "' must be a label-valued type");
+ return setPropertyFlag(PropertyFlag.SINGLE_ARTIFACT, "single_artifact");
+ }
+
+ /**
+ * Forces silent ruleclass filtering on the label type attribute.
+ * This flag is introduced to handle plugins, do not use it in other cases.
+ */
+ public Builder<TYPE> silentRuleClassFilter() {
+ Preconditions.checkState((type == Type.LABEL) || (type == Type.LABEL_LIST),
+ "must be a label-valued type");
+ return setPropertyFlag(PropertyFlag.SILENT_RULECLASS_FILTER, "silent_ruleclass_filter");
+ }
+
+ /**
+ * Skip analysis time filetype check. Don't use it if avoidable.
+ */
+ public Builder<TYPE> skipAnalysisTimeFileTypeCheck() {
+ Preconditions.checkState((type == Type.LABEL) || (type == Type.LABEL_LIST),
+ "must be a label-valued type");
+ return setPropertyFlag(PropertyFlag.SKIP_ANALYSIS_TIME_FILETYPE_CHECK,
+ "skip_analysis_time_filetype_check");
+ }
+
+ /**
+ * Mark the built attribute as order-independent.
+ */
+ public Builder<TYPE> orderIndependent() {
+ Preconditions.checkNotNull(type.getListElementType(),
+ "attribute '" + name + "' must be a list");
+ return setPropertyFlag(PropertyFlag.ORDER_INDEPENDENT, "order-independent");
+ }
+
+ /**
+ * Defines the configuration transition for this attribute. Defaults to
+ * {@code NONE}.
+ */
+ public Builder<TYPE> cfg(Transition configTransition) {
+ Preconditions.checkState(this.configTransition == ConfigurationTransition.NONE,
+ "the configuration transition is already set");
+ this.configTransition = configTransition;
+ return this;
+ }
+
+ public Builder<TYPE> cfg(Configurator<?, ?> configurator) {
+ this.configurator = configurator;
+ return this;
+ }
+
+ /**
+ * Requires the attribute target to be executable; only for label or label
+ * list attributes. Defaults to {@code false}.
+ */
+ public Builder<TYPE> exec() {
+ return setPropertyFlag(PropertyFlag.EXECUTABLE, "executable");
+ }
+
+ /**
+ * Indicates that the attribute (like srcs or hdrs) should be used as an input when calculating
+ * compile_one_dependency.
+ */
+ public Builder<TYPE> direct_compile_time_input() {
+ return setPropertyFlag(PropertyFlag.DIRECT_COMPILE_TIME_INPUT,
+ "direct_compile_time_input");
+ }
+
+ /**
+ * Makes the built attribute undocumented.
+ *
+ * @param reason explanation why the attribute is undocumented. This is not
+ * used but required for documentation
+ */
+ public Builder<TYPE> undocumented(String reason) {
+ return setPropertyFlag(PropertyFlag.UNDOCUMENTED, "undocumented");
+ }
+
+ /**
+ * Sets the attribute default value. The type of the default value must
+ * match the type parameter. (e.g. list=[], integer=0, string="",
+ * label=null). The {@code defaultValue} must be immutable.
+ *
+ * <p>If defaultValue is of type Label and is a target, that target will
+ * become an implicit dependency of the Rule; we will load the target
+ * (and its dependencies) if it encounters the Rule and build the target
+ * if needs to apply the Rule.
+ */
+ public Builder<TYPE> value(TYPE defaultValue) {
+ Preconditions.checkState(!valueSet, "the default value is already set");
+ value = defaultValue;
+ valueSet = true;
+ return this;
+ }
+
+ /**
+ * See value(TYPE) above. This method is only meant for Skylark usage.
+ */
+ public Builder<TYPE> defaultValue(Object defaultValue) throws ConversionException {
+ Preconditions.checkState(!valueSet, "the default value is already set");
+ value = type.convert(defaultValue, "attribute " + name);
+ valueSet = true;
+ return this;
+ }
+
+ /**
+ * Sets the attribute default value to a computed default value - use
+ * this when the default value is a function of other attributes of the
+ * Rule. The type of the computed default value for a mandatory attribute
+ * must match the type parameter: (e.g. list=[], integer=0, string="",
+ * label=null). The {@code defaultValue} implementation must be immutable.
+ *
+ * <p>If computedDefault returns a Label that is a target, that target will
+ * become an implicit dependency of this Rule; we will load the target
+ * (and its dependencies) if it encounters the Rule and build the target if
+ * needs to apply the Rule.
+ */
+ public Builder<TYPE> value(ComputedDefault defaultValue) {
+ Preconditions.checkState(!valueSet, "the default value is already set");
+ value = defaultValue;
+ valueSet = true;
+ return this;
+ }
+
+ /**
+ * Sets the attribute default value to be late-bound, i.e., it is derived from the build
+ * configuration.
+ */
+ public Builder<TYPE> value(LateBoundDefault<?> defaultValue) {
+ Preconditions.checkState(!valueSet, "the default value is already set");
+ Preconditions.checkState(name.isEmpty() || isLateBound(name));
+ value = defaultValue;
+ valueSet = true;
+ return this;
+ }
+
+ /**
+ * Returns true if a late-bound value has been set. Useful only for Skylark.
+ */
+ public boolean hasLateBoundValue() {
+ return value != null && value instanceof LateBoundDefault;
+ }
+
+ /**
+ * Sets a condition predicate. The default value of the attribute only applies if the condition
+ * evaluates to true. If the value is explicitly provided, then this condition is ignored.
+ *
+ * <p>The condition is only evaluated if the attribute is not explicitly set, and after all
+ * explicit attributes have been set. It can generally not access default values of other
+ * attributes.
+ */
+ public Builder<TYPE> condition(Predicate<AttributeMap> condition) {
+ Preconditions.checkState(this.condition == null, "the condition is already set");
+ this.condition = condition;
+ return this;
+ }
+
+ /**
+ * Switches on the capability of an attribute to be published to the rule's
+ * tag set.
+ */
+ public Builder<TYPE> taggable() {
+ return setPropertyFlag(PropertyFlag.TAGGABLE, "taggable");
+ }
+
+ /**
+ * If this is a label or label-list attribute, then this sets the allowed
+ * rule types for the labels occurring in the attribute. If the attribute
+ * contains Labels of any other rule type, then an error is produced during
+ * the analysis phase. Defaults to allow any types.
+ *
+ * <p>This only works on a per-target basis, not on a per-file basis; with
+ * other words, it works for 'deps' attributes, but not 'srcs' attributes.
+ */
+ public Builder<TYPE> allowedRuleClasses(Iterable<String> allowedRuleClasses) {
+ return allowedRuleClasses(
+ new RuleClass.Builder.RuleClassNamePredicate(allowedRuleClasses));
+ }
+
+ /**
+ * If this is a label or label-list attribute, then this sets the allowed
+ * rule types for the labels occurring in the attribute. If the attribute
+ * contains Labels of any other rule type, then an error is produced during
+ * the analysis phase. Defaults to allow any types.
+ *
+ * <p>This only works on a per-target basis, not on a per-file basis; with
+ * other words, it works for 'deps' attributes, but not 'srcs' attributes.
+ */
+ public Builder<TYPE> allowedRuleClasses(Predicate<RuleClass> allowedRuleClasses) {
+ Preconditions.checkState((type == Type.LABEL) || (type == Type.LABEL_LIST),
+ "must be a label-valued type");
+ propertyFlags.add(PropertyFlag.STRICT_LABEL_CHECKING);
+ allowedRuleClassesForLabels = allowedRuleClasses;
+ return this;
+ }
+
+ /**
+ * If this is a label or label-list attribute, then this sets the allowed
+ * rule types for the labels occurring in the attribute. If the attribute
+ * contains Labels of any other rule type, then an error is produced during
+ * the analysis phase. Defaults to allow any types.
+ *
+ * <p>This only works on a per-target basis, not on a per-file basis; with
+ * other words, it works for 'deps' attributes, but not 'srcs' attributes.
+ */
+ public Builder<TYPE> allowedRuleClasses(String... allowedRuleClasses) {
+ return allowedRuleClasses(ImmutableSet.copyOf(allowedRuleClasses));
+ }
+
+ /**
+ * If this is a label or label-list attribute, then this sets the allowed
+ * file types for file labels occurring in the attribute. If the attribute
+ * contains labels that correspond to files of any other type, then an error
+ * is produced during the analysis phase.
+ *
+ * <p>This only works on a per-target basis, not on a per-file basis; with
+ * other words, it works for 'deps' attributes, but not 'srcs' attributes.
+ */
+ public Builder<TYPE> allowedFileTypes(FileTypeSet allowedFileTypes) {
+ Preconditions.checkState((type == Type.LABEL) || (type == Type.LABEL_LIST),
+ "must be a label-valued type");
+ propertyFlags.add(PropertyFlag.STRICT_LABEL_CHECKING);
+ allowedFileTypesForLabelsSet = true;
+ allowedFileTypesForLabels = allowedFileTypes;
+ return this;
+ }
+
+ /**
+ * Allow all files for legacy compatibility. All uses of this method should be audited and then
+ * removed. In some cases, it's correct to allow any file, but mostly the set of files should be
+ * restricted to a reasonable set.
+ */
+ public Builder<TYPE> legacyAllowAnyFileType() {
+ return allowedFileTypes(FileTypeSet.ANY_FILE);
+ }
+
+ /**
+ * If this is a label or label-list attribute, then this sets the allowed
+ * file types for file labels occurring in the attribute. If the attribute
+ * contains labels that correspond to files of any other type, then an error
+ * is produced during the analysis phase.
+ *
+ * <p>This only works on a per-target basis, not on a per-file basis; with
+ * other words, it works for 'deps' attributes, but not 'srcs' attributes.
+ */
+ public Builder<TYPE> allowedFileTypes(FileType... allowedFileTypes) {
+ return allowedFileTypes(FileTypeSet.of(allowedFileTypes));
+ }
+
+ /**
+ * If this is a label or label-list attribute, then this sets the allowed
+ * rule types with warning for the labels occurring in the attribute. If the attribute
+ * contains Labels of any other rule type (other than this or those set in
+ * allowedRuleClasses()), then a warning is produced during
+ * the analysis phase. Defaults to deny any types.
+ *
+ * <p>This only works on a per-target basis, not on a per-file basis; with
+ * other words, it works for 'deps' attributes, but not 'srcs' attributes.
+ */
+ public Builder<TYPE> allowedRuleClassesWithWarning(Collection<String> allowedRuleClasses) {
+ return allowedRuleClassesWithWarning(
+ new RuleClass.Builder.RuleClassNamePredicate(allowedRuleClasses));
+ }
+
+ /**
+ * If this is a label or label-list attribute, then this sets the allowed
+ * rule types for the labels occurring in the attribute. If the attribute
+ * contains Labels of any other rule type (other than this or those set in
+ * allowedRuleClasses()), then a warning is produced during
+ * the analysis phase. Defaults to deny any types.
+ *
+ * <p>This only works on a per-target basis, not on a per-file basis; with
+ * other words, it works for 'deps' attributes, but not 'srcs' attributes.
+ */
+ public Builder<TYPE> allowedRuleClassesWithWarning(Predicate<RuleClass> allowedRuleClasses) {
+ Preconditions.checkState((type == Type.LABEL) || (type == Type.LABEL_LIST),
+ "must be a label-valued type");
+ propertyFlags.add(PropertyFlag.STRICT_LABEL_CHECKING);
+ allowedRuleClassesForLabelsWarning = allowedRuleClasses;
+ return this;
+ }
+
+ /**
+ * If this is a label or label-list attribute, then this sets the allowed
+ * rule types for the labels occurring in the attribute. If the attribute
+ * contains Labels of any other rule type (other than this or those set in
+ * allowedRuleClasses()), then a warning is produced during
+ * the analysis phase. Defaults to deny any types.
+ *
+ * <p>This only works on a per-target basis, not on a per-file basis; with
+ * other words, it works for 'deps' attributes, but not 'srcs' attributes.
+ */
+ public Builder<TYPE> allowedRuleClassesWithWarning(String... allowedRuleClasses) {
+ return allowedRuleClassesWithWarning(ImmutableSet.copyOf(allowedRuleClasses));
+ }
+
+ /**
+ * Sets a set of mandatory Skylark providers. Every configured target occurring in
+ * this label type attribute has to provide all of these providers, otherwise an
+ * error is produces during the analysis phase for every missing provider.
+ */
+ public Builder<TYPE> mandatoryProviders(Iterable<String> providers) {
+ Preconditions.checkState((type == Type.LABEL) || (type == Type.LABEL_LIST),
+ "must be a label-valued type");
+ this.mandatoryProviders = ImmutableSet.copyOf(providers);
+ return this;
+ }
+
+ /**
+ * Asserts that a particular aspect needs to be computed for all direct dependencies through
+ * this attribute.
+ */
+ public Builder<TYPE> aspect(Class<? extends AspectFactory<?, ?, ?>> aspect) {
+ this.aspects.add(aspect);
+ return this;
+ }
+ /**
+ * Sets the predicate-like edge validity checker.
+ */
+ public Builder<TYPE> validityPredicate(ValidityPredicate validityPredicate) {
+ propertyFlags.add(PropertyFlag.STRICT_LABEL_CHECKING);
+ this.validityPredicate = validityPredicate;
+ return this;
+ }
+
+ /**
+ * The value of the attribute must be one of allowedValues.
+ */
+ public Builder<TYPE> allowedValues(PredicateWithMessage<Object> allowedValues) {
+ this.allowedValues = allowedValues;
+ propertyFlags.add(PropertyFlag.CHECK_ALLOWED_VALUES);
+ return this;
+ }
+
+ /**
+ * Makes the built attribute "non-configurable", i.e. its value cannot be influenced by
+ * the build configuration. Attributes are "configurable" unless explicitly opted out here.
+ *
+ * <p>Non-configurability indicates an exceptional state: there exists Blaze logic that needs
+ * the attribute's value, has no access to configurations, and can't apply a workaround
+ * through an appropriate {@link AbstractAttributeMapper} implementation. Scenarios like
+ * this should be as uncommon as possible, so it's important we maintain clear documentation
+ * on what causes them and why users consequently can't configure certain attributes.
+ *
+ * @param reason why this attribute can't be configurable. This isn't used by Blaze - it's
+ * solely a documentation mechanism.
+ */
+ public Builder<TYPE> nonconfigurable(String reason) {
+ Preconditions.checkState(!reason.isEmpty());
+ return setPropertyFlag(PropertyFlag.NONCONFIGURABLE, "nonconfigurable");
+ }
+
+ /**
+ * Creates the attribute. Uses name, type, optionality, configuration type
+ * and the default value configured by the builder.
+ */
+ public Attribute build() {
+ return build(this.name);
+ }
+
+ /**
+ * Creates the attribute. Uses type, optionality, configuration type
+ * and the default value configured by the builder. Use the name
+ * passed as an argument. This function is used by Skylark where the
+ * name is provided only when we build. We don't want to modify the
+ * builder, as it is shared in a multithreaded environment.
+ */
+ public Attribute build(String name) {
+ Preconditions.checkState(!name.isEmpty(), "name has not been set");
+ // TODO(bazel-team): Remove this check again, and remove all allowedFileTypes() calls.
+ if ((type == Type.LABEL) || (type == Type.LABEL_LIST)) {
+ if ((name.startsWith("$") || name.startsWith(":")) && !allowedFileTypesForLabelsSet) {
+ allowedFileTypesForLabelsSet = true;
+ allowedFileTypesForLabels = FileTypeSet.ANY_FILE;
+ }
+ if (!allowedFileTypesForLabelsSet) {
+ throw new IllegalStateException(name);
+ }
+ }
+ return new Attribute(name, type, Sets.immutableEnumSet(propertyFlags),
+ valueSet ? value : type.getDefaultValue(), configTransition, configurator,
+ allowedRuleClassesForLabels, allowedRuleClassesForLabelsWarning,
+ allowedFileTypesForLabels, allowedFileTypesForLabelsSet, validityPredicate, condition,
+ allowedValues, mandatoryProviders, ImmutableSet.copyOf(aspects));
+ }
+ }
+
+ /**
+ * A computed default is a default value for a Rule attribute that is a
+ * function of other attributes of the rule.
+ *
+ * <p>Attributes whose defaults are computed are first initialized to the default
+ * for their type, and then the computed defaults are evaluated after all
+ * non-computed defaults have been initialized. There is no defined order
+ * among computed defaults, so they must not depend on each other.
+ *
+ * <p>If a computed default reads the value of another attribute, at least one of
+ * the following must be true:
+ *
+ * <ol>
+ * <li>The other attribute must be declared in the computed default's constructor</li>
+ * <li>The other attribute must be non-configurable ({@link Builder#nonconfigurable()}</li>
+ * </ol>
+ *
+ * <p>The reason for enforced declarations is that, since attribute values might be
+ * configurable, a computed default that depends on them may itself take multiple
+ * values. Since we have no access to a target's configuration at the time these values
+ * are computed, we need the ability to probe the default's *complete* dependency space.
+ * Declared dependencies allow us to do so sanely. Non-configurable attributes don't have
+ * this problem because their value is fixed and known even without configuration information.
+ *
+ * <p>Implementations of this interface must be immutable.
+ */
+ public abstract static class ComputedDefault {
+ private final List<String> dependencies;
+ List<String> dependencies() { return dependencies; }
+
+ /**
+ * Create a computed default that can read all non-configurable attribute values and no
+ * configurable attribute values.
+ */
+ public ComputedDefault() {
+ dependencies = ImmutableList.of();
+ }
+
+ /**
+ * Create a computed default that can read all non-configurable attributes values and one
+ * explicitly specified configurable attribute value
+ */
+ public ComputedDefault(String depAttribute) {
+ dependencies = ImmutableList.of(depAttribute);
+ }
+
+ /**
+ * Create a computed default that can read all non-configurable attributes values and two
+ * explicitly specified configurable attribute values.
+ */
+ public ComputedDefault(String depAttribute1, String depAttribute2) {
+ dependencies = ImmutableList.of(depAttribute1, depAttribute2);
+ }
+
+ public abstract Object getDefault(AttributeMap rule);
+ }
+
+ /**
+ * Marker interface for late-bound values. Unfortunately, we can't refer to BuildConfiguration
+ * right now, since that is in a separate compilation unit.
+ *
+ * <p>Implementations of this interface must be immutable.
+ *
+ * <p>Use sparingly - having different values for attributes during loading and analysis can
+ * confuse users.
+ */
+ public interface LateBoundDefault<T> {
+ /**
+ * Whether to look up the label in the host configuration. This is only here for the host JDK -
+ * we usually need to look up labels in the target configuration.
+ */
+ boolean useHostConfiguration();
+
+ /**
+ * Returns the set of required configuration fragments, i.e., fragments that will be accessed by
+ * the code.
+ */
+ Set<Class<?>> getRequiredConfigurationFragments();
+
+ /**
+ * The default value for the attribute that is set during the loading phase.
+ */
+ Object getDefault();
+
+ /**
+ * The actual value for the attribute for the analysis phase, which depends on the build
+ * configuration. Note that configurations transitions are applied after the late-bound
+ * attribute was evaluated.
+ */
+ Object getDefault(Rule rule, T o) throws EvalException;
+ }
+
+ /**
+ * Abstract super class for label-typed {@link LateBoundDefault} implementations that simplifies
+ * the client code a little and makes it a bit more type-safe.
+ */
+ public abstract static class LateBoundLabel<T> implements LateBoundDefault<T> {
+ private final Label label;
+ private final ImmutableSet<Class<?>> requiredConfigurationFragments;
+
+ public LateBoundLabel() {
+ this((Label) null);
+ }
+
+ public LateBoundLabel(Label label) {
+ this.label = label;
+ this.requiredConfigurationFragments = ImmutableSet.of();
+ }
+
+ public LateBoundLabel(Label label, Class<?>... requiredConfigurationFragments) {
+ this.label = label;
+ this.requiredConfigurationFragments = ImmutableSet.copyOf(requiredConfigurationFragments);
+ }
+
+ public LateBoundLabel(String label) {
+ this(Label.parseAbsoluteUnchecked(label));
+ }
+
+ public LateBoundLabel(String label, Class<?>... requiredConfigurationFragments) {
+ this(Label.parseAbsoluteUnchecked(label), requiredConfigurationFragments);
+ }
+
+ @Override
+ public boolean useHostConfiguration() {
+ return false;
+ }
+
+ @Override
+ public ImmutableSet<Class<?>> getRequiredConfigurationFragments() {
+ return requiredConfigurationFragments;
+ }
+
+ @Override
+ public final Label getDefault() {
+ return label;
+ }
+
+ @Override
+ public abstract Label getDefault(Rule rule, T configuration);
+ }
+
+ /**
+ * Abstract super class for label-list-typed {@link LateBoundDefault} implementations that
+ * simplifies the client code a little and makes it a bit more type-safe.
+ */
+ public abstract static class LateBoundLabelList<T> implements LateBoundDefault<T> {
+ private final ImmutableList<Label> labels;
+
+ public LateBoundLabelList() {
+ this.labels = ImmutableList.of();
+ }
+
+ public LateBoundLabelList(List<Label> labels) {
+ this.labels = ImmutableList.copyOf(labels);
+ }
+
+ @Override
+ public boolean useHostConfiguration() {
+ return false;
+ }
+
+ @Override
+ public ImmutableSet<Class<?>> getRequiredConfigurationFragments() {
+ return ImmutableSet.of();
+ }
+
+ @Override
+ public final List<Label> getDefault() {
+ return labels;
+ }
+
+ @Override
+ public abstract List<Label> getDefault(Rule rule, T configuration);
+ }
+
+ /**
+ * A class for late bound attributes defined in Skylark.
+ */
+ public static final class SkylarkLateBound implements LateBoundDefault<Object> {
+
+ private final SkylarkCallbackFunction callback;
+
+ public SkylarkLateBound(SkylarkCallbackFunction callback) {
+ this.callback = callback;
+ }
+
+ @Override
+ public boolean useHostConfiguration() {
+ return false;
+ }
+
+ @Override
+ public ImmutableSet<Class<?>> getRequiredConfigurationFragments() {
+ return ImmutableSet.of();
+ }
+
+ @Override
+ public Object getDefault() {
+ return null;
+ }
+
+ @Override
+ public Object getDefault(Rule rule, Object o) throws EvalException {
+ Map<String, Object> attrValues = new HashMap<>();
+ // TODO(bazel-team): support configurable attributes here. RawAttributeMapper will throw
+ // an exception on any instance of configurable attributes.
+ AttributeMap attributes = RawAttributeMapper.of(rule);
+ for (Attribute attr : rule.getAttributes()) {
+ if (!attr.isLateBound()) {
+ Object value = attributes.get(attr.getName(), attr.getType());
+ if (value != null) {
+ attrValues.put(attr.getName(), value);
+ }
+ }
+ }
+ ClassObject attrs = new SkylarkClassObject(attrValues,
+ "No such regular (non late-bound) attribute '%s'.");
+ return callback.call(attrs, o);
+ }
+ }
+
+ private final String name;
+
+ private final Type<?> type;
+
+ private final Set<PropertyFlag> propertyFlags;
+
+ // Exactly one of these conditions is true:
+ // 1. defaultValue == null.
+ // 2. defaultValue instanceof ComputedDefault &&
+ // type.isValid(defaultValue.getDefault())
+ // 3. type.isValid(defaultValue).
+ // 4. defaultValue instanceof LateBoundDefault &&
+ // type.isValid(defaultValue.getDefault(configuration))
+ // (We assume a hypothetical Type.isValid(Object) predicate.)
+ private final Object defaultValue;
+
+ private final Transition configTransition;
+
+ private final Configurator<?, ?> configurator;
+
+ /**
+ * For label or label-list attributes, this predicate returns which rule
+ * classes are allowed for the targets in the attribute.
+ */
+ private final Predicate<RuleClass> allowedRuleClassesForLabels;
+
+ /**
+ * For label or label-list attributes, this predicate returns which rule
+ * classes are allowed for the targets in the attribute with warning.
+ */
+ private final Predicate<RuleClass> allowedRuleClassesForLabelsWarning;
+
+ /**
+ * For label or label-list attributes, this predicate returns which file
+ * types are allowed for targets in the attribute that happen to be file
+ * targets (rather than rules).
+ */
+ private final FileTypeSet allowedFileTypesForLabels;
+ private final boolean allowedFileTypesForLabelsSet;
+
+ /**
+ * This predicate-like object checks
+ * if the edge between two rules using this attribute is valid
+ * in the dependency graph. Returns null if valid, otherwise an error message.
+ */
+ private final ValidityPredicate validityPredicate;
+
+ private final Predicate<AttributeMap> condition;
+
+ private final PredicateWithMessage<Object> allowedValues;
+
+ private final ImmutableSet<String> mandatoryProviders;
+
+ private final ImmutableSet<Class<? extends AspectFactory<?, ?, ?>>> aspects;
+
+ /**
+ * Constructs a rule attribute with the specified name, type and default
+ * value.
+ *
+ * @param name the name of the attribute
+ * @param type the type of the attribute
+ * @param defaultValue the default value to use for this attribute if none is
+ * specified in rule declaration in the BUILD file. Must be null, or of
+ * type "type". May be an instance of ComputedDefault, in which case
+ * its getDefault() method must return an instance of "type", or null.
+ * Must be immutable.
+ * @param configTransition the configuration transition for this attribute
+ * (which must be of type LABEL, LABEL_LIST, NODEP_LABEL or
+ * NODEP_LABEL_LIST).
+ */
+ private Attribute(String name, Type<?> type, Set<PropertyFlag> propertyFlags,
+ Object defaultValue, Transition configTransition,
+ Configurator<?, ?> configurator,
+ Predicate<RuleClass> allowedRuleClassesForLabels,
+ Predicate<RuleClass> allowedRuleClassesForLabelsWarning,
+ FileTypeSet allowedFileTypesForLabels,
+ boolean allowedFileTypesForLabelsSet,
+ ValidityPredicate validityPredicate,
+ Predicate<AttributeMap> condition,
+ PredicateWithMessage<Object> allowedValues,
+ ImmutableSet<String> mandatoryProviders,
+ ImmutableSet<Class<? extends AspectFactory<?, ?, ?>>> aspects) {
+ Preconditions.checkNotNull(configTransition);
+ Preconditions.checkArgument(
+ (configTransition == ConfigurationTransition.NONE && configurator == null)
+ || type == Type.LABEL || type == Type.LABEL_LIST
+ || type == Type.NODEP_LABEL || type == Type.NODEP_LABEL_LIST,
+ "Configuration transitions can only be specified for label or label list attributes");
+ Preconditions.checkArgument(isLateBound(name) == (defaultValue instanceof LateBoundDefault),
+ "late bound attributes require a default value that is late bound (and vice versa): "
+ + name);
+ if (isLateBound(name)) {
+ LateBoundDefault<?> lateBoundDefault = (LateBoundDefault<?>) defaultValue;
+ Preconditions.checkArgument((configurator == null),
+ "a late bound attribute cannot specify a configurator");
+ Preconditions.checkArgument(!lateBoundDefault.useHostConfiguration()
+ || (configTransition == ConfigurationTransition.HOST),
+ "a late bound default value using the host configuration must use the host transition");
+ }
+
+ this.name = name;
+ this.type = type;
+ this.propertyFlags = propertyFlags;
+ this.defaultValue = defaultValue;
+ this.configTransition = configTransition;
+ this.configurator = configurator;
+ this.allowedRuleClassesForLabels = allowedRuleClassesForLabels;
+ this.allowedRuleClassesForLabelsWarning = allowedRuleClassesForLabelsWarning;
+ this.allowedFileTypesForLabels = allowedFileTypesForLabels;
+ this.allowedFileTypesForLabelsSet = allowedFileTypesForLabelsSet;
+ this.validityPredicate = validityPredicate;
+ this.condition = condition;
+ this.allowedValues = allowedValues;
+ this.mandatoryProviders = mandatoryProviders;
+ this.aspects = aspects;
+ }
+
+ /**
+ * Returns the name of this attribute.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns the logical type of this attribute. (May differ from the actual
+ * representation as a value in the build interpreter; for example, an
+ * attribute may logically be a list of labels, but be represented as a list
+ * of strings.)
+ */
+ public Type<?> getType() {
+ return type;
+ }
+
+ private boolean getPropertyFlag(PropertyFlag flag) {
+ return propertyFlags.contains(flag);
+ }
+
+ /**
+ * Returns true if this parameter is mandatory.
+ */
+ public boolean isMandatory() {
+ return getPropertyFlag(PropertyFlag.MANDATORY);
+ }
+
+ /**
+ * Returns true if this list parameter cannot have an empty list as a value.
+ */
+ public boolean isNonEmpty() {
+ return getPropertyFlag(PropertyFlag.NON_EMPTY);
+ }
+
+ /**
+ * Returns true if this label parameter must produce a single artifact.
+ */
+ public boolean isSingleArtifact() {
+ return getPropertyFlag(PropertyFlag.SINGLE_ARTIFACT);
+ }
+
+ /**
+ * Returns true if this label type parameter is checked by silent ruleclass filtering.
+ */
+ public boolean isSilentRuleClassFilter() {
+ return getPropertyFlag(PropertyFlag.SILENT_RULECLASS_FILTER);
+ }
+
+ /**
+ * Returns true if this label type parameter skips the analysis time filetype check.
+ */
+ public boolean isSkipAnalysisTimeFileTypeCheck() {
+ return getPropertyFlag(PropertyFlag.SKIP_ANALYSIS_TIME_FILETYPE_CHECK);
+ }
+
+ /**
+ * Returns true if this parameter is order-independent.
+ */
+ public boolean isOrderIndependent() {
+ return getPropertyFlag(PropertyFlag.ORDER_INDEPENDENT);
+ }
+
+ /**
+ * Returns the configuration transition for this attribute for label or label
+ * list attributes. For other attributes it will always return {@code NONE}.
+ */
+ public Transition getConfigurationTransition() {
+ return configTransition;
+ }
+
+ /**
+ * Returns the configurator instance for this attribute for label or label list attributes.
+ * For other attributes it will always return {@code null}.
+ */
+ public Configurator<?, ?> getConfigurator() {
+ return configurator;
+ }
+
+ /**
+ * Returns whether the target is required to be executable for label or label
+ * list attributes. For other attributes it always returns {@code false}.
+ */
+ public boolean isExecutable() {
+ return getPropertyFlag(PropertyFlag.EXECUTABLE);
+ }
+
+ /**
+ * Returns {@code true} iff the rule is a direct input for an action.
+ */
+ public boolean isDirectCompileTimeInput() {
+ return getPropertyFlag(PropertyFlag.DIRECT_COMPILE_TIME_INPUT);
+ }
+
+ /**
+ * Returns {@code true} iff this attribute requires documentation.
+ */
+ public boolean isDocumented() {
+ return !getPropertyFlag(PropertyFlag.UNDOCUMENTED);
+ }
+
+ /**
+ * Returns {@code true} iff this attribute should be published to the rule's
+ * tag set. Note that not all Type classes support tag conversion.
+ */
+ public boolean isTaggable() {
+ return getPropertyFlag(PropertyFlag.TAGGABLE);
+ }
+
+ public boolean isStrictLabelCheckingEnabled() {
+ return getPropertyFlag(PropertyFlag.STRICT_LABEL_CHECKING);
+ }
+
+ /**
+ * Returns true if the value of this attribute should be a part of a given set.
+ */
+ public boolean checkAllowedValues() {
+ return getPropertyFlag(PropertyFlag.CHECK_ALLOWED_VALUES);
+ }
+
+ /**
+ * Returns true if this attribute's value can be influenced by the build configuration.
+ */
+ public boolean isConfigurable() {
+ return !(type == Type.OUTPUT // Excluded because of Rule#populateExplicitOutputFiles.
+ || type == Type.OUTPUT_LIST
+ || getPropertyFlag(PropertyFlag.NONCONFIGURABLE));
+ }
+
+ /**
+ * Returns a predicate that evaluates to true for rule classes that are
+ * allowed labels in this attribute. If this is not a label or label-list
+ * attribute, the returned predicate always evaluates to true.
+ */
+ public Predicate<RuleClass> getAllowedRuleClassesPredicate() {
+ return allowedRuleClassesForLabels;
+ }
+
+ /**
+ * Returns a predicate that evaluates to true for rule classes that are
+ * allowed labels in this attribute with warning. If this is not a label or label-list
+ * attribute, the returned predicate always evaluates to true.
+ */
+ public Predicate<RuleClass> getAllowedRuleClassesWarningPredicate() {
+ return allowedRuleClassesForLabelsWarning;
+ }
+
+ /**
+ * Returns the set of mandatory Skylark providers.
+ */
+ public ImmutableSet<String> getMandatoryProviders() {
+ return mandatoryProviders;
+ }
+
+ public FileTypeSet getAllowedFileTypesPredicate() {
+ return allowedFileTypesForLabels;
+ }
+
+ public ValidityPredicate getValidityPredicate() {
+ return validityPredicate;
+ }
+
+ public Predicate<AttributeMap> getCondition() {
+ return condition == null ? Predicates.<AttributeMap>alwaysTrue() : condition;
+ }
+
+ public PredicateWithMessage<Object> getAllowedValues() {
+ return allowedValues;
+ }
+
+ /**
+ * Returns the set of aspects required for dependencies through this attribute.
+ */
+ public ImmutableSet<Class<? extends AspectFactory<?, ?, ?>>> getAspects() {
+ return aspects;
+ }
+
+ /**
+ * Returns the default value of this attribute in the context of the
+ * specified Rule. For attributes with a computed default, i.e. {@code
+ * hasComputedDefault()}, {@code rule} must be non-null since the result may
+ * depend on the values of its other attributes.
+ *
+ * <p>The result may be null (although this is not a value in the build
+ * language).
+ *
+ * <p>During population of the rule's attribute dictionary, all non-computed
+ * defaults must be set before all computed ones.
+ *
+ * @param rule the rule to which this attribute belongs; non-null if
+ * {@code hasComputedDefault()}; ignored otherwise.
+ */
+ public Object getDefaultValue(Rule rule) {
+ if (!getCondition().apply(rule == null ? null : NonconfigurableAttributeMapper.of(rule))) {
+ return null;
+ } else if (defaultValue instanceof LateBoundDefault<?>) {
+ return ((LateBoundDefault<?>) defaultValue).getDefault();
+ } else {
+ return defaultValue;
+ }
+ }
+
+ /**
+ * Returns the default value of this attribute, even if it has a condition, is a computed default,
+ * or a late-bound default.
+ */
+ @VisibleForTesting
+ public Object getDefaultValueForTesting() {
+ return defaultValue;
+ }
+
+ public LateBoundDefault<?> getLateBoundDefault() {
+ Preconditions.checkState(isLateBound());
+ return (LateBoundDefault<?>) defaultValue;
+ }
+
+ /**
+ * Returns true iff this attribute has a computed default or a condition.
+ *
+ * @see #getDefaultValue(Rule)
+ */
+ boolean hasComputedDefault() {
+ return (defaultValue instanceof ComputedDefault) || (condition != null);
+ }
+
+ /**
+ * Returns if this attribute is an implicit dependency according to the naming policy that
+ * designates implicit attributes.
+ */
+ public boolean isImplicit() {
+ return isImplicit(getName());
+ }
+
+ /**
+ * Returns if an attribute with the given name is an implicit dependency according to the
+ * naming policy that designates implicit attributes.
+ */
+ public static boolean isImplicit(String name) {
+ return name.startsWith("$");
+ }
+
+ /**
+ * Returns if this attribute is late-bound according to the naming policy that designates
+ * late-bound attributes.
+ */
+ public boolean isLateBound() {
+ return isLateBound(getName());
+ }
+
+ /**
+ * Returns if an attribute with the given name is late-bound according to the naming policy
+ * that designates late-bound attributes.
+ */
+ public static boolean isLateBound(String name) {
+ return name.startsWith(":");
+ }
+
+ @Override
+ public String toString() {
+ return "Attribute(" + name + ", " + type + ")";
+ }
+
+ @Override
+ public int compareTo(Attribute other) {
+ return name.compareTo(other.name);
+ }
+
+ /**
+ * Returns a replica builder of this Attribute.
+ */
+ public Attribute.Builder<?> cloneBuilder() {
+ Builder<?> builder = new Builder<>(name, this.type);
+ builder.allowedFileTypesForLabels = allowedFileTypesForLabels;
+ builder.allowedFileTypesForLabelsSet = allowedFileTypesForLabelsSet;
+ builder.allowedRuleClassesForLabels = allowedRuleClassesForLabels;
+ builder.allowedRuleClassesForLabelsWarning = allowedRuleClassesForLabelsWarning;
+ builder.validityPredicate = validityPredicate;
+ builder.condition = condition;
+ builder.configTransition = configTransition;
+ builder.propertyFlags = propertyFlags.isEmpty() ?
+ EnumSet.noneOf(PropertyFlag.class) : EnumSet.copyOf(propertyFlags);
+ builder.value = defaultValue;
+ builder.valueSet = false;
+ builder.allowedValues = allowedValues;
+
+ return builder;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/AttributeContainer.java b/src/main/java/com/google/devtools/build/lib/packages/AttributeContainer.java
new file mode 100644
index 0000000..be7584a
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/AttributeContainer.java
@@ -0,0 +1,116 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+import com.google.devtools.build.lib.events.Location;
+
+import java.util.BitSet;
+
+/**
+ * Provides attribute setting and retrieval for a Rule. Encapsulating attribute access
+ * here means it can be passed around independently of the Rule itself. In particular,
+ * it can be consumed by independent {@link AttributeMap} instances that can apply
+ * varying kinds of logic for determining the "value" of an attribute. For example,
+ * a configurable attribute's "value" may be a { config --> value } dictionary
+ * or a configuration-bound lookup on that dictionary, depending on the context in
+ * which it's requested.
+ *
+ * <p>This class provides the lowest-level access to attribute information. It is *not*
+ * intended to be a robust public interface, but rather just an input to {@link AttributeMap}
+ * instances. Use those instances for all domain-level attribute access.
+ */
+public class AttributeContainer {
+
+ private final RuleClass ruleClass;
+
+ // Attribute values, keyed by attribute index:
+ private final Object[] attributeValues;
+
+ // Whether an attribute value has been set explicitly in the BUILD file, keyed by attribute index.
+ private final BitSet attributeValueExplicitlySpecified;
+
+ // Attribute locations, keyed by attribute index:
+ private final Location[] attributeLocations;
+
+ /**
+ * Create a container for a rule of the given rule class.
+ */
+ AttributeContainer(RuleClass ruleClass) {
+ this.ruleClass = ruleClass;
+ this.attributeValues = new Object[ruleClass.getAttributeCount()];
+ this.attributeValueExplicitlySpecified = new BitSet(ruleClass.getAttributeCount());
+ this.attributeLocations = new Location[ruleClass.getAttributeCount()];
+ }
+
+ /**
+ * Returns an attribute value by instance, or null on no match.
+ */
+ public Object getAttr(Attribute attribute) {
+ return getAttr(attribute.getName());
+ }
+
+ /**
+ * Returns an attribute value by name, or null on no match.
+ */
+ public Object getAttr(String attrName) {
+ Integer idx = ruleClass.getAttributeIndex(attrName);
+ return idx != null ? attributeValues[idx] : null;
+ }
+
+ /**
+ * Returns true iff the given attribute exists for this rule and its value
+ * is explicitly set in the BUILD file (as opposed to its default value).
+ */
+ public boolean isAttributeValueExplicitlySpecified(Attribute attribute) {
+ return isAttributeValueExplicitlySpecified(attribute.getName());
+ }
+
+ public boolean isAttributeValueExplicitlySpecified(String attributeName) {
+ Integer idx = ruleClass.getAttributeIndex(attributeName);
+ return idx != null ? attributeValueExplicitlySpecified.get(idx) : false;
+ }
+
+ /**
+ * Returns the location of the attribute definition for this rule, or null if not found.
+ */
+ public Location getAttributeLocation(String attrName) {
+ Integer idx = ruleClass.getAttributeIndex(attrName);
+ return idx != null ? attributeLocations[idx] : null;
+ }
+
+ Object getAttributeValue(int index) {
+ return attributeValues[index];
+ }
+
+ void setAttributeValue(Attribute attribute, Object value, boolean explicit) {
+ Integer index = ruleClass.getAttributeIndex(attribute.getName());
+ attributeValues[index] = value;
+ attributeValueExplicitlySpecified.set(index, explicit);
+ }
+
+ void setAttributeValueByName(String attrName, Object value) {
+ Integer index = ruleClass.getAttributeIndex(attrName);
+ attributeValues[index] = value;
+ attributeValueExplicitlySpecified.set(index);
+ }
+
+ void setAttributeLocation(int attrIndex, Location location) {
+ attributeLocations[attrIndex] = location;
+ }
+
+ void setAttributeLocation(Attribute attribute, Location location) {
+ Integer index = ruleClass.getAttributeIndex(attribute.getName());
+ attributeLocations[index] = location;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/AttributeMap.java b/src/main/java/com/google/devtools/build/lib/packages/AttributeMap.java
new file mode 100644
index 0000000..eeb6951
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/AttributeMap.java
@@ -0,0 +1,108 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.syntax.Label;
+
+import javax.annotation.Nullable;
+
+/**
+ * The interface for accessing a {@link Rule}'s attributes.
+ *
+ * <p>Since what an attribute lookup should return can be ambiguous (e.g. for configurable
+ * attributes, should we return a configuration-resolved value or the original, unresolved
+ * selection expression?), different implementations can apply different policies for how to
+ * fulfill these methods. Calling code can then use the appropriate implementation for whatever
+ * its particular needs are.
+ */
+public interface AttributeMap {
+ /**
+ * Returns the name of the rule; this is equivalent to {@code getLabel().getName()}.
+ */
+ String getName();
+
+ /**
+ * Returns the label of the rule.
+ */
+ Label getLabel();
+
+ /**
+ * Returns the value of the named rule attribute, which must be of the given type. If it does not
+ * exist or has the wrong type, throws an {@link IllegalArgumentException}.
+ */
+ <T> T get(String attributeName, Type<T> type);
+
+ /**
+ * Returns the names of all attributes covered by this map.
+ */
+ Iterable<String> getAttributeNames();
+
+ /**
+ * Returns the type of the given attribute, if it exists. Otherwise returns null.
+ */
+ @Nullable Type<?> getAttributeType(String attrName);
+
+ /**
+ * Returns the attribute definition whose name is {@code attrName}, or null
+ * if not found.
+ */
+ @Nullable Attribute getAttributeDefinition(String attrName);
+
+ /**
+ * Returns true iff the value of the specified attribute is explicitly set in the BUILD file (as
+ * opposed to its default value). This also returns true if the value from the BUILD file is the
+ * same as the default value.
+ *
+ * <p>It is probably a good idea to avoid this method in default value and implicit outputs
+ * computation, because it is confusing that setting an attribute to an empty list (for example)
+ * is different from not setting it at all.
+ */
+ boolean isAttributeValueExplicitlySpecified(String attributeName);
+
+ /**
+ * An interface which accepts {@link Attribute}s, used by {@link #visitLabels}.
+ */
+ interface AcceptsLabelAttribute {
+ /**
+ * Accept a (Label, Attribute) pair describing a dependency edge.
+ *
+ * @param label the target node of the (Rule, Label) edge.
+ * The source node should already be known.
+ * @param attribute the attribute.
+ */
+ void acceptLabelAttribute(Label label, Attribute attribute);
+ }
+
+ /**
+ * For all attributes that contain labels in their values (either by *being* a label or
+ * being a collection that includes labels), visits every label and notifies the
+ * specified observer at each visit.
+ */
+ void visitLabels(AcceptsLabelAttribute observer);
+
+ // TODO(bazel-team): These methods are here to support computed defaults that inherit
+ // package-level default values. Instead, we should auto-inherit and remove the computed
+ // defaults. If we really need to give access to package-level defaults, we should come up with
+ // a more generic interface.
+ String getPackageDefaultHdrsCheck();
+
+ Boolean getPackageDefaultObsolete();
+
+ Boolean getPackageDefaultTestOnly();
+
+ String getPackageDefaultDeprecation();
+
+ ImmutableList<String> getPackageDefaultCopts();
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/BuildFileContainsErrorsException.java b/src/main/java/com/google/devtools/build/lib/packages/BuildFileContainsErrorsException.java
new file mode 100644
index 0000000..d9283c3
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/BuildFileContainsErrorsException.java
@@ -0,0 +1,46 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+import javax.annotation.Nullable;
+
+/**
+ * Exception indicating a failed attempt to access a package that could not
+ * be read or had syntax errors.
+ */
+public class BuildFileContainsErrorsException extends NoSuchPackageException {
+
+ private Package pkg;
+
+ public BuildFileContainsErrorsException(String packageName, String message) {
+ super(packageName, "error loading package", message);
+ }
+
+ public BuildFileContainsErrorsException(String packageName, String message,
+ Throwable cause) {
+ super(packageName, "error loading package", message, cause);
+ }
+
+ public BuildFileContainsErrorsException(Package pkg, String msg) {
+ this(pkg.getName(), msg);
+ this.pkg = pkg;
+ }
+
+ @Override
+ @Nullable
+ public Package getPackage() {
+ return pkg;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/BuildFileNotFoundException.java b/src/main/java/com/google/devtools/build/lib/packages/BuildFileNotFoundException.java
new file mode 100644
index 0000000..2c70a4e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/BuildFileNotFoundException.java
@@ -0,0 +1,31 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+/**
+ * Exception indicating an attempt to access a package which is not found or
+ * does not exist.
+ */
+public class BuildFileNotFoundException extends NoSuchPackageException {
+
+ public BuildFileNotFoundException(String packageName, String message) {
+ super(packageName, message);
+ }
+
+ public BuildFileNotFoundException(String packageName, String message,
+ Throwable cause) {
+ super(packageName, message, cause);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/CachingPackageLocator.java b/src/main/java/com/google/devtools/build/lib/packages/CachingPackageLocator.java
new file mode 100644
index 0000000..f6badad
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/CachingPackageLocator.java
@@ -0,0 +1,45 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.vfs.Path;
+
+/**
+ * CachingPackageLocator implementations locate a package by its name.
+ *
+ * <p> Similar to #pkgcache.PathPackageLocator, but implementations are required
+ * to cache results and handle deleted packages.
+ *
+ * <p> This interface exists for two reasons: (1) to avoid creating a bad dependency edge from the
+ * PythonPreprocessor to lib.pkgcache ("dependency injection") and (2) to allow Skyframe to use
+ * pieces of legacy code while still updating the Skyframe node graph.
+ */
+public interface CachingPackageLocator {
+
+ /**
+ * Returns path of BUILD file for specified package iff the specified package exists, null
+ * otherwise (e.g. invalid package name, no build file, or package has been deleted via
+ * --deleted_packages)..
+ *
+ * <p> The package's root directory may be computed by calling getParentFile().
+ *
+ * <p> Instances of this interface are required to cache the results.
+ *
+ * <p> This method must be thread-safe.
+ */
+ @ThreadSafe
+ Path getBuildFileForPackage(String packageName);
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/ConstantRuleVisibility.java b/src/main/java/com/google/devtools/build/lib/packages/ConstantRuleVisibility.java
new file mode 100644
index 0000000..bb64015
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/ConstantRuleVisibility.java
@@ -0,0 +1,94 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.syntax.Label;
+
+import java.io.Serializable;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A rule visibility that simply says yes or no. It corresponds to public,
+ * legacy_public and private visibilities.
+ */
+@Immutable @ThreadSafe
+public class ConstantRuleVisibility implements RuleVisibility, Serializable {
+ static final Label LEGACY_PUBLIC_LABEL; // same as "public"; used for automated depot cleanup
+ private static final Label PUBLIC_LABEL;
+ private static final Label PRIVATE_LABEL;
+
+ public static final ConstantRuleVisibility PUBLIC =
+ new ConstantRuleVisibility(true);
+
+ public static final ConstantRuleVisibility PRIVATE =
+ new ConstantRuleVisibility(false);
+
+ static {
+ try {
+ PUBLIC_LABEL = Label.parseAbsolute("//visibility:public");
+ LEGACY_PUBLIC_LABEL = Label.parseAbsolute("//visibility:legacy_public");
+ PRIVATE_LABEL = Label.parseAbsolute("//visibility:private");
+ } catch (Label.SyntaxException e) {
+ throw new IllegalStateException();
+ }
+ }
+
+ private final boolean result;
+
+ public ConstantRuleVisibility(boolean result) {
+ this.result = result;
+ }
+
+ public boolean isPubliclyVisible() {
+ return result;
+ }
+
+ @Override
+ public List<Label> getDependencyLabels() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public List<Label> getDeclaredLabels() {
+ return ImmutableList.of(result ? PUBLIC_LABEL : PRIVATE_LABEL);
+ }
+
+ /**
+ * Tries to parse a list of labels into a {@link ConstantRuleVisibility}.
+ *
+ * @param labels the list of labels to parse
+ * @return The resulting visibility object, or null if the list of labels
+ * could not be parsed.
+ */
+ public static ConstantRuleVisibility tryParse(List<Label> labels) {
+ if (labels.size() != 1) {
+ return null;
+ }
+ return tryParse(labels.get(0));
+ }
+
+ public static ConstantRuleVisibility tryParse(Label label) {
+ if (PUBLIC_LABEL.equals(label) || LEGACY_PUBLIC_LABEL.equals(label)) {
+ return PUBLIC;
+ } else if (PRIVATE_LABEL.equals(label)) {
+ return PRIVATE;
+ } else {
+ return null;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/DefaultSetting.java b/src/main/java/com/google/devtools/build/lib/packages/DefaultSetting.java
new file mode 100644
index 0000000..1c89b4e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/DefaultSetting.java
@@ -0,0 +1,33 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+/**
+ * A feature of the build that can be switched on and off on a per-package
+ * basis.
+ *
+ * <p>This interface is only for marking targets as being affected by a feature;
+ * their implementation can be anywhere.
+ *
+ * Implementations of this interface must be immutable.
+ */
+public interface DefaultSetting {
+ String getSettingName();
+
+ /**
+ * Returns if the default setting in question affects the specific target.
+ */
+ boolean appliesTo(Target target);
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/DuplicatePackageException.java b/src/main/java/com/google/devtools/build/lib/packages/DuplicatePackageException.java
new file mode 100644
index 0000000..dc21cba
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/DuplicatePackageException.java
@@ -0,0 +1,32 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+/**
+ * Exception indicating that the same package (i.e. BUILD file) can be loaded
+ * via different package paths.
+ */
+// TODO(bazel-team): (2009) Change exception hierarchy so that DuplicatePackageException is no
+// longer a child of NoSuchPackageException.
+public class DuplicatePackageException extends NoSuchPackageException {
+
+ public DuplicatePackageException(String packageName, String message) {
+ super(packageName, message);
+ }
+
+ public DuplicatePackageException(String packageName, String message, Throwable cause) {
+ super(packageName, message, cause);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/EnumFilterConverter.java b/src/main/java/com/google/devtools/build/lib/packages/EnumFilterConverter.java
new file mode 100644
index 0000000..31242e3
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/EnumFilterConverter.java
@@ -0,0 +1,97 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+import com.google.devtools.build.lib.util.StringUtil;
+import com.google.devtools.common.options.Converter;
+import com.google.devtools.common.options.OptionsParsingException;
+
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+/**
+ * Converter that translates a string of the form "value1,value2,-value3,value4"
+ * into a corresponding set of allowed Enum values.
+ *
+ * <p>Values preceded by '-' are excluded from this set. So "value1,-value2,value3"
+ * translates to the set [EnumType.value1, EnumType.value3].
+ *
+ * <p>If *all* values are exclusions (e.g. "-value1,-value2,-value3"), the returned
+ * set contains all values for the Enum type *except* those specified.
+ */
+class EnumFilterConverter<E extends Enum<E>> implements Converter<Set<E>> {
+
+ private final Set<String> allowedValues = new LinkedHashSet<>();
+ private final Class<E> typeClass;
+ private final String prettyEnumName;
+
+ /**
+ * Constructor.
+ *
+ * @param typeClass this should be E.class (Java generics can't infer that directly)
+ * @param userFriendlyName a user-friendly description of this enum type
+ */
+ public EnumFilterConverter(Class<E> typeClass, String userFriendlyName) {
+ this.typeClass = typeClass;
+ this.prettyEnumName = userFriendlyName;
+ for (E value : EnumSet.allOf(typeClass)) {
+ allowedValues.add(value.name());
+ }
+ }
+
+ /**
+ * Returns the set of allowed values for the option.
+ *
+ * Implements {@link #convert(String)}.
+ */
+ @Override
+ public Set<E> convert(String input) throws OptionsParsingException {
+ if (input.equals("")) {
+ return Collections.emptySet();
+ }
+ EnumSet<E> includedSet = EnumSet.noneOf(typeClass);
+ EnumSet<E> excludedSet = EnumSet.noneOf(typeClass);
+ for (String value : input.split(",", -1)) {
+ boolean excludeFlag = value.startsWith("-");
+ String s = (excludeFlag ? value.substring(1) : value).toUpperCase();
+ if (!allowedValues.contains(s)) {
+ throw new OptionsParsingException("Invalid " + prettyEnumName + " filter '" + value +
+ "' in the input '" + input + "'");
+ }
+ (excludeFlag ? excludedSet : includedSet).add(E.valueOf(typeClass, s));
+ }
+ if (includedSet.isEmpty()) {
+ includedSet = EnumSet.complementOf(excludedSet);
+ } else {
+ includedSet.removeAll(excludedSet);
+ }
+ if (includedSet.isEmpty()) {
+ throw new OptionsParsingException(
+ Character.toUpperCase(prettyEnumName.charAt(0)) + prettyEnumName.substring(1) +
+ " filter '" + input + "' definition cannot match any tests");
+ }
+ return includedSet;
+ }
+
+ /**
+ * Implements {@link #getTypeDescription()}.
+ */
+ @Override
+ public final String getTypeDescription() {
+ return "comma-separated list of values: "
+ + StringUtil.joinEnglishList(allowedValues).toLowerCase();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/EnvironmentGroup.java b/src/main/java/com/google/devtools/build/lib/packages/EnvironmentGroup.java
new file mode 100644
index 0000000..07da227
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/EnvironmentGroup.java
@@ -0,0 +1,241 @@
+// Copyright 2015 Google Inc. 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.packages;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.syntax.Label;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Model for the "environment_group' rule: the piece of Bazel's rule constraint system that binds
+ * thematically related environments together and determines which environments a rule supports
+ * by default. See {@link com.google.devtools.build.lib.analysis.constraints.ConstraintSemantics}
+ * for precise semantic details of how this information is used.
+ *
+ * <p>Note that "environment_group" is implemented as a loading-time function, not a rule. This is
+ * to support proper discovery of defaults: Say rule A has no explicit constraints and depends
+ * on rule B, which is explicitly constrained to environment ":bar". Since A declares nothing
+ * explicitly, it's implicitly constrained to DEFAULTS (whatever that is). Therefore, the
+ * dependency is only allowed if DEFAULTS doesn't include environments beyond ":bar". To figure
+ * that out, we need to be able to look up the environment group for ":bar", which is what this
+ * class provides.
+ *
+ * <p>If we implemented this as a rule, we'd have to provide that lookup via rule dependencies,
+ * e.g. something like:
+ *
+ * <code>
+ * environment(
+ * name = 'bar',
+ * group = [':sample_environments'],
+ * is_default = 1
+ * )
+ * </code>
+ *
+ * <p>But this won't work. This would let us find the environment group for ":bar", but the only way
+ * to determine what other environments belong to the group is to have the group somehow reference
+ * them. That would produce circular dependencies in the build graph, which is no good.
+ */
+@Immutable
+public class EnvironmentGroup implements Target {
+ private final Label label;
+ private final Location location;
+ private final Package containingPackage;
+ private final Set<Label> environments;
+ private final Set<Label> defaults;
+
+ /**
+ * Predicate that matches labels from a different package than the initialized package.
+ */
+ private static final class DifferentPackage implements Predicate<Label> {
+ private final Package containingPackage;
+
+ private DifferentPackage(Package containingPackage) {
+ this.containingPackage = containingPackage;
+ }
+
+ @Override
+ public boolean apply(Label environment) {
+ return !environment.getPackageName().equals(containingPackage.getName());
+ }
+ }
+
+ /**
+ * Instantiates a new group without verifying the soundness of its contents. See the validation
+ * methods below for appropriate checks.
+ *
+ * @param label the build label identifying this group
+ * @param pkg the package this group belongs to
+ * @param environments the set of environments that belong to this group
+ * @param defaults the environments a rule implicitly supports unless otherwise specified
+ * @param location location in the BUILD file of this group
+ */
+ EnvironmentGroup(Label label, Package pkg, final List<Label> environments, List<Label> defaults,
+ Location location) {
+ this.label = label;
+ this.location = location;
+ this.containingPackage = pkg;
+ this.environments = ImmutableSet.copyOf(environments);
+ this.defaults = ImmutableSet.copyOf(defaults);
+ }
+
+ /**
+ * Checks that all environments declared by this group are in the same package as the group (so
+ * we can perform an environment --> environment_group lookup and know the package is available)
+ * and checks that all defaults are legitimate members of the group.
+ *
+ * <p>Does <b>not</b> check that the referenced environments exist (see
+ * {@link #checkEnvironmentsExist).
+ *
+ * @return a list of validation errors that occurred
+ */
+ List<Event> validateMembership() {
+ List<Event> events = new ArrayList<>();
+
+ // All environments should belong to the same package as this group.
+ for (Label environment :
+ Iterables.filter(environments, new DifferentPackage(containingPackage))) {
+ events.add(Event.error(location,
+ environment + " is not in the same package as group " + label));
+ }
+
+ // The defaults must be a subset of the member environments.
+ for (Label unknownDefault : Sets.difference(defaults, environments)) {
+ events.add(Event.error(location, "default " + unknownDefault + " is not a "
+ + "declared environment for group " + getLabel()));
+ }
+
+ return events;
+ }
+
+ /**
+ * Given the set of targets in this group's package, checks that all of the group's declared
+ * environments are part of that set (i.e. the group doesn't reference non-existant labels).
+ *
+ * @param pkgTargets mapping from label name to target instance for this group's package
+ * @return a list of validation errors that occurred
+ */
+ List<Event> checkEnvironmentsExist(Map<String, Target> pkgTargets) {
+ List<Event> events = new ArrayList<>();
+ for (Label envName : environments) {
+ Target env = pkgTargets.get(envName.getName());
+ if (env == null) {
+ events.add(Event.error(location, "environment " + envName + " does not exist"));
+ } else if (!env.getTargetKind().equals("environment rule")) {
+ events.add(Event.error(location, env.getLabel() + " is not a valid environment"));
+ }
+ }
+ return events;
+ }
+
+ /**
+ * Returns the environments that belong to this group.
+ */
+ public Set<Label> getEnvironments() {
+ return environments;
+ }
+
+ /**
+ * Returns the environments a rule supports by default, i.e. if it has no explicit references to
+ * environments in this group.
+ */
+ public Set<Label> getDefaults() {
+ return defaults;
+ }
+
+ /**
+ * Determines whether or not an environment is a default. Returns false if the environment
+ * doesn't belong to this group.
+ */
+ public boolean isDefault(Label environment) {
+ return defaults.contains(environment);
+ }
+
+ @Override
+ public Label getLabel() {
+ return label;
+ }
+
+ @Override
+ public String getName() {
+ return label.getName();
+ }
+
+ @Override
+ public Package getPackage() {
+ return containingPackage;
+ }
+
+ @Override
+ public String getTargetKind() {
+ return targetKind();
+ }
+
+ @Override
+ public Rule getAssociatedRule() {
+ return null;
+ }
+
+ @Override
+ public License getLicense() {
+ return License.NO_LICENSE;
+ }
+
+ @Override
+ public Location getLocation() {
+ return location;
+ }
+
+ @Override
+ public String toString() {
+ return targetKind() + " " + getLabel();
+ }
+
+ @Override
+ public Set<License.DistributionType> getDistributions() {
+ return Collections.emptySet();
+ }
+
+ @Override
+ public RuleVisibility getVisibility() {
+ return ConstantRuleVisibility.PRIVATE; // No rule should be referencing an environment_group.
+ }
+
+ public static String targetKind() {
+ return "environment group";
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ // In a distributed implementation these may not be the same object.
+ if (o == this) {
+ return true;
+ } else if (!(o instanceof EnvironmentGroup)) {
+ return false;
+ } else {
+ return ((EnvironmentGroup) o).getLabel().equals(getLabel());
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/ExternalPackage.java b/src/main/java/com/google/devtools/build/lib/packages/ExternalPackage.java
new file mode 100644
index 0000000..9c92b33
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/ExternalPackage.java
@@ -0,0 +1,193 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.events.StoredEventHandler;
+import com.google.devtools.build.lib.packages.PackageIdentifier.RepositoryName;
+import com.google.devtools.build.lib.packages.RuleFactory.InvalidRuleException;
+import com.google.devtools.build.lib.syntax.EvalException;
+import com.google.devtools.build.lib.syntax.FuncallExpression;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.syntax.Label.SyntaxException;
+import com.google.devtools.build.lib.vfs.Path;
+
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * This creates the //external package, where targets not homed in this repository can be bound.
+ */
+public class ExternalPackage extends Package {
+
+ private Map<RepositoryName, Rule> repositoryMap;
+
+ ExternalPackage() {
+ super(PackageIdentifier.createInDefaultRepo("external"));
+ }
+
+ /**
+ * Returns a description of the repository with the given name, or null if there's no such
+ * repository.
+ */
+ public Rule getRepositoryInfo(RepositoryName repositoryName) {
+ return repositoryMap.get(repositoryName);
+ }
+
+ /**
+ * Holder for a binding's actual label and location.
+ */
+ public static class Binding {
+ private final Label actual;
+ private final Location location;
+
+ public Binding(Label actual, Location location) {
+ this.actual = actual;
+ this.location = location;
+ }
+
+ public Label getActual() {
+ return actual;
+ }
+
+ public Location getLocation() {
+ return location;
+ }
+
+ /**
+ * Checks if the label is bound, i.e., starts with //external:.
+ */
+ public static boolean isBoundLabel(Label label) {
+ return label.getPackageName().equals("external");
+ }
+ }
+
+ /**
+ * Given a workspace file path, creates an ExternalPackage.
+ */
+ public static class ExternalPackageBuilder
+ extends AbstractBuilder<ExternalPackage, ExternalPackageBuilder> {
+ private Map<Label, Binding> bindMap = Maps.newHashMap();
+ private Map<RepositoryName, Rule> repositoryMap = Maps.newHashMap();
+
+ public ExternalPackageBuilder(Path workspacePath) {
+ super(new ExternalPackage());
+ setFilename(workspacePath);
+ setMakeEnv(new MakeEnvironment.Builder());
+ }
+
+ @Override
+ protected ExternalPackageBuilder self() {
+ return this;
+ }
+
+ @Override
+ public ExternalPackage build() {
+ pkg.repositoryMap = ImmutableMap.copyOf(repositoryMap);
+ return super.build();
+ }
+
+ public void addBinding(Label label, Binding binding) {
+ bindMap.put(label, binding);
+ }
+
+ public void resolveBindTargets(RuleClass ruleClass)
+ throws EvalException, NoSuchBindingException {
+ for (Entry<Label, Binding> entry : bindMap.entrySet()) {
+ resolveLabel(entry.getKey(), entry.getValue());
+ }
+
+ for (Entry<Label, Binding> entry : bindMap.entrySet()) {
+ try {
+ addRule(ruleClass, entry);
+ } catch (NameConflictException | InvalidRuleException e) {
+ throw new EvalException(entry.getValue().location, e.getMessage());
+ }
+ }
+ }
+
+ // Uses tortoise and the hare algorithm to detect cycles.
+ private void resolveLabel(final Label virtual, Binding binding)
+ throws NoSuchBindingException {
+ Label actual = binding.getActual();
+ Label tortoise = virtual;
+ Label hare = actual;
+ boolean moveTortoise = true;
+ while (Binding.isBoundLabel(actual)) {
+ if (tortoise == hare) {
+ throw new NoSuchBindingException("cycle detected resolving " + virtual + " binding");
+ }
+
+ Label previous = actual; // For the exception.
+ binding = bindMap.get(actual);
+ if (binding == null) {
+ throw new NoSuchBindingException("no binding found for target " + previous + " (via "
+ + virtual + ")");
+ }
+ actual = binding.getActual();
+ hare = actual;
+ moveTortoise = !moveTortoise;
+ if (moveTortoise) {
+ tortoise = bindMap.get(tortoise).getActual();
+ }
+ }
+ bindMap.put(virtual, binding);
+ }
+
+ private void addRule(RuleClass klass, Map.Entry<Label, Binding> bindingEntry)
+ throws InvalidRuleException, NameConflictException {
+ Label virtual = bindingEntry.getKey();
+ Label actual = bindingEntry.getValue().actual;
+ Location location = bindingEntry.getValue().location;
+
+ Map<String, Object> attributes = Maps.newHashMap();
+ // Bound rules don't have a name field, but this works because we don't want more than one
+ // with the same virtual name.
+ attributes.put("name", virtual.getName());
+ attributes.put("actual", actual);
+ StoredEventHandler handler = new StoredEventHandler();
+ Rule rule = RuleFactory.createAndAddRule(this, klass, attributes, handler, null, location);
+ rule.setVisibility(ConstantRuleVisibility.PUBLIC);
+ }
+
+ /**
+ * This is used when a binding is invalid, either because one of the targets is malformed,
+ * refers to a package that does not exist, or creates a circular dependency.
+ */
+ public class NoSuchBindingException extends NoSuchThingException {
+ public NoSuchBindingException(String message) {
+ super(message);
+ }
+ }
+
+ /**
+ * Creates an external repository rule.
+ * @throws SyntaxException if the repository name is invalid.
+ */
+ public ExternalPackageBuilder createAndAddRepositoryRule(RuleClass ruleClass,
+ Map<String, Object> kwargs, FuncallExpression ast)
+ throws InvalidRuleException, NameConflictException, SyntaxException {
+ StoredEventHandler eventHandler = new StoredEventHandler();
+ Rule rule = RuleFactory.createAndAddRule(this, ruleClass, kwargs, eventHandler, ast,
+ ast.getLocation());
+ // Propagate Rule errors to the builder.
+ addEvents(eventHandler.getEvents());
+ repositoryMap.put(RepositoryName.create("@" + rule.getName()), rule);
+ return this;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/FileTarget.java b/src/main/java/com/google/devtools/build/lib/packages/FileTarget.java
new file mode 100644
index 0000000..60f30ce
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/FileTarget.java
@@ -0,0 +1,92 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.packages.License.DistributionType;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.util.FileType.HasFilename;
+
+import java.util.Set;
+
+/**
+ * Common superclass for InputFile and OutputFile which provides implementation
+ * for the file operations in common.
+ */
+public abstract class FileTarget implements Target, HasFilename {
+ protected final Package pkg;
+ protected final Label label;
+
+ /**
+ * Constructs a file with the given label, which must be in the given package.
+ */
+ protected FileTarget(Package pkg, Label label) {
+ Preconditions.checkArgument(label.getPackageFragment().equals(pkg.getNameFragment()));
+ this.pkg = pkg;
+ this.label = label;
+ }
+
+ @Override
+ public String getFilename() {
+ return label.getName();
+ }
+
+ @Override
+ public Label getLabel() {
+ return label;
+ }
+
+ @Override
+ public String getName() {
+ return label.getName();
+ }
+
+ @Override
+ public Package getPackage() {
+ return pkg;
+ }
+
+ @Override
+ public String toString() {
+ return getTargetKind() + "(" + getLabel() + ")"; // Just for debugging
+ }
+
+ @Override
+ public int hashCode() {
+ return label.hashCode();
+ }
+
+ @Override
+ public Set<DistributionType> getDistributions() {
+ return getPackage().getDefaultDistribs();
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * <p>File licenses are strange, and require some special handling. When
+ * you ask "What license covers this file?" in a query, the answer should
+ * be the license declared for the enclosing package. On the other hand,
+ * if the file is a source for a rule target, and the rule's license declares
+ * more exceptions than the default inherited by the file, the rule's
+ * more liberal target should override the stricter license of the file. In
+ * other words, the license of the rule always overrides the license of
+ * the non-rule file targets that are inputs to that rule.
+ */
+ @Override
+ public License getLicense() {
+ return getPackage().getDefaultLicense();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/GlobCache.java b/src/main/java/com/google/devtools/build/lib/packages/GlobCache.java
new file mode 100644
index 0000000..3e150b4
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/GlobCache.java
@@ -0,0 +1,347 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.base.Throwables;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.SettableFuture;
+import com.google.devtools.build.lib.concurrent.ThreadSafety;
+import com.google.devtools.build.lib.util.Pair;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.lib.vfs.Symlinks;
+import com.google.devtools.build.lib.vfs.UnixGlob;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Caches the results of glob expansion for a package.
+ */
+@ThreadSafety.ThreadCompatible
+public class GlobCache {
+ public static class BadGlobException extends Exception {
+ BadGlobException(String message) {
+ super(message);
+ }
+ }
+
+ /**
+ * A mapping from glob expressions (e.g. "*.java") to the list of files it
+ * matched (in the order returned by VFS) at the time the package was
+ * constructed. Required for sound dependency analysis.
+ *
+ * We don't use a Multimap because it provides no way to distinguish "key not
+ * present" from (key -> {}).
+ */
+ private final Map<Pair<String, Boolean>, Future<List<Path>>> globCache = new HashMap<>();
+
+ /**
+ * The directory in which our package's BUILD file resides.
+ */
+ private final Path packageDirectory;
+
+ /**
+ * The name of the package we belong to.
+ */
+ private final PackageIdentifier packageId;
+
+ /**
+ * The package locator-based directory traversal predicate.
+ */
+ private final Predicate<Path> childDirectoryPredicate;
+
+ /**
+ * System call caching layer.
+ */
+ private AtomicReference<? extends UnixGlob.FilesystemCalls> syscalls;
+
+ /**
+ * The thread pool for glob evaluation.
+ */
+ private final ThreadPoolExecutor globExecutor;
+
+ /**
+ * Create a glob expansion cache.
+ * @param packageDirectory globs will be expanded relatively to this
+ * directory.
+ * @param packageId the name of the package this cache belongs to.
+ * @param locator the package locator.
+ * @param globExecutor thread pool for glob evaluation.
+ */
+ public GlobCache(final Path packageDirectory,
+ final PackageIdentifier packageId,
+ final CachingPackageLocator locator,
+ AtomicReference<? extends UnixGlob.FilesystemCalls> syscalls,
+ ThreadPoolExecutor globExecutor) {
+ this.packageDirectory = Preconditions.checkNotNull(packageDirectory);
+ this.packageId = Preconditions.checkNotNull(packageId);
+ this.globExecutor = Preconditions.checkNotNull(globExecutor);
+ this.syscalls = syscalls == null ? new AtomicReference<>(UnixGlob.DEFAULT_SYSCALLS) : syscalls;
+
+ Preconditions.checkNotNull(locator);
+ final PathFragment pkgNameFrag = packageId.getPackageFragment();
+ childDirectoryPredicate = new Predicate<Path>() {
+ @Override
+ public boolean apply(Path directory) {
+ if (directory.equals(packageDirectory)) {
+ return true;
+ }
+
+ PathFragment pkgName = pkgNameFrag.getRelative(directory.relativeTo(packageDirectory));
+ UnixGlob.FilesystemCalls syscalls = GlobCache.this.syscalls.get();
+ if (syscalls != UnixGlob.DEFAULT_SYSCALLS) {
+ // This is needed because in case the BUILD file exists, we do not call readdir() on its
+ // directory. However, the package needs to be re-evaluated if the BUILD file is removed.
+ // Therefore, we add this BUILD file to our dependencies by statting it through the
+ // recording syscall object so that the BUILD file itself is added to the dependencies of
+ // this package.
+ //
+ // The stat() call issued by locator.getBuildFileForPackage() does not quite cut it
+ // because 1. it is cached so there may not be a stat() call at all and 2. even if there
+ // is, it does not go through the proxy in GlobCache.this.syscalls.
+ //
+ // Note that this does not cause any significant slowdown; the BUILD file cache will have
+ // already evaluated the very same stat, so we don't pay any I/O cost, only a cache
+ // lookup.
+ syscalls.statNullable(directory.getChild("BUILD"), Symlinks.FOLLOW);
+ }
+
+ return locator.getBuildFileForPackage(pkgName.getPathString()) == null;
+ }
+ };
+ }
+
+ /**
+ * Returns the future result of evaluating glob "pattern" against this
+ * package's directory, using the package's cache of previously-started
+ * globs if possible.
+ *
+ * @return the list of paths matching the pattern, relative to the package's
+ * directory.
+ * @throws BadGlobException if the glob was syntactically invalid, or
+ * contained uplevel references.
+ */
+ Future<List<Path>> getGlobAsync(String pattern, boolean excludeDirs)
+ throws BadGlobException {
+ Future<List<Path>> cached = globCache.get(Pair.of(pattern, excludeDirs));
+ if (cached == null) {
+ cached = safeGlob(pattern, excludeDirs);
+ setGlobPaths(pattern, excludeDirs, cached);
+ }
+ return cached;
+ }
+
+ @VisibleForTesting
+ List<String> getGlob(String pattern)
+ throws IOException, BadGlobException, InterruptedException {
+ return getGlob(pattern, false);
+ }
+
+ @VisibleForTesting
+ protected List<String> getGlob(String pattern, boolean excludeDirs)
+ throws IOException, BadGlobException, InterruptedException {
+ Future<List<Path>> futureResult = getGlobAsync(pattern, excludeDirs);
+ List<Path> globPaths = fromFuture(futureResult);
+ // Replace the UnixGlob.GlobFuture with a completed future object, to allow
+ // garbage collection of the GlobFuture and GlobVisitor objects.
+ if (!(futureResult instanceof SettableFuture<?>)) {
+ SettableFuture<List<Path>> completedFuture = SettableFuture.create();
+ completedFuture.set(globPaths);
+ globCache.put(Pair.of(pattern, excludeDirs), completedFuture);
+ }
+
+ List<String> result = Lists.newArrayListWithCapacity(globPaths.size());
+ for (Path path : globPaths) {
+ String relative = path.relativeTo(packageDirectory).getPathString();
+ // Don't permit "" (meaning ".") in the glob expansion, since it's
+ // invalid as a label, plus users should say explicitly if they
+ // really want to name the package directory.
+ if (!relative.isEmpty()) {
+ result.add(relative);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Adds glob entries to the cache, making sure they are sorted first.
+ */
+ @VisibleForTesting
+ void setGlobPaths(String pattern, boolean excludeDirectories, Future<List<Path>> result) {
+ globCache.put(Pair.of(pattern, excludeDirectories), result);
+ }
+
+ /**
+ * Actually execute a glob against the filesystem. Otherwise similar to
+ * getGlob().
+ */
+ @VisibleForTesting
+ Future<List<Path>> safeGlob(String pattern, boolean excludeDirs) throws BadGlobException {
+ // Forbidden patterns:
+ if (pattern.indexOf('?') != -1) {
+ throw new BadGlobException("glob pattern '" + pattern + "' contains forbidden '?' wildcard");
+ }
+ // Patterns forbidden by UnixGlob library:
+ String error = UnixGlob.checkPatternForError(pattern);
+ if (error != null) {
+ throw new BadGlobException(error + " (in glob pattern '" + pattern + "')");
+ }
+ return UnixGlob.forPath(packageDirectory)
+ .addPattern(pattern)
+ .setExcludeDirectories(excludeDirs)
+ .setDirectoryFilter(childDirectoryPredicate)
+ .setThreadPool(globExecutor)
+ .setFilesystemCalls(syscalls)
+ .globAsync(true);
+ }
+
+ /**
+ * Sanitize the future exceptions - the only expected checked exception
+ * is IOException.
+ */
+ private static List<Path> fromFuture(Future<List<Path>> future)
+ throws IOException, InterruptedException {
+ try {
+ return future.get();
+ } catch (ExecutionException e) {
+ Throwable cause = e.getCause();
+ Throwables.propagateIfPossible(cause,
+ IOException.class, InterruptedException.class);
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Returns true iff all this package's globs are up-to-date. That is,
+ * re-evaluating the package's BUILD file at this moment would yield an
+ * equivalent Package instance. (This call requires filesystem I/O to
+ * re-evaluate the globs.)
+ */
+ public boolean globsUpToDate() throws InterruptedException {
+ // Start all globs in parallel.
+ Map<Pair<String, Boolean>, Future<List<Path>>> newGlobs = new HashMap<>();
+ try {
+ for (Map.Entry<Pair<String, Boolean>, Future<List<Path>>> entry : globCache.entrySet()) {
+ Pair<String, Boolean> key = entry.getKey();
+ try {
+ newGlobs.put(key, safeGlob(key.first, key.second));
+ } catch (BadGlobException e) {
+ return false;
+ }
+ }
+
+ for (Map.Entry<Pair<String, Boolean>, Future<List<Path>>> entry : globCache.entrySet()) {
+ try {
+ Pair<String, Boolean> key = entry.getKey();
+ List<Path> newGlob = fromFuture(newGlobs.get(key));
+ List<Path> oldGlob = fromFuture(entry.getValue());
+ if (!oldGlob.equals(newGlob)) {
+ return false;
+ }
+ } catch (IOException e) {
+ return false;
+ }
+ }
+
+ return true;
+ } finally {
+ finishBackgroundTasks(newGlobs.values());
+ }
+ }
+
+ /**
+ * Evaluate the build language expression "glob(includes, excludes)" in the
+ * context of this package.
+ *
+ * <p>Called by PackageFactory via Package.
+ */
+ public List<String> glob(List<String> includes, List<String> excludes, boolean excludeDirs)
+ throws IOException, BadGlobException, InterruptedException {
+ // Start globbing all patterns in parallel. The getGlob() calls below will
+ // block on an individual pattern's results, but the other globs can
+ // continue in the background.
+ for (String pattern : Iterables.concat(includes, excludes)) {
+ getGlobAsync(pattern, excludeDirs);
+ }
+
+ Set<String> results = new LinkedHashSet<>();
+ for (String pattern : includes) {
+ results.addAll(getGlob(pattern, excludeDirs));
+ }
+ for (String pattern : excludes) {
+ results.removeAll(getGlob(pattern, excludeDirs));
+ }
+
+ Preconditions.checkState(!results.contains(null), "glob returned null");
+ return new ArrayList<>(results);
+ }
+
+ public Set<Pair<String, Boolean>> getKeySet() {
+ return globCache.keySet();
+ }
+
+ /**
+ * Block on the completion of all potentially-abandoned background tasks.
+ */
+ public void finishBackgroundTasks() {
+ finishBackgroundTasks(globCache.values());
+ }
+
+ public void cancelBackgroundTasks() {
+ cancelBackgroundTasks(globCache.values());
+ }
+
+ private static void finishBackgroundTasks(Collection<Future<List<Path>>> tasks) {
+ for (Future<List<Path>> task : tasks) {
+ try {
+ fromFuture(task);
+ } catch (IOException | InterruptedException e) {
+ // Ignore: If this was still going on in the background, some other
+ // failure already occurred.
+ }
+ }
+ }
+
+ private static void cancelBackgroundTasks(Collection<Future<List<Path>>> tasks) {
+ for (Future<List<Path>> task : tasks) {
+ task.cancel(true);
+
+ try {
+ task.get();
+ } catch (ExecutionException | InterruptedException e) {
+ // We don't care. Point is, the task does not bother us anymore.
+ }
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "GlobCache for " + packageId + " in " + packageDirectory;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/ImplicitOutputsFunction.java b/src/main/java/com/google/devtools/build/lib/packages/ImplicitOutputsFunction.java
new file mode 100644
index 0000000..9f72fd0
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/ImplicitOutputsFunction.java
@@ -0,0 +1,421 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+import static com.google.devtools.build.lib.syntax.SkylarkFunction.castMap;
+import static java.util.Collections.singleton;
+
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.common.escape.Escaper;
+import com.google.common.escape.Escapers;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.syntax.ClassObject;
+import com.google.devtools.build.lib.syntax.ClassObject.SkylarkClassObject;
+import com.google.devtools.build.lib.syntax.EvalException;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.syntax.SkylarkCallbackFunction;
+import com.google.devtools.build.lib.util.StringUtil;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+/**
+ * A function interface allowing rules to specify their set of implicit outputs
+ * in a more dynamic way than just simple template-substitution. For example,
+ * the set of implicit outputs may be a function of rule attributes.
+ */
+public abstract class ImplicitOutputsFunction {
+
+ /**
+ * Implicit output functions for Skylark supporting key value access of expanded implicit outputs.
+ */
+ public abstract static class SkylarkImplicitOutputsFunction extends ImplicitOutputsFunction {
+
+ public abstract ImmutableMap<String, String> calculateOutputs(AttributeMap map)
+ throws EvalException;
+
+ @Override
+ public Iterable<String> getImplicitOutputs(AttributeMap map) throws EvalException {
+ return calculateOutputs(map).values();
+ }
+ }
+
+ /**
+ * Implicit output functions executing Skylark code.
+ */
+ public static final class SkylarkImplicitOutputsFunctionWithCallback
+ extends SkylarkImplicitOutputsFunction {
+
+ private final SkylarkCallbackFunction callback;
+ private final Location loc;
+
+ public SkylarkImplicitOutputsFunctionWithCallback(
+ SkylarkCallbackFunction callback, Location loc) {
+ this.callback = callback;
+ this.loc = loc;
+ }
+
+ @Override
+ public ImmutableMap<String, String> calculateOutputs(AttributeMap map) throws EvalException {
+ Map<String, Object> attrValues = new HashMap<>();
+ for (String attrName : map.getAttributeNames()) {
+ // TODO(bazel-team): support configurable attributes - which value would we want to
+ // pass on to the child outputs function? Maybe implicit output functions shouldn't
+ // have access to configurable values (makes them too complicated?). Maybe they
+ // should have *full* access (gives them the most power?).
+ Object value = map.get(attrName, map.getAttributeType(attrName));
+ if (value != null) {
+ attrValues.put(attrName, value);
+ }
+ }
+ ClassObject attrs = new SkylarkClassObject(attrValues, "No such attribute '%s'");
+ try {
+ ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
+ for (Map.Entry<String, String> entry : castMap(callback.call(attrs),
+ String.class, String.class, "implicit outputs function return value")) {
+ Iterable<String> substitutions = fromTemplates(entry.getValue()).getImplicitOutputs(map);
+ if (!Iterables.isEmpty(substitutions)) {
+ builder.put(entry.getKey(), Iterables.getOnlyElement(substitutions));
+ }
+ }
+ return builder.build();
+ } catch (IllegalArgumentException e) {
+ throw new EvalException(loc, e.getMessage());
+ }
+ }
+ }
+
+ /**
+ * Implicit output functions using a simple an output map.
+ */
+ public static final class SkylarkImplicitOutputsFunctionWithMap
+ extends SkylarkImplicitOutputsFunction {
+
+ private final ImmutableMap<String, String> outputMap;
+
+ public SkylarkImplicitOutputsFunctionWithMap(ImmutableMap<String, String> outputMap) {
+ this.outputMap = outputMap;
+ }
+
+ @Override
+ public ImmutableMap<String, String> calculateOutputs(AttributeMap map) {
+ ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
+ for (Map.Entry<String, String> entry : outputMap.entrySet()) {
+ Iterable<String> substitutions = fromTemplates(entry.getValue()).getImplicitOutputs(map);
+ if (!Iterables.isEmpty(substitutions)) {
+ builder.put(entry.getKey(), Iterables.getOnlyElement(substitutions));
+ }
+ }
+ return builder.build();
+ }
+ }
+
+ /**
+ * Implicit output functions which can not throw an EvalException.
+ */
+ public abstract static class SafeImplicitOutputsFunction extends ImplicitOutputsFunction {
+ @Override
+ public abstract Iterable<String> getImplicitOutputs(AttributeMap map);
+ }
+
+ /**
+ * An interface to objects that can retrieve rule attributes.
+ */
+ public interface AttributeValueGetter {
+ /**
+ * Returns the value(s) of attribute "attr" in "rule", or empty set if attribute unknown.
+ */
+ Set<String> get(AttributeMap rule, String attr);
+ }
+
+ /**
+ * The default rule attribute retriever.
+ *
+ * <p>Custom {@link AttributeValueGetter} implementations may delegate to this object as a
+ * fallback mechanism.
+ */
+ public static final AttributeValueGetter DEFAULT_RULE_ATTRIBUTE_GETTER =
+ new AttributeValueGetter() {
+ @Override
+ public Set<String> get(AttributeMap rule, String attr) {
+ return attributeValues(rule, attr);
+ }
+ };
+
+ private static final Escaper PERCENT_ESCAPER = Escapers.builder().addEscape('%', "%%").build();
+
+ /**
+ * Given a newly-constructed Rule instance (with attributes populated),
+ * returns the list of output files that this rule produces implicitly.
+ */
+ public abstract Iterable<String> getImplicitOutputs(AttributeMap rule) throws EvalException;
+
+ /**
+ * The implicit output function that returns no files.
+ */
+ public static final SafeImplicitOutputsFunction NONE = new SafeImplicitOutputsFunction() {
+ @Override public Iterable<String> getImplicitOutputs(AttributeMap rule) {
+ return Collections.emptyList();
+ }
+ };
+
+ /**
+ * A convenience wrapper for {@link #fromTemplates(Iterable)}.
+ */
+ public static SafeImplicitOutputsFunction fromTemplates(String... templates) {
+ return fromTemplates(Arrays.asList(templates));
+ }
+
+ /**
+ * The implicit output function that generates files based on a set of
+ * template substitutions using rule attribute values.
+ *
+ * @param templates The templates used to construct the name of the implicit
+ * output file target. The substring "%{name}" will be replaced by the
+ * actual name of the rule, the substring "%{srcs}" will be replaced by the
+ * name of each source file without its extension. If multiple %{}
+ * substrings exist, the cross-product of them is generated.
+ */
+ public static SafeImplicitOutputsFunction fromTemplates(final Iterable<String> templates) {
+ return new SafeImplicitOutputsFunction() {
+ // TODO(bazel-team): parse the templates already here
+ @Override
+ public Iterable<String> getImplicitOutputs(AttributeMap rule) {
+ Iterable<String> result = null;
+ for (String template : templates) {
+ List<String> substitutions = substitutePlaceholderIntoTemplate(template, rule);
+ if (substitutions.isEmpty()) {
+ continue;
+ }
+ if (result == null) {
+ result = substitutions;
+ } else {
+ result = Iterables.concat(result, substitutions);
+ }
+ }
+ if (result == null) {
+ return ImmutableList.<String>of();
+ } else {
+ return Sets.newLinkedHashSet(result);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return StringUtil.joinEnglishList(templates);
+ }
+ };
+ }
+
+ /**
+ * A convenience wrapper for {@link #fromFunctions(Iterable)}.
+ */
+ public static SafeImplicitOutputsFunction fromFunctions(
+ SafeImplicitOutputsFunction... functions) {
+ return fromFunctions(Arrays.asList(functions));
+ }
+
+ /**
+ * The implicit output function that generates files based on a set of
+ * template substitutions using rule attribute values.
+ *
+ * @param functions The functions used to construct the name of the implicit
+ * output file target. The substring "%{name}" will be replaced by the
+ * actual name of the rule, the substring "%{srcs}" will be replaced by the
+ * name of each source file without its extension. If multiple %{}
+ * substrings exist, the cross-product of them is generated.
+ */
+ public static SafeImplicitOutputsFunction fromFunctions(
+ final Iterable<SafeImplicitOutputsFunction> functions) {
+ return new SafeImplicitOutputsFunction() {
+ @Override
+ public Iterable<String> getImplicitOutputs(AttributeMap rule) {
+ Collection<String> result = new LinkedHashSet<>();
+ for (SafeImplicitOutputsFunction function : functions) {
+ Iterables.addAll(result, function.getImplicitOutputs(rule));
+ }
+ return result;
+ }
+ @Override
+ public String toString() {
+ return StringUtil.joinEnglishList(functions);
+ }
+ };
+ }
+
+ /**
+ * Coerces attribute "attrName" of the specified rule into a sequence of
+ * strings. Helper function for {@link #fromTemplates(Iterable)}.
+ */
+ private static Set<String> attributeValues(AttributeMap rule, String attrName) {
+ // Special case "name" since it's not treated as an attribute.
+ if (attrName.equals("name")) {
+ return singleton(rule.getName());
+ } else if (attrName.equals("dirname")) {
+ PathFragment dir = new PathFragment(rule.getName()).getParentDirectory();
+ return (dir.segmentCount() == 0) ? singleton("") : singleton(dir.getPathString() + "/");
+ } else if (attrName.equals("basename")) {
+ return singleton(new PathFragment(rule.getName()).getBaseName());
+ }
+
+ Type<?> attrType = rule.getAttributeType(attrName);
+ if (attrType == null) { return Collections.emptySet(); }
+ // String attributes and lists are easy.
+ if (Type.STRING == attrType) {
+ return singleton(rule.get(attrName, Type.STRING));
+ } else if (Type.STRING_LIST == attrType) {
+ Iterable<String> values = rule.get(attrName, Type.STRING_LIST);
+ // TODO(bazel-team): extract this for modularization
+ if ("locales".equals(attrName)) {
+ // Locales have to be lowercased before used in a file name for
+ // consistency with file naming guidelines, and convert dash-style
+ // (en-US-pseudo) to underscore-style (en_US_pseudo).
+ values = Iterables.transform(values,
+ new Function<String, String>() {
+ @Override
+ public String apply(String s) {
+ return s.toLowerCase().replace('-', '_');
+ }
+ });
+ }
+ return Sets.newLinkedHashSet(values);
+ } else if (Type.LABEL_LIST == attrType) {
+ // Labels are most often used to change the extension,
+ // e.g. %.foo -> %.java, so we return the basename w/o extension.
+ return Sets.newLinkedHashSet(
+ Iterables.transform(rule.get(attrName, Type.LABEL_LIST),
+ new Function<Label, String>() {
+ @Override
+ public String apply(Label label) {
+ return FileSystemUtils.removeExtension(label.getName());
+ }
+ }));
+ } else if (Type.OUTPUT == attrType) {
+ Label out = rule.get(attrName, Type.OUTPUT);
+ return singleton(out.getName());
+ } else if (Type.OUTPUT_LIST == attrType) {
+ return Sets.newLinkedHashSet(
+ Iterables.transform(rule.get(attrName, Type.OUTPUT_LIST),
+ new Function<Label, String>() {
+ @Override
+ public String apply(Label label) {
+ return label.getName();
+ }
+ }));
+ }
+ throw new IllegalArgumentException(
+ "Don't know how to handle " + attrName + " : " + attrType);
+ }
+
+ /**
+ * Collects all named placeholders from the template while replacing them with %s.
+ *
+ * <p>Example: for {@code template} "%{name}_%{locales}.foo", it will return "%s_%s.foo" and
+ * store "name" and "locales" in {@code placeholders}.
+ *
+ * <p>Incomplete placeholders are treated like text: for "a-%{x}-%{y" this method returns
+ * "a-%s-%%{y" and stores "x" in {@code placeholders}.
+ *
+ * @param template a string with placeholders of the format %{...}
+ * @param placeholders a collection to collect placeholders into; may contain duplicates if not a
+ * Set
+ * @return a format string for {@link String#format}, created from the template string with every
+ * placeholder replaced by %s
+ */
+ public static String createPlaceholderSubstitutionFormatString(String template,
+ Collection<String> placeholders) {
+ return createPlaceholderSubstitutionFormatStringRecursive(template, placeholders,
+ new StringBuilder());
+ }
+
+ private static String createPlaceholderSubstitutionFormatStringRecursive(String template,
+ Collection<String> placeholders, StringBuilder formatBuilder) {
+ int start = template.indexOf("%{");
+ if (start < 0) {
+ return formatBuilder.append(PERCENT_ESCAPER.escape(template)).toString();
+ }
+
+ int end = template.indexOf("}", start + 2);
+ if (end < 0) {
+ return formatBuilder.append(PERCENT_ESCAPER.escape(template)).toString();
+ }
+
+ formatBuilder.append(PERCENT_ESCAPER.escape(template.substring(0, start))).append("%s");
+ placeholders.add(template.substring(start + 2, end));
+ return createPlaceholderSubstitutionFormatStringRecursive(template.substring(end + 1),
+ placeholders, formatBuilder);
+ }
+
+ /**
+ * Given a template string, replaces all placeholders of the form %{...} with
+ * the values from attributeSource. If there are multiple placeholders, then
+ * the output is the cross product of substitutions.
+ */
+ public static ImmutableList<String> substitutePlaceholderIntoTemplate(String template,
+ AttributeMap rule) {
+ return substitutePlaceholderIntoTemplate(template, rule, DEFAULT_RULE_ATTRIBUTE_GETTER, null);
+ }
+
+ /**
+ * Substitutes attribute-placeholders in a template string, producing all possible combinations.
+ *
+ * @param template the template string, may contain named placeholders for rule attributes, like
+ * <code>%{name}</code> or <code>%{deps}</code>
+ * @param rule the rule whose attributes the placeholders correspond to
+ * @param placeholdersInTemplate if specified, will contain all placeholders found in the
+ * template; may contain duplicates
+ * @return all possible combinations of the attributes referenced by the placeholders,
+ * substituted into the template; empty if any of the placeholders expands to no values
+ */
+ public static ImmutableList<String> substitutePlaceholderIntoTemplate(String template,
+ AttributeMap rule, AttributeValueGetter attributeGetter,
+ @Nullable List<String> placeholdersInTemplate) {
+ List<String> placeholders = (placeholdersInTemplate == null)
+ ? Lists.<String>newArrayList()
+ : placeholdersInTemplate;
+ String formatStr = createPlaceholderSubstitutionFormatString(template, placeholders);
+ if (placeholders.isEmpty()) {
+ return ImmutableList.of(template);
+ }
+
+ List<Set<String>> values = Lists.newArrayListWithCapacity(placeholders.size());
+ for (String placeholder : placeholders) {
+ Set<String> attrValues = attributeGetter.get(rule, placeholder);
+ if (attrValues.isEmpty()) {
+ return ImmutableList.<String>of();
+ }
+ values.add(attrValues);
+ }
+ ImmutableList.Builder<String> out = new ImmutableList.Builder<>();
+ for (List<String> combination : Sets.cartesianProduct(values)) {
+ out.add(String.format(formatStr, combination.toArray()));
+ }
+ return out.build();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/InputFile.java b/src/main/java/com/google/devtools/build/lib/packages/InputFile.java
new file mode 100644
index 0000000..130e2b7
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/InputFile.java
@@ -0,0 +1,124 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+/**
+ * A file that is an input to the build system.
+ *
+ * <p>In the build system, a file is considered an <i>input</i> file iff it is
+ * not generated by the build system (e.g. it's maintained under version
+ * control, or created by the test harness). It has nothing to do with the
+ * type of the file; a generated file containing <code>Java</code> source code
+ * is an OutputFile, not an InputFile.
+ */
+@Immutable @ThreadSafe
+public final class InputFile extends FileTarget {
+ private final Location location;
+ private final RuleVisibility visibility;
+ private final License license;
+
+ /**
+ * Constructs an input file with the given label, which must be a label for
+ * the given package, and package-default visibility.
+ */
+ InputFile(Package pkg, Label label, Location location) {
+ this(pkg, label, location, null, License.NO_LICENSE);
+ }
+
+ /**
+ * Constructs an input file with the given label, which must be a label for the given package
+ * that was parsed from the specified location, and has the specified visibility.
+ */
+ InputFile(Package pkg, Label label, Location location, RuleVisibility visibility,
+ License license) {
+ super(pkg, label);
+ Preconditions.checkNotNull(location);
+ this.location = location;
+ this.visibility = visibility;
+ this.license = license;
+ }
+
+ public boolean isVisibilitySpecified() {
+ return visibility != null;
+ }
+
+ @Override
+ public RuleVisibility getVisibility() {
+ if (visibility != null) {
+ return visibility;
+ } else {
+ return pkg.getDefaultVisibility();
+ }
+ }
+
+ public boolean isLicenseSpecified() {
+ return license != null && license != License.NO_LICENSE;
+ }
+
+ @Override
+ public License getLicense() {
+ if (license != null) {
+ return license;
+ } else {
+ return pkg.getDefaultLicense();
+ }
+ }
+
+ /**
+ * Returns the path to the location of the input file (which is necessarily
+ * within the source tree, not beneath <code>bin</code> or
+ * <code>genfiles</code>.
+ *
+ * <p>Prefer {@link #getExecPath} if possible.
+ */
+ public Path getPath() {
+ return pkg.getPackageDirectory().getRelative(label.getName());
+ }
+
+ /**
+ * Returns the exec path of the file, i.e. the path relative to the package source root.
+ */
+ public PathFragment getExecPath() {
+ return label.toPathFragment();
+ }
+
+ @Override
+ public int hashCode() {
+ return label.hashCode();
+ }
+
+ @Override
+ public String getTargetKind() {
+ return "source file";
+ }
+
+ @Override
+ public Rule getAssociatedRule() {
+ return null;
+ }
+
+ @Override
+ public Location getLocation() {
+ return location;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/InvalidPackageNameException.java b/src/main/java/com/google/devtools/build/lib/packages/InvalidPackageNameException.java
new file mode 100644
index 0000000..69561b8
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/InvalidPackageNameException.java
@@ -0,0 +1,29 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+/**
+ * Exception indicating that a package name was invalid.
+ */
+public class InvalidPackageNameException extends NoSuchPackageException {
+
+ public InvalidPackageNameException(String packageName, String message) {
+ super(packageName, message);
+ }
+
+ public InvalidPackageNameException(String packageName, String message, Throwable cause) {
+ super(packageName, message, cause);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/License.java b/src/main/java/com/google/devtools/build/lib/packages/License.java
new file mode 100644
index 0000000..fe63c9c
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/License.java
@@ -0,0 +1,327 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableTable;
+import com.google.common.collect.Sets;
+import com.google.common.collect.Table;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.syntax.Label.SyntaxException;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Support for license and distribution checking.
+ */
+@Immutable @ThreadSafe
+public final class License {
+
+ private final Set<LicenseType> licenseTypes;
+ private final Set<Label> exceptions;
+
+ /**
+ * The error that's thrown if a build file contains an invalid license string.
+ */
+ public static class LicenseParsingException extends Exception {
+ public LicenseParsingException(String s) {
+ super(s);
+ }
+ }
+
+ /**
+ * LicenseType is the basis of the License lattice - stricter licenses should
+ * be declared before less-strict licenses in the enum.
+ *
+ * <p>Note that the order is important for the purposes of finding the least
+ * restrictive license.
+ */
+ public enum LicenseType {
+ BY_EXCEPTION_ONLY,
+ RESTRICTED,
+ RESTRICTED_IF_STATICALLY_LINKED,
+ RECIPROCAL,
+ NOTICE,
+ PERMISSIVE,
+ UNENCUMBERED,
+ NONE
+ }
+
+ /**
+ * Gets the least restrictive license type from the list of licenses declared
+ * for a target. For the purposes of license checking, the license type set of
+ * a declared license can be reduced to its least restrictive member.
+ *
+ * @param types a collection of license types
+ * @return the least restrictive license type
+ */
+ @VisibleForTesting
+ static LicenseType leastRestrictive(Collection<LicenseType> types) {
+ return types.isEmpty() ? LicenseType.BY_EXCEPTION_ONLY : Collections.max(types);
+ }
+
+ /**
+ * An instance of LicenseType.None with no exceptions, used for packages
+ * outside of third_party which have no license clause in their BUILD files.
+ */
+ public static final License NO_LICENSE =
+ new License(ImmutableSet.of(LicenseType.NONE), Collections.<Label>emptySet());
+
+ /**
+ * A default instance of Distributions which is used for packages which
+ * have no "distribs" declaration. If nothing is declared, we opt for the
+ * most permissive kind of distribution, which is the internal-only distrib.
+ */
+ public static final Set<DistributionType> DEFAULT_DISTRIB =
+ Collections.singleton(DistributionType.INTERNAL);
+
+ /**
+ * The types of distribution that are supported.
+ */
+ public enum DistributionType {
+ INTERNAL,
+ WEB,
+ CLIENT,
+ EMBEDDED
+ }
+
+ /**
+ * Parses a set of strings declaring distribution types.
+ *
+ * @param distStrings strings containing distribution declarations from BUILD
+ * files
+ * @return a new, unmodifiable set of DistributionTypes
+ * @throws LicenseParsingException
+ */
+ public static Set<DistributionType> parseDistributions(Collection<String> distStrings)
+ throws LicenseParsingException {
+ if (distStrings.isEmpty()) {
+ return Collections.unmodifiableSet(EnumSet.of(DistributionType.INTERNAL));
+ } else {
+ Set<DistributionType> result = EnumSet.noneOf(DistributionType.class);
+ for (String distStr : distStrings) {
+ try {
+ DistributionType dist = Enum.valueOf(DistributionType.class, distStr.toUpperCase());
+ result.add(dist);
+ } catch (IllegalArgumentException e) {
+ throw new LicenseParsingException("Invalid distribution type '" + distStr + "'");
+ }
+ }
+ return Collections.unmodifiableSet(result);
+ }
+ }
+
+ private static final Object MARKER = new Object();
+
+ /**
+ * The license incompatibility set. This contains the set of
+ * (Distribution,License) pairs that should generate errors.
+ */
+ private static Table<DistributionType, LicenseType, Object> LICENSE_INCOMPATIBILIES =
+ createLicenseIncompatibilitySet();
+
+ private static Table<DistributionType, LicenseType, Object> createLicenseIncompatibilitySet() {
+ Table<DistributionType, LicenseType, Object> result = HashBasedTable.create();
+ result.put(DistributionType.CLIENT, LicenseType.RESTRICTED, MARKER);
+ result.put(DistributionType.EMBEDDED, LicenseType.RESTRICTED, MARKER);
+ result.put(DistributionType.INTERNAL, LicenseType.BY_EXCEPTION_ONLY, MARKER);
+ result.put(DistributionType.CLIENT, LicenseType.BY_EXCEPTION_ONLY, MARKER);
+ result.put(DistributionType.WEB, LicenseType.BY_EXCEPTION_ONLY, MARKER);
+ result.put(DistributionType.EMBEDDED, LicenseType.BY_EXCEPTION_ONLY, MARKER);
+ return ImmutableTable.copyOf(result);
+ }
+
+ /**
+ * The license warning set. This contains the set of
+ * (Distribution,License) pairs that should generate warnings when the user
+ * requests verbose license checking.
+ */
+ private static Table<DistributionType, LicenseType, Object> LICENSE_WARNINGS =
+ createLicenseWarningsSet();
+
+ private static Table<DistributionType, LicenseType, Object> createLicenseWarningsSet() {
+ Table<DistributionType, LicenseType, Object> result = HashBasedTable.create();
+ result.put(DistributionType.CLIENT, LicenseType.RECIPROCAL, MARKER);
+ result.put(DistributionType.EMBEDDED, LicenseType.RECIPROCAL, MARKER);
+ result.put(DistributionType.CLIENT, LicenseType.NOTICE, MARKER);
+ result.put(DistributionType.EMBEDDED, LicenseType.NOTICE, MARKER);
+ return ImmutableTable.copyOf(result);
+ }
+
+ private License(Set<LicenseType> licenseTypes, Set<Label> exceptions) {
+ // Defensive copy is done in .of()
+ this.licenseTypes = licenseTypes;
+ this.exceptions = exceptions;
+ }
+
+ public static License of(Collection<LicenseType> licenses, Collection<Label> exceptions) {
+ Set<LicenseType> licenseSet = ImmutableSet.copyOf(licenses);
+ Set<Label> exceptionSet = ImmutableSet.copyOf(exceptions);
+
+ if (exceptionSet.isEmpty() && licenseSet.equals(ImmutableSet.of(LicenseType.NONE))) {
+ return License.NO_LICENSE;
+ }
+
+ return new License(licenseSet, exceptionSet);
+ }
+ /**
+ * Computes a license which can be used to check if a package is compatible
+ * with some kinds of distribution. The list of licenses is scanned for the
+ * least restrictive, and the exceptions are added.
+ *
+ * @param licStrings the list of license strings declared for the package
+ * @throws LicenseParsingException if there are any parsing problems
+ */
+ public static License parseLicense(List<String> licStrings) throws LicenseParsingException {
+ /*
+ * The semantics of comparison for licenses depends on a stable iteration
+ * order for both license types and exceptions. For licenseTypes, it will be
+ * the comparison order from the enumerated types; for exceptions, it will
+ * be lexicographic order achieved using TreeSets.
+ */
+ Set<LicenseType> licenseTypes = EnumSet.noneOf(LicenseType.class);
+ Set<Label> exceptions = Sets.newTreeSet();
+ for (String str : licStrings) {
+ if (str.startsWith("exception=")) {
+ try {
+ Label label = Label.parseAbsolute(str.substring("exception=".length()));
+ exceptions.add(label);
+ } catch (SyntaxException e) {
+ throw new LicenseParsingException(e.getMessage());
+ }
+ } else {
+ try {
+ licenseTypes.add(LicenseType.valueOf(str.toUpperCase()));
+ } catch (IllegalArgumentException e) {
+ throw new LicenseParsingException("invalid license type: '" + str + "'");
+ }
+ }
+ }
+
+ return License.of(licenseTypes, exceptions);
+ }
+
+ /**
+ * Checks if this license is compatible with distributing a particular target
+ * in some set of distribution modes.
+ *
+ * @param dists the modes of distribution
+ * @param target the target which is being checked, and which will be used for
+ * checking exceptions
+ * @param licensedTarget the target which declared the license being checked.
+ * @param eventHandler a reporter where any licensing issues discovered should be
+ * reported
+ * @param staticallyLinked whether the target is statically linked under this command
+ * @return true if the license is compatible with the distributions
+ */
+ public boolean checkCompatibility(Set<DistributionType> dists,
+ Target target, Label licensedTarget, EventHandler eventHandler,
+ boolean staticallyLinked) {
+ Location location = (target instanceof Rule) ? ((Rule) target).getLocation() : null;
+
+ LicenseType leastRestrictiveLicense;
+ if (licenseTypes.contains(LicenseType.RESTRICTED_IF_STATICALLY_LINKED)) {
+ Set<LicenseType> tempLicenses = EnumSet.copyOf(licenseTypes);
+ tempLicenses.remove(LicenseType.RESTRICTED_IF_STATICALLY_LINKED);
+ if (staticallyLinked) {
+ tempLicenses.add(LicenseType.RESTRICTED);
+ } else {
+ tempLicenses.add(LicenseType.UNENCUMBERED);
+ }
+ leastRestrictiveLicense = leastRestrictive(tempLicenses);
+ } else {
+ leastRestrictiveLicense = leastRestrictive(licenseTypes);
+ }
+ for (DistributionType dt : dists) {
+ if (LICENSE_INCOMPATIBILIES.contains(dt, leastRestrictiveLicense)) {
+ if (!exceptions.contains(target.getLabel())) {
+ eventHandler.handle(Event.error(location, "Build target '" + target.getLabel()
+ + "' is not compatible with license '" + this + "' from target '"
+ + licensedTarget + "'"));
+ return false;
+ }
+ } else if (LICENSE_WARNINGS.contains(dt, leastRestrictiveLicense)) {
+ eventHandler.handle(
+ Event.warn(location, "Build target '" + target
+ + "' has a potential licensing issue "
+ + "with a '" + this + "' license from target '" + licensedTarget + "'"));
+ }
+ }
+ return true;
+ }
+
+ /**
+ * @return an immutable set of {@link LicenseType}s contained in this {@code
+ * License}
+ */
+ public Set<LicenseType> getLicenseTypes() {
+ return licenseTypes;
+ }
+
+ /**
+ * @return an immutable set of {@link Label}s that describe exceptions to the
+ * {@code License}
+ */
+ public Set<Label> getExceptions() {
+ return exceptions;
+ }
+
+ /**
+ * A simple toString implementation which generates a canonical form of the
+ * license. (The order of license types is guaranteed to be canonical by
+ * EnumSet, and the order of exceptions is guaranteed to be lexicographic
+ * order by TreeSet.)
+ */
+ @Override
+ public String toString() {
+ if (exceptions.isEmpty()) {
+ return licenseTypes.toString().toLowerCase();
+ } else {
+ return licenseTypes.toString().toLowerCase() + " with exceptions " + exceptions.toString();
+ }
+ }
+
+ /**
+ * A simple equals implementation leveraging the support built into Set that
+ * delegates to its contents.
+ */
+ @Override
+ public boolean equals(Object o) {
+ return o == this ||
+ o instanceof License &&
+ ((License) o).licenseTypes.equals(this.licenseTypes) &&
+ ((License) o).exceptions.equals(this.exceptions);
+ }
+
+ /**
+ * A simple hashCode implementation leveraging the support built into Set that
+ * delegates to its contents.
+ */
+ @Override
+ public int hashCode() {
+ return licenseTypes.hashCode() * 43 + exceptions.hashCode();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/MakeEnvironment.java b/src/main/java/com/google/devtools/build/lib/packages/MakeEnvironment.java
new file mode 100644
index 0000000..6c0d849
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/MakeEnvironment.java
@@ -0,0 +1,184 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nullable;
+
+/**
+ * Environment for varref variables (formerly called "Makefile
+ * variables").
+ *
+ * <p><code>update</code> emulates a very restricted subset of the behaviour of
+ * GNU Make's environment. In particular, does not attempt to simulate Make's
+ * complex range of assigment operators.
+ */
+@Immutable @ThreadSafe
+public class MakeEnvironment {
+ /**
+ * The platform set regexp that matches all platforms. Canonical.
+ */
+ public static final String MATCH_ANY = ".*";
+
+ // A platform-specific binding of a value for a given variable.
+ static class Binding {
+ private final String value;
+ private final String platformSetRegexp;
+
+ Binding(String value, String platformSetRegexp) {
+ this.value = value;
+ this.platformSetRegexp = platformSetRegexp;
+ }
+
+ @Override
+ public String toString() {
+ return value + " (" + platformSetRegexp + ")";
+ }
+
+ String getValue() {
+ return value;
+ }
+
+ String getPlatformSetRegexp() {
+ return platformSetRegexp;
+ }
+ }
+
+ // Maps each variable name to the [short] list of platform-specific bindings
+ // for it. The first matching binding is definitive.
+ private final ImmutableMap<String, ImmutableList<Binding>> env;
+
+ private MakeEnvironment(ImmutableMap<String, ImmutableList<Binding>> env) {
+ this.env = env;
+ }
+
+ /**
+ * @return the "Make" value from the environment whose name is "varname", or
+ * null iff the variable is not defined in the environment.
+ */
+ public String lookup(String varname, String platform) {
+ List<Binding> bindings = env.get(varname);
+ if (bindings == null) {
+ return null;
+ }
+ // First, look for a matching non-default binding.
+ // (The order in 'bindings' is the reverse of the order of vardefs in the BUILD file, so
+ // the first match in this for loop selects the last matching definition in the BUILD file.)
+ for (Binding binding : bindings) {
+ if (!binding.platformSetRegexp.equals(MATCH_ANY) &&
+ platform.matches(binding.platformSetRegexp)) {
+ return binding.value;
+ }
+ }
+ // If we didn't find a matching non-default binding,
+ // try using the last default binding.
+ for (Binding binding : bindings) {
+ if (binding.platformSetRegexp.equals(MATCH_ANY)) {
+ return binding.value;
+ }
+ }
+ return null;
+ }
+
+ Map<String, ImmutableList<Binding>> getBindings() {
+ return env;
+ }
+
+ /**
+ * Interface for creating a MakeEnvironment, settings its environment values,
+ * and exposing it in immutable state.
+ */
+ public static class Builder {
+ private final Map<String, LinkedList<Binding>> env = new HashMap<>();
+ private Map<String, String> platformSets = ImmutableMap.<String, String>of("any", MATCH_ANY);
+
+ /**
+ * Performs an update of Makefile variable 'var' to value 'value' for all
+ * platforms belonging to the specified 'platformSetRegexp'. Corresponds to
+ * vardef. We explicitly do not support the various complex nuances of
+ * Make's assignment operator.
+ *
+ * <p>The most recent binding for a particular variable takes precedence, even if
+ * a more specific binding came earlier.
+ *
+ * @param varname the name of the Makefile variable;
+ * @param value the string value to assign;
+ * @param platformSetRegexp a set of platforms for which this variable definition
+ * should take effect. This is expressed as a regexp over gplatform
+ * strings.
+ */
+ public void update(String varname, String value, String platformSetRegexp) {
+ if (varname == null || value == null || platformSetRegexp == null) {
+ throw new NullPointerException();
+ }
+ LinkedList<Binding> bindings = env.get(varname);
+ if (bindings == null) {
+ bindings = new LinkedList<Binding>();
+ env.put(varname, bindings);
+ }
+ // push new bindings onto head of list (=> most recent binding is
+ // definitive):
+ bindings.addFirst(new Binding(value, platformSetRegexp));
+ }
+
+ /**
+ * Sets the nickname to regexp mapping for <tt>vardef</tt>.
+ */
+ public void setPlatformSetRegexps(Map<String, String> sets) {
+ this.platformSets = sets;
+ }
+
+ @Nullable
+ public String getPlatformSetRegexp(String nickname) {
+ return this.platformSets.get(nickname);
+ }
+
+ /**
+ * Returns a new MakeEnvironment with environment settings corresponding
+ * to the cumulative results of this builder's {@link #update} calls.
+ */
+ public MakeEnvironment build() {
+ Map<String, ImmutableList<Binding>> newMap = new HashMap<>();
+ for (Map.Entry<String, LinkedList<Binding>> entry : env.entrySet()) {
+ newMap.put(entry.getKey(), ImmutableList.copyOf(entry.getValue()));
+ }
+ return new MakeEnvironment(ImmutableMap.copyOf(newMap));
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean equals(Object that) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String toString() {
+ return "MakeEnvironment=" + env;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/MethodLibrary.java b/src/main/java/com/google/devtools/build/lib/packages/MethodLibrary.java
new file mode 100644
index 0000000..5969697
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/MethodLibrary.java
@@ -0,0 +1,1053 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+import static com.google.devtools.build.lib.syntax.SkylarkFunction.cast;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.packages.Type.ConversionException;
+import com.google.devtools.build.lib.syntax.AbstractFunction;
+import com.google.devtools.build.lib.syntax.AbstractFunction.NoArgFunction;
+import com.google.devtools.build.lib.syntax.ClassObject;
+import com.google.devtools.build.lib.syntax.ClassObject.SkylarkClassObject;
+import com.google.devtools.build.lib.syntax.DotExpression;
+import com.google.devtools.build.lib.syntax.Environment;
+import com.google.devtools.build.lib.syntax.EvalException;
+import com.google.devtools.build.lib.syntax.EvalUtils;
+import com.google.devtools.build.lib.syntax.FuncallExpression;
+import com.google.devtools.build.lib.syntax.Function;
+import com.google.devtools.build.lib.syntax.MixedModeFunction;
+import com.google.devtools.build.lib.syntax.SelectorValue;
+import com.google.devtools.build.lib.syntax.SkylarkBuiltin;
+import com.google.devtools.build.lib.syntax.SkylarkBuiltin.Param;
+import com.google.devtools.build.lib.syntax.SkylarkEnvironment;
+import com.google.devtools.build.lib.syntax.SkylarkList;
+import com.google.devtools.build.lib.syntax.SkylarkModule;
+import com.google.devtools.build.lib.syntax.SkylarkNestedSet;
+import com.google.devtools.build.lib.syntax.SkylarkType;
+import com.google.devtools.build.lib.syntax.SkylarkType.SkylarkFunctionType;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.concurrent.ExecutionException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A helper class containing built in functions for the Build and the Build Extension Language.
+ */
+public class MethodLibrary {
+
+ private MethodLibrary() {}
+
+ // Convert string index in the same way Python does.
+ // If index is negative, starts from the end.
+ // If index is outside bounds, it is restricted to the valid range.
+ private static int getPythonStringIndex(int index, int stringLength) {
+ if (index < 0) {
+ index += stringLength;
+ }
+ return Math.max(Math.min(index, stringLength), 0);
+ }
+
+ // Emulate Python substring function
+ // It converts out of range indices, and never fails
+ private static String getPythonSubstring(String str, int start, int end) {
+ start = getPythonStringIndex(start, str.length());
+ end = getPythonStringIndex(end, str.length());
+ if (start > end) {
+ return "";
+ } else {
+ return str.substring(start, end);
+ }
+ }
+
+ public static int getListIndex(Object key, int listSize, FuncallExpression ast)
+ throws ConversionException, EvalException {
+ // Get the nth element in the list
+ int index = Type.INTEGER.convert(key, "index operand");
+ if (index < 0) {
+ index += listSize;
+ }
+ if (index < 0 || index >= listSize) {
+ throw new EvalException(ast.getLocation(), "List index out of range (index is "
+ + index + ", but list has " + listSize + " elements)");
+ }
+ return index;
+ }
+
+ // supported string methods
+
+ @SkylarkBuiltin(name = "join", objectType = StringModule.class, returnType = String.class,
+ doc = "Returns a string in which the string elements of the argument have been "
+ + "joined by this string as a separator. Example:<br>"
+ + "<pre class=language-python>\"|\".join([\"a\", \"b\", \"c\"]) == \"a|b|c\"</pre>",
+ mandatoryParams = {
+ @Param(name = "elements", type = SkylarkList.class, doc = "The objects to join.")})
+ private static Function join = new MixedModeFunction("join",
+ ImmutableList.of("this", "elements"), 2, false) {
+ @Override
+ public Object call(Object[] args, FuncallExpression ast) throws ConversionException {
+ String thiz = Type.STRING.convert(args[0], "'join' operand");
+ List<?> seq = Type.OBJECT_LIST.convert(args[1], "'join' argument");
+ StringBuilder sb = new StringBuilder();
+ for (Iterator<?> i = seq.iterator(); i.hasNext();) {
+ sb.append(i.next().toString());
+ if (i.hasNext()) {
+ sb.append(thiz);
+ }
+ }
+ return sb.toString();
+ }
+ };
+
+ @SkylarkBuiltin(name = "lower", objectType = StringModule.class, returnType = String.class,
+ doc = "Returns the lower case version of this string.")
+ private static Function lower = new MixedModeFunction("lower",
+ ImmutableList.of("this"), 1, false) {
+ @Override
+ public Object call(Object[] args, FuncallExpression ast) throws ConversionException {
+ String thiz = Type.STRING.convert(args[0], "'lower' operand");
+ return thiz.toLowerCase();
+ }
+ };
+
+ @SkylarkBuiltin(name = "upper", objectType = StringModule.class, returnType = String.class,
+ doc = "Returns the upper case version of this string.")
+ private static Function upper = new MixedModeFunction("upper",
+ ImmutableList.of("this"), 1, false) {
+ @Override
+ public Object call(Object[] args, FuncallExpression ast) throws ConversionException {
+ String thiz = Type.STRING.convert(args[0], "'upper' operand");
+ return thiz.toUpperCase();
+ }
+ };
+
+ @SkylarkBuiltin(name = "replace", objectType = StringModule.class, returnType = String.class,
+ doc = "Returns a copy of the string in which the occurrences "
+ + "of <code>old</code> have been replaced with <code>new</code>, optionally restricting "
+ + "the number of replacements to <code>maxsplit</code>.",
+ mandatoryParams = {
+ @Param(name = "old", type = String.class, doc = "The string to be replaced."),
+ @Param(name = "new", type = String.class, doc = "The string to replace with.")},
+ optionalParams = {
+ @Param(name = "maxsplit", type = Integer.class, doc = "The maximum number of replacements.")})
+ private static Function replace =
+ new MixedModeFunction("replace", ImmutableList.of("this", "old", "new", "maxsplit"), 3, false) {
+ @Override
+ public Object call(Object[] args, FuncallExpression ast) throws EvalException,
+ ConversionException {
+ String thiz = Type.STRING.convert(args[0], "'replace' operand");
+ String old = Type.STRING.convert(args[1], "'replace' argument");
+ String neww = Type.STRING.convert(args[2], "'replace' argument");
+ int maxsplit =
+ args[3] != null ? Type.INTEGER.convert(args[3], "'replace' argument")
+ : Integer.MAX_VALUE;
+ StringBuffer sb = new StringBuffer();
+ try {
+ Matcher m = Pattern.compile(old, Pattern.LITERAL).matcher(thiz);
+ for (int i = 0; i < maxsplit && m.find(); i++) {
+ m.appendReplacement(sb, Matcher.quoteReplacement(neww));
+ }
+ m.appendTail(sb);
+ } catch (IllegalStateException e) {
+ throw new EvalException(ast.getLocation(), e.getMessage() + " in call to replace");
+ }
+ return sb.toString();
+ }
+ };
+
+ @SkylarkBuiltin(name = "split", objectType = StringModule.class, returnType = SkylarkList.class,
+ doc = "Returns a list of all the words in the string, using <code>sep</code> "
+ + "as the separator, optionally limiting the number of splits to <code>maxsplit</code>.",
+ optionalParams = {
+ @Param(name = "sep", type = String.class,
+ doc = "The string to split on, default is space (\" \")."),
+ @Param(name = "maxsplit", type = Integer.class, doc = "The maximum number of splits.")})
+ private static Function split = new MixedModeFunction("split",
+ ImmutableList.of("this", "sep", "maxsplit"), 1, false) {
+ @Override
+ public Object call(Object[] args, FuncallExpression ast, Environment env)
+ throws ConversionException {
+ String thiz = Type.STRING.convert(args[0], "'split' operand");
+ String sep = args[1] != null
+ ? Type.STRING.convert(args[1], "'split' argument")
+ : " ";
+ int maxsplit = args[2] != null
+ ? Type.INTEGER.convert(args[2], "'split' argument") + 1 // last is remainder
+ : -1;
+ String[] ss = Pattern.compile(sep, Pattern.LITERAL).split(thiz,
+ maxsplit);
+ List<String> result = java.util.Arrays.asList(ss);
+ return env.isSkylarkEnabled() ? SkylarkList.list(result, String.class) : result;
+ }
+ };
+
+ @SkylarkBuiltin(name = "rfind", objectType = StringModule.class, returnType = Integer.class,
+ doc = "Returns the last index where <code>sub</code> is found, "
+ + "or -1 if no such index exists, optionally restricting to "
+ + "[<code>start</code>:<code>end</code>], "
+ + "<code>start</code> being inclusive and <code>end</code> being exclusive.",
+ mandatoryParams = {
+ @Param(name = "sub", type = String.class, doc = "The substring to find.")},
+ optionalParams = {
+ @Param(name = "start", type = Integer.class, doc = "Restrict to search from this position."),
+ @Param(name = "end", type = Integer.class, doc = "Restrict to search before this position.")})
+ private static Function rfind =
+ new MixedModeFunction("rfind", ImmutableList.of("this", "sub", "start", "end"), 2, false) {
+ @Override
+ public Object call(Object[] args, FuncallExpression ast)
+ throws ConversionException {
+ String thiz = Type.STRING.convert(args[0], "'rfind' operand");
+ String sub = Type.STRING.convert(args[1], "'rfind' argument");
+ int start = 0;
+ if (args[2] != null) {
+ start = Type.INTEGER.convert(args[2], "'rfind' argument");
+ }
+ int end = thiz.length();
+ if (args[3] != null) {
+ end = Type.INTEGER.convert(args[3], "'rfind' argument");
+ }
+ int subpos = getPythonSubstring(thiz, start, end).lastIndexOf(sub);
+ start = getPythonStringIndex(start, thiz.length());
+ return subpos < 0 ? subpos : subpos + start;
+ }
+ };
+
+ @SkylarkBuiltin(name = "find", objectType = StringModule.class, returnType = Integer.class,
+ doc = "Returns the first index where <code>sub</code> is found, "
+ + "or -1 if no such index exists, optionally restricting to "
+ + "[<code>start</code>:<code>end]</code>, "
+ + "<code>start</code> being inclusive and <code>end</code> being exclusive.",
+ mandatoryParams = {
+ @Param(name = "sub", type = String.class, doc = "The substring to find.")},
+ optionalParams = {
+ @Param(name = "start", type = Integer.class, doc = "Restrict to search from this position."),
+ @Param(name = "end", type = Integer.class, doc = "Restrict to search before this position.")})
+ private static Function find =
+ new MixedModeFunction("find", ImmutableList.of("this", "sub", "start", "end"), 2, false) {
+ @Override
+ public Object call(Object[] args, FuncallExpression ast)
+ throws ConversionException {
+ String thiz = Type.STRING.convert(args[0], "'find' operand");
+ String sub = Type.STRING.convert(args[1], "'find' argument");
+ int start = 0;
+ if (args[2] != null) {
+ start = Type.INTEGER.convert(args[2], "'find' argument");
+ }
+ int end = thiz.length();
+ if (args[3] != null) {
+ end = Type.INTEGER.convert(args[3], "'find' argument");
+ }
+ int subpos = getPythonSubstring(thiz, start, end).indexOf(sub);
+ start = getPythonStringIndex(start, thiz.length());
+ return subpos < 0 ? subpos : subpos + start;
+ }
+ };
+
+ @SkylarkBuiltin(name = "count", objectType = StringModule.class, returnType = Integer.class,
+ doc = "Returns the number of (non-overlapping) occurrences of substring <code>sub</code> in "
+ + "string, optionally restricting to [<code>start</code>:<code>end</code>], "
+ + "<code>start</code> being inclusive and <code>end</code> being exclusive.",
+ mandatoryParams = {
+ @Param(name = "sub", type = String.class, doc = "The substring to count.")},
+ optionalParams = {
+ @Param(name = "start", type = Integer.class, doc = "Restrict to search from this position."),
+ @Param(name = "end", type = Integer.class, doc = "Restrict to search before this position.")})
+ private static Function count =
+ new MixedModeFunction("count", ImmutableList.of("this", "sub", "start", "end"), 2, false) {
+ @Override
+ public Object call(Object[] args, FuncallExpression ast)
+ throws ConversionException {
+ String thiz = Type.STRING.convert(args[0], "'count' operand");
+ String sub = Type.STRING.convert(args[1], "'count' argument");
+ int start = 0;
+ if (args[2] != null) {
+ start = Type.INTEGER.convert(args[2], "'count' argument");
+ }
+ int end = thiz.length();
+ if (args[3] != null) {
+ end = Type.INTEGER.convert(args[3], "'count' argument");
+ }
+ String str = getPythonSubstring(thiz, start, end);
+ if (sub.equals("")) {
+ return str.length() + 1;
+ }
+ int count = 0;
+ int index = -1;
+ while ((index = str.indexOf(sub)) >= 0) {
+ count++;
+ str = str.substring(index + sub.length());
+ }
+ return count;
+ }
+ };
+
+ @SkylarkBuiltin(name = "endswith", objectType = StringModule.class, returnType = Boolean.class,
+ doc = "Returns True if the string ends with <code>sub</code>, "
+ + "otherwise False, optionally restricting to [<code>start</code>:<code>end</code>], "
+ + "<code>start</code> being inclusive and <code>end</code> being exclusive.",
+ mandatoryParams = {
+ @Param(name = "sub", type = String.class, doc = "The substring to check.")},
+ optionalParams = {
+ @Param(name = "start", type = Integer.class, doc = "Test beginning at this position."),
+ @Param(name = "end", type = Integer.class, doc = "Stop comparing at this position.")})
+ private static Function endswith =
+ new MixedModeFunction("endswith", ImmutableList.of("this", "sub", "start", "end"), 2, false) {
+ @Override
+ public Object call(Object[] args, FuncallExpression ast)
+ throws ConversionException {
+ String thiz = Type.STRING.convert(args[0], "'endswith' operand");
+ String sub = Type.STRING.convert(args[1], "'endswith' argument");
+ int start = 0;
+ if (args[2] != null) {
+ start = Type.INTEGER.convert(args[2], "'endswith' argument");
+ }
+ int end = thiz.length();
+ if (args[3] != null) {
+ end = Type.INTEGER.convert(args[3], "");
+ }
+
+ return getPythonSubstring(thiz, start, end).endsWith(sub);
+ }
+ };
+
+ @SkylarkBuiltin(name = "startswith", objectType = StringModule.class, returnType = Boolean.class,
+ doc = "Returns True if the string starts with <code>sub</code>, "
+ + "otherwise False, optionally restricting to [<code>start</code>:<code>end</code>], "
+ + "<code>start</code> being inclusive and <code>end</code> being exclusive.",
+ mandatoryParams = {
+ @Param(name = "sub", type = String.class, doc = "The substring to check.")},
+ optionalParams = {
+ @Param(name = "start", type = Integer.class, doc = "Test beginning at this position."),
+ @Param(name = "end", type = Integer.class, doc = "Stop comparing at this position.")})
+ private static Function startswith =
+ new MixedModeFunction("startswith", ImmutableList.of("this", "sub", "start", "end"), 2, false) {
+ @Override
+ public Object call(Object[] args, FuncallExpression ast) throws ConversionException {
+ String thiz = Type.STRING.convert(args[0], "'startswith' operand");
+ String sub = Type.STRING.convert(args[1], "'startswith' argument");
+ int start = 0;
+ if (args[2] != null) {
+ start = Type.INTEGER.convert(args[2], "'startswith' argument");
+ }
+ int end = thiz.length();
+ if (args[3] != null) {
+ end = Type.INTEGER.convert(args[3], "'startswith' argument");
+ }
+ return getPythonSubstring(thiz, start, end).startsWith(sub);
+ }
+ };
+
+ // TODO(bazel-team): Maybe support an argument to tell the type of the whitespace.
+ @SkylarkBuiltin(name = "strip", objectType = StringModule.class, returnType = String.class,
+ doc = "Returns a copy of the string in which all whitespace characters "
+ + "have been stripped from the beginning and the end of the string.")
+ private static Function strip =
+ new MixedModeFunction("strip", ImmutableList.of("this"), 1, false) {
+ @Override
+ public Object call(Object[] args, FuncallExpression ast)
+ throws ConversionException {
+ String operand = Type.STRING.convert(args[0], "'strip' operand");
+ return operand.trim();
+ }
+ };
+
+ // substring operator
+ @SkylarkBuiltin(name = "$substring", hidden = true,
+ doc = "String[<code>start</code>:<code>end</code>] returns a substring.")
+ private static Function substring = new MixedModeFunction("$substring",
+ ImmutableList.of("this", "start", "end"), 3, false) {
+ @Override
+ public Object call(Object[] args, FuncallExpression ast) throws ConversionException {
+ String thiz = Type.STRING.convert(args[0], "substring operand");
+ int left = Type.INTEGER.convert(args[1], "substring operand");
+ int right = Type.INTEGER.convert(args[2], "substring operand");
+ return getPythonSubstring(thiz, left, right);
+ }
+ };
+
+ // supported list methods
+ @SkylarkBuiltin(name = "append", hidden = true,
+ doc = "Adds an item to the end of the list.")
+ private static Function append = new MixedModeFunction("append",
+ ImmutableList.of("this", "x"), 2, false) {
+ @Override
+ public Object call(Object[] args, FuncallExpression ast) throws EvalException,
+ ConversionException {
+ List<Object> thiz = Type.OBJECT_LIST.convert(args[0], "'append' operand");
+ thiz.add(args[1]);
+ return Environment.NONE;
+ }
+ };
+
+ @SkylarkBuiltin(name = "extend", hidden = true,
+ doc = "Adds all items to the end of the list.")
+ private static Function extend = new MixedModeFunction("extend",
+ ImmutableList.of("this", "x"), 2, false) {
+ @Override
+ public Object call(Object[] args, FuncallExpression ast) throws EvalException,
+ ConversionException {
+ List<Object> thiz = Type.OBJECT_LIST.convert(args[0], "'extend' operand");
+ List<Object> l = Type.OBJECT_LIST.convert(args[1], "'extend' argument");
+ thiz.addAll(l);
+ return Environment.NONE;
+ }
+ };
+
+ // dictionary access operator
+ @SkylarkBuiltin(name = "$index", hidden = true,
+ doc = "Returns the nth element of a list or string, "
+ + "or looks up a value in a dictionary.")
+ private static Function index = new MixedModeFunction("$index",
+ ImmutableList.of("this", "index"), 2, false) {
+ @Override
+ public Object call(Object[] args, FuncallExpression ast) throws EvalException,
+ ConversionException {
+ Object collectionCandidate = args[0];
+ Object key = args[1];
+
+ if (collectionCandidate instanceof Map<?, ?>) {
+ Map<?, ?> dictionary = (Map<?, ?>) collectionCandidate;
+ if (!dictionary.containsKey(key)) {
+ throw new EvalException(ast.getLocation(), "Key '" + key + "' not found in dictionary");
+ }
+ return dictionary.get(key);
+ } else if (collectionCandidate instanceof List<?>) {
+
+ List<Object> list = Type.OBJECT_LIST.convert(collectionCandidate, "index operand");
+
+ if (!list.isEmpty()) {
+ int index = getListIndex(key, list.size(), ast);
+ return list.get(index);
+ }
+
+ throw new EvalException(ast.getLocation(), "List is empty");
+ } else if (collectionCandidate instanceof SkylarkList) {
+ SkylarkList list = (SkylarkList) collectionCandidate;
+
+ if (!list.isEmpty()) {
+ int index = getListIndex(key, list.size(), ast);
+ return list.get(index);
+ }
+
+ throw new EvalException(ast.getLocation(), "List is empty");
+ } else if (collectionCandidate instanceof String) {
+ String str = (String) collectionCandidate;
+ int index = getListIndex(key, str.length(), ast);
+ return str.substring(index, index + 1);
+
+ } else {
+ // TODO(bazel-team): This is dead code, get rid of it.
+ throw new EvalException(ast.getLocation(), String.format(
+ "Unsupported datatype (%s) for indexing, only works for dict and list",
+ EvalUtils.getDatatypeName(collectionCandidate)));
+ }
+ }
+ };
+
+ @SkylarkBuiltin(name = "values", objectType = DictModule.class, returnType = SkylarkList.class,
+ doc = "Return the list of values.")
+ private static Function values = new NoArgFunction("values") {
+ @Override
+ public Object call(Object self, FuncallExpression ast, Environment env)
+ throws EvalException, InterruptedException {
+ Map<?, ?> dict = (Map<?, ?>) self;
+ return convert(dict.values(), env, ast.getLocation());
+ }
+ };
+
+ @SkylarkBuiltin(name = "items", objectType = DictModule.class, returnType = SkylarkList.class,
+ doc = "Return the list of key-value tuples.")
+ private static Function items = new NoArgFunction("items") {
+ @Override
+ public Object call(Object self, FuncallExpression ast, Environment env)
+ throws EvalException, InterruptedException {
+ Map<?, ?> dict = (Map<?, ?>) self;
+ List<Object> list = Lists.newArrayListWithCapacity(dict.size());
+ for (Map.Entry<?, ?> entries : dict.entrySet()) {
+ List<?> item = ImmutableList.of(entries.getKey(), entries.getValue());
+ list.add(env.isSkylarkEnabled() ? SkylarkList.tuple(item) : item);
+ }
+ return convert(list, env, ast.getLocation());
+ }
+ };
+
+ @SkylarkBuiltin(name = "keys", objectType = DictModule.class, returnType = SkylarkList.class,
+ doc = "Return the list of keys.")
+ private static Function keys = new NoArgFunction("keys") {
+ @Override
+ public Object call(Object self, FuncallExpression ast, Environment env)
+ throws EvalException, InterruptedException {
+ Map<?, ?> dict = (Map<?, ?>) self;
+ return convert(dict.keySet(), env, ast.getLocation());
+ }
+ };
+
+ @SuppressWarnings("unchecked")
+ private static Iterable<Object> convert(Collection<?> list, Environment env, Location loc)
+ throws EvalException {
+ if (env.isSkylarkEnabled()) {
+ return SkylarkList.list(list, loc);
+ } else {
+ return Lists.newArrayList(list);
+ }
+ }
+
+ // unary minus
+ @SkylarkBuiltin(name = "-", hidden = true, doc = "Unary minus operator.")
+ private static Function minus = new MixedModeFunction("-", ImmutableList.of("this"), 1, false) {
+ @Override
+ public Object call(Object[] args, FuncallExpression ast) throws ConversionException {
+ int num = Type.INTEGER.convert(args[0], "'unary minus' argument");
+ return -num;
+ }
+ };
+
+ @SkylarkBuiltin(name = "list", returnType = SkylarkList.class,
+ doc = "Converts a collection (e.g. set or dictionary) to a list.",
+ mandatoryParams = {@Param(name = "x", doc = "The object to convert.")})
+ private static Function list = new MixedModeFunction("list",
+ ImmutableList.of("list"), 1, false) {
+ @Override
+ public Object call(Object[] args, FuncallExpression ast) throws EvalException {
+ Location loc = ast.getLocation();
+ return SkylarkList.list(EvalUtils.toCollection(args[0], loc), loc);
+ }
+ };
+
+ @SkylarkBuiltin(name = "len", returnType = Integer.class, doc =
+ "Returns the length of a string, list, tuple, set, or dictionary.",
+ mandatoryParams = {@Param(name = "x", doc = "The object to check length of.")})
+ private static Function len = new MixedModeFunction("len",
+ ImmutableList.of("list"), 1, false) {
+
+ @Override
+ public Object call(Object[] args, FuncallExpression ast) throws EvalException {
+ Object arg = args[0];
+ int l = EvalUtils.size(arg);
+ if (l == -1) {
+ throw new EvalException(ast.getLocation(),
+ EvalUtils.getDatatypeName(arg) + " is not iterable");
+ }
+ return l;
+ }
+ };
+
+ @SkylarkBuiltin(name = "str", returnType = String.class, doc =
+ "Converts any object to string. This is useful for debugging.",
+ mandatoryParams = {@Param(name = "x", doc = "The object to convert.")})
+ private static Function str = new MixedModeFunction("str", ImmutableList.of("this"), 1, false) {
+ @Override
+ public Object call(Object[] args, FuncallExpression ast) throws EvalException {
+ return EvalUtils.printValue(args[0]);
+ }
+ };
+
+ @SkylarkBuiltin(name = "bool", returnType = Boolean.class, doc = "Converts an object to boolean. "
+ + "It returns False if the object is None, False, an empty string, the number 0, or an "
+ + "empty collection. Otherwise, it returns True.",
+ mandatoryParams = {@Param(name = "x", doc = "The variable to convert.")})
+ private static Function bool = new MixedModeFunction("bool",
+ ImmutableList.of("this"), 1, false) {
+ @Override
+ public Object call(Object[] args, FuncallExpression ast) throws EvalException {
+ return EvalUtils.toBoolean(args[0]);
+ }
+ };
+
+ @SkylarkBuiltin(name = "struct", returnType = SkylarkClassObject.class, doc =
+ "Creates an immutable struct using the keyword arguments as fields. It is used to group "
+ + "multiple values together.Example:<br>"
+ + "<pre class=language-python>s = struct(x = 2, y = 3)\n"
+ + "return s.x + s.y # returns 5</pre>")
+ private static Function struct = new AbstractFunction("struct") {
+
+ @Override
+ public Object call(List<Object> args, Map<String, Object> kwargs, FuncallExpression ast,
+ Environment env) throws EvalException, InterruptedException {
+ if (args.size() > 0) {
+ throw new EvalException(ast.getLocation(), "struct only supports keyword arguments");
+ }
+ return new SkylarkClassObject(kwargs, ast.getLocation());
+ }
+ };
+
+ @SkylarkBuiltin(name = "set", returnType = SkylarkNestedSet.class,
+ doc = "Creates a set from the <code>items</code>, that supports nesting. "
+ + "The nesting is applied to other nested sets among <code>items</code>.<br>"
+ + "Examples:<br>"
+ + "<pre class=language-python>set([1, set([2, 3]), 2])\n"
+ + "set([1, 2, 3], order=\"compile\")</pre>",
+ optionalParams = {
+ @Param(name = "items", type = SkylarkList.class,
+ doc = "The items to initialize the set with."),
+ @Param(name = "order", type = String.class,
+ doc = "The ordering strategy for the set if it's nested, "
+ + "possible values are: <code>stable</code> (default), <code>compile</code>, "
+ + "<code>link</code> or <code>naive_link</code>.")})
+ private static final Function set =
+ new MixedModeFunction("set", ImmutableList.of("items", "order"), 0, false) {
+ @Override
+ public Object call(Object[] args, FuncallExpression ast) throws EvalException,
+ ConversionException {
+ Order order;
+ if (args[1] == null || args[1].equals("stable")) {
+ order = Order.STABLE_ORDER;
+ } else if (args[1].equals("compile")) {
+ order = Order.COMPILE_ORDER;
+ } else if (args[1].equals("link")) {
+ order = Order.LINK_ORDER;
+ } else if (args[1].equals("naive_link")) {
+ order = Order.NAIVE_LINK_ORDER;
+ } else {
+ throw new EvalException(ast.getLocation(), "Invalid order: " + args[1]);
+ }
+
+ if (args[0] == null) {
+ return new SkylarkNestedSet(order, SkylarkList.EMPTY_LIST, ast.getLocation());
+ }
+ return new SkylarkNestedSet(order, args[0], ast.getLocation());
+ }
+ };
+
+ @SkylarkBuiltin(name = "enumerate", returnType = SkylarkList.class,
+ doc = "Return a list of pairs, with the index (int) and the item from the input list.\n"
+ + "<pre class=language-python>"
+ + "enumerate([24, 21, 84]) == [[0, 24], [1, 21], [2, 84]]</pre>\n",
+ mandatoryParams = {
+ @Param(name = "list", type = SkylarkList.class,
+ doc = "input list"),
+ })
+ private static Function enumerate = new MixedModeFunction("enumerate",
+ ImmutableList.of("list"), 1, false) {
+ @Override
+ public Object call(Object[] args, FuncallExpression ast) throws EvalException,
+ ConversionException {
+ List<Object> input = Type.OBJECT_LIST.convert(args[0], "'enumerate' operand");
+ List<List<Object>> result = Lists.newArrayList();
+ int count = 0;
+ for (Object obj : input) {
+ result.add(Lists.newArrayList(count, obj));
+ count++;
+ }
+ return result;
+ }
+ };
+
+ @SkylarkBuiltin(name = "range", returnType = SkylarkList.class,
+ doc = "Creates a list where items go from <code>start</code> to <end>, using a "
+ + "<code>step</code> increment. If a single argument is provided, items will "
+ + "range from 0 to that element."
+ + "<pre class=language-python>range(4) == [0, 1, 2, 3]\n"
+ + "range(3, 9, 2) == [3, 5, 7]\n"
+ + "range(3, 0, -1) == [3, 2, 1]</pre>",
+ mandatoryParams = {
+ @Param(name = "start", type = Integer.class,
+ doc = "Value of the first element"),
+ },
+ optionalParams = {
+ @Param(name = "end", type = SkylarkList.class,
+ doc = "Generation of the list stops before <code>end</code> is reached."),
+ @Param(name = "step", type = String.class,
+ doc = "The increment (default is 1). It may be negative.")})
+ private static final Function range =
+ new MixedModeFunction("range", ImmutableList.of("start", "stop", "step"), 1, false) {
+ @Override
+ public Object call(Object[] args, FuncallExpression ast) throws EvalException,
+ ConversionException {
+ int start;
+ int stop;
+ if (args[1] == null) {
+ start = 0;
+ stop = Type.INTEGER.convert(args[0], "stop");
+ } else {
+ start = Type.INTEGER.convert(args[0], "start");
+ stop = Type.INTEGER.convert(args[1], "stop");
+ }
+ int step = args[2] == null ? 1 : Type.INTEGER.convert(args[2], "step");
+ if (step == 0) {
+ throw new EvalException(ast.getLocation(), "step cannot be 0");
+ }
+ List<Integer> result = Lists.newArrayList();
+ if (step > 0) {
+ while (start < stop) {
+ result.add(start);
+ start += step;
+ }
+ } else {
+ while (start > stop) {
+ result.add(start);
+ start += step;
+ }
+ }
+ return SkylarkList.list(result, Integer.class);
+ }
+ };
+
+ /**
+ * Returns a function-value implementing "select" (i.e. configurable attributes)
+ * in the specified package context.
+ */
+ @SkylarkBuiltin(name = "select",
+ doc = "Creates a SelectorValue from the dict parameter.",
+ mandatoryParams = {@Param(name = "x", type = Map.class, doc = "The parameter to convert.")})
+ private static final Function select = new MixedModeFunction("select",
+ ImmutableList.of("x"), 1, false) {
+ @Override
+ public Object call(Object[] args, FuncallExpression ast)
+ throws EvalException, ConversionException {
+ Object dict = args[0];
+ if (!(dict instanceof Map<?, ?>)) {
+ throw new EvalException(ast.getLocation(),
+ "select({...}) argument isn't a dictionary");
+ }
+ return new SelectorValue((Map<?, ?>) dict);
+ }
+ };
+
+ /**
+ * Returns true if the object has a field of the given name, otherwise false.
+ */
+ @SkylarkBuiltin(name = "hasattr", returnType = Boolean.class,
+ doc = "Returns True if the object <code>x</code> has a field of the given <code>name</code>, "
+ + "otherwise False. Example:<br>"
+ + "<pre class=language-python>hasattr(ctx.attr, \"myattr\")</pre>",
+ mandatoryParams = {
+ @Param(name = "object", doc = "The object to check."),
+ @Param(name = "name", type = String.class, doc = "The name of the field.")})
+ private static final Function hasattr =
+ new MixedModeFunction("hasattr", ImmutableList.of("object", "name"), 2, false) {
+
+ @Override
+ public Object call(Object[] args, FuncallExpression ast, Environment env)
+ throws EvalException, ConversionException {
+ Object obj = args[0];
+ String name = cast(args[1], String.class, "name", ast.getLocation());
+
+ if (obj instanceof ClassObject && ((ClassObject) obj).getValue(name) != null) {
+ return true;
+ }
+
+ if (env.getFunctionNames(obj.getClass()).contains(name)) {
+ return true;
+ }
+
+ try {
+ return FuncallExpression.getMethodNames(obj.getClass()).contains(name);
+ } catch (ExecutionException e) {
+ // This shouldn't happen
+ throw new EvalException(ast.getLocation(), e.getMessage());
+ }
+ }
+ };
+
+ @SkylarkBuiltin(name = "getattr",
+ doc = "Returns the struct's field of the given name if exists, otherwise <code>default</code>"
+ + " if specified, otherwise rasies an error. For example, <code>getattr(x, \"foobar\")"
+ + "</code> is equivalent to <code>x.foobar</code>."
+ + "Example:<br>"
+ + "<pre class=language-python>getattr(ctx.attr, \"myattr\")\n"
+ + "getattr(ctx.attr, \"myattr\", \"mydefault\")</pre>",
+ mandatoryParams = {
+ @Param(name = "object", doc = "The struct which's field is accessed."),
+ @Param(name = "name", doc = "The name of the struct field.")},
+ optionalParams = {
+ @Param(name = "default", doc = "The default value to return in case the struct "
+ + "doesn't have a field of the given name.")})
+ private static final Function getattr = new MixedModeFunction(
+ "getattr", ImmutableList.of("object", "name", "default"), 2, false) {
+ @Override
+ public Object call(Object[] args, FuncallExpression ast, Environment env)
+ throws EvalException {
+ Object obj = args[0];
+ String name = cast(args[1], String.class, "name", ast.getLocation());
+ Object result = DotExpression.eval(obj, name, ast.getLocation());
+ if (result == null) {
+ if (args[2] != null) {
+ return args[2];
+ } else {
+ throw new EvalException(ast.getLocation(), "Object of type '"
+ + EvalUtils.getDatatypeName(obj) + "' has no field '" + name + "'");
+ }
+ }
+ return result;
+ }
+ };
+
+ @SkylarkBuiltin(name = "dir", returnType = SkylarkList.class,
+ doc = "Returns the list of the names (list of strings) of the fields and "
+ + "methods of the parameter object.",
+ mandatoryParams = {@Param(name = "object", doc = "The object to check.")})
+ private static final Function dir = new MixedModeFunction(
+ "dir", ImmutableList.of("object"), 1, false) {
+ @Override
+ public Object call(Object[] args, FuncallExpression ast, Environment env)
+ throws EvalException {
+ Object obj = args[0];
+ // Order the fields alphabetically.
+ Set<String> fields = new TreeSet<>();
+ if (obj instanceof ClassObject) {
+ fields.addAll(((ClassObject) obj).getKeys());
+ }
+ fields.addAll(env.getFunctionNames(obj.getClass()));
+ try {
+ fields.addAll(FuncallExpression.getMethodNames(obj.getClass()));
+ } catch (ExecutionException e) {
+ // This shouldn't happen
+ throw new EvalException(ast.getLocation(), e.getMessage());
+ }
+ return SkylarkList.list(fields, String.class);
+ }
+ };
+
+ @SkylarkBuiltin(name = "type", returnType = String.class,
+ doc = "Returns the type name of its argument.",
+ mandatoryParams = {@Param(name = "object", doc = "The object to check type of.")})
+ private static final Function type = new MixedModeFunction("type",
+ ImmutableList.of("object"), 1, false) {
+ @Override
+ public Object call(Object[] args, FuncallExpression ast) throws EvalException {
+ // There is no 'type' type in Skylark, so we return a string with the type name.
+ return EvalUtils.getDatatypeName(args[0]);
+ }
+ };
+
+ @SkylarkBuiltin(name = "fail",
+ doc = "Raises an error (the execution stops), except if the <code>when</code> condition "
+ + "is False.",
+ returnType = Environment.NoneType.class,
+ mandatoryParams = {
+ @Param(name = "msg", type = String.class, doc = "Error message to display for the user")},
+ optionalParams = {
+ @Param(name = "attr", type = String.class,
+ doc = "The name of the attribute that caused the error"),
+ @Param(name = "when", type = Boolean.class,
+ doc = "When False, the function does nothing. Default is True.")})
+ private static final Function fail = new MixedModeFunction(
+ "fail", ImmutableList.of("msg", "attr", "when"), 1, false) {
+ @Override
+ public Object call(Object[] args, FuncallExpression ast, Environment env)
+ throws EvalException {
+ if (args[2] != null) {
+ if (!EvalUtils.toBoolean(args[2])) {
+ return Environment.NONE;
+ }
+ }
+ String msg = cast(args[0], String.class, "msg", ast.getLocation());
+ if (args[1] != null) {
+ msg = "attribute " + cast(args[1], String.class, "attr", ast.getLocation())
+ + ": " + msg;
+ }
+ throw new EvalException(ast.getLocation(), msg);
+ }
+ };
+
+ @SkylarkBuiltin(name = "print", returnType = Environment.NoneType.class,
+ doc = "Prints <code>msg</code> to the console.",
+ mandatoryParams = {
+ @Param(name = "*args", doc = "The objects to print.")},
+ optionalParams = {
+ @Param(name = "sep", type = String.class,
+ doc = "The separator string between the objects, default is space (\" \").")})
+ private static final Function print = new AbstractFunction("print") {
+ @Override
+ public Object call(List<Object> args, Map<String, Object> kwargs, FuncallExpression ast,
+ Environment env) throws EvalException, InterruptedException {
+ String sep = " ";
+ if (kwargs.containsKey("sep")) {
+ sep = cast(kwargs.remove("sep"), String.class, "sep", ast.getLocation());
+ }
+ if (kwargs.size() > 0) {
+ throw new EvalException(ast.getLocation(),
+ "unexpected keywords: '" + kwargs.keySet() + "'");
+ }
+ String msg = Joiner.on(sep).join(Iterables.transform(args,
+ new com.google.common.base.Function<Object, String>() {
+ @Override
+ public String apply(Object input) {
+ return EvalUtils.printValue(input);
+ }
+ }));
+ ((SkylarkEnvironment) env).handleEvent(Event.warn(ast.getLocation(), msg));
+ return Environment.NONE;
+ }
+ };
+
+ /**
+ * Skylark String module.
+ */
+ @SkylarkModule(name = "string", doc =
+ "A language built-in type to support strings. "
+ + "Example of string literals:<br>"
+ + "<pre class=language-python>a = 'abc\\ndef'\n"
+ + "b = \"ab'cd\"\n"
+ + "c = \"\"\"multiline string\"\"\"</pre>"
+ + "Strings are iterable and support the <code>in</code> operator. Examples:<br>"
+ + "<pre class=language-python>\"a\" in \"abc\" # evaluates as True\n"
+ + "l = []\n"
+ + "for s in \"abc\":\n"
+ + " l += [s] # l == [\"a\", \"b\", \"c\"]</pre>")
+ public static final class StringModule {}
+
+ /**
+ * Skylark Dict module.
+ */
+ @SkylarkModule(name = "dict", doc =
+ "A language built-in type to support dicts. "
+ + "Example of dict literal:<br>"
+ + "<pre class=language-python>d = {\"a\": 2, \"b\": 5}</pre>"
+ + "Accessing elements works just like in Python:<br>"
+ + "<pre class=language-python>e = d[\"a\"] # e == 2</pre>"
+ + "Dicts support the <code>+</code> operator to concatenate two dicts. In case of multiple "
+ + "keys the second one overrides the first one. Examples:<br>"
+ + "<pre class=language-python>"
+ + "d = {\"a\" : 1} + {\"b\" : 2} # d == {\"a\" : 1, \"b\" : 2}\n"
+ + "d += {\"c\" : 3} # d == {\"a\" : 1, \"b\" : 2, \"c\" : 3}\n"
+ + "d = d + {\"c\" : 5} # d == {\"a\" : 1, \"b\" : 2, \"c\" : 5}</pre>"
+ + "Since the language doesn't have mutable objects <code>d[\"a\"] = 5</code> automatically "
+ + "translates to <code>d = d + {\"a\" : 5}</code>.<br>"
+ + "Dicts are iterable, the iteration works on their keyset.<br>"
+ + "Dicts support the <code>in</code> operator, testing membership in the keyset of the dict. "
+ + "Example:<br>"
+ + "<pre class=language-python>\"a\" in {\"a\" : 2, \"b\" : 5} # evaluates as True</pre>")
+ public static final class DictModule {}
+
+ public static final Map<Function, SkylarkType> stringFunctions = ImmutableMap
+ .<Function, SkylarkType>builder()
+ .put(join, SkylarkType.STRING)
+ .put(lower, SkylarkType.STRING)
+ .put(upper, SkylarkType.STRING)
+ .put(replace, SkylarkType.STRING)
+ .put(split, SkylarkType.of(List.class, String.class))
+ .put(rfind, SkylarkType.INT)
+ .put(find, SkylarkType.INT)
+ .put(endswith, SkylarkType.BOOL)
+ .put(startswith, SkylarkType.BOOL)
+ .put(strip, SkylarkType.STRING)
+ .put(substring, SkylarkType.STRING)
+ .put(count, SkylarkType.INT)
+ .build();
+
+ public static final List<Function> listFunctions = ImmutableList
+ .<Function>builder()
+ .add(append)
+ .add(extend)
+ .build();
+
+ public static final Map<Function, SkylarkType> dictFunctions = ImmutableMap
+ .<Function, SkylarkType>builder()
+ .put(items, SkylarkType.of(List.class))
+ .put(keys, SkylarkType.of(Set.class))
+ .put(values, SkylarkType.of(List.class))
+ .build();
+
+ private static final Map<Function, SkylarkType> pureGlobalFunctions = ImmutableMap
+ .<Function, SkylarkType>builder()
+ // TODO(bazel-team): String methods are added two times, because there are
+ // a lot of cases when they are used as global functions in the depot. Those
+ // should be cleaned up first.
+ .put(minus, SkylarkType.INT)
+ .put(select, SkylarkType.of(SelectorValue.class))
+ .put(len, SkylarkType.INT)
+ .put(str, SkylarkType.STRING)
+ .put(bool, SkylarkType.BOOL)
+ .build();
+
+ private static final Map<Function, SkylarkType> skylarkGlobalFunctions = ImmutableMap
+ .<Function, SkylarkType>builder()
+ .putAll(pureGlobalFunctions)
+ .put(list, SkylarkType.of(SkylarkList.class))
+ .put(struct, SkylarkType.of(ClassObject.class))
+ .put(hasattr, SkylarkType.BOOL)
+ .put(getattr, SkylarkType.UNKNOWN)
+ .put(set, SkylarkType.of(SkylarkNestedSet.class))
+ .put(dir, SkylarkType.of(SkylarkList.class, String.class))
+ .put(enumerate, SkylarkType.of(SkylarkList.class))
+ .put(range, SkylarkType.of(SkylarkList.class, Integer.class))
+ .put(type, SkylarkType.of(String.class))
+ .put(fail, SkylarkType.NONE)
+ .put(print, SkylarkType.NONE)
+ .build();
+
+ /**
+ * Set up a given environment for supported class methods.
+ */
+ public static void setupMethodEnvironment(Environment env) {
+ env.registerFunction(Map.class, index.getName(), index);
+ setupMethodEnvironment(env, Map.class, dictFunctions.keySet());
+ env.registerFunction(String.class, index.getName(), index);
+ setupMethodEnvironment(env, String.class, stringFunctions.keySet());
+ if (env.isSkylarkEnabled()) {
+ env.registerFunction(SkylarkList.class, index.getName(), index);
+ setupMethodEnvironment(env, skylarkGlobalFunctions.keySet());
+ } else {
+ env.registerFunction(List.class, index.getName(), index);
+ env.registerFunction(ImmutableList.class, index.getName(), index);
+ // TODO(bazel-team): listFunctions are not allowed in Skylark extensions (use += instead).
+ // It is allowed in BUILD files only for backward-compatibility.
+ setupMethodEnvironment(env, List.class, listFunctions);
+ setupMethodEnvironment(env, stringFunctions.keySet());
+ setupMethodEnvironment(env, pureGlobalFunctions.keySet());
+ }
+ }
+
+ private static void setupMethodEnvironment(
+ Environment env, Class<?> nameSpace, Iterable<Function> functions) {
+ for (Function function : functions) {
+ env.registerFunction(nameSpace, function.getName(), function);
+ }
+ }
+
+ private static void setupMethodEnvironment(Environment env, Iterable<Function> functions) {
+ for (Function function : functions) {
+ env.update(function.getName(), function);
+ }
+ }
+
+ private static void setupValidationEnvironment(
+ Map<Function, SkylarkType> functions, Map<String, SkylarkType> result) {
+ for (Map.Entry<Function, SkylarkType> function : functions.entrySet()) {
+ String name = function.getKey().getName();
+ result.put(name, SkylarkFunctionType.of(name, function.getValue()));
+ }
+ }
+
+ public static void setupValidationEnvironment(
+ Map<SkylarkType, Map<String, SkylarkType>> builtIn) {
+ Map<String, SkylarkType> global = builtIn.get(SkylarkType.GLOBAL);
+ setupValidationEnvironment(skylarkGlobalFunctions, global);
+
+ Map<String, SkylarkType> dict = new HashMap<>();
+ setupValidationEnvironment(dictFunctions, dict);
+ builtIn.put(SkylarkType.of(Map.class), dict);
+
+ Map<String, SkylarkType> string = new HashMap<>();
+ setupValidationEnvironment(stringFunctions, string);
+ builtIn.put(SkylarkType.STRING, string);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/NoSuchPackageException.java b/src/main/java/com/google/devtools/build/lib/packages/NoSuchPackageException.java
new file mode 100644
index 0000000..720d3d5
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/NoSuchPackageException.java
@@ -0,0 +1,58 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+import javax.annotation.Nullable;
+
+/**
+ * Exception indicating an attempt to access a package which is not found, does
+ * not exist, or can't be parsed into a package.
+ */
+public abstract class NoSuchPackageException extends NoSuchThingException {
+
+ private final String packageName;
+
+ public NoSuchPackageException(String packageName, String message) {
+ this(packageName, "no such package", message);
+ }
+
+ public NoSuchPackageException(String packageName, String message,
+ Throwable cause) {
+ this(packageName, "no such package", message, cause);
+ }
+
+ protected NoSuchPackageException(String packageName, String messagePrefix, String message) {
+ super(messagePrefix + " '" + packageName + "': " + message);
+ this.packageName = packageName;
+ }
+
+ protected NoSuchPackageException(String packageName, String messagePrefix, String message,
+ Throwable cause) {
+ super(messagePrefix + " '" + packageName + "': " + message, cause);
+ this.packageName = packageName;
+ }
+
+ public String getPackageName() {
+ return packageName;
+ }
+
+ /**
+ * Return the package if parsing completed enough to construct it. May return null.
+ */
+ @Nullable
+ public Package getPackage() {
+ return null;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/NoSuchTargetException.java b/src/main/java/com/google/devtools/build/lib/packages/NoSuchTargetException.java
new file mode 100644
index 0000000..fa180fa
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/NoSuchTargetException.java
@@ -0,0 +1,70 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+import com.google.devtools.build.lib.syntax.Label;
+
+import javax.annotation.Nullable;
+
+/**
+ * Exception indicating an attempt to access a target which is not found or does
+ * not exist.
+ */
+public class NoSuchTargetException extends NoSuchThingException {
+
+ @Nullable private final Label label;
+ // TODO(bazel-team): rename/refactor this class and NoSuchPackageException since it's confusing
+ // that they embed Target/Package instances.
+ @Nullable private final Target target;
+ private final boolean packageLoadedSuccessfully;
+
+ public NoSuchTargetException(String message) {
+ this(null, message);
+ }
+
+ public NoSuchTargetException(@Nullable Label label, String message) {
+ this((label != null ? "no such target '" + label + "': " : "") + message, label, null, null);
+ }
+
+ public NoSuchTargetException(Target targetInError, NoSuchPackageException nspe) {
+ this(String.format("Target '%s' contains an error and its package is in error",
+ targetInError.getLabel()), targetInError.getLabel(), targetInError, nspe);
+ }
+
+ private NoSuchTargetException(String message, @Nullable Label label, @Nullable Target target,
+ @Nullable NoSuchPackageException nspe) {
+ super(message, nspe);
+ this.label = label;
+ this.target = target;
+ this.packageLoadedSuccessfully = nspe != null ? false : true;
+ }
+
+ @Nullable
+ public Label getLabel() {
+ return label;
+ }
+
+ /**
+ * Return the target (in error) if parsing completed enough to construct it. May return null.
+ */
+ @Nullable
+ public Target getTarget() {
+ return target;
+ }
+
+ public boolean getPackageLoadedSuccessfully() {
+ return packageLoadedSuccessfully;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/NoSuchThingException.java b/src/main/java/com/google/devtools/build/lib/packages/NoSuchThingException.java
new file mode 100644
index 0000000..49e703e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/NoSuchThingException.java
@@ -0,0 +1,31 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+/**
+ * Exception indicating an attempt to access something which is not found or
+ * does not exist.
+ */
+public class NoSuchThingException extends Exception {
+
+ public NoSuchThingException(String message) {
+ super(message);
+ }
+
+ public NoSuchThingException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/NonconfigurableAttributeMapper.java b/src/main/java/com/google/devtools/build/lib/packages/NonconfigurableAttributeMapper.java
new file mode 100644
index 0000000..d54c847
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/NonconfigurableAttributeMapper.java
@@ -0,0 +1,56 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+
+/**
+ * {@link AttributeMap} implementation that triggers an {@link IllegalStateException} if called
+ * on any attribute that supports configurable values, as determined by
+ * {@link Attribute#isConfigurable()}.
+ *
+ * <p>This is particularly useful for logic that doesn't have access to configurations - it
+ * protects against undefined behavior in response to unexpected configuration-dependent inputs.
+ */
+public class NonconfigurableAttributeMapper extends AbstractAttributeMapper {
+ private NonconfigurableAttributeMapper(Rule rule) {
+ super(rule.getPackage(), rule.getRuleClassObject(), rule.getLabel(),
+ rule.getAttributeContainer());
+ }
+
+ /**
+ * Example usage:
+ *
+ * <pre>
+ * Label fooLabel = NonconfigurableAttributeMapper.of(rule).get("foo", Type.LABEL);
+ * </pre>
+ */
+ public static NonconfigurableAttributeMapper of (Rule rule) {
+ return new NonconfigurableAttributeMapper(rule);
+ }
+
+ @Override
+ public <T> T get(String attributeName, Type<T> type) {
+ Preconditions.checkState(!getAttributeDefinition(attributeName).isConfigurable(),
+ "Attribute '" + attributeName + "' is potentially configurable - not allowed here");
+ return super.get(attributeName, type);
+ }
+
+ @Override
+ protected <T> Iterable<T> visitAttribute(String attributeName, Type<T> type) {
+ T value = get(attributeName, type);
+ return value == null ? ImmutableList.<T>of() : ImmutableList.of(value);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/OutputFile.java b/src/main/java/com/google/devtools/build/lib/packages/OutputFile.java
new file mode 100644
index 0000000..9c91afb
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/OutputFile.java
@@ -0,0 +1,67 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.syntax.Label;
+
+/**
+ * A generated file that is the output of a rule.
+ */
+public final class OutputFile extends FileTarget {
+
+ private final Rule generatingRule;
+
+ /**
+ * Constructs an output file with the given label, which must be in the given
+ * package.
+ */
+ OutputFile(Package pkg, Label label, Rule generatingRule) {
+ super(pkg, label);
+ this.generatingRule = generatingRule;
+ }
+
+ @Override
+ public RuleVisibility getVisibility() {
+ return generatingRule.getVisibility();
+ }
+
+ /**
+ * Returns the rule which generates this output file.
+ */
+ public Rule getGeneratingRule() {
+ return generatingRule;
+ }
+
+ @Override
+ public String getTargetKind() {
+ return "generated file";
+ }
+
+ @Override
+ public Rule getAssociatedRule() {
+ return getGeneratingRule();
+ }
+
+ @Override
+ public Location getLocation() {
+ return generatingRule.getLocation();
+ }
+
+ @Override
+ public int hashCode() {
+ return label.hashCode();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/Package.java b/src/main/java/com/google/devtools/build/lib/packages/Package.java
new file mode 100644
index 0000000..a2216e0
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/Package.java
@@ -0,0 +1,1516 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSortedSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.devtools.build.lib.collect.CollectionUtils;
+import com.google.devtools.build.lib.collect.ImmutableSortedKeyMap;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.packages.AttributeMap.AcceptsLabelAttribute;
+import com.google.devtools.build.lib.packages.License.DistributionType;
+import com.google.devtools.build.lib.packages.PackageDeserializer.PackageDeserializationException;
+import com.google.devtools.build.lib.packages.PackageFactory.Globber;
+
+import com.google.devtools.build.lib.syntax.FuncallExpression;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.syntax.Label.SyntaxException;
+import com.google.devtools.build.lib.util.Pair;
+import com.google.devtools.build.lib.vfs.Canonicalizer;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.PrintStream;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A package, which is a container of {@link Rule}s, each of
+ * which contains a dictionary of named attributes.
+ *
+ * <p>Package instances are intended to be immutable and for all practical
+ * purposes can be treated as such. Note, however, that some member variables
+ * exposed via the public interface are not strictly immutable, so until their
+ * types are guaranteed immutable we're not applying the {@code @Immutable}
+ * annotation here.
+ */
+public class Package implements Serializable {
+
+ /**
+ * Common superclass for all name-conflict exceptions.
+ */
+ public static class NameConflictException extends Exception {
+ protected NameConflictException(String message) {
+ super(message);
+ }
+ }
+
+ /**
+ * The repository identifier for this package.
+ */
+ private final PackageIdentifier packageIdentifier;
+
+ /**
+ * The name of the package, e.g. "foo/bar".
+ */
+ protected final String name;
+
+ /**
+ * Like name, but in the form of a PathFragment.
+ */
+ private final PathFragment nameFragment;
+
+ /**
+ * The filename of this package's BUILD file.
+ */
+ protected Path filename;
+
+ /**
+ * The directory in which this package's BUILD file resides. All InputFile
+ * members of the packages are located relative to this directory.
+ */
+ private Path packageDirectory;
+
+ /**
+ * The root of the source tree in which this package was found. It is an invariant that
+ * {@code sourceRoot.getRelative(name).equals(packageDirectory)}.
+ */
+ private Path sourceRoot;
+
+ /**
+ * The "Make" environment of this package, containing package-local
+ * definitions of "Make" variables.
+ */
+ private MakeEnvironment makeEnv;
+
+ /**
+ * The collection of all targets defined in this package, indexed by name.
+ */
+ protected Map<String, Target> targets;
+
+ /**
+ * Default visibility for rules that do not specify it. null is interpreted
+ * as VISIBILITY_PRIVATE.
+ */
+ private RuleVisibility defaultVisibility;
+ private boolean defaultVisibilitySet;
+
+ /**
+ * Default package-level 'obsolete' value for rules that do not specify it.
+ */
+ private boolean defaultObsolete = false;
+
+ /**
+ * Default package-level 'testonly' value for rules that do not specify it.
+ */
+ private boolean defaultTestOnly = false;
+
+ /**
+ * Default package-level 'deprecation' value for rules that do not specify it.
+ */
+ private String defaultDeprecation;
+
+ /**
+ * Default header strictness checking for rules that do not specify it.
+ */
+ private String defaultHdrsCheck;
+
+ /**
+ * Default copts for cc_* rules. The rules' individual copts will append to
+ * this value.
+ */
+ private ImmutableList<String> defaultCopts;
+
+ /**
+ * The InputFile target corresponding to this package's BUILD file.
+ */
+ private InputFile buildFile;
+
+ /**
+ * True iff this package's BUILD files contained lexical or grammatical
+ * errors, or experienced errors during evaluation, or semantic errors during
+ * the construction of any rule.
+ *
+ * <p>Note: A package containing errors does not necessarily prevent a build;
+ * if all the rules needed for a given build were constructed prior to the
+ * first error, the build may proceed.
+ */
+ private boolean containsErrors;
+
+ /**
+ * True iff this package contains errors that were caused by temporary conditions (e.g. an I/O
+ * error). If this is true, {@link #containsErrors} is also true.
+ */
+ private boolean containsTemporaryErrors;
+
+ /**
+ * The set of labels subincluded by this package.
+ */
+ private Set<Label> subincludes;
+
+ /**
+ * The list of transitive closure of the Skylark file dependencies.
+ */
+ private ImmutableList<Label> skylarkFileDependencies;
+
+ /**
+ * The package's default "licenses" and "distribs" attributes, as specified
+ * in calls to licenses() and distribs() in the BUILD file.
+ */
+ // These sets contain the values specified by the most recent licenses() or
+ // distribs() declarations encountered during package parsing:
+ private License defaultLicense;
+ private Set<License.DistributionType> defaultDistributionSet;
+
+
+ /**
+ * The names of the package() attributes that declare default values for rule
+ * {@link RuleClass#COMPATIBLE_ENVIRONMENT_ATTR} and {@link RuleClass#RESTRICTED_ENVIRONMENT_ATTR}
+ * values when not explicitly specified.
+ */
+ public static final String DEFAULT_COMPATIBLE_WITH_ATTRIBUTE = "default_compatible_with";
+ public static final String DEFAULT_RESTRICTED_TO_ATTRIBUTE = "default_restricted_to";
+
+ private Set<Label> defaultCompatibleWith = ImmutableSet.of();
+ private Set<Label> defaultRestrictedTo = ImmutableSet.of();
+
+ private ImmutableSet<String> features;
+
+ private ImmutableList<Event> events;
+
+ // Hack to avoid having to copy every attribute. See #readObject and #readResolve.
+ // This will always be null for externally observable instances.
+ private Package deserializedPkg = null;
+
+ /**
+ * Package initialization, part 1 of 3: instantiates a new package with the
+ * given name.
+ *
+ * <p>As part of initialization, {@link Builder} constructs {@link InputFile}
+ * and {@link PackageGroup} instances that require a valid Package instance where
+ * {@link Package#getNameFragment()} is accessible. That's why these settings are
+ * applied here at the start.
+ *
+ * @precondition {@code name} must be a suffix of
+ * {@code filename.getParentDirectory())}.
+ */
+ protected Package(PackageIdentifier packageId) {
+ this.packageIdentifier = packageId;
+ this.nameFragment = Canonicalizer.fragments().intern(packageId.getPackageFragment());
+ this.name = nameFragment.getPathString();
+ }
+
+ private void writeObject(ObjectOutputStream out) {
+ com.google.devtools.build.lib.query2.proto.proto2api.Build.Package pb =
+ PackageSerializer.serializePackage(this);
+ try {
+ pb.writeDelimitedTo(out);
+ } catch (IOException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ private void readObject(ObjectInputStream in) throws IOException {
+ com.google.devtools.build.lib.query2.proto.proto2api.Build.Package pb =
+ com.google.devtools.build.lib.query2.proto.proto2api.Build.Package.parseDelimitedFrom(in);
+ Package pkg;
+ try {
+ pkg = new PackageDeserializer(null, null).deserialize(pb);
+ } catch (PackageDeserializationException e) {
+ throw new IllegalStateException(e);
+ }
+ deserializedPkg = pkg;
+ }
+
+ protected Object readResolve() {
+ // This method needs to be protected so serialization works for subclasses.
+ return deserializedPkg;
+ }
+
+ // See: http://docs.oracle.com/javase/6/docs/platform/serialization/spec/input.html#6053
+ @SuppressWarnings("unused")
+ private void readObjectNoData() {
+ throw new IllegalStateException();
+ }
+
+ /** Returns this packages' identifier. */
+ public PackageIdentifier getPackageIdentifier() {
+ return packageIdentifier;
+ }
+
+ /**
+ * Package initialization: part 2 of 3: sets this package's default header
+ * strictness checking.
+ *
+ * <p>This is needed to support C++-related rule classes
+ * which accesses {@link #getDefaultHdrsCheck} from the still-under-construction
+ * package.
+ */
+ protected void setDefaultHdrsCheck(String defaultHdrsCheck) {
+ this.defaultHdrsCheck = defaultHdrsCheck;
+ }
+
+ /**
+ * Set the default 'obsolete' value for this package.
+ */
+ protected void setDefaultObsolete(boolean obsolete) {
+ defaultObsolete = obsolete;
+ }
+
+ /**
+ * Set the default 'testonly' value for this package.
+ */
+ protected void setDefaultTestOnly(boolean testOnly) {
+ defaultTestOnly = testOnly;
+ }
+
+ /**
+ * Set the default 'deprecation' value for this package.
+ */
+ protected void setDefaultDeprecation(String deprecation) {
+ defaultDeprecation = deprecation;
+ }
+
+ /**
+ * Sets the default value to use for a rule's {@link RuleClass#COMPATIBLE_ENVIRONMENT_ATTR}
+ * attribute when not explicitly specified by the rule.
+ */
+ protected void setDefaultCompatibleWith(Set<Label> environments) {
+ defaultCompatibleWith = environments;
+ }
+
+ /**
+ * Sets the default value to use for a rule's {@link RuleClass#RESTRICTED_ENVIRONMENT_ATTR}
+ * attribute when not explicitly specified by the rule.
+ */
+ protected void setDefaultRestrictedTo(Set<Label> environments) {
+ defaultRestrictedTo = environments;
+ }
+
+ public static Path getSourceRoot(Path buildFile, PathFragment nameFragment) {
+ Path current = buildFile.getParentDirectory();
+ for (int i = 0, len = nameFragment.segmentCount(); i < len && current != null; i++) {
+ current = current.getParentDirectory();
+ }
+ return current;
+ }
+
+ /**
+ * Package initialization: part 3 of 3: applies all other settings and completes
+ * initialization of the package.
+ *
+ * <p>Only after this method is called can this package be considered "complete"
+ * and be shared publicly.
+ */
+ protected void finishInit(AbstractBuilder<?, ?> builder) {
+ // If any error occurred during evaluation of this package, consider all
+ // rules in the package to be "in error" also (even if they were evaluated
+ // prior to the error). This behaviour is arguably stricter than need be,
+ // but stopping a build only for some errors but not others creates user
+ // confusion.
+ if (builder.containsErrors) {
+ for (Rule rule : builder.getTargets(Rule.class)) {
+ rule.setContainsErrors();
+ }
+ }
+ this.filename = builder.filename;
+ this.packageDirectory = filename.getParentDirectory();
+
+ this.sourceRoot = getSourceRoot(filename, nameFragment);
+ if ((sourceRoot == null
+ || !sourceRoot.getRelative(nameFragment).equals(packageDirectory))
+ && !filename.getBaseName().equals("WORKSPACE")) {
+ throw new IllegalArgumentException(
+ "Invalid BUILD file name for package '" + name + "': " + filename);
+ }
+
+ this.makeEnv = builder.makeEnv.build();
+ this.targets = ImmutableSortedKeyMap.copyOf(builder.targets);
+ this.defaultVisibility = builder.defaultVisibility;
+ this.defaultVisibilitySet = builder.defaultVisibilitySet;
+ if (builder.defaultCopts == null) {
+ this.defaultCopts = ImmutableList.of();
+ } else {
+ this.defaultCopts = ImmutableList.copyOf(builder.defaultCopts);
+ }
+ this.buildFile = builder.buildFile;
+ this.containsErrors = builder.containsErrors;
+ this.containsTemporaryErrors = builder.containsTemporaryErrors;
+ this.subincludes = builder.subincludes.keySet();
+ this.skylarkFileDependencies = builder.skylarkFileDependencies;
+ this.defaultLicense = builder.defaultLicense;
+ this.defaultDistributionSet = builder.defaultDistributionSet;
+ this.features = ImmutableSortedSet.copyOf(builder.features);
+ this.events = ImmutableList.copyOf(builder.events);
+ }
+
+ /**
+ * Returns the list of subincluded labels on which the validity of this package depends.
+ */
+ public Set<Label> getSubincludeLabels() {
+ return subincludes;
+ }
+
+ /**
+ * Returns the list of transitive closure of the Skylark file dependencies of this package.
+ */
+ public ImmutableList<Label> getSkylarkFileDependencies() {
+ return skylarkFileDependencies;
+ }
+
+ /**
+ * Returns the filename of the BUILD file which defines this package. The
+ * parent directory of the BUILD file is the package directory.
+ */
+ public Path getFilename() {
+ return filename;
+ }
+
+ /**
+ * Returns the source root (a directory) beneath which this package's BUILD file was found.
+ *
+ * Assumes invariant:
+ * {@code getSourceRoot().getRelative(getName()).equals(getPackageDirectory())}
+ */
+ public Path getSourceRoot() {
+ return sourceRoot;
+ }
+
+ /**
+ * Returns the directory containing the package's BUILD file.
+ */
+ public Path getPackageDirectory() {
+ return packageDirectory;
+ }
+
+ /**
+ * Returns the name of this package. If this build is using external repositories then this name
+ * may not be unique!
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Like {@link #getName}, but has type {@code PathFragment}.
+ */
+ public PathFragment getNameFragment() {
+ return nameFragment;
+ }
+
+ /**
+ * Returns the "Make" value from the package's make environment whose name
+ * is "varname", or null iff the variable is not defined in the environment.
+ */
+ public String lookupMakeVariable(String varname, String platform) {
+ return makeEnv.lookup(varname, platform);
+ }
+
+ /**
+ * Returns the make environment. This should only ever be used for serialization -- how the
+ * make variables are implemented is an implementation detail.
+ */
+ MakeEnvironment getMakeEnvironment() {
+ return makeEnv;
+ }
+
+ /**
+ * Returns the label of this package's BUILD file.
+ *
+ * Typically <code>getBuildFileLabel().getName().equals("BUILD")</code> --
+ * though not necessarily: data in a subdirectory of a test package may use a
+ * different filename to avoid inadvertently creating a new package.
+ */
+ Label getBuildFileLabel() {
+ return buildFile.getLabel();
+ }
+
+ /**
+ * Returns the InputFile target for this package's BUILD file.
+ */
+ public InputFile getBuildFile() {
+ return buildFile;
+ }
+
+ /**
+ * Returns true if errors were encountered during evaluation of this package.
+ * (The package may be incomplete and its contents should not be relied upon
+ * for critical operations. However, any Rules belonging to the package are
+ * guaranteed to be intact, unless their <code>containsErrors()</code> flag
+ * is set.)
+ */
+ public boolean containsErrors() {
+ return containsErrors;
+ }
+
+ /**
+ * True iff this package contains errors that were caused by temporary conditions (e.g. an I/O
+ * error). If this is true, {@link #containsErrors()} also returns true.
+ */
+ public boolean containsTemporaryErrors() {
+ return containsTemporaryErrors;
+ }
+
+ public List<Event> getEvents() {
+ return events;
+ }
+
+ /**
+ * Returns an (immutable, unordered) view of all the targets belonging to this package.
+ */
+ public Collection<Target> getTargets() {
+ return getTargets(targets);
+ }
+
+ /**
+ * Common getTargets implementation, accessible by both {@link Package} and
+ * {@link Package.AbstractBuilder}.
+ */
+ private static Collection<Target> getTargets(Map<String, Target> targetMap) {
+ return Collections.unmodifiableCollection(targetMap.values());
+ }
+
+ /**
+ * Returns a (read-only, unordered) iterator of all the targets belonging
+ * to this package which are instances of the specified class.
+ */
+ public <T extends Target> Iterable<T> getTargets(Class<T> targetClass) {
+ return getTargets(targets, targetClass);
+ }
+
+ /**
+ * Common getTargets implementation, accessible by both {@link Package} and
+ * {@link Package.AbstractBuilder}.
+ */
+ private static <T extends Target> Iterable<T> getTargets(Map<String, Target> targetMap,
+ Class<T> targetClass) {
+ return Iterables.filter(targetMap.values(), targetClass);
+ }
+
+ /**
+ * Returns a (read-only, unordered) iterator over the rules in this package.
+ */
+ @VisibleForTesting // Legacy. Production code should use getTargets(Class) instead
+ Iterable<? extends Rule> getRules() {
+ return getTargets(Rule.class);
+ }
+
+ /**
+ * Returns a (read-only, unordered) iterator over the files in this package.
+ */
+ @VisibleForTesting // Legacy. Production code should use getTargets(Class) instead
+ Iterable<? extends FileTarget> getFiles() {
+ return getTargets(FileTarget.class);
+ }
+
+ /**
+ * Returns the rule that corresponds to a particular BUILD target name. Useful
+ * for walking through the dependency graph of a target.
+ * Fails if the target is not a Rule.
+ */
+ @VisibleForTesting
+ Rule getRule(String targetName) {
+ return (Rule) targets.get(targetName);
+ }
+
+ /**
+ * Returns the features specified in the <code>package()</code> declaration.
+ */
+ public ImmutableSet<String> getFeatures() {
+ return features;
+ }
+
+ /**
+ * Returns the target (a member of this package) whose name is "targetName".
+ * First rules are searched, then output files, then input files. The target
+ * name must be valid, as defined by {@code LabelValidator#validateTargetName}.
+ *
+ * @throws NoSuchTargetException if the specified target was not found.
+ */
+ public Target getTarget(String targetName) throws NoSuchTargetException {
+ Target target = targets.get(targetName);
+ if (target != null) {
+ return target;
+ }
+
+ // No such target.
+
+ // If there's a file on the disk that's not mentioned in the BUILD file,
+ // produce a more informative error. NOTE! this code path is only executed
+ // on failure, which is (relatively) very rare. In the common case no
+ // stat(2) is executed.
+ Path filename = getPackageDirectory().getRelative(targetName);
+ String suffix;
+ if (!new PathFragment(targetName).isNormalized()) {
+ // Don't check for file existence in this case because the error message
+ // would be confusing and wrong. If the targetName is "foo/bar/.", and
+ // there is a directory "foo/bar", it doesn't mean that "//pkg:foo/bar/."
+ // is a valid label.
+ suffix = "";
+ } else if (filename.isDirectory()) {
+ suffix = "; however, a source directory of this name exists. (Perhaps add "
+ + "'exports_files([\"" + targetName + "\"])' to " + name + "/BUILD, or define a "
+ + "filegroup?)";
+ } else if (filename.exists()) {
+ suffix = "; however, a source file of this name exists. (Perhaps add "
+ + "'exports_files([\"" + targetName + "\"])' to " + name + "/BUILD?)";
+ } else {
+ suffix = "";
+ }
+
+ try {
+ throw new NoSuchTargetException(createLabel(targetName), "target '" + targetName
+ + "' not declared in package '" + name + "'" + suffix + " defined by "
+ + this.filename);
+ } catch (Label.SyntaxException e) {
+ throw new IllegalArgumentException(targetName);
+ }
+ }
+
+ /**
+ * Creates a label for a target inside this package.
+ *
+ * @throws SyntaxException if the {@code targetName} is invalid
+ */
+ public Label createLabel(String targetName) throws SyntaxException {
+ return Label.create(packageIdentifier, targetName);
+ }
+
+ /**
+ * Returns the default visibility for this package.
+ */
+ public RuleVisibility getDefaultVisibility() {
+ if (defaultVisibility != null) {
+ return defaultVisibility;
+ } else {
+ return ConstantRuleVisibility.PRIVATE;
+ }
+ }
+
+ /**
+ * Returns the default obsolete value.
+ */
+ public Boolean getDefaultObsolete() {
+ return defaultObsolete;
+ }
+
+ /**
+ * Returns the default testonly value.
+ */
+ public Boolean getDefaultTestOnly() {
+ return defaultTestOnly;
+ }
+
+ /**
+ * Returns the default obsolete value.
+ */
+ public String getDefaultDeprecation() {
+ return defaultDeprecation;
+ }
+
+ /**
+ * Gets the default header checking mode.
+ */
+ public String getDefaultHdrsCheck() {
+ return defaultHdrsCheck != null ? defaultHdrsCheck : "loose";
+ }
+
+ /**
+ * Returns the default copts value, to which rules should append their
+ * specific copts.
+ */
+ public ImmutableList<String> getDefaultCopts() {
+ return defaultCopts;
+ }
+
+ /**
+ * Returns whether the default header checking mode has been set or it is the
+ * default value.
+ */
+ public boolean isDefaultHdrsCheckSet() {
+ return defaultHdrsCheck != null;
+ }
+
+ public boolean isDefaultVisibilitySet() {
+ return defaultVisibilitySet;
+ }
+
+ /**
+ * Gets the parsed license object for the default license
+ * declared by this package.
+ */
+ public License getDefaultLicense() {
+ return defaultLicense;
+ }
+
+ /**
+ * Returns the parsed set of distributions declared as the default for this
+ * package.
+ */
+ public Set<License.DistributionType> getDefaultDistribs() {
+ return defaultDistributionSet;
+ }
+
+ /**
+ * Returns the default value to use for a rule's {@link RuleClass#COMPATIBLE_ENVIRONMENT_ATTR}
+ * attribute when not explicitly specified by the rule.
+ */
+ public Set<Label> getDefaultCompatibleWith() {
+ return defaultCompatibleWith;
+ }
+
+ /**
+ * Returns the default value to use for a rule's {@link RuleClass#RESTRICTED_ENVIRONMENT_ATTR}
+ * attribute when not explicitly specified by the rule.
+ */
+ public Set<Label> getDefaultRestrictedTo() {
+ return defaultRestrictedTo;
+ }
+
+ @Override
+ public String toString() {
+ return "Package(" + name + ")=" + (targets != null ? getRules() : "initializing...");
+ }
+
+ /**
+ * Dumps the package for debugging. Do not depend on the exact format/contents of this debugging
+ * output.
+ */
+ public void dump(PrintStream out) {
+ out.println(" Package " + getName() + " (" + getFilename() + ")");
+
+ // Rules:
+ out.println(" Rules");
+ for (Rule rule : getTargets(Rule.class)) {
+ out.println(" " + rule.getTargetKind() + " " + rule.getLabel());
+ for (Attribute attr : rule.getAttributes()) {
+ for (Object possibleValue : AggregatingAttributeMapper.of(rule)
+ .visitAttribute(attr.getName(), attr.getType())) {
+ out.println(" " + attr.getName() + " = " + possibleValue);
+ }
+ }
+ }
+
+ // Files:
+ out.println(" Files");
+ for (FileTarget file : getTargets(FileTarget.class)) {
+ out.print(" " + file.getTargetKind() + " " + file.getLabel());
+ if (file instanceof OutputFile) {
+ out.println(" (generated by " + ((OutputFile) file).getGeneratingRule().getLabel() + ")");
+ } else {
+ out.println();
+ }
+ }
+
+ // TODO(bazel-team): (2009) perhaps dump also:
+ // - subincludes
+ // - globs
+ // - containsErrors
+ // - makeEnv
+ }
+
+ /**
+ * Builder class for {@link Package}.
+ *
+ * <p>Should only be used by the package loading and the package deserialization machineries.
+ */
+ static class Builder extends AbstractBuilder<Package, Builder> {
+ Builder(PackageIdentifier packageId) {
+ super(new Package(packageId));
+ }
+
+ @Override
+ protected Builder self() {
+ return this;
+ }
+ }
+
+ /** Builder class for {@link Package} that does its own globbing. */
+ public static class LegacyBuilder extends AbstractBuilder<Package, LegacyBuilder> {
+
+ private Globber globber = null;
+
+ LegacyBuilder(PackageIdentifier packageId) {
+ super(AbstractBuilder.newPackage(packageId));
+ }
+
+ @Override
+ protected LegacyBuilder self() {
+ return this;
+ }
+
+ /**
+ * Sets the globber used for this package's glob expansions.
+ */
+ LegacyBuilder setGlobber(Globber globber) {
+ this.globber = globber;
+ return this;
+ }
+
+ /**
+ * Removes a target from the {@link Package} under construction. Intended to be used only by
+ * {@link PackageFunction} to remove targets whose labels cross subpackage boundaries.
+ */
+ public void removeTarget(Target target) {
+ if (target.getPackage() == pkg) {
+ this.targets.remove(target.getName());
+ }
+ }
+
+ /**
+ * Returns the glob patterns requested by {@link PackageFactory} during evaluation of this
+ * package's BUILD file. Intended to be used only by {@link PackageFunction} to mark the
+ * appropriate Skyframe dependencies after the fact.
+ */
+ public Set<Pair<String, Boolean>> getGlobPatterns() {
+ return globber.getGlobPatterns();
+ }
+ }
+
+ abstract static class AbstractBuilder<P extends Package, B extends AbstractBuilder<P, B>> {
+ /**
+ * The output instance for this builder. Needs to be instantiated and
+ * available with name info throughout initialization. All other settings
+ * are applied during {@link #build}. See {@link Package#Package(String)}
+ * and {@link Package#finishInit} for details.
+ */
+ protected P pkg;
+
+ protected Path filename = null;
+ private Label buildFileLabel = null;
+ private InputFile buildFile = null;
+ private MakeEnvironment.Builder makeEnv = null;
+ private RuleVisibility defaultVisibility = null;
+ private boolean defaultVisibilitySet;
+ private List<String> defaultCopts = null;
+ private List<String> features = new ArrayList<>();
+ private List<Event> events = Lists.newArrayList();
+ private boolean containsErrors = false;
+ private boolean containsTemporaryErrors = false;
+
+ private License defaultLicense = License.NO_LICENSE;
+ private Set<License.DistributionType> defaultDistributionSet = License.DEFAULT_DISTRIB;
+
+ protected Map<String, Target> targets = new HashMap<>();
+ protected Map<Label, EnvironmentGroup> environmentGroups = new HashMap<>();
+
+ protected Map<Label, Path> subincludes = null;
+ protected ImmutableList<Label> skylarkFileDependencies = null;
+
+ /**
+ * True iff the "package" function has already been called in this package.
+ */
+ private boolean packageFunctionUsed;
+
+ /**
+ * The collection of the prefixes of every output file. Maps every prefix
+ * to an output file whose prefix it is.
+ *
+ * <p>This is needed to make the output file prefix conflict check be
+ * reasonably fast. However, since it can potentially take a lot of memory and
+ * is useless after the package has been loaded, it isn't passed to the
+ * package itself.
+ */
+ private Map<String, OutputFile> outputFilePrefixes = new HashMap<>();
+
+ private boolean alreadyBuilt = false;
+
+ private EventHandler builderEventHandler = new EventHandler() {
+ @Override
+ public void handle(Event event) {
+ addEvent(event);
+ }
+ };
+
+ protected AbstractBuilder(P pkg) {
+ this.pkg = pkg;
+ if (pkg.getName().startsWith("javatests/")) {
+ setDefaultTestonly(true);
+ }
+ }
+
+ protected static Package newPackage(PackageIdentifier packageId) {
+ return new Package(packageId);
+ }
+
+ protected abstract B self();
+
+ protected PackageIdentifier getPackageIdentifier() {
+ return pkg.getPackageIdentifier();
+ }
+
+ /**
+ * Sets the name of this package's BUILD file.
+ */
+ B setFilename(Path filename) {
+ this.filename = filename;
+ try {
+ buildFileLabel = createLabel(filename.getBaseName());
+ addInputFile(buildFileLabel, Location.fromFile(filename));
+ } catch (Label.SyntaxException e) {
+ // This can't actually happen.
+ throw new AssertionError("Package BUILD file has an illegal name: " + filename);
+ }
+ return self();
+ }
+
+ public Label getBuildFileLabel() {
+ return buildFileLabel;
+ }
+
+ Path getFilename() {
+ return filename;
+ }
+
+ /**
+ * Sets this package's Make environment.
+ */
+ B setMakeEnv(MakeEnvironment.Builder makeEnv) {
+ this.makeEnv = makeEnv;
+ return self();
+ }
+
+ /**
+ * Sets the default visibility for this package. Called at most once per
+ * package from PackageFactory.
+ */
+ B setDefaultVisibility(RuleVisibility visibility) {
+ this.defaultVisibility = visibility;
+ this.defaultVisibilitySet = true;
+ return self();
+ }
+
+ /**
+ * Sets whether the default visibility is set in the BUILD file.
+ */
+ B setDefaultVisibilitySet(boolean defaultVisibilitySet) {
+ this.defaultVisibilitySet = defaultVisibilitySet;
+ return self();
+ }
+
+ /**
+ * Sets the default value of 'obsolete'. Rule-level 'obsolete' will override this.
+ */
+ B setDefaultObsolete(boolean defaultObsolete) {
+ pkg.setDefaultObsolete(defaultObsolete);
+ return self();
+ }
+
+ /** Sets the default value of 'testonly'. Rule-level 'testonly' will override this. */
+ B setDefaultTestonly(boolean defaultTestonly) {
+ pkg.setDefaultTestOnly(defaultTestonly);
+ return self();
+ }
+
+ /**
+ * Sets the default value of 'deprecation'. Rule-level 'deprecation' will append to this.
+ */
+ B setDefaultDeprecation(String defaultDeprecation) {
+ pkg.setDefaultDeprecation(defaultDeprecation);
+ return self();
+ }
+
+ /**
+ * Returns whether the "package" function has been called yet
+ */
+ public boolean isPackageFunctionUsed() {
+ return packageFunctionUsed;
+ }
+
+ public void setPackageFunctionUsed() {
+ packageFunctionUsed = true;
+ }
+
+ /**
+ * Sets the default header checking mode.
+ */
+ public B setDefaultHdrsCheck(String hdrsCheck) {
+ // Note that this setting is propagated directly to the package because
+ // other code needs the ability to read this info directly from the
+ // under-construction package. See {@link Package#setDefaultHdrsCheck}.
+ pkg.setDefaultHdrsCheck(hdrsCheck);
+ return self();
+ }
+
+ /**
+ * Sets the default value of copts. Rule-level copts will append to this.
+ */
+ public B setDefaultCopts(List<String> defaultCopts) {
+ this.defaultCopts = defaultCopts;
+ return self();
+ }
+
+ public B addFeatures(Iterable<String> features) {
+ Iterables.addAll(this.features, features);
+ return self();
+ }
+
+ /**
+ * Declares that errors were encountering while loading this package.
+ */
+ public B setContainsErrors() {
+ containsErrors = true;
+ return self();
+ }
+
+ public boolean containsErrors() {
+ return containsErrors;
+ }
+
+ B setContainsTemporaryErrors() {
+ setContainsErrors();
+ containsTemporaryErrors = true;
+ return self();
+ }
+
+ public B addEvents(Iterable<Event> events) {
+ for (Event event : events) {
+ addEvent(event);
+ }
+ return self();
+ }
+
+ public B addEvent(Event event) {
+ this.events.add(event);
+ return self();
+ }
+
+ B setSkylarkFileDependencies(ImmutableList<Label> skylarkFileDependencies) {
+ this.skylarkFileDependencies = skylarkFileDependencies;
+ return self();
+ }
+
+ /**
+ * Sets the default license for this package.
+ */
+ void setDefaultLicense(License license) {
+ this.defaultLicense = license;
+ }
+
+ License getDefaultLicense() {
+ return defaultLicense;
+ }
+
+ /**
+ * Initializes the default set of distributions for targets in this package.
+ *
+ * TODO(bazel-team): (2011) consider moving the license & distribs info into Metadata--maybe
+ * even in the Build language.
+ */
+ void setDefaultDistribs(Set<DistributionType> dists) {
+ this.defaultDistributionSet = dists;
+ }
+
+ Set<DistributionType> getDefaultDistribs() {
+ return defaultDistributionSet;
+ }
+
+ /**
+ * Sets the default value to use for a rule's {@link RuleClass#COMPATIBLE_ENVIRONMENT_ATTR}
+ * attribute when not explicitly specified by the rule. Records a package error if
+ * any labels are duplicated.
+ */
+ void setDefaultCompatibleWith(List<Label> environments, String attrName, Location location) {
+ if (!checkForDuplicateLabels(environments, "package " + pkg.getName(), attrName, location,
+ builderEventHandler)) {
+ setContainsErrors();
+ }
+ pkg.setDefaultCompatibleWith(ImmutableSet.copyOf(environments));
+ }
+
+ /**
+ * Sets the default value to use for a rule's {@link RuleClass#RESTRICTED_ENVIRONMENT_ATTR}
+ * attribute when not explicitly specified by the rule. Records a package error if
+ * any labels are duplicated.
+ */
+ void setDefaultRestrictedTo(List<Label> environments, String attrName, Location location) {
+ if (!checkForDuplicateLabels(environments, "package " + pkg.getName(), attrName, location,
+ builderEventHandler)) {
+ setContainsErrors();
+ }
+
+ pkg.setDefaultRestrictedTo(ImmutableSet.copyOf(environments));
+ }
+
+ /**
+ * Returns a new Rule belonging to this package instance, and uses the given Label.
+ *
+ * <p>Useful for RuleClass instantiation, where the rule name is checked by trying to create a
+ * Label. This label can then be used again here.
+ */
+ Rule newRuleWithLabel(Label label, RuleClass ruleClass, FuncallExpression ast,
+ Location location) {
+ return new Rule(pkg, label, ruleClass, ast, location);
+ }
+
+ /**
+ * Called by the parser when a "mocksubinclude" is encountered, to record the
+ * mappings from labels to absolute paths upon which that the validity of
+ * this package depends.
+ */
+ void addSubinclude(Label label, Path resolvedPath) {
+ if (subincludes == null) {
+ // This is a TreeMap because the order needs to be deterministic.
+ subincludes = Maps.newTreeMap();
+ }
+
+ Path oldResolvedPath = subincludes.put(label, resolvedPath);
+ if (oldResolvedPath != null && !oldResolvedPath.equals(resolvedPath)){
+ // The same label should have been resolved to the same path
+ throw new IllegalStateException("Ambiguous subinclude path");
+ }
+ }
+
+ public Set<Label> getSubincludeLabels() {
+ return subincludes == null ? Sets.<Label>newHashSet() : subincludes.keySet();
+ }
+
+ public Map<Label, Path> getSubincludes() {
+ return subincludes == null ? Maps.<Label, Path>newHashMap() : subincludes;
+ }
+
+ public Collection<Target> getTargets() {
+ return Package.getTargets(targets);
+ }
+
+ /**
+ * Returns an (immutable, unordered) view of all the targets belonging to
+ * this package which are instances of the specified class.
+ */
+ <T extends Target> Iterable<T> getTargets(Class<T> targetClass) {
+ return Package.getTargets(targets, targetClass);
+ }
+
+ /**
+ * An input file name conflicts with an existing package member.
+ */
+ static class GeneratedLabelConflict extends NameConflictException {
+ private GeneratedLabelConflict(String message) {
+ super(message);
+ }
+ }
+
+ /**
+ * Creates an input file target in this package with the specified name.
+ *
+ * @param targetName name of the input file. This must be a valid target
+ * name as defined by {@link
+ * com.google.devtools.build.lib.cmdline.LabelValidator#validateTargetName}.
+ * @return the newly-created InputFile, or the old one if it already existed.
+ * @throws GeneratedLabelConflict if the name was already taken by a Rule or
+ * an OutputFile target.
+ * @throws IllegalArgumentException if the name is not a valid label
+ */
+ InputFile createInputFile(String targetName, Location location)
+ throws GeneratedLabelConflict {
+ Target existing = targets.get(targetName);
+ if (existing == null) {
+ try {
+ return addInputFile(createLabel(targetName), location);
+ } catch (Label.SyntaxException e) {
+ throw new IllegalArgumentException("FileTarget in package " + pkg.getName()
+ + " has illegal name: " + targetName);
+ }
+ } else if (existing instanceof InputFile) {
+ return (InputFile) existing; // idempotent
+ } else {
+ throw new GeneratedLabelConflict("generated label '//" + pkg.getName() + ":"
+ + targetName + "' conflicts with existing "
+ + existing.getTargetKind());
+ }
+ }
+
+ /**
+ * Sets the visibility and license for an input file. The input file must already exist as
+ * a member of this package.
+ * @throws IllegalArgumentException if the input file doesn't exist in this
+ * package's target map.
+ */
+ void setVisibilityAndLicense(InputFile inputFile, RuleVisibility visibility, License license) {
+ String filename = inputFile.getName();
+ Target cacheInstance = targets.get(filename);
+ if (cacheInstance == null || !(cacheInstance instanceof InputFile)) {
+ throw new IllegalArgumentException("Can't set visibility for nonexistent FileTarget "
+ + filename + " in package " + pkg.getName() + ".");
+ }
+ if (!((InputFile) cacheInstance).isVisibilitySpecified()
+ || cacheInstance.getVisibility() != visibility
+ || cacheInstance.getLicense() != license) {
+ targets.put(filename, new InputFile(
+ pkg, cacheInstance.getLabel(), cacheInstance.getLocation(), visibility, license));
+ }
+ }
+
+ /**
+ * Creates a label for a target inside this package.
+ *
+ * @throws SyntaxException if the {@code targetName} is invalid
+ */
+ Label createLabel(String targetName) throws SyntaxException {
+ return Label.create(pkg.getPackageIdentifier(), targetName);
+ }
+
+ /**
+ * Adds a package group to the package.
+ */
+ void addPackageGroup(String name, Collection<String> packages, Collection<Label> includes,
+ EventHandler eventHandler, Location location)
+ throws NameConflictException, Label.SyntaxException {
+ PackageGroup group =
+ new PackageGroup(createLabel(name), pkg, packages, includes, eventHandler, location);
+ Target existing = targets.get(group.getName());
+ if (existing != null) {
+ throw nameConflict(group, existing);
+ }
+
+ targets.put(group.getName(), group);
+
+ if (group.containsErrors()) {
+ setContainsErrors();
+ }
+ }
+
+ /**
+ * Checks if any labels in the given list appear multiple times and reports an appropriate
+ * error message if so. Returns true if no duplicates were found, false otherwise.
+ *
+ * TODO(bazel-team): apply this to all build functions (maybe automatically?), possibly
+ * integrate with RuleClass.checkForDuplicateLabels.
+ */
+ private static boolean checkForDuplicateLabels(Collection<Label> labels, String owner,
+ String attrName, Location location, EventHandler eventHandler) {
+ Set<Label> dupes = CollectionUtils.duplicatedElementsOf(labels);
+ for (Label dupe : dupes) {
+ eventHandler.handle(Event.error(location, String.format(
+ "label '%s' is duplicated in the '%s' list of '%s'", dupe, attrName, owner)));
+ }
+ return dupes.isEmpty();
+ }
+
+ /**
+ * Adds an environment group to the package.
+ */
+ void addEnvironmentGroup(String name, List<Label> environments, List<Label> defaults,
+ EventHandler eventHandler, Location location)
+ throws NameConflictException, SyntaxException {
+
+ if (!checkForDuplicateLabels(environments, name, "environments", location, eventHandler)
+ || !checkForDuplicateLabels(defaults, name, "defaults", location, eventHandler)) {
+ setContainsErrors();
+ return;
+ }
+
+ EnvironmentGroup group = new EnvironmentGroup(createLabel(name), pkg, environments,
+ defaults, location);
+ Target existing = targets.get(group.getName());
+ if (existing != null) {
+ throw nameConflict(group, existing);
+ }
+
+ targets.put(group.getName(), group);
+ Collection<Event> membershipErrors = group.validateMembership();
+ if (!membershipErrors.isEmpty()) {
+ for (Event error : membershipErrors) {
+ eventHandler.handle(error);
+ }
+ setContainsErrors();
+ return;
+ }
+
+ // For each declared environment, make sure it doesn't also belong to some other group.
+ for (Label environment : group.getEnvironments()) {
+ EnvironmentGroup otherGroup = environmentGroups.get(environment);
+ if (otherGroup != null) {
+ eventHandler.handle(Event.error(location, "environment " + environment + " belongs to"
+ + " both " + group.getLabel() + " and " + otherGroup.getLabel()));
+ setContainsErrors();
+ } else {
+ environmentGroups.put(environment, group);
+ }
+ }
+ }
+
+ void addRule(Rule rule) throws NameConflictException {
+ checkForConflicts(rule);
+ // Now, modify the package:
+ for (OutputFile outputFile : rule.getOutputFiles()) {
+ targets.put(outputFile.getName(), outputFile);
+ PathFragment outputFileFragment = new PathFragment(outputFile.getName());
+ for (int i = 1; i < outputFileFragment.segmentCount(); i++) {
+ String prefix = outputFileFragment.subFragment(0, i).toString();
+ if (!outputFilePrefixes.containsKey(prefix)) {
+ outputFilePrefixes.put(prefix, outputFile);
+ }
+ }
+ }
+ targets.put(rule.getName(), rule);
+ if (rule.containsErrors()) {
+ this.setContainsErrors();
+ }
+ }
+
+ private B beforeBuild() {
+ Preconditions.checkNotNull(pkg);
+ Preconditions.checkNotNull(filename);
+ Preconditions.checkNotNull(buildFileLabel);
+ Preconditions.checkNotNull(makeEnv);
+ // Freeze subincludes.
+ subincludes = (subincludes == null)
+ ? Collections.<Label, Path>emptyMap()
+ : Collections.unmodifiableMap(subincludes);
+
+ // We create the original BUILD InputFile when the package filename is set; however, the
+ // visibility may be overridden with an exports_files directive, so we need to obtain the
+ // current instance here.
+ buildFile = (InputFile) Preconditions.checkNotNull(targets.get(buildFileLabel.getName()));
+
+ List<Rule> rules = Lists.newArrayList(getTargets(Rule.class));
+
+ // All labels mentioned in a rule that refer to an unknown target in the
+ // current package are assumed to be InputFiles, so let's create them:
+ for (final Rule rule : rules) {
+ AggregatingAttributeMapper.of(rule).visitLabels(new AcceptsLabelAttribute() {
+ @Override
+ public void acceptLabelAttribute(Label label, Attribute attribute) {
+ createInputFileMaybe(label, rule.getAttributeLocation(attribute.getName()));
+ }
+ });
+ }
+
+ // "test_suite" rules have the idiosyncratic semantics of implicitly
+ // depending on all tests in the package, iff tests=[] and suites=[].
+ // Note, we implement this here when the Package is fully constructed,
+ // since clearly this information isn't available at Rule construction
+ // time, as forward references are permitted.
+ List<Label> allTests = new ArrayList<>();
+ for (Rule rule : rules) {
+ if (TargetUtils.isTestRule(rule) && !TargetUtils.hasManualTag(rule)
+ && !TargetUtils.isObsolete(rule)) {
+ allTests.add(rule.getLabel());
+ }
+ }
+ for (Rule rule : rules) {
+ AttributeMap attributes = NonconfigurableAttributeMapper.of(rule);
+ if (rule.getRuleClass().equals("test_suite")
+ && attributes.get("tests", Type.LABEL_LIST).isEmpty()
+ && attributes.get("suites", Type.LABEL_LIST).isEmpty()) {
+ rule.setAttributeValueByName("$implicit_tests", allTests);
+ }
+ }
+ return self();
+ }
+
+ /** Intended to be used only by {@link PackageFunction}. */
+ public B buildPartial() {
+ if (alreadyBuilt) {
+ return self();
+ }
+ return beforeBuild();
+ }
+
+ /** Intended to be used only by {@link PackageFunction}. */
+ public P finishBuild() {
+ if (alreadyBuilt) {
+ return pkg;
+ }
+ // Freeze targets and distributions.
+ targets = ImmutableMap.copyOf(targets);
+ defaultDistributionSet =
+ Collections.unmodifiableSet(defaultDistributionSet);
+
+ // Now all targets have been loaded, so we can check all declared environments in an
+ // environment group exist.
+ for (EnvironmentGroup envGroup : ImmutableSet.copyOf(environmentGroups.values())) {
+ Collection<Event> errors = envGroup.checkEnvironmentsExist(targets);
+ if (!errors.isEmpty()) {
+ addEvents(errors);
+ setContainsErrors();
+ }
+ }
+
+ // Build the package.
+ pkg.finishInit(this);
+ alreadyBuilt = true;
+ return pkg;
+ }
+
+ public P build() {
+ if (alreadyBuilt) {
+ return pkg;
+ }
+ beforeBuild();
+ return finishBuild();
+ }
+
+ /**
+ * If "label" refers to a non-existent target in the current package, create
+ * an InputFile target.
+ */
+ void createInputFileMaybe(Label label, Location location) {
+ if (label != null && label.getPackageFragment().equals(pkg.getNameFragment())) {
+ if (!targets.containsKey(label.getName())) {
+ addInputFile(label, location);
+ }
+ }
+ }
+
+ private InputFile addInputFile(Label label, Location location) {
+ InputFile inputFile = new InputFile(pkg, label, location);
+ Target prev = targets.put(label.getName(), inputFile);
+ Preconditions.checkState(prev == null);
+ return inputFile;
+ }
+
+ /**
+ * Precondition check for addRule. We must maintain these invariants of the
+ * package:
+ * - Each name refers to at most one target.
+ * - No rule with errors is inserted into the package.
+ * - The generating rule of every output file in the package must itself be
+ * in the package.
+ */
+ private void checkForConflicts(Rule rule) throws NameConflictException {
+ String name = rule.getName();
+ Target existing = targets.get(name);
+ if (existing != null) {
+ throw nameConflict(rule, existing);
+ }
+ Map<String, OutputFile> outputFiles = new HashMap<>();
+
+ for (OutputFile outputFile : rule.getOutputFiles()) {
+ String outputFileName = outputFile.getName();
+ if (outputFiles.put(outputFileName, outputFile) != null) { // dups within a single rule:
+ throw duplicateOutputFile(outputFile, outputFile);
+ }
+ existing = targets.get(outputFileName);
+ if (existing != null) {
+ throw duplicateOutputFile(outputFile, existing);
+ }
+
+ // Check if this output file is the prefix of an already existing one
+ if (outputFilePrefixes.containsKey(outputFileName)) {
+ throw conflictingOutputFile(outputFile, outputFilePrefixes.get(outputFileName));
+ }
+
+ // Check if a prefix of this output file matches an already existing one
+ PathFragment outputFileFragment = new PathFragment(outputFileName);
+ for (int i = 1; i < outputFileFragment.segmentCount(); i++) {
+ String prefix = outputFileFragment.subFragment(0, i).toString();
+ if (outputFiles.containsKey(prefix)) {
+ throw conflictingOutputFile(outputFile, outputFiles.get(prefix));
+ }
+ if (targets.containsKey(prefix)
+ && targets.get(prefix) instanceof OutputFile) {
+ throw conflictingOutputFile(outputFile, (OutputFile) targets.get(prefix));
+ }
+
+ if (!outputFilePrefixes.containsKey(prefix)) {
+ outputFilePrefixes.put(prefix, outputFile);
+ }
+ }
+ }
+
+ checkForInputOutputConflicts(rule, outputFiles.keySet());
+ }
+
+ /**
+ * A utility method that checks for conflicts between
+ * input file names and output file names for a rule from a build
+ * file.
+ * @param rule the rule whose inputs and outputs are
+ * to be checked for conflicts.
+ * @param outputFiles a set containing the names of output
+ * files to be generated by the rule.
+ * @throws NameConflictException if a conflict is found.
+ */
+ private void checkForInputOutputConflicts(Rule rule, Set<String> outputFiles)
+ throws NameConflictException {
+ PathFragment packageFragment = rule.getLabel().getPackageFragment();
+ for (Label inputLabel : rule.getLabels()) {
+ if (packageFragment.equals(inputLabel.getPackageFragment())
+ && outputFiles.contains(inputLabel.getName())) {
+ throw inputOutputNameConflict(rule, inputLabel.getName());
+ }
+ }
+ }
+
+ /** An output file conflicts with another output file or the BUILD file. */
+ private NameConflictException duplicateOutputFile(OutputFile duplicate, Target existing) {
+ return new NameConflictException(duplicate.getTargetKind() + " '" + duplicate.getName()
+ + "' in rule '" + duplicate.getGeneratingRule().getName() + "' "
+ + conflictsWith(existing));
+ }
+
+ /** The package contains two targets with the same name. */
+ private NameConflictException nameConflict(Target duplicate, Target existing) {
+ return new NameConflictException(duplicate.getTargetKind() + " '" + duplicate.getName()
+ + "' in package '" + duplicate.getLabel().getPackageName() + "' "
+ + conflictsWith(existing));
+ }
+
+ /** A a rule has a input/output name conflict. */
+ private NameConflictException inputOutputNameConflict(Rule rule, String conflictingName) {
+ return new NameConflictException("rule '" + rule.getName() + "' has file '"
+ + conflictingName + "' as both an input and an output");
+ }
+
+ private static NameConflictException conflictingOutputFile(
+ OutputFile added, OutputFile existing) {
+ if (added.getGeneratingRule() == existing.getGeneratingRule()) {
+ return new NameConflictException(String.format(
+ "rule '%s' has conflicting output files '%s' and '%s'", added.getGeneratingRule()
+ .getName(), added.getName(), existing.getName()));
+ } else {
+ return new NameConflictException(String.format(
+ "output file '%s' of rule '%s' conflicts with output file '%s' of rule '%s'", added
+ .getName(), added.getGeneratingRule().getName(), existing.getName(), existing
+ .getGeneratingRule().getName()));
+ }
+ }
+
+ /**
+ * Utility function for generating exception messages.
+ */
+ private static String conflictsWith(Target target) {
+ String message = "conflicts with existing ";
+ if (target instanceof OutputFile) {
+ return message + "generated file from rule '"
+ + ((OutputFile) target).getGeneratingRule().getName()
+ + "'";
+ } else {
+ return message + target.getTargetKind();
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/PackageDeserializer.java b/src/main/java/com/google/devtools/build/lib/packages/PackageDeserializer.java
new file mode 100644
index 0000000..5eca0f4
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/PackageDeserializer.java
@@ -0,0 +1,536 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableList.Builder;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.events.NullEventHandler;
+import com.google.devtools.build.lib.events.StoredEventHandler;
+import com.google.devtools.build.lib.packages.License.DistributionType;
+import com.google.devtools.build.lib.packages.License.LicenseParsingException;
+import com.google.devtools.build.lib.packages.Package.AbstractBuilder.GeneratedLabelConflict;
+import com.google.devtools.build.lib.packages.Package.NameConflictException;
+import com.google.devtools.build.lib.packages.RuleClass.ParsedAttributeValue;
+import com.google.devtools.build.lib.query2.proto.proto2api.Build;
+import com.google.devtools.build.lib.query2.proto.proto2api.Build.StringDictUnaryEntry;
+import com.google.devtools.build.lib.syntax.FilesetEntry;
+import com.google.devtools.build.lib.syntax.GlobCriteria;
+import com.google.devtools.build.lib.syntax.GlobList;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.syntax.Label.SyntaxException;
+import com.google.devtools.build.lib.vfs.FileSystem;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Functionality to deserialize loaded packages.
+ */
+public class PackageDeserializer {
+
+ // Workaround for Java serialization not allowing to pass in a context manually.
+ // volatile is needed to ensure that the objects are published safely.
+ // TODO(bazel-team): Subclass ObjectOutputStream to pass through environment variables.
+ public static volatile RuleClassProvider defaultRuleClassProvider;
+ public static volatile FileSystem defaultDeserializerFileSystem;
+
+ private class Context {
+ private final Package.Builder packageBuilder;
+ private final Path buildFilePath;
+
+ public Context(Path buildFilePath, Package.Builder packageBuilder) {
+ this.buildFilePath = buildFilePath;
+ this.packageBuilder = packageBuilder;
+ }
+
+ Location deserializeLocation(Build.Location location) {
+ return new ExplicitLocation(buildFilePath, location);
+ }
+
+ ParsedAttributeValue deserializeAttribute(Type<?> expectedType,
+ Build.Attribute attrPb)
+ throws PackageDeserializationException {
+ Object value = deserializeAttributeValue(expectedType, attrPb);
+ return new ParsedAttributeValue(
+ attrPb.hasExplicitlySpecified() ? attrPb.getExplicitlySpecified() : false,
+ value,
+ deserializeLocation(attrPb.getParseableLocation()));
+ }
+
+ void deserializeInputFile(Build.SourceFile sourceFile)
+ throws PackageDeserializationException {
+ InputFile inputFile;
+ try {
+ inputFile = packageBuilder.createInputFile(
+ deserializeLabel(sourceFile.getName()).getName(),
+ deserializeLocation(sourceFile.getParseableLocation()));
+ } catch (GeneratedLabelConflict e) {
+ throw new PackageDeserializationException(e);
+ }
+
+ if (!sourceFile.getVisibilityLabelList().isEmpty() || sourceFile.hasLicense()) {
+ packageBuilder.setVisibilityAndLicense(inputFile,
+ PackageFactory.getVisibility(deserializeLabels(sourceFile.getVisibilityLabelList())),
+ deserializeLicense(sourceFile.getLicense()));
+ }
+ }
+
+ void deserializePackageGroup(Build.PackageGroup packageGroupPb)
+ throws PackageDeserializationException {
+ List<String> specifications = new ArrayList<>();
+ for (String containedPackage : packageGroupPb.getContainedPackageList()) {
+ specifications.add("//" + containedPackage);
+ }
+
+ try {
+ packageBuilder.addPackageGroup(
+ deserializeLabel(packageGroupPb.getName()).getName(),
+ specifications,
+ deserializeLabels(packageGroupPb.getIncludedPackageGroupList()),
+ NullEventHandler.INSTANCE, // TODO(bazel-team): Handle errors properly
+ deserializeLocation(packageGroupPb.getParseableLocation()));
+ } catch (Label.SyntaxException | Package.NameConflictException e) {
+ throw new PackageDeserializationException(e);
+ }
+ }
+
+ void deserializeRule(Build.Rule rulePb)
+ throws PackageDeserializationException {
+ RuleClass ruleClass = ruleClassProvider.getRuleClassMap().get(rulePb.getRuleClass());
+ if (ruleClass == null) {
+ throw new PackageDeserializationException(
+ String.format("Invalid rule class '%s'", ruleClass));
+ }
+
+ Map<String, ParsedAttributeValue> attributeValues = new HashMap<>();
+ for (Build.Attribute attrPb : rulePb.getAttributeList()) {
+ Type<?> type = ruleClass.getAttributeByName(attrPb.getName()).getType();
+ attributeValues.put(attrPb.getName(), deserializeAttribute(type, attrPb));
+ }
+
+ Label ruleLabel = deserializeLabel(rulePb.getName());
+ Location ruleLocation = deserializeLocation(rulePb.getParseableLocation());
+ try {
+ Rule rule = ruleClass.createRuleWithParsedAttributeValues(
+ ruleLabel, packageBuilder, ruleLocation, attributeValues,
+ NullEventHandler.INSTANCE);
+ packageBuilder.addRule(rule);
+
+ Preconditions.checkState(!rule.containsErrors());
+ } catch (NameConflictException | SyntaxException e) {
+ throw new PackageDeserializationException(e);
+ }
+ }
+ }
+
+ private final FileSystem fileSystem;
+ private final RuleClassProvider ruleClassProvider;
+
+ @Immutable
+ private static final class ExplicitLocation extends Location {
+ private final PathFragment path;
+ private final int startLine;
+ private final int startColumn;
+ private final int endLine;
+ private final int endColumn;
+
+ private ExplicitLocation(Path path, Build.Location location) {
+ super(
+ location.hasStartOffset() && location.hasEndOffset() ? location.getStartOffset() : 0,
+ location.hasStartOffset() && location.hasEndOffset() ? location.getEndOffset() : 0);
+ this.path = path.asFragment();
+ if (location.hasStartLine() && location.hasStartColumn() &&
+ location.hasEndLine() && location.hasEndColumn()) {
+ this.startLine = location.getStartLine();
+ this.startColumn = location.getStartColumn();
+ this.endLine = location.getEndLine();
+ this.endColumn = location.getEndColumn();
+ } else {
+ this.startLine = 0;
+ this.startColumn = 0;
+ this.endLine = 0;
+ this.endColumn = 0;
+ }
+ }
+
+ @Override
+ public PathFragment getPath() {
+ return path;
+ }
+
+ @Override
+ public LineAndColumn getStartLineAndColumn() {
+ return new LineAndColumn(startLine, startColumn);
+ }
+
+ @Override
+ public LineAndColumn getEndLineAndColumn() {
+ return new LineAndColumn(endLine, endColumn);
+ }
+ }
+
+ public PackageDeserializer(FileSystem fileSystem, RuleClassProvider ruleClassProvider) {
+ if (fileSystem == null) {
+ fileSystem = defaultDeserializerFileSystem;
+ }
+ this.fileSystem = Preconditions.checkNotNull(fileSystem);
+ if (ruleClassProvider == null) {
+ ruleClassProvider = defaultRuleClassProvider;
+ }
+ this.ruleClassProvider = Preconditions.checkNotNull(ruleClassProvider);
+ }
+
+ /**
+ * Exception thrown when something goes wrong during package deserialization.
+ */
+ public static class PackageDeserializationException extends Exception {
+ private PackageDeserializationException(String message) {
+ super(message);
+ }
+
+ private PackageDeserializationException(String message, Exception reason) {
+ super(message, reason);
+ }
+
+ private PackageDeserializationException(Exception reason) {
+ super(reason);
+ }
+ }
+
+ private static Label deserializeLabel(String labelName) throws PackageDeserializationException {
+ try {
+ return Label.parseRepositoryLabel(labelName);
+ } catch (Label.SyntaxException e) {
+ throw new PackageDeserializationException("Invalid label: " + e.getMessage(), e);
+ }
+ }
+
+ private static List<Label> deserializeLabels(List<String> labelNames)
+ throws PackageDeserializationException {
+ ImmutableList.Builder<Label> result = ImmutableList.builder();
+ for (String labelName : labelNames) {
+ result.add(deserializeLabel(labelName));
+ }
+
+ return result.build();
+ }
+
+ private static License deserializeLicense(Build.License licensePb)
+ throws PackageDeserializationException {
+ List<String> licenseStrings = new ArrayList<>();
+ licenseStrings.addAll(licensePb.getLicenseTypeList());
+ for (String exception : licensePb.getExceptionList()) {
+ licenseStrings.add("exception=" + exception);
+ }
+
+ try {
+ return License.parseLicense(licenseStrings);
+ } catch (LicenseParsingException e) {
+ throw new PackageDeserializationException(e);
+ }
+ }
+
+ private static Set<DistributionType> deserializeDistribs(List<String> distributions)
+ throws PackageDeserializationException {
+ try {
+ return License.parseDistributions(distributions);
+ } catch (LicenseParsingException e) {
+ throw new PackageDeserializationException(e);
+ }
+ }
+
+ private static TriState deserializeTriStateValue(String value)
+ throws PackageDeserializationException {
+ if (value.equals("yes")) {
+ return TriState.YES;
+ } else if (value.equals("no")) {
+ return TriState.NO;
+ } else if (value.equals("auto")) {
+ return TriState.AUTO;
+ } else {
+ throw new PackageDeserializationException(
+ String.format("Invalid tristate value: '%s'", value));
+ }
+ }
+
+ private static List<FilesetEntry> deserializeFilesetEntries(
+ List<Build.FilesetEntry> filesetPbs)
+ throws PackageDeserializationException {
+ ImmutableList.Builder<FilesetEntry> result = ImmutableList.builder();
+ for (Build.FilesetEntry filesetPb : filesetPbs) {
+ Label srcLabel = deserializeLabel(filesetPb.getSource());
+ List<Label> files =
+ filesetPb.getFilesPresent() ? deserializeLabels(filesetPb.getFileList()) : null;
+ List<String> excludes =
+ filesetPb.getExcludeList().isEmpty() ?
+ null : ImmutableList.copyOf(filesetPb.getExcludeList());
+ String destDir = filesetPb.getDestinationDirectory();
+ FilesetEntry.SymlinkBehavior symlinkBehavior =
+ pbToSymlinkBehavior(filesetPb.getSymlinkBehavior());
+ String stripPrefix = filesetPb.hasStripPrefix() ? filesetPb.getStripPrefix() : null;
+
+ result.add(
+ new FilesetEntry(srcLabel, files, excludes, destDir, symlinkBehavior, stripPrefix));
+ }
+
+ return result.build();
+ }
+
+ /**
+ * Deserialize a package from its representation as a protocol message. The inverse of
+ * {@link PackageSerializer#serializePackage}.
+ */
+ private void deserializeInternal(Build.Package packagePb, StoredEventHandler eventHandler,
+ Package.Builder builder) throws PackageDeserializationException {
+ Path buildFile = fileSystem.getPath(packagePb.getBuildFilePath());
+ Preconditions.checkNotNull(buildFile);
+ Context context = new Context(buildFile, builder);
+ builder.setFilename(buildFile);
+
+ if (packagePb.hasDefaultVisibilitySet() && packagePb.getDefaultVisibilitySet()) {
+ builder.setDefaultVisibility(
+ PackageFactory.getVisibility(
+ deserializeLabels(packagePb.getDefaultVisibilityLabelList())));
+ }
+
+ // It's important to do this after setting the default visibility, since that implicitly sets
+ // this bit to true
+ builder.setDefaultVisibilitySet(packagePb.getDefaultVisibilitySet());
+ if (packagePb.hasDefaultObsolete()) {
+ builder.setDefaultObsolete(packagePb.getDefaultObsolete());
+ }
+ if (packagePb.hasDefaultTestonly()) {
+ builder.setDefaultTestonly(packagePb.getDefaultTestonly());
+ }
+ if (packagePb.hasDefaultDeprecation()) {
+ builder.setDefaultDeprecation(packagePb.getDefaultDeprecation());
+ }
+
+ builder.setDefaultCopts(packagePb.getDefaultCoptList());
+ if (packagePb.hasDefaultHdrsCheck()) {
+ builder.setDefaultHdrsCheck(packagePb.getDefaultHdrsCheck());
+ }
+ if (packagePb.hasDefaultLicense()) {
+ builder.setDefaultLicense(deserializeLicense(packagePb.getDefaultLicense()));
+ }
+ builder.setDefaultDistribs(deserializeDistribs(packagePb.getDefaultDistribList()));
+
+ for (String subinclude : packagePb.getSubincludeLabelList()) {
+ Label label = deserializeLabel(subinclude);
+ builder.addSubinclude(label, null);
+ }
+
+ ImmutableList.Builder<Label> skylarkFileDependencies = ImmutableList.builder();
+ for (String skylarkFile : packagePb.getSkylarkLabelList()) {
+ skylarkFileDependencies.add(deserializeLabel(skylarkFile));
+ }
+ builder.setSkylarkFileDependencies(skylarkFileDependencies.build());
+
+ MakeEnvironment.Builder makeEnvBuilder = new MakeEnvironment.Builder();
+ for (Build.MakeVar makeVar : packagePb.getMakeVariableList()) {
+ for (Build.MakeVarBinding binding : makeVar.getBindingList()) {
+ makeEnvBuilder.update(
+ makeVar.getName(), binding.getValue(), binding.getPlatformSetRegexp());
+ }
+ }
+ builder.setMakeEnv(makeEnvBuilder);
+
+ for (Build.SourceFile sourceFile : packagePb.getSourceFileList()) {
+ context.deserializeInputFile(sourceFile);
+ }
+
+ for (Build.PackageGroup packageGroupPb :
+ packagePb.getPackageGroupList()) {
+ context.deserializePackageGroup(packageGroupPb);
+ }
+
+ for (Build.Rule rulePb : packagePb.getRuleList()) {
+ context.deserializeRule(rulePb);
+ }
+
+ for (Build.Event event : packagePb.getEventList()) {
+ deserializeEvent(context, eventHandler, event);
+ }
+
+ if (packagePb.hasContainsErrors() && packagePb.getContainsErrors()) {
+ builder.setContainsErrors();
+ }
+ if (packagePb.hasContainsTemporaryErrors() && packagePb.getContainsTemporaryErrors()) {
+ builder.setContainsTemporaryErrors();
+ }
+ }
+
+ /**
+ * Deserialize a protocol message to a package. The inverse of
+ * {@link PackageSerializer#serializePackage}.
+ */
+ public Package deserialize(Build.Package packagePb)
+ throws PackageDeserializationException {
+ Package.Builder builder;
+ try {
+ builder = new Package.Builder(
+ new PackageIdentifier(packagePb.getRepository(), new PathFragment(packagePb.getName())));
+ } catch (SyntaxException e) {
+ throw new PackageDeserializationException(e);
+ }
+ StoredEventHandler eventHandler = new StoredEventHandler();
+ deserializeInternal(packagePb, eventHandler, builder);
+ builder.addEvents(eventHandler.getEvents());
+ return builder.build();
+ }
+
+ private static void deserializeEvent(
+ Context context, StoredEventHandler eventHandler, Build.Event event) {
+ Location location = null;
+ if (event.hasLocation()) {
+ location = context.deserializeLocation(event.getLocation());
+ }
+
+ String message = event.getMessage();
+ switch (event.getKind()) {
+ case ERROR: eventHandler.handle(Event.error(location, message)); break;
+ case WARNING: eventHandler.handle(Event.warn(location, message)); break;
+ case INFO: eventHandler.handle(Event.info(location, message)); break;
+ case PROGRESS: eventHandler.handle(Event.progress(location, message)); break;
+ default: break; // Ignore
+ }
+ }
+
+ private static List<?> deserializeGlobs(List<?> matches,
+ Build.Attribute attrPb) {
+ if (attrPb.getGlobCriteriaCount() == 0) {
+ return matches;
+ }
+
+ Builder<GlobCriteria> criteriaBuilder = ImmutableList.builder();
+ for (Build.GlobCriteria criteriaPb : attrPb.getGlobCriteriaList()) {
+ if (criteriaPb.hasGlob() && criteriaPb.getGlob()) {
+ criteriaBuilder.add(GlobCriteria.fromGlobCall(
+ ImmutableList.copyOf(criteriaPb.getIncludeList()),
+ ImmutableList.copyOf(criteriaPb.getExcludeList())));
+ } else {
+ criteriaBuilder.add(
+ GlobCriteria.fromList(ImmutableList.copyOf(criteriaPb.getIncludeList())));
+ }
+ }
+
+ @SuppressWarnings({"unchecked", "rawtypes"}) GlobList<?> result =
+ new GlobList(criteriaBuilder.build(), matches);
+ return result;
+ }
+
+ // TODO(bazel-team): Verify that these put sane values in the attribute
+ private static Object deserializeAttributeValue(Type<?> expectedType,
+ Build.Attribute attrPb)
+ throws PackageDeserializationException {
+ switch (attrPb.getType()) {
+ case INTEGER:
+ return new Integer(attrPb.getIntValue());
+
+ case STRING:
+ if (expectedType == Type.NODEP_LABEL) {
+ return deserializeLabel(attrPb.getStringValue());
+ } else {
+ return attrPb.getStringValue();
+ }
+
+ case LABEL:
+ case OUTPUT:
+ return deserializeLabel(attrPb.getStringValue());
+
+ case STRING_LIST:
+ if (expectedType == Type.NODEP_LABEL_LIST) {
+ return deserializeGlobs(deserializeLabels(attrPb.getStringListValueList()), attrPb);
+ } else {
+ return deserializeGlobs(ImmutableList.copyOf(attrPb.getStringListValueList()), attrPb);
+ }
+
+ case LABEL_LIST:
+ case OUTPUT_LIST:
+ return deserializeGlobs(deserializeLabels(attrPb.getStringListValueList()), attrPb);
+
+ case DISTRIBUTION_SET:
+ return deserializeDistribs(attrPb.getStringListValueList());
+
+ case LICENSE:
+ return deserializeLicense(attrPb.getLicense());
+
+ case STRING_DICT: {
+ ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
+ for (Build.StringDictEntry entry : attrPb.getStringDictValueList()) {
+ builder.put(entry.getKey(), entry.getValue());
+ }
+ return builder.build();
+ }
+
+ case STRING_DICT_UNARY: {
+ ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
+ for (StringDictUnaryEntry entry : attrPb.getStringDictUnaryValueList()) {
+ builder.put(entry.getKey(), entry.getValue());
+ }
+ return builder.build();
+ }
+
+ case FILESET_ENTRY_LIST:
+ return deserializeFilesetEntries(attrPb.getFilesetListValueList());
+
+ case LABEL_LIST_DICT: {
+ ImmutableMap.Builder<String, List<Label>> builder = ImmutableMap.builder();
+ for (Build.LabelListDictEntry entry : attrPb.getLabelListDictValueList()) {
+ builder.put(entry.getKey(), deserializeLabels(entry.getValueList()));
+ }
+ return builder.build();
+ }
+
+ case STRING_LIST_DICT: {
+ ImmutableMap.Builder<String, List<String>> builder = ImmutableMap.builder();
+ for (Build.StringListDictEntry entry : attrPb.getStringListDictValueList()) {
+ builder.put(entry.getKey(), ImmutableList.copyOf(entry.getValueList()));
+ }
+ return builder.build();
+ }
+
+ case BOOLEAN:
+ return attrPb.getBooleanValue();
+
+ case TRISTATE:
+ return deserializeTriStateValue(attrPb.getStringValue());
+
+ default:
+ throw new PackageDeserializationException("Invalid discriminator: " + attrPb.getType());
+ }
+ }
+
+ private static FilesetEntry.SymlinkBehavior pbToSymlinkBehavior(
+ Build.FilesetEntry.SymlinkBehavior symlinkBehavior) {
+ switch (symlinkBehavior) {
+ case COPY:
+ return FilesetEntry.SymlinkBehavior.COPY;
+ case DEREFERENCE:
+ return FilesetEntry.SymlinkBehavior.DEREFERENCE;
+ default:
+ throw new IllegalStateException();
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/PackageFactory.java b/src/main/java/com/google/devtools/build/lib/packages/PackageFactory.java
new file mode 100644
index 0000000..abf63f9
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/PackageFactory.java
@@ -0,0 +1,1272 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import com.google.devtools.build.lib.cmdline.LabelValidator;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.events.NullEventHandler;
+import com.google.devtools.build.lib.events.StoredEventHandler;
+import com.google.devtools.build.lib.packages.GlobCache.BadGlobException;
+import com.google.devtools.build.lib.packages.License.DistributionType;
+import com.google.devtools.build.lib.packages.Type.ConversionException;
+import com.google.devtools.build.lib.syntax.AbstractFunction;
+import com.google.devtools.build.lib.syntax.AssignmentStatement;
+import com.google.devtools.build.lib.syntax.BuildFileAST;
+import com.google.devtools.build.lib.syntax.Environment;
+import com.google.devtools.build.lib.syntax.Environment.NoSuchVariableException;
+import com.google.devtools.build.lib.syntax.EvalException;
+import com.google.devtools.build.lib.syntax.Expression;
+import com.google.devtools.build.lib.syntax.FuncallExpression;
+import com.google.devtools.build.lib.syntax.Function;
+import com.google.devtools.build.lib.syntax.GlobList;
+import com.google.devtools.build.lib.syntax.Ident;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.syntax.MixedModeFunction;
+import com.google.devtools.build.lib.syntax.ParserInputSource;
+import com.google.devtools.build.lib.syntax.SkylarkEnvironment;
+import com.google.devtools.build.lib.syntax.Statement;
+import com.google.devtools.build.lib.util.Pair;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.lib.vfs.UnixGlob;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.logging.Logger;
+
+import javax.annotation.Nullable;
+
+/**
+ * The package factory is responsible for constructing Package instances
+ * from a BUILD file's abstract syntax tree (AST).
+ *
+ * <p>A PackageFactory is a heavy-weight object; create them sparingly.
+ * Typically only one is needed per client application.
+ */
+public final class PackageFactory {
+ /**
+ * An argument to the {@code package()} function.
+ */
+ public abstract static class PackageArgument<T> {
+ private final String name;
+ private final Type<T> type;
+
+ protected PackageArgument(String name, Type<T> type) {
+ this.name = name;
+ this.type = type;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ private void convertAndProcess(
+ Package.LegacyBuilder pkgBuilder, Location location, Object value)
+ throws EvalException, ConversionException {
+ T typedValue = type.convert(value, "'package' argument", pkgBuilder.getBuildFileLabel());
+ process(pkgBuilder, location, typedValue);
+ }
+
+ /**
+ * Process an argument.
+ *
+ * @param pkgBuilder the package builder to be mutated
+ * @param location the location of the {@code package} function for error reporting
+ * @param value the value of the argument. Typically passed to {@link Type#convert}
+ */
+ protected abstract void process(
+ Package.LegacyBuilder pkgBuilder, Location location, T value)
+ throws EvalException;
+ }
+
+ /** Interface for evaluating globs during package loading. */
+ public static interface Globber {
+ /** An opaque token for fetching the result of a glob computation. */
+ abstract static class Token {}
+
+ /**
+ * Asynchronously starts the given glob computation and returns a token for fetching the
+ * result.
+ */
+ Token runAsync(List<String> includes, List<String> excludes, boolean excludeDirs)
+ throws BadGlobException;
+
+ /** Fetches the result of a previously started glob computation. */
+ List<String> fetch(Token token) throws IOException, InterruptedException;
+
+ /** Should be called when the globber is about to be discarded due to an interrupt. */
+ void onInterrupt();
+
+ /** Should be called when the globber is no longer needed. */
+ void onCompletion();
+
+ /** Returns all the glob computations requested before {@link #onCompletion} was called. */
+ Set<Pair<String, Boolean>> getGlobPatterns();
+ }
+
+ /**
+ * An extension to the global namespace of the BUILD language.
+ */
+ public interface EnvironmentExtension {
+ /**
+ * Update the global environment with the identifiers this extension contributes.
+ */
+ void update(Environment environment, MakeEnvironment.Builder pkgMakeEnv,
+ Label buildFileLabel);
+
+ Iterable<PackageArgument<?>> getPackageArguments();
+ }
+
+ private static final int EXCLUDE_DIR_DEFAULT = 1;
+
+ private static class DefaultVisibility extends PackageArgument<List<Label>> {
+ private DefaultVisibility() {
+ super("default_visibility", Type.LABEL_LIST);
+ }
+
+ @Override
+ protected void process(Package.LegacyBuilder pkgBuilder, Location location,
+ List<Label> value) {
+ pkgBuilder.setDefaultVisibility(getVisibility(value));
+ }
+ }
+
+ private static class DefaultObsolete extends PackageArgument<Boolean> {
+ private DefaultObsolete() {
+ super("default_obsolete", Type.BOOLEAN);
+ }
+
+ @Override
+ protected void process(Package.LegacyBuilder pkgBuilder, Location location,
+ Boolean value) {
+ pkgBuilder.setDefaultObsolete(value);
+ }
+ }
+
+ private static class DefaultTestOnly extends PackageArgument<Boolean> {
+ private DefaultTestOnly() {
+ super("default_testonly", Type.BOOLEAN);
+ }
+
+ @Override
+ protected void process(Package.LegacyBuilder pkgBuilder, Location location,
+ Boolean value) {
+ pkgBuilder.setDefaultTestonly(value);
+ }
+ }
+
+ private static class DefaultDeprecation extends PackageArgument<String> {
+ private DefaultDeprecation() {
+ super("default_deprecation", Type.STRING);
+ }
+
+ @Override
+ protected void process(Package.LegacyBuilder pkgBuilder, Location location,
+ String value) {
+ pkgBuilder.setDefaultDeprecation(value);
+ }
+ }
+
+ private static class Features extends PackageArgument<List<String>> {
+ private Features() {
+ super("features", Type.STRING_LIST);
+ }
+
+ @Override
+ protected void process(Package.LegacyBuilder pkgBuilder, Location location,
+ List<String> value) {
+ pkgBuilder.addFeatures(value);
+ }
+ }
+
+ private static class DefaultLicenses extends PackageArgument<License> {
+ private DefaultLicenses() {
+ super("licenses", Type.LICENSE);
+ }
+
+ @Override
+ protected void process(Package.LegacyBuilder pkgBuilder, Location location,
+ License value) {
+ pkgBuilder.setDefaultLicense(value);
+ }
+ }
+
+ private static class DefaultDistribs extends PackageArgument<Set<DistributionType>> {
+ private DefaultDistribs() {
+ super("distribs", Type.DISTRIBUTIONS);
+ }
+
+ @Override
+ protected void process(Package.LegacyBuilder pkgBuilder, Location location,
+ Set<DistributionType> value) {
+ pkgBuilder.setDefaultDistribs(value);
+ }
+ }
+
+ /**
+ * Declares the package() attribute specifying the default value for
+ * {@link RuleClass#COMPATIBLE_ENVIRONMENT_ATTR} when not explicitly specified.
+ */
+ private static class DefaultCompatibleWith extends PackageArgument<List<Label>> {
+ private DefaultCompatibleWith() {
+ super(Package.DEFAULT_COMPATIBLE_WITH_ATTRIBUTE, Type.LABEL_LIST);
+ }
+
+ @Override
+ protected void process(Package.LegacyBuilder pkgBuilder, Location location,
+ List<Label> value) {
+ pkgBuilder.setDefaultCompatibleWith(value, Package.DEFAULT_COMPATIBLE_WITH_ATTRIBUTE,
+ location);
+ }
+ }
+
+ /**
+ * Declares the package() attribute specifying the default value for
+ * {@link RuleClass#RESTRICTED_ENVIRONMENT_ATTR} when not explicitly specified.
+ */
+ private static class DefaultRestrictedTo extends PackageArgument<List<Label>> {
+ private DefaultRestrictedTo() {
+ super(Package.DEFAULT_RESTRICTED_TO_ATTRIBUTE, Type.LABEL_LIST);
+ }
+
+ @Override
+ protected void process(Package.LegacyBuilder pkgBuilder, Location location,
+ List<Label> value) {
+ pkgBuilder.setDefaultRestrictedTo(value, Package.DEFAULT_RESTRICTED_TO_ATTRIBUTE, location);
+ }
+ }
+
+ public static final String PKG_CONTEXT = "$pkg_context";
+
+ /** {@link Globber} that uses the legacy GlobCache. */
+ public static class LegacyGlobber implements Globber {
+
+ private final GlobCache globCache;
+
+ public LegacyGlobber(GlobCache globCache) {
+ this.globCache = globCache;
+ }
+
+ private class Token extends Globber.Token {
+ public final List<String> includes;
+ public final List<String> excludes;
+ public final boolean excludeDirs;
+
+ public Token(List<String> includes, List<String> excludes, boolean excludeDirs) {
+ this.includes = includes;
+ this.excludes = excludes;
+ this.excludeDirs = excludeDirs;
+ }
+ }
+
+ @Override
+ public Set<Pair<String, Boolean>> getGlobPatterns() {
+ return globCache.getKeySet();
+ }
+
+ @Override
+ public Token runAsync(List<String> includes, List<String> excludes, boolean excludeDirs)
+ throws BadGlobException {
+ for (String pattern : Iterables.concat(includes, excludes)) {
+ globCache.getGlobAsync(pattern, excludeDirs);
+ }
+ return new Token(includes, excludes, excludeDirs);
+ }
+
+ @Override
+ public List<String> fetch(Globber.Token token) throws IOException, InterruptedException {
+ Token legacyToken = (Token) token;
+ try {
+ return globCache.glob(legacyToken.includes, legacyToken.excludes,
+ legacyToken.excludeDirs);
+ } catch (BadGlobException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ @Override
+ public void onInterrupt() {
+ globCache.cancelBackgroundTasks();
+ }
+
+ @Override
+ public void onCompletion() {
+ globCache.finishBackgroundTasks();
+ }
+ }
+
+ private static final Logger LOG = Logger.getLogger(PackageFactory.class.getName());
+
+ private final RuleFactory ruleFactory;
+ private final RuleClassProvider ruleClassProvider;
+ private final Environment globalEnv;
+
+ private AtomicReference<? extends UnixGlob.FilesystemCalls> syscalls;
+ private Preprocessor.Factory preprocessorFactory = Preprocessor.Factory.NullFactory.INSTANCE;
+
+ private final ThreadPoolExecutor threadPool;
+ private Map<String, String> platformSetRegexps;
+
+ private final ImmutableList<EnvironmentExtension> environmentExtensions;
+ private final ImmutableMap<String, PackageArgument<?>> packageArguments;
+
+ /**
+ * Constructs a {@code PackageFactory} instance with the given rule factory.
+ */
+ public PackageFactory(RuleClassProvider ruleClassProvider) {
+ this(ruleClassProvider, null, ImmutableList.<EnvironmentExtension>of());
+ }
+
+ @VisibleForTesting
+ public PackageFactory(RuleClassProvider ruleClassProvider,
+ EnvironmentExtension environmentExtensions) {
+ this(ruleClassProvider, null, ImmutableList.of(environmentExtensions));
+ }
+
+ /**
+ * Constructs a {@code PackageFactory} instance with a specific glob path translator
+ * and rule factory.
+ */
+ @VisibleForTesting
+ public PackageFactory(RuleClassProvider ruleClassProvider,
+ Map<String, String> platformSetRegexps,
+ Iterable<EnvironmentExtension> environmentExtensions) {
+ this.platformSetRegexps = platformSetRegexps;
+ this.ruleFactory = new RuleFactory(ruleClassProvider);
+ this.ruleClassProvider = ruleClassProvider;
+ globalEnv = newGlobalEnvironment();
+ threadPool = new ThreadPoolExecutor(100, 100, 3L, TimeUnit.SECONDS,
+ new LinkedBlockingQueue<Runnable>(),
+ new ThreadFactoryBuilder().setNameFormat("PackageFactory %d").build());
+ // Do not consume threads when not in use.
+ threadPool.allowCoreThreadTimeOut(true);
+ this.environmentExtensions = ImmutableList.copyOf(environmentExtensions);
+ this.packageArguments = createPackageArguments();
+ }
+
+ /**
+ * Sets the preprocessor used.
+ */
+ public void setPreprocessorFactory(Preprocessor.Factory preprocessorFactory) {
+ this.preprocessorFactory = preprocessorFactory;
+ }
+
+ /**
+ * Sets the syscalls cache used in globbing.
+ */
+ public void setSyscalls(AtomicReference<? extends UnixGlob.FilesystemCalls> syscalls) {
+ this.syscalls = Preconditions.checkNotNull(syscalls);
+ }
+
+ /**
+ * Returns the static environment initialized once and shared by all packages
+ * created by this factory. No updates occur to this environment once created.
+ */
+ @VisibleForTesting
+ public Environment getEnvironment() {
+ return globalEnv;
+ }
+
+ /**
+ * Returns the immutable, unordered set of names of all the known rule
+ * classes.
+ */
+ public Set<String> getRuleClassNames() {
+ return ruleFactory.getRuleClassNames();
+ }
+
+ /**
+ * Returns the {@link RuleClass} for the specified rule class name.
+ */
+ public RuleClass getRuleClass(String ruleClassName) {
+ return ruleFactory.getRuleClass(ruleClassName);
+ }
+
+ /**
+ * Returns the {@link RuleClassProvider} of this {@link PackageFactory}.
+ */
+ public RuleClassProvider getRuleClassProvider() {
+ return ruleClassProvider;
+ }
+
+ /**
+ * Creates the list of arguments for the 'package' function.
+ */
+ private ImmutableMap<String, PackageArgument<?>> createPackageArguments() {
+ ImmutableList.Builder<PackageArgument<?>> arguments =
+ ImmutableList.<PackageArgument<?>>builder()
+ .add(new DefaultDeprecation())
+ .add(new DefaultDistribs())
+ .add(new DefaultLicenses())
+ .add(new DefaultObsolete())
+ .add(new DefaultTestOnly())
+ .add(new DefaultVisibility())
+ .add(new Features())
+ .add(new DefaultCompatibleWith())
+ .add(new DefaultRestrictedTo());
+
+ for (EnvironmentExtension extension : environmentExtensions) {
+ arguments.addAll(extension.getPackageArguments());
+ }
+
+ ImmutableMap.Builder<String, PackageArgument<?>> packageArguments = ImmutableMap.builder();
+ for (PackageArgument<?> argument : arguments.build()) {
+ packageArguments.put(argument.getName(), argument);
+ }
+ return packageArguments.build();
+ }
+
+ /****************************************************************************
+ * Environment function factories.
+ */
+
+ /**
+ * Returns a function-value implementing "glob" in the specified package
+ * context.
+ *
+ * @param async if true, start globs in the background but don't block on their completion.
+ * Only use this for heuristic preloading.
+ */
+ private static Function newGlobFunction(
+ final PackageContext originalContext, final boolean async) {
+ List<String> params = ImmutableList.of("include", "exclude", "exclude_directories");
+ return new MixedModeFunction("glob", params, 1, false) {
+ @Override
+ public Object call(Object[] namedArguments, FuncallExpression ast, Environment env)
+ throws EvalException, ConversionException, InterruptedException {
+
+ // Skylark build extensions need to get the PackageContext from the Environment;
+ // async glob functions cannot do the same because the Environment is not thread safe.
+ PackageContext context;
+ if (originalContext == null) {
+ Preconditions.checkArgument(!async);
+ try {
+ context = (PackageContext) env.lookup(PKG_CONTEXT);
+ } catch (NoSuchVariableException e) {
+ throw new EvalException(ast.getLocation(), e.getMessage());
+ }
+ } else {
+ context = originalContext;
+ }
+
+ List<String> includes = Type.STRING_LIST.convert(namedArguments[0], "'glob' argument");
+ List<String> excludes = namedArguments[1] == null
+ ? Collections.<String>emptyList()
+ : Type.STRING_LIST.convert(namedArguments[1], "'glob' argument");
+ int excludeDirs = namedArguments[2] == null
+ ? EXCLUDE_DIR_DEFAULT
+ : Type.INTEGER.convert(namedArguments[2], "'glob' argument");
+
+ if (async) {
+ try {
+ context.globber.runAsync(includes, excludes, excludeDirs != 0);
+ } catch (GlobCache.BadGlobException e) {
+ // Ignore: errors will appear during the actual evaluation of the package.
+ }
+ return GlobList.captureResults(includes, excludes, ImmutableList.<String>of());
+ } else {
+ return handleGlob(includes, excludes, excludeDirs != 0, context, ast);
+ }
+ }
+ };
+ }
+
+ /**
+ * Adds a glob to the package, reporting any errors it finds.
+ *
+ * @param includes the list of includes which must be non-null
+ * @param excludes the list of excludes which must be non-null
+ * @param context the package context
+ * @param ast the AST
+ * @return the list of matches
+ * @throws EvalException if globbing failed
+ */
+ private static GlobList<String> handleGlob(List<String> includes, List<String> excludes,
+ boolean excludeDirs, PackageContext context, FuncallExpression ast)
+ throws EvalException, InterruptedException {
+ try {
+ Globber.Token globToken = context.globber.runAsync(includes, excludes, excludeDirs);
+ List<String> matches = context.globber.fetch(globToken);
+ return GlobList.captureResults(includes, excludes, matches);
+ } catch (IOException expected) {
+ context.eventHandler.handle(Event.error(ast.getLocation(),
+ "error globbing [" + Joiner.on(", ").join(includes) + "]: " + expected.getMessage()));
+ context.pkgBuilder.setContainsTemporaryErrors();
+ return GlobList.captureResults(includes, excludes, ImmutableList.<String>of());
+ } catch (GlobCache.BadGlobException e) {
+ throw new EvalException(ast.getLocation(), e.getMessage());
+ }
+ }
+
+ /**
+ * Returns a function value implementing the "mocksubinclude" function,
+ * emitted by the PythonPreprocessor. We annotate the
+ * package with additional dependencies. (A 'real' subinclude will never be
+ * seen by the parser, because the presence of "subinclude" triggers
+ * preprocessing.)
+ */
+ private static Function newMockSubincludeFunction(final PackageContext context) {
+ return new MixedModeFunction("mocksubinclude", ImmutableList.of("label", "path"), 2, false) {
+ @Override
+ public Object call(Object[] args, FuncallExpression ast)
+ throws ConversionException {
+ Label label = Type.LABEL.convert(args[0], "'mocksubinclude' argument",
+ context.pkgBuilder.getBuildFileLabel());
+ String pathString = Type.STRING.convert(args[1], "'mocksubinclude' argument");
+ Path path = pathString.isEmpty()
+ ? null
+ : context.pkgBuilder.getFilename().getRelative(pathString);
+ // A subinclude within a package counts as a file declaration.
+ if (label.getPackageIdentifier().equals(context.pkgBuilder.getPackageIdentifier())) {
+ Location location = ast.getLocation();
+ if (location == null) {
+ location = Location.fromFile(context.pkgBuilder.getFilename());
+ }
+ context.pkgBuilder.createInputFileMaybe(label, location);
+ }
+
+ context.pkgBuilder.addSubinclude(label, path);
+ return Environment.NONE;
+ }
+ };
+ }
+
+ /**
+ * Fake function: subinclude calls are ignored
+ * They will disappear after the Python preprocessing.
+ */
+ private static Function newSubincludeFunction() {
+ return new MixedModeFunction("subinclude", ImmutableList.of("file"), 1, false) {
+ @Override
+ public Object call(Object[] args, FuncallExpression ast) {
+ return Environment.NONE;
+ }
+ };
+ }
+
+ /**
+ * Returns a function value implementing "environment_group" in the specified package context.
+ * Syntax is as follows:
+ *
+ * <pre>{@code
+ * environment_group(
+ * name = "sample_group",
+ * environments = [":env1", ":env2", ...],
+ * defaults = [":env1", ...]
+ * )
+ * }</pre>
+ *
+ * <p>Where ":env1", "env2", ... are all environment rules declared in the same package. All
+ * parameters are mandatory.
+ */
+ private static Function newEnvironmentGroupFunction(final PackageContext context) {
+ List<String> params = ImmutableList.of("name", "environments", "defaults");
+ return new MixedModeFunction("environment_group", params, params.size(), true) {
+ @Override
+ public Object call(Object[] namedArgs, FuncallExpression ast)
+ throws EvalException, ConversionException {
+ Preconditions.checkState(namedArgs[0] != null);
+ String name = Type.STRING.convert(namedArgs[0], "'environment_group' argument");
+ Preconditions.checkState(namedArgs[1] != null);
+ List<Label> environments = Type.LABEL_LIST.convert(
+ namedArgs[1], "'environment_group argument'", context.pkgBuilder.getBuildFileLabel());
+ Preconditions.checkState(namedArgs[2] != null);
+ List<Label> defaults = Type.LABEL_LIST.convert(
+ namedArgs[2], "'environment_group argument'", context.pkgBuilder.getBuildFileLabel());
+
+ try {
+ context.pkgBuilder.addEnvironmentGroup(name, environments, defaults,
+ context.eventHandler, ast.getLocation());
+ return Environment.NONE;
+ } catch (Label.SyntaxException e) {
+ throw new EvalException(ast.getLocation(),
+ "environment group has invalid name: " + name + ": " + e.getMessage());
+ } catch (Package.NameConflictException e) {
+ throw new EvalException(ast.getLocation(), e.getMessage());
+ }
+ }
+ };
+ }
+
+ /**
+ * Returns a function-value implementing "exports_files" in the specified
+ * package context.
+ */
+ private static Function newExportsFilesFunction(final PackageContext context) {
+ final Package.LegacyBuilder pkgBuilder = context.pkgBuilder;
+ List<String> params = ImmutableList.of("srcs", "visibility", "licenses");
+ return new MixedModeFunction("exports_files", params, 1, false) {
+ @Override
+ public Object call(Object[] namedArgs, FuncallExpression ast)
+ throws EvalException, ConversionException {
+
+ List<String> files = Type.STRING_LIST.convert(namedArgs[0], "'exports_files' operand");
+
+ RuleVisibility visibility = namedArgs[1] == null
+ ? ConstantRuleVisibility.PUBLIC
+ : getVisibility(Type.LABEL_LIST.convert(
+ namedArgs[1],
+ "'exports_files' operand",
+ pkgBuilder.getBuildFileLabel()));
+ License license = namedArgs[2] == null
+ ? null
+ : Type.LICENSE.convert(namedArgs[2], "'exports_files' operand");
+
+ for (String file : files) {
+ String errorMessage = LabelValidator.validateTargetName(file);
+ if (errorMessage != null) {
+ throw new EvalException(ast.getLocation(), errorMessage);
+ }
+ try {
+ InputFile inputFile = pkgBuilder.createInputFile(file, ast.getLocation());
+ if (inputFile.isVisibilitySpecified()
+ && inputFile.getVisibility() != visibility) {
+ throw new EvalException(ast.getLocation(),
+ String.format("visibility for exported file '%s' declared twice",
+ inputFile.getName()));
+ }
+ if (license != null && inputFile.isLicenseSpecified()) {
+ throw new EvalException(ast.getLocation(),
+ String.format("licenses for exported file '%s' declared twice",
+ inputFile.getName()));
+ }
+ if (license == null && pkgBuilder.getDefaultLicense() == License.NO_LICENSE
+ && pkgBuilder.getBuildFileLabel().toString().startsWith("//third_party/")) {
+ throw new EvalException(ast.getLocation(),
+ "third-party file '" + inputFile.getName() + "' lacks a license declaration "
+ + "with one of the following types: notice, reciprocal, permissive, "
+ + "restricted, unencumbered, by_exception_only");
+ }
+
+ pkgBuilder.setVisibilityAndLicense(inputFile, visibility, license);
+ } catch (Package.Builder.GeneratedLabelConflict e) {
+ throw new EvalException(ast.getLocation(), e.getMessage());
+ }
+ }
+ return Environment.NONE;
+ }
+ };
+ }
+
+ /**
+ * Returns a function-value implementing "licenses" in the specified package
+ * context.
+ * TODO(bazel-team): Remove in favor of package.licenses.
+ */
+ private static Function newLicensesFunction(final PackageContext context) {
+ return new MixedModeFunction("licenses", ImmutableList.of("object"), 1, false) {
+ @Override
+ public Object call(Object[] args, FuncallExpression ast) {
+ try {
+ License license = Type.LICENSE.convert(args[0], "'licenses' operand");
+ context.pkgBuilder.setDefaultLicense(license);
+ } catch (ConversionException e) {
+ context.eventHandler.handle(Event.error(ast.getLocation(), e.getMessage()));
+ context.pkgBuilder.setContainsErrors();
+ }
+ return Environment.NONE;
+ }
+ };
+ }
+
+ /**
+ * Returns a function-value implementing "distribs" in the specified package
+ * context.
+ * TODO(bazel-team): Remove in favor of package.distribs.
+ */
+ private static Function newDistribsFunction(final PackageContext context) {
+ return new MixedModeFunction("distribs", ImmutableList.of("object"), 1, false) {
+ @Override
+ public Object call(Object[] args, FuncallExpression ast) {
+ try {
+ Set<DistributionType> distribs = Type.DISTRIBUTIONS.convert(args[0],
+ "'distribs' operand");
+ context.pkgBuilder.setDefaultDistribs(distribs);
+ } catch (ConversionException e) {
+ context.eventHandler.handle(Event.error(ast.getLocation(), e.getMessage()));
+ context.pkgBuilder.setContainsErrors();
+ }
+ return Environment.NONE;
+ }
+ };
+ }
+
+ private static Function newPackageGroupFunction(final PackageContext context) {
+ List<String> params = ImmutableList.of("name", "packages", "includes");
+ return new MixedModeFunction("package_group", params, 1, true) {
+ @Override
+ public Object call(Object[] namedArgs, FuncallExpression ast)
+ throws EvalException, ConversionException {
+ Preconditions.checkState(namedArgs[0] != null);
+ String name = Type.STRING.convert(namedArgs[0], "'package_group' argument");
+ List<String> packages = namedArgs[1] == null
+ ? Collections.<String>emptyList()
+ : Type.STRING_LIST.convert(namedArgs[1], "'package_group' argument");
+ List<Label> includes = namedArgs[2] == null
+ ? Collections.<Label>emptyList()
+ : Type.LABEL_LIST.convert(namedArgs[2], "'package_group argument'",
+ context.pkgBuilder.getBuildFileLabel());
+
+ try {
+ context.pkgBuilder.addPackageGroup(name, packages, includes, context.eventHandler,
+ ast.getLocation());
+ return Environment.NONE;
+ } catch (Label.SyntaxException e) {
+ throw new EvalException(ast.getLocation(),
+ "package group has invalid name: " + name + ": " + e.getMessage());
+ } catch (Package.NameConflictException e) {
+ throw new EvalException(ast.getLocation(), e.getMessage());
+ }
+ }
+ };
+ }
+
+ public static RuleVisibility getVisibility(List<Label> original) {
+ RuleVisibility result;
+
+ result = ConstantRuleVisibility.tryParse(original);
+ if (result != null) {
+ return result;
+ }
+
+ result = PackageGroupsRuleVisibility.tryParse(original);
+ return result;
+ }
+
+ /**
+ * Returns a function-value implementing "package" in the specified package
+ * context.
+ */
+ private static Function newPackageFunction(
+ final Map<String, PackageArgument<?>> packageArguments) {
+ return new MixedModeFunction("package", packageArguments.keySet(), 0, true) {
+ @Override
+ public Object call(Object[] namedArguments, FuncallExpression ast, Environment env)
+ throws EvalException, ConversionException {
+
+ Package.LegacyBuilder pkgBuilder = getContext(env, ast).pkgBuilder;
+
+ // Validate parameter list
+ if (pkgBuilder.isPackageFunctionUsed()) {
+ throw new EvalException(ast.getLocation(),
+ "'package' can only be used once per BUILD file");
+ }
+ pkgBuilder.setPackageFunctionUsed();
+
+ // Parse params
+ boolean foundParameter = false;
+
+ int argNumber = 0;
+ for (Map.Entry<String, PackageArgument<?>> entry : packageArguments.entrySet()) {
+ Object arg = namedArguments[argNumber];
+ argNumber += 1;
+ if (arg == null) {
+ continue;
+ }
+
+ foundParameter = true;
+ entry.getValue().convertAndProcess(pkgBuilder, ast.getLocation(), arg);
+ }
+
+ if (!foundParameter) {
+ throw new EvalException(ast.getLocation(),
+ "at least one argument must be given to the 'package' function");
+ }
+
+ return Environment.NONE;
+ }
+ };
+ }
+
+ // Helper function for createRuleFunction.
+ private static void addRule(RuleFactory ruleFactory,
+ String ruleClassName,
+ PackageContext context,
+ Map<String, Object> kwargs,
+ FuncallExpression ast)
+ throws RuleFactory.InvalidRuleException, Package.NameConflictException {
+ RuleClass ruleClass = getBuiltInRuleClass(ruleClassName, ruleFactory);
+ RuleFactory.createAndAddRule(context, ruleClass, kwargs, ast);
+ }
+
+ private static RuleClass getBuiltInRuleClass(String ruleClassName, RuleFactory ruleFactory) {
+ if (ruleFactory.getRuleClassNames().contains(ruleClassName)) {
+ return ruleFactory.getRuleClass(ruleClassName);
+ }
+ throw new IllegalArgumentException("no such rule class: " + ruleClassName);
+ }
+
+ /**
+ * Get the PackageContext by looking up in the environment.
+ */
+ private static PackageContext getContext(Environment env, FuncallExpression ast)
+ throws EvalException {
+ try {
+ return (PackageContext) env.lookup(PKG_CONTEXT);
+ } catch (NoSuchVariableException e) {
+ throw new EvalException(ast.getLocation(), e.getMessage());
+ }
+ }
+
+ /**
+ * Returns a function-value implementing the build rule "ruleClass" (e.g. cc_library) in the
+ * specified package context.
+ */
+ private static Function newRuleFunction(final RuleFactory ruleFactory,
+ final String ruleClass) {
+ return new AbstractFunction(ruleClass) {
+ @Override
+ public Object call(List<Object> args, Map<String, Object> kwargs, FuncallExpression ast,
+ Environment env)
+ throws EvalException {
+ if (!args.isEmpty()) {
+ throw new EvalException(ast.getLocation(),
+ "build rules do not accept positional parameters");
+ }
+
+ try {
+ addRule(ruleFactory, ruleClass, getContext(env, ast), kwargs, ast);
+ } catch (RuleFactory.InvalidRuleException | Package.NameConflictException e) {
+ throw new EvalException(ast.getLocation(), e.getMessage());
+ }
+ return Environment.NONE;
+ }
+ };
+ }
+
+ /**
+ * Returns a new environment populated with common entries that can be shared
+ * across packages and that don't require the context.
+ */
+ private static Environment newGlobalEnvironment() {
+ Environment env = new Environment();
+ MethodLibrary.setupMethodEnvironment(env);
+ return env;
+ }
+
+ /****************************************************************************
+ * Package creation.
+ */
+
+ /**
+ * Loads, scans parses and evaluates the build file at "buildFile", and
+ * creates and returns a Package builder instance capable of building a package identified by
+ * "packageId".
+ *
+ * <p>This method returns a builder to allow the caller to do additional work, if necessary.
+ *
+ * <p>This method assumes "packageId" is a valid package name according to the
+ * {@link LabelValidator#validatePackageName} heuristic.
+ *
+ * <p>See {@link #evaluateBuildFile} for information on AST retention.
+ *
+ * <p>Executes {@code globber.onCompletion()} on completion and executes
+ * {@code globber.onInterrupt()} on an {@link InterruptedException}.
+ */
+ private Package.LegacyBuilder createPackage(PackageIdentifier packageId, Path buildFile,
+ List<Statement> preludeStatements, ParserInputSource inputSource,
+ Map<PathFragment, SkylarkEnvironment> imports, ImmutableList<Label> skylarkFileDependencies,
+ CachingPackageLocator locator, RuleVisibility defaultVisibility, Globber globber)
+ throws InterruptedException {
+ StoredEventHandler localReporter = new StoredEventHandler();
+ Preprocessor.Result preprocessingResult = preprocess(packageId, buildFile, inputSource, globber,
+ localReporter);
+ return createPackageFromPreprocessingResult(packageId, buildFile, preprocessingResult,
+ localReporter.getEvents(), preludeStatements, imports, skylarkFileDependencies, locator,
+ defaultVisibility, globber);
+ }
+
+ /**
+ * Same as {@link #createPackage}, but using a {@link Preprocessor.Result} from
+ * {@link #preprocess}.
+ *
+ * <p>Executes {@code globber.onCompletion()} on completion and executes
+ * {@code globber.onInterrupt()} on an {@link InterruptedException}.
+ */
+ // Used outside of bazel!
+ public Package.LegacyBuilder createPackageFromPreprocessingResult(PackageIdentifier packageId,
+ Path buildFile,
+ Preprocessor.Result preprocessingResult,
+ Iterable<Event> preprocessingEvents,
+ List<Statement> preludeStatements,
+ Map<PathFragment, SkylarkEnvironment> imports,
+ ImmutableList<Label> skylarkFileDependencies,
+ CachingPackageLocator locator,
+ RuleVisibility defaultVisibility,
+ Globber globber) throws InterruptedException {
+ StoredEventHandler localReporter = new StoredEventHandler();
+ // Run the lexer and parser with a local reporter, so that errors from other threads do not
+ // show up below. Merge the local and global reporters afterwards.
+ // Logged messages are used as a testability hook tracing the parsing progress
+ LOG.fine("Starting to parse " + packageId);
+ BuildFileAST buildFileAST = BuildFileAST.parseBuildFile(
+ preprocessingResult.result, preludeStatements, localReporter, locator, false);
+ LOG.fine("Finished parsing of " + packageId);
+
+ MakeEnvironment.Builder makeEnv = new MakeEnvironment.Builder();
+ if (platformSetRegexps != null) {
+ makeEnv.setPlatformSetRegexps(platformSetRegexps);
+ }
+ try {
+ // At this point the package is guaranteed to exist. It may have parse or
+ // evaluation errors, resulting in a diminished number of rules.
+ prefetchGlobs(packageId, buildFileAST, preprocessingResult.preprocessed,
+ buildFile, globber, defaultVisibility, makeEnv);
+ return evaluateBuildFile(
+ packageId, buildFileAST, buildFile, globber,
+ Iterables.concat(preprocessingEvents, localReporter.getEvents()),
+ defaultVisibility, preprocessingResult.containsErrors,
+ preprocessingResult.containsTransientErrors, makeEnv, imports, skylarkFileDependencies);
+ } catch (InterruptedException e) {
+ globber.onInterrupt();
+ throw e;
+ } finally {
+ globber.onCompletion();
+ }
+ }
+
+ /**
+ * Same as {@link #createPackage}, but does the required validation of "packageName" first,
+ * throwing a {@link NoSuchPackageException} if the name is invalid.
+ */
+ @VisibleForTesting
+ public Package createPackageForTesting(PackageIdentifier packageId,
+ Path buildFile,
+ CachingPackageLocator locator,
+ EventHandler eventHandler) throws NoSuchPackageException, InterruptedException {
+ String error = LabelValidator.validatePackageName(
+ packageId.getPackageFragment().getPathString());
+ if (error != null) {
+ throw new BuildFileNotFoundException(packageId.toString(),
+ "illegal package name: '" + packageId.toString() + "' (" + error + ")");
+ }
+ ParserInputSource inputSource = maybeGetParserInputSource(buildFile, eventHandler);
+ if (inputSource == null) {
+ throw new BuildFileContainsErrorsException(packageId.toString(), "IOException occured");
+ }
+ Package result = createPackage(packageId, buildFile,
+ ImmutableList.<Statement>of(), inputSource,
+ ImmutableMap.<PathFragment, SkylarkEnvironment>of(),
+ ImmutableList.<Label>of(),
+ locator, ConstantRuleVisibility.PUBLIC,
+ createLegacyGlobber(buildFile.getParentDirectory(), packageId, locator)).build();
+ Event.replayEventsOn(eventHandler, result.getEvents());
+ return result;
+ }
+
+ /** Preprocesses the given BUILD file. */
+ // Used outside of bazel!
+ public Preprocessor.Result preprocess(
+ PackageIdentifier packageId,
+ Path buildFile,
+ CachingPackageLocator locator,
+ EventHandler eventHandler) throws InterruptedException {
+ ParserInputSource inputSource = maybeGetParserInputSource(buildFile, eventHandler);
+ if (inputSource == null) {
+ return Preprocessor.Result.transientError(buildFile);
+ }
+ Globber globber = createLegacyGlobber(buildFile.getParentDirectory(), packageId, locator);
+ try {
+ return preprocess(packageId, buildFile, inputSource, globber, eventHandler);
+ } finally {
+ globber.onCompletion();
+ }
+ }
+
+ /**
+ * Preprocesses the given BUILD file, executing {@code globber.onInterrupt()} on an
+ * {@link InterruptedException}.
+ */
+ // Used outside of bazel!
+ public Preprocessor.Result preprocess(
+ PackageIdentifier packageId,
+ Path buildFile,
+ ParserInputSource inputSource,
+ Globber globber,
+ EventHandler eventHandler) throws InterruptedException {
+ Preprocessor preprocessor = preprocessorFactory.getPreprocessor();
+ if (preprocessor == null) {
+ return Preprocessor.Result.noPreprocessing(inputSource);
+ }
+ try {
+ return preprocessor.preprocess(inputSource, packageId.toString(), globber, eventHandler,
+ globalEnv, ruleFactory.getRuleClassNames());
+ } catch (IOException e) {
+ eventHandler.handle(Event.error(Location.fromFile(buildFile),
+ "preprocessing failed: " + e.getMessage()));
+ return Preprocessor.Result.transientError(buildFile);
+ } catch (InterruptedException e) {
+ globber.onInterrupt();
+ throw e;
+ }
+ }
+
+ // Used outside of bazel!
+ public LegacyGlobber createLegacyGlobber(Path packageDirectory, PackageIdentifier packageId,
+ CachingPackageLocator locator) {
+ return new LegacyGlobber(new GlobCache(packageDirectory, packageId, locator, syscalls,
+ threadPool));
+ }
+
+ @Nullable
+ private ParserInputSource maybeGetParserInputSource(Path buildFile, EventHandler eventHandler) {
+ try {
+ return ParserInputSource.create(buildFile);
+ } catch (IOException e) {
+ eventHandler.handle(Event.error(Location.fromFile(buildFile), e.getMessage()));
+ return null;
+ }
+ }
+
+ /**
+ * This tuple holds the current package builder, current lexer, etc, for the
+ * duration of the evaluation of one BUILD file. (We use a PackageContext
+ * object in preference to storing these values in mutable fields of the
+ * PackageFactory.)
+ *
+ * <p>PLEASE NOTE: references to PackageContext objects are held by many
+ * Function closures, but should become unreachable once the Environment is
+ * discarded at the end of evaluation. Please be aware of your memory
+ * footprint when making changes here!
+ */
+ public static class PackageContext {
+
+ final Package.LegacyBuilder pkgBuilder;
+ final Globber globber;
+ final EventHandler eventHandler;
+
+ @VisibleForTesting
+ public PackageContext(Package.LegacyBuilder pkgBuilder, Globber globber,
+ EventHandler eventHandler) {
+ this.pkgBuilder = pkgBuilder;
+ this.eventHandler = eventHandler;
+ this.globber = globber;
+ }
+ }
+
+ /**
+ * Returns the list of native rule functions created using the {@link RuleClassProvider}
+ * of this {@link PackageFactory}.
+ */
+ public ImmutableList<Function> collectNativeRuleFunctions() {
+ ImmutableList.Builder<Function> builder = ImmutableList.builder();
+ for (String ruleClass : ruleFactory.getRuleClassNames()) {
+ builder.add(newRuleFunction(ruleFactory, ruleClass));
+ }
+ builder.add(newGlobFunction(null, false));
+ builder.add(newPackageFunction(packageArguments));
+ return builder.build();
+ }
+
+ private void buildPkgEnv(Environment pkgEnv, String packageName,
+ MakeEnvironment.Builder pkgMakeEnv, PackageContext context, RuleFactory ruleFactory) {
+ pkgEnv.update("distribs", newDistribsFunction(context));
+ pkgEnv.update("glob", newGlobFunction(context, /*async=*/false));
+ pkgEnv.update("mocksubinclude", newMockSubincludeFunction(context));
+ pkgEnv.update("licenses", newLicensesFunction(context));
+ pkgEnv.update("exports_files", newExportsFilesFunction(context));
+ pkgEnv.update("package_group", newPackageGroupFunction(context));
+ pkgEnv.update("package", newPackageFunction(packageArguments));
+ pkgEnv.update("subinclude", newSubincludeFunction());
+ pkgEnv.update("environment_group", newEnvironmentGroupFunction(context));
+
+ pkgEnv.update("PACKAGE_NAME", packageName);
+
+ for (String ruleClass : ruleFactory.getRuleClassNames()) {
+ Function ruleFunction = newRuleFunction(ruleFactory, ruleClass);
+ pkgEnv.update(ruleClass, ruleFunction);
+ }
+
+ for (EnvironmentExtension extension : environmentExtensions) {
+ extension.update(pkgEnv, pkgMakeEnv, context.pkgBuilder.getBuildFileLabel());
+ }
+ }
+
+ /**
+ * Constructs a Package instance, evaluates the BUILD-file AST inside the
+ * build environment, and populates the package with Rule instances as it
+ * goes. As with most programming languages, evaluation stops when an
+ * exception is encountered: no further rules after the point of failure will
+ * be constructed. We assume that rules constructed before the point of
+ * failure are valid; this assumption is not entirely correct, since a
+ * "vardef" after a rule declaration can affect the behavior of that rule.
+ *
+ * <p>Rule attribute checking is performed during evaluation. Each attribute
+ * must conform to the type specified for that <i>(rule class, attribute
+ * name)</i> pair. Errors reported at this stage include: missing value for
+ * mandatory attribute, value of wrong type. Such error cause Rule
+ * construction to be aborted, so the resulting package will have missing
+ * members.
+ *
+ * @see PackageFactory#PackageFactory
+ */
+ @VisibleForTesting // used by PackageFactoryApparatus
+ public Package.LegacyBuilder evaluateBuildFile(PackageIdentifier packageId,
+ BuildFileAST buildFileAST, Path buildFilePath, Globber globber,
+ Iterable<Event> pastEvents, RuleVisibility defaultVisibility, boolean containsError,
+ boolean containsTransientError, MakeEnvironment.Builder pkgMakeEnv,
+ Map<PathFragment, SkylarkEnvironment> imports,
+ ImmutableList<Label> skylarkFileDependencies) throws InterruptedException {
+ // Important: Environment should be unreachable by the end of this method!
+ StoredEventHandler eventHandler = new StoredEventHandler();
+ Environment pkgEnv = new Environment(globalEnv, eventHandler);
+
+ Package.LegacyBuilder pkgBuilder =
+ new Package.LegacyBuilder(packageId)
+ .setGlobber(globber)
+ .setFilename(buildFilePath)
+ .setMakeEnv(pkgMakeEnv)
+ .setDefaultVisibility(defaultVisibility)
+ // "defaultVisibility" comes from the command line. Let's give the BUILD file a chance to
+ // set default_visibility once, be reseting the PackageBuilder.defaultVisibilitySet flag.
+ .setDefaultVisibilitySet(false)
+ .setSkylarkFileDependencies(skylarkFileDependencies);
+
+ Event.replayEventsOn(eventHandler, pastEvents);
+
+ // Stuff that closes over the package context:
+ PackageContext context = new PackageContext(pkgBuilder, globber, eventHandler);
+ buildPkgEnv(pkgEnv, packageId.toString(), pkgMakeEnv, context, ruleFactory);
+
+ if (containsError) {
+ pkgBuilder.setContainsErrors();
+ }
+
+ if (containsTransientError) {
+ pkgBuilder.setContainsTemporaryErrors();
+ }
+
+ if (!validatePackageIdentifier(packageId, buildFileAST.getLocation(), eventHandler)) {
+ pkgBuilder.setContainsErrors();
+ }
+
+ pkgEnv.setImportedExtensions(imports);
+ pkgEnv.updateAndPropagate(PKG_CONTEXT, context);
+ pkgEnv.updateAndPropagate(Environment.PKG_NAME, packageId.toString());
+
+ if (!validateAssignmentStatements(pkgEnv, buildFileAST, eventHandler)) {
+ pkgBuilder.setContainsErrors();
+ }
+
+ if (buildFileAST.containsErrors()) {
+ pkgBuilder.setContainsErrors();
+ }
+
+ // TODO(bazel-team): (2009) the invariant "if errors are reported, mark the package
+ // as containing errors" is strewn all over this class. Refactor to use an
+ // event sensor--and see if we can simplify the calling code in
+ // createPackage().
+ if (!buildFileAST.exec(pkgEnv, eventHandler)) {
+ pkgBuilder.setContainsErrors();
+ }
+
+ pkgBuilder.addEvents(eventHandler.getEvents());
+ return pkgBuilder;
+ }
+
+ /**
+ * Visit all targets and expand the globs in parallel.
+ */
+ private void prefetchGlobs(PackageIdentifier packageId, BuildFileAST buildFileAST,
+ boolean wasPreprocessed, Path buildFilePath, Globber globber,
+ RuleVisibility defaultVisibility, MakeEnvironment.Builder pkgMakeEnv)
+ throws InterruptedException {
+ if (wasPreprocessed) {
+ // No point in prefetching globs here: preprocessing implies eager evaluation
+ // of all globs.
+ return;
+ }
+ // Important: Environment should be unreachable by the end of this method!
+ Environment pkgEnv = new Environment();
+
+ Package.LegacyBuilder pkgBuilder =
+ new Package.LegacyBuilder(packageId)
+ .setFilename(buildFilePath)
+ .setMakeEnv(pkgMakeEnv)
+ .setDefaultVisibility(defaultVisibility)
+ // "defaultVisibility" comes from the command line. Let's give the BUILD file a chance to
+ // set default_visibility once, be reseting the PackageBuilder.defaultVisibilitySet flag.
+ .setDefaultVisibilitySet(false);
+
+ // Stuff that closes over the package context:
+ PackageContext context = new PackageContext(pkgBuilder, globber, NullEventHandler.INSTANCE);
+ buildPkgEnv(pkgEnv, packageId.toString(), pkgMakeEnv, context, ruleFactory);
+ pkgEnv.update("glob", newGlobFunction(context, /*async=*/true));
+ // The Fileset function is heavyweight in that it can run glob(). Avoid this during the
+ // preloading phase.
+ pkgEnv.remove("FilesetEntry");
+
+ buildFileAST.exec(pkgEnv, NullEventHandler.INSTANCE);
+ }
+
+
+ /**
+ * Tests a build AST to ensure that it contains no assignment statements that
+ * redefine built-in build rules.
+ *
+ * @param pkgEnv a package environment initialized with all of the built-in
+ * build rules
+ * @param ast the build file AST to be tested
+ * @param eventHandler a eventHandler where any errors should be logged
+ * @return true if the build file contains no redefinitions of built-in
+ * functions
+ */
+ private static boolean validateAssignmentStatements(Environment pkgEnv,
+ BuildFileAST ast,
+ EventHandler eventHandler) {
+ for (Statement stmt : ast.getStatements()) {
+ if (stmt instanceof AssignmentStatement) {
+ Expression lvalue = ((AssignmentStatement) stmt).getLValue();
+ if (!(lvalue instanceof Ident)) {
+ continue;
+ }
+ String target = ((Ident) lvalue).getName();
+ if (pkgEnv.lookup(target, null) != null) {
+ eventHandler.handle(Event.error(stmt.getLocation(), "Reassignment of builtin build "
+ + "function '" + target + "' not permitted"));
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ // Reports an error and returns false iff package identifier was illegal.
+ private static boolean validatePackageIdentifier(PackageIdentifier packageId, Location location,
+ EventHandler eventHandler) {
+ String error = LabelValidator.validatePackageName(packageId.getPackageFragment().toString());
+ if (error != null) {
+ eventHandler.handle(Event.error(location, error));
+ return false; // Invalid package name 'foo'
+ }
+ return true;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/PackageGroup.java b/src/main/java/com/google/devtools/build/lib/packages/PackageGroup.java
new file mode 100644
index 0000000..17ba29c
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/PackageGroup.java
@@ -0,0 +1,152 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.packages.License.DistributionType;
+import com.google.devtools.build.lib.syntax.Label;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * This class represents a package group. It has a name and a set of packages
+ * and can be asked if a specific package is included in it. The package set is
+ * represented as a list of PathFragments.
+ */
+public class PackageGroup implements Target {
+ private boolean containsErrors;
+ private final Label label;
+ private final Location location;
+ private final Package containingPackage;
+ private final List<PackageSpecification> packageSpecifications;
+ private final List<Label> includes;
+
+ public PackageGroup(Label label, Package pkg, Collection<String> packages,
+ Collection<Label> includes, EventHandler eventHandler, Location location) {
+ this.label = label;
+ this.location = location;
+ this.containingPackage = pkg;
+ this.includes = ImmutableList.copyOf(includes);
+
+ ImmutableList.Builder<PackageSpecification> packagesBuilder = ImmutableList.builder();
+ for (String containedPackage : packages) {
+ PackageSpecification specification = null;
+ try {
+ specification = PackageSpecification.fromString(containedPackage);
+ } catch (PackageSpecification.InvalidPackageSpecificationException e) {
+ containsErrors = true;
+ eventHandler.handle(Event.error(location, e.getMessage()));
+ }
+
+ if (specification != null) {
+ packagesBuilder.add(specification);
+ }
+ }
+ this.packageSpecifications = packagesBuilder.build();
+ }
+
+ public boolean containsErrors() {
+ return containsErrors;
+ }
+
+ public Iterable<PackageSpecification> getPackageSpecifications() {
+ return packageSpecifications;
+ }
+
+ public boolean contains(Package pkg) {
+ for (PackageSpecification specification : packageSpecifications) {
+ if (specification.containsPackage(pkg.getNameFragment())) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public List<Label> getIncludes() {
+ return includes;
+ }
+
+ public List<String> getContainedPackages() {
+ List<String> result = Lists.newArrayListWithCapacity(packageSpecifications.size());
+ for (PackageSpecification specification : packageSpecifications) {
+ result.add(specification.toString());
+ }
+ return result;
+ }
+
+ @Override
+ public Rule getAssociatedRule() {
+ return null;
+ }
+
+ @Override
+ public Set<DistributionType> getDistributions() {
+ return Collections.emptySet();
+ }
+
+ @Override
+ public Label getLabel() {
+ return label;
+ }
+
+ @Override public String getName() {
+ return label.getName();
+ }
+
+ @Override
+ public License getLicense() {
+ return License.NO_LICENSE;
+ }
+
+ @Override
+ public Package getPackage() {
+ return containingPackage;
+ }
+
+ @Override
+ public String getTargetKind() {
+ return targetKind();
+ }
+
+ @Override
+ public Location getLocation() {
+ return location;
+ }
+
+ @Override
+ public String toString() {
+ return targetKind() + " " + getLabel();
+ }
+
+ @Override
+ public RuleVisibility getVisibility() {
+ // Package groups are always public to avoid a PackageGroupConfiguredTarget
+ // needing itself for the visibility check. It may work, but I did not
+ // think it over completely.
+ return ConstantRuleVisibility.PUBLIC;
+ }
+
+ public static String targetKind() {
+ return "package group";
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/PackageGroupsRuleVisibility.java b/src/main/java/com/google/devtools/build/lib/packages/PackageGroupsRuleVisibility.java
new file mode 100644
index 0000000..70ffa11
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/PackageGroupsRuleVisibility.java
@@ -0,0 +1,81 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.syntax.Label;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * A rule visibility that allows visibility to a list of package groups.
+ */
+@Immutable @ThreadSafe
+public class PackageGroupsRuleVisibility implements RuleVisibility {
+ public static final String PACKAGE_LABEL = "__pkg__";
+ public static final String SUBTREE_LABEL = "__subpackages__";
+ private final List<Label> packageGroups;
+ private final List<PackageSpecification> directPackages;
+ private final List<Label> declaredLabels;
+
+ public PackageGroupsRuleVisibility(List<Label> labels) {
+ declaredLabels = ImmutableList.copyOf(labels);
+ ImmutableList.Builder<PackageSpecification> directPackageBuilder = ImmutableList.builder();
+ ImmutableList.Builder<Label> packageGroupBuilder = ImmutableList.builder();
+
+ for (Label label : labels) {
+ PackageSpecification specification = PackageSpecification.fromLabel(label);
+ if (specification != null) {
+ directPackageBuilder.add(specification);
+ } else {
+ packageGroupBuilder.add(label);
+ }
+ }
+
+ packageGroups = packageGroupBuilder.build();
+ directPackages = directPackageBuilder.build();
+ }
+
+ public Collection<Label> getPackageGroups() {
+ return packageGroups;
+ }
+
+ public Collection<PackageSpecification> getDirectPackages() {
+ return directPackages;
+ }
+
+ @Override
+ public List<Label> getDependencyLabels() {
+ return packageGroups;
+ }
+
+ @Override
+ public List<Label> getDeclaredLabels() {
+ return declaredLabels;
+ }
+
+ /**
+ * Tries to parse a list of labels into a {@link PackageGroupsRuleVisibility}.
+ *
+ * @param labels the list of labels to parse
+ * @return The resulting visibility object. A list of labels can always be
+ * parsed into a PackageGroupsRuleVisibility.
+ */
+ public static PackageGroupsRuleVisibility tryParse(List<Label> labels) {
+ return new PackageGroupsRuleVisibility(labels);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/PackageIdentifier.java b/src/main/java/com/google/devtools/build/lib/packages/PackageIdentifier.java
new file mode 100644
index 0000000..7b16e3c
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/PackageIdentifier.java
@@ -0,0 +1,271 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ComparisonChain;
+import com.google.devtools.build.lib.syntax.Label.SyntaxException;
+import com.google.devtools.build.lib.util.StringCanonicalizer;
+import com.google.devtools.build.lib.util.StringUtilities;
+import com.google.devtools.build.lib.vfs.Canonicalizer;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.ObjectStreamException;
+import java.io.Serializable;
+import java.util.Objects;
+
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * Uniquely identifies a package, given a repository name and a package's path fragment.
+ *
+ * <p>The repository the build is happening in is the <i>default workspace</i>, and is identified
+ * by the workspace name "". Other repositories can be named in the WORKSPACE file. These
+ * workspaces are prefixed by {@literal @}.</p>
+ */
+@Immutable
+public final class PackageIdentifier implements Comparable<PackageIdentifier>, Serializable {
+
+ /**
+ * A human-readable name for the repository.
+ */
+ public static final class RepositoryName {
+ private final String name;
+
+ /**
+ * Makes sure that name is a valid repository name and creates a new RepositoryName using it.
+ * @throws SyntaxException if the name is invalid.
+ */
+ public static RepositoryName create(String name) throws SyntaxException {
+ String errorMessage = validate(name);
+ if (errorMessage != null) {
+ errorMessage = "invalid repository name '"
+ + StringUtilities.sanitizeControlChars(name) + "': " + errorMessage;
+ throw new SyntaxException(errorMessage);
+ }
+ return new RepositoryName(StringCanonicalizer.intern(name));
+ }
+
+ private RepositoryName(String name) {
+ this.name = name;
+ }
+
+ /**
+ * Performs validity checking. Returns null on success, an error message otherwise.
+ */
+ private static String validate(String name) {
+ if (name.isEmpty()) {
+ return null;
+ }
+
+ if (!name.startsWith("@")) {
+ return "workspace name must start with '@'";
+ }
+
+ // "@" isn't a valid workspace name.
+ if (name.length() == 1) {
+ return "empty workspace name";
+ }
+
+ // Check for any character outside of [/0-9A-Z_a-z-]. Try to evaluate the
+ // conditional quickly (by looking in decreasing order of character class
+ // likelihood).
+ for (int i = name.length() - 1; i >= 1; --i) {
+ char c = name.charAt(i);
+ if ((c < 'a' || c > 'z') && c != '_' && c != '-'
+ && (c < '0' || c > '9') && (c < 'A' || c > 'Z')) {
+ return "workspace names may contain only A-Z, a-z, 0-9, '-' and '_'";
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the repository name without the leading "{@literal @}". For the default repository,
+ * returns "".
+ */
+ public String strippedName() {
+ if (name.isEmpty()) {
+ return name;
+ }
+ return name.substring(1);
+ }
+
+ /**
+ * Returns if this is the default repository, that is, {@link #name} is "".
+ */
+ public boolean isDefault() {
+ return name.isEmpty();
+ }
+
+ /**
+ * Returns the repository name, with leading "{@literal @}" (or "" for the default repository).
+ */
+ @Override
+ public String toString() {
+ return name;
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (this == object) {
+ return true;
+ }
+ if (object instanceof RepositoryName) {
+ return name.equals(((RepositoryName) object).name);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return name.hashCode();
+ }
+ }
+
+ public static final String DEFAULT_REPOSITORY = "";
+
+ /**
+ * Helper for serializing PackageIdentifiers.
+ *
+ * <p>PackageIdentifier's field should be final, but then it couldn't be deserialized. This
+ * allows the fields to be deserialized and copied into a new PackageIdentifier.</p>
+ */
+ private static final class SerializationProxy implements Serializable {
+ PackageIdentifier packageId;
+
+ public SerializationProxy(PackageIdentifier packageId) {
+ this.packageId = packageId;
+ }
+
+ private void writeObject(ObjectOutputStream out) throws IOException {
+ out.writeObject(packageId.repository.toString());
+ out.writeObject(packageId.pkgName);
+ }
+
+ private void readObject(ObjectInputStream in)
+ throws IOException, ClassNotFoundException {
+ try {
+ packageId = new PackageIdentifier((String) in.readObject(), (PathFragment) in.readObject());
+ } catch (SyntaxException e) {
+ throw new IOException("Error serializing package identifier: " + e.getMessage());
+ }
+ }
+
+ @SuppressWarnings("unused")
+ private void readObjectNoData() throws ObjectStreamException {
+ }
+
+ private Object readResolve() {
+ return packageId;
+ }
+ }
+
+ // Temporary factory for identifiers without explicit repositories.
+ // TODO(bazel-team): remove all usages of this.
+ public static PackageIdentifier createInDefaultRepo(String name) {
+ return createInDefaultRepo(new PathFragment(name));
+ }
+
+ public static PackageIdentifier createInDefaultRepo(PathFragment name) {
+ try {
+ return new PackageIdentifier(DEFAULT_REPOSITORY, name);
+ } catch (SyntaxException e) {
+ throw new IllegalArgumentException("could not create package identifier for " + name
+ + ": " + e.getMessage());
+ }
+ }
+
+ /**
+ * The identifier for this repository. This is either "" or prefixed with an "@",
+ * e.g., "@myrepo".
+ */
+ private final RepositoryName repository;
+
+ /** The name of the package. Canonical (i.e. x.equals(y) <=> x==y). */
+ private final PathFragment pkgName;
+
+ public PackageIdentifier(String repository, PathFragment pkgName) throws SyntaxException {
+ this(RepositoryName.create(repository), pkgName);
+ }
+
+ public PackageIdentifier(RepositoryName repository, PathFragment pkgName) {
+ Preconditions.checkNotNull(repository);
+ Preconditions.checkNotNull(pkgName);
+ this.repository = repository;
+ this.pkgName = Canonicalizer.fragments().intern(pkgName);
+ }
+
+ private Object writeReplace() throws ObjectStreamException {
+ return new SerializationProxy(this);
+ }
+
+ private void readObject(ObjectInputStream in)
+ throws IOException, ClassNotFoundException {
+ throw new IOException("Serialization is allowed only by proxy");
+ }
+
+ @SuppressWarnings("unused")
+ private void readObjectNoData() throws ObjectStreamException {
+ }
+
+ public RepositoryName getRepository() {
+ return repository;
+ }
+
+ public PathFragment getPackageFragment() {
+ return pkgName;
+ }
+
+ /**
+ * Returns the name of this package.
+ *
+ * <p>There are certain places that expect the path fragment as the package name ('foo/bar') as a
+ * package identifier. This isn't specific enough for packages in other repositories, so their
+ * stringified version is '@baz//foo/bar'.</p>
+ */
+ @Override
+ public String toString() {
+ return (repository.isDefault() ? "" : repository + "//") + pkgName;
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (this == object) {
+ return true;
+ }
+ if (object instanceof PackageIdentifier) {
+ PackageIdentifier that = (PackageIdentifier) object;
+ return repository.equals(that.repository) && pkgName.equals(that.pkgName);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(repository, pkgName);
+ }
+
+ @Override
+ public int compareTo(PackageIdentifier that) {
+ return ComparisonChain.start()
+ .compare(repository.toString(), that.repository.toString())
+ .compare(pkgName, that.pkgName)
+ .result();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/google/devtools/build/lib/packages/PackageLoadedEvent.java b/src/main/java/com/google/devtools/build/lib/packages/PackageLoadedEvent.java
new file mode 100644
index 0000000..3dbf3c2
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/PackageLoadedEvent.java
@@ -0,0 +1,60 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+/**
+ * An event that is fired after a package is loaded.
+ */
+public final class PackageLoadedEvent {
+ private final String packageName;
+ private final long timeInMillis;
+ private final boolean reloading;
+ private final boolean successful;
+
+ public PackageLoadedEvent(String packageName, long timeInMillis, boolean reloading,
+ boolean successful) {
+ this.packageName = packageName;
+ this.timeInMillis = timeInMillis;
+ this.reloading = reloading;
+ this.successful = successful;
+ }
+
+ /**
+ * Returns the package name.
+ */
+ public String getPackageName() {
+ return packageName;
+ }
+
+ /**
+ * Returns time which was spent to load a package.
+ */
+ public long getTimeInMillis() {
+ return timeInMillis;
+ }
+
+ /**
+ * Returns true if package had been loaded before.
+ */
+ public boolean isReloading() {
+ return reloading;
+ }
+
+ /**
+ * Returns true if package was loaded successfully.
+ */
+ public boolean isSuccessful() {
+ return successful;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/PackageNotInCacheException.java b/src/main/java/com/google/devtools/build/lib/packages/PackageNotInCacheException.java
new file mode 100644
index 0000000..f191a1c
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/PackageNotInCacheException.java
@@ -0,0 +1,24 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+/**
+ * Exception indicating that a package is not in the cache, although it should
+ * have been loaded.
+ */
+public class PackageNotInCacheException extends NoSuchPackageException {
+ public PackageNotInCacheException(String packageName) {
+ super(packageName, "package '" + packageName + "' not in cache");
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/PackageSerializer.java b/src/main/java/com/google/devtools/build/lib/packages/PackageSerializer.java
new file mode 100644
index 0000000..8971cf2
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/PackageSerializer.java
@@ -0,0 +1,462 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+import static com.google.devtools.build.lib.packages.Type.BOOLEAN;
+import static com.google.devtools.build.lib.packages.Type.DISTRIBUTIONS;
+import static com.google.devtools.build.lib.packages.Type.FILESET_ENTRY_LIST;
+import static com.google.devtools.build.lib.packages.Type.INTEGER;
+import static com.google.devtools.build.lib.packages.Type.INTEGER_LIST;
+import static com.google.devtools.build.lib.packages.Type.LABEL;
+import static com.google.devtools.build.lib.packages.Type.LABEL_LIST;
+import static com.google.devtools.build.lib.packages.Type.LABEL_LIST_DICT;
+import static com.google.devtools.build.lib.packages.Type.LICENSE;
+import static com.google.devtools.build.lib.packages.Type.NODEP_LABEL;
+import static com.google.devtools.build.lib.packages.Type.NODEP_LABEL_LIST;
+import static com.google.devtools.build.lib.packages.Type.OUTPUT;
+import static com.google.devtools.build.lib.packages.Type.OUTPUT_LIST;
+import static com.google.devtools.build.lib.packages.Type.STRING;
+import static com.google.devtools.build.lib.packages.Type.STRING_DICT;
+import static com.google.devtools.build.lib.packages.Type.STRING_DICT_UNARY;
+import static com.google.devtools.build.lib.packages.Type.STRING_LIST;
+import static com.google.devtools.build.lib.packages.Type.STRING_LIST_DICT;
+import static com.google.devtools.build.lib.packages.Type.TRISTATE;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.packages.License.DistributionType;
+import com.google.devtools.build.lib.packages.MakeEnvironment.Binding;
+import com.google.devtools.build.lib.query2.proto.proto2api.Build;
+import com.google.devtools.build.lib.syntax.FilesetEntry;
+import com.google.devtools.build.lib.syntax.GlobCriteria;
+import com.google.devtools.build.lib.syntax.GlobList;
+import com.google.devtools.build.lib.syntax.Label;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Functionality to serialize loaded packages.
+ */
+public class PackageSerializer {
+ private static Build.SourceFile serializeInputFile(InputFile inputFile) {
+ Build.SourceFile.Builder result = Build.SourceFile.newBuilder();
+ result.setName(inputFile.getLabel().toString());
+ if (inputFile.isVisibilitySpecified()) {
+ for (Label visibilityLabel : inputFile.getVisibility().getDeclaredLabels()) {
+ result.addVisibilityLabel(visibilityLabel.toString());
+ }
+ }
+ if (inputFile.isLicenseSpecified()) {
+ result.setLicense(serializeLicense(inputFile.getLicense()));
+ }
+
+ result.setParseableLocation(serializeLocation(inputFile.getLocation()));
+ return result.build();
+ }
+
+ private static Build.Location serializeLocation(Location location) {
+ Build.Location.Builder result = Build.Location.newBuilder();
+
+ result.setStartOffset(location.getStartOffset());
+ if (location.getStartLineAndColumn() != null) {
+ result.setStartLine(location.getStartLineAndColumn().getLine());
+ result.setStartColumn(location.getStartLineAndColumn().getColumn());
+ }
+
+ result.setEndOffset(location.getEndOffset());
+ if (location.getEndLineAndColumn() != null) {
+ result.setEndLine(location.getEndLineAndColumn().getLine());
+ result.setEndColumn(location.getEndLineAndColumn().getColumn());
+ }
+
+ return result.build();
+ }
+
+ private static Build.PackageGroup serializePackageGroup(PackageGroup packageGroup) {
+ Build.PackageGroup.Builder result = Build.PackageGroup.newBuilder();
+
+ result.setName(packageGroup.getLabel().toString());
+ result.setParseableLocation(serializeLocation(packageGroup.getLocation()));
+
+ for (PackageSpecification packageSpecification : packageGroup.getPackageSpecifications()) {
+ result.addContainedPackage(packageSpecification.toString());
+ }
+
+ for (Label include : packageGroup.getIncludes()) {
+ result.addIncludedPackageGroup(include.toString());
+ }
+
+ return result.build();
+ }
+
+ private static Build.Rule serializeRule(Rule rule) {
+ Build.Rule.Builder result = Build.Rule.newBuilder();
+ result.setName(rule.getLabel().toString());
+ result.setRuleClass(rule.getRuleClass());
+ result.setParseableLocation(serializeLocation(rule.getLocation()));
+ for (Attribute attribute : rule.getAttributes()) {
+ Object value = attribute.getName().equals("visibility")
+ ? rule.getVisibility().getDeclaredLabels()
+ // TODO(bazel-team): support configurable attributes. AggregatingAttributeMapper
+ // may make more sense here. Computed defaults may add complications.
+ : RawAttributeMapper.of(rule).get(attribute.getName(), attribute.getType());
+ if (value != null) {
+ PackageSerializer.addAttributeToProto(result, attribute, value,
+ rule.getAttributeLocation(attribute.getName()),
+ rule.isAttributeValueExplicitlySpecified(attribute),
+ true);
+ }
+ }
+
+ return result.build();
+ }
+
+ private static List<Build.MakeVar> serializeMakeEnvironment(MakeEnvironment makeEnv) {
+ List<Build.MakeVar> result = new ArrayList<>();
+
+ for (Map.Entry<String, ImmutableList<Binding>> var : makeEnv.getBindings().entrySet()) {
+ Build.MakeVar.Builder varPb = Build.MakeVar.newBuilder();
+ varPb.setName(var.getKey());
+ for (Binding binding : var.getValue()) {
+ Build.MakeVarBinding.Builder bindingPb = Build.MakeVarBinding.newBuilder();
+ bindingPb.setValue(binding.getValue());
+ bindingPb.setPlatformSetRegexp(binding.getPlatformSetRegexp());
+ varPb.addBinding(bindingPb);
+ }
+
+ result.add(varPb.build());
+ }
+
+ return result;
+ }
+
+ private static Build.License serializeLicense(License license) {
+ Build.License.Builder result = Build.License.newBuilder();
+
+ for (License.LicenseType licenseType : license.getLicenseTypes()) {
+ result.addLicenseType(licenseType.toString());
+ }
+
+ for (Label exception : license.getExceptions()) {
+ result.addException(exception.toString());
+ }
+ return result.build();
+ }
+
+ private static Build.Event serializeEvent(Event event) {
+ Build.Event.Builder result = Build.Event.newBuilder();
+ result.setMessage(event.getMessage());
+ if (event.getLocation() != null) {
+ result.setLocation(serializeLocation(event.getLocation()));
+ }
+
+ Build.Event.EventKind kind;
+
+ switch (event.getKind()) {
+ case ERROR:
+ kind = Build.Event.EventKind.ERROR;
+ break;
+ case WARNING:
+ kind = Build.Event.EventKind.WARNING;
+ break;
+ case INFO:
+ kind = Build.Event.EventKind.INFO;
+ break;
+ case PROGRESS:
+ kind = Build.Event.EventKind.PROGRESS;
+ break;
+ default: throw new IllegalStateException();
+ }
+
+ result.setKind(kind);
+ return result.build();
+ }
+
+ private static void serializePackageInternal(Package pkg, Build.Package.Builder builder) {
+ builder.setName(pkg.getName());
+ builder.setRepository(pkg.getPackageIdentifier().getRepository().toString());
+ builder.setBuildFilePath(pkg.getFilename().getPathString());
+ // The extra bit is needed to handle the corner case when the default visibility is [], i.e.
+ // zero labels.
+ builder.setDefaultVisibilitySet(pkg.isDefaultVisibilitySet());
+ if (pkg.isDefaultVisibilitySet()) {
+ for (Label visibilityLabel : pkg.getDefaultVisibility().getDeclaredLabels()) {
+ builder.addDefaultVisibilityLabel(visibilityLabel.toString());
+ }
+ }
+
+ builder.setDefaultObsolete(pkg.getDefaultObsolete());
+ builder.setDefaultTestonly(pkg.getDefaultTestOnly());
+ if (pkg.getDefaultDeprecation() != null) {
+ builder.setDefaultDeprecation(pkg.getDefaultDeprecation());
+ }
+
+ for (String defaultCopt : pkg.getDefaultCopts()) {
+ builder.addDefaultCopt(defaultCopt);
+ }
+
+ if (pkg.isDefaultHdrsCheckSet()) {
+ builder.setDefaultHdrsCheck(pkg.getDefaultHdrsCheck());
+ }
+
+ builder.setDefaultLicense(serializeLicense(pkg.getDefaultLicense()));
+
+ for (DistributionType distributionType : pkg.getDefaultDistribs()) {
+ builder.addDefaultDistrib(distributionType.toString());
+ }
+
+ for (String feature : pkg.getFeatures()) {
+ builder.addDefaultSetting(feature);
+ }
+
+ for (Label subincludeLabel : pkg.getSubincludeLabels()) {
+ builder.addSubincludeLabel(subincludeLabel.toString());
+ }
+
+ for (Label skylarkLabel : pkg.getSkylarkFileDependencies()) {
+ builder.addSkylarkLabel(skylarkLabel.toString());
+ }
+
+ for (Build.MakeVar makeVar :
+ serializeMakeEnvironment(pkg.getMakeEnvironment())) {
+ builder.addMakeVariable(makeVar);
+ }
+
+ for (Target target : pkg.getTargets()) {
+ if (target instanceof InputFile) {
+ builder.addSourceFile(serializeInputFile((InputFile) target));
+ } else if (target instanceof OutputFile) {
+ // Output files are ignored; they are recorded in rules.
+ } else if (target instanceof PackageGroup) {
+ builder.addPackageGroup(serializePackageGroup((PackageGroup) target));
+ } else if (target instanceof Rule) {
+ builder.addRule(serializeRule((Rule) target));
+ }
+ }
+
+ for (Event event : pkg.getEvents()) {
+ builder.addEvent(serializeEvent(event));
+ }
+
+ builder.setContainsErrors(pkg.containsErrors());
+ builder.setContainsTemporaryErrors(pkg.containsTemporaryErrors());
+ }
+
+ /**
+ * Serialize a package to a protocol message. The inverse of
+ * {@link PackageDeserializer#deserialize}.
+ */
+ public static Build.Package serializePackage(Package pkg) {
+ Build.Package.Builder builder = Build.Package.newBuilder();
+ serializePackageInternal(pkg, builder);
+ return builder.build();
+ }
+
+ /**
+ * Adds the serialized version of the specified attribute to the specified message.
+ *
+ * @param result the message to amend
+ * @param attr the attribute to add
+ * @param value the value of the attribute
+ * @param location the location of the attribute in the source file
+ * @param explicitlySpecified whether the attribute was explicitly specified or not
+ * @param includeGlobs add glob expression for attributes that contain them
+ */
+ // TODO(bazel-team): This is a copy of the code in ProtoOutputFormatter.
+ @SuppressWarnings("unchecked")
+ public static void addAttributeToProto(
+ Build.Rule.Builder result, Attribute attr, Object value, Location location,
+ Boolean explicitlySpecified, boolean includeGlobs) {
+ // Get the attribute type. We need to convert and add appropriately
+ com.google.devtools.build.lib.packages.Type<?> type = attr.getType();
+
+ Build.Attribute.Builder attrPb = Build.Attribute.newBuilder();
+
+ // Set the type, name and source
+ Build.Attribute.Discriminator attributeProtoType = ProtoUtils.getDiscriminatorFromType(type);
+ attrPb.setName(attr.getName());
+ attrPb.setType(attributeProtoType);
+
+ if (location != null) {
+ attrPb.setParseableLocation(serializeLocation(location));
+ }
+
+ if (explicitlySpecified != null) {
+ attrPb.setExplicitlySpecified(explicitlySpecified);
+ }
+
+ /*
+ * Set the appropriate type and value. Since string and string list store
+ * values for multiple types, use the toString() method on the objects
+ * instead of casting them. Note that Boolean and TriState attributes have
+ * both an integer and string representation.
+ */
+ if (type == INTEGER) {
+ attrPb.setIntValue((Integer) value);
+ } else if (type == STRING || type == LABEL || type == NODEP_LABEL || type == OUTPUT) {
+ attrPb.setStringValue(value.toString());
+ } else if (type == STRING_LIST || type == LABEL_LIST || type == NODEP_LABEL_LIST
+ || type == OUTPUT_LIST || type == DISTRIBUTIONS) {
+ Collection<?> values = (Collection<?>) value;
+ for (Object entry : values) {
+ attrPb.addStringListValue(entry.toString());
+ }
+ } else if (type == INTEGER_LIST) {
+ Collection<Integer> values = (Collection<Integer>) value;
+ for (Integer entry : values) {
+ attrPb.addIntListValue(entry);
+ }
+ } else if (type == BOOLEAN) {
+ if ((Boolean) value) {
+ attrPb.setStringValue("true");
+ attrPb.setBooleanValue(true);
+ } else {
+ attrPb.setStringValue("false");
+ attrPb.setBooleanValue(false);
+ }
+ // This maintains partial backward compatibility for external users of the
+ // protobuf that were expecting an integer field and not a true boolean.
+ attrPb.setIntValue((Boolean) value ? 1 : 0);
+ } else if (type == TRISTATE) {
+ switch ((TriState) value) {
+ case AUTO:
+ attrPb.setIntValue(-1);
+ attrPb.setStringValue("auto");
+ attrPb.setTristateValue(Build.Attribute.Tristate.AUTO);
+ break;
+ case NO:
+ attrPb.setIntValue(0);
+ attrPb.setStringValue("no");
+ attrPb.setTristateValue(Build.Attribute.Tristate.NO);
+ break;
+ case YES:
+ attrPb.setIntValue(1);
+ attrPb.setStringValue("yes");
+ attrPb.setTristateValue(Build.Attribute.Tristate.YES);
+ break;
+ }
+ } else if (type == LICENSE) {
+ License license = (License) value;
+ Build.License.Builder licensePb = Build.License.newBuilder();
+ for (License.LicenseType licenseType : license.getLicenseTypes()) {
+ licensePb.addLicenseType(licenseType.toString());
+ }
+ for (Label exception : license.getExceptions()) {
+ licensePb.addException(exception.toString());
+ }
+ attrPb.setLicense(licensePb);
+ } else if (type == STRING_DICT) {
+ Map<String, String> dict = (Map<String, String>) value;
+ for (Map.Entry<String, String> dictEntry : dict.entrySet()) {
+ Build.StringDictEntry entry = Build.StringDictEntry.newBuilder()
+ .setKey(dictEntry.getKey())
+ .setValue(dictEntry.getValue())
+ .build();
+ attrPb.addStringDictValue(entry);
+ }
+ } else if (type == STRING_DICT_UNARY) {
+ Map<String, String> dict = (Map<String, String>) value;
+ for (Map.Entry<String, String> dictEntry : dict.entrySet()) {
+ Build.StringDictUnaryEntry entry = Build.StringDictUnaryEntry.newBuilder()
+ .setKey(dictEntry.getKey())
+ .setValue(dictEntry.getValue())
+ .build();
+ attrPb.addStringDictUnaryValue(entry);
+ }
+ } else if (type == STRING_LIST_DICT) {
+ Map<String, List<String>> dict = (Map<String, List<String>>) value;
+ for (Map.Entry<String, List<String>> dictEntry : dict.entrySet()) {
+ Build.StringListDictEntry.Builder entry = Build.StringListDictEntry.newBuilder()
+ .setKey(dictEntry.getKey());
+ for (Object dictEntryValue : dictEntry.getValue()) {
+ entry.addValue(dictEntryValue.toString());
+ }
+ attrPb.addStringListDictValue(entry);
+ }
+ } else if (type == LABEL_LIST_DICT) {
+ Map<String, List<Label>> dict = (Map<String, List<Label>>) value;
+ for (Map.Entry<String, List<Label>> dictEntry : dict.entrySet()) {
+ Build.LabelListDictEntry.Builder entry = Build.LabelListDictEntry.newBuilder()
+ .setKey(dictEntry.getKey());
+ for (Object dictEntryValue : dictEntry.getValue()) {
+ entry.addValue(dictEntryValue.toString());
+ }
+ attrPb.addLabelListDictValue(entry);
+ }
+ } else if (type == FILESET_ENTRY_LIST) {
+ List<FilesetEntry> filesetEntries = (List<FilesetEntry>) value;
+ for (FilesetEntry filesetEntry : filesetEntries) {
+ Build.FilesetEntry.Builder filesetEntryPb = Build.FilesetEntry.newBuilder()
+ .setSource(filesetEntry.getSrcLabel().toString())
+ .setDestinationDirectory(filesetEntry.getDestDir().getPathString())
+ .setSymlinkBehavior(symlinkBehaviorToPb(filesetEntry.getSymlinkBehavior()))
+ .setStripPrefix(filesetEntry.getStripPrefix())
+ .setFilesPresent(filesetEntry.getFiles() != null);
+
+ if (filesetEntry.getFiles() != null) {
+ for (Label file : filesetEntry.getFiles()) {
+ filesetEntryPb.addFile(file.toString());
+ }
+ }
+
+ if (filesetEntry.getExcludes() != null) {
+ for (String exclude : filesetEntry.getExcludes()) {
+ filesetEntryPb.addExclude(exclude);
+ }
+ }
+
+ attrPb.addFilesetListValue(filesetEntryPb);
+ }
+ } else {
+ throw new IllegalStateException("Unknown type: " + type);
+ }
+
+ if (includeGlobs && value instanceof GlobList<?>) {
+ GlobList<?> globList = (GlobList<?>) value;
+
+ for (GlobCriteria criteria : globList.getCriteria()) {
+ Build.GlobCriteria.Builder criteriaPb = Build.GlobCriteria.newBuilder();
+ criteriaPb.setGlob(criteria.isGlob());
+ for (String include : criteria.getIncludePatterns()) {
+ criteriaPb.addInclude(include);
+ }
+ for (String exclude : criteria.getExcludePatterns()) {
+ criteriaPb.addExclude(exclude);
+ }
+
+ attrPb.addGlobCriteria(criteriaPb);
+ }
+ }
+ result.addAttribute(attrPb);
+ }
+
+ // This is needed because I do not want to use the SymlinkBehavior from the
+ // protocol buffer all over the place, so there are two classes that do
+ // essentially the same thing.
+ private static Build.FilesetEntry.SymlinkBehavior symlinkBehaviorToPb(
+ FilesetEntry.SymlinkBehavior symlinkBehavior) {
+ switch (symlinkBehavior) {
+ case COPY:
+ return Build.FilesetEntry.SymlinkBehavior.COPY;
+ case DEREFERENCE:
+ return Build.FilesetEntry.SymlinkBehavior.DEREFERENCE;
+ default:
+ throw new AssertionError("Unhandled FilesetEntry.SymlinkBehavior");
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/PackageSpecification.java b/src/main/java/com/google/devtools/build/lib/packages/PackageSpecification.java
new file mode 100644
index 0000000..20524aa
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/PackageSpecification.java
@@ -0,0 +1,150 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+import com.google.devtools.build.lib.cmdline.LabelValidator;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+/**
+ * A class that represents some packages that are included in the visibility list of a rule.
+ */
+public abstract class PackageSpecification {
+ private static final String PACKAGE_LABEL = "__pkg__";
+ private static final String SUBTREE_LABEL = "__subpackages__";
+ private static final String ALL_BENEATH_SUFFIX = "/...";
+ public static final PackageSpecification EVERYTHING =
+ new AllPackagesBeneath(new PathFragment(""));
+
+ public abstract boolean containsPackage(PathFragment packageName);
+
+ @Override
+ public int hashCode() {
+ return toString().hashCode();
+ }
+
+ @Override
+ public boolean equals(Object that) {
+ if (this == that) {
+ return true;
+ }
+
+ if (!(that instanceof PackageSpecification)) {
+ return false;
+ }
+
+ return this.toString().equals(that.toString());
+ }
+
+ /**
+ * Parses a string as a visibility specification.
+ * Throws {@link InvalidPackageSpecificationException} if the label cannot be parsed.
+ *
+ * <p>Note that these strings are different from what {@link #fromLabel} understands.
+ */
+ public static PackageSpecification fromString(final String spec)
+ throws InvalidPackageSpecificationException {
+ String result = spec;
+ boolean allBeneath = false;
+
+ if (result.startsWith("//")) {
+ result = spec.substring(2);
+ } else {
+ throw new InvalidPackageSpecificationException("invalid package label: " + spec);
+ }
+
+ if (result.indexOf(':') >= 0) {
+ throw new InvalidPackageSpecificationException("invalid package label: " + spec);
+ }
+
+ if (result.equals("...")) {
+ // Special case: //... will not end in /...
+ return EVERYTHING;
+ }
+
+ if (result.endsWith(ALL_BENEATH_SUFFIX)) {
+ allBeneath = true;
+ result = result.substring(0, result.length() - ALL_BENEATH_SUFFIX.length());
+ }
+
+ String errorMessage = LabelValidator.validatePackageName(result);
+ if (errorMessage == null) {
+ return allBeneath ?
+ new AllPackagesBeneath(new PathFragment(result)) :
+ new SinglePackage(new PathFragment(result));
+ } else {
+ throw new InvalidPackageSpecificationException(errorMessage);
+ }
+ }
+
+ /**
+ * Parses a label as a visibility specification. returns null if the label cannot be parsed.
+ *
+ * <p>Note that these strings are different from what {@link #fromString} understands.
+ */
+ public static PackageSpecification fromLabel(Label label) {
+ if (label.getName().equals(PACKAGE_LABEL)) {
+ return new SinglePackage(label.getPackageFragment());
+ } else if (label.getName().equals(SUBTREE_LABEL)) {
+ return new AllPackagesBeneath(label.getPackageFragment());
+ } else {
+ return null;
+ }
+ }
+
+ private static class SinglePackage extends PackageSpecification {
+ private PathFragment singlePackageName;
+
+ public SinglePackage(PathFragment packageName) {
+ this.singlePackageName = packageName;
+ }
+
+ @Override
+ public boolean containsPackage(PathFragment packageName) {
+ return this.singlePackageName.equals(packageName);
+ }
+
+ @Override
+ public String toString() {
+ return singlePackageName.toString();
+ }
+ }
+
+ private static class AllPackagesBeneath extends PackageSpecification {
+ private PathFragment prefix;
+
+ public AllPackagesBeneath(PathFragment prefix) {
+ this.prefix = prefix;
+ }
+
+ @Override
+ public boolean containsPackage(PathFragment packageName) {
+ return packageName.startsWith(prefix);
+ }
+
+ @Override
+ public String toString() {
+ return prefix.equals(new PathFragment("")) ? "..." : prefix.toString() + "/...";
+ }
+ }
+
+ /**
+ * Exception class to be thrown when a specification cannot be parsed.
+ */
+ public static class InvalidPackageSpecificationException extends Exception {
+ public InvalidPackageSpecificationException(String message) {
+ super(message);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/PredicateWithMessage.java b/src/main/java/com/google/devtools/build/lib/packages/PredicateWithMessage.java
new file mode 100644
index 0000000..4316f02
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/PredicateWithMessage.java
@@ -0,0 +1,30 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+import com.google.common.base.Predicate;
+
+/**
+ * A predicate which supports error messages.
+ * @param <T> - the predicate is applied on T objects
+ */
+public interface PredicateWithMessage<T> extends Predicate<T> {
+
+ /**
+ * The error message to display when predicate checks param. Only makes sense to
+ * call this method is apply(param) returns false.
+ */
+ public String getErrorReason(T param);
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/PredicatesWithMessage.java b/src/main/java/com/google/devtools/build/lib/packages/PredicatesWithMessage.java
new file mode 100644
index 0000000..a7f04bf
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/PredicatesWithMessage.java
@@ -0,0 +1,38 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+/**
+ * A helper class for PredicateWithMessage with default predicates.
+ */
+public abstract class PredicatesWithMessage implements PredicateWithMessage<Object> {
+
+ private static final PredicateWithMessage<?> ALWAYS_TRUE = new PredicateWithMessage<Object>() {
+ @Override
+ public boolean apply(Object input) {
+ return true;
+ }
+
+ @Override
+ public String getErrorReason(Object param) {
+ throw new UnsupportedOperationException();
+ }
+ };
+
+ @SuppressWarnings("unchecked")
+ public static <T> PredicateWithMessage<T> alwaysTrue() {
+ return (PredicateWithMessage<T>) ALWAYS_TRUE;
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/Preprocessor.java b/src/main/java/com/google/devtools/build/lib/packages/Preprocessor.java
new file mode 100644
index 0000000..8eda1e9
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/Preprocessor.java
@@ -0,0 +1,155 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.packages.PackageFactory.Globber;
+import com.google.devtools.build.lib.syntax.Environment;
+import com.google.devtools.build.lib.syntax.ParserInputSource;
+import com.google.devtools.build.lib.vfs.Path;
+
+import java.io.IOException;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+/** A Preprocessor is an interface to implement generic text-based preprocessing of BUILD files. */
+public interface Preprocessor {
+ /** Factory for {@link Preprocessor} instances. */
+ interface Factory {
+ /** Supplier for {@link Factory} instances. */
+ interface Supplier {
+ /**
+ * Returns a Preprocessor factory to use for getting Preprocessor instances.
+ *
+ * <p>The CachingPackageLocator is provided so the constructed preprocessors can look up
+ * other BUILD files.
+ */
+ Factory getFactory(CachingPackageLocator loc);
+
+ /** Supplier that always returns {@code NullFactory.INSTANCE}. */
+ static class NullSupplier implements Supplier {
+
+ public static final NullSupplier INSTANCE = new NullSupplier();
+
+ private NullSupplier() {
+ }
+
+ @Override
+ public Factory getFactory(CachingPackageLocator loc) {
+ return NullFactory.INSTANCE;
+ }
+ }
+ }
+
+ /**
+ * Returns whether this {@link Factory} is still suitable for providing {@link Preprocessor}s.
+ * If not, all previous preprocessing results should be assumed to be invalid and a new
+ * {@link Factory} should be created via {@link Supplier#getFactory}.
+ */
+ boolean isStillValid();
+
+ /**
+ * Returns a Preprocessor instance capable of preprocessing a BUILD file independently (e.g. it
+ * ought to be fine to call {@link #getPreprocessor} for each BUILD file).
+ */
+ @Nullable
+ Preprocessor getPreprocessor();
+
+ /** Factory that always returns {@code null} {@link Preprocessor}s. */
+ static class NullFactory implements Factory {
+ public static final NullFactory INSTANCE = new NullFactory();
+
+ private NullFactory() {
+ }
+
+ @Override
+ public boolean isStillValid() {
+ return true;
+ }
+
+ @Override
+ public Preprocessor getPreprocessor() {
+ return null;
+ }
+ }
+ }
+
+ /**
+ * A (result, success) tuple indicating the outcome of preprocessing.
+ */
+ static class Result {
+ public final ParserInputSource result;
+ public final boolean preprocessed;
+ public final boolean containsErrors;
+ public final boolean containsTransientErrors;
+
+ private Result(ParserInputSource result,
+ boolean preprocessed, boolean containsPersistentErrors, boolean containsTransientErrors) {
+ this.result = result;
+ this.preprocessed = preprocessed;
+ this.containsErrors = containsPersistentErrors || containsTransientErrors;
+ this.containsTransientErrors = containsTransientErrors;
+ }
+
+ /** Convenience factory for a {@link Result} wrapping non-preprocessed BUILD file contents. */
+ public static Result noPreprocessing(ParserInputSource buildFileSource) {
+ return new Result(buildFileSource, /*preprocessed=*/false, /*containsErrors=*/false,
+ /*containsTransientErrors=*/false);
+ }
+
+ /**
+ * Factory for a successful preprocessing result, meaning that the BUILD file was able to be
+ * read and has valid syntax and was preprocessed. But note that there may have been be errors
+ * during preprocessing.
+ */
+ public static Result success(ParserInputSource result, boolean containsErrors) {
+ return new Result(result, /*preprocessed=*/true, /*containsPersistentErrors=*/containsErrors,
+ /*containsTransientErrors=*/false);
+ }
+
+ public static Result invalidSyntax(Path buildFile) {
+ return new Result(ParserInputSource.create("", buildFile), /*preprocessed=*/true,
+ /*containsPersistentErrors=*/true, /*containsTransientErrors=*/false);
+ }
+
+ public static Result transientError(Path buildFile) {
+ return new Result(ParserInputSource.create("", buildFile), /*preprocessed=*/false,
+ /*containsPersistentErrors=*/false, /*containsTransientErrors=*/true);
+ }
+ }
+
+ /**
+ * Returns a Result resulting from applying Python preprocessing to the contents of "in". If
+ * errors happen, they must be reported both as an event on eventHandler and in the function
+ * return value.
+ *
+ * @param in the BUILD file to be preprocessed.
+ * @param packageName the BUILD file's package.
+ * @param globber a globber for evaluating globs.
+ * @param eventHandler a eventHandler on which to report warnings/errors.
+ * @param globalEnv the GLOBALS Python environment.
+ * @param ruleNames the set of names of all rules in the build language.
+ * @throws IOException if there was an I/O problem during preprocessing.
+ * @return a pair of the ParserInputSource and a map of subincludes seen during the evaluation
+ */
+ Result preprocess(
+ ParserInputSource in,
+ String packageName,
+ Globber globber,
+ EventHandler eventHandler,
+ Environment globalEnv,
+ Set<String> ruleNames)
+ throws IOException, InterruptedException;
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/ProtoUtils.java b/src/main/java/com/google/devtools/build/lib/packages/ProtoUtils.java
new file mode 100644
index 0000000..7b6eaf1
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/ProtoUtils.java
@@ -0,0 +1,83 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+import static com.google.devtools.build.lib.packages.Type.BOOLEAN;
+import static com.google.devtools.build.lib.packages.Type.DISTRIBUTIONS;
+import static com.google.devtools.build.lib.packages.Type.FILESET_ENTRY_LIST;
+import static com.google.devtools.build.lib.packages.Type.INTEGER;
+import static com.google.devtools.build.lib.packages.Type.INTEGER_LIST;
+import static com.google.devtools.build.lib.packages.Type.LABEL;
+import static com.google.devtools.build.lib.packages.Type.LABEL_LIST;
+import static com.google.devtools.build.lib.packages.Type.LABEL_LIST_DICT;
+import static com.google.devtools.build.lib.packages.Type.LICENSE;
+import static com.google.devtools.build.lib.packages.Type.NODEP_LABEL;
+import static com.google.devtools.build.lib.packages.Type.NODEP_LABEL_LIST;
+import static com.google.devtools.build.lib.packages.Type.OUTPUT;
+import static com.google.devtools.build.lib.packages.Type.OUTPUT_LIST;
+import static com.google.devtools.build.lib.packages.Type.STRING;
+import static com.google.devtools.build.lib.packages.Type.STRING_DICT;
+import static com.google.devtools.build.lib.packages.Type.STRING_DICT_UNARY;
+import static com.google.devtools.build.lib.packages.Type.STRING_LIST;
+import static com.google.devtools.build.lib.packages.Type.STRING_LIST_DICT;
+import static com.google.devtools.build.lib.packages.Type.TRISTATE;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.query2.proto.proto2api.Build.Attribute.Discriminator;
+
+import java.util.Map;
+
+/**
+ * Shared code used in proto buffer output for rules and rule classes.
+ */
+public class ProtoUtils {
+ /**
+ * This map contains all attribute types that are recognized by the protocol
+ * output formatter.
+ */
+ private static final Map<Type<?>, Discriminator> TYPE_MAP
+ = new ImmutableMap.Builder<Type<?>, Discriminator>()
+ .put(INTEGER, Discriminator.INTEGER)
+ .put(DISTRIBUTIONS, Discriminator.DISTRIBUTION_SET)
+ .put(LABEL, Discriminator.LABEL)
+ // NODEP_LABEL attributes are not really strings. This is implemented
+ // this way for the sake of backward compatibility.
+ .put(NODEP_LABEL, Discriminator.STRING)
+ .put(LABEL_LIST, Discriminator.LABEL_LIST)
+ .put(NODEP_LABEL_LIST, Discriminator.STRING_LIST)
+ .put(STRING, Discriminator.STRING)
+ .put(STRING_LIST, Discriminator.STRING_LIST)
+ .put(OUTPUT, Discriminator.OUTPUT)
+ .put(OUTPUT_LIST, Discriminator.OUTPUT_LIST)
+ .put(LICENSE, Discriminator.LICENSE)
+ .put(STRING_DICT, Discriminator.STRING_DICT)
+ .put(FILESET_ENTRY_LIST, Discriminator.FILESET_ENTRY_LIST)
+ .put(LABEL_LIST_DICT, Discriminator.LABEL_LIST_DICT)
+ .put(STRING_LIST_DICT, Discriminator.STRING_LIST_DICT)
+ .put(BOOLEAN, Discriminator.BOOLEAN)
+ .put(TRISTATE, Discriminator.TRISTATE)
+ .put(INTEGER_LIST, Discriminator.INTEGER_LIST)
+ .put(STRING_DICT_UNARY, Discriminator.STRING_DICT_UNARY)
+ .build();
+
+ /**
+ * Returns the appropriate Attribute.Discriminator value from an internal attribute type.
+ */
+ public static Discriminator getDiscriminatorFromType(Type<?> type) {
+ Preconditions.checkArgument(TYPE_MAP.containsKey(type));
+ return TYPE_MAP.get(type);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/RawAttributeMapper.java b/src/main/java/com/google/devtools/build/lib/packages/RawAttributeMapper.java
new file mode 100644
index 0000000..a174193
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/RawAttributeMapper.java
@@ -0,0 +1,58 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.syntax.Label;
+
+/**
+ * {@link AttributeMap} implementation that returns raw attribute information as contained
+ * within a {@link Rule}. In particular, configurable attributes of the form
+ * { config1: "value1", config2: "value2" } are passed through without being resolved to a
+ * final value.
+ */
+public class RawAttributeMapper extends AbstractAttributeMapper {
+ RawAttributeMapper(Package pkg, RuleClass ruleClass, Label ruleLabel,
+ AttributeContainer attributes) {
+ super(pkg, ruleClass, ruleLabel, attributes);
+ }
+
+ public static RawAttributeMapper of(Rule rule) {
+ return new RawAttributeMapper(rule.getPackage(), rule.getRuleClassObject(), rule.getLabel(),
+ rule.getAttributeContainer());
+ }
+
+ @Override
+ protected <T> Iterable<T> visitAttribute(String attributeName, Type<T> type) {
+ T value = get(attributeName, type);
+ return value == null ? ImmutableList.<T>of() : ImmutableList.of(value);
+ }
+
+ /**
+ * Returns true if the given attribute is configurable for this rule instance, false
+ * otherwise.
+ */
+ public <T> boolean isConfigurable(String attributeName, Type<T> type) {
+ return getSelector(attributeName, type) != null;
+ }
+
+ /**
+ * If the attribute is configurable for this rule instance, returns its configuration
+ * keys. Else returns an empty list.
+ */
+ public <T> Iterable<Label> getConfigurabilityKeys(String attributeName, Type<T> type) {
+ Type.Selector<T> selector = getSelector(attributeName, type);
+ return selector == null ? ImmutableList.<Label>of() : selector.getEntries().keySet();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/RelativePackageNameResolver.java b/src/main/java/com/google/devtools/build/lib/packages/RelativePackageNameResolver.java
new file mode 100644
index 0000000..ade196b
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/RelativePackageNameResolver.java
@@ -0,0 +1,81 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+/**
+ * Resolves relative package names to absolute ones. Handles the absolute
+ * package path marker ("//") and uplevel references ("..").
+ */
+public class RelativePackageNameResolver {
+ private final PathFragment offset;
+ private final boolean discardBuild;
+
+ /**
+ * @param offset the base package path used to resolve relative paths
+ * @param discardBuild if true, discards the last package path segment if
+ * it is called "BUILD"
+ */
+ public RelativePackageNameResolver(PathFragment offset, boolean discardBuild) {
+ Preconditions.checkArgument(!offset.containsUplevelReferences(),
+ "offset should not contain uplevel references");
+
+ this.offset = offset;
+ this.discardBuild = discardBuild;
+ }
+
+ /**
+ * Resolves the given package name with respect to the offset given in the
+ * constructor.
+ *
+ * @param pkg the relative package name to be resolved
+ * @return the absolute package name
+ * @throws InvalidPackageNameException if the package name cannot be resolved
+ * (only syntactic checks are done -- it is not checked if the package
+ * really exists or not)
+ */
+ public String resolve(String pkg) throws InvalidPackageNameException {
+ boolean isAbsolute;
+ String relativePkg;
+
+ if (pkg.startsWith("//")) {
+ isAbsolute = true;
+ relativePkg = pkg.substring(2);
+ } else if (pkg.startsWith("/")) {
+ throw new InvalidPackageNameException(pkg,
+ "package name cannot start with a single slash");
+ } else {
+ isAbsolute = false;
+ relativePkg = pkg;
+ }
+
+ PathFragment relative = new PathFragment(relativePkg);
+
+ if (discardBuild && relative.getBaseName().equals("BUILD")) {
+ relative = relative.getParentDirectory();
+ }
+
+ PathFragment result = isAbsolute ? relative : offset.getRelative(relative);
+ result = result.normalize();
+ if (result.containsUplevelReferences()) {
+ throw new InvalidPackageNameException(pkg,
+ "package name contains too many '..' segments");
+ }
+
+ return result.getPathString();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/Rule.java b/src/main/java/com/google/devtools/build/lib/packages/Rule.java
new file mode 100644
index 0000000..cff91f6
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/Rule.java
@@ -0,0 +1,666 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.LinkedListMultimap;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Lists;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.packages.Attribute.ConfigurationTransition;
+import com.google.devtools.build.lib.packages.License.DistributionType;
+import com.google.devtools.build.lib.syntax.EvalException;
+import com.google.devtools.build.lib.syntax.FuncallExpression;
+import com.google.devtools.build.lib.syntax.GlobList;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.syntax.Label.SyntaxException;
+import com.google.devtools.build.lib.util.BinaryPredicate;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * An instance of a build rule in the build language. A rule has a name, a
+ * package to which it belongs, a class such as <code>cc_library</code>, and
+ * set of typed attributes. The set of attribute names and types is a property
+ * of the rule's class. The use of the term "class" here has nothing to do
+ * with Java classes. All rules are implemented by the same Java classes, Rule
+ * and RuleClass.
+ *
+ * <p>Here is a typical rule as it appears in a BUILD file:
+ * <pre>
+ * cc_library(name = 'foo',
+ * defines = ['-Dkey=value'],
+ * srcs = ['foo.cc'],
+ * deps = ['bar'])
+ * </pre>
+ */
+public final class Rule implements Target {
+ /** Dependency predicate that includes all dependencies */
+ public static final BinaryPredicate<Rule, Attribute> ALL_DEPS =
+ new BinaryPredicate<Rule, Attribute>() {
+ @Override
+ public boolean apply(Rule x, Attribute y) {
+ return true;
+ }
+ };
+
+ /** Dependency predicate that excludes host dependencies */
+ public static final BinaryPredicate<Rule, Attribute> NO_HOST_DEPS =
+ new BinaryPredicate<Rule, Attribute>() {
+ @Override
+ public boolean apply(Rule rule, Attribute attribute) {
+ // isHostConfiguration() is only defined for labels and label lists.
+ if (attribute.getType() != Type.LABEL && attribute.getType() != Type.LABEL_LIST) {
+ return true;
+ }
+
+ return attribute.getConfigurationTransition() != ConfigurationTransition.HOST;
+ }
+ };
+
+ /** Dependency predicate that excludes implicit dependencies */
+ public static final BinaryPredicate<Rule, Attribute> NO_IMPLICIT_DEPS =
+ new BinaryPredicate<Rule, Attribute>() {
+ @Override
+ public boolean apply(Rule rule, Attribute attribute) {
+ return rule.isAttributeValueExplicitlySpecified(attribute);
+ }
+ };
+
+ /**
+ * Dependency predicate that excludes those edges that are not present in the
+ * configured target graph.
+ */
+ public static final BinaryPredicate<Rule, Attribute> NO_NODEP_ATTRIBUTES =
+ new BinaryPredicate<Rule, Attribute>() {
+ @Override
+ public boolean apply(Rule rule, Attribute attribute) {
+ return attribute.getType() != Type.NODEP_LABEL &&
+ attribute.getType() != Type.NODEP_LABEL_LIST;
+ }
+ };
+
+ /** Label predicate that allows every label. */
+ public static final Predicate<Label> ALL_LABELS = Predicates.alwaysTrue();
+
+ /**
+ * Checks to see if the attribute has the isDirectCompileTimeInput property.
+ */
+ public static final BinaryPredicate<Rule, Attribute> DIRECT_COMPILE_TIME_INPUT =
+ new BinaryPredicate<Rule, Attribute>() {
+ @Override
+ public boolean apply(Rule rule, Attribute attribute) {
+ return attribute.isDirectCompileTimeInput();
+ }
+ };
+
+ /**
+ * Returns a predicate that computes the logical and of the two given predicates.
+ */
+ public static <X, Y> BinaryPredicate<X, Y> and(
+ final BinaryPredicate<X, Y> a, final BinaryPredicate<X, Y> b) {
+ return new BinaryPredicate<X, Y>() {
+ @Override
+ public boolean apply(X x, Y y) {
+ return a.apply(x, y) && b.apply(x, y);
+ }
+ };
+ }
+
+ private final Label label;
+
+ private final Package pkg;
+
+ private final RuleClass ruleClass;
+
+ private final AttributeContainer attributes;
+ private final RawAttributeMapper attributeMap;
+
+ private RuleVisibility visibility;
+
+ private boolean containsErrors;
+
+ private final Location location;
+
+ private final FuncallExpression ast; // may be null
+
+ // Initialized in the call to populateOutputFiles.
+ private List<OutputFile> outputFiles;
+ private ListMultimap<String, OutputFile> outputFileMap;
+
+ Rule(Package pkg, Label label, RuleClass ruleClass, FuncallExpression ast, Location location) {
+ this.pkg = Preconditions.checkNotNull(pkg);
+ this.label = label;
+ this.ruleClass = Preconditions.checkNotNull(ruleClass);
+ this.location = Preconditions.checkNotNull(location);
+ this.attributes = new AttributeContainer(ruleClass);
+ this.attributeMap = new RawAttributeMapper(pkg, ruleClass, label, attributes);
+ this.containsErrors = false;
+ this.ast = ast;
+ }
+
+ void setVisibility(RuleVisibility visibility) {
+ this.visibility = visibility;
+ }
+
+ void setAttributeValue(Attribute attribute, Object value, boolean explicit) {
+ attributes.setAttributeValue(attribute, value, explicit);
+ }
+
+ void setAttributeValueByName(String attrName, Object value) {
+ attributes.setAttributeValueByName(attrName, value);
+ }
+
+ void setAttributeLocation(int attrIndex, Location location) {
+ attributes.setAttributeLocation(attrIndex, location);
+ }
+
+ void setAttributeLocation(Attribute attribute, Location location) {
+ attributes.setAttributeLocation(attribute, location);
+ }
+
+ void setContainsErrors() {
+ this.containsErrors = true;
+ }
+
+ @Override
+ public Label getLabel() {
+ return attributeMap.getLabel();
+ }
+
+ @Override
+ public String getName() {
+ return attributeMap.getName();
+ }
+
+ @Override
+ public Package getPackage() {
+ return pkg;
+ }
+
+ public RuleClass getRuleClassObject() {
+ return ruleClass;
+ }
+
+ @Override
+ public String getTargetKind() {
+ return ruleClass.getTargetKind();
+ }
+
+ /**
+ * Returns the class of this rule. (e.g. "cc_library")
+ */
+ public String getRuleClass() {
+ return ruleClass.getName();
+ }
+
+ /**
+ * Returns the build features that apply to this rule.
+ */
+ public ImmutableSet<String> getFeatures() {
+ return pkg.getFeatures();
+ }
+
+ /**
+ * Returns true iff the outputs of this rule should be created beneath the
+ * bin directory, false if beneath genfiles. For most rule
+ * classes, this is a constant, but for genrule, it is a property of the
+ * individual rule instance, derived from the 'output_to_bindir' attribute.
+ */
+ public boolean hasBinaryOutput() {
+ return ruleClass.getName().equals("genrule") // this is unfortunate...
+ ? NonconfigurableAttributeMapper.of(this).get("output_to_bindir", Type.BOOLEAN)
+ : ruleClass.hasBinaryOutput();
+ }
+
+ /**
+ * Returns the AST for this rule. Returns null if the package factory chose
+ * not to retain the AST when evaluateBuildFile was called for this rule's
+ * package.
+ */
+ public FuncallExpression getSyntaxTree() {
+ return ast;
+ }
+
+ /**
+ * Returns true iff there were errors while constructing this rule, such as
+ * attributes with missing values or values of the wrong type.
+ */
+ public boolean containsErrors() {
+ return containsErrors;
+ }
+
+ /**
+ * Returns an (unmodifiable, unordered) collection containing all the
+ * Attribute definitions for this kind of rule. (Note, this doesn't include
+ * the <i>values</i> of the attributes, merely the schema. Call
+ * get[Type]Attr() methods to access the actual values.)
+ */
+ public Collection<Attribute> getAttributes() {
+ return ruleClass.getAttributes();
+ }
+
+ /**
+ * Returns true if this rule has any attributes that are configurable.
+ *
+ * <p>Note this is *not* the same as having attribute *types* that are configurable. For example,
+ * "deps" is configurable, in that one can write a rule that sets "deps" to a configuration
+ * dictionary. But if *this* rule's instance of "deps" doesn't do that, its instance
+ * of "deps" is not considered configurable.
+ *
+ * <p>In other words, this method signals which rules might have their attribute values
+ * influenced by the configuration.
+ */
+ public boolean hasConfigurableAttributes() {
+ for (Attribute attribute : getAttributes()) {
+ if (attributeMap.isConfigurable(attribute.getName(), attribute.getType())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns the attribute definition whose name is {@code attrName}, or null
+ * if not found. (Use get[X]Attr for the actual value.)
+ *
+ * @deprecated use {@link AbstractAttributeMapper#getAttributeDefinition} instead
+ */
+ @Deprecated
+ public Attribute getAttributeDefinition(String attrName) {
+ return attributeMap.getAttributeDefinition(attrName);
+ }
+
+ /**
+ * Returns an (unmodifiable, ordered) collection containing all the declared output files of this
+ * rule.
+ *
+ * <p>All implicit output files (declared in the {@link RuleClass}) are
+ * listed first, followed by any explicit files (declared via the 'outs' attribute). Additionally
+ * both implicit and explicit outputs will retain the relative order in which they were declared.
+ *
+ * <p>This ordering is useful because it is propagated through to the list of targets returned by
+ * getOuts() and allows targets to access their implicit outputs easily via
+ * {@code getOuts().get(N)} (providing that N is less than the number of implicit outputs).
+ *
+ * <p>The fact that the relative order of the explicit outputs is also retained is less obviously
+ * useful but is still well defined.
+ */
+ public Collection<OutputFile> getOutputFiles() {
+ return outputFiles;
+ }
+
+ /**
+ * Returns an (unmodifiable, ordered) map containing the list of output files for every
+ * output type attribute.
+ */
+ public ListMultimap<String, OutputFile> getOutputFileMap() {
+ return outputFileMap;
+ }
+
+ @Override
+ public Location getLocation() {
+ return location;
+ }
+
+ @Override
+ public Rule getAssociatedRule() {
+ return this;
+ }
+
+ /**
+ * Returns this rule's raw attribute info, suitable for being fed into an
+ * {@link AttributeMap} for user-level attribute access. Don't use this method
+ * for direct attribute access.
+ */
+ public AttributeContainer getAttributeContainer() {
+ return attributes;
+ }
+
+ /********************************************************************
+ * Attribute accessor functions.
+ *
+ * The below provide access to attribute definitions and other generic
+ * metadata.
+ *
+ * For access to attribute *values* (e.g. "What's the value of attribute
+ * X for Rule Y?"), go through {@link RuleContext#attributes}. If no
+ * RuleContext is available, create a localized {@link AbstractAttributeMapper}
+ * instance instead.
+ ********************************************************************/
+
+ /**
+ * Returns the default value for the attribute {@code attrName}, which may be
+ * of any type, but must exist (an exception is thrown otherwise).
+ */
+ public Object getAttrDefaultValue(String attrName) {
+ Object defaultValue = ruleClass.getAttributeByName(attrName).getDefaultValue(this);
+ // Computed defaults not expected here.
+ Preconditions.checkState(!(defaultValue instanceof Attribute.ComputedDefault));
+ return defaultValue;
+ }
+
+ /**
+ * Returns true iff the rule class has an attribute with the given name and type.
+ */
+ public boolean isAttrDefined(String attrName, Type<?> type) {
+ return ruleClass.hasAttr(attrName, type);
+ }
+
+ /**
+ * Returns true iff the value of the specified attribute is explicitly set in
+ * the BUILD file (as opposed to its default value). This also returns true if
+ * the value from the BUILD file is the same as the default value.
+ */
+ public boolean isAttributeValueExplicitlySpecified(Attribute attribute) {
+ return attributes.isAttributeValueExplicitlySpecified(attribute);
+ }
+
+ /**
+ * Returns true iff the value of the specified attribute is explicitly set in the BUILD file (as
+ * opposed to its default value). This also returns true if the value from the BUILD file is the
+ * same as the default value. In addition, this method return false if the rule has no attribute
+ * with the given name.
+ */
+ public boolean isAttributeValueExplicitlySpecified(String attrName) {
+ return attributeMap.isAttributeValueExplicitlySpecified(attrName);
+ }
+
+ /**
+ * Returns the location of the attribute definition for this rule, if known;
+ * or the location of the whole rule otherwise. "attrName" need not be a
+ * valid attribute name for this rule.
+ */
+ public Location getAttributeLocation(String attrName) {
+ Location attrLocation = null;
+ if (!attrName.equals("name")) {
+ attrLocation = attributes.getAttributeLocation(attrName);
+ }
+ return attrLocation != null ? attrLocation : getLocation();
+ }
+
+ /**
+ * Returns a new List instance containing all direct dependencies (all types).
+ */
+ public Collection<Label> getLabels() {
+ return getLabels(Rule.ALL_DEPS);
+ }
+
+ /**
+ * Returns a new Collection containing all Labels that match a given Predicate,
+ * not including outputs.
+ *
+ * @param predicate A binary predicate that determines if a label should be
+ * included in the result. The predicate is evaluated with this rule and
+ * the attribute that contains the label. The label will be contained in the
+ * result iff (the predicate returned {@code true} and the labels are not outputs)
+ */
+ public Collection<Label> getLabels(final BinaryPredicate<Rule, Attribute> predicate) {
+ final Set<Label> labels = new HashSet<>();
+ // TODO(bazel-team): move this to AttributeMap, too. Just like visitLabels, which labels should
+ // be visited may depend on the calling context. We shouldn't implicitly decide this for
+ // the caller.
+ AggregatingAttributeMapper.of(this).visitLabels(new AttributeMap.AcceptsLabelAttribute() {
+ @Override
+ public void acceptLabelAttribute(Label label, Attribute attribute) {
+ if (predicate.apply(Rule.this, attribute)) {
+ labels.add(label);
+ }
+ }
+ });
+ return labels;
+ }
+
+ /**
+ * Check if this rule is valid according to the validityPredicate of its RuleClass.
+ */
+ void checkValidityPredicate(EventHandler eventHandler) {
+ PredicateWithMessage<Rule> predicate = getRuleClassObject().getValidityPredicate();
+ if (!predicate.apply(this)) {
+ reportError(predicate.getErrorReason(this), eventHandler);
+ }
+ }
+
+ /**
+ * Collects the output files (both implicit and explicit). All the implicit output files are added
+ * first, followed by any explicit files. Additionally both implicit and explicit output files
+ * will retain the relative order in which they were declared.
+ */
+ void populateOutputFiles(EventHandler eventHandler,
+ Package.AbstractBuilder<?, ?> pkgBuilder) throws SyntaxException {
+ Preconditions.checkState(outputFiles == null);
+ // Order is important here: implicit before explicit
+ outputFiles = Lists.newArrayList();
+ outputFileMap = LinkedListMultimap.create();
+ populateImplicitOutputFiles(eventHandler, pkgBuilder);
+ populateExplicitOutputFiles(eventHandler);
+ outputFiles = ImmutableList.copyOf(outputFiles);
+ outputFileMap = ImmutableListMultimap.copyOf(outputFileMap);
+ }
+
+ // Explicit output files are user-specified attributes of type OUTPUT.
+ private void populateExplicitOutputFiles(EventHandler eventHandler) throws SyntaxException {
+ NonconfigurableAttributeMapper nonConfigurableAttributes =
+ NonconfigurableAttributeMapper.of(this);
+ for (Attribute attribute : ruleClass.getAttributes()) {
+ String name = attribute.getName();
+ Type<?> type = attribute.getType();
+ if (type == Type.OUTPUT) {
+ Label outputLabel = nonConfigurableAttributes.get(name, Type.OUTPUT);
+ if (outputLabel != null) {
+ addLabelOutput(attribute, outputLabel, eventHandler);
+ }
+ } else if (type == Type.OUTPUT_LIST) {
+ for (Label label : nonConfigurableAttributes.get(name, Type.OUTPUT_LIST)) {
+ addLabelOutput(attribute, label, eventHandler);
+ }
+ }
+ }
+ }
+
+ /**
+ * Implicit output files come from rule-specific patterns, and are a function
+ * of the rule's "name", "srcs", and other attributes.
+ */
+ private void populateImplicitOutputFiles(EventHandler eventHandler,
+ Package.AbstractBuilder<?, ?> pkgBuilder) {
+ try {
+ for (String out : ruleClass.getImplicitOutputsFunction().getImplicitOutputs(attributeMap)) {
+ try {
+ addOutputFile(pkgBuilder.createLabel(out), eventHandler);
+ } catch (SyntaxException e) {
+ reportError("illegal output file name '" + out + "' in rule "
+ + getLabel(), eventHandler);
+ }
+ }
+ } catch (EvalException e) {
+ reportError(e.print(), eventHandler);
+ }
+ }
+
+ private void addLabelOutput(Attribute attribute, Label label, EventHandler eventHandler)
+ throws SyntaxException {
+ if (!label.getPackageIdentifier().equals(pkg.getPackageIdentifier())) {
+ throw new IllegalStateException("Label for attribute " + attribute
+ + " should refer to '" + pkg.getName()
+ + "' but instead refers to '" + label.getPackageFragment()
+ + "' (label '" + label.getName() + "')");
+ }
+ if (label.getName().equals(".")) {
+ throw new SyntaxException("output file name can't be equal '.'");
+ }
+ OutputFile outputFile = addOutputFile(label, eventHandler);
+ outputFileMap.put(attribute.getName(), outputFile);
+ }
+
+ private OutputFile addOutputFile(Label label, EventHandler eventHandler) {
+ if (label.getName().equals(getName())) {
+ // TODO(bazel-team): for now (23 Apr 2008) this is just a warning. After
+ // June 1st we should make it an error.
+ reportWarning("target '" + getName() + "' is both a rule and a file; please choose "
+ + "another name for the rule", eventHandler);
+ }
+ OutputFile outputFile = new OutputFile(pkg, label, this);
+ outputFiles.add(outputFile);
+ return outputFile;
+ }
+
+ void reportError(String message, EventHandler eventHandler) {
+ eventHandler.handle(Event.error(location, message));
+ this.containsErrors = true;
+ }
+
+ void reportWarning(String message, EventHandler eventHandler) {
+ eventHandler.handle(Event.warn(location, message));
+ }
+
+ @Override
+ public int hashCode() {
+ return label.hashCode();
+ }
+
+ /**
+ * Returns a string of the form "cc_binary rule //foo:foo"
+ *
+ * @return a string of the form "cc_binary rule //foo:foo"
+ */
+ @Override
+ public String toString() {
+ return getRuleClass() + " rule " + getLabel();
+ }
+
+ /**
+ * Returns the effective visibility of this Rule. Visibility is computed from
+ * these sources in this order of preference:
+ * - 'visibility' attribute
+ * - 'default_visibility;' attribute of package() declaration
+ * - public.
+ */
+ @Override
+ public RuleVisibility getVisibility() {
+ if (visibility != null) {
+ return visibility;
+ }
+
+ if (getRuleClassObject().isPublicByDefault()) {
+ return ConstantRuleVisibility.PUBLIC;
+ }
+
+ return pkg.getDefaultVisibility();
+ }
+
+ public boolean isVisibilitySpecified() {
+ return visibility != null;
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public Set<DistributionType> getDistributions() {
+ if (isAttrDefined("distribs", Type.DISTRIBUTIONS)
+ && isAttributeValueExplicitlySpecified("distribs")) {
+ return NonconfigurableAttributeMapper.of(this).get("distribs", Type.DISTRIBUTIONS);
+ } else {
+ return getPackage().getDefaultDistribs();
+ }
+ }
+
+ @Override
+ public License getLicense() {
+ if (isAttrDefined("licenses", Type.LICENSE)
+ && isAttributeValueExplicitlySpecified("licenses")) {
+ return NonconfigurableAttributeMapper.of(this).get("licenses", Type.LICENSE);
+ } else {
+ return getPackage().getDefaultLicense();
+ }
+ }
+
+ /**
+ * Returns the license of the output of the binary created by this rule, or
+ * null if it is not specified.
+ */
+ public License getToolOutputLicense(AttributeMap attributes) {
+ if (isAttrDefined("output_licenses", Type.LICENSE)
+ && attributes.isAttributeValueExplicitlySpecified("output_licenses")) {
+ return attributes.get("output_licenses", Type.LICENSE);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Returns the globs that were expanded to create an attribute value, or
+ * null if unknown or not applicable.
+ */
+ public static GlobList<?> getGlobInfo(Object attributeValue) {
+ if (attributeValue instanceof GlobList<?>) {
+ return (GlobList<?>) attributeValue;
+ } else {
+ return null;
+ }
+ }
+
+ private void checkForNullLabel(Label labelToCheck, String where) {
+ if (labelToCheck == null) {
+ throw new IllegalStateException(String.format(
+ "null label in rule %s, %s", getLabel().toString(), where));
+ }
+ }
+
+ // Consistency check: check if this label contains any weird labels (i.e.
+ // null-valued, with a packageFragment that is null...). The bug that prompted
+ // the introduction of this code is #2210848 (NullPointerException in
+ // Package.checkForConflicts() ).
+ void checkForNullLabels() {
+ AggregatingAttributeMapper.of(this).visitLabels(
+ new AttributeMap.AcceptsLabelAttribute() {
+ @Override
+ public void acceptLabelAttribute(Label labelToCheck, Attribute attribute) {
+ checkForNullLabel(labelToCheck, "attribute " + attribute.getName());
+ }
+ });
+ for (OutputFile outputFile : getOutputFiles()) {
+ checkForNullLabel(outputFile.getLabel(), "output file");
+ }
+ }
+
+ /**
+ * Returns the Set of all tags exhibited by this target. May be empty.
+ */
+ public Set<String> getRuleTags() {
+ Set<String> ruleTags = new LinkedHashSet<>();
+ for (Attribute attribute : getRuleClassObject().getAttributes()) {
+ if (attribute.isTaggable()) {
+ Type<?> attrType = attribute.getType();
+ String name = attribute.getName();
+ // This enforces the expectation that taggable attributes are non-configurable.
+ Object value = NonconfigurableAttributeMapper.of(this).get(name, attrType);
+ Set<String> tags = attrType.toTagSet(value, name);
+ ruleTags.addAll(tags);
+ }
+ }
+ return ruleTags;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/RuleClass.java b/src/main/java/com/google/devtools/build/lib/packages/RuleClass.java
new file mode 100644
index 0000000..f495502
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/RuleClass.java
@@ -0,0 +1,1511 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+import static com.google.devtools.build.lib.packages.Attribute.ConfigurationTransition.HOST;
+import static com.google.devtools.build.lib.packages.Attribute.attr;
+import static com.google.devtools.build.lib.packages.Type.BOOLEAN;
+import static com.google.devtools.build.lib.packages.Type.LABEL_LIST;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Ordering;
+import com.google.devtools.build.lib.collect.CollectionUtils;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.syntax.Argument;
+import com.google.devtools.build.lib.syntax.Environment;
+import com.google.devtools.build.lib.syntax.FuncallExpression;
+import com.google.devtools.build.lib.syntax.GlobList;
+import com.google.devtools.build.lib.syntax.Ident;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.syntax.Label.SyntaxException;
+import com.google.devtools.build.lib.syntax.SkylarkEnvironment;
+import com.google.devtools.build.lib.syntax.UserDefinedFunction;
+import com.google.devtools.build.lib.util.StringUtil;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.ArrayList;
+import java.util.BitSet;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * Instances of RuleClass encapsulate the set of attributes of a given "class" of rule, such as
+ * <code>cc_binary</code>.
+ *
+ * <p>This is an instance of the "meta-class" pattern for Rules: we achieve using <i>values</i>
+ * what subclasses achieve using <i>types</i>. (The "Design Patterns" book doesn't include this
+ * pattern, so think of it as something like a cross between a Flyweight and a State pattern. Like
+ * Flyweight, we avoid repeatedly storing data that belongs to many instances. Like State, we
+ * delegate from Rule to RuleClass for the specific behavior of that rule (though unlike state, a
+ * Rule object never changes its RuleClass). This avoids the need to declare one Java class per
+ * class of Rule, yet achieves the same behavior.)
+ *
+ * <p>The use of a metaclass also allows us to compute a mapping from Attributes to small integers
+ * and share this between all rules of the same metaclass. This means we can save the attribute
+ * dictionary for each rule instance using an array, which is much more compact than a hashtable.
+ *
+ * <p>Rule classes whose names start with "$" are considered "abstract"; since they are not valid
+ * identifiers, they cannot be named in the build language. However, they are useful for grouping
+ * related attributes which are inherited.
+ *
+ * <p>The exact values in this class are important. In particular:
+ * <ul>
+ * <li>Changing an attribute from MANDATORY to OPTIONAL creates the potential for null-pointer
+ * exceptions in code that expects a value.
+ * <li>Attributes whose names are preceded by a "$" or a ":" are "hidden", and cannot be redefined
+ * in a BUILD file. They are a useful way of adding a special dependency. By convention,
+ * attributes starting with "$" are implicit dependencies, and those starting with a ":" are
+ * late-bound implicit dependencies, i.e. dependencies that can only be resolved when the
+ * configuration is known.
+ * <li>Attributes should not be introduced into the hierarchy higher then necessary.
+ * <li>The 'deps' and 'data' attributes are treated specially by the code that builds the runfiles
+ * tree. All targets appearing in these attributes appears beneath the ".runfiles" tree; in
+ * addition, "deps" may have rule-specific semantics.
+ * </ul>
+ */
+@Immutable
+public final class RuleClass {
+ /**
+ * A constraint for the package name of the Rule instances.
+ */
+ public static class PackageNameConstraint implements PredicateWithMessage<Rule> {
+
+ public static final int ANY_SEGMENT = 0;
+
+ private final int pathSegment;
+
+ private final Set<String> values;
+
+ /**
+ * The pathSegment-th segment of the package must be one of the specified values.
+ * The path segment indexing starts from 1.
+ */
+ public PackageNameConstraint(int pathSegment, String... values) {
+ this.values = ImmutableSet.copyOf(values);
+ this.pathSegment = pathSegment;
+ }
+
+ @Override
+ public boolean apply(Rule input) {
+ PathFragment path = input.getLabel().getPackageFragment();
+ if (pathSegment == ANY_SEGMENT) {
+ return path.getFirstSegment(values) != PathFragment.INVALID_SEGMENT;
+ } else {
+ return path.segmentCount() >= pathSegment
+ && values.contains(path.getSegment(pathSegment - 1));
+ }
+ }
+
+ @Override
+ public String getErrorReason(Rule param) {
+ if (pathSegment == ANY_SEGMENT) {
+ return param.getRuleClass() + " rules have to be under a " +
+ StringUtil.joinEnglishList(values, "or", "'") + " directory";
+ } else if (pathSegment == 1) {
+ return param.getRuleClass() + " rules are only allowed in "
+ + StringUtil.joinEnglishList(StringUtil.append(values, "//", ""), "or");
+ } else {
+ return param.getRuleClass() + " rules are only allowed in packages which " +
+ StringUtil.ordinal(pathSegment) + " is " + StringUtil.joinEnglishList(values, "or");
+ }
+ }
+
+ @VisibleForTesting
+ public int getPathSegment() {
+ return pathSegment;
+ }
+
+ @VisibleForTesting
+ public Collection<String> getValues() {
+ return values;
+ }
+ }
+
+ /**
+ * Using this callback function, rules can override their own configuration during the
+ * analysis phase.
+ */
+ public interface Configurator<TConfig, TRule> {
+ TConfig apply(TRule rule, TConfig configuration);
+ }
+
+ /**
+ * A factory or builder class for rule implementations.
+ */
+ public interface ConfiguredTargetFactory<TConfiguredTarget, TContext> {
+ /**
+ * Returns a fully initialized configured target instance using the given context.
+ */
+ TConfiguredTarget create(TContext ruleContext) throws InterruptedException;
+ }
+
+ /**
+ * Default rule configurator, it doesn't change the assigned configuration.
+ */
+ public static final RuleClass.Configurator<Object, Object> NO_CHANGE =
+ new RuleClass.Configurator<Object, Object>() {
+ @Override
+ public Object apply(Object rule, Object configuration) {
+ return configuration;
+ }
+ };
+
+ /**
+ * For Bazel's constraint system: the attribute that declares the set of environments a rule
+ * supports, overriding the defaults for their respective groups.
+ */
+ public static final String RESTRICTED_ENVIRONMENT_ATTR = "restricted_to";
+
+ /**
+ * For Bazel's constraint system: the attribute that declares the set of environments a rule
+ * supports, appending them to the defaults for their respective groups.
+ */
+ public static final String COMPATIBLE_ENVIRONMENT_ATTR = "compatible_with";
+
+ /**
+ * For Bazel's constraint system: the implicit attribute used to store rule class restriction
+ * defaults as specified by {@link Builder#restrictedTo}.
+ */
+ public static final String DEFAULT_RESTRICTED_ENVIRONMENT_ATTR =
+ "$" + RESTRICTED_ENVIRONMENT_ATTR;
+
+ /**
+ * For Bazel's constraint system: the implicit attribute used to store rule class compatibility
+ * defaults as specified by {@link Builder#compatibleWith}.
+ */
+ public static final String DEFAULT_COMPATIBLE_ENVIRONMENT_ATTR =
+ "$" + COMPATIBLE_ENVIRONMENT_ATTR;
+
+ /**
+ * Checks if an attribute is part of the constraint system.
+ */
+ public static boolean isConstraintAttribute(String attr) {
+ return RESTRICTED_ENVIRONMENT_ATTR.equals(attr)
+ || COMPATIBLE_ENVIRONMENT_ATTR.equals(attr)
+ || DEFAULT_RESTRICTED_ENVIRONMENT_ATTR.equals(attr)
+ || DEFAULT_COMPATIBLE_ENVIRONMENT_ATTR.equals(attr);
+ }
+
+ /**
+ * A support class to make it easier to create {@code RuleClass} instances.
+ * This class follows the 'fluent builder' pattern.
+ *
+ * <p>The {@link #addAttribute} method will throw an exception if an attribute
+ * of that name already exists. Use {@link #overrideAttribute} in that case.
+ */
+ public static final class Builder {
+ private static final Pattern RULE_NAME_PATTERN = Pattern.compile("[A-Za-z][A-Za-z0-9_]*");
+
+ /**
+ * The type of the rule class, which determines valid names and required
+ * attributes.
+ */
+ public enum RuleClassType {
+ /**
+ * Abstract rules are intended for rule classes that are just used to
+ * factor out common attributes, and for rule classes that are used only
+ * internally. These rules cannot be instantiated by a BUILD file.
+ *
+ * <p>The rule name must contain a '$' and {@link
+ * TargetUtils#isTestRuleName} must return false for the name.
+ */
+ ABSTRACT {
+ @Override
+ public void checkName(String name) {
+ Preconditions.checkArgument(
+ (name.contains("$") && !TargetUtils.isTestRuleName(name)) || name.equals(""));
+ }
+
+ @Override
+ public void checkAttributes(Map<String, Attribute> attributes) {
+ // No required attributes.
+ }
+ },
+
+ /**
+ * Invisible rule classes should contain a dollar sign so that they cannot be instantiated
+ * by the user. They are different from abstract rules in that they can be instantiated
+ * at will.
+ */
+ INVISIBLE {
+ @Override
+ public void checkName(String name) {
+ Preconditions.checkArgument(name.contains("$"));
+ }
+
+ @Override
+ public void checkAttributes(Map<String, Attribute> attributes) {
+ // No required attributes.
+ }
+ },
+
+ /**
+ * Normal rules are instantiable by BUILD files. Their names must therefore
+ * obey the rules for identifiers in the BUILD language. In addition,
+ * {@link TargetUtils#isTestRuleName} must return false for the name.
+ */
+ NORMAL {
+ @Override
+ public void checkName(String name) {
+ Preconditions.checkArgument(!TargetUtils.isTestRuleName(name)
+ && RULE_NAME_PATTERN.matcher(name).matches(), "Invalid rule name: " + name);
+ }
+
+ @Override
+ public void checkAttributes(Map<String, Attribute> attributes) {
+ for (Attribute attribute : REQUIRED_ATTRIBUTES_FOR_NORMAL_RULES) {
+ Attribute presentAttribute = attributes.get(attribute.getName());
+ Preconditions.checkState(presentAttribute != null,
+ "Missing mandatory '%s' attribute in normal rule class.", attribute.getName());
+ Preconditions.checkState(presentAttribute.getType().equals(attribute.getType()),
+ "Mandatory attribute '%s' in normal rule class has incorrect type (expcected" +
+ " %s).", attribute.getName(), attribute.getType());
+ }
+ }
+ },
+
+ /**
+ * Workspace rules can only be instantiated from a WORKSPACE file. Their names obey the
+ * rule for identifiers.
+ */
+ WORKSPACE {
+ @Override
+ public void checkName(String name) {
+ Preconditions.checkArgument(RULE_NAME_PATTERN.matcher(name).matches());
+ }
+
+ @Override
+ public void checkAttributes(Map<String, Attribute> attributes) {
+ // No required attributes.
+ }
+ },
+
+ /**
+ * Test rules are instantiable by BUILD files and are handled specially
+ * when run with the 'test' command. Their names must obey the rules
+ * for identifiers in the BUILD language and {@link
+ * TargetUtils#isTestRuleName} must return true for the name.
+ *
+ * <p>In addition, test rules must contain certain attributes. See {@link
+ * Builder#REQUIRED_ATTRIBUTES_FOR_TESTS}.
+ */
+ TEST {
+ @Override
+ public void checkName(String name) {
+ Preconditions.checkArgument(TargetUtils.isTestRuleName(name)
+ && RULE_NAME_PATTERN.matcher(name).matches());
+ }
+
+ @Override
+ public void checkAttributes(Map<String, Attribute> attributes) {
+ for (Attribute attribute : REQUIRED_ATTRIBUTES_FOR_TESTS) {
+ Attribute presentAttribute = attributes.get(attribute.getName());
+ Preconditions.checkState(presentAttribute != null,
+ "Missing mandatory '%s' attribute in test rule class.", attribute.getName());
+ Preconditions.checkState(presentAttribute.getType().equals(attribute.getType()),
+ "Mandatory attribute '%s' in test rule class has incorrect type (expcected %s).",
+ attribute.getName(), attribute.getType());
+ }
+ }
+ };
+
+ /**
+ * Checks whether the given name is valid for the current rule class type.
+ *
+ * @throws IllegalArgumentException if the name is not valid
+ */
+ public abstract void checkName(String name);
+
+ /**
+ * Checks whether the given set of attributes contains all the required
+ * attributes for the current rule class type.
+ *
+ * @throws IllegalArgumentException if a required attribute is missing
+ */
+ public abstract void checkAttributes(Map<String, Attribute> attributes);
+ }
+
+ /**
+ * A predicate that filters rule classes based on their names.
+ */
+ public static class RuleClassNamePredicate implements Predicate<RuleClass> {
+
+ private final Set<String> ruleClasses;
+
+ public RuleClassNamePredicate(Iterable<String> ruleClasses) {
+ this.ruleClasses = ImmutableSet.copyOf(ruleClasses);
+ }
+
+ public RuleClassNamePredicate(String... ruleClasses) {
+ this.ruleClasses = ImmutableSet.copyOf(ruleClasses);
+ }
+
+ public RuleClassNamePredicate() {
+ this(ImmutableSet.<String>of());
+ }
+
+ @Override
+ public boolean apply(RuleClass ruleClass) {
+ return ruleClasses.contains(ruleClass.getName());
+ }
+
+ @Override
+ public int hashCode() {
+ return ruleClasses.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ return (o instanceof RuleClassNamePredicate) &&
+ ruleClasses.equals(((RuleClassNamePredicate) o).ruleClasses);
+ }
+
+ @Override
+ public String toString() {
+ return ruleClasses.isEmpty() ? "nothing" : StringUtil.joinEnglishList(ruleClasses);
+ }
+ }
+
+ /**
+ * List of required attributes for normal rules, name and type.
+ */
+ public static final List<Attribute> REQUIRED_ATTRIBUTES_FOR_NORMAL_RULES = ImmutableList.of(
+ attr("tags", Type.STRING_LIST).build()
+ );
+
+ /**
+ * List of required attributes for test rules, name and type.
+ */
+ public static final List<Attribute> REQUIRED_ATTRIBUTES_FOR_TESTS = ImmutableList.of(
+ attr("tags", Type.STRING_LIST).build(),
+ attr("size", Type.STRING).build(),
+ attr("timeout", Type.STRING).build(),
+ attr("flaky", Type.BOOLEAN).build(),
+ attr("shard_count", Type.INTEGER).build(),
+ attr("local", Type.BOOLEAN).build()
+ );
+
+ private String name;
+ private final RuleClassType type;
+ private final boolean skylark;
+ private boolean documented;
+ private boolean publicByDefault = false;
+ private boolean binaryOutput = true;
+ private boolean workspaceOnly = false;
+ private boolean outputsDefaultExecutable = false;
+ private ImplicitOutputsFunction implicitOutputsFunction = ImplicitOutputsFunction.NONE;
+ private Configurator<?, ?> configurator = NO_CHANGE;
+ private ConfiguredTargetFactory<?, ?> configuredTargetFactory = null;
+ private PredicateWithMessage<Rule> validityPredicate =
+ PredicatesWithMessage.<Rule>alwaysTrue();
+ private Predicate<String> preferredDependencyPredicate = Predicates.alwaysFalse();
+ private List<Class<?>> advertisedProviders = new ArrayList<>();
+ private UserDefinedFunction configuredTargetFunction = null;
+ private SkylarkEnvironment ruleDefinitionEnvironment = null;
+ private Set<Class<?>> configurationFragments = new LinkedHashSet<>();
+ private boolean failIfMissingConfigurationFragment;
+
+ private final Map<String, Attribute> attributes = new LinkedHashMap<>();
+
+ /**
+ * Constructs a new {@code RuleClassBuilder} using all attributes from all
+ * parent rule classes. An attribute cannot exist in more than one parent.
+ *
+ * <p>The rule type affects the the allowed names and the required
+ * attributes (see {@link RuleClassType}).
+ *
+ * @throws IllegalArgumentException if an attribute with the same name exists
+ * in more than one parent
+ */
+ public Builder(String name, RuleClassType type, boolean skylark, RuleClass... parents) {
+ this.name = name;
+ this.skylark = skylark;
+ this.type = type;
+ this.documented = type != RuleClassType.ABSTRACT;
+ for (RuleClass parent : parents) {
+ if (parent.getValidityPredicate() != PredicatesWithMessage.<Rule>alwaysTrue()) {
+ setValidityPredicate(parent.getValidityPredicate());
+ }
+ if (parent.preferredDependencyPredicate != Predicates.<String>alwaysFalse()) {
+ setPreferredDependencyPredicate(parent.preferredDependencyPredicate);
+ }
+ configurationFragments.addAll(parent.requiredConfigurationFragments);
+ failIfMissingConfigurationFragment |= parent.failIfMissingConfigurationFragment;
+
+ for (Attribute attribute : parent.getAttributes()) {
+ String attrName = attribute.getName();
+ Preconditions.checkArgument(
+ !attributes.containsKey(attrName) || attributes.get(attrName) == attribute,
+ String.format("Attribute %s is inherited multiple times in %s ruleclass",
+ attrName, name));
+ attributes.put(attrName, attribute);
+ }
+ }
+ // TODO(bazel-team): move this testonly attribute setting to somewhere else
+ // preferably to some base RuleClass implementation.
+ if (this.type.equals(RuleClassType.TEST)) {
+ Attribute.Builder<Boolean> testOnlyAttr = attr("testonly", BOOLEAN).value(true)
+ .nonconfigurable("policy decision: this shouldn't depend on the configuration");
+ if (attributes.containsKey("testonly")) {
+ override(testOnlyAttr);
+ } else {
+ add(testOnlyAttr);
+ }
+ }
+ }
+
+ /**
+ * Checks that required attributes for test rules are present, creates the
+ * {@link RuleClass} object and returns it.
+ *
+ * @throws IllegalStateException if any of the required attributes is missing
+ */
+ public RuleClass build() {
+ return build(name);
+ }
+
+ /**
+ * Same as {@link #build} except with setting the name parameter.
+ */
+ public RuleClass build(String name) {
+ Preconditions.checkArgument(this.name.isEmpty() || this.name.equals(name));
+ type.checkName(name);
+ type.checkAttributes(attributes);
+ boolean skylarkExecutable =
+ skylark && (type == RuleClassType.NORMAL || type == RuleClassType.TEST);
+ Preconditions.checkState(
+ (type == RuleClassType.ABSTRACT)
+ == (configuredTargetFactory == null && configuredTargetFunction == null));
+ Preconditions.checkState(skylarkExecutable == (configuredTargetFunction != null));
+ Preconditions.checkState(skylarkExecutable == (ruleDefinitionEnvironment != null));
+ return new RuleClass(name, skylarkExecutable, documented, publicByDefault, binaryOutput,
+ workspaceOnly, outputsDefaultExecutable, implicitOutputsFunction, configurator,
+ configuredTargetFactory, validityPredicate, preferredDependencyPredicate,
+ ImmutableSet.copyOf(advertisedProviders), configuredTargetFunction,
+ ruleDefinitionEnvironment, configurationFragments, failIfMissingConfigurationFragment,
+ attributes.values().toArray(new Attribute[0]));
+ }
+
+ /**
+ * Declares that the implementation of this rule class requires the given configuration
+ * fragments to be present in the configuration. The value is inherited by subclasses.
+ *
+ * <p>For backwards compatibility, if the set is empty, all fragments may be accessed. But note
+ * that this is only enforced in the {@link com.google.devtools.build.lib.analysis.RuleContext}
+ * class.
+ */
+ public Builder requiresConfigurationFragments(Class<?>... configurationFragment) {
+ Collections.addAll(configurationFragments, configurationFragment);
+ return this;
+ }
+
+ public Builder failIfMissingConfigurationFragment() {
+ this.failIfMissingConfigurationFragment = true;
+ return this;
+ }
+
+ public Builder setUndocumented() {
+ documented = false;
+ return this;
+ }
+
+ public Builder publicByDefault() {
+ publicByDefault = true;
+ return this;
+ }
+
+ public Builder setWorkspaceOnly() {
+ workspaceOnly = true;
+ return this;
+ }
+
+ /**
+ * Determines the outputs of this rule to be created beneath the {@code
+ * genfiles} directory. By default, files are created beneath the {@code bin}
+ * directory.
+ *
+ * <p>This property is not inherited and this method should not be called by
+ * builder of {@link RuleClassType#ABSTRACT} rule class.
+ *
+ * @throws IllegalStateException if called for abstract rule class builder
+ */
+ public Builder setOutputToGenfiles() {
+ Preconditions.checkState(type != RuleClassType.ABSTRACT,
+ "Setting not inherited property (output to genrules) of abstract rule class '%s'", name);
+ this.binaryOutput = false;
+ return this;
+ }
+
+ /**
+ * Sets the implicit outputs function of the rule class. The default implicit
+ * outputs function is {@link ImplicitOutputsFunction#NONE}.
+ *
+ * <p>This property is not inherited and this method should not be called by
+ * builder of {@link RuleClassType#ABSTRACT} rule class.
+ *
+ * @throws IllegalStateException if called for abstract rule class builder
+ */
+ public Builder setImplicitOutputsFunction(
+ ImplicitOutputsFunction implicitOutputsFunction) {
+ Preconditions.checkState(type != RuleClassType.ABSTRACT,
+ "Setting not inherited property (implicit output function) of abstract rule class '%s'",
+ name);
+ this.implicitOutputsFunction = implicitOutputsFunction;
+ return this;
+ }
+
+ public Builder cfg(Configurator<?, ?> configurator) {
+ Preconditions.checkState(type != RuleClassType.ABSTRACT,
+ "Setting not inherited property (cfg) of abstract rule class '%s'", name);
+ this.configurator = configurator;
+ return this;
+ }
+
+ public Builder factory(ConfiguredTargetFactory<?, ?> factory) {
+ this.configuredTargetFactory = factory;
+ return this;
+ }
+
+ public Builder setValidityPredicate(PredicateWithMessage<Rule> predicate) {
+ this.validityPredicate = predicate;
+ return this;
+ }
+
+ public Builder setPreferredDependencyPredicate(Predicate<String> predicate) {
+ this.preferredDependencyPredicate = predicate;
+ return this;
+ }
+
+ /**
+ * State that the rule class being built possibly supplies the specified provider to its direct
+ * dependencies.
+ *
+ * <p>When computing the set of aspects required for a rule, only the providers listed here are
+ * considered. The presence of a provider here does not mean that the rule <b>must</b> implement
+ * said provider, merely that it <b>can</b>. After the configured target is constructed from
+ * this rule, aspects will be filtered according to the set of actual providers.
+ *
+ * <p>This is here so that we can do the loading phase overestimation required for
+ * "blaze query", which does not have the configured targets available.
+ *
+ * <p>It's okay for the rule class eventually not to supply it (possibly based on analysis phase
+ * logic), but if a provider is not advertised but is supplied, aspects that require the it will
+ * not be evaluated for the rule.
+ */
+ public Builder advertiseProvider(Class<?>... providers) {
+ Collections.addAll(advertisedProviders, providers);
+ return this;
+ }
+
+ private void addAttribute(Attribute attribute) {
+ Preconditions.checkState(!attributes.containsKey(attribute.getName()),
+ "An attribute with the name '%s' already exists.", attribute.getName());
+ attributes.put(attribute.getName(), attribute);
+ }
+
+ private void overrideAttribute(Attribute attribute) {
+ String attrName = attribute.getName();
+ Preconditions.checkState(attributes.containsKey(attrName),
+ "No such attribute '%s' to override in ruleclass '%s'.", attrName, name);
+ Type<?> origType = attributes.get(attrName).getType();
+ Type<?> newType = attribute.getType();
+ Preconditions.checkState(origType.equals(newType),
+ "The type of the new attribute '%s' is different from the original one '%s'.",
+ newType, origType);
+ attributes.put(attrName, attribute);
+ }
+
+ /**
+ * Builds attribute from the attribute builder and adds it to this rule
+ * class.
+ *
+ * @param attr attribute builder
+ */
+ public <TYPE> Builder add(Attribute.Builder<TYPE> attr) {
+ addAttribute(attr.build());
+ return this;
+ }
+
+ /**
+ * Builds attribute from the attribute builder and overrides the attribute
+ * with the same name.
+ *
+ * @throws IllegalArgumentException if the attribute does not override one of the same name
+ */
+ public <TYPE> Builder override(Attribute.Builder<TYPE> attr) {
+ overrideAttribute(attr.build());
+ return this;
+ }
+
+ /**
+ * Adds or overrides the attribute in the rule class. Meant for Skylark usage.
+ */
+ public void addOrOverrideAttribute(Attribute attribute) {
+ if (attributes.containsKey(attribute.getName())) {
+ overrideAttribute(attribute);
+ } else {
+ addAttribute(attribute);
+ }
+ }
+
+ /**
+ * Sets the rule implementation function. Meant for Skylark usage.
+ */
+ public Builder setConfiguredTargetFunction(UserDefinedFunction func) {
+ this.configuredTargetFunction = func;
+ return this;
+ }
+
+ /**
+ * Sets the rule definition environment. Meant for Skylark usage.
+ */
+ public Builder setRuleDefinitionEnvironment(SkylarkEnvironment env) {
+ this.ruleDefinitionEnvironment = env;
+ return this;
+ }
+
+ /**
+ * Removes an attribute with the same name from this rule class.
+ *
+ * @throws IllegalArgumentException if the attribute with this name does
+ * not exist
+ */
+ public <TYPE> Builder removeAttribute(String name) {
+ Preconditions.checkState(attributes.containsKey(name), "No such attribute '%s' to remove.",
+ name);
+ attributes.remove(name);
+ return this;
+ }
+
+ /**
+ * This rule class outputs a default executable for every rule with the same name as
+ * the rules's. Only works for Skylark.
+ */
+ public <TYPE> Builder setOutputsDefaultExecutable() {
+ this.outputsDefaultExecutable = true;
+ return this;
+ }
+
+ /**
+ * Declares that instances of this rule are compatible with the specified environments,
+ * in addition to the defaults declared by their environment groups. This can be overridden
+ * by rule-specific declarations. See
+ * {@link com.google.devtools.build.lib.analysis.constraints.ConstraintSemantics} for details.
+ */
+ public <TYPE> Builder compatibleWith(Label... environments) {
+ add(attr(DEFAULT_COMPATIBLE_ENVIRONMENT_ATTR, LABEL_LIST).cfg(HOST)
+ .value(ImmutableList.copyOf(environments)));
+ return this;
+ }
+
+ /**
+ * Declares that instances of this rule are restricted to the specified environments, i.e.
+ * these override the defaults declared by their environment groups. This can be overridden
+ * by rule-specific declarations. See
+ * {@link com.google.devtools.build.lib.analysis.constraints.ConstraintSemantics} for details.
+ *
+ * <p>The input list cannot be empty.
+ */
+ public <TYPE> Builder restrictedTo(Label firstEnvironment, Label... otherEnvironments) {
+ ImmutableList<Label> environments = ImmutableList.<Label>builder().add(firstEnvironment)
+ .add(otherEnvironments).build();
+ add(attr(DEFAULT_RESTRICTED_ENVIRONMENT_ATTR, LABEL_LIST).cfg(HOST).value(environments));
+ return this;
+
+ }
+
+ /**
+ * Returns an Attribute.Builder object which contains a replica of the
+ * same attribute in the parent rule if exists.
+ *
+ * @param name the name of the attribute
+ */
+ public Attribute.Builder<?> copy(String name) {
+ Preconditions.checkArgument(attributes.containsKey(name),
+ "Attribute %s does not exist in parent rule class.", name);
+ return attributes.get(name).cloneBuilder();
+ }
+ }
+
+ private final String name; // e.g. "cc_library"
+
+ /**
+ * The kind of target represented by this RuleClass (e.g. "cc_library rule").
+ * Note: Even though there is partial duplication with the {@link RuleClass#name} field,
+ * we want to store this as a separate field instead of generating it on demand in order to
+ * avoid string duplication.
+ */
+ private final String targetKind;
+
+ private final boolean skylarkExecutable;
+ private final boolean documented;
+ private final boolean publicByDefault;
+ private final boolean binaryOutput;
+ private final boolean workspaceOnly;
+ private final boolean outputsDefaultExecutable;
+
+ /**
+ * A (unordered) mapping from attribute names to small integers indexing into
+ * the {@code attributes} array.
+ */
+ private final Map<String, Integer> attributeIndex = new HashMap<>();
+
+ /**
+ * All attributes of this rule class (including inherited ones) ordered by
+ * attributeIndex value.
+ */
+ private final Attribute[] attributes;
+
+ /**
+ * The set of implicit outputs generated by a rule, expressed as a function
+ * of that rule.
+ */
+ private final ImplicitOutputsFunction implicitOutputsFunction;
+
+ /**
+ * The set of implicit outputs generated by a rule, expressed as a function
+ * of that rule.
+ */
+ private final Configurator<?, ?> configurator;
+
+ /**
+ * The factory that creates configured targets from this rule.
+ */
+ private final ConfiguredTargetFactory<?, ?> configuredTargetFactory;
+
+ /**
+ * The constraint the package name of the rule instance must fulfill
+ */
+ private final PredicateWithMessage<Rule> validityPredicate;
+
+ /**
+ * See {@link #isPreferredDependency}.
+ */
+ private final Predicate<String> preferredDependencyPredicate;
+
+ /**
+ * The list of transitive info providers this class advertises to aspects.
+ */
+ private final ImmutableSet<Class<?>> advertisedProviders;
+
+ /**
+ * The Skylark rule implementation of this RuleClass. Null for non Skylark executable RuleClasses.
+ */
+ @Nullable private final UserDefinedFunction configuredTargetFunction;
+
+ /**
+ * The Skylark rule definition environment of this RuleClass.
+ * Null for non Skylark executable RuleClasses.
+ */
+ @Nullable private final SkylarkEnvironment ruleDefinitionEnvironment;
+
+ /**
+ * The set of required configuration fragments; this should list all fragments that can be
+ * accessed by the rule implementation. If empty, all fragments are allowed to be accessed for
+ * backwards compatibility.
+ */
+ private final ImmutableSet<Class<?>> requiredConfigurationFragments;
+
+ /**
+ * Whether to fail during analysis if a configuration fragment is missing. The default behavior is
+ * to create fail actions for all declared outputs, i.e., to fail during execution, if any of the
+ * outputs is actually attempted to be built.
+ */
+ private final boolean failIfMissingConfigurationFragment;
+
+ /**
+ * Constructs an instance of RuleClass whose name is 'name', attributes
+ * are 'attributes'. The {@code srcsAllowedFiles} determines which types of
+ * files are allowed as parameters to the "srcs" attribute; rules are always
+ * allowed. For the "deps" attribute, there are four cases:
+ * <ul>
+ * <li>if the parameter is a file, it is allowed if its file type is given
+ * in {@code depsAllowedFiles},
+ * <li>if the parameter is a rule and the rule class is accepted by
+ * {@code depsAllowedRules}, then it is allowed,
+ * <li>if the parameter is a rule and the rule class is not accepted by
+ * {@code depsAllowedRules}, but accepted by
+ * {@code depsAllowedRulesWithWarning}, then it is allowed, but
+ * triggers a warning;
+ * <li>all other parameters trigger an error.
+ * </ul>
+ *
+ * <p>The {@code depsAllowedRules} predicate should have a {@code toString}
+ * method which returns a plain English enumeration of the allowed rule class
+ * names, if it does not allow all rule classes.
+ * @param workspaceOnly
+ */
+ @VisibleForTesting
+ RuleClass(String name,
+ boolean skylarkExecutable, boolean documented, boolean publicByDefault,
+ boolean binaryOutput, boolean workspaceOnly, boolean outputsDefaultExecutable,
+ ImplicitOutputsFunction implicitOutputsFunction,
+ Configurator<?, ?> configurator,
+ ConfiguredTargetFactory<?, ?> configuredTargetFactory,
+ PredicateWithMessage<Rule> validityPredicate, Predicate<String> preferredDependencyPredicate,
+ ImmutableSet<Class<?>> advertisedProviders,
+ @Nullable UserDefinedFunction configuredTargetFunction,
+ @Nullable SkylarkEnvironment ruleDefinitionEnvironment,
+ Set<Class<?>> allowedConfigurationFragments, boolean failIfMissingConfigurationFragment,
+ Attribute... attributes) {
+ this.name = name;
+ this.targetKind = name + " rule";
+ this.skylarkExecutable = skylarkExecutable;
+ this.documented = documented;
+ this.publicByDefault = publicByDefault;
+ this.binaryOutput = binaryOutput;
+ this.implicitOutputsFunction = implicitOutputsFunction;
+ this.configurator = Preconditions.checkNotNull(configurator);
+ this.configuredTargetFactory = configuredTargetFactory;
+ this.validityPredicate = validityPredicate;
+ this.preferredDependencyPredicate = preferredDependencyPredicate;
+ this.advertisedProviders = advertisedProviders;
+ this.configuredTargetFunction = configuredTargetFunction;
+ this.ruleDefinitionEnvironment = ruleDefinitionEnvironment;
+ // Do not make a defensive copy as builder does that already
+ this.attributes = attributes;
+ this.workspaceOnly = workspaceOnly;
+ this.outputsDefaultExecutable = outputsDefaultExecutable;
+ this.requiredConfigurationFragments = ImmutableSet.copyOf(allowedConfigurationFragments);
+ this.failIfMissingConfigurationFragment = failIfMissingConfigurationFragment;
+
+ // create the index:
+ int index = 0;
+ for (Attribute attribute : attributes) {
+ attributeIndex.put(attribute.getName(), index++);
+ }
+ }
+
+ /**
+ * Returns the function which determines the set of implicit outputs
+ * generated by a given rule.
+ *
+ * <p>An implicit output is an OutputFile that automatically comes into
+ * existence when a rule of this class is declared, and whose name is derived
+ * from the name of the rule.
+ *
+ * <p>Implicit outputs are a widely-relied upon. All ".so",
+ * and "_deploy.jar" targets referenced in BUILD files are examples.
+ */
+ @VisibleForTesting
+ public ImplicitOutputsFunction getImplicitOutputsFunction() {
+ return implicitOutputsFunction;
+ }
+
+ @SuppressWarnings("unchecked")
+ public <C, R> Configurator<C, R> getConfigurator() {
+ return (Configurator<C, R>) configurator;
+ }
+
+ @SuppressWarnings("unchecked")
+ public <CT, RC> ConfiguredTargetFactory<CT, RC> getConfiguredTargetFactory() {
+ return (ConfiguredTargetFactory<CT, RC>) configuredTargetFactory;
+ }
+
+ /**
+ * Returns the class of rule that this RuleClass represents (e.g. "cc_library").
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns the target kind of this class of rule (e.g. "cc_library rule").
+ */
+ String getTargetKind() {
+ return targetKind;
+ }
+
+ public boolean getWorkspaceOnly() {
+ return workspaceOnly;
+ }
+
+ /**
+ * Returns true iff the attribute 'attrName' is defined for this rule class,
+ * and has type 'type'.
+ */
+ public boolean hasAttr(String attrName, Type<?> type) {
+ Integer index = getAttributeIndex(attrName);
+ return index != null && getAttribute(index).getType() == type;
+ }
+
+ /**
+ * Returns the index of the specified attribute name. Use of indices allows
+ * space-efficient storage of attribute values in rules, since hashtables are
+ * not required. (The index mapping is specific to each RuleClass and an
+ * attribute may have a different index in the parent RuleClass.)
+ *
+ * <p>Returns null if the named attribute is not defined for this class of Rule.
+ */
+ Integer getAttributeIndex(String attrName) {
+ return attributeIndex.get(attrName);
+ }
+
+ /**
+ * Returns the attribute whose index is 'attrIndex'. Fails if attrIndex is
+ * not in range.
+ */
+ Attribute getAttribute(int attrIndex) {
+ return attributes[attrIndex];
+ }
+
+ /**
+ * Returns the attribute whose name is 'attrName'; fails if not found.
+ */
+ public Attribute getAttributeByName(String attrName) {
+ return attributes[getAttributeIndex(attrName)];
+ }
+
+ /**
+ * Returns the attribute whose name is {@code attrName}, or null if not
+ * found.
+ */
+ Attribute getAttributeByNameMaybe(String attrName) {
+ Integer i = getAttributeIndex(attrName);
+ return i == null ? null : attributes[i];
+ }
+
+ /**
+ * Returns the number of attributes defined for this rule class.
+ */
+ public int getAttributeCount() {
+ return attributeIndex.size();
+ }
+
+ /**
+ * Returns an (immutable) list of all Attributes defined for this class of
+ * rule, ordered by increasing index.
+ */
+ public List<Attribute> getAttributes() {
+ return ImmutableList.copyOf(attributes);
+ }
+
+ public PredicateWithMessage<Rule> getValidityPredicate() {
+ return validityPredicate;
+ }
+
+ /**
+ * Returns the set of advertised transitive info providers.
+ *
+ * <p>When computing the set of aspects required for a rule, only the providers listed here are
+ * considered. The presence of a provider here does not mean that the rule <b>must</b> implement
+ * said provider, merely that it <b>can</b>. After the configured target is constructed from this
+ * rule, aspects will be filtered according to the set of actual providers.
+ *
+ * <p>This is here so that we can do the loading phase overestimation required for "blaze query",
+ * which does not have the configured targets available.
+ *
+ * <p>This should in theory only contain subclasses of
+ * {@link com.google.devtools.build.lib.analysis.TransitiveInfoProvider}, but our current dependency
+ * structure does not allow a reference to that class here.
+ */
+ public ImmutableSet<Class<?>> getAdvertisedProviders() {
+ return advertisedProviders;
+ }
+
+ /**
+ * For --compile_one_dependency: if multiple rules consume the specified target,
+ * should we choose this one over the "unpreferred" options?
+ */
+ public boolean isPreferredDependency(String filename) {
+ return preferredDependencyPredicate.apply(filename);
+ }
+
+ /**
+ * The set of required configuration fragments; this contains all fragments that can be
+ * accessed by the rule implementation. If empty, all fragments are allowed to be accessed for
+ * backwards compatibility.
+ */
+ public Set<Class<?>> getRequiredConfigurationFragments() {
+ return requiredConfigurationFragments;
+ }
+
+ /**
+ * Checks if the configuration fragment may be accessed (i.e., if it's declared). If no fragments
+ * are declared, this allows access to all fragments for backwards compatibility.
+ */
+ public boolean isLegalConfigurationFragment(Class<?> configurationFragment) {
+ // For now, we allow all rules that don't declare allowed fragments to access any fragment.
+ // TODO(bazel-team): Declare fragment dependencies for all rules and remove this.
+ if (requiredConfigurationFragments.isEmpty()) {
+ return true;
+ }
+ return requiredConfigurationFragments.contains(configurationFragment);
+ }
+
+ /**
+ * Whether to fail analysis if any of the required configuration fragments are missing.
+ */
+ public boolean failIfMissingConfigurationFragment() {
+ return failIfMissingConfigurationFragment;
+ }
+
+ /**
+ * Helper function for {@link RuleFactory#createRule}.
+ */
+ Rule createRuleWithLabel(Package.AbstractBuilder<?, ?> pkgBuilder, Label ruleLabel,
+ Map<String, Object> attributeValues, EventHandler eventHandler, FuncallExpression ast,
+ Location location) throws SyntaxException {
+ Rule rule = pkgBuilder.newRuleWithLabel(ruleLabel, this, null, location);
+ createRuleCommon(rule, pkgBuilder, attributeValues, eventHandler, ast);
+ return rule;
+ }
+
+ private void createRuleCommon(Rule rule, Package.AbstractBuilder<?, ?> pkgBuilder,
+ Map<String, Object> attributeValues, EventHandler eventHandler, FuncallExpression ast)
+ throws SyntaxException {
+ populateRuleAttributeValues(
+ rule, pkgBuilder, attributeValues, eventHandler, ast);
+ rule.populateOutputFiles(eventHandler, pkgBuilder);
+ rule.checkForNullLabels();
+ rule.checkValidityPredicate(eventHandler);
+ }
+
+ static class ParsedAttributeValue {
+ private final boolean explicitlySpecified;
+ private final Object value;
+ private final Location location;
+
+ ParsedAttributeValue(boolean explicitlySpecified, Object value, Location location) {
+ this.explicitlySpecified = explicitlySpecified;
+ this.value = value;
+ this.location = location;
+ }
+
+ public boolean getExplicitlySpecified() {
+ return explicitlySpecified;
+ }
+
+ public Object getValue() {
+ return value;
+ }
+
+ public Location getLocation() {
+ return location;
+ }
+ }
+
+ /**
+ * Creates a rule with the attribute values that are already parsed.
+ *
+ * <p><b>WARNING:</b> This assumes that the attribute values here have the right type and
+ * bypasses some sanity checks. If they are of the wrong type, everything will come down burning.
+ */
+ @SuppressWarnings("unchecked")
+ Rule createRuleWithParsedAttributeValues(Label label,
+ Package.AbstractBuilder<?, ?> pkgBuilder, Location ruleLocation,
+ Map<String, ParsedAttributeValue> attributeValues, EventHandler eventHandler)
+ throws SyntaxException{
+ Rule rule = pkgBuilder.newRuleWithLabel(label, this, null, ruleLocation);
+ rule.checkValidityPredicate(eventHandler);
+
+ for (Attribute attribute : rule.getRuleClassObject().getAttributes()) {
+ ParsedAttributeValue value = attributeValues.get(attribute.getName());
+ if (attribute.isMandatory()) {
+ Preconditions.checkState(value != null);
+ }
+
+ if (value == null) {
+ continue;
+ }
+
+ checkAllowedValues(rule, attribute, value.getValue(), eventHandler);
+ rule.setAttributeValue(attribute, value.getValue(), value.getExplicitlySpecified());
+ rule.setAttributeLocation(attribute, value.getLocation());
+
+ if (attribute.getName().equals("visibility")) {
+ // TODO(bazel-team): Verify that this cast works
+ rule.setVisibility(PackageFactory.getVisibility((List<Label>) value.getValue()));
+ }
+ }
+
+ rule.populateOutputFiles(eventHandler, pkgBuilder);
+ Preconditions.checkState(!rule.containsErrors());
+ return rule;
+ }
+
+ /**
+ * Populates the attributes table of new rule "rule" from the
+ * "attributeValues" mapping from attribute names to values in the build
+ * language. Errors are reported on "reporter". "ast" is used to associate
+ * location information with each rule attribute.
+ */
+ private void populateRuleAttributeValues(Rule rule,
+ Package.AbstractBuilder<?, ?> pkgBuilder,
+ Map<String, Object> attributeValues,
+ EventHandler eventHandler,
+ FuncallExpression ast) {
+ BitSet definedAttrs = new BitSet(); // set of attr indices
+
+ for (Map.Entry<String, Object> entry : attributeValues.entrySet()) {
+ String attributeName = entry.getKey();
+ Object attributeValue = entry.getValue();
+ if (attributeValue == Environment.NONE) { // Ignore all None values.
+ continue;
+ }
+ Integer attrIndex = setRuleAttributeValue(rule, eventHandler, attributeName, attributeValue);
+ if (attrIndex != null) {
+ definedAttrs.set(attrIndex);
+ checkAttrValNonEmpty(rule, eventHandler, attributeValue, attrIndex);
+ }
+ }
+
+ // Save the location of each non-default attribute definition:
+ if (ast != null) {
+ for (Argument arg : ast.getArguments()) {
+ Ident keyword = arg.getName();
+ if (keyword != null) {
+ String name = keyword.getName();
+ Integer attrIndex = getAttributeIndex(name);
+ if (attrIndex != null) {
+ rule.setAttributeLocation(attrIndex, arg.getValue().getLocation());
+ }
+ }
+ }
+ }
+
+ List<Attribute> attrsWithComputedDefaults = new ArrayList<>();
+
+ // Set defaults; ensure that every mandatory attribute has a value. Use
+ // the default if none is specified.
+ int numAttributes = getAttributeCount();
+ for (int attrIndex = 0; attrIndex < numAttributes; ++attrIndex) {
+ if (!definedAttrs.get(attrIndex)) {
+ Attribute attr = getAttribute(attrIndex);
+ if (attr.isMandatory()) {
+ rule.reportError(rule.getLabel() + ": missing value for mandatory "
+ + "attribute '" + attr.getName() + "' in '"
+ + name + "' rule", eventHandler);
+ }
+
+ if (attr.hasComputedDefault()) {
+ attrsWithComputedDefaults.add(attr);
+ } else {
+ Object defaultValue = getAttributeNoncomputedDefaultValue(attr, pkgBuilder);
+ checkAttrValNonEmpty(rule, eventHandler, defaultValue, attrIndex);
+ checkAllowedValues(rule, attr, defaultValue, eventHandler);
+ rule.setAttributeValue(attr, defaultValue, /*explicit=*/false);
+ }
+ }
+ }
+
+ // Evaluate and set any computed defaults now that all non-computed
+ // TODO(bazel-team): remove this special casing. Thanks to configurable attributes refactoring,
+ // computed defaults don't get bound to their final values at this point, so we no longer
+ // have to wait until regular attributes have been initialized.
+ for (Attribute attr : attrsWithComputedDefaults) {
+ rule.setAttributeValue(attr, attr.getDefaultValue(rule), /*explicit=*/false);
+ }
+
+ // Now that all attributes are bound to values, collect and store configurable attribute keys.
+ populateConfigDependenciesAttribute(rule);
+ checkForDuplicateLabels(rule, eventHandler);
+ checkThirdPartyRuleHasLicense(rule, pkgBuilder, eventHandler);
+ checkForValidSizeAndTimeoutValues(rule, eventHandler);
+ }
+
+ /**
+ * Collects all labels used as keys for configurable attributes and places them into
+ * the special implicit attribute that tracks them.
+ */
+ private static void populateConfigDependenciesAttribute(Rule rule) {
+ RawAttributeMapper attributes = RawAttributeMapper.of(rule);
+ Attribute configDepsAttribute = attributes.getAttributeDefinition("$config_dependencies");
+ if (configDepsAttribute == null) {
+ // Not currently compatible with Skylark rules.
+ return;
+ }
+
+ Set<Label> configLabels = new LinkedHashSet<>();
+ for (Attribute attr : rule.getAttributes()) {
+ Type.Selector<?> selector = attributes.getSelector(attr.getName(), attr.getType());
+ if (selector != null) {
+ for (Label label : selector.getEntries().keySet()) {
+ if (!Type.Selector.isReservedLabel(label)) {
+ configLabels.add(label);
+ }
+ }
+ }
+ }
+
+ rule.setAttributeValue(configDepsAttribute, ImmutableList.copyOf(configLabels),
+ /*explicit=*/false);
+ }
+
+ private void checkAttrValNonEmpty(
+ Rule rule, EventHandler eventHandler, Object attributeValue, Integer attrIndex) {
+ if (attributeValue instanceof List<?>) {
+ Attribute attr = getAttribute(attrIndex);
+ if (attr.isNonEmpty() && ((List<?>) attributeValue).isEmpty()) {
+ rule.reportError(rule.getLabel() + ": non empty " + "attribute '" + attr.getName()
+ + "' in '" + name + "' rule '" + rule.getLabel() + "' has to have at least one value",
+ eventHandler);
+ }
+ }
+ }
+
+ /**
+ * Report an error for each label that appears more than once in a LABEL_LIST attribute
+ * of the given rule.
+ *
+ * @param rule The rule.
+ * @param eventHandler The eventHandler to use to report the duplicated deps.
+ */
+ private static void checkForDuplicateLabels(Rule rule, EventHandler eventHandler) {
+ for (Attribute attribute : rule.getAttributes()) {
+ if (attribute.getType() == Type.LABEL_LIST) {
+ checkForDuplicateLabels(rule, attribute, eventHandler);
+ }
+ }
+ }
+
+ /**
+ * Reports an error against the specified rule if it's beneath third_party
+ * but does not have a declared license.
+ */
+ private static void checkThirdPartyRuleHasLicense(Rule rule,
+ Package.AbstractBuilder<?, ?> pkgBuilder, EventHandler eventHandler) {
+ if (rule.getLabel().getPackageName().startsWith("third_party/")) {
+ License license = rule.getLicense();
+ if (license == null) {
+ license = pkgBuilder.getDefaultLicense();
+ }
+ if (license == License.NO_LICENSE) {
+ rule.reportError("third-party rule '" + rule.getLabel() + "' lacks a license declaration "
+ + "with one of the following types: notice, reciprocal, permissive, "
+ + "restricted, unencumbered, by_exception_only",
+ eventHandler);
+ }
+ }
+ }
+
+ /**
+ * Report an error for each label that appears more than once in the given attribute
+ * of the given rule.
+ *
+ * @param rule The rule.
+ * @param attribute The attribute to check. Must exist in rule and be of type LABEL_LIST.
+ * @param eventHandler The eventHandler to use to report the duplicated deps.
+ */
+ private static void checkForDuplicateLabels(Rule rule, Attribute attribute,
+ EventHandler eventHandler) {
+ final String attrName = attribute.getName();
+ // This attribute may be selectable, so iterate over each selection possibility in turn.
+ // TODO(bazel-team): merge '*' condition into all lists when implemented.
+ AggregatingAttributeMapper attributeMap = AggregatingAttributeMapper.of(rule);
+ for (List<Label> labels : attributeMap.visitAttribute(attrName, Type.LABEL_LIST)) {
+ if (!labels.isEmpty()) {
+ Set<Label> duplicates = CollectionUtils.duplicatedElementsOf(labels);
+ for (Label label : duplicates) {
+ rule.reportError(
+ String.format("Label '%s' is duplicated in the '%s' attribute of rule '%s'",
+ label, attrName, rule.getName()), eventHandler);
+ }
+ }
+ }
+ }
+
+ /**
+ * Report an error if the rule has a timeout or size attribute that is not a
+ * legal value. These attributes appear on all tests.
+ *
+ * @param rule the rule to check
+ * @param eventHandler the eventHandler to use to report the duplicated deps
+ */
+ private static void checkForValidSizeAndTimeoutValues(Rule rule, EventHandler eventHandler) {
+ if (rule.getRuleClassObject().hasAttr("size", Type.STRING)) {
+ String size = NonconfigurableAttributeMapper.of(rule).get("size", Type.STRING);
+ if (TestSize.getTestSize(size) == null) {
+ rule.reportError(
+ String.format("In rule '%s', size '%s' is not a valid size.", rule.getName(), size),
+ eventHandler);
+ }
+ }
+ if (rule.getRuleClassObject().hasAttr("timeout", Type.STRING)) {
+ String timeout = NonconfigurableAttributeMapper.of(rule).get("timeout", Type.STRING);
+ if (TestTimeout.getTestTimeout(timeout) == null) {
+ rule.reportError(
+ String.format(
+ "In rule '%s', timeout '%s' is not a valid timeout.", rule.getName(), timeout),
+ eventHandler);
+ }
+ }
+ }
+
+ /**
+ * Returns the default value for the specified rule attribute.
+ *
+ * For most rule attributes, the default value is either explicitly specified
+ * in the attribute, or implicitly based on the type of the attribute, except
+ * for some special cases (e.g. "licenses", "distribs") where it comes from
+ * some other source, such as state in the package.
+ *
+ * Precondition: {@code !attr.hasComputedDefault()}. (Computed defaults are
+ * evaluated in second pass.)
+ */
+ private static Object getAttributeNoncomputedDefaultValue(Attribute attr,
+ Package.AbstractBuilder<?, ?> pkgBuilder) {
+ if (attr.getName().equals("licenses")) {
+ return pkgBuilder.getDefaultLicense();
+ }
+ if (attr.getName().equals("distribs")) {
+ return pkgBuilder.getDefaultDistribs();
+ }
+ return attr.getDefaultValue(null);
+ }
+
+ /**
+ * Sets the value of attribute "attrName" in rule "rule", by converting the
+ * build-language value "attrVal" to the appropriate type for the attribute.
+ * Returns the attribute index iff successful, null otherwise.
+ *
+ * <p>In case of failure, error messages are reported on "handler", and "rule"
+ * is marked as containing errors.
+ */
+ @SuppressWarnings("unchecked")
+ private Integer setRuleAttributeValue(Rule rule,
+ EventHandler eventHandler,
+ String attrName,
+ Object attrVal) {
+ if (attrName.equals("name")) {
+ return null; // "name" is handled specially
+ }
+
+ Integer attrIndex = getAttributeIndex(attrName);
+ if (attrIndex == null) {
+ rule.reportError(rule.getLabel() + ": no such attribute '" + attrName +
+ "' in '" + name + "' rule", eventHandler);
+ return null;
+ }
+
+ Attribute attr = getAttribute(attrIndex);
+ Object converted;
+ try {
+ String what = "attribute '" + attrName + "' in '" + name + "' rule";
+ converted = attr.getType().selectableConvert(attrVal, what, rule.getLabel());
+
+ if ((converted instanceof Type.Selector<?>) && !attr.isConfigurable()) {
+ rule.reportError(rule.getLabel() + ": attribute \"" + attr.getName()
+ + "\" is not configurable", eventHandler);
+ return null;
+ }
+
+ if ((converted instanceof List<?>) && !(converted instanceof GlobList<?>)) {
+ if (attr.isOrderIndependent()) {
+ converted = Ordering.natural().sortedCopy((List<? extends Comparable<?>>) converted);
+ }
+ converted = ImmutableList.copyOf((List<?>) converted);
+ }
+ } catch (Type.ConversionException e) {
+ rule.reportError(rule.getLabel() + ": " + e.getMessage(), eventHandler);
+ return null;
+ }
+
+ if (attrName.equals("visibility")) {
+ List<Label> attrList = (List<Label>) converted;
+ if (!attrList.isEmpty() &&
+ ConstantRuleVisibility.LEGACY_PUBLIC_LABEL.equals(attrList.get(0))) {
+ rule.reportError(rule.getLabel() + ": //visibility:legacy_public only allowed in package "
+ + "declaration", eventHandler);
+ }
+ rule.setVisibility(PackageFactory.getVisibility(attrList));
+ }
+
+ checkAllowedValues(rule, attr, converted, eventHandler);
+ rule.setAttributeValue(attr, converted, /*explicit=*/true);
+ return attrIndex;
+ }
+
+ private void checkAllowedValues(Rule rule, Attribute attribute, Object value,
+ EventHandler eventHandler) {
+ if (attribute.checkAllowedValues()) {
+ PredicateWithMessage<Object> allowedValues = attribute.getAllowedValues();
+ if (!allowedValues.apply(value)) {
+ rule.reportError(String.format(rule.getLabel() + ": invalid value in '%s' attribute: %s",
+ attribute.getName(),
+ allowedValues.getErrorReason(value)), eventHandler);
+ }
+ }
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+
+ public boolean isDocumented() {
+ return documented;
+ }
+
+ public boolean isPublicByDefault() {
+ return publicByDefault;
+ }
+
+ /**
+ * Returns true iff the outputs of this rule should be created beneath the
+ * <i>bin</i> directory, false if beneath <i>genfiles</i>. For most rule
+ * classes, this is a constant, but for genrule, it is a property of the
+ * individual rule instance, derived from the 'output_to_bindir' attribute;
+ * see Rule.hasBinaryOutput().
+ */
+ boolean hasBinaryOutput() {
+ return binaryOutput;
+ }
+
+ /**
+ * Returns this RuleClass's custom Skylark rule implementation.
+ */
+ @Nullable public UserDefinedFunction getConfiguredTargetFunction() {
+ return configuredTargetFunction;
+ }
+
+ /**
+ * Returns this RuleClass's rule definition environment.
+ */
+ @Nullable public SkylarkEnvironment getRuleDefinitionEnvironment() {
+ return ruleDefinitionEnvironment;
+ }
+
+ /**
+ * Returns true if this RuleClass is an executable Skylark RuleClass (i.e. it is
+ * Skylark and Normal or Test RuleClass).
+ */
+ public boolean isSkylarkExecutable() {
+ return skylarkExecutable;
+ }
+
+ /**
+ * Returns true if this rule class outputs a default executable for every rule.
+ */
+ public boolean outputsDefaultExecutable() {
+ return outputsDefaultExecutable;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/RuleClassProvider.java b/src/main/java/com/google/devtools/build/lib/packages/RuleClassProvider.java
new file mode 100644
index 0000000..90fdfca
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/RuleClassProvider.java
@@ -0,0 +1,49 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.syntax.SkylarkEnvironment;
+import com.google.devtools.build.lib.syntax.ValidationEnvironment;
+
+import java.util.Map;
+
+/**
+ * The collection of the supported build rules. Provides an Environment for Skylark rule creation.
+ */
+public interface RuleClassProvider {
+ /**
+ * Returns a map from rule names to rule class objects.
+ */
+ Map<String, RuleClass> getRuleClassMap();
+
+ /**
+ * Returns a new Skylark Environment instance for rule creation. Implementations need to be
+ * thread safe.
+ */
+ SkylarkEnvironment createSkylarkRuleClassEnvironment(
+ EventHandler eventHandler, String astFileContentHashCode);
+
+ /**
+ * Returns a validation environment for static analysis of skylark files.
+ * The environment has to contain all built-in functions and objects.
+ */
+ ValidationEnvironment getSkylarkValidationEnvironment();
+
+ /**
+ * Returns the Skylark module to register the native rules with.
+ */
+ Object getNativeModule();
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/RuleErrorConsumer.java b/src/main/java/com/google/devtools/build/lib/packages/RuleErrorConsumer.java
new file mode 100644
index 0000000..84d00c0
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/RuleErrorConsumer.java
@@ -0,0 +1,47 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+/**
+ * A thin interface exposing only the warning and error reporting functionality
+ * of a rule.
+ *
+ * <p>When a class or a method needs only this functionality but not the whole
+ * {@code RuleConfiguredTarget}, it can use this thin interface instead.
+ *
+ * <p>This interface should only be implemented by {@code RuleConfiguredTarget}
+ * and its subclasses.
+ */
+public interface RuleErrorConsumer {
+ /**
+ * Consume a non-attribute-specific warning in a rule.
+ */
+ void ruleWarning(String message);
+
+ /**
+ * Consume a non-attribute-specific error in a rule.
+ */
+ void ruleError(String message);
+
+ /**
+ * Consume an attribute-specific warning in a rule.
+ */
+ void attributeWarning(String attrName, String message);
+
+ /**
+ * Consume an attribute-specific error in a rule.
+ */
+ void attributeError(String attrName, String message);
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/RuleFactory.java b/src/main/java/com/google/devtools/build/lib/packages/RuleFactory.java
new file mode 100644
index 0000000..c79bbaa
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/RuleFactory.java
@@ -0,0 +1,145 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.packages.Package.NameConflictException;
+import com.google.devtools.build.lib.packages.PackageFactory.PackageContext;
+import com.google.devtools.build.lib.syntax.FuncallExpression;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.syntax.Label.SyntaxException;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Given a rule class and a set of attributes, returns a Rule instance. Also
+ * performs a number of checks and associates the rule and the owning package
+ * with each other.
+ *
+ * <p>Note: the code that actually populates the RuleClass map has been moved
+ * to {@link RuleClassProvider}.
+ */
+public class RuleFactory {
+
+ /**
+ * Maps rule class name to the metaclass instance for that rule.
+ */
+ private final ImmutableMap<String, RuleClass> ruleClassMap;
+
+ /**
+ * Constructs a RuleFactory instance.
+ */
+ public RuleFactory(RuleClassProvider provider) {
+ this.ruleClassMap = ImmutableMap.copyOf(provider.getRuleClassMap());
+ }
+
+ /**
+ * Returns the (immutable, unordered) set of names of all the known rule classes.
+ */
+ public Set<String> getRuleClassNames() {
+ return ruleClassMap.keySet();
+ }
+
+ /**
+ * Returns the RuleClass for the specified rule class name.
+ */
+ public RuleClass getRuleClass(String ruleClassName) {
+ return ruleClassMap.get(ruleClassName);
+ }
+
+ /**
+ * Creates and returns a rule instance.
+ *
+ * <p>It is the caller's responsibility to add the rule to the package (the
+ * caller may choose not to do so if, for example, the rule has errors).
+ *
+ * @param pkgBuilder the under-construction package to which the rule belongs
+ * @param ruleClass the class of the rule; this must not be null
+ * @param attributeValues a map of attribute names to attribute values. Each
+ * attribute must be defined for this class of rule, and have a value
+ * of the appropriate type. There must be a map entry for each
+ * non-optional attribute of this class of rule.
+ * @param eventHandler a eventHandler on which errors and warnings are reported during
+ * rule creation
+ * @param ast the abstract syntax tree of the rule expression (optional)
+ * @param location the location at which this rule was declared
+ * @throws InvalidRuleException if the rule could not be constructed for any
+ * reason (e.g. no <code>name</code> attribute is defined)
+ * @throws NameConflictException
+ */
+ static Rule createAndAddRule(Package.AbstractBuilder<?, ?> pkgBuilder,
+ RuleClass ruleClass,
+ Map<String, Object> attributeValues,
+ EventHandler eventHandler,
+ FuncallExpression ast,
+ Location location) throws InvalidRuleException, NameConflictException {
+ Preconditions.checkNotNull(ruleClass);
+ String ruleClassName = ruleClass.getName();
+ Object nameObject = attributeValues.get("name");
+ if (!(nameObject instanceof String)) {
+ throw new InvalidRuleException(ruleClassName + " rule has no 'name' attribute");
+ }
+ String name = (String) nameObject;
+ Label label;
+ try {
+ // Test that this would form a valid label name -- in particular, this
+ // catches cases where Makefile variables $(foo) appear in "name".
+ label = pkgBuilder.createLabel(name);
+ } catch (Label.SyntaxException e) {
+ throw new InvalidRuleException("illegal rule name: " + name + ": " + e.getMessage());
+ }
+ boolean inWorkspaceFile = location.getPath() != null
+ && location.getPath().endsWith(new PathFragment("WORKSPACE"));
+ if (ruleClass.getWorkspaceOnly() && !inWorkspaceFile) {
+ throw new RuleFactory.InvalidRuleException(ruleClass + " must be in the WORKSPACE file "
+ + "(used by " + label + ")");
+ } else if (!ruleClass.getWorkspaceOnly() && inWorkspaceFile) {
+ throw new RuleFactory.InvalidRuleException(ruleClass + " cannot be in the WORKSPACE file "
+ + "(used by " + label + ")");
+ }
+
+ try {
+ Rule rule = ruleClass.createRuleWithLabel(pkgBuilder, label, attributeValues,
+ eventHandler, ast, location);
+ pkgBuilder.addRule(rule);
+ return rule;
+ } catch (SyntaxException e) {
+ throw new RuleFactory.InvalidRuleException(ruleClass + " " + e.getMessage());
+ }
+ }
+
+ public static Rule createAndAddRule(PackageContext context,
+ RuleClass ruleClass,
+ Map<String, Object> attributeValues,
+ FuncallExpression ast) throws InvalidRuleException, NameConflictException {
+ return createAndAddRule(context.pkgBuilder, ruleClass, attributeValues, context.eventHandler,
+ ast, ast.getLocation());
+ }
+
+ /**
+ * InvalidRuleException is thrown by createRule() if the Rule could not be
+ * constructed. It contains an error message.
+ */
+ public static class InvalidRuleException extends Exception {
+ private InvalidRuleException(String message) {
+ super(message);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/RuleVisibility.java b/src/main/java/com/google/devtools/build/lib/packages/RuleVisibility.java
new file mode 100644
index 0000000..ef1a126
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/RuleVisibility.java
@@ -0,0 +1,49 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+import com.google.devtools.build.lib.syntax.Label;
+
+import java.util.List;
+
+/**
+ * A RuleVisibility specifies which other rules can depend on a specified rule.
+ * Note that the actual method that performs this check is declared in
+ * RuleConfiguredTargetVisibility.
+ *
+ * <p>The conversion to ConfiguredTargetVisibility is handled in an ugly
+ * if-ladder, because I want to avoid this package depending on build.lib.view.
+ *
+ * All implementations of this interface are immutable.
+ */
+public interface RuleVisibility {
+ /**
+ * Returns the list of labels that need to be loaded so that the visibility
+ * decision can be made during analysis time. E.g. for package group
+ * visibility, this is the list of package groups referenced. Does not include
+ * labels that have special meanings in the visibility declaration, e.g.
+ * "//visibility:*" or "//*:__pkg__".
+ */
+ List<Label> getDependencyLabels();
+
+ /**
+ * Returns the list of labels used during the declaration of this visibility.
+ * These do not necessarily represent loadable labels: for example, for public
+ * or private visibilities, the special labels "//visibility:*" will be
+ * returned, and so will be the special "//*:__pkg__" labels indicating a
+ * single package.
+ */
+ List<Label> getDeclaredLabels();
+}
+
diff --git a/src/main/java/com/google/devtools/build/lib/packages/SkylarkFileType.java b/src/main/java/com/google/devtools/build/lib/packages/SkylarkFileType.java
new file mode 100644
index 0000000..f6098cf
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/SkylarkFileType.java
@@ -0,0 +1,58 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.syntax.SkylarkCallable;
+import com.google.devtools.build.lib.syntax.SkylarkModule;
+import com.google.devtools.build.lib.util.FileType;
+import com.google.devtools.build.lib.util.FileType.HasFilename;
+import com.google.devtools.build.lib.util.FileTypeSet;
+
+/**
+ * A wrapper class for FileType and FileTypeSet functionality in Skylark.
+ */
+@SkylarkModule(name = "FileType", doc = "File type for file filtering.")
+public class SkylarkFileType {
+
+ private final FileType fileType;
+
+ private SkylarkFileType(FileType fileType) {
+ this.fileType = fileType;
+ }
+
+ public static SkylarkFileType of(Iterable<String> extensions) {
+ return new SkylarkFileType(FileType.of(extensions));
+ }
+
+ public FileTypeSet getFileTypeSet() {
+ return FileTypeSet.of(fileType);
+ }
+
+ @SkylarkCallable(doc = "")
+ public ImmutableList<HasFilename> filter(Iterable<HasFilename> files) {
+ return ImmutableList.copyOf(FileType.filter(files, fileType));
+ }
+
+ @SkylarkCallable(doc = "")
+ public boolean matches(String fileName) {
+ return fileType.apply(fileName);
+ }
+
+ @VisibleForTesting
+ public Object getExtensions() {
+ return fileType.getExtensions();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/Target.java b/src/main/java/com/google/devtools/build/lib/packages/Target.java
new file mode 100644
index 0000000..ec5bc86
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/Target.java
@@ -0,0 +1,81 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.packages.License.DistributionType;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.syntax.SkylarkCallable;
+import com.google.devtools.build.lib.syntax.SkylarkModule;
+
+import java.util.Set;
+
+/**
+ * A node in the build dependency graph, identified by a Label.
+ */
+@SkylarkModule(name = "target", doc = "A BUILD target.")
+public interface Target {
+
+ /**
+ * Returns the label of this target. (e.g. "//foo:bar")
+ */
+ @SkylarkCallable(name = "label", doc = "")
+ Label getLabel();
+
+ /**
+ * Returns the name of this rule (relative to its owning package).
+ */
+ @SkylarkCallable(name = "name", doc = "")
+ String getName();
+
+ /**
+ * Returns the Package to which this rule belongs.
+ */
+ Package getPackage();
+
+ /**
+ * Returns a string describing this kind of target: e.g. "cc_library rule",
+ * "source file", "generated file".
+ */
+ String getTargetKind();
+
+ /**
+ * Returns the rule associated with this target, if any.
+ *
+ * If this is a Rule, returns itself; it this is an OutputFile, returns its
+ * generating rule; if this is an input file, returns null.
+ */
+ Rule getAssociatedRule();
+
+ /**
+ * Returns the license associated with this target.
+ */
+ License getLicense();
+
+ /**
+ * Returns the place where the target was defined.
+ */
+ Location getLocation();
+
+ /**
+ * Returns the set of distribution types associated with this target.
+ */
+ Set<DistributionType> getDistributions();
+
+ /**
+ * Returns the visibility of this target.
+ */
+ RuleVisibility getVisibility();
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/TargetUtils.java b/src/main/java/com/google/devtools/build/lib/packages/TargetUtils.java
new file mode 100644
index 0000000..3710eeb
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/TargetUtils.java
@@ -0,0 +1,265 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.syntax.Label;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.annotation.Nullable;
+
+/**
+ * Utility functions over Targets that don't really belong in the base {@link
+ * Target} interface.
+ */
+public final class TargetUtils {
+
+ // *_test / test_suite attribute that used to specify constraint keywords.
+ private static final String CONSTRAINTS_ATTR = "tags";
+
+ private TargetUtils() {} // Uninstantiable.
+
+ public static boolean isTestRuleName(String name) {
+ return name.endsWith("_test");
+ }
+
+ public static boolean isTestSuiteRuleName(String name) {
+ return name.equals("test_suite");
+ }
+
+ /**
+ * Returns true iff {@code target} is a {@code *_test} rule; excludes {@code
+ * test_suite}.
+ */
+ public static boolean isTestRule(Target target) {
+ return (target instanceof Rule) && isTestRuleName(((Rule) target).getRuleClass());
+ }
+
+ /**
+ * Returns true iff {@code target} is a {@code test_suite} rule.
+ */
+ public static boolean isTestSuiteRule(Target target) {
+ return target instanceof Rule &&
+ isTestSuiteRuleName(((Rule) target).getRuleClass());
+ }
+
+ /**
+ * Returns true iff {@code target} is a {@code *_test} or {@code test_suite}.
+ */
+ public static boolean isTestOrTestSuiteRule(Target target) {
+ return isTestRule (target) || isTestSuiteRule(target);
+ }
+
+ /**
+ * Returns true if {@code target} has "manual" in the tags attribute and thus should be ignored by
+ * command-line wildcards or by test_suite $implicit_tests attribute.
+ */
+ public static boolean hasManualTag(Target target) {
+ return (target instanceof Rule) && hasConstraint((Rule) target, "manual");
+ }
+
+ /**
+ * Returns true if test marked as "exclusive" by the appropriate keyword
+ * in the tags attribute.
+ *
+ * Method assumes that passed target is a test rule, so usually it should be
+ * used only after isTestRule() or isTestOrTestSuiteRule(). Behavior is
+ * undefined otherwise.
+ */
+ public static boolean isExclusiveTestRule(Rule rule) {
+ return hasConstraint(rule, "exclusive");
+ }
+
+ /**
+ * Returns true if test marked as "local" by the appropriate keyword
+ * in the tags attribute.
+ *
+ * Method assumes that passed target is a test rule, so usually it should be
+ * used only after isTestRule() or isTestOrTestSuiteRule(). Behavior is
+ * undefined otherwise.
+ */
+ public static boolean isLocalTestRule(Rule rule) {
+ return hasConstraint(rule, "local")
+ || NonconfigurableAttributeMapper.of(rule).get("local", Type.BOOLEAN);
+ }
+
+ /**
+ * Returns true if the rule is a test or test suite and is local or exclusive.
+ * Wraps the above calls into one generic check safely applicable to any rule.
+ */
+ public static boolean isTestRuleAndRunsLocally(Rule rule) {
+ return isTestOrTestSuiteRule(rule) &&
+ (isLocalTestRule(rule) || isExclusiveTestRule(rule));
+ }
+
+ /**
+ * Returns true if test marked as "external" by the appropriate keyword
+ * in the tags attribute.
+ *
+ * Method assumes that passed target is a test rule, so usually it should be
+ * used only after isTestRule() or isTestOrTestSuiteRule(). Behavior is
+ * undefined otherwise.
+ */
+ public static boolean isExternalTestRule(Rule rule) {
+ return hasConstraint(rule, "external");
+ }
+
+ /**
+ * Returns true, iff the given target is a rule and it has the attribute
+ * <code>obsolete<code/> set to one.
+ */
+ public static boolean isObsolete(Target target) {
+ if (!(target instanceof Rule)) {
+ return false;
+ }
+ Rule rule = (Rule) target;
+ return (rule.isAttrDefined("obsolete", Type.BOOLEAN))
+ && NonconfigurableAttributeMapper.of(rule).get("obsolete", Type.BOOLEAN);
+ }
+
+ /**
+ * If the given target is a rule, returns its <code>deprecation<code/> value, or null if unset.
+ */
+ @Nullable
+ public static String getDeprecation(Target target) {
+ if (!(target instanceof Rule)) {
+ return null;
+ }
+ Rule rule = (Rule) target;
+ return (rule.isAttrDefined("deprecation", Type.STRING))
+ ? NonconfigurableAttributeMapper.of(rule).get("deprecation", Type.STRING)
+ : null;
+ }
+
+ /**
+ * Checks whether specified constraint keyword is present in the
+ * tags attribute of the test or test suite rule.
+ *
+ * Method assumes that provided rule is a test or a test suite. Behavior is
+ * undefined otherwise.
+ */
+ private static boolean hasConstraint(Rule rule, String keyword) {
+ return NonconfigurableAttributeMapper.of(rule).get(CONSTRAINTS_ATTR, Type.STRING_LIST)
+ .contains(keyword);
+ }
+
+ /**
+ * Returns the execution info. These include execution requirement
+ * tags ('requires-*' as well as "local") as keys with empty values.
+ */
+ public static Map<String, String> getExecutionInfo(Rule rule) {
+ // tags may contain duplicate values.
+ Map<String, String> map = new HashMap<>();
+ for (String tag :
+ NonconfigurableAttributeMapper.of(rule).get(CONSTRAINTS_ATTR, Type.STRING_LIST)) {
+ if (tag.startsWith("requires-") || tag.equals("local")) {
+ map.put(tag, "");
+ }
+ }
+ return ImmutableMap.copyOf(map);
+ }
+
+ /**
+ * Returns the language part of the rule name (e.g. "foo" for foo_test or foo_binary).
+ *
+ * <p>In practice this is the part before the "_", if any, otherwise the entire rule class name.
+ *
+ * <p>Precondition: isTestRule(target) || isRunnableNonTestRule(target).
+ */
+ public static String getRuleLanguage(Target target) {
+ return getRuleLanguage(((Rule) target).getRuleClass());
+ }
+
+ /**
+ * Returns the language part of the rule name (e.g. "foo" for foo_test or foo_binary).
+ *
+ * <p>In practice this is the part before the "_", if any, otherwise the entire rule class name.
+ */
+ public static String getRuleLanguage(String ruleClass) {
+ int index = ruleClass.lastIndexOf("_");
+ // Chop off "_binary" or "_test".
+ return index != -1 ? ruleClass.substring(0, index) : ruleClass;
+ }
+
+ private static boolean isExplicitDependency(Rule rule, Label label) {
+ if (rule.getVisibility().getDependencyLabels().contains(label)) {
+ return true;
+ }
+
+ ExplicitEdgeVisitor visitor = new ExplicitEdgeVisitor(rule, label);
+ AggregatingAttributeMapper.of(rule).visitLabels(visitor);
+ return visitor.isExplicit();
+ }
+
+ private static class ExplicitEdgeVisitor implements AttributeMap.AcceptsLabelAttribute {
+ private final Label expectedLabel;
+ private final Rule rule;
+ private boolean isExplicit = false;
+
+ public ExplicitEdgeVisitor(Rule rule, Label expected) {
+ this.rule = rule;
+ this.expectedLabel = expected;
+ }
+
+ @Override
+ public void acceptLabelAttribute(Label label, Attribute attr) {
+ if (isExplicit || !rule.isAttributeValueExplicitlySpecified(attr)) {
+ // Nothing to do here.
+ } else if (expectedLabel.equals(label)) {
+ isExplicit = true;
+ }
+ }
+
+ public boolean isExplicit() {
+ return isExplicit;
+ }
+ }
+
+ /**
+ * Return {@link Location} for {@link Target} target, if it should not be null.
+ */
+ public static Location getLocationMaybe(Target target) {
+ return (target instanceof Rule) || (target instanceof InputFile) ? target.getLocation() : null;
+ }
+
+ /**
+ * Return nicely formatted error message that {@link Label} label that was pointed to by
+ * {@link Target} target did not exist, due to {@link NoSuchThingException} e.
+ */
+ public static String formatMissingEdge(@Nullable Target target, Label label,
+ NoSuchThingException e) {
+ // instanceof returns false if target is null (which is exploited here)
+ if (target instanceof Rule) {
+ Rule rule = (Rule) target;
+ return !isExplicitDependency(rule, label)
+ ? ("every rule of type " + rule.getRuleClass() + " implicitly depends upon the target '"
+ + label + "', but this target could not be found. "
+ + "If this is an integration test, maybe you forgot to add a mock for your new tool?")
+ : e.getMessage() + " and referenced by '" + target.getLabel() + "'";
+ } else if (target instanceof InputFile) {
+ return e.getMessage() + " (this is usually caused by a missing package group in the"
+ + " package-level visibility declaration)";
+ } else {
+ if (target != null) {
+ return "in target '" + target.getLabel() + "', no such label '" + label + "': "
+ + e.getMessage();
+ }
+ return e.getMessage();
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/TestSize.java b/src/main/java/com/google/devtools/build/lib/packages/TestSize.java
new file mode 100644
index 0000000..425a343
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/TestSize.java
@@ -0,0 +1,123 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+import com.google.devtools.common.options.OptionsParsingException;
+
+import java.util.Set;
+
+/**
+ * Possible test sizes.
+ *
+ * Test size may affect the way how test is executed - e.g., it will determine
+ * default timeout value and estimated local resource usage.
+ */
+public enum TestSize {
+
+ // Small tests use small amount of memory, but CPU intensive.
+ SMALL(TestTimeout.SHORT, 2),
+ // Medium tests tend to use larger amount of memory.
+ MEDIUM(TestTimeout.MODERATE, 10),
+ // All other tests estimated to use fairly large amount of memory.
+ LARGE(TestTimeout.LONG, 20),
+ ENORMOUS(TestTimeout.ETERNAL, 30);
+
+ private final TestTimeout timeout;
+ private final int defaultShards;
+
+ private TestSize(TestTimeout defaultTimeout, int defaultShards) {
+ this.timeout = defaultTimeout;
+ this.defaultShards = defaultShards;
+ }
+
+ /**
+ * Returns default timeout in seconds.
+ */
+ public TestTimeout getDefaultTimeout() {
+ return timeout;
+ }
+
+ /**
+ * Returns default number of shards.
+ */
+ public int getDefaultShards() { return defaultShards; }
+
+ /**
+ * Returns test size of the given test target, or null if the size attribute is unrecognized.
+ */
+ public static TestSize getTestSize(Rule testTarget) {
+ String attr = NonconfigurableAttributeMapper.of(testTarget).get("size", Type.STRING);
+ return getTestSize(attr);
+ }
+
+ /**
+ * Returns {@link TestSize} matching the given timeout or null if the
+ * given timeout doesn't match any {@link TestSize}.
+ *
+ * @param timeout The timeout associated with the desired TestSize.
+ */
+ public static TestSize getTestSize(TestTimeout timeout) {
+ for (TestSize size : TestSize.values()) {
+ if (size.timeout == timeout) {
+ return size;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Normal practice is to always use size tags as lower case strings.
+ */
+ @Override
+ public String toString() {
+ return super.toString().toLowerCase();
+ }
+
+ /**
+ * Returns the enum associated with a test's size or null if the tag is
+ * not lower case or an unknown size.
+ */
+ public static TestSize getTestSize(String attr) {
+ if (!attr.equals(attr.toLowerCase())) {
+ return null;
+ }
+ try {
+ return TestSize.valueOf(attr.toUpperCase());
+ } catch (IllegalArgumentException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Converter for the --test_size_filters option.
+ */
+ public static class TestSizeFilterConverter extends EnumFilterConverter<TestSize> {
+ public TestSizeFilterConverter() {
+ super(TestSize.class, "test size");
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * <p>This override is necessary to prevent OptionsData
+ * from throwing a "must be assignable from the converter return type" exception.
+ * OptionsData doesn't recognize the generic type and actual type are the same.
+ */
+ @Override
+ public final Set<TestSize> convert(String input) throws OptionsParsingException {
+ return super.convert(input);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/TestTargetUtils.java b/src/main/java/com/google/devtools/build/lib/packages/TestTargetUtils.java
new file mode 100644
index 0000000..dbd4dae
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/TestTargetUtils.java
@@ -0,0 +1,404 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Sets;
+import com.google.devtools.build.lib.cmdline.ResolvedTargets;
+import com.google.devtools.build.lib.cmdline.TargetParsingException;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.pkgcache.TargetProvider;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.util.Pair;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Utility functions over test Targets that don't really belong in the base {@link Target}
+ * interface.
+ */
+public final class TestTargetUtils {
+ /**
+ * Returns a predicate to be used for test size filtering, i.e., that only accepts tests of the
+ * given size.
+ */
+ public static Predicate<Target> testSizeFilter(final Set<TestSize> allowedSizes) {
+ return new Predicate<Target>() {
+ @Override
+ public boolean apply(Target target) {
+ if (!(target instanceof Rule)) {
+ return false;
+ }
+ return allowedSizes.contains(TestSize.getTestSize((Rule) target));
+ }
+ };
+ }
+
+ /**
+ * Returns a predicate to be used for test timeout filtering, i.e., that only accepts tests of
+ * the given timeout.
+ **/
+ public static Predicate<Target> testTimeoutFilter(final Set<TestTimeout> allowedTimeouts) {
+ return new Predicate<Target>() {
+ @Override
+ public boolean apply(Target target) {
+ if (!(target instanceof Rule)) {
+ return false;
+ }
+ return allowedTimeouts.contains(TestTimeout.getTestTimeout((Rule) target));
+ }
+ };
+ }
+
+ /**
+ * Returns a predicate to be used for test language filtering, i.e., that only accepts tests of
+ * the specified languages. The reporter and the list of rule names are only used to warn about
+ * unknown languages.
+ */
+ public static Predicate<Target> testLangFilter(List<String> langFilterList,
+ EventHandler reporter, Set<String> allRuleNames) {
+ final Set<String> requiredLangs = new HashSet<>();
+ final Set<String> excludedLangs = new HashSet<>();
+
+ for (String lang : langFilterList) {
+ if (lang.startsWith("-")) {
+ lang = lang.substring(1);
+ excludedLangs.add(lang);
+ } else {
+ requiredLangs.add(lang);
+ }
+ if (!allRuleNames.contains(lang + "_test")) {
+ reporter.handle(
+ Event.warn("Unknown language '" + lang + "' in --test_lang_filters option"));
+ }
+ }
+
+ return new Predicate<Target>() {
+ @Override
+ public boolean apply(Target rule) {
+ String ruleLang = TargetUtils.getRuleLanguage(rule);
+ return (requiredLangs.isEmpty() || requiredLangs.contains(ruleLang))
+ && !excludedLangs.contains(ruleLang);
+ }
+ };
+ }
+
+ /**
+ * Returns whether a test with the specified tags matches a filter (as specified by the set
+ * of its positive and its negative filters).
+ */
+ public static boolean testMatchesFilters(Collection<String> testTags,
+ Collection<String> requiredTags, Collection<String> excludedTags,
+ boolean mustMatchAllPositive) {
+
+ for (String tag : excludedTags) {
+ if (testTags.contains(tag)) {
+ return false;
+ }
+ }
+
+ // Check required tags, if there are any.
+ if (!requiredTags.isEmpty()) {
+ if (mustMatchAllPositive) {
+ // Require all tags to be present.
+ for (String tag : requiredTags) {
+ if (!testTags.contains(tag)) {
+ return false;
+ }
+ }
+ return true;
+ } else {
+ // Require at least one positive tag.
+ for (String tag : requiredTags) {
+ if (testTags.contains(tag)) {
+ return true;
+ }
+ }
+ }
+
+ return false; // No positive tag found.
+ }
+
+ return true; // No tags are required.
+ }
+
+ /**
+ * Returns a predicate to be used for test tag filtering, i.e., that only accepts tests that match
+ * all of the required tags and none of the excluded tags.
+ */
+ // TODO(bazel-team): This also applies to non-test rules, so should probably be moved to
+ // TargetUtils.
+ public static Predicate<Target> tagFilter(List<String> tagFilterList) {
+ Pair<Collection<String>, Collection<String>> tagLists = sortTagsBySense(tagFilterList);
+ final Collection<String> requiredTags = tagLists.first;
+ final Collection<String> excludedTags = tagLists.second;
+ return new Predicate<Target>() {
+ @Override
+ public boolean apply(Target input) {
+ if (!(input instanceof Rule)) {
+ return false;
+ }
+ // Note that test_tags are those originating from the XX_test rule,
+ // whereas the requiredTags and excludedTags originate from the command
+ // line or test_suite rule.
+ return testMatchesFilters(((Rule) input).getRuleTags(),
+ requiredTags, excludedTags, false);
+ }
+ };
+ }
+
+ /**
+ * Separates a list of text "tags" into a Pair of Collections, where
+ * the first element are the required or positive tags and the second element
+ * are the excluded or negative tags.
+ * This should work on tag list provided from the command line
+ * --test_tags_filters flag or on tag filters explicitly declared in the
+ * suite.
+ *
+ * @param tagList A collection of text targets to separate.
+ */
+ public static Pair<Collection<String>, Collection<String>> sortTagsBySense(
+ Iterable<String> tagList) {
+ Collection<String> requiredTags = new HashSet<>();
+ Collection<String> excludedTags = new HashSet<>();
+
+ for (String tag : tagList) {
+ if (tag.startsWith("-")) {
+ excludedTags.add(tag.substring(1));
+ } else if (tag.startsWith("+")) {
+ requiredTags.add(tag.substring(1));
+ } else if (tag.equals("manual")) {
+ // Ignore manual attribute because it is an exception: it is not a filter
+ // but a property of test_suite
+ continue;
+ } else {
+ requiredTags.add(tag);
+ }
+ }
+ return Pair.of(requiredTags, excludedTags);
+ }
+
+ /**
+ * Returns the (new, mutable) set of test rules, expanding all 'test_suite' rules into the
+ * individual tests they group together and preserving other test target instances.
+ *
+ * Method assumes that passed collection contains only *_test and test_suite rules. While, at this
+ * point it will successfully preserve non-test rules as well, there is no guarantee that this
+ * behavior will be kept in the future.
+ *
+ * @param targetProvider a target provider
+ * @param eventHandler a failure eventHandler to report loading failures to
+ * @param targets Collection of the *_test and test_suite configured targets
+ * @return a duplicate-free iterable of the tests under the specified targets
+ */
+ public static ResolvedTargets<Target> expandTestSuites(TargetProvider targetProvider,
+ EventHandler eventHandler, Iterable<? extends Target> targets, boolean strict,
+ boolean keepGoing)
+ throws TargetParsingException {
+ Closure closure = new Closure(targetProvider, eventHandler, strict, keepGoing);
+ ResolvedTargets.Builder<Target> result = ResolvedTargets.builder();
+ for (Target target : targets) {
+ if (TargetUtils.isTestRule(target)) {
+ result.add(target);
+ } else if (TargetUtils.isTestSuiteRule(target)) {
+ result.addAll(closure.getTestsInSuite((Rule) target));
+ } else {
+ result.add(target);
+ }
+ }
+ if (closure.hasError) {
+ result.setError();
+ }
+ return result.build();
+ }
+
+ // TODO(bazel-team): This is a copy of TestsExpression.Closure with some minor changes; this
+ // should be unified.
+ private static final class Closure {
+ private final TargetProvider targetProvider;
+
+ private final EventHandler eventHandler;
+
+ private final boolean keepGoing;
+
+ private final boolean strict;
+
+ private final Map<Target, Set<Target>> testsInSuite = new HashMap<>();
+
+ private boolean hasError;
+
+ public Closure(TargetProvider targetProvider, EventHandler eventHandler, boolean strict,
+ boolean keepGoing) {
+ this.targetProvider = targetProvider;
+ this.eventHandler = eventHandler;
+ this.strict = strict;
+ this.keepGoing = keepGoing;
+ }
+
+ /**
+ * Computes and returns the set of test rules in a particular suite. Uses
+ * dynamic programming---a memoized version of {@link #computeTestsInSuite}.
+ */
+ private Set<Target> getTestsInSuite(Rule testSuite) throws TargetParsingException {
+ Set<Target> tests = testsInSuite.get(testSuite);
+ if (tests == null) {
+ tests = Sets.newHashSet();
+ testsInSuite.put(testSuite, tests); // break cycles by inserting empty set early.
+ computeTestsInSuite(testSuite, tests);
+ }
+ return tests;
+ }
+
+ /**
+ * Populates 'result' with all the tests associated with the specified
+ * 'testSuite'. Throws an exception if any target is missing.
+ *
+ * CAUTION! Keep this logic consistent with {@code TestsSuiteConfiguredTarget}!
+ */
+ private void computeTestsInSuite(Rule testSuite, Set<Target> result)
+ throws TargetParsingException {
+ List<Target> testsAndSuites = new ArrayList<>();
+ // Note that testsAndSuites can contain input file targets; the test_suite rule does not
+ // restrict the set of targets that can appear in tests or suites.
+ testsAndSuites.addAll(getPrerequisites(testSuite, "tests"));
+ testsAndSuites.addAll(getPrerequisites(testSuite, "suites"));
+
+ // 1. Add all tests
+ for (Target test : testsAndSuites) {
+ if (TargetUtils.isTestRule(test)) {
+ result.add(test);
+ } else if (strict && !TargetUtils.isTestSuiteRule(test)) {
+ // If strict mode is enabled, then give an error for any non-test, non-test-suite targets.
+ eventHandler.handle(Event.error(testSuite.getLocation(),
+ "in test_suite rule '" + testSuite.getLabel()
+ + "': expecting a test or a test_suite rule but '" + test.getLabel()
+ + "' is not one."));
+ hasError = true;
+ if (!keepGoing) {
+ throw new TargetParsingException("Test suite expansion failed.");
+ }
+ }
+ }
+
+ // 2. Add implicit dependencies on tests in same package, if any.
+ for (Target target : getPrerequisites(testSuite, "$implicit_tests")) {
+ // The Package construction of $implicit_tests ensures that this check never fails, but we
+ // add it here anyway for compatibility with future code.
+ if (TargetUtils.isTestRule(target)) {
+ result.add(target);
+ }
+ }
+
+ // 3. Filter based on tags, size, env.
+ filterTests(testSuite, result);
+
+ // 4. Expand all suites recursively.
+ for (Target suite : testsAndSuites) {
+ if (TargetUtils.isTestSuiteRule(suite)) {
+ result.addAll(getTestsInSuite((Rule) suite));
+ }
+ }
+ }
+
+ /**
+ * Returns the set of rules named by the attribute 'attrName' of test_suite rule 'testSuite'.
+ * The attribute must be a list of labels. If a target cannot be resolved, then an error is
+ * reported to the environment (which may throw an exception if {@code keep_going} is disabled).
+ */
+ private Collection<Target> getPrerequisites(Rule testSuite, String attrName)
+ throws TargetParsingException {
+ try {
+ List<Target> targets = new ArrayList<>();
+ // TODO(bazel-team): This serializes package loading in some cases. We might want to make
+ // this multi-threaded.
+ for (Label label :
+ NonconfigurableAttributeMapper.of(testSuite).get(attrName, Type.LABEL_LIST)) {
+ targets.add(targetProvider.getTarget(eventHandler, label));
+ }
+ return targets;
+ } catch (NoSuchThingException e) {
+ if (keepGoing) {
+ hasError = true;
+ eventHandler.handle(Event.error(e.getMessage()));
+ return ImmutableList.of();
+ }
+ throw new TargetParsingException(e.getMessage(), e);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new TargetParsingException("interrupted", e);
+ }
+ }
+
+ /**
+ * Filters 'tests' (by mutation) according to the 'tags' attribute, specifically those that
+ * match ALL of the tags in tagsAttribute.
+ *
+ * @precondition {@code env.getAccessor().isTestSuite(testSuite)}
+ * @precondition {@code env.getAccessor().isTestRule(test)} for all test in tests
+ */
+ private void filterTests(Rule testSuite, Set<Target> tests) {
+ List<String> tagsAttribute =
+ NonconfigurableAttributeMapper.of(testSuite).get("tags", Type.STRING_LIST);
+ // Split the tags list into positive and negative tags
+ Pair<Collection<String>, Collection<String>> tagLists = sortTagsBySense(tagsAttribute);
+ Collection<String> positiveTags = tagLists.first;
+ Collection<String> negativeTags = tagLists.second;
+
+ Iterator<Target> it = tests.iterator();
+ while (it.hasNext()) {
+ Rule test = (Rule) it.next();
+ AttributeMap nonConfigurableAttributes = NonconfigurableAttributeMapper.of(test);
+ List<String> testTags =
+ new ArrayList<>(nonConfigurableAttributes.get("tags", Type.STRING_LIST));
+ testTags.add(nonConfigurableAttributes.get("size", Type.STRING));
+ if (!includeTest(testTags, positiveTags, negativeTags)) {
+ it.remove();
+ }
+ }
+ }
+
+ /**
+ * Decides whether to include a test in a test_suite or not.
+ * @param testTags Collection of all tags exhibited by a given test.
+ * @param positiveTags Tags declared by the suite. A Test must match ALL of these.
+ * @param negativeTags Tags declared by the suite. A Test must match NONE of these.
+ * @return false is the test is to be removed.
+ */
+ private static boolean includeTest(Collection<String> testTags,
+ Collection<String> positiveTags, Collection<String> negativeTags) {
+ // Add this test if it matches ALL of the positive tags and NONE of the
+ // negative tags in the tags attribute.
+ for (String tag : negativeTags) {
+ if (testTags.contains(tag)) {
+ return false;
+ }
+ }
+ for (String tag : positiveTags) {
+ if (!testTags.contains(tag)) {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/TestTimeout.java b/src/main/java/com/google/devtools/build/lib/packages/TestTimeout.java
new file mode 100644
index 0000000..cfa8047
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/TestTimeout.java
@@ -0,0 +1,198 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+import com.google.common.base.Splitter;
+import com.google.common.collect.Maps;
+import com.google.devtools.common.options.Converter;
+import com.google.devtools.common.options.OptionsParsingException;
+
+import java.util.ArrayList;
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Symbolic labels of test timeout. Borrows heavily from {@link TestSize}.
+ */
+public enum TestTimeout {
+
+ // These symbolic labels are used in the build files.
+ SHORT(0, 60, 60),
+ MODERATE(30, 300, 300),
+ LONG(300, 900, 900),
+ ETERNAL(900, 365 * 24 * 60 /* One year */, 3600);
+
+ /**
+ * Default --test_timeout flag, used when collecting code coverage.
+ */
+ public static String COVERAGE_CMD_TIMEOUT = "--test_timeout=300,600,1200,3600";
+
+ private final Integer rangeMin;
+ private final Integer rangeMax;
+ private final Integer timeout;
+
+ private TestTimeout(Integer rangeMin, Integer rangeMax, Integer timeout) {
+ this.rangeMin = rangeMin;
+ this.rangeMax = rangeMax;
+ this.timeout = timeout;
+ }
+
+ /**
+ * Returns the enum associated with a test's timeout or null if the tag is
+ * not lower case or an unknown size.
+ */
+ public static TestTimeout getTestTimeout(String attr) {
+ if (!attr.equals(attr.toLowerCase())) {
+ return null;
+ }
+ try {
+ return TestTimeout.valueOf(attr.toUpperCase());
+ } catch (IllegalArgumentException e) {
+ return null;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return super.toString().toLowerCase();
+ }
+
+ /**
+ * We print to upper case to make the test timeout warnings more readable.
+ */
+ public String prettyPrint() {
+ return super.toString().toUpperCase();
+ }
+
+ public Integer getTimeout() {
+ return timeout;
+ }
+ /**
+ * Returns true iff the given time in seconds is exactly in the range of valid
+ * execution times for this TestSize.
+ */
+ public boolean isInRangeExact(Integer timeInSeconds) {
+ return timeInSeconds >= rangeMin && timeInSeconds < rangeMax;
+ }
+
+ /**
+ * Returns true iff the given time in seconds is approximately (+/- 75%) in the range of valid
+ * execution times for this TestSize.
+ */
+ public boolean isInRangeFuzzy(Integer timeInSeconds) {
+ return timeInSeconds >= rangeMin - (rangeMin * .75)
+ && (this == ETERNAL || timeInSeconds <= rangeMax + (rangeMax * .75));
+ }
+
+ /**
+ * Returns suggested test size for the given time in seconds.
+ */
+ public static TestTimeout getSuggestedTestTimeout(Integer timeInSeconds) {
+ for (TestTimeout testTimeout : values()) {
+ if (testTimeout.isInRangeExact(timeInSeconds)) {
+ return testTimeout;
+ }
+ }
+ return ETERNAL;
+ }
+
+ /**
+ * Returns test timeout of the given test target using explicitly specified timeout
+ * or default through to the size label's associated default.
+ */
+ public static TestTimeout getTestTimeout(Rule testTarget) {
+ String attr = NonconfigurableAttributeMapper.of(testTarget).get("timeout", Type.STRING);
+ if (!attr.equals(attr.toLowerCase())) {
+ return null; // attribute values must be lowercase
+ }
+ try {
+ return TestTimeout.valueOf(attr.toUpperCase());
+ } catch (IllegalArgumentException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Converter for the --test_timeout option.
+ */
+ public static class TestTimeoutConverter implements Converter<Map<TestTimeout, Integer>> {
+ public TestTimeoutConverter() {}
+
+ @Override
+ public Map<TestTimeout, Integer> convert(String input) throws OptionsParsingException {
+ List<Integer> values = new ArrayList<>();
+ for (String token : Splitter.on(',').limit(6).split(input)) {
+ // Handle the case of "2," which is accepted as legal... Because Splitter.split is lazy,
+ // there's no way of knowing if an empty string is a trailing or an intermediate one,
+ // so we can't fully emulate String.split(String, 0).
+ if (!token.isEmpty() || values.size() > 1) {
+ try {
+ values.add(Integer.valueOf(token));
+ } catch (NumberFormatException e) {
+ throw new OptionsParsingException("'" + input + "' is not an int");
+ }
+ }
+ }
+ EnumMap<TestTimeout, Integer> timeouts = Maps.newEnumMap(TestTimeout.class);
+ if (values.size() == 1) {
+ timeouts.put(SHORT, values.get(0));
+ timeouts.put(MODERATE, values.get(0));
+ timeouts.put(LONG, values.get(0));
+ timeouts.put(ETERNAL, values.get(0));
+ } else if (values.size() == 4) {
+ timeouts.put(SHORT, values.get(0));
+ timeouts.put(MODERATE, values.get(1));
+ timeouts.put(LONG, values.get(2));
+ timeouts.put(ETERNAL, values.get(3));
+ } else {
+ throw new OptionsParsingException("Invalid number of comma-separated entries");
+ }
+ for (TestTimeout label : values()) {
+ if (!timeouts.containsKey(label) || timeouts.get(label) <= 0) {
+ timeouts.put(label, label.getTimeout());
+ }
+ }
+ return timeouts;
+ }
+
+ @Override
+ public String getTypeDescription() {
+ return "a single integer or comma-separated list of 4 integers";
+ }
+ }
+
+ /**
+ * Converter for the --test_timeout_filters option.
+ */
+ public static class TestTimeoutFilterConverter extends EnumFilterConverter<TestTimeout> {
+ public TestTimeoutFilterConverter() {
+ super(TestTimeout.class, "test timeout");
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * <p>This override is necessary to prevent OptionsData
+ * from throwing a "must be assignable from the converter return type" exception.
+ * OptionsData doesn't recognize the generic type and actual type are the same.
+ */
+ @Override
+ public final Set<TestTimeout> convert(String input) throws OptionsParsingException {
+ return super.convert(input);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/TriState.java b/src/main/java/com/google/devtools/build/lib/packages/TriState.java
new file mode 100644
index 0000000..0cea28a
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/TriState.java
@@ -0,0 +1,22 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+/**
+ * Enum used to represent tri-state parameters in rule attributes (yes/no/auto).
+ */
+public enum TriState {
+ YES, NO, AUTO
+}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/Type.java b/src/main/java/com/google/devtools/build/lib/packages/Type.java
new file mode 100644
index 0000000..9172a1f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/Type.java
@@ -0,0 +1,1025 @@
+// Copyright 2014 Google Inc. 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.packages;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
+import com.google.devtools.build.lib.packages.License.DistributionType;
+import com.google.devtools.build.lib.packages.License.LicenseParsingException;
+import com.google.devtools.build.lib.syntax.EvalUtils;
+import com.google.devtools.build.lib.syntax.FilesetEntry;
+import com.google.devtools.build.lib.syntax.GlobList;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.syntax.SelectorValue;
+import com.google.devtools.build.lib.util.LoggingUtil;
+import com.google.devtools.build.lib.util.StringCanonicalizer;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.logging.Level;
+
+import javax.annotation.Nullable;
+
+/**
+ * <p>Root of Type symbol hierarchy for values in the build language.</p>
+ *
+ * <p>Type symbols are primarily used for their <code>convert</code> method,
+ * which is a kind of cast operator enabling conversion from untyped (Object)
+ * references to values in the build language, to typed references.</p>
+ *
+ * <p>For example, this code type-converts a value <code>x</code> returned by
+ * the evaluator, to a list of strings:</p>
+ *
+ * <pre>
+ * Object x = expr.eval(env);
+ * List<String> s = Type.STRING_LIST.convert(x);
+ * </pre>
+ */
+public abstract class Type<T> {
+
+ private Type() {}
+
+ /**
+ * Converts untyped Object x resulting from the evaluation of an expression in the build language,
+ * into a typed object of type T.
+ *
+ * <p>x must be *directly* convertible to this type. This therefore disqualifies "selector
+ * expressions" of the form "{ config1: 'value1_of_orig_type', config2: 'value2_of_orig_type; }"
+ * (which support configurable attributes). To handle those expressions, see
+ * {@link #selectableConvert}.
+ *
+ * @param x the build-interpreter value to convert.
+ * @param what a string description of what x is for; should be included in
+ * any exception thrown. Grammatically, must describe a syntactic
+ * construct, e.g. "attribute 'srcs' of rule foo".
+ * @param currentRule the label of the current BUILD rule; must be non-null if resolution of
+ * package-relative label strings is required
+ * @throws ConversionException if there was a problem performing the type conversion
+ */
+ public abstract T convert(Object x, String what, @Nullable Label currentRule)
+ throws ConversionException;
+ // TODO(bazel-team): Check external calls (e.g. in PackageFactory), verify they always want
+ // this over selectableConvert.
+
+ /**
+ * Equivalent to <code>convert(x, null)</code>. Useful for converting values to types that do not
+ * involve the type <code>LABEL</code> and hence do not require the label of the current package.
+ */
+ public final T convert(Object x, String what) throws ConversionException {
+ return convert(x, what, null);
+ }
+
+ /**
+ * Variation of {@link #convert} that supports selector expressions for configurable attributes
+ * (i.e. "{ config1: 'value1_of_orig_type', config2: 'value2_of_orig_type; }"). If x is a
+ * selector expression, returns a {@link Selector} instance that contains key-mapped entries
+ * of the native type. Else, returns the native type directly.
+ *
+ * <p>The caller is responsible for casting the returned value appropriately.
+ */
+ public Object selectableConvert(Object x, String what, @Nullable Label currentRule)
+ throws ConversionException {
+ if (x instanceof SelectorValue) {
+ return new Selector<T>(((SelectorValue) x).getDictionary(), what, currentRule, this);
+ }
+ return convert(x, what, currentRule);
+ }
+
+ public abstract T cast(Object value);
+
+ @Override
+ public abstract String toString();
+
+ /**
+ * Returns the default value for this type; may return null iff no default is defined for this
+ * type.
+ */
+ public abstract T getDefaultValue();
+
+ /**
+ * If this type contains labels (e.g. it *is* a label or it's a collection of labels),
+ * returns a list of those labels for a value of that type. If this type doesn't
+ * contain labels, returns an empty list.
+ *
+ * <p>This is used to support reliable label visitation in
+ * {@link AbstractAttributeMapper#visitLabels}. To preserve that reliability, every
+ * type should faithfully define its own instance of this method. In other words,
+ * be careful about defining default instances in base types that get auto-inherited
+ * by their children. Keep all definitions as explicit as possible.
+ */
+ public abstract Iterable<Label> getLabels(Object value);
+
+ /**
+ * {@link #getLabels} return value for types that don't contain labels.
+ */
+ private static final Iterable<Label> NO_LABELS_HERE = ImmutableList.of();
+
+ /**
+ * Converts an initialized Type object into a tag set representation.
+ * This operation is only valid for certain sub-Types which are guaranteed
+ * to be properly initialized.
+ *
+ * @param value the actual value
+ * @throws UnsupportedOperationException if the concrete type does not support
+ * tag conversion or if a convertible type has no initialized value.
+ */
+ public Set<String> toTagSet(Object value, String name) {
+ String msg = "Attribute " + name + " does not support tag conversion.";
+ throw new UnsupportedOperationException(msg);
+ }
+
+ /**
+ * The type of an integer.
+ */
+ public static final Type<Integer> INTEGER = new IntegerType();
+
+ /**
+ * The type of a string.
+ */
+ public static final Type<String> STRING = new StringType();
+
+ /**
+ * The type of a boolean.
+ */
+ public static final Type<Boolean> BOOLEAN = new BooleanType();
+
+ /**
+ * The type of a TriState with values: true (x>0), false (x==0), auto (x<0).
+ */
+ public static final Type<TriState> TRISTATE = new TriStateType();
+
+ /**
+ * The type of a label. Labels are not actually a first-class datatype in
+ * the build language, but they are so frequently used in the definitions of
+ * attributes that it's worth treating them specially (and providing support
+ * for resolution of relative-labels in the <code>convert()</code> method).
+ */
+ public static final Type<Label> LABEL = new LabelType();
+
+ /**
+ * This is a label type that does not cause dependencies. It is needed because
+ * certain rules want to verify the type of a target referenced by one of their attributes, but
+ * if there was a dependency edge there, it would be a circular dependency.
+ */
+ public static final Type<Label> NODEP_LABEL = new LabelType();
+
+ /**
+ * The type of a license. Like Label, licenses aren't first-class, but
+ * they're important enough to justify early syntax error detection.
+ */
+ public static final Type<License> LICENSE = new LicenseType();
+
+ /**
+ * The type of a single distribution. Only used internally, as a type
+ * symbol, not a converter.
+ */
+ public static final Type<DistributionType> DISTRIBUTION = new Type<DistributionType>() {
+ @Override
+ public DistributionType cast(Object value) {
+ return (DistributionType) value;
+ }
+
+ @Override
+ public DistributionType convert(Object x, String what, Label currentRule) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public DistributionType getDefaultValue() {
+ return null;
+ }
+
+ @Override
+ public Iterable<Label> getLabels(Object value) {
+ return NO_LABELS_HERE;
+ }
+
+ @Override
+ public String toString() {
+ return "distribution";
+ }
+ };
+
+ /**
+ * The type of a set of distributions. Distributions are not a first-class type,
+ * but they do warrant early syntax checking.
+ */
+ public static final Type<Set<DistributionType>> DISTRIBUTIONS = new Distributions();
+
+ /**
+ * The type of an output file, treated as a {@link #LABEL}.
+ */
+ public static final Type<Label> OUTPUT = new OutputType();
+
+ /**
+ * The type of a FilesetEntry attribute inside a Fileset.
+ */
+ public static final Type<FilesetEntry> FILESET_ENTRY = new FilesetEntryType();
+
+ /**
+ * The type of a list of not-yet-typed objects.
+ */
+ public static final ObjectListType OBJECT_LIST = new ObjectListType();
+
+ /**
+ * The type of a list of {@linkplain #STRING strings}.
+ */
+ public static final ListType<String> STRING_LIST = ListType.create(STRING);
+
+ /**
+ * The type of a list of {@linkplain #INTEGER strings}.
+ */
+ public static final ListType<Integer> INTEGER_LIST = ListType.create(INTEGER);
+
+ /**
+ * The type of a dictionary of {@linkplain #STRING strings}.
+ */
+ public static final DictType<String, String> STRING_DICT = DictType.create(STRING, STRING);
+
+ /**
+ * The type of a list of {@linkplain #OUTPUT outputs}.
+ */
+ public static final ListType<Label> OUTPUT_LIST = ListType.create(OUTPUT);
+
+ /**
+ * The type of a list of {@linkplain #LABEL labels}.
+ */
+ public static final ListType<Label> LABEL_LIST = ListType.create(LABEL);
+
+ /**
+ * The type of a list of {@linkplain #NODEP_LABEL labels} that do not cause
+ * dependencies.
+ */
+ public static final ListType<Label> NODEP_LABEL_LIST = ListType.create(NODEP_LABEL);
+
+ /**
+ * The type of a dictionary of {@linkplain #STRING_LIST label lists}.
+ */
+ public static final DictType<String, List<String>> STRING_LIST_DICT =
+ DictType.create(STRING, STRING_LIST);
+
+ /**
+ * The type of a dictionary of {@linkplain #STRING strings}, where each entry
+ * maps to a single string value.
+ */
+ public static final DictType<String, String> STRING_DICT_UNARY = DictType.create(STRING, STRING);
+
+ /**
+ * The type of a dictionary of {@linkplain #LABEL_LIST label lists}.
+ */
+ public static final DictType<String, List<Label>> LABEL_LIST_DICT =
+ DictType.create(STRING, LABEL_LIST);
+
+ /**
+ * The type of a list of {@linkplain #FILESET_ENTRY FilesetEntries}.
+ */
+ public static final ListType<FilesetEntry> FILESET_ENTRY_LIST = ListType.create(FILESET_ENTRY);
+
+ /**
+ * For ListType objects, returns the type of the elements of the list; for
+ * all other types, returns null. (This non-obvious implementation strategy
+ * is necessitated by the wildcard capture rules of the Java type system,
+ * which disallow conversion from Type{List{ELEM}} to Type{List{?}}.)
+ */
+ public Type<?> getListElementType() {
+ return null;
+ }
+
+ /**
+ * ConversionException is thrown when a type-conversion fails; it contains
+ * an explanatory error message.
+ */
+ public static class ConversionException extends Exception {
+ private static String message(Type<?> type, Object value, String what) {
+ StringBuilder builder = new StringBuilder();
+ builder.append("expected value of type '").append(type).append("'");
+ if (what != null) {
+ builder.append(" for ").append(what);
+ }
+ builder.append(", but got '");
+ EvalUtils.printValue(value, builder);
+ builder.append("' (").append(EvalUtils.getDatatypeName(value)).append(")");
+ return builder.toString();
+ }
+
+ private ConversionException(Type<?> type, Object value, String what) {
+ super(message(type, value, what));
+ }
+
+ private ConversionException(String message) {
+ super(message);
+ }
+ }
+
+ /********************************************************************
+ * *
+ * Subclasses *
+ * *
+ ********************************************************************/
+
+ private static class ObjectType extends Type<Object> {
+ @Override
+ public Object cast(Object value) {
+ return value;
+ }
+
+ @Override
+ public String getDefaultValue() {
+ throw new UnsupportedOperationException(
+ "ObjectType has no default value");
+ }
+
+ @Override
+ public Iterable<Label> getLabels(Object value) {
+ return NO_LABELS_HERE;
+ }
+
+ @Override
+ public String toString() {
+ return "object";
+ }
+
+ @Override
+ public Object convert(Object x, String what, Label currentRule) {
+ return x;
+ }
+ }
+
+ private static class IntegerType extends Type<Integer> {
+ @Override
+ public Integer cast(Object value) {
+ return (Integer) value;
+ }
+
+ @Override
+ public Integer getDefaultValue() {
+ return 0;
+ }
+
+ @Override
+ public Iterable<Label> getLabels(Object value) {
+ return NO_LABELS_HERE;
+ }
+
+ @Override
+ public String toString() {
+ return "int";
+ }
+
+ @Override
+ public Integer convert(Object x, String what, Label currentRule)
+ throws ConversionException {
+ if (!(x instanceof Integer)) {
+ throw new ConversionException(this, x, what);
+ }
+ return (Integer) x;
+ }
+ }
+
+ private static class BooleanType extends Type<Boolean> {
+ @Override
+ public Boolean cast(Object value) {
+ return (Boolean) value;
+ }
+
+ @Override
+ public Boolean getDefaultValue() {
+ return false;
+ }
+
+ @Override
+ public Iterable<Label> getLabels(Object value) {
+ return NO_LABELS_HERE;
+ }
+
+ @Override
+ public String toString() {
+ return "boolean";
+ }
+
+ // Conversion to boolean must also tolerate integers of 0 and 1 only.
+ @Override
+ public Boolean convert(Object x, String what, Label currentRule)
+ throws ConversionException {
+ if (x instanceof Boolean) {
+ return (Boolean) x;
+ }
+ Integer xAsInteger = INTEGER.convert(x, what, currentRule);
+ if (xAsInteger == 0) {
+ return false;
+ } else if (xAsInteger == 1) {
+ return true;
+ }
+ throw new ConversionException("boolean is not one of [0, 1]");
+ }
+
+ /**
+ * Booleans attributes are converted to tags based on their names.
+ */
+ @Override
+ public Set<String> toTagSet(Object value, String name) {
+ if (value == null) {
+ String msg = "Illegal tag conversion from null on Attribute " + name + ".";
+ throw new IllegalStateException(msg);
+ }
+ String tag = (Boolean) value ? name : "no" + name;
+ return new ImmutableSet.Builder<String>()
+ .add(tag)
+ .build();
+ }
+ }
+
+ /**
+ * Tristate values are needed for cases where user intent matters.
+ *
+ * <p>Tristate values are not explicitly interchangeable with booleans and are
+ * handled explicitly as TriStates. Prefer Booleans with default values where
+ * possible. The main use case for TriState values is when a Rule's behavior
+ * must interact with a Flag value in a complicated way.</p>
+ */
+ private static class TriStateType extends Type<TriState> {
+ @Override
+ public TriState cast(Object value) {
+ return (TriState) value;
+ }
+
+ @Override
+ public TriState getDefaultValue() {
+ return TriState.AUTO;
+ }
+
+ @Override
+ public Iterable<Label> getLabels(Object value) {
+ return NO_LABELS_HERE;
+ }
+
+ @Override
+ public String toString() {
+ return "tristate";
+ }
+
+ // Like BooleanType, this must handle integers as well.
+ @Override
+ public TriState convert(Object x, String what, Label currentRule)
+ throws ConversionException {
+ if (x instanceof TriState) {
+ return (TriState) x;
+ }
+ if (x instanceof Boolean) {
+ return ((Boolean) x) ? TriState.YES : TriState.NO;
+ }
+ Integer xAsInteger = INTEGER.convert(x, what, currentRule);
+ if (xAsInteger == -1) {
+ return TriState.AUTO;
+ } else if (xAsInteger == 1) {
+ return TriState.YES;
+ } else if (xAsInteger == 0) {
+ return TriState.NO;
+ }
+ throw new ConversionException(this, x, "TriState values is not one of [-1, 0, 1]");
+ }
+ }
+
+ private static class StringType extends Type<String> {
+ @Override
+ public String cast(Object value) {
+ return (String) value;
+ }
+
+ @Override
+ public String getDefaultValue() {
+ return "";
+ }
+
+ @Override
+ public Iterable<Label> getLabels(Object value) {
+ return NO_LABELS_HERE;
+ }
+
+ @Override
+ public String toString() {
+ return "string";
+ }
+
+ @Override
+ public String convert(Object x, String what, Label currentRule)
+ throws ConversionException {
+ if (!(x instanceof String)) {
+ throw new ConversionException(this, x, what);
+ }
+ return StringCanonicalizer.intern((String) x);
+ }
+
+ /**
+ * A String is representable as a set containing its value.
+ */
+ @Override
+ public Set<String> toTagSet(Object value, String name) {
+ if (value == null) {
+ String msg = "Illegal tag conversion from null on Attribute " + name + ".";
+ throw new IllegalStateException(msg);
+ }
+ return new ImmutableSet.Builder<String>()
+ .add((String) value)
+ .build();
+ }
+ }
+
+ private static class FilesetEntryType extends Type<FilesetEntry> {
+ @Override
+ public FilesetEntry cast(Object value) {
+ return (FilesetEntry) value;
+ }
+
+ @Override
+ public FilesetEntry convert(Object x, String what, Label currentRule)
+ throws ConversionException {
+ if (!(x instanceof FilesetEntry)) {
+ throw new ConversionException(this, x, what);
+ }
+ return (FilesetEntry) x;
+ }
+
+ @Override
+ public String toString() {
+ return "FilesetEntry";
+ }
+
+ @Override
+ public FilesetEntry getDefaultValue() {
+ return null;
+ }
+
+ @Override
+ public Iterable<Label> getLabels(Object value) {
+ return cast(value).getLabels();
+ }
+ }
+
+ private static class LabelType extends Type<Label> {
+ @Override
+ public Label cast(Object value) {
+ return (Label) value;
+ }
+
+ @Override
+ public Label getDefaultValue() {
+ return null; // Labels have no default value
+ }
+
+ @Override
+ public Iterable<Label> getLabels(Object value) {
+ return ImmutableList.of(cast(value));
+ }
+
+ @Override
+ public String toString() {
+ return "label";
+ }
+
+ @Override
+ public Label convert(Object x, String what, Label currentRule)
+ throws ConversionException {
+ if (x instanceof Label) {
+ return (Label) x;
+ }
+ try {
+ return currentRule.getRelative(
+ STRING.convert(x, what, currentRule));
+ } catch (Label.SyntaxException e) {
+ throw new ConversionException("invalid label '" + x + "' in "
+ + what + ": "+ e.getMessage());
+ }
+ }
+ }
+
+ /**
+ * Like Label, LicenseType is a derived type, which is declared specially
+ * in order to allow syntax validation. It represents the licenses, as
+ * described in {@ref License}.
+ */
+ public static class LicenseType extends Type<License> {
+ @Override
+ public License cast(Object value) {
+ return (License) value;
+ }
+
+ @Override
+ public License convert(Object x, String what, Label currentRule) throws ConversionException {
+ try {
+ List<String> licenseStrings = STRING_LIST.convert(x, what);
+ return License.parseLicense(licenseStrings);
+ } catch (LicenseParsingException e) {
+ throw new ConversionException(e.getMessage());
+ }
+ }
+
+ @Override
+ public License getDefaultValue() {
+ return License.NO_LICENSE;
+ }
+
+ @Override
+ public Iterable<Label> getLabels(Object value) {
+ return NO_LABELS_HERE;
+ }
+
+ @Override
+ public String toString() {
+ return "license";
+ }
+ }
+
+ /**
+ * Like Label, Distributions is a derived type, which is declared specially
+ * in order to allow syntax validation. It represents the declared distributions
+ * of a target, as described in {@ref License}.
+ */
+ private static class Distributions extends Type<Set<DistributionType>> {
+ @SuppressWarnings("unchecked")
+ @Override
+ public Set<DistributionType> cast(Object value) {
+ return (Set<DistributionType>) value;
+ }
+
+ @Override
+ public Set<DistributionType> convert(Object x, String what, Label currentRule)
+ throws ConversionException {
+ try {
+ List<String> distribStrings = STRING_LIST.convert(x, what);
+ return License.parseDistributions(distribStrings);
+ } catch (LicenseParsingException e) {
+ throw new ConversionException(e.getMessage());
+ }
+ }
+
+ @Override
+ public Set<DistributionType> getDefaultValue() {
+ return Collections.emptySet();
+ }
+
+ @Override
+ public Iterable<Label> getLabels(Object what) {
+ return NO_LABELS_HERE;
+ }
+
+ @Override
+ public String toString() {
+ return "distributions";
+ }
+
+ @Override
+ public Type<DistributionType> getListElementType() {
+ return DISTRIBUTION;
+ }
+ }
+
+ private static class OutputType extends Type<Label> {
+ @Override
+ public Label cast(Object value) {
+ return (Label) value;
+ }
+
+ @Override
+ public Label getDefaultValue() {
+ return null;
+ }
+
+ @Override
+ public Iterable<Label> getLabels(Object value) {
+ return ImmutableList.of(cast(value));
+ }
+
+ @Override
+ public String toString() {
+ return "output";
+ }
+
+ @Override
+ public Label convert(Object x, String what, Label currentRule)
+ throws ConversionException {
+
+ String value;
+ try {
+ value = STRING.convert(x, what, currentRule);
+ } catch (ConversionException e) {
+ throw new ConversionException(this, x, what);
+ }
+ try {
+ // Enforce value is relative to the currentRule.
+ Label result = currentRule.getRelative(value);
+ if (!result.getPackageName().equals(currentRule.getPackageName())) {
+ throw new ConversionException("label '" + value + "' is not in the current package");
+ }
+ return result;
+ } catch (Label.SyntaxException e) {
+ throw new ConversionException(
+ "illegal output file name '" + value + "' in rule " + currentRule + ": "
+ + e.getMessage());
+ }
+ }
+ }
+
+ /**
+ * A type to support dictionary attributes.
+ */
+ public static class DictType<KEY, VALUE> extends Type<Map<KEY, VALUE>> {
+
+ private final Type<KEY> keyType;
+ private final Type<VALUE> valueType;
+
+ private final Map<KEY, VALUE> empty = ImmutableMap.of();
+
+ private static <KEY, VALUE> DictType<KEY, VALUE> create(
+ Type<KEY> keyType, Type<VALUE> valueType) {
+ return new DictType<>(keyType, valueType);
+ }
+
+ private DictType(Type<KEY> keyType, Type<VALUE> valueType) {
+ this.keyType = keyType;
+ this.valueType = valueType;
+ }
+
+ public Type<KEY> getKeyType() {
+ return keyType;
+ }
+
+ public Type<VALUE> getValueType() {
+ return valueType;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public Map<KEY, VALUE> cast(Object value) {
+ return (Map<KEY, VALUE>) value;
+ }
+
+ @Override
+ public String toString() {
+ return "dict(" + keyType + ", " + valueType + ")";
+ }
+
+ @Override
+ public Map<KEY, VALUE> convert(Object x, String what, Label currentRule)
+ throws ConversionException {
+ if (!(x instanceof Map<?, ?>)) {
+ throw new ConversionException(String.format(
+ "Expected a map for dictionary but got a %s", x.getClass().getName()));
+ }
+ ImmutableMap.Builder<KEY, VALUE> result = ImmutableMap.builder();
+ Map<?, ?> o = (Map<?, ?>) x;
+ for (Entry<?, ?> elem : o.entrySet()) {
+ result.put(
+ keyType.convert(elem.getKey(), "dict key element", currentRule),
+ valueType.convert(elem.getValue(), "dict value element", currentRule));
+ }
+ return result.build();
+ }
+
+ @Override
+ public Map<KEY, VALUE> getDefaultValue() {
+ return empty;
+ }
+
+ @Override
+ public Iterable<Label> getLabels(Object value) {
+ ImmutableList.Builder<Label> labels = ImmutableList.builder();
+ for (Map.Entry<KEY, VALUE> entry : cast(value).entrySet()) {
+ labels.addAll(keyType.getLabels(entry.getKey()));
+ labels.addAll(valueType.getLabels(entry.getValue()));
+ }
+ return labels.build();
+ }
+ }
+
+ public static class ListType<ELEM> extends Type<List<ELEM>> {
+
+ private final Type<ELEM> elemType;
+
+ private final List<ELEM> empty = ImmutableList.of();
+
+ private static <ELEM> ListType<ELEM> create(Type<ELEM> elemType) {
+ return new ListType<>(elemType);
+ }
+
+ private ListType(Type<ELEM> elemType) {
+ this.elemType = elemType;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public List<ELEM> cast(Object value) {
+ return (List<ELEM>) value;
+ }
+
+ @Override
+ public Type<ELEM> getListElementType() {
+ return elemType;
+ }
+
+ @Override
+ public List<ELEM> getDefaultValue() {
+ return empty;
+ }
+
+ @Override
+ public Iterable<Label> getLabels(Object value) {
+ ImmutableList.Builder<Label> labels = ImmutableList.builder();
+ for (ELEM entry : cast(value)) {
+ labels.addAll(elemType.getLabels(entry));
+ }
+ return labels.build();
+ }
+
+ @Override
+ public String toString() {
+ return "list(" + elemType + ")";
+ }
+
+ @Override
+ public List<ELEM> convert(Object x, String what, Label currentRule)
+ throws ConversionException {
+ if (!(x instanceof Iterable<?>)) {
+ throw new ConversionException(this, x, what);
+ }
+ List<ELEM> result = new ArrayList<>();
+ int index = 0;
+ for (Object elem : (Iterable<?>) x) {
+ ELEM converted = elemType.convert(elem, "element " + index + " of " + what, currentRule);
+ if (converted != null) {
+ result.add(converted);
+ } else {
+ // shouldn't happen but it does, rarely
+ String message = "Converting a list with a null element: "
+ + "element " + index + " of " + what + " in " + currentRule;
+ LoggingUtil.logToRemote(Level.WARNING, message,
+ new ConversionException(message));
+ }
+ ++index;
+ }
+ if (x instanceof GlobList<?>) {
+ return new GlobList<>(((GlobList<?>) x).getCriteria(), result);
+ } else {
+ return result;
+ }
+ }
+
+ /**
+ * A list is representable as a tag set as the contents of itself expressed
+ * as Strings. So a List<String> is effectively converted to a Set<String>.
+ */
+ @Override
+ public Set<String> toTagSet(Object items, String name) {
+ if (items == null) {
+ String msg = "Illegal tag conversion from null on Attribute" + name + ".";
+ throw new IllegalStateException(msg);
+ }
+ Set<String> tags = new LinkedHashSet<>();
+ @SuppressWarnings("unchecked")
+ List<ELEM> itemsAsListofElem = (List<ELEM>) items;
+ for (ELEM element : itemsAsListofElem) {
+ tags.add(element.toString());
+ }
+ return tags;
+ }
+ }
+
+ public static class ObjectListType extends ListType<Object> {
+
+ private static final Type<Object> elemType = new ObjectType();
+
+ private ObjectListType() {
+ super(elemType);
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public List<Object> convert(Object x, String what, Label currentRule)
+ throws ConversionException {
+ if (x instanceof List) {
+ return (List<Object>) x;
+ } else if (x instanceof Iterable) {
+ return ImmutableList.copyOf((Iterable<?>) x);
+ } else {
+ throw new ConversionException(this, x, what);
+ }
+ }
+ }
+
+ /**
+ * The type of a general list.
+ */
+ public static final ListType<Object> LIST = new ListType<>(new ObjectType());
+
+ /**
+ * Returns whether the specified type is a label type or not.
+ */
+ public static boolean isLabelType(Type<?> type) {
+ return type == LABEL || type == LABEL_LIST
+ || type == NODEP_LABEL || type == NODEP_LABEL_LIST
+ || type == LABEL_LIST_DICT || type == FILESET_ENTRY_LIST;
+ }
+
+ /**
+ * Special Type that represents a selector expression for configurable attributes. Holds a
+ * mapping of <Label, T> entries, where keys are configurability patterns and values are
+ * objects of the attribute's native Type.
+ */
+ public static final class Selector<T> {
+
+ private final Type<T> originalType;
+ private final Map<Label, T> map;
+ private final Label defaultConditionLabel;
+ private final boolean hasDefaultCondition;
+
+ /**
+ * Value to use when none of an attribute's selection criteria match.
+ */
+ @VisibleForTesting
+ public static final String DEFAULT_CONDITION_KEY = "//conditions:default";
+
+ @VisibleForTesting
+ Selector(Object x, String what, @Nullable Label currentRule, Type<T> originalType)
+ throws ConversionException {
+ Preconditions.checkState(x instanceof Map<?, ?>);
+
+ try {
+ defaultConditionLabel = Label.parseAbsolute(DEFAULT_CONDITION_KEY);
+ } catch (Label.SyntaxException e) {
+ throw new IllegalStateException(DEFAULT_CONDITION_KEY + " is not a valid label");
+ }
+
+
+ this.originalType = originalType;
+ Map<Label, T> result = Maps.newLinkedHashMap();
+ boolean foundDefaultCondition = false;
+ for (Entry<?, ?> entry : ((Map<?, ?>) x).entrySet()) {
+ Label key = LABEL.convert(entry.getKey(), what, currentRule);
+ if (key.equals(defaultConditionLabel)) {
+ foundDefaultCondition = true;
+ }
+ result.put(key, originalType.convert(entry.getValue(), what, currentRule));
+ }
+ map = ImmutableMap.copyOf(result);
+ hasDefaultCondition = foundDefaultCondition;
+ }
+
+ /**
+ * Returns the selector's (configurability pattern --gt; matching values) map.
+ */
+ public Map<Label, T> getEntries() {
+ return map;
+ }
+
+ /**
+ * Returns the value to use when none of the attribute's selection keys match.
+ */
+ public T getDefault() {
+ return map.get(defaultConditionLabel);
+ }
+
+ /**
+ * Returns whether or not this selector has a default condition.
+ */
+ public boolean hasDefault() {
+ return hasDefaultCondition;
+ }
+
+ /**
+ * Returns the native Type for this attribute (i.e. what this would be if it wasn't a
+ * selector expression).
+ */
+ public Type<T> getOriginalType() {
+ return originalType;
+ }
+
+ /**
+ * Returns true for labels that are "reserved selector key words" and not intended to
+ * map to actual targets.
+ */
+ public static boolean isReservedLabel(Label label) {
+ return label.toString().equals(DEFAULT_CONDITION_KEY);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/pkgcache/CompileOneDependencyTransformer.java b/src/main/java/com/google/devtools/build/lib/pkgcache/CompileOneDependencyTransformer.java
new file mode 100644
index 0000000..ae14f18
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/pkgcache/CompileOneDependencyTransformer.java
@@ -0,0 +1,186 @@
+// Copyright 2014 Google Inc. 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.pkgcache;
+
+import com.google.common.collect.Lists;
+import com.google.devtools.build.lib.cmdline.ResolvedTargets;
+import com.google.devtools.build.lib.cmdline.TargetParsingException;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.packages.FileTarget;
+import com.google.devtools.build.lib.packages.NoSuchPackageException;
+import com.google.devtools.build.lib.packages.NoSuchThingException;
+import com.google.devtools.build.lib.packages.Package;
+import com.google.devtools.build.lib.packages.RawAttributeMapper;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.syntax.Label;
+
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * Implementation of --compile_one_dependency.
+ */
+final class CompileOneDependencyTransformer {
+
+ private final PackageManager pkgManager;
+
+ public CompileOneDependencyTransformer(PackageManager pkgManager) {
+ this.pkgManager = pkgManager;
+ }
+
+ /**
+ * For each input file in the original result, returns a rule in the same package which has the
+ * input file as a source.
+ */
+ public ResolvedTargets<Target> transformCompileOneDependency(EventHandler eventHandler,
+ ResolvedTargets<Target> original) throws TargetParsingException {
+ if (original.hasError()) {
+ return original;
+ }
+ ResolvedTargets.Builder<Target> builder = ResolvedTargets.builder();
+ for (Target target : original.getTargets()) {
+ builder.add(transformCompileOneDependency(eventHandler, target));
+ }
+ return builder.build();
+ }
+
+ /**
+ * Returns a list of rules in the given package sorted by BUILD file order. When
+ * multiple rules depend on a target, we choose the first match in this list (after
+ * filtering for preferred dependencies - see below).
+ *
+ * <p>Rules with configurable attributes are skipped, as this code doesn't know which
+ * configuration will be applied, so it can't reliably determine what their 'srcs'
+ * will look like.
+ */
+ private Iterable<Rule> getOrderedRuleList(Package pkg) {
+ List<Rule> orderedList = Lists.newArrayList();
+ for (Rule rule : pkg.getTargets(Rule.class)) {
+ if (!rule.hasConfigurableAttributes()) {
+ orderedList.add(rule);
+ }
+ }
+
+ Collections.sort(orderedList, new Comparator<Rule>() {
+ @Override
+ public int compare(Rule o1, Rule o2) {
+ return Integer.compare(
+ o1.getLocation().getStartOffset(),
+ o2.getLocation().getStartOffset());
+ }
+ });
+ return orderedList;
+ }
+
+ private Target transformCompileOneDependency(EventHandler eventHandler, Target target)
+ throws TargetParsingException {
+ if (!(target instanceof FileTarget)) {
+ throw new TargetParsingException("--compile_one_dependency target '" +
+ target.getLabel() + "' must be a file");
+ }
+
+ Package pkg;
+ try {
+ pkg = pkgManager.getLoadedPackage(target.getLabel().getPackageIdentifier());
+ } catch (NoSuchPackageException e) {
+ throw new IllegalStateException(e);
+ }
+
+ Iterable<Rule> orderedRuleList = getOrderedRuleList(pkg);
+ // Consuming rule to return if no "preferred" rules have been found.
+ Rule fallbackRule = null;
+
+ for (Rule rule : orderedRuleList) {
+ try {
+ // The call to getSrcTargets here can be removed in favor of the
+ // rule.getLabels() call below once we update "srcs" for all rules.
+ if (SrcTargetUtil.getSrcTargets(eventHandler, rule, pkgManager).contains(target)) {
+ if (rule.getRuleClassObject().isPreferredDependency(target.getName())) {
+ return rule;
+ } else if (fallbackRule == null) {
+ fallbackRule = rule;
+ }
+ }
+ } catch (NoSuchThingException e) {
+ // Nothing to see here. Move along.
+ } catch (InterruptedException e) {
+ throw new TargetParsingException("interrupted");
+ }
+ }
+
+ Rule result = null;
+
+ // For each rule, see if it has directCompileTimeInputAttribute,
+ // and if so check the targets listed in that attribute match the label.
+ for (Rule rule : orderedRuleList) {
+ if (rule.getLabels(Rule.DIRECT_COMPILE_TIME_INPUT).contains(target.getLabel())) {
+ if (rule.getRuleClassObject().isPreferredDependency(target.getName())) {
+ result = rule;
+ } else if (fallbackRule == null) {
+ fallbackRule = rule;
+ }
+ }
+ }
+
+ if (result == null) {
+ result = fallbackRule;
+ }
+
+ if (result == null) {
+ throw new TargetParsingException(
+ "Couldn't find dependency on target '" + target.getLabel() + "'");
+ }
+
+ try {
+ // If the rule has source targets, return it.
+ if (!SrcTargetUtil.getSrcTargets(eventHandler, result, pkgManager).isEmpty()) {
+ return result;
+ }
+ } catch (NoSuchThingException e) {
+ throw new TargetParsingException(
+ "Couldn't find dependency on target '" + target.getLabel() + "'");
+ } catch (InterruptedException e) {
+ throw new TargetParsingException("interrupted");
+ }
+
+ for (Rule rule : orderedRuleList) {
+ RawAttributeMapper attributes = RawAttributeMapper.of(rule);
+ // We don't know what configuration we're using at this point, so we can't be sure
+ // which deps/srcs apply to this invocation if they're configurable for this rule.
+ // So exclude such rules for consideration.
+ if (attributes.isConfigurable("deps", Type.LABEL_LIST)
+ || attributes.isConfigurable("srcs", Type.LABEL_LIST)) {
+ continue;
+ }
+ RuleClass ruleClass = rule.getRuleClassObject();
+ if (ruleClass.hasAttr("deps", Type.LABEL_LIST) &&
+ ruleClass.hasAttr("srcs", Type.LABEL_LIST)) {
+ for (Label dep : attributes.get("deps", Type.LABEL_LIST)) {
+ if (dep.equals(result.getLabel())) {
+ if (!attributes.get("srcs", Type.LABEL_LIST).isEmpty()) {
+ return rule;
+ }
+ }
+ }
+ }
+ }
+
+ throw new TargetParsingException(
+ "Couldn't find dependency on target '" + target.getLabel() + "'");
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/pkgcache/FilteringPolicies.java b/src/main/java/com/google/devtools/build/lib/pkgcache/FilteringPolicies.java
new file mode 100644
index 0000000..df01326
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/pkgcache/FilteringPolicies.java
@@ -0,0 +1,126 @@
+// Copyright 2014 Google Inc. 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.pkgcache;
+
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.packages.TargetUtils;
+
+import java.util.Objects;
+
+/**
+ * Utility class for predefined filtering policies.
+ */
+public final class FilteringPolicies {
+
+ private FilteringPolicies() {
+ }
+
+ /**
+ * Base class for singleton filtering policies.
+ */
+ private abstract static class AbstractFilteringPolicy implements FilteringPolicy {
+ @Override
+ public int hashCode() {
+ return getClass().getSimpleName().hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (obj == this) {
+ return true;
+ }
+ return getClass().equals(obj.getClass());
+ }
+ }
+
+ private static class NoFilter extends AbstractFilteringPolicy {
+ @Override
+ public boolean shouldRetain(Target target, boolean explicit) {
+ return true;
+ }
+ }
+
+ public static final FilteringPolicy NO_FILTER = new NoFilter();
+
+ private static class FilterManualAndObsolete extends AbstractFilteringPolicy {
+ @Override
+ public boolean shouldRetain(Target target, boolean explicit) {
+ return explicit || !(TargetUtils.hasManualTag(target) || TargetUtils.isObsolete(target));
+ }
+ }
+
+ public static final FilteringPolicy FILTER_MANUAL_AND_OBSOLETE = new FilterManualAndObsolete();
+
+ private static class FilterTests extends AbstractFilteringPolicy {
+ @Override
+ public boolean shouldRetain(Target target, boolean explicit) {
+ return TargetUtils.isTestOrTestSuiteRule(target)
+ && FILTER_MANUAL_AND_OBSOLETE.shouldRetain(target, explicit);
+ }
+ }
+
+ public static final FilteringPolicy FILTER_TESTS = new FilterTests();
+
+ private static class RulesOnly extends AbstractFilteringPolicy {
+ @Override
+ public boolean shouldRetain(Target target, boolean explicit) {
+ return target instanceof Rule;
+ }
+ }
+
+ public static final FilteringPolicy RULES_ONLY = new RulesOnly();
+
+ /**
+ * Returns the result of applying y, if target passes x.
+ */
+ public static FilteringPolicy and(final FilteringPolicy x,
+ final FilteringPolicy y) {
+ return new AndFilteringPolicy(x, y);
+ }
+
+ private static class AndFilteringPolicy implements FilteringPolicy {
+ private final FilteringPolicy firstPolicy;
+ private final FilteringPolicy secondPolicy;
+
+ public AndFilteringPolicy(FilteringPolicy firstPolicy, FilteringPolicy secondPolicy) {
+ this.firstPolicy = Preconditions.checkNotNull(firstPolicy);
+ this.secondPolicy = Preconditions.checkNotNull(secondPolicy);
+ }
+
+ @Override
+ public boolean shouldRetain(Target target, boolean explicit) {
+ return firstPolicy.shouldRetain(target, explicit)
+ && secondPolicy.shouldRetain(target, explicit);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(firstPolicy, secondPolicy);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof AndFilteringPolicy)) {
+ return false;
+ }
+ AndFilteringPolicy other = (AndFilteringPolicy) obj;
+ return other.firstPolicy.equals(firstPolicy) && other.secondPolicy.equals(secondPolicy);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/pkgcache/FilteringPolicy.java b/src/main/java/com/google/devtools/build/lib/pkgcache/FilteringPolicy.java
new file mode 100644
index 0000000..ac27fb0
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/pkgcache/FilteringPolicy.java
@@ -0,0 +1,35 @@
+// Copyright 2014 Google Inc. 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.pkgcache;
+
+import com.google.devtools.build.lib.concurrent.ThreadSafety;
+import com.google.devtools.build.lib.packages.Target;
+
+import java.io.Serializable;
+
+/**
+ * A filtering policy defines how target patterns are matched. For instance, we may wish to select
+ * only tests, no tests, or remove obsolete targets.
+ */
+public interface FilteringPolicy extends Serializable {
+
+ /**
+ * Returns true if this target should be retained.
+ *
+ * @param explicit true iff the label was specified explicitly, as opposed to being discovered by
+ * a wildcard.
+ */
+ @ThreadSafety.ThreadSafe
+ boolean shouldRetain(Target target, boolean explicit);
+}
diff --git a/src/main/java/com/google/devtools/build/lib/pkgcache/LoadedPackage.java b/src/main/java/com/google/devtools/build/lib/pkgcache/LoadedPackage.java
new file mode 100644
index 0000000..3c6f70f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/pkgcache/LoadedPackage.java
@@ -0,0 +1,46 @@
+// Copyright 2014 Google Inc. 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.pkgcache;
+
+import com.google.devtools.build.lib.events.Event;
+
+/**
+ * A loaded package that can verify whether it is still up to date.
+ */
+interface LoadedPackage {
+ /**
+ * Returns the actual loaded {@link Package} object.
+ */
+ Package getPackage();
+
+ /**
+ * Returns true iff the entry is still valid.
+ *
+ * <p>An entry is valid when the package it denotes has not been moved, deleted, or changed. This
+ * requires disk I/O to fetch metadata and re-evaluate globs.
+ */
+ boolean isValid() throws InterruptedException;
+
+ /**
+ * Returns true iff the the contents of the package are guaranteed not to have changed after
+ * between {@link #isValid()} calls and syncs of the associated package loader.
+ */
+ boolean contentsCouldNotHaveChanged();
+
+ /**
+ * Returns the set of events (sorted by the order they were reported) that occurred during the
+ * loading of the package.
+ */
+ Iterable<Event> getEvents();
+}
diff --git a/src/main/java/com/google/devtools/build/lib/pkgcache/LoadedPackageProvider.java b/src/main/java/com/google/devtools/build/lib/pkgcache/LoadedPackageProvider.java
new file mode 100644
index 0000000..1c51810
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/pkgcache/LoadedPackageProvider.java
@@ -0,0 +1,50 @@
+// Copyright 2014 Google Inc. 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.pkgcache;
+
+import com.google.devtools.build.lib.packages.NoSuchPackageException;
+import com.google.devtools.build.lib.packages.NoSuchTargetException;
+import com.google.devtools.build.lib.packages.Package;
+import com.google.devtools.build.lib.packages.PackageIdentifier;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.syntax.Label;
+
+/**
+ * Read-only API for retrieving packages, i.e., calling this API should not result in packages being
+ * loaded.
+ *
+ * <p><b>Concurrency</b>: Implementations should be thread-safe.
+ */
+// TODO(bazel-team): Skyframe doesn't really implement this - can we remove it?
+public interface LoadedPackageProvider {
+
+ /**
+ * Returns a package if it was recently loaded, i.e., since the most recent cache sync. This
+ * throws an exception if the package was not loaded, even if it exists on disk.
+ */
+ Package getLoadedPackage(PackageIdentifier packageIdentifier) throws NoSuchPackageException;
+
+ /**
+ * Returns a target if it was recently loaded, i.e., since the most recent cache sync. This
+ * throws an exception if the target was not loaded or not validated, even if it exists in the
+ * surrounding package.
+ */
+ Target getLoadedTarget(Label label) throws NoSuchPackageException, NoSuchTargetException;
+
+ /**
+ * Returns true iff the specified target is current, i.e. a request for its label using {@link
+ * #getLoadedTarget} would return the same target instance.
+ */
+ boolean isTargetCurrent(Target target);
+}
diff --git a/src/main/java/com/google/devtools/build/lib/pkgcache/LoadingFailedException.java b/src/main/java/com/google/devtools/build/lib/pkgcache/LoadingFailedException.java
new file mode 100644
index 0000000..537746b
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/pkgcache/LoadingFailedException.java
@@ -0,0 +1,29 @@
+// Copyright 2014 Google Inc. 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.pkgcache;
+
+/**
+ * An exception indicating that there was a problem during the loading phase for one or more
+ * targets in such a way that the build cannot proceed (for example because keep_going is disabled).
+ */
+public class LoadingFailedException extends Exception {
+
+ public LoadingFailedException(String message) {
+ super(message);
+ }
+
+ public LoadingFailedException(String message, Throwable cause) {
+ super(message + ": " + cause.getMessage(), cause);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/pkgcache/LoadingFailureEvent.java b/src/main/java/com/google/devtools/build/lib/pkgcache/LoadingFailureEvent.java
new file mode 100644
index 0000000..d7e0256
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/pkgcache/LoadingFailureEvent.java
@@ -0,0 +1,39 @@
+// Copyright 2014 Google Inc. 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.pkgcache;
+
+import com.google.devtools.build.lib.syntax.Label;
+
+/**
+ * This event is fired during the build, when it becomes known that the loading
+ * of a target cannot be completed because of an error in one of its
+ * dependencies.
+ */
+public class LoadingFailureEvent {
+ private final Label failedTarget;
+ private final Label failureReason;
+
+ public LoadingFailureEvent(Label failedTarget, Label failureReason) {
+ this.failedTarget = failedTarget;
+ this.failureReason = failureReason;
+ }
+
+ public Label getFailedTarget() {
+ return failedTarget;
+ }
+
+ public Label getFailureReason() {
+ return failureReason;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/pkgcache/LoadingPhaseCompleteEvent.java b/src/main/java/com/google/devtools/build/lib/pkgcache/LoadingPhaseCompleteEvent.java
new file mode 100644
index 0000000..19c22ec
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/pkgcache/LoadingPhaseCompleteEvent.java
@@ -0,0 +1,86 @@
+// Copyright 2014 Google Inc. 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.pkgcache;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.syntax.Label;
+
+import java.util.Collection;
+
+/**
+ * This event is fired after the loading phase is complete.
+ */
+public class LoadingPhaseCompleteEvent {
+ private final Collection<Target> targets;
+ private final Collection<Target> filteredTargets;
+ private final PackageManager.PackageManagerStatistics pkgManagerStats;
+ private final long timeInMs;
+
+ /**
+ * Construct the event.
+ *
+ * @param targets the set of active targets that remain
+ * @param pkgManagerStats statistics about the package cache
+ */
+ public LoadingPhaseCompleteEvent(Collection<Target> targets, Collection<Target> filteredTargets,
+ PackageManager.PackageManagerStatistics pkgManagerStats, long timeInMs) {
+ this.targets = targets;
+ this.filteredTargets = filteredTargets;
+ this.pkgManagerStats = pkgManagerStats;
+ this.timeInMs = timeInMs;
+ }
+
+ /**
+ * @return The set of active targets remaining, which is a subset of the
+ * targets we attempted to load.
+ */
+ public Collection<Target> getTargets() {
+ return targets;
+ }
+
+ /**
+ * @return The set of filtered targets.
+ */
+ public Collection<Target> getFilteredTargets() {
+ return filteredTargets;
+ }
+
+ /**
+ * @return The set of active target labels remaining, which is a subset of the
+ * targets we attempted to load.
+ */
+ public Iterable<Label> getLabels() {
+ return Iterables.transform(targets, TO_LABEL);
+ }
+
+ public long getTimeInMs() {
+ return timeInMs;
+ }
+
+ /**
+ * Returns the PackageCache statistics.
+ */
+ public PackageManager.PackageManagerStatistics getPkgManagerStats() {
+ return pkgManagerStats;
+ }
+
+ private static final Function<Target, Label> TO_LABEL = new Function<Target, Label>() {
+ @Override
+ public Label apply(Target input) {
+ return input.getLabel();
+ }
+ };
+}
diff --git a/src/main/java/com/google/devtools/build/lib/pkgcache/LoadingPhaseRunner.java b/src/main/java/com/google/devtools/build/lib/pkgcache/LoadingPhaseRunner.java
new file mode 100644
index 0000000..4d8eea6
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/pkgcache/LoadingPhaseRunner.java
@@ -0,0 +1,661 @@
+// Copyright 2014 Google Inc. 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.pkgcache;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+import com.google.common.base.Stopwatch;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Sets;
+import com.google.common.eventbus.EventBus;
+import com.google.devtools.build.lib.cmdline.ResolvedTargets;
+import com.google.devtools.build.lib.cmdline.TargetParsingException;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadCompatible;
+import com.google.devtools.build.lib.events.DelegatingEventHandler;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.packages.NoSuchPackageException;
+import com.google.devtools.build.lib.packages.NoSuchTargetException;
+import com.google.devtools.build.lib.packages.NoSuchThingException;
+import com.google.devtools.build.lib.packages.NonconfigurableAttributeMapper;
+import com.google.devtools.build.lib.packages.Package;
+import com.google.devtools.build.lib.packages.PackageIdentifier;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.packages.TestSize;
+import com.google.devtools.build.lib.packages.TestTargetUtils;
+import com.google.devtools.build.lib.packages.TestTimeout;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.common.options.Converters.CommaSeparatedOptionListConverter;
+import com.google.devtools.common.options.Option;
+import com.google.devtools.common.options.OptionsBase;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Logger;
+
+import javax.annotation.Nullable;
+
+/**
+ * Implements the loading phase; responsible for:
+ * <ul>
+ * <li>target pattern evaluation
+ * <li>test suite expansion
+ * <li>loading the labels needed to construct the build configuration
+ * <li>loading the labels needed for the analysis with the build configuration
+ * <li>loading the transitive closure of the targets and the configuration labels
+ * </ul>
+ *
+ * <p>In order to ensure correctness of incremental loading and of full cache hits, this class is
+ * very restrictive about access to its internal state and to its collaborators. In particular, none
+ * of the collaborators of this class may change in incompatible ways, such as changing the relative
+ * working directory for the target pattern parser, without notifying this class.
+ *
+ * <p>For full caching, this class tracks the exact values of all inputs to the loading phase. To
+ * maximize caching, it is vital that these change as rarely as possible.
+ */
+public class LoadingPhaseRunner {
+
+ /**
+ * Loading phase options.
+ */
+ public static class Options extends OptionsBase {
+
+ @Option(name = "loading_phase_threads",
+ defaultValue = "200",
+ category = "undocumented",
+ help = "Number of parallel threads to use for the loading phase.")
+ public int loadingPhaseThreads;
+
+ @Option(name = "build_tests_only",
+ defaultValue = "false",
+ category = "what",
+ help = "If specified, only *_test and test_suite rules will be built "
+ + "and other targets specified on the command line will be ignored. "
+ + "By default everything that was requested will be built.")
+ public boolean buildTestsOnly;
+
+ @Option(name = "compile_one_dependency",
+ defaultValue = "false",
+ category = "what",
+ help = "Compile a single dependency of the argument files. "
+ + "This is useful for syntax checking source files in IDEs, "
+ + "for example, by rebuilding a single target that depends on "
+ + "the source file to detect errors as early as possible in the "
+ + "edit/build/test cycle. This argument affects the way all "
+ + "non-flag arguments are interpreted; instead of being targets "
+ + "to build they are source filenames. For each source filename "
+ + "an arbitrary target that depends on it will be built.")
+ public boolean compileOneDependency;
+
+ @Option(name = "test_tag_filters",
+ converter = CommaSeparatedOptionListConverter.class,
+ defaultValue = "",
+ category = "what",
+ help = "Specifies a comma-separated list of test tags. Each tag can be optionally " +
+ "preceded with '-' to specify excluded tags. Only those test targets will be " +
+ "found that contain at least one included tag and do not contain any excluded " +
+ "tags. This option affects --build_tests_only behavior and the test command."
+ )
+ public List<String> testTagFilterList;
+
+ @Option(name = "test_size_filters",
+ converter = TestSize.TestSizeFilterConverter.class,
+ defaultValue = "",
+ category = "what",
+ help = "Specifies a comma-separated list of test sizes. Each size can be optionally " +
+ "preceded with '-' to specify excluded sizes. Only those test targets will be " +
+ "found that contain at least one included size and do not contain any excluded " +
+ "sizes. This option affects --build_tests_only behavior and the test command."
+ )
+ public Set<TestSize> testSizeFilterSet;
+
+ @Option(name = "test_timeout_filters",
+ converter = TestTimeout.TestTimeoutFilterConverter.class,
+ defaultValue = "",
+ category = "what",
+ help = "Specifies a comma-separated list of test timeouts. Each timeout can be " +
+ "optionally preceded with '-' to specify excluded timeouts. Only those test " +
+ "targets will be found that contain at least one included timeout and do not " +
+ "contain any excluded timeouts. This option affects --build_tests_only behavior " +
+ "and the test command."
+ )
+ public Set<TestTimeout> testTimeoutFilterSet;
+
+ @Option(name = "test_lang_filters",
+ converter = CommaSeparatedOptionListConverter.class,
+ defaultValue = "",
+ category = "what",
+ help = "Specifies a comma-separated list of test languages. Each language can be " +
+ "optionally preceded with '-' to specify excluded languages. Only those " +
+ "test targets will be found that are written in the specified languages. " +
+ "The name used for each language should be the same as the language prefix in the " +
+ "*_test rule, e.g. one of 'cc', 'java', 'py', etc." +
+ "This option affects --build_tests_only behavior and the test command."
+ )
+ public List<String> testLangFilterList;
+ }
+
+ /**
+ * A callback interface to notify the caller about specific events.
+ * TODO(bazel-team): maybe we should use the EventBus instead?
+ */
+ public interface Callback {
+ /**
+ * Called after the target patterns have been resolved to give the caller a chance to validate
+ * the list before proceeding.
+ */
+ void notifyTargets(Collection<Target> targets) throws LoadingFailedException;
+
+ /**
+ * Called after loading has finished, to notify the caller about the visited packages.
+ *
+ * <p>The set of visited packages is the set of packages in the transitive closure of the
+ * union of the top level targets.
+ */
+ void notifyVisitedPackages(Set<PackageIdentifier> visitedPackages);
+ }
+
+ /**
+ * The result of the loading phase, i.e., whether there were errors, and which targets were
+ * successfully loaded, plus some related metadata.
+ */
+ public static final class LoadingResult {
+ private final boolean hasTargetPatternError;
+ private final boolean hasLoadingError;
+ private final ImmutableSet<Target> targetsToAnalyze;
+ private final ImmutableSet<Target> testsToRun;
+ private final ImmutableMap<PackageIdentifier, Path> packageRoots;
+ // TODO(bazel-team): consider moving this to LoadedPackageProvider
+ private final ImmutableSet<PackageIdentifier> visitedPackages;
+
+ public LoadingResult(boolean hasTargetPatternError, boolean hasLoadingError,
+ Collection<Target> targetsToAnalyze, Collection<Target> testsToRun,
+ ImmutableMap<PackageIdentifier, Path> packageRoots,
+ Set<PackageIdentifier> visitedPackages) {
+ this.hasTargetPatternError = hasTargetPatternError;
+ this.hasLoadingError = hasLoadingError;
+ this.targetsToAnalyze =
+ targetsToAnalyze == null ? null : ImmutableSet.copyOf(targetsToAnalyze);
+ this.testsToRun = testsToRun == null ? null : ImmutableSet.copyOf(testsToRun);
+ this.packageRoots = packageRoots;
+ this.visitedPackages = ImmutableSet.copyOf(visitedPackages);
+ }
+
+ /** Whether there were errors during target pattern evaluation. */
+ public boolean hasTargetPatternError() {
+ return hasTargetPatternError;
+ }
+
+ /** Whether there were errors during the loading phase. */
+ public boolean hasLoadingError() {
+ return hasLoadingError;
+ }
+
+ /** Successfully loaded targets that should be built. */
+ public Collection<Target> getTargets() {
+ return targetsToAnalyze;
+ }
+
+ /** Successfully loaded targets that should be run as tests. Must be a subset of the targets. */
+ public Collection<Target> getTestsToRun() {
+ return testsToRun;
+ }
+
+ /**
+ * The map from package names to the package root where each package was found; this is used to
+ * set up the symlink tree.
+ */
+ public ImmutableMap<PackageIdentifier, Path> getPackageRoots() {
+ return packageRoots;
+ }
+
+ /**
+ * Returns all packages that were visited during this loading phase.
+ *
+ * <p>We use this to decide when to evict ConfiguredTarget nodes from the graph.
+ */
+ @ThreadCompatible
+ private ImmutableSet<PackageIdentifier> getVisitedPackages() {
+ return visitedPackages;
+ }
+ }
+
+ private static final class ParseFailureListenerImpl extends DelegatingEventHandler
+ implements ParseFailureListener {
+ private final EventBus eventBus;
+
+ private ParseFailureListenerImpl(EventHandler delegate, EventBus eventBus) {
+ super(delegate);
+ this.eventBus = eventBus;
+ }
+
+ @Override
+ public void parsingError(String targetPattern, String message) {
+ if (eventBus != null) {
+ eventBus.post(new ParsingFailedEvent(targetPattern, message));
+ }
+ }
+ }
+
+ private static final Logger LOG = Logger.getLogger(LoadingPhaseRunner.class.getName());
+
+ private final PackageManager packageManager;
+ private final TargetPatternEvaluator targetPatternEvaluator;
+ private final Set<String> ruleNames;
+ private final TransitivePackageLoader pkgLoader;
+
+ public LoadingPhaseRunner(PackageManager packageManager,
+ Set<String> ruleNames) {
+ this.packageManager = packageManager;
+ this.targetPatternEvaluator = packageManager.getTargetPatternEvaluator();
+ this.ruleNames = ruleNames;
+ this.pkgLoader = packageManager.newTransitiveLoader();
+ }
+
+ public TargetPatternEvaluator getTargetPatternEvaluator() {
+ return targetPatternEvaluator;
+ }
+
+ public void updatePatternEvaluator(PathFragment relativeWorkingDirectory) {
+ targetPatternEvaluator.updateOffset(relativeWorkingDirectory);
+ }
+
+ /**
+ * This method only exists for the benefit of InfoCommand, which needs to construct
+ * a {@code BuildConfigurationCollection} without running a full loading phase. Don't
+ * add any more clients; instead, we should change info so that it doesn't need the configuration.
+ */
+ public LoadedPackageProvider loadForConfigurations(EventHandler eventHandler,
+ Set<Label> labelsToLoad, boolean keepGoing) throws InterruptedException {
+ // Use a new Label Visitor here to avoid erasing the cache on the existing one.
+ TransitivePackageLoader transitivePackageLoader = packageManager.newTransitiveLoader();
+ boolean loadingSuccessful = transitivePackageLoader.sync(
+ eventHandler, ImmutableSet.<Target>of(),
+ labelsToLoad, keepGoing, /*parallelThreads=*/10,
+ /*maxDepth=*/Integer.MAX_VALUE);
+ return loadingSuccessful ? packageManager : null;
+ }
+
+ /**
+ * Performs target pattern evaluation, test suite expansion (if requested), and loads the
+ * transitive closure of the resulting targets as well as of the targets needed to use the
+ * given build configuration provider.
+ */
+ public LoadingResult execute(EventHandler eventHandler, EventBus eventBus,
+ List<String> targetPatterns, Options options,
+ ListMultimap<String, Label> labelsToLoadUnconditionally, boolean keepGoing,
+ boolean determineTests, @Nullable Callback callback)
+ throws TargetParsingException, LoadingFailedException, InterruptedException {
+ LOG.info("Starting pattern evaluation");
+ Stopwatch timer = Stopwatch.createStarted();
+ if (options.buildTestsOnly && options.compileOneDependency) {
+ throw new LoadingFailedException("--compile_one_dependency cannot be used together with "
+ + "the --build_tests_only option or the 'bazel test' command ");
+ }
+
+ EventHandler parseFailureListener = new ParseFailureListenerImpl(eventHandler, eventBus);
+ // Determine targets to build:
+ ResolvedTargets<Target> targets = getTargetsToBuild(parseFailureListener,
+ targetPatterns, options.compileOneDependency, keepGoing);
+
+ ImmutableSet<Target> filteredTargets = targets.getFilteredTargets();
+
+ boolean buildTestsOnly = options.buildTestsOnly;
+ ImmutableSet<Target> testsToRun = null;
+ ImmutableSet<Target> testFilteredTargets = ImmutableSet.of();
+
+ // Now we have a list of targets to build. If the --build_tests_only option was specified or we
+ // want to run tests, we need to determine the list of targets to test. For that, we remove
+ // manual tests and apply the command line filters. Also, if --build_tests_only is specified,
+ // then the list of filtered targets will be set as build list as well.
+ if (determineTests || buildTestsOnly) {
+ // Parse the targets to get the tests.
+ ResolvedTargets<Target> testTargets = determineTests(parseFailureListener,
+ targetPatterns, options, keepGoing);
+ if (testTargets.getTargets().isEmpty() && !testTargets.getFilteredTargets().isEmpty()) {
+ eventHandler.handle(Event.warn("All specified test targets were excluded by filters"));
+ }
+
+ if (buildTestsOnly) {
+ // Replace original targets to build with test targets, so that only targets that are
+ // actually going to be built are loaded in the loading phase. Note that this has a side
+ // effect that any test_suite target requested to be built is replaced by the set of *_test
+ // targets it represents; for example, this affects the status and the summary reports.
+ Set<Target> allFilteredTargets = new HashSet<>();
+ allFilteredTargets.addAll(targets.getTargets());
+ allFilteredTargets.addAll(targets.getFilteredTargets());
+ allFilteredTargets.removeAll(testTargets.getTargets());
+ allFilteredTargets.addAll(testTargets.getFilteredTargets());
+ testFilteredTargets = ImmutableSet.copyOf(allFilteredTargets);
+ filteredTargets = ImmutableSet.of();
+
+ targets = ResolvedTargets.<Target>builder()
+ .merge(testTargets)
+ .mergeError(targets.hasError())
+ .build();
+ if (determineTests) {
+ testsToRun = testTargets.getTargets();
+ }
+ } else /*if (determineTests)*/ {
+ testsToRun = testTargets.getTargets();
+ targets = ResolvedTargets.<Target>builder()
+ .merge(targets)
+ // Avoid merge() here which would remove the filteredTargets from the targets.
+ .addAll(testsToRun)
+ .mergeError(testTargets.hasError())
+ .build();
+ // filteredTargets is correct in this case - it cannot contain tests that got back in
+ // through test_suite expansion, because the test determination would also filter those out.
+ // However, that's not obvious, and it might be better to explicitly recompute it.
+ }
+ if (testsToRun != null) {
+ // Note that testsToRun can still be null here, if buildTestsOnly && !shouldRunTests.
+ Preconditions.checkState(targets.getTargets().containsAll(testsToRun));
+ }
+ }
+
+ eventBus.post(new TargetParsingCompleteEvent(targets.getTargets(),
+ filteredTargets, testFilteredTargets,
+ timer.stop().elapsed(TimeUnit.MILLISECONDS)));
+
+ if (targets.hasError()) {
+ eventHandler.handle(Event.warn("Target pattern parsing failed. Continuing anyway"));
+ }
+
+ if (callback != null) {
+ callback.notifyTargets(targets.getTargets());
+ }
+
+ maybeReportDeprecation(eventHandler, targets.getTargets());
+
+ // Load the transitive closure of all targets.
+ LoadingResult result = doLoadingPhase(eventHandler, eventBus, targets.getTargets(),
+ testsToRun, labelsToLoadUnconditionally, keepGoing, options.loadingPhaseThreads,
+ targets.hasError());
+
+ if (callback != null) {
+ callback.notifyVisitedPackages(result.getVisitedPackages());
+ }
+
+ return result;
+ }
+
+ /**
+ * Visit the transitive closure of the targets, populating the package cache
+ * and ensuring that all labels can be resolved and all rules were free from
+ * errors.
+ *
+ * @param targetsToLoad the list of command-line target patterns specified by the user
+ * @param testsToRun the tests to run as a subset of the targets to load
+ * @param labelsToLoadUnconditionally the labels to load unconditionally (presumably for the build
+ * configuration)
+ * @param keepGoing if true, don't throw ViewCreationFailedException if some
+ * targets could not be loaded, just skip thm.
+ */
+ private LoadingResult doLoadingPhase(EventHandler eventHandler, EventBus eventBus,
+ ImmutableSet<Target> targetsToLoad, Collection<Target> testsToRun,
+ ListMultimap<String, Label> labelsToLoadUnconditionally, boolean keepGoing,
+ int loadingPhaseThreads, boolean hasError)
+ throws InterruptedException, LoadingFailedException {
+ eventHandler.handle(Event.progress("Loading..."));
+ Stopwatch timer = Stopwatch.createStarted();
+ LOG.info("Starting loading phase");
+
+ Set<Label> labelsToLoad = ImmutableSet.copyOf(labelsToLoadUnconditionally.values());
+
+ // For each label in {@code targetsToLoad}, ensure that the target to which
+ // it refers exists, and also every target in its transitive closure of label
+ // dependencies. Success guarantees that a call to
+ // {@code getConfiguredTarget} for the same targets will not fail; the
+ // configuration process is intolerant of missing packages/targets. Before
+ // calling getConfiguredTarget(), clients must ensure that all necessary
+ // packages/targets have been visited since the last sync/clear.
+ boolean loadingSuccessful = pkgLoader.sync(eventHandler, targetsToLoad, labelsToLoad,
+ keepGoing, loadingPhaseThreads, Integer.MAX_VALUE);
+
+ ImmutableSet<Target> targetsToAnalyze;
+ if (loadingSuccessful) {
+ // Success: all loaded targets will be analyzed.
+ targetsToAnalyze = targetsToLoad;
+ } else if (keepGoing) {
+ // Keep going: filter out the error-free targets and only continue with those.
+ targetsToAnalyze = filterErrorFreeTargets(eventBus, targetsToLoad,
+ pkgLoader, labelsToLoadUnconditionally);
+
+ // Tell the user about the subset of successful targets.
+ int requested = targetsToLoad.size();
+ int loaded = targetsToAnalyze.size();
+ if (0 < loaded && loaded < requested) {
+ String message = String.format("Loading succeeded for only %d of %d targets", loaded,
+ requested);
+ eventHandler.handle(Event.info(message));
+ LOG.info(message);
+ }
+ } else {
+ throw new LoadingFailedException("Loading failed; build aborted");
+ }
+
+ Set<Target> filteredTargets = targetsToAnalyze;
+ try {
+ // We use strict test_suite expansion here to match the analysis-time checks.
+ ResolvedTargets<Target> expandedResult = TestTargetUtils.expandTestSuites(
+ packageManager, eventHandler, targetsToAnalyze, /*strict=*/true, /*keepGoing=*/true);
+ targetsToAnalyze = expandedResult.getTargets();
+ filteredTargets = Sets.difference(filteredTargets, targetsToAnalyze);
+ if (expandedResult.hasError()) {
+ if (!keepGoing) {
+ throw new LoadingFailedException("Could not expand test suite target");
+ }
+ loadingSuccessful = false;
+ }
+ } catch (TargetParsingException e) {
+ // This shouldn't happen, because we've already loaded the targets successfully.
+ throw (AssertionError) (new AssertionError("Unexpected target failure").initCause(e));
+ }
+
+ // Perform some operations on the set of packages containing the collected targets.
+ ImmutableMap<PackageIdentifier, Path> packageRoots = collectPackageRoots(
+ pkgLoader.getErrorFreeVisitedPackages());
+
+ Set<PackageIdentifier> visitedPackageNames = pkgLoader.getVisitedPackageNames();
+
+ // Clear some targets from the cache to free memory.
+ packageManager.partiallyClear();
+
+ eventBus.post(new LoadingPhaseCompleteEvent(
+ targetsToAnalyze, filteredTargets, packageManager.getStatistics(),
+ timer.stop().elapsed(TimeUnit.MILLISECONDS)));
+ LOG.info("Loading phase finished");
+
+ // testsToRun can contain targets that aren't analyzed, but the BuildView ignores those.
+ return new LoadingResult(hasError, !loadingSuccessful, targetsToAnalyze, testsToRun,
+ packageRoots, visitedPackageNames);
+ }
+
+ private Collection<Target> getTargetsForLabels(Collection<Label> labels) {
+ Set<Target> result = new HashSet<>();
+
+ for (Label label : labels) {
+ try {
+ result.add(packageManager.getLoadedTarget(label));
+ } catch (NoSuchPackageException e) {
+ Package pkg = Preconditions.checkNotNull(e.getPackage());
+ try {
+ result.add(pkg.getTarget(label.getName()));
+ } catch (NoSuchTargetException ex) {
+ throw new IllegalStateException(ex);
+ }
+ } catch (NoSuchThingException e) {
+ throw new IllegalStateException(e); // The target should have been loaded
+ }
+ }
+
+ return result;
+ }
+
+ private ImmutableSet<Target> filterErrorFreeTargets(
+ EventBus eventBus, Collection<Target> targetsToLoad,
+ TransitivePackageLoader pkgLoader,
+ ListMultimap<String, Label> labelsToLoadUnconditionally) throws LoadingFailedException {
+ // Error out if any of the labels needed for the configuration could not be loaded.
+ Collection<Label> labelsToLoad = new ArrayList<>(labelsToLoadUnconditionally.values());
+ for (Target target : targetsToLoad) {
+ labelsToLoad.add(target.getLabel());
+ }
+ Multimap<Label, Label> rootCauses = pkgLoader.getRootCauses(labelsToLoad);
+ for (Map.Entry<String, Label> entry : labelsToLoadUnconditionally.entries()) {
+ if (rootCauses.containsKey(entry.getValue())) {
+ throw new LoadingFailedException("Failed to load required " + entry.getKey()
+ + " target: '" + entry.getValue() + "'");
+ }
+ }
+
+ // Post root causes for command-line targets that could not be loaded.
+ for (Map.Entry<Label, Label> entry : rootCauses.entries()) {
+ eventBus.post(new LoadingFailureEvent(entry.getKey(), entry.getValue()));
+ }
+
+ return ImmutableSet.copyOf(Sets.difference(ImmutableSet.copyOf(targetsToLoad),
+ ImmutableSet.copyOf(getTargetsForLabels(rootCauses.keySet()))));
+ }
+
+ /**
+ * Returns a map of collected package names to root paths.
+ */
+ private static ImmutableMap<PackageIdentifier, Path> collectPackageRoots(
+ Collection<Package> packages) {
+ // Make a map of the package names to their root paths.
+ ImmutableMap.Builder<PackageIdentifier, Path> packageRoots = ImmutableMap.builder();
+ for (Package pkg : packages) {
+ packageRoots.put(pkg.getPackageIdentifier(), pkg.getSourceRoot());
+ }
+ return packageRoots.build();
+ }
+
+ /**
+ * Interpret the command-line arguments.
+ *
+ * @param targetPatterns the list of command-line target patterns specified by the user
+ * @param compileOneDependency if true, enables alternative interpretation of targetPatterns; see
+ * {@link Options#compileOneDependency}
+ * @throws TargetParsingException if parsing failed and !keepGoing
+ */
+ private ResolvedTargets<Target> getTargetsToBuild(EventHandler eventHandler,
+ List<String> targetPatterns, boolean compileOneDependency,
+ boolean keepGoing) throws TargetParsingException, InterruptedException {
+ ResolvedTargets<Target> result =
+ targetPatternEvaluator.parseTargetPatternList(eventHandler, targetPatterns,
+ FilteringPolicies.FILTER_MANUAL_AND_OBSOLETE, keepGoing);
+ if (compileOneDependency) {
+ return new CompileOneDependencyTransformer(packageManager)
+ .transformCompileOneDependency(eventHandler, result);
+ }
+ return result;
+ }
+
+ /**
+ * Interpret test target labels from the command-line arguments and return the corresponding set
+ * of targets, handling the filter flags, and expanding test suites.
+ *
+ * @param eventHandler the error event eventHandler
+ * @param targetPatterns the list of command-line target patterns specified by the user
+ * @param options the loading phase options
+ * @param keepGoing value of the --keep_going flag
+ */
+ private ResolvedTargets<Target> determineTests(EventHandler eventHandler,
+ List<String> targetPatterns, Options options, boolean keepGoing)
+ throws TargetParsingException, InterruptedException {
+ // Parse the targets to get the tests.
+ ResolvedTargets.Builder<Target> testTargetsBuilder = ResolvedTargets.builder();
+ for (String targetPattern : targetPatterns) {
+ if (targetPattern.startsWith("-")) {
+ ResolvedTargets<Target> someNegativeTargets = targetPatternEvaluator.parseTargetPatternList(
+ eventHandler, ImmutableList.of(targetPattern.substring(1)),
+ FilteringPolicies.FILTER_TESTS, keepGoing);
+ ResolvedTargets<Target> moreNegativeTargets = TestTargetUtils.expandTestSuites(
+ packageManager, eventHandler, someNegativeTargets.getTargets(), /*strict=*/false,
+ keepGoing);
+ testTargetsBuilder.filter(Predicates.not(Predicates.in(moreNegativeTargets.getTargets())));
+ testTargetsBuilder.mergeError(moreNegativeTargets.hasError());
+ } else {
+ ResolvedTargets<Target> somePositiveTargets = targetPatternEvaluator.parseTargetPatternList(
+ eventHandler, ImmutableList.of(targetPattern),
+ FilteringPolicies.FILTER_TESTS, keepGoing);
+ ResolvedTargets<Target> morePositiveTargets = TestTargetUtils.expandTestSuites(
+ packageManager, eventHandler, somePositiveTargets.getTargets(), /*strict=*/false,
+ keepGoing);
+ testTargetsBuilder.addAll(morePositiveTargets.getTargets());
+ testTargetsBuilder.mergeError(morePositiveTargets.hasError());
+ }
+ }
+ testTargetsBuilder.filter(getTestFilter(eventHandler, options));
+ return testTargetsBuilder.build();
+ }
+
+ /**
+ * Convert the options into a test filter.
+ */
+ private Predicate<Target> getTestFilter(EventHandler eventHandler, Options options) {
+ Predicate<Target> testFilter = Predicates.alwaysTrue();
+ if (!options.testSizeFilterSet.isEmpty()) {
+ testFilter = Predicates.and(testFilter,
+ TestTargetUtils.testSizeFilter(options.testSizeFilterSet));
+ }
+ if (!options.testTimeoutFilterSet.isEmpty()) {
+ testFilter = Predicates.and(testFilter,
+ TestTargetUtils.testTimeoutFilter(options.testTimeoutFilterSet));
+ }
+ if (!options.testTagFilterList.isEmpty()) {
+ testFilter = Predicates.and(testFilter,
+ TestTargetUtils.tagFilter(options.testTagFilterList));
+ }
+ if (!options.testLangFilterList.isEmpty()) {
+ testFilter = Predicates.and(testFilter,
+ TestTargetUtils.testLangFilter(options.testLangFilterList, eventHandler, ruleNames));
+ }
+ return testFilter;
+ }
+
+ /**
+ * Emit a warning when a deprecated target is mentioned on the command line.
+ *
+ * <p>Note that this does not stop us from emitting "target X depends on deprecated target Y"
+ * style warnings for the same target and it is a good thing; <i>depending</i> on a target and
+ * <i>wanting</i> to build it are different things.
+ */
+ private void maybeReportDeprecation(EventHandler eventHandler, Collection<Target> targets) {
+ for (Rule rule : Iterables.filter(targets, Rule.class)) {
+ if (rule.isAttributeValueExplicitlySpecified("deprecation")) {
+ eventHandler.handle(Event.warn(rule.getLocation(), String.format(
+ "target '%s' is deprecated: %s", rule.getLabel(),
+ NonconfigurableAttributeMapper.of(rule).get("deprecation", Type.STRING))));
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/pkgcache/PackageCacheBackedTargetPatternResolver.java b/src/main/java/com/google/devtools/build/lib/pkgcache/PackageCacheBackedTargetPatternResolver.java
new file mode 100644
index 0000000..e4dc010
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/pkgcache/PackageCacheBackedTargetPatternResolver.java
@@ -0,0 +1,263 @@
+// Copyright 2014 Google Inc. 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.pkgcache;
+
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.cmdline.LabelValidator;
+import com.google.devtools.build.lib.cmdline.ResolvedTargets;
+import com.google.devtools.build.lib.cmdline.TargetParsingException;
+import com.google.devtools.build.lib.cmdline.TargetPatternResolver;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.packages.BuildFileContainsErrorsException;
+import com.google.devtools.build.lib.packages.NoSuchPackageException;
+import com.google.devtools.build.lib.packages.NoSuchTargetException;
+import com.google.devtools.build.lib.packages.NoSuchThingException;
+import com.google.devtools.build.lib.packages.Package;
+import com.google.devtools.build.lib.packages.PackageIdentifier;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.concurrent.ThreadPoolExecutor;
+
+/**
+ * An implementation of the {@link TargetPatternResolver} that uses the {@link
+ * RecursivePackageProvider} as the backing implementation.
+ */
+final class PackageCacheBackedTargetPatternResolver implements TargetPatternResolver<Target> {
+
+ private final RecursivePackageProvider packageProvider;
+ private final EventHandler eventHandler;
+ private final boolean keepGoing;
+ private final FilteringPolicy policy;
+ private final ThreadPoolExecutor packageVisitorPool;
+
+ PackageCacheBackedTargetPatternResolver(RecursivePackageProvider packageProvider,
+ EventHandler eventHandler, boolean keepGoing, FilteringPolicy policy,
+ ThreadPoolExecutor packageVisitorPool) {
+ this.packageProvider = packageProvider;
+ this.eventHandler = eventHandler;
+ this.keepGoing = keepGoing;
+ this.policy = policy;
+ this.packageVisitorPool = packageVisitorPool;
+ }
+
+ @Override
+ public void warn(String msg) {
+ eventHandler.handle(Event.warn(msg));
+ }
+
+ @Override
+ public Target getTargetOrNull(String targetName) throws InterruptedException {
+ try {
+ return packageProvider.getTarget(eventHandler, Label.parseAbsolute(targetName));
+ } catch (NoSuchPackageException | NoSuchTargetException | Label.SyntaxException e) {
+ return null;
+ }
+ }
+
+ @Override
+ public ResolvedTargets<Target> getExplicitTarget(String targetName)
+ throws TargetParsingException, InterruptedException {
+ Label label = TargetPatternResolverUtil.label(targetName);
+ return getExplicitTarget(label, targetName);
+ }
+
+ private ResolvedTargets<Target> getExplicitTarget(Label label, String originalLabel)
+ throws TargetParsingException, InterruptedException {
+ try {
+ Target target = packageProvider.getTarget(eventHandler, label);
+ if (policy.shouldRetain(target, true)) {
+ return ResolvedTargets.of(target);
+ }
+ return ResolvedTargets.<Target>empty();
+ } catch (BuildFileContainsErrorsException e) {
+ // We don't need to report an error here because errors
+ // would have already been reported in this case.
+ return handleParsingError(eventHandler, originalLabel,
+ new TargetParsingException(e.getMessage(), e), keepGoing);
+ } catch (NoSuchThingException e) {
+ return handleParsingError(eventHandler, originalLabel,
+ new TargetParsingException(e.getMessage(), e), keepGoing);
+ }
+ }
+
+ /**
+ * Handles an error differently based on the value of keepGoing.
+ *
+ * @param badPattern The pattern we were unable to parse.
+ * @param e The underlying exception.
+ * @param keepGoing It true, report a warning and return.
+ * If false, throw the exception.
+ * @return the empty set.
+ * @throws TargetParsingException if !keepGoing.
+ */
+ private ResolvedTargets<Target> handleParsingError(EventHandler eventHandler, String badPattern,
+ TargetParsingException e, boolean keepGoing) throws TargetParsingException {
+ if (eventHandler instanceof ParseFailureListener) {
+ ((ParseFailureListener) eventHandler).parsingError(badPattern, e.getMessage());
+ }
+ if (keepGoing) {
+ eventHandler.handle(Event.error("Skipping '" + badPattern + "': " + e.getMessage()));
+ return ResolvedTargets.<Target>failed();
+ } else {
+ throw e;
+ }
+ }
+
+ @Override
+ public ResolvedTargets<Target> getTargetsInPackage(String originalPattern, String packageName,
+ boolean rulesOnly) throws TargetParsingException, InterruptedException {
+ FilteringPolicy actualPolicy = rulesOnly
+ ? FilteringPolicies.and(FilteringPolicies.RULES_ONLY, policy)
+ : policy;
+ return getTargetsInPackage(originalPattern, packageName, actualPolicy);
+ }
+
+ private ResolvedTargets<Target> getTargetsInPackage(String originalPattern, String packageName,
+ FilteringPolicy policy) throws TargetParsingException, InterruptedException {
+ // Normalise, e.g "foo//bar" -> "foo/bar"; "foo/" -> "foo":
+ packageName = new PathFragment(packageName).toString();
+
+ // it's possible for this check to pass, but for Label.validatePackageNameFull to report an
+ // error because the package name is illegal. That's a little weird, but we can live with
+ // that for now--see test case: testBadPackageNameButGoodEnoughForALabel. (BTW I tried
+ // duplicating that validation logic in Label but it was extremely tricky.)
+ if (LabelValidator.validatePackageName(packageName) != null) {
+ return handleParsingError(eventHandler, originalPattern,
+ new TargetParsingException(
+ "'" + packageName + "' is not a valid package name"), keepGoing);
+ }
+ Package pkg;
+ try {
+ pkg = packageProvider.getPackage(
+ eventHandler, PackageIdentifier.createInDefaultRepo(packageName));
+ } catch (NoSuchPackageException e) {
+ return handleParsingError(eventHandler, originalPattern, new TargetParsingException(
+ TargetPatternResolverUtil.getParsingErrorMessage(
+ e.getMessage(), originalPattern)), keepGoing);
+ }
+
+ if (pkg.containsErrors()) {
+ // Report an error, but continue (and return partial results) if keepGoing is specified.
+ handleParsingError(eventHandler, originalPattern, new TargetParsingException(
+ TargetPatternResolverUtil.getParsingErrorMessage(
+ "package contains errors", originalPattern)), keepGoing);
+ }
+
+ return TargetPatternResolverUtil.resolvePackageTargets(pkg, policy);
+ }
+
+ @Override
+ public ResolvedTargets<Target> findTargetsBeneathDirectory(String originalPattern,
+ String pathPrefix, boolean rulesOnly) throws TargetParsingException, InterruptedException {
+ FilteringPolicy actualPolicy = rulesOnly
+ ? FilteringPolicies.and(FilteringPolicies.RULES_ONLY, policy)
+ : policy;
+ return findTargetsBeneathDirectory(eventHandler, originalPattern, pathPrefix, actualPolicy,
+ keepGoing, pathPrefix.isEmpty());
+ }
+
+ private ResolvedTargets<Target> findTargetsBeneathDirectory(final EventHandler eventHandler,
+ final String originalPattern, String pathPrefix, final FilteringPolicy policy,
+ final boolean keepGoing, boolean useTopLevelExcludes)
+ throws TargetParsingException, InterruptedException {
+ PathFragment directory = new PathFragment(pathPrefix);
+ if (directory.containsUplevelReferences()) {
+ throw new TargetParsingException("up-level references are not permitted: '"
+ + pathPrefix + "'");
+ }
+ if (!pathPrefix.isEmpty() && (LabelValidator.validatePackageName(pathPrefix) != null)) {
+ return handleParsingError(eventHandler, pathPrefix, new TargetParsingException(
+ "'" + pathPrefix + "' is not a valid package name"), keepGoing);
+ }
+
+ final ResolvedTargets.Builder<Target> builder = ResolvedTargets.concurrentBuilder();
+ try {
+ packageProvider.visitPackageNamesRecursively(eventHandler, directory,
+ useTopLevelExcludes, packageVisitorPool,
+ new PathPackageLocator.AcceptsPathFragment() {
+ @Override
+ public void accept(PathFragment packageName) {
+ String pkgName = packageName.getPathString();
+ try {
+ // Get the targets without transforming. We'll do that later below.
+ builder.merge(getTargetsInPackage(originalPattern, pkgName,
+ FilteringPolicies.NO_FILTER));
+ } catch (InterruptedException e) {
+ throw new RuntimeParsingException(new TargetParsingException("interrupted"));
+ } catch (TargetParsingException e) {
+ // We'd like to make visitPackageNamesRecursively() generic
+ // over some checked exception type (TargetParsingException in
+ // this case). To do so, we'd have to make AbstractQueueVisitor
+ // generic over the same exception type. That won't work due to
+ // type erasure. As a workaround, we wrap the exception here,
+ // and unwrap it below.
+ throw new RuntimeParsingException(e);
+ }
+ }
+ });
+ } catch (RuntimeParsingException e) {
+ throw e.unwrap();
+ } catch (UnsupportedOperationException e) {
+ throw new TargetParsingException("recursive target patterns are not permitted: '"
+ + originalPattern + "'");
+ }
+
+ if (builder.isEmpty()) {
+ return handleParsingError(eventHandler, originalPattern,
+ new TargetParsingException("no targets found beneath '" + directory + "'"),
+ keepGoing);
+ }
+
+ // Apply the transform after the check so we only return the
+ // error if the tree really contains no targets.
+ ResolvedTargets<Target> intermediateResult = builder.build();
+ ResolvedTargets.Builder<Target> filteredBuilder = ResolvedTargets.builder();
+ if (intermediateResult.hasError()) {
+ filteredBuilder.setError();
+ }
+ for (Target target : intermediateResult.getTargets()) {
+ if (policy.shouldRetain(target, false)) {
+ filteredBuilder.add(target);
+ }
+ }
+ return filteredBuilder.build();
+ }
+
+ @Override
+ public boolean isPackage(String packageName) {
+ return packageProvider.isPackage(packageName);
+ }
+
+ @Override
+ public String getTargetKind(Target target) {
+ return target.getTargetKind();
+ }
+
+ private static final class RuntimeParsingException extends RuntimeException {
+ private TargetParsingException parsingException;
+
+ public RuntimeParsingException(TargetParsingException cause) {
+ super(cause);
+ this.parsingException = Preconditions.checkNotNull(cause);
+ }
+
+ public TargetParsingException unwrap() {
+ return parsingException;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/pkgcache/PackageCacheOptions.java b/src/main/java/com/google/devtools/build/lib/pkgcache/PackageCacheOptions.java
new file mode 100644
index 0000000..f9d3efc
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/pkgcache/PackageCacheOptions.java
@@ -0,0 +1,142 @@
+// Copyright 2014 Google Inc. 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.pkgcache;
+
+import com.google.devtools.build.lib.Constants;
+import com.google.devtools.build.lib.packages.ConstantRuleVisibility;
+import com.google.devtools.build.lib.packages.RuleVisibility;
+import com.google.devtools.build.lib.syntax.CommaSeparatedPackageNameListConverter;
+import com.google.devtools.common.options.Converter;
+import com.google.devtools.common.options.Converters;
+import com.google.devtools.common.options.Option;
+import com.google.devtools.common.options.OptionsBase;
+import com.google.devtools.common.options.OptionsParsingException;
+
+import java.util.List;
+
+/**
+ * Options for configuring the PackageCache.
+ */
+public class PackageCacheOptions extends OptionsBase {
+ /**
+ * A converter for package path that defaults to {@code Constants.DEFAULT_PACKAGE_PATH} if the
+ * option is not given.
+ *
+ * <p>Required because you cannot specify a non-constant value in annotation attributes.
+ */
+ public static class PackagePathConverter implements Converter<List<String>> {
+ @Override
+ public List<String> convert(String input) throws OptionsParsingException {
+ return input.isEmpty()
+ ? Constants.DEFAULT_PACKAGE_PATH
+ : new Converters.ColonSeparatedOptionListConverter().convert(input);
+ }
+
+ @Override
+ public String getTypeDescription() {
+ return "a string";
+ }
+ }
+
+ /**
+ * Converter for the {@code --default_visibility} option.
+ */
+ public static class DefaultVisibilityConverter implements Converter<RuleVisibility> {
+ @Override
+ public RuleVisibility convert(String input) throws OptionsParsingException {
+ if (input.equals("public")) {
+ return ConstantRuleVisibility.PUBLIC;
+ } else if (input.equals("private")) {
+ return ConstantRuleVisibility.PRIVATE;
+ } else {
+ throw new OptionsParsingException("Not a valid default visibility: '" + input
+ + "' (should be 'public' or 'private'");
+ }
+ }
+
+ @Override
+ public String getTypeDescription() {
+ return "default visibility";
+ }
+ }
+
+ @Option(name = "package_path",
+ defaultValue = "",
+ category = "package loading",
+ converter = PackagePathConverter.class,
+ help = "A colon-separated list of where to look for packages. "
+ + "Elements beginning with '%workspace%' are relative to the enclosing "
+ + "workspace. If omitted or empty, the default is the output of "
+ + "'blaze info default-package-path'.")
+ public List<String> packagePath;
+
+ @Option(name = "show_package_location",
+ defaultValue = "false",
+ category = "verbosity",
+ deprecationWarning = "This flag is no longer supported and will go away soon.",
+ help = "If enabled, causes Blaze to print the location on the --package_path "
+ + "from which each package was loaded.")
+ public boolean showPackageLocation;
+
+ @Option(name = "show_loading_progress",
+ defaultValue = "true",
+ category = "verbosity",
+ help = "If enabled, causes Blaze to print \"Loading package:\" messages.")
+ public boolean showLoadingProgress;
+
+ @Option(name = "deleted_packages",
+ defaultValue = "",
+ category = "package loading",
+ converter = CommaSeparatedPackageNameListConverter.class,
+ help = "A comma-separated list of names of packages which the "
+ + "build system will consider non-existent, even if they are "
+ + "visible somewhere on the package path."
+ + "\n"
+ + "Use this option when deleting a subpackage 'x/y' of an "
+ + "existing package 'x'. For example, after deleting x/y/BUILD "
+ + "in your client, the build system may complain if it "
+ + "encounters a label '//x:y/z' if that is still provided by another "
+ + "package_path entry. Specifying --deleted_packages x/y avoids this "
+ + "problem.")
+ public List<String> deletedPackages;
+
+ @Option(name = "default_visibility",
+ defaultValue = "private",
+ category = "undocumented",
+ converter = DefaultVisibilityConverter.class,
+ help = "Default visibility for packages that don't set it explicitly ('public' or "
+ + "'private').")
+ public RuleVisibility defaultVisibility;
+
+ @Option(name = "min_pkg_count_for_ct_node_eviction",
+ defaultValue = "3700",
+ // Why is the default value 3700? As of December 2013, a medium target loads about this many
+ // packages, uses ~310MB RAM to only load [1] or ~990MB to load and analyze [2,3]. So we
+ // can likely load and analyze this many packages without worrying about Blaze OOM'ing.
+ //
+ // If the total number of unique packages so far [4] is higher than the value of this flag,
+ // then we evict CT nodes [5] from the Skyframe graph.
+ //
+ // [1] blaze -x build --nobuild --noanalyze //medium:target
+ // [2] blaze -x build --nobuild //medium:target
+ // [3] according to "blaze info used-heap-size"
+ // [4] this means the number of unique packages loaded by builds, including the current one,
+ // since the last CT node eviction [5]
+ // [5] "CT node eviction" means clearing those nodes from the Skyframe graph that correspond
+ // to ConfiguredTargets; this is done using SkyframeExecutor.resetConfiguredTargets
+ category = "undocumented",
+ help = "Threshold for number of loaded packages before skyframe-m1 cache eviction kicks in")
+ public int minLoadedPkgCountForCtNodeEviction;
+}
diff --git a/src/main/java/com/google/devtools/build/lib/pkgcache/PackageManager.java b/src/main/java/com/google/devtools/build/lib/pkgcache/PackageManager.java
new file mode 100644
index 0000000..4d5e687
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/pkgcache/PackageManager.java
@@ -0,0 +1,90 @@
+// Copyright 2014 Google Inc. 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.pkgcache;
+
+import com.google.devtools.build.lib.concurrent.ThreadSafety;
+import com.google.devtools.build.lib.packages.CachingPackageLocator;
+
+import java.io.PrintStream;
+
+/**
+ * A PackageManager keeps state about loaded packages around for quick lookup, and provides
+ * related functionality: Recursive package finding, loaded package checking, etc.
+ */
+public interface PackageManager extends PackageProvider, CachingPackageLocator,
+ LoadedPackageProvider {
+
+ /**
+ * Returns the package cache statistics.
+ */
+ PackageManagerStatistics getStatistics();
+
+ /**
+ * Removes cached data which is not needed anymore after loading is complete, to reduce memory
+ * consumption between builds. Whether or not this method is called does not affect correctness.
+ */
+ void partiallyClear();
+
+ /**
+ * Dump the contents of the package manager in human-readable form.
+ * Used by 'bazel dump' and the BuildTool's unexpected exception handler.
+ */
+ void dump(PrintStream printStream);
+
+ /**
+ * Returns the package locator used by this package manager.
+ *
+ * <p>If you are tempted to call {@code getPackagePath().getPathEntries().get(0)}, be warned that
+ * this is probably not the value you are looking for! Look at the methods of {@code
+ * BazelRuntime} instead.
+ */
+ @ThreadSafety.ThreadSafe
+ PathPackageLocator getPackagePath();
+
+ /**
+ * Collects statistics of the package manager since the last sync.
+ */
+ interface PackageManagerStatistics {
+
+ /**
+ * Returns the number of packages loaded since the last sync. I.e. the cache
+ * misses.
+ */
+ int getPackagesLoaded();
+
+ /**
+ * Returns the number of packages looked up since the last sync.
+ */
+ int getPackagesLookedUp();
+
+ /**
+ * Returns the number of all the packages currently loaded.
+ *
+ * <p>
+ * Note that this method is not affected by sync(), and the packages it
+ * returns are not guaranteed to be up-to-date.
+ */
+ int getCacheSize();
+ }
+
+ /**
+ * Retrieve a target pattern parser that works with this package manager.
+ */
+ TargetPatternEvaluator getTargetPatternEvaluator();
+
+ /**
+ * Construct a new {@link TransitivePackageLoader}.
+ */
+ TransitivePackageLoader newTransitiveLoader();
+}
diff --git a/src/main/java/com/google/devtools/build/lib/pkgcache/PackageProvider.java b/src/main/java/com/google/devtools/build/lib/pkgcache/PackageProvider.java
new file mode 100644
index 0000000..0573768e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/pkgcache/PackageProvider.java
@@ -0,0 +1,60 @@
+// Copyright 2014 Google Inc. 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.pkgcache;
+
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.packages.NoSuchPackageException;
+import com.google.devtools.build.lib.packages.Package;
+import com.google.devtools.build.lib.packages.PackageIdentifier;
+
+/**
+ * API for retrieving packages. Implementations generally load packages to fulfill requests.
+ *
+ * <p><b>Concurrency</b>: Implementations should be thread safe for {@link #getPackage}.
+ */
+public interface PackageProvider extends TargetProvider {
+
+ /**
+ * Returns the {@link Package} named "packageName". If there is no such package (e.g.
+ * {@code isPackage(packageName)} returns false), throws a {@link NoSuchPackageException}.
+ *
+ * <p>The returned package may contain lexical/grammatical errors, in which
+ * case <code>pkg.containsErrors() == true</code>. Such packages may be
+ * missing some rules. Any rules that are present may soundly be used for
+ * builds, though.
+ *
+ * @param eventHandler the eventHandler on which to report warning and errors; if the package
+ * has been loaded by another thread, this eventHandler won't see any warnings or errors
+ * @param packageName a legal package name.
+ * @throws NoSuchPackageException if the package could not be found.
+ * @throws InterruptedException if the package loading was interrupted.
+ */
+ Package getPackage(EventHandler eventHandler, PackageIdentifier packageName)
+ throws NoSuchPackageException, InterruptedException;
+
+ /**
+ * Returns whether a package with the given name exists. That is, returns whether all the
+ * following hold
+ * <ol>
+ * <li>{@code packageName} is a valid package name</li>
+ * <li>there is a BUILD file for the package</li>
+ * <li>the package is not considered deleted via --deleted_packages</li>
+ * </ol>
+ *
+ * <p> If these don't hold, then attempting to read the package with {@link #getPackage} may fail
+ * or may return a package containing errors.
+ */
+ boolean isPackage(String packageName);
+}
diff --git a/src/main/java/com/google/devtools/build/lib/pkgcache/ParseFailureListener.java b/src/main/java/com/google/devtools/build/lib/pkgcache/ParseFailureListener.java
new file mode 100644
index 0000000..e3cf5ca
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/pkgcache/ParseFailureListener.java
@@ -0,0 +1,28 @@
+// Copyright 2014 Google Inc. 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.pkgcache;
+
+import com.google.common.eventbus.EventBus;
+import com.google.devtools.build.lib.events.EventHandler;
+
+/**
+ * Represents a listener which reports parse errors to the underlying
+ * {@link EventHandler} and {@link EventBus} (if non-null).
+ */
+public interface ParseFailureListener {
+
+ /** Reports a parsing failure. */
+ void parsingError(String badPattern, String message);
+}
diff --git a/src/main/java/com/google/devtools/build/lib/pkgcache/ParsingFailedEvent.java b/src/main/java/com/google/devtools/build/lib/pkgcache/ParsingFailedEvent.java
new file mode 100644
index 0000000..7eb9169
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/pkgcache/ParsingFailedEvent.java
@@ -0,0 +1,42 @@
+// Copyright 2014 Google Inc. 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.pkgcache;
+
+/**
+ * This event is fired when a target or target pattern fails to parse.
+ * In some cases (not all) this happens before targets are created,
+ * and thus in these cases there are no status lines.
+ * Therefore, the parse failure is reported separately.
+ */
+public class ParsingFailedEvent {
+ private final String targetPattern;
+ private final String message;
+
+ /**
+ * Creates a new parsing failed event with the given pattern and message.
+ */
+ public ParsingFailedEvent(String targetPattern, String message) {
+ this.targetPattern = targetPattern;
+ this.message = message;
+ }
+
+ public String getPattern() {
+ return targetPattern;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/pkgcache/PathPackageLocator.java b/src/main/java/com/google/devtools/build/lib/pkgcache/PathPackageLocator.java
new file mode 100644
index 0000000..a2af5f2
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/pkgcache/PathPackageLocator.java
@@ -0,0 +1,213 @@
+// Copyright 2014 Google Inc. 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.pkgcache;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.packages.BuildFileNotFoundException;
+import com.google.devtools.build.lib.packages.NoSuchPackageException;
+import com.google.devtools.build.lib.vfs.FileStatus;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.lib.vfs.Symlinks;
+import com.google.devtools.build.lib.vfs.UnixGlob;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.logging.Logger;
+
+/**
+ * A mapping from the name of a package to the location of its BUILD file.
+ * The implementation composes an ordered sequence of directories according to
+ * the package-path rules.
+ *
+ * <p>All methods are thread-safe, and (assuming no change to the underlying
+ * filesystem) idempotent.
+ */
+public class PathPackageLocator {
+
+ public static final Set<String> DEFAULT_TOP_LEVEL_EXCLUDES =
+ ImmutableSet.of("experimental", "obsolete");
+
+ /**
+ * An interface which accepts {@link PathFragment}s.
+ */
+ public interface AcceptsPathFragment {
+
+ /**
+ * Accept a {@link PathFragment}.
+ *
+ * @param fragment The path fragment.
+ */
+ void accept(PathFragment fragment);
+ }
+
+ private static final Logger LOG = Logger.getLogger(PathPackageLocator.class.getName());
+
+ private final ImmutableList<Path> pathEntries;
+
+ /**
+ * Constructs a PathPackageLocator based on the specified list of package root directories.
+ */
+ public PathPackageLocator(List<Path> pathEntries) {
+ this.pathEntries = ImmutableList.copyOf(pathEntries);
+ }
+
+ /**
+ * Constructs a PathPackageLocator based on the specified array of package root directories.
+ */
+ public PathPackageLocator(Path... pathEntries) {
+ this(Arrays.asList(pathEntries));
+ }
+
+ /**
+ * Returns the path to the build file for this package.
+ *
+ * <p>The package's root directory may be computed by calling getParentFile()
+ * on the result of this function.
+ *
+ * <p>Instances of this interface do not attempt to do any caching, nor
+ * implement checks for package-boundary crossing logic; the PackageCache
+ * does that.
+ *
+ * <p>If the same package exists beneath multiple package path entries, the
+ * first path that matches always wins.
+ */
+ public Path getPackageBuildFile(String packageName) throws NoSuchPackageException {
+ Path buildFile = getPackageBuildFileNullable(packageName, UnixGlob.DEFAULT_SYSCALLS_REF);
+ if (buildFile == null) {
+ throw new BuildFileNotFoundException(packageName, "BUILD file not found on package path");
+ }
+ return buildFile;
+ }
+
+ /**
+ * Like #getPackageBuildFile(), but returns null instead of throwing.
+ *
+ * @param packageName the name of the package.
+ * @param cache a filesystem-level cache of stat() calls.
+ */
+ public Path getPackageBuildFileNullable(String packageName,
+ AtomicReference<? extends UnixGlob.FilesystemCalls> cache) {
+ return getFilePath(new PathFragment(packageName).getRelative("BUILD"), cache);
+ }
+
+
+ /**
+ * Returns an immutable ordered list of the directories on the package path.
+ */
+ public ImmutableList<Path> getPathEntries() {
+ return pathEntries;
+ }
+
+ @Override
+ public String toString() {
+ return "PathPackageLocator" + pathEntries;
+ }
+
+ /**
+ * A factory of PathPackageLocators from a list of path elements. Elements
+ * may contain "%workspace%", indicating the workspace.
+ *
+ * @param pathElements Each element must be an absolute path, relative path,
+ * or some string "%workspace%" + relative, where relative is itself a
+ * relative path. The special symbol "%workspace%" means to interpret
+ * the path relative to the nearest enclosing workspace. Relative
+ * paths are interpreted relative to the client's working directory,
+ * which may be below the workspace.
+ * @param eventHandler The eventHandler.
+ * @param workspace The nearest enclosing package root directory.
+ * @param clientWorkingDirectory The client's working directory.
+ * @return a list of {@link Path}s.
+ */
+ public static PathPackageLocator create(List<String> pathElements,
+ EventHandler eventHandler,
+ Path workspace,
+ Path clientWorkingDirectory) {
+ List<Path> resolvedPaths = new ArrayList<>();
+ final String workspaceWildcard = "%workspace%";
+
+ for (String pathElement : pathElements) {
+ // Replace "%workspace%" with the path of the enclosing workspace directory.
+ pathElement = pathElement.replace(workspaceWildcard, workspace.getPathString());
+
+ PathFragment pathElementFragment = new PathFragment(pathElement);
+
+ // If the path string started with "%workspace%" or "/", it is already absolute,
+ // so the following line is a no-op.
+ Path rootPath = clientWorkingDirectory.getRelative(pathElementFragment);
+
+ if (!pathElementFragment.isAbsolute() && !clientWorkingDirectory.equals(workspace)) {
+ eventHandler.handle(
+ Event.warn("The package path element '" + pathElementFragment + "' will be "
+ + "taken relative to your working directory. You may have intended "
+ + "to have the path taken relative to your workspace directory. "
+ + "If so, please use the '" + workspaceWildcard + "' wildcard."));
+ }
+
+ if (rootPath.exists()) {
+ resolvedPaths.add(rootPath);
+ } else {
+ LOG.fine("package path element " + rootPath + " does not exist, ignoring");
+ }
+ }
+ return new PathPackageLocator(resolvedPaths);
+ }
+
+ /**
+ * Returns the path to the WORKSPACE file for this build.
+ *
+ * <p>If there are WORKSPACE files beneath multiple package path entries, the first one always
+ * wins.
+ */
+ public Path getWorkspaceFile() {
+ AtomicReference<? extends UnixGlob.FilesystemCalls> cache = UnixGlob.DEFAULT_SYSCALLS_REF;
+ // TODO(bazel-team): correctness in the presence of changes to the location of the WORKSPACE
+ // file.
+ return getFilePath(new PathFragment("WORKSPACE"), cache);
+ }
+
+ private Path getFilePath(PathFragment suffix,
+ AtomicReference<? extends UnixGlob.FilesystemCalls> cache) {
+ for (Path pathEntry : pathEntries) {
+ Path buildFile = pathEntry.getRelative(suffix);
+ FileStatus stat = cache.get().statNullable(buildFile, Symlinks.FOLLOW);
+ if (stat != null && stat.isFile()) {
+ return buildFile;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public int hashCode() {
+ return pathEntries.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof PathPackageLocator)) {
+ return false;
+ }
+ return this.getPathEntries().equals(((PathPackageLocator) other).getPathEntries());
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/pkgcache/RecursivePackageProvider.java b/src/main/java/com/google/devtools/build/lib/pkgcache/RecursivePackageProvider.java
new file mode 100644
index 0000000..8d7bd1d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/pkgcache/RecursivePackageProvider.java
@@ -0,0 +1,57 @@
+// Copyright 2014 Google Inc. 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.pkgcache;
+
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.concurrent.ThreadPoolExecutor;
+
+import javax.annotation.Nullable;
+
+/**
+ * Support for resolving {@code package/...} target patterns.
+ */
+public interface RecursivePackageProvider extends PackageProvider {
+
+ /**
+ * <p>Visits the names of all packages beneath the given directory recursively and concurrently.
+ *
+ * <p>Note: This operation needs to stat directories recursively. It could be very expensive when
+ * there is a big tree under the given directory.
+ *
+ * <p>Over a single iteration, package names are unique.
+ *
+ * <p>This method uses the given thread pool to call the observer method, possibly concurrently
+ * (depending on the thread pool). When this method terminates, however, all such threads will
+ * have completed.
+ *
+ * <p>To abort the traversal, call {@link Thread#interrupt()} on the calling thread.
+ *
+ * <p>This method guarantees that all BUILD files it returns correspond to valid package names
+ * that are not marked as deleted within the current build.
+ *
+ * @param eventHandler an eventHandler which should be used to log any errors that occur while
+ * scanning directories for BUILD files
+ * @param directory a relative, canonical path specifying the directory to search
+ * @param useTopLevelExcludes whether to skip a pre-set list of top level directories
+ * @param visitorPool the thread pool to use to visit packages in parallel
+ * @param observer is called for each path fragment found; thread-safe if the thread pool supports
+ * multiple parallel threads
+ * @throws InterruptedException if the calling thread was interrupted.
+ */
+ void visitPackageNamesRecursively(EventHandler eventHandler, PathFragment directory,
+ boolean useTopLevelExcludes, @Nullable ThreadPoolExecutor visitorPool,
+ PathPackageLocator.AcceptsPathFragment observer) throws InterruptedException;
+}
diff --git a/src/main/java/com/google/devtools/build/lib/pkgcache/SrcTargetUtil.java b/src/main/java/com/google/devtools/build/lib/pkgcache/SrcTargetUtil.java
new file mode 100644
index 0000000..85d967e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/pkgcache/SrcTargetUtil.java
@@ -0,0 +1,148 @@
+// Copyright 2014 Google Inc. 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.pkgcache;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.devtools.build.lib.concurrent.ThreadSafety;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.packages.AttributeMap;
+import com.google.devtools.build.lib.packages.FileTarget;
+import com.google.devtools.build.lib.packages.NoSuchPackageException;
+import com.google.devtools.build.lib.packages.NoSuchTargetException;
+import com.google.devtools.build.lib.packages.RawAttributeMapper;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.syntax.Label;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A helper class for getting source and header files from a given {@link Rule}.
+ */
+public final class SrcTargetUtil {
+ private SrcTargetUtil() {
+ }
+
+ /**
+ * Given a Rule, returns an immutable list of FileTarget for its sources, in the order they appear
+ * in its "srcs", "src" or "srcjar" attribute, and any filegroups or other rules it references.
+ * An empty list is returned if no "srcs" or "src" attribute exists for this rule. The list may
+ * contain OutputFiles if the sources were generated by another rule.
+ *
+ * <p>This method should be considered only a heuristic, and should not be used during the
+ * analysis phase.
+ *
+ * <p>(We could remove the throws clauses if we restrict the results to srcs within the same
+ * package.)
+ *
+ * @throws NoSuchTargetException or NoSuchPackageException when a source label cannot be resolved
+ * to a Target
+ */
+ @ThreadSafety.ThreadSafe
+ public static List<FileTarget> getSrcTargets(EventHandler eventHandler, Rule rule,
+ TargetProvider provider)
+ throws NoSuchTargetException, NoSuchPackageException, InterruptedException {
+ return getTargets(eventHandler, rule, SOURCE_ATTRIBUTES, Sets.newHashSet(rule), provider);
+ }
+
+ // Attributes referring to "sources".
+ private static final ImmutableSet<String> SOURCE_ATTRIBUTES =
+ ImmutableSet.of("srcs", "src", "srcjar");
+
+ // Attributes referring to "headers".
+ private static final ImmutableSet<String> HEADER_ATTRIBUTES =
+ ImmutableSet.of("hdrs");
+
+ // The attribute to search in filegroups.
+ private static final ImmutableSet<String> FILEGROUP_ATTRIBUTES =
+ ImmutableSet.of("srcs");
+
+ /**
+ * Same as {@link #getSrcTargets}, but for both source and headers (i.e. also traversing
+ * the "hdrs" attribute).
+ */
+ @ThreadSafety.ThreadSafe
+ public static List<FileTarget> getSrcAndHdrTargets(EventHandler eventHandler, Rule rule,
+ TargetProvider provider)
+ throws NoSuchTargetException, NoSuchPackageException, InterruptedException {
+ ImmutableSet<String> srcAndHdrAttributes = ImmutableSet.<String>builder()
+ .addAll(SOURCE_ATTRIBUTES)
+ .addAll(HEADER_ATTRIBUTES)
+ .build();
+ return getTargets(eventHandler, rule, srcAndHdrAttributes, Sets.newHashSet(rule), provider);
+ }
+
+ @ThreadSafety.ThreadSafe
+ public static List<FileTarget> getHdrTargets(EventHandler eventHandler, Rule rule,
+ TargetProvider provider)
+ throws NoSuchTargetException, NoSuchPackageException, InterruptedException {
+ ImmutableSet<String> srcAndHdrAttributes = ImmutableSet.<String>builder()
+ .addAll(HEADER_ATTRIBUTES)
+ .build();
+ return getTargets(eventHandler, rule, srcAndHdrAttributes, Sets.newHashSet(rule), provider);
+ }
+
+ /**
+ * @see #getSrcTargets(EventHandler, Rule, TargetProvider)
+ */
+ private static List<FileTarget> getTargets(EventHandler eventHandler,
+ Rule rule,
+ ImmutableSet<String> attributes,
+ Set<Rule> visitedRules,
+ TargetProvider targetProvider)
+ throws NoSuchTargetException, NoSuchPackageException, InterruptedException {
+ Preconditions.checkState(!rule.hasConfigurableAttributes()); // Not currently supported.
+ List<Label> srcLabels = Lists.newArrayList();
+ AttributeMap attributeMap = RawAttributeMapper.of(rule);
+ for (String attrName : attributes) {
+ if (rule.isAttrDefined(attrName, Type.LABEL_LIST)) {
+ srcLabels.addAll(attributeMap.get(attrName, Type.LABEL_LIST));
+ } else if (rule.isAttrDefined(attrName, Type.LABEL)) {
+ Label srcLabel = attributeMap.get(attrName, Type.LABEL);
+ if (srcLabel != null) {
+ srcLabels.add(srcLabel);
+ }
+ }
+ }
+ if (srcLabels.isEmpty()) {
+ return ImmutableList.of();
+ }
+ List<FileTarget> srcTargets = new ArrayList<>();
+ for (Label label : srcLabels) {
+ Target target = targetProvider.getTarget(eventHandler, label);
+ if (target instanceof FileTarget) {
+ srcTargets.add((FileTarget) target);
+ } else {
+ Rule srcRule = target.getAssociatedRule();
+ if (srcRule != null && !visitedRules.contains(srcRule)) {
+ visitedRules.add(srcRule);
+ if ("filegroup".equals(srcRule.getRuleClass())) {
+ srcTargets.addAll(getTargets(eventHandler, srcRule, FILEGROUP_ATTRIBUTES, visitedRules,
+ targetProvider));
+ } else {
+ srcTargets.addAll(srcRule.getOutputFiles());
+ }
+ }
+ }
+ }
+ return ImmutableList.copyOf(srcTargets);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/pkgcache/TargetEdgeObserver.java b/src/main/java/com/google/devtools/build/lib/pkgcache/TargetEdgeObserver.java
new file mode 100644
index 0000000..acc858f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/pkgcache/TargetEdgeObserver.java
@@ -0,0 +1,59 @@
+// Copyright 2014 Google Inc. 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.pkgcache;
+
+import com.google.devtools.build.lib.packages.Attribute;
+import com.google.devtools.build.lib.packages.NoSuchThingException;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.syntax.Label;
+
+import javax.annotation.Nullable;
+
+/**
+ * An observer of the visitation over a target graph.
+ */
+public interface TargetEdgeObserver {
+
+ /**
+ * Called when an edge is discovered.
+ * May be called more than once for the same
+ * (from, to) pair.
+ *
+ * @param from the originating node.
+ * @param attribute The attribute which defines the edge.
+ * Non-null iff (from instanceof Rule).
+ * @param to the target node.
+ */
+ void edge(Target from, Attribute attribute, Target to);
+
+ /**
+ * Called when a Target has a reference to a non-existent target.
+ *
+ * @param target the target. May be null (e.g. in the case of an implicit
+ * dependency on a subincluded file).
+ * @param to a label reference in the rule, which does not correspond
+ * to a valid target.
+ * @param e the corresponding exception thrown
+ */
+ void missingEdge(@Nullable Target target, Label to, NoSuchThingException e);
+
+ /**
+ * Called when a node is discovered. May be called
+ * more than once for the same node.
+ *
+ * @param node the target.
+ */
+ void node(Target node);
+}
diff --git a/src/main/java/com/google/devtools/build/lib/pkgcache/TargetParsingCompleteEvent.java b/src/main/java/com/google/devtools/build/lib/pkgcache/TargetParsingCompleteEvent.java
new file mode 100644
index 0000000..48d8861
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/pkgcache/TargetParsingCompleteEvent.java
@@ -0,0 +1,87 @@
+// Copyright 2014 Google Inc. 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.pkgcache;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.syntax.Label;
+
+import java.util.Collection;
+
+/**
+ * This event is fired just after target pattern evaluation is completed.
+ */
+public class TargetParsingCompleteEvent {
+
+ private final ImmutableSet<Target> targets;
+ private final ImmutableSet<Target> filteredTargets;
+ private final ImmutableSet<Target> testFilteredTargets;
+ private final long timeInMs;
+
+ /**
+ * Construct the event.
+ * @param targets The targets that were parsed from the
+ * command-line pattern.
+ */
+ public TargetParsingCompleteEvent(Collection<Target> targets,
+ Collection<Target> filteredTargets, Collection<Target> testFilteredTargets,
+ long timeInMs) {
+ this.timeInMs = timeInMs;
+ this.targets = ImmutableSet.copyOf(targets);
+ this.filteredTargets = ImmutableSet.copyOf(filteredTargets);
+ this.testFilteredTargets = ImmutableSet.copyOf(testFilteredTargets);
+ }
+
+ @VisibleForTesting
+ public TargetParsingCompleteEvent(Collection<Target> targets) {
+ this(targets, ImmutableSet.<Target>of(), ImmutableSet.<Target>of(), 0);
+ }
+
+ /**
+ * @return the parsed targets, which will subsequently be loaded
+ */
+ public ImmutableSet<Target> getTargets() {
+ return targets;
+ }
+
+ public Iterable<Label> getLabels() {
+ return Iterables.transform(targets, new Function<Target, Label>() {
+ @Override
+ public Label apply(Target input) {
+ return input.getLabel();
+ }
+ });
+ }
+
+ /**
+ * @return the filtered targets (i.e., using -//foo:bar on the command-line)
+ */
+ public ImmutableSet<Target> getFilteredTargets() {
+ return filteredTargets;
+ }
+
+ /**
+ * @return the test-filtered targets, if --build_test_only is in effect
+ */
+ public ImmutableSet<Target> getTestFilteredTargets() {
+ return testFilteredTargets;
+ }
+
+ public long getTimeInMs() {
+ return timeInMs;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/pkgcache/TargetPatternEvaluator.java b/src/main/java/com/google/devtools/build/lib/pkgcache/TargetPatternEvaluator.java
new file mode 100644
index 0000000..94e956b
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/pkgcache/TargetPatternEvaluator.java
@@ -0,0 +1,97 @@
+// Copyright 2014 Google Inc. 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.pkgcache;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.devtools.build.lib.cmdline.ResolvedTargets;
+import com.google.devtools.build.lib.cmdline.TargetParsingException;
+import com.google.devtools.build.lib.concurrent.ThreadSafety;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadHostile;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A parser for target patterns. Target patterns are a generalisation of
+ * labels to include wildcards for finding all packages recursively
+ * beneath some root, and for finding all targets within a package.
+ *
+ * <p>A list of target patterns implies a union of all the labels of each
+ * pattern. Each item in a list of target patterns may include a prefix
+ * negation operator, indicating that the sets of targets for this pattern
+ * should be subtracted from the set of targets for the preceding patterns (note
+ * this means that order matters). Thus, the following list of target patterns:
+ * <pre>foo/... -foo/bar:all</pre>
+ * means "all targets beneath <tt>foo</tt> except for those targets in
+ * package <tt>foo/bar</tt>.
+ */
+@ThreadSafety.ConditionallyThreadSafe // as long as you don't call updateOffset.
+public interface TargetPatternEvaluator {
+ /**
+ * Attempts to parse an ordered list of target patterns, computing the union
+ * of the set of targets represented by each pattern, unless it is preceded by
+ * "-", in which case the set difference is computed. Implements the
+ * specification described in the class-level comment.
+ */
+ ResolvedTargets<Target> parseTargetPatternList(EventHandler eventHandler,
+ List<String> targetPatterns, FilteringPolicy policy, boolean keepGoing)
+ throws TargetParsingException, InterruptedException;
+
+ /**
+ * Attempts to parse a single target pattern while consulting the package
+ * cache to check for the existence of packages and directories and the build
+ * targets in them. Implements the specification described in the
+ * class-level comment. Returns a {@link ResolvedTargets} object.
+ *
+ * <p>If an error is encountered, a {@link TargetParsingException} is thrown,
+ * unless {@code keepGoing} is set to true. In that case, the returned object
+ * will have its error bit set.
+ */
+ ResolvedTargets<Target> parseTargetPattern(EventHandler eventHandler, String pattern,
+ boolean keepGoing) throws TargetParsingException, InterruptedException;
+
+ /**
+ * Attempts to parse and load the given collection of patterns; the returned map contains the
+ * results for each pattern successfully parsed.
+ *
+ * <p>If an error is encountered, a {@link TargetParsingException} is thrown, unless {@code
+ * keepGoing} is set to true. In that case, the patterns that failed to load have the error flag
+ * set.
+ */
+ Map<String, ResolvedTargets<Target>> preloadTargetPatterns(EventHandler eventHandler,
+ Collection<String> patterns, boolean keepGoing)
+ throws TargetParsingException, InterruptedException;
+
+
+ /**
+ * Update the parser's offset, given the workspace and working directory.
+ *
+ * @param relativeWorkingDirectory the working directory relative to the workspace
+ */
+ @ThreadHostile
+ void updateOffset(PathFragment relativeWorkingDirectory);
+
+ /**
+ * @return the offset of this parser from the root of the workspace.
+ * Non-absolute package-names will be resolved relative
+ * to this offset.
+ */
+ @VisibleForTesting
+ String getOffset();
+}
diff --git a/src/main/java/com/google/devtools/build/lib/pkgcache/TargetPatternResolverUtil.java b/src/main/java/com/google/devtools/build/lib/pkgcache/TargetPatternResolverUtil.java
new file mode 100644
index 0000000..6bbf55c
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/pkgcache/TargetPatternResolverUtil.java
@@ -0,0 +1,69 @@
+// Copyright 2014 Google Inc. 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.pkgcache;
+
+import com.google.devtools.build.lib.cmdline.ResolvedTargets;
+import com.google.devtools.build.lib.cmdline.TargetParsingException;
+import com.google.devtools.build.lib.packages.Package;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.util.StringUtilities;
+
+/**
+ * Common utility methods for target pattern resolution.
+ */
+public final class TargetPatternResolverUtil {
+ private TargetPatternResolverUtil() {
+ // Utility class.
+ }
+
+ // Parse 'label' as a Label, mapping Label.SyntaxException into
+ // TargetParsingException.
+ public static Label label(String label) throws TargetParsingException {
+ try {
+ return Label.parseAbsolute(label);
+ } catch (Label.SyntaxException e) {
+ throw invalidTarget(label, e.getMessage());
+ }
+ }
+
+ /**
+ * Returns a new exception indicating that a command-line target is invalid.
+ */
+ private static TargetParsingException invalidTarget(String packageName,
+ String additionalMessage) {
+ return new TargetParsingException("invalid target format: '" +
+ StringUtilities.sanitizeControlChars(packageName) + "'; " +
+ StringUtilities.sanitizeControlChars(additionalMessage));
+ }
+
+ public static String getParsingErrorMessage(String message, String originalPattern) {
+ if (originalPattern == null) {
+ return message;
+ } else {
+ return String.format("while parsing '%s': %s", originalPattern, message);
+ }
+ }
+
+ public static ResolvedTargets<Target> resolvePackageTargets(Package pkg,
+ FilteringPolicy policy) {
+ ResolvedTargets.Builder<Target> builder = ResolvedTargets.builder();
+ for (Target target : pkg.getTargets()) {
+ if (policy.shouldRetain(target, false)) {
+ builder.add(target);
+ }
+ }
+ return builder.build();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/pkgcache/TargetProvider.java b/src/main/java/com/google/devtools/build/lib/pkgcache/TargetProvider.java
new file mode 100644
index 0000000..72dc834
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/pkgcache/TargetProvider.java
@@ -0,0 +1,40 @@
+// Copyright 2014 Google Inc. 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.pkgcache;
+
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.packages.NoSuchPackageException;
+import com.google.devtools.build.lib.packages.NoSuchTargetException;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.syntax.Label;
+
+/**
+ * API for retrieving targets.
+ *
+ * <p><b>Concurrency</b>: Implementations should be thread safe.
+ */
+public interface TargetProvider {
+ /**
+ * Returns the Target identified by "label", loading, parsing and evaluating the package if it is
+ * not already loaded.
+ *
+ * @throws NoSuchPackageException if the package could not be found
+ * @throws NoSuchTargetException if the package was loaded successfully, but
+ * the specified {@link Target} was not found in it
+ * @throws InterruptedException if the package loading was interrupted
+ */
+ Target getTarget(EventHandler eventHandler, Label label) throws NoSuchPackageException,
+ NoSuchTargetException, InterruptedException;
+}
diff --git a/src/main/java/com/google/devtools/build/lib/pkgcache/TransitivePackageLoader.java b/src/main/java/com/google/devtools/build/lib/pkgcache/TransitivePackageLoader.java
new file mode 100644
index 0000000..14990b2
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/pkgcache/TransitivePackageLoader.java
@@ -0,0 +1,90 @@
+// Copyright 2014 Google Inc. 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.pkgcache;
+
+import com.google.common.collect.Multimap;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.packages.Package;
+import com.google.devtools.build.lib.packages.PackageIdentifier;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.syntax.Label;
+
+import java.util.Collection;
+import java.util.Set;
+
+/**
+ * Visits a set of Targets and Labels transitively.
+ */
+public interface TransitivePackageLoader {
+
+ /**
+ * Visit the specified labels and follow the transitive closure of their
+ * outbound dependencies. If the targets have previously been visited,
+ * may do an up-to-date check which will not trigger any of the observers.
+ *
+ * @param eventHandler the error and warnings eventHandler; must be thread-safe
+ * @param targetsToVisit the targets to visit
+ * @param labelsToVisit the labels to visit in addition to the targets
+ * @param keepGoing if false, stop visitation upon first error.
+ * @param parallelThreads number of threads to use in the visitation.
+ * @param maxDepth the maximum depth to traverse to.
+ */
+ boolean sync(EventHandler eventHandler,
+ Set<Target> targetsToVisit,
+ Set<Label> labelsToVisit,
+ boolean keepGoing,
+ int parallelThreads,
+ int maxDepth) throws InterruptedException;
+
+ /**
+ * Returns a read-only view of the set of targets visited since this visitor
+ * was constructed.
+ *
+ * <p>Not thread-safe; do not call during visitation.
+ */
+ // TODO(bazel-team): This is only used in legacy non-Skyframe code.
+ Set<Label> getVisitedTargets();
+
+ /**
+ * Returns a read-only view of the set of packages visited since this visitor
+ * was constructed.
+ *
+ * <p>Not thread-safe; do not call during visitation.
+ */
+ Set<PackageIdentifier> getVisitedPackageNames();
+
+ /**
+ * Returns a read-only view of the set of the actual packages visited without error since this
+ * visitor was constructed.
+ *
+ * <p>Use {@link #getVisitedPackageNames()} instead when possible.
+ *
+ * <p>Not thread-safe; do not call during visitation.
+ */
+ Set<Package> getErrorFreeVisitedPackages();
+
+ /**
+ * Return a mapping between the specified top-level targets and root causes. Note that targets in
+ * the input that are transitively error free will not be in the output map. "Top-level" targets
+ * are the targetsToVisit and labelsToVisit specified in the last sync.
+ *
+ * <p>May only be called once a keep_going visitation is complete, and prior to
+ * trimErrorTracking().
+ *
+ * @param targetsToLoad the set of targets to be checked. Implementations may choose to only
+ * return root causes for targets in this set that were requested top-level targets.
+ * @return a mapping of targets to root causes
+ */
+ Multimap<Label, Label> getRootCauses(Collection<Label> targetsToLoad);
+}
diff --git a/src/main/java/com/google/devtools/build/lib/profiler/Describable.java b/src/main/java/com/google/devtools/build/lib/profiler/Describable.java
new file mode 100644
index 0000000..a5cc08a
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/profiler/Describable.java
@@ -0,0 +1,29 @@
+// Copyright 2014 Google Inc. 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.profiler;
+
+/**
+ * Allows class to implement profiler-friendly (and user-friendly)
+ * textual description of the object that would uniquely identify an object in
+ * the profiler data dump.
+ */
+public interface Describable {
+
+ /**
+ * Returns textual description that will uniquely identify an object.
+ */
+ String describe();
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/profiler/MemoryProfiler.java b/src/main/java/com/google/devtools/build/lib/profiler/MemoryProfiler.java
new file mode 100644
index 0000000..25ebd53
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/profiler/MemoryProfiler.java
@@ -0,0 +1,80 @@
+// Copyright 2014 Google Inc. 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.profiler;
+
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.lang.management.ManagementFactory;
+import java.lang.management.MemoryUsage;
+
+/**
+ * Blaze memory profiler.
+ *
+ * <p>At each call to {@code profile} performs garbage collection and stores
+ * heap and non-heap memory usage in an external file.
+ *
+ * <p><em>Heap memory</em> is the runtime data area from which memory for all
+ * class instances and arrays is allocated. <em>Non-heap memory</em> includes
+ * the method area and memory required for the internal processing or
+ * optimization of the JVM. It stores per-class structures such as a runtime
+ * constant pool, field and method data, and the code for methods and
+ * constructors. The Java Native Interface (JNI) code or the native library of
+ * an application and the JVM implementation allocate memory from the
+ * <em>native heap</em>.
+ *
+ * <p>The script in /devtools/blaze/scripts/blaze-memchart.sh can be used for post processing.
+ */
+public final class MemoryProfiler {
+
+ private static final MemoryProfiler INSTANCE = new MemoryProfiler();
+
+ public static MemoryProfiler instance() {
+ return INSTANCE;
+ }
+
+ private PrintStream memoryProfile;
+ private ProfilePhase currentPhase;
+
+ public synchronized void start(OutputStream out) {
+ this.memoryProfile = (out == null) ? null : new PrintStream(out);
+ this.currentPhase = ProfilePhase.INIT;
+ }
+
+ public synchronized void stop() {
+ if (memoryProfile != null) {
+ memoryProfile.close();
+ memoryProfile = null;
+ }
+ }
+
+ public synchronized void markPhase(ProfilePhase nextPhase) {
+ if (memoryProfile != null) {
+ String name = currentPhase.description;
+ ManagementFactory.getMemoryMXBean().gc();
+ MemoryUsage memoryUsage = ManagementFactory.getMemoryMXBean().getHeapMemoryUsage();
+ memoryProfile.println(name + ":heap:init:" + memoryUsage.getInit());
+ memoryProfile.println(name + ":heap:used:" + memoryUsage.getUsed());
+ memoryProfile.println(name + ":heap:commited:" + memoryUsage.getCommitted());
+ memoryProfile.println(name + ":heap:max:" + memoryUsage.getMax());
+
+ memoryUsage = ManagementFactory.getMemoryMXBean().getNonHeapMemoryUsage();
+ memoryProfile.println(name + ":non-heap:init:" + memoryUsage.getInit());
+ memoryProfile.println(name + ":non-heap:used:" + memoryUsage.getUsed());
+ memoryProfile.println(name + ":non-heap:commited:" + memoryUsage.getCommitted());
+ memoryProfile.println(name + ":non-heap:max:" + memoryUsage.getMax());
+ currentPhase = nextPhase;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/profiler/ProfileInfo.java b/src/main/java/com/google/devtools/build/lib/profiler/ProfileInfo.java
new file mode 100644
index 0000000..4ce3a93
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/profiler/ProfileInfo.java
@@ -0,0 +1,926 @@
+// Copyright 2014 Google Inc. 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.profiler;
+
+import static com.google.devtools.build.lib.profiler.ProfilerTask.CRITICAL_PATH;
+import static com.google.devtools.build.lib.profiler.ProfilerTask.CRITICAL_PATH_COMPONENT;
+import static com.google.devtools.build.lib.profiler.ProfilerTask.TASK_COUNT;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.devtools.build.lib.util.VarInt;
+import com.google.devtools.build.lib.vfs.Path;
+
+import java.io.BufferedInputStream;
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.nio.ByteBuffer;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Deque;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.zip.Inflater;
+import java.util.zip.InflaterInputStream;
+
+/**
+ * Holds parsed profile file information and provides various ways of
+ * accessing it (mostly through different dictionaries or sorted lists).
+ *
+ * Class should not be instantiated directly but through the use of the
+ * ProfileLoader.loadProfile() method.
+ */
+public class ProfileInfo {
+
+ /**
+ * Immutable container for the aggregated stats.
+ */
+ public static final class AggregateAttr {
+ public final int count;
+ public final long totalTime;
+
+ AggregateAttr(int count, long totalTime) {
+ this.count = count;
+ this.totalTime = totalTime;
+ }
+ }
+
+ /**
+ * Immutable compact representation of the Map<ProfilerTask, AggregateAttr>.
+ */
+ static final class CompactStatistics {
+ final byte[] content;
+
+ CompactStatistics(byte[] content) {
+ this.content = content;
+ }
+
+ /**
+ * Create compact task statistic instance using provided array.
+ * Array length must exactly match ProfilerTask value space.
+ * Each statistic is stored in the array according to the ProfilerTask
+ * value ordinal() number. Absent statistics are represented by null.
+ */
+ CompactStatistics(AggregateAttr[] stats) {
+ Preconditions.checkArgument(stats.length == TASK_COUNT);
+ ByteBuffer sink = ByteBuffer.allocate(TASK_COUNT * (1 + 5 + 10));
+ for (int i = 0; i < TASK_COUNT; i++) {
+ if (stats[i] != null && stats[i].count > 0) {
+ sink.put((byte) i);
+ VarInt.putVarInt(stats[i].count, sink);
+ VarInt.putVarLong(stats[i].totalTime, sink);
+ }
+ }
+ content = sink.position() > 0 ? Arrays.copyOfRange(sink.array(), 0, sink.position()) : null;
+ }
+
+ boolean isEmpty() { return content == null; }
+
+ /**
+ * Converts instance back into AggregateAttr[TASK_COUNT]. See
+ * constructor documentation for more information.
+ */
+ AggregateAttr[] toArray() {
+ AggregateAttr[] stats = new AggregateAttr[TASK_COUNT];
+ if (!isEmpty()) {
+ ByteBuffer source = ByteBuffer.wrap(content);
+ while (source.hasRemaining()) {
+ byte id = source.get();
+ int count = VarInt.getVarInt(source);
+ long time = VarInt.getVarLong(source);
+ stats[id] = new AggregateAttr(count, time);
+ }
+ }
+ return stats;
+ }
+
+ /**
+ * Returns AggregateAttr instance for the given ProfilerTask value.
+ */
+ AggregateAttr getAttr(ProfilerTask task) {
+ if (isEmpty()) { return ZERO; }
+ ByteBuffer source = ByteBuffer.wrap(content);
+ byte id = (byte) task.ordinal();
+ while (source.hasRemaining()) {
+ if (id == source.get()) {
+ int count = VarInt.getVarInt(source);
+ long time = VarInt.getVarLong(source);
+ return new AggregateAttr(count, time);
+ } else {
+ VarInt.getVarInt(source);
+ VarInt.getVarLong(source);
+ }
+ }
+ return ZERO;
+ }
+
+ /**
+ * Returns cumulative time stored in this instance across whole
+ * ProfilerTask dimension.
+ */
+ long getTotalTime() {
+ if (isEmpty()) { return 0; }
+ ByteBuffer source = ByteBuffer.wrap(content);
+ long totalTime = 0;
+ while (source.hasRemaining()) {
+ source.get();
+ VarInt.getVarInt(source);
+ totalTime += VarInt.getVarLong(source);
+ }
+ return totalTime;
+ }
+ }
+
+ /**
+ * Container for the profile record information.
+ *
+ * <p> TODO(bazel-team): (2010) Current Task instance heap size is 72 bytes. And there are
+ * millions of them. Consider trimming some attributes.
+ */
+ public final class Task implements Comparable<Task> {
+ public final long threadId;
+ public final int id;
+ public final int parentId;
+ public final long startTime;
+ public final long duration;
+ public final ProfilerTask type;
+ final CompactStatistics stats;
+ // Contains statistic for a task and all subtasks. Populated only for root tasks.
+ CompactStatistics aggregatedStats = null;
+ // Subtasks are stored as an array for performance and memory utilization
+ // reasons (we can easily deal with millions of those objects).
+ public Task[] subtasks = NO_TASKS;
+ final int descIndex;
+ // Reference to the related task (e.g. ACTION_GRAPH->ACTION task relation).
+ private Task relatedTask;
+
+ Task(long threadId, int id, int parentId, long startTime, long duration,
+ ProfilerTask type, int descIndex, CompactStatistics stats) {
+ this.threadId = threadId;
+ this.id = id;
+ this.parentId = parentId;
+ this.startTime = startTime;
+ this.duration = duration;
+ this.type = type;
+ this.descIndex = descIndex;
+ this.stats = stats;
+ relatedTask = null;
+ }
+
+ public String getDescription() {
+ return descriptionList.get(descIndex);
+ }
+
+ public boolean hasStats() {
+ return !stats.isEmpty();
+ }
+
+ public long getInheritedDuration() {
+ return stats.getTotalTime();
+ }
+
+ public AggregateAttr[] getStatAttrArray() {
+ Preconditions.checkNotNull(stats);
+ return stats.toArray();
+ }
+
+ private void combineStats(int[] counts, long[] duration) {
+ int ownIndex = type.ordinal();
+ if (parentId != 0) {
+ // Parent task already accounted for this task total duration. We need to adjust
+ // for the inherited duration.
+ duration[ownIndex] -= getInheritedDuration();
+ }
+ AggregateAttr[] ownStats = stats.toArray();
+ for (int i = 0; i < TASK_COUNT; i++) {
+ AggregateAttr attr = ownStats[i];
+ if (attr != null) {
+ counts[i] += attr.count;
+ duration[i] += attr.totalTime;
+ }
+ }
+ for (Task task : subtasks) {
+ task.combineStats(counts, duration);
+ }
+ }
+
+ /**
+ * Calculates aggregated statistics covering all subtasks (including
+ * nested ones). Must be called only for parent tasks.
+ */
+ void calculateRootStats() {
+ Preconditions.checkState(parentId == 0);
+ int[] counts = new int[TASK_COUNT];
+ long[] duration = new long[TASK_COUNT];
+ combineStats(counts, duration);
+ AggregateAttr[] statArray = ProfileInfo.createEmptyStatArray();
+ for (int i = 0; i < TASK_COUNT; i++) {
+ statArray[i] = new AggregateAttr(counts[i], duration[i]);
+ }
+ this.aggregatedStats = new CompactStatistics(statArray);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ return (o instanceof ProfileInfo.Task) && ((Task) o).id == this.id;
+ }
+
+ @Override
+ public int hashCode() {
+ return this.id;
+ }
+
+ @Override
+ public String toString() {
+ return type + "(" + id + "," + getDescription() + ")";
+ }
+
+ /**
+ * Tasks records by default sorted by their id. Since id was obtained using
+ * AtomicInteger, this comparison will correctly sort tasks in time-ascending
+ * order regardless of their origin thread.
+ */
+ @Override
+ public int compareTo(Task task) {
+ return this.id - task.id;
+ }
+ }
+
+ /**
+ * Represents node on critical build path
+ */
+ public static final class CriticalPathEntry {
+ public final Task task;
+ public final long duration;
+ public final long cumulativeDuration;
+ public final CriticalPathEntry next;
+
+ private long criticalTime = 0L;
+
+ public CriticalPathEntry(Task task, long duration, CriticalPathEntry next) {
+ this.task = task;
+ this.duration = duration;
+ this.next = next;
+ this.cumulativeDuration =
+ duration + (next != null ? next.cumulativeDuration : 0);
+ }
+
+ private void setCriticalTime(long duration) {
+ criticalTime = duration;
+ }
+
+ public long getCriticalTime() {
+ return criticalTime;
+ }
+ }
+
+ /**
+ * Helper class to create space-efficient task multimap, used to associate
+ * array of tasks with specific key.
+ */
+ private abstract static class TaskMapCreator<K> implements Comparator<Task> {
+ @Override
+ public abstract int compare(Task a, Task b);
+ public abstract K getKey(Task task);
+
+ public Map<K, Task[]> createTaskMap(List<Task> taskList) {
+ // Created map usually will end up with thousands of entries, so we
+ // preinitialize it to the 10000.
+ Map<K, Task[]> taskMap = Maps.newHashMapWithExpectedSize(10000);
+ if (taskList.size() == 0) { return taskMap; }
+ Task[] taskArray = taskList.toArray(new Task[taskList.size()]);
+ Arrays.sort(taskArray, this);
+ K key = getKey(taskArray[0]);
+ int start = 0;
+ for (int i = 0; i < taskArray.length; i++) {
+ K currentKey = getKey(taskArray[i]);
+ if (!key.equals(currentKey)) {
+ taskMap.put(key, Arrays.copyOfRange(taskArray, start, i));
+ key = currentKey;
+ start = i;
+ }
+ }
+ if (start < taskArray.length) {
+ taskMap.put(key, Arrays.copyOfRange(taskArray, start, taskArray.length));
+ }
+ return taskMap;
+ }
+ }
+
+ /**
+ * An interface to pass back profile loading and aggregation messages.
+ */
+ public interface InfoListener {
+ void info(String text);
+ void warn(String text);
+ }
+
+ private static final Task[] NO_TASKS = new Task[0];
+ private static final AggregateAttr ZERO = new AggregateAttr(0, 0);
+
+ public final String comment;
+ private boolean corruptedOrIncomplete = false;
+
+ // TODO(bazel-team): (2010) In one case, this list took 277MB of heap. Ideally it should be
+ // replaced with a trie.
+ private final List<String> descriptionList;
+ private final Map<Task, Task> parallelBuilderCompletionQueueTasks;
+ public final Map<Long, Task[]> tasksByThread;
+ public final List<Task> allTasksById;
+ public List<Task> rootTasksById; // Not final due to the late initialization.
+ public final List<Task> phaseTasks;
+
+ public final Map<Task, Task[]> actionDependencyMap;
+ // Used to create fake Action tasks if ACTIONG_GRAPH task does not have
+ // corresponding ACTION task. For action dependency calculations we will
+ // create fake ACTION tasks and assign them negative ids.
+ private int fakeActionId = 0;
+
+ private ProfileInfo(String comment) {
+ this.comment = comment;
+
+ descriptionList = Lists.newArrayListWithExpectedSize(10000);
+ tasksByThread = Maps.newHashMap();
+ parallelBuilderCompletionQueueTasks = Maps.newHashMap();
+ allTasksById = Lists.newArrayListWithExpectedSize(50000);
+ phaseTasks = Lists.newArrayList();
+ actionDependencyMap = Maps.newHashMapWithExpectedSize(10000);
+ }
+
+ private void addTask(Task task) {
+ allTasksById.add(task);
+ }
+
+ /**
+ * Returns true if profile datafile was corrupted or incomplete
+ * and false otherwise.
+ */
+ public boolean isCorruptedOrIncomplete() {
+ return corruptedOrIncomplete;
+ }
+
+ /**
+ * Returns number of missing actions which were faked in order to complete
+ * action graph.
+ */
+ public int getMissingActionsCount() {
+ return -fakeActionId;
+ }
+
+ /**
+ * Initializes minimum internal data structures necessary to obtain individual
+ * task statistic. This method is sufficient to initialize data for dumping.
+ */
+ public void calculateStats() {
+ if (allTasksById.size() == 0) {
+ return;
+ }
+
+ Collections.sort(allTasksById);
+
+ Map<Integer, Task[]> subtaskMap = new TaskMapCreator<Integer>() {
+ @Override
+ public int compare(Task a, Task b) {
+ return a.parentId != b.parentId ? a.parentId - b.parentId : a.compareTo(b);
+ }
+ @Override
+ public Integer getKey(Task task) { return task.parentId; }
+ }.createTaskMap(allTasksById);
+ for (Task task : allTasksById) {
+ Task[] subtasks = subtaskMap.get(task.id);
+ if (subtasks != null) {
+ task.subtasks = subtasks;
+ }
+ }
+ rootTasksById = Arrays.asList(subtaskMap.get(0));
+
+ for (Task task : rootTasksById) {
+ task.calculateRootStats();
+ if (task.type == ProfilerTask.PHASE) {
+ if (!phaseTasks.isEmpty()) {
+ phaseTasks.get(phaseTasks.size() - 1).relatedTask = task;
+ }
+ phaseTasks.add(task);
+ }
+ }
+ }
+
+ /**
+ * Analyzes task relationships and dependencies. Used for the detailed profile
+ * analysis.
+ */
+ public void analyzeRelationships() {
+ tasksByThread.putAll(new TaskMapCreator<Long>() {
+ @Override
+ public int compare(Task a, Task b) {
+ return a.threadId != b.threadId ? (a.threadId < b.threadId ? -1 : 1) : a.compareTo(b);
+ }
+ @Override
+ public Long getKey(Task task) { return task.threadId; }
+ }.createTaskMap(rootTasksById));
+
+ buildDependencyMap();
+ }
+
+ /**
+ * Calculates cumulative time attributed to the specific task type.
+ * Expects to be called only for root (parentId = 0) tasks.
+ * calculateStats() must have been called first.
+ */
+ public AggregateAttr getStatsForType(ProfilerTask type, Collection<Task> tasks) {
+ long totalTime = 0;
+ int count = 0;
+ for (Task task : tasks) {
+ if (task.parentId > 0) {
+ throw new IllegalArgumentException("task " + task.id + " is not a root task");
+ }
+ AggregateAttr attr = task.aggregatedStats.getAttr(type);
+ count += attr.count;
+ totalTime += attr.totalTime;
+ if (task.type == type) {
+ count++;
+ totalTime += (task.duration - task.getInheritedDuration());
+ }
+ }
+ return new AggregateAttr(count, totalTime);
+ }
+
+ /**
+ * Returns list of all root tasks related to (in other words, started during)
+ * the specified phase task.
+ */
+ public List<Task> getTasksForPhase(Task phaseTask) {
+ Preconditions.checkArgument(phaseTask.type == ProfilerTask.PHASE,
+ "Unsupported task type %s", phaseTask.type);
+
+ // Algorithm below takes into account fact that rootTasksById list is sorted
+ // by the task id and task id values are monotonically increasing with time
+ // (this property is guaranteed by the profiler). Thus list is effectively
+ // sorted by the startTime. We are trying to select a sublist that includes
+ // all tasks that were started later than the given task but earlier than
+ // its completion time.
+ int startIndex = Collections.binarySearch(rootTasksById, phaseTask);
+ Preconditions.checkState(startIndex >= 0,
+ "Phase task %s is not a root task", phaseTask.id);
+ int endIndex = (phaseTask.relatedTask != null)
+ ? Collections.binarySearch(rootTasksById, phaseTask.relatedTask)
+ : rootTasksById.size();
+ Preconditions.checkState(endIndex >= startIndex,
+ "Failed to find end of the phase marked by the task %s", phaseTask.id);
+ return rootTasksById.subList(startIndex, endIndex);
+ }
+
+ /**
+ * Returns task with "Build artifacts" description - corresponding to the
+ * execution phase. Usually used to location ACTION_GRAPH task tree.
+ */
+ public Task getPhaseTask(ProfilePhase phase) {
+ for (Task task : phaseTasks) {
+ if (task.getDescription().equals(phase.description)) {
+ return task;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns duration of the given phase in ns.
+ */
+ public long getPhaseDuration(Task phaseTask) {
+ Preconditions.checkArgument(phaseTask.type == ProfilerTask.PHASE,
+ "Unsupported task type %s", phaseTask.type);
+
+ long duration;
+ if (phaseTask.relatedTask != null) {
+ duration = phaseTask.relatedTask.startTime - phaseTask.startTime;
+ } else {
+ Task lastTask = rootTasksById.get(rootTasksById.size() - 1);
+ duration = lastTask.startTime + lastTask.duration - phaseTask.startTime;
+ }
+ Preconditions.checkState(duration >= 0);
+ return duration;
+ }
+
+
+ /**
+ * Builds map of dependencies between ACTION tasks based on dependencies
+ * between ACTION_GRAPH tasks
+ */
+ private Task buildActionTaskTree(Task actionGraphTask, List<Task> actionTasksByDescription) {
+ Task actionTask = actionGraphTask.relatedTask;
+ if (actionTask == null) {
+ actionTask = actionTasksByDescription.get(actionGraphTask.descIndex);
+ if (actionTask == null) {
+ // If we cannot find ACTION task that corresponds to the ACTION_GRAPH task,
+ // most likely scenario is that we dealing with either aborted or failed
+ // build. In this case we will find or create fake zero-duration action
+ // task and still reconstruct dependency graph.
+ actionTask = new Task(-1, --fakeActionId, 0, 0, 0,
+ ProfilerTask.ACTION, actionGraphTask.descIndex, new CompactStatistics((byte[]) null));
+ actionTask.calculateRootStats();
+ actionTasksByDescription.set(actionGraphTask.descIndex, actionTask);
+ }
+ actionGraphTask.relatedTask = actionTask;
+ }
+ if (actionGraphTask.subtasks.length != 0) {
+ List<Task> list = Lists.newArrayListWithCapacity(actionGraphTask.subtasks.length);
+ for (Task task : actionGraphTask.subtasks) {
+ if (task.type == ProfilerTask.ACTION_GRAPH) {
+ list.add(buildActionTaskTree(task, actionTasksByDescription));
+ }
+ }
+ if (!list.isEmpty()) {
+ Task[] actionPrerequisites = list.toArray(new Task[list.size()]);
+ Arrays.sort(actionPrerequisites);
+ actionDependencyMap.put(actionTask, actionPrerequisites);
+ }
+ }
+ return actionTask;
+ }
+
+ /**
+ * Builds map of dependencies between ACTION tasks based on dependencies
+ * between ACTION_GRAPH tasks. Root of that dependency tree would be
+ * getBuildPhaseTask().
+ *
+ * <p> Also marks related ACTION and ACTION_SUBMIT tasks.
+ */
+ private void buildDependencyMap() {
+ Task analysisPhaseTask = getPhaseTask(ProfilePhase.ANALYZE);
+ Task executionPhaseTask = getPhaseTask(ProfilePhase.EXECUTE);
+ if ((executionPhaseTask == null) || (analysisPhaseTask == null)) {
+ return;
+ }
+ // Association between ACTION_GRAPH tasks and ACTION tasks can be established through
+ // description id. So we create appropriate xref list.
+ List<Task> actionTasksByDescription = Lists.newArrayList(new Task[descriptionList.size()]);
+ for (Task task : getTasksForPhase(executionPhaseTask)) {
+ if (task.type == ProfilerTask.ACTION) {
+ actionTasksByDescription.set(task.descIndex, task);
+ }
+ }
+ List<Task> list = new ArrayList<>();
+ for (Task task : getTasksForPhase(analysisPhaseTask)) {
+ if (task.type == ProfilerTask.ACTION_GRAPH) {
+ list.add(buildActionTaskTree(task, actionTasksByDescription));
+ }
+ }
+ Task[] actionPrerequisites = list.toArray(new Task[list.size()]);
+ Arrays.sort(actionPrerequisites);
+ actionDependencyMap.put(executionPhaseTask, actionPrerequisites);
+
+ // Scan through all execution phase tasks to identify ACTION_SUBMIT tasks and associate
+ // them with ACTION task counterparts. ACTION_SUBMIT tasks are not necessarily root
+ // tasks so we need to scan ALL tasks.
+ for (Task task : allTasksById.subList(executionPhaseTask.id, allTasksById.size())) {
+ if (task.type == ProfilerTask.ACTION_SUBMIT) {
+ Task actionTask = actionTasksByDescription.get(task.descIndex);
+ if (actionTask != null) {
+ task.relatedTask = actionTask;
+ actionTask.relatedTask = task;
+ }
+ } else if (task.type == ProfilerTask.ACTION_BUILDER) {
+ Task actionTask = actionTasksByDescription.get(task.descIndex);
+ if (actionTask != null) {
+ parallelBuilderCompletionQueueTasks.put(actionTask, task);
+ }
+ }
+ }
+ }
+
+ /**
+ * Calculates critical path for the specific action
+ * excluding specified nested task types (e.g. VFS-related time) and not
+ * accounting for overhead related to the Blaze scheduler.
+ */
+ private CriticalPathEntry computeCriticalPathForAction(
+ Set<ProfilerTask> ignoredTypes, Set<Task> ignoredTasks,
+ Task actionTask, Map<Task, CriticalPathEntry> cache, Deque<Task> stack) {
+
+ // Loop check is expensive for the Deque (and we don't want to use hash sets because adding
+ // and removing elements was shown to be very expensive). To avoid quadratic costs we're
+ // checking for infinite loop only when deque's size equal to the power of 2 and >= 32.
+ if ((stack.size() & 0x1F) == 0 && Integer.bitCount(stack.size()) == 1) {
+ if (stack.contains(actionTask)) {
+ // This situation will appear if build has ended with the
+ // IllegalStateException thrown by the
+ // ParallelBuilder.getNextCompletedAction(), warning user about
+ // possible cycle in the dependency graph. But the exception text
+ // is more friendly and will actually identify the loop.
+ // Do not use Preconditions class below due to the very expensive
+ // toString() calls used in the message.
+ throw new IllegalStateException ("Dependency graph contains loop:\n"
+ + actionTask + " in the\n" + Joiner.on('\n').join(stack));
+ }
+ }
+ stack.addLast(actionTask);
+ CriticalPathEntry entry;
+ try {
+ entry = cache.get(actionTask);
+ long entryDuration = 0;
+ if (entry == null) {
+ Task[] actionPrerequisites = actionDependencyMap.get(actionTask);
+ if (actionPrerequisites != null) {
+ for (Task task : actionPrerequisites) {
+ CriticalPathEntry candidate =
+ computeCriticalPathForAction(ignoredTypes, ignoredTasks, task, cache, stack);
+ if (entry == null || entryDuration < candidate.cumulativeDuration) {
+ entry = candidate;
+ entryDuration = candidate.cumulativeDuration;
+ }
+ }
+ }
+ if (actionTask.type == ProfilerTask.ACTION) {
+ long duration = actionTask.duration;
+ if (ignoredTasks.contains(actionTask)) {
+ duration = 0L;
+ } else {
+ for (ProfilerTask type : ignoredTypes) {
+ duration -= actionTask.aggregatedStats.getAttr(type).totalTime;
+ }
+ }
+
+ entry = new CriticalPathEntry(actionTask, duration, entry);
+ cache.put(actionTask, entry);
+ }
+ }
+ } finally {
+ stack.removeLast();
+ }
+ return entry;
+ }
+
+ /**
+ * Returns the critical path information from the {@code CriticalPathComputer} recorded stats.
+ * This code does not have the "Critical" column (Time difference if we removed this node from
+ * the critical path).
+ */
+ public CriticalPathEntry getCriticalPathNewVersion() {
+ for (Task task : rootTasksById) {
+ if (task.type == CRITICAL_PATH) {
+ CriticalPathEntry entry = null;
+ for (Task shared : task.subtasks) {
+ entry = new CriticalPathEntry(shared, shared.duration, entry);
+ }
+ return entry;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Calculates critical path for the given action graph excluding
+ * specified tasks (usually ones that belong to the "real" critical path).
+ */
+ public CriticalPathEntry getCriticalPath(Set<ProfilerTask> ignoredTypes) {
+ Task actionTask = getPhaseTask(ProfilePhase.EXECUTE);
+ if (actionTask == null) {
+ return null;
+ }
+ Map <Task, CriticalPathEntry> cache = Maps.newHashMapWithExpectedSize(1000);
+ CriticalPathEntry result = computeCriticalPathForAction(ignoredTypes,
+ new HashSet<Task>(), actionTask, cache,
+ new ArrayDeque<Task>());
+ if (result != null) {
+ return result;
+ }
+ return getCriticalPathNewVersion();
+ }
+
+ /**
+ * Calculates critical path time that will be saved by eliminating specific
+ * entry from the critical path
+ */
+ public void analyzeCriticalPath(Set<ProfilerTask> ignoredTypes, CriticalPathEntry path) {
+ // With light critical path we do not need to analyze since it is already preprocessed
+ // by blaze build.
+ if (path != null && path.task.type == CRITICAL_PATH_COMPONENT) {
+ return;
+ }
+ for (CriticalPathEntry entry = path; entry != null; entry = entry.next) {
+ Map <Task, CriticalPathEntry> cache = Maps.newHashMapWithExpectedSize(1000);
+ entry.setCriticalTime(path.cumulativeDuration -
+ computeCriticalPathForAction(ignoredTypes, Sets.newHashSet(entry.task),
+ getPhaseTask(ProfilePhase.EXECUTE), cache, new ArrayDeque<Task>())
+ .cumulativeDuration);
+ }
+ }
+
+ /**
+ * Return the next critical path entry for the task or null if there is none.
+ */
+ public CriticalPathEntry getNextCriticalPathEntryForTask(CriticalPathEntry path, Task task) {
+ for (CriticalPathEntry entry = path; entry != null; entry = entry.next) {
+ if (entry.task.id == task.id) {
+ return entry;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns time action waited in the execution queue (difference between
+ * ACTION task start time and ACTION_SUBMIT task start time).
+ */
+ public long getActionWaitTime(Task actionTask) {
+ // Light critical path does not record wait time.
+ if (actionTask.type == ProfilerTask.CRITICAL_PATH_COMPONENT) {
+ return 0;
+ }
+ Preconditions.checkArgument(actionTask.type == ProfilerTask.ACTION);
+ if (actionTask.relatedTask != null) {
+ Preconditions.checkState(actionTask.relatedTask.type == ProfilerTask.ACTION_SUBMIT);
+ long time = actionTask.startTime - actionTask.relatedTask.startTime;
+ Preconditions.checkState(time >= 0);
+ return time;
+ } else {
+ return 0L; // submission time is not available.
+ }
+ }
+
+ /**
+ * Returns time action waited in the parallel builder completion queue
+ * (difference between ACTION task end time and ACTION_BUILDER start time).
+ */
+ public long getActionQueueTime(Task actionTask) {
+ // Light critical path does not record queue time.
+ if (actionTask.type == ProfilerTask.CRITICAL_PATH_COMPONENT) {
+ return 0;
+ }
+ Preconditions.checkArgument(actionTask.type == ProfilerTask.ACTION);
+ Task related = parallelBuilderCompletionQueueTasks.get(actionTask);
+ if (related != null) {
+ Preconditions.checkState(related.type == ProfilerTask.ACTION_BUILDER);
+ long time = related.startTime - (actionTask.startTime + actionTask.duration);
+ Preconditions.checkState(time >= 0);
+ return time;
+ } else {
+ return 0L; // queue task is not available.
+ }
+ }
+
+ /**
+ * Returns an empty array used to store task statistics. Array index
+ * corresponds to the ProfilerTask ordinal() value associated with the
+ * given statistic. Absent statistics are stored as null.
+ * <p>
+ * In essence, it is a fast equivalent of Map<ProfilerTask, AggregateAttr>.
+ */
+ public static AggregateAttr[] createEmptyStatArray() {
+ return new AggregateAttr[TASK_COUNT];
+ }
+
+ /**
+ * Loads and parses Blaze profile file.
+ *
+ * @param profileFile profile file path
+ *
+ * @return ProfileInfo object with some fields populated (call calculateStats()
+ * and analyzeRelationships() to populate the remaining fields)
+ * @throws UnsupportedEncodingException if the file format is invalid
+ * @throws IOException if the file can't be read
+ */
+ public static ProfileInfo loadProfile(Path profileFile)
+ throws IOException {
+ // It is extremely important to wrap InflaterInputStream using
+ // BufferedInputStream because majority of reads would be done using
+ // readInt()/readLong() methods and InflaterInputStream is very inefficient
+ // in handling small read requests (performance difference with 1MB buffer
+ // used below is almost 10x).
+ DataInputStream in = new DataInputStream(
+ new BufferedInputStream(new InflaterInputStream(
+ profileFile.getInputStream(), new Inflater(false), 65536), 1024 * 1024));
+
+ if (in.readInt() != Profiler.MAGIC) {
+ in.close();
+ throw new UnsupportedEncodingException("Invalid profile datafile format");
+ }
+ if (in.readInt() != Profiler.VERSION) {
+ in.close();
+ throw new UnsupportedEncodingException("Incompatible profile datafile version");
+ }
+ String fileComment = in.readUTF();
+
+ // Read list of used record types
+ int typeCount = in.readInt();
+ boolean hasUnknownTypes = false;
+ Set<String> supportedTasks = new HashSet<>();
+ for (ProfilerTask task : ProfilerTask.values()) {
+ supportedTasks.add(task.toString());
+ }
+ List<ProfilerTask> typeList = new ArrayList<>();
+ for (int i = 0; i < typeCount; i++) {
+ String name = in.readUTF();
+ if (supportedTasks.contains(name)) {
+ typeList.add(ProfilerTask.valueOf(name));
+ } else {
+ hasUnknownTypes = true;
+ typeList.add(ProfilerTask.UNKNOWN);
+ }
+ }
+
+ ProfileInfo info = new ProfileInfo(fileComment);
+
+ // Read record until we encounter end marker (-1).
+ // TODO(bazel-team): Maybe this still should handle corrupted(truncated) files.
+ try {
+ int size;
+ while ((size = in.readInt()) != Profiler.EOF_MARKER) {
+ byte[] backingArray = new byte[size];
+ in.readFully(backingArray);
+ ByteBuffer buffer = ByteBuffer.wrap(backingArray);
+ long threadId = VarInt.getVarLong(buffer);
+ int id = VarInt.getVarInt(buffer);
+ int parentId = VarInt.getVarInt(buffer);
+ long startTime = VarInt.getVarLong(buffer);
+ long duration = VarInt.getVarLong(buffer);
+ int descIndex = VarInt.getVarInt(buffer) - 1;
+ if (descIndex == -1) {
+ String desc = in.readUTF();
+ descIndex = info.descriptionList.size();
+ info.descriptionList.add(desc);
+ }
+ ProfilerTask type = typeList.get(buffer.get());
+ byte[] stats = null;
+ if (buffer.hasRemaining()) {
+ // Copy aggregated stats.
+ int offset = buffer.position();
+ stats = Arrays.copyOfRange(backingArray, offset, size);
+ if (hasUnknownTypes) {
+ while (buffer.hasRemaining()) {
+ byte attrType = buffer.get();
+ if (typeList.get(attrType) == ProfilerTask.UNKNOWN) {
+ // We're dealing with unknown aggregated type - update stats array to
+ // use ProfilerTask.UNKNOWN.ordinal() value.
+ stats[buffer.position() - 1 - offset] = (byte) ProfilerTask.UNKNOWN.ordinal();
+ }
+ VarInt.getVarInt(buffer);
+ VarInt.getVarLong(buffer);
+ }
+ }
+ }
+ ProfileInfo.Task task = info.new Task(threadId, id, parentId, startTime, duration, type,
+ descIndex, new CompactStatistics(stats));
+ info.addTask(task);
+ }
+ } catch (IOException e) {
+ info.corruptedOrIncomplete = true;
+ } finally {
+ in.close();
+ }
+
+ return info;
+ }
+
+ /**
+ * Loads and parses Blaze profile file, and reports what it is doing.
+ *
+ * @param profileFile profile file path
+ * @param reporter for progress messages and warnings
+ *
+ * @return ProfileInfo object with most fields populated
+ * (call analyzeRelationships() to populate the remaining fields)
+ * @throws UnsupportedEncodingException if the file format is invalid
+ * @throws IOException if the file can't be read
+ */
+ public static ProfileInfo loadProfileVerbosely(Path profileFile, InfoListener reporter)
+ throws IOException {
+ reporter.info("Loading " + profileFile.getPathString());
+ ProfileInfo profileInfo = ProfileInfo.loadProfile(profileFile);
+ if (profileInfo.isCorruptedOrIncomplete()) {
+ reporter.warn("Profile file is incomplete or corrupted - not all records were parsed");
+ }
+ reporter.info(profileInfo.comment + ", " + profileInfo.allTasksById.size() + " record(s)");
+ return profileInfo;
+ }
+
+ /*
+ * Sorts and aggregates Blaze profile file, and reports what it is doing.
+ */
+ public static void aggregateProfile(ProfileInfo profileInfo, InfoListener reporter) {
+ reporter.info("Aggregating task statistics");
+ profileInfo.calculateStats();
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/profiler/ProfilePhase.java b/src/main/java/com/google/devtools/build/lib/profiler/ProfilePhase.java
new file mode 100644
index 0000000..fa4b862
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/profiler/ProfilePhase.java
@@ -0,0 +1,42 @@
+// Copyright 2014 Google Inc. 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.profiler;
+
+/**
+ * Build phase markers. Used as a separators between different build phases.
+ */
+public enum ProfilePhase {
+ LAUNCH("launch", "Launch Blaze", 0x3F9FCF9F), // 9C9
+ INIT("init", "Initialize command", 0x3F9F9FCF), // 99C
+ LOAD("loading", "Load packages", 0x3FCFFFCF), // CFC
+ ANALYZE("analysis", "Analyze dependencies", 0x3FCFCFFF), // CCF
+ LICENSE("license checking", "Analyze licenses", 0x3FCFFFFF), // CFF
+ PREPARE("preparation", "Prepare for build", 0x3FFFFFCF), // FFC
+ EXECUTE("execution", "Build artifacts", 0x3FFFCFCF), // FCC
+ FINISH("finish", "Complete build",0x3FFFCFFF); // FCF
+
+ /** Short name for the phase */
+ public final String nick;
+ /** Human readable description for the phase. */
+ public final String description;
+ /** Default color of the task, when rendered in a chart. */
+ public final int color;
+
+ ProfilePhase(String nick, String description, int color) {
+ this.nick = nick;
+ this.description = description;
+ this.color = color;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/profiler/ProfilePhaseStatistics.java b/src/main/java/com/google/devtools/build/lib/profiler/ProfilePhaseStatistics.java
new file mode 100644
index 0000000..f3eb525
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/profiler/ProfilePhaseStatistics.java
@@ -0,0 +1,42 @@
+// Copyright 2014 Google Inc. 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.profiler;
+
+/**
+ * Hold pre-formatted statistics of a profiled execution phase.
+ *
+ * TODO(bazel-team): Change String statistics into StatisticsTable[], where StatisticsTable is an
+ * Object with a title (can be null), header[columns] (can be null), data[rows][columns],
+ * alignment[columns] (left/right).
+ * The HtmlChartsVisitor can turn that into HTML tables, the text formatter can calculate the max
+ * for each column and format the text accordingly.
+ */
+public class ProfilePhaseStatistics {
+ private final String title;
+ private final String statistics;
+
+ public ProfilePhaseStatistics (String title, String statistics) {
+ this.title = title;
+ this.statistics = statistics;
+ }
+
+ public String getTitle(){
+ return title;
+ }
+
+ public String getStatistics(){
+ return statistics;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/profiler/Profiler.java b/src/main/java/com/google/devtools/build/lib/profiler/Profiler.java
new file mode 100644
index 0000000..d592848
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/profiler/Profiler.java
@@ -0,0 +1,871 @@
+// Copyright 2014 Google Inc. 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.profiler;
+
+import static com.google.devtools.build.lib.profiler.ProfilerTask.TASK_COUNT;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.util.Clock;
+import com.google.devtools.build.lib.util.VarInt;
+
+import java.io.BufferedOutputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.PriorityQueue;
+import java.util.Queue;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.zip.Deflater;
+import java.util.zip.DeflaterOutputStream;
+
+/**
+ * Blaze internal profiler. Provides facility to report various Blaze tasks and
+ * store them (asynchronously) in the file for future analysis.
+ * <p>
+ * Implemented as singleton so any caller should use Profiler.instance() to
+ * obtain reference.
+ * <p>
+ * Internally, profiler uses two data structures - ThreadLocal task stack to track
+ * nested tasks and single ConcurrentLinkedQueue to gather all completed tasks.
+ * <p>
+ * Also, due to the nature of the provided functionality (instrumentation of all
+ * Blaze components), build.lib.profiler package will be used by almost every
+ * other Blaze package, so special attention should be paid to avoid any
+ * dependencies on the rest of the Blaze code, including build.lib.util and
+ * build.lib.vfs. This is important because build.lib.util and build.lib.vfs
+ * contain Profiler invocations and any dependency on those two packages would
+ * create circular relationship.
+ * <p>
+ * All gathered instrumentation data will be stored in the file. Please, note,
+ * that while file format is described here it is considered internal and can
+ * change at any time. For scripting, using blaze analyze-profile --dump=raw
+ * would be more robust and stable solution.
+ * <p>
+ * <pre>
+ * Profiler file consists of the deflated stream with following overall structure:
+ * HEADER
+ * TASK_TYPE_TABLE
+ * TASK_RECORD...
+ * EOF_MARKER
+ *
+ * HEADER:
+ * int32: magic token (Profiler.MAGIC)
+ * int32: version format (Profiler.VERSION)
+ * string: file comment
+ *
+ * TASK_TYPE_TABLE:
+ * int32: number of type names below
+ * string... : type names. Each of the type names is assigned id according to
+ * their position in this table starting from 0.
+ *
+ * TASK_RECORD:
+ * int32 size: size of the encoded task record
+ * byte[size] encoded_task_record:
+ * varint64: thread id - as was returned by Thread.getId()
+ * varint32: task id - starting from 1.
+ * varint32: parent task id for subtasks or 0 for root tasks
+ * varint64: start time in ns, relative to the Profiler.start() invocation
+ * varint64: task duration in ns
+ * byte: task type id (see TASK_TYPE_TABLE)
+ * varint32: description string index incremented by 1 (>0) or 0 this is
+ * a first occurrence of the description string
+ * AGGREGATED_STAT...: remainder of the field (if present) represents
+ * aggregated stats for that task
+ * string: *optional* description string, will appear only if description
+ * string index above was 0. In that case this string will be
+ * assigned next sequential id so every unique description string
+ * will appear in the file only once - after that it will be
+ * referenced by id.
+ *
+ * AGGREGATE_STAT:
+ * byte: stat type
+ * varint32: total number of subtask invocations
+ * varint64: cumulative duration of subtask invocations in ns.
+ *
+ * EOF_MARKER:
+ * int64: -1 - please note that this corresponds to the thread id in the
+ * TASK_RECORD which is always > 0
+ * </pre>
+ *
+ * @see ProfilerTask enum for recognized task types.
+ */
+//@ThreadSafe - commented out to avoid cyclic dependency with lib.util package
+public final class Profiler {
+ static final int MAGIC = 0x11223344;
+
+ // File version number. Note that merely adding new record types in
+ // the ProfilerTask does not require bumping version number as long as original
+ // enum values are not renamed or deleted.
+ static final int VERSION = 0x03;
+
+ // EOF marker. Must be < 0.
+ static final int EOF_MARKER = -1;
+
+ // Profiler will check for gathered data and persist all of it in the
+ // separate thread every SAVE_DELAY ms.
+ private static final int SAVE_DELAY = 2000; // ms
+
+ /**
+ * The profiler (a static singleton instance). Inactive by default.
+ */
+ private static final Profiler instance = new Profiler();
+
+ /**
+ * A task that was very slow.
+ */
+ public final class SlowTask implements Comparable<SlowTask> {
+ final long durationNanos;
+ final Object object;
+ ProfilerTask type;
+
+ private SlowTask(TaskData taskData) {
+ this.durationNanos = taskData.duration;
+ this.object = taskData.object;
+ this.type = taskData.type;
+ }
+
+ @Override
+ public int compareTo(SlowTask other) {
+ long delta = durationNanos - other.durationNanos;
+ if (delta < 0) { // Very clumsy
+ return -1;
+ } else if (delta > 0) {
+ return 1;
+ } else {
+ return 0;
+ }
+ }
+
+ public long getDurationNanos() {
+ return durationNanos;
+ }
+
+ public String getDescription() {
+ return toDescription(object);
+ }
+
+ public ProfilerTask getType() {
+ return type;
+ }
+ }
+
+ /**
+ * Container for the single task record.
+ * Should never be instantiated directly - use TaskStack.create() instead.
+ *
+ * Class itself is not thread safe, but all access to it from Profiler
+ * methods is.
+ */
+ //@ThreadCompatible - commented out to avoid cyclic dependency with lib.util.
+ private final class TaskData {
+ final long threadId;
+ final long startTime;
+ long duration = 0L;
+ final int id;
+ final int parentId;
+ int[] counts; // number of invocations per ProfilerTask type
+ long[] durations; // time spend in the task per ProfilerTask type
+ final ProfilerTask type;
+ final Object object;
+
+ TaskData(long startTime, TaskData parent,
+ ProfilerTask eventType, Object object) {
+ threadId = Thread.currentThread().getId();
+ counts = null;
+ durations = null;
+ id = taskId.incrementAndGet();
+ parentId = (parent == null ? 0 : parent.id);
+ this.startTime = startTime;
+ this.type = eventType;
+ this.object = Preconditions.checkNotNull(object);
+ }
+
+ /**
+ * Aggregates information about an *immediate* subtask.
+ */
+ public void aggregateChild(ProfilerTask type, long duration) {
+ int index = type.ordinal();
+ if (counts == null) {
+ // one entry for each ProfilerTask type
+ counts = new int[TASK_COUNT];
+ durations = new long[TASK_COUNT];
+ }
+ counts[index]++;
+ durations[index] += duration;
+ }
+
+ @Override
+ public String toString() {
+ return "Thread " + threadId + ", task " + id + ", type " + type + ", " + object;
+ }
+ }
+
+ /**
+ * Tracks nested tasks for each thread.
+ *
+ * java.util.ArrayDeque is the most efficient stack implementation in the
+ * Java Collections Framework (java.util.Stack class is older synchronized
+ * alternative). It is, however, used here strictly for LIFO operations.
+ * However, ArrayDeque is 1.6 only. For 1.5 best approach would be to utilize
+ * ArrayList and emulate stack using it.
+ */
+ //@ThreadSafe - commented out to avoid cyclic dependency with lib.util.
+ private final class TaskStack extends ThreadLocal<List<TaskData>> {
+
+ @Override
+ public List<TaskData> initialValue() {
+ return new ArrayList<>();
+ }
+
+ public TaskData peek() {
+ List<TaskData> list = get();
+ if (list.isEmpty()) {
+ return null;
+ }
+ return list.get(list.size() - 1);
+ }
+
+ public TaskData pop() {
+ List<TaskData> list = get();
+ return list.remove(list.size() - 1);
+ }
+
+ public boolean isEmpty() {
+ return get().isEmpty();
+ }
+
+ public void push(ProfilerTask eventType, Object object) {
+ get().add(create(clock.nanoTime(), eventType, object));
+ }
+
+ public TaskData create(long startTime, ProfilerTask eventType, Object object) {
+ return new TaskData(startTime, peek(), eventType, object);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder(
+ "Current task stack for thread " + Thread.currentThread().getName() + ":\n");
+ List<TaskData> list = get();
+ for (int i = list.size() - 1; i >= 0; i--) {
+ builder.append(list.get(i).toString());
+ builder.append("\n");
+ }
+ return builder.toString();
+ }
+ }
+
+ private static String toDescription(Object object) {
+ return (object instanceof Describable)
+ ? ((Describable) object).describe()
+ : object.toString();
+ }
+
+ /**
+ * Implements datastore for object description indices. Intended to be used
+ * only by the Profiler.save() method.
+ */
+ //@ThreadCompatible - commented out to avoid cyclic dependency with lib.util.
+ private final class ObjectDescriber {
+ private Map<Object, Integer> descMap = new IdentityHashMap<>(2000);
+ private int indexCounter = 0;
+
+ ObjectDescriber() { }
+
+ int getDescriptionIndex(Object object) {
+ Integer index = descMap.get(object);
+ return (index != null) ? index : -1;
+ }
+
+ String getDescription(Object object) {
+ String description = toDescription(object);
+
+ Integer oldIndex = descMap.put(object, indexCounter++);
+ // Do not use Preconditions class below due to the rather expensive
+ // toString() calls used in the message.
+ if (oldIndex != null) {
+ throw new IllegalStateException(" Object '" + description + "' @ "
+ + System.identityHashCode(object) + " already had description index "
+ + oldIndex + " while assigning index " + descMap.get(object));
+ } else if (description.length() > 20000) {
+ // Note size 64k byte limitation in DataOutputStream#writeUTF().
+ description = description.substring(0, 20000);
+ }
+ return description;
+ }
+
+ boolean isUnassigned(int index) {
+ return (index < 0);
+ }
+ }
+
+ /**
+ * Aggregator class that keeps track of the slowest tasks of the specified type.
+ *
+ * <p><code>priorityQueues</p> is sharded so that all threads need not compete for the same
+ * lock if they do the same operation at the same time. Access to the individual queues is
+ * synchronized on the queue objects themselves.
+ */
+ private final class SlowestTaskAggregator {
+ private static final int SHARDS = 16;
+ private final int size;
+
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ private final PriorityQueue<SlowTask>[] priorityQueues = new PriorityQueue[SHARDS];
+
+ SlowestTaskAggregator(int size) {
+ this.size = size;
+
+ for (int i = 0; i < SHARDS; i++) {
+ priorityQueues[i] = new PriorityQueue<SlowTask>(size + 1);
+ }
+ }
+
+ // @ThreadSafe
+ void add(TaskData taskData) {
+ PriorityQueue<SlowTask> queue =
+ priorityQueues[(int) (Thread.currentThread().getId() % SHARDS)];
+ synchronized (queue) {
+ if (queue.size() == size) {
+ // Optimization: check if we are faster than the fastest element. If we are, we would
+ // be the ones to fall off the end of the queue, therefore, we can safely return early.
+ if (queue.peek().getDurationNanos() > taskData.duration) {
+ return;
+ }
+
+ queue.add(new SlowTask(taskData));
+ queue.remove();
+ } else {
+ queue.add(new SlowTask(taskData));
+ }
+ }
+ }
+
+ // @ThreadSafe
+ void clear() {
+ for (int i = 0; i < SHARDS; i++) {
+ PriorityQueue<SlowTask> queue = priorityQueues[i];
+ synchronized (queue) {
+ queue.clear();
+ }
+ }
+ }
+
+ // @ThreadSafe
+ Iterable<SlowTask> getSlowestTasks() {
+ // This is slow, but since it only happens during the end of the invocation, it's OK
+ PriorityQueue<SlowTask> merged = new PriorityQueue<>(size * SHARDS);
+ for (int i = 0; i < SHARDS; i++) {
+ PriorityQueue<SlowTask> queue = priorityQueues[i];
+ synchronized (queue) {
+ merged.addAll(queue);
+ }
+ }
+
+ while (merged.size() > size) {
+ merged.remove();
+ }
+
+ return merged;
+ }
+ }
+
+ /**
+ * Which {@link ProfilerTask}s are profiled.
+ */
+ public enum ProfiledTaskKinds {
+ /**
+ * Do not profile anything.
+ *
+ * <p>Performance is best with this case, but we lose critical path analysis and slowest
+ * operation tracking.
+ */
+ NONE {
+ @Override
+ boolean isProfiling(ProfilerTask type) {
+ return false;
+ }
+ },
+
+ /**
+ * Profile on a few, known-to-be-slow tasks.
+ *
+ * <p>Performance is somewhat decreased in comparison to {@link #NONE}, but we still track the
+ * slowest operations (VFS).
+ */
+ SLOWEST {
+ @Override
+ boolean isProfiling(ProfilerTask type) {
+ return type.collectsSlowestInstances();
+ }
+ },
+
+ /**
+ * Profile all tasks.
+ *
+ * <p>This is in use when {@code --profile} is specified.
+ */
+ ALL {
+ @Override
+ boolean isProfiling(ProfilerTask type) {
+ return true;
+ }
+ };
+
+ /** Whether the Profiler collects data for the given task type. */
+ abstract boolean isProfiling(ProfilerTask type);
+ }
+
+ private Clock clock;
+ private ProfiledTaskKinds profiledTaskKinds;
+ private volatile long profileStartTime = 0L;
+ private volatile boolean recordAllDurations = false;
+ private AtomicInteger taskId = new AtomicInteger();
+
+ private TaskStack taskStack;
+ private Queue<TaskData> taskQueue;
+ private DataOutputStream out;
+ private Timer timer;
+ private IOException saveException;
+ private ObjectDescriber describer;
+ @SuppressWarnings("unchecked")
+ private final SlowestTaskAggregator[] slowestTasks =
+ new SlowestTaskAggregator[ProfilerTask.values().length];
+
+ private Profiler() {
+ for (ProfilerTask task : ProfilerTask.values()) {
+ if (task.slowestInstancesCount != 0) {
+ slowestTasks[task.ordinal()] = new SlowestTaskAggregator(task.slowestInstancesCount);
+ }
+ }
+ }
+
+ public static Profiler instance() {
+ return instance;
+ }
+
+ /**
+ * Returns the nanoTime of the current profiler instance, or an arbitrary
+ * constant if not active.
+ */
+ public static long nanoTimeMaybe() {
+ if (instance.isActive()) {
+ return instance.clock.nanoTime();
+ }
+ return -1;
+ }
+
+ /**
+ * Enable profiling.
+ *
+ * <p>Subsequent calls to beginTask/endTask will be recorded
+ * in the provided output stream. Please note that stream performance is
+ * extremely important and buffered streams should be utilized.
+ *
+ * @param profiledTaskKinds which kinds of {@link ProfilerTask}s to track
+ * @param stream output stream to store profile data. Note: passing unbuffered stream object
+ * reference may result in significant performance penalties
+ * @param comment a comment to insert in the profile data
+ * @param recordAllDurations iff true, record all tasks regardless of their duration; otherwise
+ * some tasks may get aggregated if they finished quick enough
+ * @param clock a {@code BlazeClock.instance()}
+ * @param execStartTimeNanos execution start time in nanos obtained from {@code clock.nanoTime()}
+ */
+ public synchronized void start(ProfiledTaskKinds profiledTaskKinds, OutputStream stream,
+ String comment, boolean recordAllDurations, Clock clock, long execStartTimeNanos)
+ throws IOException {
+ Preconditions.checkState(!isActive(), "Profiler already active");
+ taskStack = new TaskStack();
+ taskQueue = new ConcurrentLinkedQueue<>();
+ describer = new ObjectDescriber();
+
+ this.profiledTaskKinds = profiledTaskKinds;
+ this.clock = clock;
+
+ // sanity check for current limitation on the number of supported types due
+ // to using enum.ordinal() to store them instead of EnumSet for performance reasons.
+ Preconditions.checkState(TASK_COUNT < 256,
+ "The profiler implementation supports only up to 255 different ProfilerTask values.");
+
+ // reset state for the new profiling session
+ taskId.set(0);
+ this.recordAllDurations = recordAllDurations;
+ this.saveException = null;
+ if (stream != null) {
+ this.timer = new Timer("ProfilerTimer", true);
+ // Wrapping deflater stream in the buffered stream proved to reduce CPU consumption caused by
+ // the save() method. Values for buffer sizes were chosen by running small amount of tests
+ // and identifying point of diminishing returns - but I have not really tried to optimize
+ // them.
+ this.out = new DataOutputStream(new BufferedOutputStream(new DeflaterOutputStream(
+ stream, new Deflater(Deflater.BEST_SPEED, false), 65536), 262144));
+
+ this.out.writeInt(MAGIC); // magic
+ this.out.writeInt(VERSION); // protocol_version
+ this.out.writeUTF(comment);
+ // ProfileTask.values() method sorts enums using their ordinal() value, so
+ // there there is no need to store ordinal() value for each entry.
+ this.out.writeInt(TASK_COUNT);
+ for (ProfilerTask type : ProfilerTask.values()) {
+ this.out.writeUTF(type.toString());
+ }
+
+ // Start save thread
+ timer.schedule(new TimerTask() {
+ @Override public void run() { save(); }
+ }, SAVE_DELAY, SAVE_DELAY);
+ } else {
+ this.out = null;
+ }
+
+ // activate profiler
+ profileStartTime = execStartTimeNanos;
+ }
+
+ public synchronized Iterable<SlowTask> getSlowestTasks() {
+ List<Iterable<SlowTask>> slowestTasksByType = new ArrayList<>();
+
+ for (SlowestTaskAggregator aggregator : slowestTasks) {
+ if (aggregator != null) {
+ slowestTasksByType.add(aggregator.getSlowestTasks());
+ }
+ }
+
+ return Iterables.concat(slowestTasksByType);
+ }
+
+ /**
+ * Disable profiling and complete profile file creation.
+ * Subsequent calls to beginTask/endTask will no longer
+ * be recorded in the profile.
+ */
+ public synchronized void stop() throws IOException {
+ if (saveException != null) {
+ throw saveException;
+ }
+ if (!isActive()) {
+ return;
+ }
+ // Log a final event to update the duration of ProfilePhase.FINISH.
+ logEvent(ProfilerTask.INFO, "Finishing");
+ save();
+ clear();
+
+ for (SlowestTaskAggregator aggregator : slowestTasks) {
+ if (aggregator != null) {
+ aggregator.clear();
+ }
+ }
+
+ if (saveException != null) {
+ throw saveException;
+ }
+ if (out != null) {
+ out.writeInt(EOF_MARKER);
+ out.close();
+ out = null;
+ }
+ }
+
+ /**
+ * Returns true iff profiling is currently enabled.
+ */
+ public boolean isActive() {
+ return profileStartTime != 0L;
+ }
+
+ public boolean isProfiling(ProfilerTask type) {
+ return profiledTaskKinds.isProfiling(type);
+ }
+
+ /**
+ * Saves all gathered information from taskQueue queue to the file.
+ * Method is invoked internally by the Timer-based thread and at the end of
+ * profiling session.
+ */
+ private synchronized void save() {
+ if (out == null) {
+ return;
+ }
+ try {
+ // Allocate the sink once to avoid GC
+ ByteBuffer sink = ByteBuffer.allocate(1024);
+ while (!taskQueue.isEmpty()) {
+ sink.clear();
+ TaskData data = taskQueue.poll();
+
+ VarInt.putVarLong(data.threadId, sink);
+ VarInt.putVarInt(data.id, sink);
+ VarInt.putVarInt(data.parentId, sink);
+ VarInt.putVarLong(data.startTime - profileStartTime, sink);
+ VarInt.putVarLong(data.duration, sink);
+
+ // To save space (and improve performance), convert all description
+ // strings to the canonical object and use IdentityHashMap to assign
+ // unique numbers for each string.
+ int descIndex = describer.getDescriptionIndex(data.object);
+ VarInt.putVarInt(descIndex + 1, sink); // Add 1 to avoid encoding negative values.
+
+ // Save types using their ordinal() value
+ sink.put((byte) data.type.ordinal());
+
+ // Save aggregated data stats.
+ if (data.counts != null) {
+ for (int i = 0; i < TASK_COUNT; i++) {
+ if (data.counts[i] > 0) {
+ sink.put((byte) i); // aggregated type ordinal value
+ VarInt.putVarInt(data.counts[i], sink);
+ VarInt.putVarLong(data.durations[i], sink);
+ }
+ }
+ }
+
+ this.out.writeInt(sink.position());
+ this.out.write(sink.array(), 0, sink.position());
+ if (describer.isUnassigned(descIndex)) {
+ this.out.writeUTF(describer.getDescription(data.object));
+ }
+ }
+ this.out.flush();
+ } catch (IOException e) {
+ saveException = e;
+ clear();
+ try {
+ out.close();
+ } catch (IOException e2) {
+ // ignore it
+ }
+ }
+ }
+
+ private synchronized void clear() {
+ profileStartTime = 0L;
+ if (timer != null) {
+ timer.cancel();
+ timer = null;
+ }
+ taskStack = null;
+ taskQueue = null;
+ describer = null;
+
+ // Note that slowest task aggregator are not cleared here because clearing happens
+ // periodically over the course of a command invocation.
+ }
+
+ /**
+ * Unless --record_full_profiler_data is given we drop small tasks and add their time to the
+ * parents duration.
+ */
+ private boolean wasTaskSlowEnoughToRecord(ProfilerTask type, long duration) {
+ return (recordAllDurations || duration >= type.minDuration);
+ }
+
+ /**
+ * Adds task directly to the main queue bypassing task stack. Used for simple
+ * tasks that are known to not have any subtasks.
+ *
+ * @param startTime task start time (obtained through {@link Profiler#nanoTimeMaybe()})
+ * @param duration task duration
+ * @param type task type
+ * @param object object associated with that task. Can be String object that
+ * describes it.
+ */
+ private void logTask(long startTime, long duration, ProfilerTask type, Object object) {
+ Preconditions.checkNotNull(object);
+ Preconditions.checkState(startTime > 0, "startTime was " + startTime);
+ if (duration < 0) {
+ // See note in Clock#nanoTime, which is used by Profiler#nanoTimeMaybe.
+ duration = 0;
+ }
+
+ TaskData parent = taskStack.peek();
+ if (parent != null) {
+ parent.aggregateChild(type, duration);
+ }
+ if (wasTaskSlowEnoughToRecord(type, duration)) {
+ TaskData data = taskStack.create(startTime, type, object);
+ data.duration = duration;
+ if (out != null) {
+ taskQueue.add(data);
+ }
+
+ SlowestTaskAggregator aggregator = slowestTasks[type.ordinal()];
+
+ if (aggregator != null) {
+ aggregator.add(data);
+ }
+ }
+ }
+
+ /**
+ * Used externally to submit simple task (one that does not have any subtasks).
+ * Depending on the minDuration attribute of the task type, task may be
+ * just aggregated into the parent task and not stored directly.
+ *
+ * @param startTime task start time (obtained through {@link
+ * Profiler#nanoTimeMaybe()})
+ * @param type task type
+ * @param object object associated with that task. Can be String object that
+ * describes it.
+ */
+ public void logSimpleTask(long startTime, ProfilerTask type, Object object) {
+ if (isActive() && isProfiling(type)) {
+ logTask(startTime, clock.nanoTime() - startTime, type, object);
+ }
+ }
+
+ /**
+ * Used externally to submit simple task (one that does not have any
+ * subtasks). Depending on the minDuration attribute of the task type, task
+ * may be just aggregated into the parent task and not stored directly.
+ *
+ * <p>Note that start and stop time must both be acquired from the same clock
+ * instance.
+ *
+ * @param startTime task start time
+ * @param stopTime task stop time
+ * @param type task type
+ * @param object object associated with that task. Can be String object that
+ * describes it.
+ */
+ public void logSimpleTask(long startTime, long stopTime, ProfilerTask type, Object object) {
+ if (isActive() && isProfiling(type)) {
+ logTask(startTime, stopTime - startTime, type, object);
+ }
+ }
+
+ /**
+ * Used externally to submit simple task (one that does not have any
+ * subtasks). Depending on the minDuration attribute of the task type, task
+ * may be just aggregated into the parent task and not stored directly.
+ *
+ * @param startTime task start time (obtained through {@link
+ * Profiler#nanoTimeMaybe()})
+ * @param duration the duration of the task
+ * @param type task type
+ * @param object object associated with that task. Can be String object that
+ * describes it.
+ */
+ public void logSimpleTaskDuration(long startTime, long duration, ProfilerTask type,
+ Object object) {
+ if (isActive() && isProfiling(type)) {
+ logTask(startTime, duration, type, object);
+ }
+ }
+
+ /**
+ * Used to log "events" - tasks with zero duration.
+ */
+ public void logEvent(ProfilerTask type, Object object) {
+ if (isActive() && isProfiling(type)) {
+ logTask(clock.nanoTime(), 0, type, object);
+ }
+ }
+
+ /**
+ * Records the beginning of the task specified by the parameters. This method
+ * should always be followed by completeTask() invocation to mark the end of
+ * task execution (usually ensured by try {} finally {} block). Failure to do
+ * so will result in task stack corruption.
+ *
+ * Use of this method allows to support nested task monitoring. For tasks that
+ * are known to not have any subtasks, logSimpleTask() should be used instead.
+ *
+ * @param type predefined task type - see ProfilerTask for available types.
+ * @param object object associated with that task. Can be String object that
+ * describes it.
+ */
+ public void startTask(ProfilerTask type, Object object) {
+ // ProfilerInfo.allTasksById is supposed to be an id -> Task map, but it is in fact a List,
+ // which means that we cannot drop tasks to which we had already assigned ids. Therefore,
+ // non-leaf tasks must not have a minimum duration. However, we don't quite consistently
+ // enforce this, and Blaze only works because we happen not to add child tasks to those parent
+ // tasks that have a minimum duration.
+ Preconditions.checkNotNull(object);
+ if (isActive() && isProfiling(type)) {
+ taskStack.push(type, object);
+ }
+ }
+
+ /**
+ * Records the end of the task and moves tasks from the thread-local stack to
+ * the main queue. Will validate that given task type matches task at the top
+ * of the stack.
+ *
+ * @param type task type.
+ */
+ public void completeTask(ProfilerTask type) {
+ if (isActive() && isProfiling(type)) {
+ long endTime = clock.nanoTime();
+ TaskData data = taskStack.pop();
+ // Do not use Preconditions class below due to the very expensive
+ // toString() calls used in the message.
+ if (data.type != type) {
+ throw new IllegalStateException("Inconsistent Profiler.completeTask() call for the "
+ + type + " task.\n " + taskStack);
+ }
+ data.duration = endTime - data.startTime;
+ if (data.parentId > 0) {
+ taskStack.peek().aggregateChild(data.type, data.duration);
+ }
+ boolean shouldRecordTask = wasTaskSlowEnoughToRecord(type, data.duration);
+ if (out != null && (shouldRecordTask || data.counts != null)) {
+ taskQueue.add(data);
+ }
+
+ if (shouldRecordTask) {
+ SlowestTaskAggregator aggregator = slowestTasks[type.ordinal()];
+
+ if (aggregator != null) {
+ aggregator.add(data);
+ }
+ }
+ }
+ }
+
+ /**
+ * Convenience method to log phase marker tasks.
+ */
+ public void markPhase(ProfilePhase phase) {
+ MemoryProfiler.instance().markPhase(phase);
+ if (isActive() && isProfiling(ProfilerTask.PHASE)) {
+ Preconditions.checkState(taskStack.isEmpty(), "Phase tasks must not be nested");
+ logEvent(ProfilerTask.PHASE, phase.description);
+ }
+ }
+
+ /**
+ * Convenience method to log spawn tasks.
+ *
+ * TODO(bazel-team): Right now method expects single string of the spawn action
+ * as task description (usually either argv[0] or a name of the main executable
+ * in case of complex shell commands). Maybe it should accept Command object
+ * and create more user friendly description.
+ */
+ public void logSpawn(long startTime, String arg0) {
+ if (isActive() && isProfiling(ProfilerTask.SPAWN)) {
+ logTask(startTime, clock.nanoTime() - startTime, ProfilerTask.SPAWN, arg0);
+ }
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/profiler/ProfilerTask.java b/src/main/java/com/google/devtools/build/lib/profiler/ProfilerTask.java
new file mode 100644
index 0000000..d06626c
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/profiler/ProfilerTask.java
@@ -0,0 +1,101 @@
+// Copyright 2014 Google Inc. 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.profiler;
+
+/**
+ * All possible types of profiler tasks. Each type also defines description and
+ * minimum duration in nanoseconds for it to be recorded as separate event and
+ * not just be aggregated into the parent event.
+ */
+public enum ProfilerTask {
+ /* WARNING:
+ * Add new Tasks at the end (before Unknown) to not break the profiles that people have created!
+ * The profile file format uses the ordinal() of this enumeration to identify the task.
+ */
+ PHASE("build phase marker", -1, 0x336699, 0),
+ ACTION("action processing", -1, 0x666699, 0),
+ ACTION_BUILDER("parallel builder completion queue", -1, 0xCC3399, 0),
+ ACTION_SUBMIT("execution queue submission", -1, 0xCC3399, 0),
+ ACTION_CHECK("action dependency checking", 10000000, 0x999933, 0),
+ ACTION_EXECUTE("action execution", -1, 0x99CCFF, 0),
+ ACTION_LOCK("action resource lock", 10000000, 0xCC9933, 0),
+ ACTION_RELEASE("action resource release", 10000000, 0x006666, 0),
+ ACTION_GRAPH("action graph dependency", -1, 0x3399FF, 0),
+ ACTION_UPDATE("update action information", 10000000, 0x993300, 0),
+ ACTION_COMPLETE("complete action execution", -1, 0xCCCC99, 0),
+ INFO("general information", -1, 0x000066, 0),
+ EXCEPTION("exception", -1, 0xFFCC66, 0),
+ CREATE_PACKAGE("package creation", -1, 0x6699CC, 0),
+ PACKAGE_VALIDITY_CHECK("package validity check", -1, 0x336699, 0),
+ SPAWN("local process spawn", -1, 0x663366, 0),
+ REMOTE_EXECUTION("remote action execution", -1, 0x9999CC, 0),
+ LOCAL_EXECUTION("local action execution", -1, 0xCCCCCC, 0),
+ SCANNER("include scanner", -1, 0x669999, 0),
+ // 30 is a good number because the slowest items are stored in a heap, with temporarily
+ // one more element, and with 31 items, a heap becomes a complete binary tree
+ LOCAL_PARSE("Local parse to prepare for remote execution", 50000000, 0x6699CC, 30),
+ UPLOAD_TIME("Remote execution upload time", 50000000, 0x6699CC, 0),
+ PROCESS_TIME("Remote execution process wall time", 50000000, 0xF999CC, 0),
+ REMOTE_QUEUE("Remote execution queuing time", 50000000, 0xCC6600, 0),
+ REMOTE_SETUP("Remote execution setup", 50000000, 0xA999CC, 0),
+ FETCH("Remote execution file fetching", 50000000, 0xBB99CC, 0),
+ VFS_STAT("VFS stat", 10000000, 0x9999FF, 30),
+ VFS_DIR("VFS readdir", 10000000, 0x0066CC, 30),
+ VFS_LINK("VFS readlink", 10000000, 0x99CCCC, 30),
+ VFS_MD5("VFS md5", 10000000, 0x999999, 30),
+ VFS_XATTR("VFS xattr", 10000000, 0x9999DD, 30),
+ VFS_DELETE("VFS delete", 10000000, 0xFFCC00, 0),
+ VFS_OPEN("VFS open", 10000000, 0x009999, 30),
+ VFS_READ("VFS read", 10000000, 0x99CC33, 30),
+ VFS_WRITE("VFS write", 10000000, 0xFF9900, 30),
+ VFS_GLOB("globbing", -1, 0x999966, 30),
+ VFS_VMFS_STAT("VMFS stat", 10000000, 0x9999FF, 0),
+ VFS_VMFS_DIR("VMFS readdir", 10000000, 0x0066CC, 0),
+ VFS_VMFS_READ("VMFS read", 10000000, 0x99CC33, 0),
+ WAIT("thread wait", 5000000, 0x66CCCC, 0),
+ CONFIGURED_TARGET("configured target creation", -1, 0x663300, 0),
+ TRANSITIVE_CLOSURE("transitive closure creation", -1, 0x996600, 0),
+ TEST("for testing only", -1, 0x000000, 0),
+ SKYFRAME_EVAL("skyframe evaluator", -1, 0xCC9900, 0),
+ SKYFUNCTION("skyfunction", -1, 0xCC6600, 0),
+ CRITICAL_PATH("critical path", -1, 0x666699, 0),
+ CRITICAL_PATH_COMPONENT("critical path component", -1, 0x666699, 0),
+ IDE_BUILD_INFO("ide_build_info", -1, 0xCC6633, 0),
+ UNKNOWN("Unknown event", -1, 0x339966, 0);
+
+ // Size of the ProfilerTask value space.
+ public static final int TASK_COUNT = ProfilerTask.values().length;
+
+ /** Human readable description for the task. */
+ public final String description;
+ /** Threshold for skipping tasks in the profile in nanoseconds, unless --record_full_profiler_data
+ * is used */
+ public final long minDuration;
+ /** Default color of the task, when rendered in a chart. */
+ public final int color;
+ /** How many of the slowest instances to keep. If 0, no slowest instance calculation is done. */
+ public final int slowestInstancesCount;
+
+ ProfilerTask(String description, long minDuration, int color, int slowestInstanceCount) {
+ this.description = description;
+ this.minDuration = minDuration;
+ this.color = color;
+ this.slowestInstancesCount = slowestInstanceCount;
+ }
+
+ /** Whether the Profiler collects the slowest instances of this task. */
+ public boolean collectsSlowestInstances() {
+ return slowestInstancesCount > 0;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/profiler/chart/AggregatingChartCreator.java b/src/main/java/com/google/devtools/build/lib/profiler/chart/AggregatingChartCreator.java
new file mode 100644
index 0000000..469e605
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/profiler/chart/AggregatingChartCreator.java
@@ -0,0 +1,161 @@
+// Copyright 2014 Google Inc. 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.profiler.chart;
+
+import com.google.devtools.build.lib.profiler.ProfileInfo;
+import com.google.devtools.build.lib.profiler.ProfileInfo.Task;
+import com.google.devtools.build.lib.profiler.ProfilePhaseStatistics;
+import com.google.devtools.build.lib.profiler.ProfilerTask;
+
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Implementation of {@link ChartCreator} that creates Gantt Charts that try to
+ * minimize the number of bars while preserving as much information about the
+ * execution of actions as possible.
+ *
+ * <p>Profiler tasks are categorized into four categories:
+ * <ul>
+ * <li>Actions: Actions executed.
+ * <li>Blaze Internal: This category contains internal blaze tasks, like loading
+ * packages, saving the action cache etc.
+ * <li>Locks: Contains tasks that indicate that a thread is waiting for
+ * resources.
+ * <li>VFS: Contains tasks that access the file system.
+ * </ul>
+ */
+public class AggregatingChartCreator implements ChartCreator {
+
+ /** The tasks in the 'actions' category. */
+ private static final Set<ProfilerTask> ACTION_TASKS = EnumSet.of(ProfilerTask.ACTION,
+ ProfilerTask.ACTION_SUBMIT);
+
+ /** The tasks in the 'blaze internal' category. */
+ private static Set<ProfilerTask> BLAZE_TASKS =
+ EnumSet.of(ProfilerTask.CREATE_PACKAGE, ProfilerTask.PACKAGE_VALIDITY_CHECK,
+ ProfilerTask.CONFIGURED_TARGET, ProfilerTask.TRANSITIVE_CLOSURE,
+ ProfilerTask.EXCEPTION, ProfilerTask.INFO, ProfilerTask.UNKNOWN);
+
+ /** The tasks in the 'locks' category. */
+ private static Set<ProfilerTask> LOCK_TASKS =
+ EnumSet.of(ProfilerTask.ACTION_LOCK, ProfilerTask.WAIT);
+
+ /** The tasks in the 'VFS' category. */
+ private static Set<ProfilerTask> VFS_TASKS =
+ EnumSet.of(ProfilerTask.VFS_STAT, ProfilerTask.VFS_DIR, ProfilerTask.VFS_LINK,
+ ProfilerTask.VFS_MD5, ProfilerTask.VFS_DELETE, ProfilerTask.VFS_OPEN,
+ ProfilerTask.VFS_READ, ProfilerTask.VFS_WRITE, ProfilerTask.VFS_GLOB,
+ ProfilerTask.VFS_XATTR);
+
+ /** The data of the profiled build. */
+ private final ProfileInfo info;
+
+ /**
+ * Statistics of the profiled build. This is expected to be a formatted
+ * string, ready to be printed out.
+ */
+ private final List<ProfilePhaseStatistics> statistics;
+
+ /** If true, VFS related information is added to the chart. */
+ private final boolean showVFS;
+
+ /** The type for bars of category 'blaze internal'. */
+ private ChartBarType blazeType;
+
+ /** The type for bars of category 'actions'. */
+ private ChartBarType actionType;
+
+ /** The type for bars of category 'locks'. */
+ private ChartBarType lockType;
+
+ /** The type for bars of category 'VFS'. */
+ private ChartBarType vfsType;
+
+ /**
+ * Creates the chart creator. The created {@link ChartCreator} does not add
+ * VFS related data to the generated chart.
+ *
+ * @param info the data of the profiled build
+ * @param statistics Statistics of the profiled build. This is expected to be
+ * a formatted string, ready to be printed out.
+ */
+ public AggregatingChartCreator(ProfileInfo info, List<ProfilePhaseStatistics> statistics) {
+ this(info, statistics, false);
+ }
+
+ /**
+ * Creates the chart creator.
+ *
+ * @param info the data of the profiled build
+ * @param statistics Statistics of the profiled build. This is expected to be
+ * a formatted string, ready to be printed out.
+ * @param showVFS if true, VFS related information is added to the chart
+ */
+ public AggregatingChartCreator(ProfileInfo info, List<ProfilePhaseStatistics> statistics,
+ boolean showVFS) {
+ this.info = info;
+ this.statistics = statistics;
+ this.showVFS = showVFS;
+ }
+
+ @Override
+ public Chart create() {
+ Chart chart = new Chart(info.comment, statistics);
+ CommonChartCreator.createCommonChartItems(chart, info);
+ createTypes(chart);
+
+ for (ProfileInfo.Task task : info.allTasksById) {
+ if (ACTION_TASKS.contains(task.type)) {
+ createBar(chart, task, actionType);
+ } else if (LOCK_TASKS.contains(task.type)) {
+ createBar(chart, task, lockType);
+ } else if (BLAZE_TASKS.contains(task.type)) {
+ createBar(chart, task, blazeType);
+ } else if (showVFS && VFS_TASKS.contains(task.type)) {
+ createBar(chart, task, vfsType);
+ }
+ }
+
+ return chart;
+ }
+
+ /**
+ * Creates a bar and adds it to the chart.
+ *
+ * @param chart the chart to add the types to
+ * @param task the profiler task from which the bar is created
+ * @param type the type of the bar
+ */
+ private void createBar(Chart chart, Task task, ChartBarType type) {
+ String label = task.type.description + ": " + task.getDescription();
+ chart.addBar(task.threadId, task.startTime, task.startTime + task.duration, type, label);
+ }
+
+ /**
+ * Creates the {@link ChartBarType}s and adds them to the chart.
+ *
+ * @param chart the chart to add the types to
+ */
+ private void createTypes(Chart chart) {
+ actionType = chart.createType("Action processing", new Color(0x000099));
+ blazeType = chart.createType("Blaze internal processing", new Color(0x999999));
+ lockType = chart.createType("Waiting for resources", new Color(0x990000));
+ if (showVFS) {
+ vfsType = chart.createType("File system access", new Color(0x009900));
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/profiler/chart/Chart.java b/src/main/java/com/google/devtools/build/lib/profiler/chart/Chart.java
new file mode 100644
index 0000000..93c7c81
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/profiler/chart/Chart.java
@@ -0,0 +1,233 @@
+// Copyright 2014 Google Inc. 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.profiler.chart;
+
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.profiler.ProfilePhaseStatistics;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Data of a Gantt Chart to visualize the data of a profiled build.
+ */
+public class Chart {
+
+ /** The type that is returned when an unknown type is looked up. */
+ public static final ChartBarType UNKNOWN_TYPE = new ChartBarType("Unknown type", Color.RED);
+
+ /** The title of the chart. */
+ private final String title;
+
+ /** Statistics of the profiled build. */
+ private final List<ProfilePhaseStatistics> statistics;
+
+ /** The rows of the chart. */
+ private final Map<Long, ChartRow> rows = new HashMap<>();
+
+ /** The columns on the chart. */
+ private final List<ChartColumn> columns = new ArrayList<>();
+
+ /** The lines on the chart. */
+ private final List<ChartLine> lines = new ArrayList<>();
+
+ /** The types of the bars in the chart. */
+ private final Map<String, ChartBarType> types = new HashMap<>();
+
+ /** The running index of the rows in the chart. */
+ private int rowIndex = 0;
+
+ /** The maximum stop value of any bar in the chart. */
+ private long maxStop;
+
+ /**
+ * Creates a chart.
+ *
+ * @param title the title of the chart
+ * @param statistics Statistics of the profiled build. This is expected to be
+ * a formatted string, ready to be printed out.
+ */
+ public Chart(String title, List<ProfilePhaseStatistics> statistics) {
+ Preconditions.checkNotNull(title);
+ Preconditions.checkNotNull(statistics);
+ this.title = title;
+ this.statistics = statistics;
+ }
+
+ /**
+ * Adds a bar to a row of the chart. If a row with the given id already
+ * exists, the bar is added to the row, otherwise a new row is created and the
+ * bar is added to it.
+ *
+ * @param id the id of the row the new bar belongs to
+ * @param start the start value of the bar
+ * @param stop the stop value of the bar
+ * @param type the type of the bar
+ * @param highlight emphasize the bar
+ * @param label the label of the bar
+ */
+ public void addBar(long id, long start, long stop, ChartBarType type, boolean highlight,
+ String label) {
+ ChartRow slot = addSlotIfAbsent(id);
+ ChartBar bar = new ChartBar(slot, start, stop, type, highlight, label);
+ slot.addBar(bar);
+ maxStop = Math.max(maxStop, stop);
+ }
+
+ /**
+ * Adds a bar to a row of the chart. If a row with the given id already
+ * exists, the bar is added to the row, otherwise a new row is created and the
+ * bar is added to it.
+ *
+ * @param id the id of the row the new bar belongs to
+ * @param start the start value of the bar
+ * @param stop the stop value of the bar
+ * @param type the type of the bar
+ * @param label the label of the bar
+ */
+ public void addBar(long id, long start, long stop, ChartBarType type, String label) {
+ addBar(id, start, stop, type, false, label);
+ }
+
+ /**
+ * Adds a vertical line to the chart.
+ */
+ public void addVerticalLine(long startId, long stopId, long pos) {
+ ChartRow startSlot = addSlotIfAbsent(startId);
+ ChartRow stopSlot = addSlotIfAbsent(stopId);
+ ChartLine line = new ChartLine(startSlot, stopSlot, pos, pos);
+ lines.add(line);
+ }
+
+ /**
+ * Adds a column to the chart.
+ *
+ * @param start the start value of the bar
+ * @param stop the stop value of the bar
+ * @param type the type of the bar
+ * @param label the label of the bar
+ */
+ public void addTimeRange(long start, long stop, ChartBarType type, String label) {
+ ChartColumn column = new ChartColumn(start, stop, type, label);
+ columns.add(column);
+ maxStop = Math.max(maxStop, stop);
+ }
+
+ /**
+ * Creates a new {@link ChartBarType} and adds it to the list of types of the
+ * chart.
+ *
+ * @param name the name of the type
+ * @param color the color of the chart
+ * @return the newly created type
+ */
+ public ChartBarType createType(String name, Color color) {
+ ChartBarType type = new ChartBarType(name, color);
+ types.put(name, type);
+ return type;
+ }
+
+ /**
+ * Returns the type with the given name. If no type with the given name
+ * exists, a type with name 'Unknown type' is added to the chart and returned.
+ *
+ * @param name the name of the type to look up
+ */
+ public ChartBarType lookUpType(String name) {
+ ChartBarType type = types.get(name);
+ if (type == null) {
+ type = UNKNOWN_TYPE;
+ types.put(type.getName(), type);
+ }
+ return type;
+ }
+
+ /**
+ * Creates a new row with the given id if no row with this id existed.
+ * Otherwise the existing row with the given id is returned.
+ *
+ * @param id the ID of the row
+ * @return the existing row, if it was already present, the newly created one
+ * otherwise
+ */
+ private ChartRow addSlotIfAbsent(long id) {
+ ChartRow slot = rows.get(id);
+ if (slot == null) {
+ slot = new ChartRow(Long.toString(id), rowIndex++);
+ rows.put(id, slot);
+ }
+ return slot;
+ }
+
+ /**
+ * Accepts a {@link ChartVisitor}. Calls {@link ChartVisitor#visit(Chart)},
+ * delegates the visitor to the rows of the chart and calls
+ * {@link ChartVisitor#endVisit(Chart)}.
+ *
+ * @param visitor the visitor to accept
+ */
+ public void accept(ChartVisitor visitor) {
+ visitor.visit(this);
+ for (ChartRow slot : rows.values()) {
+ slot.accept(visitor);
+ }
+ int rowCount = getRowCount();
+ for (ChartColumn column : columns) {
+ column.setRowCount(rowCount);
+ column.accept(visitor);
+ }
+ for (ChartLine line : lines) {
+ line.accept(visitor);
+ }
+ visitor.endVisit(this);
+ }
+
+ /**
+ * Returns the {@link ChartBarType}s, sorted by name.
+ */
+ public List<ChartBarType> getSortedTypes() {
+ List<ChartBarType> list = new ArrayList<>(types.values());
+ Collections.sort(list);
+ return list;
+ }
+
+ /**
+ * Returns the {@link ChartRow}s, sorted by their index.
+ */
+ public List<ChartRow> getSortedRows() {
+ List<ChartRow> list = new ArrayList<>(rows.values());
+ Collections.sort(list);
+ return list;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public List<ProfilePhaseStatistics> getStatistics() {
+ return statistics;
+ }
+
+ public int getRowCount() {
+ return rows.size();
+ }
+
+ public long getMaxStop() {
+ return maxStop;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/profiler/chart/ChartBar.java b/src/main/java/com/google/devtools/build/lib/profiler/chart/ChartBar.java
new file mode 100644
index 0000000..20d92f0
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/profiler/chart/ChartBar.java
@@ -0,0 +1,106 @@
+// Copyright 2014 Google Inc. 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.profiler.chart;
+
+import com.google.common.base.Preconditions;
+
+/**
+ * A bar in a row of a Gantt Chart.
+ */
+public class ChartBar {
+
+ /**
+ * The start value of the bar. This value has no unit. The interpretation of
+ * the value is up to the user of the class.
+ */
+ private final long start;
+
+ /**
+ * The stop value of the bar. This value has no unit. The interpretation of
+ * the value is up to the user of the class.
+ */
+ private final long stop;
+
+ /** The type of the bar. */
+ private final ChartBarType type;
+
+ /** Emphasize the bar */
+ private boolean highlight;
+
+ /** The label of the bar. */
+ private final String label;
+
+ /** The chart row this bar belongs to. */
+ private final ChartRow row;
+
+ /**
+ * Creates a chart bar.
+ *
+ * @param row the chart row this bar belongs to
+ * @param start the start value of the bar
+ * @param stop the stop value of the bar
+ * @param type the type of the bar
+ * @param label the label of the bar
+ */
+ public ChartBar(ChartRow row, long start, long stop, ChartBarType type, boolean highlight,
+ String label) {
+ Preconditions.checkNotNull(row);
+ Preconditions.checkNotNull(type);
+ Preconditions.checkNotNull(label);
+ this.row = row;
+ this.start = start;
+ this.stop = stop;
+ this.type = type;
+ this.highlight = highlight;
+ this.label = label;
+ }
+
+ /**
+ * Accepts a {@link ChartVisitor}. Calls {@link ChartVisitor#visit(ChartBar)}.
+ *
+ * @param visitor the visitor to accept
+ */
+ public void accept(ChartVisitor visitor) {
+ visitor.visit(this);
+ }
+
+ public long getStart() {
+ return start;
+ }
+
+ public long getStop() {
+ return stop;
+ }
+
+ public long getWidth() {
+ return stop - start;
+ }
+
+ public ChartBarType getType() {
+ return type;
+ }
+
+ public boolean getHighlight() {
+ return highlight;
+ }
+
+ public String getLabel() {
+ return label;
+ }
+
+ public ChartRow getRow() {
+ return row;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/profiler/chart/ChartBarType.java b/src/main/java/com/google/devtools/build/lib/profiler/chart/ChartBarType.java
new file mode 100644
index 0000000..e0f3885
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/profiler/chart/ChartBarType.java
@@ -0,0 +1,82 @@
+// Copyright 2014 Google Inc. 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.profiler.chart;
+
+import com.google.common.base.Preconditions;
+
+/**
+ * The type of a bar in a Gantt Chart. A type consists of a name and a color.
+ * Types are used to create the legend of a Gantt Chart.
+ */
+public class ChartBarType implements Comparable<ChartBarType> {
+
+ /** The name of the type. */
+ private final String name;
+
+ /** The color of the type. */
+ private final Color color;
+
+ /**
+ * Creates a {@link ChartBarType}.
+ *
+ * @param name the name of the type
+ * @param color the color of the type
+ */
+ public ChartBarType(String name, Color color) {
+ Preconditions.checkNotNull(name);
+ Preconditions.checkNotNull(color);
+ this.name = name;
+ this.color = color;
+ }
+
+ @Override
+ public int hashCode() {
+ return name.hashCode();
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * <p>Equality of two types is defined by the equality of their names.
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ return name.equals(((ChartBarType) obj).name);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * <p>Compares types by their names.
+ */
+ @Override
+ public int compareTo(ChartBarType o) {
+ return name.compareTo(o.name);
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public Color getColor() {
+ return color;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/profiler/chart/ChartColumn.java b/src/main/java/com/google/devtools/build/lib/profiler/chart/ChartColumn.java
new file mode 100644
index 0000000..25fffe8
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/profiler/chart/ChartColumn.java
@@ -0,0 +1,93 @@
+// Copyright 2014 Google Inc. 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.profiler.chart;
+
+import com.google.common.base.Preconditions;
+
+/**
+ * A chart column. The column can be used to highlight a time-range.
+ */
+public class ChartColumn {
+
+ /**
+ * The start value of the bar. This value has no unit. The interpretation of
+ * the value is up to the user of the class.
+ */
+ private final long start;
+
+ /**
+ * The stop value of the bar. This value has no unit. The interpretation of
+ * the value is up to the user of the class.
+ */
+ private final long stop;
+
+ /** The type of the bar. */
+ private final ChartBarType type;
+
+ /** The label of the bar. */
+ private final String label;
+
+ private int rowCount;
+
+ /**
+ * Creates a chart column.
+ *
+ * @param start the start value of the bar
+ * @param stop the stop value of the bar
+ * @param type the type of the bar
+ * @param label the label of the bar
+ */
+ public ChartColumn(long start, long stop, ChartBarType type, String label) {
+ Preconditions.checkNotNull(type);
+ Preconditions.checkNotNull(label);
+ this.start = start;
+ this.stop = stop;
+ this.type = type;
+ this.label = label;
+ }
+
+ /**
+ * Accepts a {@link ChartVisitor}. Calls {@link ChartVisitor#visit(ChartBar)}.
+ *
+ * @param visitor the visitor to accept
+ */
+ public void accept(ChartVisitor visitor) {
+ visitor.visit(this);
+ }
+
+ public long getStart() {
+ return start;
+ }
+
+ public long getWidth() {
+ return stop - start;
+ }
+
+ public ChartBarType getType() {
+ return type;
+ }
+
+ public String getLabel() {
+ return label;
+ }
+
+ public int getRowCount() {
+ return rowCount;
+ }
+
+ public void setRowCount(int rowCount) {
+ this.rowCount = rowCount;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/profiler/chart/ChartCreator.java b/src/main/java/com/google/devtools/build/lib/profiler/chart/ChartCreator.java
new file mode 100644
index 0000000..31781c8
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/profiler/chart/ChartCreator.java
@@ -0,0 +1,28 @@
+// Copyright 2014 Google Inc. 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.profiler.chart;
+
+import com.google.devtools.build.lib.profiler.chart.Chart;
+
+/**
+ * Interface for classes that are capable of creating {@link Chart}s.
+ */
+public interface ChartCreator {
+
+ /**
+ * Creates a {@link Chart}.
+ */
+ Chart create();
+}
diff --git a/src/main/java/com/google/devtools/build/lib/profiler/chart/ChartLine.java b/src/main/java/com/google/devtools/build/lib/profiler/chart/ChartLine.java
new file mode 100644
index 0000000..d9bd755
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/profiler/chart/ChartLine.java
@@ -0,0 +1,62 @@
+// Copyright 2014 Google Inc. 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.profiler.chart;
+
+import com.google.common.base.Preconditions;
+
+/**
+ * A chart line. Such lines can be used to connect boxes.
+ */
+public class ChartLine {
+ private final ChartRow startRow;
+ private final ChartRow stopRow;
+ private final long startTime;
+ /**
+ * Creates a chart line.
+ *
+ * @param startRow the start row
+ * @param stopRow the end row
+ * @param startTime the start time
+ * @param stopTime the end time
+ */
+ public ChartLine(ChartRow startRow, ChartRow stopRow, long startTime, long stopTime) {
+ Preconditions.checkNotNull(startRow);
+ Preconditions.checkNotNull(stopRow);
+ this.startRow = startRow;
+ this.stopRow = stopRow;
+ this.startTime = startTime;
+ }
+
+ /**
+ * Accepts a {@link ChartVisitor}. Calls {@link ChartVisitor#visit(ChartBar)}.
+ *
+ * @param visitor the visitor to accept
+ */
+ public void accept(ChartVisitor visitor) {
+ visitor.visit(this);
+ }
+
+ public ChartRow getStartRow() {
+ return startRow;
+ }
+
+ public ChartRow getStopRow() {
+ return stopRow;
+ }
+
+ public long getStartTime() {
+ return startTime;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/profiler/chart/ChartRow.java b/src/main/java/com/google/devtools/build/lib/profiler/chart/ChartRow.java
new file mode 100644
index 0000000..96597c9
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/profiler/chart/ChartRow.java
@@ -0,0 +1,96 @@
+// Copyright 2014 Google Inc. 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.profiler.chart;
+
+import com.google.common.base.Preconditions;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A row of a Gantt Chart. A chart row is identified by its id and has an index that
+ * determines its location in the chart.
+ */
+public class ChartRow implements Comparable<ChartRow> {
+
+ /** The unique id of this row. */
+ private final String id;
+
+ /** The index, i.e., the row number of the row in the chart. */
+ private final int index;
+
+ /** The list of bars in this row. */
+ private final List<ChartBar> bars = new ArrayList<>();
+
+ /**
+ * Creates a chart row.
+ *
+ * @param id the unique id of this row
+ * @param index the index, i.e., the row number, of the row in the chart
+ */
+ public ChartRow(String id, int index) {
+ Preconditions.checkNotNull(id);
+ this.id = id;
+ this.index = index;
+ }
+
+ /**
+ * Adds a bar to the chart row.
+ *
+ * @param bar the {@link ChartBar} to add
+ */
+ public void addBar(ChartBar bar) {
+ bars.add(bar);
+ }
+
+ /**
+ * Returns the bars of the row as an unmodifieable list.
+ */
+ public List<ChartBar> getBars() {
+ return Collections.unmodifiableList(bars);
+ }
+
+ /**
+ * Accepts a {@link ChartVisitor}. Calls {@link ChartVisitor#visit(ChartRow)}
+ * and delegates the visitor to the bars of the chart row.
+ *
+ * @param visitor the visitor to accept
+ */
+ public void accept(ChartVisitor visitor) {
+ visitor.visit(this);
+ for (ChartBar bar : bars) {
+ bar.accept(visitor);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * <p>Compares to rows by their index.
+ */
+ @Override
+ public int compareTo(ChartRow other) {
+ return index - other.index;
+ }
+
+ public int getIndex() {
+ return index;
+ }
+
+ public String getId() {
+ return id;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/profiler/chart/ChartVisitor.java b/src/main/java/com/google/devtools/build/lib/profiler/chart/ChartVisitor.java
new file mode 100644
index 0000000..b06b3ab
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/profiler/chart/ChartVisitor.java
@@ -0,0 +1,65 @@
+// Copyright 2014 Google Inc. 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.profiler.chart;
+
+/**
+ * Visitor for {@link Chart} objects.
+ */
+public interface ChartVisitor {
+
+ /**
+ * Visits a {@link Chart} object before its children, i.e., rows and bars, are
+ * visited.
+ *
+ * @param chart the {@link Chart} to visit
+ */
+ void visit(Chart chart);
+
+ /**
+ * Visits a {@link Chart} object after its children, i.e., rows and bars, are
+ * visited.
+ *
+ * @param chart the {@link Chart} to visit
+ */
+ void endVisit(Chart chart);
+
+ /**
+ * Visits a {@link ChartRow} object.
+ *
+ * @param chartRow the {@link ChartRow} to visit
+ */
+ void visit(ChartRow chartRow);
+
+ /**
+ * Visits a {@link ChartBar} object.
+ *
+ * @param chartBar the {@link ChartBar} to visit
+ */
+ void visit(ChartBar chartBar);
+
+ /**
+ * Visits a {@link ChartColumn} object.
+ *
+ * @param chartColumn the {@link ChartColumn} to visit
+ */
+ void visit(ChartColumn chartColumn);
+
+ /**
+ * Visits a {@link ChartLine} object.
+ *
+ * @param chartLine the {@link ChartLine} to visit
+ */
+ void visit(ChartLine chartLine);
+}
diff --git a/src/main/java/com/google/devtools/build/lib/profiler/chart/Color.java b/src/main/java/com/google/devtools/build/lib/profiler/chart/Color.java
new file mode 100644
index 0000000..d527093
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/profiler/chart/Color.java
@@ -0,0 +1,52 @@
+// Copyright 2014 Google Inc. 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.profiler.chart;
+
+/**
+ * Represents a color in ARGB format, 8 bits per channel.
+ */
+public final class Color {
+ public static final Color RED = new Color(0xff0000);
+ public static final Color GREEN = new Color(0x00ff00);
+ public static final Color GRAY = new Color(0x808080);
+ public static final Color BLACK = new Color(0x000000);
+
+ private final int argb;
+
+ public Color(int rgb) {
+ this.argb = rgb | 0xff000000;
+ }
+
+ public Color(int argb, boolean hasAlpha) {
+ this.argb = argb;
+ }
+
+ public int getRed() {
+ return (argb >> 16) & 0xFF;
+ }
+
+ public int getGreen() {
+ return (argb >> 8) & 0xFF;
+ }
+
+ public int getBlue() {
+ return argb & 0xFF;
+ }
+
+ public int getAlpha() {
+ return (argb >> 24) & 0xFF;
+ }
+}
+
diff --git a/src/main/java/com/google/devtools/build/lib/profiler/chart/CommonChartCreator.java b/src/main/java/com/google/devtools/build/lib/profiler/chart/CommonChartCreator.java
new file mode 100644
index 0000000..ed1da20
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/profiler/chart/CommonChartCreator.java
@@ -0,0 +1,52 @@
+// Copyright 2014 Google Inc. 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.profiler.chart;
+
+import com.google.devtools.build.lib.profiler.ProfileInfo;
+import com.google.devtools.build.lib.profiler.ProfilePhase;
+
+/**
+ * Provides some common functions for {@link ChartCreator}s.
+ */
+public final class CommonChartCreator {
+
+ static void createCommonChartItems(Chart chart, ProfileInfo info) {
+ createTypes(chart);
+
+ // add common info
+ for (ProfilePhase phase : ProfilePhase.values()) {
+ addColumn(chart,info,phase);
+ }
+ }
+
+ private static void addColumn(Chart chart, ProfileInfo info, ProfilePhase phase) {
+ ProfileInfo.Task task = info.getPhaseTask(phase);
+ if (task != null) {
+ String label = task.type.description + ": " + task.getDescription();
+ ChartBarType type = chart.lookUpType(task.getDescription());
+ long stop = task.startTime + info.getPhaseDuration(task);
+ chart.addTimeRange(task.startTime, stop, type, label);
+ }
+ }
+
+ /**
+ * Creates the {@link ChartBarType}s and adds them to the chart.
+ */
+ private static void createTypes(Chart chart) {
+ for (ProfilePhase phase : ProfilePhase.values()) {
+ chart.createType(phase.description, new Color(phase.color, true));
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/profiler/chart/DetailedChartCreator.java b/src/main/java/com/google/devtools/build/lib/profiler/chart/DetailedChartCreator.java
new file mode 100644
index 0000000..1e097c3
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/profiler/chart/DetailedChartCreator.java
@@ -0,0 +1,103 @@
+// Copyright 2014 Google Inc. 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.profiler.chart;
+
+import com.google.devtools.build.lib.profiler.ProfileInfo;
+import com.google.devtools.build.lib.profiler.ProfileInfo.CriticalPathEntry;
+import com.google.devtools.build.lib.profiler.ProfileInfo.Task;
+import com.google.devtools.build.lib.profiler.ProfilePhaseStatistics;
+import com.google.devtools.build.lib.profiler.ProfilerTask;
+
+import java.util.EnumSet;
+import java.util.List;
+
+/**
+ * Implementation of {@link ChartCreator} that creates Gantt Charts that contain
+ * bars for all tasks in the profile.
+ */
+public class DetailedChartCreator implements ChartCreator {
+
+ /** The data of the profiled build. */
+ private final ProfileInfo info;
+
+ /**
+ * Statistics of the profiled build. This is expected to be a formatted
+ * string, ready to be printed out.
+ */
+ private final List<ProfilePhaseStatistics> statistics;
+
+ /**
+ * Creates the chart creator.
+ *
+ * @param info the data of the profiled build
+ * @param statistics Statistics of the profiled build. This is expected to be
+ * a formatted string, ready to be printed out.
+ */
+ public DetailedChartCreator(ProfileInfo info, List<ProfilePhaseStatistics> statistics) {
+ this.info = info;
+ this.statistics = statistics;
+ }
+
+ @Override
+ public Chart create() {
+ Chart chart = new Chart(info.comment, statistics);
+ CommonChartCreator.createCommonChartItems(chart, info);
+ createTypes(chart);
+
+ // calculate the critical path
+ EnumSet<ProfilerTask> typeFilter = EnumSet.noneOf(ProfilerTask.class);
+ CriticalPathEntry criticalPath = info.getCriticalPath(typeFilter);
+ info.analyzeCriticalPath(typeFilter, criticalPath);
+
+ for (Task task : info.allTasksById) {
+ String label = task.type.description + ": " + task.getDescription();
+ ChartBarType type = chart.lookUpType(task.type.description);
+ long stop = task.startTime + task.duration;
+ CriticalPathEntry entry = null;
+
+ // for top level tasks, check if they are on the critical path
+ if (task.parentId == 0 && criticalPath != null) {
+ entry = info.getNextCriticalPathEntryForTask(criticalPath, task);
+ // find next top-level entry
+ if (entry != null) {
+ CriticalPathEntry nextEntry = entry.next;
+ while (nextEntry != null && nextEntry.task.parentId != 0) {
+ nextEntry = nextEntry.next;
+ }
+ if (nextEntry != null) {
+ // time is start and not stop as we traverse the critical back backwards
+ chart.addVerticalLine(task.threadId, nextEntry.task.threadId, task.startTime);
+ }
+ }
+ }
+
+ chart.addBar(task.threadId, task.startTime, stop, type, (entry != null), label);
+ }
+
+ return chart;
+ }
+
+ /**
+ * Creates a {@link ChartBarType} for every known {@link ProfilerTask} and
+ * adds it to the chart.
+ *
+ * @param chart the chart to add the types to
+ */
+ private void createTypes(Chart chart) {
+ for (ProfilerTask task : ProfilerTask.values()) {
+ chart.createType(task.description, new Color(task.color));
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/profiler/chart/HtmlChartVisitor.java b/src/main/java/com/google/devtools/build/lib/profiler/chart/HtmlChartVisitor.java
new file mode 100644
index 0000000..8fa3d0e4
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/profiler/chart/HtmlChartVisitor.java
@@ -0,0 +1,368 @@
+// Copyright 2014 Google Inc. 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.profiler.chart;
+
+import com.google.devtools.build.lib.profiler.ProfilePhaseStatistics;
+
+import java.io.PrintStream;
+import java.util.List;
+
+/**
+ * {@link ChartVisitor} that builds HTML from the visited chart and prints it
+ * out to the given {@link PrintStream}.
+ */
+public class HtmlChartVisitor implements ChartVisitor {
+
+ /** The default width of a second in the chart. */
+ private static final int DEFAULT_PIXEL_PER_SECOND = 50;
+
+ /** The horizontal offset of second zero. */
+ private static final int H_OFFSET = 40;
+
+ /** The font size of the row labels. */
+ private static final int ROW_LABEL_FONT_SIZE = 7;
+
+ /** The height of a bar in pixels. */
+ private static final int BAR_HEIGHT = 8;
+
+ /** The space between twp bars in pixels. */
+ private static final int BAR_SPACE = 2;
+
+ /** The height of a row. */
+ private static final int ROW_HEIGHT = BAR_HEIGHT + BAR_SPACE;
+
+ /** The {@link PrintStream} to output the HTML to. */
+ private final PrintStream out;
+
+ /** The maxmimum stop time of any bar in the chart. */
+ private long maxStop;
+
+ /** The width of a second in the chart. */
+ private final int pixelsPerSecond;
+
+ /**
+ * Creates the visitor, with a default width of a second of 50 pixels.
+ *
+ * @param out the {@link PrintStream} to output the HTML to
+ */
+ public HtmlChartVisitor(PrintStream out) {
+ this(out, DEFAULT_PIXEL_PER_SECOND);
+ }
+
+ /**
+ * Creates the visitor.
+ *
+ * @param out the {@link PrintStream} to output the HTML to
+ * @param pixelsPerSecond The width of a second in the chart. (In pixels)
+ */
+ public HtmlChartVisitor(PrintStream out, int pixelsPerSecond) {
+ this.out = out;
+ this.pixelsPerSecond = pixelsPerSecond;
+ }
+
+ @Override
+ public void visit(Chart chart) {
+ maxStop = chart.getMaxStop();
+ out.println("<html><head>");
+ out.printf("<title>%s</title>", chart.getTitle());
+ out.println("<style type=\"text/css\"><!--");
+
+ printCss(chart.getSortedTypes());
+
+ out.println("--></style>");
+ out.println("</head>");
+ out.println("<body>");
+
+ heading(chart.getTitle(), 1);
+
+ printContentBox();
+
+ heading("Tasks", 2);
+ out.println("<p>To get more information about a task point the mouse at one of the bars.</p>");
+
+ out.printf("<div style='position:relative; height: %dpx; margin: %dpx'>\n",
+ chart.getRowCount() * ROW_HEIGHT, H_OFFSET + 10);
+ }
+
+ @Override
+ public void endVisit(Chart chart) {
+ printTimeAxis(chart);
+ out.println("</div>");
+
+ heading("Legend", 2);
+ printLegend(chart.getSortedTypes());
+
+ heading("Statistics", 2);
+ printStatistics(chart.getStatistics());
+
+ out.println("</body>");
+ out.println("</html>");
+}
+
+ @Override
+ public void visit(ChartColumn column) {
+ int width = scale(column.getWidth());
+ if (width == 0) {
+ return;
+ }
+ int left = scale(column.getStart());
+ int height = column.getRowCount() * ROW_HEIGHT;
+ String style = chartTypeNameAsCSSClass(column.getType().getName());
+ box(left, 0, width, height, style, column.getLabel(), 10);
+ }
+
+
+ @Override
+ public void visit(ChartRow slot) {
+ String style = slot.getIndex() % 2 == 0 ? "shade-even" : "shade-odd";
+ int top = slot.getIndex() * ROW_HEIGHT;
+ int width = scale(maxStop) + 1;
+
+ label(-H_OFFSET, top, width + H_OFFSET, ROW_HEIGHT, ROW_LABEL_FONT_SIZE, slot.getId());
+ box(0, top, width, ROW_HEIGHT, style, "", 0);
+ }
+
+ @Override
+ public void visit(ChartBar bar) {
+ int width = scale(bar.getWidth());
+ if (width == 0) {
+ return;
+ }
+ int left = scale(bar.getStart());
+ int top = bar.getRow().getIndex() * ROW_HEIGHT;
+ String style = chartTypeNameAsCSSClass(bar.getType().getName());
+ if (bar.getHighlight()) {
+ style += "-highlight";
+ }
+ box(left, top + 2, width, BAR_HEIGHT, style, bar.getLabel(), 20);
+ }
+
+ @Override
+ public void visit(ChartLine chartLine) {
+ int start = chartLine.getStartRow().getIndex() * ROW_HEIGHT;
+ int stop = chartLine.getStopRow().getIndex() * ROW_HEIGHT;
+ int time = scale(chartLine.getStartTime());
+
+ if (start < stop) {
+ verticalLine(time, start + 1, 1, (stop - start) + ROW_HEIGHT, Color.RED);
+ } else {
+ verticalLine(time, stop + 1, 1, (start - stop) + ROW_HEIGHT, Color.RED);
+ }
+ }
+
+ /**
+ * Converts the given value from the bar of the chart to pixels.
+ */
+ private int scale(long value) {
+ return (int) (value / (1000000000L / pixelsPerSecond));
+ }
+
+ /**
+ * Prints a box with links to the sections of the generated HTML document.
+ */
+ private void printContentBox() {
+ out.println("<div style='position:fixed; top:1em; right:1em; z-index:50; padding: 1ex;"
+ + "border:1px solid #888; background-color:#eee; width:100px'><h3>Content</h3>");
+ out.println("<p style='text-align:left;font-size:small;margin:2px'>"
+ + "<a href='#Tasks'>Tasks</a></p>");
+ out.println("<p style='text-align:left;font-size:small;margin:2px'>"
+ + "<a href='#Legend'>Legend</a></p>");
+ out.println("<p style='text-align:left;font-size:small;margin:2px'>"
+ + "<a href='#Statistics'>Statistics</a></p></div>");
+ }
+
+ /**
+ * Prints the time axis of the chart and vertical lines for every second.
+ */
+ private void printTimeAxis(Chart chart) {
+ int location = 0;
+ int second = 0;
+ int end = scale(chart.getMaxStop());
+ while (location < end) {
+ label(location + 4, -17, pixelsPerSecond, ROW_HEIGHT, 0, second + "s");
+ verticalLine(location, -20, 1, chart.getRowCount() * ROW_HEIGHT + 20, Color.GRAY);
+ location += pixelsPerSecond;
+ second += 1;
+ }
+ }
+
+ private void printCss(List<ChartBarType> types) {
+ out.println("body { font-family: Sans; }");
+ out.printf("div.shade-even { position:absolute; border: 0px; background-color:#dddddd }\n");
+ out.printf("div.shade-odd { position:absolute; border: 0px; background-color:#eeeeee }\n");
+ for (ChartBarType type : types) {
+ String name = chartTypeNameAsCSSClass(type.getName());
+ String color = formatColor(type.getColor());
+
+ out.printf(
+ "div.%s-border { position:absolute; border:1px solid grey; background-color:%s }\n",
+ name, color);
+ out.printf(
+ "div.%s-highlight { position:absolute; border:1px solid red; background-color:%s }\n",
+ name, color);
+ out.printf("div.%s { position:absolute; border:0px; margin:1px; background-color:%s }\n",
+ name, color);
+ }
+ }
+
+ /**
+ * Prints the legend for the chart at the current position in the document. The
+ * legend is printed in columns of 10 rows each.
+ *
+ * @param types the list of {@link ChartBarType}s to print in the legend.
+ */
+ private void printLegend(List<ChartBarType> types) {
+ final int boxHeight = 20;
+ final int lineHeight = 25;
+ final int entriesPerColumn = 10;
+ final int legendWidth = 350;
+ int legendHeight;
+ if (types.size() / entriesPerColumn >= 1) {
+ legendHeight = entriesPerColumn;
+ } else {
+ legendHeight = types.size() % entriesPerColumn;
+ }
+
+ out.printf("<div style='position:relative; height: %dpx;'>",
+ (legendHeight + 1) * lineHeight);
+
+ int left = -legendWidth;
+ int top;
+ int i = 0;
+ for (ChartBarType type : types) {
+ if (i % entriesPerColumn == 0) {
+ left += legendWidth;
+ i = 0;
+ }
+ top = lineHeight * i;
+ String style = chartTypeNameAsCSSClass(type.getName()) + "-border";
+ box(left, top, boxHeight, boxHeight, style, type.getName(), 0);
+ label(left + lineHeight + 10, top, legendWidth - 10, boxHeight, 0, type.getName());
+ i++;
+ }
+ out.println("</div>");
+ }
+
+ private void printStatistics(List<ProfilePhaseStatistics> statistics) {
+ boolean first = true;
+
+ out.println("<table border=\"0\" width=\"100%\"><tr>");
+ for (ProfilePhaseStatistics stat : statistics) {
+ if (!first) {
+ out.println("<td><div style=\"width:20px;\"> </div></td>");
+ } else {
+ first = false;
+ }
+ out.println("<td valign=\"top\">");
+ String title = stat.getTitle();
+ if (title != "") {
+ heading(title, 3);
+ }
+ out.println("<pre>" + stat.getStatistics() + "</pre></td>");
+ }
+ out.println("</tr></table>");
+ }
+
+ /**
+ * Prints a head-line at the current position in the document.
+ *
+ * @param text the text to print
+ * @param level the headline level
+ */
+ private void heading(String text, int level) {
+ anchor(text);
+ out.printf("<h%d >%s</h%d>\n", level, text, level);
+ }
+
+ /**
+ * Prints a box with the given location, size, background color and border.
+ *
+ * @param x the x location of the top left corner of the box
+ * @param y the y location of the top left corner of the box
+ * @param width the width location of the box
+ * @param height the height location of the box
+ * @param style the CSS style class to use for the box
+ * @param title the text displayed when the mouse hovers over the box
+ */
+ private void box(int x, int y, int width, int height, String style, String title, int zIndex) {
+ out.printf("<div class=\"%s\" title=\"%s\" "
+ + "style=\"left:%dpx; top:%dpx; width:%dpx; height:%dpx; z-index:%d\"></div>\n",
+ style, title, x, y, width, height, zIndex);
+ }
+
+ /**
+ * Prints a label with the given location, size, background color and border.
+ *
+ * @param x the x location of the top left corner of the box
+ * @param y the y location of the top left corner of the box
+ * @param width the width location of the box
+ * @param height the height location of the box
+ * @param fontSize the font size of text in the box, 0 for default
+ * @param text the text displayed in the box
+ */
+ private void label(int x, int y, int width, int height, int fontSize, String text) {
+ if (fontSize > 0) {
+ out.printf("<div style=\"position:absolute; left:%dpx; top:%dpx; width:%dpx; "
+ + "height:%dpx; font-size:%dpt\">%s</div>\n",
+ x, y, width, height, fontSize, text);
+ } else {
+ out.printf("<div style=\"position:absolute; left:%dpx; top:%dpx; width:%dpx; "
+ + "height:%dpx\">%s</div>\n",
+ x, y, width, height, text);
+ }
+ }
+
+ /**
+ * Prints a vertical line of given width, height and color at the given
+ * location.
+ *
+ * @param x the x location of the start point of the line
+ * @param y the y location of the start point of the line
+ * @param width the width of the line
+ * @param length the length of the line
+ * @param color the color of the line
+ */
+ private void verticalLine(int x, int y, int width, int length, Color color) {
+ out.printf("<div style='position: absolute; left: %dpx; top: %dpx; width: %dpx; "
+ + "height: %dpx; border-left: %dpx solid %s'" + "></div>\n",
+ x, y, width, length, width, formatColor(color));
+ }
+
+ /**
+ * Prints an HTML anchor with the given name,
+ */
+ private void anchor(String name) {
+ out.println("<a name='" + name + "'/>");
+ }
+
+ /**
+ * Formats the given {@link Color} to a css style color string.
+ */
+ private String formatColor(Color color) {
+ int r = color.getRed();
+ int g = color.getGreen();
+ int b = color.getBlue();
+ int a = color.getAlpha();
+
+ return String.format("rgba(%d,%d,%d,%f)", r, g, b, (a / 255.0));
+ }
+
+ /**
+ * Transform the name into a form suitable as a css class.
+ */
+ private String chartTypeNameAsCSSClass(String name) {
+ return name.replace(' ', '_');
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/query2/BlazeQueryEnvironment.java b/src/main/java/com/google/devtools/build/lib/query2/BlazeQueryEnvironment.java
new file mode 100644
index 0000000..d6c2992
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/query2/BlazeQueryEnvironment.java
@@ -0,0 +1,633 @@
+// Copyright 2014 Google Inc. 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.query2;
+
+import static com.google.devtools.build.lib.packages.Type.BOOLEAN;
+import static com.google.devtools.build.lib.packages.Type.TRISTATE;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
+import com.google.devtools.build.lib.cmdline.ResolvedTargets;
+import com.google.devtools.build.lib.cmdline.TargetParsingException;
+import com.google.devtools.build.lib.events.ErrorSensingEventHandler;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.graph.Digraph;
+import com.google.devtools.build.lib.graph.Node;
+import com.google.devtools.build.lib.packages.AggregatingAttributeMapper;
+import com.google.devtools.build.lib.packages.Attribute;
+import com.google.devtools.build.lib.packages.NoSuchThingException;
+import com.google.devtools.build.lib.packages.NonconfigurableAttributeMapper;
+import com.google.devtools.build.lib.packages.OutputFile;
+import com.google.devtools.build.lib.packages.Package;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.packages.TargetUtils;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.pkgcache.PackageProvider;
+import com.google.devtools.build.lib.pkgcache.TargetEdgeObserver;
+import com.google.devtools.build.lib.pkgcache.TargetPatternEvaluator;
+import com.google.devtools.build.lib.pkgcache.TargetProvider;
+import com.google.devtools.build.lib.query2.engine.BlazeQueryEvalResult;
+import com.google.devtools.build.lib.query2.engine.QueryEnvironment;
+import com.google.devtools.build.lib.query2.engine.QueryException;
+import com.google.devtools.build.lib.query2.engine.QueryExpression;
+import com.google.devtools.build.lib.query2.engine.SkyframeRestartQueryException;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.util.BinaryPredicate;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * The environment of a Blaze query. Not thread-safe.
+ */
+public class BlazeQueryEnvironment implements QueryEnvironment<Target> {
+ protected final ErrorSensingEventHandler eventHandler;
+ private final TargetProvider targetProvider;
+ private final TargetPatternEvaluator targetPatternEvaluator;
+ private final Digraph<Target> graph = new Digraph<>();
+ private final ErrorPrintingTargetEdgeErrorObserver errorObserver;
+ private final LabelVisitor labelVisitor;
+ private final Map<String, Set<Target>> letBindings = new HashMap<>();
+ private final Map<String, ResolvedTargets<Target>> resolvedTargetPatterns = new HashMap<>();
+ protected final boolean keepGoing;
+ private final boolean strictScope;
+ protected final int loadingPhaseThreads;
+
+ private final BinaryPredicate<Rule, Attribute> dependencyFilter;
+ private final Predicate<Label> labelFilter;
+
+ private final Set<Setting> settings;
+ private final List<QueryFunction> extraFunctions;
+ private final BlazeTargetAccessor accessor = new BlazeTargetAccessor();
+
+ /**
+ * Note that the correct operation of this class critically depends on the Reporter being a
+ * singleton object, shared by all cooperating classes contributing to Query.
+ * @param strictScope if true, fail the whole query if a label goes out of scope.
+ * @param loadingPhaseThreads the number of threads to use during loading
+ * the packages for the query.
+ * @param labelFilter a predicate that determines if a specific label is
+ * allowed to be visited during query execution. If it returns false,
+ * the query execution is stopped with an error message.
+ * @param settings a set of enabled settings
+ */
+ public BlazeQueryEnvironment(PackageProvider packageProvider,
+ TargetPatternEvaluator targetPatternEvaluator,
+ boolean keepGoing,
+ boolean strictScope,
+ int loadingPhaseThreads,
+ Predicate<Label> labelFilter,
+ EventHandler eventHandler,
+ Set<Setting> settings,
+ Iterable<QueryFunction> extraFunctions) {
+ this.eventHandler = new ErrorSensingEventHandler(eventHandler);
+ this.targetProvider = packageProvider;
+ this.targetPatternEvaluator = targetPatternEvaluator;
+ this.errorObserver = new ErrorPrintingTargetEdgeErrorObserver(this.eventHandler);
+ this.keepGoing = keepGoing;
+ this.strictScope = strictScope;
+ this.loadingPhaseThreads = loadingPhaseThreads;
+ this.dependencyFilter = constructDependencyFilter(settings);
+ this.labelVisitor = new LabelVisitor(packageProvider, dependencyFilter);
+ this.labelFilter = labelFilter;
+ this.settings = Sets.immutableEnumSet(settings);
+ this.extraFunctions = ImmutableList.copyOf(extraFunctions);
+ }
+
+ /**
+ * Note that the correct operation of this class critically depends on the Reporter being a
+ * singleton object, shared by all cooperating classes contributing to Query.
+ * @param loadingPhaseThreads the number of threads to use during loading
+ * the packages for the query.
+ * @param settings a set of enabled settings
+ */
+ public BlazeQueryEnvironment(PackageProvider packageProvider,
+ TargetPatternEvaluator targetPatternEvaluator,
+ boolean keepGoing,
+ int loadingPhaseThreads,
+ EventHandler eventHandler,
+ Set<Setting> settings,
+ Iterable<QueryFunction> extraFunctions) {
+ this(packageProvider, targetPatternEvaluator, keepGoing, /*strictScope=*/true,
+ loadingPhaseThreads, Rule.ALL_LABELS, eventHandler, settings, extraFunctions);
+ }
+
+ private static BinaryPredicate<Rule, Attribute> constructDependencyFilter(Set<Setting> settings) {
+ BinaryPredicate<Rule, Attribute> specifiedFilter =
+ settings.contains(Setting.NO_HOST_DEPS) ? Rule.NO_HOST_DEPS : Rule.ALL_DEPS;
+ if (settings.contains(Setting.NO_IMPLICIT_DEPS)) {
+ specifiedFilter = Rule.and(specifiedFilter, Rule.NO_IMPLICIT_DEPS);
+ }
+ if (settings.contains(Setting.NO_NODEP_DEPS)) {
+ specifiedFilter = Rule.and(specifiedFilter, Rule.NO_NODEP_ATTRIBUTES);
+ }
+ return specifiedFilter;
+ }
+
+ /**
+ * Evaluate the specified query expression in this environment.
+ *
+ * @return a {@link BlazeQueryEvalResult} object that contains the resulting set of targets, the
+ * partial graph, and a bit to indicate whether errors occured during evaluation; note that the
+ * success status can only be false if {@code --keep_going} was in effect
+ * @throws QueryException if the evaluation failed and {@code --nokeep_going} was in
+ * effect
+ */
+ public BlazeQueryEvalResult<Target> evaluateQuery(QueryExpression expr) throws QueryException {
+ // Some errors are reported as QueryExceptions and others as ERROR events
+ // (if --keep_going).
+ eventHandler.resetErrors();
+ resolvedTargetPatterns.clear();
+
+ // In the --nokeep_going case, errors are reported in the order in which the patterns are
+ // specified; using a linked hash set here makes sure that the left-most error is reported.
+ Set<String> targetPatternSet = new LinkedHashSet<>();
+ expr.collectTargetPatterns(targetPatternSet);
+ try {
+ resolvedTargetPatterns.putAll(preloadOrThrow(targetPatternSet));
+ } catch (TargetParsingException e) {
+ // Unfortunately, by evaluating the patterns in parallel, we lose some location information.
+ throw new QueryException(expr, e.getMessage());
+ }
+
+ Set<Target> resultNodes;
+ try {
+ resultNodes = expr.eval(this);
+ } catch (QueryException e) {
+ throw new QueryException(e, expr);
+ }
+
+ if (eventHandler.hasErrors()) {
+ if (!keepGoing) {
+ // This case represents loading-phase errors reported during evaluation
+ // of target patterns that don't cause evaluation to fail per se.
+ throw new QueryException("Evaluation of query \"" + expr
+ + "\" failed due to BUILD file errors");
+ } else {
+ eventHandler.handle(Event.warn("--keep_going specified, ignoring errors. "
+ + "Results may be inaccurate"));
+ }
+ }
+
+ return new BlazeQueryEvalResult<>(!eventHandler.hasErrors(), resultNodes, graph);
+ }
+
+ public BlazeQueryEvalResult<Target> evaluateQuery(String query) throws QueryException {
+ return evaluateQuery(QueryExpression.parse(query, this));
+ }
+
+ @Override
+ public void reportBuildFileError(QueryExpression caller, String message) throws QueryException {
+ if (!keepGoing) {
+ throw new QueryException(caller, message);
+ } else {
+ // Keep consistent with evaluateQuery() above.
+ eventHandler.handle(Event.error("Evaluation of query \"" + caller + "\" failed: " + message));
+ }
+ }
+
+ @Override
+ public Set<Target> getTargetsMatchingPattern(QueryExpression caller,
+ String pattern) throws QueryException {
+ // We can safely ignore the boolean error flag. The evaluateQuery() method above wraps the
+ // entire query computation in an error sensor.
+
+ Set<Target> targets = new LinkedHashSet<>(resolvedTargetPatterns.get(pattern).getTargets());
+
+ // Sets.filter would be more convenient here, but can't deal with exceptions.
+ Iterator<Target> targetIterator = targets.iterator();
+ while (targetIterator.hasNext()) {
+ Target target = targetIterator.next();
+ if (!validateScope(target.getLabel(), strictScope)) {
+ targetIterator.remove();
+ }
+ }
+
+ Set<PathFragment> packages = new HashSet<>();
+ for (Target target : targets) {
+ packages.add(target.getLabel().getPackageFragment());
+ }
+
+ Set<Target> result = new LinkedHashSet<>();
+ for (Target target : targets) {
+ result.add(getOrCreate(target));
+
+ // Preservation of graph order: it is important that targets obtained via
+ // a wildcard such as p:* are correctly ordered w.r.t. each other, so to
+ // ensure this, we add edges between any pair of directly connected
+ // targets in this set.
+ if (target instanceof OutputFile) {
+ OutputFile outputFile = (OutputFile) target;
+ if (targets.contains(outputFile.getGeneratingRule())) {
+ makeEdge(outputFile, outputFile.getGeneratingRule());
+ }
+ } else if (target instanceof Rule) {
+ Rule rule = (Rule) target;
+ for (Label label : rule.getLabels(dependencyFilter)) {
+ if (!packages.contains(label.getPackageFragment())) {
+ continue; // don't cause additional package loading
+ }
+ try {
+ if (!validateScope(label, strictScope)) {
+ continue; // Don't create edges to targets which are out of scope.
+ }
+ Target to = getTargetOrThrow(label);
+ if (targets.contains(to)) {
+ makeEdge(rule, to);
+ }
+ } catch (NoSuchThingException e) {
+ /* ignore */
+ } catch (InterruptedException e) {
+ throw new QueryException("interrupted");
+ }
+ }
+ }
+ }
+ return result;
+ }
+
+ public Node<Target> getTarget(Label label) throws TargetNotFoundException, QueryException {
+ // Can't use strictScope here because we are expecting a target back.
+ validateScope(label, true);
+ try {
+ return getNode(getTargetOrThrow(label));
+ } catch (NoSuchThingException e) {
+ throw new TargetNotFoundException(e);
+ } catch (InterruptedException e) {
+ throw new QueryException("interrupted");
+ }
+ }
+
+ private Node<Target> getNode(Target target) {
+ return graph.createNode(target);
+ }
+
+ private Collection<Node<Target>> getNodes(Iterable<Target> target) {
+ Set<Node<Target>> result = new LinkedHashSet<>();
+ for (Target t : target) {
+ result.add(getNode(t));
+ }
+ return result;
+ }
+
+ @Override
+ public Target getOrCreate(Target target) {
+ return getNode(target).getLabel();
+ }
+
+ @Override
+ public Collection<Target> getFwdDeps(Target target) {
+ return getTargetsFromNodes(getNode(target).getSuccessors());
+ }
+
+ @Override
+ public Collection<Target> getReverseDeps(Target target) {
+ return getTargetsFromNodes(getNode(target).getPredecessors());
+ }
+
+ @Override
+ public Set<Target> getTransitiveClosure(Set<Target> targetNodes) {
+ for (Target node : targetNodes) {
+ checkBuilt(node);
+ }
+ return getTargetsFromNodes(graph.getFwdReachable(getNodes(targetNodes)));
+ }
+
+ /**
+ * Checks that the graph rooted at 'targetNode' has been completely built;
+ * fails if not. Callers of {@link #getTransitiveClosure} must ensure that
+ * {@link #buildTransitiveClosure} has been called before.
+ *
+ * <p>It would be inefficient and failure-prone to make getTransitiveClosure
+ * call buildTransitiveClosure directly. Also, it would cause
+ * nondeterministic behavior of the operators, since the set of packages
+ * loaded (and hence errors reported) would depend on the ordering details of
+ * the query operators' implementations.
+ */
+ private void checkBuilt(Target targetNode) {
+ Preconditions.checkState(
+ labelVisitor.hasVisited(targetNode.getLabel()),
+ "getTransitiveClosure(%s) called without prior call to buildTransitiveClosure()",
+ targetNode);
+ }
+
+ protected void preloadTransitiveClosure(Set<Target> targets, int maxDepth) throws QueryException {
+ }
+
+ @Override
+ public void buildTransitiveClosure(QueryExpression caller,
+ Set<Target> targetNodes,
+ int maxDepth) throws QueryException {
+ Set<Target> targets = targetNodes;
+ preloadTransitiveClosure(targets, maxDepth);
+
+ try {
+ labelVisitor.syncWithVisitor(eventHandler, targets, keepGoing,
+ loadingPhaseThreads, maxDepth, errorObserver, new GraphBuildingObserver());
+ } catch (InterruptedException e) {
+ throw new QueryException(caller, "transitive closure computation was interrupted");
+ }
+
+ if (errorObserver.hasErrors()) {
+ reportBuildFileError(caller, "errors were encountered while computing transitive closure");
+ }
+ }
+
+ @Override
+ public Set<Target> getNodesOnPath(Target from, Target to) {
+ return getTargetsFromNodes(graph.getShortestPath(getNode(from), getNode(to)));
+ }
+
+ @Override
+ public Set<Target> getVariable(String name) {
+ return letBindings.get(name);
+ }
+
+ @Override
+ public Set<Target> setVariable(String name, Set<Target> value) {
+ return letBindings.put(name, value);
+ }
+
+ /**
+ * It suffices to synchronize the modifications of this.graph from within the
+ * GraphBuildingObserver, because that's the only concurrent part.
+ * Concurrency is always encapsulated within the evaluation of a single query
+ * operator (e.g. deps(), somepath(), etc).
+ */
+ private class GraphBuildingObserver implements TargetEdgeObserver {
+
+ @Override
+ public synchronized void edge(Target from, Attribute attribute, Target to) {
+ Preconditions.checkState(attribute == null ||
+ dependencyFilter.apply(((Rule) from), attribute),
+ "Disallowed edge from LabelVisitor: %s --> %s", from, to);
+ makeEdge(from, to);
+ }
+
+ @Override
+ public synchronized void node(Target node) {
+ graph.createNode(node);
+ }
+
+ @Override
+ public void missingEdge(Target target, Label to, NoSuchThingException e) {
+ // No - op.
+ }
+ }
+
+ private void makeEdge(Target from, Target to) {
+ graph.addEdge(from, to);
+ }
+
+ private boolean validateScope(Label label, boolean strict) throws QueryException {
+ if (!labelFilter.apply(label)) {
+ String error = String.format("target '%s' is not within the scope of the query", label);
+ if (strict) {
+ throw new QueryException(error);
+ } else {
+ eventHandler.handle(Event.warn(error + ". Skipping"));
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public Set<Target> evalTargetPattern(QueryExpression caller, String pattern)
+ throws QueryException {
+ if (!resolvedTargetPatterns.containsKey(pattern)) {
+ try {
+ resolvedTargetPatterns.putAll(preloadOrThrow(ImmutableList.of(pattern)));
+ } catch (TargetParsingException e) {
+ // Will skip the target and keep going if -k is specified.
+ resolvedTargetPatterns.put(pattern, ResolvedTargets.<Target>empty());
+ reportBuildFileError(caller, e.getMessage());
+ }
+ }
+ return getTargetsMatchingPattern(caller, pattern);
+ }
+
+ private Map<String, ResolvedTargets<Target>> preloadOrThrow(Collection<String> patterns)
+ throws TargetParsingException {
+ try {
+ // Note that this may throw a RuntimeException if deps are missing in Skyframe.
+ return targetPatternEvaluator.preloadTargetPatterns(
+ eventHandler, patterns, keepGoing);
+ } catch (InterruptedException e) {
+ // TODO(bazel-team): Propagate the InterruptedException from here [skyframe-loading].
+ throw new TargetParsingException("interrupted");
+ }
+ }
+
+ private Target getTargetOrThrow(Label label)
+ throws NoSuchThingException, SkyframeRestartQueryException, InterruptedException {
+ Target target = targetProvider.getTarget(eventHandler, label);
+ if (target == null) {
+ throw new SkyframeRestartQueryException();
+ }
+ return target;
+ }
+
+ // TODO(bazel-team): rename this to getDependentFiles when all implementations
+ // of QueryEnvironment is fixed.
+ @Override
+ public Set<Target> getBuildFiles(final QueryExpression caller, Set<Target> nodes)
+ throws QueryException {
+ Set<Target> dependentFiles = new LinkedHashSet<>();
+ Set<Package> seenPackages = new HashSet<>();
+ // Keep track of seen labels, to avoid adding a fake subinclude label that also exists as a
+ // real target.
+ Set<Label> seenLabels = new HashSet<>();
+
+ // Adds all the package definition files (BUILD files and build
+ // extensions) for package "pkg", to "buildfiles".
+ for (Target x : nodes) {
+ Package pkg = x.getPackage();
+ if (seenPackages.add(pkg)) {
+ addIfUniqueLabel(getNode(pkg.getBuildFile()), seenLabels, dependentFiles);
+ for (Label subinclude
+ : Iterables.concat(pkg.getSubincludeLabels(), pkg.getSkylarkFileDependencies())) {
+ addIfUniqueLabel(getSubincludeTarget(subinclude, pkg), seenLabels, dependentFiles);
+
+ // Also add the BUILD file of the subinclude.
+ try {
+ addIfUniqueLabel(getSubincludeTarget(
+ subinclude.getLocalTargetLabel("BUILD"), pkg), seenLabels, dependentFiles);
+ } catch (Label.SyntaxException e) {
+ throw new AssertionError("BUILD should always parse as a target name", e);
+ }
+ }
+ }
+ }
+ return dependentFiles;
+ }
+
+ private static void addIfUniqueLabel(Node<Target> node, Set<Label> labels, Set<Target> nodes) {
+ if (labels.add(node.getLabel().getLabel())) {
+ nodes.add(node.getLabel());
+ }
+ }
+
+ private Node<Target> getSubincludeTarget(final Label label, Package pkg) {
+ return getNode(new FakeSubincludeTarget(label, pkg.getBuildFile().getLocation()));
+ }
+
+ @Override
+ public TargetAccessor<Target> getAccessor() {
+ return accessor;
+ }
+
+ @Override
+ public boolean isSettingEnabled(Setting setting) {
+ return settings.contains(Preconditions.checkNotNull(setting));
+ }
+
+ @Override
+ public Iterable<QueryFunction> getFunctions() {
+ ImmutableList.Builder<QueryFunction> builder = ImmutableList.builder();
+ builder.addAll(DEFAULT_QUERY_FUNCTIONS);
+ builder.addAll(extraFunctions);
+ return builder.build();
+ }
+
+ private final class BlazeTargetAccessor implements TargetAccessor<Target> {
+
+ @Override
+ public String getTargetKind(Target target) {
+ return target.getTargetKind();
+ }
+
+ @Override
+ public String getLabel(Target target) {
+ return target.getLabel().toString();
+ }
+
+ @Override
+ public List<Target> getLabelListAttr(QueryExpression caller, Target target, String attrName,
+ String errorMsgPrefix) throws QueryException {
+ Preconditions.checkArgument(target instanceof Rule);
+
+ List<Target> result = new ArrayList<>();
+ Rule rule = (Rule) target;
+
+ AggregatingAttributeMapper attrMap = AggregatingAttributeMapper.of(rule);
+ Type<?> attrType = attrMap.getAttributeType(attrName);
+ if (attrType == null) {
+ // Return an empty list if the attribute isn't defined for this rule.
+ return ImmutableList.of();
+ }
+ for (Object value : attrMap.visitAttribute(attrName, attrType)) {
+ // Computed defaults may have null values.
+ if (value != null) {
+ for (Label label : attrType.getLabels(value)) {
+ try {
+ result.add(getTarget(label).getLabel());
+ } catch (TargetNotFoundException e) {
+ reportBuildFileError(caller, errorMsgPrefix + e.getMessage());
+ }
+ }
+ }
+ }
+
+ return result;
+ }
+
+ @Override
+ public List<String> getStringListAttr(Target target, String attrName) {
+ Preconditions.checkArgument(target instanceof Rule);
+ return NonconfigurableAttributeMapper.of((Rule) target).get(attrName, Type.STRING_LIST);
+ }
+
+ @Override
+ public String getStringAttr(Target target, String attrName) {
+ Preconditions.checkArgument(target instanceof Rule);
+ return NonconfigurableAttributeMapper.of((Rule) target).get(attrName, Type.STRING);
+ }
+
+ @Override
+ public Iterable<String> getAttrAsString(Target target, String attrName) {
+ Preconditions.checkArgument(target instanceof Rule);
+ List<String> values = new ArrayList<>(); // May hold null values.
+ Attribute attribute = ((Rule) target).getAttributeDefinition(attrName);
+ if (attribute != null) {
+ Type<?> attributeType = attribute.getType();
+ for (Object attrValue : AggregatingAttributeMapper.of((Rule) target).visitAttribute(
+ attribute.getName(), attributeType)) {
+
+ // Ugly hack to maintain backward 'attr' query compatibility for BOOLEAN and TRISTATE
+ // attributes. These are internally stored as actual Boolean or TriState objects but were
+ // historically queried as integers. To maintain compatibility, we inspect their actual
+ // value and return the integer equivalent represented as a String. This code is the
+ // opposite of the code in BooleanType and TriStateType respectively.
+ if (attributeType == BOOLEAN) {
+ values.add(Type.BOOLEAN.cast(attrValue) ? "1" : "0");
+ } else if (attributeType == TRISTATE) {
+ switch (Type.TRISTATE.cast(attrValue)) {
+ case AUTO :
+ values.add("-1");
+ break;
+ case NO :
+ values.add("0");
+ break;
+ case YES :
+ values.add("1");
+ break;
+ default :
+ throw new AssertionError("This can't happen!");
+ }
+ } else {
+ values.add(attrValue == null ? null : attrValue.toString());
+ }
+ }
+ }
+ return values;
+ }
+
+ @Override
+ public boolean isRule(Target target) {
+ return target instanceof Rule;
+ }
+
+ @Override
+ public boolean isTestRule(Target target) {
+ return TargetUtils.isTestRule(target);
+ }
+
+ @Override
+ public boolean isTestSuite(Target target) {
+ return TargetUtils.isTestSuiteRule(target);
+ }
+ }
+
+ /** Given a set of target nodes, returns the targets. */
+ private static Set<Target> getTargetsFromNodes(Iterable<Node<Target>> input) {
+ Set<Target> result = new LinkedHashSet<>();
+ for (Node<Target> node : input) {
+ result.add(node.getLabel());
+ }
+ return result;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/query2/ErrorPrintingTargetEdgeErrorObserver.java b/src/main/java/com/google/devtools/build/lib/query2/ErrorPrintingTargetEdgeErrorObserver.java
new file mode 100644
index 0000000..d47b8f3
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/query2/ErrorPrintingTargetEdgeErrorObserver.java
@@ -0,0 +1,53 @@
+// Copyright 2014 Google Inc. 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.query2;
+
+import com.google.devtools.build.lib.concurrent.ThreadSafety;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.packages.NoSuchThingException;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.packages.TargetUtils;
+import com.google.devtools.build.lib.syntax.Label;
+
+/**
+ * Record errors, such as missing package/target or rules containing errors,
+ * encountered during visitation. Emit an error message upon encountering
+ * missing edges
+ *
+ * The accessor {@link #hasErrors}) may not be called until the concurrent phase
+ * is over, i.e. all external calls to visit() methods have completed.
+ */
+@ThreadSafety.ConditionallyThreadSafe // condition: only call hasErrors
+ // once the visitation is complete.
+class ErrorPrintingTargetEdgeErrorObserver extends TargetEdgeErrorObserver {
+
+ private final EventHandler eventHandler;
+
+ /**
+ * @param eventHandler eventHandler to route exceptions to as errors.
+ */
+ public ErrorPrintingTargetEdgeErrorObserver(EventHandler eventHandler) {
+ this.eventHandler = eventHandler;
+ }
+
+ @ThreadSafety.ThreadSafe
+ @Override
+ public void missingEdge(Target target, Label label, NoSuchThingException e) {
+ eventHandler.handle(Event.error(TargetUtils.getLocationMaybe(target),
+ TargetUtils.formatMissingEdge(target, label, e)));
+ super.missingEdge(target, label, e);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/query2/FakeSubincludeTarget.java b/src/main/java/com/google/devtools/build/lib/query2/FakeSubincludeTarget.java
new file mode 100644
index 0000000..f1a6469
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/query2/FakeSubincludeTarget.java
@@ -0,0 +1,86 @@
+// Copyright 2014 Google Inc. 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.query2;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.packages.ConstantRuleVisibility;
+import com.google.devtools.build.lib.packages.License;
+import com.google.devtools.build.lib.packages.Package;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.packages.RuleVisibility;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.syntax.Label;
+
+import java.util.Set;
+
+/**
+ * A fake Target - Use only so that "blaze query" can report subincluded files as Targets.
+ */
+public class FakeSubincludeTarget implements Target {
+
+ private final Label label;
+ private final Location location;
+
+ FakeSubincludeTarget(Label label, Location location) {
+ this.label = Preconditions.checkNotNull(label);
+ this.location = Preconditions.checkNotNull(location);
+ }
+
+ @Override
+ public Label getLabel() {
+ return label;
+ }
+
+ @Override
+ public String getName() {
+ return label.getName();
+ }
+
+ @Override
+ public Package getPackage() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String getTargetKind() {
+ return "source file";
+ }
+
+ @Override
+ public Rule getAssociatedRule() {
+ return null;
+ }
+
+ @Override
+ public License getLicense() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Location getLocation() {
+ return location;
+ }
+
+ @Override
+ public Set<License.DistributionType> getDistributions() {
+ return ImmutableSet.of();
+ }
+
+ @Override
+ public RuleVisibility getVisibility() {
+ return ConstantRuleVisibility.PUBLIC;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/query2/LabelVisitor.java b/src/main/java/com/google/devtools/build/lib/query2/LabelVisitor.java
new file mode 100644
index 0000000..b72e3aa
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/query2/LabelVisitor.java
@@ -0,0 +1,481 @@
+// Copyright 2014 Google Inc. 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.query2;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Throwables;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.MapMaker;
+import com.google.common.collect.Multimaps;
+import com.google.common.collect.SetMultimap;
+import com.google.devtools.build.lib.concurrent.AbstractQueueVisitor;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.packages.AggregatingAttributeMapper;
+import com.google.devtools.build.lib.packages.Attribute;
+import com.google.devtools.build.lib.packages.AttributeMap;
+import com.google.devtools.build.lib.packages.InputFile;
+import com.google.devtools.build.lib.packages.NoSuchThingException;
+import com.google.devtools.build.lib.packages.OutputFile;
+import com.google.devtools.build.lib.packages.Package;
+import com.google.devtools.build.lib.packages.PackageGroup;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.pkgcache.PackageProvider;
+import com.google.devtools.build.lib.pkgcache.TargetEdgeObserver;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.util.BinaryPredicate;
+
+import java.util.Collection;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * <p>Visit the transitive closure of a label. Primarily used to "fault in"
+ * packages to the packageProvider and ensure the necessary targets exists, in
+ * advance of the configuration step, which is intolerant of missing
+ * packages/targets.
+ *
+ * <p>LabelVisitor loads packages concurrently where possible, to increase I/O
+ * parallelism. However, the public interface is not thread-safe: calls to
+ * public methods should not be made concurrently.
+ *
+ * <p>LabelVisitor is stateful: It remembers the previous visitation and can
+ * check its validity on subsequent calls to sync() instead of doing the normal
+ * visitation.
+ *
+ * <p>TODO(bazel-team): (2009) a small further optimization could be achieved if we
+ * create tasks at the package (not individual label) level, since package
+ * loading is the expensive step. This would require additional bookkeeping to
+ * maintain the list of labels that we need to visit once a package becomes
+ * available. Profiling suggests that there is still a potential benefit to be
+ * gained: when the set of packages is known a-priori, loading a set of packages
+ * that took 20 seconds can be done under 5 in the sequential case or 7 in the
+ * current (parallel) case.
+ *
+ * <h4>Concurrency</h4>
+ *
+ * <p>The sync() methods of this class is thread-compatible. The accessor
+ * ({@link #hasVisited} and similar must not be called until the concurrent phase
+ * is over, i.e. all external calls to visit() methods have completed.
+ */
+final class LabelVisitor {
+
+ /**
+ * Attributes of a visitation which determine whether it is up-to-date or not.
+ */
+ private class VisitationAttributes {
+ private Collection<Target> targetsToVisit;
+ private boolean success = false;
+ private boolean visitSubincludes = true;
+ private int maxDepth = 0;
+
+ /**
+ * Returns true if and only if this visitation attribute is still up-to-date.
+ */
+ boolean current() {
+ return targetsToVisit.equals(lastVisitation.targetsToVisit)
+ && maxDepth <= lastVisitation.maxDepth
+ && visitSubincludes == lastVisitation.visitSubincludes;
+ }
+ }
+
+ /*
+ * Interrupts during the loading phase ===================================
+ *
+ * Bazel can be interrupted in the middle of the loading phase. The mechanics
+ * of this are far from trivial, so there is an explanation of how they are
+ * supposed to work. For a description how the same thing works in the
+ * execution phase, see ParallelBuilder.java .
+ *
+ * The sequence of events that happen when the user presses Ctrl-C is the
+ * following:
+ *
+ * 1. A SIGINT gets delivered to the Bazel client process.
+ *
+ * 2. The client process delivers the SIGINT to the server process.
+ *
+ * 3. The interruption state of the main thread is set to true.
+ *
+ * 4. Sooner or later, this results in an InterruptedException being thrown.
+ * Usually this takes place because the main thread is interrupted during
+ * AbstractQueueVisitor.awaitTermination(). The only exception to this is when
+ * the interruption occurs during the loading of a package of a label
+ * specified on the command line; in this case, the InterruptedException is
+ * thrown during the loading of an individual package (see below where this
+ * can occur)
+ *
+ * 5. The main thread calls ThreadPoolExecutor.shutdown(), which in turn
+ * interrupts every worker thread. Then the main thread waits for their
+ * termination.
+ *
+ * 6. An InterruptedException is thrown during the loading of an individual
+ * package in the worker threads.
+ *
+ * 7. All worker threads terminate.
+ *
+ * 8. An InterruptedException is thrown from
+ * AbstractQueueVisitor.awaitTermination()
+ *
+ * 9. This exception causes the execution of the currently running command to
+ * terminate prematurely.
+ *
+ * The interruption of the loading of an individual package can happen in two
+ * different ways depending on whether Python preprocessing is in effect or
+ * not.
+ *
+ * If there is no Python preprocessing:
+ *
+ * 1. We periodically check the interruption state of the thread in
+ * UnixGlob.reallyGlob(). If it is interrupted, an InterruptedException is
+ * thrown.
+ *
+ * 2. The stack is unwound until we are out of the part of the call stack
+ * responsible for package loading. This either means that the worker thread
+ * terminates or that the label parsing terminates if the package that is
+ * being loaded was specified on the command line.
+ *
+ * If there is Python preprocessing, events are a bit more complicated. In
+ * this case, the real work happens on the thread the Python preprocessor is
+ * called from, but in a bit more convoluted way: a new thread is spawned by
+ * to handle the input from the Python process and
+ * the output to the Python process is handled on the main thread. The reading
+ * thread parses requests from the preprocessor, and passes them using a queue
+ * to the writing thread (that is, the main thread), so that we can do the
+ * work there. This is important because this way, we don't have any work that
+ * we need to interrupt in a thread that is not spawned by us. So:
+ *
+ * 1. The interrupted state of the main thread is set.
+ *
+ * 2. This results in an InterruptedException during the execution of the task
+ * in PythonStdinInputStream.getNextMessage().
+ *
+ * 3. We exit from RequestParser.Request.run() prematurely, set a flag to
+ * signal that we were interrupted, and throw an InterruptedIOException.
+ *
+ * 4. The Python child process and reading thread are terminated.
+ *
+ * 5. Based on the flag we set in step 3, we realize that the termination was
+ * due to an interruption, and an InterruptedException is thrown. This can
+ * either raise an AbnormalTerminationException, or make Command.execute()
+ * return normally, so we check for both cases.
+ *
+ * 6. This InterruptedException causes the loading of the package to terminate
+ * prematurely.
+ *
+ * Life is not simple.
+ */
+ private final PackageProvider packageProvider;
+ private final BinaryPredicate<Rule, Attribute> edgeFilter;
+ private final SetMultimap<Package, Target> visitedMap =
+ Multimaps.synchronizedSetMultimap(HashMultimap.<Package, Target>create());
+ private final ConcurrentMap<Label, Integer> visitedTargets = new MapMaker().makeMap();
+
+ private VisitationAttributes lastVisitation;
+
+ /**
+ * Constant for limiting the permitted depth of recursion.
+ */
+ private static final int RECURSION_LIMIT = 100;
+
+ /**
+ * Construct a LabelVisitor.
+ *
+ * @param packageProvider how to resolve labels to targets.
+ * @param edgeFilter which edges may be traversed.
+ */
+ public LabelVisitor(PackageProvider packageProvider,
+ BinaryPredicate<Rule, Attribute> edgeFilter) {
+ this.packageProvider = packageProvider;
+ this.lastVisitation = new VisitationAttributes();
+ this.edgeFilter = edgeFilter;
+ }
+
+ boolean syncWithVisitor(EventHandler eventHandler, Collection<Target> targetsToVisit,
+ boolean keepGoing, int parallelThreads, int maxDepth, TargetEdgeObserver... observers)
+ throws InterruptedException {
+ VisitationAttributes nextVisitation = new VisitationAttributes();
+ nextVisitation.targetsToVisit = targetsToVisit;
+ nextVisitation.maxDepth = maxDepth;
+
+ if (!lastVisitation.success || !nextVisitation.current()) {
+ try {
+ nextVisitation.success = redoVisitation(eventHandler, nextVisitation, keepGoing,
+ parallelThreads, maxDepth, observers);
+ return nextVisitation.success;
+ } finally {
+ lastVisitation = nextVisitation;
+ }
+ } else {
+ return true;
+ }
+ }
+
+ // Does a bounded transitive visitation starting at the given top-level targets.
+ private boolean redoVisitation(EventHandler eventHandler,
+ VisitationAttributes visitation,
+ boolean keepGoing,
+ int parallelThreads,
+ int maxDepth,
+ TargetEdgeObserver... observers)
+ throws InterruptedException {
+ visitedMap.clear();
+ visitedTargets.clear();
+
+ Visitor visitor = new Visitor(eventHandler, keepGoing, parallelThreads, maxDepth, observers);
+
+ Throwable uncaught = null;
+ boolean result;
+ try {
+ visitor.visitTargets(visitation.targetsToVisit);
+ } catch (Throwable t) {
+ visitor.stopNewActions();
+ uncaught = t;
+ } finally {
+ // Run finish() in finally block to ensure we don't leak threads on exceptions.
+ result = visitor.finish();
+ }
+ Throwables.propagateIfPossible(uncaught);
+ return result;
+ }
+
+ boolean hasVisited(Label target) {
+ return visitedTargets.containsKey(target);
+ }
+
+ @VisibleForTesting class Visitor extends AbstractQueueVisitor {
+
+ private final static String THREAD_NAME = "LabelVisitor";
+
+ private final EventHandler eventHandler;
+ private final boolean keepGoing;
+ private final int maxDepth;
+ private final Iterable<TargetEdgeObserver> observers;
+ private final TargetEdgeErrorObserver errorObserver;
+ private final AtomicBoolean stopNewActions = new AtomicBoolean(false);
+ private static final boolean CONCURRENT = true;
+
+
+ public Visitor(EventHandler eventHandler, boolean keepGoing, int parallelThreads,
+ int maxDepth, TargetEdgeObserver... observers) {
+ // Observing the loading phase of a typical large package (with all subpackages) shows
+ // maximum thread-level concurrency of ~20. Limiting the total number of threads to 200 is
+ // therefore conservative and should help us avoid hitting native limits.
+ super(CONCURRENT, parallelThreads, parallelThreads, 1L, TimeUnit.SECONDS, !keepGoing,
+ THREAD_NAME);
+ this.eventHandler = eventHandler;
+ this.maxDepth = maxDepth;
+ this.errorObserver = new TargetEdgeErrorObserver();
+ ImmutableList.Builder<TargetEdgeObserver> builder = ImmutableList.builder();
+ for (TargetEdgeObserver observer : observers) {
+ builder.add(observer);
+ }
+ builder.add(errorObserver);
+ this.observers = builder.build();
+ this.keepGoing = keepGoing;
+ }
+
+ /**
+ * Visit the specified labels and follow the transitive closure of their
+ * outbound dependencies.
+ *
+ * @param targets the targets to visit
+ */
+ @ThreadSafe
+ public void visitTargets(Iterable<Target> targets) {
+ for (Target target : targets) {
+ visit(null, null, target, 0, 0);
+ }
+ }
+
+ @ThreadSafe
+ public boolean finish() throws InterruptedException {
+ work(true);
+ return !errorObserver.hasErrors();
+ }
+
+ @Override
+ protected boolean blockNewActions() {
+ return (!keepGoing && errorObserver.hasErrors()) || super.blockNewActions() ||
+ stopNewActions.get();
+ }
+
+ public void stopNewActions() {
+ stopNewActions.set(true);
+ }
+
+ private void enqueueTarget(
+ final Target from, final Attribute attr, final Label label, final int depth,
+ final int count) {
+ // Don't perform the targetProvider lookup if at the maximum depth already.
+ if (depth >= maxDepth) {
+ return;
+ } else if (attr != null && from instanceof Rule) {
+ if (!edgeFilter.apply((Rule) from, attr)) {
+ return;
+ }
+ }
+
+ // Avoid thread-related overhead when not crossing packages.
+ // Can start a new thread when count reaches 100, to prevent infinite recursion.
+ if (from != null && from.getLabel().getPackageFragment() == label.getPackageFragment() &&
+ !blockNewActions() && count < RECURSION_LIMIT) {
+ newVisitRunnable(from, attr, label, depth, count + 1).run();
+ } else {
+ enqueue(newVisitRunnable(from, attr, label, depth, 0));
+ }
+ }
+
+ private Runnable newVisitRunnable(final Target from, final Attribute attr, final Label label,
+ final int depth, final int count) {
+ return new Runnable () {
+ @Override
+ public void run() {
+ try {
+ Target target = packageProvider.getTarget(eventHandler, label);
+ if (target == null) {
+ // Let target visitation continue so we can discover additional unknown inputs.
+ return;
+ }
+ visit(from, attr, packageProvider.getTarget(eventHandler, label), depth + 1, count);
+ } catch (NoSuchThingException e) {
+ observeError(from, label, e);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ };
+ }
+
+ private void visitTargetVisibility(Target target, int depth, int count) {
+ Attribute attribute = null;
+ if (target instanceof Rule) {
+ attribute = ((Rule) target).getRuleClassObject().getAttributeByName("visibility");
+ }
+
+ for (Label label : target.getVisibility().getDependencyLabels()) {
+ enqueueTarget(target, attribute, label, depth, count);
+ }
+ }
+
+ /**
+ * Visit all the labels in a given rule.
+ *
+ * <p>Called in a worker thread if CONCURRENT.
+ *
+ * @param rule the rule to visit
+ */
+ @ThreadSafe
+ private void visitRule(final Rule rule, final int depth, final int count) {
+ // Follow all labels defined by this rule:
+ AggregatingAttributeMapper.of(rule).visitLabels(new AttributeMap.AcceptsLabelAttribute() {
+ @Override
+ public void acceptLabelAttribute(Label label, Attribute attribute) {
+ enqueueTarget(rule, attribute, label, depth, count);
+ }
+ });
+ }
+
+ @ThreadSafe
+ private void visitPackageGroup(PackageGroup packageGroup, int depth, int count) {
+ for (final Label include : packageGroup.getIncludes()) {
+ enqueueTarget(packageGroup, null, include, depth, count);
+ }
+ }
+
+ /**
+ * Visits the target and its package.
+ *
+ * <p>Potentially blocking invocations into the package cache are
+ * enqueued in the worker pool if CONCURRENT.
+ */
+ private void visit(
+ Target from, Attribute attribute, final Target target, int depth, int count) {
+ if (depth > maxDepth) {
+ return;
+ }
+
+ if (from != null) {
+ observeEdge(from, attribute, target);
+ }
+
+ visitedMap.put(target.getPackage(), target);
+ visitTargetNode(target, depth, count);
+ }
+
+ /**
+ * Visit the specified target.
+ * Called in a worker thread if CONCURRENT.
+ *
+ * @param target the target to visit
+ */
+ private void visitTargetNode(Target target, int depth, int count) {
+ Integer minTargetDepth = visitedTargets.putIfAbsent(target.getLabel(), depth);
+ if (minTargetDepth != null) {
+ // The target was already visited at a greater depth.
+ // The closure we are about to build is therefore a subset of what
+ // has already been built, and we can skip it.
+ // Also special case MAX_VALUE, where we never want to revisit targets.
+ // (This avoids loading phase overhead outside of queries).
+ if (maxDepth == Integer.MAX_VALUE || minTargetDepth <= depth) {
+ return;
+ }
+ // Check again in case it was overwritten by another thread.
+ synchronized (visitedTargets) {
+ if (visitedTargets.get(target.getLabel()) <= depth) {
+ return;
+ }
+ visitedTargets.put(target.getLabel(), depth);
+ }
+ }
+
+ observeNode(target);
+ if (target instanceof OutputFile) {
+ Rule rule = ((OutputFile) target).getGeneratingRule();
+ observeEdge(target, null, rule);
+ // This is the only recursive call to visit which doesn't pass through enqueueTarget().
+ visit(null, null, rule, depth + 1, count + 1);
+ visitTargetVisibility(target, depth, count);
+ } else if (target instanceof InputFile) {
+ visitTargetVisibility(target, depth, count);
+ } else if (target instanceof Rule) {
+ visitTargetVisibility(target, depth, count);
+ visitRule((Rule) target, depth, count);
+ } else if (target instanceof PackageGroup) {
+ visitPackageGroup((PackageGroup) target, depth, count);
+ }
+ }
+
+ private void observeEdge(Target from, Attribute attribute, Target to) {
+ for (TargetEdgeObserver observer : observers) {
+ observer.edge(from, attribute, to);
+ }
+ }
+
+ private void observeNode(Target target) {
+ for (TargetEdgeObserver observer : observers) {
+ observer.node(target);
+ }
+ }
+
+ private void observeError(Target from, Label label, NoSuchThingException e) {
+ for (TargetEdgeObserver observer : observers) {
+ observer.missingEdge(from, label, e);
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/query2/SkyframeQueryEnvironment.java b/src/main/java/com/google/devtools/build/lib/query2/SkyframeQueryEnvironment.java
new file mode 100644
index 0000000..318d844
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/query2/SkyframeQueryEnvironment.java
@@ -0,0 +1,58 @@
+// Copyright 2014 Google Inc. 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.query2;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.pkgcache.PackageProvider;
+import com.google.devtools.build.lib.pkgcache.TargetPatternEvaluator;
+import com.google.devtools.build.lib.pkgcache.TransitivePackageLoader;
+import com.google.devtools.build.lib.query2.engine.QueryException;
+import com.google.devtools.build.lib.syntax.Label;
+
+import java.util.Set;
+
+/**
+ * A BlazeQueryEnvironment for Skyframe builds. Currently, this is used to preload transitive
+ * closures of targets quickly.
+ */
+public final class SkyframeQueryEnvironment extends BlazeQueryEnvironment {
+
+ private final TransitivePackageLoader transitivePackageLoader;
+ private static final int MAX_DEPTH_FULL_SCAN_LIMIT = 20;
+
+ public SkyframeQueryEnvironment(
+ TransitivePackageLoader transitivePackageLoader, PackageProvider packageProvider,
+ TargetPatternEvaluator targetPatternEvaluator, boolean keepGoing, int loadingPhaseThreads,
+ EventHandler eventHandler, Set<Setting> settings, Iterable<QueryFunction> functions) {
+ super(packageProvider, targetPatternEvaluator, keepGoing, loadingPhaseThreads, eventHandler,
+ settings, functions);
+ this.transitivePackageLoader = transitivePackageLoader;
+ }
+
+ @Override
+ protected void preloadTransitiveClosure(Set<Target> targets, int maxDepth) throws QueryException {
+ if (maxDepth >= MAX_DEPTH_FULL_SCAN_LIMIT) {
+ // Only do the full visitation if "maxDepth" is large enough. Otherwise, the benefits of
+ // preloading will be outweighed by the cost of doing more work than necessary.
+ try {
+ transitivePackageLoader.sync(eventHandler, targets, ImmutableSet.<Label>of(), keepGoing,
+ loadingPhaseThreads, -1);
+ } catch (InterruptedException e) {
+ throw new QueryException("interrupted");
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/query2/TargetEdgeErrorObserver.java b/src/main/java/com/google/devtools/build/lib/query2/TargetEdgeErrorObserver.java
new file mode 100644
index 0000000..5a075d2
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/query2/TargetEdgeErrorObserver.java
@@ -0,0 +1,83 @@
+// Copyright 2014 Google Inc. 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.query2;
+
+import com.google.devtools.build.lib.concurrent.ThreadSafety;
+import com.google.devtools.build.lib.packages.Attribute;
+import com.google.devtools.build.lib.packages.NoSuchThingException;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.pkgcache.TargetEdgeObserver;
+import com.google.devtools.build.lib.syntax.Label;
+
+/**
+ * Record errors, such as missing package/target or rules containing errors,
+ * encountered during visitation. Emit an error message upon encountering
+ * missing edges
+ *
+ * The accessor {@link #hasErrors}) may not be called until the concurrent phase
+ * is over, i.e. all external calls to visit() methods have completed.
+ *
+ * If you need to report errors to the console during visitation, use the
+ * subclass {@link com.google.devtools.build.lib.query2.ErrorPrintingTargetEdgeErrorObserver}.
+ */
+class TargetEdgeErrorObserver implements TargetEdgeObserver {
+
+ /**
+ * True iff errors were encountered. Note, may be set to "true" during the
+ * concurrent phase. Volatile, because it is assigned by worker threads and
+ * read by the main thread without monitor synchronization.
+ */
+ private volatile boolean hasErrors = false;
+
+ /**
+ * Reports an unresolved label error and records the fact that an error was
+ * encountered.
+ * @param target the target that referenced the unresolved label
+ * @param label the label that could not be resolved
+ * @param e the exception that was thrown when the label could not be resolved
+ */
+ @ThreadSafety.ThreadSafe
+ @Override
+ public void missingEdge(Target target, Label label, NoSuchThingException e) {
+ hasErrors = true;
+ }
+
+ /**
+ * Returns true iff any errors (such as missing targets or packages, or rules
+ * with errors) have been encountered during any work.
+ *
+ * <p>Not thread-safe; do not call during visitation.
+ *
+ * @return true iff no errors (such as missing targets or packages, or rules
+ * with errors) have been encountered during any work.
+ */
+ public boolean hasErrors() {
+ return hasErrors;
+ }
+
+ @Override
+ public void edge(Target from, Attribute attribute, Target to) {
+ // No-op.
+ }
+
+ @Override
+ public void node(Target node) {
+ if (node.getPackage().containsErrors() ||
+ ((node instanceof Rule) && ((Rule) node).containsErrors())) {
+ this.hasErrors = true; // Note, this is thread-safe.
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/query2/engine/AllPathsFunction.java b/src/main/java/com/google/devtools/build/lib/query2/engine/AllPathsFunction.java
new file mode 100644
index 0000000..d2d5f05
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/query2/engine/AllPathsFunction.java
@@ -0,0 +1,96 @@
+// Copyright 2014 Google Inc. 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.query2.engine;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Sets;
+import com.google.devtools.build.lib.query2.engine.QueryEnvironment.Argument;
+import com.google.devtools.build.lib.query2.engine.QueryEnvironment.ArgumentType;
+import com.google.devtools.build.lib.query2.engine.QueryEnvironment.QueryFunction;
+
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Implementation of the <code>allpaths()</code> function.
+ */
+public class AllPathsFunction implements QueryFunction {
+ AllPathsFunction() {
+ }
+
+ @Override
+ public String getName() {
+ return "allpaths";
+ }
+
+ @Override
+ public int getMandatoryArguments() {
+ return 2;
+ }
+
+ @Override
+ public List<ArgumentType> getArgumentTypes() {
+ return ImmutableList.of(ArgumentType.EXPRESSION, ArgumentType.EXPRESSION);
+ }
+
+ @Override
+ public <T> Set<T> eval(QueryEnvironment<T> env, QueryExpression expression, List<Argument> args)
+ throws QueryException {
+ QueryExpression from = args.get(0).getExpression();
+ QueryExpression to = args.get(1).getExpression();
+
+ Set<T> fromValue = from.eval(env);
+ Set<T> toValue = to.eval(env);
+
+ // Algorithm: compute "reachableFromX", the forward transitive closure of
+ // the "from" set, then find the intersection of "reachableFromX" with the
+ // reverse transitive closure of the "to" set. The reverse transitive
+ // closure and intersection operations are interleaved for efficiency.
+ // "result" holds the intersection.
+
+ env.buildTransitiveClosure(expression, fromValue, Integer.MAX_VALUE);
+
+ Set<T> reachableFromX = env.getTransitiveClosure(fromValue);
+ Set<T> result = intersection(reachableFromX, toValue);
+ LinkedList<T> worklist = new LinkedList<>(result);
+
+ T n;
+ while ((n = worklist.poll()) != null) {
+ for (T np : env.getReverseDeps(n)) {
+ if (reachableFromX.contains(np)) {
+ if (result.add(np)) {
+ worklist.add(np);
+ }
+ }
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Returns a (new, mutable, unordered) set containing the intersection of the
+ * two specified sets.
+ */
+ private static <T> Set<T> intersection(Set<T> x, Set<T> y) {
+ Set<T> result = new HashSet<>();
+ if (x.size() > y.size()) {
+ Sets.intersection(y, x).copyInto(result);
+ } else {
+ Sets.intersection(x, y).copyInto(result);
+ }
+ return result;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/query2/engine/AttrFunction.java b/src/main/java/com/google/devtools/build/lib/query2/engine/AttrFunction.java
new file mode 100644
index 0000000..054cf78b
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/query2/engine/AttrFunction.java
@@ -0,0 +1,78 @@
+// Copyright 2014 Google Inc. 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.query2.engine;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.query2.engine.QueryEnvironment.Argument;
+import com.google.devtools.build.lib.query2.engine.QueryEnvironment.ArgumentType;
+
+import java.util.List;
+
+/**
+ * An attr(attribute, pattern, argument) filter expression, which computes
+ * the set of subset of nodes in 'argument' which correspond to rules with
+ * defined attribute 'attribute' with attribute value matching the unanchored
+ * regexp 'pattern'. For list attributes, the attribute value will be defined as
+ * a usual List.toString() representation (using '[' as first character, ']' as
+ * last character and ", " as a delimiter between multiple values). Also, all
+ * label-based attributes will use fully-qualified label names instead of
+ * original value specified in the BUILD file.
+ *
+ * <pre>expr ::= ATTR '(' ATTRNAME ',' WORD ',' expr ')'</pre>
+ *
+ * Examples
+ * <pre>
+ * attr(linkshared,1,//project/...) find all rules under in the //project/... that
+ * have attribute linkshared set to 1.
+ * </pre>
+ */
+class AttrFunction extends RegexFilterExpression {
+ AttrFunction() {
+ }
+
+ @Override
+ public String getName() {
+ return "attr";
+ }
+
+ @Override
+ protected String getPattern(List<Argument> args) {
+ return args.get(1).getWord();
+ }
+
+ @Override
+ public int getMandatoryArguments() {
+ return 3;
+ }
+
+ @Override
+ public List<ArgumentType> getArgumentTypes() {
+ return ImmutableList.of(ArgumentType.WORD, ArgumentType.WORD, ArgumentType.EXPRESSION);
+ }
+
+ @Override
+ protected <T> String getFilterString(QueryEnvironment<T> env, List<Argument> args, T target) {
+ throw new IllegalStateException(
+ "The 'attr' regex filter gets its match values directly from getFilterStrings");
+ }
+
+ @Override
+ protected <T> Iterable<String> getFilterStrings(QueryEnvironment<T> env,
+ List<Argument> args, T target) {
+ if (env.getAccessor().isRule(target)) {
+ return env.getAccessor().getAttrAsString(target, args.get(0).getWord());
+ }
+ return ImmutableList.of();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/query2/engine/BinaryOperatorExpression.java b/src/main/java/com/google/devtools/build/lib/query2/engine/BinaryOperatorExpression.java
new file mode 100644
index 0000000..e5e600f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/query2/engine/BinaryOperatorExpression.java
@@ -0,0 +1,93 @@
+// Copyright 2014 Google Inc. 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.query2.engine;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+
+import java.util.Collection;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A binary algebraic set operation.
+ *
+ * <pre>
+ * expr ::= expr (INTERSECT expr)+
+ * | expr ('^' expr)+
+ * | expr (UNION expr)+
+ * | expr ('+' expr)+
+ * | expr (EXCEPT expr)+
+ * | expr ('-' expr)+
+ * </pre>
+ */
+class BinaryOperatorExpression extends QueryExpression {
+
+ private final Lexer.TokenKind operator; // ::= INTERSECT/CARET | UNION/PLUS | EXCEPT/MINUS
+ private final ImmutableList<QueryExpression> operands;
+
+ BinaryOperatorExpression(Lexer.TokenKind operator,
+ List<QueryExpression> operands) {
+ Preconditions.checkState(operands.size() > 1);
+ this.operator = operator;
+ this.operands = ImmutableList.copyOf(operands);
+ }
+
+ @Override
+ public <T> Set<T> eval(QueryEnvironment<T> env) throws QueryException {
+ Set<T> lhsValue = new LinkedHashSet<>(operands.get(0).eval(env));
+
+ for (int i = 1; i < operands.size(); i++) {
+ Set<T> rhsValue = operands.get(i).eval(env);
+ switch (operator) {
+ case INTERSECT:
+ case CARET:
+ lhsValue.retainAll(rhsValue);
+ break;
+ case UNION:
+ case PLUS:
+ lhsValue.addAll(rhsValue);
+ break;
+ case EXCEPT:
+ case MINUS:
+ lhsValue.removeAll(rhsValue);
+ break;
+ default:
+ throw new IllegalStateException("operator=" + operator);
+ }
+ }
+ return lhsValue;
+ }
+
+ @Override
+ public void collectTargetPatterns(Collection<String> literals) {
+ for (QueryExpression subExpression : operands) {
+ subExpression.collectTargetPatterns(literals);
+ }
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder result = new StringBuilder();
+ for (int i = 1; i < operands.size(); i++) {
+ result.append("(");
+ }
+ result.append(operands.get(0));
+ for (int i = 1; i < operands.size(); i++) {
+ result.append(" " + operator.getPrettyName() + " " + operands.get(i) + ")");
+ }
+ return result.toString();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/query2/engine/BlazeQueryEvalResult.java b/src/main/java/com/google/devtools/build/lib/query2/engine/BlazeQueryEvalResult.java
new file mode 100644
index 0000000..7f2060f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/query2/engine/BlazeQueryEvalResult.java
@@ -0,0 +1,36 @@
+// Copyright 2014 Google Inc. 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.query2.engine;
+
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.graph.Digraph;
+
+import java.util.Set;
+
+/** {@link QueryEvalResult} along with a digraph giving the structure of the results. */
+public class BlazeQueryEvalResult<T> extends QueryEvalResult<T> {
+
+ private final Digraph<T> graph;
+
+ public BlazeQueryEvalResult(boolean success, Set<T> resultSet, Digraph<T> graph) {
+ super(success, resultSet);
+ this.graph = Preconditions.checkNotNull(graph);
+ }
+
+ /** Returns the result as a directed graph over elements. */
+ public Digraph<T> getResultGraph() {
+ return graph.extractSubgraph(resultSet);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/query2/engine/BuildFilesFunction.java b/src/main/java/com/google/devtools/build/lib/query2/engine/BuildFilesFunction.java
new file mode 100644
index 0000000..d606a6a
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/query2/engine/BuildFilesFunction.java
@@ -0,0 +1,56 @@
+// Copyright 2014 Google Inc. 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.query2.engine;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.query2.engine.QueryEnvironment.Argument;
+import com.google.devtools.build.lib.query2.engine.QueryEnvironment.ArgumentType;
+import com.google.devtools.build.lib.query2.engine.QueryEnvironment.QueryFunction;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A buildfiles(x) query expression, which computes the set of BUILD files and
+ * subincluded files for each target in set x. The result is unordered. This
+ * operator is typically used for determinining what files or packages to check
+ * out.
+ *
+ * <pre>expr ::= BUILDFILES '(' expr ')'</pre>
+ */
+class BuildFilesFunction implements QueryFunction {
+ BuildFilesFunction() {
+ }
+
+ @Override
+ public String getName() {
+ return "buildfiles";
+ }
+
+ @Override
+ public <T> Set<T> eval(QueryEnvironment<T> env, QueryExpression expression, List<Argument> args)
+ throws QueryException {
+ return env.getBuildFiles(expression, args.get(0).getExpression().eval(env));
+ }
+
+ @Override
+ public int getMandatoryArguments() {
+ return 1;
+ }
+
+ @Override
+ public List<ArgumentType> getArgumentTypes() {
+ return ImmutableList.of(ArgumentType.EXPRESSION);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/query2/engine/DepsFunction.java b/src/main/java/com/google/devtools/build/lib/query2/engine/DepsFunction.java
new file mode 100644
index 0000000..2b693eb
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/query2/engine/DepsFunction.java
@@ -0,0 +1,88 @@
+// Copyright 2014 Google Inc. 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.query2.engine;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.query2.engine.QueryEnvironment.Argument;
+import com.google.devtools.build.lib.query2.engine.QueryEnvironment.ArgumentType;
+import com.google.devtools.build.lib.query2.engine.QueryEnvironment.QueryFunction;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A "deps" query expression, which computes the dependencies of the argument. An optional
+ * integer-literal second argument may be specified; its value bounds the search from the arguments.
+ *
+ * <pre>expr ::= DEPS '(' expr ')'</pre>
+ * <pre> | DEPS '(' expr ',' WORD ')'</pre>
+ */
+final class DepsFunction implements QueryFunction {
+ DepsFunction() {
+ }
+
+ @Override
+ public String getName() {
+ return "deps";
+ }
+
+ @Override
+ public int getMandatoryArguments() {
+ return 1; // last argument is optional
+ }
+
+ @Override
+ public List<ArgumentType> getArgumentTypes() {
+ return ImmutableList.of(ArgumentType.EXPRESSION, ArgumentType.INTEGER);
+ }
+
+ /**
+ * Breadth-first search from the arguments.
+ */
+ @Override
+ public <T> Set<T> eval(QueryEnvironment<T> env, QueryExpression expression, List<Argument> args)
+ throws QueryException {
+ Set<T> argumentValue = args.get(0).getExpression().eval(env);
+ int depthBound = args.size() > 1 ? args.get(1).getInteger() : Integer.MAX_VALUE;
+ env.buildTransitiveClosure(expression, argumentValue, depthBound);
+
+ Set<T> visited = new LinkedHashSet<>();
+ Collection<T> current = argumentValue;
+
+ // We need to iterate depthBound + 1 times.
+ for (int i = 0; i <= depthBound; i++) {
+ List<T> next = new ArrayList<>();
+ for (T node : current) {
+ if (!visited.add(node)) {
+ // Already visited; if we see a node in a later round, then we don't need to visit it
+ // again, because the depth at which we see it at must be greater than or equal to the
+ // last visit.
+ continue;
+ }
+
+ next.addAll(env.getFwdDeps(node));
+ }
+ if (next.isEmpty()) {
+ // Exit when there are no more nodes to visit.
+ break;
+ }
+ current = next;
+ }
+
+ return visited;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/query2/engine/FilterFunction.java b/src/main/java/com/google/devtools/build/lib/query2/engine/FilterFunction.java
new file mode 100644
index 0000000..bd89950
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/query2/engine/FilterFunction.java
@@ -0,0 +1,63 @@
+// Copyright 2014 Google Inc. 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.query2.engine;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.query2.engine.QueryEnvironment.Argument;
+import com.google.devtools.build.lib.query2.engine.QueryEnvironment.ArgumentType;
+
+import java.util.List;
+
+/**
+ * A label(pattern, argument) filter expression, which computes the set of subset
+ * of nodes in 'argument' whose label matches the unanchored regexp 'pattern'.
+ *
+ * <pre>expr ::= FILTER '(' WORD ',' expr ')'</pre>
+ *
+ * Example patterns:
+ * <pre>
+ * '//third_party' Match all targets in the //third_party/...
+ * (equivalent to 'intersect //third_party/...)
+ * '\.jar$' Match all *.jar targets.
+ * </pre>
+ */
+class FilterFunction extends RegexFilterExpression {
+ FilterFunction() {
+ }
+
+ @Override
+ public String getName() {
+ return "filter";
+ }
+
+ @Override
+ protected String getPattern(List<Argument> args) {
+ return args.get(0).getWord();
+ }
+
+ @Override
+ public int getMandatoryArguments() {
+ return 2;
+ }
+
+ @Override
+ public List<ArgumentType> getArgumentTypes() {
+ return ImmutableList.of(ArgumentType.WORD, ArgumentType.EXPRESSION);
+ }
+
+ @Override
+ protected <T> String getFilterString(QueryEnvironment<T> env, List<Argument> args, T target) {
+ return env.getAccessor().getLabel(target);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/query2/engine/FunctionExpression.java b/src/main/java/com/google/devtools/build/lib/query2/engine/FunctionExpression.java
new file mode 100644
index 0000000..62734fd
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/query2/engine/FunctionExpression.java
@@ -0,0 +1,59 @@
+// Copyright 2014 Google Inc. 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.query2.engine;
+
+import com.google.common.base.Functions;
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.query2.engine.QueryEnvironment.Argument;
+import com.google.devtools.build.lib.query2.engine.QueryEnvironment.ArgumentType;
+import com.google.devtools.build.lib.query2.engine.QueryEnvironment.QueryFunction;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A query expression for user-defined query functions.
+ */
+public class FunctionExpression extends QueryExpression {
+ QueryFunction function;
+ List<Argument> args;
+
+ public FunctionExpression(QueryFunction function, List<Argument> args) {
+ this.function = function;
+ this.args = ImmutableList.copyOf(args);
+ }
+
+ @Override
+ public <T> Set<T> eval(QueryEnvironment<T> env) throws QueryException {
+ return function.<T>eval(env, this, args);
+ }
+
+ @Override
+ public void collectTargetPatterns(Collection<String> literals) {
+ for (Argument arg : args) {
+ if (arg.getType() == ArgumentType.EXPRESSION) {
+ arg.getExpression().collectTargetPatterns(literals);
+ }
+ }
+ }
+
+ @Override
+ public String toString() {
+ return function.getName() +
+ "(" + Joiner.on(", ").join(Iterables.transform(args, Functions.toStringFunction())) + ")";
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/query2/engine/KindFunction.java b/src/main/java/com/google/devtools/build/lib/query2/engine/KindFunction.java
new file mode 100644
index 0000000..7ae80b8
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/query2/engine/KindFunction.java
@@ -0,0 +1,70 @@
+// Copyright 2014 Google Inc. 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.query2.engine;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.query2.engine.QueryEnvironment.Argument;
+import com.google.devtools.build.lib.query2.engine.QueryEnvironment.ArgumentType;
+
+import java.util.List;
+
+/**
+ * A kind(pattern, argument) filter expression, which computes the set of subset
+ * of nodes in 'argument' whose kind matches the unanchored regexp 'pattern'.
+ *
+ * <pre>expr ::= KIND '(' WORD ',' expr ')'</pre>
+ *
+ * Example patterns:
+ * <pre>
+ * ' file' Match all file targets.
+ * 'source file' Match all test source file targets.
+ * 'generated file' Match all test generated file targets.
+ * ' rule' Match all rule targets.
+ * 'foo_*' Match all rules starting with "foo_",
+ * 'test' Match all test (rule) targets.
+ * </pre>
+ *
+ * Note, the space before "file" is needed to prevent unwanted matches against
+ * (e.g.) "filegroup rule".
+ */
+class KindFunction extends RegexFilterExpression {
+
+ KindFunction() {
+ }
+
+ @Override
+ public String getName() {
+ return "kind";
+ }
+
+ @Override
+ protected String getPattern(List<Argument> args) {
+ return args.get(0).getWord();
+ }
+
+ @Override
+ public int getMandatoryArguments() {
+ return 2;
+ }
+
+ @Override
+ public List<ArgumentType> getArgumentTypes() {
+ return ImmutableList.of(ArgumentType.WORD, ArgumentType.EXPRESSION);
+ }
+
+ @Override
+ protected <T> String getFilterString(QueryEnvironment<T> env, List<Argument> args, T target) {
+ return env.getAccessor().getTargetKind(target);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/query2/engine/LabelsFunction.java b/src/main/java/com/google/devtools/build/lib/query2/engine/LabelsFunction.java
new file mode 100644
index 0000000..1093d85
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/query2/engine/LabelsFunction.java
@@ -0,0 +1,72 @@
+// Copyright 2014 Google Inc. 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.query2.engine;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.query2.engine.QueryEnvironment.Argument;
+import com.google.devtools.build.lib.query2.engine.QueryEnvironment.ArgumentType;
+import com.google.devtools.build.lib.query2.engine.QueryEnvironment.QueryFunction;
+
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A label(attr_name, argument) expression, which computes the set of targets
+ * whose labels appear in the specified attribute of some rule in 'argument'.
+ *
+ * <pre>expr ::= LABELS '(' WORD ',' expr ')'</pre>
+ *
+ * Example:
+ * <pre>
+ * labels(srcs, //foo) The 'srcs' source files to the //foo rule.
+ * </pre>
+ */
+class LabelsFunction implements QueryFunction {
+ LabelsFunction() {
+ }
+
+ @Override
+ public String getName() {
+ return "labels";
+ }
+
+ @Override
+ public int getMandatoryArguments() {
+ return 2;
+ }
+
+ @Override
+ public List<ArgumentType> getArgumentTypes() {
+ return ImmutableList.of(ArgumentType.WORD, ArgumentType.EXPRESSION);
+ }
+
+ @Override
+ public <T> Set<T> eval(QueryEnvironment<T> env, QueryExpression expression, List<Argument> args)
+ throws QueryException {
+ Set<T> inputs = args.get(1).getExpression().eval(env);
+ Set<T> result = new LinkedHashSet<>();
+ String attrName = args.get(0).getWord();
+ for (T input : inputs) {
+ if (env.getAccessor().isRule(input)) {
+ List<T> targets = env.getAccessor().getLabelListAttr(expression, input, attrName,
+ "in '" + attrName + "' of rule " + env.getAccessor().getLabel(input) + ": ");
+ for (T target : targets) {
+ result.add(env.getOrCreate(target));
+ }
+ }
+ }
+ return result;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/query2/engine/LetExpression.java b/src/main/java/com/google/devtools/build/lib/query2/engine/LetExpression.java
new file mode 100644
index 0000000..3e17cce
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/query2/engine/LetExpression.java
@@ -0,0 +1,78 @@
+// Copyright 2014 Google Inc. 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.query2.engine;
+
+import java.util.Collection;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+/**
+ * A let expression.
+ *
+ * <pre>expr ::= LET WORD = expr IN expr</pre>
+ */
+class LetExpression extends QueryExpression {
+
+ private static final String VAR_NAME_PATTERN = "[a-zA-Z_][a-zA-Z0-9_]*$";
+
+ // Variables names may be any legal identifier in the C programming language
+ private static final Pattern NAME_PATTERN = Pattern.compile("^" + VAR_NAME_PATTERN);
+
+ // Variable references are prepended with the "$" character.
+ // A variable named "x" is referenced as "$x".
+ private static final Pattern REF_PATTERN = Pattern.compile("^\\$" + VAR_NAME_PATTERN);
+
+ static boolean isValidVarReference(String varName) {
+ return REF_PATTERN.matcher(varName).matches();
+ }
+
+ static String getNameFromReference(String reference) {
+ return reference.substring(1);
+ }
+
+ private final String varName;
+ private final QueryExpression varExpr;
+ private final QueryExpression bodyExpr;
+
+ LetExpression(String varName, QueryExpression varExpr, QueryExpression bodyExpr) {
+ this.varName = varName;
+ this.varExpr = varExpr;
+ this.bodyExpr = bodyExpr;
+ }
+
+ @Override
+ public <T> Set<T> eval(QueryEnvironment<T> env) throws QueryException {
+ if (!NAME_PATTERN.matcher(varName).matches()) {
+ throw new QueryException(this, "invalid variable name '" + varName + "' in let expression");
+ }
+ Set<T> varValue = varExpr.eval(env);
+ Set<T> prevValue = env.setVariable(varName, varValue);
+ try {
+ return bodyExpr.eval(env);
+ } finally {
+ env.setVariable(varName, prevValue); // restore
+ }
+ }
+
+ @Override
+ public void collectTargetPatterns(Collection<String> literals) {
+ varExpr.collectTargetPatterns(literals);
+ bodyExpr.collectTargetPatterns(literals);
+ }
+
+ @Override
+ public String toString() {
+ return "let " + varName + " = " + varExpr + " in " + bodyExpr;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/query2/engine/Lexer.java b/src/main/java/com/google/devtools/build/lib/query2/engine/Lexer.java
new file mode 100644
index 0000000..45b6f61
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/query2/engine/Lexer.java
@@ -0,0 +1,281 @@
+// Copyright 2014 Google Inc. 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.query2.engine;
+
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A tokenizer for the Blaze query language, revision 2.
+ *
+ * Note, we can avoid a lot of quoting by noting that the characters [() ,] do
+ * not appear in any label, filename, function name, or regular expression we care about.
+ *
+ * No string escapes are allowed ("\"). Given the domain, that's not currently
+ * a problem.
+ */
+final class Lexer {
+
+ /**
+ * Discriminator for different kinds of tokens.
+ */
+ public enum TokenKind {
+ WORD("word"),
+ EOF("EOF"),
+
+ COMMA(","),
+ EQUALS("="),
+ LPAREN("("),
+ MINUS("-"),
+ PLUS("+"),
+ RPAREN(")"),
+ CARET("^"),
+
+ __ALL_IDENTIFIERS_FOLLOW(""), // See below
+
+ IN("in"),
+ LET("let"),
+ SET("set"),
+
+ INTERSECT("intersect"),
+ EXCEPT("except"),
+ UNION("union");
+
+ private final String prettyName;
+
+ private TokenKind(String prettyName) {
+ this.prettyName = prettyName;
+ }
+
+ public String getPrettyName() {
+ return prettyName;
+ }
+ }
+
+ public static final Set<TokenKind> BINARY_OPERATORS = EnumSet.of(
+ TokenKind.INTERSECT,
+ TokenKind.CARET,
+ TokenKind.UNION,
+ TokenKind.PLUS,
+ TokenKind.EXCEPT,
+ TokenKind.MINUS);
+
+ private static final Map<String, TokenKind> keywordMap = new HashMap<>();
+ static {
+ for (TokenKind kind : EnumSet.allOf(TokenKind.class)) {
+ if (kind.ordinal() > TokenKind.__ALL_IDENTIFIERS_FOLLOW.ordinal()) {
+ keywordMap.put(kind.getPrettyName(), kind);
+ }
+ }
+ }
+
+ /**
+ * Returns true iff 'word' is a reserved word of the language.
+ */
+ static boolean isReservedWord(String word) {
+ return keywordMap.containsKey(word);
+ }
+
+ /**
+ * Tokens returned by the Lexer.
+ */
+ static class Token {
+
+ public final TokenKind kind;
+ public final String word;
+
+ Token(TokenKind kind) {
+ this.kind = kind;
+ this.word = null;
+ }
+
+ Token(String word) {
+ this.kind = TokenKind.WORD;
+ this.word = word;
+ }
+
+ @Override
+ public String toString() {
+ return kind == TokenKind.WORD ? word : kind.getPrettyName();
+ }
+ }
+
+ /**
+ * Entry point to the lexer. Returns the list of tokens for the specified
+ * input, or throws QueryException.
+ */
+ public static List<Token> scan(char[] buffer) throws QueryException {
+ Lexer lexer = new Lexer(buffer);
+ lexer.tokenize();
+ return lexer.tokens;
+ }
+
+ // Input buffer and position
+ private char[] buffer;
+ private int pos;
+
+ private final List<Token> tokens = new ArrayList<>();
+
+ private Lexer(char[] buffer) {
+ this.buffer = buffer;
+ this.pos = 0;
+ }
+
+ private void addToken(Token s) {
+ tokens.add(s);
+ }
+
+ /**
+ * Scans a quoted word delimited by 'quot'.
+ *
+ * ON ENTRY: 'pos' is 1 + the index of the first delimiter
+ * ON EXIT: 'pos' is 1 + the index of the last delimiter.
+ *
+ * @return the word token.
+ */
+ private Token quotedWord(char quot) throws QueryException {
+ int oldPos = pos - 1;
+ while (pos < buffer.length) {
+ char c = buffer[pos++];
+ switch (c) {
+ case '\'':
+ case '"':
+ if (c == quot) {
+ // close-quote, all done.
+ return new Token(bufferSlice(oldPos + 1, pos - 1));
+ }
+ }
+ }
+ throw new QueryException("unclosed quotation");
+ }
+
+ private TokenKind getTokenKindForWord(String word) {
+ TokenKind kind = keywordMap.get(word);
+ return kind == null ? TokenKind.WORD : kind;
+ }
+
+ // Unquoted words may contain [-*$], but not start with them. For user convenience, unquoted
+ // words must include UNIX filenames, labels and target label patterns, and simple regexps
+ // (e.g. cc_.*). Keep consistent with TargetLiteral.toString()!
+ private String scanWord() {
+ int oldPos = pos - 1;
+ while (pos < buffer.length) {
+ switch (buffer[pos]) {
+ case 'a': case 'b': case 'c': case 'd': case 'e': case 'f':
+ case 'g': case 'h': case 'i': case 'j': case 'k': case 'l':
+ case 'm': case 'n': case 'o': case 'p': case 'q': case 'r':
+ case 's': case 't': case 'u': case 'v': case 'w': case 'x':
+ case 'y': case 'z':
+ case 'A': case 'B': case 'C': case 'D': case 'E': case 'F':
+ case 'G': case 'H': case 'I': case 'J': case 'K': case 'L':
+ case 'M': case 'N': case 'O': case 'P': case 'Q': case 'R':
+ case 'S': case 'T': case 'U': case 'V': case 'W': case 'X':
+ case 'Y': case 'Z':
+ case '0': case '1': case '2': case '3': case '4': case '5':
+ case '6': case '7': case '8': case '9':
+ case '*': case '/': case '@': case '.': case '-': case '_':
+ case ':': case '$':
+ pos++;
+ break;
+ default:
+ return bufferSlice(oldPos, pos);
+ }
+ }
+ return bufferSlice(oldPos, pos);
+ }
+
+ /**
+ * Scans a word or keyword.
+ *
+ * ON ENTRY: 'pos' is 1 + the index of the first char in the word.
+ * ON EXIT: 'pos' is 1 + the index of the last char in the word.
+ *
+ * @return the word or keyword token.
+ */
+ private Token wordOrKeyword() {
+ String word = scanWord();
+ TokenKind kind = getTokenKindForWord(word);
+ return kind == TokenKind.WORD ? new Token(word) : new Token(kind);
+ }
+
+ /**
+ * Performs tokenization of the character buffer of file contents provided to
+ * the constructor.
+ */
+ private void tokenize() throws QueryException {
+ while (pos < buffer.length) {
+ char c = buffer[pos];
+ pos++;
+ switch (c) {
+ case '(': {
+ addToken(new Token(TokenKind.LPAREN));
+ break;
+ }
+ case ')': {
+ addToken(new Token(TokenKind.RPAREN));
+ break;
+ }
+ case ',': {
+ addToken(new Token(TokenKind.COMMA));
+ break;
+ }
+ case '+': {
+ addToken(new Token(TokenKind.PLUS));
+ break;
+ }
+ case '-': {
+ addToken(new Token(TokenKind.MINUS));
+ break;
+ }
+ case '=': {
+ addToken(new Token(TokenKind.EQUALS));
+ break;
+ }
+ case '^': {
+ addToken(new Token(TokenKind.CARET));
+ break;
+ }
+ case '\n':
+ case ' ':
+ case '\t':
+ case '\r': {
+ /* ignore */
+ break;
+ }
+ case '\'':
+ case '\"': {
+ addToken(quotedWord(c));
+ break;
+ }
+ default: {
+ addToken(wordOrKeyword());
+ break;
+ } // default
+ } // switch
+ } // while
+
+ addToken(new Token(TokenKind.EOF));
+
+ this.buffer = null; // release buffer now that we have our tokens
+ }
+
+ private String bufferSlice(int start, int end) {
+ return new String(this.buffer, start, end - start);
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/query2/engine/QueryEnvironment.java b/src/main/java/com/google/devtools/build/lib/query2/engine/QueryEnvironment.java
new file mode 100644
index 0000000..46a7afd
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/query2/engine/QueryEnvironment.java
@@ -0,0 +1,351 @@
+// Copyright 2014 Google Inc. 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.query2.engine;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+
+import javax.annotation.Nonnull;
+
+/**
+ * The environment of a Blaze query. Implementations do not need to be thread-safe. The generic type
+ * T represents a node of the graph on which the query runs; as such, there is no restriction on T.
+ * However, query assumes a certain graph model, and the {@link TargetAccessor} class is used to
+ * access properties of these nodes.
+ *
+ * @param <T> the node type of the dependency graph
+ */
+public interface QueryEnvironment<T> {
+ /**
+ * Type of an argument of a user-defined query function.
+ */
+ public enum ArgumentType {
+ EXPRESSION, WORD, INTEGER;
+ }
+
+ /**
+ * Value of an argument of a user-defined query function.
+ */
+ public static class Argument {
+ private final ArgumentType type;
+ private final QueryExpression expression;
+ private final String word;
+ private final int integer;
+
+ private Argument(ArgumentType type, QueryExpression expression, String word, int integer) {
+ this.type = type;
+ this.expression = expression;
+ this.word = word;
+ this.integer = integer;
+ }
+
+ static Argument of(QueryExpression expression) {
+ return new Argument(ArgumentType.EXPRESSION, expression, null, 0);
+ }
+
+ static Argument of(String word) {
+ return new Argument(ArgumentType.WORD, null, word, 0);
+ }
+
+ static Argument of(int integer) {
+ return new Argument(ArgumentType.INTEGER, null, null, integer);
+ }
+
+ public ArgumentType getType() {
+ return type;
+ }
+
+ public QueryExpression getExpression() {
+ return expression;
+ }
+
+ public String getWord() {
+ return word;
+ }
+
+ public int getInteger() {
+ return integer;
+ }
+
+ @Override
+ public String toString() {
+ switch (type) {
+ case WORD: return "'" + word + "'";
+ case EXPRESSION: return expression.toString();
+ case INTEGER: return Integer.toString(integer);
+ default: throw new IllegalStateException();
+ }
+ }
+ }
+
+ /**
+ * A user-defined query function.
+ */
+ public interface QueryFunction {
+ /**
+ * Name of the function as it appears in the query language.
+ */
+ String getName();
+
+ /**
+ * The number of arguments that are required. The rest is optional.
+ *
+ * <p>This should be greater than or equal to zero and at smaller than or equal to the length
+ * of the list returned by {@link #getArgumentTypes}.
+ */
+ int getMandatoryArguments();
+
+ /**
+ * The types of the arguments of the function.
+ */
+ List<ArgumentType> getArgumentTypes();
+
+ /**
+ * Called when a user-defined function is to be evaluated.
+ *
+ * @param env the query environment this function is evaluated in.
+ * @param expression the expression being evaluated.
+ * @param args the input arguments. These are type-checked against the specification returned
+ * by {@link #getArgumentTypes} and {@link #getMandatoryArguments}
+ */
+ <T> Set<T> eval(QueryEnvironment<T> env, QueryExpression expression, List<Argument> args)
+ throws QueryException;
+ }
+
+ /**
+ * Exception type for the case where a target cannot be found. It's basically a wrapper for
+ * whatever exception is internally thrown.
+ */
+ public static final class TargetNotFoundException extends Exception {
+ public TargetNotFoundException(String msg) {
+ super(msg);
+ }
+
+ public TargetNotFoundException(Throwable cause) {
+ super(cause.getMessage(), cause);
+ }
+ }
+
+ /**
+ * Returns the set of target nodes in the graph for the specified target
+ * pattern, in 'blaze build' syntax.
+ */
+ Set<T> getTargetsMatchingPattern(QueryExpression owner, String pattern)
+ throws QueryException;
+
+ /** Ensures the specified target exists. */
+ // NOTE(bazel-team): this method is left here as scaffolding from a previous refactoring. It may
+ // be possible to remove it.
+ T getOrCreate(T target);
+
+ /** Returns the direct forward dependencies of the specified target. */
+ Collection<T> getFwdDeps(T target);
+
+ /** Returns the direct reverse dependencies of the specified target. */
+ Collection<T> getReverseDeps(T target);
+
+ /**
+ * Returns the forward transitive closure of all of the targets in
+ * "targets". Callers must ensure that {@link #buildTransitiveClosure}
+ * has been called for the relevant subgraph.
+ */
+ Set<T> getTransitiveClosure(Set<T> targets);
+
+ /**
+ * Construct the dependency graph for a depth-bounded forward transitive closure
+ * of all nodes in "targetNodes". The identity of the calling expression is
+ * required to produce error messages.
+ *
+ * <p>If a larger transitive closure was already built, returns it to
+ * improve incrementality, since all depth-constrained methods filter it
+ * after it is built anyway.
+ */
+ void buildTransitiveClosure(QueryExpression caller,
+ Set<T> targetNodes,
+ int maxDepth) throws QueryException;
+
+ /**
+ * Returns the set of nodes on some path from "from" to "to".
+ */
+ Set<T> getNodesOnPath(T from, T to);
+
+ /**
+ * Returns the value of the specified variable, or null if it is undefined.
+ */
+ Set<T> getVariable(String name);
+
+ /**
+ * Sets the value of the specified variable. If value is null the variable
+ * becomes undefined. Returns the previous value, if any.
+ */
+ Set<T> setVariable(String name, Set<T> value);
+
+ void reportBuildFileError(QueryExpression expression, String msg) throws QueryException;
+
+ /**
+ * Returns the set of BUILD, included, sub-included and Skylark files that define the given set of
+ * targets. Each such file is itself represented as a target in the result.
+ */
+ Set<T> getBuildFiles(QueryExpression caller, Set<T> nodes) throws QueryException;
+
+ /**
+ * Returns an object that can be used to query information about targets. Implementations should
+ * create a single instance and return that for all calls. A class can implement both {@code
+ * QueryEnvironment} and {@code TargetAccessor} at the same time, in which case this method simply
+ * returns {@code this}.
+ */
+ TargetAccessor<T> getAccessor();
+
+ /**
+ * Whether the given setting is enabled. The code should default to return {@code false} for all
+ * unknown settings. The enum is used rather than a method for each setting so that adding more
+ * settings is backwards-compatible.
+ *
+ * @throws NullPointerException if setting is null
+ */
+ boolean isSettingEnabled(@Nonnull Setting setting);
+
+ /**
+ * Returns the set of query functions implemented by this query environment.
+ */
+ Iterable<QueryFunction> getFunctions();
+
+ /**
+ * Settings for the query engine. See {@link QueryEnvironment#isSettingEnabled}.
+ */
+ public static enum Setting {
+
+ /**
+ * Whether to evaluate tests() expressions in strict mode. If {@link #isSettingEnabled} returns
+ * true for this setting, then the tests() expression will give an error when expanding tests
+ * suites, if the test suite contains any non-test targets.
+ */
+ TESTS_EXPRESSION_STRICT,
+
+ /**
+ * Do not consider implicit deps (any label that was not explicitly specified in the BUILD file)
+ * when traversing dependency edges.
+ */
+ NO_IMPLICIT_DEPS,
+
+ /**
+ * Do not consider host dependencies when traversing dependency edges.
+ */
+ NO_HOST_DEPS,
+
+ /**
+ * Do not consider nodep attributes when traversing dependency edges.
+ */
+ NO_NODEP_DEPS;
+ }
+
+ /**
+ * An adapter interface giving access to properties of T. There are four types of targets: rules,
+ * package groups, source files, and generated files. Of these, only rules can have attributes.
+ */
+ public static interface TargetAccessor<T> {
+ /**
+ * Returns the target type represented as a string of the form {@code <type> rule} or
+ * {@code package group} or {@code source file} or {@code generated file}. This is widely used
+ * for target filtering, so implementations must use the Blaze rule class naming scheme.
+ */
+ String getTargetKind(T target);
+
+ /**
+ * Returns the full label of the target as a string, e.g. {@code //some:target}.
+ */
+ String getLabel(T target);
+
+ /**
+ * Returns whether the given target is a rule.
+ */
+ boolean isRule(T target);
+
+ /**
+ * Returns whether the given target is a test target. If this returns true, then {@link #isRule}
+ * must also return true for the target.
+ */
+ boolean isTestRule(T target);
+
+ /**
+ * Returns whether the given target is a test suite target. If this returns true, then {@link
+ * #isRule} must also return true for the target, but {@link #isTestRule} must return false;
+ * test suites are not test rules, and vice versa.
+ */
+ boolean isTestSuite(T target);
+
+ /**
+ * If the attribute of the given name on the given target is a label or label list, then this
+ * method returns the list of corresponding target instances. Otherwise returns an empty list.
+ * If an error occurs during resolution, it throws a {@link QueryException} using the caller and
+ * error message prefix.
+ *
+ * @throws IllegalArgumentException if target is not a rule (according to {@link #isRule})
+ */
+ List<T> getLabelListAttr(QueryExpression caller, T target, String attrName,
+ String errorMsgPrefix) throws QueryException;
+
+ /**
+ * If the attribute of the given name on the given target is a string list, then this method
+ * returns it.
+ *
+ * @throws IllegalArgumentException if target is not a rule (according to {@link #isRule}), or
+ * if the target does not have an attribute of type string list
+ * with the given name
+ */
+ List<String> getStringListAttr(T target, String attrName);
+
+ /**
+ * If the attribute of the given name on the given target is a string, then this method returns
+ * it.
+ *
+ * @throws IllegalArgumentException if target is not a rule (according to {@link #isRule}), or
+ * if the target does not have an attribute of type string with
+ * the given name
+ */
+ String getStringAttr(T target, String attrName);
+
+ /**
+ * Returns the given attribute represented as a list of strings. For "normal" attributes,
+ * this should just be a list of size one containing the attribute's value. For configurable
+ * attributes, there should be one entry for each possible value the attribute may take.
+ *
+ *<p>Note that for backwards compatibility, tristate and boolean attributes are returned as
+ * int using the values {@code 0, 1} and {@code -1}. If there is no such attribute, this
+ * method returns an empty list.
+ *
+ * @throws IllegalArgumentException if target is not a rule (according to {@link #isRule})
+ */
+ Iterable<String> getAttrAsString(T target, String attrName);
+ }
+
+ /** List of the default query functions. */
+ public static final List<QueryFunction> DEFAULT_QUERY_FUNCTIONS =
+ ImmutableList.<QueryFunction>of(
+ new AllPathsFunction(),
+ new BuildFilesFunction(),
+ new AttrFunction(),
+ new FilterFunction(),
+ new LabelsFunction(),
+ new KindFunction(),
+ new SomeFunction(),
+ new SomePathFunction(),
+ new TestsFunction(),
+ new DepsFunction(),
+ new RdepsFunction()
+ );
+}
diff --git a/src/main/java/com/google/devtools/build/lib/query2/engine/QueryEvalResult.java b/src/main/java/com/google/devtools/build/lib/query2/engine/QueryEvalResult.java
new file mode 100644
index 0000000..5bcea7e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/query2/engine/QueryEvalResult.java
@@ -0,0 +1,51 @@
+// Copyright 2015 Google Inc. 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.query2.engine;
+
+import com.google.common.base.Preconditions;
+
+import java.util.Set;
+
+/**
+ * The result of a query evaluation, containing a set of elements.
+ *
+ * @param <T> the node type of the elements.
+ */
+public class QueryEvalResult<T> {
+
+ protected final boolean success;
+ protected final Set<T> resultSet;
+
+ public QueryEvalResult(
+ boolean success, Set<T> resultSet) {
+ this.success = success;
+ this.resultSet = Preconditions.checkNotNull(resultSet);
+ }
+
+ /**
+ * Whether the query was successful. This can only be false if the query was run with
+ * <code>keep_going</code>, otherwise evaluation will throw a {@link QueryException}.
+ */
+ public boolean getSuccess() {
+ return success;
+ }
+
+ /**
+ * Returns the result as a set of targets.
+ */
+ public Set<T> getResultSet() {
+ return resultSet;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/query2/engine/QueryException.java b/src/main/java/com/google/devtools/build/lib/query2/engine/QueryException.java
new file mode 100644
index 0000000..71c1a8a
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/query2/engine/QueryException.java
@@ -0,0 +1,58 @@
+// Copyright 2014 Google Inc. 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.query2.engine;
+
+/**
+ */
+public class QueryException extends Exception {
+
+ /**
+ * Returns a better error message for the query.
+ */
+ static String describeFailedQuery(QueryException e, QueryExpression toplevel) {
+ QueryExpression badQuery = e.getFailedExpression();
+ if (badQuery == null) {
+ return "Evaluation failed: " + e.getMessage();
+ }
+ return badQuery == toplevel
+ ? "Evaluation of query \"" + toplevel + "\" failed: " + e.getMessage()
+ : "Evaluation of subquery \"" + badQuery
+ + "\" failed (did you want to use --keep_going?): " + e.getMessage();
+ }
+
+ private final QueryExpression expression;
+
+ public QueryException(QueryException e, QueryExpression toplevel) {
+ super(describeFailedQuery(e, toplevel), e);
+ this.expression = null;
+ }
+
+ public QueryException(QueryExpression expression, String message) {
+ super(message);
+ this.expression = expression;
+ }
+
+ public QueryException(String message) {
+ this(null, message);
+ }
+
+ /**
+ * Returns the subexpression for which evaluation failed, or null if
+ * the failure occurred during lexing/parsing.
+ */
+ public QueryExpression getFailedExpression() {
+ return expression;
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/query2/engine/QueryExpression.java b/src/main/java/com/google/devtools/build/lib/query2/engine/QueryExpression.java
new file mode 100644
index 0000000..23603f1
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/query2/engine/QueryExpression.java
@@ -0,0 +1,83 @@
+// Copyright 2014 Google Inc. 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.query2.engine;
+
+import java.util.Collection;
+import java.util.Set;
+
+/**
+ * Base class for expressions in the Blaze query language, revision 2.
+ *
+ * <p>All queries return a subgraph of the dependency graph, represented
+ * as a set of target nodes.
+ *
+ * <p>All queries must ensure that sufficient graph edges are created in the
+ * QueryEnvironment so that all nodes in the result are correctly ordered
+ * according to the type of query. For example, "deps" queries require that
+ * all the nodes in the transitive closure of its argument set are correctly
+ * ordered w.r.t. each other; "somepath" queries require that the order of the
+ * nodes on the resulting path are correctly ordered; algebraic set operations
+ * such as intersect and union are inherently unordered.
+ *
+ * <h2>Package overview</h2>
+ *
+ * <p>This package consists of two basic class hierarchies. The first, {@code
+ * QueryExpression}, is the set of different query expressions in the language,
+ * and the {@link #eval} method of each defines the semantics. The result of
+ * evaluating a query is set of Blaze {@code Target}s (a file or rule). The
+ * set may be interpreted as either a set or as nodes of a DAG, depending on
+ * the context.
+ *
+ * <p>The second hierarchy is {@code OutputFormatter}. Its subclasses define
+ * different ways of printing out the result of a query. Each accepts a {@code
+ * Digraph} of {@code Target}s, and an output stream.
+ */
+public abstract class QueryExpression {
+
+ /**
+ * Scan and parse the specified query expression.
+ */
+ public static QueryExpression parse(String query, QueryEnvironment<?> env)
+ throws QueryException {
+ return QueryParser.parse(query, env);
+ }
+
+ protected QueryExpression() {}
+
+ /**
+ * Evaluates this query in the specified environment, and returns a subgraph,
+ * concretely represented a new (possibly-immutable) set of target nodes.
+ *
+ * Failures resulting from evaluation of an ill-formed query cause
+ * QueryException to be thrown.
+ *
+ * The reporting of failures arising from errors in BUILD files depends on
+ * the --keep_going flag. If enabled (the default), then QueryException is
+ * thrown. If disabled, evaluation will stumble on to produce a (possibly
+ * inaccurate) result, but a result nonetheless.
+ */
+ public abstract <T> Set<T> eval(QueryEnvironment<T> env) throws QueryException;
+
+ /**
+ * Collects all target patterns that are referenced anywhere within this query expression and adds
+ * them to the given collection, which must be mutable.
+ */
+ public abstract void collectTargetPatterns(Collection<String> literals);
+
+ /**
+ * Returns this query expression pretty-printed.
+ */
+ @Override
+ public abstract String toString();
+}
diff --git a/src/main/java/com/google/devtools/build/lib/query2/engine/QueryParser.java b/src/main/java/com/google/devtools/build/lib/query2/engine/QueryParser.java
new file mode 100644
index 0000000..bcd89cc
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/query2/engine/QueryParser.java
@@ -0,0 +1,261 @@
+// Copyright 2014 Google Inc. 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.query2.engine;
+
+import static com.google.devtools.build.lib.query2.engine.Lexer.BINARY_OPERATORS;
+
+import com.google.devtools.build.lib.query2.engine.Lexer.TokenKind;
+import com.google.devtools.build.lib.query2.engine.QueryEnvironment.Argument;
+import com.google.devtools.build.lib.query2.engine.QueryEnvironment.ArgumentType;
+import com.google.devtools.build.lib.query2.engine.QueryEnvironment.QueryFunction;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * LL(1) recursive descent parser for the Blaze query language, revision 2.
+ *
+ * In the grammar below, non-terminals are lowercase and terminals are
+ * uppercase, or character literals.
+ *
+ * <pre>
+ * expr ::= WORD
+ * | LET WORD = expr IN expr
+ * | '(' expr ')'
+ * | WORD '(' expr ( ',' expr ) * ')'
+ * | expr INTERSECT expr
+ * | expr '^' expr
+ * | expr UNION expr
+ * | expr '+' expr
+ * | expr EXCEPT expr
+ * | expr '-' expr
+ * | SET '(' WORD * ')'
+ * </pre>
+ */
+final class QueryParser {
+
+ private Lexer.Token token; // current lookahead token
+ private final List<Lexer.Token> tokens;
+ private final Iterator<Lexer.Token> tokenIterator;
+ private final Map<String, QueryFunction> functions;
+
+ /**
+ * Scan and parse the specified query expression.
+ */
+ static QueryExpression parse(String query, QueryEnvironment<?> env) throws QueryException {
+ QueryParser parser = new QueryParser(
+ Lexer.scan(query.toCharArray()), env);
+ QueryExpression expr = parser.parseExpression();
+ if (parser.token.kind != TokenKind.EOF) {
+ throw new QueryException("unexpected token '" + parser.token
+ + "' after query expression '" + expr + "'");
+ }
+ return expr;
+ }
+
+ private QueryParser(List<Lexer.Token> tokens, QueryEnvironment<?> env) {
+ this.functions = new HashMap<>();
+ for (QueryFunction queryFunction : env.getFunctions()) {
+ this.functions.put(queryFunction.getName(), queryFunction);
+ }
+ this.tokens = tokens;
+ this.tokenIterator = tokens.iterator();
+ nextToken();
+ }
+
+ /**
+ * Returns an exception. Don't forget to throw it.
+ */
+ private QueryException syntaxError(Lexer.Token token) {
+ String message = "premature end of input";
+ if (token.kind != TokenKind.EOF) {
+ StringBuilder buf = new StringBuilder("syntax error at '");
+ String sep = "";
+ for (int index = tokens.indexOf(token),
+ max = Math.min(tokens.size() - 1, index + 3); // 3 tokens of context
+ index < max; ++index) {
+ buf.append(sep).append(tokens.get(index));
+ sep = " ";
+ }
+ buf.append("'");
+ message = buf.toString();
+ }
+ return new QueryException(message);
+ }
+
+ /**
+ * Consumes the current token. If it is not of the specified (expected)
+ * kind, throws QueryException. Returns the value associated with the
+ * consumed token, if any.
+ */
+ private String consume(TokenKind kind) throws QueryException {
+ if (token.kind != kind) {
+ throw syntaxError(token);
+ }
+ String word = token.word;
+ nextToken();
+ return word;
+ }
+
+ /**
+ * Consumes the current token, which must be a WORD containing an integer
+ * literal. Returns that integer, or throws a QueryException otherwise.
+ */
+ private int consumeIntLiteral() throws QueryException {
+ String intString = consume(TokenKind.WORD);
+ try {
+ return Integer.parseInt(intString);
+ } catch (NumberFormatException e) {
+ throw new QueryException("expected an integer literal: '" + intString + "'");
+ }
+ }
+
+ private void nextToken() {
+ if (token == null || token.kind != TokenKind.EOF) {
+ token = tokenIterator.next();
+ }
+ }
+
+ /**
+ * expr ::= primary
+ * | expr INTERSECT expr
+ * | expr '^' expr
+ * | expr UNION expr
+ * | expr '+' expr
+ * | expr EXCEPT expr
+ * | expr '-' expr
+ */
+ private QueryExpression parseExpression() throws QueryException {
+ // All operators are left-associative and of equal precedence.
+ return parseBinaryOperatorTail(parsePrimary());
+ }
+
+ /**
+ * tail ::= ( <op> <primary> )*
+ * All operators have equal precedence.
+ * This factoring is required for left-associative binary operators in LL(1).
+ */
+ private QueryExpression parseBinaryOperatorTail(QueryExpression lhs) throws QueryException {
+ if (!BINARY_OPERATORS.contains(token.kind)) {
+ return lhs;
+ }
+
+ List<QueryExpression> operands = new ArrayList<>();
+ operands.add(lhs);
+ TokenKind lastOperator = token.kind;
+
+ while (BINARY_OPERATORS.contains(token.kind)) {
+ TokenKind operator = token.kind;
+ consume(operator);
+ if (operator != lastOperator) {
+ lhs = new BinaryOperatorExpression(lastOperator, operands);
+ operands.clear();
+ operands.add(lhs);
+ lastOperator = operator;
+ }
+ QueryExpression rhs = parsePrimary();
+ operands.add(rhs);
+ }
+ return new BinaryOperatorExpression(lastOperator, operands);
+ }
+
+ /**
+ * primary ::= WORD
+ * | LET WORD = expr IN expr
+ * | '(' expr ')'
+ * | WORD '(' expr ( ',' expr ) * ')'
+ * | DEPS '(' expr ')'
+ * | DEPS '(' expr ',' WORD ')'
+ * | RDEPS '(' expr ',' expr ')'
+ * | RDEPS '(' expr ',' expr ',' WORD ')'
+ * | SET '(' WORD * ')'
+ */
+ private QueryExpression parsePrimary() throws QueryException {
+ switch (token.kind) {
+ case WORD: {
+ String word = consume(TokenKind.WORD);
+ if (token.kind == TokenKind.LPAREN) {
+ QueryFunction function = functions.get(word);
+ if (function == null) {
+ throw syntaxError(token);
+ }
+ List<Argument> args = new ArrayList<>();
+ TokenKind tokenKind = TokenKind.LPAREN;
+ int argsSeen = 0;
+ for (ArgumentType type : function.getArgumentTypes()) {
+ if (token.kind == TokenKind.RPAREN && argsSeen >= function.getMandatoryArguments()) {
+ break;
+ }
+
+ consume(tokenKind);
+ tokenKind = TokenKind.COMMA;
+ switch (type) {
+ case EXPRESSION:
+ args.add(Argument.of(parseExpression()));
+ break;
+
+ case WORD:
+ args.add(Argument.of(consume(TokenKind.WORD)));
+ break;
+
+ case INTEGER:
+ args.add(Argument.of(consumeIntLiteral()));
+ break;
+
+ default:
+ throw new IllegalStateException();
+ }
+
+ argsSeen++;
+ }
+
+ consume(TokenKind.RPAREN);
+ return new FunctionExpression(function, args);
+ } else {
+ return new TargetLiteral(word);
+ }
+ }
+ case LET: {
+ consume(TokenKind.LET);
+ String name = consume(TokenKind.WORD);
+ consume(TokenKind.EQUALS);
+ QueryExpression varExpr = parseExpression();
+ consume(TokenKind.IN);
+ QueryExpression bodyExpr = parseExpression();
+ return new LetExpression(name, varExpr, bodyExpr);
+ }
+ case LPAREN: {
+ consume(TokenKind.LPAREN);
+ QueryExpression expr = parseExpression();
+ consume(TokenKind.RPAREN);
+ return expr;
+ }
+ case SET: {
+ nextToken();
+ consume(TokenKind.LPAREN);
+ List<TargetLiteral> words = new ArrayList<>();
+ while (token.kind == TokenKind.WORD) {
+ words.add(new TargetLiteral(consume(TokenKind.WORD)));
+ }
+ consume(TokenKind.RPAREN);
+ return new SetExpression(words);
+ }
+ default:
+ throw syntaxError(token);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/query2/engine/RdepsFunction.java b/src/main/java/com/google/devtools/build/lib/query2/engine/RdepsFunction.java
new file mode 100644
index 0000000..6a8734b
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/query2/engine/RdepsFunction.java
@@ -0,0 +1,99 @@
+// Copyright 2014 Google Inc. 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.query2.engine;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.query2.engine.QueryEnvironment.Argument;
+import com.google.devtools.build.lib.query2.engine.QueryEnvironment.ArgumentType;
+import com.google.devtools.build.lib.query2.engine.QueryEnvironment.QueryFunction;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * An "rdeps" query expression, which computes the reverse dependencies of the argument within the
+ * transitive closure of the universe. An optional integer-literal third argument may be
+ * specified; its value bounds the search from the arguments.
+ *
+ * <pre>expr ::= RDEPS '(' expr ',' expr ')'</pre>
+ * <pre> | RDEPS '(' expr ',' expr ',' WORD ')'</pre>
+ */
+final class RdepsFunction implements QueryFunction {
+ RdepsFunction() {
+ }
+
+ @Override
+ public String getName() {
+ return "rdeps";
+ }
+
+ @Override
+ public int getMandatoryArguments() {
+ return 2; // last argument is optional
+ }
+
+ @Override
+ public List<ArgumentType> getArgumentTypes() {
+ return ImmutableList.of(
+ ArgumentType.EXPRESSION, ArgumentType.EXPRESSION, ArgumentType.INTEGER);
+ }
+
+ /**
+ * Compute the transitive closure of the universe, then breadth-first search from the argument
+ * towards the universe while staying within the transitive closure.
+ */
+ @Override
+ public <T> Set<T> eval(QueryEnvironment<T> env, QueryExpression expression, List<Argument> args)
+ throws QueryException {
+ Set<T> universeValue = args.get(0).getExpression().eval(env);
+ Set<T> argumentValue = args.get(1).getExpression().eval(env);
+ int depthBound = args.size() > 2 ? args.get(2).getInteger() : Integer.MAX_VALUE;
+
+ env.buildTransitiveClosure(expression, universeValue, Integer.MAX_VALUE);
+
+ Set<T> visited = new LinkedHashSet<>();
+ Set<T> reachableFromUniverse = env.getTransitiveClosure(universeValue);
+ Collection<T> current = argumentValue;
+
+ // We need to iterate depthBound + 1 times.
+ for (int i = 0; i <= depthBound; i++) {
+ List<T> next = new ArrayList<>();
+ for (T node : current) {
+ if (!reachableFromUniverse.contains(node)) {
+ // Traversed outside the transitive closure of the universe.
+ continue;
+ }
+
+ if (!visited.add(node)) {
+ // Already visited; if we see a node in a later round, then we don't need to visit it
+ // again, because the depth at which we see it at must be greater than or equal to the
+ // last visit.
+ continue;
+ }
+
+ next.addAll(env.getReverseDeps(node));
+ }
+ if (next.isEmpty()) {
+ // Exit when there are no more nodes to visit.
+ break;
+ }
+ current = next;
+ }
+
+ return visited;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/query2/engine/RegexFilterExpression.java b/src/main/java/com/google/devtools/build/lib/query2/engine/RegexFilterExpression.java
new file mode 100644
index 0000000..1dbe5e6
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/query2/engine/RegexFilterExpression.java
@@ -0,0 +1,83 @@
+// Copyright 2014 Google Inc. 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.query2.engine;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.query2.engine.QueryEnvironment.Argument;
+import com.google.devtools.build.lib.query2.engine.QueryEnvironment.QueryFunction;
+
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+/**
+ * An abstract class that provides generic regex filter expression. Actual
+ * expression are implemented by the subclasses.
+ */
+abstract class RegexFilterExpression implements QueryFunction {
+ protected RegexFilterExpression() {
+ }
+
+ @Override
+ public <T> Set<T> eval(QueryEnvironment<T> env, QueryExpression expression, List<Argument> args)
+ throws QueryException {
+ Pattern compiledPattern;
+ try {
+ compiledPattern = Pattern.compile(getPattern(args));
+ } catch (IllegalArgumentException e) {
+ throw new QueryException(expression, "illegal pattern regexp in '" + this + "': "
+ + e.getMessage());
+ }
+
+ QueryExpression argument = args.get(args.size() - 1).getExpression();
+
+ Set<T> result = new LinkedHashSet<>();
+ for (T target : argument.eval(env)) {
+ for (String str : getFilterStrings(env, args, target)) {
+ if ((str != null) && compiledPattern.matcher(str).find()) {
+ result.add(target);
+ break;
+ }
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Returns string for the given target that must be matched against pattern.
+ * May return null, in which case matching is guaranteed to fail.
+ */
+ protected abstract <T> String getFilterString(
+ QueryEnvironment<T> env, List<Argument> args, T target);
+
+ /**
+ * Returns a list of strings for the given target that must be matched against
+ * pattern. The filter matches if *any* of these strings matches.
+ *
+ * <p>Unless subclasses have an explicit reason to override this method, it's fine
+ * to keep the default implementation that just delegates to {@link #getFilterString}.
+ * Overriding this method is useful for subclasses that want to match against a
+ * universe of possible values. For example, with configurable attributes, an
+ * attribute might have different values depending on the build configuration. One
+ * may wish the filter to match if *any* of those values matches.
+ */
+ protected <T> Iterable<String> getFilterStrings(
+ QueryEnvironment<T> env, List<Argument> args, T target) {
+ String filterString = getFilterString(env, args, target);
+ return filterString == null ? ImmutableList.<String>of() : ImmutableList.of(filterString);
+ }
+
+ protected abstract String getPattern(List<Argument> args);
+}
diff --git a/src/main/java/com/google/devtools/build/lib/query2/engine/SetExpression.java b/src/main/java/com/google/devtools/build/lib/query2/engine/SetExpression.java
new file mode 100644
index 0000000..a28c679
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/query2/engine/SetExpression.java
@@ -0,0 +1,70 @@
+// Copyright 2014 Google Inc. 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.query2.engine;
+
+import com.google.common.base.Joiner;
+
+import java.util.Collection;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A set(word, ..., word) expression, which computes the union of zero or more
+ * target patterns separated by whitespace. This is intended to support the
+ * use-case in which a set of labels written to a file by a previous query
+ * expression can be modified externally, then used as input to another query,
+ * like so:
+ *
+ * <pre>
+ * % blaze query 'somepath(foo, bar)' | grep ... | sed ... | awk ... >file
+ * % blaze query "kind(qux_library, set($(<file)))"
+ * </pre>
+ *
+ * <p>The grammar currently restricts the operands of set() to being zero or
+ * more words (target patterns), with no intervening punctuation. In principle
+ * this could be extended to arbitrary expressions without grammatical
+ * ambiguity, but this seems excessively general for now.
+ *
+ * <pre>expr ::= SET '(' WORD * ')'</pre>
+ */
+class SetExpression extends QueryExpression {
+
+ private final List<TargetLiteral> words;
+
+ SetExpression(List<TargetLiteral> words) {
+ this.words = words;
+ }
+
+ @Override
+ public <T> Set<T> eval(QueryEnvironment<T> env) throws QueryException {
+ Set<T> result = new LinkedHashSet<>();
+ for (TargetLiteral expr : words) {
+ result.addAll(expr.eval(env));
+ }
+ return result;
+ }
+
+ @Override
+ public void collectTargetPatterns(Collection<String> literals) {
+ for (TargetLiteral expr : words) {
+ expr.collectTargetPatterns(literals);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "set(" + Joiner.on(' ').join(words) + ")";
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/query2/engine/SkyframeRestartQueryException.java b/src/main/java/com/google/devtools/build/lib/query2/engine/SkyframeRestartQueryException.java
new file mode 100644
index 0000000..d720ec9
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/query2/engine/SkyframeRestartQueryException.java
@@ -0,0 +1,24 @@
+// Copyright 2014 Google Inc. 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.query2.engine;
+
+/**
+ * This exception is thrown when a query operation was unable to complete because of a Skyframe
+ * missing dependency.
+ */
+public class SkyframeRestartQueryException extends RuntimeException {
+ public SkyframeRestartQueryException() {
+ super("need skyframe retry. missing dep");
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/query2/engine/SomeFunction.java b/src/main/java/com/google/devtools/build/lib/query2/engine/SomeFunction.java
new file mode 100644
index 0000000..384b474
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/query2/engine/SomeFunction.java
@@ -0,0 +1,59 @@
+// Copyright 2014 Google Inc. 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.query2.engine;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.query2.engine.QueryEnvironment.Argument;
+import com.google.devtools.build.lib.query2.engine.QueryEnvironment.ArgumentType;
+import com.google.devtools.build.lib.query2.engine.QueryEnvironment.QueryFunction;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A some(x) filter expression, which returns an arbitrary node in set x, or
+ * fails if x is empty.
+ *
+ * <pre>expr ::= SOME '(' expr ')'</pre>
+ */
+class SomeFunction implements QueryFunction {
+ SomeFunction() {
+ }
+
+ @Override
+ public String getName() {
+ return "some";
+ }
+
+ @Override
+ public int getMandatoryArguments() {
+ return 1;
+ }
+
+ @Override
+ public List<ArgumentType> getArgumentTypes() {
+ return ImmutableList.of(ArgumentType.EXPRESSION);
+ }
+
+ @Override
+ public <T> Set<T> eval(QueryEnvironment<T> env, QueryExpression expression, List<Argument> args)
+ throws QueryException {
+ Set<T> argumentValue = args.get(0).getExpression().eval(env);
+ if (argumentValue.isEmpty()) {
+ throw new QueryException(expression, "argument set is empty");
+ }
+ return ImmutableSet.of(argumentValue.iterator().next());
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/query2/engine/SomePathFunction.java b/src/main/java/com/google/devtools/build/lib/query2/engine/SomePathFunction.java
new file mode 100644
index 0000000..b90bcdf
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/query2/engine/SomePathFunction.java
@@ -0,0 +1,87 @@
+// Copyright 2014 Google Inc. 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.query2.engine;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+import com.google.common.collect.Sets.SetView;
+import com.google.devtools.build.lib.query2.engine.QueryEnvironment.Argument;
+import com.google.devtools.build.lib.query2.engine.QueryEnvironment.ArgumentType;
+import com.google.devtools.build.lib.query2.engine.QueryEnvironment.QueryFunction;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A somepath(x, y) query expression, which computes the set of nodes
+ * on some arbitrary path from a target in set x to a target in set y.
+ *
+ * <pre>expr ::= SOMEPATH '(' expr ',' expr ')'</pre>
+ */
+class SomePathFunction implements QueryFunction {
+ SomePathFunction() {
+ }
+
+ @Override
+ public String getName() {
+ return "somepath";
+ }
+
+ @Override
+ public int getMandatoryArguments() {
+ return 2;
+ }
+
+ @Override
+ public List<ArgumentType> getArgumentTypes() {
+ return ImmutableList.of(ArgumentType.EXPRESSION, ArgumentType.EXPRESSION);
+ }
+
+ @Override
+ public <T> Set<T> eval(QueryEnvironment<T> env, QueryExpression expression, List<Argument> args)
+ throws QueryException {
+ Set<T> fromValue = args.get(0).getExpression().eval(env);
+ Set<T> toValue = args.get(1).getExpression().eval(env);
+
+ // Implementation strategy: for each x in "from", compute its forward
+ // transitive closure. If it intersects "to", then do a path search from x
+ // to an arbitrary node in the intersection, and return the path. This
+ // avoids computing the full transitive closure of "from" in some cases.
+
+ env.buildTransitiveClosure(expression, fromValue, Integer.MAX_VALUE);
+
+ // This set contains all nodes whose TC does not intersect "toValue".
+ Set<T> done = new HashSet<>();
+
+ for (T x : fromValue) {
+ if (done.contains(x)) {
+ continue;
+ }
+ Set<T> xtc = env.getTransitiveClosure(ImmutableSet.of(x));
+ SetView<T> result;
+ if (xtc.size() > toValue.size()) {
+ result = Sets.intersection(toValue, xtc);
+ } else {
+ result = Sets.intersection(xtc, toValue);
+ }
+ if (!result.isEmpty()) {
+ return env.getNodesOnPath(x, result.iterator().next());
+ }
+ done.addAll(xtc);
+ }
+ return ImmutableSet.of();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/query2/engine/TargetLiteral.java b/src/main/java/com/google/devtools/build/lib/query2/engine/TargetLiteral.java
new file mode 100644
index 0000000..b6a57cc
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/query2/engine/TargetLiteral.java
@@ -0,0 +1,72 @@
+// Copyright 2014 Google Inc. 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.query2.engine;
+
+import com.google.common.base.Preconditions;
+
+import java.util.Collection;
+import java.util.Set;
+
+/**
+ * A literal set of targets, using 'blaze build' syntax. Or, a reference to a
+ * variable name. (The syntax of the string "pattern" determines which.)
+ *
+ * TODO(bazel-team): Perhaps we should distinguish NAME from WORD in the parser,
+ * based on the characters in it? Also, perhaps we should not allow NAMEs to
+ * be quoted like WORDs can be.
+ *
+ * <pre>expr ::= NAME | WORD</pre>
+ */
+final class TargetLiteral extends QueryExpression {
+
+ private final String pattern;
+
+ TargetLiteral(String pattern) {
+ this.pattern = Preconditions.checkNotNull(pattern);
+ }
+
+ public boolean isVariableReference() {
+ return LetExpression.isValidVarReference(pattern);
+ }
+
+ @Override
+ public <T> Set<T> eval(QueryEnvironment<T> env) throws QueryException {
+ if (isVariableReference()) {
+ String varName = LetExpression.getNameFromReference(pattern);
+ Set<T> value = env.getVariable(varName);
+ if (value == null) {
+ throw new QueryException(this, "undefined variable '" + varName + "'");
+ }
+ return env.getVariable(varName);
+ }
+
+ return env.getTargetsMatchingPattern(this, pattern);
+ }
+
+ @Override
+ public void collectTargetPatterns(Collection<String> literals) {
+ if (!isVariableReference()) {
+ literals.add(pattern);
+ }
+ }
+
+ @Override
+ public String toString() {
+ // Keep predicate consistent with Lexer.scanWord!
+ boolean needsQuoting = Lexer.isReservedWord(pattern)
+ || pattern.isEmpty()
+ || "$-*".indexOf(pattern.charAt(0)) != -1;
+ return needsQuoting ? ("\"" + pattern + "\"") : pattern;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/query2/engine/TestsFunction.java b/src/main/java/com/google/devtools/build/lib/query2/engine/TestsFunction.java
new file mode 100644
index 0000000..c902609
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/query2/engine/TestsFunction.java
@@ -0,0 +1,257 @@
+// Copyright 2014 Google Inc. 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.query2.engine;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Sets;
+import com.google.devtools.build.lib.query2.engine.QueryEnvironment.Argument;
+import com.google.devtools.build.lib.query2.engine.QueryEnvironment.ArgumentType;
+import com.google.devtools.build.lib.query2.engine.QueryEnvironment.QueryFunction;
+import com.google.devtools.build.lib.query2.engine.QueryEnvironment.Setting;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A tests(x) filter expression, which returns all the tests in set x,
+ * expanding test_suite rules into their constituents.
+ *
+ * <p>Unfortunately this class reproduces a substantial amount of logic from
+ * {@code TestSuiteConfiguredTarget}, albeit in a somewhat simplified form.
+ * This is basically inevitable since the expansion of test_suites cannot be
+ * done during the loading phase, because it involves inter-package references.
+ * We make no attempt to validate the input, or report errors or warnings other
+ * than missing target.
+ *
+ * <pre>expr ::= TESTS '(' expr ')'</pre>
+ */
+class TestsFunction implements QueryFunction {
+ TestsFunction() {
+ }
+
+ @Override
+ public String getName() {
+ return "tests";
+ }
+
+ @Override
+ public int getMandatoryArguments() {
+ return 1;
+ }
+
+ @Override
+ public List<ArgumentType> getArgumentTypes() {
+ return ImmutableList.of(ArgumentType.EXPRESSION);
+ }
+
+ @Override
+ public <T> Set<T> eval(QueryEnvironment<T> env, QueryExpression expression, List<Argument> args)
+ throws QueryException {
+ Closure<T> closure = new Closure<>(expression, env);
+ Set<T> result = new HashSet<>();
+ for (T target : args.get(0).getExpression().eval(env)) {
+ if (env.getAccessor().isTestRule(target)) {
+ result.add(target);
+ } else if (env.getAccessor().isTestSuite(target)) {
+ for (T test : closure.getTestsInSuite(target)) {
+ result.add(env.getOrCreate(test));
+ }
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Decides whether to include a test in a test_suite or not.
+ * @param testTags Collection of all tags exhibited by a given test.
+ * @param positiveTags Tags declared by the suite. A test must match ALL of these.
+ * @param negativeTags Tags declared by the suite. A test must match NONE of these.
+ * @return false is the test is to be removed.
+ */
+ private static boolean includeTest(Collection<String> testTags,
+ Collection<String> positiveTags, Collection<String> negativeTags) {
+ // Add this test if it matches ALL of the positive tags and NONE of the
+ // negative tags in the tags attribute.
+ for (String tag : negativeTags) {
+ if (testTags.contains(tag)) {
+ return false;
+ }
+ }
+ for (String tag : positiveTags) {
+ if (!testTags.contains(tag)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Separates a list of text "tags" into a Pair of Collections, where
+ * the first element are the required or positive tags and the second element
+ * are the excluded or negative tags.
+ * This should work on tag list provided from the command line
+ * --test_tags_filters flag or on tag filters explicitly declared in the
+ * suite.
+ *
+ * Keep this function in sync with the version in
+ * java.com.google.devtools.build.lib.view.packages.TestTargetUtils.sortTagsBySense
+ *
+ * @param tagList A collection of text tags to separate.
+ */
+ private static void sortTagsBySense(
+ Collection<String> tagList, Set<String> requiredTags, Set<String> excludedTags) {
+ for (String tag : tagList) {
+ if (tag.startsWith("-")) {
+ excludedTags.add(tag.substring(1));
+ } else if (tag.startsWith("+")) {
+ requiredTags.add(tag.substring(1));
+ } else if (tag.equals("manual")) {
+ // Ignore manual attribute because it is an exception: it is not a filter
+ // but a property of test_suite
+ continue;
+ } else {
+ requiredTags.add(tag);
+ }
+ }
+ }
+
+ /**
+ * A closure over the temporary state needed to compute the expression. This makes the evaluation
+ * thread-safe, as long as instances of this class are used only within a single thread.
+ */
+ private final class Closure<T> {
+ private final QueryExpression expression;
+ /** A dynamically-populated mapping from test_suite rules to their tests. */
+ private final Map<T, Set<T>> testsInSuite = new HashMap<>();
+
+ /** The environment in which this query is being evaluated. */
+ private final QueryEnvironment<T> env;
+
+ private final boolean strict;
+
+ private Closure(QueryExpression expression, QueryEnvironment<T> env) {
+ this.expression = expression;
+ this.env = env;
+ this.strict = env.isSettingEnabled(Setting.TESTS_EXPRESSION_STRICT);
+ }
+
+ /**
+ * Computes and returns the set of test rules in a particular suite. Uses
+ * dynamic programming---a memoized version of {@link #computeTestsInSuite}.
+ *
+ * @precondition env.getAccessor().isTestSuite(testSuite)
+ */
+ private Set<T> getTestsInSuite(T testSuite) throws QueryException {
+ Set<T> tests = testsInSuite.get(testSuite);
+ if (tests == null) {
+ tests = Sets.newHashSet();
+ testsInSuite.put(testSuite, tests); // break cycles by inserting empty set early.
+ computeTestsInSuite(testSuite, tests);
+ }
+ return tests;
+ }
+
+ /**
+ * Populates 'result' with all the tests associated with the specified
+ * 'testSuite'. Throws an exception if any target is missing.
+ *
+ * <p>CAUTION! Keep this logic consistent with {@code TestsSuiteConfiguredTarget}!
+ *
+ * @precondition env.getAccessor().isTestSuite(testSuite)
+ */
+ private void computeTestsInSuite(T testSuite, Set<T> result) throws QueryException {
+ List<T> testsAndSuites = new ArrayList<>();
+ // Note that testsAndSuites can contain input file targets; the test_suite rule does not
+ // restrict the set of targets that can appear in tests or suites.
+ testsAndSuites.addAll(getPrerequisites(testSuite, "tests"));
+ testsAndSuites.addAll(getPrerequisites(testSuite, "suites"));
+
+ // 1. Add all tests
+ for (T test : testsAndSuites) {
+ if (env.getAccessor().isTestRule(test)) {
+ result.add(test);
+ } else if (strict && !env.getAccessor().isTestSuite(test)) {
+ // If strict mode is enabled, then give an error for any non-test, non-test-suite targets.
+ env.reportBuildFileError(expression, "The label '"
+ + env.getAccessor().getLabel(test) + "' in the test_suite '"
+ + env.getAccessor().getLabel(testSuite) + "' does not refer to a test or test_suite "
+ + "rule!");
+ }
+ }
+
+ // 2. Add implicit dependencies on tests in same package, if any.
+ for (T target : getPrerequisites(testSuite, "$implicit_tests")) {
+ // The Package construction of $implicit_tests ensures that this check never fails, but we
+ // add it here anyway for compatibility with future code.
+ if (env.getAccessor().isTestRule(target)) {
+ result.add(target);
+ }
+ }
+
+ // 3. Filter based on tags, size, env.
+ filterTests(testSuite, result);
+
+ // 4. Expand all suites recursively.
+ for (T suite : testsAndSuites) {
+ if (env.getAccessor().isTestSuite(suite)) {
+ result.addAll(getTestsInSuite(suite));
+ }
+ }
+ }
+
+ /**
+ * Returns the set of rules named by the attribute 'attrName' of test_suite rule 'testSuite'.
+ * The attribute must be a list of labels. If a target cannot be resolved, then an error is
+ * reported to the environment (which may throw an exception if {@code keep_going} is disabled).
+ *
+ * @precondition env.getAccessor().isTestSuite(testSuite)
+ */
+ private List<T> getPrerequisites(T testSuite, String attrName) throws QueryException {
+ return env.getAccessor().getLabelListAttr(expression, testSuite, attrName,
+ "couldn't expand '" + attrName
+ + "' attribute of test_suite " + env.getAccessor().getLabel(testSuite) + ": ");
+ }
+
+ /**
+ * Filters 'tests' (by mutation) according to the 'tags' attribute, specifically those that
+ * match ALL of the tags in tagsAttribute.
+ *
+ * @precondition {@code env.getAccessor().isTestSuite(testSuite)}
+ * @precondition {@code env.getAccessor().isTestRule(test)} for all test in tests
+ */
+ private void filterTests(T testSuite, Set<T> tests) {
+ List<String> tagsAttribute = env.getAccessor().getStringListAttr(testSuite, "tags");
+ // Split the tags list into positive and negative tags
+ Set<String> requiredTags = new HashSet<>();
+ Set<String> excludedTags = new HashSet<>();
+ sortTagsBySense(tagsAttribute, requiredTags, excludedTags);
+
+ Iterator<T> it = tests.iterator();
+ while (it.hasNext()) {
+ T test = it.next();
+ List<String> testTags = new ArrayList<>(env.getAccessor().getStringListAttr(test, "tags"));
+ testTags.add(env.getAccessor().getStringAttr(test, "size"));
+ if (!includeTest(testTags, requiredTags, excludedTags)) {
+ it.remove();
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/query2/output/GraphOutputFormatter.java b/src/main/java/com/google/devtools/build/lib/query2/output/GraphOutputFormatter.java
new file mode 100644
index 0000000..5ded5e2
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/query2/output/GraphOutputFormatter.java
@@ -0,0 +1,174 @@
+// Copyright 2014 Google Inc. 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.query2.output;
+
+import com.google.devtools.build.lib.collect.CollectionUtils;
+import com.google.devtools.build.lib.collect.EquivalenceRelation;
+import com.google.devtools.build.lib.graph.Digraph;
+import com.google.devtools.build.lib.graph.DotOutputVisitor;
+import com.google.devtools.build.lib.graph.LabelSerializer;
+import com.google.devtools.build.lib.graph.Node;
+import com.google.devtools.build.lib.packages.Target;
+
+import java.io.PrintStream;
+import java.io.PrintWriter;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * An output formatter that prints the result as factored graph in AT&T
+ * GraphViz format.
+ */
+class GraphOutputFormatter extends OutputFormatter {
+
+ private int graphNodeStringLimit;
+ private boolean graphFactored;
+
+ @Override
+ public String getName() {
+ return "graph";
+ }
+
+ @Override
+ public void output(QueryOptions options, Digraph<Target> result, PrintStream out) {
+ this.graphNodeStringLimit = options.graphNodeStringLimit;
+ this.graphFactored = options.graphFactored;
+
+ if (graphFactored) {
+ outputFactored(result, new PrintWriter(out));
+ } else {
+ outputUnfactored(result, new PrintWriter(out));
+ }
+ }
+
+ private void outputUnfactored(Digraph<Target> result, PrintWriter out) {
+ result.visitNodesBeforeEdges(
+ new DotOutputVisitor<Target>(out, LABEL_STRINGIFIER) {
+ @Override
+ public void beginVisit() {
+ super.beginVisit();
+ // TODO(bazel-team): (2009) make this the default in Digraph.
+ out.println(" node [shape=box];");
+ }
+ });
+ }
+
+ private void outputFactored(Digraph<Target> result, PrintWriter out) {
+ EquivalenceRelation<Node<Target>> equivalenceRelation = createEquivalenceRelation();
+
+ // Notes on ordering:
+ // - Digraph.getNodes() returns nodes in no particular order
+ // - CollectionUtils.partition inserts elements into unordered sets
+ // This means partitions may contain nodes in a different order than perhaps expected.
+ // Example (package //foo):
+ // some_rule(
+ // name = 'foo',
+ // srcs = ['a', 'b', 'c'],
+ // )
+ // Querying for deps('foo') will return (among others) the 'foo' node with successors 'a', 'b'
+ // and 'c' (in this order), however when asking the Digraph for all of its nodes, the returned
+ // collection may be ordered differently.
+ Collection<Set<Node<Target>>> partition =
+ CollectionUtils.partition(result.getNodes(), equivalenceRelation);
+
+ Digraph<Set<Node<Target>>> factoredGraph = result.createImageUnderPartition(partition);
+
+ // Concatenate the labels of all topologically-equivalent nodes.
+ LabelSerializer<Set<Node<Target>>> labelSerializer = new LabelSerializer<Set<Node<Target>>>() {
+ @Override
+ public String serialize(Node<Set<Node<Target>>> node) {
+ int actualLimit = graphNodeStringLimit - RESERVED_LABEL_CHARS;
+ boolean firstItem = true;
+ StringBuffer buf = new StringBuffer();
+ int count = 0;
+ for (Node<Target> eqNode : node.getLabel()) {
+ String labelString = eqNode.getLabel().getLabel().toString();
+ if (!firstItem) {
+ buf.append("\\n");
+
+ // Use -1 to denote no limit, as it is easier than trying to pass MAX_INT on the cmdline
+ if (graphNodeStringLimit != -1 && (buf.length() + labelString.length() > actualLimit)) {
+ buf.append("...and ");
+ buf.append(node.getLabel().size() - count);
+ buf.append(" more items");
+ break;
+ }
+ }
+
+ buf.append(labelString);
+ count++;
+ firstItem = false;
+ }
+ return buf.toString();
+ }
+ };
+
+ factoredGraph.visitNodesBeforeEdges(
+ new DotOutputVisitor<Set<Node<Target>>>(out, labelSerializer) {
+ @Override
+ public void beginVisit() {
+ super.beginVisit();
+ // TODO(bazel-team): (2009) make this the default in Digraph.
+ out.println(" node [shape=box];");
+ }
+ });
+ }
+
+ /**
+ * Returns an equivalence relation for nodes in the specified graph.
+ *
+ * <p>Two nodes are considered equal iff they have equal topology (predecessors and successors).
+ *
+ * TODO(bazel-team): Make this a method of Digraph.
+ */
+ private static <LABEL> EquivalenceRelation<Node<LABEL>> createEquivalenceRelation() {
+ return new EquivalenceRelation<Node<LABEL>>() {
+ @Override
+ public int compare(Node<LABEL> x, Node<LABEL> y) {
+ if (x == y) {
+ return 0;
+ }
+
+ if (x.numPredecessors() != y.numPredecessors()
+ || x.numSuccessors() != y.numSuccessors()) {
+ return -1;
+ }
+
+ Set<Node<LABEL>> xpred = new HashSet<>(x.getPredecessors());
+ Set<Node<LABEL>> ypred = new HashSet<>(y.getPredecessors());
+ if (!xpred.equals(ypred)) {
+ return -1;
+ }
+
+ Set<Node<LABEL>> xsucc = new HashSet<>(x.getSuccessors());
+ Set<Node<LABEL>> ysucc = new HashSet<>(y.getSuccessors());
+ if (!xsucc.equals(ysucc)) {
+ return -1;
+ }
+
+ return 0;
+ }
+ };
+ }
+
+ private static final int RESERVED_LABEL_CHARS = "\\n...and 9999999 more items".length();
+
+ private static final LabelSerializer<Target> LABEL_STRINGIFIER = new LabelSerializer<Target>() {
+ @Override
+ public String serialize(Node<Target> node) {
+ return node.getLabel().getLabel().toString();
+ }
+ };
+}
diff --git a/src/main/java/com/google/devtools/build/lib/query2/output/OutputFormatter.java b/src/main/java/com/google/devtools/build/lib/query2/output/OutputFormatter.java
new file mode 100644
index 0000000..b7a9d64
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/query2/output/OutputFormatter.java
@@ -0,0 +1,486 @@
+// Copyright 2014 Google Inc. 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.query2.output;
+
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.graph.Digraph;
+import com.google.devtools.build.lib.graph.Node;
+import com.google.devtools.build.lib.packages.AggregatingAttributeMapper;
+import com.google.devtools.build.lib.packages.Attribute;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.syntax.EvalUtils;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.util.BinaryPredicate;
+import com.google.devtools.build.lib.util.Pair;
+import com.google.devtools.common.options.EnumConverter;
+
+import java.io.IOException;
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Interface for classes which order, format and print the result of a Blaze
+ * graph query.
+ */
+public abstract class OutputFormatter {
+
+ /**
+ * Discriminator for different kinds of OutputFormatter.
+ */
+ public enum Type {
+ LABEL,
+ LABEL_KIND,
+ BUILD,
+ MINRANK,
+ MAXRANK,
+ PACKAGE,
+ LOCATION,
+ GRAPH,
+ XML,
+ PROTO,
+ RECORD,
+ }
+
+ /**
+ * Where the value of an attribute comes from
+ */
+ protected enum AttributeValueSource {
+ RULE, // Explicitly specified on the rule
+ PACKAGE, // Package default
+ DEFAULT // Rule class default
+ }
+
+ public static final Function<Node<Target>, Target> EXTRACT_NODE_LABEL =
+ new Function<Node<Target>, Target>() {
+ @Override
+ public Target apply(Node<Target> input) {
+ return input.getLabel();
+ }
+ };
+
+ /**
+ * Converter from strings to OutputFormatter.Type.
+ */
+ public static class Converter extends EnumConverter<Type> {
+ public Converter() { super(Type.class, "output formatter"); }
+ }
+
+ public static ImmutableList<OutputFormatter> getDefaultFormatters() {
+ return ImmutableList.of(
+ new LabelOutputFormatter(false),
+ new LabelOutputFormatter(true),
+ new BuildOutputFormatter(),
+ new MinrankOutputFormatter(),
+ new MaxrankOutputFormatter(),
+ new PackageOutputFormatter(),
+ new LocationOutputFormatter(),
+ new GraphOutputFormatter(),
+ new XmlOutputFormatter(),
+ new ProtoOutputFormatter());
+ }
+
+ public static String formatterNames(Iterable<OutputFormatter> formatters) {
+ return Joiner.on(", ").join(Iterables.transform(formatters,
+ new Function<OutputFormatter, String>() {
+ @Override
+ public String apply(OutputFormatter input) {
+ return input.getName();
+ }
+ }));
+ }
+
+ /**
+ * Returns the output formatter for the specified command-line options.
+ */
+ public static OutputFormatter getFormatter(
+ Iterable<OutputFormatter> formatters, String type) {
+ for (OutputFormatter formatter : formatters) {
+ if (formatter.getName().equals(type)) {
+ return formatter;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Given a set of query options, returns a BinaryPredicate suitable for
+ * passing to {@link Rule#getLabels()}, {@link XmlOutputFormatter}, etc.
+ */
+ public static BinaryPredicate<Rule, Attribute> getDependencyFilter(QueryOptions queryOptions) {
+ // TODO(bazel-team): Optimize: and(ALL_DEPS, x) -> x, etc.
+ return Rule.and(
+ queryOptions.includeHostDeps ? Rule.ALL_DEPS : Rule.NO_HOST_DEPS,
+ queryOptions.includeImplicitDeps ? Rule.ALL_DEPS : Rule.NO_IMPLICIT_DEPS);
+ }
+
+ /**
+ * Format the result (a set of target nodes implicitly ordered according to
+ * the graph maintained by the QueryEnvironment), and print it to "out".
+ */
+ public abstract void output(QueryOptions options, Digraph<Target> result, PrintStream out)
+ throws IOException;
+
+ /**
+ * Unordered output formatter (wrt. dependency ordering).
+ *
+ * <p>Formatters that support unordered output may be used when only the set of query results is
+ * requested but their ordering is irrelevant.
+ *
+ * <p>The benefit of using a unordered formatter is that we can save the potentially expensive
+ * subgraph extraction step before presenting the query results.
+ */
+ public interface UnorderedFormatter {
+ void outputUnordered(QueryOptions options, Iterable<Target> result, PrintStream out)
+ throws IOException;
+ }
+
+ /**
+ * Returns the user-visible name of the output formatter.
+ */
+ public abstract String getName();
+
+ /**
+ * An output formatter that prints the labels of the resulting target set in
+ * topological order, optionally with the target's kind.
+ */
+ private static class LabelOutputFormatter extends OutputFormatter implements UnorderedFormatter{
+
+ private final boolean showKind;
+
+ public LabelOutputFormatter(boolean showKind) {
+ this.showKind = showKind;
+ }
+
+ @Override
+ public String getName() {
+ return showKind ? "label_kind" : "label";
+ }
+
+ @Override
+ public void outputUnordered(QueryOptions options, Iterable<Target> result, PrintStream out) {
+ for (Target target : result) {
+ if (showKind) {
+ out.print(target.getTargetKind());
+ out.print(' ');
+ }
+ out.println(target.getLabel());
+ }
+ }
+
+ @Override
+ public void output(QueryOptions options, Digraph<Target> result, PrintStream out) {
+ Iterable<Target> ordered = Iterables.transform(
+ result.getTopologicalOrder(new TargetOrdering()), EXTRACT_NODE_LABEL);
+ outputUnordered(options, ordered, out);
+ }
+ }
+
+ /**
+ * An ordering of Targets based on the ordering of their labels.
+ */
+ static class TargetOrdering implements Comparator<Target> {
+ @Override
+ public int compare(Target o1, Target o2) {
+ return o1.getLabel().compareTo(o2.getLabel());
+ }
+ }
+
+ /**
+ * An output formatter that prints the names of the packages of the target
+ * set, in lexicographical order without duplicates.
+ */
+ private static class PackageOutputFormatter extends OutputFormatter implements
+ UnorderedFormatter {
+ @Override
+ public String getName() {
+ return "package";
+ }
+
+ @Override
+ public void outputUnordered(QueryOptions options, Iterable<Target> result, PrintStream out) {
+ Set<String> packageNames = Sets.newTreeSet();
+ for (Target target : result) {
+ packageNames.add(target.getLabel().getPackageName());
+ }
+ for (String packageName : packageNames) {
+ out.println(packageName);
+ }
+ }
+
+ @Override
+ public void output(QueryOptions options, Digraph<Target> result, PrintStream out) {
+ Iterable<Target> ordered = Iterables.transform(
+ result.getTopologicalOrder(new TargetOrdering()), EXTRACT_NODE_LABEL);
+ outputUnordered(options, ordered, out);
+ }
+ }
+
+ /**
+ * An output formatter that prints the labels of the targets, preceded by
+ * their locations and kinds, in topological order. For output files, the
+ * location of the generating rule is given; for input files, the location of
+ * line 1 is given.
+ */
+ private static class LocationOutputFormatter extends OutputFormatter implements
+ UnorderedFormatter {
+ @Override
+ public String getName() {
+ return "location";
+ }
+
+ @Override
+ public void outputUnordered(QueryOptions options, Iterable<Target> result, PrintStream out) {
+ for (Target target : result) {
+ Location location = target.getLocation();
+ out.println(location.print() + ": " + target.getTargetKind() + " " + target.getLabel());
+ }
+ }
+
+ @Override
+ public void output(QueryOptions options, Digraph<Target> result, PrintStream out) {
+ Iterable<Target> ordered = Iterables.transform(
+ result.getTopologicalOrder(new TargetOrdering()), EXTRACT_NODE_LABEL);
+ outputUnordered(options, ordered, out);
+ }
+ }
+
+ /**
+ * An output formatter that prints the generating rules using the syntax of
+ * the BUILD files. If multiple targets are generated by the same rule, it is
+ * printed only once.
+ */
+ private static class BuildOutputFormatter extends OutputFormatter implements UnorderedFormatter {
+ @Override
+ public String getName() {
+ return "build";
+ }
+
+ private void outputRule(Rule rule, PrintStream out) {
+ out.println(String.format("# %s", rule.getLocation()));
+ out.println(String.format("%s(", rule.getRuleClass()));
+ out.println(String.format(" name = \"%s\",", rule.getName()));
+
+ for (Attribute attr : rule.getAttributes()) {
+ Pair<Iterable<Object>, AttributeValueSource> values = getAttributeValues(rule, attr);
+ if (Iterables.size(values.first) != 1) {
+ continue; // TODO(bazel-team): handle configurable attributes.
+ }
+ if (values.second != AttributeValueSource.RULE) {
+ continue; // Don't print default values.
+ }
+ Object value = Iterables.getOnlyElement(values.first);
+ out.print(String.format(" %s = ", attr.getName()));
+ if (value instanceof Label) {
+ value = value.toString();
+ } else if (value instanceof List<?> && EvalUtils.isImmutable(value)) {
+ // Display it as a list (and not as a tuple). Attributes can never be tuples.
+ value = new ArrayList<>((List<?>) value);
+ }
+ EvalUtils.prettyPrintValue(value, out);
+ out.println(",");
+ }
+ out.println(String.format(")\n"));
+ }
+
+ @Override
+ public void outputUnordered(QueryOptions options, Iterable<Target> result, PrintStream out) {
+ Set<Label> printed = new HashSet<>();
+ for (Target target : result) {
+ Rule rule = target.getAssociatedRule();
+ if (rule == null || printed.contains(rule.getLabel())) {
+ continue;
+ }
+ outputRule(rule, out);
+ printed.add(rule.getLabel());
+ }
+ }
+
+ @Override
+ public void output(QueryOptions options, Digraph<Target> result, PrintStream out) {
+ Iterable<Target> ordered = Iterables.transform(
+ result.getTopologicalOrder(new TargetOrdering()), EXTRACT_NODE_LABEL);
+ outputUnordered(options, ordered, out);
+ }
+ }
+
+ /**
+ * An output formatter that prints the labels in minimum rank order, preceded by
+ * their rank number. "Roots" have rank 0, their direct prerequisites have
+ * rank 1, etc. All nodes in a cycle are considered of equal rank. MINRANK
+ * shows the lowest rank for a given node, i.e. the length of the shortest
+ * path from a zero-rank node to it.
+ *
+ * If the result came from a <code>deps(x)</code> query, then the MINRANKs
+ * correspond to the shortest path from x to each of its prerequisites.
+ */
+ private static class MinrankOutputFormatter extends OutputFormatter {
+ @Override
+ public String getName() {
+ return "minrank";
+ }
+
+ @Override
+ public void output(QueryOptions options, Digraph<Target> result, PrintStream out) {
+ // getRoots() isn't defined for cyclic graphs, so in order to handle
+ // cycles correctly, we need work on the strong component graph, as
+ // cycles should be treated a "clump" of nodes all on the same rank.
+ // Graphs may contain cycles because there are errors in BUILD files.
+
+ Digraph<Set<Node<Target>>> scGraph = result.getStrongComponentGraph();
+ Set<Node<Set<Node<Target>>>> rankNodes = scGraph.getRoots();
+ Set<Node<Set<Node<Target>>>> seen = new HashSet<>();
+ seen.addAll(rankNodes);
+ for (int rank = 0; !rankNodes.isEmpty(); rank++) {
+ // Print out this rank:
+ for (Node<Set<Node<Target>>> xScc : rankNodes) {
+ for (Node<Target> x : xScc.getLabel()) {
+ out.println(rank + " " + x.getLabel().getLabel());
+ }
+ }
+
+ // Find the next rank:
+ Set<Node<Set<Node<Target>>>> nextRankNodes = new LinkedHashSet<>();
+ for (Node<Set<Node<Target>>> x : rankNodes) {
+ for (Node<Set<Node<Target>>> y : x.getSuccessors()) {
+ if (seen.add(y)) {
+ nextRankNodes.add(y);
+ }
+ }
+ }
+ rankNodes = nextRankNodes;
+ }
+ }
+ }
+
+ /**
+ * An output formatter that prints the labels in maximum rank order, preceded
+ * by their rank number. "Roots" have rank 0, all other nodes have a rank
+ * which is one greater than the maximum rank of each of their predecessors.
+ * All nodes in a cycle are considered of equal rank. MAXRANK shows the
+ * highest rank for a given node, i.e. the length of the longest non-cyclic
+ * path from a zero-rank node to it.
+ *
+ * If the result came from a <code>deps(x)</code> query, then the MAXRANKs
+ * correspond to the longest path from x to each of its prerequisites.
+ */
+ private static class MaxrankOutputFormatter extends OutputFormatter {
+ @Override
+ public String getName() {
+ return "maxrank";
+ }
+
+ @Override
+ public void output(QueryOptions options, Digraph<Target> result, PrintStream out) {
+ // In order to handle cycles correctly, we need work on the strong
+ // component graph, as cycles should be treated a "clump" of nodes all on
+ // the same rank. Graphs may contain cycles because there are errors in BUILD files.
+
+ // Dynamic programming algorithm:
+ // rank(x) = max(rank(p)) + 1 foreach p in preds(x)
+ // TODO(bazel-team): Move to Digraph.
+ class DP {
+ final Map<Node<Set<Node<Target>>>, Integer> ranks = new HashMap<>();
+
+ int rank(Node<Set<Node<Target>>> node) {
+ Integer rank = ranks.get(node);
+ if (rank == null) {
+ int maxPredRank = -1;
+ for (Node<Set<Node<Target>>> p : node.getPredecessors()) {
+ maxPredRank = Math.max(maxPredRank, rank(p));
+ }
+ rank = maxPredRank + 1;
+ ranks.put(node, rank);
+ }
+ return rank;
+ }
+ }
+ DP dp = new DP();
+
+ // Now sort by rank...
+ List<Pair<Integer, Label>> output = new ArrayList<>();
+ for (Node<Set<Node<Target>>> x : result.getStrongComponentGraph().getNodes()) {
+ int rank = dp.rank(x);
+ for (Node<Target> y : x.getLabel()) {
+ output.add(Pair.of(rank, y.getLabel().getLabel()));
+ }
+ }
+ Collections.sort(output, new Comparator<Pair<Integer, Label>>() {
+ @Override
+ public int compare(Pair<Integer, Label> x, Pair<Integer, Label> y) {
+ return x.first - y.first;
+ }
+ });
+
+ for (Pair<Integer, Label> pair : output) {
+ out.println(pair.first + " " + pair.second);
+ }
+ }
+ }
+
+ /**
+ * Returns the possible values of the specified attribute in the specified rule. For
+ * non-configured attributes, this is a single value. For configurable attributes, this
+ * may be multiple values.
+ *
+ * <p>This is needed because the visibility attribute is replaced with an empty list
+ * during package loading if it is public or private in order not to visit
+ * the package called 'visibility'.
+ *
+ * @return a pair, where the first value is the set of possible values and the
+ * second is an enum that tells where the values come from (declared on the
+ * rule, declared as a package level default or a
+ * global default)
+ */
+ protected static Pair<Iterable<Object>, AttributeValueSource> getAttributeValues(
+ Rule rule, Attribute attr) {
+ List<Object> values = new LinkedList<>(); // Not an ImmutableList: may host null values.
+ AttributeValueSource source;
+
+ if (attr.getName().equals("visibility")) {
+ values.add(rule.getVisibility().getDeclaredLabels());
+ if (rule.isVisibilitySpecified()) {
+ source = AttributeValueSource.RULE;
+ } else if (rule.getPackage().isDefaultVisibilitySet()) {
+ source = AttributeValueSource.PACKAGE;
+ } else {
+ source = AttributeValueSource.DEFAULT;
+ }
+ } else {
+ for (Object o :
+ AggregatingAttributeMapper.of(rule).visitAttribute(attr.getName(), attr.getType())) {
+ values.add(o);
+ }
+ source = rule.isAttributeValueExplicitlySpecified(attr)
+ ? AttributeValueSource.RULE : AttributeValueSource.DEFAULT;
+ }
+
+ return Pair.of((Iterable<Object>) values, source);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/query2/output/ProtoOutputFormatter.java b/src/main/java/com/google/devtools/build/lib/query2/output/ProtoOutputFormatter.java
new file mode 100644
index 0000000..53fbb21
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/query2/output/ProtoOutputFormatter.java
@@ -0,0 +1,491 @@
+// Copyright 2014 Google Inc. 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.query2.output;
+
+import static com.google.devtools.build.lib.packages.Type.BOOLEAN;
+import static com.google.devtools.build.lib.packages.Type.DISTRIBUTIONS;
+import static com.google.devtools.build.lib.packages.Type.FILESET_ENTRY_LIST;
+import static com.google.devtools.build.lib.packages.Type.INTEGER;
+import static com.google.devtools.build.lib.packages.Type.INTEGER_LIST;
+import static com.google.devtools.build.lib.packages.Type.LABEL;
+import static com.google.devtools.build.lib.packages.Type.LABEL_LIST;
+import static com.google.devtools.build.lib.packages.Type.LABEL_LIST_DICT;
+import static com.google.devtools.build.lib.packages.Type.LICENSE;
+import static com.google.devtools.build.lib.packages.Type.NODEP_LABEL;
+import static com.google.devtools.build.lib.packages.Type.NODEP_LABEL_LIST;
+import static com.google.devtools.build.lib.packages.Type.OUTPUT;
+import static com.google.devtools.build.lib.packages.Type.OUTPUT_LIST;
+import static com.google.devtools.build.lib.packages.Type.STRING;
+import static com.google.devtools.build.lib.packages.Type.STRING_DICT;
+import static com.google.devtools.build.lib.packages.Type.STRING_DICT_UNARY;
+import static com.google.devtools.build.lib.packages.Type.STRING_LIST;
+import static com.google.devtools.build.lib.packages.Type.STRING_LIST_DICT;
+import static com.google.devtools.build.lib.packages.Type.TRISTATE;
+import static com.google.devtools.build.lib.query2.proto.proto2api.Build.Target.Discriminator.GENERATED_FILE;
+import static com.google.devtools.build.lib.query2.proto.proto2api.Build.Target.Discriminator.PACKAGE_GROUP;
+import static com.google.devtools.build.lib.query2.proto.proto2api.Build.Target.Discriminator.RULE;
+import static com.google.devtools.build.lib.query2.proto.proto2api.Build.Target.Discriminator.SOURCE_FILE;
+
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.graph.Digraph;
+import com.google.devtools.build.lib.packages.Attribute;
+import com.google.devtools.build.lib.packages.InputFile;
+import com.google.devtools.build.lib.packages.License;
+import com.google.devtools.build.lib.packages.OutputFile;
+import com.google.devtools.build.lib.packages.PackageGroup;
+import com.google.devtools.build.lib.packages.ProtoUtils;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.packages.TriState;
+import com.google.devtools.build.lib.query2.FakeSubincludeTarget;
+import com.google.devtools.build.lib.query2.output.OutputFormatter.UnorderedFormatter;
+import com.google.devtools.build.lib.query2.proto.proto2api.Build;
+import com.google.devtools.build.lib.syntax.FilesetEntry;
+import com.google.devtools.build.lib.syntax.GlobCriteria;
+import com.google.devtools.build.lib.syntax.GlobList;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.syntax.SkylarkEnvironment;
+import com.google.devtools.build.lib.util.BinaryPredicate;
+
+import java.io.IOException;
+import java.io.PrintStream;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * An output formatter that outputs a protocol buffer representation
+ * of a query result and outputs the proto bytes to the output print stream.
+ * By taking the bytes and calling {@code mergeFrom()} on a
+ * {@code Build.QueryResult} object the full result can be reconstructed.
+ */
+public class ProtoOutputFormatter extends OutputFormatter implements UnorderedFormatter {
+
+ /**
+ * A special attribute name for the rule implementation hash code.
+ */
+ public static final String RULE_IMPLEMENTATION_HASH_ATTR_NAME = "$rule_implementation_hash";
+
+ private BinaryPredicate<Rule, Attribute> dependencyFilter;
+
+ protected void setDependencyFilter(QueryOptions options) {
+ this.dependencyFilter = OutputFormatter.getDependencyFilter(options);
+ }
+
+ @Override
+ public String getName() {
+ return "proto";
+ }
+
+ @Override
+ public void outputUnordered(QueryOptions options, Iterable<Target> result, PrintStream out)
+ throws IOException {
+ setDependencyFilter(options);
+
+ Build.QueryResult.Builder queryResult = Build.QueryResult.newBuilder();
+ for (Target target : result) {
+ addTarget(queryResult, target);
+ }
+
+ queryResult.build().writeTo(out);
+ }
+
+ @Override
+ public void output(QueryOptions options, Digraph<Target> result, PrintStream out)
+ throws IOException {
+ outputUnordered(options, result.getLabels(), out);
+ }
+
+ /**
+ * Add the target to the query result.
+ * @param queryResult The query result that contains all rule, input and
+ * output targets.
+ * @param target The query target being converted to a protocol buffer.
+ */
+ private void addTarget(Build.QueryResult.Builder queryResult, Target target) {
+ queryResult.addTarget(toTargetProtoBuffer(target));
+ }
+
+ /**
+ * Converts a logical Target object into a Target protobuffer.
+ */
+ protected Build.Target toTargetProtoBuffer(Target target) {
+ Build.Target.Builder targetPb = Build.Target.newBuilder();
+
+ String location = target.getLocation().print();
+ if (target instanceof Rule) {
+ Rule rule = (Rule) target;
+ Build.Rule.Builder rulePb = Build.Rule.newBuilder()
+ .setName(rule.getLabel().toString())
+ .setRuleClass(rule.getRuleClass())
+ .setLocation(location);
+
+ for (Attribute attr : rule.getAttributes()) {
+ addAttributeToProto(rulePb, attr, getAttributeValues(rule, attr).first, null,
+ rule.isAttributeValueExplicitlySpecified(attr), false);
+ }
+
+ SkylarkEnvironment env = rule.getRuleClassObject().getRuleDefinitionEnvironment();
+ if (env != null) {
+ // The RuleDefinitionEnvironment is always defined for Skylark rules and
+ // always null for non Skylark rules.
+ rulePb.addAttribute(
+ Build.Attribute.newBuilder()
+ .setName(RULE_IMPLEMENTATION_HASH_ATTR_NAME)
+ .setType(ProtoUtils.getDiscriminatorFromType(
+ com.google.devtools.build.lib.packages.Type.STRING))
+ .setStringValue(env.getTransitiveFileContentHashCode()));
+ }
+
+ // Include explicit elements for all direct inputs and outputs of a rule;
+ // this goes beyond what is available from the attributes above, since it
+ // may also (depending on options) include implicit outputs,
+ // host-configuration outputs, and default values.
+ for (Label label : rule.getLabels(dependencyFilter)) {
+ rulePb.addRuleInput(label.toString());
+ }
+ for (OutputFile outputFile : rule.getOutputFiles()) {
+ Label fileLabel = outputFile.getLabel();
+ rulePb.addRuleOutput(fileLabel.toString());
+ }
+ for (String feature : rule.getFeatures()) {
+ rulePb.addDefaultSetting(feature);
+ }
+
+ targetPb.setType(RULE);
+ targetPb.setRule(rulePb);
+ } else if (target instanceof OutputFile) {
+ OutputFile outputFile = (OutputFile) target;
+ Label label = outputFile.getLabel();
+
+ Rule generatingRule = outputFile.getGeneratingRule();
+ Build.GeneratedFile output = Build.GeneratedFile.newBuilder()
+ .setLocation(location)
+ .setGeneratingRule(generatingRule.getLabel().toString())
+ .setName(label.toString())
+ .build();
+
+ targetPb.setType(GENERATED_FILE);
+ targetPb.setGeneratedFile(output);
+ } else if (target instanceof InputFile) {
+ InputFile inputFile = (InputFile) target;
+ Label label = inputFile.getLabel();
+
+ Build.SourceFile.Builder input = Build.SourceFile.newBuilder()
+ .setLocation(location)
+ .setName(label.toString());
+
+ if (inputFile.getName().equals("BUILD")) {
+ for (Label subinclude : inputFile.getPackage().getSubincludeLabels()) {
+ input.addSubinclude(subinclude.toString());
+ }
+
+ for (Label skylarkFileDep : inputFile.getPackage().getSkylarkFileDependencies()) {
+ input.addSubinclude(skylarkFileDep.toString());
+ }
+
+ for (String feature : inputFile.getPackage().getFeatures()) {
+ input.addFeature(feature);
+ }
+ }
+
+ for (Label visibilityDependency : target.getVisibility().getDependencyLabels()) {
+ input.addPackageGroup(visibilityDependency.toString());
+ }
+
+ for (Label visibilityDeclaration : target.getVisibility().getDeclaredLabels()) {
+ input.addVisibilityLabel(visibilityDeclaration.toString());
+ }
+
+ targetPb.setType(SOURCE_FILE);
+ targetPb.setSourceFile(input);
+ } else if (target instanceof FakeSubincludeTarget) {
+ Label label = target.getLabel();
+ Build.SourceFile input = Build.SourceFile.newBuilder()
+ .setLocation(location)
+ .setName(label.toString())
+ .build();
+
+ targetPb.setType(SOURCE_FILE);
+ targetPb.setSourceFile(input);
+ } else if (target instanceof PackageGroup) {
+ PackageGroup packageGroup = (PackageGroup) target;
+ Build.PackageGroup.Builder packageGroupPb = Build.PackageGroup.newBuilder()
+ .setName(packageGroup.getLabel().toString());
+ for (String containedPackage : packageGroup.getContainedPackages()) {
+ packageGroupPb.addContainedPackage(containedPackage);
+ }
+ for (Label include : packageGroup.getIncludes()) {
+ packageGroupPb.addIncludedPackageGroup(include.toString());
+ }
+
+ targetPb.setType(PACKAGE_GROUP);
+ targetPb.setPackageGroup(packageGroupPb);
+ } else {
+ throw new IllegalArgumentException(target.toString());
+ }
+
+ return targetPb.build();
+ }
+
+ /**
+ * Adds the serialized version of the specified attribute to the specified message.
+ *
+ * @param rulePb the message to amend
+ * @param attr the attribute to add
+ * @param value the possible values of the attribute (can be a multi-value list for
+ * configurable attributes)
+ * @param location the location of the attribute in the source file
+ * @param explicitlySpecified whether the attribute was explicitly specified or not
+ * @param includeGlobs add glob expression for attributes that contain them
+ */
+ @SuppressWarnings("unchecked")
+ public static void addAttributeToProto(
+ Build.Rule.Builder rulePb, Attribute attr, Iterable<Object> values,
+ Location location, Boolean explicitlySpecified, boolean includeGlobs) {
+ // Get the attribute type. We need to convert and add appropriately
+ com.google.devtools.build.lib.packages.Type<?> type = attr.getType();
+
+ Build.Attribute.Builder attrPb = Build.Attribute.newBuilder();
+
+ // Set the type, name and source
+ attrPb.setName(attr.getName());
+ attrPb.setType(ProtoUtils.getDiscriminatorFromType(type));
+
+ if (location != null) {
+ attrPb.setParseableLocation(serialize(location));
+ }
+
+ if (explicitlySpecified != null) {
+ attrPb.setExplicitlySpecified(explicitlySpecified);
+ }
+
+ // Convenience binding for single-value attributes. Because those attributes can only
+ // have a single value, when we encounter configurable versions of them we need to
+ // react somehow to having multiple possible values to report. We currently just
+ // refrain from setting *any* value in that scenario. This variable is set to null
+ // to indicate that scenario.
+ Object singleAttributeValue = Iterables.size(values) == 1
+ ? Iterables.getOnlyElement(values)
+ : null;
+
+ /*
+ * Set the appropriate type and value. Since string and string list store
+ * values for multiple types, use the toString() method on the objects
+ * instead of casting them. Note that Boolean and TriState attributes have
+ * both an integer and string representation.
+ */
+ if (type == INTEGER) {
+ if (singleAttributeValue != null) {
+ attrPb.setIntValue((Integer) singleAttributeValue);
+ }
+ } else if (type == STRING || type == LABEL || type == NODEP_LABEL || type == OUTPUT) {
+ if (singleAttributeValue != null) {
+ attrPb.setStringValue(singleAttributeValue.toString());
+ }
+ } else if (type == STRING_LIST || type == LABEL_LIST || type == NODEP_LABEL_LIST
+ || type == OUTPUT_LIST || type == DISTRIBUTIONS) {
+ for (Object value : values) {
+ for (Object entry : (Collection<?>) value) {
+ attrPb.addStringListValue(entry.toString());
+ }
+ }
+ } else if (type == INTEGER_LIST) {
+ for (Object value : values) {
+ for (Integer entry : (Collection<Integer>) value) {
+ attrPb.addIntListValue(entry);
+ }
+ }
+ } else if (type == BOOLEAN) {
+ if (singleAttributeValue != null) {
+ if ((Boolean) singleAttributeValue) {
+ attrPb.setStringValue("true");
+ attrPb.setBooleanValue(true);
+ } else {
+ attrPb.setStringValue("false");
+ attrPb.setBooleanValue(false);
+ }
+ // This maintains partial backward compatibility for external users of the
+ // protobuf that were expecting an integer field and not a true boolean.
+ attrPb.setIntValue((Boolean) singleAttributeValue ? 1 : 0);
+ }
+ } else if (type == TRISTATE) {
+ if (singleAttributeValue != null) {
+ switch ((TriState) singleAttributeValue) {
+ case AUTO:
+ attrPb.setIntValue(-1);
+ attrPb.setStringValue("auto");
+ attrPb.setTristateValue(Build.Attribute.Tristate.AUTO);
+ break;
+ case NO:
+ attrPb.setIntValue(0);
+ attrPb.setStringValue("no");
+ attrPb.setTristateValue(Build.Attribute.Tristate.NO);
+ break;
+ case YES:
+ attrPb.setIntValue(1);
+ attrPb.setStringValue("yes");
+ attrPb.setTristateValue(Build.Attribute.Tristate.YES);
+ break;
+ default:
+ throw new IllegalStateException("Execpted AUTO/NO/YES to cover all possible cases");
+ }
+ }
+ } else if (type == LICENSE) {
+ if (singleAttributeValue != null) {
+ License license = (License) singleAttributeValue;
+ Build.License.Builder licensePb = Build.License.newBuilder();
+ for (License.LicenseType licenseType : license.getLicenseTypes()) {
+ licensePb.addLicenseType(licenseType.toString());
+ }
+ for (Label exception : license.getExceptions()) {
+ licensePb.addException(exception.toString());
+ }
+ attrPb.setLicense(licensePb);
+ }
+ } else if (type == STRING_DICT) {
+ // TODO(bazel-team): support better de-duping here and in other dictionaries.
+ for (Object value : values) {
+ Map<String, String> dict = (Map<String, String>) value;
+ for (Map.Entry<String, String> keyValueList : dict.entrySet()) {
+ Build.StringDictEntry entry = Build.StringDictEntry.newBuilder()
+ .setKey(keyValueList.getKey())
+ .setValue(keyValueList.getValue())
+ .build();
+ attrPb.addStringDictValue(entry);
+ }
+ }
+ } else if (type == STRING_DICT_UNARY) {
+ for (Object value : values) {
+ Map<String, String> dict = (Map<String, String>) value;
+ for (Map.Entry<String, String> dictEntry : dict.entrySet()) {
+ Build.StringDictUnaryEntry entry = Build.StringDictUnaryEntry.newBuilder()
+ .setKey(dictEntry.getKey())
+ .setValue(dictEntry.getValue())
+ .build();
+ attrPb.addStringDictUnaryValue(entry);
+ }
+ }
+ } else if (type == STRING_LIST_DICT) {
+ for (Object value : values) {
+ Map<String, List<String>> dict = (Map<String, List<String>>) value;
+ for (Map.Entry<String, List<String>> dictEntry : dict.entrySet()) {
+ Build.StringListDictEntry.Builder entry = Build.StringListDictEntry.newBuilder()
+ .setKey(dictEntry.getKey());
+ for (Object dictEntryValue : dictEntry.getValue()) {
+ entry.addValue(dictEntryValue.toString());
+ }
+ attrPb.addStringListDictValue(entry);
+ }
+ }
+ } else if (type == LABEL_LIST_DICT) {
+ for (Object value : values) {
+ Map<String, List<Label>> dict = (Map<String, List<Label>>) value;
+ for (Map.Entry<String, List<Label>> dictEntry : dict.entrySet()) {
+ Build.LabelListDictEntry.Builder entry = Build.LabelListDictEntry.newBuilder()
+ .setKey(dictEntry.getKey());
+ for (Object dictEntryValue : dictEntry.getValue()) {
+ entry.addValue(dictEntryValue.toString());
+ }
+ attrPb.addLabelListDictValue(entry);
+ }
+ }
+ } else if (type == FILESET_ENTRY_LIST) {
+ for (Object value : values) {
+ List<FilesetEntry> filesetEntries = (List<FilesetEntry>) value;
+ for (FilesetEntry filesetEntry : filesetEntries) {
+ Build.FilesetEntry.Builder filesetEntryPb = Build.FilesetEntry.newBuilder()
+ .setSource(filesetEntry.getSrcLabel().toString())
+ .setDestinationDirectory(filesetEntry.getDestDir().getPathString())
+ .setSymlinkBehavior(symlinkBehaviorToPb(filesetEntry.getSymlinkBehavior()))
+ .setStripPrefix(filesetEntry.getStripPrefix())
+ .setFilesPresent(filesetEntry.getFiles() != null);
+
+ if (filesetEntry.getFiles() != null) {
+ for (Label file : filesetEntry.getFiles()) {
+ filesetEntryPb.addFile(file.toString());
+ }
+ }
+
+ if (filesetEntry.getExcludes() != null) {
+ for (String exclude : filesetEntry.getExcludes()) {
+ filesetEntryPb.addExclude(exclude);
+ }
+ }
+
+ attrPb.addFilesetListValue(filesetEntryPb);
+ }
+ }
+ } else {
+ throw new IllegalStateException("Unknown type: " + type);
+ }
+
+ if (includeGlobs) {
+ for (Object value : values) {
+ if (value instanceof GlobList<?>) {
+ GlobList<?> globList = (GlobList<?>) value;
+
+ for (GlobCriteria criteria : globList.getCriteria()) {
+ Build.GlobCriteria.Builder criteriaPb = Build.GlobCriteria.newBuilder()
+ .setGlob(criteria.isGlob());
+ for (String include : criteria.getIncludePatterns()) {
+ criteriaPb.addInclude(include);
+ }
+ for (String exclude : criteria.getExcludePatterns()) {
+ criteriaPb.addExclude(exclude);
+ }
+
+ attrPb.addGlobCriteria(criteriaPb);
+ }
+ }
+ }
+ }
+
+ rulePb.addAttribute(attrPb);
+ }
+
+ // This is needed because I do not want to use the SymlinkBehavior from the
+ // protocol buffer all over the place, so there are two classes that do
+ // essentially the same thing.
+ private static Build.FilesetEntry.SymlinkBehavior symlinkBehaviorToPb(
+ FilesetEntry.SymlinkBehavior symlinkBehavior) {
+ switch (symlinkBehavior) {
+ case COPY:
+ return Build.FilesetEntry.SymlinkBehavior.COPY;
+ case DEREFERENCE:
+ return Build.FilesetEntry.SymlinkBehavior.DEREFERENCE;
+ default:
+ throw new AssertionError("Unhandled FilesetEntry.SymlinkBehavior");
+ }
+ }
+
+ private static Build.Location serialize(Location location) {
+ Build.Location.Builder result = Build.Location.newBuilder();
+
+ result.setStartOffset(location.getStartOffset());
+ if (location.getStartLineAndColumn() != null) {
+ result.setStartLine(location.getStartLineAndColumn().getLine());
+ result.setStartColumn(location.getStartLineAndColumn().getColumn());
+ }
+
+ result.setEndOffset(location.getEndOffset());
+ if (location.getEndLineAndColumn() != null) {
+ result.setEndLine(location.getEndLineAndColumn().getLine());
+ result.setEndColumn(location.getEndLineAndColumn().getColumn());
+ }
+
+ return result.build();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/query2/output/QueryOptions.java b/src/main/java/com/google/devtools/build/lib/query2/output/QueryOptions.java
new file mode 100644
index 0000000..810e8c7
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/query2/output/QueryOptions.java
@@ -0,0 +1,136 @@
+// Copyright 2014 Google Inc. 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.query2.output;
+
+import com.google.devtools.build.lib.query2.engine.QueryEnvironment.Setting;
+import com.google.devtools.common.options.Option;
+import com.google.devtools.common.options.OptionsBase;
+
+import java.util.EnumSet;
+import java.util.Set;
+
+/**
+ * Command-line options for the Blaze query language, revision 2.
+ */
+public class QueryOptions extends OptionsBase {
+
+ @Option(name = "output",
+ defaultValue = "label",
+ category = "query",
+ help = "The format in which the query results should be printed."
+ + " Allowed values are: label, label_kind, minrank, maxrank, package, location, graph,"
+ + " xml, proto, record.")
+ public String outputFormat;
+
+ @Option(name = "order_results",
+ defaultValue = "true",
+ category = "query",
+ help = "Output the results in dependency-ordered (default) or unordered fashion. The"
+ + " unordered output is faster but only supported when --output is one of label,"
+ + " label_kind, location, package, proto, record, xml.")
+ public boolean orderResults;
+
+ @Option(name = "keep_going",
+ abbrev = 'k',
+ defaultValue = "false",
+ category = "strategy",
+ help = "Continue as much as possible after an error. While the "
+ + "target that failed, and those that depend on it, cannot be "
+ + "analyzed, other prerequisites of these "
+ + "targets can be.")
+ public boolean keepGoing;
+
+ @Option(name = "loading_phase_threads",
+ defaultValue = "200",
+ category = "undocumented",
+ help = "Number of parallel threads to use for the loading phase.")
+ public int loadingPhaseThreads;
+
+ @Option(name = "host_deps",
+ defaultValue = "true",
+ category = "query",
+ help = "If enabled, dependencies on 'host configuration' targets will be included in "
+ + "the dependency graph over which the query operates. A 'host configuration' "
+ + "dependency edge, such as the one from any 'proto_library' rule to the Protocol "
+ + "Compiler, usually points to a tool executed during the build (on the host machine) "
+ + "rather than a part of the same 'target' program. Queries whose purpose is to "
+ + "discover the set of things needed during a build will typically enable this option; "
+ + "queries aimed at revealing the structure of a single program will typically disable "
+ + "this option.")
+ public boolean includeHostDeps;
+
+ @Option(name = "implicit_deps",
+ defaultValue = "true",
+ category = "query",
+ help = "If enabled, implicit dependencies will be included in the dependency graph over "
+ + "which the query operates. An implicit dependency is one that is not explicitly "
+ + "specified in the BUILD file but added by blaze.")
+ public boolean includeImplicitDeps;
+
+ @Option(name = "graph:node_limit",
+ defaultValue = "512",
+ category = "query",
+ help = "The maximum length of the label string for a graph node in the output. Longer labels"
+ + " will be truncated; -1 means no truncation. This option is only applicable to"
+ + " --output=graph.")
+ public int graphNodeStringLimit;
+
+ @Option(name = "graph:factored",
+ defaultValue = "true",
+ category = "query",
+ help = "If true, then the graph will be emitted 'factored', i.e. "
+ + "topologically-equivalent nodes will be merged together and their "
+ + "labels concatenated. This option is only applicable to "
+ + "--output=graph.")
+ public boolean graphFactored;
+
+ @Option(name = "xml:line_numbers",
+ defaultValue = "true",
+ category = "query",
+ help = "If true, XML output contains line numbers. Disabling this option "
+ + "may make diffs easier to read. This option is only applicable to "
+ + "--output=xml.")
+ public boolean xmlLineNumbers;
+
+ @Option(name = "xml:default_values",
+ defaultValue = "false",
+ category = "query",
+ help = "If true, rule attributes whose value is not explicitly specified "
+ + "in the BUILD file are printed; otherwise they are omitted.")
+ public boolean xmlShowDefaultValues;
+
+ @Option(name = "strict_test_suite",
+ defaultValue = "false",
+ category = "query",
+ help = "If true, the tests() expression gives an error if it encounters a test_suite "
+ + "containing non-test targets.")
+ public boolean strictTestSuite;
+
+ /**
+ * Return the current options as a set of QueryEnvironment settings.
+ */
+ public Set<Setting> toSettings() {
+ Set<Setting> settings = EnumSet.noneOf(Setting.class);
+ if (strictTestSuite) {
+ settings.add(Setting.TESTS_EXPRESSION_STRICT);
+ }
+ if (!includeHostDeps) {
+ settings.add(Setting.NO_HOST_DEPS);
+ }
+ if (!includeImplicitDeps) {
+ settings.add(Setting.NO_IMPLICIT_DEPS);
+ }
+ return settings;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/query2/output/XmlOutputFormatter.java b/src/main/java/com/google/devtools/build/lib/query2/output/XmlOutputFormatter.java
new file mode 100644
index 0000000..c01c90e4
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/query2/output/XmlOutputFormatter.java
@@ -0,0 +1,352 @@
+// Copyright 2014 Google Inc. 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.query2.output;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.graph.Digraph;
+import com.google.devtools.build.lib.packages.Attribute;
+import com.google.devtools.build.lib.packages.InputFile;
+import com.google.devtools.build.lib.packages.License;
+import com.google.devtools.build.lib.packages.OutputFile;
+import com.google.devtools.build.lib.packages.PackageGroup;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.query2.FakeSubincludeTarget;
+import com.google.devtools.build.lib.syntax.FilesetEntry;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.util.BinaryPredicate;
+import com.google.devtools.build.lib.util.Pair;
+
+import org.w3c.dom.DOMException;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import java.io.PrintStream;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.TransformerFactoryConfigurationError;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+
+/**
+ * An output formatter that prints the result as XML.
+ */
+class XmlOutputFormatter extends OutputFormatter implements OutputFormatter.UnorderedFormatter {
+
+ private boolean xmlLineNumbers;
+ private boolean showDefaultValues;
+ private BinaryPredicate<Rule, Attribute> dependencyFilter;
+
+ @Override
+ public String getName() {
+ return "xml";
+ }
+
+ @Override
+ public void outputUnordered(QueryOptions options, Iterable<Target> result, PrintStream out) {
+ this.xmlLineNumbers = options.xmlLineNumbers;
+ this.showDefaultValues = options.xmlShowDefaultValues;
+ this.dependencyFilter = OutputFormatter.getDependencyFilter(options);
+
+ Document doc;
+ try {
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ doc = factory.newDocumentBuilder().newDocument();
+ } catch (ParserConfigurationException e) {
+ // This shouldn't be possible: all the configuration is hard-coded.
+ throw new IllegalStateException("XML output failed", e);
+ }
+ doc.setXmlVersion("1.1");
+ Element queryElem = doc.createElement("query");
+ queryElem.setAttribute("version", "2");
+ doc.appendChild(queryElem);
+ for (Target target : result) {
+ queryElem.appendChild(createTargetElement(doc, target));
+ }
+ try {
+ Transformer transformer = TransformerFactory.newInstance().newTransformer();
+ transformer.setOutputProperty(OutputKeys.INDENT, "yes");
+ transformer.transform(new DOMSource(doc), new StreamResult(out));
+ } catch (TransformerFactoryConfigurationError | TransformerException e) {
+ // This shouldn't be possible: all the configuration is hard-coded.
+ throw new IllegalStateException("XML output failed", e);
+ }
+ }
+
+ @Override
+ public void output(QueryOptions options, Digraph<Target> result, PrintStream out) {
+ Iterable<Target> ordered = Iterables.transform(
+ result.getTopologicalOrder(new TargetOrdering()), OutputFormatter.EXTRACT_NODE_LABEL);
+ outputUnordered(options, ordered, out);
+ }
+
+ /**
+ * Creates and returns a new DOM tree for the specified build target.
+ *
+ * XML structure:
+ * - element tag is <source-file>, <generated-file> or <rule
+ * class="cc_library">, following the terminology of
+ * {@link Target#getTargetKind()}.
+ * - 'name' attribute is target's label.
+ * - 'location' attribute is consistent with output of --output location.
+ * - rule attributes are represented in the DOM structure.
+ */
+ private Element createTargetElement(Document doc, Target target) {
+ Element elem;
+ if (target instanceof Rule) {
+ Rule rule = (Rule) target;
+ elem = doc.createElement("rule");
+ elem.setAttribute("class", rule.getRuleClass());
+ for (Attribute attr: rule.getAttributes()) {
+ Pair<Iterable<Object>, AttributeValueSource> values = getAttributeValues(rule, attr);
+ if (values.second == AttributeValueSource.RULE || showDefaultValues) {
+ Element attrElem = createValueElement(doc, attr.getType(), values.first);
+ attrElem.setAttribute("name", attr.getName());
+ elem.appendChild(attrElem);
+ }
+ }
+
+ // Include explicit elements for all direct inputs and outputs of a rule;
+ // this goes beyond what is available from the attributes above, since it
+ // may also (depending on options) include implicit outputs,
+ // host-configuration outputs, and default values.
+ for (Label label : rule.getLabels(dependencyFilter)) {
+ Element inputElem = doc.createElement("rule-input");
+ inputElem.setAttribute("name", label.toString());
+ elem.appendChild(inputElem);
+ }
+ for (OutputFile outputFile: rule.getOutputFiles()) {
+ Element outputElem = doc.createElement("rule-output");
+ outputElem.setAttribute("name", outputFile.getLabel().toString());
+ elem.appendChild(outputElem);
+ }
+ for (String feature : rule.getFeatures()) {
+ Element outputElem = doc.createElement("rule-default-setting");
+ outputElem.setAttribute("name", feature);
+ elem.appendChild(outputElem);
+ }
+ } else if (target instanceof PackageGroup) {
+ PackageGroup packageGroup = (PackageGroup) target;
+ elem = doc.createElement("package-group");
+ elem.setAttribute("name", packageGroup.getName());
+ Element includes = createValueElement(doc,
+ com.google.devtools.build.lib.packages.Type.LABEL_LIST,
+ packageGroup.getIncludes());
+ includes.setAttribute("name", "includes");
+ elem.appendChild(includes);
+ Element packages = createValueElement(doc,
+ com.google.devtools.build.lib.packages.Type.STRING_LIST,
+ packageGroup.getContainedPackages());
+ packages.setAttribute("name", "packages");
+ elem.appendChild(packages);
+ } else if (target instanceof OutputFile) {
+ OutputFile outputFile = (OutputFile) target;
+ elem = doc.createElement("generated-file");
+ elem.setAttribute("generating-rule",
+ outputFile.getGeneratingRule().getLabel().toString());
+ } else if (target instanceof InputFile) {
+ elem = doc.createElement("source-file");
+ InputFile inputFile = (InputFile) target;
+ if (inputFile.getName().equals("BUILD")) {
+ addSubincludedFilesToElement(doc, elem, inputFile);
+ addSkylarkFilesToElement(doc, elem, inputFile);
+ addFeaturesToElement(doc, elem, inputFile);
+ }
+
+ addPackageGroupsToElement(doc, elem, inputFile);
+ } else if (target instanceof FakeSubincludeTarget) {
+ elem = doc.createElement("source-file");
+ } else {
+ throw new IllegalArgumentException(target.toString());
+ }
+
+ elem.setAttribute("name", target.getLabel().toString());
+ String location = target.getLocation().print();
+ if (!xmlLineNumbers) {
+ int firstColon = location.indexOf(":");
+ if (firstColon != -1) {
+ location = location.substring(0, firstColon);
+ }
+ }
+
+ elem.setAttribute("location", location);
+ return elem;
+ }
+
+ private void addPackageGroupsToElement(Document doc, Element parent, Target target) {
+ for (Label visibilityDependency : target.getVisibility().getDependencyLabels()) {
+ Element elem = doc.createElement("package-group");
+ elem.setAttribute("name", visibilityDependency.toString());
+ parent.appendChild(elem);
+ }
+
+ for (Label visibilityDeclaration : target.getVisibility().getDeclaredLabels()) {
+ Element elem = doc.createElement("visibility-label");
+ elem.setAttribute("name", visibilityDeclaration.toString());
+ parent.appendChild(elem);
+ }
+ }
+
+ private void addFeaturesToElement(Document doc, Element parent, InputFile inputFile) {
+ for (String feature : inputFile.getPackage().getFeatures()) {
+ Element elem = doc.createElement("feature");
+ elem.setAttribute("name", feature);
+ parent.appendChild(elem);
+ }
+ }
+
+ private void addSubincludedFilesToElement(Document doc, Element parent, InputFile inputFile) {
+ for (Label subinclude : inputFile.getPackage().getSubincludeLabels()) {
+ Element elem = doc.createElement("subinclude");
+ elem.setAttribute("name", subinclude.toString());
+ parent.appendChild(elem);
+ }
+ }
+
+ private void addSkylarkFilesToElement(Document doc, Element parent, InputFile inputFile) {
+ for (Label skylarkFileDep : inputFile.getPackage().getSkylarkFileDependencies()) {
+ Element elem = doc.createElement("load");
+ elem.setAttribute("name", skylarkFileDep.toString());
+ parent.appendChild(elem);
+ }
+ }
+
+ /**
+ * Creates and returns a new DOM tree for the specified attribute values.
+ * For non-configurable attributes, this is a single value. For configurable
+ * attributes, this contains one value for each configuration.
+ * (Only toplevel values are named attributes; list elements are unnamed.)
+ *
+ * <p>In the case of configurable attributes, multi-value attributes (e.g. lists)
+ * merge all configured lists into an aggregate flattened list. Single-value attributes
+ * simply refrain to set a value and annotate the DOM element as configurable.
+ *
+ * <P>(The ungainly qualified class name is required to avoid ambiguity with
+ * OutputFormatter.Type.)
+ */
+ private static Element createValueElement(Document doc,
+ com.google.devtools.build.lib.packages.Type<?> type, Iterable<Object> values) {
+ // "Import static" with method scope:
+ com.google.devtools.build.lib.packages.Type<?>
+ FILESET_ENTRY = com.google.devtools.build.lib.packages.Type.FILESET_ENTRY,
+ LABEL_LIST = com.google.devtools.build.lib.packages.Type.LABEL_LIST,
+ LICENSE = com.google.devtools.build.lib.packages.Type.LICENSE,
+ STRING_LIST = com.google.devtools.build.lib.packages.Type.STRING_LIST;
+
+ final Element elem;
+ final boolean hasMultipleValues = Iterables.size(values) > 1;
+ com.google.devtools.build.lib.packages.Type<?> elemType = type.getListElementType();
+ if (elemType != null) { // it's a list (includes "distribs")
+ elem = doc.createElement("list");
+ for (Object value : values) {
+ for (Object elemValue : (Collection<?>) value) {
+ elem.appendChild(createValueElement(doc, elemType, elemValue));
+ }
+ }
+ } else if (type instanceof com.google.devtools.build.lib.packages.Type.DictType) {
+ Set<Object> visitedValues = new HashSet<>();
+ elem = doc.createElement("dict");
+ com.google.devtools.build.lib.packages.Type.DictType<?, ?> dictType =
+ (com.google.devtools.build.lib.packages.Type.DictType<?, ?>) type;
+ for (Object value : values) {
+ for (Map.Entry<?, ?> entry : ((Map<?, ?>) value).entrySet()) {
+ if (visitedValues.add(entry.getKey())) {
+ Element pairElem = doc.createElement("pair");
+ elem.appendChild(pairElem);
+ pairElem.appendChild(createValueElement(doc,
+ dictType.getKeyType(), entry.getKey()));
+ pairElem.appendChild(createValueElement(doc,
+ dictType.getValueType(), entry.getValue()));
+ }
+ }
+ }
+ } else if (type == LICENSE) {
+ elem = createSingleValueElement(doc, "license", hasMultipleValues);
+ if (!hasMultipleValues) {
+ License license = (License) Iterables.getOnlyElement(values);
+
+ Element exceptions = createValueElement(doc, LABEL_LIST, license.getExceptions());
+ exceptions.setAttribute("name", "exceptions");
+ elem.appendChild(exceptions);
+
+ Element licenseTypes = createValueElement(doc, STRING_LIST, license.getLicenseTypes());
+ licenseTypes.setAttribute("name", "license-types");
+ elem.appendChild(licenseTypes);
+ }
+ } else if (type == FILESET_ENTRY) {
+ // Fileset entries: not configurable.
+ FilesetEntry filesetEntry = (FilesetEntry) Iterables.getOnlyElement(values);
+ elem = doc.createElement("fileset-entry");
+ elem.setAttribute("srcdir", filesetEntry.getSrcLabel().toString());
+ elem.setAttribute("destdir", filesetEntry.getDestDir().toString());
+ elem.setAttribute("symlinks", filesetEntry.getSymlinkBehavior().toString());
+ elem.setAttribute("strip_prefix", filesetEntry.getStripPrefix());
+
+ if (filesetEntry.getExcludes() != null) {
+ Element excludes =
+ createValueElement(doc, LABEL_LIST, filesetEntry.getExcludes());
+ excludes.setAttribute("name", "excludes");
+ elem.appendChild(excludes);
+ }
+ if (filesetEntry.getFiles() != null) {
+ Element files = createValueElement(doc, LABEL_LIST, filesetEntry.getFiles());
+ files.setAttribute("name", "files");
+ elem.appendChild(files);
+ }
+ } else { // INTEGER STRING LABEL DISTRIBUTION OUTPUT
+ elem = createSingleValueElement(doc, type.toString(), hasMultipleValues);
+ if (!hasMultipleValues && !Iterables.isEmpty(values)) {
+ Object value = Iterables.getOnlyElement(values);
+ // Values such as those of attribute "linkstamp" may be null.
+ if (value != null) {
+ try {
+ elem.setAttribute("value", value.toString());
+ } catch (DOMException e) {
+ elem.setAttribute("value", "[[[ERROR: could not be encoded as XML]]]");
+ }
+ }
+ }
+ }
+ return elem;
+ }
+
+ private static Element createValueElement(Document doc,
+ com.google.devtools.build.lib.packages.Type<?> type, Object value) {
+ return createValueElement(doc, type, ImmutableList.of(value));
+ }
+
+ /**
+ * Creates the given DOM element, adding <code>configurable="yes"</code> if it represents
+ * a configurable single-value attribute (configurable list attributes simply have their
+ * lists merged into an aggregate flat list).
+ */
+ private static Element createSingleValueElement(Document doc, String name,
+ boolean configurable) {
+ Element elem = doc.createElement(name);
+ if (configurable) {
+ elem.setAttribute("configurable", "yes");
+ }
+ return elem;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/RuleConfiguredTargetFactory.java b/src/main/java/com/google/devtools/build/lib/rules/RuleConfiguredTargetFactory.java
new file mode 100644
index 0000000..45df124
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/RuleConfiguredTargetFactory.java
@@ -0,0 +1,25 @@
+// Copyright 2014 Google Inc. 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.rules;
+
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.packages.RuleClass;
+
+/**
+ * A shortcut class to the appropriate specialization of {@code RuleClass.ConfiguredTargetFactory}.
+ */
+public interface RuleConfiguredTargetFactory
+ extends RuleClass.ConfiguredTargetFactory<ConfiguredTarget, RuleContext> {
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/SkylarkAttr.java b/src/main/java/com/google/devtools/build/lib/rules/SkylarkAttr.java
new file mode 100644
index 0000000..dfd6a92
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/SkylarkAttr.java
@@ -0,0 +1,350 @@
+// Copyright 2014 Google Inc. 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.rules;
+
+import static com.google.devtools.build.lib.syntax.SkylarkFunction.castList;
+
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.packages.Attribute;
+import com.google.devtools.build.lib.packages.Attribute.ConfigurationTransition;
+import com.google.devtools.build.lib.packages.Attribute.SkylarkLateBound;
+import com.google.devtools.build.lib.packages.SkylarkFileType;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.packages.Type.ConversionException;
+import com.google.devtools.build.lib.syntax.Environment;
+import com.google.devtools.build.lib.syntax.EvalException;
+import com.google.devtools.build.lib.syntax.FuncallExpression;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.syntax.SkylarkBuiltin;
+import com.google.devtools.build.lib.syntax.SkylarkBuiltin.Param;
+import com.google.devtools.build.lib.syntax.SkylarkCallbackFunction;
+import com.google.devtools.build.lib.syntax.SkylarkEnvironment;
+import com.google.devtools.build.lib.syntax.SkylarkFunction;
+import com.google.devtools.build.lib.syntax.SkylarkList;
+import com.google.devtools.build.lib.syntax.SkylarkModule;
+import com.google.devtools.build.lib.syntax.UserDefinedFunction;
+import com.google.devtools.build.lib.util.FileTypeSet;
+
+import java.util.Map;
+
+/**
+ * A helper class to provide Attr module in Skylark.
+ */
+@SkylarkModule(name = "attr", namespace = true, onlyLoadingPhase = true,
+ doc = "Module for creating new attributes. "
+ + "They are only for use with the <code>rule</code> function.")
+public final class SkylarkAttr {
+
+ private static final String MANDATORY_DOC =
+ "set to true if users have to explicitely specify the value";
+
+ private static final String ALLOW_FILES_DOC =
+ "whether File targets are allowed. Can be True, False (default), or "
+ + "a FileType filter.";
+
+ private static final String ALLOW_RULES_DOC =
+ "which rule targets (name of the classes) are allowed."
+ + "This is deprecated (kept only for compatiblity), use providers instead.";
+
+ private static final String FLAGS_DOC =
+ "deprecated, will be removed";
+
+ private static final String DEFAULT_DOC =
+ "sets the default value of the attribute.";
+
+ private static final String CONFIGURATION_DOC =
+ "configuration of the attribute. "
+ + "For example, use DATA_CFG or HOST_CFG.";
+
+ private static final String EXECUTABLE_DOC =
+ "set to True if the labels have to be executable. Access the labels with "
+ + "ctx.executable.<attribute_name>";
+
+ private static Attribute.Builder<?> createAttribute(Type<?> type, Map<String, Object> arguments,
+ FuncallExpression ast, SkylarkEnvironment env) throws EvalException, ConversionException {
+ final Location loc = ast.getLocation();
+ // We use an empty name now so that we can set it later.
+ // This trick makes sense only in the context of Skylark (builtin rules should not use it).
+ Attribute.Builder<?> builder = Attribute.attr("", type);
+
+ Object defaultValue = arguments.get("default");
+ if (defaultValue != null) {
+ if (defaultValue instanceof UserDefinedFunction) {
+ // Late bound attribute. Non label type attributes already caused a type check error.
+ builder.value(new SkylarkLateBound(
+ new SkylarkCallbackFunction((UserDefinedFunction) defaultValue, ast, env)));
+ } else {
+ builder.defaultValue(defaultValue);
+ }
+ }
+
+ for (String flag : castList(arguments.get("flags"), String.class)) {
+ builder.setPropertyFlag(flag);
+ }
+
+ if (arguments.containsKey("mandatory") && (Boolean) arguments.get("mandatory")) {
+ builder.setPropertyFlag("MANDATORY");
+ }
+
+ if (arguments.containsKey("executable") && (Boolean) arguments.get("executable")) {
+ builder.setPropertyFlag("EXECUTABLE");
+ }
+
+ if (arguments.containsKey("single_file") && (Boolean) arguments.get("single_file")) {
+ builder.setPropertyFlag("SINGLE_ARTIFACT");
+ }
+
+ if (arguments.containsKey("allow_files")) {
+ Object fileTypesObj = arguments.get("allow_files");
+ if (fileTypesObj == Boolean.TRUE) {
+ builder.allowedFileTypes(FileTypeSet.ANY_FILE);
+ } else if (fileTypesObj == Boolean.FALSE) {
+ builder.allowedFileTypes(FileTypeSet.NO_FILE);
+ } else if (fileTypesObj instanceof SkylarkFileType) {
+ builder.allowedFileTypes(((SkylarkFileType) fileTypesObj).getFileTypeSet());
+ } else {
+ throw new EvalException(loc, "allow_files should be a boolean or a filetype object.");
+ }
+ } else if (type.equals(Type.LABEL) || type.equals(Type.LABEL_LIST)) {
+ builder.allowedFileTypes(FileTypeSet.NO_FILE);
+ }
+
+ Object ruleClassesObj = arguments.get("allow_rules");
+ if (ruleClassesObj != null) {
+ builder.allowedRuleClasses(castList(ruleClassesObj, String.class,
+ "allowed rule classes for attribute definition"));
+ }
+
+ if (arguments.containsKey("providers")) {
+ builder.mandatoryProviders(castList(arguments.get("providers"), String.class));
+ }
+
+ if (arguments.containsKey("cfg")) {
+ builder.cfg((ConfigurationTransition) arguments.get("cfg"));
+ }
+ return builder;
+ }
+
+ private static Object createAttribute(Map<String, Object> kwargs, Type<?> type,
+ FuncallExpression ast, Environment env) throws EvalException {
+ try {
+ return createAttribute(type, kwargs, ast, (SkylarkEnvironment) env);
+ } catch (ConversionException e) {
+ throw new EvalException(ast.getLocation(), e.getMessage());
+ }
+ }
+
+ @SkylarkBuiltin(name = "int", doc =
+ "Creates an attribute of type int.",
+ objectType = SkylarkAttr.class,
+ returnType = Attribute.class,
+ optionalParams = {
+ @Param(name = "default", type = Integer.class,
+ doc = DEFAULT_DOC + " If not specified, default is 0."),
+ @Param(name = "flags", type = SkylarkList.class, generic1 = String.class, doc = FLAGS_DOC),
+ @Param(name = "mandatory", type = Boolean.class, doc = MANDATORY_DOC),
+ @Param(name = "cfg", type = ConfigurationTransition.class, doc = CONFIGURATION_DOC)})
+ private static SkylarkFunction integer = new SkylarkFunction("int") {
+ @Override
+ public Object call(Map<String, Object> kwargs, FuncallExpression ast, Environment env)
+ throws EvalException {
+ return createAttribute(kwargs, Type.INTEGER, ast, env);
+ }
+ };
+
+ @SkylarkBuiltin(name = "string", doc =
+ "Creates an attribute of type string.",
+ objectType = SkylarkAttr.class,
+ returnType = Attribute.class,
+ optionalParams = {
+ @Param(name = "default", type = String.class,
+ doc = DEFAULT_DOC + " If not specified, default is \"\"."),
+ @Param(name = "flags", type = SkylarkList.class, generic1 = String.class, doc = FLAGS_DOC),
+ @Param(name = "mandatory", type = Boolean.class, doc = MANDATORY_DOC),
+ @Param(name = "cfg", type = ConfigurationTransition.class, doc = CONFIGURATION_DOC)})
+ private static SkylarkFunction string = new SkylarkFunction("string") {
+ @Override
+ public Object call(Map<String, Object> kwargs, FuncallExpression ast, Environment env)
+ throws EvalException {
+ return createAttribute(kwargs, Type.STRING, ast, env);
+ }
+ };
+
+ @SkylarkBuiltin(name = "label", doc =
+ "Creates an attribute of type Label. "
+ + "It is the only way to specify a dependency to another target. "
+ + "If you need a dependency that the user cannot overwrite, make the attribute "
+ + "private (starts with <code>_</code>).",
+ objectType = SkylarkAttr.class,
+ returnType = Attribute.class,
+ optionalParams = {
+ @Param(name = "default", type = Label.class, callbackEnabled = true,
+ doc = DEFAULT_DOC + " If not specified, default is None. "
+ + "Use the <code>Label</code> function to specify a default value."),
+ @Param(name = "executable", type = Boolean.class, doc = EXECUTABLE_DOC),
+ @Param(name = "flags", type = SkylarkList.class, generic1 = String.class, doc = FLAGS_DOC),
+ @Param(name = "allow_files", doc = ALLOW_FILES_DOC),
+ @Param(name = "mandatory", type = Boolean.class, doc = MANDATORY_DOC),
+ @Param(name = "providers", type = SkylarkList.class, generic1 = String.class,
+ doc = "mandatory providers every dependency has to have"),
+ @Param(name = "allow_rules", type = SkylarkList.class, generic1 = String.class,
+ doc = ALLOW_RULES_DOC),
+ @Param(name = "single_file", doc =
+ "if true, the label must correspond to a single File. "
+ + "Access it through ctx.file.<attribute_name>."),
+ @Param(name = "cfg", type = ConfigurationTransition.class, doc = CONFIGURATION_DOC)})
+ private static SkylarkFunction label = new SkylarkFunction("label") {
+ @Override
+ public Object call(Map<String, Object> kwargs, FuncallExpression ast, Environment env)
+ throws EvalException {
+ return createAttribute(kwargs, Type.LABEL, ast, env);
+ }
+ };
+
+ @SkylarkBuiltin(name = "string_list", doc =
+ "Creates an attribute of type list of strings",
+ objectType = SkylarkAttr.class,
+ returnType = Attribute.class,
+ optionalParams = {
+ @Param(name = "default", type = SkylarkList.class, generic1 = String.class,
+ doc = DEFAULT_DOC + " If not specified, default is []."),
+ @Param(name = "flags", type = SkylarkList.class, generic1 = String.class, doc = FLAGS_DOC),
+ @Param(name = "mandatory", type = Boolean.class, doc = MANDATORY_DOC),
+ @Param(name = "cfg", type = ConfigurationTransition.class,
+ doc = CONFIGURATION_DOC)})
+ private static SkylarkFunction stringList = new SkylarkFunction("string_list") {
+ @Override
+ public Object call(Map<String, Object> kwargs, FuncallExpression ast, Environment env)
+ throws EvalException {
+ return createAttribute(kwargs, Type.STRING_LIST, ast, env);
+ }
+ };
+
+ @SkylarkBuiltin(name = "label_list", doc =
+ "Creates an attribute of type list of labels. "
+ + "See <code>label</code> for more information.",
+ objectType = SkylarkAttr.class,
+ returnType = Attribute.class,
+ optionalParams = {
+ @Param(name = "default", type = SkylarkList.class, generic1 = Label.class,
+ callbackEnabled = true,
+ doc = DEFAULT_DOC + " If not specified, default is []. "
+ + "Use the <code>Label</code> function to specify a default value."),
+ @Param(name = "flags", type = SkylarkList.class, generic1 = String.class, doc = FLAGS_DOC),
+ @Param(name = "allow_files", doc = ALLOW_FILES_DOC),
+ @Param(name = "mandatory", type = Boolean.class, doc = MANDATORY_DOC),
+ @Param(name = "allow_rules", type = SkylarkList.class, generic1 = String.class,
+ doc = ALLOW_RULES_DOC),
+ @Param(name = "providers", type = SkylarkList.class, generic1 = String.class,
+ doc = "mandatory providers every dependency has to have"),
+ @Param(name = "cfg", type = ConfigurationTransition.class, doc = CONFIGURATION_DOC)})
+ private static SkylarkFunction labelList = new SkylarkFunction("label_list") {
+ @Override
+ public Object call(Map<String, Object> kwargs, FuncallExpression ast, Environment env)
+ throws EvalException {
+ return createAttribute(kwargs, Type.LABEL_LIST, ast, env);
+ }
+ };
+
+ @SkylarkBuiltin(name = "bool", doc =
+ "Creates an attribute of type bool. Its default value is False.",
+ objectType = SkylarkAttr.class,
+ returnType = Attribute.class,
+ optionalParams = {
+ @Param(name = "default", type = Boolean.class, doc = DEFAULT_DOC),
+ @Param(name = "flags", type = SkylarkList.class, generic1 = String.class, doc = FLAGS_DOC),
+ @Param(name = "mandatory", type = Boolean.class, doc = MANDATORY_DOC),
+ @Param(name = "cfg", type = ConfigurationTransition.class, doc = CONFIGURATION_DOC)})
+ private static SkylarkFunction bool = new SkylarkFunction("bool") {
+ @Override
+ public Object call(Map<String, Object> kwargs, FuncallExpression ast, Environment env)
+ throws EvalException {
+ return createAttribute(kwargs, Type.BOOLEAN, ast, env);
+ }
+ };
+
+ @SkylarkBuiltin(name = "output", doc =
+ "Creates an attribute of type output. Its default value is None. "
+ + "The user provides a file name (string) and the rule must create an action that "
+ + "generates the file.",
+ objectType = SkylarkAttr.class,
+ returnType = Attribute.class,
+ optionalParams = {
+ @Param(name = "default", type = Label.class, doc = DEFAULT_DOC),
+ @Param(name = "flags", type = SkylarkList.class, generic1 = String.class, doc = FLAGS_DOC),
+ @Param(name = "mandatory", type = Boolean.class, doc = MANDATORY_DOC),
+ @Param(name = "cfg", type = ConfigurationTransition.class, doc = CONFIGURATION_DOC)})
+ private static SkylarkFunction output = new SkylarkFunction("output") {
+ @Override
+ public Object call(Map<String, Object> kwargs, FuncallExpression ast, Environment env)
+ throws EvalException {
+ return createAttribute(kwargs, Type.OUTPUT, ast, env);
+ }
+ };
+
+ @SkylarkBuiltin(name = "output_list", doc =
+ "Creates an attribute of type list of outputs. Its default value is []. "
+ + "See <code>output</code> above for more information.",
+ objectType = SkylarkAttr.class,
+ returnType = Attribute.class,
+ optionalParams = {
+ @Param(name = "default", type = SkylarkList.class, generic1 = Label.class, doc = DEFAULT_DOC),
+ @Param(name = "flags", type = SkylarkList.class, generic1 = String.class, doc = FLAGS_DOC),
+ @Param(name = "mandatory", type = Boolean.class, doc = MANDATORY_DOC),
+ @Param(name = "cfg", type = ConfigurationTransition.class, doc = CONFIGURATION_DOC)})
+ private static SkylarkFunction outputList = new SkylarkFunction("output_list") {
+ @Override
+ public Object call(Map<String, Object> kwargs, FuncallExpression ast, Environment env)
+ throws EvalException {
+ return createAttribute(kwargs, Type.OUTPUT_LIST, ast, env);
+ }
+ };
+
+ @SkylarkBuiltin(name = "string_dict", doc =
+ "Creates an attribute of type dictionary, mapping from string to string. "
+ + "Its default value is {}.",
+ objectType = SkylarkAttr.class,
+ returnType = Attribute.class,
+ optionalParams = {
+ @Param(name = "default", type = Map.class, doc = DEFAULT_DOC),
+ @Param(name = "flags", type = SkylarkList.class, generic1 = String.class, doc = FLAGS_DOC),
+ @Param(name = "mandatory", type = Boolean.class, doc = MANDATORY_DOC),
+ @Param(name = "cfg", type = ConfigurationTransition.class, doc = CONFIGURATION_DOC)})
+ private static SkylarkFunction stringDict = new SkylarkFunction("string_dict") {
+ @Override
+ public Object call(Map<String, Object> kwargs, FuncallExpression ast, Environment env)
+ throws EvalException {
+ return createAttribute(kwargs, Type.STRING_DICT, ast, env);
+ }
+ };
+
+ @SkylarkBuiltin(name = "license", doc =
+ "Creates an attribute of type license. Its default value is NO_LICENSE.",
+ // TODO(bazel-team): Implement proper license support for Skylark.
+ objectType = SkylarkAttr.class,
+ returnType = Attribute.class,
+ optionalParams = {
+ @Param(name = "default", doc = DEFAULT_DOC),
+ @Param(name = "flags", type = SkylarkList.class, generic1 = String.class, doc = FLAGS_DOC),
+ @Param(name = "mandatory", type = Boolean.class, doc = MANDATORY_DOC),
+ @Param(name = "cfg", type = ConfigurationTransition.class, doc = CONFIGURATION_DOC)})
+ private static SkylarkFunction license = new SkylarkFunction("license") {
+ @Override
+ public Object call(Map<String, Object> kwargs, FuncallExpression ast, Environment env)
+ throws EvalException {
+ return createAttribute(kwargs, Type.LICENSE, ast, env);
+ }
+ };
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/SkylarkCommandLine.java b/src/main/java/com/google/devtools/build/lib/rules/SkylarkCommandLine.java
new file mode 100644
index 0000000..e51805e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/SkylarkCommandLine.java
@@ -0,0 +1,89 @@
+// Copyright 2014 Google Inc. 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.rules;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.syntax.EvalException;
+import com.google.devtools.build.lib.syntax.SkylarkBuiltin;
+import com.google.devtools.build.lib.syntax.SkylarkBuiltin.Param;
+import com.google.devtools.build.lib.syntax.SkylarkFunction.SimpleSkylarkFunction;
+import com.google.devtools.build.lib.syntax.SkylarkList;
+import com.google.devtools.build.lib.syntax.SkylarkModule;
+import com.google.devtools.build.lib.syntax.SkylarkNestedSet;
+
+import java.util.Map;
+
+/**
+ * A Skylark module class to create memory efficient command lines.
+ */
+@SkylarkModule(name = "cmd_helper", namespace = true,
+ doc = "Module for creating memory efficient command lines.")
+public class SkylarkCommandLine {
+
+ @SkylarkBuiltin(name = "join_paths",
+ doc = "Creates a single command line argument joining the paths of a set "
+ + "of files on the separator string.",
+ objectType = SkylarkCommandLine.class,
+ returnType = String.class,
+ mandatoryParams = {
+ @Param(name = "separator", type = String.class, doc = "the separator string to join on"),
+ @Param(name = "files", type = SkylarkNestedSet.class, generic1 = Artifact.class,
+ doc = "the files to concatenate")})
+ private static SimpleSkylarkFunction joinPaths =
+ new SimpleSkylarkFunction("join_paths") {
+ @Override
+ public Object call(Map<String, Object> params, Location loc)
+ throws EvalException {
+ final String separator = (String) params.get("separator");
+ final NestedSet<Artifact> artifacts =
+ ((SkylarkNestedSet) params.get("files")).getSet(Artifact.class);
+ // TODO(bazel-team): lazy evaluate
+ return Artifact.joinExecPaths(separator, artifacts);
+ }
+ };
+
+ // TODO(bazel-team): this method should support sets of objects and substitute all struct fields.
+ @SkylarkBuiltin(name = "template",
+ doc = "Transforms a set of files to a list of strings using the template string.",
+ objectType = SkylarkCommandLine.class,
+ returnType = SkylarkList.class,
+ mandatoryParams = {
+ @Param(name = "items", type = SkylarkNestedSet.class, generic1 = Artifact.class,
+ doc = "The set of structs to transform."),
+ @Param(name = "template", type = String.class,
+ doc = "The template to use for the transformation, %{path} and %{short_path} "
+ + "being substituted with the corresponding fields of each file.")})
+ private static SimpleSkylarkFunction template = new SimpleSkylarkFunction("template") {
+ @Override
+ public Object call(Map<String, Object> params, Location loc)
+ throws EvalException {
+ final String template = (String) params.get("template");
+ SkylarkNestedSet items = (SkylarkNestedSet) params.get("items");
+ return SkylarkList.lazyList(Iterables.transform(items, new Function<Object, String>() {
+ @Override
+ public String apply(Object input) {
+ Artifact artifact = (Artifact) input;
+ return template
+ .replace("%{path}", artifact.getExecPathString())
+ .replace("%{short_path}", artifact.getRootRelativePathString());
+ }
+ }), String.class);
+ }
+ };
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/SkylarkModules.java b/src/main/java/com/google/devtools/build/lib/rules/SkylarkModules.java
new file mode 100644
index 0000000..ae81f81
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/SkylarkModules.java
@@ -0,0 +1,198 @@
+// Copyright 2014 Google Inc. 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.rules;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.collect.CollectionUtils;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.packages.MethodLibrary;
+import com.google.devtools.build.lib.syntax.Environment;
+import com.google.devtools.build.lib.syntax.Function;
+import com.google.devtools.build.lib.syntax.SkylarkBuiltin;
+import com.google.devtools.build.lib.syntax.SkylarkEnvironment;
+import com.google.devtools.build.lib.syntax.SkylarkFunction;
+import com.google.devtools.build.lib.syntax.SkylarkModule;
+import com.google.devtools.build.lib.syntax.SkylarkType;
+import com.google.devtools.build.lib.syntax.SkylarkType.SkylarkFunctionType;
+import com.google.devtools.build.lib.syntax.ValidationEnvironment;
+
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A class to handle all Skylark modules, to create and setup Validation and regular Environments.
+ */
+public class SkylarkModules {
+
+ public static final ImmutableList<Class<?>> MODULES = ImmutableList.of(
+ SkylarkAttr.class,
+ SkylarkCommandLine.class,
+ SkylarkRuleClassFunctions.class,
+ SkylarkRuleImplementationFunctions.class);
+
+ private static final ImmutableMap<Class<?>, ImmutableList<Function>> FUNCTION_MAP;
+ private static final ImmutableMap<String, Object> OBJECTS;
+
+ static {
+ try {
+ ImmutableMap.Builder<Class<?>, ImmutableList<Function>> functionMap = ImmutableMap.builder();
+ ImmutableMap.Builder<String, Object> objects = ImmutableMap.builder();
+ for (Class<?> moduleClass : MODULES) {
+ if (moduleClass.isAnnotationPresent(SkylarkModule.class)) {
+ objects.put(moduleClass.getAnnotation(SkylarkModule.class).name(),
+ moduleClass.newInstance());
+ }
+ ImmutableList.Builder<Function> functions = ImmutableList.builder();
+ collectSkylarkFunctionsAndObjectsFromFields(moduleClass, functions, objects);
+ functionMap.put(moduleClass, functions.build());
+ }
+ FUNCTION_MAP = functionMap.build();
+ OBJECTS = objects.build();
+ } catch (InstantiationException | IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Returns a new SkylarkEnvironment with the elements of the Skylark modules.
+ */
+ public static SkylarkEnvironment getNewEnvironment(
+ EventHandler eventHandler, String astFileContentHashCode) {
+ SkylarkEnvironment env = new SkylarkEnvironment(eventHandler, astFileContentHashCode);
+ setupEnvironment(env);
+ return env;
+ }
+
+ @VisibleForTesting
+ public static SkylarkEnvironment getNewEnvironment(EventHandler eventHandler) {
+ return getNewEnvironment(eventHandler, null);
+ }
+
+ private static void setupEnvironment(Environment env) {
+ MethodLibrary.setupMethodEnvironment(env);
+ for (Map.Entry<Class<?>, ImmutableList<Function>> entry : FUNCTION_MAP.entrySet()) {
+ for (Function function : entry.getValue()) {
+ if (function.getObjectType() != null) {
+ env.registerFunction(function.getObjectType(), function.getName(), function);
+ } else {
+ env.update(function.getName(), function);
+ }
+ }
+ }
+ for (Map.Entry<String, Object> entry : OBJECTS.entrySet()) {
+ env.update(entry.getKey(), entry.getValue());
+ }
+ }
+
+ /**
+ * Returns a new ValidationEnvironment with the elements of the Skylark modules.
+ */
+ public static ValidationEnvironment getValidationEnvironment() {
+ return getValidationEnvironment(ImmutableMap.<String, SkylarkType>of());
+ }
+
+ /**
+ * Returns a new ValidationEnvironment with the elements of the Skylark modules and extraObjects.
+ */
+ public static ValidationEnvironment getValidationEnvironment(
+ ImmutableMap<String, SkylarkType> extraObjects) {
+ Map<SkylarkType, Map<String, SkylarkType>> builtIn = new HashMap<>();
+ Map<String, SkylarkType> global = new HashMap<>();
+ builtIn.put(SkylarkType.GLOBAL, global);
+ collectSkylarkTypesFromFields(Environment.class, builtIn);
+ for (Class<?> moduleClass : MODULES) {
+ if (moduleClass.isAnnotationPresent(SkylarkModule.class)) {
+ global.put(moduleClass.getAnnotation(SkylarkModule.class).name(),
+ SkylarkType.of(moduleClass));
+ }
+ }
+ global.put("native", SkylarkType.UNKNOWN);
+ MethodLibrary.setupValidationEnvironment(builtIn);
+ for (Class<?> module : MODULES) {
+ collectSkylarkTypesFromFields(module, builtIn);
+ }
+ global.putAll(extraObjects);
+ return new ValidationEnvironment(CollectionUtils.toImmutable(builtIn));
+ }
+
+ /**
+ * Collects the SkylarkFunctions from the fields of the class of the object parameter
+ * and adds them into the builder.
+ */
+ private static void collectSkylarkFunctionsAndObjectsFromFields(Class<?> type,
+ ImmutableList.Builder<Function> functions, ImmutableMap.Builder<String, Object> objects) {
+ try {
+ for (Field field : type.getDeclaredFields()) {
+ if (field.isAnnotationPresent(SkylarkBuiltin.class)) {
+ // Fields in Skylark modules are sometimes private. Nevertheless they have to
+ // be annotated with SkylarkBuiltin.
+ field.setAccessible(true);
+ SkylarkBuiltin annotation = field.getAnnotation(SkylarkBuiltin.class);
+ if (SkylarkFunction.class.isAssignableFrom(field.getType())) {
+ SkylarkFunction function = (SkylarkFunction) field.get(null);
+ if (!function.isConfigured()) {
+ function.configure(annotation);
+ }
+ functions.add(function);
+ } else {
+ objects.put(annotation.name(), field.get(null));
+ }
+ }
+ }
+ } catch (IllegalArgumentException | IllegalAccessException e) {
+ // This should never happen.
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Collects the SkylarkFunctions from the fields of the class of the object parameter
+ * and adds their class and their corresponding return value to the builder.
+ */
+ private static void collectSkylarkTypesFromFields(Class<?> classObject,
+ Map<SkylarkType, Map<String, SkylarkType>> builtIn) {
+ for (Field field : classObject.getDeclaredFields()) {
+ if (field.isAnnotationPresent(SkylarkBuiltin.class)) {
+ SkylarkBuiltin annotation = field.getAnnotation(SkylarkBuiltin.class);
+ if (SkylarkFunction.class.isAssignableFrom(field.getType())) {
+ try {
+ // TODO(bazel-team): infer the correct types.
+ SkylarkType objectType = annotation.objectType().equals(Object.class)
+ ? SkylarkType.GLOBAL
+ : SkylarkType.of(annotation.objectType());
+ if (!builtIn.containsKey(objectType)) {
+ builtIn.put(objectType, new HashMap<String, SkylarkType>());
+ }
+ // TODO(bazel-team): add parameters to SkylarkFunctionType
+ SkylarkType returnType = SkylarkType.getReturnType(annotation);
+ builtIn.get(objectType).put(annotation.name(),
+ SkylarkFunctionType.of(annotation.name(), returnType));
+ } catch (IllegalArgumentException e) {
+ // This should never happen.
+ throw new RuntimeException(e);
+ }
+ } else if (Function.class.isAssignableFrom(field.getType())) {
+ builtIn.get(SkylarkType.GLOBAL).put(annotation.name(),
+ SkylarkFunctionType.of(annotation.name(), SkylarkType.UNKNOWN));
+ } else {
+ builtIn.get(SkylarkType.GLOBAL).put(annotation.name(), SkylarkType.of(field.getType()));
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/SkylarkRuleClassFunctions.java b/src/main/java/com/google/devtools/build/lib/rules/SkylarkRuleClassFunctions.java
new file mode 100644
index 0000000..39b8836
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/SkylarkRuleClassFunctions.java
@@ -0,0 +1,430 @@
+// Copyright 2014 Google Inc. 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.rules;
+
+import static com.google.devtools.build.lib.packages.Attribute.ConfigurationTransition.DATA;
+import static com.google.devtools.build.lib.packages.Attribute.ConfigurationTransition.HOST;
+import static com.google.devtools.build.lib.packages.Attribute.attr;
+import static com.google.devtools.build.lib.packages.Type.BOOLEAN;
+import static com.google.devtools.build.lib.packages.Type.INTEGER;
+import static com.google.devtools.build.lib.packages.Type.LABEL;
+import static com.google.devtools.build.lib.packages.Type.LABEL_LIST;
+import static com.google.devtools.build.lib.packages.Type.STRING;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.analysis.BaseRuleClasses;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.analysis.config.RunUnder;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.packages.Attribute;
+import com.google.devtools.build.lib.packages.Attribute.ConfigurationTransition;
+import com.google.devtools.build.lib.packages.Attribute.LateBoundLabel;
+import com.google.devtools.build.lib.packages.AttributeMap;
+import com.google.devtools.build.lib.packages.ImplicitOutputsFunction.SkylarkImplicitOutputsFunctionWithCallback;
+import com.google.devtools.build.lib.packages.ImplicitOutputsFunction.SkylarkImplicitOutputsFunctionWithMap;
+import com.google.devtools.build.lib.packages.Package.NameConflictException;
+import com.google.devtools.build.lib.packages.PackageFactory;
+import com.google.devtools.build.lib.packages.PackageFactory.PackageContext;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.RuleClass.Builder;
+import com.google.devtools.build.lib.packages.RuleClass.Builder.RuleClassType;
+import com.google.devtools.build.lib.packages.RuleFactory;
+import com.google.devtools.build.lib.packages.RuleFactory.InvalidRuleException;
+import com.google.devtools.build.lib.packages.SkylarkFileType;
+import com.google.devtools.build.lib.packages.TargetUtils;
+import com.google.devtools.build.lib.packages.TestSize;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.packages.Type.ConversionException;
+import com.google.devtools.build.lib.syntax.AbstractFunction;
+import com.google.devtools.build.lib.syntax.ClassObject;
+import com.google.devtools.build.lib.syntax.ClassObject.SkylarkClassObject;
+import com.google.devtools.build.lib.syntax.Environment;
+import com.google.devtools.build.lib.syntax.Environment.NoSuchVariableException;
+import com.google.devtools.build.lib.syntax.EvalException;
+import com.google.devtools.build.lib.syntax.EvalUtils;
+import com.google.devtools.build.lib.syntax.FuncallExpression;
+import com.google.devtools.build.lib.syntax.Function;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.syntax.SkylarkBuiltin;
+import com.google.devtools.build.lib.syntax.SkylarkBuiltin.Param;
+import com.google.devtools.build.lib.syntax.SkylarkCallbackFunction;
+import com.google.devtools.build.lib.syntax.SkylarkEnvironment;
+import com.google.devtools.build.lib.syntax.SkylarkFunction;
+import com.google.devtools.build.lib.syntax.SkylarkFunction.SimpleSkylarkFunction;
+import com.google.devtools.build.lib.syntax.SkylarkList;
+import com.google.devtools.build.lib.syntax.UserDefinedFunction;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+
+/**
+ * A helper class to provide an easier API for Skylark rule definitions.
+ * This is experimental code.
+ */
+public class SkylarkRuleClassFunctions {
+
+ //TODO(bazel-team): proper enum support
+ @SkylarkBuiltin(name = "DATA_CFG", returnType = ConfigurationTransition.class,
+ doc = "The default runfiles collection state.")
+ private static final Object dataTransition = ConfigurationTransition.DATA;
+
+ @SkylarkBuiltin(name = "HOST_CFG", returnType = ConfigurationTransition.class,
+ doc = "The default runfiles collection state.")
+ private static final Object hostTransition = ConfigurationTransition.HOST;
+
+ private static final Attribute.ComputedDefault DEPRECATION =
+ new Attribute.ComputedDefault() {
+ @Override
+ public Object getDefault(AttributeMap rule) {
+ return rule.getPackageDefaultDeprecation();
+ }
+ };
+
+ private static final Attribute.ComputedDefault TEST_ONLY =
+ new Attribute.ComputedDefault() {
+ @Override
+ public Object getDefault(AttributeMap rule) {
+ return rule.getPackageDefaultTestOnly();
+ }
+ };
+
+ private static final LateBoundLabel<BuildConfiguration> RUN_UNDER =
+ new LateBoundLabel<BuildConfiguration>() {
+ @Override
+ public Label getDefault(Rule rule, BuildConfiguration configuration) {
+ RunUnder runUnder = configuration.getRunUnder();
+ return runUnder == null ? null : runUnder.getLabel();
+ }
+ };
+
+ // TODO(bazel-team): Copied from ConfiguredRuleClassProvider for the transition from built-in
+ // rules to skylark extensions. Using the same instance would require a large refactoring.
+ // If we don't want to support old built-in rules and Skylark simultaneously
+ // (except for transition phase) it's probably OK.
+ private static LoadingCache<String, Label> labelCache =
+ CacheBuilder.newBuilder().build(new CacheLoader<String, Label>() {
+ @Override
+ public Label load(String from) throws Exception {
+ try {
+ return Label.parseAbsolute(from);
+ } catch (Label.SyntaxException e) {
+ throw new Exception(from);
+ }
+ }
+ });
+
+ // TODO(bazel-team): Remove the code duplication (BaseRuleClasses and this class).
+ private static final RuleClass baseRule =
+ BaseRuleClasses.commonCoreAndSkylarkAttributes(
+ new RuleClass.Builder("$base_rule", RuleClassType.ABSTRACT, true))
+ .add(attr("expect_failure", STRING))
+ .build();
+
+ private static final RuleClass testBaseRule =
+ new RuleClass.Builder("$test_base_rule", RuleClassType.ABSTRACT, true, baseRule)
+ .add(attr("size", STRING).value("medium").taggable()
+ .nonconfigurable("used in loading phase rule validation logic"))
+ .add(attr("timeout", STRING).taggable()
+ .nonconfigurable("used in loading phase rule validation logic").value(
+ new Attribute.ComputedDefault() {
+ @Override
+ public Object getDefault(AttributeMap rule) {
+ TestSize size = TestSize.getTestSize(rule.get("size", Type.STRING));
+ if (size != null) {
+ String timeout = size.getDefaultTimeout().toString();
+ if (timeout != null) {
+ return timeout;
+ }
+ }
+ return "illegal";
+ }
+ }))
+ .add(attr("flaky", BOOLEAN).value(false).taggable()
+ .nonconfigurable("taggable - called in Rule.getRuleTags"))
+ .add(attr("shard_count", INTEGER).value(-1))
+ .add(attr("local", BOOLEAN).value(false).taggable()
+ .nonconfigurable("policy decision: this should be consistent across configurations"))
+ .add(attr("$test_runtime", LABEL_LIST).cfg(HOST).value(ImmutableList.of(
+ labelCache.getUnchecked("//tools/test:runtime"))))
+ .add(attr(":run_under", LABEL).cfg(DATA).value(RUN_UNDER))
+ .build();
+
+ /**
+ * In native code, private values start with $.
+ * In Skylark, private values start with _, because of the grammar.
+ */
+ private static String attributeToNative(String oldName, Location loc, boolean isLateBound)
+ throws EvalException {
+ if (oldName.isEmpty()) {
+ throw new EvalException(loc, "Attribute name cannot be empty");
+ }
+ if (isLateBound) {
+ if (oldName.charAt(0) != '_') {
+ throw new EvalException(loc, "When an attribute value is a function, "
+ + "the attribute must be private (start with '_')");
+ }
+ return ":" + oldName.substring(1);
+ }
+ if (oldName.charAt(0) == '_') {
+ return "$" + oldName.substring(1);
+ }
+ return oldName;
+ }
+
+ // TODO(bazel-team): implement attribute copy and other rule properties
+
+ @SkylarkBuiltin(name = "rule", doc =
+ "Creates a new rule. Store it in a global value, so that it can be loaded and called "
+ + "from BUILD files.",
+ onlyLoadingPhase = true,
+ returnType = Function.class,
+ mandatoryParams = {
+ @Param(name = "implementation", type = UserDefinedFunction.class,
+ doc = "the function implementing this rule, has to have exactly one parameter: "
+ + "<code>ctx</code>. The function is called during analysis phase for each "
+ + "instance of the rule. It can access the attributes provided by the user. "
+ + "It must create actions to generate all the declared outputs.")
+ },
+ optionalParams = {
+ @Param(name = "test", type = Boolean.class, doc = "Whether this rule is a test rule. "
+ + "If True, the rule must end with <code>_test</code> (otherwise it cannot)."),
+ @Param(name = "attrs", doc =
+ "dictionary to declare all the attributes of the rule. It maps from an attribute name "
+ + "to an attribute object (see 'attr' module). Attributes starting with <code>_</code> "
+ + "are private, and can be used to add an implicit dependency on a label."),
+ @Param(name = "outputs", doc = "outputs of this rule. "
+ + "It is a dictionary mapping from string to a template name. For example: "
+ + "<code>{\"ext\": \"${name}.ext\"}</code>. <br>"
+ // TODO(bazel-team): Make doc more clear, wrt late-bound attributes.
+ + "It may also be a function (which receives <code>ctx.attr</code> as argument) "
+ + "returning such a dictionary."),
+ @Param(name = "executable", type = Boolean.class,
+ doc = "whether this rule always outputs an executable of the same name or not. If True, "
+ + "there must be an action that generates <code>ctx.outputs.executable</code>.")})
+ private static final SkylarkFunction rule = new SkylarkFunction("rule") {
+
+ @Override
+ public Object call(Map<String, Object> arguments, FuncallExpression ast,
+ Environment funcallEnv) throws EvalException, ConversionException {
+ final Location loc = ast.getLocation();
+
+ RuleClassType type = RuleClassType.NORMAL;
+ if (arguments.containsKey("test") && EvalUtils.toBoolean(arguments.get("test"))) {
+ type = RuleClassType.TEST;
+ }
+
+ // We'll set the name later, pass the empty string for now.
+ final RuleClass.Builder builder = type == RuleClassType.TEST
+ ? new RuleClass.Builder("", type, true, testBaseRule)
+ : new RuleClass.Builder("", type, true, baseRule);
+
+ for (Map.Entry<String, Attribute.Builder> attr : castMap(
+ arguments.get("attrs"), String.class, Attribute.Builder.class, "attrs")) {
+ Attribute.Builder<?> attrBuilder = attr.getValue();
+ String attrName = attributeToNative(attr.getKey(), loc,
+ attrBuilder.hasLateBoundValue());
+ builder.addOrOverrideAttribute(attrBuilder.build(attrName));
+ }
+ if (arguments.containsKey("executable") && (Boolean) arguments.get("executable")) {
+ builder.addOrOverrideAttribute(
+ attr("$is_executable", BOOLEAN).value(true)
+ .nonconfigurable("Called from RunCommand.isExecutable, which takes a Target")
+ .build());
+ builder.setOutputsDefaultExecutable();
+ }
+
+ if (arguments.containsKey("outputs")) {
+ final Object implicitOutputs = arguments.get("outputs");
+ if (implicitOutputs instanceof UserDefinedFunction) {
+ UserDefinedFunction func = (UserDefinedFunction) implicitOutputs;
+ final SkylarkCallbackFunction callback =
+ new SkylarkCallbackFunction(func, ast, (SkylarkEnvironment) funcallEnv);
+ builder.setImplicitOutputsFunction(
+ new SkylarkImplicitOutputsFunctionWithCallback(callback, loc));
+ } else {
+ builder.setImplicitOutputsFunction(new SkylarkImplicitOutputsFunctionWithMap(
+ toMap(castMap(arguments.get("outputs"), String.class, String.class,
+ "implicit outputs of the rule class"))));
+ }
+ }
+
+ builder.setConfiguredTargetFunction(
+ (UserDefinedFunction) arguments.get("implementation"));
+ builder.setRuleDefinitionEnvironment((SkylarkEnvironment) funcallEnv);
+ return new RuleFunction(builder, type);
+ }
+ };
+
+ // This class is needed for testing
+ static final class RuleFunction extends AbstractFunction {
+ // Note that this means that we can reuse the same builder.
+ // This is fine since we don't modify the builder from here.
+ private final RuleClass.Builder builder;
+ private final RuleClassType type;
+
+ public RuleFunction(Builder builder, RuleClassType type) {
+ super("rule");
+ this.builder = builder;
+ this.type = type;
+ }
+
+ @Override
+ public Object call(List<Object> args, Map<String, Object> kwargs, FuncallExpression ast,
+ Environment env) throws EvalException, InterruptedException {
+ try {
+ String ruleClassName = ast.getFunction().getName();
+ if (ruleClassName.startsWith("_")) {
+ throw new EvalException(ast.getLocation(), "Invalid rule class name '" + ruleClassName
+ + "', cannot be private");
+ }
+ if (type == RuleClassType.TEST != TargetUtils.isTestRuleName(ruleClassName)) {
+ throw new EvalException(ast.getLocation(), "Invalid rule class name '" + ruleClassName
+ + "', test rule class names must end with '_test' and other rule classes must not");
+ }
+ RuleClass ruleClass = builder.build(ruleClassName);
+ PackageContext pkgContext = (PackageContext) env.lookup(PackageFactory.PKG_CONTEXT);
+ return RuleFactory.createAndAddRule(pkgContext, ruleClass, kwargs, ast);
+ } catch (InvalidRuleException | NameConflictException | NoSuchVariableException e) {
+ throw new EvalException(ast.getLocation(), e.getMessage());
+ }
+ }
+
+ @VisibleForTesting
+ RuleClass.Builder getBuilder() {
+ return builder;
+ }
+ }
+
+ @SkylarkBuiltin(name = "Label", doc = "Creates a Label referring to a BUILD target. Use "
+ + "this function only when you want to give a default value for the label attributes. "
+ + "Example: <br><pre class=language-python>Label(\"//tools:default\")</pre>",
+ returnType = Label.class,
+ mandatoryParams = {@Param(name = "label_string", type = String.class,
+ doc = "the label string")})
+ private static final SkylarkFunction label = new SimpleSkylarkFunction("Label") {
+ @Override
+ public Object call(Map<String, Object> arguments, Location loc) throws EvalException,
+ ConversionException {
+ String labelString = (String) arguments.get("label_string");
+ try {
+ return labelCache.get(labelString);
+ } catch (ExecutionException e) {
+ throw new EvalException(loc, "Illegal absolute label syntax: " + labelString);
+ }
+ }
+ };
+
+ @SkylarkBuiltin(name = "FileType",
+ doc = "Creates a file filter from a list of strings. For example, to match files ending "
+ + "with .cc or .cpp, use: <pre class=language-python>FileType([\".cc\", \".cpp\"])</pre>",
+ returnType = SkylarkFileType.class,
+ mandatoryParams = {
+ @Param(name = "types", type = SkylarkList.class, generic1 = String.class,
+ doc = "a list of the accepted file extensions")})
+ private static final SkylarkFunction fileType = new SimpleSkylarkFunction("FileType") {
+ @Override
+ public Object call(Map<String, Object> arguments, Location loc) throws EvalException,
+ ConversionException {
+ return SkylarkFileType.of(castList(arguments.get("types"), String.class));
+ }
+ };
+
+ @SkylarkBuiltin(name = "to_proto",
+ doc = "Creates a text message from the struct parameter. This method only works if all "
+ + "struct elements (recursively) are strings, ints, booleans, other structs or a "
+ + "list of these types. Quotes and new lines in strings are escaped. "
+ + "Examples:<br><pre class=language-python>"
+ + "struct(key=123).to_proto()\n# key: 123\n\n"
+ + "struct(key=True).to_proto()\n# key: true\n\n"
+ + "struct(key=[1, 2, 3]).to_proto()\n# key: 1\n# key: 2\n# key: 3\n\n"
+ + "struct(key='text').to_proto()\n# key: \"text\"\n\n"
+ + "struct(key=struct(inner_key='text')).to_proto()\n"
+ + "# key {\n# inner_key: \"text\"\n# }\n\n"
+ + "struct(key=[struct(inner_key=1), struct(inner_key=2)]).to_proto()\n"
+ + "# key {\n# inner_key: 1\n# }\n# key {\n# inner_key: 2\n# }\n\n"
+ + "struct(key=struct(inner_key=struct(inner_inner_key='text'))).to_proto()\n"
+ + "# key {\n# inner_key {\n# inner_inner_key: \"text\"\n# }\n# }\n</pre>",
+ objectType = SkylarkClassObject.class, returnType = String.class)
+ private static final SkylarkFunction toProto = new SimpleSkylarkFunction("to_proto") {
+ @Override
+ public Object call(Map<String, Object> arguments, Location loc) throws EvalException,
+ ConversionException {
+ ClassObject object = (ClassObject) arguments.get("self");
+ StringBuilder sb = new StringBuilder();
+ printTextMessage(object, sb, 0, loc);
+ return sb.toString();
+ }
+
+ private void printTextMessage(ClassObject object, StringBuilder sb,
+ int indent, Location loc) throws EvalException {
+ for (String key : object.getKeys()) {
+ printTextMessage(key, object.getValue(key), sb, indent, loc);
+ }
+ }
+
+ private void printSimpleTextMessage(String key, Object value, StringBuilder sb,
+ int indent, Location loc, String container) throws EvalException {
+ if (value instanceof ClassObject) {
+ print(sb, key + " {", indent);
+ printTextMessage((ClassObject) value, sb, indent + 1, loc);
+ print(sb, "}", indent);
+ } else if (value instanceof String) {
+ print(sb, key + ": \"" + escape((String) value) + "\"", indent);
+ } else if (value instanceof Integer) {
+ print(sb, key + ": " + value, indent);
+ } else if (value instanceof Boolean) {
+ // We're relying on the fact that Java converts Booleans to Strings in the same way
+ // as the protocol buffers do.
+ print(sb, key + ": " + value, indent);
+ } else {
+ throw new EvalException(loc,
+ "Invalid text format, expected a struct, a string, a bool, or an int but got a "
+ + EvalUtils.getDatatypeName(value) + " for " + container + " '" + key + "'");
+ }
+ }
+
+ private void printTextMessage(String key, Object value, StringBuilder sb,
+ int indent, Location loc) throws EvalException {
+ if (value instanceof SkylarkList) {
+ for (Object item : ((SkylarkList) value)) {
+ // TODO(bazel-team): There should be some constraint on the fields of the structs
+ // in the same list but we ignore that for now.
+ printSimpleTextMessage(key, item, sb, indent, loc, "list element in struct field");
+ }
+ } else {
+ printSimpleTextMessage(key, value, sb, indent, loc, "struct field");
+ }
+ }
+
+ private String escape(String string) {
+ // TODO(bazel-team): use guava's SourceCodeEscapers when it's released.
+ return string.replace("\"", "\\\"").replace("\n", "\\n");
+ }
+
+ private void print(StringBuilder sb, String text, int indent) {
+ for (int i = 0; i < indent; i++) {
+ sb.append(" ");
+ }
+ sb.append(text);
+ sb.append("\n");
+ }
+ };
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/SkylarkRuleConfiguredTargetBuilder.java b/src/main/java/com/google/devtools/build/lib/rules/SkylarkRuleConfiguredTargetBuilder.java
new file mode 100644
index 0000000..528e0f1
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/SkylarkRuleConfiguredTargetBuilder.java
@@ -0,0 +1,213 @@
+// Copyright 2014 Google Inc. 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.rules;
+
+import static com.google.devtools.build.lib.syntax.SkylarkFunction.cast;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTargetBuilder;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.Runfiles;
+import com.google.devtools.build.lib.analysis.RunfilesProvider;
+import com.google.devtools.build.lib.analysis.RunfilesSupport;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.packages.TargetUtils;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.syntax.ClassObject.SkylarkClassObject;
+import com.google.devtools.build.lib.syntax.Environment;
+import com.google.devtools.build.lib.syntax.EvalException;
+import com.google.devtools.build.lib.syntax.Function;
+import com.google.devtools.build.lib.syntax.SkylarkEnvironment;
+import com.google.devtools.build.lib.syntax.SkylarkNestedSet;
+
+/**
+ * A helper class to build Rule Configured Targets via runtime loaded rule implementations
+ * defined using the Skylark Build Extension Language. This is experimental code.
+ */
+public final class SkylarkRuleConfiguredTargetBuilder {
+
+ /**
+ * Create a Rule Configured Target from the ruleContext and the ruleImplementation.
+ */
+ public static ConfiguredTarget buildRule(RuleContext ruleContext,
+ Function ruleImplementation) {
+ String expectError = ruleContext.attributes().get("expect_failure", Type.STRING);
+ try {
+ SkylarkRuleContext skylarkRuleContext = new SkylarkRuleContext(ruleContext);
+ SkylarkEnvironment env = ruleContext.getRule().getRuleClassObject()
+ .getRuleDefinitionEnvironment().cloneEnv(
+ ruleContext.getAnalysisEnvironment().getEventHandler());
+ // Collect the symbols to disable statically and pass at the next call, so we don't need to
+ // clone the RuleDefinitionEnvironment.
+ env.disableOnlyLoadingPhaseObjects();
+ Object target = ruleImplementation.call(ImmutableList.<Object>of(skylarkRuleContext),
+ ImmutableMap.<String, Object>of(), null, env);
+
+ if (ruleContext.hasErrors()) {
+ return null;
+ } else if (!(target instanceof SkylarkClassObject) && target != Environment.NONE) {
+ ruleContext.ruleError("Rule implementation doesn't return a struct");
+ return null;
+ } else if (!expectError.isEmpty()) {
+ ruleContext.ruleError("Expected error not found: " + expectError);
+ return null;
+ }
+ ConfiguredTarget configuredTarget = createTarget(ruleContext, target);
+ checkOrphanArtifacts(ruleContext);
+ return configuredTarget;
+
+ } catch (InterruptedException e) {
+ ruleContext.ruleError(e.getMessage());
+ return null;
+ } catch (EvalException e) {
+ // If the error was expected, return an empty target.
+ if (!expectError.isEmpty() && e.getMessage().matches(expectError)) {
+ return new com.google.devtools.build.lib.analysis.RuleConfiguredTargetBuilder(ruleContext)
+ .add(RunfilesProvider.class, RunfilesProvider.EMPTY)
+ .build();
+ }
+ ruleContext.ruleError("\n" + e.print());
+ return null;
+ }
+ }
+
+ private static void checkOrphanArtifacts(RuleContext ruleContext) throws EvalException {
+ ImmutableSet<Artifact> orphanArtifacts =
+ ruleContext.getAnalysisEnvironment().getOrphanArtifacts();
+ if (!orphanArtifacts.isEmpty()) {
+ throw new EvalException(null, "The following files have no generating action:\n"
+ + Joiner.on("\n").join(Iterables.transform(orphanArtifacts,
+ new com.google.common.base.Function<Artifact, String>() {
+ @Override
+ public String apply(Artifact artifact) {
+ return artifact.getRootRelativePathString();
+ }
+ })));
+ }
+ }
+
+ // TODO(bazel-team): this whole defaulting - overriding executable, runfiles and files_to_build
+ // is getting out of hand. Clean this whole mess up.
+ private static ConfiguredTarget createTarget(RuleContext ruleContext, Object target)
+ throws EvalException {
+ Artifact executable = getExecutable(ruleContext, target);
+ RuleConfiguredTargetBuilder builder = new RuleConfiguredTargetBuilder(ruleContext);
+ // Set the default files to build.
+ NestedSetBuilder<Artifact> filesToBuild = NestedSetBuilder.<Artifact>stableOrder()
+ .addAll(ruleContext.getOutputArtifacts());
+ if (executable != null) {
+ filesToBuild.add(executable);
+ }
+ builder.setFilesToBuild(filesToBuild.build());
+ return addStructFields(ruleContext, builder, target, executable);
+ }
+
+ private static Artifact getExecutable(RuleContext ruleContext, Object target)
+ throws EvalException {
+ Artifact executable = ruleContext.getRule().getRuleClassObject().outputsDefaultExecutable()
+ // This doesn't actually create a new Artifact just returns the one
+ // created in SkylarkruleContext.
+ ? ruleContext.createOutputArtifact() : null;
+ if (target instanceof SkylarkClassObject) {
+ SkylarkClassObject struct = (SkylarkClassObject) target;
+ if (struct.getValue("executable") != null) {
+ // We need this because of genrule.bzl. This overrides the default executable.
+ executable = cast(
+ struct.getValue("executable"), Artifact.class, "executable", struct.getCreationLoc());
+ }
+ }
+ return executable;
+ }
+
+ private static ConfiguredTarget addStructFields(RuleContext ruleContext,
+ RuleConfiguredTargetBuilder builder, Object target, Artifact executable)
+ throws EvalException {
+ Location loc = null;
+ Runfiles statelessRunfiles = null;
+ Runfiles dataRunfiles = null;
+ Runfiles defaultRunfiles = null;
+ if (target instanceof SkylarkClassObject) {
+ SkylarkClassObject struct = (SkylarkClassObject) target;
+ loc = struct.getCreationLoc();
+ for (String key : struct.getKeys()) {
+ if (key.equals("files")) {
+ // If we specify files_to_build we don't have the executable in it by default.
+ builder.setFilesToBuild(cast(struct.getValue("files"),
+ SkylarkNestedSet.class, "files", loc).getSet(Artifact.class));
+ } else if (key.equals("runfiles")) {
+ statelessRunfiles = cast(struct.getValue("runfiles"), Runfiles.class, "runfiles", loc);
+ } else if (key.equals("data_runfiles")) {
+ dataRunfiles =
+ cast(struct.getValue("data_runfiles"), Runfiles.class, "data_runfiles", loc);
+ } else if (key.equals("default_runfiles")) {
+ defaultRunfiles =
+ cast(struct.getValue("default_runfiles"), Runfiles.class, "default_runfiles", loc);
+ } else if (!key.equals("executable")) {
+ // We handled executable already.
+ builder.addSkylarkTransitiveInfo(key, struct.getValue(key), loc);
+ }
+ }
+ }
+
+ if ((statelessRunfiles != null) && (dataRunfiles != null || defaultRunfiles != null)) {
+ throw new EvalException(loc, "Cannot specify the provider 'runfiles' "
+ + "together with 'data_runfiles' or 'default_runfiles'");
+ }
+
+ if (statelessRunfiles == null && dataRunfiles == null && defaultRunfiles == null) {
+ // No runfiles specified, set default
+ statelessRunfiles = Runfiles.EMPTY;
+ }
+
+ RunfilesProvider runfilesProvider = statelessRunfiles != null
+ ? RunfilesProvider.simple(merge(statelessRunfiles, executable))
+ : RunfilesProvider.withData(
+ // The executable doesn't get into the default runfiles if we have runfiles states.
+ // This is to keep skylark genrule consistent with the original genrule.
+ defaultRunfiles != null ? defaultRunfiles : Runfiles.EMPTY,
+ dataRunfiles != null ? dataRunfiles : Runfiles.EMPTY);
+ builder.addProvider(RunfilesProvider.class, runfilesProvider);
+
+ Runfiles computedDefaultRunfiles = runfilesProvider.getDefaultRunfiles();
+ // This works because we only allowed to call a rule *_test iff it's a test type rule.
+ boolean testRule = TargetUtils.isTestRuleName(ruleContext.getRule().getRuleClass());
+ if (testRule && computedDefaultRunfiles.isEmpty()) {
+ throw new EvalException(loc, "Test rules have to define runfiles");
+ }
+ if (executable != null || testRule) {
+ RunfilesSupport runfilesSupport = computedDefaultRunfiles.isEmpty()
+ ? null : RunfilesSupport.withExecutable(ruleContext, computedDefaultRunfiles, executable);
+ builder.setRunfilesSupport(runfilesSupport, executable);
+ }
+ try {
+ return builder.build();
+ } catch (IllegalArgumentException e) {
+ throw new EvalException(loc, e.getMessage());
+ }
+ }
+
+ private static Runfiles merge(Runfiles runfiles, Artifact executable) {
+ if (executable == null) {
+ return runfiles;
+ }
+ return new Runfiles.Builder().addArtifact(executable).merge(runfiles).build();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/SkylarkRuleContext.java b/src/main/java/com/google/devtools/build/lib/rules/SkylarkRuleContext.java
new file mode 100644
index 0000000..fc06677
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/SkylarkRuleContext.java
@@ -0,0 +1,484 @@
+// Copyright 2014 Google Inc. 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.rules;
+
+import com.google.common.base.Preconditions;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.Root;
+import com.google.devtools.build.lib.analysis.AnalysisUtils;
+import com.google.devtools.build.lib.analysis.ConfigurationMakeVariableContext;
+import com.google.devtools.build.lib.analysis.FilesToRunProvider;
+import com.google.devtools.build.lib.analysis.LabelExpander;
+import com.google.devtools.build.lib.analysis.LabelExpander.NotUniqueExpansionException;
+import com.google.devtools.build.lib.analysis.MakeVariableExpander.ExpansionException;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.packages.Attribute;
+import com.google.devtools.build.lib.packages.Attribute.ConfigurationTransition;
+import com.google.devtools.build.lib.packages.ImplicitOutputsFunction;
+import com.google.devtools.build.lib.packages.ImplicitOutputsFunction.SkylarkImplicitOutputsFunction;
+import com.google.devtools.build.lib.packages.OutputFile;
+import com.google.devtools.build.lib.packages.RawAttributeMapper;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.shell.ShellUtils;
+import com.google.devtools.build.lib.shell.ShellUtils.TokenizationException;
+import com.google.devtools.build.lib.syntax.ClassObject.SkylarkClassObject;
+import com.google.devtools.build.lib.syntax.Environment;
+import com.google.devtools.build.lib.syntax.EvalException;
+import com.google.devtools.build.lib.syntax.FuncallExpression.FuncallException;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.syntax.SkylarkCallable;
+import com.google.devtools.build.lib.syntax.SkylarkList;
+import com.google.devtools.build.lib.syntax.SkylarkModule;
+import com.google.devtools.build.lib.syntax.SkylarkType;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+/**
+ * A Skylark API for the ruleContext.
+ */
+@SkylarkModule(name = "ctx", doc = "The context of the rule containing helper functions and "
+ + "information about attributes, depending targets and outputs. "
+ + "You get a ctx object as an argument to the <code>implementation</code> function when "
+ + "you create a rule.")
+public final class SkylarkRuleContext {
+
+ public static final String PROVIDER_CLASS_PREFIX = "com.google.devtools.build.lib.";
+
+ static final LoadingCache<String, Class<?>> classCache = CacheBuilder.newBuilder()
+ .initialCapacity(10)
+ .maximumSize(100)
+ .build(new CacheLoader<String, Class<?>>() {
+
+ @Override
+ public Class<?> load(String key) throws Exception {
+ String classPath = SkylarkRuleContext.PROVIDER_CLASS_PREFIX + key;
+ return Class.forName(classPath);
+ }
+ });
+
+ private final RuleContext ruleContext;
+
+ // TODO(bazel-team): support configurable attributes.
+ private final SkylarkClassObject attrObject;
+
+ private final SkylarkClassObject outputsObject;
+
+ private final SkylarkClassObject executableObject;
+
+ private final SkylarkClassObject fileObject;
+
+ private final SkylarkClassObject filesObject;
+
+ private final SkylarkClassObject targetsObject;
+
+ private final SkylarkClassObject targetObject;
+
+ // TODO(bazel-team): we only need this because of the css_binary rule.
+ private final ImmutableMap<Artifact, Label> artifactLabelMap;
+
+ private final ImmutableMap<Artifact, FilesToRunProvider> executableRunfilesMap;
+
+ /**
+ * In native code, private values start with $.
+ * In Skylark, private values start with _, because of the grammar.
+ */
+ private String attributeToSkylark(String oldName) {
+ if (!oldName.isEmpty() && (oldName.charAt(0) == '$' || oldName.charAt(0) == ':')) {
+ return "_" + oldName.substring(1);
+ }
+ return oldName;
+ }
+
+ /**
+ * Creates a new SkylarkRuleContext using ruleContext.
+ */
+ public SkylarkRuleContext(RuleContext ruleContext) throws EvalException {
+ this.ruleContext = Preconditions.checkNotNull(ruleContext);
+
+ HashMap<String, Object> outputsBuilder = new HashMap<>();
+ if (ruleContext.getRule().getRuleClassObject().outputsDefaultExecutable()) {
+ addOutput(outputsBuilder, "executable", ruleContext.createOutputArtifact());
+ }
+ ImplicitOutputsFunction implicitOutputsFunction =
+ ruleContext.getRule().getRuleClassObject().getImplicitOutputsFunction();
+
+ if (implicitOutputsFunction instanceof SkylarkImplicitOutputsFunction) {
+ SkylarkImplicitOutputsFunction func = (SkylarkImplicitOutputsFunction)
+ ruleContext.getRule().getRuleClassObject().getImplicitOutputsFunction();
+ for (Map.Entry<String, String> entry : func.calculateOutputs(
+ RawAttributeMapper.of(ruleContext.getRule())).entrySet()) {
+ addOutput(outputsBuilder, entry.getKey(),
+ ruleContext.getImplicitOutputArtifact(entry.getValue()));
+ }
+ }
+
+ ImmutableMap.Builder<Artifact, Label> artifactLabelMapBuilder =
+ ImmutableMap.builder();
+ for (Attribute a : ruleContext.getRule().getAttributes()) {
+ String attrName = a.getName();
+ Type<?> type = a.getType();
+ if (type != Type.OUTPUT && type != Type.OUTPUT_LIST) {
+ continue;
+ }
+ ImmutableList.Builder<Artifact> artifactsBuilder = ImmutableList.builder();
+ for (OutputFile outputFile : ruleContext.getRule().getOutputFileMap().get(attrName)) {
+ Artifact artifact = ruleContext.createOutputArtifact(outputFile);
+ artifactsBuilder.add(artifact);
+ artifactLabelMapBuilder.put(artifact, outputFile.getLabel());
+ }
+ ImmutableList<Artifact> artifacts = artifactsBuilder.build();
+
+ if (type == Type.OUTPUT) {
+ if (artifacts.size() == 1) {
+ addOutput(outputsBuilder, attrName, Iterables.getOnlyElement(artifacts));
+ } else {
+ addOutput(outputsBuilder, attrName, Environment.NONE);
+ }
+ } else if (type == Type.OUTPUT_LIST) {
+ addOutput(outputsBuilder, attrName,
+ SkylarkList.list(artifacts, Artifact.class));
+ } else {
+ throw new IllegalArgumentException(
+ "Type of " + attrName + "(" + type + ") is not output type ");
+ }
+ }
+ artifactLabelMap = artifactLabelMapBuilder.build();
+ outputsObject = new SkylarkClassObject(outputsBuilder, "No such output '%s'");
+
+ ImmutableMap.Builder<String, Object> builder = new ImmutableMap.Builder<>();
+ ImmutableMap.Builder<String, Object> executableBuilder = new ImmutableMap.Builder<>();
+ ImmutableMap.Builder<Artifact, FilesToRunProvider> executableRunfilesbuilder =
+ new ImmutableMap.Builder<>();
+ ImmutableMap.Builder<String, Object> fileBuilder = new ImmutableMap.Builder<>();
+ ImmutableMap.Builder<String, Object> filesBuilder = new ImmutableMap.Builder<>();
+ ImmutableMap.Builder<String, Object> targetBuilder = new ImmutableMap.Builder<>();
+ ImmutableMap.Builder<String, Object> targetsBuilder = new ImmutableMap.Builder<>();
+ for (Attribute a : ruleContext.getRule().getAttributes()) {
+ Type<?> type = a.getType();
+ Object val = ruleContext.attributes().get(a.getName(), type);
+ builder.put(attributeToSkylark(a.getName()), val == null ? Environment.NONE
+ // Attribute values should be type safe
+ : SkylarkType.convertToSkylark(val, null));
+ if (type != Type.LABEL && type != Type.LABEL_LIST) {
+ continue;
+ }
+ String skyname = attributeToSkylark(a.getName());
+ Mode mode = getMode(a.getName());
+ if (a.isExecutable()) {
+ // In Skylark only label (not label list) type attributes can have the Executable flag.
+ FilesToRunProvider provider = ruleContext.getExecutablePrerequisite(a.getName(), mode);
+ if (provider != null && provider.getExecutable() != null) {
+ Artifact executable = provider.getExecutable();
+ executableBuilder.put(skyname, executable);
+ executableRunfilesbuilder.put(executable, provider);
+ } else {
+ executableBuilder.put(skyname, Environment.NONE);
+ }
+ }
+ if (a.isSingleArtifact()) {
+ // In Skylark only label (not label list) type attributes can have the SingleArtifact flag.
+ Artifact artifact = ruleContext.getPrerequisiteArtifact(a.getName(), mode);
+ if (artifact != null) {
+ fileBuilder.put(skyname, artifact);
+ } else {
+ fileBuilder.put(skyname, Environment.NONE);
+ }
+ }
+ filesBuilder.put(skyname, ruleContext.getPrerequisiteArtifacts(a.getName(), mode).list());
+ targetsBuilder.put(skyname, SkylarkList.list(
+ ruleContext.getPrerequisites(a.getName(), mode), TransitiveInfoCollection.class));
+ if (type == Type.LABEL) {
+ Object prereq = ruleContext.getPrerequisite(a.getName(), mode);
+ if (prereq != null) {
+ targetBuilder.put(skyname, prereq);
+ } else {
+ targetBuilder.put(skyname, Environment.NONE);
+ }
+ }
+ }
+ attrObject = new SkylarkClassObject(builder.build(), "No such attribute '%s'");
+ executableObject = new SkylarkClassObject(executableBuilder.build(), "No such executable. "
+ + "Make sure there is a '%s' label type attribute marked as 'executable'");
+ fileObject = new SkylarkClassObject(fileBuilder.build(),
+ "No such file. Make sure there is a '%s' label type attribute marked as 'single_file'");
+ filesObject = new SkylarkClassObject(filesBuilder.build(),
+ "No such files. Make sure there is a '%s' label or label_list type attribute");
+ targetObject = new SkylarkClassObject(targetBuilder.build(),
+ "No such target. Make sure there is a '%s' label type attribute");
+ targetsObject = new SkylarkClassObject(targetsBuilder.build(),
+ "No such targets. Make sure there is a '%s' label or label_list type attribute");
+ executableRunfilesMap = executableRunfilesbuilder.build();
+ }
+
+ private void addOutput(HashMap<String, Object> outputsBuilder, String key, Object value)
+ throws EvalException {
+ if (outputsBuilder.containsKey(key)) {
+ throw new EvalException(null, "Multiple outputs with the same key: " + key);
+ }
+ outputsBuilder.put(key, value);
+ }
+
+ /**
+ * Returns the original ruleContext.
+ */
+ public RuleContext getRuleContext() {
+ return ruleContext;
+ }
+
+ private Mode getMode(String attributeName) {
+ return ruleContext.getAttributeMode(attributeName);
+ }
+
+ @SkylarkCallable(name = "attr", structField = true,
+ doc = "A struct to access the values of the attributes. The values are provided by "
+ + "the user (if not, a default value is used).")
+ public SkylarkClassObject getAttr() {
+ return attrObject;
+ }
+
+ /**
+ * <p>See {@link RuleContext#getExecutablePrerequisite(String, Mode)}.
+ */
+ @SkylarkCallable(name = "executable", structField = true,
+ doc = "A <code>struct</code> containing executable files defined in label type "
+ + "attributes marked as <code>executable=True</code>. The struct fields correspond "
+ + "to the attribute names. The struct value is always a <code>file</code>s or "
+ + "<code>None</code>. If a non-mandatory attribute is not specified in the rule "
+ + "the corresponding struct value is <code>None</code>. If a label type is not "
+ + "marked as <code>executable=True</code>, no corresponding struct field is generated.")
+ public SkylarkClassObject getExecutable() {
+ return executableObject;
+ }
+
+ /**
+ * See {@link RuleContext#getPrerequisiteArtifact(String, Mode)}.
+ */
+ @SkylarkCallable(name = "file", structField = true,
+ doc = "A <code>struct</code> containing files defined in label type "
+ + "attributes marked as <code>single_file=True</code>. The struct fields correspond "
+ + "to the attribute names. The struct value is always a <code>file</code> or "
+ + "<code>None</code>. If a non-mandatory attribute is not specified in the rule "
+ + "the corresponding struct value is <code>None</code>. If a label type is not "
+ + "marked as <code>single_file=True</code>, no corresponding struct field is generated.")
+ public SkylarkClassObject getFile() {
+ return fileObject;
+ }
+
+ /**
+ * See {@link RuleContext#getPrerequisiteArtifacts(String, Mode)}.
+ */
+ @SkylarkCallable(name = "files", structField = true,
+ doc = "A <code>struct</code> containing files defined in label or label list "
+ + "type attributes. The struct fields correspond to the attribute names. The struct "
+ + "values are <code>list</code> of <code>file</code>s. If a non-mandatory attribute is "
+ + "not specified in the rule, an empty list is generated.")
+ public SkylarkClassObject getFiles() {
+ return filesObject;
+ }
+
+ /**
+ * See {@link RuleContext#getPrerequisite(String, Mode)}.
+ */
+ @SkylarkCallable(name = "target", structField = true,
+ doc = "A <code>struct</code> containing prerequisite targets defined in label type "
+ + "attributes. The struct fields correspond to the attribute names. The struct value "
+ + "is always a <code>target</code> or <code>None</code>. If a non-mandatory attribute "
+ + "is not specified in the rule, the corresponding struct value is <code>None</code>.")
+ public SkylarkClassObject getTarget() {
+ return targetObject;
+ }
+
+ /**
+ * See {@link RuleContext#getPrerequisites(String, Mode)}.
+ */
+ @SkylarkCallable(name = "targets", structField = true,
+ doc = "A <code>struct</code> containing prerequisite targets defined in label or label list "
+ + "type attributes. The struct fields correspond to the attribute names. The struct "
+ + "values are <code>list</code> of <code>target</code>s. If a non-mandatory attribute is "
+ + "not specified in the rule, an empty list is generated.")
+ public SkylarkClassObject getTargets() {
+ return targetsObject;
+ }
+
+ @SkylarkCallable(name = "label", structField = true, doc = "The label of this rule.")
+ public Label getLabel() {
+ return ruleContext.getLabel();
+ }
+
+ @SkylarkCallable(name = "configuration", structField = true,
+ doc = "Returns the default configuration. See the <code>configuration</code> type for "
+ + "more details.")
+ public BuildConfiguration getConfiguration() {
+ return ruleContext.getConfiguration();
+ }
+
+ @SkylarkCallable(name = "host_configuration", structField = true,
+ doc = "Returns the host configuration. See the <code>configuration</code> type for "
+ + "more details.")
+ public BuildConfiguration getHostConfiguration() {
+ return ruleContext.getHostConfiguration();
+ }
+
+ @SkylarkCallable(name = "data_configuration", structField = true,
+ doc = "Returns the data configuration. See the <code>configuration</code> type for "
+ + "more details.")
+ public BuildConfiguration getDataConfiguration() {
+ return ruleContext.getConfiguration().getConfiguration(ConfigurationTransition.DATA);
+ }
+
+ @SkylarkCallable(structField = true,
+ doc = "A <code>struct</code> containing all the output files."
+ + " The struct is generated the following way:<br>"
+ + "<ul><li>If the rule is marked as <code>executable=True</code> the struct has an "
+ + "\"executable\" field with the rules default executable <code>file</code> value."
+ + "<li>For every entry in the rule's <code>outputs</code> dict a field is generated with "
+ + "the same name and the corresponding <code>file</code> value."
+ + "<li>For every output type attribute a struct field is generated with the "
+ + "same name and the corresponding <code>file</code> value or <code>None</code>, "
+ + "if no value is specified in the rule."
+ + "<li>For every output list type attribute a struct field is generated with the "
+ + "same name and corresponding <code>list</code> of <code>file</code>s value "
+ + "(an empty list if no value is specified in the rule.</ul>")
+ public SkylarkClassObject outputs() {
+ return outputsObject;
+ }
+
+ @Override
+ public String toString() {
+ return ruleContext.getLabel().toString();
+ }
+
+ @SkylarkCallable(doc = "Splits a shell command to a list of tokens.", hidden = true)
+ public List<String> tokenize(String optionString) throws FuncallException {
+ List<String> options = new ArrayList<String>();
+ try {
+ ShellUtils.tokenize(options, optionString);
+ } catch (TokenizationException e) {
+ throw new FuncallException(e.getMessage() + " while tokenizing '" + optionString + "'");
+ }
+ return ImmutableList.copyOf(options);
+ }
+
+ @SkylarkCallable(doc =
+ "Expands all references to labels embedded within a string for all files using a mapping "
+ + "from definition labels (i.e. the label in the output type attribute) to files. Deprecated.",
+ hidden = true)
+ public String expand(@Nullable String expression,
+ List<Artifact> artifacts, Label labelResolver) throws FuncallException {
+ try {
+ Map<Label, Iterable<Artifact>> labelMap = new HashMap<>();
+ for (Artifact artifact : artifacts) {
+ labelMap.put(artifactLabelMap.get(artifact), ImmutableList.of(artifact));
+ }
+ return LabelExpander.expand(expression, labelMap, labelResolver);
+ } catch (NotUniqueExpansionException e) {
+ throw new FuncallException(e.getMessage() + " while expanding '" + expression + "'");
+ }
+ }
+
+ @SkylarkCallable(doc =
+ "Creates a file with the given filename. You must create an action that generates "
+ + "the file. If the file should be publicly visible, declare a rule "
+ + "output instead when possible.")
+ public Artifact newFile(Root root, String filename) {
+ PathFragment fragment = ruleContext.getLabel().getPackageFragment();
+ for (String pathFragmentString : filename.split("/")) {
+ fragment = fragment.getRelative(pathFragmentString);
+ }
+ return ruleContext.getAnalysisEnvironment().getDerivedArtifact(fragment, root);
+ }
+
+ @SkylarkCallable(doc =
+ "Creates a new file, derived from the given file and suffix. "
+ + "You must create an action that generates "
+ + "the file. If the file should be publicly visible, declare a rule "
+ + "output instead when possible.")
+ public Artifact newFile(Root root, Artifact baseArtifact, String suffix) {
+ PathFragment original = baseArtifact.getRootRelativePath();
+ PathFragment fragment = original.replaceName(original.getBaseName() + suffix);
+ return ruleContext.getAnalysisEnvironment().getDerivedArtifact(fragment, root);
+ }
+
+ @SkylarkCallable(doc = "", hidden = true)
+ public NestedSet<Artifact> middleMan(String attribute) {
+ return AnalysisUtils.getMiddlemanFor(ruleContext, attribute);
+ }
+
+ @SkylarkCallable(doc = "", hidden = true)
+ public boolean checkPlaceholders(String template, List<String> allowedPlaceholders) {
+ List<String> actualPlaceHolders = new LinkedList<>();
+ Set<String> allowedPlaceholderSet = ImmutableSet.copyOf(allowedPlaceholders);
+ ImplicitOutputsFunction.createPlaceholderSubstitutionFormatString(template, actualPlaceHolders);
+ for (String placeholder : actualPlaceHolders) {
+ if (!allowedPlaceholderSet.contains(placeholder)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @SkylarkCallable(doc = "")
+ public String expandMakeVariables(String attributeName, String command,
+ final Map<String, String> additionalSubstitutions) {
+ return ruleContext.expandMakeVariables(attributeName,
+ command, new ConfigurationMakeVariableContext(ruleContext.getRule().getPackage(),
+ ruleContext.getConfiguration()) {
+ @Override
+ public String lookupMakeVariable(String name) throws ExpansionException {
+ if (additionalSubstitutions.containsKey(name)) {
+ return additionalSubstitutions.get(name);
+ } else {
+ return super.lookupMakeVariable(name);
+ }
+ }
+ });
+ }
+
+ FilesToRunProvider getExecutableRunfiles(Artifact executable) {
+ return executableRunfilesMap.get(executable);
+ }
+
+ @SkylarkCallable(name = "info_file", structField = true, hidden = true,
+ doc = "Returns the file that is used to hold the non-volatile workspace status for the "
+ + "current build request.")
+ public Artifact getStableWorkspaceStatus() {
+ return ruleContext.getAnalysisEnvironment().getStableWorkspaceStatusArtifact();
+ }
+
+ @SkylarkCallable(name = "version_file", structField = true, hidden = true,
+ doc = "Returns the file that is used to hold the volatile workspace status for the "
+ + "current build request.")
+ public Artifact getVolatileWorkspaceStatus() {
+ return ruleContext.getAnalysisEnvironment().getVolatileWorkspaceStatusArtifact();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/SkylarkRuleImplementationFunctions.java b/src/main/java/com/google/devtools/build/lib/rules/SkylarkRuleImplementationFunctions.java
new file mode 100644
index 0000000..1f7d160
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/SkylarkRuleImplementationFunctions.java
@@ -0,0 +1,367 @@
+// Copyright 2014 Google Inc. 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.rules;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.AnalysisUtils;
+import com.google.devtools.build.lib.analysis.CommandHelper;
+import com.google.devtools.build.lib.analysis.FilesToRunProvider;
+import com.google.devtools.build.lib.analysis.MakeVariableExpander;
+import com.google.devtools.build.lib.analysis.Runfiles;
+import com.google.devtools.build.lib.analysis.RunfilesProvider;
+import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
+import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
+import com.google.devtools.build.lib.analysis.actions.CommandLine;
+import com.google.devtools.build.lib.analysis.actions.FileWriteAction;
+import com.google.devtools.build.lib.analysis.actions.SpawnAction;
+import com.google.devtools.build.lib.analysis.actions.TemplateExpansionAction;
+import com.google.devtools.build.lib.analysis.actions.TemplateExpansionAction.Substitution;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.packages.Type.ConversionException;
+import com.google.devtools.build.lib.syntax.Environment;
+import com.google.devtools.build.lib.syntax.EvalException;
+import com.google.devtools.build.lib.syntax.EvalUtils;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.syntax.SkylarkBuiltin;
+import com.google.devtools.build.lib.syntax.SkylarkBuiltin.Param;
+import com.google.devtools.build.lib.syntax.SkylarkFunction;
+import com.google.devtools.build.lib.syntax.SkylarkFunction.SimpleSkylarkFunction;
+import com.google.devtools.build.lib.syntax.SkylarkList;
+import com.google.devtools.build.lib.syntax.SkylarkNestedSet;
+import com.google.devtools.build.lib.util.ShellEscaper;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+
+// TODO(bazel-team): function argument names are often duplicated,
+// figure out a nicely readable way to get rid of the duplications.
+/**
+ * A helper class to provide an easier API for Skylark rule implementations
+ * and hide the original Java API. This is experimental code.
+ */
+public class SkylarkRuleImplementationFunctions {
+
+ // TODO(bazel-team): add all the remaining parameters
+ // TODO(bazel-team): merge executable and arguments
+ /**
+ * A Skylark built-in function to create and register a SpawnAction using a
+ * dictionary of parameters:
+ * createSpawnAction(
+ * inputs = [input1, input2, ...],
+ * outputs = [output1, output2, ...],
+ * executable = executable,
+ * arguments = [argument1, argument2, ...],
+ * mnemonic = 'mnemonic',
+ * command = 'command',
+ * register = 1
+ * )
+ */
+ @SkylarkBuiltin(name = "action",
+ doc = "Creates an action that runs an executable or a shell command.",
+ objectType = SkylarkRuleContext.class,
+ returnType = Environment.NoneType.class,
+ mandatoryParams = {
+ @Param(name = "outputs", type = SkylarkList.class, generic1 = Artifact.class,
+ doc = "list of the output files of the action")},
+ optionalParams = {
+ @Param(name = "inputs", type = SkylarkList.class, generic1 = Artifact.class,
+ doc = "list of the input files of the action"),
+ @Param(name = "executable", doc = "the executable file to be called by the action"),
+ @Param(name = "arguments", type = SkylarkList.class, generic1 = String.class,
+ doc = "command line arguments of the action"),
+ @Param(name = "mnemonic", type = String.class, doc = "mnemonic"),
+ @Param(name = "command", doc = "shell command to execute"),
+ @Param(name = "command_line", doc = "a command line to execute"),
+ @Param(name = "progress_message", type = String.class,
+ doc = "progress message to show to the user during the build"),
+ @Param(name = "use_default_shell_env", type = Boolean.class,
+ doc = "whether the action should use the built in shell environment or not"),
+ @Param(name = "env", type = Map.class, doc = "sets the dictionary of environment variables"),
+ @Param(name = "execution_requirements", type = Map.class,
+ doc = "information for scheduling the action"),
+ @Param(name = "input_manifests", type = Map.class,
+ doc = "sets the map of input manifests files; "
+ + "they are typicially generated by the command_helper")})
+ private static final SkylarkFunction createSpawnAction =
+ new SimpleSkylarkFunction("action") {
+
+ @Override
+ public Object call(Map<String, Object> params, Location loc) throws EvalException,
+ ConversionException {
+ SkylarkRuleContext ctx = (SkylarkRuleContext) params.get("self");
+ SpawnAction.Builder builder = new SpawnAction.Builder();
+ // TODO(bazel-team): builder still makes unnecessary copies of inputs, outputs and args.
+ builder.addInputs(castList(params.get("inputs"), Artifact.class));
+ builder.addOutputs(castList(params.get("outputs"), Artifact.class));
+ builder.addArguments(castList(params.get("arguments"), String.class));
+ if (params.containsKey("executable")) {
+ Object exe = params.get("executable");
+ if (exe instanceof Artifact) {
+ Artifact executable = (Artifact) exe;
+ builder.addInput(executable);
+ FilesToRunProvider provider = ctx.getExecutableRunfiles(executable);
+ if (provider == null) {
+ builder.setExecutable((Artifact) exe);
+ } else {
+ builder.setExecutable(provider);
+ }
+ } else if (exe instanceof PathFragment) {
+ builder.setExecutable((PathFragment) exe);
+ } else {
+ throw new EvalException(loc, "expected file or PathFragment for "
+ + "executable but got " + EvalUtils.getDatatypeName(exe) + " instead");
+ }
+ }
+ if (params.containsKey("command") == params.containsKey("executable")) {
+ throw new EvalException(loc, "You must specify either 'command' or 'executable' argument");
+ }
+ if (params.containsKey("command")) {
+ Object command = params.get("command");
+ if (command instanceof String) {
+ builder.setShellCommand((String) command);
+ } else if (command instanceof SkylarkList) {
+ SkylarkList commandList = (SkylarkList) command;
+ if (commandList.size() < 3) {
+ throw new EvalException(loc, "'command' list has to be of size at least 3");
+ }
+ builder.setShellCommand(castList(commandList, String.class, "command"));
+ } else {
+ throw new EvalException(loc, "expected string or list of strings for "
+ + "command instead of " + EvalUtils.getDatatypeName(command));
+ }
+ }
+ if (params.containsKey("command_line")) {
+ builder.setCommandLine(CommandLine.ofCharSequences(ImmutableList.copyOf(castList(
+ params.get("command_line"), CharSequence.class, "command line"))));
+ }
+ if (params.containsKey("mnemonic")) {
+ builder.setMnemonic((String) params.get("mnemonic"));
+ }
+ if (params.containsKey("env")) {
+ builder.setEnvironment(
+ toMap(castMap(params.get("env"), String.class, String.class, "env")));
+ }
+ if (params.containsKey("progress_message")) {
+ builder.setProgressMessage((String) params.get("progress_message"));
+ }
+ if (params.containsKey("use_default_shell_env")
+ && EvalUtils.toBoolean(params.get("use_default_shell_env"))) {
+ builder.useDefaultShellEnvironment();
+ }
+ if (params.containsKey("execution_requirements")) {
+ builder.setExecutionInfo(toMap(castMap(params.get("execution_requirements"),
+ String.class, String.class, "execution_requirements")));
+ }
+ if (params.containsKey("input_manifests")) {
+ for (Map.Entry<PathFragment, Artifact> entry : castMap(params.get("input_manifests"),
+ PathFragment.class, Artifact.class, "input manifest file map")) {
+ builder.addInputManifest(entry.getValue(), entry.getKey());
+ }
+ }
+ // Always register the action
+ ctx.getRuleContext().registerAction(builder.build(ctx.getRuleContext()));
+ return Environment.NONE;
+ }
+ };
+
+ // TODO(bazel-team): improve this method to be more memory friendly
+ @SkylarkBuiltin(name = "file_action",
+ doc = "Creates a file write action.",
+ objectType = SkylarkRuleContext.class,
+ returnType = Environment.NoneType.class,
+ optionalParams = {
+ @Param(name = "executable", type = Boolean.class,
+ doc = "whether the output file should be executable (default is False)"),
+ },
+ mandatoryParams = {
+ @Param(name = "output", type = Artifact.class, doc = "the output file"),
+ @Param(name = "content", type = String.class, doc = "the contents of the file")})
+ private static final SkylarkFunction createFileWriteAction =
+ new SimpleSkylarkFunction("file_action") {
+
+ @Override
+ public Object call(Map<String, Object> params, Location loc) throws EvalException,
+ ConversionException {
+ SkylarkRuleContext ctx = (SkylarkRuleContext) params.get("self");
+ boolean executable = params.containsKey("executable") && (Boolean) params.get("executable");
+ FileWriteAction action = new FileWriteAction(
+ ctx.getRuleContext().getActionOwner(),
+ (Artifact) params.get("output"),
+ (String) params.get("content"),
+ executable);
+ ctx.getRuleContext().registerAction(action);
+ return action;
+ }
+ };
+
+ @SkylarkBuiltin(name = "template_action",
+ doc = "Creates a template expansion action.",
+ objectType = SkylarkRuleContext.class,
+ returnType = Environment.NoneType.class,
+ mandatoryParams = {
+ @Param(name = "template", type = Artifact.class, doc = "the template file"),
+ @Param(name = "output", type = Artifact.class, doc = "the output file"),
+ @Param(name = "substitutions", type = Map.class,
+ doc = "substitutions to make when expanding the template")},
+ optionalParams = {
+ @Param(name = "executable", type = Boolean.class,
+ doc = "whether the output file should be executable (default is False)")})
+ private static final SkylarkFunction createTemplateAction =
+ new SimpleSkylarkFunction("template_action") {
+
+ @Override
+ public Object call(Map<String, Object> params, Location loc) throws EvalException,
+ ConversionException {
+ SkylarkRuleContext ctx = (SkylarkRuleContext) params.get("self");
+ ImmutableList.Builder<Substitution> substitutions = ImmutableList.builder();
+ for (Map.Entry<String, String> substitution
+ : castMap(params.get("substitutions"), String.class, String.class, "substitutions")) {
+ substitutions.add(Substitution.of(substitution.getKey(), substitution.getValue()));
+ }
+
+ boolean executable = params.containsKey("executable") && (Boolean) params.get("executable");
+ TemplateExpansionAction action = new TemplateExpansionAction(
+ ctx.getRuleContext().getActionOwner(),
+ (Artifact) params.get("template"),
+ (Artifact) params.get("output"),
+ substitutions.build(),
+ executable);
+ ctx.getRuleContext().registerAction(action);
+ return action;
+ }
+ };
+
+ /**
+ * A built in Skylark helper function to access the
+ * Transitive info providers of Transitive info collections.
+ */
+ @SkylarkBuiltin(name = "provider",
+ doc = "Returns the transitive info provider provided by the target.",
+ mandatoryParams = {
+ @Param(name = "target", type = TransitiveInfoCollection.class,
+ doc = "the configured target which provides the provider"),
+ @Param(name = "type", type = String.class, doc = "the class type of the provider")})
+ private static final SkylarkFunction provider = new SimpleSkylarkFunction("provider") {
+ @Override
+ public Object call(Map<String, Object> params, Location loc) throws EvalException {
+ TransitiveInfoCollection target = (TransitiveInfoCollection) params.get("target");
+ String type = (String) params.get("type");
+ try {
+ Class<?> classType = SkylarkRuleContext.classCache.get(type);
+ Class<? extends TransitiveInfoProvider> convertedClass =
+ classType.asSubclass(TransitiveInfoProvider.class);
+ Object result = target.getProvider(convertedClass);
+ return result == null ? Environment.NONE : result;
+ } catch (ExecutionException e) {
+ throw new EvalException(loc, "Unknown class type " + type);
+ } catch (ClassCastException e) {
+ throw new EvalException(loc, "Not a TransitiveInfoProvider " + type);
+ }
+ }
+ };
+
+ // TODO(bazel-team): Remove runfile states from Skylark.
+ @SkylarkBuiltin(name = "runfiles",
+ doc = "Creates a runfiles object.",
+ objectType = SkylarkRuleContext.class,
+ returnType = Runfiles.class,
+ optionalParams = {
+ @Param(name = "files", type = SkylarkList.class, generic1 = Artifact.class,
+ doc = "The list of files to be added to the runfiles."),
+ // TODO(bazel-team): If we have a memory efficient support for lazy list containing NestedSets
+ // we can remove this and just use files = [file] + list(set)
+ @Param(name = "transitive_files", type = SkylarkNestedSet.class, generic1 = Artifact.class,
+ doc = "The (transitive) set of files to be added to the runfiles."),
+ @Param(name = "collect_data", type = Boolean.class, doc = "Whether to collect the data "
+ + "runfiles from the dependencies in srcs, data and deps attributes."),
+ @Param(name = "collect_default", type = Boolean.class, doc = "Whether to collect the default "
+ + "runfiles from the dependencies in srcs, data and deps attributes.")})
+ private static final SkylarkFunction runfiles = new SimpleSkylarkFunction("runfiles") {
+ @Override
+ public Object call(Map<String, Object> params, Location loc) throws EvalException,
+ ConversionException {
+ SkylarkRuleContext ctx = (SkylarkRuleContext) params.get("self");
+ Runfiles.Builder builder = new Runfiles.Builder();
+ if (params.containsKey("collect_data") && (Boolean) params.get("collect_data")) {
+ builder.addRunfiles(ctx.getRuleContext(), RunfilesProvider.DATA_RUNFILES);
+ }
+ if (params.containsKey("collect_default") && (Boolean) params.get("collect_default")) {
+ builder.addRunfiles(ctx.getRuleContext(), RunfilesProvider.DEFAULT_RUNFILES);
+ }
+ if (params.containsKey("files")) {
+ builder.addArtifacts(castList(params.get("files"), Artifact.class));
+ }
+ if (params.containsKey("transitive_files")) {
+ builder.addTransitiveArtifacts(cast(params.get("transitive_files"),
+ SkylarkNestedSet.class, "files", loc).getSet(Artifact.class));
+ }
+ return builder.build();
+ }
+ };
+
+ @SkylarkBuiltin(name = "command_helper", doc = "Creates a command helper class.",
+ objectType = SkylarkRuleContext.class,
+ returnType = CommandHelper.class,
+ mandatoryParams = {
+ @Param(name = "tools", type = SkylarkList.class, generic1 = TransitiveInfoCollection.class,
+ doc = "list of tools (list of targets)"),
+ @Param(name = "label_dict", type = Map.class,
+ doc = "dictionary of resolved labels and the corresponding list of artifacts "
+ + "(a dict of Label : list of files)")})
+ private static final SkylarkFunction createCommandHelper =
+ new SimpleSkylarkFunction("command_helper") {
+ @SuppressWarnings("unchecked")
+ @Override
+ protected Object call(Map<String, Object> params, Location loc)
+ throws ConversionException, EvalException {
+ SkylarkRuleContext ctx = (SkylarkRuleContext) params.get("self");
+ return new CommandHelper(ctx.getRuleContext(),
+ AnalysisUtils.getProviders(
+ castList(params.get("tools"), TransitiveInfoCollection.class),
+ FilesToRunProvider.class),
+ // TODO(bazel-team): this cast to Map is unchecked and is not safe.
+ // The best way to fix this probably is to convert CommandHelper to Skylark.
+ ImmutableMap.copyOf((Map<Label, Iterable<Artifact>>) params.get("label_dict")));
+ }
+ };
+
+
+ @SkylarkBuiltin(name = "var",
+ doc = "get the value bound to a configuration variable in the context",
+ objectType = SkylarkRuleContext.class,
+ mandatoryParams = {
+ @Param(name = "name", type = String.class, doc = "the name of the variable")
+ },
+ returnType = String.class)
+ private static final SkylarkFunction configurationMakeVariableContext =
+ new SimpleSkylarkFunction("var") {
+ @SuppressWarnings("unchecked")
+ @Override
+ protected Object call(Map<String, Object> params, Location loc)
+ throws ConversionException, EvalException {
+ SkylarkRuleContext ctx = (SkylarkRuleContext) params.get("self");
+ String name = (String) params.get("name");
+ try {
+ return ctx.getRuleContext().getConfigurationMakeVariableContext()
+ .lookupMakeVariable(name);
+ } catch (MakeVariableExpander.ExpansionException e) {
+ throw new EvalException(loc, "configuration variable "
+ + ShellEscaper.escapeString(name) + " not defined");
+ }
+ }
+ };
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CcBinary.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcBinary.java
new file mode 100644
index 0000000..6efcd9d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcBinary.java
@@ -0,0 +1,635 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.ParameterFile;
+import com.google.devtools.build.lib.analysis.AnalysisEnvironment;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTargetBuilder;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.Runfiles;
+import com.google.devtools.build.lib.analysis.RunfilesProvider;
+import com.google.devtools.build.lib.analysis.RunfilesSupport;
+import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
+import com.google.devtools.build.lib.analysis.Util;
+import com.google.devtools.build.lib.analysis.actions.FileWriteAction;
+import com.google.devtools.build.lib.analysis.actions.SpawnAction;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.packages.TargetUtils;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.rules.RuleConfiguredTargetFactory;
+import com.google.devtools.build.lib.rules.cpp.CppConfiguration.DynamicMode;
+import com.google.devtools.build.lib.rules.cpp.Link.LinkStaticness;
+import com.google.devtools.build.lib.rules.cpp.Link.LinkTargetType;
+import com.google.devtools.build.lib.rules.cpp.LinkerInputs.LibraryToLink;
+import com.google.devtools.build.lib.rules.test.BaselineCoverageAction;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.util.FileType;
+import com.google.devtools.build.lib.util.FileTypeSet;
+import com.google.devtools.build.lib.util.OsUtils;
+import com.google.devtools.build.lib.util.Pair;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A ConfiguredTarget for <code>cc_binary</code> rules.
+ */
+public abstract class CcBinary implements RuleConfiguredTargetFactory {
+
+ private final CppSemantics semantics;
+
+ protected CcBinary(CppSemantics semantics) {
+ this.semantics = semantics;
+ }
+
+ // TODO(bazel-team): should this use Link.SHARED_LIBRARY_FILETYPES?
+ private static final FileTypeSet SHARED_LIBRARY_FILETYPES = FileTypeSet.of(
+ CppFileTypes.SHARED_LIBRARY,
+ CppFileTypes.VERSIONED_SHARED_LIBRARY);
+
+ /**
+ * The maximum number of inputs for any single .dwp generating action. For cases where
+ * this value is exceeded, the action is split up into "batches" that fall under the limit.
+ * See {@link #createDebugPackagerActions} for details.
+ */
+ @VisibleForTesting
+ public static final int MAX_INPUTS_PER_DWP_ACTION = 100;
+
+ /**
+ * Intermediate dwps are written to this subdirectory under the main dwp's output path.
+ */
+ @VisibleForTesting
+ public static final String INTERMEDIATE_DWP_DIR = "_dwps";
+
+ private static Runfiles collectRunfiles(RuleContext context,
+ CcCommon common,
+ CcLinkingOutputs linkingOutputs,
+ CppCompilationContext cppCompilationContext,
+ LinkStaticness linkStaticness,
+ NestedSet<Artifact> filesToBuild,
+ Iterable<Artifact> fakeLinkerInputs,
+ boolean fake) {
+ Runfiles.Builder builder = new Runfiles.Builder();
+ Function<TransitiveInfoCollection, Runfiles> runfilesMapping =
+ CppRunfilesProvider.runfilesFunction(linkStaticness != LinkStaticness.DYNAMIC);
+ boolean linkshared = isLinkShared(context);
+ builder.addTransitiveArtifacts(filesToBuild);
+ // Add the shared libraries to the runfiles. This adds any shared libraries that are in the
+ // srcs of this target.
+ builder.addArtifacts(linkingOutputs.getLibrariesForRunfiles(true));
+ builder.addRunfiles(context, RunfilesProvider.DEFAULT_RUNFILES);
+ builder.add(context, runfilesMapping);
+ CcToolchainProvider toolchain = CppHelper.getToolchain(context);
+ // Add the C++ runtime libraries if linking them dynamically.
+ if (linkStaticness == LinkStaticness.DYNAMIC) {
+ builder.addTransitiveArtifacts(toolchain.getDynamicRuntimeLinkInputs());
+ }
+ // For cc_binary and cc_test rules, there is an implicit dependency on
+ // the malloc library package, which is specified by the "malloc" attribute.
+ // As the BUILD encyclopedia says, the "malloc" attribute should be ignored
+ // if linkshared=1.
+ if (!linkshared) {
+ TransitiveInfoCollection malloc = CppHelper.mallocForTarget(context);
+ builder.addTarget(malloc, RunfilesProvider.DEFAULT_RUNFILES);
+ builder.addTarget(malloc, runfilesMapping);
+ }
+
+ if (fake) {
+ // Add the object files, libraries, and linker scripts that are used to
+ // link this executable.
+ builder.addSymlinksToArtifacts(Iterables.filter(fakeLinkerInputs, Artifact.MIDDLEMAN_FILTER));
+ // The crosstool inputs for the link action are not sufficient; we also need the crosstool
+ // inputs for compilation. Node that these cannot be middlemen because Runfiles does not
+ // know how to expand them.
+ builder.addTransitiveArtifacts(toolchain.getCrosstool());
+ builder.addTransitiveArtifacts(toolchain.getLibcLink());
+ // Add the sources files that are used to compile the object files.
+ // We add the headers in the transitive closure and our own sources in the srcs
+ // attribute. We do not provide the auxiliary inputs, because they are only used when we
+ // do FDO compilation, and cc_fake_binary does not support FDO.
+ builder.addSymlinksToArtifacts(
+ Iterables.transform(common.getCAndCppSources(), Pair.<Artifact, Label>firstFunction()));
+ builder.addSymlinksToArtifacts(cppCompilationContext.getDeclaredIncludeSrcs());
+ }
+ return builder.build();
+ }
+
+ @Override
+ public ConfiguredTarget create(RuleContext context) {
+ return CcBinary.init(semantics, context, /*fake =*/ false, /*useTestOnlyFlags =*/ false);
+ }
+
+ public static ConfiguredTarget init(CppSemantics semantics, RuleContext ruleContext, boolean fake,
+ boolean useTestOnlyFlags) {
+ ruleContext.checkSrcsSamePackage(true);
+ CcCommon common = new CcCommon(ruleContext);
+ CppConfiguration cppConfiguration = ruleContext.getFragment(CppConfiguration.class);
+
+ LinkTargetType linkType =
+ isLinkShared(ruleContext) ? LinkTargetType.DYNAMIC_LIBRARY : LinkTargetType.EXECUTABLE;
+
+ CcLibraryHelper helper = new CcLibraryHelper(ruleContext, semantics)
+ .setLinkType(linkType)
+ .setHeadersCheckingMode(common.determineHeadersCheckingMode())
+ .addCopts(common.getCopts())
+ .setNoCopts(common.getNoCopts())
+ .addLinkopts(common.getLinkopts())
+ .addDefines(common.getDefines())
+ .addCompilationPrerequisites(common.getSharedLibrariesFromSrcs())
+ .addCompilationPrerequisites(common.getStaticLibrariesFromSrcs())
+ .addSources(common.getCAndCppSources())
+ .addPrivateHeaders(FileType.filter(
+ ruleContext.getPrerequisiteArtifacts("srcs", Mode.TARGET).list(),
+ CppFileTypes.CPP_HEADER))
+ .addObjectFiles(common.getObjectFilesFromSrcs(false))
+ .addPicObjectFiles(common.getObjectFilesFromSrcs(true))
+ .addPicIndependentObjectFiles(common.getLinkerScripts())
+ .addDeps(ruleContext.getPrerequisites("deps", Mode.TARGET))
+ .addDeps(ImmutableList.of(CppHelper.mallocForTarget(ruleContext)))
+ .setEnableLayeringCheck(ruleContext.getFeatures().contains(CppRuleClasses.LAYERING_CHECK))
+ .addSystemIncludeDirs(common.getSystemIncludeDirs())
+ .addIncludeDirs(common.getIncludeDirs())
+ .addLooseIncludeDirs(common.getLooseIncludeDirs())
+ .setFake(fake);
+
+ CcLibraryHelper.Info info = helper.build();
+ CppCompilationContext cppCompilationContext = info.getCppCompilationContext();
+ CcCompilationOutputs ccCompilationOutputs = info.getCcCompilationOutputs();
+
+ // if cc_binary includes "linkshared=1", then gcc will be invoked with
+ // linkopt "-shared", which causes the result of linking to be a shared
+ // library. In this case, the name of the executable target should end
+ // in ".so".
+ PathFragment executableName = Util.getWorkspaceRelativePath(
+ ruleContext.getTarget(), "", OsUtils.executableExtension());
+ CppLinkAction.Builder linkActionBuilder = determineLinkerArguments(
+ ruleContext, common, cppConfiguration, ccCompilationOutputs,
+ cppCompilationContext.getCompilationPrerequisites(), fake, executableName);
+ linkActionBuilder.setUseTestOnlyFlags(useTestOnlyFlags);
+ linkActionBuilder.addNonLibraryInputs(ccCompilationOutputs.getHeaderTokenFiles());
+
+ CcToolchainProvider ccToolchain = CppHelper.getToolchain(ruleContext);
+ LinkStaticness linkStaticness = getLinkStaticness(ruleContext, common, cppConfiguration);
+ if (linkStaticness == LinkStaticness.DYNAMIC) {
+ linkActionBuilder.setRuntimeInputs(
+ ccToolchain.getDynamicRuntimeLinkMiddleman(),
+ ccToolchain.getDynamicRuntimeLinkInputs());
+ } else {
+ linkActionBuilder.setRuntimeInputs(
+ ccToolchain.getStaticRuntimeLinkMiddleman(),
+ ccToolchain.getStaticRuntimeLinkInputs());
+ // Only force a static link of libgcc if static runtime linking is enabled (which
+ // can't be true if runtimeInputs is empty).
+ // TODO(bazel-team): Move this to CcToolchain.
+ if (!ccToolchain.getStaticRuntimeLinkInputs().isEmpty()) {
+ linkActionBuilder.addLinkopt("-static-libgcc");
+ }
+ }
+
+ linkActionBuilder.setLinkType(linkType);
+ linkActionBuilder.setLinkStaticness(linkStaticness);
+ linkActionBuilder.setFake(fake);
+
+ // store immutable context now, recreate builder later
+ CppLinkAction.Context linkContext = new CppLinkAction.Context(linkActionBuilder);
+
+ CppLinkAction linkAction = linkActionBuilder.build();
+ ruleContext.registerAction(linkAction);
+ LibraryToLink outputLibrary = linkAction.getOutputLibrary();
+ Iterable<Artifact> fakeLinkerInputs =
+ fake ? linkAction.getInputs() : ImmutableList.<Artifact>of();
+ Artifact executable = outputLibrary.getArtifact();
+ CcLinkingOutputs.Builder linkingOutputsBuilder = new CcLinkingOutputs.Builder();
+ if (isLinkShared(ruleContext)) {
+ if (CppFileTypes.SHARED_LIBRARY.matches(executableName)) {
+ linkingOutputsBuilder.addDynamicLibrary(outputLibrary);
+ linkingOutputsBuilder.addExecutionDynamicLibrary(outputLibrary);
+ } else {
+ ruleContext.attributeError("linkshared", "'linkshared' used in non-shared library");
+ }
+ }
+ // Also add all shared libraries from srcs.
+ for (Artifact library : common.getSharedLibrariesFromSrcs()) {
+ LibraryToLink symlink = common.getDynamicLibrarySymlink(library, true);
+ linkingOutputsBuilder.addDynamicLibrary(symlink);
+ linkingOutputsBuilder.addExecutionDynamicLibrary(symlink);
+ }
+ CcLinkingOutputs linkingOutputs = linkingOutputsBuilder.build();
+ NestedSet<Artifact> filesToBuild = NestedSetBuilder.create(Order.STABLE_ORDER, executable);
+
+ // Create the stripped binary, but don't add it to filesToBuild; it's only built when requested.
+ Artifact strippedFile = ruleContext.getImplicitOutputArtifact(
+ CppRuleClasses.CC_BINARY_STRIPPED);
+ createStripAction(ruleContext, cppConfiguration, executable, strippedFile);
+
+ DwoArtifactsCollector dwoArtifacts =
+ collectTransitiveDwoArtifacts(ruleContext, common, cppConfiguration, ccCompilationOutputs);
+ Artifact dwpFile =
+ ruleContext.getImplicitOutputArtifact(CppRuleClasses.CC_BINARY_DEBUG_PACKAGE);
+ createDebugPackagerActions(ruleContext, cppConfiguration, dwpFile, dwoArtifacts);
+
+ // The debug package should include the dwp file only if it was explicitly requested.
+ Artifact explicitDwpFile = dwpFile;
+ if (!cppConfiguration.useFission()) {
+ explicitDwpFile = null;
+ }
+
+ // TODO(bazel-team): Do we need to put original shared libraries (along with
+ // mangled symlinks) into the RunfilesSupport object? It does not seem
+ // logical since all symlinked libraries will be linked anyway and would
+ // not require manual loading but if we do, then we would need to collect
+ // their names and use a different constructor below.
+ Runfiles runfiles = collectRunfiles(ruleContext, common, linkingOutputs,
+ cppCompilationContext, linkStaticness, filesToBuild, fakeLinkerInputs, fake);
+ RunfilesSupport runfilesSupport = RunfilesSupport.withExecutable(
+ ruleContext, runfiles, executable, ruleContext.getConfiguration().buildRunfiles());
+
+ TransitiveLipoInfoProvider transitiveLipoInfo;
+ if (cppConfiguration.isLipoContextCollector()) {
+ transitiveLipoInfo = common.collectTransitiveLipoLabels(ccCompilationOutputs);
+ } else {
+ transitiveLipoInfo = TransitiveLipoInfoProvider.EMPTY;
+ }
+
+ RuleConfiguredTargetBuilder ruleBuilder = new RuleConfiguredTargetBuilder(ruleContext);
+ common.addTransitiveInfoProviders(
+ ruleBuilder, filesToBuild, ccCompilationOutputs, cppCompilationContext, linkingOutputs,
+ dwoArtifacts, transitiveLipoInfo);
+
+ Map<Artifact, IncludeScannable> scannableMap = new LinkedHashMap<>();
+ if (cppConfiguration.isLipoContextCollector()) {
+ for (IncludeScannable scannable : transitiveLipoInfo.getTransitiveIncludeScannables()) {
+ // These should all be CppCompileActions, which should have only one source file.
+ // This is also checked when they are put into the nested set.
+ Artifact source =
+ Iterables.getOnlyElement(scannable.getIncludeScannerSources());
+ scannableMap.put(source, scannable);
+ }
+ }
+
+ return ruleBuilder
+ .add(RunfilesProvider.class, RunfilesProvider.simple(runfiles))
+ .add(
+ CppDebugPackageProvider.class,
+ new CppDebugPackageProvider(strippedFile, executable, explicitDwpFile))
+ .setRunfilesSupport(runfilesSupport, executable)
+ .setBaselineCoverageArtifacts(createBaselineCoverageArtifacts(
+ ruleContext, common, ccCompilationOutputs, fake))
+ .addProvider(LipoContextProvider.class, new LipoContextProvider(
+ cppCompilationContext, ImmutableMap.copyOf(scannableMap)))
+ .addProvider(CppLinkAction.Context.class, linkContext)
+ .build();
+ }
+
+ /**
+ * Creates an action to strip an executable.
+ */
+ private static void createStripAction(RuleContext context,
+ CppConfiguration cppConfiguration, Artifact input, Artifact output) {
+ context.registerAction(new SpawnAction.Builder()
+ .addInput(input)
+ .addTransitiveInputs(CppHelper.getToolchain(context).getStrip())
+ .addOutput(output)
+ .useDefaultShellEnvironment()
+ .setExecutable(cppConfiguration.getStripExecutable())
+ .addArguments("-S", "-p", "-o", output.getExecPathString())
+ .addArguments("-R", ".gnu.switches.text.quote_paths")
+ .addArguments("-R", ".gnu.switches.text.bracket_paths")
+ .addArguments("-R", ".gnu.switches.text.system_paths")
+ .addArguments("-R", ".gnu.switches.text.cpp_defines")
+ .addArguments("-R", ".gnu.switches.text.cpp_includes")
+ .addArguments("-R", ".gnu.switches.text.cl_args")
+ .addArguments("-R", ".gnu.switches.text.lipo_info")
+ .addArguments("-R", ".gnu.switches.text.annotation")
+ .addArguments(cppConfiguration.getStripOpts())
+ .addArgument(input.getExecPathString())
+ .setProgressMessage("Stripping " + output.prettyPrint() + " for " + context.getLabel())
+ .setMnemonic("CcStrip")
+ .build(context));
+ }
+
+ /**
+ * Given 'temps', traverse this target and its dependencies and collect up all
+ * the object files, libraries, linker options, linkstamps attributes and linker scripts.
+ */
+ private static CppLinkAction.Builder determineLinkerArguments(RuleContext context,
+ CcCommon common, CppConfiguration cppConfiguration, CcCompilationOutputs compilationOutputs,
+ ImmutableSet<Artifact> compilationPrerequisites,
+ boolean fake, PathFragment executableName) {
+ CppLinkAction.Builder builder = new CppLinkAction.Builder(context, executableName)
+ .setCrosstoolInputs(CppHelper.getToolchain(context).getLink())
+ .addNonLibraryInputs(compilationPrerequisites);
+
+ // Determine the object files to link in.
+ boolean usePic = CppHelper.usePic(context, !isLinkShared(context)) && !fake;
+ Iterable<Artifact> compiledObjectFiles = compilationOutputs.getObjectFiles(usePic);
+
+ if (fake) {
+ builder.addFakeNonLibraryInputs(compiledObjectFiles);
+ } else {
+ builder.addNonLibraryInputs(compiledObjectFiles);
+ }
+
+ builder.addNonLibraryInputs(common.getObjectFilesFromSrcs(usePic));
+ builder.addNonLibraryInputs(common.getLinkerScripts());
+
+ // Determine the libraries to link in.
+ // First libraries from srcs. Shared library artifacts here are substituted with mangled symlink
+ // artifacts generated by getDynamicLibraryLink(). This is done to minimize number of -rpath
+ // entries during linking process.
+ for (Artifact library : common.getLibrariesFromSrcs()) {
+ if (SHARED_LIBRARY_FILETYPES.matches(library.getFilename())) {
+ builder.addLibrary(common.getDynamicLibrarySymlink(library, true));
+ } else {
+ builder.addLibrary(LinkerInputs.opaqueLibraryToLink(library));
+ }
+ }
+
+ // Then libraries from the closure of deps.
+ List<String> linkopts = new ArrayList<>();
+ Map<Artifact, ImmutableList<Artifact>> linkstamps = new LinkedHashMap<>();
+
+ NestedSet<LibraryToLink> librariesInDepsClosure =
+ findLibrariesToLinkInDepsClosure(context, common, cppConfiguration, linkopts, linkstamps);
+ builder.addLinkopts(linkopts);
+ builder.addLinkstamps(linkstamps);
+
+ builder.addLibraries(librariesInDepsClosure);
+ return builder;
+ }
+
+ /**
+ * Explore the transitive closure of our deps to collect linking information.
+ */
+ private static NestedSet<LibraryToLink> findLibrariesToLinkInDepsClosure(
+ RuleContext context,
+ CcCommon common,
+ CppConfiguration cppConfiguration,
+ List<String> linkopts,
+ Map<Artifact,
+ ImmutableList<Artifact>> linkstamps) {
+ // This is true for both FULLY STATIC and MOSTLY STATIC linking.
+ boolean linkingStatically =
+ getLinkStaticness(context, common, cppConfiguration) != LinkStaticness.DYNAMIC;
+
+ CcLinkParams linkParams = collectCcLinkParams(
+ context, common, linkingStatically, isLinkShared(context));
+ linkopts.addAll(linkParams.flattenedLinkopts());
+ linkstamps.putAll(CppHelper.resolveLinkstamps(context, linkParams));
+ return linkParams.getLibraries();
+ }
+
+ /**
+ * Gets the linkopts to use for this binary. These options are NOT used when
+ * linking other binaries that depend on this binary.
+ *
+ * @return a new List instance that contains the linkopts for this binary
+ * target.
+ */
+ private static ImmutableList<String> getBinaryLinkopts(RuleContext context,
+ CcCommon common) {
+ List<String> linkopts = new ArrayList<>();
+ if (isLinkShared(context)) {
+ linkopts.add("-shared");
+ }
+ linkopts.addAll(common.getLinkopts());
+ return ImmutableList.copyOf(linkopts);
+ }
+
+ private static boolean linkstaticAttribute(RuleContext context) {
+ return context.attributes().get("linkstatic", Type.BOOLEAN);
+ }
+
+ /**
+ * Returns "true" if the {@code linkshared} attribute exists and is set.
+ */
+ private static final boolean isLinkShared(RuleContext context) {
+ return context.getRule().getRuleClassObject().hasAttr("linkshared", Type.BOOLEAN)
+ && context.attributes().get("linkshared", Type.BOOLEAN);
+ }
+
+ private static final boolean dashStaticInLinkopts(CcCommon common,
+ CppConfiguration cppConfiguration) {
+ return common.getLinkopts().contains("-static")
+ || cppConfiguration.getLinkOptions().contains("-static");
+ }
+
+ private static final LinkStaticness getLinkStaticness(RuleContext context,
+ CcCommon common, CppConfiguration cppConfiguration) {
+ if (cppConfiguration.getDynamicMode() == DynamicMode.FULLY) {
+ return LinkStaticness.DYNAMIC;
+ } else if (dashStaticInLinkopts(common, cppConfiguration)) {
+ return LinkStaticness.FULLY_STATIC;
+ } else if (cppConfiguration.getDynamicMode() == DynamicMode.OFF
+ || linkstaticAttribute(context)) {
+ return LinkStaticness.MOSTLY_STATIC;
+ } else {
+ return LinkStaticness.DYNAMIC;
+ }
+ }
+
+ /**
+ * Collects .dwo artifacts either transitively or directly, depending on the link type.
+ *
+ * <p>For a cc_binary, we only include the .dwo files corresponding to the .o files that are
+ * passed into the link. For static linking, this includes all transitive dependencies. But
+ * for dynamic linking, dependencies are separately linked into their own shared libraries,
+ * so we don't need them here.
+ */
+ private static DwoArtifactsCollector collectTransitiveDwoArtifacts(RuleContext context,
+ CcCommon common, CppConfiguration cppConfiguration, CcCompilationOutputs compilationOutputs) {
+ if (getLinkStaticness(context, common, cppConfiguration) == LinkStaticness.DYNAMIC) {
+ return DwoArtifactsCollector.directCollector(compilationOutputs);
+ } else {
+ return CcCommon.collectTransitiveDwoArtifacts(context, compilationOutputs);
+ }
+ }
+
+ @VisibleForTesting
+ public static Iterable<Artifact> getDwpInputs(
+ RuleContext context, NestedSet<Artifact> picDwoArtifacts, NestedSet<Artifact> dwoArtifacts) {
+ return CppHelper.usePic(context, !isLinkShared(context)) ? picDwoArtifacts : dwoArtifacts;
+ }
+
+ /**
+ * Creates the actions needed to generate this target's "debug info package"
+ * (i.e. its .dwp file).
+ */
+ private static void createDebugPackagerActions(RuleContext context,
+ CppConfiguration cppConfiguration, Artifact dwpOutput,
+ DwoArtifactsCollector dwoArtifactsCollector) {
+ Iterable<Artifact> allInputs = getDwpInputs(context,
+ dwoArtifactsCollector.getPicDwoArtifacts(),
+ dwoArtifactsCollector.getDwoArtifacts());
+
+ // No inputs? Just generate a trivially empty .dwp.
+ //
+ // Note this condition automatically triggers for any build where fission is disabled.
+ // Because rules referencing .dwp targets may be invoked with or without fission, we need
+ // to support .dwp generation even when fission is disabled. Since no actual functionality
+ // is expected then, an empty file is appropriate.
+ if (Iterables.isEmpty(allInputs)) {
+ context.registerAction(
+ new FileWriteAction(context.getActionOwner(), dwpOutput, "", false));
+ return;
+ }
+
+ // Get the tool inputs necessary to run the dwp command.
+ NestedSet<Artifact> dwpTools = CppHelper.getToolchain(context).getDwp();
+ Preconditions.checkState(!dwpTools.isEmpty());
+
+ // We apply a hierarchical action structure to limit the maximum number of inputs to any
+ // single action.
+ //
+ // While the dwp tools consumes .dwo files, it can also consume intermediate .dwp files,
+ // allowing us to split a large input set into smaller batches of arbitrary size and order.
+ // Aside from the parallelism performance benefits this offers, this also reduces input
+ // size requirements: if a.dwo, b.dwo, c.dwo, and e.dwo are each 1 KB files, we can apply
+ // two intermediate actions DWP(a.dwo, b.dwo) --> i1.dwp and DWP(c.dwo, e.dwo) --> i2.dwp.
+ // When we then apply the final action DWP(i1.dwp, i2.dwp) --> finalOutput.dwp, the inputs
+ // to this action will usually total far less than 4 KB.
+ //
+ // This list tracks every action we'll need to generate the output .dwp with batching.
+ List<SpawnAction.Builder> packagers = new ArrayList<>();
+
+ // Step 1: generate our batches. We currently break into arbitrary batches of fixed maximum
+ // input counts, but we can always apply more intelligent heuristics if the need arises.
+ SpawnAction.Builder currentPackager = newDwpAction(cppConfiguration, dwpTools);
+ int inputsForCurrentPackager = 0;
+
+ for (Artifact dwoInput : allInputs) {
+ if (inputsForCurrentPackager == MAX_INPUTS_PER_DWP_ACTION) {
+ packagers.add(currentPackager);
+ currentPackager = newDwpAction(cppConfiguration, dwpTools);
+ inputsForCurrentPackager = 0;
+ }
+ currentPackager.addInputArgument(dwoInput);
+ inputsForCurrentPackager++;
+ }
+ packagers.add(currentPackager);
+
+ // Step 2: given the batches, create the actions.
+ if (packagers.size() == 1) {
+ // If we only have one batch, make a single "original inputs --> final output" action.
+ context.registerAction(Iterables.getOnlyElement(packagers)
+ .addArgument("-o")
+ .addOutputArgument(dwpOutput)
+ .setMnemonic("CcGenerateDwp")
+ .build(context));
+ } else {
+ // If we have multiple batches, make them all intermediate actions, then pipe their outputs
+ // into an additional action that outputs the final artifact.
+ //
+ // Note this only creates a hierarchy one level deep (i.e. we don't check if the number of
+ // intermediate outputs exceeds the maximum batch size). This is okay for current needs,
+ // which shouldn't stress those limits.
+ List<Artifact> intermediateOutputs = new ArrayList<>();
+
+ int count = 1;
+ for (SpawnAction.Builder packager : packagers) {
+ Artifact intermediateOutput =
+ getIntermediateDwpFile(context.getAnalysisEnvironment(), dwpOutput, count++);
+ context.registerAction(packager
+ .addArgument("-o")
+ .addOutputArgument(intermediateOutput)
+ .setMnemonic("CcGenerateIntermediateDwp")
+ .build(context));
+ intermediateOutputs.add(intermediateOutput);
+ }
+
+ // Now create the final action.
+ context.registerAction(newDwpAction(cppConfiguration, dwpTools)
+ .addInputArguments(intermediateOutputs)
+ .addArgument("-o")
+ .addOutputArgument(dwpOutput)
+ .setMnemonic("CcGenerateDwp")
+ .build(context));
+ }
+ }
+
+ /**
+ * Returns a new SpawnAction builder for generating dwp files, pre-initialized with
+ * standard settings.
+ */
+ private static SpawnAction.Builder newDwpAction(CppConfiguration cppConfiguration,
+ NestedSet<Artifact> dwpTools) {
+ return new SpawnAction.Builder()
+ .addTransitiveInputs(dwpTools)
+ .setExecutable(cppConfiguration.getDwpExecutable())
+ .useParameterFile(ParameterFile.ParameterFileType.UNQUOTED);
+ }
+
+ /**
+ * Creates an intermediate dwp file keyed off the name and path of the final output.
+ */
+ private static Artifact getIntermediateDwpFile(AnalysisEnvironment env, Artifact dwpOutput,
+ int orderNumber) {
+ PathFragment outputPath = dwpOutput.getRootRelativePath();
+ PathFragment intermediatePath =
+ FileSystemUtils.appendWithoutExtension(outputPath, "-" + orderNumber);
+ return env.getDerivedArtifact(
+ outputPath.getParentDirectory().getRelative(
+ INTERMEDIATE_DWP_DIR + "/" + intermediatePath.getPathString()),
+ dwpOutput.getRoot());
+ }
+
+ /**
+ * Collect link parameters from the transitive closure.
+ */
+ private static CcLinkParams collectCcLinkParams(RuleContext context, CcCommon common,
+ boolean linkingStatically, boolean linkShared) {
+ CcLinkParams.Builder builder = CcLinkParams.builder(linkingStatically, linkShared);
+
+ if (isLinkShared(context)) {
+ // CcLinkingOutputs is empty because this target is not configured yet
+ builder.addCcLibrary(context, common, false, CcLinkingOutputs.EMPTY);
+ } else {
+ builder.addTransitiveTargets(
+ context.getPrerequisites("deps", Mode.TARGET),
+ CcLinkParamsProvider.TO_LINK_PARAMS, CcSpecificLinkParamsProvider.TO_LINK_PARAMS);
+ builder.addTransitiveTarget(CppHelper.mallocForTarget(context));
+ builder.addLinkOpts(getBinaryLinkopts(context, common));
+ }
+ return builder.build();
+ }
+
+ private static ImmutableList<Artifact> createBaselineCoverageArtifacts(
+ RuleContext context, CcCommon common, CcCompilationOutputs compilationOutputs,
+ boolean fake) {
+ if (!TargetUtils.isTestRule(context.getRule()) && !fake) {
+ Iterable<Artifact> objectFiles = compilationOutputs.getObjectFiles(
+ CppHelper.usePic(context, !isLinkShared(context)));
+ return BaselineCoverageAction.getBaselineCoverageArtifacts(context,
+ common.getInstrumentedFilesProvider(objectFiles).getInstrumentedFiles());
+ } else {
+ return ImmutableList.of();
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CcCommon.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcCommon.java
new file mode 100644
index 0000000..3f5ff76
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcCommon.java
@@ -0,0 +1,678 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Maps;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.AnalysisEnvironment;
+import com.google.devtools.build.lib.analysis.AnalysisUtils;
+import com.google.devtools.build.lib.analysis.CompilationPrerequisitesProvider;
+import com.google.devtools.build.lib.analysis.FileProvider;
+import com.google.devtools.build.lib.analysis.FilesToCompileProvider;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTargetBuilder;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.TempsProvider;
+import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.rules.cpp.CcToolchainFeatures.FeatureConfiguration;
+import com.google.devtools.build.lib.rules.cpp.CppConfiguration.DynamicMode;
+import com.google.devtools.build.lib.rules.cpp.CppConfiguration.HeadersCheckingMode;
+import com.google.devtools.build.lib.rules.cpp.LinkerInputs.LibraryToLink;
+import com.google.devtools.build.lib.rules.test.InstrumentedFilesCollector;
+import com.google.devtools.build.lib.rules.test.InstrumentedFilesCollector.LocalMetadataCollector;
+import com.google.devtools.build.lib.rules.test.InstrumentedFilesProvider;
+import com.google.devtools.build.lib.rules.test.InstrumentedFilesProviderImpl;
+import com.google.devtools.build.lib.shell.ShellUtils;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.util.FileType;
+import com.google.devtools.build.lib.util.FileTypeSet;
+import com.google.devtools.build.lib.util.Pair;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+/**
+ * Common parts of the implementation of cc rules.
+ */
+public final class CcCommon {
+
+ private static final String NO_COPTS_ATTRIBUTE = "nocopts";
+
+ private static final FileTypeSet SOURCE_TYPES = FileTypeSet.of(
+ CppFileTypes.CPP_SOURCE,
+ CppFileTypes.CPP_HEADER,
+ CppFileTypes.C_SOURCE,
+ CppFileTypes.ASSEMBLER_WITH_C_PREPROCESSOR);
+
+ /**
+ * Collects all metadata files generated by C++ compilation actions that output the .o files
+ * on the input.
+ */
+ private static final LocalMetadataCollector CC_METADATA_COLLECTOR =
+ new LocalMetadataCollector() {
+ @Override
+ public void collectMetadataArtifacts(Iterable<Artifact> objectFiles,
+ AnalysisEnvironment analysisEnvironment, NestedSetBuilder<Artifact> metadataFilesBuilder) {
+ for (Artifact artifact : objectFiles) {
+ Action action = analysisEnvironment.getLocalGeneratingAction(artifact);
+ if (action instanceof CppCompileAction) {
+ addOutputs(metadataFilesBuilder, action, CppFileTypes.COVERAGE_NOTES);
+ }
+ }
+ }
+ };
+
+ /** C++ configuration */
+ private final CppConfiguration cppConfiguration;
+
+ /** The Artifacts from srcs. */
+ private final ImmutableList<Artifact> sources;
+
+ private final ImmutableList<Pair<Artifact, Label>> cAndCppSources;
+
+ /** Expanded and tokenized copts attribute. Set by initCopts(). */
+ private final ImmutableList<String> copts;
+
+ /**
+ * The expanded linkopts for this rule.
+ */
+ private final ImmutableList<String> linkopts;
+
+ private final RuleContext ruleContext;
+
+ public CcCommon(RuleContext ruleContext) {
+ this.ruleContext = ruleContext;
+ this.cppConfiguration = ruleContext.getFragment(CppConfiguration.class);
+ this.sources = hasAttribute("srcs", Type.LABEL_LIST)
+ ? ruleContext.getPrerequisiteArtifacts("srcs", Mode.TARGET).list()
+ : ImmutableList.<Artifact>of();
+
+ this.cAndCppSources = collectCAndCppSources();
+ copts = initCopts();
+ linkopts = initLinkopts();
+ }
+
+ ImmutableList<Artifact> getTemps(CcCompilationOutputs compilationOutputs) {
+ return cppConfiguration.isLipoContextCollector()
+ ? ImmutableList.<Artifact>of()
+ : compilationOutputs.getTemps();
+ }
+
+ /**
+ * Returns our own linkopts from the rule attribute. This determines linker
+ * options to use when building this target and anything that depends on it.
+ */
+ public ImmutableList<String> getLinkopts() {
+ return linkopts;
+ }
+
+ public ImmutableList<String> getCopts() {
+ return copts;
+ }
+
+ private boolean hasAttribute(String name, Type<?> type) {
+ return ruleContext.getRule().getRuleClassObject().hasAttr(name, type);
+ }
+
+ private static NestedSet<Artifact> collectExecutionDynamicLibraryArtifacts(
+ RuleContext ruleContext,
+ List<LibraryToLink> executionDynamicLibraries) {
+ Iterable<Artifact> artifacts = LinkerInputs.toLibraryArtifacts(executionDynamicLibraries);
+ if (!Iterables.isEmpty(artifacts)) {
+ return NestedSetBuilder.wrap(Order.STABLE_ORDER, artifacts);
+ }
+
+ Iterable<CcExecutionDynamicLibrariesProvider> deps = ruleContext
+ .getPrerequisites("deps", Mode.TARGET, CcExecutionDynamicLibrariesProvider.class);
+
+ NestedSetBuilder<Artifact> builder = NestedSetBuilder.stableOrder();
+ for (CcExecutionDynamicLibrariesProvider dep : deps) {
+ builder.addTransitive(dep.getExecutionDynamicLibraryArtifacts());
+ }
+ return builder.build();
+ }
+
+ /**
+ * Collects all .dwo artifacts in this target's transitive closure.
+ */
+ public static DwoArtifactsCollector collectTransitiveDwoArtifacts(
+ RuleContext ruleContext,
+ CcCompilationOutputs compilationOutputs) {
+ ImmutableList.Builder<TransitiveInfoCollection> deps =
+ ImmutableList.<TransitiveInfoCollection>builder();
+
+ deps.addAll(ruleContext.getPrerequisites("deps", Mode.TARGET));
+
+ if (ruleContext.getRule().getRuleClassObject().hasAttr("malloc", Type.LABEL)) {
+ deps.add(CppHelper.mallocForTarget(ruleContext));
+ }
+ if (ruleContext.getRule().getRuleClassObject().hasAttr("implementation", Type.LABEL_LIST)) {
+ deps.addAll(ruleContext.getPrerequisites("implementation", Mode.TARGET));
+ }
+
+ return compilationOutputs == null // Possible in LIPO collection mode (see initializationHook).
+ ? DwoArtifactsCollector.emptyCollector()
+ : DwoArtifactsCollector.transitiveCollector(compilationOutputs, deps.build());
+ }
+
+ public TransitiveLipoInfoProvider collectTransitiveLipoLabels(CcCompilationOutputs outputs) {
+ if (cppConfiguration.getFdoSupport().getFdoRoot() == null
+ || !cppConfiguration.isLipoContextCollector()) {
+ return TransitiveLipoInfoProvider.EMPTY;
+ }
+
+ NestedSetBuilder<IncludeScannable> scannableBuilder = NestedSetBuilder.stableOrder();
+ CppHelper.addTransitiveLipoInfoForCommonAttributes(ruleContext, outputs, scannableBuilder);
+ if (hasAttribute("implementation", Type.LABEL_LIST)) {
+ for (TransitiveLipoInfoProvider impl : AnalysisUtils.getProviders(
+ ruleContext.getPrerequisites("implementation", Mode.TARGET),
+ TransitiveLipoInfoProvider.class)) {
+ scannableBuilder.addTransitive(impl.getTransitiveIncludeScannables());
+ }
+ }
+
+ return new TransitiveLipoInfoProvider(scannableBuilder.build());
+ }
+
+ private NestedSet<LinkerInput> collectTransitiveCcNativeLibraries(
+ RuleContext ruleContext,
+ List<? extends LinkerInput> dynamicLibraries) {
+ NestedSetBuilder<LinkerInput> builder = NestedSetBuilder.linkOrder();
+ builder.addAll(dynamicLibraries);
+ for (CcNativeLibraryProvider dep :
+ ruleContext.getPrerequisites("deps", Mode.TARGET, CcNativeLibraryProvider.class)) {
+ builder.addTransitive(dep.getTransitiveCcNativeLibraries());
+ }
+ return builder.build();
+ }
+
+ /**
+ * Returns a list of ({@link Artifact}, {@link Label}) pairs. Each pair represents an input
+ * source file and the label of the rule that generates it (or the label of the source file
+ * itself if it is an input file)
+ */
+ ImmutableList<Pair<Artifact, Label>> getCAndCppSources() {
+ return cAndCppSources;
+ }
+
+ private boolean shouldProcessHeaders() {
+ boolean crosstoolSupportsHeaderParsing =
+ CppHelper.getToolchain(ruleContext).supportsHeaderParsing();
+ return crosstoolSupportsHeaderParsing && (
+ ruleContext.getFeatures().contains(CppRuleClasses.PREPROCESS_HEADERS)
+ || ruleContext.getFeatures().contains(CppRuleClasses.PARSE_HEADERS));
+ }
+
+ private ImmutableList<Pair<Artifact, Label>> collectCAndCppSources() {
+ Map<Artifact, Label> map = Maps.newLinkedHashMap();
+ if (!hasAttribute("srcs", Type.LABEL_LIST)) {
+ return ImmutableList.<Pair<Artifact, Label>>of();
+ }
+ Iterable<FileProvider> providers =
+ ruleContext.getPrerequisites("srcs", Mode.TARGET, FileProvider.class);
+ // TODO(bazel-team): Move header processing logic down in the stack (to CcLibraryHelper or
+ // such).
+ boolean processHeaders = shouldProcessHeaders();
+ if (processHeaders && hasAttribute("hdrs", Type.LABEL_LIST)) {
+ providers = Iterables.concat(providers,
+ ruleContext.getPrerequisites("hdrs", Mode.TARGET, FileProvider.class));
+ }
+ for (FileProvider provider : providers) {
+ for (Artifact artifact : FileType.filter(provider.getFilesToBuild(), SOURCE_TYPES)) {
+ boolean isHeader = CppFileTypes.CPP_HEADER.matches(artifact.getExecPath());
+ if ((isHeader && !processHeaders)
+ || CppFileTypes.CPP_TEXTUAL_INCLUDE.matches(artifact.getExecPath())) {
+ continue;
+ }
+ Label oldLabel = map.put(artifact, provider.getLabel());
+ // TODO(bazel-team): We currently do not warn for duplicate headers with
+ // different labels, as that would require cleaning up the code base
+ // without significant benefit; we should eventually make this
+ // consistent one way or the other.
+ if (!isHeader && oldLabel != null && !oldLabel.equals(provider.getLabel())) {
+ ruleContext.attributeError("srcs", String.format(
+ "Artifact '%s' is duplicated (through '%s' and '%s')",
+ artifact.getExecPathString(), oldLabel, provider.getLabel()));
+ }
+ }
+ }
+
+ ImmutableList.Builder<Pair<Artifact, Label>> result = ImmutableList.builder();
+ for (Map.Entry<Artifact, Label> entry : map.entrySet()) {
+ result.add(Pair.of(entry.getKey(), entry.getValue()));
+ }
+
+ return result.build();
+ }
+
+ Iterable<Artifact> getLibrariesFromSrcs() {
+ return FileType.filter(sources, CppFileTypes.ARCHIVE, CppFileTypes.PIC_ARCHIVE,
+ CppFileTypes.ALWAYS_LINK_LIBRARY, CppFileTypes.ALWAYS_LINK_PIC_LIBRARY,
+ CppFileTypes.SHARED_LIBRARY,
+ CppFileTypes.VERSIONED_SHARED_LIBRARY);
+ }
+
+ Iterable<Artifact> getSharedLibrariesFromSrcs() {
+ return getSharedLibrariesFrom(sources);
+ }
+
+ static Iterable<Artifact> getSharedLibrariesFrom(Iterable<Artifact> collection) {
+ return FileType.filter(collection, CppFileTypes.SHARED_LIBRARY,
+ CppFileTypes.VERSIONED_SHARED_LIBRARY);
+ }
+
+ Iterable<Artifact> getStaticLibrariesFromSrcs() {
+ return FileType.filter(sources, CppFileTypes.ARCHIVE, CppFileTypes.ALWAYS_LINK_LIBRARY);
+ }
+
+ Iterable<LibraryToLink> getPicStaticLibrariesFromSrcs() {
+ return LinkerInputs.opaqueLibrariesToLink(
+ FileType.filter(sources, CppFileTypes.PIC_ARCHIVE,
+ CppFileTypes.ALWAYS_LINK_PIC_LIBRARY));
+ }
+
+ Iterable<Artifact> getObjectFilesFromSrcs(final boolean usePic) {
+ if (usePic) {
+ return Iterables.filter(sources, new Predicate<Artifact>() {
+ @Override
+ public boolean apply(Artifact artifact) {
+ String filename = artifact.getExecPathString();
+
+ // For compatibility with existing BUILD files, any ".o" files listed
+ // in srcs are assumed to be position-independent code, or
+ // at least suitable for inclusion in shared libraries, unless they
+ // end with ".nopic.o". (The ".nopic.o" extension is an undocumented
+ // feature to give users at least some control over this.) Note that
+ // some target platforms do not require shared library code to be PIC.
+ return CppFileTypes.PIC_OBJECT_FILE.matches(filename) ||
+ (CppFileTypes.OBJECT_FILE.matches(filename) && !filename.endsWith(".nopic.o"));
+ }
+ });
+ } else {
+ return FileType.filter(sources, CppFileTypes.OBJECT_FILE);
+ }
+ }
+
+ /**
+ * Returns the files from headers and does some sanity checks. Note that this method reports
+ * warnings to the {@link RuleContext} as a side effect, and so should only be called once for any
+ * given rule.
+ */
+ public static List<Artifact> getHeaders(RuleContext ruleContext) {
+ List<Artifact> hdrs = new ArrayList<>();
+ for (TransitiveInfoCollection target :
+ ruleContext.getPrerequisitesIf("hdrs", Mode.TARGET, FileProvider.class)) {
+ FileProvider provider = target.getProvider(FileProvider.class);
+ for (Artifact artifact : provider.getFilesToBuild()) {
+ if (!CppRuleClasses.DISALLOWED_HDRS_FILES.matches(artifact.getFilename())) {
+ hdrs.add(artifact);
+ } else {
+ ruleContext.attributeWarning("hdrs", "file '" + artifact.getFilename()
+ + "' from target '" + target.getLabel() + "' is not allowed in hdrs");
+ }
+ }
+ }
+ return hdrs;
+ }
+
+ /**
+ * Uses {@link #getHeaders(RuleContext)} to get the {@code hdrs} on this target. This method will
+ * return an empty list if there is no {@code hdrs} attribute on this rule type.
+ */
+ List<Artifact> getHeaders() {
+ if (!hasAttribute("hdrs", Type.LABEL_LIST)) {
+ return ImmutableList.of();
+ }
+ return getHeaders(ruleContext);
+ }
+
+ HeadersCheckingMode determineHeadersCheckingMode() {
+ HeadersCheckingMode headersCheckingMode = cppConfiguration.getHeadersCheckingMode();
+
+ // Package default overrides command line option.
+ if (ruleContext.getRule().getPackage().isDefaultHdrsCheckSet()) {
+ String value =
+ ruleContext.getRule().getPackage().getDefaultHdrsCheck().toUpperCase(Locale.ENGLISH);
+ headersCheckingMode = HeadersCheckingMode.valueOf(value);
+ }
+
+ // 'hdrs_check' attribute overrides package default.
+ if (hasAttribute("hdrs_check", Type.STRING)
+ && ruleContext.getRule().isAttributeValueExplicitlySpecified("hdrs_check")) {
+ try {
+ String value = ruleContext.attributes().get("hdrs_check", Type.STRING)
+ .toUpperCase(Locale.ENGLISH);
+ headersCheckingMode = HeadersCheckingMode.valueOf(value);
+ } catch (IllegalArgumentException e) {
+ ruleContext.attributeError("hdrs_check", "must be one of: 'loose', 'warn' or 'strict'");
+ }
+ }
+
+ return headersCheckingMode;
+ }
+
+ /**
+ * Expand and tokenize the copts and nocopts attributes.
+ */
+ private ImmutableList<String> initCopts() {
+ if (!hasAttribute("copts", Type.STRING_LIST)) {
+ return ImmutableList.<String>of();
+ }
+ // TODO(bazel-team): getAttributeCopts should not tokenize the strings.
+ // Make a warning for now.
+ List<String> tokens = new ArrayList<>();
+ for (String str : ruleContext.attributes().get("copts", Type.STRING_LIST)) {
+ tokens.clear();
+ try {
+ ShellUtils.tokenize(tokens, str);
+ if (tokens.size() > 1) {
+ ruleContext.attributeWarning("copts",
+ "each item in the list should contain only one option");
+ }
+ } catch (ShellUtils.TokenizationException e) {
+ // ignore, the error is reported in the getAttributeCopts call
+ }
+ }
+
+ Pattern nocopts = getNoCopts(ruleContext);
+ if (nocopts != null && nocopts.matcher("-Wno-future-warnings").matches()) {
+ ruleContext.attributeWarning("nocopts",
+ "Regular expression '" + nocopts.pattern() + "' is too general; for example, it matches "
+ + "'-Wno-future-warnings'. Thus it might *re-enable* compiler warnings we wish to "
+ + "disable globally. To disable all compiler warnings, add '-w' to copts instead");
+ }
+
+ return ImmutableList.<String>builder()
+ .addAll(getPackageCopts(ruleContext))
+ .addAll(CppHelper.getAttributeCopts(ruleContext, "copts"))
+ .build();
+ }
+
+ private static ImmutableList<String> getPackageCopts(RuleContext ruleContext) {
+ List<String> unexpanded = ruleContext.getRule().getPackage().getDefaultCopts();
+ return ImmutableList.copyOf(CppHelper.expandMakeVariables(ruleContext, "copts", unexpanded));
+ }
+
+ Pattern getNoCopts() {
+ return getNoCopts(ruleContext);
+ }
+
+ /**
+ * Returns nocopts pattern built from the make variable expanded nocopts
+ * attribute.
+ */
+ private static Pattern getNoCopts(RuleContext ruleContext) {
+ Pattern nocopts = null;
+ if (ruleContext.getRule().isAttrDefined(NO_COPTS_ATTRIBUTE, Type.STRING)) {
+ String nocoptsAttr = ruleContext.expandMakeVariables(NO_COPTS_ATTRIBUTE,
+ ruleContext.attributes().get(NO_COPTS_ATTRIBUTE, Type.STRING));
+ try {
+ nocopts = Pattern.compile(nocoptsAttr);
+ } catch (PatternSyntaxException e) {
+ ruleContext.attributeError(NO_COPTS_ATTRIBUTE,
+ "invalid regular expression '" + nocoptsAttr + "': " + e.getMessage());
+ }
+ }
+ return nocopts;
+ }
+
+ // TODO(bazel-team): calculating nocopts every time is not very efficient,
+ // fix this after the rule migration. The problem is that in some cases we call this after
+ // the RCT is created (so RuleContext is not accessible), in some cases during the creation.
+ // It would probably make more sense to use TransitiveInfoProviders.
+ /**
+ * Returns true if the rule context has a nocopts regex that matches the given value, false
+ * otherwise.
+ */
+ static boolean noCoptsMatches(String option, RuleContext ruleContext) {
+ Pattern nocopts = getNoCopts(ruleContext);
+ return nocopts == null ? false : nocopts.matcher(option).matches();
+ }
+
+ private static final String DEFINES_ATTRIBUTE = "defines";
+
+ /**
+ * Returns a list of define tokens from "defines" attribute.
+ *
+ * <p>We tokenize the "defines" attribute, to ensure that the handling of
+ * quotes and backslash escapes is consistent Bazel's treatment of the "copts" attribute.
+ *
+ * <p>But we require that the "defines" attribute consists of a single token.
+ */
+ public List<String> getDefines() {
+ List<String> defines = new ArrayList<>();
+ for (String define :
+ ruleContext.attributes().get(DEFINES_ATTRIBUTE, Type.STRING_LIST)) {
+ List<String> tokens = new ArrayList<>();
+ try {
+ ShellUtils.tokenize(tokens, ruleContext.expandMakeVariables(DEFINES_ATTRIBUTE, define));
+ if (tokens.size() == 1) {
+ defines.add(tokens.get(0));
+ } else if (tokens.isEmpty()) {
+ ruleContext.attributeError(DEFINES_ATTRIBUTE, "empty definition not allowed");
+ } else {
+ ruleContext.attributeError(DEFINES_ATTRIBUTE,
+ "definition contains too many tokens (found " + tokens.size()
+ + ", expecting exactly one)");
+ }
+ } catch (ShellUtils.TokenizationException e) {
+ ruleContext.attributeError(DEFINES_ATTRIBUTE, e.getMessage());
+ }
+ }
+ return defines;
+ }
+
+ /**
+ * Collects our own linkopts from the rule attribute. This determines linker
+ * options to use when building this library and anything that depends on it.
+ */
+ private final ImmutableList<String> initLinkopts() {
+ if (!hasAttribute("linkopts", Type.STRING_LIST)) {
+ return ImmutableList.<String>of();
+ }
+ List<String> ourLinkopts = ruleContext.attributes().get("linkopts", Type.STRING_LIST);
+ List<String> result = new ArrayList<>();
+ if (ourLinkopts != null) {
+ boolean allowDashStatic = !cppConfiguration.forceIgnoreDashStatic()
+ && (cppConfiguration.getDynamicMode() != DynamicMode.FULLY);
+ for (String linkopt : ourLinkopts) {
+ if (linkopt.equals("-static") && !allowDashStatic) {
+ continue;
+ }
+ CppHelper.expandAttribute(ruleContext, result, "linkopts", linkopt, true);
+ }
+ }
+ return ImmutableList.copyOf(result);
+ }
+
+ /**
+ * Determines a list of loose include directories that are only allowed to be referenced when
+ * headers checking is {@link HeadersCheckingMode#LOOSE} or {@link HeadersCheckingMode#WARN}.
+ */
+ List<PathFragment> getLooseIncludeDirs() {
+ List<PathFragment> result = new ArrayList<>();
+ // The package directory of the rule contributes includes. Note that this also covers all
+ // non-subpackage sub-directories.
+ PathFragment rulePackage = ruleContext.getLabel().getPackageFragment();
+ result.add(rulePackage);
+
+ // Gather up all the dirs from the rule's srcs as well as any of the srcs outputs.
+ if (hasAttribute("srcs", Type.LABEL_LIST)) {
+ for (FileProvider src :
+ ruleContext.getPrerequisites("srcs", Mode.TARGET, FileProvider.class)) {
+ PathFragment packageDir = src.getLabel().getPackageFragment();
+ for (Artifact a : src.getFilesToBuild()) {
+ result.add(packageDir);
+ // Attempt to gather subdirectories that might contain include files.
+ result.add(a.getRootRelativePath().getParentDirectory());
+ }
+ }
+ }
+
+ // Add in any 'includes' attribute values as relative path fragments
+ if (ruleContext.getRule().isAttributeValueExplicitlySpecified("includes")) {
+ PathFragment packageFragment = ruleContext.getLabel().getPackageFragment();
+ // For now, anything with an 'includes' needs a blanket declaration
+ result.add(packageFragment.getRelative("**"));
+ }
+ return result;
+ }
+
+ List<PathFragment> getSystemIncludeDirs() {
+ // Add in any 'includes' attribute values as relative path fragments
+ if (!ruleContext.getRule().isAttributeValueExplicitlySpecified("includes")
+ || !cppConfiguration.useIsystemForIncludes()) {
+ return ImmutableList.of();
+ }
+ return getIncludeDirsFromIncludesAttribute();
+ }
+
+ List<PathFragment> getIncludeDirs() {
+ if (!ruleContext.getRule().isAttributeValueExplicitlySpecified("includes")
+ || cppConfiguration.useIsystemForIncludes()) {
+ return ImmutableList.of();
+ }
+ return getIncludeDirsFromIncludesAttribute();
+ }
+
+ private List<PathFragment> getIncludeDirsFromIncludesAttribute() {
+ List<PathFragment> result = new ArrayList<>();
+ PathFragment packageFragment = ruleContext.getLabel().getPackageFragment();
+ for (String includesAttr : ruleContext.attributes().get("includes", Type.STRING_LIST)) {
+ includesAttr = ruleContext.expandMakeVariables("includes", includesAttr);
+ if (includesAttr.startsWith("/")) {
+ ruleContext.attributeWarning("includes",
+ "ignoring invalid absolute path '" + includesAttr + "'");
+ continue;
+ }
+ PathFragment includesPath = packageFragment.getRelative(includesAttr).normalize();
+ if (!includesPath.isNormalized()) {
+ ruleContext.attributeError("includes",
+ "Path references a path above the execution root.");
+ }
+ result.add(includesPath);
+ result.add(ruleContext.getConfiguration().getGenfilesFragment().getRelative(includesPath));
+ }
+ return result;
+ }
+
+ /**
+ * Collects compilation prerequisite artifacts.
+ */
+ static CompilationPrerequisitesProvider collectCompilationPrerequisites(
+ RuleContext ruleContext, CppCompilationContext context) {
+ // TODO(bazel-team): Use context.getCompilationPrerequisites() instead.
+ NestedSetBuilder<Artifact> prerequisites = NestedSetBuilder.stableOrder();
+ if (ruleContext.getRule().getRuleClassObject().hasAttr("srcs", Type.LABEL_LIST)) {
+ for (FileProvider provider : ruleContext
+ .getPrerequisites("srcs", Mode.TARGET, FileProvider.class)) {
+ prerequisites.addAll(FileType.filter(provider.getFilesToBuild(), SOURCE_TYPES));
+ }
+ }
+ prerequisites.addTransitive(context.getDeclaredIncludeSrcs());
+ return new CompilationPrerequisitesProvider(prerequisites.build());
+ }
+
+ /**
+ * Replaces shared library artifact with mangled symlink and creates related
+ * symlink action. For artifacts that should retain filename (e.g. libraries
+ * with SONAME tag), link is created to the parent directory instead.
+ *
+ * This action is performed to minimize number of -rpath entries used during
+ * linking process (by essentially "collecting" as many shared libraries as
+ * possible in the single directory), since we will be paying quadratic price
+ * for each additional entry on the -rpath.
+ *
+ * @param library Shared library artifact that needs to be mangled
+ * @param preserveName true if filename should be preserved, false - mangled.
+ * @return mangled symlink artifact.
+ */
+ public LibraryToLink getDynamicLibrarySymlink(Artifact library, boolean preserveName) {
+ return SolibSymlinkAction.getDynamicLibrarySymlink(
+ ruleContext, library, preserveName, true, ruleContext.getConfiguration());
+ }
+
+ /**
+ * Returns any linker scripts found in the dependencies of the rule.
+ */
+ Iterable<Artifact> getLinkerScripts() {
+ return FileType.filter(ruleContext.getPrerequisiteArtifacts("deps", Mode.TARGET).list(),
+ CppFileTypes.LINKER_SCRIPT);
+ }
+
+ ImmutableList<Artifact> getFilesToCompile(CcCompilationOutputs compilationOutputs) {
+ return cppConfiguration.isLipoContextCollector()
+ ? ImmutableList.<Artifact>of()
+ : compilationOutputs.getObjectFiles(CppHelper.usePic(ruleContext, false));
+ }
+
+ InstrumentedFilesProvider getInstrumentedFilesProvider(Iterable<Artifact> files) {
+ return cppConfiguration.isLipoContextCollector()
+ ? InstrumentedFilesProviderImpl.EMPTY
+ : new InstrumentedFilesProviderImpl(new InstrumentedFilesCollector(
+ ruleContext, CppRuleClasses.INSTRUMENTATION_SPEC, CC_METADATA_COLLECTOR, files));
+ }
+
+ public static FeatureConfiguration configureFeatures(RuleContext ruleContext) {
+ CcToolchainProvider toolchain = CppHelper.getToolchain(ruleContext);
+ Set<String> requestedFeatures = ImmutableSet.of(CppRuleClasses.MODULE_MAP_HOME_CWD);
+ return toolchain.getFeatures().getFeatureConfiguration(requestedFeatures);
+ }
+
+ public void addTransitiveInfoProviders(RuleConfiguredTargetBuilder builder,
+ NestedSet<Artifact> filesToBuild,
+ CcCompilationOutputs ccCompilationOutputs,
+ CppCompilationContext cppCompilationContext,
+ CcLinkingOutputs linkingOutputs,
+ DwoArtifactsCollector dwoArtifacts,
+ TransitiveLipoInfoProvider transitiveLipoInfo) {
+ List<Artifact> instrumentedObjectFiles = new ArrayList<>();
+ instrumentedObjectFiles.addAll(ccCompilationOutputs.getObjectFiles(false));
+ instrumentedObjectFiles.addAll(ccCompilationOutputs.getObjectFiles(true));
+ builder
+ .setFilesToBuild(filesToBuild)
+ .add(CppCompilationContext.class, cppCompilationContext)
+ .add(TransitiveLipoInfoProvider.class, transitiveLipoInfo)
+ .add(CcExecutionDynamicLibrariesProvider.class,
+ new CcExecutionDynamicLibrariesProvider(collectExecutionDynamicLibraryArtifacts(
+ ruleContext, linkingOutputs.getExecutionDynamicLibraries())))
+ .add(CcNativeLibraryProvider.class, new CcNativeLibraryProvider(
+ collectTransitiveCcNativeLibraries(ruleContext, linkingOutputs.getDynamicLibraries())))
+ .add(InstrumentedFilesProvider.class, getInstrumentedFilesProvider(
+ instrumentedObjectFiles))
+ .add(FilesToCompileProvider.class, new FilesToCompileProvider(
+ getFilesToCompile(ccCompilationOutputs)))
+ .add(CompilationPrerequisitesProvider.class,
+ collectCompilationPrerequisites(ruleContext, cppCompilationContext))
+ .add(TempsProvider.class, new TempsProvider(getTemps(ccCompilationOutputs)))
+ .add(CppDebugFileProvider.class, new CppDebugFileProvider(
+ dwoArtifacts.getDwoArtifacts(),
+ dwoArtifacts.getPicDwoArtifacts()));
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CcCompilationOutputs.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcCompilationOutputs.java
new file mode 100644
index 0000000..b9fa4e8
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcCompilationOutputs.java
@@ -0,0 +1,207 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.Artifact;
+
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A structured representation of the compilation outputs of a C++ rule.
+ */
+public class CcCompilationOutputs {
+ /**
+ * All .o files built by the target.
+ */
+ private final ImmutableList<Artifact> objectFiles;
+
+ /**
+ * All .pic.o files built by the target.
+ */
+ private final ImmutableList<Artifact> picObjectFiles;
+
+ /**
+ * All .dwo files built by the target, corresponding to .o outputs.
+ */
+ private final ImmutableList<Artifact> dwoFiles;
+
+ /**
+ * All .pic.dwo files built by the target, corresponding to .pic.o outputs.
+ */
+ private final ImmutableList<Artifact> picDwoFiles;
+
+ /**
+ * All artifacts that are created if "--save_temps" is true.
+ */
+ private final ImmutableList<Artifact> temps;
+
+ /**
+ * All token .h.processed files created when preprocessing or parsing headers.
+ */
+ private final ImmutableList<Artifact> headerTokenFiles;
+
+ private final List<IncludeScannable> lipoScannables;
+
+ private CcCompilationOutputs(ImmutableList<Artifact> objectFiles,
+ ImmutableList<Artifact> picObjectFiles, ImmutableList<Artifact> dwoFiles,
+ ImmutableList<Artifact> picDwoFiles, ImmutableList<Artifact> temps,
+ ImmutableList<Artifact> headerTokenFiles,
+ ImmutableList<IncludeScannable> lipoScannables) {
+ this.objectFiles = objectFiles;
+ this.picObjectFiles = picObjectFiles;
+ this.dwoFiles = dwoFiles;
+ this.picDwoFiles = picDwoFiles;
+ this.temps = temps;
+ this.headerTokenFiles = headerTokenFiles;
+ this.lipoScannables = lipoScannables;
+ }
+
+ /**
+ * Returns an unmodifiable view of the .o or .pic.o files set.
+ *
+ * @param usePic whether to return .pic.o files
+ */
+ public ImmutableList<Artifact> getObjectFiles(boolean usePic) {
+ return usePic ? picObjectFiles : objectFiles;
+ }
+
+ /**
+ * Returns an unmodifiable view of the .dwo files set.
+ */
+ public ImmutableList<Artifact> getDwoFiles() {
+ return dwoFiles;
+ }
+
+ /**
+ * Returns an unmodifiable view of the .pic.dwo files set.
+ */
+ public ImmutableList<Artifact> getPicDwoFiles() {
+ return picDwoFiles;
+ }
+
+ /**
+ * Returns an unmodifiable view of the temp files set.
+ */
+ public ImmutableList<Artifact> getTemps() {
+ return temps;
+ }
+
+ /**
+ * Returns an unmodifiable view of the .h.processed files.
+ */
+ public Iterable<Artifact> getHeaderTokenFiles() {
+ return headerTokenFiles;
+ }
+
+ /**
+ * Returns the {@link IncludeScannable} objects this C++ compile action contributes to a
+ * LIPO context collector.
+ */
+ public List<IncludeScannable> getLipoScannables() {
+ return lipoScannables;
+ }
+
+ public static final class Builder {
+ private final Set<Artifact> objectFiles = new LinkedHashSet<>();
+ private final Set<Artifact> picObjectFiles = new LinkedHashSet<>();
+ private final Set<Artifact> dwoFiles = new LinkedHashSet<>();
+ private final Set<Artifact> picDwoFiles = new LinkedHashSet<>();
+ private final Set<Artifact> temps = new LinkedHashSet<>();
+ private final Set<Artifact> headerTokenFiles = new LinkedHashSet<>();
+ private final List<IncludeScannable> lipoScannables = new ArrayList<>();
+
+ public CcCompilationOutputs build() {
+ return new CcCompilationOutputs(ImmutableList.copyOf(objectFiles),
+ ImmutableList.copyOf(picObjectFiles), ImmutableList.copyOf(dwoFiles),
+ ImmutableList.copyOf(picDwoFiles), ImmutableList.copyOf(temps),
+ ImmutableList.copyOf(headerTokenFiles),
+ ImmutableList.copyOf(lipoScannables));
+ }
+
+ public Builder merge(CcCompilationOutputs outputs) {
+ this.objectFiles.addAll(outputs.objectFiles);
+ this.picObjectFiles.addAll(outputs.picObjectFiles);
+ this.dwoFiles.addAll(outputs.dwoFiles);
+ this.picDwoFiles.addAll(outputs.picDwoFiles);
+ this.temps.addAll(outputs.temps);
+ this.headerTokenFiles.addAll(outputs.headerTokenFiles);
+ this.lipoScannables.addAll(outputs.lipoScannables);
+ return this;
+ }
+
+ /**
+ * Adds an .o file.
+ */
+ public Builder addObjectFile(Artifact artifact) {
+ objectFiles.add(artifact);
+ return this;
+ }
+
+ public Builder addObjectFiles(Iterable<Artifact> artifacts) {
+ Iterables.addAll(objectFiles, artifacts);
+ return this;
+ }
+
+ /**
+ * Adds a .pic.o file.
+ */
+ public Builder addPicObjectFile(Artifact artifact) {
+ picObjectFiles.add(artifact);
+ return this;
+ }
+
+ public Builder addPicObjectFiles(Iterable<Artifact> artifacts) {
+ Iterables.addAll(picObjectFiles, artifacts);
+ return this;
+ }
+
+ public Builder addDwoFile(Artifact artifact) {
+ dwoFiles.add(artifact);
+ return this;
+ }
+
+ public Builder addPicDwoFile(Artifact artifact) {
+ picDwoFiles.add(artifact);
+ return this;
+ }
+
+ /**
+ * Adds temp files.
+ */
+ public Builder addTemps(Iterable<Artifact> artifacts) {
+ Iterables.addAll(temps, artifacts);
+ return this;
+ }
+
+ public Builder addHeaderTokenFile(Artifact artifact) {
+ headerTokenFiles.add(artifact);
+ return this;
+ }
+
+ /**
+ * Adds an {@link IncludeScannable} that this compilation output object contributes to a
+ * LIPO context collector.
+ */
+ public Builder addLipoScannable(IncludeScannable scannable) {
+ lipoScannables.add(scannable);
+ return this;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CcExecutionDynamicLibrariesProvider.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcExecutionDynamicLibrariesProvider.java
new file mode 100644
index 0000000..39ce942
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcExecutionDynamicLibrariesProvider.java
@@ -0,0 +1,49 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+
+/**
+ * A target that provides the execution-time dynamic libraries of a C++ rule.
+ */
+@Immutable
+public final class CcExecutionDynamicLibrariesProvider implements TransitiveInfoProvider {
+ public static final CcExecutionDynamicLibrariesProvider EMPTY =
+ new CcExecutionDynamicLibrariesProvider(
+ NestedSetBuilder.<Artifact>emptySet(Order.STABLE_ORDER));
+
+ private final NestedSet<Artifact> ccExecutionDynamicLibraries;
+
+ public CcExecutionDynamicLibrariesProvider(NestedSet<Artifact> ccExecutionDynamicLibraries) {
+ this.ccExecutionDynamicLibraries = ccExecutionDynamicLibraries;
+ }
+
+ /**
+ * Returns the execution-time dynamic libraries.
+ *
+ * <p>This normally returns the dynamic library created by the rule itself. However, if the rule
+ * does not create any dynamic libraries, then it returns the combined results of calling
+ * getExecutionDynamicLibraryArtifacts on all the rule's deps. This behaviour is so that this
+ * method is useful for a cc_library with deps but no srcs.
+ */
+ public NestedSet<Artifact> getExecutionDynamicLibraryArtifacts() {
+ return ccExecutionDynamicLibraries;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CcLibrary.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcLibrary.java
new file mode 100644
index 0000000..428eedb
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcLibrary.java
@@ -0,0 +1,395 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.AlwaysBuiltArtifactsProvider;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTargetBuilder;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.Runfiles;
+import com.google.devtools.build.lib.analysis.RunfilesProvider;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.packages.AttributeMap;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.rules.RuleConfiguredTargetFactory;
+import com.google.devtools.build.lib.rules.cpp.Link.LinkTargetType;
+import com.google.devtools.build.lib.rules.cpp.LinkerInputs.LibraryToLink;
+import com.google.devtools.build.lib.rules.test.BaselineCoverageAction;
+import com.google.devtools.build.lib.rules.test.InstrumentedFilesProvider;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.util.FileType;
+import com.google.devtools.build.lib.util.FileTypeSet;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A ConfiguredTarget for <code>cc_library</code> rules.
+ */
+public abstract class CcLibrary implements RuleConfiguredTargetFactory {
+
+ private final CppSemantics semantics;
+
+ protected CcLibrary(CppSemantics semantics) {
+ this.semantics = semantics;
+ }
+
+ // These file extensions don't generate object files.
+ private static final FileTypeSet NO_OBJECT_GENERATING_FILETYPES = FileTypeSet.of(
+ CppFileTypes.CPP_HEADER, CppFileTypes.ARCHIVE, CppFileTypes.PIC_ARCHIVE,
+ CppFileTypes.ALWAYS_LINK_LIBRARY, CppFileTypes.ALWAYS_LINK_PIC_LIBRARY,
+ CppFileTypes.SHARED_LIBRARY);
+
+ private static final Predicate<LibraryToLink> PIC_STATIC_FILTER = new Predicate<LibraryToLink>() {
+ @Override
+ public boolean apply(LibraryToLink input) {
+ String name = input.getArtifact().getExecPath().getBaseName();
+ return !name.endsWith(".nopic.a") && !name.endsWith(".nopic.lo");
+ }
+ };
+
+ private static Runfiles collectRunfiles(RuleContext context,
+ CcLinkingOutputs ccLinkingOutputs,
+ boolean neverLink, boolean addDynamicRuntimeInputArtifactsToRunfiles,
+ boolean linkingStatically) {
+ Runfiles.Builder builder = new Runfiles.Builder();
+
+ // neverlink= true creates a library that will never be linked into any binary that depends on
+ // it, but instead be loaded as an extension. So we need the dynamic library for this in the
+ // runfiles.
+ builder.addArtifacts(ccLinkingOutputs.getLibrariesForRunfiles(linkingStatically && !neverLink));
+ builder.add(context, CppRunfilesProvider.runfilesFunction(linkingStatically));
+ if (context.getRule().isAttrDefined("implements", Type.LABEL_LIST)) {
+ builder.addTargets(context.getPrerequisites("implements", Mode.TARGET),
+ RunfilesProvider.DEFAULT_RUNFILES);
+ builder.addTargets(context.getPrerequisites("implements", Mode.TARGET),
+ CppRunfilesProvider.runfilesFunction(linkingStatically));
+ }
+ if (context.getRule().isAttrDefined("implementation", Type.LABEL_LIST)) {
+ builder.addTargets(context.getPrerequisites("implementation", Mode.TARGET),
+ RunfilesProvider.DEFAULT_RUNFILES);
+ builder.addTargets(context.getPrerequisites("implementation", Mode.TARGET),
+ CppRunfilesProvider.runfilesFunction(linkingStatically));
+ }
+
+ builder.addDataDeps(context);
+
+ if (addDynamicRuntimeInputArtifactsToRunfiles) {
+ builder.addTransitiveArtifacts(CppHelper.getToolchain(context).getDynamicRuntimeLinkInputs());
+ }
+ return builder.build();
+ }
+
+ @Override
+ public ConfiguredTarget create(RuleContext context) {
+ RuleConfiguredTargetBuilder builder = new RuleConfiguredTargetBuilder(context);
+ LinkTargetType linkType = getStaticLinkType(context);
+ boolean linkStatic = context.attributes().get("linkstatic", Type.BOOLEAN);
+ init(semantics, context, builder, linkType,
+ /*neverLink =*/ false,
+ linkStatic,
+ /*collectLinkstamp =*/ true,
+ /*addDynamicRuntimeInputArtifactsToRunfiles =*/ false);
+ return builder.build();
+ }
+
+ public static void init(CppSemantics semantics, RuleContext ruleContext,
+ RuleConfiguredTargetBuilder targetBuilder, LinkTargetType linkType,
+ boolean neverLink,
+ boolean linkStatic,
+ boolean collectLinkstamp,
+ boolean addDynamicRuntimeInputArtifactsToRunfiles) {
+ final CcCommon common = new CcCommon(ruleContext);
+
+ CcLibraryHelper helper = new CcLibraryHelper(ruleContext, semantics)
+ .setLinkType(linkType)
+ .enableCcNativeLibrariesProvider()
+ .enableInterfaceSharedObjects()
+ .enableCompileProviders()
+ .setNeverLink(neverLink)
+ .setHeadersCheckingMode(common.determineHeadersCheckingMode())
+ .addCopts(common.getCopts())
+ .setNoCopts(common.getNoCopts())
+ .addLinkopts(common.getLinkopts())
+ .addDefines(common.getDefines())
+ .addCompilationPrerequisites(common.getSharedLibrariesFromSrcs())
+ .addCompilationPrerequisites(common.getStaticLibrariesFromSrcs())
+ .addSources(common.getCAndCppSources())
+ .addPublicHeaders(common.getHeaders())
+ .addObjectFiles(common.getObjectFilesFromSrcs(false))
+ .addPicObjectFiles(common.getObjectFilesFromSrcs(true))
+ .addPicIndependentObjectFiles(common.getLinkerScripts())
+ .addDeps(ruleContext.getPrerequisites("deps", Mode.TARGET))
+ .setEnableLayeringCheck(ruleContext.getFeatures().contains(CppRuleClasses.LAYERING_CHECK))
+ .setCompileHeaderModules(ruleContext.getFeatures().contains(CppRuleClasses.HEADER_MODULES))
+ .addSystemIncludeDirs(common.getSystemIncludeDirs())
+ .addIncludeDirs(common.getIncludeDirs())
+ .addLooseIncludeDirs(common.getLooseIncludeDirs())
+ .setEmitHeaderTargetModuleMaps(
+ ruleContext.getRule().getRuleClass().equals("cc_public_library"));
+
+ if (collectLinkstamp) {
+ helper.addLinkstamps(ruleContext.getPrerequisites("linkstamp", Mode.TARGET));
+ }
+
+ if (ruleContext.getRule().isAttrDefined("implements", Type.LABEL_LIST)) {
+ helper.addDeps(ruleContext.getPrerequisites("implements", Mode.TARGET));
+ }
+
+ if (ruleContext.getRule().isAttrDefined("implementation", Type.LABEL_LIST)) {
+ helper.addDeps(ruleContext.getPrerequisites("implementation", Mode.TARGET));
+ }
+
+ PathFragment soImplFilename = null;
+ if (ruleContext.getRule().isAttrDefined("outs", Type.STRING_LIST)) {
+ List<String> outs = ruleContext.attributes().get("outs", Type.STRING_LIST);
+ if (outs.size() > 1) {
+ ruleContext.attributeError("outs", "must be a singleton list");
+ } else if (outs.size() == 1) {
+ soImplFilename = CppHelper.getLinkedFilename(ruleContext, LinkTargetType.DYNAMIC_LIBRARY);
+ soImplFilename = soImplFilename.replaceName(outs.get(0));
+ if (!soImplFilename.getPathString().endsWith(".so")) { // Sanity check.
+ ruleContext.attributeError("outs", "file name must end in '.so'");
+ }
+ }
+ }
+
+ if (ruleContext.getRule().isAttrDefined("srcs", Type.LABEL_LIST)) {
+ helper.addPrivateHeaders(FileType.filter(
+ ruleContext.getPrerequisiteArtifacts("srcs", Mode.TARGET).list(),
+ CppFileTypes.CPP_HEADER));
+ ruleContext.checkSrcsSamePackage(true);
+ }
+
+ if (common.getLinkopts().contains("-static")) {
+ ruleContext.attributeWarning("linkopts", "Using '-static' here won't work. "
+ + "Did you mean to use 'linkstatic=1' instead?");
+ }
+
+ boolean createDynamicLibrary =
+ !linkStatic && !appearsToHaveNoObjectFiles(ruleContext.attributes());
+ helper.setCreateDynamicLibrary(createDynamicLibrary);
+ helper.setDynamicLibraryPath(soImplFilename);
+
+ /*
+ * Add the libraries from srcs, if any. For static/mostly static
+ * linking we setup the dynamic libraries if there are no static libraries
+ * to choose from. Path to the libraries will be mangled to avoid using
+ * absolute path names on the -rpath, but library filenames will be
+ * preserved (since some libraries might have SONAME tag) - symlink will
+ * be created to the parent directory instead.
+ *
+ * For compatibility with existing BUILD files, any ".a" or ".lo" files listed in
+ * srcs are assumed to be position-independent code, or at least suitable for
+ * inclusion in shared libraries, unless they end with ".nopic.a" or ".nopic.lo".
+ *
+ * Note that some target platforms do not require shared library code to be PIC.
+ */
+ Iterable<LibraryToLink> staticLibrariesFromSrcs =
+ LinkerInputs.opaqueLibrariesToLink(common.getStaticLibrariesFromSrcs());
+ helper.addStaticLibraries(staticLibrariesFromSrcs);
+ helper.addPicStaticLibraries(Iterables.filter(staticLibrariesFromSrcs, PIC_STATIC_FILTER));
+ helper.addPicStaticLibraries(common.getPicStaticLibrariesFromSrcs());
+ helper.addDynamicLibraries(Iterables.transform(common.getSharedLibrariesFromSrcs(),
+ new Function<Artifact, LibraryToLink>() {
+ @Override
+ public LibraryToLink apply(Artifact library) {
+ return common.getDynamicLibrarySymlink(library, true);
+ }
+ }));
+ CcLibraryHelper.Info info = helper.build();
+
+ /*
+ * We always generate a static library, even if there aren't any source files.
+ * This keeps things simpler by avoiding special cases when making use of the library.
+ * For example, this is needed to ensure that building a library with "bazel build"
+ * will also build all of the library's "deps".
+ * However, we only generate a dynamic library if there are source files.
+ */
+ // For now, we don't add the precompiled libraries to the files to build.
+ CcLinkingOutputs linkedLibraries = info.getCcLinkingOutputsExcludingPrecompiledLibraries();
+
+ NestedSet<Artifact> artifactsToForce =
+ collectArtifactsToForce(ruleContext, common, info.getCcCompilationOutputs());
+
+ NestedSetBuilder<Artifact> filesBuilder = NestedSetBuilder.stableOrder();
+ filesBuilder.addAll(LinkerInputs.toLibraryArtifacts(linkedLibraries.getStaticLibraries()));
+ filesBuilder.addAll(LinkerInputs.toLibraryArtifacts(linkedLibraries.getPicStaticLibraries()));
+ filesBuilder.addAll(LinkerInputs.toNonSolibArtifacts(linkedLibraries.getDynamicLibraries()));
+ filesBuilder.addAll(
+ LinkerInputs.toNonSolibArtifacts(linkedLibraries.getExecutionDynamicLibraries()));
+
+ CcLinkingOutputs linkingOutputs = info.getCcLinkingOutputs();
+ warnAboutEmptyLibraries(
+ ruleContext, info.getCcCompilationOutputs(), linkType, linkStatic);
+ NestedSet<Artifact> filesToBuild = filesBuilder.build();
+
+ Runfiles staticRunfiles = collectRunfiles(ruleContext,
+ linkingOutputs, neverLink, addDynamicRuntimeInputArtifactsToRunfiles, true);
+ Runfiles sharedRunfiles = collectRunfiles(ruleContext,
+ linkingOutputs, neverLink, addDynamicRuntimeInputArtifactsToRunfiles, false);
+
+ List<Artifact> instrumentedObjectFiles = new ArrayList<>();
+ instrumentedObjectFiles.addAll(info.getCcCompilationOutputs().getObjectFiles(false));
+ instrumentedObjectFiles.addAll(info.getCcCompilationOutputs().getObjectFiles(true));
+ InstrumentedFilesProvider instrumentedFilesProvider =
+ common.getInstrumentedFilesProvider(instrumentedObjectFiles);
+ targetBuilder
+ .setFilesToBuild(filesToBuild)
+ .addProviders(info.getProviders())
+ .add(InstrumentedFilesProvider.class, instrumentedFilesProvider)
+ .add(RunfilesProvider.class, RunfilesProvider.withData(staticRunfiles, sharedRunfiles))
+ // Remove this?
+ .add(CppRunfilesProvider.class, new CppRunfilesProvider(staticRunfiles, sharedRunfiles))
+ .setBaselineCoverageArtifacts(BaselineCoverageAction.getBaselineCoverageArtifacts(
+ ruleContext, instrumentedFilesProvider.getInstrumentedFiles()))
+ .add(ImplementedCcPublicLibrariesProvider.class,
+ new ImplementedCcPublicLibrariesProvider(getImplementedCcPublicLibraries(ruleContext)))
+ .add(AlwaysBuiltArtifactsProvider.class,
+ new AlwaysBuiltArtifactsProvider(artifactsToForce));
+ }
+
+ private static NestedSet<Artifact> collectArtifactsToForce(RuleContext ruleContext,
+ CcCommon common, CcCompilationOutputs ccCompilationOutputs) {
+ // Ensure that we build all the dependencies, otherwise users may get confused.
+ NestedSetBuilder<Artifact> artifactsToForceBuilder = NestedSetBuilder.stableOrder();
+ artifactsToForceBuilder.addTransitive(
+ NestedSetBuilder.wrap(Order.STABLE_ORDER, common.getFilesToCompile(ccCompilationOutputs)));
+ for (AlwaysBuiltArtifactsProvider dep :
+ ruleContext.getPrerequisites("deps", Mode.TARGET, AlwaysBuiltArtifactsProvider.class)) {
+ artifactsToForceBuilder.addTransitive(dep.getArtifactsToAlwaysBuild());
+ }
+ return artifactsToForceBuilder.build();
+ }
+
+ /**
+ * Returns the type of the generated static library.
+ */
+ private static LinkTargetType getStaticLinkType(RuleContext context) {
+ return context.attributes().get("alwayslink", Type.BOOLEAN)
+ ? LinkTargetType.ALWAYS_LINK_STATIC_LIBRARY
+ : LinkTargetType.STATIC_LIBRARY;
+ }
+
+ private static void warnAboutEmptyLibraries(RuleContext ruleContext,
+ CcCompilationOutputs ccCompilationOutputs, LinkTargetType linkType,
+ boolean linkstaticAttribute) {
+ if (ruleContext.getFragment(CppConfiguration.class).isLipoContextCollector()) {
+ // Do not signal warnings in the lipo context collector configuration. These will be duly
+ // signaled in the target configuration, and there can be spurious warnings since targets in
+ // the LIPO context collector configuration do not compile anything.
+ return;
+ }
+ if (ccCompilationOutputs.getObjectFiles(false).isEmpty()
+ && ccCompilationOutputs.getObjectFiles(true).isEmpty()) {
+ if (linkType == LinkTargetType.ALWAYS_LINK_STATIC_LIBRARY
+ || linkType == LinkTargetType.ALWAYS_LINK_PIC_STATIC_LIBRARY) {
+ ruleContext.attributeWarning("alwayslink",
+ "'alwayslink' has no effect if there are no 'srcs'");
+ }
+ if (!linkstaticAttribute && !appearsToHaveNoObjectFiles(ruleContext.attributes())) {
+ ruleContext.attributeWarning("linkstatic",
+ "setting 'linkstatic=1' is recommended if there are no object files");
+ }
+ } else {
+ if (!linkstaticAttribute && appearsToHaveNoObjectFiles(ruleContext.attributes())) {
+ Artifact element = ccCompilationOutputs.getObjectFiles(false).isEmpty()
+ ? ccCompilationOutputs.getObjectFiles(true).get(0)
+ : ccCompilationOutputs.getObjectFiles(false).get(0);
+ ruleContext.attributeWarning("srcs",
+ "this library appears at first glance to have no object files, "
+ + "but on closer inspection it does have something to link, e.g. "
+ + element.prettyPrint() + ". "
+ + "(You may have used some very confusing rule names in srcs? "
+ + "Or the library consists entirely of a linker script?) "
+ + "Bazel assumed linkstatic=1, but this may be inappropriate. "
+ + "You may need to add an explicit '.cc' file to 'srcs'. "
+ + "Alternatively, add 'linkstatic=1' to suppress this warning");
+ }
+ }
+ }
+
+ private static ImmutableList<Label> getImplementedCcPublicLibraries(RuleContext context) {
+ if (context.getRule().getRuleClassObject().hasAttr("implements", Type.LABEL_LIST)) {
+ return ImmutableList.copyOf(context.attributes().get("implements", Type.LABEL_LIST));
+ } else {
+ return ImmutableList.of();
+ }
+ }
+
+ /**
+ * Returns true if the rule (which must be a cc_library rule)
+ * appears to have no object files. This only looks at the rule
+ * itself, not at any other rules (from this package or other
+ * packages) that it might reference.
+ *
+ * <p>
+ * In some cases, this may return "false" even
+ * though the rule actually has no object files.
+ * For example, it will return false for a rule such as
+ * <code>cc_library(name = 'foo', srcs = [':bar'])</code>
+ * because we can't tell what ':bar' is; it might
+ * be a genrule that generates a source file, or it might
+ * be a genrule that generates a header file.
+ *
+ * <p>
+ * In other cases, this may return "true" even
+ * though the rule actually does have object files.
+ * For example, it will return true for a rule such as
+ * <code>cc_library(name = 'foo', srcs = ['bar.h'])</code>
+ * but as in the other example above, we can't tell whether
+ * 'bar.h' is a file name or a rule name, and 'bar.h' could
+ * in fact be the name of a genrule that generates a source file.
+ */
+ public static boolean appearsToHaveNoObjectFiles(AttributeMap rule) {
+ // Temporary hack while configurable attributes is under development. This has no effect
+ // for any rule that doesn't use configurable attributes.
+ // TODO(bazel-team): remove this hack for a more principled solution.
+ try {
+ rule.get("srcs", Type.LABEL_LIST);
+ } catch (ClassCastException e) {
+ // "srcs" is actually a configurable selector. Assume object files are possible somewhere.
+ return false;
+ }
+
+ List<Label> srcs = rule.get("srcs", Type.LABEL_LIST);
+ if (srcs != null) {
+ for (Label srcfile : srcs) {
+ /*
+ * We cheat a little bit here by looking at the file extension
+ * of the Label treated as file name. In general that might
+ * not necessarily work, because of the possibility that the
+ * user might give a rule a funky name ending in one of these
+ * extensions, e.g.
+ * genrule(name = 'foo.h', outs = ['foo.cc'], ...) // Funky rule name!
+ * cc_library(name = 'bar', srcs = ['foo.h']) // This DOES have object files.
+ */
+ if (!NO_OBJECT_GENERATING_FILETYPES.matches(srcfile.getName())) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CcLibraryHelper.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcLibraryHelper.java
new file mode 100644
index 0000000..3812bf5
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcLibraryHelper.java
@@ -0,0 +1,905 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import com.google.common.base.Function;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicates;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.AnalysisUtils;
+import com.google.devtools.build.lib.analysis.CompilationPrerequisitesProvider;
+import com.google.devtools.build.lib.analysis.FileProvider;
+import com.google.devtools.build.lib.analysis.FilesToCompileProvider;
+import com.google.devtools.build.lib.analysis.LanguageDependentFragment;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.Runfiles;
+import com.google.devtools.build.lib.analysis.RunfilesProvider;
+import com.google.devtools.build.lib.analysis.TempsProvider;
+import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
+import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.rules.cpp.CcToolchainFeatures.FeatureConfiguration;
+import com.google.devtools.build.lib.rules.cpp.CppConfiguration.HeadersCheckingMode;
+import com.google.devtools.build.lib.rules.cpp.Link.LinkTargetType;
+import com.google.devtools.build.lib.rules.cpp.LinkerInputs.LibraryToLink;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.util.Pair;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+import javax.annotation.Nullable;
+
+/**
+ * A class to create C/C++ compile and link actions in a way that is consistent with cc_library.
+ * Rules that generate source files and emulate cc_library on top of that should use this class
+ * instead of the lower-level APIs in CppHelper and CppModel.
+ *
+ * <p>Rules that want to use this class are required to have implicit dependencies on the
+ * toolchain, the STL, the lipo context, and so on. Optionally, they can also have copts,
+ * and malloc attributes, but note that these require explicit calls to the corresponding setter
+ * methods.
+ */
+public final class CcLibraryHelper {
+ /** Function for extracting module maps from CppCompilationDependencies. */
+ public static final Function<TransitiveInfoCollection, CppModuleMap> CPP_DEPS_TO_MODULES =
+ new Function<TransitiveInfoCollection, CppModuleMap>() {
+ @Override
+ @Nullable
+ public CppModuleMap apply(TransitiveInfoCollection dep) {
+ CppCompilationContext context = dep.getProvider(CppCompilationContext.class);
+ return context == null ? null : context.getCppModuleMap();
+ }
+ };
+
+ /**
+ * Contains the providers as well as the compilation and linking outputs, and the compilation
+ * context.
+ */
+ public static final class Info {
+ private final Map<Class<? extends TransitiveInfoProvider>, TransitiveInfoProvider> providers;
+ private final CcCompilationOutputs compilationOutputs;
+ private final CcLinkingOutputs linkingOutputs;
+ private final CcLinkingOutputs linkingOutputsExcludingPrecompiledLibraries;
+ private final CppCompilationContext context;
+
+ private Info(Map<Class<? extends TransitiveInfoProvider>, TransitiveInfoProvider> providers,
+ CcCompilationOutputs compilationOutputs, CcLinkingOutputs linkingOutputs,
+ CcLinkingOutputs linkingOutputsExcludingPrecompiledLibraries,
+ CppCompilationContext context) {
+ this.providers = Collections.unmodifiableMap(providers);
+ this.compilationOutputs = compilationOutputs;
+ this.linkingOutputs = linkingOutputs;
+ this.linkingOutputsExcludingPrecompiledLibraries =
+ linkingOutputsExcludingPrecompiledLibraries;
+ this.context = context;
+ }
+
+ public Map<Class<? extends TransitiveInfoProvider>, TransitiveInfoProvider> getProviders() {
+ return providers;
+ }
+
+ public CcCompilationOutputs getCcCompilationOutputs() {
+ return compilationOutputs;
+ }
+
+ public CcLinkingOutputs getCcLinkingOutputs() {
+ return linkingOutputs;
+ }
+
+ /**
+ * Returns the linking outputs before adding the pre-compiled libraries. Avoid using this -
+ * pre-compiled and locally compiled libraries should be treated identically. This method only
+ * exists for backwards compatibility.
+ */
+ public CcLinkingOutputs getCcLinkingOutputsExcludingPrecompiledLibraries() {
+ return linkingOutputsExcludingPrecompiledLibraries;
+ }
+
+ public CppCompilationContext getCppCompilationContext() {
+ return context;
+ }
+
+ /**
+ * Adds the static, pic-static, and dynamic (both compile-time and execution-time) libraries to
+ * the given builder.
+ */
+ public void addLinkingOutputsTo(NestedSetBuilder<Artifact> filesBuilder) {
+ filesBuilder.addAll(LinkerInputs.toLibraryArtifacts(linkingOutputs.getStaticLibraries()));
+ filesBuilder.addAll(LinkerInputs.toLibraryArtifacts(linkingOutputs.getPicStaticLibraries()));
+ filesBuilder.addAll(LinkerInputs.toNonSolibArtifacts(linkingOutputs.getDynamicLibraries()));
+ filesBuilder.addAll(
+ LinkerInputs.toNonSolibArtifacts(linkingOutputs.getExecutionDynamicLibraries()));
+ }
+ }
+
+ private final RuleContext ruleContext;
+ private final BuildConfiguration configuration;
+ private final CppSemantics semantics;
+
+ private final List<Artifact> publicHeaders = new ArrayList<>();
+ private final List<Artifact> privateHeaders = new ArrayList<>();
+ private final List<PathFragment> additionalExportedHeaders = new ArrayList<>();
+ private final List<Pair<Artifact, Label>> sources = new ArrayList<>();
+ private final List<Artifact> objectFiles = new ArrayList<>();
+ private final List<Artifact> picObjectFiles = new ArrayList<>();
+ private final List<String> copts = new ArrayList<>();
+ @Nullable private Pattern nocopts;
+ private final List<String> linkopts = new ArrayList<>();
+ private final Set<String> defines = new LinkedHashSet<>();
+ private final List<TransitiveInfoCollection> deps = new ArrayList<>();
+ private final List<Artifact> linkstamps = new ArrayList<>();
+ private final List<Artifact> prerequisites = new ArrayList<>();
+ private final List<PathFragment> looseIncludeDirs = new ArrayList<>();
+ private final List<PathFragment> systemIncludeDirs = new ArrayList<>();
+ private final List<PathFragment> includeDirs = new ArrayList<>();
+ @Nullable private PathFragment dynamicLibraryPath;
+ private LinkTargetType linkType = LinkTargetType.STATIC_LIBRARY;
+ private HeadersCheckingMode headersCheckingMode = HeadersCheckingMode.LOOSE;
+ private boolean neverlink;
+ private boolean fake;
+
+ private final List<LibraryToLink> staticLibraries = new ArrayList<>();
+ private final List<LibraryToLink> picStaticLibraries = new ArrayList<>();
+ private final List<LibraryToLink> dynamicLibraries = new ArrayList<>();
+
+ private boolean emitCppModuleMaps = true;
+ private boolean enableLayeringCheck;
+ private boolean compileHeaderModules;
+ private boolean emitCompileActionsIfEmpty = true;
+ private boolean emitCcNativeLibrariesProvider;
+ private boolean emitCcSpecificLinkParamsProvider;
+ private boolean emitInterfaceSharedObjects;
+ private boolean emitDynamicLibrary = true;
+ private boolean checkDepsGenerateCpp = true;
+ private boolean emitCompileProviders;
+ private boolean emitHeaderTargetModuleMaps = false;
+
+ public CcLibraryHelper(RuleContext ruleContext, CppSemantics semantics) {
+ this.ruleContext = Preconditions.checkNotNull(ruleContext);
+ this.configuration = ruleContext.getConfiguration();
+ this.semantics = Preconditions.checkNotNull(semantics);
+ }
+
+ /**
+ * Add the corresponding files as header files, i.e., these files will not be compiled, but are
+ * made visible as includes to dependent rules.
+ */
+ public CcLibraryHelper addPublicHeaders(Collection<Artifact> headers) {
+ this.publicHeaders.addAll(headers);
+ return this;
+ }
+
+ /**
+ * Add the corresponding files as public header files, i.e., these files will not be compiled, but
+ * are made visible as includes to dependent rules in module maps.
+ */
+ public CcLibraryHelper addPublicHeaders(Artifact... headers) {
+ return addPublicHeaders(Arrays.asList(headers));
+ }
+
+ /**
+ * Add the corresponding files as private header files, i.e., these files will not be compiled,
+ * but are not made visible as includes to dependent rules in module maps.
+ */
+ public CcLibraryHelper addPrivateHeaders(Iterable<Artifact> privateHeaders) {
+ Iterables.addAll(this.privateHeaders, privateHeaders);
+ return this;
+ }
+
+ /**
+ * Add the corresponding files as public header files, i.e., these files will not be compiled, but
+ * are made visible as includes to dependent rules in module maps.
+ */
+ public CcLibraryHelper addAdditionalExportedHeaders(
+ Iterable<PathFragment> additionalExportedHeaders) {
+ Iterables.addAll(this.additionalExportedHeaders, additionalExportedHeaders);
+ return this;
+ }
+
+ /**
+ * Add the corresponding files as source files. These may also be header files, in which case
+ * they will not be compiled, but also not made visible as includes to dependent rules.
+ */
+ // TODO(bazel-team): This is inconsistent with the documentation on CppModel.
+ public CcLibraryHelper addSources(Collection<Artifact> sources) {
+ for (Artifact source : sources) {
+ this.sources.add(Pair.of(source, ruleContext.getLabel()));
+ }
+ return this;
+ }
+
+ /**
+ * Add the corresponding files as source files. These may also be header files, in which case
+ * they will not be compiled, but also not made visible as includes to dependent rules.
+ */
+ // TODO(bazel-team): This is inconsistent with the documentation on CppModel.
+ public CcLibraryHelper addSources(Iterable<Pair<Artifact, Label>> sources) {
+ Iterables.addAll(this.sources, sources);
+ return this;
+ }
+
+ /**
+ * Add the corresponding files as source files. These may also be header files, in which case
+ * they will not be compiled, but also not made visible as includes to dependent rules.
+ */
+ public CcLibraryHelper addSources(Artifact... sources) {
+ return addSources(Arrays.asList(sources));
+ }
+
+ /**
+ * Add the corresponding files as linker inputs for non-PIC links. If the corresponding files are
+ * compiled with PIC, the final link may or may not fail. Note that the final link may not happen
+ * here, if {@code --start_end_lib} is enabled, but instead at any binary that transitively
+ * depends on the current rule.
+ */
+ public CcLibraryHelper addObjectFiles(Iterable<Artifact> objectFiles) {
+ Iterables.addAll(this.objectFiles, objectFiles);
+ return this;
+ }
+
+ /**
+ * Add the corresponding files as linker inputs for PIC links. If the corresponding files are not
+ * compiled with PIC, the final link may or may not fail. Note that the final link may not happen
+ * here, if {@code --start_end_lib} is enabled, but instead at any binary that transitively
+ * depends on the current rule.
+ */
+ public CcLibraryHelper addPicObjectFiles(Iterable<Artifact> picObjectFiles) {
+ Iterables.addAll(this.picObjectFiles, picObjectFiles);
+ return this;
+ }
+
+ /**
+ * Add the corresponding files as linker inputs for both PIC and non-PIC links.
+ */
+ public CcLibraryHelper addPicIndependentObjectFiles(Iterable<Artifact> objectFiles) {
+ addPicObjectFiles(objectFiles);
+ return addObjectFiles(objectFiles);
+ }
+
+ /**
+ * Add the corresponding files as linker inputs for both PIC and non-PIC links.
+ */
+ public CcLibraryHelper addPicIndependentObjectFiles(Artifact... objectFiles) {
+ return addPicIndependentObjectFiles(Arrays.asList(objectFiles));
+ }
+
+ /**
+ * Add the corresponding files as static libraries into the linker outputs (i.e., after the linker
+ * action) - this makes them available for linking to binary rules that depend on this rule.
+ */
+ public CcLibraryHelper addStaticLibraries(Iterable<LibraryToLink> libraries) {
+ Iterables.addAll(staticLibraries, libraries);
+ return this;
+ }
+
+ /**
+ * Add the corresponding files as static libraries into the linker outputs (i.e., after the linker
+ * action) - this makes them available for linking to binary rules that depend on this rule.
+ */
+ public CcLibraryHelper addPicStaticLibraries(Iterable<LibraryToLink> libraries) {
+ Iterables.addAll(picStaticLibraries, libraries);
+ return this;
+ }
+
+ /**
+ * Add the corresponding files as dynamic libraries into the linker outputs (i.e., after the
+ * linker action) - this makes them available for linking to binary rules that depend on this
+ * rule.
+ */
+ public CcLibraryHelper addDynamicLibraries(Iterable<LibraryToLink> libraries) {
+ Iterables.addAll(dynamicLibraries, libraries);
+ return this;
+ }
+
+ /**
+ * Adds the copts to the compile command line.
+ */
+ public CcLibraryHelper addCopts(Iterable<String> copts) {
+ Iterables.addAll(this.copts, copts);
+ return this;
+ }
+
+ /**
+ * Sets a pattern that is used to filter copts; set to {@code null} for no filtering.
+ */
+ public CcLibraryHelper setNoCopts(@Nullable Pattern nocopts) {
+ this.nocopts = nocopts;
+ return this;
+ }
+
+ /**
+ * Adds the given options as linker options to the link command.
+ */
+ public CcLibraryHelper addLinkopts(Iterable<String> linkopts) {
+ Iterables.addAll(this.linkopts, linkopts);
+ return this;
+ }
+
+ /**
+ * Adds the given defines to the compiler command line.
+ */
+ public CcLibraryHelper addDefines(Iterable<String> defines) {
+ Iterables.addAll(this.defines, defines);
+ return this;
+ }
+
+ /**
+ * Adds the given targets as dependencies - this can include explicit dependencies on other
+ * rules (like from a "deps" attribute) and also implicit dependencies on runtime libraries.
+ */
+ public CcLibraryHelper addDeps(Iterable<? extends TransitiveInfoCollection> deps) {
+ for (TransitiveInfoCollection dep : deps) {
+ Preconditions.checkArgument(dep.getConfiguration() == null
+ || dep.getConfiguration().equals(configuration));
+ this.deps.add(dep);
+ }
+ return this;
+ }
+
+ /**
+ * Adds the given linkstamps. Note that linkstamps are usually not compiled at the library level,
+ * but only in the dependent binary rules.
+ */
+ public CcLibraryHelper addLinkstamps(Iterable<? extends TransitiveInfoCollection> linkstamps) {
+ for (TransitiveInfoCollection linkstamp : linkstamps) {
+ Iterables.addAll(this.linkstamps,
+ linkstamp.getProvider(FileProvider.class).getFilesToBuild());
+ }
+ return this;
+ }
+
+ /**
+ * Adds the given prerequisites as prerequisites for the generated compile actions. This ensures
+ * that the corresponding files exist - otherwise the action fails. Note that these dependencies
+ * add edges to the action graph, and can therefore increase the length of the critical path,
+ * i.e., make the build slower.
+ */
+ public CcLibraryHelper addCompilationPrerequisites(Iterable<Artifact> prerequisites) {
+ Iterables.addAll(this.prerequisites, prerequisites);
+ return this;
+ }
+
+ /**
+ * Adds the given directories to the loose include directories that are only allowed to be
+ * referenced when headers checking is {@link HeadersCheckingMode#LOOSE} or {@link
+ * HeadersCheckingMode#WARN}.
+ */
+ public CcLibraryHelper addLooseIncludeDirs(Iterable<PathFragment> looseIncludeDirs) {
+ Iterables.addAll(this.looseIncludeDirs, looseIncludeDirs);
+ return this;
+ }
+
+ /**
+ * Adds the given directories to the system include directories (they are passed with {@code
+ * "-isystem"} to the compiler); these are also passed to dependent rules.
+ */
+ public CcLibraryHelper addSystemIncludeDirs(Iterable<PathFragment> systemIncludeDirs) {
+ Iterables.addAll(this.systemIncludeDirs, systemIncludeDirs);
+ return this;
+ }
+
+ /**
+ * Adds the given directories to the quote include directories (they are passed with {@code
+ * "-iquote"} to the compiler); these are also passed to dependent rules.
+ */
+ public CcLibraryHelper addIncludeDirs(Iterable<PathFragment> includeDirs) {
+ Iterables.addAll(this.includeDirs, includeDirs);
+ return this;
+ }
+
+ /**
+ * Overrides the path for the generated dynamic library - this should only be called if the
+ * dynamic library is an implicit or explicit output of the rule, i.e., if it is accessible by
+ * name from other rules in the same package. Set to {@code null} to use the default computation.
+ */
+ public CcLibraryHelper setDynamicLibraryPath(@Nullable PathFragment dynamicLibraryPath) {
+ this.dynamicLibraryPath = dynamicLibraryPath;
+ return this;
+ }
+
+ /**
+ * Marks the output of this rule as alwayslink, i.e., the corresponding symbols will be retained
+ * by the linker even if they are not otherwise used. This is useful for libraries that register
+ * themselves somewhere during initialization.
+ *
+ * <p>This only sets the link type (see {@link #setLinkType}), either to a static library or to
+ * an alwayslink static library (blaze uses a different file extension to signal alwayslink to
+ * downstream code).
+ */
+ public CcLibraryHelper setAlwayslink(boolean alwayslink) {
+ linkType = alwayslink
+ ? LinkTargetType.ALWAYS_LINK_STATIC_LIBRARY
+ : LinkTargetType.STATIC_LIBRARY;
+ return this;
+ }
+
+ /**
+ * Directly set the link type. This can be used instead of {@link #setAlwayslink}. Setting
+ * anything other than a static link causes this class to skip the link action creation.
+ */
+ public CcLibraryHelper setLinkType(LinkTargetType linkType) {
+ this.linkType = Preconditions.checkNotNull(linkType);
+ return this;
+ }
+
+ /**
+ * Marks the resulting code as neverlink, i.e., the code will not be linked into dependent
+ * libraries or binaries - the header files are still available.
+ */
+ public CcLibraryHelper setNeverLink(boolean neverlink) {
+ this.neverlink = neverlink;
+ return this;
+ }
+
+ /**
+ * Sets the given headers checking mode. The default is {@link HeadersCheckingMode#LOOSE}.
+ */
+ public CcLibraryHelper setHeadersCheckingMode(HeadersCheckingMode headersCheckingMode) {
+ this.headersCheckingMode = Preconditions.checkNotNull(headersCheckingMode);
+ return this;
+ }
+
+ /**
+ * Marks the resulting code as fake, i.e., the code will not actually be compiled or linked, but
+ * instead, the compile command is written to a file and added to the runfiles. This is currently
+ * used for non-compilation tests. Unfortunately, the design is problematic, so please don't add
+ * any further uses.
+ */
+ public CcLibraryHelper setFake(boolean fake) {
+ this.fake = fake;
+ return this;
+ }
+
+ /**
+ * This adds the {@link CcNativeLibraryProvider} to the providers created by this class.
+ */
+ public CcLibraryHelper enableCcNativeLibrariesProvider() {
+ this.emitCcNativeLibrariesProvider = true;
+ return this;
+ }
+
+ /**
+ * This adds the {@link CcSpecificLinkParamsProvider} to the providers created by this class.
+ * Otherwise the result will contain an instance of {@link CcLinkParamsProvider}.
+ */
+ public CcLibraryHelper enableCcSpecificLinkParamsProvider() {
+ this.emitCcSpecificLinkParamsProvider = true;
+ return this;
+ }
+
+ /**
+ * This disables C++ module map generation for the current rule. Don't call this unless you know
+ * what you are doing.
+ */
+ public CcLibraryHelper disableCppModuleMapGeneration() {
+ this.emitCppModuleMaps = false;
+ return this;
+ }
+
+ /**
+ * This enables or disables use of module maps during compilation, i.e., layering checks.
+ */
+ public CcLibraryHelper setEnableLayeringCheck(boolean enableLayeringCheck) {
+ this.enableLayeringCheck = enableLayeringCheck;
+ return this;
+ }
+
+ /**
+ * This enabled or disables compilation of C++ header modules.
+ * TODO(bazel-team): Add a cc_toolchain flag that allows fully disabling this feature and document
+ * this feature.
+ * See http://clang.llvm.org/docs/Modules.html.
+ */
+ public CcLibraryHelper setCompileHeaderModules(boolean compileHeaderModules) {
+ this.compileHeaderModules = compileHeaderModules;
+ return this;
+ }
+
+ /**
+ * Enables or disables generation of compile actions if there are no sources. Some rules declare a
+ * .a or .so implicit output, which requires that these files are created even if there are no
+ * source files, so be careful when calling this.
+ */
+ public CcLibraryHelper setGenerateCompileActionsIfEmpty(boolean emitCompileActionsIfEmpty) {
+ this.emitCompileActionsIfEmpty = emitCompileActionsIfEmpty;
+ return this;
+ }
+
+ /**
+ * Enables the optional generation of interface dynamic libraries - this is only used when the
+ * linker generates a dynamic library, and only if the crosstool supports it. The default is not
+ * to generate interface dynamic libraries.
+ */
+ public CcLibraryHelper enableInterfaceSharedObjects() {
+ this.emitInterfaceSharedObjects = true;
+ return this;
+ }
+
+ /**
+ * This enables or disables the generation of a dynamic library link action. The default is to
+ * generate a dynamic library. Note that the selection between dynamic or static linking is
+ * performed at the binary rule level.
+ */
+ public CcLibraryHelper setCreateDynamicLibrary(boolean emitDynamicLibrary) {
+ this.emitDynamicLibrary = emitDynamicLibrary;
+ return this;
+ }
+
+ /**
+ * Disables checking that the deps actually are C++ rules. By default, the {@link #build} method
+ * uses {@link LanguageDependentFragment.Checker#depSupportsLanguage} to check that all deps
+ * provide C++ providers.
+ */
+ public CcLibraryHelper setCheckDepsGenerateCpp(boolean checkDepsGenerateCpp) {
+ this.checkDepsGenerateCpp = checkDepsGenerateCpp;
+ return this;
+ }
+
+ /**
+ * Enables the output of {@link FilesToCompileProvider} and {@link
+ * CompilationPrerequisitesProvider}.
+ */
+ // TODO(bazel-team): We probably need to adjust this for the multi-language rules.
+ public CcLibraryHelper enableCompileProviders() {
+ this.emitCompileProviders = true;
+ return this;
+ }
+
+ /**
+ * Sets whether to emit the transitive module map references of a public library headers target.
+ */
+ public CcLibraryHelper setEmitHeaderTargetModuleMaps(boolean emitHeaderTargetModuleMaps) {
+ this.emitHeaderTargetModuleMaps = emitHeaderTargetModuleMaps;
+ return this;
+ }
+
+ /**
+ * Create the C++ compile and link actions, and the corresponding C++-related providers.
+ */
+ public Info build() {
+ // Fail early if there is no lipo context collector on the rule - otherwise we end up failing
+ // in lipo optimization.
+ Preconditions.checkState(
+ // 'cc_inc_library' rules do not compile, and thus are not affected by LIPO.
+ ruleContext.getRule().getRuleClass().equals("cc_inc_library")
+ || ruleContext.getRule().isAttrDefined(":lipo_context_collector", Type.LABEL));
+
+ if (checkDepsGenerateCpp) {
+ for (LanguageDependentFragment dep :
+ AnalysisUtils.getProviders(deps, LanguageDependentFragment.class)) {
+ LanguageDependentFragment.Checker.depSupportsLanguage(
+ ruleContext, dep, CppRuleClasses.LANGUAGE);
+ }
+ }
+
+ CcLinkingOutputs ccLinkingOutputs = CcLinkingOutputs.EMPTY;
+ CcCompilationOutputs ccOutputs = new CcCompilationOutputs.Builder().build();
+ FeatureConfiguration featureConfiguration = CcCommon.configureFeatures(ruleContext);
+
+ CppModel model = new CppModel(ruleContext, semantics)
+ .addSources(sources)
+ .addCopts(copts)
+ .setLinkTargetType(linkType)
+ .setNeverLink(neverlink)
+ .setFake(fake)
+ .setAllowInterfaceSharedObjects(emitInterfaceSharedObjects)
+ .setCreateDynamicLibrary(emitDynamicLibrary)
+ // Note: this doesn't actually save the temps, it just makes the CppModel use the
+ // configurations --save_temps setting to decide whether to actually save the temps.
+ .setSaveTemps(true)
+ .setEnableLayeringCheck(enableLayeringCheck)
+ .setCompileHeaderModules(compileHeaderModules)
+ .setNoCopts(nocopts)
+ .setDynamicLibraryPath(dynamicLibraryPath)
+ .addLinkopts(linkopts)
+ .setFeatureConfiguration(featureConfiguration);
+ CppCompilationContext cppCompilationContext =
+ initializeCppCompilationContext(model, featureConfiguration);
+ model.setContext(cppCompilationContext);
+ if (emitCompileActionsIfEmpty || !sources.isEmpty() || compileHeaderModules) {
+ Preconditions.checkState(
+ !compileHeaderModules || cppCompilationContext.getCppModuleMap() != null,
+ "All cc rules must support module maps.");
+ ccOutputs = model.createCcCompileActions();
+ if (!objectFiles.isEmpty() || !picObjectFiles.isEmpty()) {
+ // Merge the pre-compiled object files into the compiler outputs.
+ ccOutputs = new CcCompilationOutputs.Builder()
+ .merge(ccOutputs)
+ .addObjectFiles(objectFiles)
+ .addPicObjectFiles(picObjectFiles)
+ .build();
+ }
+ if (linkType.isStaticLibraryLink()) {
+ // TODO(bazel-team): This can't create the link action for a cc_binary yet.
+ ccLinkingOutputs = model.createCcLinkActions(ccOutputs);
+ }
+ }
+ CcLinkingOutputs originalLinkingOutputs = ccLinkingOutputs;
+ if (!(
+ staticLibraries.isEmpty() && picStaticLibraries.isEmpty() && dynamicLibraries.isEmpty())) {
+ // Merge the pre-compiled libraries (static & dynamic) into the linker outputs.
+ ccLinkingOutputs = new CcLinkingOutputs.Builder()
+ .merge(ccLinkingOutputs)
+ .addStaticLibraries(staticLibraries)
+ .addPicStaticLibraries(picStaticLibraries)
+ .addDynamicLibraries(dynamicLibraries)
+ .addExecutionDynamicLibraries(dynamicLibraries)
+ .build();
+ }
+
+ DwoArtifactsCollector dwoArtifacts = DwoArtifactsCollector.transitiveCollector(ccOutputs, deps);
+ Runfiles cppStaticRunfiles = collectCppRunfiles(ccLinkingOutputs, true);
+ Runfiles cppSharedRunfiles = collectCppRunfiles(ccLinkingOutputs, false);
+
+ // By very careful when adding new providers here - it can potentially affect a lot of rules.
+ // We should consider merging most of these providers into a single provider.
+ Map<Class<? extends TransitiveInfoProvider>, TransitiveInfoProvider> providers =
+ new LinkedHashMap<>();
+ providers.put(CppRunfilesProvider.class,
+ new CppRunfilesProvider(cppStaticRunfiles, cppSharedRunfiles));
+ providers.put(CppCompilationContext.class, cppCompilationContext);
+ providers.put(CppDebugFileProvider.class, new CppDebugFileProvider(
+ dwoArtifacts.getDwoArtifacts(), dwoArtifacts.getPicDwoArtifacts()));
+ providers.put(TransitiveLipoInfoProvider.class, collectTransitiveLipoInfo(ccOutputs));
+ providers.put(TempsProvider.class, getTemps(ccOutputs));
+ if (emitCompileProviders) {
+ providers.put(FilesToCompileProvider.class, new FilesToCompileProvider(
+ getFilesToCompile(ccOutputs)));
+ providers.put(CompilationPrerequisitesProvider.class,
+ CcCommon.collectCompilationPrerequisites(ruleContext, cppCompilationContext));
+ }
+
+ // TODO(bazel-team): Maybe we can infer these from other data at the places where they are
+ // used.
+ if (emitCcNativeLibrariesProvider) {
+ providers.put(CcNativeLibraryProvider.class,
+ new CcNativeLibraryProvider(collectNativeCcLibraries(ccLinkingOutputs)));
+ }
+ providers.put(CcExecutionDynamicLibrariesProvider.class,
+ collectExecutionDynamicLibraryArtifacts(ccLinkingOutputs.getExecutionDynamicLibraries()));
+
+ boolean forcePic = ruleContext.getFragment(CppConfiguration.class).forcePic();
+ if (emitCcSpecificLinkParamsProvider) {
+ providers.put(CcSpecificLinkParamsProvider.class, new CcSpecificLinkParamsProvider(
+ createCcLinkParamsStore(ccLinkingOutputs, cppCompilationContext, forcePic)));
+ } else {
+ providers.put(CcLinkParamsProvider.class, new CcLinkParamsProvider(
+ createCcLinkParamsStore(ccLinkingOutputs, cppCompilationContext, forcePic)));
+ }
+ return new Info(providers, ccOutputs, ccLinkingOutputs, originalLinkingOutputs,
+ cppCompilationContext);
+ }
+
+ /**
+ * Create context for cc compile action from generated inputs.
+ */
+ private CppCompilationContext initializeCppCompilationContext(CppModel model,
+ FeatureConfiguration featureConfiguration) {
+ CppCompilationContext.Builder contextBuilder =
+ new CppCompilationContext.Builder(ruleContext);
+
+ // Setup the include path; local include directories come before those inherited from deps or
+ // from the toolchain; in case of aliasing (same include file found on different entries),
+ // prefer the local include rather than the inherited one.
+
+ // Add in the roots for well-formed include names for source files and
+ // generated files. It is important that the execRoot (EMPTY_FRAGMENT) comes
+ // before the genfilesFragment to preferably pick up source files. Otherwise
+ // we might pick up stale generated files.
+ contextBuilder.addQuoteIncludeDir(PathFragment.EMPTY_FRAGMENT);
+ contextBuilder.addQuoteIncludeDir(ruleContext.getConfiguration().getGenfilesFragment());
+
+ for (PathFragment systemIncludeDir : systemIncludeDirs) {
+ contextBuilder.addSystemIncludeDir(systemIncludeDir);
+ }
+ for (PathFragment includeDir : includeDirs) {
+ contextBuilder.addIncludeDir(includeDir);
+ }
+
+ contextBuilder.mergeDependentContexts(
+ AnalysisUtils.getProviders(deps, CppCompilationContext.class));
+ CppHelper.mergeToolchainDependentContext(ruleContext, contextBuilder);
+
+ // But defines come after those inherited from deps.
+ contextBuilder.addDefines(defines);
+
+ // There are no ordering constraints for declared include dirs/srcs, or the pregrepped headers.
+ contextBuilder.addDeclaredIncludeSrcs(publicHeaders);
+ contextBuilder.addDeclaredIncludeSrcs(privateHeaders);
+ contextBuilder.addPregreppedHeaderMap(
+ CppHelper.createExtractInclusions(ruleContext, publicHeaders));
+ contextBuilder.addPregreppedHeaderMap(
+ CppHelper.createExtractInclusions(ruleContext, privateHeaders));
+ contextBuilder.addCompilationPrerequisites(prerequisites);
+
+ // Add this package's dir to declaredIncludeDirs, & this rule's headers to declaredIncludeSrcs
+ // Note: no include dir for STRICT mode.
+ if (headersCheckingMode == HeadersCheckingMode.WARN) {
+ contextBuilder.addDeclaredIncludeWarnDir(ruleContext.getLabel().getPackageFragment());
+ for (PathFragment looseIncludeDir : looseIncludeDirs) {
+ contextBuilder.addDeclaredIncludeWarnDir(looseIncludeDir);
+ }
+ } else if (headersCheckingMode == HeadersCheckingMode.LOOSE) {
+ contextBuilder.addDeclaredIncludeDir(ruleContext.getLabel().getPackageFragment());
+ for (PathFragment looseIncludeDir : looseIncludeDirs) {
+ contextBuilder.addDeclaredIncludeDir(looseIncludeDir);
+ }
+ }
+
+ if (emitCppModuleMaps) {
+ CppModuleMap cppModuleMap = CppHelper.addCppModuleMapToContext(ruleContext, contextBuilder);
+ // TODO(bazel-team): addCppModuleMapToContext second-guesses whether module maps should
+ // actually be enabled, so we need to double-check here. Who would write code like this?
+ if (cppModuleMap != null) {
+ CppModuleMapAction action = new CppModuleMapAction(ruleContext.getActionOwner(),
+ cppModuleMap,
+ privateHeaders,
+ publicHeaders,
+ collectModuleMaps(),
+ additionalExportedHeaders,
+ compileHeaderModules,
+ featureConfiguration.isEnabled(CppRuleClasses.MODULE_MAP_HOME_CWD));
+ ruleContext.registerAction(action);
+ }
+ if (model.getGeneratesPicHeaderModule()) {
+ contextBuilder.setPicHeaderModule(model.getPicHeaderModule(cppModuleMap.getArtifact()));
+ }
+ if (model.getGeratesNoPicHeaderModule()) {
+ contextBuilder.setHeaderModule(model.getHeaderModule(cppModuleMap.getArtifact()));
+ }
+ }
+
+ semantics.setupCompilationContext(ruleContext, contextBuilder);
+ return contextBuilder.build();
+ }
+
+ private Iterable<CppModuleMap> collectModuleMaps() {
+ // Cpp module maps may be null for some rules. We filter the nulls out at the end.
+ List<CppModuleMap> result = new ArrayList<>();
+ Iterables.addAll(result, Iterables.transform(deps, CPP_DEPS_TO_MODULES));
+ CppCompilationContext stl =
+ ruleContext.getPrerequisite(":stl", Mode.TARGET, CppCompilationContext.class);
+ if (stl != null) {
+ result.add(stl.getCppModuleMap());
+ }
+
+ CcToolchainProvider toolchain = CppHelper.getToolchain(ruleContext);
+ if (toolchain != null) {
+ result.add(toolchain.getCppCompilationContext().getCppModuleMap());
+ }
+
+ if (emitHeaderTargetModuleMaps) {
+ for (HeaderTargetModuleMapProvider provider : AnalysisUtils.getProviders(
+ deps, HeaderTargetModuleMapProvider.class)) {
+ result.addAll(provider.getCppModuleMaps());
+ }
+ }
+
+ return Iterables.filter(result, Predicates.<CppModuleMap>notNull());
+ }
+
+ private TransitiveLipoInfoProvider collectTransitiveLipoInfo(CcCompilationOutputs outputs) {
+ if (ruleContext.getFragment(CppConfiguration.class).getFdoSupport().getFdoRoot() == null) {
+ return TransitiveLipoInfoProvider.EMPTY;
+ }
+ NestedSetBuilder<IncludeScannable> scannableBuilder = NestedSetBuilder.stableOrder();
+ // TODO(bazel-team): Only fetch the STL prerequisite in one place.
+ TransitiveInfoCollection stl = ruleContext.getPrerequisite(":stl", Mode.TARGET);
+ if (stl != null) {
+ TransitiveLipoInfoProvider provider = stl.getProvider(TransitiveLipoInfoProvider.class);
+ if (provider != null) {
+ scannableBuilder.addTransitive(provider.getTransitiveIncludeScannables());
+ }
+ }
+
+ for (TransitiveLipoInfoProvider dep :
+ AnalysisUtils.getProviders(deps, TransitiveLipoInfoProvider.class)) {
+ scannableBuilder.addTransitive(dep.getTransitiveIncludeScannables());
+ }
+
+ for (IncludeScannable scannable : outputs.getLipoScannables()) {
+ Preconditions.checkState(scannable.getIncludeScannerSources().size() == 1);
+ scannableBuilder.add(scannable);
+ }
+ return new TransitiveLipoInfoProvider(scannableBuilder.build());
+ }
+
+ private Runfiles collectCppRunfiles(
+ CcLinkingOutputs ccLinkingOutputs, boolean linkingStatically) {
+ Runfiles.Builder builder = new Runfiles.Builder();
+ builder.addTargets(deps, RunfilesProvider.DEFAULT_RUNFILES);
+ builder.addTargets(deps, CppRunfilesProvider.runfilesFunction(linkingStatically));
+ // Add the shared libraries to the runfiles.
+ builder.addArtifacts(ccLinkingOutputs.getLibrariesForRunfiles(linkingStatically));
+ return builder.build();
+ }
+
+ private CcLinkParamsStore createCcLinkParamsStore(
+ final CcLinkingOutputs ccLinkingOutputs, final CppCompilationContext cppCompilationContext,
+ final boolean forcePic) {
+ return new CcLinkParamsStore() {
+ @Override
+ protected void collect(CcLinkParams.Builder builder, boolean linkingStatically,
+ boolean linkShared) {
+ builder.addLinkstamps(linkstamps, cppCompilationContext);
+ builder.addTransitiveTargets(deps,
+ CcLinkParamsProvider.TO_LINK_PARAMS, CcSpecificLinkParamsProvider.TO_LINK_PARAMS);
+ if (!neverlink) {
+ builder.addLibraries(ccLinkingOutputs.getPreferredLibraries(linkingStatically,
+ /*preferPic=*/linkShared || forcePic));
+ builder.addLinkOpts(linkopts);
+ }
+ }
+ };
+ }
+
+ private NestedSet<LinkerInput> collectNativeCcLibraries(CcLinkingOutputs ccLinkingOutputs) {
+ NestedSetBuilder<LinkerInput> result = NestedSetBuilder.linkOrder();
+ result.addAll(ccLinkingOutputs.getDynamicLibraries());
+ for (CcNativeLibraryProvider dep : AnalysisUtils.getProviders(
+ deps, CcNativeLibraryProvider.class)) {
+ result.addTransitive(dep.getTransitiveCcNativeLibraries());
+ }
+
+ return result.build();
+ }
+
+ private CcExecutionDynamicLibrariesProvider collectExecutionDynamicLibraryArtifacts(
+ List<LibraryToLink> executionDynamicLibraries) {
+ Iterable<Artifact> artifacts = LinkerInputs.toLibraryArtifacts(executionDynamicLibraries);
+ if (!Iterables.isEmpty(artifacts)) {
+ return new CcExecutionDynamicLibrariesProvider(
+ NestedSetBuilder.wrap(Order.STABLE_ORDER, artifacts));
+ }
+
+ NestedSetBuilder<Artifact> builder = NestedSetBuilder.stableOrder();
+ for (CcExecutionDynamicLibrariesProvider dep :
+ AnalysisUtils.getProviders(deps, CcExecutionDynamicLibrariesProvider.class)) {
+ builder.addTransitive(dep.getExecutionDynamicLibraryArtifacts());
+ }
+ return builder.isEmpty()
+ ? CcExecutionDynamicLibrariesProvider.EMPTY
+ : new CcExecutionDynamicLibrariesProvider(builder.build());
+ }
+
+ private TempsProvider getTemps(CcCompilationOutputs compilationOutputs) {
+ return ruleContext.getFragment(CppConfiguration.class).isLipoContextCollector()
+ ? new TempsProvider(ImmutableList.<Artifact>of())
+ : new TempsProvider(compilationOutputs.getTemps());
+ }
+
+ private ImmutableList<Artifact> getFilesToCompile(CcCompilationOutputs compilationOutputs) {
+ return ruleContext.getFragment(CppConfiguration.class).isLipoContextCollector()
+ ? ImmutableList.<Artifact>of()
+ : compilationOutputs.getObjectFiles(CppHelper.usePic(ruleContext, false));
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CcLinkParams.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcLinkParams.java
new file mode 100644
index 0000000..4e4804b
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcLinkParams.java
@@ -0,0 +1,357 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import com.google.common.base.Function;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.rules.cpp.LinkerInputs.LibraryToLink;
+
+import java.util.Collection;
+import java.util.Objects;
+
+/**
+ * Parameters to be passed to the linker.
+ *
+ * <p>The parameters concerned are the link options (strings) passed to the linker, linkstamps and a
+ * list of libraries to be linked in.
+ *
+ * <p>Items in the collections are stored in nested sets. Link options and libraries are stored in
+ * link order (preorder) and linkstamps are sorted.
+ */
+public final class CcLinkParams {
+ private final NestedSet<ImmutableList<String>> linkOpts;
+ private final NestedSet<Linkstamp> linkstamps;
+ private final NestedSet<LibraryToLink> libraries;
+
+ private CcLinkParams(NestedSet<ImmutableList<String>> linkOpts,
+ NestedSet<Linkstamp> linkstamps,
+ NestedSet<LibraryToLink> libraries) {
+ this.linkOpts = linkOpts;
+ this.linkstamps = linkstamps;
+ this.libraries = libraries;
+ }
+
+ /**
+ * @return the linkopts
+ */
+ public NestedSet<ImmutableList<String>> getLinkopts() {
+ return linkOpts;
+ }
+
+ public ImmutableList<String> flattenedLinkopts() {
+ return ImmutableList.copyOf(Iterables.concat(linkOpts));
+ }
+
+ /**
+ * @return the linkstamps
+ */
+ public NestedSet<Linkstamp> getLinkstamps() {
+ return linkstamps;
+ }
+
+ /**
+ * @return the libraries
+ */
+ public NestedSet<LibraryToLink> getLibraries() {
+ return libraries;
+ }
+
+ public static final Builder builder(boolean linkingStatically, boolean linkShared) {
+ return new Builder(linkingStatically, linkShared);
+ }
+
+ /**
+ * Builder for {@link CcLinkParams}.
+ *
+ *
+ */
+ public static final class Builder {
+
+ /**
+ * linkingStatically is true when we're linking this target in either FULLY STATIC mode
+ * (linkopts=["-static"]) or MOSTLY STATIC mode (linkstatic=1). When this is true, we want to
+ * use static versions of any libraries that this target depends on (except possibly system
+ * libraries, which are not handled by CcLinkParams). When this is false, we want to use dynamic
+ * versions of any libraries that this target depends on.
+ */
+ private final boolean linkingStatically;
+
+ /**
+ * linkShared is true when we're linking with "-shared" (linkshared=1).
+ */
+ private final boolean linkShared;
+
+ private ImmutableList.Builder<String> localLinkoptsBuilder = ImmutableList.builder();
+
+ private final NestedSetBuilder<ImmutableList<String>> linkOptsBuilder =
+ NestedSetBuilder.linkOrder();
+ private final NestedSetBuilder<Linkstamp> linkstampsBuilder =
+ NestedSetBuilder.compileOrder();
+ private final NestedSetBuilder<LibraryToLink> librariesBuilder =
+ NestedSetBuilder.linkOrder();
+
+ private boolean built = false;
+
+ private Builder(boolean linkingStatically, boolean linkShared) {
+ this.linkingStatically = linkingStatically;
+ this.linkShared = linkShared;
+ }
+
+ /**
+ * Build a {@link CcLinkParams} object.
+ */
+ public CcLinkParams build() {
+ Preconditions.checkState(!built);
+ // Not thread-safe, but builders should not be shared across threads.
+ built = true;
+ ImmutableList<String> localLinkopts = localLinkoptsBuilder.build();
+ if (!localLinkopts.isEmpty()) {
+ linkOptsBuilder.add(localLinkopts);
+ }
+ return new CcLinkParams(linkOptsBuilder.build(), linkstampsBuilder.build(),
+ librariesBuilder.build());
+ }
+
+ private boolean add(CcLinkParamsStore store) {
+ if (store != null) {
+ CcLinkParams args = store.get(linkingStatically, linkShared);
+ addTransitiveArgs(args);
+ }
+ return store != null;
+ }
+
+ /**
+ * Includes link parameters from a collection of dependency targets.
+ */
+ public Builder addTransitiveTargets(Iterable<? extends TransitiveInfoCollection> targets) {
+ for (TransitiveInfoCollection target : targets) {
+ addTransitiveTarget(target);
+ }
+ return this;
+ }
+
+ /**
+ * Includes link parameters from a dependency target.
+ *
+ * <p>The target should implement {@link CcLinkParamsProvider}. If it does not,
+ * the method does not do anything.
+ */
+ public Builder addTransitiveTarget(TransitiveInfoCollection target) {
+ return addTransitiveProvider(target.getProvider(CcLinkParamsProvider.class));
+ }
+
+ /**
+ * Includes link parameters from a dependency target. The target is checked for the given
+ * mappings in the order specified, and the first mapping that returns a non-null result is
+ * added.
+ */
+ @SafeVarargs
+ public final Builder addTransitiveTarget(TransitiveInfoCollection target,
+ Function<TransitiveInfoCollection, CcLinkParamsStore> firstMapping,
+ @SuppressWarnings("unchecked") // Java arrays don't preserve generic arguments.
+ Function<TransitiveInfoCollection, CcLinkParamsStore>... remainingMappings) {
+ if (add(firstMapping.apply(target))) {
+ return this;
+ }
+ for (Function<TransitiveInfoCollection, CcLinkParamsStore> mapping : remainingMappings) {
+ if (add(mapping.apply(target))) {
+ return this;
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Includes link parameters from a CcLinkParamsProvider provider.
+ */
+ public Builder addTransitiveProvider(CcLinkParamsProvider provider) {
+ if (provider == null) {
+ return this;
+ }
+
+ CcLinkParams args = provider.getCcLinkParams(linkingStatically, linkShared);
+ addTransitiveArgs(args);
+ return this;
+ }
+
+ /**
+ * Includes link parameters from the given targets. Each target is checked for the given
+ * mappings in the order specified, and the first mapping that returns a non-null result is
+ * added.
+ */
+ @SafeVarargs
+ public final Builder addTransitiveTargets(
+ Iterable<? extends TransitiveInfoCollection> targets,
+ Function<TransitiveInfoCollection, CcLinkParamsStore> firstMapping,
+ @SuppressWarnings("unchecked") // Java arrays don't preserve generic arguments.
+ Function<TransitiveInfoCollection, CcLinkParamsStore>... remainingMappings) {
+ for (TransitiveInfoCollection target : targets) {
+ addTransitiveTarget(target, firstMapping, remainingMappings);
+ }
+ return this;
+ }
+
+ /**
+ * Includes link parameters from the given targets. Each target is checked for the given
+ * mappings in the order specified, and the first mapping that returns a non-null result is
+ * added.
+ *
+ * @deprecated don't add any new uses; all existing uses need to be audited and possibly merged
+ * into a single call - some of them may introduce semantic changes which need to be
+ * carefully vetted
+ */
+ @Deprecated
+ @SafeVarargs
+ public final Builder addTransitiveLangTargets(
+ Iterable<? extends TransitiveInfoCollection> targets,
+ Function<TransitiveInfoCollection, CcLinkParamsStore> firstMapping,
+ @SuppressWarnings("unchecked") // Java arrays don't preserve generic arguments.
+ Function<TransitiveInfoCollection, CcLinkParamsStore>... remainingMappings) {
+ return addTransitiveTargets(targets, firstMapping, remainingMappings);
+ }
+
+ /**
+ * Merges the other {@link CcLinkParams} object into this one.
+ */
+ public Builder addTransitiveArgs(CcLinkParams args) {
+ linkOptsBuilder.addTransitive(args.getLinkopts());
+ linkstampsBuilder.addTransitive(args.getLinkstamps());
+ librariesBuilder.addTransitive(args.getLibraries());
+ return this;
+ }
+
+ /**
+ * Adds a collection of link options.
+ */
+ public Builder addLinkOpts(Collection<String> linkOpts) {
+ localLinkoptsBuilder.addAll(linkOpts);
+ return this;
+ }
+
+ /**
+ * Adds a collection of linkstamps.
+ */
+ public Builder addLinkstamps(Iterable<Artifact> linkstamps, CppCompilationContext context) {
+ ImmutableList<Artifact> declaredIncludeSrcs =
+ ImmutableList.copyOf(context.getDeclaredIncludeSrcs());
+ for (Artifact linkstamp : linkstamps) {
+ linkstampsBuilder.add(new Linkstamp(linkstamp, declaredIncludeSrcs));
+ }
+ return this;
+ }
+
+ /**
+ * Adds a library artifact.
+ */
+ public Builder addLibrary(LibraryToLink library) {
+ librariesBuilder.add(library);
+ return this;
+ }
+
+ /**
+ * Adds a collection of library artifacts.
+ */
+ public Builder addLibraries(Iterable<LibraryToLink> libraries) {
+ librariesBuilder.addAll(libraries);
+ return this;
+ }
+
+ /**
+ * Processes typical dependencies a C/C++ library.
+ *
+ * <p>A helper method that processes getValues() and merges contents of
+ * getPreferredLibraries() and getLinkOpts() into the current link params
+ * object.
+ */
+ public Builder addCcLibrary(RuleContext context, CcCommon common, boolean neverlink,
+ CcLinkingOutputs linkingOutputs) {
+ addTransitiveTargets(
+ context.getPrerequisites("deps", Mode.TARGET),
+ CcLinkParamsProvider.TO_LINK_PARAMS, CcSpecificLinkParamsProvider.TO_LINK_PARAMS);
+
+ if (!neverlink) {
+ addLibraries(linkingOutputs.getPreferredLibraries(linkingStatically,
+ linkShared || context.getFragment(CppConfiguration.class).forcePic()));
+ addLinkOpts(common.getLinkopts());
+ }
+ return this;
+ }
+ }
+
+ /**
+ * A linkstamp that also knows about its declared includes.
+ *
+ * <p>This object is required because linkstamp files may include other headers which
+ * will have to be provided during compilation.
+ */
+ public static final class Linkstamp {
+ private final Artifact artifact;
+ private final ImmutableList<Artifact> declaredIncludeSrcs;
+
+ private Linkstamp(Artifact artifact, ImmutableList<Artifact> declaredIncludeSrcs) {
+ this.artifact = Preconditions.checkNotNull(artifact);
+ this.declaredIncludeSrcs = Preconditions.checkNotNull(declaredIncludeSrcs);
+ }
+
+ /**
+ * Returns the linkstamp artifact.
+ */
+ public Artifact getArtifact() {
+ return artifact;
+ }
+
+ /**
+ * Returns the declared includes.
+ */
+ public ImmutableList<Artifact> getDeclaredIncludeSrcs() {
+ return declaredIncludeSrcs;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(artifact, declaredIncludeSrcs);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof Linkstamp)) {
+ return false;
+ }
+ Linkstamp other = (Linkstamp) obj;
+ return artifact.equals(other.artifact)
+ && declaredIncludeSrcs.equals(other.declaredIncludeSrcs);
+ }
+ }
+
+ /**
+ * Empty CcLinkParams.
+ */
+ public static final CcLinkParams EMPTY = new CcLinkParams(
+ NestedSetBuilder.<ImmutableList<String>>emptySet(Order.LINK_ORDER),
+ NestedSetBuilder.<Linkstamp>emptySet(Order.COMPILE_ORDER),
+ NestedSetBuilder.<LibraryToLink>emptySet(Order.LINK_ORDER));
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CcLinkParamsProvider.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcLinkParamsProvider.java
new file mode 100644
index 0000000..11f6011
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcLinkParamsProvider.java
@@ -0,0 +1,50 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import com.google.common.base.Function;
+import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
+import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.rules.cpp.CcLinkParamsStore.CcLinkParamsStoreImpl;
+
+/**
+ * A target that provides C linker parameters.
+ */
+@Immutable
+public final class CcLinkParamsProvider implements TransitiveInfoProvider {
+ public static final Function<TransitiveInfoCollection, CcLinkParamsStore> TO_LINK_PARAMS =
+ new Function<TransitiveInfoCollection, CcLinkParamsStore>() {
+ @Override
+ public CcLinkParamsStore apply(TransitiveInfoCollection input) {
+ CcLinkParamsProvider provider = input.getProvider(
+ CcLinkParamsProvider.class);
+ return provider == null ? null : provider.store;
+ }
+ };
+
+ private final CcLinkParamsStoreImpl store;
+
+ public CcLinkParamsProvider(CcLinkParamsStore store) {
+ this.store = new CcLinkParamsStoreImpl(store);
+ }
+
+ /**
+ * Returns link parameters given static / shared linking settings.
+ */
+ public CcLinkParams getCcLinkParams(boolean linkingStatically, boolean linkShared) {
+ return store.get(linkingStatically, linkShared);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CcLinkParamsStore.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcLinkParamsStore.java
new file mode 100644
index 0000000..a150488
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcLinkParamsStore.java
@@ -0,0 +1,136 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.rules.cpp.CcLinkParams.Builder;
+
+/**
+ * A cache of C link parameters.
+ *
+ * <p>The cache holds instances of {@link CcLinkParams} for combinations of
+ * linkingStatically and linkShared. If a requested value is not available in
+ * the cache, it is computed and then stored.
+ *
+ * <p>Typically this class is used on targets that may be linked in as C
+ * libraries as in the following example:
+ *
+ * <pre>
+ * class SomeTarget implements CcLinkParamsProvider {
+ * private final CcLinkParamsStore ccLinkParamsStore = new CcLinkParamsStore() {
+ * @Override
+ * protected void collect(CcLinkParams.Builder builder, boolean linkingStatically,
+ * boolean linkShared) {
+ * builder.add[...]
+ * }
+ * };
+ *
+ * @Override
+ * public CcLinkParams getCcLinkParams(boolean linkingStatically, boolean linkShared) {
+ * return ccLinkParamsStore.get(linkingStatically, linkShared);
+ * }
+ * }
+ * </pre>
+ */
+public abstract class CcLinkParamsStore {
+
+ private CcLinkParams staticSharedParams;
+ private CcLinkParams staticNoSharedParams;
+ private CcLinkParams noStaticSharedParams;
+ private CcLinkParams noStaticNoSharedParams;
+
+ private CcLinkParams compute(boolean linkingStatically, boolean linkShared) {
+ CcLinkParams.Builder builder = CcLinkParams.builder(linkingStatically, linkShared);
+ collect(builder, linkingStatically, linkShared);
+ return builder.build();
+ }
+
+ /**
+ * Returns {@link CcLinkParams} for a combination of parameters.
+ *
+ * <p>The {@link CcLinkParams} instance is computed lazily and cached.
+ */
+ public synchronized CcLinkParams get(boolean linkingStatically, boolean linkShared) {
+ CcLinkParams result = lookup(linkingStatically, linkShared);
+ if (result == null) {
+ result = compute(linkingStatically, linkShared);
+ put(linkingStatically, linkShared, result);
+ }
+ return result;
+ }
+
+ private CcLinkParams lookup(boolean linkingStatically, boolean linkShared) {
+ if (linkingStatically) {
+ return linkShared ? staticSharedParams : staticNoSharedParams;
+ } else {
+ return linkShared ? noStaticSharedParams : noStaticNoSharedParams;
+ }
+ }
+
+ private void put(boolean linkingStatically, boolean linkShared, CcLinkParams params) {
+ Preconditions.checkNotNull(params);
+ if (linkingStatically) {
+ if (linkShared) {
+ staticSharedParams = params;
+ } else {
+ staticNoSharedParams = params;
+ }
+ } else {
+ if (linkShared) {
+ noStaticSharedParams = params;
+ } else {
+ noStaticNoSharedParams = params;
+ }
+ }
+ }
+
+ /**
+ * Hook for building the actual link params.
+ *
+ * <p>Users should override this method and call methods of the builder to
+ * set up the actual CcLinkParams objects.
+ *
+ * <p>Implementations of this method must not fail or try to report errors on the
+ * configured target.
+ */
+ protected abstract void collect(CcLinkParams.Builder builder, boolean linkingStatically,
+ boolean linkShared);
+
+ /**
+ * An empty CcLinkParamStore.
+ */
+ public static final CcLinkParamsStore EMPTY = new CcLinkParamsStore() {
+
+ @Override
+ protected void collect(Builder builder, boolean linkingStatically, boolean linkShared) {}
+ };
+
+ /**
+ * An implementation class for the CcLinkParamsStore.
+ */
+ public static final class CcLinkParamsStoreImpl extends CcLinkParamsStore {
+
+ public CcLinkParamsStoreImpl(CcLinkParamsStore store) {
+ super.staticSharedParams = store.get(true, true);
+ super.staticNoSharedParams = store.get(true, false);
+ super.noStaticSharedParams = store.get(false, true);
+ super.noStaticNoSharedParams = store.get(false, false);
+ }
+
+ @Override
+ protected void collect(Builder builder, boolean linkingStatically, boolean linkShared) {}
+ }
+}
+
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CcLinkingOutputs.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcLinkingOutputs.java
new file mode 100644
index 0000000..6b45c79
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcLinkingOutputs.java
@@ -0,0 +1,243 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.rules.cpp.Link.LinkStaticness;
+import com.google.devtools.build.lib.rules.cpp.LinkerInputs.LibraryToLink;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A structured representation of the link outputs of a C++ rule.
+ */
+public class CcLinkingOutputs {
+
+ public static final CcLinkingOutputs EMPTY = new Builder().build();
+
+ private final ImmutableList<LibraryToLink> staticLibraries;
+
+ private final ImmutableList<LibraryToLink> picStaticLibraries;
+
+ private final ImmutableList<LibraryToLink> dynamicLibraries;
+
+ private final ImmutableList<LibraryToLink> executionDynamicLibraries;
+
+ private CcLinkingOutputs(ImmutableList<LibraryToLink> staticLibraries,
+ ImmutableList<LibraryToLink> picStaticLibraries,
+ ImmutableList<LibraryToLink> dynamicLibraries,
+ ImmutableList<LibraryToLink> executionDynamicLibraries) {
+ this.staticLibraries = staticLibraries;
+ this.picStaticLibraries = picStaticLibraries;
+ this.dynamicLibraries = dynamicLibraries;
+ this.executionDynamicLibraries = executionDynamicLibraries;
+ }
+
+ public ImmutableList<LibraryToLink> getStaticLibraries() {
+ return staticLibraries;
+ }
+
+ public ImmutableList<LibraryToLink> getPicStaticLibraries() {
+ return picStaticLibraries;
+ }
+
+ public ImmutableList<LibraryToLink> getDynamicLibraries() {
+ return dynamicLibraries;
+ }
+
+ public ImmutableList<LibraryToLink> getExecutionDynamicLibraries() {
+ return executionDynamicLibraries;
+ }
+
+ /**
+ * Add the ".a", ".pic.a" and/or ".so" files in appropriate order of preference depending on the
+ * link preferences.
+ *
+ * <p>This method tries to simulate a search path for adding static and dynamic libraries,
+ * allowing either to be preferred over the other depending on the link {@link LinkStaticness}.
+ *
+ * TODO(bazel-team): (2009) we should preserve the relative ordering of first and second
+ * choice libraries. E.g. if srcs=['foo.a','bar.so','baz.a'] then we should link them in the
+ * same order. Currently we link entries from the first choice list before those from the
+ * second choice list, i.e. in the order {@code ['bar.so', 'foo.a', 'baz.a']}.
+ *
+ * @param linkingStatically whether to prefer static over dynamic libraries. Should be
+ * <code>true</code> for binaries that are linked in fully static or mostly static mode.
+ * @param preferPic whether to prefer pic over non pic libraries (usually used when linking
+ * shared)
+ */
+ public List<LibraryToLink> getPreferredLibraries(
+ boolean linkingStatically, boolean preferPic) {
+ return getPreferredLibraries(linkingStatically, preferPic, false);
+ }
+
+ /**
+ * Returns the shared libraries that are linked against and therefore also need to be in the
+ * runfiles.
+ */
+ public Iterable<Artifact> getLibrariesForRunfiles(boolean linkingStatically) {
+ List<LibraryToLink> libraries =
+ getPreferredLibraries(linkingStatically, /*preferPic*/false, true);
+ return CcCommon.getSharedLibrariesFrom(LinkerInputs.toLibraryArtifacts(libraries));
+ }
+
+ /**
+ * Add the ".a", ".pic.a" and/or ".so" files in appropriate order of
+ * preference depending on the link preferences.
+ */
+ private List<LibraryToLink> getPreferredLibraries(boolean linkingStatically, boolean preferPic,
+ boolean forRunfiles) {
+ List<LibraryToLink> candidates = new ArrayList<>();
+ // It's important that this code keeps the invariant that preferPic has no effect on the output
+ // of .so libraries. That is, the resulting list should contain the same .so files in the same
+ // order.
+ if (linkingStatically) { // Prefer the static libraries.
+ if (preferPic) {
+ // First choice is the PIC static libraries.
+ // Second choice is the other static libraries (may cause link error if they're not PIC,
+ // but I think this is preferable to linking dynamically when you asked for statically).
+ candidates.addAll(picStaticLibraries);
+ candidates.addAll(staticLibraries);
+ } else {
+ // First choice is the non-pic static libraries (best performance);
+ // second choice is the staticPicLibraries (at least they're static;
+ // we can live with the extra overhead of PIC).
+ candidates.addAll(staticLibraries);
+ candidates.addAll(picStaticLibraries);
+ }
+ candidates.addAll(forRunfiles ? executionDynamicLibraries : dynamicLibraries);
+ } else {
+ // First choice is the dynamicLibraries.
+ candidates.addAll(forRunfiles ? executionDynamicLibraries : dynamicLibraries);
+ if (preferPic) {
+ // Second choice is the staticPicLibraries (at least they're PIC, so we won't get a
+ // link error).
+ candidates.addAll(picStaticLibraries);
+ candidates.addAll(staticLibraries);
+ } else {
+ candidates.addAll(staticLibraries);
+ candidates.addAll(picStaticLibraries);
+ }
+ }
+ return filterCandidates(candidates);
+ }
+
+ /**
+ * Helper method to filter the candidates by removing equivalent library
+ * entries from the list of candidates.
+ *
+ * @param candidates the library candidates to filter
+ * @return the list of libraries with equivalent duplicate libraries removed.
+ */
+ private List<LibraryToLink> filterCandidates(List<LibraryToLink> candidates) {
+ List<LibraryToLink> libraries = new ArrayList<>();
+ Set<String> identifiers = new HashSet<>();
+ for (LibraryToLink library : candidates) {
+ if (identifiers.add(libraryIdentifierOf(library.getOriginalLibraryArtifact()))) {
+ libraries.add(library);
+ }
+ }
+ return libraries;
+ }
+
+ /**
+ * Returns the library identifier of an artifact: a string that is different for different
+ * libraries, but is the same for the shared, static and pic versions of the same library.
+ */
+ private static String libraryIdentifierOf(Artifact libraryArtifact) {
+ String name = libraryArtifact.getRootRelativePath().getPathString();
+ String basename = FileSystemUtils.removeExtension(name);
+ // Need to special-case file types with double extension.
+ return name.endsWith(".pic.a")
+ ? FileSystemUtils.removeExtension(basename)
+ : name.endsWith(".nopic.a")
+ ? FileSystemUtils.removeExtension(basename)
+ : name.endsWith(".pic.lo")
+ ? FileSystemUtils.removeExtension(basename)
+ : basename;
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static final class Builder {
+ private final Set<LibraryToLink> staticLibraries = new LinkedHashSet<>();
+ private final Set<LibraryToLink> picStaticLibraries = new LinkedHashSet<>();
+ private final Set<LibraryToLink> dynamicLibraries = new LinkedHashSet<>();
+ private final Set<LibraryToLink> executionDynamicLibraries = new LinkedHashSet<>();
+
+ public CcLinkingOutputs build() {
+ return new CcLinkingOutputs(ImmutableList.copyOf(staticLibraries),
+ ImmutableList.copyOf(picStaticLibraries), ImmutableList.copyOf(dynamicLibraries),
+ ImmutableList.copyOf(executionDynamicLibraries));
+ }
+
+ public Builder merge(CcLinkingOutputs outputs) {
+ staticLibraries.addAll(outputs.getStaticLibraries());
+ picStaticLibraries.addAll(outputs.getPicStaticLibraries());
+ dynamicLibraries.addAll(outputs.getDynamicLibraries());
+ executionDynamicLibraries.addAll(outputs.getExecutionDynamicLibraries());
+ return this;
+ }
+
+ public Builder addStaticLibrary(LibraryToLink library) {
+ staticLibraries.add(library);
+ return this;
+ }
+
+ public Builder addStaticLibraries(Iterable<LibraryToLink> libraries) {
+ Iterables.addAll(staticLibraries, libraries);
+ return this;
+ }
+
+ public Builder addPicStaticLibrary(LibraryToLink library) {
+ picStaticLibraries.add(library);
+ return this;
+ }
+
+ public Builder addPicStaticLibraries(Iterable<LibraryToLink> libraries) {
+ Iterables.addAll(picStaticLibraries, libraries);
+ return this;
+ }
+
+ public Builder addDynamicLibrary(LibraryToLink library) {
+ dynamicLibraries.add(library);
+ return this;
+ }
+
+ public Builder addDynamicLibraries(Iterable<LibraryToLink> libraries) {
+ Iterables.addAll(dynamicLibraries, libraries);
+ return this;
+ }
+
+ public Builder addExecutionDynamicLibrary(LibraryToLink library) {
+ executionDynamicLibraries.add(library);
+ return this;
+ }
+
+ public Builder addExecutionDynamicLibraries(Iterable<LibraryToLink> libraries) {
+ Iterables.addAll(executionDynamicLibraries, libraries);
+ return this;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CcNativeLibraryProvider.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcNativeLibraryProvider.java
new file mode 100644
index 0000000..5e96291
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcNativeLibraryProvider.java
@@ -0,0 +1,43 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+
+/**
+ * A target that provides native libraries in the transitive closure of its deps that are needed for
+ * executing C++ code.
+ */
+@Immutable
+public final class CcNativeLibraryProvider implements TransitiveInfoProvider {
+
+ private final NestedSet<LinkerInput> transitiveCcNativeLibraries;
+
+ public CcNativeLibraryProvider(NestedSet<LinkerInput> transitiveCcNativeLibraries) {
+ this.transitiveCcNativeLibraries = transitiveCcNativeLibraries;
+ }
+
+ /**
+ * Collects native libraries in the transitive closure of its deps that are needed for executing
+ * C/C++ code.
+ *
+ * <p>In effect, returns all dynamic library (.so) artifacts provided by the transitive closure.
+ */
+ public NestedSet<LinkerInput> getTransitiveCcNativeLibraries() {
+ return transitiveCcNativeLibraries;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CcSpecificLinkParamsProvider.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcSpecificLinkParamsProvider.java
new file mode 100644
index 0000000..dfcecc2
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcSpecificLinkParamsProvider.java
@@ -0,0 +1,48 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import com.google.common.base.Function;
+import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
+import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.rules.cpp.CcLinkParamsStore.CcLinkParamsStoreImpl;
+
+/**
+ * A target that provides libraries to be only linked into other C++ targets (and not targets
+ * for other languages)
+ */
+@Immutable
+public final class CcSpecificLinkParamsProvider implements TransitiveInfoProvider {
+ private final CcLinkParamsStoreImpl store;
+
+ public CcSpecificLinkParamsProvider(CcLinkParamsStore store) {
+ this.store = new CcLinkParamsStoreImpl(store);
+ }
+
+ public CcLinkParamsStore getLinkParams() {
+ return store;
+ }
+
+ public static final Function<TransitiveInfoCollection, CcLinkParamsStore> TO_LINK_PARAMS =
+ new Function<TransitiveInfoCollection, CcLinkParamsStore>() {
+ @Override
+ public CcLinkParamsStore apply(TransitiveInfoCollection input) {
+ CcSpecificLinkParamsProvider provider = input.getProvider(
+ CcSpecificLinkParamsProvider.class);
+ return provider == null ? null : provider.getLinkParams();
+ }
+ };
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CcTest.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcTest.java
new file mode 100644
index 0000000..7827183
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcTest.java
@@ -0,0 +1,36 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.rules.RuleConfiguredTargetFactory;
+
+/**
+ * A configured target class for cc_test rules.
+ */
+public abstract class CcTest implements RuleConfiguredTargetFactory {
+
+ private final CppSemantics semantics;
+
+ protected CcTest(CppSemantics semantics) {
+ this.semantics = semantics;
+ }
+
+ @Override
+ public ConfiguredTarget create(RuleContext context) throws InterruptedException {
+ return CcBinary.init(semantics, context, /*fake =*/ false, /*useTestOnlyFlags =*/ true);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CcToolchain.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcToolchain.java
new file mode 100644
index 0000000..bd39d0f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcToolchain.java
@@ -0,0 +1,249 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import static com.google.devtools.build.lib.packages.Type.BOOLEAN;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.Actions;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.AnalysisUtils;
+import com.google.devtools.build.lib.analysis.CompilationHelper;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.FileProvider;
+import com.google.devtools.build.lib.analysis.LicensesProvider;
+import com.google.devtools.build.lib.analysis.LicensesProvider.TargetLicense;
+import com.google.devtools.build.lib.analysis.MiddlemanProvider;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTargetBuilder;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.Runfiles;
+import com.google.devtools.build.lib.analysis.RunfilesProvider;
+import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.packages.License;
+import com.google.devtools.build.lib.rules.RuleConfiguredTargetFactory;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.List;
+
+/**
+ * Implementation for the cc_toolchain rule.
+ */
+public class CcToolchain implements RuleConfiguredTargetFactory {
+
+ @Override
+ public ConfiguredTarget create(RuleContext ruleContext) {
+ final Label label = ruleContext.getLabel();
+ final NestedSet<Artifact> crosstool = ruleContext.getPrerequisite("all_files", Mode.HOST)
+ .getProvider(FileProvider.class).getFilesToBuild();
+ final NestedSet<Artifact> crosstoolMiddleman = getFiles(ruleContext, "all_files");
+ final NestedSet<Artifact> compile = getFiles(ruleContext, "compiler_files");
+ final NestedSet<Artifact> strip = getFiles(ruleContext, "strip_files");
+ final NestedSet<Artifact> objcopy = getFiles(ruleContext, "objcopy_files");
+ final NestedSet<Artifact> link = getFiles(ruleContext, "linker_files");
+ final NestedSet<Artifact> dwp = getFiles(ruleContext, "dwp_files");
+ final NestedSet<Artifact> libcLink = inputsForLibcLink(ruleContext);
+ String purposePrefix = Actions.escapeLabel(label) + "_";
+ String runtimeSolibDirBase = "_solib_" + "_" + Actions.escapeLabel(label);
+ final PathFragment runtimeSolibDir = ruleContext.getConfiguration()
+ .getBinFragment().getRelative(runtimeSolibDirBase);
+
+ CppConfiguration cppConfiguration = ruleContext.getFragment(CppConfiguration.class);
+ // Static runtime inputs.
+ TransitiveInfoCollection staticRuntimeLibDep = selectDep(ruleContext, "static_runtime_libs",
+ cppConfiguration.getStaticRuntimeLibsLabel());
+ final NestedSet<Artifact> staticRuntimeLinkInputs;
+ final Artifact staticRuntimeLinkMiddleman;
+ if (cppConfiguration.supportsEmbeddedRuntimes()) {
+ staticRuntimeLinkInputs = staticRuntimeLibDep
+ .getProvider(FileProvider.class)
+ .getFilesToBuild();
+ } else {
+ staticRuntimeLinkInputs = NestedSetBuilder.emptySet(Order.STABLE_ORDER);
+ }
+
+ if (!staticRuntimeLinkInputs.isEmpty()) {
+ NestedSet<Artifact> staticRuntimeLinkMiddlemanSet = CompilationHelper.getAggregatingMiddleman(
+ ruleContext,
+ purposePrefix + "static_runtime_link",
+ staticRuntimeLibDep);
+ staticRuntimeLinkMiddleman = staticRuntimeLinkMiddlemanSet.isEmpty()
+ ? null : Iterables.getOnlyElement(staticRuntimeLinkMiddlemanSet);
+ } else {
+ staticRuntimeLinkMiddleman = null;
+ }
+
+ Preconditions.checkState(
+ (staticRuntimeLinkMiddleman == null) == staticRuntimeLinkInputs.isEmpty());
+
+ // Dynamic runtime inputs.
+ TransitiveInfoCollection dynamicRuntimeLibDep = selectDep(ruleContext, "dynamic_runtime_libs",
+ cppConfiguration.getDynamicRuntimeLibsLabel());
+ final NestedSet<Artifact> dynamicRuntimeLinkInputs;
+ final Artifact dynamicRuntimeLinkMiddleman;
+ if (cppConfiguration.supportsEmbeddedRuntimes()) {
+ NestedSetBuilder<Artifact> dynamicRuntimeLinkInputsBuilder = NestedSetBuilder.stableOrder();
+ for (Artifact artifact : dynamicRuntimeLibDep
+ .getProvider(FileProvider.class).getFilesToBuild()) {
+ if (CppHelper.SHARED_LIBRARY_FILETYPES.matches(artifact.getFilename())) {
+ dynamicRuntimeLinkInputsBuilder.add(SolibSymlinkAction.getCppRuntimeSymlink(
+ ruleContext, artifact, runtimeSolibDirBase,
+ ruleContext.getConfiguration()).getArtifact());
+ } else {
+ dynamicRuntimeLinkInputsBuilder.add(artifact);
+ }
+ }
+ dynamicRuntimeLinkInputs = dynamicRuntimeLinkInputsBuilder.build();
+ } else {
+ dynamicRuntimeLinkInputs = NestedSetBuilder.emptySet(Order.STABLE_ORDER);
+ }
+
+ if (!dynamicRuntimeLinkInputs.isEmpty()) {
+ List<Artifact> dynamicRuntimeLinkMiddlemanSet =
+ CppHelper.getAggregatingMiddlemanForCppRuntimes(
+ ruleContext,
+ purposePrefix + "dynamic_runtime_link",
+ dynamicRuntimeLibDep,
+ runtimeSolibDirBase,
+ ruleContext.getConfiguration());
+ dynamicRuntimeLinkMiddleman = dynamicRuntimeLinkMiddlemanSet.isEmpty()
+ ? null : Iterables.getOnlyElement(dynamicRuntimeLinkMiddlemanSet);
+ } else {
+ dynamicRuntimeLinkMiddleman = null;
+ }
+
+ Preconditions.checkState(
+ (dynamicRuntimeLinkMiddleman == null) == dynamicRuntimeLinkInputs.isEmpty());
+
+ CppCompilationContext.Builder contextBuilder =
+ new CppCompilationContext.Builder(ruleContext);
+ CppModuleMap moduleMap = createCrosstoolModuleMap(ruleContext);
+ if (moduleMap != null) {
+ contextBuilder.setCppModuleMap(moduleMap);
+ }
+ final CppCompilationContext context = contextBuilder.build();
+ boolean supportsParamFiles = ruleContext.attributes().get("supports_param_files", BOOLEAN);
+ boolean supportsHeaderParsing =
+ ruleContext.attributes().get("supports_header_parsing", BOOLEAN);
+
+ CcToolchainProvider provider = new CcToolchainProvider(
+ Preconditions.checkNotNull(ruleContext.getFragment(CppConfiguration.class)),
+ crosstool,
+ fullInputsForCrosstool(ruleContext, crosstoolMiddleman),
+ compile,
+ strip,
+ objcopy,
+ fullInputsForLink(ruleContext, link),
+ dwp,
+ libcLink,
+ staticRuntimeLinkInputs,
+ staticRuntimeLinkMiddleman,
+ dynamicRuntimeLinkInputs,
+ dynamicRuntimeLinkMiddleman,
+ runtimeSolibDir,
+ context,
+ supportsParamFiles,
+ supportsHeaderParsing);
+ RuleConfiguredTargetBuilder builder =
+ new RuleConfiguredTargetBuilder(ruleContext)
+ .add(CcToolchainProvider.class, provider)
+ .setFilesToBuild(new NestedSetBuilder<Artifact>(Order.STABLE_ORDER).build())
+ .add(RunfilesProvider.class, RunfilesProvider.simple(Runfiles.EMPTY));
+
+ // If output_license is specified on the cc_toolchain rule, override the transitive licenses
+ // with that one. This is necessary because cc_toolchain is used in the target configuration,
+ // but it is sort-of-kind-of a tool, but various parts of it are linked into the output...
+ // ...so we trust the judgment of the author of the cc_toolchain rule to figure out what
+ // licenses should be propagated to C++ targets.
+ License outputLicense = ruleContext.getRule().getToolOutputLicense(ruleContext.attributes());
+ if (outputLicense != null && outputLicense != License.NO_LICENSE) {
+ final NestedSet<TargetLicense> license = NestedSetBuilder.create(Order.STABLE_ORDER,
+ new TargetLicense(ruleContext.getLabel(), outputLicense));
+ LicensesProvider licensesProvider = new LicensesProvider() {
+ @Override
+ public NestedSet<TargetLicense> getTransitiveLicenses() {
+ return license;
+ }
+ };
+
+ builder.add(LicensesProvider.class, licensesProvider);
+ }
+
+ return builder.build();
+ }
+
+ private NestedSet<Artifact> inputsForLibcLink(RuleContext ruleContext) {
+ TransitiveInfoCollection libcLink = ruleContext.getPrerequisite(":libc_link", Mode.HOST);
+ return libcLink != null
+ ? libcLink.getProvider(FileProvider.class).getFilesToBuild()
+ : NestedSetBuilder.<Artifact>emptySet(Order.STABLE_ORDER);
+ }
+
+ private NestedSet<Artifact> fullInputsForCrosstool(RuleContext ruleContext,
+ NestedSet<Artifact> crosstoolMiddleman) {
+ return NestedSetBuilder.<Artifact>stableOrder()
+ .addTransitive(crosstoolMiddleman)
+ // Use "libc_link" here, because it is functionally identical to the case
+ // below. If we introduce separate filegroups for compiling and linking, we
+ // need to fix that here.
+ .addTransitive(AnalysisUtils.getMiddlemanFor(ruleContext, ":libc_link"))
+ .build();
+ }
+
+ private NestedSet<Artifact> fullInputsForLink(RuleContext ruleContext, NestedSet<Artifact> link) {
+ return NestedSetBuilder.<Artifact>stableOrder()
+ .addTransitive(link)
+ .addTransitive(AnalysisUtils.getMiddlemanFor(ruleContext, ":libc_link"))
+ .add(ruleContext.getAnalysisEnvironment().getEmbeddedToolArtifact(
+ CppRuleClasses.BUILD_INTERFACE_SO))
+ .build();
+ }
+
+ private CppModuleMap createCrosstoolModuleMap(RuleContext ruleContext) {
+ if (ruleContext.getPrerequisite("module_map", Mode.HOST) == null) {
+ return null;
+ }
+ Artifact moduleMapArtifact = ruleContext.getPrerequisiteArtifact("module_map", Mode.HOST);
+ if (moduleMapArtifact == null) {
+ return null;
+ }
+ return new CppModuleMap(moduleMapArtifact, "crosstool");
+ }
+
+ private TransitiveInfoCollection selectDep(
+ RuleContext ruleContext, String attribute, Label label) {
+ for (TransitiveInfoCollection dep : ruleContext.getPrerequisites(attribute, Mode.TARGET)) {
+ if (dep.getLabel().equals(label)) {
+ return dep;
+ }
+ }
+
+ return ruleContext.getPrerequisites(attribute, Mode.TARGET).get(0);
+ }
+
+ private NestedSet<Artifact> getFiles(RuleContext context, String attribute) {
+ TransitiveInfoCollection dep = context.getPrerequisite(attribute, Mode.HOST);
+ MiddlemanProvider middlemanProvider = dep.getProvider(MiddlemanProvider.class);
+ // We use the middleman if we can (if the dep is a filegroup), otherwise, just the regular
+ // filesToBuild (e.g. if it is a simple input file)
+ return middlemanProvider != null
+ ? middlemanProvider.getMiddlemanArtifact()
+ : dep.getProvider(FileProvider.class).getFilesToBuild();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CcToolchainFeatures.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcToolchainFeatures.java
new file mode 100644
index 0000000..29ab45c
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcToolchainFeatures.java
@@ -0,0 +1,802 @@
+// Copyright 2015 Google Inc. 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.rules.cpp;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Multimap;
+import com.google.devtools.build.lib.analysis.config.InvalidConfigurationException;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.view.config.crosstool.CrosstoolConfig.CToolchain;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.Serializable;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Queue;
+import java.util.Set;
+
+/**
+ * Provides access to features supported by a specific toolchain.
+ *
+ * <p>This class can be generated from the CToolchain protocol buffer.
+ *
+ * <p>TODO(bazel-team): Implement support for specifying the toolchain configuration directly from
+ * the BUILD file.
+ *
+ * <p>TODO(bazel-team): Find a place to put the public-facing documentation and link to it from
+ * here.
+ *
+ * <p>TODO(bazel-team): Split out Feature as CcToolchainFeature, which will modularize the
+ * crosstool configuration into one part that is about handling a set of features (including feature
+ * selection) and one part that is about how to apply a single feature (parsing flags and expanding
+ * them from build variables).
+ */
+@Immutable
+public class CcToolchainFeatures implements Serializable {
+
+ /**
+ * Thrown when a flag value cannot be expanded under a set of build variables.
+ *
+ * <p>This happens for example when a flag references a variable that is not provided by the
+ * action, or when a flag group references multiple variables of sequence type.
+ */
+ public static class ExpansionException extends RuntimeException {
+ ExpansionException(String message) {
+ super(message);
+ }
+ }
+
+ /**
+ * A piece of a single flag.
+ *
+ * <p>A single flag can contain a combination of text and variables (for example
+ * "-f %{var1}/%{var2}"). We split the flag into chunks, where each chunk represents either a
+ * text snippet, or a variable that is to be replaced.
+ */
+ interface FlagChunk {
+
+ /**
+ * Expands this chunk.
+ *
+ * @param variables variable names mapped to their values for a single flag expansion.
+ * @param flag the flag content to append to.
+ */
+ void expand(Map<String, String> variables, StringBuilder flag);
+ }
+
+ /**
+ * A plain text chunk of a flag.
+ */
+ @Immutable
+ private static class StringChunk implements FlagChunk, Serializable {
+ private final String text;
+
+ private StringChunk(String text) {
+ this.text = text;
+ }
+
+ @Override
+ public void expand(Map<String, String> variables, StringBuilder flag) {
+ flag.append(text);
+ }
+ }
+
+ /**
+ * A chunk of a flag into which a variable should be expanded.
+ */
+ @Immutable
+ private static class VariableChunk implements FlagChunk, Serializable {
+ private final String variableName;
+
+ private VariableChunk(String variableName) {
+ this.variableName = variableName;
+ }
+
+ @Override
+ public void expand(Map<String, String> variables, StringBuilder flag) {
+ String value = variables.get(variableName);
+ if (value == null) {
+ // We check all variables in FlagGroup.expandCommandLine, so if we arrive here with a
+ // null value, the variable map originally handed to the feature selection must have
+ // contained an explicit null value.
+ throw new ExpansionException("Internal blaze error: build variable was set to 'null'.");
+ }
+ flag.append(variables.get(variableName));
+ }
+ }
+
+ /**
+ * Parser for toolchain flags.
+ *
+ * <p>A flag contains a snippet of text supporting variable expansion. For example, a flag value
+ * "-f %{var1}/%{var2}" will expand the values of the variables "var1" and "var2" in the
+ * corresponding places in the string.
+ *
+ * <p>The {@code FlagParser} takes a flag string and parses it into a list of {@code FlagChunk}
+ * objects, where each chunk represents either a snippet of text or a variable to be expanded. In
+ * the above example, the resulting chunks would be ["-f ", var1, "/", var2].
+ *
+ * <p>In addition to the list of chunks, the {@code FlagParser} also provides the set of variables
+ * necessary for the expansion of this flag via {@code getUsedVariables}.
+ *
+ * <p>To get a literal percent character, "%%" can be used in the flag text.
+ */
+ private static class FlagParser {
+
+ /**
+ * The given flag value.
+ */
+ private final String value;
+
+ /**
+ * The current position in {@value} during parsing.
+ */
+ private int current = 0;
+
+ private final ImmutableList.Builder<FlagChunk> chunks = ImmutableList.builder();
+ private final ImmutableSet.Builder<String> usedVariables = ImmutableSet.builder();
+
+ private FlagParser(String value) throws InvalidConfigurationException {
+ this.value = value;
+ parse();
+ }
+
+ /**
+ * @return the parsed chunks for this flag.
+ */
+ private ImmutableList<FlagChunk> getChunks() {
+ return chunks.build();
+ }
+
+ /**
+ * @return all variable names needed to expand this flag.
+ */
+ private ImmutableSet<String> getUsedVariables() {
+ return usedVariables.build();
+ }
+
+ /**
+ * Parses the flag.
+ *
+ * @throws InvalidConfigurationException if there is a parsing error.
+ */
+ private void parse() throws InvalidConfigurationException {
+ while (current < value.length()) {
+ if (atVariableStart()) {
+ parseVariableChunk();
+ } else {
+ parseStringChunk();
+ }
+ }
+ }
+
+ /**
+ * @return whether the current position is the start of a variable.
+ */
+ private boolean atVariableStart() {
+ // We parse a variable when value starts with '%', but not '%%'.
+ return value.charAt(current) == '%'
+ && (current + 1 >= value.length() || value.charAt(current + 1) != '%');
+ }
+
+ /**
+ * Parses a chunk of text until the next '%', which indicates either an escaped literal '%'
+ * or a variable.
+ */
+ private void parseStringChunk() {
+ int start = current;
+ // We only parse string chunks starting with '%' if they also start with '%%'.
+ // In that case, we want to have a single '%' in the string, so we start at the second
+ // character.
+ // Note that for flags like "abc%%def" this will lead to two string chunks, the first
+ // referencing the subtring "abc", and a second referencing the substring "%def".
+ if (value.charAt(current) == '%') {
+ current = current + 1;
+ start = current;
+ }
+ current = value.indexOf('%', current + 1);
+ if (current == -1) {
+ current = value.length();
+ }
+ final String text = value.substring(start, current);
+ chunks.add(new StringChunk(text));
+ }
+
+ /**
+ * Parses a variable to be expanded.
+ *
+ * @throws InvalidConfigurationException if there is a parsing error.
+ */
+ private void parseVariableChunk() throws InvalidConfigurationException {
+ current = current + 1;
+ if (current >= value.length() || value.charAt(current) != '{') {
+ abort("expected '{'");
+ }
+ current = current + 1;
+ if (current >= value.length() || value.charAt(current) == '}') {
+ abort("expected variable name");
+ }
+ int end = value.indexOf('}', current);
+ final String name = value.substring(current, end);
+ usedVariables.add(name);
+ chunks.add(new VariableChunk(name));
+ current = end + 1;
+ }
+
+ /**
+ * @throws InvalidConfigurationException with the given error text, adding information about
+ * the current position in the flag.
+ */
+ private void abort(String error) throws InvalidConfigurationException {
+ throw new InvalidConfigurationException("Invalid toolchain configuration: " + error
+ + " at position " + current + " while parsing a flag containing '" + value + "'");
+ }
+ }
+
+ /**
+ * A single flag to be expanded under a set of variables.
+ *
+ * <p>TODO(bazel-team): Consider specializing Flag for the simple case that a flag is just a bit
+ * of text.
+ */
+ @Immutable
+ private static class Flag implements Serializable {
+ private final ImmutableList<FlagChunk> chunks;
+
+ private Flag(ImmutableList<FlagChunk> chunks) {
+ this.chunks = chunks;
+ }
+
+ /**
+ * Expand this flag into a single new entry in {@code commandLine}.
+ */
+ private void expandCommandLine(Map<String, String> variables, List<String> commandLine) {
+ StringBuilder flag = new StringBuilder();
+ for (FlagChunk chunk : chunks) {
+ chunk.expand(variables, flag);
+ }
+ commandLine.add(flag.toString());
+ }
+ }
+
+ /**
+ * A group of flags.
+ */
+ @Immutable
+ private static class FlagGroup implements Serializable {
+ private final ImmutableList<Flag> flags;
+ private final ImmutableSet<String> usedVariables;
+
+ private FlagGroup(CToolchain.FlagGroup flagGroup) throws InvalidConfigurationException {
+ ImmutableList.Builder<Flag> flags = ImmutableList.builder();
+ ImmutableSet.Builder<String> usedVariables = ImmutableSet.builder();
+ for (String flag : flagGroup.getFlagList()) {
+ FlagParser parser = new FlagParser(flag);
+ flags.add(new Flag(parser.getChunks()));
+ usedVariables.addAll(parser.getUsedVariables());
+ }
+ this.flags = flags.build();
+ this.usedVariables = usedVariables.build();
+ }
+
+ /**
+ * Expands all flags in this group and adds them to {@code commandLine}.
+ *
+ * <p>The flags of the group will be expanded either:
+ * <ul>
+ * <li>once, if there is no variable of sequence type in any of the group's flags, or</li>
+ * <li>for each element in the sequence, if there is one variable of sequence type within
+ * the flags.</li>
+ * </ul>
+ *
+ * <p>Having more than a single variable of sequence type in a single flag group is not
+ * supported.
+ */
+ private void expandCommandLine(Multimap<String, String> variables, List<String> commandLine) {
+ Map<String, String> variableView = new HashMap<>();
+ String sequenceName = null;
+ for (String name : usedVariables) {
+ Collection<String> value = variables.get(name);
+ if (value.isEmpty()) {
+ throw new ExpansionException("Invalid toolchain configuration: unknown variable '" + name
+ + "' can not be expanded.");
+ } else if (value.size() > 1) {
+ if (sequenceName != null) {
+ throw new ExpansionException(
+ "Invalid toolchain configuration: trying to expand two variable list in one "
+ + "flag group: '" + sequenceName + "' and '" + name + "'");
+ }
+ sequenceName = name;
+ } else {
+ variableView.put(name, value.iterator().next());
+ }
+ }
+ if (sequenceName != null) {
+ for (String value : variables.get(sequenceName)) {
+ variableView.put(sequenceName, value);
+ expandOnce(variableView, commandLine);
+ }
+ } else {
+ expandOnce(variableView, commandLine);
+ }
+ }
+
+ /**
+ * Expanding all flags of this group into {@code commandLine}.
+ */
+ private void expandOnce(Map<String, String> variables, List<String> commandLine) {
+ for (Flag flag : flags) {
+ flag.expandCommandLine(variables, commandLine);
+ }
+ }
+ }
+
+ /**
+ * Groups a set of flags to apply for certain actions.
+ */
+ @Immutable
+ private static class FlagSet implements Serializable {
+ private final ImmutableSet<String> actions;
+ private final ImmutableList<FlagGroup> flagGroups;
+
+ private FlagSet(CToolchain.FlagSet flagSet) throws InvalidConfigurationException {
+ this.actions = ImmutableSet.copyOf(flagSet.getActionList());
+ ImmutableList.Builder<FlagGroup> builder = ImmutableList.builder();
+ for (CToolchain.FlagGroup flagGroup : flagSet.getFlagGroupList()) {
+ builder.add(new FlagGroup(flagGroup));
+ }
+ this.flagGroups = builder.build();
+ }
+
+ /**
+ * Adds the flags that apply to the given {@code action} to {@code commandLine}.
+ */
+ private void expandCommandLine(String action, Multimap<String, String> variables,
+ List<String> commandLine) {
+ if (!actions.contains(action)) {
+ return;
+ }
+ for (FlagGroup flagGroup : flagGroups) {
+ flagGroup.expandCommandLine(variables, commandLine);
+ }
+ }
+ }
+
+ /**
+ * Contains flags for a specific feature.
+ */
+ @Immutable
+ private static class Feature implements Serializable {
+ private final String name;
+ private final ImmutableList<FlagSet> flagSets;
+
+ private Feature(CToolchain.Feature feature) throws InvalidConfigurationException {
+ this.name = feature.getName();
+ ImmutableList.Builder<FlagSet> builder = ImmutableList.builder();
+ for (CToolchain.FlagSet flagSet : feature.getFlagSetList()) {
+ builder.add(new FlagSet(flagSet));
+ }
+ this.flagSets = builder.build();
+ }
+
+ /**
+ * @return the features's name.
+ */
+ private String getName() {
+ return name;
+ }
+
+ /**
+ * Adds the flags that apply to the given {@code action} to {@code commandLine}.
+ */
+ private void expandCommandLine(String action, Multimap<String, String> variables,
+ List<String> commandLine) {
+ for (FlagSet flagSet : flagSets) {
+ flagSet.expandCommandLine(action, variables, commandLine);
+ }
+ }
+ }
+
+ /**
+ * Captures the set of enabled features for a rule.
+ */
+ @Immutable
+ public static class FeatureConfiguration {
+ private final ImmutableSet<String> enabledFeatureNames;
+ private final ImmutableList<Feature> enabledFeatures;
+
+ public FeatureConfiguration() {
+ enabledFeatureNames = ImmutableSet.of();
+ enabledFeatures = ImmutableList.of();
+ }
+
+ private FeatureConfiguration(ImmutableList<Feature> enabledFeatures) {
+ this.enabledFeatures = enabledFeatures;
+ ImmutableSet.Builder<String> builder = ImmutableSet.builder();
+ for (Feature feature : enabledFeatures) {
+ builder.add(feature.getName());
+ }
+ this.enabledFeatureNames = builder.build();
+ }
+
+ /**
+ * @return whether the given {@code feature} is enabled.
+ */
+ boolean isEnabled(String feature) {
+ return enabledFeatureNames.contains(feature);
+ }
+
+ /**
+ * @return the command line for the given {@code action}.
+ */
+ List<String> getCommandLine(String action, Multimap<String, String> variables) {
+ List<String> commandLine = new ArrayList<>();
+ for (Feature feature : enabledFeatures) {
+ feature.expandCommandLine(action, variables, commandLine);
+ }
+ return commandLine;
+ }
+ }
+
+ /**
+ * All features in the order in which they were specified in the configuration.
+ *
+ * <p>We guarantee the command line to be in the order in which the flags were specified in the
+ * configuration.
+ */
+ private final ImmutableList<Feature> features;
+
+ /**
+ * Maps from the feature's name to the feature.
+ */
+ private final ImmutableMap<String, Feature> featuresByName;
+
+ /**
+ * Maps from a feature to a set of all the features it has a direct 'implies' edge to.
+ */
+ private final ImmutableMultimap<Feature, Feature> implies;
+
+ /**
+ * Maps from a feature to all features that have an direct 'implies' edge to this feature.
+ */
+ private final ImmutableMultimap<Feature, Feature> impliedBy;
+
+ /**
+ * Maps from a feature to a set of feature sets, where:
+ * <ul>
+ * <li>a feature set satisfies the 'requires' condition, if all features in the feature set are
+ * enabled</li>
+ * <li>the 'requires' condition is satisfied, if at least one of the feature sets satisfies the
+ * 'requires' condition.</li>
+ * </ul>
+ */
+ private final ImmutableMultimap<Feature, ImmutableSet<Feature>> requires;
+
+ /**
+ * Maps from a feature to all features that have a requirement referencing it.
+ *
+ * <p>This will be used to determine which features need to be re-checked after a feature was
+ * disabled.
+ */
+ private final ImmutableMultimap<Feature, Feature> requiredBy;
+
+ /**
+ * A cache of feature selection results, so we do not recalculate the feature selection for
+ * all actions.
+ */
+ private transient LoadingCache<Collection<String>, FeatureConfiguration>
+ configurationCache = buildConfigurationCache();
+
+ /**
+ * Constructs the feature configuration from a {@code CToolchain} protocol buffer.
+ *
+ * @param toolchain the toolchain configuration as specified by the user.
+ * @throws InvalidConfigurationException if the configuration has logical errors.
+ */
+ CcToolchainFeatures(CToolchain toolchain) throws InvalidConfigurationException {
+ // Build up the feature graph.
+ // First, we build up the map of name -> features in one pass, so that earlier features can
+ // reference later features in their configuration.
+ ImmutableList.Builder<Feature> features = ImmutableList.builder();
+ HashMap<String, Feature> featuresByName = new HashMap<>();
+ for (CToolchain.Feature toolchainFeature : toolchain.getFeatureList()) {
+ Feature feature = new Feature(toolchainFeature);
+ features.add(feature);
+ if (featuresByName.put(feature.getName(), feature) != null) {
+ throw new InvalidConfigurationException("Invalid toolchain configuration: feature '"
+ + feature.getName() + "' was specified multiple times.");
+ }
+ }
+ this.features = features.build();
+ this.featuresByName = ImmutableMap.copyOf(featuresByName);
+
+ // Next, we build up all forward references for 'implies' and 'requires' edges.
+ ImmutableMultimap.Builder<Feature, Feature> implies = ImmutableMultimap.builder();
+ ImmutableMultimap.Builder<Feature, ImmutableSet<Feature>> requires =
+ ImmutableMultimap.builder();
+ // We also store the reverse 'implied by' and 'required by' edges during this pass.
+ ImmutableMultimap.Builder<Feature, Feature> impliedBy = ImmutableMultimap.builder();
+ ImmutableMultimap.Builder<Feature, Feature> requiredBy = ImmutableMultimap.builder();
+ for (CToolchain.Feature toolchainFeature : toolchain.getFeatureList()) {
+ String name = toolchainFeature.getName();
+ Feature feature = featuresByName.get(name);
+ for (CToolchain.FeatureSet requiredFeatures : toolchainFeature.getRequiresList()) {
+ ImmutableSet.Builder<Feature> allOf = ImmutableSet.builder();
+ for (String requiredName : requiredFeatures.getFeatureList()) {
+ Feature required = getFeatureOrFail(requiredName, name);
+ allOf.add(required);
+ requiredBy.put(required, feature);
+ }
+ requires.put(feature, allOf.build());
+ }
+ for (String impliedName : toolchainFeature.getImpliesList()) {
+ Feature implied = getFeatureOrFail(impliedName, name);
+ impliedBy.put(implied, feature);
+ implies.put(feature, implied);
+ }
+ }
+ this.implies = implies.build();
+ this.requires = requires.build();
+ this.impliedBy = impliedBy.build();
+ this.requiredBy = requiredBy.build();
+ }
+
+ /**
+ * Assign an empty cache after default-deserializing all non-transient members.
+ */
+ private void readObject(ObjectInputStream in) throws ClassNotFoundException, IOException {
+ in.defaultReadObject();
+ this.configurationCache = buildConfigurationCache();
+ }
+
+ /**
+ * @return an empty {@code FeatureConfiguration} cache.
+ */
+ private LoadingCache<Collection<String>, FeatureConfiguration> buildConfigurationCache() {
+ return CacheBuilder.newBuilder()
+ // TODO(klimek): Benchmark and tweak once we support a larger configuration.
+ .maximumSize(10000)
+ .build(new CacheLoader<Collection<String>, FeatureConfiguration>() {
+ @Override
+ public FeatureConfiguration load(Collection<String> requestedFeatures) {
+ return computeFeatureConfiguration(requestedFeatures);
+ }
+ });
+ }
+
+ /**
+ * Given a list of {@code requestedFeatures}, returns all features that are enabled by the
+ * toolchain configuration.
+ *
+ * <p>A requested feature will not be enabled if the toolchain does not support it (which may
+ * depend on other requested features).
+ *
+ * <p>Additional features will be enabled if the toolchain supports them and they are implied by
+ * requested features.
+ */
+ FeatureConfiguration getFeatureConfiguration(Collection<String> requestedFeatures) {
+ return configurationCache.getUnchecked(requestedFeatures);
+ }
+
+ private FeatureConfiguration computeFeatureConfiguration(Collection<String> requestedFeatures) {
+ // Command line flags will be output in the order in which they are specified in the toolchain
+ // configuration.
+ return new FeatureSelection(requestedFeatures).run();
+ }
+
+ /**
+ * Convenience method taking a variadic string argument list for testing.
+ */
+ FeatureConfiguration getFeatureConfiguration(String... requestedFeatures) {
+ return getFeatureConfiguration(Arrays.asList(requestedFeatures));
+ }
+
+ /**
+ * @return the feature with the given {@code name}.
+ *
+ * @throws InvalidConfigurationException if no feature with the given name was configured.
+ */
+ private Feature getFeatureOrFail(String name, String reference)
+ throws InvalidConfigurationException {
+ if (!featuresByName.containsKey(name)) {
+ throw new InvalidConfigurationException("Invalid toolchain configuration: feature '" + name
+ + "', which is referenced from feature '" + reference + "', is not defined.");
+ }
+ return featuresByName.get(name);
+ }
+
+ @VisibleForTesting
+ Collection<String> getFeatureNames() {
+ Collection<String> featureNames = new HashSet<>();
+ for (Feature feature : features) {
+ featureNames.add(feature.getName());
+ }
+ return featureNames;
+ }
+
+ /**
+ * Implements the feature selection algorithm.
+ *
+ * <p>Feature selection is done by first enabling all features reachable by an 'implies' edge,
+ * and then iteratively pruning features that have unmet requirements.
+ */
+ private class FeatureSelection {
+
+ /**
+ * The features Bazel would like to enable; either because they are supported and generally
+ * useful, or because the user required them (for example through the command line).
+ */
+ private final ImmutableSet<Feature> requestedFeatures;
+
+ /**
+ * The currently enabled feature; during feature selection, we first put all features reachable
+ * via an 'implies' edge into the enabled feature set, and than prune that set from features
+ * that have unmet requirements.
+ */
+ private Set<Feature> enabled = new HashSet<>();
+
+ private FeatureSelection(Collection<String> requestedFeatures) {
+ ImmutableSet.Builder<Feature> builder = ImmutableSet.builder();
+ for (String name : requestedFeatures) {
+ if (featuresByName.containsKey(name)) {
+ builder.add(featuresByName.get(name));
+ }
+ }
+ this.requestedFeatures = builder.build();
+ }
+
+ /**
+ * @return all enabled features in the order in which they were specified in the configuration.
+ */
+ private FeatureConfiguration run() {
+ for (Feature feature : requestedFeatures) {
+ enableAllImpliedBy(feature);
+ }
+ disableUnsupportedFeatures();
+ ImmutableList.Builder<Feature> enabledFeaturesInOrder = ImmutableList.builder();
+ for (Feature feature : features) {
+ if (enabled.contains(feature)) {
+ enabledFeaturesInOrder.add(feature);
+ }
+ }
+ return new FeatureConfiguration(enabledFeaturesInOrder.build());
+ }
+
+ /**
+ * Transitively and unconditionally enable all features implied by the given feature and the
+ * feature itself to the enabled feature set.
+ */
+ private void enableAllImpliedBy(Feature feature) {
+ if (enabled.contains(feature)) {
+ return;
+ }
+ enabled.add(feature);
+ for (Feature implied : implies.get(feature)) {
+ enableAllImpliedBy(implied);
+ }
+ }
+
+ /**
+ * Remove all unsupported features from the enabled feature set.
+ */
+ private void disableUnsupportedFeatures() {
+ Queue<Feature> check = new ArrayDeque<>(enabled);
+ while (!check.isEmpty()) {
+ checkFeature(check.poll());
+ }
+ }
+
+ /**
+ * Check if the given feature is still satisfied within the set of currently enabled features.
+ *
+ * <p>If it is not, remove the feature from the set of enabled features, and re-check all
+ * features that may now also become disabled.
+ */
+ private void checkFeature(Feature feature) {
+ if (!enabled.contains(feature) || isSatisfied(feature)) {
+ return;
+ }
+ enabled.remove(feature);
+
+ // Once we disable a feature, we have to re-check all features that can be affected by
+ // that removal.
+ // 1. A feature that implied the current feature is now going to be disabled.
+ for (Feature impliesCurrent : impliedBy.get(feature)) {
+ checkFeature(impliesCurrent);
+ }
+ // 2. A feature that required the current feature may now be disabled, depending on whether
+ // the requirement was optional.
+ for (Feature requiresCurrent : requiredBy.get(feature)) {
+ checkFeature(requiresCurrent);
+ }
+ // 3. A feature that this feature implied may now be disabled if no other feature also implies
+ // it.
+ for (Feature implied : implies.get(feature)) {
+ checkFeature(implied);
+ }
+ }
+
+ /**
+ * @return whether all requirements of the feature are met in the set of currently enabled
+ * features.
+ */
+ private boolean isSatisfied(Feature feature) {
+ return (requestedFeatures.contains(feature) || isImpliedByEnabledFeature(feature))
+ && allImplicationsEnabled(feature) && allRequirementsMet(feature);
+ }
+
+ /**
+ * @return whether a currently enabled feature implies the given feature.
+ */
+ private boolean isImpliedByEnabledFeature(Feature feature) {
+ for (Feature implies : impliedBy.get(feature)) {
+ if (enabled.contains(implies)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * @return whether all implications of the given feature are enabled.
+ */
+ private boolean allImplicationsEnabled(Feature feature) {
+ for (Feature implied : implies.get(feature)) {
+ if (!enabled.contains(implied)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * @return whether all requirements are enabled.
+ *
+ * <p>This implies that for any of the feature sets all of the specified features are enabled.
+ */
+ private boolean allRequirementsMet(Feature feature) {
+ if (!requires.containsKey(feature)) {
+ return true;
+ }
+ for (ImmutableSet<Feature> requiresAllOf : requires.get(feature)) {
+ boolean requirementMet = true;
+ for (Feature required : requiresAllOf) {
+ if (!enabled.contains(required)) {
+ requirementMet = false;
+ break;
+ }
+ }
+ if (requirementMet) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CcToolchainProvider.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcToolchainProvider.java
new file mode 100644
index 0000000..e1940a5
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcToolchainProvider.java
@@ -0,0 +1,226 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import javax.annotation.Nullable;
+
+/**
+ * Information about a C++ compiler used by the <code>cc_*</code> rules.
+ */
+@Immutable
+public final class CcToolchainProvider implements TransitiveInfoProvider {
+ /**
+ * An empty toolchain to be returned in the error case (instead of null).
+ */
+ public static final CcToolchainProvider EMPTY_TOOLCHAIN_IS_ERROR = new CcToolchainProvider(
+ null,
+ NestedSetBuilder.<Artifact>emptySet(Order.STABLE_ORDER),
+ NestedSetBuilder.<Artifact>emptySet(Order.STABLE_ORDER),
+ NestedSetBuilder.<Artifact>emptySet(Order.STABLE_ORDER),
+ NestedSetBuilder.<Artifact>emptySet(Order.STABLE_ORDER),
+ NestedSetBuilder.<Artifact>emptySet(Order.STABLE_ORDER),
+ NestedSetBuilder.<Artifact>emptySet(Order.STABLE_ORDER),
+ NestedSetBuilder.<Artifact>emptySet(Order.STABLE_ORDER),
+ NestedSetBuilder.<Artifact>emptySet(Order.STABLE_ORDER),
+ NestedSetBuilder.<Artifact>emptySet(Order.STABLE_ORDER),
+ null,
+ NestedSetBuilder.<Artifact>emptySet(Order.STABLE_ORDER),
+ null,
+ PathFragment.EMPTY_FRAGMENT,
+ CppCompilationContext.EMPTY,
+ false,
+ false);
+
+ @Nullable private final CppConfiguration cppConfiguration;
+ private final NestedSet<Artifact> crosstool;
+ private final NestedSet<Artifact> crosstoolMiddleman;
+ private final NestedSet<Artifact> compile;
+ private final NestedSet<Artifact> strip;
+ private final NestedSet<Artifact> objCopy;
+ private final NestedSet<Artifact> link;
+ private final NestedSet<Artifact> dwp;
+ private final NestedSet<Artifact> libcLink;
+ private final NestedSet<Artifact> staticRuntimeLinkInputs;
+ @Nullable private final Artifact staticRuntimeLinkMiddleman;
+ private final NestedSet<Artifact> dynamicRuntimeLinkInputs;
+ @Nullable private final Artifact dynamicRuntimeLinkMiddleman;
+ private final PathFragment dynamicRuntimeSolibDir;
+ private final CppCompilationContext cppCompilationContext;
+ private final boolean supportsParamFiles;
+ private final boolean supportsHeaderParsing;
+
+ public CcToolchainProvider(
+ @Nullable CppConfiguration cppConfiguration,
+ NestedSet<Artifact> crosstool,
+ NestedSet<Artifact> crosstoolMiddleman,
+ NestedSet<Artifact> compile,
+ NestedSet<Artifact> strip,
+ NestedSet<Artifact> objCopy,
+ NestedSet<Artifact> link,
+ NestedSet<Artifact> dwp,
+ NestedSet<Artifact> libcLink,
+ NestedSet<Artifact> staticRuntimeLinkInputs,
+ @Nullable Artifact staticRuntimeLinkMiddleman,
+ NestedSet<Artifact> dynamicRuntimeLinkInputs,
+ @Nullable Artifact dynamicRuntimeLinkMiddleman,
+ PathFragment dynamicRuntimeSolibDir,
+ CppCompilationContext cppCompilationContext,
+ boolean supportsParamFiles,
+ boolean supportsHeaderParsing) {
+ this.cppConfiguration = cppConfiguration;
+ this.crosstool = Preconditions.checkNotNull(crosstool);
+ this.crosstoolMiddleman = Preconditions.checkNotNull(crosstoolMiddleman);
+ this.compile = Preconditions.checkNotNull(compile);
+ this.strip = Preconditions.checkNotNull(strip);
+ this.objCopy = Preconditions.checkNotNull(objCopy);
+ this.link = Preconditions.checkNotNull(link);
+ this.dwp = Preconditions.checkNotNull(dwp);
+ this.libcLink = Preconditions.checkNotNull(libcLink);
+ this.staticRuntimeLinkInputs = Preconditions.checkNotNull(staticRuntimeLinkInputs);
+ this.staticRuntimeLinkMiddleman = staticRuntimeLinkMiddleman;
+ this.dynamicRuntimeLinkInputs = Preconditions.checkNotNull(dynamicRuntimeLinkInputs);
+ this.dynamicRuntimeLinkMiddleman = dynamicRuntimeLinkMiddleman;
+ this.dynamicRuntimeSolibDir = Preconditions.checkNotNull(dynamicRuntimeSolibDir);
+ this.cppCompilationContext = Preconditions.checkNotNull(cppCompilationContext);
+ this.supportsParamFiles = supportsParamFiles;
+ this.supportsHeaderParsing = supportsHeaderParsing;
+ }
+
+ /**
+ * Returns all the files in Crosstool. Is not a middleman.
+ */
+ public NestedSet<Artifact> getCrosstool() {
+ return crosstool;
+ }
+
+ /**
+ * Returns a middleman for all the files in Crosstool.
+ */
+ public NestedSet<Artifact> getCrosstoolMiddleman() {
+ return crosstoolMiddleman;
+ }
+
+ /**
+ * Returns the files necessary for compilation.
+ */
+ public NestedSet<Artifact> getCompile() {
+ // If include scanning is disabled, we need the entire crosstool filegroup, including header
+ // files. If it is enabled, we use the filegroup without header files - they are found by
+ // include scanning. For go, we also don't need the header files.
+ return cppConfiguration != null && cppConfiguration.shouldScanIncludes() ? compile : crosstool;
+ }
+
+ /**
+ * Returns the files necessary for a 'strip' invocation.
+ */
+ public NestedSet<Artifact> getStrip() {
+ return strip;
+ }
+
+ /**
+ * Returns the files necessary for an 'objcopy' invocation.
+ */
+ public NestedSet<Artifact> getObjcopy() {
+ return objCopy;
+ }
+
+ /**
+ * Returns the files necessary for linking, including the files needed for libc.
+ */
+ public NestedSet<Artifact> getLink() {
+ return link;
+ }
+
+ public NestedSet<Artifact> getDwp() {
+ return dwp;
+ }
+
+ public NestedSet<Artifact> getLibcLink() {
+ return libcLink;
+ }
+
+ /**
+ * Returns the static runtime libraries.
+ */
+ public NestedSet<Artifact> getStaticRuntimeLinkInputs() {
+ return staticRuntimeLinkInputs;
+ }
+
+ /**
+ * Returns an aggregating middleman that represents the static runtime libraries.
+ */
+ @Nullable public Artifact getStaticRuntimeLinkMiddleman() {
+ return staticRuntimeLinkMiddleman;
+ }
+
+ /**
+ * Returns the dynamic runtime libraries.
+ */
+ public NestedSet<Artifact> getDynamicRuntimeLinkInputs() {
+ return dynamicRuntimeLinkInputs;
+ }
+
+ /**
+ * Returns an aggregating middleman that represents the dynamic runtime libraries.
+ */
+ @Nullable public Artifact getDynamicRuntimeLinkMiddleman() {
+ return dynamicRuntimeLinkMiddleman;
+ }
+
+ /**
+ * Returns the name of the directory where the solib symlinks for the dynamic runtime libraries
+ * live. The directory itself will be under the root of the host configuration in the 'bin'
+ * directory.
+ */
+ public PathFragment getDynamicRuntimeSolibDir() {
+ return dynamicRuntimeSolibDir;
+ }
+
+ /**
+ * Returns the C++ compilation context for the toolchain.
+ */
+ public CppCompilationContext getCppCompilationContext() {
+ return cppCompilationContext;
+ }
+
+ /**
+ * Whether the toolchains supports parameter files.
+ */
+ public boolean supportsParamFiles() {
+ return supportsParamFiles;
+ }
+
+ /**
+ * Whether the toolchains supports header parsing.
+ */
+ public boolean supportsHeaderParsing() {
+ return supportsHeaderParsing;
+ }
+
+ /**
+ * Returns the configured features of the toolchain.
+ */
+ public CcToolchainFeatures getFeatures() {
+ return cppConfiguration.getFeatures();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CcToolchainRule.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcToolchainRule.java
new file mode 100644
index 0000000..6c68f00
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcToolchainRule.java
@@ -0,0 +1,71 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import static com.google.devtools.build.lib.packages.Attribute.ConfigurationTransition.HOST;
+import static com.google.devtools.build.lib.packages.Attribute.attr;
+import static com.google.devtools.build.lib.packages.Type.BOOLEAN;
+import static com.google.devtools.build.lib.packages.Type.LABEL;
+import static com.google.devtools.build.lib.packages.Type.LABEL_LIST;
+import static com.google.devtools.build.lib.packages.Type.LICENSE;
+import static com.google.devtools.build.lib.packages.Type.STRING;
+
+import com.google.devtools.build.lib.analysis.BaseRuleClasses;
+import com.google.devtools.build.lib.analysis.BlazeRule;
+import com.google.devtools.build.lib.analysis.RuleDefinition;
+import com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.packages.Attribute.LateBoundLabel;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.RuleClass.Builder;
+import com.google.devtools.build.lib.syntax.Label;
+
+/**
+ * Rule definition for compiler definition.
+ */
+@BlazeRule(name = "cc_toolchain",
+ ancestors = { BaseRuleClasses.BaseRule.class },
+ factoryClass = CcToolchain.class)
+public final class CcToolchainRule implements RuleDefinition {
+ private static final LateBoundLabel<BuildConfiguration> LIBC_LINK =
+ new LateBoundLabel<BuildConfiguration>() {
+ @Override
+ public Label getDefault(Rule rule, BuildConfiguration configuration) {
+ return configuration.getFragment(CppConfiguration.class).getLibcLabel();
+ }
+ };
+
+ @Override
+ public RuleClass build(Builder builder, RuleDefinitionEnvironment env) {
+ return builder
+ .setUndocumented()
+ .add(attr("output_licenses", LICENSE))
+ .add(attr("cpu", STRING).mandatory())
+ .add(attr("all_files", LABEL).legacyAllowAnyFileType().cfg(HOST).mandatory())
+ .add(attr("compiler_files", LABEL).legacyAllowAnyFileType().cfg(HOST).mandatory())
+ .add(attr("strip_files", LABEL).legacyAllowAnyFileType().cfg(HOST).mandatory())
+ .add(attr("objcopy_files", LABEL).legacyAllowAnyFileType().cfg(HOST).mandatory())
+ .add(attr("linker_files", LABEL).legacyAllowAnyFileType().cfg(HOST).mandatory())
+ .add(attr("dwp_files", LABEL).legacyAllowAnyFileType().cfg(HOST).mandatory())
+ .add(attr("static_runtime_libs", LABEL_LIST).legacyAllowAnyFileType().mandatory())
+ .add(attr("dynamic_runtime_libs", LABEL_LIST).legacyAllowAnyFileType().mandatory())
+ .add(attr("module_map", LABEL).legacyAllowAnyFileType().cfg(HOST))
+ .add(attr("supports_param_files", BOOLEAN).value(true))
+ .add(attr("supports_header_parsing", BOOLEAN).value(false))
+ // TODO(bazel-team): Should be using the TARGET configuration.
+ .add(attr(":libc_link", LABEL).cfg(HOST).value(LIBC_LINK))
+ .build();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppBuildInfo.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppBuildInfo.java
new file mode 100644
index 0000000..78a5f89
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppBuildInfo.java
@@ -0,0 +1,89 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.Root;
+import com.google.devtools.build.lib.analysis.buildinfo.BuildInfoCollection;
+import com.google.devtools.build.lib.analysis.buildinfo.BuildInfoFactory;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * C++ build info creation - generates header files that contain the corresponding build-info data.
+ */
+public final class CppBuildInfo implements BuildInfoFactory {
+ public static final BuildInfoKey KEY = new BuildInfoKey("C++");
+
+ private static final PathFragment BUILD_INFO_NONVOLATILE_HEADER_NAME =
+ new PathFragment("build-info-nonvolatile.h");
+ private static final PathFragment BUILD_INFO_VOLATILE_HEADER_NAME =
+ new PathFragment("build-info-volatile.h");
+ // TODO(bazel-team): (2011) Get rid of the redacted build info. We should try to make
+ // the linkstamping process handle the case where those values are undefined.
+ private static final PathFragment BUILD_INFO_REDACTED_HEADER_NAME =
+ new PathFragment("build-info-redacted.h");
+
+ @Override
+ public BuildInfoCollection create(BuildInfoContext buildInfoContext, BuildConfiguration config,
+ Artifact buildInfo, Artifact buildChangelist) {
+ List<Action> actions = new ArrayList<>();
+ WriteBuildInfoHeaderAction redactedInfo = getHeader(buildInfoContext, config,
+ BUILD_INFO_REDACTED_HEADER_NAME,
+ Artifact.NO_ARTIFACTS, true, true);
+ WriteBuildInfoHeaderAction nonvolatileInfo = getHeader(buildInfoContext, config,
+ BUILD_INFO_NONVOLATILE_HEADER_NAME,
+ ImmutableList.of(buildInfo),
+ false, true);
+ WriteBuildInfoHeaderAction volatileInfo = getHeader(buildInfoContext, config,
+ BUILD_INFO_VOLATILE_HEADER_NAME,
+ ImmutableList.of(buildChangelist),
+ true, false);
+ actions.add(redactedInfo);
+ actions.add(nonvolatileInfo);
+ actions.add(volatileInfo);
+ return new BuildInfoCollection(actions,
+ ImmutableList.of(nonvolatileInfo.getPrimaryOutput(), volatileInfo.getPrimaryOutput()),
+ ImmutableList.of(redactedInfo.getPrimaryOutput()));
+ }
+
+ private WriteBuildInfoHeaderAction getHeader(BuildInfoContext buildInfoContext,
+ BuildConfiguration config, PathFragment headerName,
+ Collection<Artifact> inputs,
+ boolean writeVolatileInfo, boolean writeNonVolatileInfo) {
+ Root outputPath = config.getIncludeDirectory();
+ final Artifact header =
+ buildInfoContext.getBuildInfoArtifact(headerName, outputPath,
+ writeVolatileInfo && !inputs.isEmpty()
+ ? BuildInfoType.NO_REBUILD : BuildInfoType.FORCE_REBUILD_IF_CHANGED);
+ return new WriteBuildInfoHeaderAction(
+ inputs, header, writeVolatileInfo, writeNonVolatileInfo);
+ }
+
+ @Override
+ public BuildInfoKey getKey() {
+ return KEY;
+ }
+
+ @Override
+ public boolean isEnabled(BuildConfiguration config) {
+ return config.hasFragment(CppConfiguration.class);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppCompilationContext.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppCompilationContext.java
new file mode 100644
index 0000000..cf39ef5
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppCompilationContext.java
@@ -0,0 +1,918 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.ActionOwner;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.MiddlemanFactory;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.util.Pair;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Immutable store of information needed for C++ compilation that is aggregated
+ * across dependencies.
+ */
+@Immutable
+public final class CppCompilationContext implements TransitiveInfoProvider {
+ /** An empty compilation context. */
+ public static final CppCompilationContext EMPTY = new Builder(null).build();
+
+ private final CommandLineContext commandLineContext;
+ private final ImmutableList<DepsContext> depsContexts;
+ private final CppModuleMap cppModuleMap;
+ private final Artifact headerModule;
+ private final Artifact picHeaderModule;
+ private final ImmutableSet<Artifact> compilationPrerequisites;
+
+ private CppCompilationContext(CommandLineContext commandLineContext,
+ List<DepsContext> depsContexts, CppModuleMap cppModuleMap, Artifact headerModule,
+ Artifact picHeaderModule) {
+ Preconditions.checkNotNull(commandLineContext);
+ Preconditions.checkArgument(!depsContexts.isEmpty());
+ this.commandLineContext = commandLineContext;
+ this.depsContexts = ImmutableList.copyOf(depsContexts);
+ this.cppModuleMap = cppModuleMap;
+ this.headerModule = headerModule;
+ this.picHeaderModule = picHeaderModule;
+
+ if (depsContexts.size() == 1) {
+ // Only LIPO targets have more than one DepsContexts. This codepath avoids creating
+ // an ImmutableSet.Builder for the vast majority of the cases.
+ compilationPrerequisites = (depsContexts.get(0).compilationPrerequisiteStampFile != null)
+ ? ImmutableSet.<Artifact>of(depsContexts.get(0).compilationPrerequisiteStampFile)
+ : ImmutableSet.<Artifact>of();
+ } else {
+ ImmutableSet.Builder<Artifact> prerequisites = ImmutableSet.builder();
+ for (DepsContext depsContext : depsContexts) {
+ if (depsContext.compilationPrerequisiteStampFile != null) {
+ prerequisites.add(depsContext.compilationPrerequisiteStampFile);
+ }
+ }
+ compilationPrerequisites = prerequisites.build();
+ }
+ }
+
+ /**
+ * Returns the compilation prerequisites consolidated into middlemen
+ * prerequisites, or an empty set if there are no prerequisites.
+ *
+ * <p>For correct dependency tracking, and to reduce the overhead to establish
+ * dependencies on generated headers, we express the dependency on compilation
+ * prerequisites as a transitive dependency via a middleman. After they have
+ * been accumulated (using
+ * {@link Builder#addCompilationPrerequisites(Iterable)},
+ * {@link Builder#mergeDependentContext(CppCompilationContext)}, and
+ * {@link Builder#mergeDependentContexts(Iterable)}, they are consolidated
+ * into a single middleman Artifact when {@link Builder#build()} is called.
+ *
+ * <p>The returned set can be empty if there are no prerequisites. Usually it
+ * contains a single middleman, but if LIPO is used there can be two.
+ */
+ public ImmutableSet<Artifact> getCompilationPrerequisites() {
+ return compilationPrerequisites;
+ }
+
+ /**
+ * Returns the immutable list of include directories to be added with "-I"
+ * (possibly empty but never null). This includes the include dirs from the
+ * transitive deps closure of the target. This list does not contain
+ * duplicates. All fragments are either absolute or relative to the exec root
+ * (see {@link BuildConfiguration#getExecRoot}).
+ */
+ public ImmutableList<PathFragment> getIncludeDirs() {
+ return commandLineContext.includeDirs;
+ }
+
+ /**
+ * Returns the immutable list of include directories to be added with
+ * "-iquote" (possibly empty but never null). This includes the include dirs
+ * from the transitive deps closure of the target. This list does not contain
+ * duplicates. All fragments are either absolute or relative to the exec root
+ * (see {@link BuildConfiguration#getExecRoot}).
+ */
+ public ImmutableList<PathFragment> getQuoteIncludeDirs() {
+ return commandLineContext.quoteIncludeDirs;
+ }
+
+ /**
+ * Returns the immutable list of include directories to be added with
+ * "-isystem" (possibly empty but never null). This includes the include dirs
+ * from the transitive deps closure of the target. This list does not contain
+ * duplicates. All fragments are either absolute or relative to the exec root
+ * (see {@link BuildConfiguration#getExecRoot}).
+ */
+ public ImmutableList<PathFragment> getSystemIncludeDirs() {
+ return commandLineContext.systemIncludeDirs;
+ }
+
+ /**
+ * Returns the immutable set of declared include directories, relative to a
+ * "-I" or "-iquote" directory" (possibly empty but never null). The returned
+ * collection may contain duplicate elements.
+ *
+ * <p>Note: The iteration order of this list is preserved as ide_build_info
+ * writes these directories and sources out and the ordering will help when
+ * used by consumers.
+ */
+ public NestedSet<PathFragment> getDeclaredIncludeDirs() {
+ if (depsContexts.isEmpty()) {
+ return NestedSetBuilder.emptySet(Order.STABLE_ORDER);
+ }
+
+ if (depsContexts.size() == 1) {
+ return depsContexts.get(0).declaredIncludeDirs;
+ }
+
+ NestedSetBuilder<PathFragment> builder = NestedSetBuilder.stableOrder();
+ for (DepsContext depsContext : depsContexts) {
+ builder.addTransitive(depsContext.declaredIncludeDirs);
+ }
+
+ return builder.build();
+ }
+
+ /**
+ * Returns the immutable set of include directories, relative to a "-I" or
+ * "-iquote" directory", from which inclusion will produce a warning (possibly
+ * empty but never null). The returned collection may contain duplicate
+ * elements.
+ *
+ * <p>Note: The iteration order of this list is preserved as ide_build_info
+ * writes these directories and sources out and the ordering will help when
+ * used by consumers.
+ */
+ public NestedSet<PathFragment> getDeclaredIncludeWarnDirs() {
+ if (depsContexts.isEmpty()) {
+ return NestedSetBuilder.emptySet(Order.STABLE_ORDER);
+ }
+
+ if (depsContexts.size() == 1) {
+ return depsContexts.get(0).declaredIncludeWarnDirs;
+ }
+
+ NestedSetBuilder<PathFragment> builder = NestedSetBuilder.stableOrder();
+ for (DepsContext depsContext : depsContexts) {
+ builder.addTransitive(depsContext.declaredIncludeWarnDirs);
+ }
+
+ return builder.build();
+ }
+
+ /**
+ * Returns the immutable set of headers that have been declared in the
+ * {@code src} or {@code headers attribute} (possibly empty but never null).
+ * The returned collection may contain duplicate elements.
+ *
+ * <p>Note: The iteration order of this list is preserved as ide_build_info
+ * writes these directories and sources out and the ordering will help when
+ * used by consumers.
+ */
+ public NestedSet<Artifact> getDeclaredIncludeSrcs() {
+ if (depsContexts.isEmpty()) {
+ return NestedSetBuilder.emptySet(Order.STABLE_ORDER);
+ }
+
+ if (depsContexts.size() == 1) {
+ return depsContexts.get(0).declaredIncludeSrcs;
+ }
+
+ NestedSetBuilder<Artifact> builder = NestedSetBuilder.stableOrder();
+ for (DepsContext depsContext : depsContexts) {
+ builder.addTransitive(depsContext.declaredIncludeSrcs);
+ }
+
+ return builder.build();
+ }
+
+ /**
+ * Returns the immutable pairs of (header file, pregrepped header file).
+ */
+ public NestedSet<Pair<Artifact, Artifact>> getPregreppedHeaders() {
+ if (depsContexts.isEmpty()) {
+ return NestedSetBuilder.emptySet(Order.STABLE_ORDER);
+ }
+
+ if (depsContexts.size() == 1) {
+ return depsContexts.get(0).pregreppedHdrs;
+ }
+
+ NestedSetBuilder<Pair<Artifact, Artifact>> builder = NestedSetBuilder.stableOrder();
+ for (DepsContext depsContext : depsContexts) {
+ builder.addTransitive(depsContext.pregreppedHdrs);
+ }
+
+ return builder.build();
+ }
+
+ /**
+ * Returns the immutable set of additional transitive inputs needed for
+ * compilation, like C++ module map artifacts.
+ */
+ public NestedSet<Artifact> getAdditionalInputs() {
+ if (depsContexts.isEmpty()) {
+ return NestedSetBuilder.emptySet(Order.STABLE_ORDER);
+ }
+
+ if (depsContexts.size() == 1) {
+ return depsContexts.get(0).auxiliaryInputs;
+ }
+
+ NestedSetBuilder<Artifact> builder = NestedSetBuilder.stableOrder();
+ for (DepsContext depsContext : depsContexts) {
+ builder.addTransitive(depsContext.auxiliaryInputs);
+ }
+
+ return builder.build();
+ }
+
+ /**
+ * Returns optional inputs that are needed by any C++ compilations that use header modules.
+ *
+ * <p>For every target that the current target depends on transitively and that is built as header
+ * module, contains:
+ * <ul>
+ * <li>the pic/non-pic header module (pcm file)</li>
+ * <li>the transitive list of module maps.</li>
+ * </ul>
+ */
+ private NestedSet<Artifact> getTransitiveAuxiliaryInputs() {
+ NestedSetBuilder<Artifact> builder = NestedSetBuilder.stableOrder();
+ for (DepsContext depsContext : depsContexts) {
+ builder.addTransitive(depsContext.transitiveAuxiliaryInputs);
+ }
+ return builder.build();
+ }
+
+ /**
+ * @return all modules maps in the transitive closure.
+ */
+ private NestedSet<Artifact> getTransitiveModuleMaps() {
+ NestedSetBuilder<Artifact> builder = NestedSetBuilder.stableOrder();
+ for (DepsContext depsContext : depsContexts) {
+ builder.addTransitive(depsContext.transitiveModuleMaps);
+ }
+ return builder.build();
+ }
+
+ /**
+ * @return all headers whose transitive closure of includes needs to be
+ * available when compiling anything in the current target.
+ */
+ protected NestedSet<Artifact> getTransitiveHeaderModuleSrcs() {
+ NestedSetBuilder<Artifact> builder = NestedSetBuilder.stableOrder();
+ for (DepsContext depsContext : depsContexts) {
+ builder.addTransitive(depsContext.transitiveHeaderModuleSrcs);
+ }
+ return builder.build();
+ }
+
+ /**
+ * @return all declared headers of the current module if the current target
+ * is compiled as a module.
+ */
+ protected NestedSet<Artifact> getHeaderModuleSrcs() {
+ NestedSetBuilder<Artifact> builder = NestedSetBuilder.stableOrder();
+ for (DepsContext depsContext : depsContexts) {
+ builder.addTransitive(depsContext.headerModuleSrcs);
+ }
+ return builder.build();
+ }
+
+ /**
+ * Returns the set of defines needed to compile this target (possibly empty
+ * but never null). This includes definitions from the transitive deps closure
+ * for the target. The order of the returned collection is deterministic.
+ */
+ public ImmutableList<String> getDefines() {
+ return commandLineContext.defines;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (!(obj instanceof CppCompilationContext)) {
+ return false;
+ }
+ CppCompilationContext other = (CppCompilationContext) obj;
+ return Objects.equals(headerModule, other.headerModule)
+ && Objects.equals(picHeaderModule, other.picHeaderModule)
+ && commandLineContext.equals(other.commandLineContext)
+ && depsContexts.equals(other.depsContexts);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(headerModule, picHeaderModule, commandLineContext, depsContexts);
+ }
+
+ /**
+ * Returns a context that is based on a given context but returns empty sets
+ * for {@link #getDeclaredIncludeDirs()} and {@link #getDeclaredIncludeWarnDirs()}.
+ */
+ public static CppCompilationContext disallowUndeclaredHeaders(CppCompilationContext context) {
+ ImmutableList.Builder<DepsContext> builder = ImmutableList.builder();
+ for (DepsContext depsContext : context.depsContexts) {
+ builder.add(new DepsContext(
+ depsContext.compilationPrerequisiteStampFile,
+ NestedSetBuilder.<PathFragment>emptySet(Order.STABLE_ORDER),
+ NestedSetBuilder.<PathFragment>emptySet(Order.STABLE_ORDER),
+ depsContext.declaredIncludeSrcs,
+ depsContext.pregreppedHdrs,
+ depsContext.auxiliaryInputs,
+ depsContext.headerModuleSrcs,
+ depsContext.transitiveAuxiliaryInputs,
+ depsContext.transitiveHeaderModuleSrcs,
+ depsContext.transitiveModuleMaps));
+ }
+ return new CppCompilationContext(context.commandLineContext, builder.build(),
+ context.cppModuleMap, context.headerModule, context.picHeaderModule);
+ }
+
+ /**
+ * Returns the context for a LIPO compile action. This uses the include dirs
+ * and defines of the library, but the declared inclusion dirs/srcs from both
+ * the library and the owner binary.
+
+ * TODO(bazel-team): this might make every LIPO target have an unnecessary large set of
+ * inclusion dirs/srcs. The correct behavior would be to merge only the contexts
+ * of actual referred targets (as listed in .imports file).
+ *
+ * <p>Undeclared inclusion checking ({@link #getDeclaredIncludeDirs()},
+ * {@link #getDeclaredIncludeWarnDirs()}, and
+ * {@link #getDeclaredIncludeSrcs()}) needs to use the union of the contexts
+ * of the involved source files.
+ *
+ * <p>For include and define command line flags ({@link #getIncludeDirs()}
+ * {@link #getQuoteIncludeDirs()}, {@link #getSystemIncludeDirs()}, and
+ * {@link #getDefines()}) LIPO compilations use the same values as non-LIPO
+ * compilation.
+ *
+ * <p>Include scanning is not handled by this method. See
+ * {@code IncludeScannable#getAuxiliaryScannables()} instead.
+ *
+ * @param ownerContext the compilation context of the owner binary
+ * @param libContext the compilation context of the library
+ */
+ public static CppCompilationContext mergeForLipo(CppCompilationContext ownerContext,
+ CppCompilationContext libContext) {
+ return new CppCompilationContext(libContext.commandLineContext,
+ ImmutableList.copyOf(Iterables.concat(ownerContext.depsContexts, libContext.depsContexts)),
+ libContext.cppModuleMap, libContext.headerModule, libContext.picHeaderModule);
+ }
+
+ /**
+ * @return the C++ module map of the owner.
+ */
+ public CppModuleMap getCppModuleMap() {
+ return cppModuleMap;
+ }
+
+ /**
+ * @return the non-pic C++ header module of the owner.
+ */
+ private Artifact getHeaderModule() {
+ return headerModule;
+ }
+
+ /**
+ * @return the pic C++ header module of the owner.
+ */
+ private Artifact getPicHeaderModule() {
+ return picHeaderModule;
+ }
+
+ /**
+ * The parts of the compilation context that influence the command line of
+ * compilation actions.
+ */
+ @Immutable
+ private static class CommandLineContext {
+ private final ImmutableList<PathFragment> includeDirs;
+ private final ImmutableList<PathFragment> quoteIncludeDirs;
+ private final ImmutableList<PathFragment> systemIncludeDirs;
+ private final ImmutableList<String> defines;
+
+ CommandLineContext(ImmutableList<PathFragment> includeDirs,
+ ImmutableList<PathFragment> quoteIncludeDirs,
+ ImmutableList<PathFragment> systemIncludeDirs,
+ ImmutableList<String> defines) {
+ this.includeDirs = includeDirs;
+ this.quoteIncludeDirs = quoteIncludeDirs;
+ this.systemIncludeDirs = systemIncludeDirs;
+ this.defines = defines;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (!(obj instanceof CommandLineContext)) {
+ return false;
+ }
+ CommandLineContext other = (CommandLineContext) obj;
+ return Objects.equals(includeDirs, other.includeDirs)
+ && Objects.equals(quoteIncludeDirs, other.quoteIncludeDirs)
+ && Objects.equals(systemIncludeDirs, other.systemIncludeDirs)
+ && Objects.equals(defines, other.defines);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(includeDirs, quoteIncludeDirs, systemIncludeDirs, defines);
+ }
+ }
+
+ /**
+ * The parts of the compilation context that defined the dependencies of
+ * actions of scheduling and inclusion validity checking.
+ */
+ @Immutable
+ private static class DepsContext {
+ private final Artifact compilationPrerequisiteStampFile;
+ private final NestedSet<PathFragment> declaredIncludeDirs;
+ private final NestedSet<PathFragment> declaredIncludeWarnDirs;
+ private final NestedSet<Artifact> declaredIncludeSrcs;
+ private final NestedSet<Pair<Artifact, Artifact>> pregreppedHdrs;
+
+ /**
+ * Optional inputs that are used by some forms of compilation, containing:
+ * <ul>
+ * <li>module map of the current target</li>
+ * <li>module maps of all direct dependencies that are not compiled as header modules</li>
+ * <li>all transitiveAuxiliaryInputs.</li>
+ * </ul>
+ */
+ private final NestedSet<Artifact> auxiliaryInputs;
+
+ /**
+ * All declared headers of the current module, if compiled as a header module.
+ */
+ private final NestedSet<Artifact> headerModuleSrcs;
+
+ private final NestedSet<Artifact> transitiveAuxiliaryInputs;
+
+ /**
+ * Headers whose transitive closure of includes needs to be available when compiling the current
+ * target. For every target that the current target depends on transitively and that is built as
+ * header module, contains all headers that are part of its header module.
+ */
+ private final NestedSet<Artifact> transitiveHeaderModuleSrcs;
+
+ /**
+ * The module maps from all targets the current target depends on transitively.
+ */
+ private final NestedSet<Artifact> transitiveModuleMaps;
+
+ DepsContext(Artifact compilationPrerequisiteStampFile,
+ NestedSet<PathFragment> declaredIncludeDirs,
+ NestedSet<PathFragment> declaredIncludeWarnDirs,
+ NestedSet<Artifact> declaredIncludeSrcs,
+ NestedSet<Pair<Artifact, Artifact>> pregreppedHdrs,
+ NestedSet<Artifact> auxiliaryInputs,
+ NestedSet<Artifact> headerModuleSrcs,
+ NestedSet<Artifact> transitiveAuxiliaryInputs,
+ NestedSet<Artifact> transitiveHeaderModuleSrcs,
+ NestedSet<Artifact> transitiveModuleMaps) {
+ this.compilationPrerequisiteStampFile = compilationPrerequisiteStampFile;
+ this.declaredIncludeDirs = declaredIncludeDirs;
+ this.declaredIncludeWarnDirs = declaredIncludeWarnDirs;
+ this.declaredIncludeSrcs = declaredIncludeSrcs;
+ this.pregreppedHdrs = pregreppedHdrs;
+ this.auxiliaryInputs = auxiliaryInputs;
+ this.headerModuleSrcs = headerModuleSrcs;
+ this.transitiveAuxiliaryInputs = transitiveAuxiliaryInputs;
+ this.transitiveHeaderModuleSrcs = transitiveHeaderModuleSrcs;
+ this.transitiveModuleMaps = transitiveModuleMaps;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (!(obj instanceof DepsContext)) {
+ return false;
+ }
+ DepsContext other = (DepsContext) obj;
+ return Objects.equals(
+ compilationPrerequisiteStampFile, other.compilationPrerequisiteStampFile)
+ && Objects.equals(declaredIncludeDirs, other.declaredIncludeDirs)
+ && Objects.equals(declaredIncludeWarnDirs, other.declaredIncludeWarnDirs)
+ && Objects.equals(declaredIncludeSrcs, other.declaredIncludeSrcs)
+ && Objects.equals(auxiliaryInputs, other.auxiliaryInputs)
+ && Objects.equals(headerModuleSrcs, other.headerModuleSrcs)
+ // Due to the NestedSet equals being ==, and the code flow only setting them if at least
+ // auxiliaryInputs is set, these checks cannot be executed. We leave them in so the equals
+ // is still correct if that connection ever changes.R
+ && Objects.equals(transitiveAuxiliaryInputs, other.transitiveAuxiliaryInputs)
+ && Objects.equals(transitiveHeaderModuleSrcs, other.transitiveHeaderModuleSrcs)
+ && Objects.equals(transitiveModuleMaps, other.transitiveModuleMaps)
+ ;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(compilationPrerequisiteStampFile,
+ declaredIncludeDirs,
+ declaredIncludeWarnDirs,
+ declaredIncludeSrcs,
+ auxiliaryInputs,
+ headerModuleSrcs,
+ transitiveAuxiliaryInputs,
+ transitiveHeaderModuleSrcs,
+ transitiveModuleMaps);
+ }
+ }
+
+ /**
+ * Builder class for {@link CppCompilationContext}.
+ */
+ public static class Builder {
+ private String purpose = "cpp_compilation_prerequisites";
+ private final Set<Artifact> compilationPrerequisites = new LinkedHashSet<>();
+ private final Set<PathFragment> includeDirs = new LinkedHashSet<>();
+ private final Set<PathFragment> quoteIncludeDirs = new LinkedHashSet<>();
+ private final Set<PathFragment> systemIncludeDirs = new LinkedHashSet<>();
+ private final NestedSetBuilder<PathFragment> declaredIncludeDirs =
+ NestedSetBuilder.stableOrder();
+ private final NestedSetBuilder<PathFragment> declaredIncludeWarnDirs =
+ NestedSetBuilder.stableOrder();
+ private final NestedSetBuilder<Artifact> declaredIncludeSrcs =
+ NestedSetBuilder.stableOrder();
+ private final NestedSetBuilder<Pair<Artifact, Artifact>> pregreppedHdrs =
+ NestedSetBuilder.stableOrder();
+ private final NestedSetBuilder<Artifact> auxiliaryInputs =
+ NestedSetBuilder.stableOrder();
+ private final NestedSetBuilder<Artifact> headerModuleSrcs =
+ NestedSetBuilder.stableOrder();
+ private final NestedSetBuilder<Artifact> transitiveAuxiliaryInputs =
+ NestedSetBuilder.stableOrder();
+ private final NestedSetBuilder<Artifact> transitiveHeaderModuleSrcs =
+ NestedSetBuilder.stableOrder();
+ private final NestedSetBuilder<Artifact> transitiveModuleMaps =
+ NestedSetBuilder.stableOrder();
+ private final Set<String> defines = new LinkedHashSet<>();
+ private CppModuleMap cppModuleMap;
+ private Artifact headerModule;
+ private Artifact picHeaderModule;
+
+ /** The rule that owns the context */
+ private final RuleContext ruleContext;
+
+ /**
+ * Creates a new builder for a {@link CppCompilationContext} instance.
+ */
+ public Builder(RuleContext ruleContext) {
+ this.ruleContext = ruleContext;
+ }
+
+ /**
+ * Overrides the purpose of this context. This is useful if a Target
+ * needs more than one CppCompilationContext. (The purpose is used to
+ * construct the name of the prerequisites middleman for the context, and
+ * all artifacts for a given Target must have distinct names.)
+ *
+ * @param purpose must be a string which is suitable for use as a filename.
+ * A single rule may have many middlemen with distinct purposes.
+ *
+ * @see MiddlemanFactory#createErrorPropagatingMiddleman
+ */
+ public Builder setPurpose(String purpose) {
+ this.purpose = purpose;
+ return this;
+ }
+
+ public String getPurpose() {
+ return purpose;
+ }
+
+ /**
+ * Merges the context of a dependency into this one by adding the contents
+ * of all of its attributes.
+ */
+ public Builder mergeDependentContext(CppCompilationContext otherContext) {
+ Preconditions.checkNotNull(otherContext);
+ compilationPrerequisites.addAll(otherContext.getCompilationPrerequisites());
+ includeDirs.addAll(otherContext.getIncludeDirs());
+ quoteIncludeDirs.addAll(otherContext.getQuoteIncludeDirs());
+ systemIncludeDirs.addAll(otherContext.getSystemIncludeDirs());
+ declaredIncludeDirs.addTransitive(otherContext.getDeclaredIncludeDirs());
+ declaredIncludeWarnDirs.addTransitive(otherContext.getDeclaredIncludeWarnDirs());
+ declaredIncludeSrcs.addTransitive(otherContext.getDeclaredIncludeSrcs());
+ pregreppedHdrs.addTransitive(otherContext.getPregreppedHeaders());
+
+ // Forward transitive information.
+ transitiveAuxiliaryInputs.addTransitive(otherContext.getTransitiveAuxiliaryInputs());
+ transitiveModuleMaps.addTransitive(otherContext.getTransitiveModuleMaps());
+ transitiveHeaderModuleSrcs.addTransitive(otherContext.getTransitiveHeaderModuleSrcs());
+
+ // All module maps of direct dependencies are inputs to the current compile independently of
+ // the build type.
+ if (otherContext.getCppModuleMap() != null) {
+ auxiliaryInputs.add(otherContext.getCppModuleMap().getArtifact());
+ }
+ if (otherContext.getHeaderModule() != null || otherContext.getPicHeaderModule() != null) {
+ // If we depend directly on a target that has a compiled header module, all targets
+ // transitively depending on us will need that header module, and all transitive module
+ // maps.
+ if (otherContext.getHeaderModule() != null) {
+ transitiveAuxiliaryInputs.add(otherContext.getHeaderModule());
+ }
+ if (otherContext.getPicHeaderModule() != null) {
+ transitiveAuxiliaryInputs.add(otherContext.getPicHeaderModule());
+ }
+ transitiveAuxiliaryInputs.addAll(otherContext.getTransitiveModuleMaps());
+
+ // All targets transitively depending on us will need to have the full transitive #include
+ // closure of the headers in that module available.
+ transitiveHeaderModuleSrcs.addAll(otherContext.getHeaderModuleSrcs());
+ }
+ // All compile actions in the current target will need the transitive inputs.
+ auxiliaryInputs.addAll(transitiveAuxiliaryInputs.build().toCollection());
+
+ defines.addAll(otherContext.getDefines());
+ return this;
+ }
+
+ /**
+ * Merges the context of some targets into this one by adding the contents
+ * of all of their attributes. Targets that do not implement
+ * {@link CppCompilationContext} are ignored.
+ */
+ public Builder mergeDependentContexts(Iterable<CppCompilationContext> targets) {
+ for (CppCompilationContext target : targets) {
+ mergeDependentContext(target);
+ }
+ return this;
+ }
+
+ /**
+ * Adds multiple compilation prerequisites.
+ */
+ public Builder addCompilationPrerequisites(Iterable<Artifact> prerequisites) {
+ // LIPO collector must not add compilation prerequisites in order to avoid
+ // the creation of a middleman action.
+ Iterables.addAll(compilationPrerequisites, prerequisites);
+ return this;
+ }
+
+ /**
+ * Add a single include directory to be added with "-I". It can be either
+ * relative to the exec root (see {@link BuildConfiguration#getExecRoot}) or
+ * absolute. Before it is stored, the include directory is normalized.
+ */
+ public Builder addIncludeDir(PathFragment includeDir) {
+ includeDirs.add(includeDir.normalize());
+ return this;
+ }
+
+ /**
+ * Add multiple include directories to be added with "-I". These can be
+ * either relative to the exec root (see {@link
+ * BuildConfiguration#getExecRoot}) or absolute. The entries are normalized
+ * before they are stored.
+ */
+ public Builder addIncludeDirs(Iterable<PathFragment> includeDirs) {
+ for (PathFragment includeDir : includeDirs) {
+ addIncludeDir(includeDir);
+ }
+ return this;
+ }
+
+ /**
+ * Add a single include directory to be added with "-iquote". It can be
+ * either relative to the exec root (see {@link
+ * BuildConfiguration#getExecRoot}) or absolute. Before it is stored, the
+ * include directory is normalized.
+ */
+ public Builder addQuoteIncludeDir(PathFragment quoteIncludeDir) {
+ quoteIncludeDirs.add(quoteIncludeDir.normalize());
+ return this;
+ }
+
+ /**
+ * Add a single include directory to be added with "-isystem". It can be
+ * either relative to the exec root (see {@link
+ * BuildConfiguration#getExecRoot}) or absolute. Before it is stored, the
+ * include directory is normalized.
+ */
+ public Builder addSystemIncludeDir(PathFragment systemIncludeDir) {
+ systemIncludeDirs.add(systemIncludeDir.normalize());
+ return this;
+ }
+
+ /**
+ * Add a single declared include dir, relative to a "-I" or "-iquote"
+ * directory".
+ */
+ public Builder addDeclaredIncludeDir(PathFragment dir) {
+ declaredIncludeDirs.add(dir);
+ return this;
+ }
+
+ /**
+ * Add a single declared include directory, relative to a "-I" or "-iquote"
+ * directory", from which inclusion will produce a warning.
+ */
+ public Builder addDeclaredIncludeWarnDir(PathFragment dir) {
+ declaredIncludeWarnDirs.add(dir);
+ return this;
+ }
+
+ /**
+ * Adds a header that has been declared in the {@code src} or {@code headers attribute}. The
+ * header will also be added to the compilation prerequisites.
+ */
+ public Builder addDeclaredIncludeSrc(Artifact header) {
+ declaredIncludeSrcs.add(header);
+ compilationPrerequisites.add(header);
+ headerModuleSrcs.add(header);
+ return this;
+ }
+
+ /**
+ * Adds multiple headers that have been declared in the {@code src} or {@code headers
+ * attribute}. The headers will also be added to the compilation prerequisites.
+ */
+ public Builder addDeclaredIncludeSrcs(Iterable<Artifact> declaredIncludeSrcs) {
+ this.declaredIncludeSrcs.addAll(declaredIncludeSrcs);
+ this.headerModuleSrcs.addAll(declaredIncludeSrcs);
+ return addCompilationPrerequisites(declaredIncludeSrcs);
+ }
+
+ /**
+ * Add a map of generated source or header Artifact to an output Artifact after grepping
+ * the file for include statements.
+ */
+ public Builder addPregreppedHeaderMap(Map<Artifact, Artifact> pregrepped) {
+ addCompilationPrerequisites(pregrepped.values());
+ for (Map.Entry<Artifact, Artifact> entry : pregrepped.entrySet()) {
+ this.pregreppedHdrs.add(Pair.of(entry.getKey(), entry.getValue()));
+ }
+ return this;
+ }
+
+ /**
+ * Adds a single define.
+ */
+ public Builder addDefine(String define) {
+ defines.add(define);
+ return this;
+ }
+
+ /**
+ * Adds multiple defines.
+ */
+ public Builder addDefines(Iterable<String> defines) {
+ Iterables.addAll(this.defines, defines);
+ return this;
+ }
+
+ /**
+ * Sets the C++ module map.
+ */
+ public Builder setCppModuleMap(CppModuleMap cppModuleMap) {
+ this.cppModuleMap = cppModuleMap;
+ return this;
+ }
+
+ /**
+ * Sets the C++ header module in non-pic mode.
+ */
+ public Builder setHeaderModule(Artifact headerModule) {
+ this.headerModule = headerModule;
+ return this;
+ }
+
+ /**
+ * Sets the C++ header module in pic mode.
+ */
+ public Builder setPicHeaderModule(Artifact picHeaderModule) {
+ this.picHeaderModule = picHeaderModule;
+ return this;
+ }
+
+ /**
+ * Builds the {@link CppCompilationContext}.
+ */
+ public CppCompilationContext build() {
+ return build(
+ ruleContext == null ? null : ruleContext.getActionOwner(),
+ ruleContext == null ? null : ruleContext.getAnalysisEnvironment().getMiddlemanFactory());
+ }
+
+ @VisibleForTesting // productionVisibility = Visibility.PRIVATE
+ public CppCompilationContext build(ActionOwner owner, MiddlemanFactory middlemanFactory) {
+ if (cppModuleMap != null) {
+ // .cppmap files should also be mandatory inputs for compile actions
+ auxiliaryInputs.add(cppModuleMap.getArtifact());
+ transitiveModuleMaps.add(cppModuleMap.getArtifact());
+ }
+
+ // We don't create middlemen in LIPO collector subtree, because some target CT
+ // will do that instead.
+ Artifact prerequisiteStampFile = (ruleContext != null
+ && ruleContext.getFragment(CppConfiguration.class).isLipoContextCollector())
+ ? getMiddlemanArtifact(middlemanFactory)
+ : createMiddleman(owner, middlemanFactory);
+
+ return new CppCompilationContext(
+ new CommandLineContext(ImmutableList.copyOf(includeDirs),
+ ImmutableList.copyOf(quoteIncludeDirs), ImmutableList.copyOf(systemIncludeDirs),
+ ImmutableList.copyOf(defines)),
+ ImmutableList.of(new DepsContext(prerequisiteStampFile,
+ declaredIncludeDirs.build(),
+ declaredIncludeWarnDirs.build(),
+ declaredIncludeSrcs.build(),
+ pregreppedHdrs.build(),
+ auxiliaryInputs.build(),
+ headerModuleSrcs.build(),
+ transitiveAuxiliaryInputs.build(),
+ transitiveHeaderModuleSrcs.build(),
+ transitiveModuleMaps.build())),
+ cppModuleMap,
+ headerModule,
+ picHeaderModule);
+ }
+
+ /**
+ * Creates a middleman for the compilation prerequisites.
+ *
+ * @return the middleman or null if there are no prerequisites
+ */
+ private Artifact createMiddleman(ActionOwner owner,
+ MiddlemanFactory middlemanFactory) {
+ if (compilationPrerequisites.isEmpty()) {
+ return null;
+ }
+
+ // Compilation prerequisites gathered in the compilationPrerequisites
+ // must be generated prior to executing C++ compilation step that depends
+ // on them (since these prerequisites include all potential header files, etc
+ // that could be referenced during compilation). So there is a definite need
+ // to ensure scheduling edge dependency. However, those prerequisites should
+ // have no effect on the decision whether C++ compilation should happen in
+ // the first place - only CppCompileAction outputs (*.o and *.d files) and
+ // all files referenced by the *.d file should be used to make that decision.
+ // If this action was never executed, then *.d file would be missing, forcing
+ // compilation to occur. If *.d file is present and has not changed then the
+ // only reason that would force us to re-compile would be change in one of
+ // the files referenced by the *.d file, since no other files participated
+ // in the compilation. We also need to propagate errors through this
+ // dependency link. So we use an error propagating middleman.
+ // Such middleman will be ignored by the dependency checker yet will still
+ // represent an edge in the action dependency graph - forcing proper execution
+ // order and error propagation.
+ return middlemanFactory.createErrorPropagatingMiddleman(
+ owner, ruleContext.getLabel().toString(), purpose,
+ ImmutableList.copyOf(compilationPrerequisites),
+ ruleContext.getConfiguration().getMiddlemanDirectory());
+ }
+
+ /**
+ * Returns the same set of artifacts as createMiddleman() would, but without
+ * actually creating middlemen.
+ */
+ private Artifact getMiddlemanArtifact(MiddlemanFactory middlemanFactory) {
+ if (compilationPrerequisites.isEmpty()) {
+ return null;
+ }
+
+ return middlemanFactory.getErrorPropagatingMiddlemanArtifact(ruleContext.getLabel()
+ .toString(), purpose, ruleContext.getConfiguration().getMiddlemanDirectory());
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppCompileAction.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppCompileAction.java
new file mode 100644
index 0000000..e90f9f7
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppCompileAction.java
@@ -0,0 +1,1356 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
+import com.google.devtools.build.lib.actions.AbstractAction;
+import com.google.devtools.build.lib.actions.ActionExecutionContext;
+import com.google.devtools.build.lib.actions.ActionExecutionException;
+import com.google.devtools.build.lib.actions.ActionInput;
+import com.google.devtools.build.lib.actions.ActionOwner;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.Artifact.MiddlemanExpander;
+import com.google.devtools.build.lib.actions.ArtifactResolver;
+import com.google.devtools.build.lib.actions.ExecException;
+import com.google.devtools.build.lib.actions.Executor;
+import com.google.devtools.build.lib.actions.ResourceSet;
+import com.google.devtools.build.lib.actions.extra.CppCompileInfo;
+import com.google.devtools.build.lib.actions.extra.ExtraActionInfo;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.analysis.config.PerLabelOptions;
+import com.google.devtools.build.lib.collect.CollectionUtils;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadCompatible;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.events.EventKind;
+import com.google.devtools.build.lib.profiler.Profiler;
+import com.google.devtools.build.lib.profiler.ProfilerTask;
+import com.google.devtools.build.lib.rules.cpp.CcToolchainFeatures.FeatureConfiguration;
+import com.google.devtools.build.lib.rules.cpp.CppConfiguration.Tool;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.util.DependencySet;
+import com.google.devtools.build.lib.util.FileType;
+import com.google.devtools.build.lib.util.Fingerprint;
+import com.google.devtools.build.lib.util.OS;
+import com.google.devtools.build.lib.util.Pair;
+import com.google.devtools.build.lib.util.ShellEscaper;
+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.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+/**
+ * Action that represents some kind of C++ compilation step.
+ */
+@ThreadCompatible
+public class CppCompileAction extends AbstractAction implements IncludeScannable {
+ /**
+ * Represents logic that determines which artifacts, if any, should be added to the actual inputs
+ * for each included file (in addition to the included file itself)
+ */
+ public interface IncludeResolver {
+ /**
+ * Returns the set of files to be added for an included file (as returned in the .d file)
+ */
+ Iterable<Artifact> getInputsForIncludedFile(
+ Artifact includedFile, ArtifactResolver artifactResolver);
+ }
+
+ public static final IncludeResolver VOID_INCLUDE_RESOLVER = new IncludeResolver() {
+ @Override
+ public Iterable<Artifact> getInputsForIncludedFile(Artifact includedFile,
+ ArtifactResolver artifactResolver) {
+ return ImmutableList.of();
+ }
+ };
+
+ private static final int VALIDATION_DEBUG = 0; // 0==none, 1==warns/errors, 2==all
+ private static final boolean VALIDATION_DEBUG_WARN = VALIDATION_DEBUG >= 1;
+
+ /**
+ * A string constant for the c compilation action.
+ */
+ public static final String C_COMPILE = "c-compile";
+
+ /**
+ * A string constant for the c++ compilation action.
+ */
+ public static final String CPP_COMPILE = "c++-compile";
+
+ /**
+ * A string constant for the c++ header parsing.
+ */
+ public static final String CPP_HEADER_PARSING = "c++-header-parsing";
+
+ /**
+ * A string constant for the c++ header preprocessing.
+ */
+ public static final String CPP_HEADER_PREPROCESSING = "c++-header-preprocessing";
+
+ /**
+ * A string constant for the c++ module compilation action.
+ * Note: currently we don't support C module compilation.
+ */
+ public static final String CPP_MODULE_COMPILE = "c++-module-compile";
+
+ /**
+ * A string constant for the preprocessing assembler action.
+ */
+ public static final String PREPROCESS_ASSEMBLE = "preprocess-assemble";
+
+
+ private final BuildConfiguration configuration;
+ protected final Artifact outputFile;
+ private final Label sourceLabel;
+ private final Artifact dwoFile;
+ private final Artifact optionalSourceFile;
+ private final NestedSet<Artifact> mandatoryInputs;
+ private final CppCompilationContext context;
+ private final Collection<PathFragment> extraSystemIncludePrefixes;
+ private final Iterable<IncludeScannable> lipoScannables;
+ private final CppCompileCommandLine cppCompileCommandLine;
+ private final boolean enableLayeringCheck;
+ private final boolean compileHeaderModules;
+
+ @VisibleForTesting
+ final CppConfiguration cppConfiguration;
+ private final Class<? extends CppCompileActionContext> actionContext;
+ private final IncludeResolver includeResolver;
+
+ /**
+ * Identifier for the actual execution time behavior of the action.
+ *
+ * <p>Required because the behavior of this class can be modified by injecting code in the
+ * constructor or by inheritance, and we want to have different cache keys for those.
+ */
+ private final UUID actionClassId;
+
+ private boolean inputsKnown = false;
+
+ /**
+ * Set when the action prepares for execution. Used to preserve state between preparation and
+ * execution.
+ */
+ private Collection<? extends ActionInput> additionalInputs = null;
+
+ /**
+ * Creates a new action to compile C/C++ source files.
+ *
+ * @param owner the owner of the action, usually the configured target that
+ * emitted it
+ * @param sourceFile the source file that should be compiled. {@code mandatoryInputs} must
+ * contain this file
+ * @param sourceLabel the label of the rule the source file is generated by
+ * @param mandatoryInputs any additional files that need to be present for the
+ * compilation to succeed, can be empty but not null, for example, extra sources for FDO.
+ * @param outputFile the object file that is written as result of the
+ * compilation, or the fake object for {@link FakeCppCompileAction}s
+ * @param dotdFile the .d file that is generated as a side-effect of
+ * compilation
+ * @param gcnoFile the coverage notes that are written in coverage mode, can
+ * be null
+ * @param dwoFile the .dwo output file where debug information is stored for Fission
+ * builds (null if Fission mode is disabled)
+ * @param optionalSourceFile an additional optional source file (null if unneeded)
+ * @param configuration the build configurations
+ * @param context the compilation context
+ * @param copts options for the compiler
+ * @param coptsFilter regular expression to remove options from {@code copts}
+ * @param compileHeaderModules whether to compile C++ header modules
+ */
+ protected CppCompileAction(ActionOwner owner,
+ // TODO(bazel-team): Eventually we will remove 'features'; all functionality in 'features'
+ // will be provided by 'featureConfiguration'.
+ ImmutableList<String> features,
+ FeatureConfiguration featureConfiguration,
+ Artifact sourceFile,
+ Label sourceLabel,
+ NestedSet<Artifact> mandatoryInputs,
+ Artifact outputFile,
+ DotdFile dotdFile,
+ @Nullable Artifact gcnoFile,
+ @Nullable Artifact dwoFile,
+ Artifact optionalSourceFile,
+ BuildConfiguration configuration,
+ CppConfiguration cppConfiguration,
+ CppCompilationContext context,
+ Class<? extends CppCompileActionContext> actionContext,
+ ImmutableList<String> copts,
+ ImmutableList<String> pluginOpts,
+ Predicate<String> coptsFilter,
+ ImmutableList<PathFragment> extraSystemIncludePrefixes,
+ boolean enableLayeringCheck,
+ @Nullable String fdoBuildStamp,
+ IncludeResolver includeResolver,
+ Iterable<IncludeScannable> lipoScannables,
+ UUID actionClassId,
+ boolean compileHeaderModules) {
+ // getInputs() method is overridden in this class so we pass a dummy empty
+ // list to the AbstractAction constructor in place of a real input collection.
+ super(owner,
+ Artifact.NO_ARTIFACTS,
+ CollectionUtils.asListWithoutNulls(outputFile, dotdFile.artifact(),
+ gcnoFile, dwoFile));
+ this.configuration = configuration;
+ this.sourceLabel = sourceLabel;
+ this.outputFile = Preconditions.checkNotNull(outputFile);
+ this.dwoFile = dwoFile;
+ this.optionalSourceFile = optionalSourceFile;
+ this.context = context;
+ this.extraSystemIncludePrefixes = extraSystemIncludePrefixes;
+ this.enableLayeringCheck = enableLayeringCheck;
+ this.includeResolver = includeResolver;
+ this.cppConfiguration = cppConfiguration;
+ if (cppConfiguration != null && !cppConfiguration.shouldScanIncludes()) {
+ inputsKnown = true;
+ }
+ this.cppCompileCommandLine = new CppCompileCommandLine(sourceFile, dotdFile,
+ context.getCppModuleMap(), copts, coptsFilter, pluginOpts,
+ (gcnoFile != null), features, featureConfiguration, fdoBuildStamp);
+ this.actionContext = actionContext;
+ this.lipoScannables = lipoScannables;
+ this.actionClassId = actionClassId;
+ this.compileHeaderModules = compileHeaderModules;
+
+ // We do not need to include the middleman artifact since it is a generated
+ // artifact and will definitely exist prior to this action execution.
+ this.mandatoryInputs = mandatoryInputs;
+ setInputs(createInputs(mandatoryInputs, context.getCompilationPrerequisites(),
+ optionalSourceFile));
+ }
+
+ private static NestedSet<Artifact> createInputs(
+ NestedSet<Artifact> mandatoryInputs,
+ Set<Artifact> prerequisites, Artifact optionalSourceFile) {
+ NestedSetBuilder<Artifact> builder = NestedSetBuilder.stableOrder();
+ if (optionalSourceFile != null) {
+ builder.add(optionalSourceFile);
+ }
+ builder.addAll(prerequisites);
+ builder.addTransitive(mandatoryInputs);
+ return builder.build();
+ }
+
+ public boolean shouldScanIncludes() {
+ return cppConfiguration.shouldScanIncludes();
+ }
+
+ @Override
+ public List<PathFragment> getBuiltInIncludeDirectories() {
+ return cppConfiguration.getBuiltInIncludeDirectories();
+ }
+
+ public String getHostSystemName() {
+ return cppConfiguration.getHostSystemName();
+ }
+
+ @Override
+ public NestedSet<Artifact> getMandatoryInputs() {
+ return mandatoryInputs;
+ }
+
+ @Override
+ public boolean inputsKnown() {
+ return inputsKnown;
+ }
+
+ /**
+ * Returns the list of additional inputs found by dependency discovery, during action preparation,
+ * and clears the stored list. {@link #prepare} must be called before this method is called, on
+ * each action execution.
+ */
+ public Collection<? extends ActionInput> getAdditionalInputs() {
+ Collection<? extends ActionInput> result = Preconditions.checkNotNull(additionalInputs);
+ additionalInputs = null;
+ return result;
+ }
+
+ @Override
+ public boolean discoversInputs() {
+ return true;
+ }
+
+ @Override
+ public void discoverInputs(ActionExecutionContext actionExecutionContext)
+ throws ActionExecutionException, InterruptedException {
+ Executor executor = actionExecutionContext.getExecutor();
+ try {
+ this.additionalInputs = executor.getContext(CppCompileActionContext.class)
+ .findAdditionalInputs(this, actionExecutionContext);
+ } catch (ExecException e) {
+ throw e.toActionExecutionException("Include scanning of rule '" + getOwner().getLabel() + "'",
+ executor.getVerboseFailures(), this);
+ }
+ }
+
+ @Override
+ public Artifact getPrimaryInput() {
+ return getSourceFile();
+ }
+
+ @Override
+ public Artifact getPrimaryOutput() {
+ return getOutputFile();
+ }
+
+ /**
+ * Returns the path of the c/cc source for gcc.
+ */
+ public final Artifact getSourceFile() {
+ return cppCompileCommandLine.sourceFile;
+ }
+
+ /**
+ * Returns the path where gcc should put its result.
+ */
+ public Artifact getOutputFile() {
+ return outputFile;
+ }
+
+ /**
+ * Returns the path of the debug info output file (when debug info is
+ * spliced out of the .o file via fission).
+ */
+ @Nullable
+ Artifact getDwoFile() {
+ return dwoFile;
+ }
+
+ protected PathFragment getInternalOutputFile() {
+ return outputFile.getExecPath();
+ }
+
+ @VisibleForTesting
+ public List<String> getPluginOpts() {
+ return cppCompileCommandLine.pluginOpts;
+ }
+
+ Collection<PathFragment> getExtraSystemIncludePrefixes() {
+ return extraSystemIncludePrefixes;
+ }
+
+ @Override
+ public Map<Artifact, Path> getLegalGeneratedScannerFileMap() {
+ Map<Artifact, Path> legalOuts = new HashMap<>();
+
+ for (Artifact a : context.getDeclaredIncludeSrcs()) {
+ if (!a.isSourceArtifact()) {
+ legalOuts.put(a, null);
+ }
+ }
+ for (Pair<Artifact, Artifact> pregreppedSrcs : context.getPregreppedHeaders()) {
+ Artifact hdr = pregreppedSrcs.getFirst();
+ Preconditions.checkState(!hdr.isSourceArtifact(), hdr);
+ legalOuts.put(hdr, pregreppedSrcs.getSecond().getPath());
+ }
+ return Collections.unmodifiableMap(legalOuts);
+ }
+
+ /**
+ * Returns the path where gcc should put the discovered dependency
+ * information.
+ */
+ public DotdFile getDotdFile() {
+ return cppCompileCommandLine.dotdFile;
+ }
+
+ protected boolean needsIncludeScanning(Executor executor) {
+ return executor.getContext(actionContext).needsIncludeScanning();
+ }
+
+ @Override
+ public String describeStrategy(Executor executor) {
+ return executor.getContext(actionContext).strategyLocality();
+ }
+
+ @VisibleForTesting
+ public CppCompilationContext getContext() {
+ return context;
+ }
+
+ @Override
+ public List<PathFragment> getQuoteIncludeDirs() {
+ return context.getQuoteIncludeDirs();
+ }
+
+ @Override
+ public List<PathFragment> getIncludeDirs() {
+ ImmutableList.Builder<PathFragment> result = ImmutableList.builder();
+ result.addAll(context.getIncludeDirs());
+ for (String opt : cppCompileCommandLine.copts) {
+ if (opt.startsWith("-I") && opt.length() > 2) {
+ // We insist on the combined form "-Idir".
+ result.add(new PathFragment(opt.substring(2)));
+ }
+ }
+ return result.build();
+ }
+
+ @Override
+ public List<PathFragment> getSystemIncludeDirs() {
+ ImmutableList.Builder<PathFragment> result = ImmutableList.builder();
+ result.addAll(context.getSystemIncludeDirs());
+ for (String opt : cppCompileCommandLine.copts) {
+ if (opt.startsWith("-isystem") && opt.length() > 8) {
+ // We insist on the combined form "-isystemdir".
+ result.add(new PathFragment(opt.substring(8)));
+ }
+ }
+ return result.build();
+ }
+
+ @Override
+ public List<String> getCmdlineIncludes() {
+ ImmutableList.Builder<String> cmdlineIncludes = ImmutableList.builder();
+ List<String> args = getArgv();
+ for (Iterator<String> argi = args.iterator(); argi.hasNext();) {
+ String arg = argi.next();
+ if (arg.equals("-include") && argi.hasNext()) {
+ cmdlineIncludes.add(argi.next());
+ }
+ }
+ return cmdlineIncludes.build();
+ }
+
+ @Override
+ public Collection<Artifact> getIncludeScannerSources() {
+ NestedSetBuilder<Artifact> builder = NestedSetBuilder.stableOrder();
+ // For every header module we use for the build we need the set of sources that it can
+ // reference.
+ builder.addAll(context.getTransitiveHeaderModuleSrcs());
+ if (CppFileTypes.CPP_MODULE_MAP.matches(getSourceFile().getPath())) {
+ // If this is an action that compiles the header module itself, the source we build is the
+ // module map, and we need to include-scan all headers that are referenced in the module map.
+ // We need to do include scanning as long as we want to support building code bases that are
+ // not fully strict layering clean.
+ builder.addAll(context.getHeaderModuleSrcs());
+ } else {
+ builder.add(getSourceFile());
+ }
+ return builder.build().toCollection();
+ }
+
+ @Override
+ public Iterable<IncludeScannable> getAuxiliaryScannables() {
+ return lipoScannables;
+ }
+
+ /**
+ * Returns the list of "-D" arguments that should be used by this gcc
+ * invocation. Only used for testing.
+ */
+ @VisibleForTesting
+ public ImmutableCollection<String> getDefines() {
+ return context.getDefines();
+ }
+
+ /**
+ * Returns an (immutable) map of environment key, value pairs to be
+ * provided to the C++ compiler.
+ */
+ public ImmutableMap<String, String> getEnvironment() {
+ Map<String, String> environment =
+ new LinkedHashMap<>(configuration.getDefaultShellEnvironment());
+ if (configuration.isCodeCoverageEnabled()) {
+ environment.put("PWD", "/proc/self/cwd");
+ }
+ if (OS.getCurrent() == OS.WINDOWS) {
+ // TODO(bazel-team): Both GCC and clang rely on their execution directories being on
+ // PATH, otherwise they fail to find dependent DLLs (and they fail silently...). On
+ // the other hand, Windows documentation says that the directory of the executable
+ // is always searched for DLLs first. Not sure what to make of it.
+ // Other options are to forward the system path (brittle), or to add a PATH field to
+ // the crosstool file.
+ environment.put("PATH", cppConfiguration.getToolPathFragment(Tool.GCC).getParentDirectory()
+ .getPathString());
+ }
+ return ImmutableMap.copyOf(environment);
+ }
+
+ /**
+ * Returns a new, mutable list of command and arguments (argv) to be passed
+ * to the gcc subprocess.
+ */
+ public final List<String> getArgv() {
+ return getArgv(getInternalOutputFile());
+ }
+
+ protected final List<String> getArgv(PathFragment outputFile) {
+ return cppCompileCommandLine.getArgv(outputFile);
+ }
+
+ @Override
+ public ExtraActionInfo.Builder getExtraActionInfo() {
+ CppCompileInfo.Builder info = CppCompileInfo.newBuilder();
+ info.setTool(cppConfiguration.getToolPathFragment(Tool.GCC).getPathString());
+ for (String option : getCompilerOptions()) {
+ info.addCompilerOption(option);
+ }
+ info.setOutputFile(outputFile.getExecPathString());
+ info.setSourceFile(getSourceFile().getExecPathString());
+ if (inputsKnown()) {
+ info.addAllSourcesAndHeaders(Artifact.toExecPaths(getInputs()));
+ } else {
+ info.addSourcesAndHeaders(getSourceFile().getExecPathString());
+ info.addAllSourcesAndHeaders(
+ Artifact.toExecPaths(context.getDeclaredIncludeSrcs()));
+ }
+
+ return super.getExtraActionInfo()
+ .setExtension(CppCompileInfo.cppCompileInfo, info.build());
+ }
+
+ /**
+ * Returns the compiler options.
+ */
+ @VisibleForTesting
+ public List<String> getCompilerOptions() {
+ return cppCompileCommandLine.getCompilerOptions();
+ }
+
+ /**
+ * Enforce that the includes actually visited during the compile were properly
+ * declared in the rules.
+ *
+ * <p>The technique is to walk through all of the reported includes that gcc
+ * emits into the .d file, and verify that they came from acceptable
+ * relative include directories. This is done in two steps:
+ *
+ * <p>First, each included file is stripped of any include path prefix from
+ * {@code quoteIncludeDirs} to produce an effective relative include dir+name.
+ *
+ * <p>Second, the remaining directory is looked up in {@code declaredIncludeDirs},
+ * a list of acceptable dirs. This list contains a set of dir fragments that
+ * have been calculated by the configured target to be allowable for inclusion
+ * by this source. If no match is found, an error is reported and an exception
+ * is thrown.
+ *
+ * @throws ActionExecutionException iff there was an undeclared dependency
+ */
+ @VisibleForTesting
+ public void validateInclusions(
+ MiddlemanExpander middlemanExpander, EventHandler eventHandler)
+ throws ActionExecutionException {
+ if (!cppConfiguration.shouldScanIncludes() || !inputsKnown()) {
+ return;
+ }
+
+ IncludeProblems errors = new IncludeProblems();
+ IncludeProblems warnings = new IncludeProblems();
+ Set<Artifact> allowedIncludes = new HashSet<>();
+ for (Artifact input : mandatoryInputs) {
+ if (input.isMiddlemanArtifact()) {
+ middlemanExpander.expand(input, allowedIncludes);
+ }
+ allowedIncludes.add(input);
+ }
+
+ if (optionalSourceFile != null) {
+ allowedIncludes.add(optionalSourceFile);
+ }
+ List<PathFragment> cxxSystemIncludeDirs =
+ cppConfiguration.getBuiltInIncludeDirectories();
+ Iterable<PathFragment> ignoreDirs = Iterables.concat(cxxSystemIncludeDirs,
+ extraSystemIncludePrefixes, context.getSystemIncludeDirs());
+
+ // Copy the sets to hash sets for fast contains checking.
+ // Avoid immutable sets here to limit memory churn.
+ Set<PathFragment> declaredIncludeDirs = Sets.newHashSet(context.getDeclaredIncludeDirs());
+ Set<PathFragment> warnIncludeDirs = Sets.newHashSet(context.getDeclaredIncludeWarnDirs());
+ Set<Artifact> declaredIncludeSrcs = Sets.newHashSet(context.getDeclaredIncludeSrcs());
+ for (Artifact input : getInputs()) {
+ if (context.getCompilationPrerequisites().contains(input)
+ || allowedIncludes.contains(input)) {
+ continue; // ignore our fixed source in mandatoryInput: we just want includes
+ }
+ // Ignore headers from built-in include directories.
+ if (FileSystemUtils.startsWithAny(input.getExecPath(), ignoreDirs)) {
+ continue;
+ }
+ if (!isDeclaredIn(input, declaredIncludeDirs, declaredIncludeSrcs)) {
+ // This call can never match the declared include sources (they would be matched above).
+ // There are no declared include sources we need to warn about, so use an empty set here.
+ if (isDeclaredIn(input, warnIncludeDirs, ImmutableSet.<Artifact>of())) {
+ warnings.add(input.getPath().toString());
+ } else {
+ errors.add(input.getPath().toString());
+ }
+ }
+ }
+ if (VALIDATION_DEBUG_WARN) {
+ synchronized (System.err) {
+ if (VALIDATION_DEBUG >= 2 || errors.hasProblems() || warnings.hasProblems()) {
+ if (errors.hasProblems()) {
+ System.err.println("ERROR: Include(s) were not in declared srcs:");
+ } else if (warnings.hasProblems()) {
+ System.err.println("WARN: Include(s) were not in declared srcs:");
+ } else {
+ System.err.println("INFO: Include(s) were OK for '" + getSourceFile()
+ + "', declared srcs:");
+ }
+ for (Artifact a : context.getDeclaredIncludeSrcs()) {
+ System.err.println(" '" + a.toDetailString() + "'");
+ }
+ System.err.println(" or under declared dirs:");
+ for (PathFragment f : Sets.newTreeSet(context.getDeclaredIncludeDirs())) {
+ System.err.println(" '" + f + "'");
+ }
+ System.err.println(" or under declared warn dirs:");
+ for (PathFragment f : Sets.newTreeSet(context.getDeclaredIncludeWarnDirs())) {
+ System.err.println(" '" + f + "'");
+ }
+ System.err.println(" with prefixes:");
+ for (PathFragment dirpath : context.getQuoteIncludeDirs()) {
+ System.err.println(" '" + dirpath + "'");
+ }
+ }
+ }
+ }
+
+ if (warnings.hasProblems()) {
+ eventHandler.handle(
+ new Event(EventKind.WARNING,
+ getOwner().getLocation(), warnings.getMessage(this, getSourceFile()),
+ Label.print(getOwner().getLabel())));
+ }
+ errors.assertProblemFree(this, getSourceFile());
+ }
+
+ /**
+ * Returns true if an included artifact is declared in a set of allowed
+ * include directories. The simple case is that the artifact's parent
+ * directory is contained in the set, or is empty.
+ *
+ * <p>This check also supports a wildcard suffix of '**' for the cases where the
+ * calculations are inexact.
+ *
+ * <p>It also handles unseen non-nested-package subdirs by walking up the path looking
+ * for matches.
+ */
+ private static boolean isDeclaredIn(Artifact input, Set<PathFragment> declaredIncludeDirs,
+ Set<Artifact> declaredIncludeSrcs) {
+ // First check if it's listed in "srcs". If so, then its declared & OK.
+ if (declaredIncludeSrcs.contains(input)) {
+ return true;
+ }
+ // If it's a derived artifact, then it MUST be listed in "srcs" as checked above.
+ // We define derived here as being not source and not under the include link tree.
+ if (!input.isSourceArtifact()
+ && !input.getRoot().getExecPath().getBaseName().equals("include")) {
+ return false;
+ }
+ // Need to do dir/package matching: first try a quick exact lookup.
+ PathFragment includeDir = input.getRootRelativePath().getParentDirectory();
+ if (includeDir.segmentCount() == 0 || declaredIncludeDirs.contains(includeDir)) {
+ return true; // OK: quick exact match.
+ }
+ // Not found in the quick lookup: try the wildcards.
+ for (PathFragment declared : declaredIncludeDirs) {
+ if (declared.getBaseName().equals("**")) {
+ if (includeDir.startsWith(declared.getParentDirectory())) {
+ return true; // OK: under a wildcard dir.
+ }
+ }
+ }
+ // Still not found: see if it is in a subdir of a declared package.
+ Path root = input.getRoot().getPath();
+ for (Path dir = input.getPath().getParentDirectory();;) {
+ if (dir.getRelative("BUILD").exists()) {
+ return false; // Bad: this is a sub-package, not a subdir of a declared package.
+ }
+ dir = dir.getParentDirectory();
+ if (dir.equals(root)) {
+ return false; // Bad: at the top, give up.
+ }
+ if (declaredIncludeDirs.contains(dir.relativeTo(root))) {
+ return true; // OK: found under a declared dir.
+ }
+ }
+ }
+
+ /**
+ * Recalculates this action's live input collection, including sources, middlemen.
+ *
+ * @throws ActionExecutionException iff any errors happen during update.
+ */
+ @VisibleForTesting
+ @ThreadCompatible
+ public final void updateActionInputs(Path execRoot,
+ ArtifactResolver artifactResolver, CppCompileActionContext.Reply reply)
+ throws ActionExecutionException {
+ if (!cppConfiguration.shouldScanIncludes()) {
+ return;
+ }
+ inputsKnown = false;
+ NestedSetBuilder<Artifact> inputs = NestedSetBuilder.stableOrder();
+ Profiler.instance().startTask(ProfilerTask.ACTION_UPDATE, this);
+ try {
+ inputs.addTransitive(mandatoryInputs);
+ if (optionalSourceFile != null) {
+ inputs.add(optionalSourceFile);
+ }
+ inputs.addAll(context.getCompilationPrerequisites());
+ populateActionInputs(execRoot, artifactResolver, reply, inputs);
+ inputsKnown = true;
+ } finally {
+ Profiler.instance().completeTask(ProfilerTask.ACTION_UPDATE);
+ synchronized (this) {
+ setInputs(inputs.build());
+ }
+ }
+ }
+
+ private DependencySet processDepset(Path execRoot, CppCompileActionContext.Reply reply)
+ throws IOException {
+ DependencySet depSet = new DependencySet(execRoot);
+
+ // artifact() is null if we are not using in-memory .d files. We also want to prepare for the
+ // case where we expected an in-memory .d file, but we did not get an appropriate response.
+ // Perhaps we produced the file locally.
+ if (getDotdFile().artifact() != null || reply == null) {
+ return depSet.read(getDotdFile().getPath());
+ } else {
+ // This is an in-memory .d file.
+ return depSet.process(reply.getContents());
+ }
+ }
+
+ /**
+ * Populates the given ordered collection with additional input artifacts
+ * relevant to the specific action implementation.
+ *
+ * <p>The default implementation updates this Action's input set by reading
+ * dynamically-discovered dependency information out of the .d file.
+ *
+ * <p>Artifacts are considered inputs but not "mandatory" inputs.
+ *
+ *
+ * @param reply the reply from the compilation.
+ * @param inputs the ordered collection of inputs to append to
+ * @throws ActionExecutionException iff the .d is missing, malformed or has
+ * unresolvable included artifacts.
+ */
+ @ThreadCompatible
+ private void populateActionInputs(Path execRoot,
+ ArtifactResolver artifactResolver, CppCompileActionContext.Reply reply,
+ NestedSetBuilder<Artifact> inputs)
+ throws ActionExecutionException {
+ try {
+ // Read .d file.
+ DependencySet depSet = processDepset(execRoot, reply);
+
+ // Determine prefixes of allowed absolute inclusions.
+ CppConfiguration toolchain = cppConfiguration;
+ List<PathFragment> systemIncludePrefixes = new ArrayList<>();
+ for (PathFragment includePath : toolchain.getBuiltInIncludeDirectories()) {
+ if (includePath.isAbsolute()) {
+ systemIncludePrefixes.add(includePath);
+ }
+ }
+ systemIncludePrefixes.addAll(extraSystemIncludePrefixes);
+
+ // Check inclusions.
+ IncludeProblems problems = new IncludeProblems();
+ Map<PathFragment, Artifact> allowedDerivedInputsMap = getAllowedDerivedInputsMap();
+ for (PathFragment execPath : depSet.getDependencies()) {
+ if (execPath.isAbsolute()) {
+ // Absolute includes from system paths are ignored.
+ if (FileSystemUtils.startsWithAny(execPath, systemIncludePrefixes)) {
+ continue;
+ }
+ // Since gcc is given only relative paths on the command line,
+ // non-system include paths here should never be absolute. If they
+ // are, it's probably due to a non-hermetic #include, & we should stop
+ // the build with an error.
+ if (execPath.startsWith(execRoot.asFragment())) {
+ execPath = execPath.relativeTo(execRoot.asFragment()); // funky but tolerable path
+ } else {
+ problems.add(execPath.getPathString());
+ continue;
+ }
+ }
+ Artifact artifact = allowedDerivedInputsMap.get(execPath);
+ if (artifact == null) {
+ artifact = artifactResolver.resolveSourceArtifact(execPath);
+ }
+ if (artifact != null) {
+ inputs.add(artifact);
+ // In some cases, execution backends need extra files for each included file. Add them
+ // to the set of actual inputs.
+ inputs.addAll(includeResolver.getInputsForIncludedFile(artifact, artifactResolver));
+ } else {
+ // Abort if we see files that we can't resolve, likely caused by
+ // undeclared includes or illegal include constructs.
+ problems.add(execPath.getPathString());
+ }
+ }
+ problems.assertProblemFree(this, getSourceFile());
+ } catch (IOException e) {
+ // Some kind of IO or parse exception--wrap & rethrow it to stop the build.
+ throw new ActionExecutionException("error while parsing .d file", e, this, false);
+ }
+ }
+
+ @Override
+ public void updateInputsFromCache(
+ ArtifactResolver artifactResolver, Collection<PathFragment> inputPaths) {
+ // Note that this method may trigger a violation of the desirable invariant that getInputs()
+ // is a superset of getMandatoryInputs(). See bug about an "action not in canonical form"
+ // error message and the integration test test_crosstool_change_and_failure().
+
+ Map<PathFragment, Artifact> allowedDerivedInputsMap = getAllowedDerivedInputsMap();
+ List<Artifact> inputs = new ArrayList<>();
+ for (PathFragment execPath : inputPaths) {
+ // The artifact may be a derived artifact, and if it has been created already, then we still
+ // want to keep it to preserve incrementality.
+ Artifact artifact = allowedDerivedInputsMap.get(execPath);
+ if (artifact == null) {
+ artifact = artifactResolver.resolveSourceArtifact(execPath);
+ }
+ // If PathFragment cannot be resolved into the artifact - ignore it. This could happen if
+ // rule definition has changed and action no longer depends on, e.g., additional source file
+ // in the separate package and that package is no longer referenced anywhere else.
+ // It is safe to ignore such paths because dependency checker would identify change in inputs
+ // (ignored path was used before) and will force action execution.
+ if (artifact != null) {
+ inputs.add(artifact);
+ }
+ }
+ inputsKnown = true;
+ synchronized (this) {
+ setInputs(inputs);
+ }
+ }
+
+ private Map<PathFragment, Artifact> getAllowedDerivedInputsMap() {
+ Map<PathFragment, Artifact> allowedDerivedInputMap = new HashMap<>();
+ addToMap(allowedDerivedInputMap, mandatoryInputs);
+ addToMap(allowedDerivedInputMap, context.getDeclaredIncludeSrcs());
+ addToMap(allowedDerivedInputMap, context.getCompilationPrerequisites());
+ Artifact artifact = getSourceFile();
+ if (!artifact.isSourceArtifact()) {
+ allowedDerivedInputMap.put(artifact.getExecPath(), artifact);
+ }
+ return allowedDerivedInputMap;
+ }
+
+ private void addToMap(Map<PathFragment, Artifact> map, Iterable<Artifact> artifacts) {
+ for (Artifact artifact : artifacts) {
+ if (!artifact.isSourceArtifact()) {
+ map.put(artifact.getExecPath(), artifact);
+ }
+ }
+ }
+
+ @Override
+ protected String getRawProgressMessage() {
+ return "Compiling " + getSourceFile().prettyPrint();
+ }
+
+ /**
+ * Return the directories in which to look for headers (pertains to headers
+ * not specifically listed in {@code declaredIncludeSrcs}). The return value
+ * may contain duplicate elements.
+ */
+ public NestedSet<PathFragment> getDeclaredIncludeDirs() {
+ return context.getDeclaredIncludeDirs();
+ }
+
+ /**
+ * Return the directories in which to look for headers and issue a warning.
+ * (pertains to headers not specifically listed in {@code
+ * declaredIncludeSrcs}). The return value may contain duplicate elements.
+ */
+ public NestedSet<PathFragment> getDeclaredIncludeWarnDirs() {
+ return context.getDeclaredIncludeWarnDirs();
+ }
+
+ /**
+ * Return explicit header files (i.e., header files explicitly listed). The
+ * return value may contain duplicate elements.
+ */
+ public NestedSet<Artifact> getDeclaredIncludeSrcs() {
+ return context.getDeclaredIncludeSrcs();
+ }
+
+ /**
+ * Return explicit header files (i.e., header files explicitly listed) in an order
+ * that is stable between builds.
+ */
+ protected final List<PathFragment> getDeclaredIncludeSrcsInStableOrder() {
+ List<PathFragment> paths = new ArrayList<>();
+ for (Artifact declaredIncludeSrc : context.getDeclaredIncludeSrcs()) {
+ paths.add(declaredIncludeSrc.getExecPath());
+ }
+ Collections.sort(paths); // Order is not important, but stability is.
+ return paths;
+ }
+
+ @Override
+ public ResourceSet estimateResourceConsumption(Executor executor) {
+ return executor.getContext(actionContext).estimateResourceConsumption(this);
+ }
+
+ @VisibleForTesting
+ public Class<? extends CppCompileActionContext> getActionContext() {
+ return actionContext;
+ }
+
+ /**
+ * Estimate resource consumption when this action is executed locally.
+ */
+ public ResourceSet estimateResourceConsumptionLocal() {
+ // We use a local compile, so much of the time is spent waiting for IO,
+ // but there is still significant CPU; hence we estimate 50% cpu usage.
+ return new ResourceSet(/*memoryMb=*/200, /*cpuUsage=*/0.5, /*ioUsage=*/0.0);
+ }
+
+ @Override
+ public String computeKey() {
+ Fingerprint f = new Fingerprint();
+ f.addUUID(actionClassId);
+ f.addStrings(getArgv());
+
+ /*
+ * getArgv() above captures all changes which affect the compilation
+ * command and hence the contents of the object file. But we need to
+ * also make sure that we reexecute the action if any of the fields
+ * that affect whether validateIncludes() will report an error or warning
+ * have changed, otherwise we might miss some errors.
+ */
+ f.addPaths(context.getDeclaredIncludeDirs());
+ f.addPaths(context.getDeclaredIncludeWarnDirs());
+ f.addPaths(getDeclaredIncludeSrcsInStableOrder());
+ f.addPaths(getExtraSystemIncludePrefixes());
+ return f.hexDigestAndReset();
+ }
+
+ @Override
+ @ThreadCompatible
+ public void execute(
+ ActionExecutionContext actionExecutionContext)
+ throws ActionExecutionException, InterruptedException {
+ Executor executor = actionExecutionContext.getExecutor();
+ CppCompileActionContext.Reply reply;
+ try {
+ reply = executor.getContext(actionContext).execWithReply(this, actionExecutionContext);
+ } catch (ExecException e) {
+ throw e.toActionExecutionException("C++ compilation of rule '" + getOwner().getLabel() + "'",
+ executor.getVerboseFailures(), this);
+ }
+ ensureCoverageNotesFilesExist();
+ IncludeScanningContext scanningContext = executor.getContext(IncludeScanningContext.class);
+ updateActionInputs(executor.getExecRoot(), scanningContext.getArtifactResolver(), reply);
+ reply = null; // Clear in-memory .d files early.
+ validateInclusions(actionExecutionContext.getMiddlemanExpander(), executor.getEventHandler());
+ }
+
+ /**
+ * Gcc only creates ".gcno" files if the compilation unit is non-empty.
+ * To ensure that the set of outputs for a CppCompileAction remains consistent
+ * and doesn't vary dynamically depending on the _contents_ of the input files,
+ * we create empty ".gcno" files if gcc didn't create them.
+ */
+ private void ensureCoverageNotesFilesExist() throws ActionExecutionException {
+ for (Artifact output : getOutputs()) {
+ if (CppFileTypes.COVERAGE_NOTES.matches(output.getFilename()) // ".gcno"
+ && !output.getPath().exists()) {
+ try {
+ FileSystemUtils.createEmptyFile(output.getPath());
+ } catch (IOException e) {
+ throw new ActionExecutionException(
+ "Error creating file '" + output.getPath() + "': " + e.getMessage(), e, this, false);
+ }
+ }
+ }
+ }
+
+ /**
+ * Provides list of include files needed for performing extra actions on this action when run
+ * remotely. The list of include files is created by performing a header scan on the known input
+ * files.
+ */
+ @Override
+ public Iterable<Artifact> getInputFilesForExtraAction(
+ ActionExecutionContext actionExecutionContext)
+ throws ActionExecutionException, InterruptedException {
+ Collection<Artifact> scannedIncludes =
+ actionExecutionContext.getExecutor().getContext(actionContext)
+ .getScannedIncludeFiles(this, actionExecutionContext);
+ // Use a set to eliminate duplicates.
+ ImmutableSet.Builder<Artifact> result = ImmutableSet.builder();
+ return result.addAll(getInputs()).addAll(scannedIncludes).build();
+ }
+
+ @Override
+ public String getMnemonic() { return "CppCompile"; }
+
+ @Override
+ public String describeKey() {
+ StringBuilder message = new StringBuilder();
+ message.append(getProgressMessage());
+ message.append('\n');
+ message.append(" Command: ");
+ message.append(
+ ShellEscaper.escapeString(cppConfiguration.getLdExecutable().getPathString()));
+ message.append('\n');
+ // Outputting one argument per line makes it easier to diff the results.
+ for (String argument : ShellEscaper.escapeAll(getArgv())) {
+ message.append(" Argument: ");
+ message.append(argument);
+ message.append('\n');
+ }
+
+ for (PathFragment path : context.getDeclaredIncludeDirs()) {
+ message.append(" Declared include directory: ");
+ message.append(ShellEscaper.escapeString(path.getPathString()));
+ message.append('\n');
+ }
+
+ for (PathFragment path : getDeclaredIncludeSrcsInStableOrder()) {
+ message.append(" Declared include source: ");
+ message.append(ShellEscaper.escapeString(path.getPathString()));
+ message.append('\n');
+ }
+
+ for (PathFragment path : getExtraSystemIncludePrefixes()) {
+ message.append(" Extra system include prefix: ");
+ message.append(ShellEscaper.escapeString(path.getPathString()));
+ message.append('\n');
+ }
+ return message.toString();
+ }
+
+ /**
+ * The compile command line for the enclosing C++ compile action.
+ */
+ public final class CppCompileCommandLine {
+ private final Artifact sourceFile;
+ private final DotdFile dotdFile;
+ private final CppModuleMap cppModuleMap;
+ private final List<String> copts;
+ private final Predicate<String> coptsFilter;
+ private final List<String> pluginOpts;
+ private final boolean isInstrumented;
+ private final Collection<String> features;
+ private final FeatureConfiguration featureConfiguration;
+
+ // The value of the BUILD_FDO_TYPE macro to be defined on command line
+ @Nullable private final String fdoBuildStamp;
+
+ public CppCompileCommandLine(Artifact sourceFile, DotdFile dotdFile, CppModuleMap cppModuleMap,
+ ImmutableList<String> copts, Predicate<String> coptsFilter,
+ ImmutableList<String> pluginOpts, boolean isInstrumented,
+ Collection<String> features, FeatureConfiguration featureConfiguration,
+ @Nullable String fdoBuildStamp) {
+ this.sourceFile = Preconditions.checkNotNull(sourceFile);
+ this.dotdFile = Preconditions.checkNotNull(dotdFile);
+ this.cppModuleMap = cppModuleMap;
+ this.copts = Preconditions.checkNotNull(copts);
+ this.coptsFilter = coptsFilter;
+ this.pluginOpts = Preconditions.checkNotNull(pluginOpts);
+ this.isInstrumented = isInstrumented;
+ this.features = Preconditions.checkNotNull(features);
+ this.featureConfiguration = featureConfiguration;
+ this.fdoBuildStamp = fdoBuildStamp;
+ }
+
+ protected List<String> getArgv(PathFragment outputFile) {
+ List<String> commandLine = new ArrayList<>();
+
+ // first: The command name.
+ commandLine.add(cppConfiguration.getToolPathFragment(Tool.GCC).getPathString());
+
+ // second: The compiler options.
+ commandLine.addAll(getCompilerOptions());
+
+ // third: The file to compile!
+ commandLine.add("-c");
+ commandLine.add(sourceFile.getExecPathString());
+
+ // finally: The output file. (Prefixed with -o).
+ commandLine.add("-o");
+ commandLine.add(outputFile.getPathString());
+
+ return commandLine;
+ }
+
+ private String getActionName() {
+ PathFragment sourcePath = sourceFile.getExecPath();
+ if (CppFileTypes.CPP_MODULE_MAP.matches(sourcePath)) {
+ return CPP_MODULE_COMPILE;
+ } else if (CppFileTypes.CPP_HEADER.matches(sourcePath)) {
+ // TODO(bazel-team): Handle C headers that probably don't work in C++ mode.
+ // TODO(bazel-team): Replace use of features.contains with featureConfiguration.isEnabled
+ // here (the other instances will need to stay with the current feature selection process
+ // until all crosstool configurations have been converted).
+ if (features.contains(CppRuleClasses.PARSE_HEADERS)) {
+ return CPP_HEADER_PARSING;
+ } else if (features.contains(CppRuleClasses.PREPROCESS_HEADERS)) {
+ return CPP_HEADER_PREPROCESSING;
+ } else {
+ // CcCommon.collectCAndCppSources() ensures we do not add headers to
+ // the compilation artifacts unless either 'parse_headers' or
+ // 'preprocess_headers' is set.
+ throw new IllegalStateException();
+ }
+ } else if (CppFileTypes.C_SOURCE.matches(sourcePath)) {
+ return C_COMPILE;
+ } else if (CppFileTypes.CPP_SOURCE.matches(sourcePath)) {
+ return CPP_COMPILE;
+ } else if (CppFileTypes.ASSEMBLER_WITH_C_PREPROCESSOR.matches(sourcePath)) {
+ return PREPROCESS_ASSEMBLE;
+ }
+ // CcLibraryHelper ensures CppCompileAction only gets instantiated for supported file types.
+ throw new IllegalStateException();
+ }
+
+ public List<String> getCompilerOptions() {
+ List<String> options = new ArrayList<>();
+
+ // TODO(bazel-team): Extract combinations of options into sections in the CROSSTOOL file.
+ if (CppFileTypes.CPP_MODULE_MAP.matches(sourceFile.getExecPath())) {
+ options.add("-x");
+ options.add("c++");
+ } else if (CppFileTypes.CPP_HEADER.matches(sourceFile.getExecPath())) {
+ // TODO(bazel-team): Read the compiler flag settings out of the CROSSTOOL file.
+ // TODO(bazel-team): Handle C headers that probably don't work in C++ mode.
+ if (features.contains(CppRuleClasses.PARSE_HEADERS)) {
+ options.add("-x");
+ options.add("c++-header");
+ // Specifying -x c++-header will make clang/gcc create precompiled
+ // headers, which we suppress with -fsyntax-only.
+ options.add("-fsyntax-only");
+ } else if (features.contains(CppRuleClasses.PREPROCESS_HEADERS)) {
+ options.add("-E");
+ options.add("-x");
+ options.add("c++");
+ } else {
+ // CcCommon.collectCAndCppSources() ensures we do not add headers to
+ // the compilation artifacts unless either 'parse_headers' or
+ // 'preprocess_headers' is set.
+ throw new IllegalStateException();
+ }
+ }
+
+ for (PathFragment quoteIncludePath : context.getQuoteIncludeDirs()) {
+ // "-iquote" is a gcc-specific option. For C compilers that don't support "-iquote",
+ // we should instead use "-I".
+ options.add("-iquote");
+ options.add(quoteIncludePath.getSafePathString());
+ }
+ for (PathFragment includePath : context.getIncludeDirs()) {
+ options.add("-I" + includePath.getSafePathString());
+ }
+ for (PathFragment systemIncludePath : context.getSystemIncludeDirs()) {
+ options.add("-isystem");
+ options.add(systemIncludePath.getSafePathString());
+ }
+
+ CppConfiguration toolchain = cppConfiguration;
+
+ // pluginOpts has to be added before defaultCopts because -fplugin must precede -plugin-arg.
+ options.addAll(pluginOpts);
+ addFilteredOptions(options, toolchain.getCompilerOptions(features));
+
+ // Enable instrumentation if requested.
+ if (isInstrumented) {
+ addFilteredOptions(options, ImmutableList.of("-fprofile-arcs", "-ftest-coverage"));
+ }
+
+ String sourceFilename = sourceFile.getExecPathString();
+ if (CppFileTypes.C_SOURCE.matches(sourceFilename)) {
+ addFilteredOptions(options, toolchain.getCOptions());
+ }
+ if (CppFileTypes.CPP_SOURCE.matches(sourceFilename)
+ || CppFileTypes.CPP_HEADER.matches(sourceFilename)
+ || CppFileTypes.CPP_MODULE_MAP.matches(sourceFilename)) {
+ addFilteredOptions(options, toolchain.getCxxOptions(features));
+ }
+
+ // Users don't expect the explicit copts to be filtered by coptsFilter, add them verbatim.
+ options.addAll(copts);
+
+ for (String warn : cppConfiguration.getCWarns()) {
+ options.add("-W" + warn);
+ }
+ for (String define : context.getDefines()) {
+ options.add("-D" + define);
+ }
+
+ // Stamp FDO builds with FDO subtype string
+ if (fdoBuildStamp != null) {
+ options.add("-D" + CppConfiguration.FDO_STAMP_MACRO + "=\"" + fdoBuildStamp + "\"");
+ }
+
+ options.addAll(toolchain.getUnfilteredCompilerOptions(features));
+
+ // GCC gives randomized names to symbols which are defined in
+ // an anonymous namespace but have external linkage. To make
+ // computation of these deterministic, we want to override the
+ // default seed for the random number generator. It's safe to use
+ // any value which differs for all translation units; we use the
+ // path to the object file.
+ options.add("-frandom-seed=" + outputFile.getExecPathString());
+
+ // Add the options of --per_file_copt, if the label or the base name of the source file
+ // matches the specified regular expression filter.
+ for (PerLabelOptions perLabelOptions : cppConfiguration.getPerFileCopts()) {
+ if ((sourceLabel != null && perLabelOptions.isIncluded(sourceLabel))
+ || perLabelOptions.isIncluded(sourceFile)) {
+ options.addAll(perLabelOptions.getOptions());
+ }
+ }
+
+ // Enable <object>.d file generation.
+ if (dotdFile != null) {
+ // Gcc options:
+ // -MD turns on .d file output as a side-effect (doesn't imply -E)
+ // -MM[D] enables user includes only, not system includes
+ // -MF <name> specifies the dotd file name
+ // Issues:
+ // -M[M] alone subverts actual .o output (implies -E)
+ // -M[M]D alone breaks some of the .d naming assumptions
+ // This combination gets user and system includes with specified name:
+ // -MD -MF <name>
+ options.add("-MD");
+ options.add("-MF");
+ options.add(dotdFile.getSafeExecPath().getPathString());
+ }
+
+ if (cppModuleMap != null && (compileHeaderModules || enableLayeringCheck)) {
+ options.add("-Xclang-only=-fmodule-maps");
+ options.add("-Xclang-only=-fmodule-name=" + cppModuleMap.getName());
+ options.add("-Xclang-only=-fmodule-map-file="
+ + cppModuleMap.getArtifact().getExecPathString());
+ options.add("-Xclang=-fno-modules-implicit-maps");
+
+ if (compileHeaderModules) {
+ options.add("-Xclang-only=-fmodules");
+ if (CppFileTypes.CPP_MODULE_MAP.matches(sourceFilename)) {
+ options.add("-Xclang=-emit-module");
+ options.add("-Xcrosstool-module-compilation");
+ }
+ // Select .pcm inputs to pass on the command line depending on whether
+ // we are in pic or non-pic mode.
+ // TODO(bazel-team): We want to add these to the compile even if the
+ // current target is not built as a module; currently that implies
+ // passing -fmodules to the compiler, which is experimental; thus, we
+ // do not use the header modules files for now if the current
+ // compilation is not modules enabled on its own.
+ boolean pic = copts.contains("-fPIC");
+ for (Artifact source : context.getAdditionalInputs()) {
+ if ((pic && source.getFilename().endsWith(".pic.pcm")) || (!pic
+ && !source.getFilename().endsWith(".pic.pcm")
+ && source.getFilename().endsWith(".pcm"))) {
+ options.add("-Xclang=-fmodule-file=" + source.getExecPathString());
+ }
+ }
+ }
+ if (enableLayeringCheck) {
+ options.add("-Xclang-only=-fmodules-strict-decluse");
+ }
+ }
+
+ if (FileType.contains(outputFile, CppFileTypes.ASSEMBLER, CppFileTypes.PIC_ASSEMBLER)) {
+ options.add("-S");
+ } else if (FileType.contains(outputFile, CppFileTypes.PREPROCESSED_C,
+ CppFileTypes.PREPROCESSED_CPP, CppFileTypes.PIC_PREPROCESSED_C,
+ CppFileTypes.PIC_PREPROCESSED_CPP)) {
+ options.add("-E");
+ }
+
+ if (cppConfiguration.useFission()) {
+ options.add("-gsplit-dwarf");
+ }
+
+ options.addAll(featureConfiguration.getCommandLine(getActionName(),
+ ImmutableMultimap.<String, String>of()));
+ return options;
+ }
+
+ // For each option in 'in', add it to 'out' unless it is matched by the 'coptsFilter' regexp.
+ private void addFilteredOptions(List<String> out, List<String> in) {
+ Iterables.addAll(out, Iterables.filter(in, coptsFilter));
+ }
+ }
+
+ /**
+ * A reference to a .d file. There are two modes:
+ * <ol>
+ * <li>an Artifact that represents a real on-disk file
+ * <li>just an execPath that refers to a virtual .d file that is not written to disk
+ * </ol>
+ */
+ public static class DotdFile {
+ private final Artifact artifact;
+ private final PathFragment execPath;
+
+ public DotdFile(Artifact artifact) {
+ this.artifact = artifact;
+ this.execPath = null;
+ }
+
+ public DotdFile(PathFragment execPath) {
+ this.artifact = null;
+ this.execPath = execPath;
+ }
+
+ /**
+ * @return the Artifact or null
+ */
+ public Artifact artifact() {
+ return artifact;
+ }
+
+ /**
+ * @return Gets the execPath regardless of whether this is a real Artifact
+ */
+ public PathFragment getSafeExecPath() {
+ return execPath == null ? artifact.getExecPath() : execPath;
+ }
+
+ /**
+ * @return the on-disk location of the .d file or null
+ */
+ public Path getPath() {
+ return artifact.getPath();
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppCompileActionBuilder.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppCompileActionBuilder.java
new file mode 100644
index 0000000..0d8da3e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppCompileActionBuilder.java
@@ -0,0 +1,439 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Functions;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.ActionOwner;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.Root;
+import com.google.devtools.build.lib.analysis.AnalysisEnvironment;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.rules.cpp.CcToolchainFeatures.FeatureConfiguration;
+import com.google.devtools.build.lib.rules.cpp.CppCompileAction.DotdFile;
+import com.google.devtools.build.lib.rules.cpp.CppCompileAction.IncludeResolver;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.util.FileType;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.UUID;
+import java.util.regex.Pattern;
+
+/**
+ * Builder class to construct C++ compile actions.
+ */
+public class CppCompileActionBuilder {
+ public static final UUID GUID = UUID.fromString("cee5db0a-d2ad-4c69-9b81-97c936a29075");
+
+ private final ActionOwner owner;
+ private final List<String> features = new ArrayList<>();
+ private CcToolchainFeatures.FeatureConfiguration featureConfiguration;
+ private final Artifact sourceFile;
+ private final Label sourceLabel;
+ private final NestedSetBuilder<Artifact> mandatoryInputsBuilder;
+ private NestedSetBuilder<Artifact> pluginInputsBuilder;
+ private Artifact optionalSourceFile;
+ private Artifact outputFile;
+ private PathFragment tempOutputFile;
+ private DotdFile dotdFile;
+ private Artifact gcnoFile;
+ private final BuildConfiguration configuration;
+ private CppCompilationContext context = CppCompilationContext.EMPTY;
+ private final List<String> copts = new ArrayList<>();
+ private final List<String> pluginOpts = new ArrayList<>();
+ private final List<Pattern> nocopts = new ArrayList<>();
+ private AnalysisEnvironment analysisEnvironment;
+ private ImmutableList<PathFragment> extraSystemIncludePrefixes = ImmutableList.of();
+ private boolean enableLayeringCheck;
+ private boolean compileHeaderModules;
+ private String fdoBuildStamp;
+ private IncludeResolver includeResolver = CppCompileAction.VOID_INCLUDE_RESOLVER;
+ private UUID actionClassId = GUID;
+ private Class<? extends CppCompileActionContext> actionContext;
+ private CppConfiguration cppConfiguration;
+ private ImmutableMap<Artifact, IncludeScannable> lipoScannableMap;
+
+ /**
+ * Creates a builder from a rule. This also uses the configuration and
+ * artifact factory from the rule.
+ */
+ public CppCompileActionBuilder(RuleContext ruleContext, Artifact sourceFile, Label sourceLabel) {
+ this.owner = ruleContext.getActionOwner();
+ this.actionContext = CppCompileActionContext.class;
+ this.cppConfiguration = ruleContext.getFragment(CppConfiguration.class);
+ this.analysisEnvironment = ruleContext.getAnalysisEnvironment();
+ this.sourceFile = sourceFile;
+ this.sourceLabel = sourceLabel;
+ this.configuration = ruleContext.getConfiguration();
+ this.mandatoryInputsBuilder = NestedSetBuilder.stableOrder();
+ this.pluginInputsBuilder = NestedSetBuilder.stableOrder();
+ this.lipoScannableMap = getLipoScannableMap(ruleContext);
+
+ features.addAll(ruleContext.getFeatures());
+ }
+
+ private static ImmutableMap<Artifact, IncludeScannable> getLipoScannableMap(
+ RuleContext ruleContext) {
+ if (!ruleContext.getFragment(CppConfiguration.class).isLipoOptimization()) {
+ return null;
+ }
+
+ LipoContextProvider provider = ruleContext.getPrerequisite(
+ ":lipo_context_collector", Mode.DONT_CHECK, LipoContextProvider.class);
+ return provider.getIncludeScannables();
+ }
+
+ /**
+ * Creates a builder for an owner that is not required to be rule.
+ */
+ public CppCompileActionBuilder(
+ ActionOwner owner, AnalysisEnvironment analysisEnvironment, Artifact sourceFile,
+ Label sourceLabel, BuildConfiguration configuration) {
+ this.owner = owner;
+ this.actionContext = CppCompileActionContext.class;
+ this.cppConfiguration = configuration.getFragment(CppConfiguration.class);
+ this.analysisEnvironment = analysisEnvironment;
+ this.sourceFile = sourceFile;
+ this.sourceLabel = sourceLabel;
+ this.configuration = configuration;
+ this.mandatoryInputsBuilder = NestedSetBuilder.stableOrder();
+ this.pluginInputsBuilder = NestedSetBuilder.stableOrder();
+ this.lipoScannableMap = ImmutableMap.of();
+ }
+
+ /**
+ * Creates a builder that is a copy of another builder.
+ */
+ public CppCompileActionBuilder(CppCompileActionBuilder other) {
+ this.owner = other.owner;
+ this.features.addAll(other.features);
+ this.featureConfiguration = other.featureConfiguration;
+ this.sourceFile = other.sourceFile;
+ this.sourceLabel = other.sourceLabel;
+ this.mandatoryInputsBuilder = NestedSetBuilder.<Artifact>stableOrder()
+ .addTransitive(other.mandatoryInputsBuilder.build());
+ this.pluginInputsBuilder = NestedSetBuilder.<Artifact>stableOrder()
+ .addTransitive(other.pluginInputsBuilder.build());
+ this.optionalSourceFile = other.optionalSourceFile;
+ this.outputFile = other.outputFile;
+ this.tempOutputFile = other.tempOutputFile;
+ this.dotdFile = other.dotdFile;
+ this.gcnoFile = other.gcnoFile;
+ this.configuration = other.configuration;
+ this.context = other.context;
+ this.copts.addAll(other.copts);
+ this.pluginOpts.addAll(other.pluginOpts);
+ this.nocopts.addAll(other.nocopts);
+ this.analysisEnvironment = other.analysisEnvironment;
+ this.extraSystemIncludePrefixes = ImmutableList.copyOf(other.extraSystemIncludePrefixes);
+ this.enableLayeringCheck = other.enableLayeringCheck;
+ this.compileHeaderModules = other.compileHeaderModules;
+ this.includeResolver = other.includeResolver;
+ this.actionClassId = other.actionClassId;
+ this.actionContext = other.actionContext;
+ this.cppConfiguration = other.cppConfiguration;
+ this.lipoScannableMap = other.lipoScannableMap;
+ }
+
+ public PathFragment getTempOutputFile() {
+ return tempOutputFile;
+ }
+
+ public Artifact getSourceFile() {
+ return sourceFile;
+ }
+
+ public CppCompilationContext getContext() {
+ return context;
+ }
+
+ public NestedSet<Artifact> getMandatoryInputs() {
+ return mandatoryInputsBuilder.build();
+ }
+
+ /**
+ * Returns the .dwo output file that matches the specified .o output file. If Fission mode
+ * isn't enabled for this build, this is null (we don't produce .dwo files in that case).
+ */
+ private static Artifact getDwoFile(Artifact outputFile, AnalysisEnvironment artifactFactory,
+ CppConfiguration cppConfiguration) {
+
+ // Only create .dwo's for .o compilations (i.e. not .ii or .S).
+ boolean isObjectOutput = CppFileTypes.OBJECT_FILE.matches(outputFile.getExecPath())
+ || CppFileTypes.PIC_OBJECT_FILE.matches(outputFile.getExecPath());
+
+ // Note configurations can be null for tests.
+ if (cppConfiguration != null && cppConfiguration.useFission() && isObjectOutput) {
+ return artifactFactory.getDerivedArtifact(
+ FileSystemUtils.replaceExtension(outputFile.getRootRelativePath(), ".dwo"),
+ outputFile.getRoot());
+ } else {
+ return null;
+ }
+ }
+
+ private static Predicate<String> getNocoptPredicate(Collection<Pattern> patterns) {
+ final ImmutableList<Pattern> finalPatterns = ImmutableList.copyOf(patterns);
+ if (finalPatterns.isEmpty()) {
+ return Predicates.alwaysTrue();
+ } else {
+ return new Predicate<String>() {
+ @Override
+ public boolean apply(String option) {
+ for (Pattern pattern : finalPatterns) {
+ if (pattern.matcher(option).matches()) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ };
+ }
+ }
+
+ private Iterable<IncludeScannable> getLipoScannables(NestedSet<Artifact> realMandatoryInputs) {
+ return lipoScannableMap == null ? ImmutableList.<IncludeScannable>of() : Iterables.filter(
+ Iterables.transform(
+ Iterables.filter(
+ FileType.filter(
+ realMandatoryInputs,
+ CppFileTypes.C_SOURCE, CppFileTypes.CPP_SOURCE,
+ CppFileTypes.ASSEMBLER_WITH_C_PREPROCESSOR),
+ Predicates.not(Predicates.equalTo(getSourceFile()))),
+ Functions.forMap(lipoScannableMap, null)),
+ Predicates.notNull());
+ }
+
+ /**
+ * Builds the Action as configured and returns the to be generated Artifact.
+ *
+ * <p>This method may be called multiple times to create multiple compile
+ * actions (usually after calling some setters to modify the generated
+ * action).
+ */
+ public CppCompileAction build() {
+ // Configuration can be null in tests.
+ NestedSetBuilder<Artifact> realMandatoryInputsBuilder = NestedSetBuilder.compileOrder();
+ realMandatoryInputsBuilder.addTransitive(mandatoryInputsBuilder.build());
+ if (tempOutputFile == null && configuration != null
+ && !configuration.getFragment(CppConfiguration.class).shouldScanIncludes()) {
+ realMandatoryInputsBuilder.addTransitive(context.getDeclaredIncludeSrcs());
+ }
+ realMandatoryInputsBuilder.addTransitive(context.getAdditionalInputs());
+ realMandatoryInputsBuilder.addTransitive(pluginInputsBuilder.build());
+ realMandatoryInputsBuilder.add(sourceFile);
+ boolean fake = tempOutputFile != null;
+
+ // Copying the collections is needed to make the builder reusable.
+ if (fake) {
+ return new FakeCppCompileAction(owner, ImmutableList.copyOf(features), featureConfiguration,
+ sourceFile, sourceLabel, realMandatoryInputsBuilder.build(), outputFile, tempOutputFile,
+ dotdFile, configuration, cppConfiguration, context, ImmutableList.copyOf(copts),
+ ImmutableList.copyOf(pluginOpts), getNocoptPredicate(nocopts),
+ extraSystemIncludePrefixes, enableLayeringCheck, fdoBuildStamp);
+ } else {
+ NestedSet<Artifact> realMandatoryInputs = realMandatoryInputsBuilder.build();
+
+ return new CppCompileAction(owner, ImmutableList.copyOf(features), featureConfiguration,
+ sourceFile, sourceLabel, realMandatoryInputs, outputFile, dotdFile,
+ gcnoFile, getDwoFile(outputFile, analysisEnvironment, cppConfiguration),
+ optionalSourceFile, configuration, cppConfiguration, context,
+ actionContext, ImmutableList.copyOf(copts),
+ ImmutableList.copyOf(pluginOpts),
+ getNocoptPredicate(nocopts),
+ extraSystemIncludePrefixes, enableLayeringCheck, fdoBuildStamp,
+ includeResolver, getLipoScannables(realMandatoryInputs), actionClassId,
+ compileHeaderModules);
+ }
+ }
+
+ /**
+ * Sets the feature configuration to be used for the action.
+ */
+ public CppCompileActionBuilder setFeatureConfiguration(
+ FeatureConfiguration featureConfiguration) {
+ this.featureConfiguration = featureConfiguration;
+ return this;
+ }
+
+ public CppCompileActionBuilder setIncludeResolver(IncludeResolver includeResolver) {
+ this.includeResolver = includeResolver;
+ return this;
+ }
+
+ public CppCompileActionBuilder setCppConfiguration(CppConfiguration cppConfiguration) {
+ this.cppConfiguration = cppConfiguration;
+ return this;
+ }
+
+ public CppCompileActionBuilder setActionContext(
+ Class<? extends CppCompileActionContext> actionContext) {
+ this.actionContext = actionContext;
+ return this;
+ }
+
+ public CppCompileActionBuilder setActionClassId(UUID uuid) {
+ this.actionClassId = uuid;
+ return this;
+ }
+
+ public CppCompileActionBuilder setExtraSystemIncludePrefixes(
+ Collection<PathFragment> extraSystemIncludePrefixes) {
+ this.extraSystemIncludePrefixes = ImmutableList.copyOf(extraSystemIncludePrefixes);
+ return this;
+ }
+
+ public CppCompileActionBuilder addPluginInput(Artifact artifact) {
+ pluginInputsBuilder.add(artifact);
+ return this;
+ }
+
+ public CppCompileActionBuilder clearPluginInputs() {
+ pluginInputsBuilder = NestedSetBuilder.stableOrder();
+ return this;
+ }
+
+ /**
+ * Set an optional source file (usually with metadata of the main source file). The optional
+ * source file can only be set once, whether via this method or through the constructor
+ * {@link #CppCompileActionBuilder(CppCompileActionBuilder)}.
+ */
+ public CppCompileActionBuilder addOptionalSourceFile(Artifact artifact) {
+ Preconditions.checkState(optionalSourceFile == null, "%s %s", optionalSourceFile, artifact);
+ optionalSourceFile = artifact;
+ return this;
+ }
+
+ public CppCompileActionBuilder addMandatoryInputs(Iterable<Artifact> artifacts) {
+ mandatoryInputsBuilder.addAll(artifacts);
+ return this;
+ }
+
+ public CppCompileActionBuilder addTransitiveMandatoryInputs(NestedSet<Artifact> artifacts) {
+ mandatoryInputsBuilder.addTransitive(artifacts);
+ return this;
+ }
+
+ public CppCompileActionBuilder setOutputFile(Artifact outputFile) {
+ this.outputFile = outputFile;
+ return this;
+ }
+
+ /**
+ * The temp output file is not an artifact, since it does not appear in the outputs of the
+ * action.
+ *
+ * <p>This is theoretically a problem if that file already existed before, since then Blaze
+ * does not delete it before executing the rule, but 1. that only applies for local
+ * execution which does not happen very often and 2. it is only a problem if the compiler is
+ * affected by the presence of this file, which it should not be.
+ */
+ public CppCompileActionBuilder setTempOutputFile(PathFragment tempOutputFile) {
+ this.tempOutputFile = tempOutputFile;
+ return this;
+ }
+
+ @VisibleForTesting
+ public CppCompileActionBuilder setDotdFileForTesting(Artifact dotdFile) {
+ this.dotdFile = new DotdFile(dotdFile);
+ return this;
+ }
+
+ public CppCompileActionBuilder setDotdFile(PathFragment outputName, String extension,
+ RuleContext ruleContext) {
+ if (configuration.getFragment(CppConfiguration.class).getInmemoryDotdFiles()) {
+ // Just set the path, no artifact is constructed
+ PathFragment file = FileSystemUtils.replaceExtension(outputName, extension);
+ Root root = configuration.getBinDirectory();
+ dotdFile = new DotdFile(root.getExecPath().getRelative(file));
+ } else {
+ dotdFile = new DotdFile(ruleContext.getRelatedArtifact(outputName, extension));
+ }
+ return this;
+ }
+
+ public CppCompileActionBuilder setGcnoFile(Artifact gcnoFile) {
+ this.gcnoFile = gcnoFile;
+ return this;
+ }
+
+ public CppCompileActionBuilder addCopt(String copt) {
+ copts.add(copt);
+ return this;
+ }
+
+ public CppCompileActionBuilder addPluginOpt(String opt) {
+ pluginOpts.add(opt);
+ return this;
+ }
+
+ public CppCompileActionBuilder clearPluginOpts() {
+ pluginOpts.clear();
+ return this;
+ }
+
+ public CppCompileActionBuilder addCopts(Iterable<? extends String> copts) {
+ Iterables.addAll(this.copts, copts);
+ return this;
+ }
+
+ public CppCompileActionBuilder addCopts(int position, Iterable<? extends String> copts) {
+ this.copts.addAll(position, ImmutableList.copyOf(copts));
+ return this;
+ }
+
+ public CppCompileActionBuilder addNocopts(Pattern nocopts) {
+ this.nocopts.add(nocopts);
+ return this;
+ }
+
+ public CppCompileActionBuilder setContext(CppCompilationContext context) {
+ this.context = context;
+ return this;
+ }
+
+ public CppCompileActionBuilder setEnableLayeringCheck(boolean enableLayeringCheck) {
+ this.enableLayeringCheck = enableLayeringCheck;
+ return this;
+ }
+
+ /**
+ * Sets whether the CompileAction should use header modules for its compilation.
+ */
+ public CppCompileActionBuilder setCompileHeaderModules(boolean compileHeaderModules) {
+ this.compileHeaderModules = compileHeaderModules;
+ return this;
+ }
+
+ public CppCompileActionBuilder setFdoBuildStamp(String fdoBuildStamp) {
+ this.fdoBuildStamp = fdoBuildStamp;
+ return this;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppCompileActionContext.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppCompileActionContext.java
new file mode 100644
index 0000000..4bbfa44
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppCompileActionContext.java
@@ -0,0 +1,84 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import com.google.devtools.build.lib.actions.ActionContextMarker;
+import com.google.devtools.build.lib.actions.ActionExecutionContext;
+import com.google.devtools.build.lib.actions.ActionExecutionException;
+import com.google.devtools.build.lib.actions.ActionInput;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.ExecException;
+import com.google.devtools.build.lib.actions.Executor.ActionContext;
+import com.google.devtools.build.lib.actions.ResourceSet;
+
+import java.io.IOException;
+import java.util.Collection;
+
+import javax.annotation.Nullable;
+
+/**
+ * Context for compiling plain C++.
+ */
+@ActionContextMarker(name = "C++")
+public interface CppCompileActionContext extends ActionContext {
+ /**
+ * Reply for the execution of a C++ compilation.
+ */
+ public interface Reply {
+ /**
+ * Returns the contents of the .d file.
+ */
+ byte[] getContents() throws IOException;
+ }
+
+ /** Does include scanning to find the list of files needed to execute the action. */
+ public Collection<? extends ActionInput> findAdditionalInputs(CppCompileAction action,
+ ActionExecutionContext actionExecutionContext)
+ throws ExecException, InterruptedException, ActionExecutionException;
+
+ /**
+ * Executes the given action and return the reply of the executor.
+ */
+ Reply execWithReply(CppCompileAction action,
+ ActionExecutionContext actionExecutionContext) throws ExecException, InterruptedException;
+
+ /**
+ * Returns the executor reply from an exec exception, if available.
+ */
+ @Nullable Reply getReplyFromException(
+ ExecException e, CppCompileAction action);
+
+ /**
+ * Returns the estimated resource consumption of the action.
+ */
+ ResourceSet estimateResourceConsumption(CppCompileAction action);
+
+ /**
+ * Returns where the action actually runs.
+ */
+ String strategyLocality();
+
+ /**
+ * Returns whether include scanning needs to be run.
+ */
+ boolean needsIncludeScanning();
+
+ /**
+ * Returns the include files that should be shipped to the executor in addition the ones that
+ * were declared.
+ */
+ Collection<Artifact> getScannedIncludeFiles(
+ CppCompileAction action, ActionExecutionContext actionExecutionContext)
+ throws ActionExecutionException, InterruptedException;
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppConfiguration.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppConfiguration.java
new file mode 100644
index 0000000..c5cc9a5
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppConfiguration.java
@@ -0,0 +1,1691 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableMap.Builder;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+import com.google.devtools.build.lib.actions.ArtifactFactory;
+import com.google.devtools.build.lib.actions.PackageRootResolver;
+import com.google.devtools.build.lib.actions.Root;
+import com.google.devtools.build.lib.analysis.ViewCreationFailedException;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.analysis.config.BuildOptions;
+import com.google.devtools.build.lib.analysis.config.CompilationMode;
+import com.google.devtools.build.lib.analysis.config.InvalidConfigurationException;
+import com.google.devtools.build.lib.analysis.config.PerLabelOptions;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.rules.cpp.CppConfigurationLoader.CppConfigurationParameters;
+import com.google.devtools.build.lib.rules.cpp.FdoSupport.FdoException;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.syntax.Label.SyntaxException;
+import com.google.devtools.build.lib.syntax.SkylarkCallable;
+import com.google.devtools.build.lib.syntax.SkylarkModule;
+import com.google.devtools.build.lib.util.IncludeScanningUtil;
+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 com.google.devtools.build.lib.view.config.crosstool.CrosstoolConfig;
+import com.google.devtools.build.lib.view.config.crosstool.CrosstoolConfig.LinkingModeFlags;
+import com.google.devtools.build.lib.view.config.crosstool.CrosstoolConfig.LipoMode;
+import com.google.devtools.build.skyframe.SkyFunction.Environment;
+import com.google.devtools.common.options.OptionsParsingException;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.zip.ZipException;
+
+/**
+ * This class represents the C/C++ parts of the {@link BuildConfiguration},
+ * including the host architecture, target architecture, compiler version, and
+ * a standard library version. It has information about the tools locations and
+ * the flags required for compiling.
+ */
+@SkylarkModule(name = "cpp", doc = "A configuration fragment for C++")
+@Immutable
+public class CppConfiguration extends BuildConfiguration.Fragment {
+ /**
+ * An enumeration of all the tools that comprise a toolchain.
+ */
+ public enum Tool {
+ AR("ar"),
+ CPP("cpp"),
+ GCC("gcc"),
+ GCOV("gcov"),
+ GCOVTOOL("gcov-tool"),
+ LD("ld"),
+ NM("nm"),
+ OBJCOPY("objcopy"),
+ OBJDUMP("objdump"),
+ STRIP("strip"),
+ DWP("dwp");
+
+ private final String namePart;
+
+ private Tool(String namePart) {
+ this.namePart = namePart;
+ }
+
+ public String getNamePart() {
+ return namePart;
+ }
+ }
+
+ /**
+ * Values for the --hdrs_check option.
+ */
+ public static enum HeadersCheckingMode {
+ /** Legacy behavior: Silently allow undeclared headers. */
+ LOOSE,
+ /** Warn about undeclared headers. */
+ WARN,
+ /** Disallow undeclared headers. */
+ STRICT
+ }
+
+ /**
+ * --dynamic_mode parses to DynamicModeFlag, but AUTO will be translated based on platform,
+ * resulting in a DynamicMode value.
+ */
+ public enum DynamicMode { OFF, DEFAULT, FULLY }
+
+ /**
+ * This enumeration is used for the --strip option.
+ */
+ public static enum StripMode {
+
+ ALWAYS("always"), // Always strip.
+ SOMETIMES("sometimes"), // Strip iff compilationMode == FASTBUILD.
+ NEVER("never"); // Never strip.
+
+ private final String mode;
+
+ private StripMode(String mode) {
+ this.mode = mode;
+ }
+
+ @Override
+ public String toString() {
+ return mode;
+ }
+ }
+
+ /** Storage for the libc label, if given. */
+ public static class LibcTop implements Serializable {
+ private final Label label;
+
+ LibcTop(Label label) {
+ Preconditions.checkArgument(label != null);
+ this.label = label;
+ }
+
+ public Label getLabel() {
+ return label;
+ }
+
+ public PathFragment getSysroot() {
+ return label.getPackageFragment();
+ }
+
+ @Override
+ public String toString() {
+ return label.toString();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ } else if (other instanceof LibcTop) {
+ return label.equals(((LibcTop) other).label);
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return label.hashCode();
+ }
+ }
+
+ /**
+ * This macro will be passed as a command-line parameter (eg. -DBUILD_FDO_TYPE="LIPO").
+ * For possible values see {@code CppModel.getFdoBuildStamp()}.
+ */
+ public static final String FDO_STAMP_MACRO = "BUILD_FDO_TYPE";
+
+ /**
+ * Represents an optional flag that can be toggled using the package features mechanism.
+ */
+ @VisibleForTesting
+ static class OptionalFlag implements Serializable {
+ private final String name;
+ private final List<String> flags;
+
+ @VisibleForTesting
+ OptionalFlag(String name, List<String> flags) {
+ this.name = name;
+ this.flags = flags;
+ }
+
+ private List<String> getFlags() {
+ return flags;
+ }
+
+ private String getName() {
+ return name;
+ }
+ }
+
+ @VisibleForTesting
+ static class FlagList implements Serializable {
+ private List<String> prefixFlags;
+ private List<OptionalFlag> optionalFlags;
+ private List<String> suffixFlags;
+
+ @VisibleForTesting
+ FlagList(List<String> prefixFlags,
+ List<OptionalFlag> optionalFlags,
+ List<String> suffixFlags) {
+ this.prefixFlags = prefixFlags;
+ this.optionalFlags = optionalFlags;
+ this.suffixFlags = suffixFlags;
+ }
+
+ @VisibleForTesting
+ List<String> evaluate(Collection<String> features) {
+ ImmutableList.Builder<String> result = ImmutableList.builder();
+ result.addAll(prefixFlags);
+ for (OptionalFlag optionalFlag : optionalFlags) {
+ // The flag is added if the default is true and the flag is not specified,
+ // or if the default is false and the flag is specified.
+ if (features.contains(optionalFlag.getName())) {
+ result.addAll(optionalFlag.getFlags());
+ }
+ }
+
+ result.addAll(suffixFlags);
+ return result.build();
+ }
+ }
+
+ private final Label crosstoolTop;
+ private final String hostSystemName;
+ private final String compiler;
+ private final String targetCpu;
+ private final String targetSystemName;
+ private final String targetLibc;
+ private final LipoMode lipoMode;
+ private final PathFragment crosstoolTopPathFragment;
+
+ private final String abi;
+ private final String abiGlibcVersion;
+
+ private final String toolchainIdentifier;
+ private final String cacheKey;
+
+ private final CcToolchainFeatures toolchainFeatures;
+ private final boolean supportsGoldLinker;
+ private final boolean supportsThinArchives;
+ private final boolean supportsStartEndLib;
+ private final boolean supportsInterfaceSharedObjects;
+ private final boolean supportsEmbeddedRuntimes;
+ private final boolean supportsFission;
+
+ // We encode three states with two booleans:
+ // (1) (false false) -> no pic code
+ // (2) (true false) -> shared libraries as pic, but not binaries
+ // (3) (true true) -> both shared libraries and binaries as pic
+ private final boolean toolchainNeedsPic;
+ private final boolean usePicForBinaries;
+
+ private final FdoSupport fdoSupport;
+
+ // TODO(bazel-team): All these labels (except for ccCompilerRuleLabel) can be removed once the
+ // transition to the cc_compiler rule is complete.
+ private final Label libcLabel;
+ private final Label staticRuntimeLibsLabel;
+ private final Label dynamicRuntimeLibsLabel;
+ private final Label ccToolchainLabel;
+
+ private final PathFragment sysroot;
+ private final PathFragment runtimeSysroot;
+ private final List<PathFragment> builtInIncludeDirectories;
+
+ private final Map<String, PathFragment> toolPaths;
+ private final PathFragment ldExecutable;
+
+ // Only used during construction.
+ private final List<String> commonLinkOptions;
+ private final ListMultimap<CompilationMode, String> linkOptionsFromCompilationMode;
+ private final ListMultimap<LipoMode, String> linkOptionsFromLipoMode;
+ private final ListMultimap<LinkingMode, String> linkOptionsFromLinkingMode;
+
+ private final FlagList compilerFlags;
+ private final FlagList cxxFlags;
+ private final FlagList unfilteredCompilerFlags;
+ private final List<String> cOptions;
+
+ private FlagList fullyStaticLinkFlags;
+ private FlagList mostlyStaticLinkFlags;
+ private FlagList mostlyStaticSharedLinkFlags;
+ private FlagList dynamicLinkFlags;
+ private FlagList dynamicLibraryLinkFlags;
+ private final List<String> testOnlyLinkFlags;
+
+ private final List<String> linkOptions;
+
+ private final List<String> objcopyOptions;
+ private final List<String> ldOptions;
+ private final List<String> arOptions;
+ private final List<String> arThinArchivesOptions;
+
+ private final Map<String, String> additionalMakeVariables;
+
+ private final CppOptions cppOptions;
+
+ // The dynamic mode for linking.
+ private final DynamicMode dynamicMode;
+ private final boolean stripBinaries;
+ private final ImmutableMap<String, String> commandLineDefines;
+ private final String solibDirectory;
+ private final CompilationMode compilationMode;
+ private final Path execRoot;
+ /**
+ * If true, the ConfiguredTarget is only used to get the necessary cross-referenced
+ * CppCompilationContexts, but registering build actions is disabled.
+ */
+ private final boolean lipoContextCollector;
+ private final Root greppedIncludesDirectory;
+
+ protected CppConfiguration(CppConfigurationParameters params)
+ throws InvalidConfigurationException {
+ CrosstoolConfig.CToolchain toolchain = params.toolchain;
+ cppOptions = params.buildOptions.get(CppOptions.class);
+ this.hostSystemName = toolchain.getHostSystemName();
+ this.compiler = toolchain.getCompiler();
+ this.targetCpu = toolchain.getTargetCpu();
+ this.lipoMode = cppOptions.getLipoMode();
+ this.targetSystemName = toolchain.getTargetSystemName();
+ this.targetLibc = toolchain.getTargetLibc();
+ this.crosstoolTop = params.crosstoolTop;
+ this.ccToolchainLabel = params.ccToolchainLabel;
+ this.compilationMode =
+ params.buildOptions.get(BuildConfiguration.Options.class).compilationMode;
+ this.lipoContextCollector = cppOptions.lipoCollector;
+ this.execRoot = params.execRoot;
+
+ // Note that the grepped includes directory is not configuration-specific; the paths of the
+ // files within that directory, however, are configuration-specific.
+ this.greppedIncludesDirectory = Root.asDerivedRoot(execRoot,
+ execRoot.getRelative(IncludeScanningUtil.GREPPED_INCLUDES));
+
+ this.crosstoolTopPathFragment = crosstoolTop.getPackageFragment();
+
+ try {
+ this.staticRuntimeLibsLabel =
+ crosstoolTop.getRelative(toolchain.hasStaticRuntimesFilegroup() ?
+ toolchain.getStaticRuntimesFilegroup() : "static-runtime-libs-" + targetCpu);
+ this.dynamicRuntimeLibsLabel =
+ crosstoolTop.getRelative(toolchain.hasDynamicRuntimesFilegroup() ?
+ toolchain.getDynamicRuntimesFilegroup() : "dynamic-runtime-libs-" + targetCpu);
+ } catch (SyntaxException e) {
+ // All of the above label.getRelative() calls are valid labels, and the crosstool_top
+ // was already checked earlier in the process.
+ throw new AssertionError(e);
+ }
+
+ if (cppOptions.lipoMode == LipoMode.BINARY) {
+ // TODO(bazel-team): implement dynamic linking with LIPO
+ this.dynamicMode = DynamicMode.OFF;
+ } else {
+ switch (cppOptions.dynamicMode) {
+ case DEFAULT:
+ this.dynamicMode = DynamicMode.DEFAULT; break;
+ case OFF: this.dynamicMode = DynamicMode.OFF; break;
+ case FULLY: this.dynamicMode = DynamicMode.FULLY; break;
+ default: throw new IllegalStateException("Invalid dynamicMode.");
+ }
+ }
+
+ this.fdoSupport = new FdoSupport(
+ params.buildOptions.get(CppOptions.class).fdoInstrument, params.fdoZip,
+ cppOptions.lipoMode, execRoot);
+
+ this.stripBinaries = (cppOptions.stripBinaries == StripMode.ALWAYS ||
+ (cppOptions.stripBinaries == StripMode.SOMETIMES &&
+ compilationMode == CompilationMode.FASTBUILD));
+
+ CrosstoolConfigurationIdentifier crosstoolConfig =
+ CrosstoolConfigurationIdentifier.fromToolchain(toolchain);
+ Preconditions.checkState(crosstoolConfig.getCpu().equals(targetCpu));
+ Preconditions.checkState(crosstoolConfig.getCompiler().equals(compiler));
+ Preconditions.checkState(crosstoolConfig.getLibc().equals(targetLibc));
+
+ this.solibDirectory = "_solib_" + targetCpu;
+
+ this.toolchainIdentifier = toolchain.getToolchainIdentifier();
+ this.cacheKey = this + ":" + crosstoolTop + ":" + params.cacheKeySuffix + ":"
+ + lipoContextCollector;
+
+ this.toolchainFeatures = new CcToolchainFeatures(toolchain);
+ this.supportsGoldLinker = toolchain.getSupportsGoldLinker();
+ this.supportsThinArchives = toolchain.getSupportsThinArchives();
+ this.supportsStartEndLib = toolchain.getSupportsStartEndLib();
+ this.supportsInterfaceSharedObjects = toolchain.getSupportsInterfaceSharedObjects();
+ this.supportsEmbeddedRuntimes = toolchain.getSupportsEmbeddedRuntimes();
+ this.supportsFission = toolchain.getSupportsFission();
+ this.toolchainNeedsPic = toolchain.getNeedsPic();
+ this.usePicForBinaries =
+ toolchain.getNeedsPic() && compilationMode != CompilationMode.OPT;
+
+ this.toolPaths = Maps.newHashMap();
+ for (CrosstoolConfig.ToolPath tool : toolchain.getToolPathList()) {
+ PathFragment path = new PathFragment(tool.getPath());
+ if (!path.isNormalized()) {
+ throw new IllegalArgumentException("The include path '" + tool.getPath()
+ + "' is not normalized.");
+ }
+ toolPaths.put(tool.getName(), crosstoolTopPathFragment.getRelative(path));
+ }
+
+ if (toolPaths.isEmpty()) {
+ // If no paths are specified, we just use the names of the tools as the path.
+ for (Tool tool : Tool.values()) {
+ toolPaths.put(tool.getNamePart(),
+ crosstoolTopPathFragment.getRelative(tool.getNamePart()));
+ }
+ } else {
+ Iterable<Tool> neededTools = Iterables.filter(EnumSet.allOf(Tool.class),
+ new Predicate<Tool>() {
+ @Override
+ public boolean apply(Tool tool) {
+ if (tool == Tool.DWP) {
+ // When fission is unsupported, don't check for the dwp tool.
+ return supportsFission();
+ } else if (tool == Tool.GCOVTOOL) {
+ // gcov-tool is optional, don't check whether it's present
+ return false;
+ } else {
+ return true;
+ }
+ }
+ });
+ for (Tool tool : neededTools) {
+ if (!toolPaths.containsKey(tool.getNamePart())) {
+ throw new IllegalArgumentException("Tool path for '" + tool.getNamePart()
+ + "' is missing");
+ }
+ }
+ }
+
+ // We can't use an ImmutableMap.Builder here; we need the ability (at least
+ // in tests) to add entries with keys that are already in the map, and only
+ // HashMap supports this (by replacing the existing entry under the key).
+ Map<String, String> commandLineDefinesBuilder = new HashMap<>();
+ for (Map.Entry<String, String> define : cppOptions.commandLineDefinedVariables) {
+ commandLineDefinesBuilder.put(define.getKey(), define.getValue());
+ }
+ commandLineDefines = ImmutableMap.copyOf(commandLineDefinesBuilder);
+
+ ListMultimap<CompilationMode, String> cFlags = ArrayListMultimap.create();
+ ListMultimap<CompilationMode, String> cxxFlags = ArrayListMultimap.create();
+ linkOptionsFromCompilationMode = ArrayListMultimap.create();
+ for (CrosstoolConfig.CompilationModeFlags flags : toolchain.getCompilationModeFlagsList()) {
+ // Remove this when CROSSTOOL files no longer contain 'coverage'.
+ if (flags.getMode() == CrosstoolConfig.CompilationMode.COVERAGE) {
+ continue;
+ }
+ CompilationMode realmode = importCompilationMode(flags.getMode());
+ cFlags.putAll(realmode, flags.getCompilerFlagList());
+ cxxFlags.putAll(realmode, flags.getCxxFlagList());
+ linkOptionsFromCompilationMode.putAll(realmode, flags.getLinkerFlagList());
+ }
+
+ ListMultimap<LipoMode, String> lipoCFlags = ArrayListMultimap.create();
+ ListMultimap<LipoMode, String> lipoCxxFlags = ArrayListMultimap.create();
+ linkOptionsFromLipoMode = ArrayListMultimap.create();
+ for (CrosstoolConfig.LipoModeFlags flags : toolchain.getLipoModeFlagsList()) {
+ LipoMode realmode = flags.getMode();
+ lipoCFlags.putAll(realmode, flags.getCompilerFlagList());
+ lipoCxxFlags.putAll(realmode, flags.getCxxFlagList());
+ linkOptionsFromLipoMode.putAll(realmode, flags.getLinkerFlagList());
+ }
+
+ linkOptionsFromLinkingMode = ArrayListMultimap.create();
+ for (LinkingModeFlags flags : toolchain.getLinkingModeFlagsList()) {
+ LinkingMode realmode = importLinkingMode(flags.getMode());
+ linkOptionsFromLinkingMode.putAll(realmode, flags.getLinkerFlagList());
+ }
+
+ this.commonLinkOptions = ImmutableList.copyOf(toolchain.getLinkerFlagList());
+ dynamicLibraryLinkFlags = new FlagList(
+ ImmutableList.copyOf(toolchain.getDynamicLibraryLinkerFlagList()),
+ convertOptionalOptions(toolchain.getOptionalDynamicLibraryLinkerFlagList()),
+ Collections.<String>emptyList());
+ this.objcopyOptions = ImmutableList.copyOf(toolchain.getObjcopyEmbedFlagList());
+ this.ldOptions = ImmutableList.copyOf(toolchain.getLdEmbedFlagList());
+ this.arOptions = copyOrDefaultIfEmpty(toolchain.getArFlagList(), "rcsD");
+ this.arThinArchivesOptions = copyOrDefaultIfEmpty(
+ toolchain.getArThinArchivesFlagList(), "rcsDT");
+
+ this.abi = toolchain.getAbiVersion();
+ this.abiGlibcVersion = toolchain.getAbiLibcVersion();
+
+ // The default value for optional string attributes is the empty string.
+ PathFragment defaultSysroot = toolchain.getBuiltinSysroot().length() == 0
+ ? null
+ : new PathFragment(toolchain.getBuiltinSysroot());
+ if ((defaultSysroot != null) && !defaultSysroot.isNormalized()) {
+ throw new IllegalArgumentException("The built-in sysroot '" + defaultSysroot
+ + "' is not normalized.");
+ }
+
+ if ((cppOptions.libcTop != null) && (defaultSysroot == null)) {
+ throw new InvalidConfigurationException("The selected toolchain " + toolchainIdentifier
+ + " does not support setting --grte_top.");
+ }
+ LibcTop libcTop = cppOptions.libcTop;
+ if ((libcTop == null) && !toolchain.getDefaultGrteTop().isEmpty()) {
+ try {
+ libcTop = new CppOptions.LibcTopConverter().convert(toolchain.getDefaultGrteTop());
+ } catch (OptionsParsingException e) {
+ throw new InvalidConfigurationException(e.getMessage(), e);
+ }
+ }
+ if ((libcTop != null) && (libcTop.getLabel() != null)) {
+ libcLabel = libcTop.getLabel();
+ } else {
+ libcLabel = null;
+ }
+
+ ImmutableList.Builder<PathFragment> builtInIncludeDirectoriesBuilder
+ = ImmutableList.builder();
+ sysroot = libcTop == null ? defaultSysroot : libcTop.getSysroot();
+ for (String s : toolchain.getCxxBuiltinIncludeDirectoryList()) {
+ builtInIncludeDirectoriesBuilder.add(
+ resolveIncludeDir(s, sysroot, crosstoolTopPathFragment));
+ }
+ builtInIncludeDirectories = builtInIncludeDirectoriesBuilder.build();
+
+ // The runtime sysroot should really be set from --grte_top. However, currently libc has no
+ // way to set the sysroot. The CROSSTOOL file does set the runtime sysroot, in the
+ // builtin_sysroot field. This implies that you can not arbitrarily mix and match Crosstool
+ // and libc versions, you must always choose compatible ones.
+ runtimeSysroot = defaultSysroot;
+
+ String sysrootFlag;
+ if (sysroot != null && !sysroot.equals(defaultSysroot)) {
+ // Only specify the --sysroot option if it is different from the built-in one.
+ sysrootFlag = "--sysroot=" + sysroot;
+ } else {
+ sysrootFlag = null;
+ }
+
+ ImmutableList.Builder<String> unfilteredCoptsBuilder = ImmutableList.builder();
+ if (sysrootFlag != null) {
+ unfilteredCoptsBuilder.add(sysrootFlag);
+ }
+ unfilteredCoptsBuilder.addAll(toolchain.getUnfilteredCxxFlagList());
+ unfilteredCompilerFlags = new FlagList(
+ unfilteredCoptsBuilder.build(),
+ convertOptionalOptions(toolchain.getOptionalUnfilteredCxxFlagList()),
+ Collections.<String>emptyList());
+
+ ImmutableList.Builder<String> linkoptsBuilder = ImmutableList.builder();
+ linkoptsBuilder.addAll(cppOptions.linkoptList);
+ if (cppOptions.experimentalOmitfp) {
+ linkoptsBuilder.add("-Wl,--eh-frame-hdr");
+ }
+ if (sysrootFlag != null) {
+ linkoptsBuilder.add(sysrootFlag);
+ }
+ this.linkOptions = linkoptsBuilder.build();
+
+ ImmutableList.Builder<String> coptsBuilder = ImmutableList.<String>builder()
+ .addAll(toolchain.getCompilerFlagList())
+ .addAll(cFlags.get(compilationMode))
+ .addAll(lipoCFlags.get(cppOptions.getLipoMode()));
+ if (cppOptions.experimentalOmitfp) {
+ coptsBuilder.add("-fomit-frame-pointer");
+ coptsBuilder.add("-fasynchronous-unwind-tables");
+ coptsBuilder.add("-DNO_FRAME_POINTER");
+ }
+ this.compilerFlags = new FlagList(
+ coptsBuilder.build(),
+ convertOptionalOptions(toolchain.getOptionalCompilerFlagList()),
+ cppOptions.coptList);
+
+ this.cOptions = ImmutableList.copyOf(cppOptions.conlyoptList);
+
+ ImmutableList.Builder<String> cxxOptsBuilder = ImmutableList.<String>builder()
+ .addAll(toolchain.getCxxFlagList())
+ .addAll(cxxFlags.get(compilationMode))
+ .addAll(lipoCxxFlags.get(cppOptions.getLipoMode()));
+
+ this.cxxFlags = new FlagList(
+ cxxOptsBuilder.build(),
+ convertOptionalOptions(toolchain.getOptionalCxxFlagList()),
+ cppOptions.cxxoptList);
+
+ this.ldExecutable = getToolPathFragment(CppConfiguration.Tool.LD);
+
+ boolean stripBinaries = (cppOptions.stripBinaries == StripMode.ALWAYS) ||
+ ((cppOptions.stripBinaries == StripMode.SOMETIMES) &&
+ (compilationMode == CompilationMode.FASTBUILD));
+
+ fullyStaticLinkFlags = new FlagList(
+ configureLinkerOptions(compilationMode, lipoMode, LinkingMode.FULLY_STATIC,
+ ldExecutable, stripBinaries),
+ convertOptionalOptions(toolchain.getOptionalLinkerFlagList()),
+ Collections.<String>emptyList());
+ mostlyStaticLinkFlags = new FlagList(
+ configureLinkerOptions(compilationMode, lipoMode, LinkingMode.MOSTLY_STATIC,
+ ldExecutable, stripBinaries),
+ convertOptionalOptions(toolchain.getOptionalLinkerFlagList()),
+ Collections.<String>emptyList());
+ mostlyStaticSharedLinkFlags = new FlagList(
+ configureLinkerOptions(compilationMode, lipoMode,
+ LinkingMode.MOSTLY_STATIC_LIBRARIES, ldExecutable, stripBinaries),
+ convertOptionalOptions(toolchain.getOptionalLinkerFlagList()),
+ Collections.<String>emptyList());
+ dynamicLinkFlags = new FlagList(
+ configureLinkerOptions(compilationMode, lipoMode, LinkingMode.DYNAMIC,
+ ldExecutable, stripBinaries),
+ convertOptionalOptions(toolchain.getOptionalLinkerFlagList()),
+ Collections.<String>emptyList());
+ testOnlyLinkFlags = ImmutableList.copyOf(toolchain.getTestOnlyLinkerFlagList());
+
+ Map<String, String> makeVariablesBuilder = new HashMap<>();
+ // The following are to be used to allow some build rules to avoid the limits on stack frame
+ // sizes and variable-length arrays. Ensure that these are always set.
+ makeVariablesBuilder.put("STACK_FRAME_UNLIMITED", "");
+ makeVariablesBuilder.put("CC_FLAGS", "");
+ for (CrosstoolConfig.MakeVariable variable : toolchain.getMakeVariableList()) {
+ makeVariablesBuilder.put(variable.getName(), variable.getValue());
+ }
+ if (sysrootFlag != null) {
+ String ccFlags = makeVariablesBuilder.get("CC_FLAGS");
+ ccFlags = ccFlags.isEmpty() ? sysrootFlag : ccFlags + " " + sysrootFlag;
+ makeVariablesBuilder.put("CC_FLAGS", ccFlags);
+ }
+ this.additionalMakeVariables = ImmutableMap.copyOf(makeVariablesBuilder);
+ }
+
+ private List<OptionalFlag> convertOptionalOptions(
+ List<CrosstoolConfig.CToolchain.OptionalFlag> optionalFlagList)
+ throws IllegalArgumentException {
+ List<OptionalFlag> result = new ArrayList<>();
+
+ for (CrosstoolConfig.CToolchain.OptionalFlag crosstoolOptionalFlag : optionalFlagList) {
+ String name = crosstoolOptionalFlag.getDefaultSettingName();
+ result.add(new OptionalFlag(
+ name,
+ ImmutableList.copyOf(crosstoolOptionalFlag.getFlagList())));
+ }
+
+ return result;
+ }
+
+ private static ImmutableList<String> copyOrDefaultIfEmpty(List<String> list,
+ String defaultValue) {
+ return list.isEmpty() ? ImmutableList.of(defaultValue) : ImmutableList.copyOf(list);
+ }
+
+ @VisibleForTesting
+ static CompilationMode importCompilationMode(CrosstoolConfig.CompilationMode mode) {
+ return CompilationMode.valueOf(mode.name());
+ }
+
+ @VisibleForTesting
+ static LinkingMode importLinkingMode(CrosstoolConfig.LinkingMode mode) {
+ return LinkingMode.valueOf(mode.name());
+ }
+
+ private static final PathFragment SYSROOT_FRAGMENT = new PathFragment("%sysroot%");
+
+ /**
+ * Resolve the given include directory. If it is not absolute, it is
+ * interpreted relative to the crosstool top. If it starts with %sysroot%/,
+ * that part is replaced with the actual sysroot.
+ */
+ static PathFragment resolveIncludeDir(String s, PathFragment sysroot,
+ PathFragment crosstoolTopPathFragment) {
+ PathFragment path = new PathFragment(s);
+ if (!path.isNormalized()) {
+ throw new IllegalArgumentException("The include path '" + s + "' is not normalized.");
+ }
+ if (path.startsWith(SYSROOT_FRAGMENT)) {
+ if (sysroot == null) {
+ throw new IllegalArgumentException("A %sysroot% prefix is only allowed if the "
+ + "default_sysroot option is set");
+ }
+ return sysroot.getRelative(path.relativeTo(SYSROOT_FRAGMENT));
+ } else {
+ return crosstoolTopPathFragment.getRelative(path);
+ }
+ }
+
+ /**
+ * Returns the configuration-independent grepped-includes directory.
+ */
+ public Root getGreppedIncludesDirectory() {
+ return greppedIncludesDirectory;
+ }
+
+ @VisibleForTesting
+ List<String> configureLinkerOptions(
+ CompilationMode compilationMode, LipoMode lipoMode, LinkingMode linkingMode,
+ PathFragment ldExecutable, boolean stripBinaries) {
+ List<String> result = new ArrayList<>();
+ result.addAll(commonLinkOptions);
+
+ result.add("-B" + ldExecutable.getParentDirectory().getPathString());
+ if (stripBinaries) {
+ result.add("-Wl,-S");
+ }
+
+ result.addAll(linkOptionsFromCompilationMode.get(compilationMode));
+ result.addAll(linkOptionsFromLipoMode.get(lipoMode));
+ result.addAll(linkOptionsFromLinkingMode.get(linkingMode));
+ return ImmutableList.copyOf(result);
+ }
+
+ /**
+ * Returns the toolchain identifier, which uniquely identifies the compiler
+ * version, target libc version, target cpu, and LIPO linkage.
+ */
+ public String getToolchainIdentifier() {
+ return toolchainIdentifier;
+ }
+
+ /**
+ * Returns the system name which is required by the toolchain to run.
+ */
+ public String getHostSystemName() {
+ return hostSystemName;
+ }
+
+ @Override
+ public String toString() {
+ return toolchainIdentifier;
+ }
+
+ /**
+ * Returns the compiler version string (e.g. "gcc-4.1.1").
+ */
+ @SkylarkCallable(name = "compiler", structField = true, doc = "C++ compiler.")
+ public String getCompiler() {
+ return compiler;
+ }
+
+ /**
+ * Returns the libc version string (e.g. "glibc-2.2.2").
+ */
+ public String getTargetLibc() {
+ return targetLibc;
+ }
+
+ /**
+ * Returns the target architecture using blaze-specific constants (e.g. "piii").
+ */
+ @SkylarkCallable(name = "cpu", structField = true, doc = "Target CPU of the C++ toolchain.")
+ public String getTargetCpu() {
+ return targetCpu;
+ }
+
+ /**
+ * Returns the path fragment that is either absolute or relative to the
+ * execution root that can be used to execute the given tool.
+ *
+ * <p>Note that you must not use this method to get the linker location, but
+ * use {@link #getLdExecutable} instead!
+ */
+ public PathFragment getToolPathFragment(CppConfiguration.Tool tool) {
+ return toolPaths.get(tool.getNamePart());
+ }
+
+ /**
+ * Returns a label that forms a dependency to the files required for the
+ * sysroot that is used.
+ */
+ public Label getLibcLabel() {
+ return libcLabel;
+ }
+
+ /**
+ * Returns a label that references the library files needed to statically
+ * link the C++ runtime (i.e. libgcc.a, libgcc_eh.a, libstdc++.a) for the
+ * target architecture.
+ */
+ public Label getStaticRuntimeLibsLabel() {
+ return supportsEmbeddedRuntimes() ? staticRuntimeLibsLabel : null;
+ }
+
+ /**
+ * Returns a label that references the library files needed to dynamically
+ * link the C++ runtime (i.e. libgcc_s.so, libstdc++.so) for the target
+ * architecture.
+ */
+ public Label getDynamicRuntimeLibsLabel() {
+ return supportsEmbeddedRuntimes() ? dynamicRuntimeLibsLabel : null;
+ }
+
+ /**
+ * Returns the label of the <code>cc_compiler</code> rule for the C++ configuration.
+ */
+ public Label getCcToolchainRuleLabel() {
+ return ccToolchainLabel;
+ }
+
+ /**
+ * Returns the abi we're using, which is a gcc version. E.g.: "gcc-3.4".
+ * Note that in practice we might be using gcc-3.4 as ABI even when compiling
+ * with gcc-4.1.0, because ABIs are backwards compatible.
+ */
+ // TODO(bazel-team): The javadoc should clarify how this is used in Blaze.
+ public String getAbi() {
+ return abi;
+ }
+
+ /**
+ * Returns the glibc version used by the abi we're using. This is a
+ * glibc version number (e.g., "2.2.2"). Note that in practice we
+ * might be using glibc 2.2.2 as ABI even when compiling with
+ * gcc-4.2.2, gcc-4.3.1, or gcc-4.4.0 (which use glibc 2.3.6),
+ * because ABIs are backwards compatible.
+ */
+ // TODO(bazel-team): The javadoc should clarify how this is used in Blaze.
+ public String getAbiGlibcVersion() {
+ return abiGlibcVersion;
+ }
+
+ /**
+ * Returns the configured features of the toolchain. Rules should not call this directly, but
+ * instead use {@code CcToolchainProvider.getFeatures}.
+ */
+ public CcToolchainFeatures getFeatures() {
+ return toolchainFeatures;
+ }
+
+ /**
+ * Returns whether the toolchain supports the gold linker.
+ */
+ public boolean supportsGoldLinker() {
+ return supportsGoldLinker;
+ }
+
+ /**
+ * Returns whether the toolchain supports thin archives.
+ */
+ public boolean supportsThinArchives() {
+ return supportsThinArchives;
+ }
+
+ /**
+ * Returns whether the toolchain supports the --start-lib/--end-lib options.
+ */
+ public boolean supportsStartEndLib() {
+ return supportsStartEndLib;
+ }
+
+ /**
+ * Returns whether build_interface_so can build interface shared objects for this toolchain.
+ * Should be true if this toolchain generates ELF objects.
+ */
+ public boolean supportsInterfaceSharedObjects() {
+ return supportsInterfaceSharedObjects;
+ }
+
+ /**
+ * Returns whether the toolchain supports linking C/C++ runtime libraries
+ * supplied inside the toolchain distribution.
+ */
+ public boolean supportsEmbeddedRuntimes() {
+ return supportsEmbeddedRuntimes;
+ }
+
+ /**
+ * Returns whether the toolchain supports EXEC_ORIGIN libraries resolution.
+ */
+ public boolean supportsExecOrigin() {
+ // We're rolling out support for this in the same release that also supports embedded runtimes.
+ return supportsEmbeddedRuntimes;
+ }
+
+ /**
+ * Returns whether the toolchain supports "Fission" C++ builds, i.e. builds
+ * where compilation partitions object code and debug symbols into separate
+ * output files.
+ */
+ public boolean supportsFission() {
+ return supportsFission;
+ }
+
+ /**
+ * Returns whether shared libraries must be compiled with position
+ * independent code on this platform.
+ */
+ public boolean toolchainNeedsPic() {
+ return toolchainNeedsPic;
+ }
+
+ /**
+ * Returns whether binaries must be compiled with position independent code.
+ */
+ public boolean usePicForBinaries() {
+ return usePicForBinaries;
+ }
+
+ /**
+ * Returns the type of archives being used.
+ */
+ public Link.ArchiveType archiveType() {
+ if (useStartEndLib()) {
+ return Link.ArchiveType.START_END_LIB;
+ }
+ if (useThinArchives()) {
+ return Link.ArchiveType.THIN;
+ }
+ return Link.ArchiveType.FAT;
+ }
+
+ /**
+ * Returns the ar flags to be used.
+ */
+ public List<String> getArFlags(boolean thinArchives) {
+ return thinArchives ? arThinArchivesOptions : arOptions;
+ }
+
+ /**
+ * Returns a string that uniquely identifies the toolchain.
+ */
+ @Override
+ public String cacheKey() {
+ return cacheKey;
+ }
+
+ /**
+ * Returns the built-in list of system include paths for the toolchain
+ * compiler. All paths in this list should be relative to the exec directory.
+ * They may be absolute if they are also installed on the remote build nodes or
+ * for local compilation.
+ */
+ public List<PathFragment> getBuiltInIncludeDirectories() {
+ return builtInIncludeDirectories;
+ }
+
+ /**
+ * Returns the sysroot to be used. If the toolchain compiler does not support
+ * different sysroots, or the sysroot is the same as the default sysroot, then
+ * this method returns <code>null</code>.
+ */
+ public PathFragment getSysroot() {
+ return sysroot;
+ }
+
+ /**
+ * Returns the run time sysroot, which is where the dynamic linker
+ * and system libraries are found at runtime. This is usually an absolute path. If the
+ * toolchain compiler does not support sysroots, then this method returns <code>null</code>.
+ */
+ public PathFragment getRuntimeSysroot() {
+ return runtimeSysroot;
+ }
+
+ /**
+ * Returns the default options to use for compiling C, C++, and assembler.
+ * This is just the options that should be used for all three languages.
+ * There may be additional C-specific or C++-specific options that should be used,
+ * in addition to the ones returned by this method;
+ */
+ public List<String> getCompilerOptions(Collection<String> features) {
+ return compilerFlags.evaluate(features);
+ }
+
+ /**
+ * Returns the list of additional C-specific options to use for compiling
+ * C. These should be go on the command line after the common options
+ * returned by {@link #getCompilerOptions}.
+ */
+ public List<String> getCOptions() {
+ return cOptions;
+ }
+
+ /**
+ * Returns the list of additional C++-specific options to use for compiling
+ * C++. These should be go on the command line after the common options
+ * returned by {@link #getCompilerOptions}.
+ */
+ public List<String> getCxxOptions(Collection<String> features) {
+ return cxxFlags.evaluate(features);
+ }
+
+ /**
+ * Returns the default list of options which cannot be filtered by BUILD
+ * rules. These should be appended to the command line after filtering.
+ */
+ public List<String> getUnfilteredCompilerOptions(Collection<String> features) {
+ return unfilteredCompilerFlags.evaluate(features);
+ }
+
+ /**
+ * Returns the set of command-line linker options, including any flags
+ * inferred from the command-line options.
+ *
+ * @see Link
+ */
+ // TODO(bazel-team): Clean up the linker options computation!
+ public List<String> getLinkOptions() {
+ return linkOptions;
+ }
+
+ /**
+ * Returns the immutable list of linker options for fully statically linked
+ * outputs. Does not include command-line options passed via --linkopt or
+ * --linkopts.
+ *
+ * @param features default settings affecting this link
+ * @param sharedLib true if the output is a shared lib, false if it's an executable
+ */
+ public List<String> getFullyStaticLinkOptions(Collection<String> features,
+ boolean sharedLib) {
+ if (sharedLib) {
+ return getSharedLibraryLinkOptions(mostlyStaticLinkFlags, features);
+ } else {
+ return fullyStaticLinkFlags.evaluate(features);
+ }
+ }
+
+ /**
+ * Returns the immutable list of linker options for mostly statically linked
+ * outputs. Does not include command-line options passed via --linkopt or
+ * --linkopts.
+ *
+ * @param features default settings affecting this link
+ * @param sharedLib true if the output is a shared lib, false if it's an executable
+ */
+ public List<String> getMostlyStaticLinkOptions(Collection<String> features,
+ boolean sharedLib) {
+ if (sharedLib) {
+ return getSharedLibraryLinkOptions(
+ supportsEmbeddedRuntimes ? mostlyStaticSharedLinkFlags : dynamicLinkFlags,
+ features);
+ } else {
+ return mostlyStaticLinkFlags.evaluate(features);
+ }
+ }
+
+ /**
+ * Returns the immutable list of linker options for artifacts that are not
+ * fully or mostly statically linked. Does not include command-line options
+ * passed via --linkopt or --linkopts.
+ *
+ * @param features default settings affecting this link
+ * @param sharedLib true if the output is a shared lib, false if it's an executable
+ */
+ public List<String> getDynamicLinkOptions(Collection<String> features,
+ boolean sharedLib) {
+ if (sharedLib) {
+ return getSharedLibraryLinkOptions(dynamicLinkFlags, features);
+ } else {
+ return dynamicLinkFlags.evaluate(features);
+ }
+ }
+
+ /**
+ * Returns link options for the specified flag list, combined with universal options
+ * for all shared libraries (regardless of link staticness).
+ */
+ private List<String> getSharedLibraryLinkOptions(FlagList flags,
+ Collection<String> features) {
+ return ImmutableList.<String>builder()
+ .addAll(flags.evaluate(features))
+ .addAll(dynamicLibraryLinkFlags.evaluate(features))
+ .build();
+ }
+
+ /**
+ * Returns test-only link options such that certain test-specific features can be configured
+ * separately (e.g. lazy binding).
+ */
+ public List<String> getTestOnlyLinkOptions() {
+ return testOnlyLinkFlags;
+ }
+
+
+ /**
+ * Returns the list of options to be used with 'objcopy' when converting
+ * binary files to object files, or {@code null} if this operation is not
+ * supported.
+ */
+ public List<String> getObjCopyOptionsForEmbedding() {
+ return objcopyOptions;
+ }
+
+ /**
+ * Returns the list of options to be used with 'ld' when converting
+ * binary files to object files, or {@code null} if this operation is not
+ * supported.
+ */
+ public List<String> getLdOptionsForEmbedding() {
+ return ldOptions;
+ }
+
+ /**
+ * Returns a map of additional make variables for use by {@link
+ * BuildConfiguration}. These are to used to allow some build rules to
+ * avoid the limits on stack frame sizes and variable-length arrays.
+ *
+ * <p>The returned map must contain an entry for {@code STACK_FRAME_UNLIMITED},
+ * though the entry may be an empty string.
+ */
+ @VisibleForTesting
+ public Map<String, String> getAdditionalMakeVariables() {
+ return additionalMakeVariables;
+ }
+
+ /**
+ * Returns the execution path to the linker binary to use for this build.
+ * Relative paths are relative to the execution root.
+ */
+ public PathFragment getLdExecutable() {
+ return ldExecutable;
+ }
+
+ /**
+ * Returns the dynamic linking mode (full, off, or default).
+ */
+ public DynamicMode getDynamicMode() {
+ return dynamicMode;
+ }
+
+ /*
+ * If true then the directory name for non-LIPO targets will have a '-lipodata' suffix in
+ * AutoFDO mode.
+ */
+ public boolean getAutoFdoLipoData() {
+ return cppOptions.autoFdoLipoData;
+ }
+
+ /**
+ * Returns the STL label if given on the command line. {@code null}
+ * otherwise.
+ */
+ public Label getStl() {
+ return cppOptions.stl;
+ }
+
+ /*
+ * Returns the command-line "Make" variable overrides.
+ */
+ @Override
+ public ImmutableMap<String, String> getCommandLineDefines() {
+ return commandLineDefines;
+ }
+
+ /**
+ * Returns the command-line override value for the specified "Make" variable
+ * for this configuration, or null if none.
+ */
+ public String getMakeVariableOverride(String var) {
+ return commandLineDefines.get(var);
+ }
+
+ public boolean shouldScanIncludes() {
+ return cppOptions.scanIncludes;
+ }
+
+ /**
+ * Returns the currently active LIPO compilation mode.
+ */
+ public LipoMode getLipoMode() {
+ return cppOptions.lipoMode;
+ }
+
+ public boolean isFdo() {
+ return cppOptions.isFdo();
+ }
+
+ public boolean isLipoOptimization() {
+ // The LIPO optimization bits are set in the LIPO context collector configuration, too.
+ return cppOptions.isLipoOptimization() && !isLipoContextCollector();
+ }
+
+ public boolean isLipoOptimizationOrInstrumentation() {
+ return cppOptions.isLipoOptimizationOrInstrumentation();
+ }
+
+ /**
+ * Returns true if it is AutoFDO LIPO build.
+ */
+ public boolean isAutoFdoLipo() {
+ return cppOptions.fdoOptimize != null && FdoSupport.isAutoFdo(cppOptions.fdoOptimize)
+ && getLipoMode() != LipoMode.OFF;
+ }
+
+ /**
+ * Returns the default header check mode.
+ */
+ public HeadersCheckingMode getHeadersCheckingMode() {
+ return cppOptions.headersCheckingMode;
+ }
+
+ /**
+ * Returns whether or not to strip the binaries.
+ */
+ public boolean shouldStripBinaries() {
+ return stripBinaries;
+ }
+
+ /**
+ * Returns the additional options to pass to strip when generating a
+ * {@code <name>.stripped} binary by this build.
+ */
+ public List<String> getStripOpts() {
+ return cppOptions.stripoptList;
+ }
+
+ /**
+ * Returns whether temporary outputs from gcc will be saved.
+ */
+ public boolean getSaveTemps() {
+ return cppOptions.saveTemps;
+ }
+
+ /**
+ * Returns the {@link PerLabelOptions} to apply to the gcc command line, if
+ * the label of the compiled file matches the regular expression.
+ */
+ public List<PerLabelOptions> getPerFileCopts() {
+ return cppOptions.perFileCopts;
+ }
+
+ public Label getLipoContextLabel() {
+ return cppOptions.getLipoContextLabel();
+ }
+
+ /**
+ * Returns the custom malloc library label.
+ */
+ public Label customMalloc() {
+ return cppOptions.customMalloc;
+ }
+
+ /**
+ * Returns the extra warnings enabled for C compilation.
+ */
+ public List<String> getCWarns() {
+ return cppOptions.cWarns;
+ }
+
+ /**
+ * Returns true if mostly-static C++ binaries should be skipped.
+ */
+ public boolean skipStaticOutputs() {
+ return cppOptions.skipStaticOutputs;
+ }
+
+ /**
+ * Returns true if Fission is specified for this build and supported by the crosstool.
+ */
+ public boolean useFission() {
+ return cppOptions.fissionModes.contains(compilationMode) && supportsFission();
+ }
+
+ /**
+ * Returns true if all C++ compilations should produce position-independent code, links should
+ * produce position-independent executables, and dependencies with equivalent pre-built pic and
+ * nopic versions should apply the pic versions. Returns false if default settings should be
+ * applied (i.e. make no special provisions for pic code).
+ */
+ public boolean forcePic() {
+ return cppOptions.forcePic;
+ }
+
+ public boolean useStartEndLib() {
+ return cppOptions.useStartEndLib && supportsStartEndLib();
+ }
+
+ public boolean useThinArchives() {
+ return cppOptions.useThinArchives && supportsThinArchives();
+ }
+
+ /**
+ * Returns true if interface shared objects should be used.
+ */
+ public boolean useInterfaceSharedObjects() {
+ return supportsInterfaceSharedObjects() && cppOptions.useInterfaceSharedObjects;
+ }
+
+ public boolean forceIgnoreDashStatic() {
+ return cppOptions.forceIgnoreDashStatic;
+ }
+
+ /**
+ * Returns true iff this build configuration requires inclusion extraction
+ * (for include scanning) in the action graph.
+ */
+ public boolean needsIncludeScanning() {
+ return cppOptions.extractInclusions;
+ }
+
+ public boolean createCppModuleMaps() {
+ return cppOptions.cppModuleMaps;
+ }
+
+ /**
+ * Returns true if shared libraries must be compiled with position independent code
+ * on this platform or in this configuration.
+ */
+ public boolean needsPic() {
+ return forcePic() || toolchainNeedsPic();
+ }
+
+ /**
+ * Returns true iff we should use ".pic.o" files when linking executables.
+ */
+ public boolean usePicObjectsForBinaries() {
+ return forcePic() || usePicForBinaries();
+ }
+
+ public boolean legacyWholeArchive() {
+ return cppOptions.legacyWholeArchive;
+ }
+
+ public boolean getSymbolCounts() {
+ return cppOptions.symbolCounts;
+ }
+
+ public boolean getInmemoryDotdFiles() {
+ return cppOptions.inmemoryDotdFiles;
+ }
+
+ public boolean useIsystemForIncludes() {
+ return cppOptions.useIsystemForIncludes;
+ }
+
+ public LibcTop getLibcTop() {
+ return cppOptions.libcTop;
+ }
+
+ public boolean getUseInterfaceSharedObjects() {
+ return cppOptions.useInterfaceSharedObjects;
+ }
+
+ /**
+ * Returns the FDO support object.
+ */
+ public FdoSupport getFdoSupport() {
+ return fdoSupport;
+ }
+
+ /**
+ * Return the name of the directory (relative to the bin directory) that
+ * holds mangled links to shared libraries. This name is always set to
+ * the '{@code _solib_<cpu_archictecture_name>}.
+ */
+ public String getSolibDirectory() {
+ return solibDirectory;
+ }
+
+ /**
+ * Returns the path to the GNU binutils 'objcopy' binary to use for this
+ * build. (Corresponds to $(OBJCOPY) in make-dbg.) Relative paths are
+ * relative to the execution root.
+ */
+ public PathFragment getObjCopyExecutable() {
+ return getToolPathFragment(CppConfiguration.Tool.OBJCOPY);
+ }
+
+ /**
+ * Returns the path to the GNU binutils 'gcc' binary that should be used
+ * by this build. This binary should support compilation of both C (*.c)
+ * and C++ (*.cc) files. Relative paths are relative to the execution root.
+ */
+ public PathFragment getCppExecutable() {
+ return getToolPathFragment(CppConfiguration.Tool.GCC);
+ }
+
+ /**
+ * Returns the path to the GNU binutils 'g++' binary that should be used
+ * by this build. This binary should support linking of both C (*.c)
+ * and C++ (*.cc) files. Relative paths are relative to the execution root.
+ */
+ public PathFragment getCppLinkExecutable() {
+ return getToolPathFragment(CppConfiguration.Tool.GCC);
+ }
+
+ /**
+ * Returns the path to the GNU binutils 'cpp' binary that should be used
+ * by this build. Relative paths are relative to the execution root.
+ */
+ public PathFragment getCpreprocessorExecutable() {
+ return getToolPathFragment(CppConfiguration.Tool.CPP);
+ }
+
+ /**
+ * Returns the path to the GNU binutils 'gcov' binary that should be used
+ * by this build to analyze C++ coverage data. Relative paths are relative to
+ * the execution root.
+ */
+ public PathFragment getGcovExecutable() {
+ return getToolPathFragment(CppConfiguration.Tool.GCOV);
+ }
+
+ /**
+ * Returns the path to the 'gcov-tool' executable that should be used
+ * by this build. Relative paths are relative to the execution root.
+ */
+ public PathFragment getGcovToolExecutable() {
+ return getToolPathFragment(CppConfiguration.Tool.GCOVTOOL);
+ }
+
+ /**
+ * Returns the path to the GNU binutils 'nm' executable that should be used
+ * by this build. Used only for testing. Relative paths are relative to the
+ * execution root.
+ */
+ public PathFragment getNmExecutable() {
+ return getToolPathFragment(CppConfiguration.Tool.NM);
+ }
+
+ /**
+ * Returns the path to the GNU binutils 'objdump' executable that should be
+ * used by this build. Used only for testing. Relative paths are relative to
+ * the execution root.
+ */
+ public PathFragment getObjdumpExecutable() {
+ return getToolPathFragment(CppConfiguration.Tool.OBJDUMP);
+ }
+
+ /**
+ * Returns the path to the GNU binutils 'ar' binary to use for this build.
+ * Relative paths are relative to the execution root.
+ */
+ public PathFragment getArExecutable() {
+ return getToolPathFragment(CppConfiguration.Tool.AR);
+ }
+
+ /**
+ * Returns the path to the GNU binutils 'strip' executable that should be used
+ * by this build. Relative paths are relative to the execution root.
+ */
+ public PathFragment getStripExecutable() {
+ return getToolPathFragment(CppConfiguration.Tool.STRIP);
+ }
+
+ /**
+ * Returns the path to the GNU binutils 'dwp' binary that should be used by this
+ * build to combine debug info output from individual C++ compilations (i.e. .dwo
+ * files) into aggregate target-level debug packages. Relative paths are relative to the
+ * execution root. See https://gcc.gnu.org/wiki/DebugFission .
+ */
+ public PathFragment getDwpExecutable() {
+ return getToolPathFragment(CppConfiguration.Tool.DWP);
+ }
+
+ /**
+ * Returns the GNU System Name
+ */
+ public String getTargetGnuSystemName() {
+ return targetSystemName;
+ }
+
+ /**
+ * Returns the architecture component of the GNU System Name
+ */
+ public String getGnuSystemArch() {
+ if (targetSystemName.indexOf('-') == -1) {
+ return targetSystemName;
+ }
+ return targetSystemName.substring(0, targetSystemName.indexOf('-'));
+ }
+
+ /**
+ * Returns whether the configuration's purpose is only to collect LIPO-related data.
+ */
+ public boolean isLipoContextCollector() {
+ return lipoContextCollector;
+ }
+
+ @Override
+ public String getName() {
+ return "cpp";
+ }
+
+ @Override
+ public void reportInvalidOptions(EventHandler reporter, BuildOptions buildOptions) {
+ CppOptions cppOptions = buildOptions.get(CppOptions.class);
+ if (stripBinaries) {
+ boolean warn = cppOptions.coptList.contains("-g");
+ for (PerLabelOptions opt : cppOptions.perFileCopts) {
+ warn |= opt.getOptions().contains("-g");
+ }
+ if (warn) {
+ reporter.handle(Event.warn("Stripping enabled, but '--copt=-g' (or --per_file_copt=...@-g) "
+ + "specified. Debug information will be generated and then stripped away. This is "
+ + "probably not what you want! Use '-c dbg' for debug mode, or use '--strip=never' "
+ + "to disable stripping"));
+ }
+ }
+
+ if (cppOptions.fdoInstrument != null && cppOptions.fdoOptimize != null) {
+ reporter.handle(Event.error("Cannot instrument and optimize for FDO at the same time. "
+ + "Remove one of the '--fdo_instrument' and '--fdo_optimize' options"));
+ }
+
+ if (cppOptions.lipoContext != null) {
+ if (cppOptions.lipoMode != LipoMode.BINARY || cppOptions.fdoOptimize == null) {
+ reporter.handle(Event.warn("The --lipo_context option can only be used together with "
+ + "--fdo_optimize=<profile zip> and --lipo=binary. LIPO context will be ignored."));
+ }
+ } else {
+ if (cppOptions.lipoMode == LipoMode.BINARY && cppOptions.fdoOptimize != null) {
+ reporter.handle(Event.error("The --lipo_context option must be specified when using "
+ + "--fdo_optimize=<profile zip> and --lipo=binary"));
+ }
+ }
+ if (cppOptions.lipoMode == LipoMode.BINARY &&
+ compilationMode != CompilationMode.OPT) {
+ reporter.handle(Event.error(
+ "'--lipo=binary' can only be used with '--compilation_mode=opt' (or '-c opt')"));
+ }
+
+ if (cppOptions.fissionModes.contains(compilationMode) && !supportsFission()) {
+ reporter.handle(
+ Event.warn("Fission is not supported by this crosstool. Please use a supporting " +
+ "crosstool to enable fission"));
+ }
+ }
+
+ @Override
+ public void addGlobalMakeVariables(Builder<String, String> globalMakeEnvBuilder) {
+ // hardcoded CC->gcc setting for unit tests
+ globalMakeEnvBuilder.put("CC", getCppExecutable().getPathString());
+
+ // Make variables provided by crosstool/gcc compiler suite.
+ globalMakeEnvBuilder.put("AR", getArExecutable().getPathString());
+ globalMakeEnvBuilder.put("NM", getNmExecutable().getPathString());
+ globalMakeEnvBuilder.put("OBJCOPY", getObjCopyExecutable().getPathString());
+ globalMakeEnvBuilder.put("STRIP", getStripExecutable().getPathString());
+
+ PathFragment gcovtool = getGcovToolExecutable();
+ if (gcovtool != null) {
+ // gcov-tool is optional in Crosstool
+ globalMakeEnvBuilder.put("GCOVTOOL", gcovtool.getPathString());
+ }
+
+ if (getTargetLibc().startsWith("glibc-")) {
+ globalMakeEnvBuilder.put("GLIBC_VERSION",
+ getTargetLibc().substring("glibc-".length()));
+ } else {
+ globalMakeEnvBuilder.put("GLIBC_VERSION", getTargetLibc());
+ }
+
+ globalMakeEnvBuilder.put("C_COMPILER", getCompiler());
+ globalMakeEnvBuilder.put("TARGET_CPU", getTargetCpu());
+
+ // Deprecated variables
+
+ // TODO(bazel-team): (2009) These variables are so rarely used we should try to eliminate
+ // them entirely. see: "cs -f=BUILD -noi GNU_TARGET" and "cs -f=build_defs -noi
+ // GNU_TARGET"
+ globalMakeEnvBuilder.put("CROSSTOOLTOP", crosstoolTopPathFragment.getPathString());
+ globalMakeEnvBuilder.put("GLIBC", getTargetLibc());
+ globalMakeEnvBuilder.put("GNU_TARGET", targetSystemName);
+
+ globalMakeEnvBuilder.putAll(getAdditionalMakeVariables());
+
+ globalMakeEnvBuilder.put("ABI_GLIBC_VERSION", getAbiGlibcVersion());
+ globalMakeEnvBuilder.put("ABI", abi);
+ }
+
+ @Override
+ public void addImplicitLabels(Multimap<String, Label> implicitLabels) {
+ if (getLibcLabel() != null) {
+ implicitLabels.put("crosstool", getLibcLabel());
+ }
+
+ implicitLabels.put("crosstool", crosstoolTop);
+ }
+
+ @Override
+ public void prepareHook(Path execRoot, ArtifactFactory artifactFactory, PathFragment genfilesPath,
+ PackageRootResolver resolver) throws ViewCreationFailedException {
+ try {
+ getFdoSupport().prepareToBuild(execRoot, genfilesPath, artifactFactory, resolver);
+ } catch (ZipException e) {
+ throw new ViewCreationFailedException("Error reading provided FDO zip file", e);
+ } catch (FdoException | IOException e) {
+ throw new ViewCreationFailedException("Error while initializing FDO support", e);
+ }
+ }
+
+ @Override
+ public void declareSkyframeDependencies(Environment env) {
+ getFdoSupport().declareSkyframeDependencies(env, execRoot);
+ }
+
+ @Override
+ public void addRoots(List<Root> roots) {
+ // Fdo root can only exist for the target configuration.
+ FdoSupport fdoSupport = getFdoSupport();
+ if (fdoSupport.getFdoRoot() != null) {
+ roots.add(fdoSupport.getFdoRoot());
+ }
+
+ // Grepped header includes; this root is not configuration specific.
+ roots.add(getGreppedIncludesDirectory());
+ }
+
+ @Override
+ public Map<String, String> getCoverageEnvironment() {
+ ImmutableMap.Builder<String, String> env = ImmutableMap.builder();
+ env.put("COVERAGE_GCOV_PATH", getGcovExecutable().getPathString());
+ PathFragment fdoInstrument = getFdoSupport().getFdoInstrument();
+ if (fdoInstrument != null) {
+ env.put("FDO_DIR", fdoInstrument.getPathString());
+ }
+ return env.build();
+ }
+
+ @Override
+ public ImmutableList<Label> getCoverageLabels() {
+ // TODO(bazel-team): Using a gcov-specific crosstool filegroup here could reduce the number of
+ // inputs significantly. We'd also need to add logic in tools/coverage/collect_coverage.sh to
+ // drop crosstool dependency if metadataFiles does not contain *.gcno artifacts.
+ return ImmutableList.of(crosstoolTop);
+ }
+
+ @Override
+ public String getOutputDirectoryName() {
+ String lipoSuffix;
+ if (getLipoMode() != LipoMode.OFF && !isAutoFdoLipo()) {
+ lipoSuffix = "-lipo";
+ } else if (getAutoFdoLipoData()) {
+ lipoSuffix = "-lipodata";
+ } else {
+ lipoSuffix = "";
+ }
+ return toolchainIdentifier + lipoSuffix;
+ }
+
+ @Override
+ public String getConfigurationNameSuffix() {
+ return isLipoContextCollector() ? "collector" : null;
+ }
+
+ @Override
+ public String getPlatformName() {
+ return getToolchainIdentifier();
+ }
+
+ @Override
+ public boolean supportsIncrementalBuild() {
+ return !isLipoOptimization();
+ }
+
+ @Override
+ public boolean performsStaticLink() {
+ return getLinkOptions().contains("-static");
+ }
+
+ /**
+ * Returns true if we should share identical native libraries between different targets.
+ */
+ public boolean shareNativeDeps() {
+ return cppOptions.shareNativeDeps;
+ }
+
+ @Override
+ public void prepareForExecutionPhase() throws IOException {
+ // _fdo has a prefix of "_", but it should nevertheless be deleted. Detailed description
+ // of the structure of the symlinks / directories can be found at FdoSupport.extractFdoZip().
+ // We actually create a directory named "blaze-fdo" under the exec root, the previous version
+ // of which is deleted in FdoSupport.prepareToBuildExec(). We cannot do that just before the
+ // execution phase because that needs to happen before the analysis phase (in order to create
+ // the artifacts corresponding to the .gcda files).
+ Path tempPath = execRoot.getRelative("_fdo");
+ if (tempPath.exists()) {
+ FileSystemUtils.deleteTree(tempPath);
+ }
+ }
+
+ @Override
+ public Map<String, Object> lateBoundOptionDefaults() {
+ // --cpu defaults to null. With that default, the actual target cpu string gets picked up
+ // by the "default_target_cpu" crosstool parameter.
+ return ImmutableMap.<String, Object>of("cpu", getTargetCpu());
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppConfigurationLoader.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppConfigurationLoader.java
new file mode 100644
index 0000000..de20283
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppConfigurationLoader.java
@@ -0,0 +1,174 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import com.google.common.base.Function;
+import com.google.devtools.build.lib.analysis.BlazeDirectories;
+import com.google.devtools.build.lib.analysis.RedirectChaser;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration.Fragment;
+import com.google.devtools.build.lib.analysis.config.BuildOptions;
+import com.google.devtools.build.lib.analysis.config.ConfigurationEnvironment;
+import com.google.devtools.build.lib.analysis.config.ConfigurationFragmentFactory;
+import com.google.devtools.build.lib.analysis.config.InvalidConfigurationException;
+import com.google.devtools.build.lib.packages.InputFile;
+import com.google.devtools.build.lib.packages.NoSuchPackageException;
+import com.google.devtools.build.lib.packages.NoSuchTargetException;
+import com.google.devtools.build.lib.packages.NoSuchThingException;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.syntax.Label.SyntaxException;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.view.config.crosstool.CrosstoolConfig;
+
+import javax.annotation.Nullable;
+
+/**
+ * Loader for C++ configurations.
+ */
+public class CppConfigurationLoader implements ConfigurationFragmentFactory {
+ @Override
+ public Class<? extends Fragment> creates() {
+ return CppConfiguration.class;
+ }
+
+ private final Function<String, String> cpuTransformer;
+
+ /**
+ * Creates a new CrosstoolConfigurationLoader instance with the given
+ * configuration provider. The configuration provider is used to perform
+ * caller-specific configuration file lookup.
+ */
+ public CppConfigurationLoader(Function<String, String> cpuTransformer) {
+ this.cpuTransformer = cpuTransformer;
+ }
+
+ @Override
+ public CppConfiguration create(ConfigurationEnvironment env, BuildOptions options)
+ throws InvalidConfigurationException {
+ CppConfigurationParameters params = createParameters(env, options);
+ if (params == null) {
+ return null;
+ }
+ return new CppConfiguration(params);
+ }
+
+ /**
+ * Value class for all the data needed to create a {@link CppConfiguration}.
+ */
+ public static class CppConfigurationParameters {
+ protected final CrosstoolConfig.CToolchain toolchain;
+ protected final String cacheKeySuffix;
+ protected final BuildOptions buildOptions;
+ protected final Label crosstoolTop;
+ protected final Label ccToolchainLabel;
+ protected final Path fdoZip;
+ protected final Path execRoot;
+
+ CppConfigurationParameters(CrosstoolConfig.CToolchain toolchain,
+ String cacheKeySuffix,
+ BuildOptions buildOptions,
+ Path fdoZip,
+ Path execRoot,
+ Label crosstoolTop,
+ Label ccToolchainLabel) {
+ this.toolchain = toolchain;
+ this.cacheKeySuffix = cacheKeySuffix;
+ this.buildOptions = buildOptions;
+ this.fdoZip = fdoZip;
+ this.execRoot = execRoot;
+ this.crosstoolTop = crosstoolTop;
+ this.ccToolchainLabel = ccToolchainLabel;
+ }
+ }
+
+ @Nullable
+ protected CppConfigurationParameters createParameters(
+ ConfigurationEnvironment env, BuildOptions options) throws InvalidConfigurationException {
+ BlazeDirectories directories = env.getBlazeDirectories();
+ if (directories == null) {
+ return null;
+ }
+ Label crosstoolTop = RedirectChaser.followRedirects(env,
+ options.get(CppOptions.class).crosstoolTop, "crosstool_top");
+ if (crosstoolTop == null) {
+ return null;
+ }
+ CrosstoolConfigurationLoader.CrosstoolFile file =
+ CrosstoolConfigurationLoader.readCrosstool(env, crosstoolTop);
+ if (file == null) {
+ return null;
+ }
+ CrosstoolConfig.CToolchain toolchain =
+ CrosstoolConfigurationLoader.selectToolchain(file.getProto(), options, cpuTransformer);
+
+ // FDO
+ // TODO(bazel-team): move this to CppConfiguration.prepareHook
+ CppOptions cppOptions = options.get(CppOptions.class);
+ Path fdoZip;
+ if (cppOptions.fdoOptimize == null) {
+ fdoZip = null;
+ } else if (cppOptions.fdoOptimize.startsWith("//")) {
+ try {
+ Target target = env.getTarget(Label.parseAbsolute(cppOptions.fdoOptimize));
+ if (target == null) {
+ return null;
+ }
+ if (!(target instanceof InputFile)) {
+ throw new InvalidConfigurationException(
+ "--fdo_optimize cannot accept targets that do not refer to input files");
+ }
+ fdoZip = env.getPath(target.getPackage(), target.getName());
+ if (fdoZip == null) {
+ throw new InvalidConfigurationException(
+ "The --fdo_optimize parameter you specified resolves to a file that does not exist");
+ }
+ } catch (NoSuchPackageException | NoSuchTargetException | SyntaxException e) {
+ throw new InvalidConfigurationException(e);
+ }
+ } else {
+ fdoZip = directories.getWorkspace().getRelative(cppOptions.fdoOptimize);
+ }
+
+ Label ccToolchainLabel;
+ try {
+ ccToolchainLabel = crosstoolTop.getRelative("cc-compiler-" + toolchain.getTargetCpu());
+ } catch (Label.SyntaxException e) {
+ throw new InvalidConfigurationException(String.format(
+ "'%s' is not a valid CPU. It should only consist of characters valid in labels",
+ toolchain.getTargetCpu()));
+ }
+
+ Target ccToolchain;
+ try {
+ ccToolchain = env.getTarget(ccToolchainLabel);
+ if (ccToolchain == null) {
+ return null;
+ }
+ } catch (NoSuchThingException e) {
+ throw new InvalidConfigurationException(String.format(
+ "The toolchain rule '%s' does not exist", ccToolchainLabel));
+ }
+
+ if (!(ccToolchain instanceof Rule)
+ || !((Rule) ccToolchain).getRuleClass().equals("cc_toolchain")) {
+ throw new InvalidConfigurationException(String.format(
+ "The label '%s' is not a cc_toolchain rule", ccToolchainLabel));
+ }
+
+ return new CppConfigurationParameters(toolchain, file.getMd5(), options,
+ fdoZip, directories.getExecRoot(), crosstoolTop, ccToolchainLabel);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppDebugFileProvider.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppDebugFileProvider.java
new file mode 100644
index 0000000..c0fcb11
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppDebugFileProvider.java
@@ -0,0 +1,54 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+
+/**
+ * A target that provides .dwo files which can be combined into a .dwp packaging step. See
+ * https://gcc.gnu.org/wiki/DebugFission for details.
+ */
+@Immutable
+public final class CppDebugFileProvider implements TransitiveInfoProvider {
+
+ private final NestedSet<Artifact> transitiveDwoFiles;
+ private final NestedSet<Artifact> transitivePicDwoFiles;
+
+ public CppDebugFileProvider(NestedSet<Artifact> transitiveDwoFiles,
+ NestedSet<Artifact> transitivePicDwoFiles) {
+ this.transitiveDwoFiles = transitiveDwoFiles;
+ this.transitivePicDwoFiles = transitivePicDwoFiles;
+ }
+
+ /**
+ * Returns the .dwo files that should be included in this target's .dwp packaging (if this
+ * target is linked) or passed through to a dependant's .dwp packaging (e.g. if this is a
+ * cc_library depended on by a statically linked cc_binary).
+ *
+ * Assumes the corresponding link consumes .o files (vs. .pic.o files).
+ */
+ public NestedSet<Artifact> getTransitiveDwoFiles() {
+ return transitiveDwoFiles;
+ }
+
+ /**
+ * Same as above, but assumes the corresponding link consumes pic.o files.
+ */
+ public NestedSet<Artifact> getTransitivePicDwoFiles() {
+ return transitivePicDwoFiles;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppDebugPackageProvider.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppDebugPackageProvider.java
new file mode 100644
index 0000000..864a4d5
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppDebugPackageProvider.java
@@ -0,0 +1,69 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+
+import javax.annotation.Nullable;
+
+/**
+ * Provides the binary artifact and its associated .dwp files, if fission is enabled.
+ * If Fission ({@link https://gcc.gnu.org/wiki/DebugFission}) is not enabled, the
+ * dwp file will be null.
+ */
+@Immutable
+public final class CppDebugPackageProvider implements TransitiveInfoProvider {
+
+ private final Artifact strippedArtifact;
+ private final Artifact unstrippedArtifact;
+ @Nullable private final Artifact dwpArtifact;
+
+ public CppDebugPackageProvider(
+ Artifact strippedArtifact,
+ Artifact unstrippedArtifact,
+ @Nullable Artifact dwpArtifact) {
+ Preconditions.checkNotNull(strippedArtifact);
+ Preconditions.checkNotNull(unstrippedArtifact);
+ this.strippedArtifact = strippedArtifact;
+ this.unstrippedArtifact = unstrippedArtifact;
+ this.dwpArtifact = dwpArtifact;
+ }
+
+ /**
+ * Returns the stripped file (the explicit ".stripped" target).
+ */
+ public final Artifact getStrippedArtifact() {
+ return strippedArtifact;
+ }
+
+ /**
+ * Returns the unstripped file (the default executable target).
+ */
+ public final Artifact getUnstrippedArtifact() {
+ return unstrippedArtifact;
+ }
+
+ /**
+ * Returns the .dwp file (for fission builds) or null if --fission=no.
+ */
+ @Nullable
+ public final Artifact getDwpArtifact() {
+ return dwpArtifact;
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppFileTypes.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppFileTypes.java
new file mode 100644
index 0000000..d9bb7b6
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppFileTypes.java
@@ -0,0 +1,141 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.util.FileType;
+
+import java.util.List;
+import java.util.regex.Pattern;
+
+/**
+ * C++-related file type definitions.
+ */
+public final class CppFileTypes {
+ public static final FileType CPP_SOURCE = FileType.of(".cc", ".cpp", ".cxx", ".C");
+ public static final FileType C_SOURCE = FileType.of(".c");
+ public static final FileType CPP_HEADER = FileType.of(".h", ".hh", ".hpp", ".hxx", ".inc");
+ public static final FileType CPP_TEXTUAL_INCLUDE = FileType.of(".inc");
+
+ public static final FileType PIC_PREPROCESSED_C = FileType.of(".pic.i");
+ public static final FileType PREPROCESSED_C = new FileType() {
+ final String ext = ".i";
+ @Override
+ public boolean apply(String filename) {
+ return filename.endsWith(ext) && !PIC_PREPROCESSED_C.matches(filename);
+ }
+ @Override
+ public List<String> getExtensions() {
+ return ImmutableList.of(ext);
+ }
+ };
+ public static final FileType PIC_PREPROCESSED_CPP = FileType.of(".pic.ii");
+ public static final FileType PREPROCESSED_CPP = new FileType() {
+ final String ext = ".ii";
+ @Override
+ public boolean apply(String filename) {
+ return filename.endsWith(ext) && !PIC_PREPROCESSED_CPP.matches(filename);
+ }
+ @Override
+ public List<String> getExtensions() {
+ return ImmutableList.of(ext);
+ }
+ };
+
+ public static final FileType ASSEMBLER_WITH_C_PREPROCESSOR = FileType.of(".S");
+ public static final FileType PIC_ASSEMBLER = FileType.of(".pic.s");
+ public static final FileType ASSEMBLER = new FileType() {
+ final String ext = ".s";
+ @Override
+ public boolean apply(String filename) {
+ return filename.endsWith(ext) && !PIC_ASSEMBLER.matches(filename);
+ }
+ @Override
+ public List<String> getExtensions() {
+ return ImmutableList.of(ext);
+ }
+ };
+
+ public static final FileType PIC_ARCHIVE = FileType.of(".pic.a");
+ public static final FileType ARCHIVE = new FileType() {
+ final String ext = ".a";
+ @Override
+ public boolean apply(String filename) {
+ return filename.endsWith(ext) && !PIC_ARCHIVE.matches(filename);
+ }
+ @Override
+ public List<String> getExtensions() {
+ return ImmutableList.of(ext);
+ }
+ };
+
+ public static final FileType ALWAYS_LINK_PIC_LIBRARY = FileType.of(".pic.lo");
+ public static final FileType ALWAYS_LINK_LIBRARY = new FileType() {
+ final String ext = ".lo";
+ @Override
+ public boolean apply(String filename) {
+ return filename.endsWith(ext) && !ALWAYS_LINK_PIC_LIBRARY.matches(filename);
+ }
+ @Override
+ public List<String> getExtensions() {
+ return ImmutableList.of(ext);
+ }
+ };
+
+ public static final FileType PIC_OBJECT_FILE = FileType.of(".pic.o");
+ public static final FileType OBJECT_FILE = new FileType() {
+ final String ext = ".o";
+ @Override
+ public boolean apply(String filename) {
+ return filename.endsWith(ext) && !PIC_OBJECT_FILE.matches(filename);
+ }
+ @Override
+ public List<String> getExtensions() {
+ return ImmutableList.of(ext);
+ }
+ };
+
+
+ public static final FileType SHARED_LIBRARY = FileType.of(".so");
+ public static final FileType INTERFACE_SHARED_LIBRARY = FileType.of(".ifso");
+ public static final FileType LINKER_SCRIPT = FileType.of(".lds");
+ // Matches shared libraries with version names in the extension, i.e.
+ // libmylib.so.2 or libmylib.so.2.10.
+ private static final Pattern VERSIONED_SHARED_LIBRARY_PATTERN =
+ Pattern.compile("^.+\\.so(\\.\\d+)+$");
+ public static final FileType VERSIONED_SHARED_LIBRARY = new FileType() {
+ @Override
+ public boolean apply(String filename) {
+ // Because regex matching can be slow, we first do a quick digit check on the final
+ // character before risking the full-on regex match. This should eliminate the performance
+ // hit on practically every non-qualifying file type.
+ if (Character.isDigit(filename.charAt(filename.length() - 1))) {
+ return VERSIONED_SHARED_LIBRARY_PATTERN.matcher(filename).matches();
+ } else {
+ return false;
+ }
+ }
+ };
+
+ public static final FileType COVERAGE_NOTES = FileType.of(".gcno");
+ public static final FileType COVERAGE_DATA = FileType.of(".gcda");
+ public static final FileType COVERAGE_DATA_IMPORTS = FileType.of(".gcda.imports");
+ public static final FileType GCC_AUTO_PROFILE = FileType.of(".afdo");
+
+ public static final FileType CPP_MODULE_MAP = FileType.of(".cppmap");
+ public static final FileType CPP_MODULE = FileType.of(".pcm");
+
+ // Output of the dwp tool
+ public static final FileType DEBUG_INFO_PACKAGE = FileType.of(".dwp");
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppHelper.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppHelper.java
new file mode 100644
index 0000000..5bc1363
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppHelper.java
@@ -0,0 +1,529 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.ActionOwner;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.MiddlemanFactory;
+import com.google.devtools.build.lib.actions.Root;
+import com.google.devtools.build.lib.analysis.AnalysisEnvironment;
+import com.google.devtools.build.lib.analysis.AnalysisUtils;
+import com.google.devtools.build.lib.analysis.FileProvider;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
+import com.google.devtools.build.lib.analysis.Util;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.rules.cpp.CcLinkParams.Linkstamp;
+import com.google.devtools.build.lib.rules.cpp.CppCompilationContext.Builder;
+import com.google.devtools.build.lib.rules.cpp.Link.LinkTargetType;
+import com.google.devtools.build.lib.shell.ShellUtils;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.syntax.Label.SyntaxException;
+import com.google.devtools.build.lib.util.FileTypeSet;
+import com.google.devtools.build.lib.util.IncludeScanningUtil;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.lib.view.config.crosstool.CrosstoolConfig.LipoMode;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nullable;
+
+/**
+ * Helper class for functionality shared by cpp related rules.
+ *
+ * <p>This class can be used only after the loading phase.
+ */
+public class CppHelper {
+ // TODO(bazel-team): should this use Link.SHARED_LIBRARY_FILETYPES?
+ public static final FileTypeSet SHARED_LIBRARY_FILETYPES = FileTypeSet.of(
+ CppFileTypes.SHARED_LIBRARY,
+ CppFileTypes.VERSIONED_SHARED_LIBRARY);
+
+ private static final FileTypeSet CPP_FILETYPES = FileTypeSet.of(
+ CppFileTypes.CPP_HEADER,
+ CppFileTypes.CPP_SOURCE);
+
+ private CppHelper() {
+ // prevents construction
+ }
+
+ /**
+ * Merges the STL and toolchain contexts into context builder. The STL is automatically determined
+ * using the ":stl" attribute.
+ */
+ public static void mergeToolchainDependentContext(RuleContext ruleContext,
+ Builder contextBuilder) {
+ TransitiveInfoCollection stl = ruleContext.getPrerequisite(":stl", Mode.TARGET);
+ if (stl != null) {
+ // TODO(bazel-team): Clean this up.
+ contextBuilder.addSystemIncludeDir(stl.getLabel().getPackageFragment().getRelative("gcc3"));
+ contextBuilder.mergeDependentContext(stl.getProvider(CppCompilationContext.class));
+ }
+ CcToolchainProvider toolchain = getToolchain(ruleContext);
+ if (toolchain != null) {
+ contextBuilder.mergeDependentContext(toolchain.getCppCompilationContext());
+ }
+ }
+
+ /**
+ * Returns the malloc implementation for the given target.
+ */
+ public static TransitiveInfoCollection mallocForTarget(RuleContext ruleContext) {
+ if (ruleContext.getFragment(CppConfiguration.class).customMalloc() != null) {
+ return ruleContext.getPrerequisite(":default_malloc", Mode.TARGET);
+ } else {
+ return ruleContext.getPrerequisite("malloc", Mode.TARGET);
+ }
+ }
+
+ /**
+ * Expands Make variables in a list of string and tokenizes the result. If the package feature
+ * no_copts_tokenization is set, tokenize only items consisting of a single make variable.
+ *
+ * @param ruleContext the ruleContext to be used as the context of Make variable expansion
+ * @param attributeName the name of the attribute to use in error reporting
+ * @param input the list of strings to expand
+ * @return a list of strings containing the expanded and tokenized values for the
+ * attribute
+ */
+ // TODO(bazel-team): Move to CcCommon; refactor CcPlugin to use either CcLibraryHelper or
+ // CcCommon.
+ static List<String> expandMakeVariables(
+ RuleContext ruleContext, String attributeName, List<String> input) {
+ boolean tokenization =
+ !ruleContext.getFeatures().contains("no_copts_tokenization");
+
+ List<String> tokens = new ArrayList<>();
+ for (String token : input) {
+ try {
+ // Legacy behavior: tokenize all items.
+ if (tokenization) {
+ ruleContext.tokenizeAndExpandMakeVars(tokens, attributeName, token);
+ } else {
+ String exp = ruleContext.expandSingleMakeVariable(attributeName, token);
+ if (exp != null) {
+ ShellUtils.tokenize(tokens, exp);
+ } else {
+ tokens.add(ruleContext.expandMakeVariables(attributeName, token));
+ }
+ }
+ } catch (ShellUtils.TokenizationException e) {
+ ruleContext.attributeError(attributeName, e.getMessage());
+ }
+ }
+ return ImmutableList.copyOf(tokens);
+ }
+
+ /**
+ * Appends the tokenized values of the copts attribute to copts.
+ */
+ public static ImmutableList<String> getAttributeCopts(RuleContext ruleContext, String attr) {
+ Preconditions.checkArgument(ruleContext.getRule().isAttrDefined(attr, Type.STRING_LIST));
+ List<String> unexpanded = ruleContext.attributes().get(attr, Type.STRING_LIST);
+
+ return ImmutableList.copyOf(expandMakeVariables(ruleContext, attr, unexpanded));
+ }
+
+ /**
+ * Expands attribute value either using label expansion
+ * (if attemptLabelExpansion == {@code true} and it does not look like make
+ * variable or flag) or tokenizes and expands make variables.
+ */
+ public static void expandAttribute(RuleContext ruleContext,
+ List<String> values, String attrName, String attrValue, boolean attemptLabelExpansion) {
+ if (attemptLabelExpansion && CppHelper.isLinkoptLabel(attrValue)) {
+ if (!CppHelper.expandLabel(ruleContext, values, attrValue)) {
+ ruleContext.attributeError(attrName, "could not resolve label '" + attrValue + "'");
+ }
+ } else {
+ ruleContext.tokenizeAndExpandMakeVars(values, attrName, attrValue);
+ }
+ }
+
+ /**
+ * Determines if a linkopt can be a label. Linkopts come in 2 varieties:
+ * literals -- flags like -Xl and makefile vars like $(LD) -- and labels,
+ * which we should expand into filenames.
+ *
+ * @param linkopt the link option to test.
+ * @return true if the linkopt is not a flag (starting with "-") or a makefile
+ * variable (starting with "$");
+ */
+ private static boolean isLinkoptLabel(String linkopt) {
+ return !linkopt.startsWith("$") && !linkopt.startsWith("-");
+ }
+
+ /**
+ * Expands a label against the target's deps, adding the expanded path strings
+ * to the linkopts.
+ *
+ * @param linkopts the linkopts to add the expanded label to
+ * @param labelName the name of the label to expand
+ * @return true if the label was expanded successfully, false otherwise
+ */
+ private static boolean expandLabel(RuleContext ruleContext, List<String> linkopts,
+ String labelName) {
+ try {
+ Label label = ruleContext.getLabel().getRelative(labelName);
+ for (FileProvider target : ruleContext
+ .getPrerequisites("deps", Mode.TARGET, FileProvider.class)) {
+ if (target.getLabel().equals(label)) {
+ for (Artifact artifact : target.getFilesToBuild()) {
+ linkopts.add(artifact.getExecPathString());
+ }
+ return true;
+ }
+ }
+ } catch (SyntaxException e) {
+ // Quietly ignore and fall through.
+ }
+ linkopts.add(labelName);
+ return false;
+ }
+
+ /**
+ * This almost trivial method looks up the :cc_toolchain attribute on the rule context, makes sure
+ * that it refers to a rule that has a {@link CcToolchainProvider} (gives an error otherwise), and
+ * returns a reference to that {@link CcToolchainProvider}. The method only returns {@code null}
+ * if there is no such attribute (this is currently not an error).
+ */
+ @Nullable public static CcToolchainProvider getToolchain(RuleContext ruleContext) {
+ if (ruleContext.attributes().getAttributeDefinition(":cc_toolchain") == null) {
+ // TODO(bazel-team): Report an error or throw an exception in this case.
+ return null;
+ }
+ TransitiveInfoCollection dep = ruleContext.getPrerequisite(":cc_toolchain", Mode.TARGET);
+ return getToolchain(ruleContext, dep);
+ }
+
+ /**
+ * This almost trivial method makes sure that the given info collection has a {@link
+ * CcToolchainProvider} (gives an error otherwise), and returns a reference to that {@link
+ * CcToolchainProvider}. The method never returns {@code null}, even if there is no toolchain.
+ */
+ public static CcToolchainProvider getToolchain(RuleContext ruleContext,
+ TransitiveInfoCollection dep) {
+ // TODO(bazel-team): Consider checking this generally at the attribute level.
+ if ((dep == null) || (dep.getProvider(CcToolchainProvider.class) == null)) {
+ ruleContext.ruleError("The selected C++ toolchain is not a cc_toolchain rule");
+ return CcToolchainProvider.EMPTY_TOOLCHAIN_IS_ERROR;
+ }
+ return dep.getProvider(CcToolchainProvider.class);
+ }
+
+ /**
+ * Returns the directory where object files are created.
+ */
+ public static PathFragment getObjDirectory(Label ruleLabel) {
+ return AnalysisUtils.getUniqueDirectory(ruleLabel, new PathFragment("_objs"));
+ }
+
+ /**
+ * Creates a grep-includes ExtractInclusions action for generated sources/headers in the
+ * needsIncludeScanning() BuildConfiguration case. Returns a map from original header
+ * Artifact to the output Artifact of grepping over it. The return value only includes
+ * entries for generated sources or headers when --extract_generated_inclusions is enabled.
+ *
+ * <p>Previously, incremental rebuilds redid all include scanning work
+ * for a given .cc source in serial. For high-latency file systems, this could cause
+ * performance problems if many headers are generated.
+ */
+ @Nullable
+ public static final Map<Artifact, Artifact> createExtractInclusions(RuleContext ruleContext,
+ Iterable<Artifact> prerequisites) {
+ Map<Artifact, Artifact> extractions = new HashMap<>();
+ for (Artifact prerequisite : prerequisites) {
+ Artifact scanned = createExtractInclusions(ruleContext, prerequisite);
+ if (scanned != null) {
+ extractions.put(prerequisite, scanned);
+ }
+ }
+ return extractions;
+ }
+
+ /**
+ * Creates a grep-includes ExtractInclusions action for generated sources/headers in the
+ * needsIncludeScanning() BuildConfiguration case.
+ *
+ * <p>Previously, incremental rebuilds redid all include scanning work for a given
+ * .cc source in serial. For high-latency file systems, this could cause
+ * performance problems if many headers are generated.
+ */
+ private static final Artifact createExtractInclusions(RuleContext ruleContext,
+ Artifact prerequisite) {
+ if (ruleContext != null &&
+ ruleContext.getFragment(CppConfiguration.class).needsIncludeScanning() &&
+ !prerequisite.isSourceArtifact() &&
+ CPP_FILETYPES.matches(prerequisite.getFilename())) {
+ Artifact scanned = getIncludesOutput(ruleContext, prerequisite);
+ ruleContext.registerAction(
+ new ExtractInclusionAction(ruleContext.getActionOwner(), prerequisite, scanned));
+ return scanned;
+ }
+ return null;
+ }
+
+ private static Artifact getIncludesOutput(RuleContext ruleContext, Artifact src) {
+ Root root = ruleContext.getFragment(CppConfiguration.class).getGreppedIncludesDirectory();
+ PathFragment relOut = IncludeScanningUtil.getRootRelativeOutputPath(src.getExecPath());
+ return ruleContext.getAnalysisEnvironment().getDerivedArtifact(relOut, root);
+ }
+
+ /**
+ * Returns the workspace-relative filename for the linked artifact.
+ */
+ public static PathFragment getLinkedFilename(RuleContext ruleContext,
+ LinkTargetType linkType) {
+ PathFragment relativePath = Util.getWorkspaceRelativePath(ruleContext.getTarget());
+ PathFragment linkedFileName = (linkType == LinkTargetType.EXECUTABLE) ?
+ relativePath :
+ relativePath.replaceName("lib" + relativePath.getBaseName() + linkType.getExtension());
+ return linkedFileName;
+ }
+
+ /**
+ * Resolves the linkstamp collection from the {@code CcLinkParams} into a map.
+ *
+ * <p>Emits a warning on the rule if there are identical linkstamp artifacts with different
+ * compilation contexts.
+ */
+ public static Map<Artifact, ImmutableList<Artifact>> resolveLinkstamps(RuleContext ruleContext,
+ CcLinkParams linkParams) {
+ Map<Artifact, ImmutableList<Artifact>> result = new LinkedHashMap<>();
+ for (Linkstamp pair : linkParams.getLinkstamps()) {
+ Artifact artifact = pair.getArtifact();
+ if (result.containsKey(artifact)) {
+ ruleContext.ruleWarning("rule inherits the '" + artifact.toDetailString()
+ + "' linkstamp file from more than one cc_library rule");
+ }
+ result.put(artifact, pair.getDeclaredIncludeSrcs());
+ }
+ return result;
+ }
+
+ public static void addTransitiveLipoInfoForCommonAttributes(
+ RuleContext ruleContext,
+ CcCompilationOutputs outputs,
+ NestedSetBuilder<IncludeScannable> scannableBuilder) {
+
+ TransitiveLipoInfoProvider stl = null;
+ if (ruleContext.getRule().getAttributeDefinition(":stl") != null &&
+ ruleContext.getPrerequisite(":stl", Mode.TARGET) != null) {
+ // If the attribute is defined, it is never null.
+ stl = ruleContext.getPrerequisite(":stl", Mode.TARGET)
+ .getProvider(TransitiveLipoInfoProvider.class);
+ }
+ if (stl != null) {
+ scannableBuilder.addTransitive(stl.getTransitiveIncludeScannables());
+ }
+
+ for (TransitiveLipoInfoProvider dep :
+ ruleContext.getPrerequisites("deps", Mode.TARGET, TransitiveLipoInfoProvider.class)) {
+ scannableBuilder.addTransitive(dep.getTransitiveIncludeScannables());
+ }
+
+ if (ruleContext.getRule().getRuleClassObject().hasAttr("malloc", Type.LABEL)) {
+ TransitiveInfoCollection malloc = mallocForTarget(ruleContext);
+ TransitiveLipoInfoProvider provider = malloc.getProvider(TransitiveLipoInfoProvider.class);
+ if (provider != null) {
+ scannableBuilder.addTransitive(provider.getTransitiveIncludeScannables());
+ }
+ }
+
+ for (IncludeScannable scannable : outputs.getLipoScannables()) {
+ Preconditions.checkState(scannable.getIncludeScannerSources().size() == 1);
+ scannableBuilder.add(scannable);
+ }
+ }
+
+ // TODO(bazel-team): figure out a way to merge these 2 methods. See the Todo in
+ // CcCommonConfiguredTarget.noCoptsMatches().
+ /**
+ * Determines if we should apply -fPIC for this rule's C++ compilations. This determination
+ * is generally made by the global C++ configuration settings "needsPic" and
+ * and "usePicForBinaries". However, an individual rule may override these settings by applying
+ * -fPIC" to its "nocopts" attribute. This allows incompatible rules to "opt out" of global PIC
+ * settings (see bug: "Provide a way to turn off -fPIC for targets that can't be built that way").
+ *
+ * @param ruleContext the context of the rule to check
+ * @param forBinary true if compiling for a binary, false if for a shared library
+ * @return true if this rule's compilations should apply -fPIC, false otherwise
+ */
+ public static boolean usePic(RuleContext ruleContext, boolean forBinary) {
+ if (CcCommon.noCoptsMatches("-fPIC", ruleContext)) {
+ return false;
+ }
+ CppConfiguration config = ruleContext.getFragment(CppConfiguration.class);
+ return forBinary ? config.usePicObjectsForBinaries() : config.needsPic();
+ }
+
+ /**
+ * Returns the LIPO context provider for configured target,
+ * or null if such a provider doesn't exist.
+ */
+ public static LipoContextProvider getLipoContextProvider(RuleContext ruleContext) {
+ if (ruleContext.getRule().getAttributeDefinition(":lipo_context_collector") == null) {
+ return null;
+ }
+
+ TransitiveInfoCollection dep =
+ ruleContext.getPrerequisite(":lipo_context_collector", Mode.DONT_CHECK);
+ return (dep != null) ? dep.getProvider(LipoContextProvider.class) : null;
+ }
+
+ // Creates CppModuleMap object, and adds it to C++ compilation context.
+ public static CppModuleMap addCppModuleMapToContext(RuleContext ruleContext,
+ CppCompilationContext.Builder contextBuilder) {
+ if (!ruleContext.getFragment(CppConfiguration.class).createCppModuleMaps()) {
+ return null;
+ }
+ if (getToolchain(ruleContext).getCppCompilationContext().getCppModuleMap() == null) {
+ return null;
+ }
+ // Create the module map artifact as a genfile.
+ PathFragment mapPath = FileSystemUtils.appendExtension(ruleContext.getLabel().toPathFragment(),
+ Iterables.getOnlyElement(CppFileTypes.CPP_MODULE_MAP.getExtensions()));
+ Artifact mapFile = ruleContext.getAnalysisEnvironment().getDerivedArtifact(mapPath,
+ ruleContext.getConfiguration().getGenfilesDirectory());
+ CppModuleMap moduleMap =
+ new CppModuleMap(mapFile, ruleContext.getLabel().toString());
+ contextBuilder.setCppModuleMap(moduleMap);
+ return moduleMap;
+ }
+
+ /**
+ * Returns a middleman for all files to build for the given configured target,
+ * substituting shared library artifacts with corresponding solib symlinks. If
+ * multiple calls are made, then it returns the same artifact for configurations
+ * with the same internal directory.
+ *
+ * <p>The resulting middleman only aggregates the inputs and must be expanded
+ * before populating the set of files necessary to execute an action.
+ */
+ static List<Artifact> getAggregatingMiddlemanForCppRuntimes(RuleContext ruleContext,
+ String purpose, TransitiveInfoCollection dep, String solibDirOverride,
+ BuildConfiguration configuration) {
+ return getMiddlemanInternal(
+ ruleContext.getAnalysisEnvironment(), ruleContext, ruleContext.getActionOwner(), purpose,
+ dep, true, true, solibDirOverride, configuration);
+ }
+
+ @VisibleForTesting
+ public static List<Artifact> getAggregatingMiddlemanForTesting(AnalysisEnvironment env,
+ RuleContext ruleContext, ActionOwner owner, String purpose, TransitiveInfoCollection dep,
+ boolean useSolibSymlinks, BuildConfiguration configuration) {
+ return getMiddlemanInternal(
+ env, ruleContext, owner, purpose, dep, useSolibSymlinks, false, null, configuration);
+ }
+
+ /**
+ * Internal implementation for getAggregatingMiddlemanForCppRuntimes.
+ */
+ private static List<Artifact> getMiddlemanInternal(AnalysisEnvironment env,
+ RuleContext ruleContext, ActionOwner actionOwner, String purpose,
+ TransitiveInfoCollection dep, boolean useSolibSymlinks, boolean isCppRuntime,
+ String solibDirOverride, BuildConfiguration configuration) {
+ if (dep == null) {
+ return ImmutableList.of();
+ }
+ MiddlemanFactory factory = env.getMiddlemanFactory();
+ Iterable<Artifact> artifacts = dep.getProvider(FileProvider.class).getFilesToBuild();
+ if (useSolibSymlinks) {
+ List<Artifact> symlinkedArtifacts = new ArrayList<>();
+ for (Artifact artifact : artifacts) {
+ symlinkedArtifacts.add(solibArtifactMaybe(
+ ruleContext, artifact, isCppRuntime, solibDirOverride, configuration));
+ }
+ artifacts = symlinkedArtifacts;
+ purpose += "_with_solib";
+ }
+ return ImmutableList.of(factory.createMiddlemanAllowMultiple(
+ env, actionOwner, purpose, artifacts, configuration.getMiddlemanDirectory()));
+ }
+
+ /**
+ * If the artifact is a shared library, returns the solib symlink artifact associated with it.
+ *
+ * @param ruleContext the context of the rule that creates the symlink
+ * @param artifact the library the solib symlink should point to
+ * @param isCppRuntime whether the library is a C++ runtime
+ * @param solibDirOverride if not null, forces the solib symlink to be in this directory
+ */
+ private static Artifact solibArtifactMaybe(RuleContext ruleContext, Artifact artifact,
+ boolean isCppRuntime, String solibDirOverride, BuildConfiguration configuration) {
+ if (SHARED_LIBRARY_FILETYPES.matches(artifact.getFilename())) {
+ return isCppRuntime
+ ? SolibSymlinkAction.getCppRuntimeSymlink(
+ ruleContext, artifact, solibDirOverride, configuration)
+ .getArtifact()
+ : SolibSymlinkAction.getDynamicLibrarySymlink(
+ ruleContext, artifact, false, true, configuration)
+ .getArtifact();
+ } else {
+ return artifact;
+ }
+ }
+
+ /**
+ * Returns the type of archives being used.
+ */
+ public static Link.ArchiveType archiveType(BuildConfiguration config) {
+ CppConfiguration cppConfig = config.getFragment(CppConfiguration.class);
+ return cppConfig.archiveType();
+ }
+
+ /**
+ * Returns the FDO build subtype.
+ */
+ public static String getFdoBuildStamp(CppConfiguration cppConfiguration) {
+ if (cppConfiguration.getFdoSupport().isAutoFdoEnabled()) {
+ return (cppConfiguration.getLipoMode() == LipoMode.BINARY) ? "ALIPO" : "AFDO";
+ }
+ if (cppConfiguration.isFdo()) {
+ return (cppConfiguration.getLipoMode() == LipoMode.BINARY) ? "LIPO" : "FDO";
+ }
+ return null;
+ }
+
+ /**
+ * Returns a relative path to the bin directory for data in AutoFDO LIPO mode.
+ */
+ public static PathFragment getLipoDataBinFragment(BuildConfiguration configuration) {
+ PathFragment parent = configuration.getBinFragment().getParentDirectory();
+ return parent.replaceName(parent.getBaseName() + "-lipodata")
+ .getChild(configuration.getBinFragment().getBaseName());
+ }
+
+ /**
+ * Returns a relative path to the genfiles directory for data in AutoFDO LIPO mode.
+ */
+ public static PathFragment getLipoDataGenfilesFragment(BuildConfiguration configuration) {
+ PathFragment parent = configuration.getGenfilesFragment().getParentDirectory();
+ return parent.replaceName(parent.getBaseName() + "-lipodata")
+ .getChild(configuration.getGenfilesFragment().getBaseName());
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppLinkAction.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppLinkAction.java
new file mode 100644
index 0000000..ecf3431
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppLinkAction.java
@@ -0,0 +1,1074 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import static java.nio.charset.StandardCharsets.ISO_8859_1;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.AbstractAction;
+import com.google.devtools.build.lib.actions.ActionExecutionContext;
+import com.google.devtools.build.lib.actions.ActionExecutionException;
+import com.google.devtools.build.lib.actions.ActionOwner;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.EnvironmentalExecException;
+import com.google.devtools.build.lib.actions.ExecException;
+import com.google.devtools.build.lib.actions.Executor;
+import com.google.devtools.build.lib.actions.ParameterFile;
+import com.google.devtools.build.lib.actions.ParameterFile.ParameterFileType;
+import com.google.devtools.build.lib.actions.ResourceSet;
+import com.google.devtools.build.lib.actions.extra.CppLinkInfo;
+import com.google.devtools.build.lib.actions.extra.ExtraActionInfo;
+import com.google.devtools.build.lib.analysis.AnalysisEnvironment;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.collect.CollectionUtils;
+import com.google.devtools.build.lib.collect.ImmutableIterable;
+import com.google.devtools.build.lib.collect.IterablesChain;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadCompatible;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.rules.cpp.Link.LinkStaticness;
+import com.google.devtools.build.lib.rules.cpp.Link.LinkTargetType;
+import com.google.devtools.build.lib.rules.cpp.LinkerInputs.LibraryToLink;
+import com.google.devtools.build.lib.util.Fingerprint;
+import com.google.devtools.build.lib.util.Pair;
+import com.google.devtools.build.lib.util.ShellEscaper;
+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.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Action that represents an ELF linking step.
+ */
+@ThreadCompatible
+public final class CppLinkAction extends AbstractAction {
+ private static final String LINK_GUID = "58ec78bd-1176-4e36-8143-439f656b181d";
+ private static final String FAKE_LINK_GUID = "da36f819-5a15-43a9-8a45-e01b60e10c8b";
+
+ private final CppConfiguration cppConfiguration;
+ private final LibraryToLink outputLibrary;
+ private final LibraryToLink interfaceOutputLibrary;
+
+ private final LinkCommandLine linkCommandLine;
+
+ /** True for cc_fake_binary targets. */
+ private final boolean fake;
+
+ private final Iterable<Artifact> mandatoryInputs;
+
+ // Linking uses a lot of memory; estimate 1 MB per input file, min 1.5 Gib.
+ // It is vital to not underestimate too much here,
+ // because running too many concurrent links can
+ // thrash the machine to the point where it stops
+ // responding to keystrokes or mouse clicks.
+ // CPU and IO do not scale similarly and still use the static minimum estimate.
+ public static final ResourceSet LINK_RESOURCES_PER_INPUT = new ResourceSet(1, 0, 0);
+
+ // This defines the minimum of each resource that will be reserved.
+ public static final ResourceSet MIN_STATIC_LINK_RESOURCES = new ResourceSet(1536, 1, 0.3);
+
+ // Dynamic linking should be cheaper than static linking.
+ public static final ResourceSet MIN_DYNAMIC_LINK_RESOURCES = new ResourceSet(1024, 0.3, 0.2);
+
+ /**
+ * Use {@link Builder} to create instances of this class. Also see there for
+ * the documentation of all parameters.
+ *
+ * <p>This constructor is intentionally private and is only to be called from
+ * {@link Builder#build()}.
+ */
+ private CppLinkAction(ActionOwner owner,
+ Iterable<Artifact> inputs,
+ ImmutableList<Artifact> outputs,
+ CppConfiguration cppConfiguration,
+ LibraryToLink outputLibrary,
+ LibraryToLink interfaceOutputLibrary,
+ boolean fake,
+ LinkCommandLine linkCommandLine) {
+ super(owner, inputs, outputs);
+ this.mandatoryInputs = inputs;
+ this.cppConfiguration = cppConfiguration;
+ this.outputLibrary = outputLibrary;
+ this.interfaceOutputLibrary = interfaceOutputLibrary;
+ this.fake = fake;
+
+ this.linkCommandLine = linkCommandLine;
+ }
+
+ private static Iterable<LinkerInput> filterLinkerInputs(Iterable<LinkerInput> inputs) {
+ return Iterables.filter(inputs, new Predicate<LinkerInput>() {
+ @Override
+ public boolean apply(LinkerInput input) {
+ return Link.VALID_LINKER_INPUTS.matches(input.getArtifact().getFilename());
+ }
+ });
+ }
+
+ private static Iterable<Artifact> filterLinkerInputArtifacts(Iterable<Artifact> inputs) {
+ return Iterables.filter(inputs, new Predicate<Artifact>() {
+ @Override
+ public boolean apply(Artifact input) {
+ return Link.VALID_LINKER_INPUTS.matches(input.getFilename());
+ }
+ });
+ }
+
+ private CppConfiguration getCppConfiguration() {
+ return cppConfiguration;
+ }
+
+ @VisibleForTesting
+ public String getTargetCpu() {
+ return getCppConfiguration().getTargetCpu();
+ }
+
+ public String getHostSystemName() {
+ return getCppConfiguration().getHostSystemName();
+ }
+
+ /**
+ * Returns the link configuration; for correctness you should not call this method during
+ * execution - only the argv is part of the action cache key, and we therefore don't guarantee
+ * that the action will be re-executed if the contents change in a way that does not affect the
+ * argv.
+ */
+ @VisibleForTesting
+ public LinkCommandLine getLinkCommandLine() {
+ return linkCommandLine;
+ }
+
+ public LibraryToLink getOutputLibrary() {
+ return outputLibrary;
+ }
+
+ public LibraryToLink getInterfaceOutputLibrary() {
+ return interfaceOutputLibrary;
+ }
+
+ /**
+ * Returns the path to the output artifact produced by the linker.
+ */
+ public Path getOutputFile() {
+ return outputLibrary.getArtifact().getPath();
+ }
+
+ @VisibleForTesting
+ public List<String> getRawLinkArgv() {
+ return linkCommandLine.getRawLinkArgv();
+ }
+
+ @VisibleForTesting
+ public List<String> getArgv() {
+ return linkCommandLine.arguments();
+ }
+
+ /**
+ * Prepares and returns the command line specification for this link.
+ * Splits appropriate parts into a .params file and adds any required
+ * linkstamp compilation steps.
+ *
+ * @return a finalized command line suitable for execution
+ */
+ public final List<String> prepareCommandLine(Path execRoot, List<String> inputFiles)
+ throws ExecException {
+ List<String> commandlineArgs;
+ // Try to shorten the command line by use of a parameter file.
+ // This makes the output with --subcommands (et al) more readable.
+ if (linkCommandLine.canBeSplit()) {
+ PathFragment paramExecPath = ParameterFile.derivePath(
+ outputLibrary.getArtifact().getExecPath());
+ Pair<List<String>, List<String>> split = linkCommandLine.splitCommandline(paramExecPath);
+ commandlineArgs = split.first;
+ writeToParamFile(execRoot, paramExecPath, split.second);
+ if (inputFiles != null) {
+ inputFiles.add(paramExecPath.getPathString());
+ }
+ } else {
+ commandlineArgs = linkCommandLine.getRawLinkArgv();
+ }
+ return linkCommandLine.finalizeWithLinkstampCommands(commandlineArgs);
+ }
+
+ private static void writeToParamFile(Path workingDir, PathFragment paramExecPath,
+ List<String> paramFileArgs) throws ExecException {
+ // Create parameter file.
+ ParameterFile paramFile = new ParameterFile(workingDir, paramExecPath, ISO_8859_1,
+ ParameterFileType.UNQUOTED);
+ Path paramFilePath = paramFile.getPath();
+ try {
+ // writeContent() fails for existing files that are marked readonly.
+ paramFilePath.delete();
+ } catch (IOException e) {
+ throw new EnvironmentalExecException("could not delete file '" + paramFilePath + "'", e);
+ }
+ paramFile.writeContent(paramFileArgs);
+
+ // Normally Blaze chmods all output files automatically (see
+ // SkyframeActionExecutor#setOutputsReadOnlyAndExecutable), but this params file is created
+ // out-of-band and is not declared as an output. By chmodding the file, other processes
+ // can observe this file being created.
+ try {
+ paramFilePath.setWritable(false);
+ paramFilePath.setExecutable(true); // for consistency with other action outputs
+ } catch (IOException e) {
+ throw new EnvironmentalExecException("could not chmod param file '" + paramFilePath + "'", e);
+ }
+ }
+
+ @Override
+ @ThreadCompatible
+ public void execute(
+ ActionExecutionContext actionExecutionContext)
+ throws ActionExecutionException, InterruptedException {
+ if (fake) {
+ executeFake();
+ } else {
+ Executor executor = actionExecutionContext.getExecutor();
+
+ try {
+ executor.getContext(CppLinkActionContext.class).exec(
+ this, actionExecutionContext);
+ } catch (ExecException e) {
+ throw e.toActionExecutionException("Linking of rule '" + getOwner().getLabel() + "'",
+ executor.getVerboseFailures(), this);
+ }
+ }
+ }
+
+ @Override
+ public String describeStrategy(Executor executor) {
+ return fake
+ ? "fake,local"
+ : executor.getContext(CppLinkActionContext.class).strategyLocality(this);
+ }
+
+ // Don't forget to update FAKE_LINK_GUID if you modify this method.
+ @ThreadCompatible
+ private void executeFake()
+ throws ActionExecutionException {
+ // The uses of getLinkConfiguration in this method may not be consistent with the computed key.
+ // I.e., this may be incrementally incorrect.
+ final Collection<Artifact> linkstampOutputs = getLinkCommandLine().getLinkstamps().values();
+
+ // Prefix all fake output files in the command line with $TEST_TMPDIR/.
+ final String outputPrefix = "$TEST_TMPDIR/";
+ List<String> escapedLinkArgv = escapeLinkArgv(linkCommandLine.getRawLinkArgv(),
+ linkstampOutputs, outputPrefix);
+ // Write the commands needed to build the real target to the fake target
+ // file.
+ StringBuilder s = new StringBuilder();
+ Joiner.on('\n').appendTo(s,
+ "# This is a fake target file, automatically generated.",
+ "# Do not edit by hand!",
+ "echo $0 is a fake target file and not meant to be executed.",
+ "exit 0",
+ "EOS",
+ "",
+ "makefile_dir=.",
+ "");
+
+ try {
+ // Concatenate all the (fake) .o files into the result.
+ for (LinkerInput linkerInput : getLinkCommandLine().getLinkerInputs()) {
+ Artifact objectFile = linkerInput.getArtifact();
+ if (CppFileTypes.OBJECT_FILE.matches(objectFile.getFilename())
+ && linkerInput.isFake()) {
+ s.append(FileSystemUtils.readContentAsLatin1(objectFile.getPath())); // (IOException)
+ }
+ }
+
+ s.append(getOutputFile().getBaseName()).append(": ");
+ for (Artifact linkstamp : linkstampOutputs) {
+ s.append("mkdir -p " + outputPrefix +
+ linkstamp.getExecPath().getParentDirectory() + " && ");
+ }
+ Joiner.on(' ').appendTo(s,
+ ShellEscaper.escapeAll(linkCommandLine.finalizeAlreadyEscapedWithLinkstampCommands(
+ escapedLinkArgv, outputPrefix)));
+ s.append('\n');
+ if (getOutputFile().exists()) {
+ getOutputFile().setWritable(true); // (IOException)
+ }
+ FileSystemUtils.writeContent(getOutputFile(), ISO_8859_1, s.toString());
+ getOutputFile().setExecutable(true); // (IOException)
+ for (Artifact linkstamp : linkstampOutputs) {
+ FileSystemUtils.touchFile(linkstamp.getPath());
+ }
+ } catch (IOException e) {
+ throw new ActionExecutionException("failed to create fake link command for rule '" +
+ getOwner().getLabel() + ": " + e.getMessage(),
+ this, false);
+ }
+ }
+
+ /**
+ * Shell-escapes the raw link command line.
+ *
+ * @param rawLinkArgv raw link command line
+ * @param linkstampOutputs linkstamp artifacts
+ * @param outputPrefix to be prepended to any outputs
+ * @return escaped link command line
+ */
+ private List<String> escapeLinkArgv(List<String> rawLinkArgv,
+ final Collection<Artifact> linkstampOutputs, final String outputPrefix) {
+ final List<String> linkstampExecPaths = Artifact.asExecPaths(linkstampOutputs);
+ ImmutableList.Builder<String> escapedArgs = ImmutableList.builder();
+ for (String rawArg : rawLinkArgv) {
+ String escapedArg;
+ if (rawArg.equals(getPrimaryOutput().getExecPathString())
+ || linkstampExecPaths.contains(rawArg)) {
+ escapedArg = outputPrefix + ShellEscaper.escapeString(rawArg);
+ } else if (rawArg.startsWith(Link.FAKE_OBJECT_PREFIX)) {
+ escapedArg = outputPrefix + ShellEscaper.escapeString(
+ rawArg.substring(Link.FAKE_OBJECT_PREFIX.length()));
+ } else {
+ escapedArg = ShellEscaper.escapeString(rawArg);
+ }
+ escapedArgs.add(escapedArg);
+ }
+ return escapedArgs.build();
+ }
+
+ @Override
+ public ExtraActionInfo.Builder getExtraActionInfo() {
+ // The uses of getLinkConfiguration in this method may not be consistent with the computed key.
+ // I.e., this may be incrementally incorrect.
+ CppLinkInfo.Builder info = CppLinkInfo.newBuilder();
+ info.addAllInputFile(Artifact.toExecPaths(
+ LinkerInputs.toLibraryArtifacts(getLinkCommandLine().getLinkerInputs())));
+ info.addAllInputFile(Artifact.toExecPaths(
+ LinkerInputs.toLibraryArtifacts(getLinkCommandLine().getRuntimeInputs())));
+ info.setOutputFile(getPrimaryOutput().getExecPathString());
+ if (interfaceOutputLibrary != null) {
+ info.setInterfaceOutputFile(interfaceOutputLibrary.getArtifact().getExecPathString());
+ }
+ info.setLinkTargetType(getLinkCommandLine().getLinkTargetType().name());
+ info.setLinkStaticness(getLinkCommandLine().getLinkStaticness().name());
+ info.addAllLinkStamp(Artifact.toExecPaths(getLinkCommandLine().getLinkstamps().values()));
+ info.addAllBuildInfoHeaderArtifact(
+ Artifact.toExecPaths(getLinkCommandLine().getBuildInfoHeaderArtifacts()));
+ info.addAllLinkOpt(getLinkCommandLine().getLinkopts());
+
+ return super.getExtraActionInfo()
+ .setExtension(CppLinkInfo.cppLinkInfo, info.build());
+ }
+
+ @Override
+ protected String computeKey() {
+ Fingerprint f = new Fingerprint();
+ f.addString(fake ? FAKE_LINK_GUID : LINK_GUID);
+ f.addString(getCppConfiguration().getLdExecutable().getPathString());
+ f.addStrings(linkCommandLine.arguments());
+ // TODO(bazel-team): For correctness, we need to ensure the invariant that all values accessed
+ // during the execution phase are also covered by the key. Above, we add the argv to the key,
+ // which covers most cases. Unfortunately, the extra action and fake support methods above also
+ // sometimes directly access settings from the link configuration that may or may not affect the
+ // key. We either need to change the code to cover them in the key computation, or change the
+ // LinkConfiguration to disallow the combinations where the value of a setting does not affect
+ // the argv.
+ f.addBoolean(linkCommandLine.isNativeDeps());
+ f.addBoolean(linkCommandLine.useTestOnlyFlags());
+ if (linkCommandLine.getRuntimeSolibDir() != null) {
+ f.addPath(linkCommandLine.getRuntimeSolibDir());
+ }
+ return f.hexDigestAndReset();
+ }
+
+ @Override
+ public String describeKey() {
+ StringBuilder message = new StringBuilder();
+ if (fake) {
+ message.append("Fake ");
+ }
+ message.append(getProgressMessage());
+ message.append('\n');
+ message.append(" Command: ");
+ message.append(ShellEscaper.escapeString(
+ getCppConfiguration().getLdExecutable().getPathString()));
+ message.append('\n');
+ // Outputting one argument per line makes it easier to diff the results.
+ for (String argument : ShellEscaper.escapeAll(linkCommandLine.arguments())) {
+ message.append(" Argument: ");
+ message.append(argument);
+ message.append('\n');
+ }
+ return message.toString();
+ }
+
+ @Override
+ public String getMnemonic() { return "CppLink"; }
+
+ @Override
+ protected String getRawProgressMessage() {
+ return "Linking " + outputLibrary.getArtifact().prettyPrint();
+ }
+
+ @Override
+ public ResourceSet estimateResourceConsumption(Executor executor) {
+ return executor.getContext(CppLinkActionContext.class).estimateResourceConsumption(this);
+ }
+
+ /**
+ * Estimate the resources consumed when this action is run locally.
+ */
+ public ResourceSet estimateResourceConsumptionLocal() {
+ // It's ok if this behaves differently even if the key is identical.
+ ResourceSet minLinkResources =
+ getLinkCommandLine().getLinkStaticness() == Link.LinkStaticness.DYNAMIC
+ ? MIN_DYNAMIC_LINK_RESOURCES
+ : MIN_STATIC_LINK_RESOURCES;
+
+ final int inputSize = Iterables.size(getLinkCommandLine().getLinkerInputs())
+ + Iterables.size(getLinkCommandLine().getRuntimeInputs());
+
+ return new ResourceSet(
+ Math.max(inputSize * LINK_RESOURCES_PER_INPUT.getMemoryMb(),
+ minLinkResources.getMemoryMb()),
+ Math.max(inputSize * LINK_RESOURCES_PER_INPUT.getCpuUsage(),
+ minLinkResources.getCpuUsage()),
+ Math.max(inputSize * LINK_RESOURCES_PER_INPUT.getIoUsage(),
+ minLinkResources.getIoUsage())
+ );
+ }
+
+ @Override
+ public Iterable<Artifact> getMandatoryInputs() {
+ return mandatoryInputs;
+ }
+
+ /**
+ * Determines whether or not this link should output a symbol counts file.
+ */
+ private static boolean enableSymbolsCounts(CppConfiguration cppConfiguration, boolean fake,
+ LinkTargetType linkType) {
+ return cppConfiguration.getSymbolCounts()
+ && cppConfiguration.supportsGoldLinker()
+ && linkType == LinkTargetType.EXECUTABLE
+ && !fake;
+ }
+
+ /**
+ * Builder class to construct {@link CppLinkAction}s.
+ */
+ public static class Builder {
+ // Builder-only
+ private final RuleContext ruleContext;
+ private final AnalysisEnvironment analysisEnvironment;
+ private final PathFragment outputPath;
+ private final CcToolchainProvider toolchain;
+ private PathFragment interfaceOutputPath;
+ private PathFragment runtimeSolibDir;
+ protected final BuildConfiguration configuration;
+ private final CppConfiguration cppConfiguration;
+
+ // Morally equivalent with {@link Context}, except these are mutable.
+ // Keep these in sync with {@link Context}.
+ private final Set<LinkerInput> nonLibraries = new LinkedHashSet<>();
+ private final NestedSetBuilder<LibraryToLink> libraries = NestedSetBuilder.linkOrder();
+ private NestedSet<Artifact> crosstoolInputs = NestedSetBuilder.emptySet(Order.STABLE_ORDER);
+ private Artifact runtimeMiddleman;
+ private NestedSet<Artifact> runtimeInputs = NestedSetBuilder.emptySet(Order.STABLE_ORDER);
+ private final NestedSetBuilder<Artifact> compilationInputs = NestedSetBuilder.stableOrder();
+ private final Set<Artifact> linkstamps = new LinkedHashSet<>();
+ private List<String> linkstampOptions = new ArrayList<>();
+ private final List<String> linkopts = new ArrayList<>();
+ private LinkTargetType linkType = LinkTargetType.STATIC_LIBRARY;
+ private LinkStaticness linkStaticness = LinkStaticness.FULLY_STATIC;
+ private boolean fake;
+ private boolean isNativeDeps;
+ private boolean useTestOnlyFlags;
+ private boolean wholeArchive;
+ private boolean supportsParamFiles = true;
+
+ /**
+ * Creates a builder that builds {@link CppLinkAction} instances.
+ *
+ * @param ruleContext the rule that owns the action
+ * @param outputPath the path of the ELF file to be created, relative to the
+ * 'bin' directory
+ */
+ public Builder(RuleContext ruleContext, PathFragment outputPath) {
+ this(ruleContext, outputPath, ruleContext.getConfiguration(),
+ ruleContext.getAnalysisEnvironment(), CppHelper.getToolchain(ruleContext));
+ }
+
+ /**
+ * Creates a builder that builds {@link CppLinkAction} instances.
+ *
+ * @param ruleContext the rule that owns the action
+ * @param outputPath the path of the ELF file to be created, relative to the
+ * 'bin' directory
+ */
+ public Builder(RuleContext ruleContext, PathFragment outputPath,
+ BuildConfiguration configuration, CcToolchainProvider toolchain) {
+ this(ruleContext, outputPath, configuration,
+ ruleContext.getAnalysisEnvironment(), toolchain);
+ }
+
+ /**
+ * Creates a builder that builds {@link CppLinkAction}s.
+ *
+ * @param ruleContext the rule that owns the action
+ * @param outputPath the path of the ELF file to be created, relative to the
+ * 'bin' directory
+ * @param configuration the configuration used to determine the tool chain
+ * and the default link options
+ */
+ private Builder(RuleContext ruleContext, PathFragment outputPath,
+ BuildConfiguration configuration, AnalysisEnvironment analysisEnvironment,
+ CcToolchainProvider toolchain) {
+ this.ruleContext = ruleContext;
+ this.analysisEnvironment = Preconditions.checkNotNull(analysisEnvironment);
+ this.outputPath = Preconditions.checkNotNull(outputPath);
+ this.configuration = Preconditions.checkNotNull(configuration);
+ this.cppConfiguration = configuration.getFragment(CppConfiguration.class);
+ this.toolchain = toolchain;
+
+ // The toolchain != null is here for CppLinkAction.createTestBuilder(). Meh.
+ if (cppConfiguration.supportsEmbeddedRuntimes() && toolchain != null) {
+ runtimeSolibDir = toolchain.getDynamicRuntimeSolibDir();
+ }
+ if (toolchain != null) {
+ supportsParamFiles = toolchain.supportsParamFiles();
+ }
+ }
+
+ /**
+ * Given a Context, creates a Builder that builds {@link CppLinkAction}s.
+ * Note well: Keep the Builder->Context and Context->Builder transforms consistent!
+ * @param ruleContext the rule that owns the action
+ * @param outputPath the path of the ELF file to be created, relative to the
+ * 'bin' directory
+ * @param linkContext an immutable CppLinkAction.Context from the original builder
+ */
+ public Builder(RuleContext ruleContext, PathFragment outputPath, Context linkContext,
+ BuildConfiguration configuration) {
+ // These Builder-only fields get set in the constructor:
+ // ruleContext, analysisEnvironment, outputPath, configuration, runtimeSolibDir
+ this(ruleContext, outputPath, configuration, ruleContext.getAnalysisEnvironment(),
+ CppHelper.getToolchain(ruleContext));
+ Preconditions.checkNotNull(linkContext);
+
+ // All linkContext fields should be transferred to this Builder.
+ this.nonLibraries.addAll(linkContext.nonLibraries);
+ this.libraries.addTransitive(linkContext.libraries);
+ this.crosstoolInputs = linkContext.crosstoolInputs;
+ this.runtimeMiddleman = linkContext.runtimeMiddleman;
+ this.runtimeInputs = linkContext.runtimeInputs;
+ this.compilationInputs.addTransitive(linkContext.compilationInputs);
+ this.linkstamps.addAll(linkContext.linkstamps);
+ this.linkopts.addAll(linkContext.linkopts);
+ this.linkType = linkContext.linkType;
+ this.linkStaticness = linkContext.linkStaticness;
+ this.fake = linkContext.fake;
+ this.isNativeDeps = linkContext.isNativeDeps;
+ this.useTestOnlyFlags = linkContext.useTestOnlyFlags;
+ }
+
+ /**
+ * Builds the Action as configured and returns it.
+ *
+ * <p>This method may only be called once.
+ */
+ public CppLinkAction build() {
+ if (interfaceOutputPath != null && (fake || linkType != LinkTargetType.DYNAMIC_LIBRARY)) {
+ throw new RuntimeException("Interface output can only be used "
+ + "with non-fake DYNAMIC_LIBRARY targets");
+ }
+
+ final Artifact output = createArtifact(outputPath);
+ final Artifact interfaceOutput = (interfaceOutputPath != null)
+ ? createArtifact(interfaceOutputPath)
+ : null;
+
+ final ImmutableList<Artifact> buildInfoHeaderArtifacts = !linkstamps.isEmpty()
+ ? ruleContext.getAnalysisEnvironment().getBuildInfo(ruleContext, CppBuildInfo.KEY)
+ : ImmutableList.<Artifact>of();
+
+ final Artifact symbolCountOutput = enableSymbolsCounts(cppConfiguration, fake, linkType)
+ ? createArtifact(output.getRootRelativePath().replaceName(
+ output.getExecPath().getBaseName() + ".sc"))
+ : null;
+
+ boolean needWholeArchive = wholeArchive || needWholeArchive(
+ linkStaticness, linkType, linkopts, isNativeDeps, cppConfiguration);
+
+ NestedSet<LibraryToLink> uniqueLibraries = libraries.build();
+ final Iterable<Artifact> filteredNonLibraryArtifacts = filterLinkerInputArtifacts(
+ LinkerInputs.toLibraryArtifacts(nonLibraries));
+ final Iterable<LinkerInput> linkerInputs = IterablesChain.<LinkerInput>builder()
+ .add(ImmutableList.copyOf(filterLinkerInputs(nonLibraries)))
+ .add(ImmutableIterable.from(Link.mergeInputsCmdLine(
+ uniqueLibraries, needWholeArchive, cppConfiguration.archiveType())))
+ .build();
+
+ // ruleContext can only be null during testing. This is kind of ugly.
+ final ImmutableSet<String> features = (ruleContext == null)
+ ? ImmutableSet.<String>of()
+ : ruleContext.getFeatures();
+
+ final LibraryToLink outputLibrary =
+ LinkerInputs.newInputLibrary(output, filteredNonLibraryArtifacts);
+ final LibraryToLink interfaceOutputLibrary = interfaceOutput == null ? null :
+ LinkerInputs.newInputLibrary(interfaceOutput, filteredNonLibraryArtifacts);
+
+ final ImmutableMap<Artifact, Artifact> linkstampMap =
+ mapLinkstampsToOutputs(linkstamps, ruleContext, output);
+
+ final ImmutableList<Artifact> actionOutputs = constructOutputs(
+ outputLibrary.getArtifact(),
+ linkstampMap.values(),
+ interfaceOutputLibrary == null ? null : interfaceOutputLibrary.getArtifact(),
+ symbolCountOutput);
+
+ LinkCommandLine linkCommandLine = new LinkCommandLine.Builder(configuration, getOwner())
+ .setOutput(outputLibrary.getArtifact())
+ .setInterfaceOutput(interfaceOutput)
+ .setSymbolCountsOutput(symbolCountOutput)
+ .setBuildInfoHeaderArtifacts(buildInfoHeaderArtifacts)
+ .setLinkerInputs(linkerInputs)
+ .setRuntimeInputs(ImmutableList.copyOf(LinkerInputs.simpleLinkerInputs(runtimeInputs)))
+ .setLinkTargetType(linkType)
+ .setLinkStaticness(linkStaticness)
+ .setLinkopts(ImmutableList.copyOf(linkopts))
+ .setFeatures(features)
+ .setLinkstamps(linkstampMap)
+ .addLinkstampCompileOptions(linkstampOptions)
+ .setRuntimeSolibDir(linkType.isStaticLibraryLink() ? null : runtimeSolibDir)
+ .setNativeDeps(isNativeDeps)
+ .setUseTestOnlyFlags(useTestOnlyFlags)
+ .setNeedWholeArchive(needWholeArchive)
+ .setInterfaceSoBuilder(getInterfaceSoBuilder())
+ .setSupportsParamFiles(supportsParamFiles)
+ .build();
+
+ // Compute the set of inputs - we only need stable order here.
+ NestedSetBuilder<Artifact> dependencyInputsBuilder = NestedSetBuilder.stableOrder();
+ dependencyInputsBuilder.addAll(buildInfoHeaderArtifacts);
+ dependencyInputsBuilder.addAll(linkstamps);
+ dependencyInputsBuilder.addTransitive(crosstoolInputs);
+ if (runtimeMiddleman != null) {
+ dependencyInputsBuilder.add(runtimeMiddleman);
+ }
+ dependencyInputsBuilder.addTransitive(compilationInputs.build());
+
+ Iterable<Artifact> expandedInputs =
+ LinkerInputs.toLibraryArtifacts(Link.mergeInputsDependencies(uniqueLibraries,
+ needWholeArchive, cppConfiguration.archiveType()));
+ // getPrimaryInput returns the first element, and that is a public interface - therefore the
+ // order here is important.
+ Iterable<Artifact> inputs = IterablesChain.<Artifact>builder()
+ .add(ImmutableList.copyOf(LinkerInputs.toLibraryArtifacts(nonLibraries)))
+ .add(dependencyInputsBuilder.build())
+ .add(ImmutableIterable.from(expandedInputs))
+ .deduplicate()
+ .build();
+
+ return new CppLinkAction(
+ getOwner(),
+ inputs,
+ actionOutputs,
+ cppConfiguration,
+ outputLibrary,
+ interfaceOutputLibrary,
+ fake,
+ linkCommandLine);
+ }
+
+ /**
+ * The default heuristic on whether we need to use whole-archive for the link.
+ */
+ private static boolean needWholeArchive(LinkStaticness staticness,
+ LinkTargetType type, Collection<String> linkopts, boolean isNativeDeps,
+ CppConfiguration cppConfig) {
+ boolean fullyStatic = (staticness == LinkStaticness.FULLY_STATIC);
+ boolean mostlyStatic = (staticness == LinkStaticness.MOSTLY_STATIC);
+ boolean sharedLinkopts = type == LinkTargetType.DYNAMIC_LIBRARY
+ || linkopts.contains("-shared")
+ || cppConfig.getLinkOptions().contains("-shared");
+ return (isNativeDeps || cppConfig.legacyWholeArchive())
+ && (fullyStatic || mostlyStatic)
+ && sharedLinkopts;
+ }
+
+ private static ImmutableList<Artifact> constructOutputs(Artifact primaryOutput,
+ Collection<Artifact> outputList, Artifact... outputs) {
+ return new ImmutableList.Builder<Artifact>()
+ .add(primaryOutput)
+ .addAll(outputList)
+ .addAll(CollectionUtils.asListWithoutNulls(outputs))
+ .build();
+ }
+
+ /**
+ * Translates a collection of linkstamp source files to an immutable
+ * mapping from source files to object files. In other words, given a
+ * set of source files, this method determines the output path to which
+ * each file should be compiled.
+ *
+ * @param linkstamps collection of linkstamp source files
+ * @param ruleContext the rule for which this link is being performed
+ * @param outputBinary the binary output path for this link
+ * @return an immutable map that pairs each source file with the
+ * corresponding object file that should be fed into the link
+ */
+ public static ImmutableMap<Artifact, Artifact> mapLinkstampsToOutputs(
+ Collection<Artifact> linkstamps, RuleContext ruleContext, Artifact outputBinary) {
+ ImmutableMap.Builder<Artifact, Artifact> mapBuilder = ImmutableMap.builder();
+
+ PathFragment outputBinaryPath = outputBinary.getRootRelativePath();
+ PathFragment stampOutputDirectory = outputBinaryPath.getParentDirectory().
+ getRelative("_objs").getRelative(outputBinaryPath.getBaseName());
+
+ for (Artifact linkstamp : linkstamps) {
+ PathFragment stampOutputPath = stampOutputDirectory.getRelative(
+ FileSystemUtils.replaceExtension(linkstamp.getRootRelativePath(), ".o"));
+ mapBuilder.put(linkstamp,
+ ruleContext.getAnalysisEnvironment().getDerivedArtifact(
+ stampOutputPath, outputBinary.getRoot()));
+ }
+ return mapBuilder.build();
+ }
+
+ protected ActionOwner getOwner() {
+ return ruleContext.getActionOwner();
+ }
+
+ protected Artifact createArtifact(PathFragment path) {
+ return analysisEnvironment.getDerivedArtifact(path, configuration.getBinDirectory());
+ }
+
+ protected Artifact getInterfaceSoBuilder() {
+ return analysisEnvironment.getEmbeddedToolArtifact(CppRuleClasses.BUILD_INTERFACE_SO);
+ }
+
+ /**
+ * Set the crosstool inputs required for the action.
+ */
+ public Builder setCrosstoolInputs(NestedSet<Artifact> inputs) {
+ this.crosstoolInputs = inputs;
+ return this;
+ }
+
+ /**
+ * Sets the C++ runtime library inputs for the action.
+ */
+ public Builder setRuntimeInputs(Artifact middleman, NestedSet<Artifact> inputs) {
+ Preconditions.checkArgument((middleman == null) == inputs.isEmpty());
+ this.runtimeMiddleman = middleman;
+ this.runtimeInputs = inputs;
+ return this;
+ }
+
+ /**
+ * Sets the interface output of the link. A non-null argument can
+ * only be provided if the link type is {@code DYNAMIC_LIBRARY}
+ * and fake is false.
+ */
+ public Builder setInterfaceOutputPath(PathFragment path) {
+ this.interfaceOutputPath = path;
+ return this;
+ }
+
+ /**
+ * Add additional inputs needed for the linkstamp compilation that is being done as part of the
+ * link.
+ */
+ public Builder addCompilationInputs(Iterable<Artifact> inputs) {
+ this.compilationInputs.addAll(inputs);
+ return this;
+ }
+
+ public Builder addTransitiveCompilationInputs(NestedSet<Artifact> inputs) {
+ this.compilationInputs.addTransitive(inputs);
+ return this;
+ }
+
+ private void addNonLibraryInput(LinkerInput input) {
+ String name = input.getArtifact().getFilename();
+ Preconditions.checkArgument(
+ !Link.ARCHIVE_LIBRARY_FILETYPES.matches(name)
+ && !Link.SHARED_LIBRARY_FILETYPES.matches(name),
+ "'%s' is a library file", input);
+ this.nonLibraries.add(input);
+ }
+ /**
+ * Adds a single artifact to the set of inputs (C++ source files, header files, etc). Artifacts
+ * that are not of recognized types will be used for dependency checking but will not be passed
+ * to the linker. The artifact must not be an archive or a shared library.
+ */
+ public Builder addNonLibraryInput(Artifact input) {
+ addNonLibraryInput(LinkerInputs.simpleLinkerInput(input));
+ return this;
+ }
+
+ /**
+ * Adds multiple artifacts to the set of inputs (C++ source files, header files, etc).
+ * Artifacts that are not of recognized types will be used for dependency checking but will
+ * not be passed to the linker. The artifacts must not be archives or shared libraries.
+ */
+ public Builder addNonLibraryInputs(Iterable<Artifact> inputs) {
+ for (Artifact input : inputs) {
+ addNonLibraryInput(LinkerInputs.simpleLinkerInput(input));
+ }
+ return this;
+ }
+
+ public Builder addFakeNonLibraryInputs(Iterable<Artifact> inputs) {
+ for (Artifact input : inputs) {
+ addNonLibraryInput(LinkerInputs.fakeLinkerInput(input));
+ }
+ return this;
+ }
+
+ private void checkLibrary(LibraryToLink input) {
+ String name = input.getArtifact().getFilename();
+ Preconditions.checkArgument(
+ Link.ARCHIVE_LIBRARY_FILETYPES.matches(name) ||
+ Link.SHARED_LIBRARY_FILETYPES.matches(name),
+ "'%s' is not a library file", input);
+ }
+
+ /**
+ * Adds a single artifact to the set of inputs. The artifact must be an archive or a shared
+ * library. Note that all directly added libraries are implicitly ordered before all nested
+ * sets added with {@link #addLibraries}, even if added in the opposite order.
+ */
+ public Builder addLibrary(LibraryToLink input) {
+ checkLibrary(input);
+ libraries.add(input);
+ return this;
+ }
+
+ /**
+ * Adds multiple artifact to the set of inputs. The artifacts must be archives or shared
+ * libraries.
+ */
+ public Builder addLibraries(NestedSet<LibraryToLink> inputs) {
+ for (LibraryToLink input : inputs) {
+ checkLibrary(input);
+ }
+ this.libraries.addTransitive(inputs);
+ return this;
+ }
+
+ /**
+ * Sets the type of ELF file to be created (.a, .so, .lo, executable). The
+ * default is {@link LinkTargetType#STATIC_LIBRARY}.
+ */
+ public Builder setLinkType(LinkTargetType linkType) {
+ this.linkType = linkType;
+ return this;
+ }
+
+ /**
+ * Sets the degree of "staticness" of the link: fully static (static binding
+ * of all symbols), mostly static (use dynamic binding only for symbols from
+ * glibc), dynamic (use dynamic binding wherever possible). The default is
+ * {@link LinkStaticness#FULLY_STATIC}.
+ */
+ public Builder setLinkStaticness(LinkStaticness linkStaticness) {
+ this.linkStaticness = linkStaticness;
+ return this;
+ }
+
+ /**
+ * Adds a C++ source file which will be compiled at link time. This is used
+ * to embed various values from the build system into binaries to identify
+ * their provenance.
+ *
+ * <p>Link stamps are also automatically added to the inputs.
+ */
+ public Builder addLinkstamps(Map<Artifact, ImmutableList<Artifact>> linkstamps) {
+ this.linkstamps.addAll(linkstamps.keySet());
+ // Add inputs for linkstamping.
+ if (!linkstamps.isEmpty()) {
+ // This will just be the compiler unless include scanning is disabled, in which case it will
+ // include all header files. Since we insist that linkstamps declare all their headers, all
+ // header files would be overkill, but that only happens when include scanning is disabled.
+ addTransitiveCompilationInputs(toolchain.getCompile());
+ for (Map.Entry<Artifact, ImmutableList<Artifact>> entry : linkstamps.entrySet()) {
+ addCompilationInputs(entry.getValue());
+ }
+ }
+ return this;
+ }
+
+ public Builder addLinkstampCompilerOptions(ImmutableList<String> linkstampOptions) {
+ this.linkstampOptions = linkstampOptions;
+ return this;
+ }
+
+ /**
+ * Adds an additional linker option.
+ */
+ public Builder addLinkopt(String linkopt) {
+ this.linkopts.add(linkopt);
+ return this;
+ }
+
+ /**
+ * Adds multiple linker options at once.
+ *
+ * @see #addLinkopt(String)
+ */
+ public Builder addLinkopts(Collection<String> linkopts) {
+ this.linkopts.addAll(linkopts);
+ return this;
+ }
+
+ /**
+ * Sets whether this link action will be used for a cc_fake_binary; false by
+ * default.
+ */
+ public Builder setFake(boolean fake) {
+ this.fake = fake;
+ return this;
+ }
+
+ /**
+ * Sets whether this link action is used for a native dependency library.
+ */
+ public Builder setNativeDeps(boolean isNativeDeps) {
+ this.isNativeDeps = isNativeDeps;
+ return this;
+ }
+
+ /**
+ * Setting this to true overrides the default whole-archive computation and force-enables
+ * whole archives for every archive in the link. This is only necessary for linking executable
+ * binaries that are supposed to export symbols.
+ *
+ * <p>Usually, the link action while use whole archives for dynamic libraries that are native
+ * deps (or the legacy whole archive flag is enabled), and that are not dynamically linked.
+ *
+ * <p>(Note that it is possible to build dynamic libraries with cc_binary rules by specifying
+ * linkshared = 1, and giving the rule a name that matches the pattern {@code
+ * lib<name>.so}.)
+ */
+ public Builder setWholeArchive(boolean wholeArchive) {
+ this.wholeArchive = wholeArchive;
+ return this;
+ }
+
+ /**
+ * Sets whether this link action should use test-specific flags (e.g. $EXEC_ORIGIN instead of
+ * $ORIGIN for the solib search path or lazy binding); false by default.
+ */
+ public Builder setUseTestOnlyFlags(boolean useTestOnlyFlags) {
+ this.useTestOnlyFlags = useTestOnlyFlags;
+ return this;
+ }
+
+ /**
+ * Sets the name of the directory where the solib symlinks for the dynamic runtime libraries
+ * live. This is usually automatically set from the cc_toolchain.
+ */
+ public Builder setRuntimeSolibDir(PathFragment runtimeSolibDir) {
+ this.runtimeSolibDir = runtimeSolibDir;
+ return this;
+ }
+
+ /**
+ * Creates a builder without the need for a {@link RuleContext}.
+ * This is to be used exclusively for testing purposes.
+ *
+ * <p>Link stamping is not supported if using this method.
+ */
+ @VisibleForTesting
+ public static Builder createTestBuilder(
+ final ActionOwner owner, final AnalysisEnvironment analysisEnvironment,
+ final PathFragment outputPath, BuildConfiguration config) {
+ return new Builder(null, outputPath, config, analysisEnvironment, null) {
+ @Override
+ protected Artifact createArtifact(PathFragment path) {
+ return new Artifact(configuration.getBinDirectory().getPath().getRelative(path),
+ configuration.getBinDirectory(), configuration.getBinFragment().getRelative(path),
+ analysisEnvironment.getOwner());
+ }
+ @Override
+ protected ActionOwner getOwner() {
+ return owner;
+ }
+ };
+ }
+ }
+
+ /**
+ * Immutable ELF linker context, suitable for serialization.
+ */
+ @Immutable @ThreadSafe
+ public static final class Context implements TransitiveInfoProvider {
+ // Morally equivalent with {@link Builder}, except these are immutable.
+ // Keep these in sync with {@link Builder}.
+ private final ImmutableSet<LinkerInput> nonLibraries;
+ private final NestedSet<LibraryToLink> libraries;
+ private final NestedSet<Artifact> crosstoolInputs;
+ private final Artifact runtimeMiddleman;
+ private final NestedSet<Artifact> runtimeInputs;
+ private final NestedSet<Artifact> compilationInputs;
+ private final ImmutableSet<Artifact> linkstamps;
+ private final ImmutableList<String> linkopts;
+ private final LinkTargetType linkType;
+ private final LinkStaticness linkStaticness;
+ private final boolean fake;
+ private final boolean isNativeDeps;
+ private final boolean useTestOnlyFlags;
+
+ /**
+ * Given a {@link Builder}, creates a {@code Context} to pass to another target.
+ * Note well: Keep the Builder->Context and Context->Builder transforms consistent!
+ * @param builder a mutable {@link CppLinkAction.Builder} to clone from
+ */
+ public Context(Builder builder) {
+ this.nonLibraries = ImmutableSet.copyOf(builder.nonLibraries);
+ this.libraries = NestedSetBuilder.<LibraryToLink>linkOrder()
+ .addTransitive(builder.libraries.build()).build();
+ this.crosstoolInputs =
+ NestedSetBuilder.<Artifact>stableOrder().addTransitive(builder.crosstoolInputs).build();
+ this.runtimeMiddleman = builder.runtimeMiddleman;
+ this.runtimeInputs =
+ NestedSetBuilder.<Artifact>stableOrder().addTransitive(builder.runtimeInputs).build();
+ this.compilationInputs = NestedSetBuilder.<Artifact>stableOrder()
+ .addTransitive(builder.compilationInputs.build()).build();
+ this.linkstamps = ImmutableSet.copyOf(builder.linkstamps);
+ this.linkopts = ImmutableList.copyOf(builder.linkopts);
+ this.linkType = builder.linkType;
+ this.linkStaticness = builder.linkStaticness;
+ this.fake = builder.fake;
+ this.isNativeDeps = builder.isNativeDeps;
+ this.useTestOnlyFlags = builder.useTestOnlyFlags;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppLinkActionContext.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppLinkActionContext.java
new file mode 100644
index 0000000..24a936b
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppLinkActionContext.java
@@ -0,0 +1,44 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import com.google.devtools.build.lib.actions.ActionContextMarker;
+import com.google.devtools.build.lib.actions.ActionExecutionContext;
+import com.google.devtools.build.lib.actions.ActionExecutionException;
+import com.google.devtools.build.lib.actions.ExecException;
+import com.google.devtools.build.lib.actions.Executor.ActionContext;
+import com.google.devtools.build.lib.actions.ResourceSet;
+
+/**
+ * Context for executing {@link CppLinkAction}s.
+ */
+@ActionContextMarker(name = "C++ link")
+public interface CppLinkActionContext extends ActionContext {
+ /**
+ * Returns where the action actually runs.
+ */
+ String strategyLocality(CppLinkAction action);
+
+ /**
+ * Returns the estimated resource consumption of the action.
+ */
+ ResourceSet estimateResourceConsumption(CppLinkAction action);
+
+ /**
+ * Executes the specified action.
+ */
+ void exec(CppLinkAction action,
+ ActionExecutionContext actionExecutionContext)
+ throws ExecException, ActionExecutionException, InterruptedException;
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppModel.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppModel.java
new file mode 100644
index 0000000..44258a5
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppModel.java
@@ -0,0 +1,707 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.AnalysisEnvironment;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.rules.cpp.CcCompilationOutputs.Builder;
+import com.google.devtools.build.lib.rules.cpp.CcToolchainFeatures.FeatureConfiguration;
+import com.google.devtools.build.lib.rules.cpp.Link.LinkStaticness;
+import com.google.devtools.build.lib.rules.cpp.Link.LinkTargetType;
+import com.google.devtools.build.lib.rules.cpp.LinkerInputs.LibraryToLink;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.util.Pair;
+import com.google.devtools.build.lib.util.RegexFilter;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.regex.Pattern;
+
+import javax.annotation.Nullable;
+
+/**
+ * Representation of a C/C++ compilation. Its purpose is to share the code that creates compilation
+ * actions between all classes that need to do so. It follows the builder pattern - load up the
+ * necessary settings and then call {@link #createCcCompileActions}.
+ *
+ * <p>This class is not thread-safe, and it should only be used once for each set of source files,
+ * i.e. calling {@link #createCcCompileActions} will throw an Exception if called twice.
+ */
+public final class CppModel {
+ private final CppSemantics semantics;
+ private final RuleContext ruleContext;
+ private final BuildConfiguration configuration;
+ private final CppConfiguration cppConfiguration;
+
+ // compile model
+ private CppCompilationContext context;
+ private final List<Pair<Artifact, Label>> sourceFiles = new ArrayList<>();
+ private final List<String> copts = new ArrayList<>();
+ private final List<PathFragment> additionalIncludes = new ArrayList<>();
+ @Nullable private Pattern nocopts;
+ private boolean fake;
+ private boolean maySaveTemps;
+ private boolean onlySingleOutput;
+ private CcCompilationOutputs compilationOutputs;
+ private boolean enableLayeringCheck;
+ private boolean compileHeaderModules;
+
+ // link model
+ private final List<String> linkopts = new ArrayList<>();
+ private LinkTargetType linkType = LinkTargetType.STATIC_LIBRARY;
+ private boolean neverLink;
+ private boolean allowInterfaceSharedObjects;
+ private boolean createDynamicLibrary = true;
+ private PathFragment soImplFilename;
+ private FeatureConfiguration featureConfiguration;
+
+ public CppModel(RuleContext ruleContext, CppSemantics semantics) {
+ this.ruleContext = ruleContext;
+ this.semantics = semantics;
+ configuration = ruleContext.getConfiguration();
+ cppConfiguration = configuration.getFragment(CppConfiguration.class);
+ }
+
+ /**
+ * If the cpp compilation is a fake, then it creates only a single compile action without PIC.
+ * Defaults to false.
+ */
+ public CppModel setFake(boolean fake) {
+ this.fake = fake;
+ return this;
+ }
+
+ /**
+ * If set, the CppModel only creates a single .o output that can be linked into a dynamic library,
+ * i.e., it never generates both PIC and non-PIC outputs. Otherwise it creates outputs that can be
+ * linked into both static binaries and dynamic libraries (if both require PIC or both require
+ * non-PIC, then it still only creates a single output). Defaults to false.
+ */
+ public CppModel setOnlySingleOutput(boolean onlySingleOutput) {
+ this.onlySingleOutput = onlySingleOutput;
+ return this;
+ }
+
+ /**
+ * If set, use compiler flags to enable compiler based layering checks.
+ */
+ public CppModel setEnableLayeringCheck(boolean enableLayeringCheck) {
+ this.enableLayeringCheck = enableLayeringCheck;
+ return this;
+ }
+
+ /**
+ * If set, add actions that compile header modules to the build.
+ * See http://clang.llvm.org/docs/Modules.html for more information.
+ */
+ public CppModel setCompileHeaderModules(boolean compileHeaderModules) {
+ this.compileHeaderModules = compileHeaderModules;
+ return this;
+ }
+
+ /**
+ * Whether to create actions for temps. This defaults to false.
+ */
+ public CppModel setSaveTemps(boolean maySaveTemps) {
+ this.maySaveTemps = maySaveTemps;
+ return this;
+ }
+
+ /**
+ * Sets the compilation context, i.e. include directories and allowed header files inclusions.
+ */
+ public CppModel setContext(CppCompilationContext context) {
+ this.context = context;
+ return this;
+ }
+
+ /**
+ * Adds a single source file to be compiled. Note that this should only be called for primary
+ * compilation units, not for header files or files that are otherwise included.
+ */
+ public CppModel addSources(Iterable<Artifact> sourceFiles, Label sourceLabel) {
+ for (Artifact sourceFile : sourceFiles) {
+ this.sourceFiles.add(Pair.of(sourceFile, sourceLabel));
+ }
+ return this;
+ }
+
+ /**
+ * Adds all the source files. Note that this should only be called for primary compilation units,
+ * not for header files or files that are otherwise included.
+ */
+ public CppModel addSources(Iterable<Pair<Artifact, Label>> sources) {
+ Iterables.addAll(this.sourceFiles, sources);
+ return this;
+ }
+
+ /**
+ * Adds the given copts.
+ */
+ public CppModel addCopts(Collection<String> copts) {
+ this.copts.addAll(copts);
+ return this;
+ }
+
+ /**
+ * Sets the nocopts pattern. This is used to filter out flags from the system defined set of
+ * flags. By default no filter is applied.
+ */
+ public CppModel setNoCopts(@Nullable Pattern nocopts) {
+ this.nocopts = nocopts;
+ return this;
+ }
+
+ /**
+ * This can be used to specify additional include directories, without modifying the compilation
+ * context.
+ */
+ public CppModel addAdditionalIncludes(Collection<PathFragment> additionalIncludes) {
+ // TODO(bazel-team): Maybe this could be handled by the compilation context instead?
+ this.additionalIncludes.addAll(additionalIncludes);
+ return this;
+ }
+
+ /**
+ * Adds the given linkopts to the optional dynamic library link command.
+ */
+ public CppModel addLinkopts(Collection<String> linkopts) {
+ this.linkopts.addAll(linkopts);
+ return this;
+ }
+
+ /**
+ * Sets the link type used for the link actions. Note that only static links are supported at this
+ * time.
+ */
+ public CppModel setLinkTargetType(LinkTargetType linkType) {
+ this.linkType = linkType;
+ return this;
+ }
+
+ public CppModel setNeverLink(boolean neverLink) {
+ this.neverLink = neverLink;
+ return this;
+ }
+
+ /**
+ * Whether to allow interface dynamic libraries. Note that setting this to true only has an effect
+ * if the configuration allows it. Defaults to false.
+ */
+ public CppModel setAllowInterfaceSharedObjects(boolean allowInterfaceSharedObjects) {
+ // TODO(bazel-team): Set the default to true, and require explicit action to disable it.
+ this.allowInterfaceSharedObjects = allowInterfaceSharedObjects;
+ return this;
+ }
+
+ public CppModel setCreateDynamicLibrary(boolean createDynamicLibrary) {
+ this.createDynamicLibrary = createDynamicLibrary;
+ return this;
+ }
+
+ public CppModel setDynamicLibraryPath(PathFragment soImplFilename) {
+ this.soImplFilename = soImplFilename;
+ return this;
+ }
+
+ /**
+ * Sets the feature configuration to be used for C/C++ actions.
+ */
+ public CppModel setFeatureConfiguration(FeatureConfiguration featureConfiguration) {
+ this.featureConfiguration = featureConfiguration;
+ return this;
+ }
+
+ /**
+ * @return the non-pic header module artifact for the current target.
+ */
+ public Artifact getHeaderModule(Artifact moduleMapArtifact) {
+ PathFragment objectDir = CppHelper.getObjDirectory(ruleContext.getLabel());
+ PathFragment outputName = objectDir.getRelative(
+ semantics.getEffectiveSourcePath(moduleMapArtifact));
+ return ruleContext.getRelatedArtifact(outputName, ".pcm");
+ }
+
+ /**
+ * @return the pic header module artifact for the current target.
+ */
+ public Artifact getPicHeaderModule(Artifact moduleMapArtifact) {
+ PathFragment objectDir = CppHelper.getObjDirectory(ruleContext.getLabel());
+ PathFragment outputName = objectDir.getRelative(
+ semantics.getEffectiveSourcePath(moduleMapArtifact));
+ return ruleContext.getRelatedArtifact(outputName, ".pic.pcm");
+ }
+
+ /**
+ * @return whether this target needs to generate pic actions.
+ */
+ public boolean getGeneratePicActions() {
+ return CppHelper.usePic(ruleContext, false);
+ }
+
+ /**
+ * @return whether this target needs to generate non-pic actions.
+ */
+ public boolean getGenerateNoPicActions() {
+ return
+ // If we always need pic for everything, then don't bother to create a no-pic action.
+ (!CppHelper.usePic(ruleContext, true) || !CppHelper.usePic(ruleContext, false))
+ // onlySingleOutput guarantees that the code is only ever linked into a dynamic library - so
+ // we don't need a no-pic action even if linking into a binary would require it.
+ && !((onlySingleOutput && getGeneratePicActions()));
+ }
+
+ /**
+ * @return whether this target needs to generate a pic header module.
+ */
+ public boolean getGeneratesPicHeaderModule() {
+ // TODO(bazel-team): Make sure cc_fake_binary works with header module support.
+ return compileHeaderModules && !fake && getGeneratePicActions();
+ }
+
+ /**
+ * @return whether this target needs to generate a non-pic header module.
+ */
+ public boolean getGeratesNoPicHeaderModule() {
+ return compileHeaderModules && !fake && getGenerateNoPicActions();
+ }
+
+ /**
+ * Returns a {@code CppCompileActionBuilder} with the common fields for a C++ compile action
+ * being initialized.
+ */
+ private CppCompileActionBuilder initializeCompileAction(Artifact sourceArtifact,
+ Label sourceLabel) {
+ CppCompileActionBuilder builder = createCompileActionBuilder(sourceArtifact, sourceLabel);
+ if (nocopts != null) {
+ builder.addNocopts(nocopts);
+ }
+
+ builder.setEnableLayeringCheck(enableLayeringCheck);
+ builder.setCompileHeaderModules(compileHeaderModules);
+ builder.setExtraSystemIncludePrefixes(additionalIncludes);
+ builder.setFdoBuildStamp(CppHelper.getFdoBuildStamp(cppConfiguration));
+ builder.setFeatureConfiguration(featureConfiguration);
+ return builder;
+ }
+
+ /**
+ * Constructs the C++ compiler actions. It generally creates one action for every specified source
+ * file. It takes into account LIPO, fake-ness, coverage, and PIC, in addition to using the
+ * settings specified on the current object. This method should only be called once.
+ */
+ public CcCompilationOutputs createCcCompileActions() {
+ CcCompilationOutputs.Builder result = new CcCompilationOutputs.Builder();
+ Preconditions.checkNotNull(context);
+ AnalysisEnvironment env = ruleContext.getAnalysisEnvironment();
+ PathFragment objectDir = CppHelper.getObjDirectory(ruleContext.getLabel());
+
+ if (compileHeaderModules) {
+ Artifact moduleMapArtifact = context.getCppModuleMap().getArtifact();
+ Label moduleMapLabel = Label.parseAbsoluteUnchecked(context.getCppModuleMap().getName());
+ PathFragment outputName = getObjectOutputPath(moduleMapArtifact, objectDir);
+ CppCompileActionBuilder builder = initializeCompileAction(moduleMapArtifact, moduleMapLabel);
+
+ // A header module compile action is just like a normal compile action, but:
+ // - the compiled source file is the module map
+ // - it creates a header module (.pcm file).
+ createSourceAction(outputName, result, env, moduleMapArtifact, builder, ".pcm");
+ }
+
+ for (Pair<Artifact, Label> source : sourceFiles) {
+ Artifact sourceArtifact = source.getFirst();
+ Label sourceLabel = source.getSecond();
+ PathFragment outputName = getObjectOutputPath(sourceArtifact, objectDir);
+ CppCompileActionBuilder builder = initializeCompileAction(sourceArtifact, sourceLabel);
+
+ if (CppFileTypes.CPP_HEADER.matches(source.first.getExecPath())) {
+ createHeaderAction(outputName, result, env, builder);
+ } else {
+ createSourceAction(outputName, result, env, sourceArtifact, builder, ".o");
+ }
+ }
+
+ compilationOutputs = result.build();
+ return compilationOutputs;
+ }
+
+ private void createHeaderAction(PathFragment outputName, Builder result, AnalysisEnvironment env,
+ CppCompileActionBuilder builder) {
+ builder.setOutputFile(ruleContext.getRelatedArtifact(outputName, ".h.processed")).setDotdFile(
+ outputName, ".h.d", ruleContext);
+ semantics.finalizeCompileActionBuilder(ruleContext, builder);
+ CppCompileAction compileAction = builder.build();
+ env.registerAction(compileAction);
+ Artifact tokenFile = compileAction.getOutputFile();
+ result.addHeaderTokenFile(tokenFile);
+ }
+
+ private void createSourceAction(PathFragment outputName,
+ CcCompilationOutputs.Builder result,
+ AnalysisEnvironment env,
+ Artifact sourceArtifact,
+ CppCompileActionBuilder builder,
+ String outputExtension) {
+ PathFragment ccRelativeName = semantics.getEffectiveSourcePath(sourceArtifact);
+ LipoContextProvider lipoProvider = null;
+ if (cppConfiguration.isLipoOptimization()) {
+ // TODO(bazel-team): we shouldn't be needing this, merging context with the binary
+ // is a superset of necessary information.
+ lipoProvider = Preconditions.checkNotNull(CppHelper.getLipoContextProvider(ruleContext),
+ outputName);
+ builder.setContext(CppCompilationContext.mergeForLipo(lipoProvider.getLipoContext(),
+ context));
+ }
+ if (fake) {
+ // For cc_fake_binary, we only create a single fake compile action. It's
+ // not necessary to use -fPIC for negative compilation tests, and using
+ // .pic.o files in cc_fake_binary would break existing uses of
+ // cc_fake_binary.
+ Artifact outputFile = ruleContext.getRelatedArtifact(outputName, outputExtension);
+ PathFragment tempOutputName =
+ FileSystemUtils.replaceExtension(outputFile.getExecPath(), ".temp" + outputExtension);
+ builder
+ .setOutputFile(outputFile)
+ .setDotdFile(outputName, ".d", ruleContext)
+ .setTempOutputFile(tempOutputName);
+ semantics.finalizeCompileActionBuilder(ruleContext, builder);
+ CppCompileAction action = builder.build();
+ env.registerAction(action);
+ result.addObjectFile(action.getOutputFile());
+ } else {
+ boolean generatePicAction = getGeneratePicActions();
+ // If we always need pic for everything, then don't bother to create a no-pic action.
+ boolean generateNoPicAction = getGenerateNoPicActions();
+ Preconditions.checkState(generatePicAction || generateNoPicAction);
+
+ // Create PIC compile actions (same as non-PIC, but use -fPIC and
+ // generate .pic.o, .pic.d, .pic.gcno instead of .o, .d, .gcno.)
+ if (generatePicAction) {
+ CppCompileActionBuilder picBuilder = copyAsPicBuilder(builder, outputName, outputExtension);
+ cppConfiguration.getFdoSupport().configureCompilation(picBuilder, ruleContext, env,
+ ruleContext.getLabel(), ccRelativeName, nocopts, /*usePic=*/true,
+ lipoProvider);
+
+ if (maySaveTemps) {
+ result.addTemps(
+ createTempsActions(sourceArtifact, outputName, picBuilder, /*usePic=*/true));
+ }
+
+ if (isCodeCoverageEnabled()) {
+ picBuilder.setGcnoFile(ruleContext.getRelatedArtifact(outputName, ".pic.gcno"));
+ }
+
+ semantics.finalizeCompileActionBuilder(ruleContext, picBuilder);
+ CppCompileAction picAction = picBuilder.build();
+ env.registerAction(picAction);
+ result.addPicObjectFile(picAction.getOutputFile());
+ if (picAction.getDwoFile() != null) {
+ // Host targets don't produce .dwo files.
+ result.addPicDwoFile(picAction.getDwoFile());
+ }
+ if (cppConfiguration.isLipoContextCollector() && !generateNoPicAction) {
+ result.addLipoScannable(picAction);
+ }
+ }
+
+ if (generateNoPicAction) {
+ builder
+ .setOutputFile(ruleContext.getRelatedArtifact(outputName, outputExtension))
+ .setDotdFile(outputName, ".d", ruleContext);
+ // Create non-PIC compile actions
+ cppConfiguration.getFdoSupport().configureCompilation(builder, ruleContext, env,
+ ruleContext.getLabel(), ccRelativeName, nocopts, /*usePic=*/false,
+ lipoProvider);
+
+ if (maySaveTemps) {
+ result.addTemps(
+ createTempsActions(sourceArtifact, outputName, builder, /*usePic=*/false));
+ }
+
+ if (!cppConfiguration.isLipoOptimization() && isCodeCoverageEnabled()) {
+ builder.setGcnoFile(ruleContext.getRelatedArtifact(outputName, ".gcno"));
+ }
+
+ semantics.finalizeCompileActionBuilder(ruleContext, builder);
+ CppCompileAction compileAction = builder.build();
+ env.registerAction(compileAction);
+ Artifact objectFile = compileAction.getOutputFile();
+ result.addObjectFile(objectFile);
+ if (compileAction.getDwoFile() != null) {
+ // Host targets don't produce .dwo files.
+ result.addDwoFile(compileAction.getDwoFile());
+ }
+ if (cppConfiguration.isLipoContextCollector()) {
+ result.addLipoScannable(compileAction);
+ }
+ }
+ }
+ }
+
+ /**
+ * Constructs the C++ linker actions. It generally generates two actions, one for a static library
+ * and one for a dynamic library. If PIC is required for shared libraries, but not for binaries,
+ * it additionally creates a third action to generate a PIC static library.
+ *
+ * <p>For dynamic libraries, this method can additionally create an interface shared library that
+ * can be used for linking, but doesn't contain any executable code. This increases the number of
+ * cache hits for link actions. Call {@link #setAllowInterfaceSharedObjects(boolean)} to enable
+ * this behavior.
+ */
+ public CcLinkingOutputs createCcLinkActions(CcCompilationOutputs ccOutputs) {
+ // For now only handle static links. Note that the dynamic library link below ignores linkType.
+ // TODO(bazel-team): Either support non-static links or move this check to setLinkType().
+ Preconditions.checkState(linkType.isStaticLibraryLink(), "can only handle static links");
+
+ CcLinkingOutputs.Builder result = new CcLinkingOutputs.Builder();
+ if (cppConfiguration.isLipoContextCollector()) {
+ // Don't try to create LIPO link actions in collector mode,
+ // because it needs some data that's not available at this point.
+ return result.build();
+ }
+
+ AnalysisEnvironment env = ruleContext.getAnalysisEnvironment();
+ boolean usePicForBinaries = CppHelper.usePic(ruleContext, true);
+ boolean usePicForSharedLibs = CppHelper.usePic(ruleContext, false);
+
+ // Create static library (.a). The linkType only reflects whether the library is alwayslink or
+ // not. The PIC-ness is determined by whether we need to use PIC or not. There are three cases
+ // for (usePicForSharedLibs usePicForBinaries):
+ //
+ // (1) (false false) -> no pic code
+ // (2) (true false) -> shared libraries as pic, but not binaries
+ // (3) (true true) -> both shared libraries and binaries as pic
+ //
+ // In case (3), we always need PIC, so only create one static library containing the PIC object
+ // files. The name therefore does not match the content.
+ //
+ // Presumably, it is done this way because the .a file is an implicit output of every cc_library
+ // rule, so we can't use ".pic.a" that in the always-PIC case.
+ PathFragment linkedFileName = CppHelper.getLinkedFilename(ruleContext, linkType);
+ CppLinkAction maybePicAction = newLinkActionBuilder(linkedFileName)
+ .addNonLibraryInputs(ccOutputs.getObjectFiles(usePicForBinaries))
+ .addNonLibraryInputs(ccOutputs.getHeaderTokenFiles())
+ .setLinkType(linkType)
+ .setLinkStaticness(LinkStaticness.FULLY_STATIC)
+ .build();
+ env.registerAction(maybePicAction);
+ result.addStaticLibrary(maybePicAction.getOutputLibrary());
+
+ // Create a second static library (.pic.a). Only in case (2) do we need both PIC and non-PIC
+ // static libraries. In that case, the first static library contains the non-PIC code, and this
+ // one contains the PIC code, so the names match the content.
+ if (!usePicForBinaries && usePicForSharedLibs) {
+ LinkTargetType picLinkType = (linkType == LinkTargetType.ALWAYS_LINK_STATIC_LIBRARY)
+ ? LinkTargetType.ALWAYS_LINK_PIC_STATIC_LIBRARY
+ : LinkTargetType.PIC_STATIC_LIBRARY;
+
+ PathFragment picFileName = CppHelper.getLinkedFilename(ruleContext, picLinkType);
+ CppLinkAction picAction = newLinkActionBuilder(picFileName)
+ .addNonLibraryInputs(ccOutputs.getObjectFiles(true))
+ .addNonLibraryInputs(ccOutputs.getHeaderTokenFiles())
+ .setLinkType(picLinkType)
+ .setLinkStaticness(LinkStaticness.FULLY_STATIC)
+ .build();
+ env.registerAction(picAction);
+ result.addPicStaticLibrary(picAction.getOutputLibrary());
+ }
+
+ if (!createDynamicLibrary) {
+ return result.build();
+ }
+
+ // Create dynamic library.
+ if (soImplFilename == null) {
+ soImplFilename = CppHelper.getLinkedFilename(ruleContext, LinkTargetType.DYNAMIC_LIBRARY);
+ }
+ List<String> sonameLinkopts = ImmutableList.of();
+ PathFragment soInterfaceFilename = null;
+ if (cppConfiguration.useInterfaceSharedObjects() && allowInterfaceSharedObjects) {
+ soInterfaceFilename =
+ CppHelper.getLinkedFilename(ruleContext, LinkTargetType.INTERFACE_DYNAMIC_LIBRARY);
+ Artifact dynamicLibrary = env.getDerivedArtifact(
+ soImplFilename, configuration.getBinDirectory());
+ sonameLinkopts = ImmutableList.of("-Wl,-soname=" +
+ SolibSymlinkAction.getDynamicLibrarySoname(dynamicLibrary.getRootRelativePath(), false));
+ }
+
+ // Should we also link in any libraries that this library depends on?
+ // That is required on some systems...
+ CppLinkAction action = newLinkActionBuilder(soImplFilename)
+ .setInterfaceOutputPath(soInterfaceFilename)
+ .addNonLibraryInputs(ccOutputs.getObjectFiles(usePicForSharedLibs))
+ .addNonLibraryInputs(ccOutputs.getHeaderTokenFiles())
+ .setLinkType(LinkTargetType.DYNAMIC_LIBRARY)
+ .setLinkStaticness(LinkStaticness.DYNAMIC)
+ .addLinkopts(linkopts)
+ .addLinkopts(sonameLinkopts)
+ .setRuntimeInputs(
+ CppHelper.getToolchain(ruleContext).getDynamicRuntimeLinkMiddleman(),
+ CppHelper.getToolchain(ruleContext).getDynamicRuntimeLinkInputs())
+ .build();
+ env.registerAction(action);
+
+ LibraryToLink dynamicLibrary = action.getOutputLibrary();
+ LibraryToLink interfaceLibrary = action.getInterfaceOutputLibrary();
+ if (interfaceLibrary == null) {
+ interfaceLibrary = dynamicLibrary;
+ }
+
+ // If shared library has neverlink=1, then leave it untouched. Otherwise,
+ // create a mangled symlink for it and from now on reference it through
+ // mangled name only.
+ if (neverLink) {
+ result.addDynamicLibrary(interfaceLibrary);
+ result.addExecutionDynamicLibrary(dynamicLibrary);
+ } else {
+ LibraryToLink libraryLink = SolibSymlinkAction.getDynamicLibrarySymlink(
+ ruleContext, interfaceLibrary.getArtifact(), false, false,
+ ruleContext.getConfiguration());
+ result.addDynamicLibrary(libraryLink);
+ LibraryToLink implLibraryLink = SolibSymlinkAction.getDynamicLibrarySymlink(
+ ruleContext, dynamicLibrary.getArtifact(), false, false,
+ ruleContext.getConfiguration());
+ result.addExecutionDynamicLibrary(implLibraryLink);
+ }
+ return result.build();
+ }
+
+ private CppLinkAction.Builder newLinkActionBuilder(PathFragment outputPath) {
+ return new CppLinkAction.Builder(ruleContext, outputPath)
+ .setCrosstoolInputs(CppHelper.getToolchain(ruleContext).getLink())
+ .addNonLibraryInputs(context.getCompilationPrerequisites());
+ }
+
+ /**
+ * Returns the output artifact path relative to the object directory.
+ */
+ private PathFragment getObjectOutputPath(Artifact source, PathFragment objectDirectory) {
+ return objectDirectory.getRelative(semantics.getEffectiveSourcePath(source));
+ }
+
+ /**
+ * Creates a basic cpp compile action builder for source file. Configures options,
+ * crosstool inputs, output and dotd file names, compilation context and copts.
+ */
+ private CppCompileActionBuilder createCompileActionBuilder(
+ Artifact source, Label label) {
+ CppCompileActionBuilder builder = new CppCompileActionBuilder(
+ ruleContext, source, label);
+
+ builder
+ .setContext(context)
+ .addCopts(copts);
+ return builder;
+ }
+
+ /**
+ * Creates cpp PIC compile action builder from the given builder by adding necessary copt and
+ * changing output and dotd file names.
+ */
+ private CppCompileActionBuilder copyAsPicBuilder(CppCompileActionBuilder builder,
+ PathFragment outputName, String outputExtension) {
+ CppCompileActionBuilder picBuilder = new CppCompileActionBuilder(builder);
+ picBuilder.addCopt("-fPIC")
+ .setOutputFile(ruleContext.getRelatedArtifact(outputName, ".pic" + outputExtension))
+ .setDotdFile(outputName, ".pic.d", ruleContext);
+ return picBuilder;
+ }
+
+ /**
+ * Create the actions for "--save_temps".
+ */
+ private ImmutableList<Artifact> createTempsActions(Artifact source, PathFragment outputName,
+ CppCompileActionBuilder builder, boolean usePic) {
+ if (!cppConfiguration.getSaveTemps()) {
+ return ImmutableList.of();
+ }
+
+ String path = source.getFilename();
+ boolean isCFile = CppFileTypes.C_SOURCE.matches(path);
+ boolean isCppFile = CppFileTypes.CPP_SOURCE.matches(path);
+
+ if (!isCFile && !isCppFile) {
+ return ImmutableList.of();
+ }
+
+ String iExt = isCFile ? ".i" : ".ii";
+ String picExt = usePic ? ".pic" : "";
+ CppCompileActionBuilder dBuilder = new CppCompileActionBuilder(builder);
+ CppCompileActionBuilder sdBuilder = new CppCompileActionBuilder(builder);
+
+ dBuilder
+ .setOutputFile(ruleContext.getRelatedArtifact(outputName, picExt + iExt))
+ .setDotdFile(outputName, picExt + iExt + ".d", ruleContext);
+ semantics.finalizeCompileActionBuilder(ruleContext, dBuilder);
+ CppCompileAction dAction = dBuilder.build();
+ ruleContext.registerAction(dAction);
+
+ sdBuilder
+ .setOutputFile(ruleContext.getRelatedArtifact(outputName, picExt + ".s"))
+ .setDotdFile(outputName, picExt + ".s.d", ruleContext);
+ semantics.finalizeCompileActionBuilder(ruleContext, sdBuilder);
+ CppCompileAction sdAction = sdBuilder.build();
+ ruleContext.registerAction(sdAction);
+ return ImmutableList.of(
+ dAction.getOutputFile(),
+ sdAction.getOutputFile());
+ }
+
+ /**
+ * Returns true iff code coverage is enabled for the given target.
+ */
+ private boolean isCodeCoverageEnabled() {
+ if (configuration.isCodeCoverageEnabled()) {
+ final RegexFilter filter = configuration.getInstrumentationFilter();
+ // If rule is matched by the instrumentation filter, enable instrumentation
+ if (filter.isIncluded(ruleContext.getLabel().toString())) {
+ return true;
+ }
+ // At this point the rule itself is not matched by the instrumentation filter. However, we
+ // might still want to instrument C++ rules if one of the targets listed in "deps" is
+ // instrumented and, therefore, can supply header files that we would want to collect code
+ // coverage for. For example, think about cc_test rule that tests functionality defined in a
+ // header file that is supplied by the cc_library.
+ //
+ // Note that we only check direct prerequisites and not the transitive closure. This is done
+ // for two reasons:
+ // a) It is a good practice to declare libraries which you directly rely on. Including headers
+ // from a library hidden deep inside the transitive closure makes build dependencies less
+ // readable and can lead to unexpected breakage.
+ // b) Traversing the transitive closure for each C++ compile action would require more complex
+ // implementation (with caching results of this method) to avoid O(N^2) slowdown.
+ if (ruleContext.getRule().isAttrDefined("deps", Type.LABEL_LIST)) {
+ for (TransitiveInfoCollection dep : ruleContext.getPrerequisites("deps", Mode.TARGET)) {
+ if (dep.getProvider(CppCompilationContext.class) != null
+ && filter.isIncluded(dep.getLabel().toString())) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppModuleMap.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppModuleMap.java
new file mode 100644
index 0000000..bb27209
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppModuleMap.java
@@ -0,0 +1,44 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+
+/**
+ * Structure for C++ module maps. Stores the name of the module and a .cppmap artifact.
+ */
+@Immutable
+public class CppModuleMap {
+ private final Artifact artifact;
+ private final String name;
+
+ public CppModuleMap(Artifact artifact, String name) {
+ this.artifact = artifact;
+ this.name = name;
+ }
+
+ public Artifact getArtifact() {
+ return artifact;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public String toString() {
+ return name + "@" + artifact;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppModuleMapAction.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppModuleMapAction.java
new file mode 100644
index 0000000..a350fc4
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppModuleMapAction.java
@@ -0,0 +1,185 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.ActionOwner;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.Executor;
+import com.google.devtools.build.lib.actions.ResourceSet;
+import com.google.devtools.build.lib.analysis.actions.AbstractFileWriteAction;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.util.Fingerprint;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Creates C++ module map artifact genfiles. These are then passed to Clang to
+ * do dependency checking.
+ */
+public class CppModuleMapAction extends AbstractFileWriteAction {
+
+ private static final String GUID = "4f407081-1951-40c1-befc-d6b4daff5de3";
+
+ // C++ module map of the current target
+ private final CppModuleMap cppModuleMap;
+
+ /**
+ * If set, the paths in the module map are relative to the current working directory instead
+ * of relative to the module map file's location.
+ */
+ private final boolean moduleMapHomeIsCwd;
+
+ // Headers and dependencies list
+ private final ImmutableList<Artifact> privateHeaders;
+ private final ImmutableList<Artifact> publicHeaders;
+ private final ImmutableList<CppModuleMap> dependencies;
+ private final ImmutableList<PathFragment> additionalExportedHeaders;
+ private final boolean compiledModule;
+
+ public CppModuleMapAction(ActionOwner owner, CppModuleMap cppModuleMap,
+ Iterable<Artifact> privateHeaders, Iterable<Artifact> publicHeaders,
+ Iterable<CppModuleMap> dependencies, Iterable<PathFragment> additionalExportedHeaders,
+ boolean compiledModule, boolean moduleMapHomeIsCwd) {
+ super(owner, ImmutableList.<Artifact>of(), cppModuleMap.getArtifact(),
+ /*makeExecutable=*/false);
+ this.cppModuleMap = cppModuleMap;
+ this.moduleMapHomeIsCwd = moduleMapHomeIsCwd;
+ this.privateHeaders = ImmutableList.copyOf(privateHeaders);
+ this.publicHeaders = ImmutableList.copyOf(publicHeaders);
+ this.dependencies = ImmutableList.copyOf(dependencies);
+ this.additionalExportedHeaders = ImmutableList.copyOf(additionalExportedHeaders);
+ this.compiledModule = compiledModule;
+ }
+
+ @Override
+ public DeterministicWriter newDeterministicWriter(EventHandler eventHandler, Executor executor) {
+ return new DeterministicWriter() {
+ @Override
+ public void writeOutputFile(OutputStream out) throws IOException {
+ StringBuilder content = new StringBuilder();
+ PathFragment fragment = cppModuleMap.getArtifact().getExecPath();
+ int segmentsToExecPath = fragment.segmentCount() - 1;
+
+ // For details about the different header types, see:
+ // http://clang.llvm.org/docs/Modules.html#header-declaration
+ String leadingPeriods = moduleMapHomeIsCwd ? "" : Strings.repeat("../", segmentsToExecPath);
+ content.append("module \"").append(cppModuleMap.getName()).append("\" {\n");
+ content.append(" export *\n");
+ for (Artifact artifact : privateHeaders) {
+ appendHeader(content, "private", artifact.getExecPath(), leadingPeriods,
+ /*canCompile=*/true);
+ }
+ for (Artifact artifact : publicHeaders) {
+ appendHeader(content, "", artifact.getExecPath(), leadingPeriods, /*canCompile=*/true);
+ }
+ for (PathFragment additionalExportedHeader : additionalExportedHeaders) {
+ appendHeader(content, "", additionalExportedHeader, leadingPeriods, /*canCompile*/false);
+ }
+ for (CppModuleMap dep : dependencies) {
+ content.append(" use \"").append(dep.getName()).append("\"\n");
+ }
+ content.append("}");
+ for (CppModuleMap dep : dependencies) {
+ content.append("\nextern module \"")
+ .append(dep.getName())
+ .append("\" \"")
+ .append(leadingPeriods)
+ .append(dep.getArtifact().getExecPath())
+ .append("\"");
+ }
+ out.write(content.toString().getBytes(StandardCharsets.ISO_8859_1));
+ }
+ };
+ }
+
+ private void appendHeader(StringBuilder content, String visibilitySpecifier, PathFragment path,
+ String leadingPeriods, boolean canCompile) {
+ content.append(" ");
+ if (!visibilitySpecifier.isEmpty()) {
+ content.append(visibilitySpecifier).append(" ");
+ }
+ if (!canCompile || !shouldCompileHeader(path)) {
+ content.append("textual ");
+ }
+ content.append("header \"").append(leadingPeriods).append(path).append("\"\n");
+ }
+
+ private boolean shouldCompileHeader(PathFragment path) {
+ return compiledModule && !CppFileTypes.CPP_TEXTUAL_INCLUDE.matches(path);
+ }
+
+ @Override
+ public String getMnemonic() {
+ return "CppModuleMap";
+ }
+
+ @Override
+ protected String computeKey() {
+ Fingerprint f = new Fingerprint();
+ f.addString(GUID);
+ f.addInt(privateHeaders.size());
+ for (Artifact artifact : privateHeaders) {
+ f.addPath(artifact.getRootRelativePath());
+ }
+ f.addInt(publicHeaders.size());
+ for (Artifact artifact : publicHeaders) {
+ f.addPath(artifact.getRootRelativePath());
+ }
+ f.addInt(dependencies.size());
+ for (CppModuleMap dep : dependencies) {
+ f.addPath(dep.getArtifact().getExecPath());
+ }
+ f.addPath(cppModuleMap.getArtifact().getExecPath());
+ f.addString(cppModuleMap.getName());
+ return f.hexDigestAndReset();
+ }
+
+ @Override
+ public ResourceSet estimateResourceConsumptionLocal() {
+ return new ResourceSet(/*memoryMb=*/0, /*cpuUsage=*/0, /*ioUsage=*/0.02);
+ }
+
+ @VisibleForTesting
+ public Collection<Artifact> getPublicHeaders() {
+ return publicHeaders;
+ }
+
+ @VisibleForTesting
+ public Collection<Artifact> getPrivateHeaders() {
+ return privateHeaders;
+ }
+
+ @VisibleForTesting
+ public ImmutableList<PathFragment> getAdditionalExportedHeaders() {
+ return additionalExportedHeaders;
+ }
+
+ @VisibleForTesting
+ public Collection<Artifact> getDependencyArtifacts() {
+ List<Artifact> artifacts = new ArrayList<>();
+ for (CppModuleMap map : dependencies) {
+ artifacts.add(map.getArtifact());
+ }
+ return artifacts;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppOptions.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppOptions.java
new file mode 100644
index 0000000..b0f2e82
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppOptions.java
@@ -0,0 +1,646 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Multimap;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration.LabelConverter;
+import com.google.devtools.build.lib.analysis.config.CompilationMode;
+import com.google.devtools.build.lib.analysis.config.FragmentOptions;
+import com.google.devtools.build.lib.analysis.config.PerLabelOptions;
+import com.google.devtools.build.lib.rules.cpp.CppConfiguration.HeadersCheckingMode;
+import com.google.devtools.build.lib.rules.cpp.CppConfiguration.LibcTop;
+import com.google.devtools.build.lib.rules.cpp.CppConfiguration.StripMode;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.syntax.Label.SyntaxException;
+import com.google.devtools.build.lib.util.OptionsUtils;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.lib.view.config.crosstool.CrosstoolConfig.LipoMode;
+import com.google.devtools.common.options.Converter;
+import com.google.devtools.common.options.Converters;
+import com.google.devtools.common.options.EnumConverter;
+import com.google.devtools.common.options.Option;
+import com.google.devtools.common.options.OptionsParsingException;
+
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+/**
+ * Command-line options for C++.
+ */
+public class CppOptions extends FragmentOptions {
+ /**
+ * Label of a filegroup that contains all crosstool files for all configurations.
+ */
+ @VisibleForTesting
+ public static final String DEFAULT_CROSSTOOL_TARGET = "//tools/cpp:toolchain";
+
+
+ /**
+ * Converter for --cwarn flag
+ */
+ public static class GccWarnConverter implements Converter<String> {
+ @Override
+ public String convert(String input) throws OptionsParsingException {
+ if (input.startsWith("no-") || input.startsWith("-W")) {
+ throw new OptionsParsingException("Not a valid gcc warning to enable");
+ }
+ return input;
+ }
+
+ @Override
+ public String getTypeDescription() {
+ return "A gcc warning to enable";
+ }
+ }
+
+ /**
+ * Converts a comma-separated list of compilation mode settings to a properly typed List.
+ */
+ public static class FissionOptionConverter implements Converter<List<CompilationMode>> {
+ @Override
+ public List<CompilationMode> convert(String input) throws OptionsParsingException {
+ ImmutableSet.Builder<CompilationMode> modes = ImmutableSet.builder();
+ if (input.equals("yes")) { // Special case: enable all modes.
+ modes.add(CompilationMode.values());
+ } else if (!input.equals("no")) { // "no" is another special case that disables all modes.
+ CompilationMode.Converter modeConverter = new CompilationMode.Converter();
+ for (String mode : Splitter.on(',').split(input)) {
+ modes.add(modeConverter.convert(mode));
+ }
+ }
+ return modes.build().asList();
+ }
+
+ @Override
+ public String getTypeDescription() {
+ return "a set of compilation modes";
+ }
+ }
+
+ /**
+ * The same as DynamicMode, but on command-line we also allow AUTO.
+ */
+ public enum DynamicModeFlag { OFF, DEFAULT, FULLY, AUTO }
+
+ /**
+ * Converter for DynamicModeFlag
+ */
+ public static class DynamicModeConverter extends EnumConverter<DynamicModeFlag> {
+ public DynamicModeConverter() {
+ super(DynamicModeFlag.class, "dynamic mode");
+ }
+ }
+
+ /**
+ * Converter for the --strip option.
+ */
+ public static class StripModeConverter extends EnumConverter<StripMode> {
+ public StripModeConverter() {
+ super(StripMode.class, "strip mode");
+ }
+ }
+
+ private static final String LIBC_RELATIVE_LABEL = ":everything";
+
+ /**
+ * Converts a String, which is an absolute path or label into a LibcTop
+ * object.
+ */
+ public static class LibcTopConverter implements Converter<LibcTop> {
+ @Override
+ public LibcTop convert(String input) throws OptionsParsingException {
+ if (!input.startsWith("//")) {
+ throw new OptionsParsingException("Not a label");
+ }
+ try {
+ Label label = Label.parseAbsolute(input).getRelative(LIBC_RELATIVE_LABEL);
+ return new LibcTop(label);
+ } catch (SyntaxException e) {
+ throw new OptionsParsingException(e.getMessage());
+ }
+ }
+
+ @Override
+ public String getTypeDescription() {
+ return "a label";
+ }
+ }
+
+ /**
+ * Converter for the --hdrs_check option.
+ */
+ public static class HdrsCheckConverter extends EnumConverter<HeadersCheckingMode> {
+ public HdrsCheckConverter() {
+ super(HeadersCheckingMode.class, "Headers check mode");
+ }
+ }
+
+ /**
+ * Checks whether a string is a valid regex pattern and compiles it.
+ */
+ public static class NullableRegexPatternConverter implements Converter<Pattern> {
+
+ @Override
+ public Pattern convert(String input) throws OptionsParsingException {
+ if (input.isEmpty()) {
+ return null;
+ }
+ try {
+ return Pattern.compile(input);
+ } catch (PatternSyntaxException e) {
+ throw new OptionsParsingException("Not a valid regular expression: " + e.getMessage());
+ }
+ }
+
+ @Override
+ public String getTypeDescription() {
+ return "a valid Java regular expression";
+ }
+ }
+
+ /**
+ * Converter for the --lipo option.
+ */
+ public static class LipoModeConverter extends EnumConverter<LipoMode> {
+ public LipoModeConverter() {
+ super(LipoMode.class, "LIPO mode");
+ }
+ }
+
+ @Option(name = "lipo input collector",
+ defaultValue = "false",
+ category = "undocumented",
+ help = "Internal flag, only used to create configurations with the LIPO-collector flag set.")
+ public boolean lipoCollector;
+
+ @Option(name = "crosstool_top",
+ defaultValue = CppOptions.DEFAULT_CROSSTOOL_TARGET,
+ category = "version",
+ converter = LabelConverter.class,
+ help = "The label of the crosstool package to be used for compiling C++ code.")
+ public Label crosstoolTop;
+
+ @Option(name = "compiler",
+ defaultValue = "null",
+ category = "version",
+ help = "The C++ compiler to use for compiling the target.")
+ public String cppCompiler;
+
+ @Option(name = "glibc",
+ defaultValue = "null",
+ category = "version",
+ help = "The version of glibc the target should be linked against. "
+ + "By default, a suitable version is chosen based on --cpu.")
+ public String glibc;
+
+ @Option(name = "thin_archives",
+ defaultValue = "false",
+ category = "strategy", // but also adds edges to the action graph
+ help = "Pass the 'T' flag to ar if supported by the toolchain. " +
+ "All supported toolchains support this setting.")
+ public boolean useThinArchives;
+
+ // O intrepid reaper of unused options: Be warned that the [no]start_end_lib
+ // option, however tempting to remove, has a use case. Look in our telemetry data.
+ @Option(name = "start_end_lib",
+ defaultValue = "true",
+ category = "strategy", // but also adds edges to the action graph
+ help = "Use the --start-lib/--end-lib ld options if supported by the toolchain.")
+ public boolean useStartEndLib;
+
+ @Option(name = "interface_shared_objects",
+ defaultValue = "true",
+ category = "strategy", // but also adds edges to the action graph
+ help = "Use interface shared objects if supported by the toolchain. " +
+ "All ELF toolchains currently support this setting.")
+ public boolean useInterfaceSharedObjects;
+
+ @Option(name = "cc_include_scanning",
+ defaultValue = "true",
+ category = "strategy",
+ help = "Whether to perform include scanning. Without it, your build will most likely "
+ + "fail.")
+ public boolean scanIncludes;
+
+ @Option(name = "extract_generated_inclusions",
+ defaultValue = "true",
+ category = "undocumented",
+ help = "Run grep-includes actions (used for include scanning) over " +
+ "generated headers and sources.")
+ public boolean extractInclusions;
+
+ @Option(name = "fission",
+ defaultValue = "no",
+ converter = FissionOptionConverter.class,
+ category = "semantics",
+ help = "Specifies which compilation modes use fission for C++ compilations and links. "
+ + " May be any combination of {'fastbuild', 'dbg', 'opt'} or the special values 'yes' "
+ + " to enable all modes and 'no' to disable all modes.")
+ public List<CompilationMode> fissionModes;
+
+ @Option(name = "dynamic_mode",
+ defaultValue = "default",
+ converter = DynamicModeConverter.class,
+ category = "semantics",
+ help = "Determines whether C++ binaries will be linked dynamically. 'default' means "
+ + "blaze will choose whether to link dynamically. 'fully' means all libraries "
+ + "will be linked dynamically. 'off' means that all libraries will be linked "
+ + "in mostly static mode.")
+ public DynamicModeFlag dynamicMode;
+
+ @Option(name = "force_pic",
+ defaultValue = "false",
+ category = "semantics",
+ help = "If enabled, all C++ compilations produce position-independent code (\"-fPIC\"),"
+ + " links prefer PIC pre-built libraries over non-PIC libraries, and links produce"
+ + " position-independent executables (\"-pie\").")
+ public boolean forcePic;
+
+ @Option(name = "force_ignore_dash_static",
+ defaultValue = "false",
+ category = "semantics",
+ help = "If set, '-static' options in the linkopts of cc_* rules will be ignored.")
+ public boolean forceIgnoreDashStatic;
+
+ @Option(name = "experimental_skip_static_outputs",
+ defaultValue = "false",
+ category = "semantics",
+ help = "This flag is experimental and may go away at any time. "
+ + "If true, linker output for mostly-static C++ executables is a tiny amount of "
+ + "dummy dependency information, and NOT a usable binary. Kludge, but can reduce "
+ + "network and disk I/O load (and thus, continuous build cycle times) by a lot. "
+ + "NOTE: use of this flag REQUIRES --distinct_host_configuration.")
+ public boolean skipStaticOutputs;
+
+ @Option(name = "hdrs_check",
+ allowMultiple = false,
+ defaultValue = "loose",
+ converter = HdrsCheckConverter.class,
+ category = "semantics",
+ help = "Headers check mode for rules that don't specify it explicitly using a "
+ + "hdrs_check attribute. Allowed values: 'loose' allows undeclared headers, 'warn' "
+ + "warns about undeclared headers, and 'strict' disallows them.")
+ public HeadersCheckingMode headersCheckingMode;
+
+ @Option(name = "copt",
+ allowMultiple = true,
+ defaultValue = "",
+ category = "flags",
+ help = "Additional options to pass to gcc.")
+ public List<String> coptList;
+
+ @Option(name = "cwarn",
+ converter = GccWarnConverter.class,
+ defaultValue = "",
+ category = "flags",
+ allowMultiple = true,
+ help = "Additional warnings to enable when compiling C or C++ source files.")
+ public List<String> cWarns;
+
+ @Option(name = "cxxopt",
+ defaultValue = "",
+ category = "flags",
+ allowMultiple = true,
+ help = "Additional option to pass to gcc when compiling C++ source files.")
+ public List<String> cxxoptList;
+
+ @Option(name = "conlyopt",
+ allowMultiple = true,
+ defaultValue = "",
+ category = "flags",
+ help = "Additional option to pass to gcc when compiling C source files.")
+ public List<String> conlyoptList;
+
+ @Option(name = "linkopt",
+ defaultValue = "",
+ category = "flags",
+ allowMultiple = true,
+ help = "Additional option to pass to gcc when linking.")
+ public List<String> linkoptList;
+
+ @Option(name = "stripopt",
+ allowMultiple = true,
+ defaultValue = "",
+ category = "flags",
+ help = "Additional options to pass to strip when generating a '<name>.stripped' binary.")
+ public List<String> stripoptList;
+
+ @Option(name = "custom_malloc",
+ defaultValue = "null",
+ category = "semantics",
+ help = "Specifies a custom malloc implementation. This setting overrides malloc " +
+ "attributes in build rules.",
+ converter = LabelConverter.class)
+ public Label customMalloc;
+
+ @Option(name = "cpp_module_maps",
+ defaultValue = "true",
+ category = "flags",
+ help = "If true then C++ targets create a module map based on BUILD files, and "
+ + "pass them to the compiler.")
+ public boolean cppModuleMaps;
+
+ @Option(name = "legacy_whole_archive",
+ defaultValue = "true",
+ category = "semantics",
+ help = "When on, use --whole-archive for cc_binary rules that have "
+ + "linkshared=1 and either linkstatic=1 or '-static' in linkopts. "
+ + "This is for backwards compatibility only. "
+ + "A better alternative is to use alwayslink=1 where required.")
+ public boolean legacyWholeArchive;
+
+ @Option(name = "strip",
+ defaultValue = "sometimes",
+ category = "flags",
+ help = "Specifies whether to strip binaries and shared libraries "
+ + " (using \"-Wl,--strip-debug\"). The default value of 'sometimes'"
+ + " means strip iff --compilation_mode=fastbuild.",
+ converter = StripModeConverter.class)
+ public StripMode stripBinaries;
+
+ @Option(name = "fdo_instrument",
+ defaultValue = "null",
+ converter = OptionsUtils.PathFragmentConverter.class,
+ category = "flags",
+ implicitRequirements = {"--copt=-Wno-error"},
+ help = "Generate binaries with FDO instrumentation. Specify the relative " +
+ "directory name for the .gcda files at runtime.")
+ public PathFragment fdoInstrument;
+
+ @Option(name = "fdo_optimize",
+ defaultValue = "null",
+ category = "flags",
+ help = "Use FDO profile information to optimize compilation. Specify the name " +
+ "of the zip file containing the .gcda file tree or an afdo file containing " +
+ "an auto profile. This flag also accepts files specified as labels, for " +
+ "example //foo/bar:file.afdo. Such labels must refer to input files; you may " +
+ "need to add an exports_files directive to the corresponding package to make " +
+ "the file visible to Blaze.")
+ public String fdoOptimize;
+
+ @Option(name = "autofdo_lipo_data",
+ defaultValue = "false",
+ category = "flags",
+ help = "If true then the directory name for non-LIPO targets will have a " +
+ "'-lipodata' suffix in AutoFDO mode.")
+ public boolean autoFdoLipoData;
+
+ @Option(name = "lipo",
+ defaultValue = "off",
+ converter = LipoModeConverter.class,
+ category = "flags",
+ help = "Enable LIPO optimization (lightweight inter-procedural optimization, The allowed "
+ + "values for this option are 'off' and 'binary', which enables LIPO. This option only "
+ + "has an effect when FDO is also enabled. Currently LIPO is only supported when "
+ + "building a single cc_binary rule.")
+ public LipoMode lipoMode;
+
+ @Option(name = "lipo_context",
+ defaultValue = "null",
+ category = "flags",
+ converter = LabelConverter.class,
+ implicitRequirements = {"--linkopt=-Wl,--warn-unresolved-symbols"},
+ help = "Specifies the binary from which the LIPO profile information comes.")
+ public Label lipoContext;
+
+ @Option(name = "experimental_stl",
+ converter = LabelConverter.class,
+ defaultValue = "null",
+ category = "version",
+ help = "If set, use this label instead of the default STL implementation. "
+ + "This option is EXPERIMENTAL and may go away in a future release.")
+ public Label stl;
+
+ @Option(name = "save_temps",
+ defaultValue = "false",
+ category = "what",
+ help = "If set, temporary outputs from gcc will be saved. "
+ + "These include .s files (assembler code), .i files (preprocessed C) and "
+ + ".ii files (preprocessed C++).")
+ public boolean saveTemps;
+
+ @Option(name = "per_file_copt",
+ allowMultiple = true,
+ converter = PerLabelOptions.PerLabelOptionsConverter.class,
+ defaultValue = "",
+ category = "semantics",
+ help = "Additional options to selectively pass to gcc when compiling certain files. "
+ + "This option can be passed multiple times. "
+ + "Syntax: regex_filter@option_1,option_2,...,option_n. Where regex_filter stands "
+ + "for a list of include and exclude regular expression patterns (Also see "
+ + "--instrumentation_filter). option_1 to option_n stand for "
+ + "arbitrary command line options. If an option contains a comma it has to be "
+ + "quoted with a backslash. Options can contain @. Only the first @ is used to "
+ + "split the string. Example: "
+ + "--per_file_copt=//foo/.*\\.cc,-//foo/bar\\.cc@-O0 adds the -O0 "
+ + "command line option to the gcc command line of all cc files in //foo/ "
+ + "except bar.cc.")
+ public List<PerLabelOptions> perFileCopts;
+
+ @Option(name = "host_crosstool_top",
+ defaultValue = "null",
+ converter = LabelConverter.class,
+ category = "semantics",
+ help = "By default, the --crosstool_top, --glibc, and --compiler options are also used " +
+ "for the host configuration. If this flag is provided, Blaze uses the default glibc " +
+ "and compiler for the given crosstool_top.")
+ public Label hostCrosstoolTop;
+
+ @Option(name = "host_copt",
+ allowMultiple = true,
+ defaultValue = "",
+ category = "flags",
+ help = "Additional options to pass to gcc for host tools.")
+ public List<String> hostCoptList;
+
+ @Option(name = "define",
+ converter = Converters.AssignmentConverter.class,
+ defaultValue = "",
+ category = "semantics",
+ allowMultiple = true,
+ help = "Each --define option specifies an assignment for a build variable.")
+ public List<Map.Entry<String, String>> commandLineDefinedVariables;
+
+ @Option(name = "grte_top",
+ defaultValue = "null", // The default value is chosen by the toolchain.
+ category = "version",
+ converter = LibcTopConverter.class,
+ help = "A label to a checked-in libc library. The default value is selected by the crosstool "
+ + "toolchain, and you almost never need to override it.")
+ public LibcTop libcTop;
+
+ @Option(name = "host_grte_top",
+ defaultValue = "null", // The default value is chosen by the toolchain.
+ category = "version",
+ converter = LibcTopConverter.class,
+ help = "If specified, this setting overrides the libc top-level directory (--grte_top) "
+ + "for the host configuration.")
+ public LibcTop hostLibcTop;
+
+ @Option(name = "output_symbol_counts",
+ defaultValue = "false",
+ category = "flags",
+ help = "If enabled, every C++ binary linked with gold will store the number of used "
+ + "symbols per object file in a .sc file.")
+ public boolean symbolCounts;
+
+ @Option(name = "experimental_inmemory_dotd_files",
+ defaultValue = "false",
+ category = "experimental",
+ help = "If enabled, C++ .d files will be passed through in memory directly from the remote "
+ + "build nodes instead of being written to disk.")
+ public boolean inmemoryDotdFiles;
+
+ @Option(name = "use_isystem_for_includes",
+ defaultValue = "true",
+ category = "undocumented",
+ help = "Instruct C and C++ compilations to treat 'includes' paths as system header " +
+ "paths, by translating it into -isystem instead of -I.")
+ public boolean useIsystemForIncludes;
+
+ @Option(name = "experimental_omitfp",
+ defaultValue = "false",
+ category = "semantics",
+ help = "If true, use libunwind for stack unwinding, and compile with " +
+ "-fomit-frame-pointer and -fasynchronous-unwind-tables.")
+ public boolean experimentalOmitfp;
+
+ @Option(name = "share_native_deps",
+ defaultValue = "true",
+ category = "strategy",
+ help = "If true, native libraries that contain identical functionality "
+ + "will be shared among different targets")
+ public boolean shareNativeDeps;
+
+ @Override
+ public FragmentOptions getHost(boolean fallback) {
+ CppOptions host = (CppOptions) getDefault();
+
+ host.commandLineDefinedVariables = commandLineDefinedVariables;
+
+ // The crosstool options are partially copied from the target configuration.
+ if (!fallback) {
+ if (hostCrosstoolTop == null) {
+ host.cppCompiler = cppCompiler;
+ host.crosstoolTop = crosstoolTop;
+ host.glibc = glibc;
+ } else {
+ host.crosstoolTop = hostCrosstoolTop;
+ }
+ }
+
+ if (hostLibcTop != null) {
+ host.libcTop = hostLibcTop;
+ } else if (hostCrosstoolTop == null) {
+ // Track libc in the host configuration if no host crosstool is set.
+ host.libcTop = libcTop;
+ }
+
+ // -g0 is the default, but allowMultiple options cannot have default values so we just pass
+ // -g0 first and let the user options override it.
+ host.coptList = ImmutableList.<String>builder().add("-g0").addAll(hostCoptList).build();
+
+ host.useThinArchives = useThinArchives;
+ host.useStartEndLib = useStartEndLib;
+ host.extractInclusions = extractInclusions;
+ host.stripBinaries = StripMode.ALWAYS;
+ host.fdoOptimize = null;
+ host.lipoMode = LipoMode.OFF;
+ host.scanIncludes = scanIncludes;
+ host.inmemoryDotdFiles = inmemoryDotdFiles;
+ host.cppModuleMaps = cppModuleMaps;
+
+ return host;
+ }
+
+ @Override
+ public void addAllLabels(Multimap<String, Label> labelMap) {
+ labelMap.put("crosstool", crosstoolTop);
+ if (hostCrosstoolTop != null) {
+ labelMap.put("crosstool", hostCrosstoolTop);
+ }
+
+ if (libcTop != null) {
+ Label libcLabel = libcTop.getLabel();
+ if (libcLabel != null) {
+ labelMap.put("crosstool", libcLabel);
+ }
+ }
+ addOptionalLabel(labelMap, "fdo", fdoOptimize);
+
+ if (stl != null) {
+ labelMap.put("STL", stl);
+ }
+
+ if (customMalloc != null) {
+ labelMap.put("custom_malloc", customMalloc);
+ }
+
+ if (getLipoContextLabel() != null) {
+ labelMap.put("lipo", getLipoContextLabel());
+ }
+ }
+
+ @Override
+ public Map<String, Set<Label>> getDefaultsLabels(BuildConfiguration.Options commonOptions) {
+ Set<Label> crosstoolLabels = new LinkedHashSet<>();
+ crosstoolLabels.add(crosstoolTop);
+ if (hostCrosstoolTop != null) {
+ crosstoolLabels.add(hostCrosstoolTop);
+ }
+
+ if (libcTop != null) {
+ Label libcLabel = libcTop.getLabel();
+ if (libcLabel != null) {
+ crosstoolLabels.add(libcLabel);
+ }
+ }
+
+ return ImmutableMap.of(
+ "CROSSTOOL", crosstoolLabels,
+ "COVERAGE", ImmutableSet.<Label>of());
+ }
+
+ public boolean isFdo() {
+ return fdoOptimize != null || fdoInstrument != null;
+ }
+
+ public boolean isLipoOptimization() {
+ return lipoMode == LipoMode.BINARY && fdoOptimize != null && lipoContext != null;
+ }
+
+ public boolean isLipoOptimizationOrInstrumentation() {
+ return lipoMode == LipoMode.BINARY &&
+ ((fdoOptimize != null && lipoContext != null) || fdoInstrument != null);
+ }
+
+ public Label getLipoContextLabel() {
+ return (lipoMode == LipoMode.BINARY && fdoOptimize != null)
+ ? lipoContext : null;
+ }
+
+ public LipoMode getLipoMode() {
+ return lipoMode;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppRuleClasses.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppRuleClasses.java
new file mode 100644
index 0000000..de7c95d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppRuleClasses.java
@@ -0,0 +1,104 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import static com.google.devtools.build.lib.packages.ImplicitOutputsFunction.fromTemplates;
+import static com.google.devtools.build.lib.rules.cpp.CppFileTypes.ALWAYS_LINK_LIBRARY;
+import static com.google.devtools.build.lib.rules.cpp.CppFileTypes.ALWAYS_LINK_PIC_LIBRARY;
+import static com.google.devtools.build.lib.rules.cpp.CppFileTypes.ARCHIVE;
+import static com.google.devtools.build.lib.rules.cpp.CppFileTypes.ASSEMBLER_WITH_C_PREPROCESSOR;
+import static com.google.devtools.build.lib.rules.cpp.CppFileTypes.CPP_HEADER;
+import static com.google.devtools.build.lib.rules.cpp.CppFileTypes.CPP_SOURCE;
+import static com.google.devtools.build.lib.rules.cpp.CppFileTypes.C_SOURCE;
+import static com.google.devtools.build.lib.rules.cpp.CppFileTypes.OBJECT_FILE;
+import static com.google.devtools.build.lib.rules.cpp.CppFileTypes.PIC_ARCHIVE;
+import static com.google.devtools.build.lib.rules.cpp.CppFileTypes.PIC_OBJECT_FILE;
+import static com.google.devtools.build.lib.rules.cpp.CppFileTypes.SHARED_LIBRARY;
+import static com.google.devtools.build.lib.rules.cpp.CppFileTypes.VERSIONED_SHARED_LIBRARY;
+
+import com.google.devtools.build.lib.analysis.LanguageDependentFragment.LibraryLanguage;
+import com.google.devtools.build.lib.packages.ImplicitOutputsFunction.SafeImplicitOutputsFunction;
+import com.google.devtools.build.lib.rules.test.InstrumentedFilesCollector.InstrumentationSpec;
+import com.google.devtools.build.lib.util.FileTypeSet;
+
+/**
+ * Rule class definitions for C++ rules.
+ */
+public class CppRuleClasses {
+ // Artifacts of these types are discarded from the 'hdrs' attribute in cc rules
+ static final FileTypeSet DISALLOWED_HDRS_FILES = FileTypeSet.of(
+ ARCHIVE,
+ PIC_ARCHIVE,
+ ALWAYS_LINK_LIBRARY,
+ ALWAYS_LINK_PIC_LIBRARY,
+ SHARED_LIBRARY,
+ VERSIONED_SHARED_LIBRARY,
+ OBJECT_FILE,
+ PIC_OBJECT_FILE);
+
+ /**
+ * The set of instrumented source file types; keep this in sync with the list above. Note that
+ * extension-less header files cannot currently be declared, so we cannot collect coverage for
+ * those.
+ */
+ static final InstrumentationSpec INSTRUMENTATION_SPEC = new InstrumentationSpec(
+ FileTypeSet.of(CPP_SOURCE, C_SOURCE, CPP_HEADER, ASSEMBLER_WITH_C_PREPROCESSOR),
+ "srcs", "deps", "data", "hdrs", "implements", "implementation");
+
+ public static final LibraryLanguage LANGUAGE = new LibraryLanguage("C++");
+
+ /**
+ * Implicit outputs for cc_binary rules.
+ */
+ public static final SafeImplicitOutputsFunction CC_BINARY_STRIPPED =
+ fromTemplates("%{name}.stripped");
+
+
+ // Used for requesting dwp "debug packages".
+ public static final SafeImplicitOutputsFunction CC_BINARY_DEBUG_PACKAGE =
+ fromTemplates("%{name}.dwp");
+
+
+ /**
+ * Path of the build_interface_so script in the Blaze binary.
+ */
+ public static final String BUILD_INTERFACE_SO = "build_interface_so";
+
+ /**
+ * A string constant for the layering_check feature.
+ */
+ public static final String LAYERING_CHECK = "layering_check";
+
+ /**
+ * A string constant for the parse_headers feature.
+ */
+ public static final String PARSE_HEADERS = "parse_headers";
+
+ /**
+ * A string constant for the preprocess_headers feature.
+ */
+ public static final String PREPROCESS_HEADERS = "preprocess_headers";
+
+ /**
+ * A string constant for the header_modules feature.
+ */
+ public static final String HEADER_MODULES = "header_modules";
+
+ /**
+ * A string constant for the module_map_home_cwd feature.
+ */
+ public static final String MODULE_MAP_HOME_CWD = "module_map_home_cwd";
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppRunfilesProvider.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppRunfilesProvider.java
new file mode 100644
index 0000000..f4aa38c
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppRunfilesProvider.java
@@ -0,0 +1,85 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import com.google.common.base.Function;
+import com.google.devtools.build.lib.analysis.Runfiles;
+import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
+import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+
+/**
+ * Runfiles provider for C++ targets.
+ *
+ * <p>Contains two {@link Runfiles} objects: one for the eventual statically linked binary and
+ * one for the one that uses shared libraries. Data dependencies are present in both.
+ */
+@Immutable
+public final class CppRunfilesProvider implements TransitiveInfoProvider {
+ private final Runfiles staticRunfiles;
+ private final Runfiles sharedRunfiles;
+
+ public CppRunfilesProvider(Runfiles staticRunfiles, Runfiles sharedRunfiles) {
+ this.staticRunfiles = staticRunfiles;
+ this.sharedRunfiles = sharedRunfiles;
+ }
+
+ public Runfiles getStaticRunfiles() {
+ return staticRunfiles;
+ }
+
+ public Runfiles getSharedRunfiles() {
+ return sharedRunfiles;
+ }
+
+ /**
+ * Returns a function that gets the static C++ runfiles from a {@link TransitiveInfoCollection}
+ * or the empty runfiles instance if it does not contain that provider.
+ */
+ public static final Function<TransitiveInfoCollection, Runfiles> STATIC_RUNFILES =
+ new Function<TransitiveInfoCollection, Runfiles>() {
+ @Override
+ public Runfiles apply(TransitiveInfoCollection input) {
+ CppRunfilesProvider provider = input.getProvider(CppRunfilesProvider.class);
+ return provider == null
+ ? Runfiles.EMPTY
+ : provider.getStaticRunfiles();
+ }
+ };
+
+ /**
+ * Returns a function that gets the shared C++ runfiles from a {@link TransitiveInfoCollection}
+ * or the empty runfiles instance if it does not contain that provider.
+ */
+ public static final Function<TransitiveInfoCollection, Runfiles> SHARED_RUNFILES =
+ new Function<TransitiveInfoCollection, Runfiles>() {
+ @Override
+ public Runfiles apply(TransitiveInfoCollection input) {
+ CppRunfilesProvider provider = input.getProvider(CppRunfilesProvider.class);
+ return provider == null
+ ? Runfiles.EMPTY
+ : provider.getSharedRunfiles();
+ }
+ };
+
+ /**
+ * Returns a function that gets the C++ runfiles from a {@link TransitiveInfoCollection} or
+ * the empty runfiles instance if it does not contain that provider.
+ */
+ public static final Function<TransitiveInfoCollection, Runfiles> runfilesFunction(
+ boolean linkingStatically) {
+ return linkingStatically ? STATIC_RUNFILES : SHARED_RUNFILES;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppSemantics.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppSemantics.java
new file mode 100644
index 0000000..600b2fa
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppSemantics.java
@@ -0,0 +1,49 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+/**
+ * Pluggable C++ compilation semantics.
+ */
+public interface CppSemantics {
+ /**
+ * Returns the "effective source path" of a source file.
+ *
+ * <p>It is used, among other things, for computing the output path.
+ */
+ PathFragment getEffectiveSourcePath(Artifact source);
+
+ /**
+ * Called before a C++ compile action is built.
+ *
+ * <p>Gives the semantics implementation the opportunity to change compile actions at the last
+ * minute.
+ */
+ void finalizeCompileActionBuilder(
+ RuleContext ruleContext, CppCompileActionBuilder actionBuilder);
+
+ /**
+ * Called before {@link CppCompilationContext}s are finalized.
+ *
+ * <p>Gives the semantics implementation the opportunity to change what the C++ rule propagates
+ * to dependent rules.
+ */
+ void setupCompilationContext(
+ RuleContext ruleContext, CppCompilationContext.Builder contextBuilder);
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CrosstoolConfigurationIdentifier.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CrosstoolConfigurationIdentifier.java
new file mode 100644
index 0000000..1111189
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CrosstoolConfigurationIdentifier.java
@@ -0,0 +1,132 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.analysis.config.BuildOptions;
+import com.google.devtools.build.lib.view.config.crosstool.CrosstoolConfig;
+import com.google.devtools.build.lib.view.config.crosstool.CrosstoolConfig.CToolchain;
+
+import java.util.Objects;
+
+/**
+ * Contains parameters which uniquely describe a crosstool configuration
+ * and methods for comparing two crosstools against each other.
+ *
+ * <p>Two crosstools which contain equivalent values of these parameters are
+ * considered equal.
+ */
+public final class CrosstoolConfigurationIdentifier implements CrosstoolConfigurationOptions {
+
+ /** The CPU associated with this crosstool configuration. */
+ private final String cpu;
+
+ /** The compiler (e.g. gcc) associated with this crosstool configuration. */
+ private final String compiler;
+
+ /** The version of libc (e.g. glibc-2.11) associated with this crosstool configuration. */
+ private final String libc;
+
+ private CrosstoolConfigurationIdentifier(String cpu, String compiler, String libc) {
+ this.cpu = cpu;
+ this.compiler = compiler;
+ this.libc = libc;
+ }
+
+ /**
+ * Creates a new crosstool configuration from the given crosstool release and
+ * configuration options.
+ */
+ public static CrosstoolConfigurationIdentifier fromReleaseAndCrosstoolConfiguration(
+ CrosstoolConfig.CrosstoolRelease release, BuildOptions buildOptions) {
+ String cpu = buildOptions.get(BuildConfiguration.Options.class).getCpu();
+ if (cpu == null) {
+ cpu = release.getDefaultTargetCpu();
+ }
+ CppOptions cppOptions = buildOptions.get(CppOptions.class);
+ return new CrosstoolConfigurationIdentifier(cpu, cppOptions.cppCompiler, cppOptions.glibc);
+ }
+
+ public static CrosstoolConfigurationIdentifier fromToolchain(CToolchain toolchain) {
+ return new CrosstoolConfigurationIdentifier(
+ toolchain.getTargetCpu(), toolchain.getCompiler(), toolchain.getTargetLibc());
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof CrosstoolConfigurationIdentifier)) {
+ return false;
+ }
+ CrosstoolConfigurationIdentifier otherCrosstool = (CrosstoolConfigurationIdentifier) other;
+ return Objects.equals(cpu, otherCrosstool.cpu)
+ && Objects.equals(compiler, otherCrosstool.compiler)
+ && Objects.equals(libc, otherCrosstool.libc);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(cpu, compiler, libc);
+ }
+
+
+ /**
+ * Returns a series of command line flags which specify the configuration options.
+ * Any of these options may be null, in which case its flag is omitted.
+ *
+ * <p>The appended string will be along the lines of
+ * " --cpu='cpu' --compiler='compiler' --glibc='libc'".
+ */
+ public String describeFlags() {
+ StringBuilder message = new StringBuilder();
+ if (getCpu() != null) {
+ message.append(" --cpu='").append(getCpu()).append("'");
+ }
+ if (getCompiler() != null) {
+ message.append(" --compiler='").append(getCompiler()).append("'");
+ }
+ if (getLibc() != null) {
+ message.append(" --glibc='").append(getLibc()).append("'");
+ }
+ return message.toString();
+ }
+
+ /** Returns true if the specified toolchain is a candidate for use with this crosstool. */
+ public boolean isCandidateToolchain(CToolchain toolchain) {
+ return (toolchain.getTargetCpu().equals(getCpu())
+ && (getLibc() == null || toolchain.getTargetLibc().equals(getLibc()))
+ && (getCompiler() == null || toolchain.getCompiler().equals(
+ getCompiler())));
+ }
+
+ @Override
+ public String toString() {
+ return describeFlags();
+ }
+
+ @Override
+ public String getCpu() {
+ return cpu;
+ }
+
+ @Override
+ public String getCompiler() {
+ return compiler;
+ }
+
+ @Override
+ public String getLibc() {
+ return libc;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CrosstoolConfigurationLoader.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CrosstoolConfigurationLoader.java
new file mode 100644
index 0000000..a113f5f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CrosstoolConfigurationLoader.java
@@ -0,0 +1,327 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.io.BaseEncoding;
+import com.google.devtools.build.lib.analysis.RedirectChaser;
+import com.google.devtools.build.lib.analysis.config.BuildOptions;
+import com.google.devtools.build.lib.analysis.config.ConfigurationEnvironment;
+import com.google.devtools.build.lib.analysis.config.InvalidConfigurationException;
+import com.google.devtools.build.lib.packages.NoSuchThingException;
+import com.google.devtools.build.lib.packages.Package;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.syntax.Label.SyntaxException;
+import com.google.devtools.build.lib.util.Pair;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.view.config.crosstool.CrosstoolConfig;
+import com.google.protobuf.TextFormat;
+import com.google.protobuf.TextFormat.ParseException;
+import com.google.protobuf.UninitializedMessageException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.concurrent.ExecutionException;
+
+import javax.annotation.Nullable;
+
+/**
+ * A loader that reads Crosstool configuration files and creates CToolchain
+ * instances from them.
+ */
+public class CrosstoolConfigurationLoader {
+ private static final String CROSSTOOL_CONFIGURATION_FILENAME = "CROSSTOOL";
+
+ /**
+ * Cache for storing result of toReleaseConfiguration function based on path and md5 sum of
+ * input file. We can use md5 because result of this function depends only on the file content.
+ */
+ private static final LoadingCache<Pair<Path, String>, CrosstoolConfig.CrosstoolRelease>
+ crosstoolReleaseCache = CacheBuilder.newBuilder().concurrencyLevel(4).maximumSize(100).build(
+ new CacheLoader<Pair<Path, String>, CrosstoolConfig.CrosstoolRelease>() {
+ @Override
+ public CrosstoolConfig.CrosstoolRelease load(Pair<Path, String> key) throws IOException {
+ char[] data = FileSystemUtils.readContentAsLatin1(key.first);
+ return toReleaseConfiguration(key.first.getPathString(), new String(data));
+ }
+ });
+
+ /**
+ * A class that holds the results of reading a CROSSTOOL file.
+ */
+ public static class CrosstoolFile {
+ private final Label crosstoolTop;
+ private Path crosstoolPath;
+ private CrosstoolConfig.CrosstoolRelease crosstool;
+ private String md5;
+
+ CrosstoolFile(Label crosstoolTop) {
+ this.crosstoolTop = crosstoolTop;
+ }
+
+ void setCrosstoolPath(Path crosstoolPath) {
+ this.crosstoolPath = crosstoolPath;
+ }
+
+ void setCrosstool(CrosstoolConfig.CrosstoolRelease crosstool) {
+ this.crosstool = crosstool;
+ }
+
+ void setMd5(String md5) {
+ this.md5 = md5;
+ }
+
+ /**
+ * Returns the crosstool top as resolved.
+ */
+ public Label getCrosstoolTop() {
+ return crosstoolTop;
+ }
+
+ /**
+ * Returns the absolute path from which the CROSSTOOL file was read.
+ */
+ public Path getCrosstoolPath() {
+ return crosstoolPath;
+ }
+
+ /**
+ * Returns the parsed contents of the CROSSTOOL file.
+ */
+ public CrosstoolConfig.CrosstoolRelease getProto() {
+ return crosstool;
+ }
+
+ /**
+ * Returns an MD5 hash of the CROSSTOOL file contents.
+ */
+ public String getMd5() {
+ return md5;
+ }
+ }
+
+ private CrosstoolConfigurationLoader() {
+ }
+
+ /**
+ * Reads the given <code>data</code> String, which must be in ascii format,
+ * into a protocol buffer. It uses the <code>name</code> parameter for error
+ * messages.
+ *
+ * @throws IOException if the parsing failed
+ */
+ @VisibleForTesting
+ static CrosstoolConfig.CrosstoolRelease toReleaseConfiguration(String name, String data)
+ throws IOException {
+ CrosstoolConfig.CrosstoolRelease.Builder builder =
+ CrosstoolConfig.CrosstoolRelease.newBuilder();
+ try {
+ TextFormat.merge(data, builder);
+ return builder.build();
+ } catch (ParseException e) {
+ throw new IOException("Could not read the crosstool configuration file '" + name + "', "
+ + "because of a parser error (" + e.getMessage() + ")");
+ } catch (UninitializedMessageException e) {
+ throw new IOException("Could not read the crosstool configuration file '" + name + "', "
+ + "because of an incomplete protocol buffer (" + e.getMessage() + ")");
+ }
+ }
+
+ private static boolean findCrosstoolConfiguration(
+ ConfigurationEnvironment env,
+ CrosstoolConfigurationLoader.CrosstoolFile file)
+ throws IOException, InvalidConfigurationException {
+ Label crosstoolTop = file.getCrosstoolTop();
+ Path path = null;
+ try {
+ Package containingPackage = env.getTarget(crosstoolTop.getLocalTargetLabel("BUILD"))
+ .getPackage();
+ if (containingPackage == null) {
+ return false;
+ }
+ path = env.getPath(containingPackage, CROSSTOOL_CONFIGURATION_FILENAME);
+ } catch (SyntaxException e) {
+ throw new InvalidConfigurationException(e);
+ } catch (NoSuchThingException e) {
+ // Handled later
+ }
+
+ // If we can't find a file, fall back to the provided alternative.
+ if (path == null || !path.exists()) {
+ throw new InvalidConfigurationException("The crosstool_top you specified was resolved to '" +
+ crosstoolTop + "', which does not contain a CROSSTOOL file. " +
+ "You can use a crosstool from the depot by specifying its label.");
+ } else {
+ // Do this before we read the data, so if it changes, we get a different MD5 the next time.
+ // Alternatively, we could calculate the MD5 of the contents, which we also read, but this
+ // is faster if the file comes from a file system with md5 support.
+ file.setCrosstoolPath(path);
+ String md5 = BaseEncoding.base16().lowerCase().encode(path.getMD5Digest());
+ CrosstoolConfig.CrosstoolRelease release;
+ try {
+ release = crosstoolReleaseCache.get(new Pair<Path, String>(path, md5));
+ file.setCrosstool(release);
+ file.setMd5(md5);
+ } catch (ExecutionException e) {
+ throw new InvalidConfigurationException(e);
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Reads a crosstool file.
+ */
+ @Nullable
+ public static CrosstoolConfigurationLoader.CrosstoolFile readCrosstool(
+ ConfigurationEnvironment env, Label crosstoolTop) throws InvalidConfigurationException {
+ crosstoolTop = RedirectChaser.followRedirects(env, crosstoolTop, "crosstool_top");
+ if (crosstoolTop == null) {
+ return null;
+ }
+ CrosstoolConfigurationLoader.CrosstoolFile file =
+ new CrosstoolConfigurationLoader.CrosstoolFile(crosstoolTop);
+ try {
+ boolean allDependenciesPresent = findCrosstoolConfiguration(env, file);
+ return allDependenciesPresent ? file : null;
+ } catch (IOException e) {
+ throw new InvalidConfigurationException(e);
+ }
+ }
+
+ /**
+ * Selects a crosstool toolchain corresponding to the given crosstool
+ * configuration options. If all of these options are null, it returns the default
+ * toolchain specified in the crosstool release. If only cpu is non-null, it
+ * returns the default toolchain for that cpu, as specified in the crosstool
+ * release. Otherwise, all values must be non-null, and this method
+ * returns the toolchain which matches all of the values.
+ *
+ * @throws NullPointerException if {@code release} is null
+ * @throws InvalidConfigurationException if no matching toolchain can be found, or
+ * if the input parameters do not obey the constraints described above
+ */
+ public static CrosstoolConfig.CToolchain selectToolchain(
+ CrosstoolConfig.CrosstoolRelease release, BuildOptions options,
+ Function<String, String> cpuTransformer)
+ throws InvalidConfigurationException {
+ CrosstoolConfigurationIdentifier config =
+ CrosstoolConfigurationIdentifier.fromReleaseAndCrosstoolConfiguration(release, options);
+ if ((config.getCompiler() != null) || (config.getLibc() != null)) {
+ ArrayList<CrosstoolConfig.CToolchain> candidateToolchains = new ArrayList<>();
+ for (CrosstoolConfig.CToolchain toolchain : release.getToolchainList()) {
+ if (config.isCandidateToolchain(toolchain)) {
+ candidateToolchains.add(toolchain);
+ }
+ }
+ switch (candidateToolchains.size()) {
+ case 0: {
+ StringBuilder message = new StringBuilder();
+ message.append("No toolchain found for");
+ message.append(config.describeFlags());
+ message.append(". Valid toolchains are: ");
+ describeToolchainList(message, release.getToolchainList());
+ throw new InvalidConfigurationException(message.toString());
+ }
+ case 1:
+ return candidateToolchains.get(0);
+ default: {
+ StringBuilder message = new StringBuilder();
+ message.append("Multiple toolchains found for");
+ message.append(config.describeFlags());
+ message.append(": ");
+ describeToolchainList(message, candidateToolchains);
+ throw new InvalidConfigurationException(message.toString());
+ }
+ }
+ }
+ String selectedIdentifier = null;
+ // We use fake CPU values to allow cross-platform builds for other languages that use the
+ // C++ toolchain. Translate to the actual target architecture.
+ String desiredCpu = cpuTransformer.apply(config.getCpu());
+ for (CrosstoolConfig.DefaultCpuToolchain selector : release.getDefaultToolchainList()) {
+ if (selector.getCpu().equals(desiredCpu)) {
+ selectedIdentifier = selector.getToolchainIdentifier();
+ break;
+ }
+ }
+ checkToolChain(selectedIdentifier, desiredCpu);
+ for (CrosstoolConfig.CToolchain toolchain : release.getToolchainList()) {
+ if (toolchain.getToolchainIdentifier().equals(selectedIdentifier)) {
+ return toolchain;
+ }
+ }
+ throw new InvalidConfigurationException("Inconsistent crosstool configuration; no toolchain "
+ + "corresponding to '" + selectedIdentifier + "' found for cpu '" + config.getCpu() + "'");
+ }
+
+ private static String describeToolchainFlags(CrosstoolConfig.CToolchain toolchain) {
+ return CrosstoolConfigurationIdentifier.fromToolchain(toolchain).describeFlags();
+ }
+
+ /**
+ * Appends a series of toolchain descriptions (as the blaze command line flags
+ * that would specify that toolchain) to 'message'.
+ */
+ private static void describeToolchainList(StringBuilder message,
+ Collection<CrosstoolConfig.CToolchain> toolchains) {
+ message.append("[");
+ for (CrosstoolConfig.CToolchain toolchain : toolchains) {
+ message.append(describeToolchainFlags(toolchain));
+ message.append(",");
+ }
+ message.append("]");
+ }
+
+ /**
+ * Makes sure that {@code selectedIdentifier} is a valid identifier for a toolchain,
+ * i.e. it starts with a letter or an underscore and continues with only dots, dashes,
+ * spaces, letters, digits or underscores (i.e. matches the following regular expression:
+ * "[a-zA-Z_][\.\- \w]*").
+ *
+ * @throws InvalidConfigurationException if selectedIdentifier is null or does not match the
+ * aforementioned regular expression.
+ */
+ private static void checkToolChain(String selectedIdentifier, String cpu)
+ throws InvalidConfigurationException {
+ if (selectedIdentifier == null) {
+ throw new InvalidConfigurationException("No toolchain found for cpu '" + cpu + "'");
+ }
+ // If you update this regex, please do so in the javadoc comment too, and also in the
+ // crosstool_config.proto file.
+ String rx = "[a-zA-Z_][\\.\\- \\w]*";
+ if (!selectedIdentifier.matches(rx)) {
+ throw new InvalidConfigurationException("Toolchain identifier for cpu '" + cpu + "' " +
+ "is illegal (does not match '" + rx + "')");
+ }
+ }
+
+ public static CrosstoolConfig.CrosstoolRelease getCrosstoolReleaseProto(
+ ConfigurationEnvironment env, BuildOptions options,
+ Label crosstoolTop, Function<String, String> cpuTransformer)
+ throws InvalidConfigurationException {
+ CrosstoolConfigurationLoader.CrosstoolFile file =
+ readCrosstool(env, crosstoolTop);
+ // Make sure that we have the requested toolchain in the result. Throw an exception if not.
+ selectToolchain(file.getProto(), options, cpuTransformer);
+ return file.getProto();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CrosstoolConfigurationOptions.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CrosstoolConfigurationOptions.java
new file mode 100644
index 0000000..e311ab6
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CrosstoolConfigurationOptions.java
@@ -0,0 +1,29 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+/**
+ * A container object which provides crosstool configuration options to the build.
+ */
+public interface CrosstoolConfigurationOptions {
+ /** Returns the CPU associated with this crosstool configuration. */
+ public String getCpu();
+
+ /** Returns the compiler associated with this crosstool configuration. */
+ public String getCompiler();
+
+ /** Returns the libc version associated with this crosstool configuration. */
+ public String getLibc();
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/DiscoveredSourceInputsHelper.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/DiscoveredSourceInputsHelper.java
new file mode 100644
index 0000000..a446125
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/DiscoveredSourceInputsHelper.java
@@ -0,0 +1,139 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.actions.ActionExecutionException;
+import com.google.devtools.build.lib.actions.ActionInput;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.ArtifactResolver;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Helper for actions that do include scanning. Currently only deals with source files, so is only
+ * appropriate for actions that do not discover generated files. Currently does not do .d file
+ * parsing, so the set of artifacts returned may be an overapproximation to the ones actually used
+ * during execution.
+ */
+public class DiscoveredSourceInputsHelper {
+
+ private DiscoveredSourceInputsHelper() {
+ }
+
+ /**
+ * Converts PathFragments into source Artifacts using an ArtifactResolver, ignoring any that are
+ * already in mandatoryInputs. Silently drops any PathFragments that cannot be resolved into
+ * Artifacts.
+ */
+ public static ImmutableList<Artifact> getDiscoveredInputsFromPaths(
+ Iterable<Artifact> mandatoryInputs, ArtifactResolver artifactResolver,
+ Collection<PathFragment> inputPaths) {
+ Set<PathFragment> knownPathFragments = new HashSet<>();
+ for (Artifact input : mandatoryInputs) {
+ knownPathFragments.add(input.getExecPath());
+ }
+ ImmutableList.Builder<Artifact> foundInputs = ImmutableList.builder();
+ for (PathFragment execPath : inputPaths) {
+ if (!knownPathFragments.add(execPath)) {
+ // Don't add any inputs that we already added, or original inputs, which we probably
+ // couldn't convert into artifacts anyway.
+ continue;
+ }
+ Artifact artifact = artifactResolver.resolveSourceArtifact(execPath);
+ // It is unlikely that this artifact is null, but tolerate the situation just in case.
+ // It is safe to ignore such paths because dependency checker would identify change in inputs
+ // (ignored path was used before) and will force action execution.
+ if (artifact != null) {
+ foundInputs.add(artifact);
+ }
+ }
+ return foundInputs.build();
+ }
+
+ /**
+ * Converts ActionInputs discovered as inputs during execution into source Artifacts, ignoring any
+ * that are already in mandatoryInputs or that live in builtInIncludeDirectories. If any
+ * ActionInputs cannot be resolved, an ActionExecutionException will be thrown.
+ *
+ * <p>This method duplicates the functionality of CppCompileAction#populateActionInputs, though it
+ * is simpler because it need not deal with derived artifacts and doesn't parse the .d file.
+ */
+ public static ImmutableList<Artifact> getDiscoveredInputsFromActionInputs(
+ Iterable<Artifact> mandatoryInputs,
+ ArtifactResolver artifactResolver,
+ Iterable<? extends ActionInput> discoveredInputs,
+ Iterable<PathFragment> builtInIncludeDirectories,
+ Action action,
+ Artifact primaryInput) throws ActionExecutionException {
+ List<PathFragment> systemIncludePrefixes = new ArrayList<>();
+ for (PathFragment includePath : builtInIncludeDirectories) {
+ if (includePath.isAbsolute()) {
+ systemIncludePrefixes.add(includePath);
+ }
+ }
+
+ // Avoid duplicates by keeping track of the ones we've seen so far, even though duplicates are
+ // unlikely, since they would have to be inputs to this (non-CppCompile) action and also
+ // #included by a C++ source file.
+ Set<Artifact> knownInputs = new HashSet<>();
+ Iterables.addAll(knownInputs, mandatoryInputs);
+ ImmutableList.Builder<Artifact> foundInputs = ImmutableList.builder();
+ // Check inclusions.
+ IncludeProblems problems = new IncludeProblems();
+ for (ActionInput input : discoveredInputs) {
+ if (input instanceof Artifact) {
+ Artifact artifact = (Artifact) input;
+ if (knownInputs.add(artifact)) {
+ foundInputs.add(artifact);
+ }
+ continue;
+ }
+ PathFragment execPath = new PathFragment(input.getExecPathString());
+ if (execPath.isAbsolute()) {
+ // Absolute includes from system paths are ignored.
+ if (FileSystemUtils.startsWithAny(execPath, systemIncludePrefixes)) {
+ continue;
+ }
+ // Theoretically, the more sophisticated logic of CppCompileAction#populateActioInputs could
+ // be used here, to allow absolute includes that started with the execRoot. However, since
+ // we don't hit this codepath for local execution, that should be unnecessary. If and when
+ // we examine the results of local execution for scanned includes, that case may need to be
+ // dealt with.
+ problems.add(execPath.getPathString());
+ }
+ Artifact artifact = artifactResolver.resolveSourceArtifact(execPath);
+ if (artifact != null) {
+ if (knownInputs.add(artifact)) {
+ foundInputs.add(artifact);
+ }
+ } else {
+ // Abort if we see files that we can't resolve, likely caused by
+ // undeclared includes or illegal include constructs.
+ problems.add(execPath.getPathString());
+ }
+ }
+ problems.assertProblemFree(action, primaryInput);
+ return foundInputs.build();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/DwoArtifactsCollector.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/DwoArtifactsCollector.java
new file mode 100644
index 0000000..142a67a
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/DwoArtifactsCollector.java
@@ -0,0 +1,120 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+
+/**
+ * Provides generic functionality for collecting the .dwo artifacts produced by any target
+ * that compiles C++ files. Supports both transitive and "only direct outputs" collection.
+ * Provides accessors for both PIC and non-PIC compilation modes.
+ */
+public class DwoArtifactsCollector {
+
+ /**
+ * The .dwo files collected by this target in non-PIC compilation mode (i.e. myobject.dwo).
+ */
+ private final NestedSet<Artifact> dwoArtifacts;
+
+ /**
+ * The .dwo files collected by this target in PIC compilation mode (i.e. myobject.pic.dwo).
+ */
+ private final NestedSet<Artifact> picDwoArtifacts;
+
+ /**
+ * Instantiates a "real" collector on meaningful data.
+ */
+ private DwoArtifactsCollector(CcCompilationOutputs compilationOutputs,
+ Iterable<TransitiveInfoCollection> deps) {
+
+ Preconditions.checkNotNull(compilationOutputs);
+ Preconditions.checkNotNull(deps);
+
+ // Note: .dwo collection works fine with any order, but tests may assume a
+ // specific order for readability / simplicity purposes. See
+ // DebugInfoPackagingTest for details.
+ NestedSetBuilder<Artifact> dwoBuilder = NestedSetBuilder.compileOrder();
+ NestedSetBuilder<Artifact> picDwoBuilder = NestedSetBuilder.compileOrder();
+
+ dwoBuilder.addAll(compilationOutputs.getDwoFiles());
+ picDwoBuilder.addAll(compilationOutputs.getPicDwoFiles());
+
+ for (TransitiveInfoCollection info : deps) {
+ CppDebugFileProvider provider = info.getProvider(CppDebugFileProvider.class);
+ if (provider != null) {
+ dwoBuilder.addTransitive(provider.getTransitiveDwoFiles());
+ picDwoBuilder.addTransitive(provider.getTransitivePicDwoFiles());
+ }
+ }
+
+ dwoArtifacts = dwoBuilder.build();
+ picDwoArtifacts = picDwoBuilder.build();
+ }
+
+ /**
+ * Instantiates an empty collector.
+ */
+ private DwoArtifactsCollector() {
+ dwoArtifacts = NestedSetBuilder.<Artifact>emptySet(Order.COMPILE_ORDER);
+ picDwoArtifacts = NestedSetBuilder.<Artifact>emptySet(Order.COMPILE_ORDER);
+ }
+
+ /**
+ * Returns a new instance that collects direct outputs and transitive dependencies.
+ *
+ * @param compilationOutputs the output compilation context for the owning target
+ * @param deps which of the target's transitive info collections should be visited
+ */
+ public static DwoArtifactsCollector transitiveCollector(CcCompilationOutputs compilationOutputs,
+ Iterable<TransitiveInfoCollection> deps) {
+ return new DwoArtifactsCollector(compilationOutputs, deps);
+ }
+
+ /**
+ * Returns a new instance that collects direct outputs only.
+ *
+ * @param compilationOutputs the output compilation context for the owning target
+ */
+ public static DwoArtifactsCollector directCollector(CcCompilationOutputs compilationOutputs) {
+ return new DwoArtifactsCollector(
+ compilationOutputs, ImmutableList.<TransitiveInfoCollection>of());
+ }
+
+ /**
+ * Returns a new instance that doesn't collect anything (its artifact sets are empty).
+ */
+ public static DwoArtifactsCollector emptyCollector() {
+ return new DwoArtifactsCollector();
+ }
+
+ /**
+ * Returns the .dwo files applicable to non-PIC compilation mode (i.e. myobject.dwo).
+ */
+ public NestedSet<Artifact> getDwoArtifacts() {
+ return dwoArtifacts;
+ }
+
+ /**
+ * Returns the .dwo files applicable to PIC compilation mode (i.e. myobject.pic.dwo).
+ */
+ public NestedSet<Artifact> getPicDwoArtifacts() {
+ return picDwoArtifacts;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/ExtractInclusionAction.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/ExtractInclusionAction.java
new file mode 100644
index 0000000..15d7010
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/ExtractInclusionAction.java
@@ -0,0 +1,85 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.AbstractAction;
+import com.google.devtools.build.lib.actions.ActionExecutionContext;
+import com.google.devtools.build.lib.actions.ActionExecutionException;
+import com.google.devtools.build.lib.actions.ActionOwner;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.Executor;
+import com.google.devtools.build.lib.actions.ResourceSet;
+
+import java.io.IOException;
+
+/**
+ * An action which greps for includes over a given .cc or .h file.
+ * This is a part of the work required for C++ include scanning.
+ *
+ * <p>Note that this may run grep-includes over-optimistically, where we previously
+ * had not. For example, consider a cc_library of generated headers. If another
+ * library depends on it, and only references one of the headers, the other
+ * grep-includes will have been wasted.
+ */
+final class ExtractInclusionAction extends AbstractAction {
+
+ private static final String GUID = "45b43e5a-4734-43bb-a05e-012313808142";
+
+ /**
+ * Constructs a new action.
+ */
+ public ExtractInclusionAction(ActionOwner owner, Artifact input, Artifact output) {
+ super(owner, ImmutableList.of(input), ImmutableList.of(output));
+ }
+
+ @Override
+ protected String computeKey() {
+ return GUID;
+ }
+
+ @Override
+ public String describeStrategy(Executor executor) {
+ return executor.getContext(CppCompileActionContext.class).strategyLocality();
+ }
+
+ @Override
+ public String getMnemonic() {
+ return "GrepIncludes";
+ }
+
+ @Override
+ protected String getRawProgressMessage() {
+ return "Extracting include lines from " + getPrimaryInput().prettyPrint();
+ }
+
+ @Override
+ public ResourceSet estimateResourceConsumption(Executor executor) {
+ return ResourceSet.ZERO;
+ }
+
+ @Override
+ public void execute(ActionExecutionContext actionExecutionContext)
+ throws ActionExecutionException, InterruptedException {
+ Executor executor = actionExecutionContext.getExecutor();
+ IncludeScanningContext context = executor.getContext(IncludeScanningContext.class);
+ try {
+ context.extractIncludes(actionExecutionContext, this, getPrimaryInput(),
+ getPrimaryOutput());
+ } catch (IOException e) {
+ throw new ActionExecutionException(e, this, false);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/FakeCppCompileAction.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/FakeCppCompileAction.java
new file mode 100644
index 0000000..bd15455
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/FakeCppCompileAction.java
@@ -0,0 +1,212 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import static java.nio.charset.StandardCharsets.ISO_8859_1;
+
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.ActionExecutionContext;
+import com.google.devtools.build.lib.actions.ActionExecutionException;
+import com.google.devtools.build.lib.actions.ActionOwner;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.ExecException;
+import com.google.devtools.build.lib.actions.Executor;
+import com.google.devtools.build.lib.actions.ResourceSet;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadCompatible;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.rules.cpp.CcToolchainFeatures.FeatureConfiguration;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.util.ShellEscaper;
+import com.google.devtools.build.lib.util.io.FileOutErr;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.io.IOException;
+import java.util.UUID;
+import java.util.logging.Logger;
+
+import javax.annotation.Nullable;
+
+/**
+ * Action that represents a fake C++ compilation step.
+ */
+@ThreadCompatible
+public class FakeCppCompileAction extends CppCompileAction {
+
+ private static final Logger LOG = Logger.getLogger(FakeCppCompileAction.class.getName());
+
+ public static final UUID GUID = UUID.fromString("b2d95c91-1434-47ae-a786-816017de8494");
+
+ private final PathFragment tempOutputFile;
+
+ FakeCppCompileAction(ActionOwner owner,
+ ImmutableList<String> features,
+ FeatureConfiguration featureConfiguration,
+ Artifact sourceFile,
+ Label sourceLabel,
+ NestedSet<Artifact> mandatoryInputs,
+ Artifact outputFile,
+ PathFragment tempOutputFile,
+ DotdFile dotdFile,
+ BuildConfiguration configuration,
+ CppConfiguration cppConfiguration,
+ CppCompilationContext context,
+ ImmutableList<String> copts,
+ ImmutableList<String> pluginOpts,
+ Predicate<String> nocopts,
+ ImmutableList<PathFragment> extraSystemIncludePrefixes,
+ boolean enableLayeringCheck,
+ @Nullable String fdoBuildStamp) {
+ super(owner, features, featureConfiguration, sourceFile, sourceLabel, mandatoryInputs,
+ outputFile, dotdFile, null, null, null,
+ configuration, cppConfiguration,
+ // We only allow inclusion of header files explicitly declared in
+ // "srcs", so we only use declaredIncludeSrcs, not declaredIncludeDirs.
+ // (Disallowing use of undeclared headers for cc_fake_binary is needed
+ // because the header files get included in the runfiles for the
+ // cc_fake_binary and for the negative compilation tests that depend on
+ // the cc_fake_binary, and the runfiles must be determined at analysis
+ // time, so they can't depend on the contents of the ".d" file.)
+ CppCompilationContext.disallowUndeclaredHeaders(context), null, copts, pluginOpts, nocopts,
+ extraSystemIncludePrefixes, enableLayeringCheck, fdoBuildStamp, VOID_INCLUDE_RESOLVER,
+ ImmutableList.<IncludeScannable>of(),
+ GUID, /*compileHeaderModules=*/false);
+ this.tempOutputFile = Preconditions.checkNotNull(tempOutputFile);
+ }
+
+ @Override
+ @ThreadCompatible
+ public void execute(ActionExecutionContext actionExecutionContext)
+ throws ActionExecutionException, InterruptedException {
+ Executor executor = actionExecutionContext.getExecutor();
+
+ // First, do an normal compilation, to generate the ".d" file. The generated
+ // object file is built to a temporary location (tempOutputFile) and ignored
+ // afterwards.
+ LOG.info("Generating " + getDotdFile());
+ CppCompileActionContext context = executor.getContext(CppCompileActionContext.class);
+ CppCompileActionContext.Reply reply = null;
+ try {
+ // We delegate stdout/stderr to nowhere, i.e. same as redirecting to /dev/null.
+ reply = context.execWithReply(
+ this, actionExecutionContext.withFileOutErr(new FileOutErr()));
+ } catch (ExecException e) {
+ // We ignore failures here (other than capturing the Distributor reply).
+ // The compilation may well fail (that's the whole point of negative compilation tests).
+ // We execute it here just for the side effect of generating the ".d" file.
+ reply = context.getReplyFromException(e, this);
+ if (reply == null) {
+ // This can only happen if the ExecException does not come from remote execution.
+ throw e.toActionExecutionException("", executor.getVerboseFailures(), this);
+ }
+ }
+ IncludeScanningContext scanningContext = executor.getContext(IncludeScanningContext.class);
+ updateActionInputs(executor.getExecRoot(), scanningContext.getArtifactResolver(), reply);
+
+ // Even cc_fake_binary rules need to properly declare their dependencies...
+ // In fact, they need to declare their dependencies even more than cc_binary rules do.
+ // CcCommonConfiguredTarget passes in an empty set of declaredIncludeDirs,
+ // so this check below will only allow inclusion of header files that are explicitly
+ // listed in the "srcs" of the cc_fake_binary or in the "srcs" of a cc_library that it
+ // depends on.
+ try {
+ validateInclusions(actionExecutionContext.getMiddlemanExpander(), executor.getEventHandler());
+ } catch (ActionExecutionException e) {
+ // TODO(bazel-team): (2009) make this into an error, once most of the current warnings
+ // are fixed.
+ executor.getEventHandler().handle(Event.warn(
+ getOwner().getLocation(),
+ e.getMessage() + ";\n this warning may eventually become an error"));
+ }
+
+ // Generate a fake ".o" file containing the command line needed to generate
+ // the real object file.
+ LOG.info("Generating " + outputFile);
+
+ // A cc_fake_binary rule generates fake .o files and a fake target file,
+ // which merely contain instructions on building the real target. We need to
+ // be careful to use a new set of output file names in the instructions, as
+ // to not overwrite the fake output files when someone tries to follow the
+ // instructions. As the real compilation is executed by the test from its
+ // runfiles directory (where writing is forbidden), we patch the command
+ // line to write to $TEST_TMPDIR instead.
+ final String outputPrefix = "$TEST_TMPDIR/";
+ String argv = Joiner.on(' ').join(
+ Iterables.transform(getArgv(outputFile.getExecPath()), new Function<String, String>() {
+ @Override
+ public String apply(String input) {
+ String result = ShellEscaper.escapeString(input);
+ if (input.equals(outputFile.getExecPathString())
+ || input.equals(getDotdFile().getSafeExecPath().getPathString())) {
+ result = outputPrefix + result;
+ }
+ return result;
+ }
+ }));
+
+ // Write the command needed to build the real .o file to the fake .o file.
+ // Generate a command to ensure that the output directory exists; otherwise
+ // the compilation would fail.
+ try {
+ // Ensure that the .d file and .o file are siblings, so that the "mkdir" below works for
+ // both.
+ Preconditions.checkState(outputFile.getExecPath().getParentDirectory().equals(
+ getDotdFile().getSafeExecPath().getParentDirectory()));
+ FileSystemUtils.writeContent(outputFile.getPath(), ISO_8859_1,
+ outputFile.getPath().getBaseName() + ": "
+ + "mkdir -p " + outputPrefix + "$(dirname " + outputFile.getExecPath() + ")"
+ + " && " + argv + "\n");
+ } catch (IOException e) {
+ throw new ActionExecutionException("failed to create fake compile command for rule '" +
+ getOwner().getLabel() + ": " + e.getMessage(),
+ this, false);
+ }
+ }
+
+ @Override
+ protected PathFragment getInternalOutputFile() {
+ return tempOutputFile;
+ }
+
+ @Override
+ public String getMnemonic() { return "FakeCppCompile"; }
+
+ @Override
+ public String describeStrategy(Executor executor) {
+ return "fake";
+ }
+
+ @Override
+ public ResourceSet estimateResourceConsumptionLocal() {
+ return new ResourceSet(/*memoryMb=*/1, /*cpuUsage=*/0.1, /*ioUsage=*/0.0);
+ }
+
+ @Override
+ public ResourceSet estimateResourceConsumption(Executor executor) {
+ return executor.getContext(CppCompileActionContext.class).estimateResourceConsumption(this);
+ }
+
+ @Override
+ protected boolean needsIncludeScanning(Executor executor) {
+ return false;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/FdoStubAction.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/FdoStubAction.java
new file mode 100644
index 0000000..f50a1ae
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/FdoStubAction.java
@@ -0,0 +1,70 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.AbstractAction;
+import com.google.devtools.build.lib.actions.ActionExecutionContext;
+import com.google.devtools.build.lib.actions.ActionOwner;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.Executor;
+import com.google.devtools.build.lib.actions.ResourceSet;
+import com.google.devtools.build.lib.vfs.Path;
+
+/**
+ * Stub action to be used as the generating action for FDO files that are extracted from the
+ * FDO zip.
+ *
+ * <p>This is needed because the extraction is currently not a bona fide action, therefore, Blaze
+ * would complain that these files have no generating action if we did not set it to an instance of
+ * this class.
+ */
+public class FdoStubAction extends AbstractAction {
+ public FdoStubAction(ActionOwner owner, Artifact output) {
+ // TODO(bazel-team): Make extracting the zip file a honest-to-God action so that we can do away
+ // with this ugliness.
+ super(owner, ImmutableList.<Artifact>of(), ImmutableList.of(output));
+ }
+
+ @Override
+ public String describeStrategy(Executor executor) {
+ return "";
+ }
+
+ @Override
+ public void execute(ActionExecutionContext actionExecutionContext) {
+ }
+
+ @Override
+ public String getMnemonic() {
+ return "FdoStubAction";
+ }
+
+ @Override
+ protected String computeKey() {
+ return "fdoStubAction";
+ }
+
+ @Override
+ public ResourceSet estimateResourceConsumption(Executor executor) {
+ return ResourceSet.ZERO;
+ }
+
+ @Override
+ public void prepare(Path execRoot) {
+ // The superclass would delete the output files here. We can't let that happen, since this
+ // action does not in fact create those files; it is only a placeholder and the actual files
+ // are created *before* the execution phase in FdoSupport.extractFdoZip()
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/FdoSupport.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/FdoSupport.java
new file mode 100644
index 0000000..911d888
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/FdoSupport.java
@@ -0,0 +1,679 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.ArtifactFactory;
+import com.google.devtools.build.lib.actions.PackageRootResolver;
+import com.google.devtools.build.lib.actions.Root;
+import com.google.devtools.build.lib.analysis.AnalysisEnvironment;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadHostile;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.skyframe.FileValue;
+import com.google.devtools.build.lib.skyframe.PrecomputedValue;
+import com.google.devtools.build.lib.syntax.Label;
+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 com.google.devtools.build.lib.vfs.RootedPath;
+import com.google.devtools.build.lib.vfs.ZipFileSystem;
+import com.google.devtools.build.lib.view.config.crosstool.CrosstoolConfig.LipoMode;
+import com.google.devtools.build.skyframe.SkyFunction;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.regex.Pattern;
+import java.util.zip.ZipException;
+
+/**
+ * Support class for FDO (feedback directed optimization) and LIPO (lightweight inter-procedural
+ * optimization).
+ *
+ * <p>There is a 1:1 relationship between {@link CppConfiguration} objects and {@code FdoSupport}
+ * objects. The FDO support of a build configuration can be retrieved using {@link
+ * CppConfiguration#getFdoSupport()}.
+ *
+ * <p>With respect to thread-safety, the {@link #prepareToBuild} method is not thread-safe, and must
+ * not be called concurrently with other methods on this class.
+ *
+ * <p>Here follows a quick run-down of how FDO/LIPO builds work (for non-FDO/LIPO builds, none
+ * of this applies):
+ *
+ * <p>{@link CppConfiguration#prepareHook} is called before the analysis phase, which calls
+ * {@link #prepareToBuild}, which extracts the FDO .zip (in case we work with an explicitly
+ * generated FDO profile file) or analyzes the .afdo.imports file next to the .afdo file (if
+ * AutoFDO is in effect).
+ *
+ * <p>.afdo.imports files contain one import a line. A line is two paths separated by a colon,
+ * with functions in the second path being referenced by functions in the first path. These are
+ * then put into the imports map. If we do AutoFDO, we don't handle individual .gcda files, so
+ * gcdaFiles will be empty.
+ *
+ * <p>Regular .fdo zip files contain .gcda files (which are added to gcdaFiles) and
+ * .gcda.imports files. There is one .gcda.imports file for every source file and it contains one
+ * path in every line, which can either be a path to a source file that contains a function
+ * referenced by the original source file or the .gcda file for such a referenced file. They
+ * both are added to the imports map.
+ *
+ * <p>If we do LIPO, we create an extra configuration that is called the "LIPO context collector",
+ * whose job it is to collect information that every configured target compiled with LIPO needs.
+ * The top-level target of this configuration is the LIPO context (always a cc_binary) and is an
+ * implicit dependency of every cc_* rule through their :lipo_context_collector attribute. The
+ * collected information is encapsulated in {@link LipoContextProvider}.
+ *
+ * <p>For each C++ compile action in the target configuration, {@link #configureCompilation} is
+ * called, which adds command line options and input files required for the build. There are
+ * three cases:
+ *
+ * <ul>
+ * <li>If we do AutoFDO, the .afdo file and the source files containing the functions imported
+ * by the original source file (as determined from the inputs map) are added.
+ * <li>If we do FDO, the .gcda file corresponding to the source file is added.
+ * <li>If we do LIPO, in addition to the .gcda file corresponding to the source file
+ * (like for FDO) the source files that contain the functions referenced by the source file and
+ * their .gcda files are added, too.
+ * </ul>
+ *
+ * <p>If we do LIPO, the actual C++ compilation context for LIPO compilation actions is pieced
+ * together from the CppCompileContext in LipoContextProvider and that of the rule being compiled.
+ * (see {@link CppCompilationContext#mergeForLipo}) This is so that the include files for the
+ * extra LIPO sources are found and is, strictly speaking, incorrect, since it also changes the
+ * declared include directories of the main source file, which in theory can result in the
+ * compilation passing even though it should fail with undeclared inclusion errors.
+ *
+ * <p>During the actual execution of the C++ compile action, the extra sources also need to be
+ * include scanned, which is the reason why they are {@link IncludeScannable} objects and not
+ * simple artifacts. We currently create these {@link IncludeScannable} objects by creating actual
+ * C++ compile actions in the LIPO context collector configuration which are then never executed.
+ * In fact, these C++ compile actions are never even registered with Skyframe. For this we
+ * propagate a bit from {@code BuildConfiguration.isActionsEnabled} to
+ * {@code CachingAnalysisEnvironment.allowRegisteringActions}, which causes actions to be silently
+ * discarded after configured targets are created.
+ */
+public class FdoSupport implements Serializable {
+
+ /**
+ * Path within profile data .zip files that is considered the root of the
+ * profile information directory tree.
+ */
+ private static final PathFragment ZIP_ROOT = new PathFragment("/");
+
+ /**
+ * Returns true if the give fdoFile represents an AutoFdo profile.
+ */
+ public static final boolean isAutoFdo(String fdoFile) {
+ return CppFileTypes.GCC_AUTO_PROFILE.matches(fdoFile);
+ }
+
+ /**
+ * Coverage information output directory passed to {@code --fdo_instrument},
+ * or {@code null} if FDO instrumentation is disabled.
+ */
+ private final PathFragment fdoInstrument;
+
+ /**
+ * Path of the profile file passed to {@code --fdo_optimize}, or
+ * {@code null} if FDO optimization is disabled. The profile file
+ * can be a coverage ZIP or an AutoFDO feedback file.
+ */
+ private final Path fdoProfile;
+
+ /**
+ * Temporary directory to which the coverage ZIP file is extracted to
+ * (relative to the exec root), or {@code null} if FDO optimization is
+ * disabled. This is used to create artifacts for the extracted files.
+ *
+ * <p>Note that this root is intentionally not registered with the artifact
+ * factory.
+ */
+ private final Root fdoRoot;
+
+ /**
+ * The relative path of the FDO root to the exec root.
+ */
+ private final PathFragment fdoRootExecPath;
+
+ /**
+ * Path of FDO files under the FDO root.
+ */
+ private final PathFragment fdoPath;
+
+ /**
+ * LIPO mode passed to {@code --lipo}. This is only used if
+ * {@code fdoProfile != null}.
+ */
+ private final LipoMode lipoMode;
+
+ /**
+ * Flag indicating whether to use AutoFDO (as opposed to
+ * instrumentation-based FDO).
+ */
+ private final boolean useAutoFdo;
+
+ /**
+ * The {@code .gcda} files that have been extracted from the ZIP file,
+ * relative to the root of the ZIP file.
+ *
+ * <p>Set only in {@link #prepareToBuild}.
+ */
+ private ImmutableSet<PathFragment> gcdaFiles = ImmutableSet.of();
+
+ /**
+ * Multimap from .gcda file base names to auxiliary input files.
+ *
+ * <p>The keys of the multimap are the exec root relative paths of .gcda files
+ * with the extension removed. The values are the lines from the accompanying
+ * .gcda.imports file.
+ *
+ * <p>The contents of the multimap are copied verbatim from the .gcda.imports
+ * files and not yet checked for validity.
+ *
+ * <p>Set only in {@link #prepareToBuild}.
+ */
+ private ImmutableMultimap<PathFragment, Artifact> imports;
+
+ /**
+ * Creates an FDO support object.
+ *
+ * @param fdoInstrument value of the --fdo_instrument option
+ * @param fdoProfile path to the profile file passed to --fdo_optimize option
+ * @param lipoMode value of the --lipo_mode option
+ */
+ public FdoSupport(PathFragment fdoInstrument, Path fdoProfile, LipoMode lipoMode, Path execRoot) {
+ this.fdoInstrument = fdoInstrument;
+ this.fdoProfile = fdoProfile;
+ this.fdoRoot = (fdoProfile == null)
+ ? null
+ : Root.asDerivedRoot(execRoot, execRoot.getRelative("blaze-fdo"));
+ this.fdoRootExecPath = fdoProfile == null
+ ? null
+ : fdoRoot.getExecPath().getRelative(new PathFragment("_fdo").getChild(
+ FileSystemUtils.removeExtension(fdoProfile.getBaseName())));
+ this.fdoPath = fdoProfile == null
+ ? null
+ : new PathFragment("_fdo").getChild(
+ FileSystemUtils.removeExtension(fdoProfile.getBaseName()));
+ this.lipoMode = lipoMode;
+ this.useAutoFdo = fdoProfile != null && isAutoFdo(fdoProfile.getBaseName());
+ }
+
+ public Root getFdoRoot() {
+ return fdoRoot;
+ }
+
+ public void declareSkyframeDependencies(SkyFunction.Environment env, Path execRoot) {
+ if (fdoProfile != null) {
+ if (isLipoEnabled()) {
+ // Incrementality is not supported for LIPO builds, see FdoSupport#scannables.
+ // Ensure that the Skyframe value containing the configuration will not be reused to avoid
+ // incrementality issues.
+ PrecomputedValue.dependOnBuildId(env);
+ return;
+ }
+
+ // IMPORTANT: Keep the following in sync with #prepareToBuild.
+ Path path;
+ if (useAutoFdo) {
+ path = fdoProfile.getParentDirectory().getRelative(
+ fdoProfile.getBaseName() + ".imports");
+ } else {
+ path = fdoProfile;
+ }
+ env.getValue(FileValue.key(RootedPath.toRootedPathMaybeUnderRoot(path,
+ ImmutableList.of(execRoot))));
+ }
+ }
+
+ /**
+ * Prepares the FDO support for building.
+ *
+ * <p>When an {@code --fdo_optimize} compile is requested, unpacks the given
+ * FDO gcda zip file into a clean working directory under execRoot.
+ *
+ * @throws FdoException if the FDO ZIP contains a file of unknown type
+ */
+ @ThreadHostile // must be called before starting the build
+ public void prepareToBuild(Path execRoot, PathFragment genfilesPath,
+ ArtifactFactory artifactDeserializer, PackageRootResolver resolver)
+ throws IOException, FdoException {
+ // The execRoot != null case is only there for testing. We cannot provide a real ZIP file in
+ // tests because ZipFileSystem does not work with a ZIP on an in-memory file system.
+ // IMPORTANT: Keep in sync with #declareSkyframeDependencies to avoid incrementality issues.
+ if (fdoProfile != null && execRoot != null) {
+ Path fdoDirPath = execRoot.getRelative(fdoRootExecPath);
+
+ FileSystemUtils.deleteTreesBelow(fdoDirPath);
+ FileSystemUtils.createDirectoryAndParents(fdoDirPath);
+
+ if (useAutoFdo) {
+ Path fdoImports = fdoProfile.getParentDirectory().getRelative(
+ fdoProfile.getBaseName() + ".imports");
+ if (isLipoEnabled()) {
+ imports = readAutoFdoImports(artifactDeserializer, fdoImports, genfilesPath, resolver);
+ }
+ FileSystemUtils.ensureSymbolicLink(
+ execRoot.getRelative(getAutoProfilePath()), fdoProfile);
+ } else {
+ Path zipFilePath = new ZipFileSystem(fdoProfile).getRootDirectory();
+ if (!zipFilePath.getRelative("blaze-out").isDirectory()) {
+ throw new ZipException("FDO zip files must be zipped directly above 'blaze-out' " +
+ "for the compiler to find the profile");
+ }
+ ImmutableSet.Builder<PathFragment> gcdaFilesBuilder = ImmutableSet.builder();
+ ImmutableMultimap.Builder<PathFragment, Artifact> importsBuilder =
+ ImmutableMultimap.builder();
+ extractFdoZip(artifactDeserializer, zipFilePath, fdoDirPath,
+ gcdaFilesBuilder, importsBuilder, resolver);
+ gcdaFiles = gcdaFilesBuilder.build();
+ imports = importsBuilder.build();
+ }
+ }
+ }
+
+ /**
+ * Recursively extracts a directory from the GCDA ZIP file into a target
+ * directory.
+ *
+ * <p>Imports files are not written to disk. Their content is directly added
+ * to an internal data structure.
+ *
+ * <p>The files are written at $EXECROOT/blaze-fdo/_fdo/(base name of profile zip), and the
+ * {@code _fdo} directory there is symlinked to from the exec root, so that the file are also
+ * available at $EXECROOT/_fdo/..., which is their exec path. We need to jump through these
+ * hoops because the FDO root 1. needs to be a source root, thus the exec path of its root is
+ * ".", 2. it must not be equal to the exec root so that the artifact factory does not get
+ * confused, 3. the files under it must be reachable by their exec path from the exec root.
+ *
+ * @throws IOException if any of the I/O operations failed
+ * @throws FdoException if the FDO ZIP contains a file of unknown type
+ */
+ private void extractFdoZip(ArtifactFactory artifactFactory, Path sourceDir,
+ Path targetDir, ImmutableSet.Builder<PathFragment> gcdaFilesBuilder,
+ ImmutableMultimap.Builder<PathFragment, Artifact> importsBuilder,
+ PackageRootResolver resolver) throws IOException, FdoException {
+ for (Path sourceFile : sourceDir.getDirectoryEntries()) {
+ Path targetFile = targetDir.getRelative(sourceFile.getBaseName());
+ if (sourceFile.isDirectory()) {
+ targetFile.createDirectory();
+ extractFdoZip(artifactFactory, sourceFile, targetFile, gcdaFilesBuilder, importsBuilder,
+ resolver);
+ } else {
+ if (CppFileTypes.COVERAGE_DATA.matches(sourceFile)) {
+ FileSystemUtils.copyFile(sourceFile, targetFile);
+ gcdaFilesBuilder.add(
+ sourceFile.relativeTo(sourceFile.getFileSystem().getRootDirectory()));
+ } else if (CppFileTypes.COVERAGE_DATA_IMPORTS.matches(sourceFile)) {
+ readCoverageImports(artifactFactory, sourceFile, importsBuilder, resolver);
+ } else {
+ throw new FdoException("FDO ZIP file contained a file of unknown type: "
+ + sourceFile);
+ }
+ }
+ }
+ }
+
+ /**
+ * Reads a .gcda.imports file and stores the imports information.
+ *
+ * @throws FdoException if an auxiliary LIPO input was not found
+ */
+ private void readCoverageImports(ArtifactFactory artifactFactory, Path importsFile,
+ ImmutableMultimap.Builder<PathFragment, Artifact> importsBuilder,
+ PackageRootResolver resolver) throws IOException, FdoException {
+ PathFragment key = importsFile.asFragment().relativeTo(ZIP_ROOT);
+ String baseName = key.getBaseName();
+ String ext = Iterables.getOnlyElement(CppFileTypes.COVERAGE_DATA_IMPORTS.getExtensions());
+ key = key.replaceName(baseName.substring(0, baseName.length() - ext.length()));
+
+ for (String line : FileSystemUtils.iterateLinesAsLatin1(importsFile)) {
+ if (!line.isEmpty()) {
+ // We can't yet fully check the validity of a line. this is done later
+ // when we actually parse the contained paths.
+ PathFragment execPath = new PathFragment(line);
+ if (execPath.isAbsolute()) {
+ throw new FdoException("Absolute paths not allowed in gcda imports file " + importsFile
+ + ": " + execPath);
+ }
+ Artifact artifact = artifactFactory.deserializeArtifact(new PathFragment(line), resolver);
+ if (artifact == null) {
+ throw new FdoException("Auxiliary LIPO input not found: " + line);
+ }
+
+ importsBuilder.put(key, artifact);
+ }
+ }
+ }
+
+ /**
+ * Reads a .afdo.imports file and stores the imports information.
+ */
+ private ImmutableMultimap<PathFragment, Artifact> readAutoFdoImports(
+ ArtifactFactory artifactFactory, Path importsFile, PathFragment genFilePath,
+ PackageRootResolver resolver)
+ throws IOException, FdoException {
+ ImmutableMultimap.Builder<PathFragment, Artifact> importBuilder = ImmutableMultimap.builder();
+ for (String line : FileSystemUtils.iterateLinesAsLatin1(importsFile)) {
+ if (!line.isEmpty()) {
+ PathFragment key = new PathFragment(line.substring(0, line.indexOf(':')));
+ if (key.startsWith(genFilePath)) {
+ key = key.relativeTo(genFilePath);
+ }
+ if (key.isAbsolute()) {
+ throw new FdoException("Absolute paths not allowed in afdo imports file " + importsFile
+ + ": " + key);
+ }
+ key = FileSystemUtils.replaceSegments(key, "PROTECTED", "_protected", true);
+ for (String auxFile : line.substring(line.indexOf(':') + 1).split(" ")) {
+ if (auxFile.length() == 0) {
+ continue;
+ }
+ Artifact artifact = artifactFactory.deserializeArtifact(new PathFragment(auxFile),
+ resolver);
+ if (artifact == null) {
+ throw new FdoException("Auxiliary LIPO input not found: " + auxFile);
+ }
+ importBuilder.put(key, artifact);
+ }
+ }
+ }
+ return importBuilder.build();
+ }
+
+ /**
+ * Returns the imports from the .afdo.imports file of a source file.
+ *
+ * @param sourceName the source file
+ */
+ private Collection<Artifact> getAutoFdoImports(PathFragment sourceName) {
+ Preconditions.checkState(isLipoEnabled());
+ ImmutableCollection<Artifact> afdoImports = imports.get(sourceName);
+ Preconditions.checkState(afdoImports != null,
+ "AutoFDO import data missing for %s", sourceName);
+ return afdoImports;
+ }
+
+ /**
+ * Returns the imports from the .gcda.imports file of an object file.
+ *
+ * @param objDirectory the object directory of the object file's target
+ * @param objectName the object file
+ */
+ private Iterable<Artifact> getImports(PathFragment objDirectory, PathFragment objectName) {
+ Preconditions.checkState(isLipoEnabled());
+ Preconditions.checkState(imports != null,
+ "Tried to look up imports of uninitialized FDOSupport");
+ PathFragment key = objDirectory.getRelative(FileSystemUtils.removeExtension(objectName));
+ ImmutableCollection<Artifact> importsForObject = imports.get(key);
+ Preconditions.checkState(importsForObject != null, "Import data missing for %s", key);
+ return importsForObject;
+ }
+
+ /**
+ * Configures a compile action builder by adding command line options and
+ * auxiliary inputs according to the FDO configuration. This method does
+ * nothing If FDO is disabled.
+ */
+ @ThreadSafe
+ public void configureCompilation(CppCompileActionBuilder builder, RuleContext ruleContext,
+ AnalysisEnvironment env, Label lipoLabel, PathFragment sourceName, final Pattern nocopts,
+ boolean usePic, LipoContextProvider lipoInputProvider) {
+ // It is a bug if this method is called with useLipo if lipo is disabled. However, it is legal
+ // if is is called with !useLipo, even though lipo is enabled.
+ Preconditions.checkArgument(lipoInputProvider == null || isLipoEnabled());
+
+ // FDO is disabled -> do nothing.
+ if ((fdoInstrument == null) && (fdoRoot == null)) {
+ return;
+ }
+
+ List<String> fdoCopts = new ArrayList<>();
+ // Instrumentation phase
+ if (fdoInstrument != null) {
+ fdoCopts.add("-fprofile-generate=" + fdoInstrument.getPathString());
+ if (lipoMode != LipoMode.OFF) {
+ fdoCopts.add("-fripa");
+ }
+ }
+
+ // Optimization phase
+ if (fdoRoot != null) {
+ // Declare dependency on contents of zip file.
+ if (env.getSkyframeEnv().valuesMissing()) {
+ return;
+ }
+ Iterable<Artifact> auxiliaryInputs = getAuxiliaryInputs(
+ ruleContext, env, lipoLabel, sourceName, usePic, lipoInputProvider);
+ builder.addMandatoryInputs(auxiliaryInputs);
+ if (!Iterables.isEmpty(auxiliaryInputs)) {
+ if (useAutoFdo) {
+ fdoCopts.add("-fauto-profile=" + getAutoProfilePath().getPathString());
+ } else {
+ fdoCopts.add("-fprofile-use=" + fdoRootExecPath);
+ }
+ fdoCopts.add("-fprofile-correction");
+ if (lipoInputProvider != null) {
+ fdoCopts.add("-fripa");
+ }
+ }
+ }
+ Iterable<String> filteredCopts = fdoCopts;
+ if (nocopts != null) {
+ // Filter fdoCopts with nocopts if they exist.
+ filteredCopts = Iterables.filter(fdoCopts, new Predicate<String>() {
+ @Override
+ public boolean apply(String copt) {
+ return !nocopts.matcher(copt).matches();
+ }
+ });
+ }
+ builder.addCopts(0, filteredCopts);
+ }
+
+ /**
+ * Returns the auxiliary files that need to be added to the {@link CppCompileAction}.
+ */
+ private Iterable<Artifact> getAuxiliaryInputs(
+ RuleContext ruleContext, AnalysisEnvironment env, Label lipoLabel, PathFragment sourceName,
+ boolean usePic, LipoContextProvider lipoContextProvider) {
+ // If --fdo_optimize was not specified, we don't have any additional inputs.
+ if (fdoProfile == null) {
+ return ImmutableSet.of();
+ } else if (useAutoFdo) {
+ ImmutableSet.Builder<Artifact> auxiliaryInputs = ImmutableSet.builder();
+
+ Artifact artifact = env.getDerivedArtifact(
+ fdoPath.getRelative(getAutoProfileRootRelativePath()), fdoRoot);
+ env.registerAction(new FdoStubAction(ruleContext.getActionOwner(), artifact));
+ auxiliaryInputs.add(artifact);
+ if (lipoContextProvider != null) {
+ auxiliaryInputs.addAll(getAutoFdoImports(sourceName));
+ }
+ return auxiliaryInputs.build();
+ } else {
+ ImmutableSet.Builder<Artifact> auxiliaryInputs = ImmutableSet.builder();
+
+ PathFragment objectName =
+ FileSystemUtils.replaceExtension(sourceName, usePic ? ".pic.o" : ".o");
+
+ auxiliaryInputs.addAll(
+ getGcdaArtifactsForObjectFileName(ruleContext, env, objectName, lipoLabel));
+
+ if (lipoContextProvider != null) {
+ for (Artifact importedFile : getImports(
+ getNonLipoObjDir(ruleContext, lipoLabel), objectName)) {
+ if (CppFileTypes.COVERAGE_DATA.matches(importedFile.getFilename())) {
+ Artifact gcdaArtifact = getGcdaArtifactsForGcdaPath(
+ ruleContext, env, importedFile.getExecPath());
+ if (gcdaArtifact == null) {
+ ruleContext.ruleError(String.format(
+ ".gcda file %s is not in the FDO zip (referenced by source file %s)",
+ importedFile.getExecPath(), sourceName));
+ } else {
+ auxiliaryInputs.add(gcdaArtifact);
+ }
+ } else {
+ auxiliaryInputs.add(importedFile);
+ }
+ }
+ }
+
+ return auxiliaryInputs.build();
+ }
+ }
+
+ /**
+ * Returns the .gcda file artifacts for a .gcda path from the .gcda.imports file or null if the
+ * referenced .gcda file is not in the FDO zip.
+ */
+ private Artifact getGcdaArtifactsForGcdaPath(RuleContext ruleContext,
+ AnalysisEnvironment env, PathFragment gcdaPath) {
+ if (!gcdaFiles.contains(gcdaPath)) {
+ return null;
+ }
+
+ Artifact artifact = env.getDerivedArtifact(fdoPath.getRelative(gcdaPath), fdoRoot);
+ env.registerAction(new FdoStubAction(ruleContext.getActionOwner(), artifact));
+ return artifact;
+ }
+
+ private PathFragment getNonLipoObjDir(RuleContext ruleContext, Label label) {
+ return ruleContext.getConfiguration().getBinFragment()
+ .getRelative(CppHelper.getObjDirectory(label));
+ }
+
+ /**
+ * Returns a list of .gcda file artifacts for an object file path.
+ *
+ * <p>The resulting set is either empty (because no .gcda file exists for the
+ * given object file) or contains one or two artifacts (the file itself and a
+ * symlink to it).
+ */
+ private ImmutableList<Artifact> getGcdaArtifactsForObjectFileName(RuleContext ruleContext,
+ AnalysisEnvironment env, PathFragment objectFileName, Label lipoLabel) {
+ // We put the .gcda files relative to the location of the .o file in the instrumentation run.
+ String gcdaExt = Iterables.getOnlyElement(CppFileTypes.COVERAGE_DATA.getExtensions());
+ PathFragment baseName = FileSystemUtils.replaceExtension(objectFileName, gcdaExt);
+ PathFragment gcdaFile = getNonLipoObjDir(ruleContext, lipoLabel).getRelative(baseName);
+
+ if (!gcdaFiles.contains(gcdaFile)) {
+ // If the object is a .pic.o file and .pic.gcda is not found, we should try finding .gcda too
+ String picoExt = Iterables.getOnlyElement(CppFileTypes.PIC_OBJECT_FILE.getExtensions());
+ baseName = FileSystemUtils.replaceExtension(objectFileName, gcdaExt, picoExt);
+ if (baseName == null) {
+ // Object file is not .pic.o
+ return ImmutableList.of();
+ }
+ gcdaFile = getNonLipoObjDir(ruleContext, lipoLabel).getRelative(baseName);
+ if (!gcdaFiles.contains(gcdaFile)) {
+ // .gcda file not found
+ return ImmutableList.of();
+ }
+ }
+
+ final Artifact artifact = env.getDerivedArtifact(fdoPath.getRelative(gcdaFile), fdoRoot);
+ env.registerAction(new FdoStubAction(ruleContext.getActionOwner(), artifact));
+
+ return ImmutableList.of(artifact);
+ }
+
+
+ private PathFragment getAutoProfilePath() {
+ return fdoRootExecPath.getRelative(getAutoProfileRootRelativePath());
+ }
+
+ private PathFragment getAutoProfileRootRelativePath() {
+ return new PathFragment(fdoProfile.getBaseName());
+ }
+
+ /**
+ * Returns whether LIPO is enabled.
+ */
+ @ThreadSafe
+ public boolean isLipoEnabled() {
+ return fdoProfile != null && lipoMode != LipoMode.OFF;
+ }
+
+ /**
+ * Returns whether AutoFDO is enabled.
+ */
+ @ThreadSafe
+ public boolean isAutoFdoEnabled() {
+ return useAutoFdo;
+ }
+
+ /**
+ * Returns an immutable list of command line arguments to add to the linker
+ * command line. If FDO is disabled, and empty list is returned.
+ */
+ @ThreadSafe
+ public ImmutableList<String> getLinkOptions() {
+ return fdoInstrument != null
+ ? ImmutableList.of("-fprofile-generate=" + fdoInstrument.getPathString())
+ : ImmutableList.<String>of();
+ }
+
+ /**
+ * Returns the path of the FDO output tree (relative to the execution root)
+ * containing the .gcda profile files, or null if FDO is not enabled.
+ */
+ @VisibleForTesting
+ public PathFragment getFdoOptimizeDir() {
+ return fdoRootExecPath;
+ }
+
+ /**
+ * Returns the path of the FDO zip containing the .gcda profile files, or null
+ * if FDO is not enabled.
+ */
+ @VisibleForTesting
+ public Path getFdoOptimizeProfile() {
+ return fdoProfile;
+ }
+
+ /**
+ * Returns the path fragment of the instrumentation output dir for gcc when
+ * FDO is enabled, or null if FDO is not enabled.
+ */
+ @ThreadSafe
+ public PathFragment getFdoInstrument() {
+ return fdoInstrument;
+ }
+
+ @VisibleForTesting
+ public void setGcdaFilesForTesting(ImmutableSet<PathFragment> gcdaFiles) {
+ this.gcdaFiles = gcdaFiles;
+ }
+
+ /**
+ * An exception indicating an issue with FDO coverage files.
+ */
+ public static final class FdoException extends Exception {
+ FdoException(String message) {
+ super(message);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/HeaderTargetModuleMapProvider.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/HeaderTargetModuleMapProvider.java
new file mode 100644
index 0000000..17e2e5c
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/HeaderTargetModuleMapProvider.java
@@ -0,0 +1,42 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+
+import java.util.List;
+
+/**
+ * A provider for cc_public_library rules to be able to convey the information about the
+ * header target's module map references to the public library target.
+ */
+@Immutable
+public final class HeaderTargetModuleMapProvider implements TransitiveInfoProvider {
+
+ private final ImmutableList<CppModuleMap> cppModuleMaps;
+
+ public HeaderTargetModuleMapProvider(Iterable<CppModuleMap> cppModuleMaps) {
+ this.cppModuleMaps = ImmutableList.copyOf(cppModuleMaps);
+ }
+
+ /**
+ * Returns the module maps referenced by cc_public_library's headers target.
+ */
+ public List<CppModuleMap> getCppModuleMaps() {
+ return cppModuleMaps;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/ImplementedCcPublicLibrariesProvider.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/ImplementedCcPublicLibrariesProvider.java
new file mode 100644
index 0000000..4f2a585
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/ImplementedCcPublicLibrariesProvider.java
@@ -0,0 +1,42 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.syntax.Label;
+
+/**
+ * A provider for cc_library rules to be able to convey the information about which
+ * cc_public_library rules they implement to dependent targets.
+ */
+@Immutable
+public final class ImplementedCcPublicLibrariesProvider implements TransitiveInfoProvider {
+
+ private final ImmutableList<Label> implementedCcPublicLibraries;
+
+ public ImplementedCcPublicLibrariesProvider(ImmutableList<Label> implementedCcPublicLibraries) {
+ this.implementedCcPublicLibraries = implementedCcPublicLibraries;
+ }
+
+ /**
+ * Returns the labels for the "$headers" target that are implemented by the target which
+ * implements this interface.
+ */
+ public ImmutableList<Label> getImplementedCcPublicLibraries() {
+ return implementedCcPublicLibraries;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/IncludeParser.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/IncludeParser.java
new file mode 100644
index 0000000..0b60b45
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/IncludeParser.java
@@ -0,0 +1,711 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import static java.nio.charset.StandardCharsets.ISO_8859_1;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.base.Supplier;
+import com.google.common.base.Suppliers;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Sets;
+import com.google.common.io.CharStreams;
+import com.google.devtools.build.lib.actions.ActionExecutionContext;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.ArtifactFactory;
+import com.google.devtools.build.lib.actions.Root;
+import com.google.devtools.build.lib.rules.cpp.IncludeParser.Inclusion.Kind;
+import com.google.devtools.build.lib.rules.cpp.RemoteIncludeExtractor.RemoteParseData;
+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 com.google.devtools.build.skyframe.SkyValue;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+import javax.annotation.Nullable;
+
+/**
+ * Scans a source file and extracts the literal inclusions it specifies. Does not store results --
+ * repeated requests to the same file will result in repeated scans. Clients should implement a
+ * caching layer in order to avoid unnecessary disk access when requesting an already scanned file.
+ */
+public class IncludeParser implements SkyValue {
+ private static final Logger LOG = Logger.getLogger(IncludeParser.class.getName());
+ private static final boolean LOG_FINE = LOG.isLoggable(Level.FINE);
+ private static final boolean LOG_FINER = LOG.isLoggable(Level.FINER);
+
+ /**
+ * Immutable object representation of the four columns making up a single Rule
+ * in a Hints set. See {@link Hints} for more details.
+ */
+ private static class Rule {
+ private enum Type { PATH, FILE, INCLUDE_QUOTE, INCLUDE_ANGLE; }
+ final Type type;
+ final Pattern pattern;
+ final String findRoot;
+ final Pattern findFilter;
+
+ private Rule(String type, String pattern, String findRoot, Pattern findFilter) {
+ this.type = Type.valueOf(type.trim().toUpperCase());
+ this.pattern = Pattern.compile("^" + pattern + "$");
+ this.findRoot = findRoot;
+ this.findFilter = findFilter;
+ }
+
+ /**
+ * @throws PatternSyntaxException, IllegalArgumentException if bad values
+ * are provided
+ */
+ public Rule(String type, String pattern, String findRoot, String findFilter) {
+ this(type, pattern, findRoot.replace("\\", "$"), Pattern.compile(findFilter));
+ Preconditions.checkArgument((this.type == Type.PATH) || (this.type == Type.FILE));
+ }
+
+ public Rule(String type, String pattern, String findRoot) {
+ this(type, pattern, findRoot, (Pattern) null);
+ Preconditions.checkArgument((this.type == Type.INCLUDE_QUOTE)
+ || (this.type == Type.INCLUDE_ANGLE));
+ }
+
+ @Override public String toString() {
+ return "" + type + " " + pattern + " " + findRoot + " " + findFilter;
+ }
+ }
+
+ /**
+ * This class is a representation of the INCLUDE_HINTS file maintained and
+ * delivered with the remote client. The hints file contains regexp-based rules
+ * to help this simple include scanner cope with computed includes, which
+ * would otherwise require a full preprocessor with symbol support. Instead of
+ * actually processing symbols to evaluate the computed includes, we instead
+ * apply rules to gather inclusions for matching paths.
+ * <p>
+ * The hints file is read, line by line, into a list of rules each of which
+ * encapsulates a line of four columns. Each non-blank, non-comment line has
+ * the format:
+ *
+ * <pre>
+ * "file"|"path" match-pattern find-root find-filter
+ * </pre>
+ *
+ * <p>
+ * The first column specifies whether the line is a rule based on matching
+ * source <em>files</em> (passed directly to gcc as inputs, or transitively
+ * #included by other inputs) or include <em>paths</em> (passed to gcc as
+ * -I, -iquote, or -isystem flags).
+ * <p>
+ * The second column is a regexp for files or paths. Whenever a compiler
+ * argument of the specified type matches that regexp, the rule is taken. (All
+ * matching rules for every path and file on a compiler command line are
+ * followed, and the results are combined.)
+ * <p>
+ * The third column is a point in the local filesystem from which to extract a
+ * recursive listing. (This follows symlinks) Backrefs may be used to refer to
+ * the regexp or its capturing groups. (This is mostly necessary because
+ * --package_path can cause input paths to carry arbitrary prefixes.)
+ * <p>
+ * The fourth column is a regexp applied to each file found by the recursive
+ * listing. All matching files are treated as dependencies.
+ */
+ public static class Hints implements SkyValue {
+
+ private static final Pattern WS_PAT = Pattern.compile("\\s+");
+
+ private final Path workingDir;
+ private final List<Rule> rules = new ArrayList<>();
+ private final ArtifactFactory artifactFactory;
+
+ private final LoadingCache<Artifact, Collection<Artifact>> fileLevelHintsCache =
+ CacheBuilder.newBuilder().build(
+ new CacheLoader<Artifact, Collection<Artifact>>() {
+ @Override
+ public Collection<Artifact> load(Artifact path) {
+ return getHintedInclusions(Rule.Type.FILE, path.getPath(), path.getRoot());
+ }
+ });
+
+ private final LoadingCache<Path, Collection<Artifact>> pathLevelHintsCache =
+ CacheBuilder.newBuilder().build(
+ new CacheLoader<Path, Collection<Artifact>>() {
+ @Override
+ public Collection<Artifact> load(Path path) {
+ return getHintedInclusions(Rule.Type.PATH, path, null);
+ }
+ });
+
+ /**
+ * Constructs a hint set for a given working/exec directory and INCLUDE_HINTS file to read.
+ *
+ * @param workingDir the working/exec directory that processed paths are relative to
+ * @param hintsFile the hints file to read
+ * @throws IOException if the hints file can't be read or parsed
+ */
+ public Hints(Path workingDir, Path hintsFile, ArtifactFactory artifactFactory)
+ throws IOException {
+ this.workingDir = workingDir;
+ this.artifactFactory = artifactFactory;
+ try (InputStream is = hintsFile.getInputStream()) {
+ for (String line : CharStreams.readLines(new InputStreamReader(is, "UTF-8"))) {
+ line = line.trim();
+ if (line.length() == 0 || line.startsWith("#")) {
+ continue;
+ }
+ String[] tokens = WS_PAT.split(line);
+ try {
+ if (tokens.length == 3) {
+ rules.add(new Rule(tokens[0], tokens[1], tokens[2]));
+ } else if (tokens.length == 4) {
+ rules.add(new Rule(tokens[0], tokens[1], tokens[2], tokens[3]));
+ } else {
+ throw new IOException("Malformed hint line: " + line);
+ }
+ } catch (PatternSyntaxException e) {
+ throw new IOException("Malformed hint regex on: " + line + "\n " + e.getMessage());
+ } catch (IllegalArgumentException e) {
+ throw new IOException("Invalid type on: " + line + "\n " + e.getMessage());
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the "file" type hinted inclusions for a given path, caching results by path.
+ */
+ public Collection<Artifact> getFileLevelHintedInclusions(Artifact path) {
+ return fileLevelHintsCache.getUnchecked(path);
+ }
+
+ public Collection<Artifact> getPathLevelHintedInclusions(Path path) {
+ return pathLevelHintsCache.getUnchecked(path);
+ }
+
+ /**
+ * Performs the work of matching a given file/path of a specified file/path type against the
+ * hints and returns the expanded paths.
+ */
+ private Collection<Artifact> getHintedInclusions(Rule.Type type, Path path,
+ @Nullable Root sourceRoot) {
+ String pathString = path.getPathString();
+ // Delay creation until we know we need one. Use a TreeSet to make sure that the results are
+ // sorted with a stable order and unique.
+ Set<Path> hints = null;
+ for (final Rule rule : rules) {
+ if (type != rule.type) {
+ continue;
+ }
+ Matcher m = rule.pattern.matcher(pathString);
+ if (!m.matches()) {
+ continue;
+ }
+ if (hints == null) { hints = Sets.newTreeSet(); }
+ Path root = workingDir.getRelative(m.replaceFirst(rule.findRoot));
+ if (LOG_FINE) {
+ LOG.fine("hint for " + rule.type + " " + pathString + " root: " + root);
+ }
+ try {
+ // The assumption is made here that all files specified by this hint are under the same
+ // package path as the original file -- this filesystem tree traversal is completely
+ // ignorant of package paths. This could be violated if there were a hint that resolved to
+ // foo/**/*.h, there was a package foo/bar, and the packages foo and foo/bar were in
+ // different package paths. In that case, this traversal would fail to pick up
+ // foo/bar/**/*.h. No examples of this currently exist in the INCLUDE_HINTS
+ // file.
+ FileSystemUtils.traverseTree(hints, root, new Predicate<Path>() {
+ @Override
+ public boolean apply(Path p) {
+ boolean take = p.isFile() && rule.findFilter.matcher(p.getPathString()).matches();
+ if (LOG_FINER && take) {
+ LOG.finer("hinted include: " + p);
+ }
+ return take;
+ }
+ });
+ } catch (IOException e) {
+ LOG.warning("Error in hint expansion: " + e);
+ }
+ }
+ if (hints != null && !hints.isEmpty()) {
+ // Transform paths into source artifacts (all hints must be to source artifacts).
+ List<Artifact> result = new ArrayList<>(hints.size());
+ for (Path hint : hints) {
+ if (hint.startsWith(workingDir)) {
+ // Paths that are under the execRoot can be resolved as source artifacts as usual. All
+ // include directories are specified relative to the execRoot, and so fall here.
+ result.add(Preconditions.checkNotNull(
+ artifactFactory.resolveSourceArtifact(hint.relativeTo(workingDir)), hint));
+ } else {
+ // The file passed in might not have been under the execRoot, for instance
+ // <workspace>/foo/foo.cc.
+ Preconditions.checkNotNull(sourceRoot, "%s %s", path, hint);
+ Path sourcePath = sourceRoot.getPath();
+ Preconditions.checkState(hint.startsWith(sourcePath),
+ "%s %s %s", hint, path, sourceRoot);
+ result.add(Preconditions.checkNotNull(
+ artifactFactory.getSourceArtifact(hint.relativeTo(sourcePath), sourceRoot)));
+ }
+ }
+ return result;
+ } else {
+ return ImmutableList.of();
+ }
+ }
+
+ private Collection<Inclusion> getHintedInclusions(Artifact path) {
+ String pathString = path.getPath().getPathString();
+ // Delay creation until we know we need one. Use a LinkedHashSet to make sure that the results
+ // are sorted with a stable order and unique.
+ Set<Inclusion> hints = null;
+ for (final Rule rule : rules) {
+ if ((rule.type != Rule.Type.INCLUDE_ANGLE) && (rule.type != Rule.Type.INCLUDE_QUOTE)) {
+ continue;
+ }
+ Matcher m = rule.pattern.matcher(pathString);
+ if (!m.matches()) {
+ continue;
+ }
+ if (hints == null) { hints = Sets.newLinkedHashSet(); }
+ Inclusion inclusion = new Inclusion(rule.findRoot, rule.type == Rule.Type.INCLUDE_QUOTE
+ ? Kind.QUOTE : Kind.ANGLE);
+ hints.add(inclusion);
+ if (LOG_FINE) {
+ LOG.fine("hint for " + rule.type + " " + pathString + " root: " + inclusion);
+ }
+ }
+ if (hints != null && !hints.isEmpty()) {
+ return ImmutableList.copyOf(hints);
+ } else {
+ return ImmutableList.of();
+ }
+ }
+ }
+
+ public Hints getHints() {
+ return hints;
+ }
+
+ /**
+ * An immutable inclusion tuple. This models an {@code #include} or {@code
+ * #include_next} line in a file without the context how this file got
+ * included.
+ */
+ public static class Inclusion {
+ /** The format of the #include in the source file -- quoted, angle bracket, etc. */
+ public enum Kind {
+ /** Quote includes: {@code #include "name"}. */
+ QUOTE,
+
+ /** Angle bracket includes: {@code #include <name>}. */
+ ANGLE,
+
+ /** Quote next includes: {@code #include_next "name"}. */
+ NEXT_QUOTE,
+
+ /** Angle next includes: {@code #include_next <name>}. */
+ NEXT_ANGLE,
+
+ /** Computed or other unhandlable includes: {@code #include HEADER}. */
+ OTHER;
+
+ /**
+ * Returns true if this is an {@code #include_next} inclusion,
+ */
+ public boolean isNext() {
+ return this == NEXT_ANGLE || this == NEXT_QUOTE;
+ }
+ }
+
+ /** The kind of inclusion. */
+ public final Kind kind;
+ /** The relative path of the inclusion. */
+ public final PathFragment pathFragment;
+
+ public Inclusion(String includeTarget, Kind kind) {
+ this.kind = kind;
+ this.pathFragment = new PathFragment(includeTarget);
+ }
+
+ public Inclusion(PathFragment pathFragment, Kind kind) {
+ this.kind = kind;
+ this.pathFragment = Preconditions.checkNotNull(pathFragment);
+ }
+
+ public String getPathString() {
+ return pathFragment.getPathString();
+ }
+
+ @Override
+ public String toString() {
+ return kind.toString() + ":" + pathFragment.getPathString();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) {
+ return true;
+ }
+ if (!(o instanceof Inclusion)) {
+ return false;
+ }
+ Inclusion that = (Inclusion) o;
+ return kind == that.kind && pathFragment.equals(that.pathFragment);
+ }
+
+ @Override
+ public int hashCode() {
+ return pathFragment.hashCode() * 37 + kind.hashCode();
+ }
+ }
+
+ /**
+ * The externally-scoped immutable hints helper that is shared by all scanners.
+ */
+ private final Hints hints;
+
+ /**
+ * A scanner that extracts includes from an individual files remotely, used when scanning files
+ * generated remotely.
+ */
+ private final Supplier<? extends RemoteIncludeExtractor> remoteExtractor;
+
+ /**
+ * Constructs a new FileParser.
+ * @param remoteExtractor a processor that extracts includes from an individual file remotely.
+ * @param hints regexps for converting computed includes into simple strings
+ */
+ public IncludeParser(@Nullable RemoteIncludeExtractor remoteExtractor, Hints hints) {
+ this.hints = hints;
+ this.remoteExtractor = Suppliers.ofInstance(remoteExtractor);
+ }
+
+ /**
+ * Constructs a new FileParser.
+ * @param remoteExtractorSupplier a supplier of a processor that extracts includes from an
+ * individual file remotely.
+ * @param hints regexps for converting computed includes into simple strings
+ */
+ public IncludeParser(Supplier<? extends RemoteIncludeExtractor> remoteExtractorSupplier,
+ Hints hints) {
+ this.hints = hints;
+ this.remoteExtractor = remoteExtractorSupplier;
+ }
+
+ /**
+ * Skips whitespace, \+NL pairs, and block-style / * * / comments. Assumes
+ * line comments are handled outside. Does not handle digraphs, trigraphs or
+ * decahexagraphs.
+ *
+ * @param chars characters to scan
+ * @param pos the starting position
+ * @return the resulting position after skipping whitespace and comments.
+ */
+ protected static int skipWhitespace(char[] chars, int pos, int end) {
+ while (pos < end) {
+ if (Character.isWhitespace(chars[pos])) {
+ pos++;
+ } else if (chars[pos] == '\\' && pos + 1 < end && chars[pos + 1] == '\n') {
+ pos++;
+ } else if (chars[pos] == '/' && pos + 1 < end && chars[pos + 1] == '*') {
+ pos += 2;
+ while (pos < end - 1) {
+ if (chars[pos++] == '*') {
+ if (chars[pos] == '/') {
+ pos++;
+ break; // proper comment end
+ }
+ }
+ }
+ } else { // not whitespace
+ return pos;
+ }
+ }
+ return pos; // pos == len, meaning we fell off the end.
+ }
+
+ /**
+ * Checks for and skips a given token.
+ *
+ * @param chars characters to scan
+ * @param pos the starting position
+ * @param expected the expected token
+ * @return the resulting position if found, otherwise -1
+ */
+ protected static int expect(char[] chars, int pos, int end, String expected) {
+ int si = 0;
+ int expectedLen = expected.length();
+ while (pos < end) {
+ if (si == expectedLen) {
+ return pos;
+ }
+ if (chars[pos++] != expected.charAt(si++)) {
+ return -1;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Finds the index of a given character token from a starting pos.
+ *
+ * @param chars characters to scan
+ * @param pos the starting position
+ * @param echar the character to find
+ * @return the resulting position of echar if found, otherwise -1
+ */
+ private static int indexOf(char[] chars, int pos, int end, char echar) {
+ while (pos < end) {
+ if (chars[pos] == echar) {
+ return pos;
+ }
+ pos++;
+ }
+ return -1;
+ }
+
+ private static final Pattern BS_NL_PAT = Pattern.compile("\\\\" + "\n");
+
+ // Keep this in sync with the auxiliary binary's scanning output format.
+ private static final ImmutableMap<Character, Kind> KIND_MAP = ImmutableMap.of(
+ '"', Kind.QUOTE,
+ '<', Kind.ANGLE,
+ 'q', Kind.NEXT_QUOTE,
+ 'a', Kind.NEXT_ANGLE);
+
+ /**
+ * Processes the output generated by an auxiliary include-scanning binary. Closes the stream upon
+ * completion.
+ *
+ * <p>If a source file has the following include statements:
+ * <pre>
+ * #include <string>
+ * #include "directory/header.h"
+ * </pre>
+ *
+ * <p>Then the output file has the following contents:
+ * <pre>
+ * "directory/header.h
+ * <string
+ * </pre>
+ * <p>Each line of the output is translated into an Inclusion object.
+ */
+ public static List<Inclusion> processIncludes(Object streamName, InputStream is)
+ throws IOException {
+ List<Inclusion> inclusions = new ArrayList<>();
+ InputStreamReader reader = new InputStreamReader(is, ISO_8859_1);
+ try {
+ for (String line : CharStreams.readLines(reader)) {
+ char qchar = line.charAt(0);
+ String name = line.substring(1);
+ Inclusion.Kind kind = KIND_MAP.get(qchar);
+ if (kind == null) {
+ throw new IOException("Illegal inclusion kind '" + qchar + "'");
+ }
+ inclusions.add(new Inclusion(name, kind));
+ }
+ } catch (IOException e) {
+ throw new IOException("Error reading include file " + streamName + ": " + e.getMessage());
+ } finally {
+ reader.close();
+ }
+ return inclusions;
+ }
+
+ @VisibleForTesting
+ Inclusion extractInclusion(String line) {
+ return extractInclusion(line.toCharArray(), 0, line.length());
+ }
+
+ /**
+ * Extracts a new, unresolved an Inclusion from a line of source.
+ *
+ * @param chars the char array containing the line chars to parse
+ * @param lineBegin the position of the first character in the line
+ * @param lineEnd the position of the character after the last
+ * @return the inclusion object if possible, null if none
+ */
+ private Inclusion extractInclusion(char[] chars, int lineBegin, int lineEnd) {
+ // expect WS#WS(include|include_next)WS("name"|<name>|junk)
+ int pos = expectIncludeKeyword(chars, lineBegin, lineEnd);
+ if (pos == -1 || pos == lineEnd) {
+ return null;
+ }
+ boolean isNext = false;
+ int npos = expect(chars, pos, lineEnd, "_next");
+ if (npos >= 0) {
+ isNext = true;
+ pos = npos;
+ }
+ if ((pos = skipWhitespace(chars, pos, lineEnd)) == lineEnd) {
+ return null;
+ }
+ if (chars[pos] == '"' || chars[pos] == '<') {
+ char qchar = chars[pos++];
+ int spos = pos;
+ pos = indexOf(chars, pos + 1, lineEnd, qchar == '<' ? '>' : '"');
+ if (pos < 0) {
+ return null;
+ }
+ if (chars[spos] == '/') {
+ return null; // disallow absolute paths
+ }
+ String name = new String(chars, spos, pos - spos);
+ if (name.contains("\n")) { // strip any \+NL pairs within name
+ name = BS_NL_PAT.matcher(name).replaceAll("");
+ }
+ if (isNext) {
+ return new Inclusion(name, qchar == '"' ? Kind.NEXT_QUOTE : Kind.NEXT_ANGLE);
+ } else {
+ return new Inclusion(name, qchar == '"' ? Kind.QUOTE : Kind.ANGLE);
+ }
+ } else {
+ return createOtherInclusion(new String(chars, pos, lineEnd - pos));
+ }
+ }
+
+ /**
+ * Extracts all inclusions from characters of a file.
+ *
+ * @param chars the file contents to parse & extract inclusions from
+ * @return a new set of inclusions, normalized to the cache
+ */
+ @VisibleForTesting
+ List<Inclusion> extractInclusions(char[] chars) {
+ List<Inclusion> inclusions = new ArrayList<>();
+ int lineBegin = 0; // the first char of each line
+ int end = chars.length; // the file end
+ while (lineBegin < end) {
+ int lineEnd = lineBegin; // the char after the last non-\n in each line
+ // skip to the next \n or after end of buffer, ignoring continuations
+ while (lineEnd < end) {
+ if (chars[lineEnd] == '\n') {
+ break;
+ } else if (chars[lineEnd] == '\\') {
+ lineEnd++;
+ if (chars[lineEnd] == '\n') {
+ lineEnd++;
+ }
+ } else {
+ lineEnd++;
+ }
+ }
+
+ // TODO(bazel-team) handle multiline block comments /* */ for the cases:
+ // /* blah blah blah
+ // lalala */ #include "foo.h"
+ // and:
+ // /* blah
+ // #include "foo.h"
+ // */
+
+ // extract the inclusion, and save only the kind we care about.
+ Inclusion inclusion = extractInclusion(chars, lineBegin, lineEnd);
+ if (inclusion != null) {
+ if (isValidInclusionKind(inclusion.kind)) {
+ inclusions.add(inclusion);
+ } else {
+ //System.err.println("Funky include " + inclusion + " in " + file);
+ }
+ }
+ lineBegin = lineEnd + 1; // next line starts after the previous line
+ }
+ return inclusions;
+ }
+
+ /**
+ * Extracts all inclusions from a given source file.
+ *
+ * @param file the file to parse & extract inclusions from
+ * @param greppedFile if non-null, this file has the already-grepped include lines of file.
+ * @param actionExecutionContext Services in the scope of the action, like the stream to which
+ * scanning messages are printed
+ * @return a new set of inclusions, normalized to the cache
+ */
+ public Collection<Inclusion> extractInclusions(Artifact file, @Nullable Path greppedFile,
+ ActionExecutionContext actionExecutionContext)
+ throws IOException, InterruptedException {
+ Collection<Inclusion> inclusions;
+ if (greppedFile != null) {
+ inclusions = processIncludes(greppedFile, greppedFile.getInputStream());
+ } else {
+ RemoteParseData remoteParseData = remoteExtractor.get() == null
+ ? null
+ : remoteExtractor.get().shouldParseRemotely(file.getPath());
+ if (remoteParseData != null && remoteParseData.shouldParseRemotely()) {
+ inclusions =
+ remoteExtractor.get().extractInclusions(file, actionExecutionContext,
+ remoteParseData);
+ } else {
+ inclusions = extractInclusions(FileSystemUtils.readContentAsLatin1(file.getPath()));
+ }
+ }
+ if (hints != null) {
+ inclusions.addAll(hints.getHintedInclusions(file));
+ }
+ return ImmutableList.copyOf(inclusions);
+ }
+
+ /**
+ * Parses include keyword in the provided char array and returns position
+ * immediately after include keyword or -1 if keyword was not found. Can be
+ * overridden by subclasses.
+ */
+ protected int expectIncludeKeyword(char[] chars, int position, int end) {
+ int pos = expect(chars, skipWhitespace(chars, position, end), end, "#");
+ if (pos > 0) {
+ int npos = skipWhitespace(chars, pos, end);
+ if ((pos = expect(chars, npos, end, "include")) > 0) {
+ return pos;
+ } else if ((pos = expect(chars, npos, end, "import")) > 0) {
+ if (expect(chars, pos, end, "_") == -1) { // Needed to avoid #import_next.
+ return pos;
+ }
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Returns true if we interested in the given inclusion kind. Can be
+ * overridden by the subclass.
+ */
+ protected boolean isValidInclusionKind(Kind kind) {
+ return kind != Kind.OTHER;
+ }
+
+ /**
+ * Returns inclusion object for non-standard inclusion cases or null if
+ * inclusion should be ignored.
+ */
+ protected Inclusion createOtherInclusion(String inclusionContent) {
+ return new Inclusion(inclusionContent, Kind.OTHER);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/IncludeProblems.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/IncludeProblems.java
new file mode 100644
index 0000000..f6be877
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/IncludeProblems.java
@@ -0,0 +1,51 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.actions.ActionExecutionException;
+import com.google.devtools.build.lib.actions.Artifact;
+
+/**
+ * Accumulator for problems encountered while reading or validating inclusion
+ * results.
+ */
+class IncludeProblems {
+
+ private StringBuilder message; // null when no problems
+
+ void add(String included) {
+ if (message == null) { message = new StringBuilder(); }
+ message.append("\n '" + included + "'");
+ }
+
+ boolean hasProblems() { return message != null; }
+
+ String getMessage(Action action, Artifact sourceFile) {
+ if (message != null) {
+ return "undeclared inclusion(s) in rule '" + action.getOwner().getLabel() + "':\n"
+ + "this rule is missing dependency declarations for the following files "
+ + "included by '" + sourceFile.prettyPrint() + "':"
+ + message;
+ }
+ return null;
+ }
+
+ void assertProblemFree(Action action, Artifact sourceFile) throws ActionExecutionException {
+ if (hasProblems()) {
+ throw new ActionExecutionException(getMessage(action, sourceFile), action, false);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/IncludeScannable.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/IncludeScannable.java
new file mode 100644
index 0000000..9c70090
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/IncludeScannable.java
@@ -0,0 +1,90 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * To be implemented by actions (such as C++ compilation steps) whose inputs
+ * can be scanned to discover other implicit inputs (such as C++ header files).
+ *
+ * <p>This is useful for remote execution strategies to be able to compute the
+ * complete set of files that must be distributed in order to execute such an action.
+ */
+public interface IncludeScannable {
+
+ /**
+ * Returns the built-in list of system include paths for the toolchain compiler. All paths in this
+ * list should be relative to the exec directory. They may be absolute if they are also installed
+ * on the remote build nodes or for local compilation.
+ */
+ List<PathFragment> getBuiltInIncludeDirectories();
+
+ /**
+ * Returns an immutable list of "-iquote" include paths that should be used by
+ * the IncludeScanner for this action. GCC searches these paths first, but
+ * only for {@code #include "foo"}, not for {@code #include <foo>}.
+ */
+ List<PathFragment> getQuoteIncludeDirs();
+
+ /**
+ * Returns an immutable list of "-I" include paths that should be used by the
+ * IncludeScanner for this action. GCC searches these paths ahead of the
+ * system include paths, but after "-iquote" include paths.
+ */
+ List<PathFragment> getIncludeDirs();
+
+ /**
+ * Returns an immutable list of "-isystem" include paths that should be used
+ * by the IncludeScanner for this action. GCC searches these paths ahead of
+ * the built-in system include paths, but after all other paths. "-isystem"
+ * paths are treated the same as normal system directories.
+ */
+ List<PathFragment> getSystemIncludeDirs();
+
+ /**
+ * Returns an immutable list of "-include" inclusions specified explicitly on
+ * the command line of this action. GCC will imagine that these files have
+ * been quote-included at the beginning of each source file.
+ */
+ List<String> getCmdlineIncludes();
+
+ /**
+ * Returns an immutable list of sources that the IncludeScanner should scan
+ * for this action.
+ */
+ Collection<Artifact> getIncludeScannerSources();
+
+ /**
+ * Returns additional scannables that need also be scanned when scanning this
+ * scannable. May be empty but not null. This is not evaluated recursively.
+ */
+ Iterable<IncludeScannable> getAuxiliaryScannables();
+
+ /**
+ * Returns a map of generated files:files grepped for headers which may be reached during include
+ * scanning. Generated files which are reached, but not in the key set, must be ignored.
+ *
+ * <p>If grepping of output files is not enabled via --extract_generated_inclusions, keys
+ * should just map to null.
+ */
+ Map<Artifact, Path> getLegalGeneratedScannerFileMap();
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/IncludeScanner.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/IncludeScanner.java
new file mode 100644
index 0000000..9c00efd
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/IncludeScanner.java
@@ -0,0 +1,177 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.devtools.build.lib.actions.ActionExecutionContext;
+import com.google.devtools.build.lib.actions.ActionExecutionException;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.EnvironmentalExecException;
+import com.google.devtools.build.lib.actions.ExecException;
+import com.google.devtools.build.lib.actions.Executor;
+import com.google.devtools.build.lib.actions.UserExecException;
+import com.google.devtools.build.lib.profiler.Profiler;
+import com.google.devtools.build.lib.profiler.ProfilerTask;
+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.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Scans source files to determine the bounding set of transitively referenced include files.
+ *
+ * <p>Note that include scanning is performance-critical code.
+ */
+public interface IncludeScanner {
+ /**
+ * Processes a source file and a list of includes extracted from command line
+ * flags. Adds all found files to the provided set {@code includes}. This
+ * method takes into account the path- and file-level hints that are part of
+ * this include scanner.
+ */
+ public void process(Artifact source, Map<Artifact, Path> legalOutputPaths,
+ List<String> cmdlineIncludes, Set<Artifact> includes,
+ ActionExecutionContext actionExecutionContext)
+ throws IOException, ExecException, InterruptedException;
+
+ /** Supplies IncludeScanners upon request. */
+ interface IncludeScannerSupplier {
+ /** Returns the possibly shared scanner to be used for a given pair of include paths. */
+ IncludeScanner scannerFor(List<Path> quoteIncludePaths, List<Path> includePaths);
+ }
+
+ /**
+ * Helper class that exists just to provide a static method that prepares the arguments with which
+ * to call an IncludeScanner.
+ */
+ class IncludeScanningPreparer {
+ private IncludeScanningPreparer() {}
+
+ /**
+ * Returns the files transitively included by the source files of the given IncludeScannable.
+ *
+ * @param action IncludeScannable whose sources' transitive includes will be returned.
+ * @param includeScannerSupplier supplies IncludeScanners to actually do the transitive
+ * scanning (and caching results) for a given source file.
+ * @param actionExecutionContext the context for {@code action}.
+ * @param profilerTaskName what the {@link Profiler} should record this call for.
+ */
+ public static Collection<Artifact> scanForIncludedInputs(IncludeScannable action,
+ IncludeScannerSupplier includeScannerSupplier,
+ ActionExecutionContext actionExecutionContext,
+ String profilerTaskName)
+ throws ExecException, InterruptedException, ActionExecutionException {
+
+ Set<Artifact> includes = Sets.newConcurrentHashSet();
+
+ Executor executor = actionExecutionContext.getExecutor();
+ Path execRoot = executor.getExecRoot();
+
+ final List<Path> absoluteBuiltInIncludeDirs = new ArrayList<>();
+
+ Profiler profiler = Profiler.instance();
+ try {
+ profiler.startTask(ProfilerTask.SCANNER, profilerTaskName);
+
+ // We need to scan the action itself, but also the auxiliary scannables
+ // (for LIPO). There is no need to call getAuxiliaryScannables
+ // recursively.
+ for (IncludeScannable scannable :
+ Iterables.concat(ImmutableList.of(action), action.getAuxiliaryScannables())) {
+
+ Map<Artifact, Path> legalOutputPaths = scannable.getLegalGeneratedScannerFileMap();
+ List<PathFragment> includeDirs = new ArrayList<>(scannable.getIncludeDirs());
+ List<PathFragment> quoteIncludeDirs = scannable.getQuoteIncludeDirs();
+ List<String> cmdlineIncludes = scannable.getCmdlineIncludes();
+
+ for (PathFragment pathFragment : scannable.getSystemIncludeDirs()) {
+ includeDirs.add(pathFragment);
+ }
+
+ // Add the system include paths to the list of include paths.
+ for (PathFragment pathFragment : action.getBuiltInIncludeDirectories()) {
+ if (pathFragment.isAbsolute()) {
+ absoluteBuiltInIncludeDirs.add(execRoot.getRelative(pathFragment));
+ }
+ includeDirs.add(pathFragment);
+ }
+
+ IncludeScanner scanner = includeScannerSupplier.scannerFor(
+ relativeTo(execRoot, quoteIncludeDirs),
+ relativeTo(execRoot, includeDirs));
+
+ for (Artifact source : scannable.getIncludeScannerSources()) {
+ // Add all include scanning entry points to the inputs; this is necessary
+ // when we have more than one source to scan from, for example when building
+ // C++ modules.
+ // In that case we have one of two cases:
+ // 1. We compile a header module - there, the .cppmap file is the main source file
+ // (which we do not include-scan, as that would require an extra parser), and
+ // thus already in the input; all headers in the .cppmap file are our entry points
+ // for include scanning, but are not yet in the inputs - they get added here.
+ // 2. We compile an object file that uses a header module; currently using a header
+ // module requires all headers it can reference to be available for the compilation.
+ // The header module can reference headers that are not in the transitive include
+ // closure of the current translation unit. Therefore, {@code CppCompileAction}
+ // adds all headers specified transitively for compiled header modules as include
+ // scanning entry points, and we need to add the entry points to the inputs here.
+ includes.add(source);
+ scanner.process(source, legalOutputPaths, cmdlineIncludes, includes,
+ actionExecutionContext);
+ }
+ }
+ } catch (IOException e) {
+ throw new EnvironmentalExecException(e.getMessage());
+ } finally {
+ profiler.completeTask(ProfilerTask.SCANNER);
+ }
+
+ // Collect inputs and output
+ List<Artifact> inputs = new ArrayList<>();
+ IncludeProblems includeProblems = new IncludeProblems();
+ for (Artifact included : includes) {
+ if (FileSystemUtils.startsWithAny(included.getPath(), absoluteBuiltInIncludeDirs)) {
+ // Skip include files found in absolute include directories. This currently only applies
+ // to grte.
+ continue;
+ }
+ if (included.getRoot().getPath().getParentDirectory() == null) {
+ throw new UserExecException(
+ "illegal absolute path to include file: " + included.getPath());
+ }
+ inputs.add(included);
+ }
+ return inputs;
+ }
+
+ private static List<Path> relativeTo(
+ Path path, Collection<PathFragment> fragments) {
+ List<Path> result = Lists.newArrayListWithCapacity(fragments.size());
+ for (PathFragment fragment : fragments) {
+ result.add(path.getRelative(fragment));
+ }
+ return result;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/IncludeScanningContext.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/IncludeScanningContext.java
new file mode 100644
index 0000000..69cd26b
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/IncludeScanningContext.java
@@ -0,0 +1,44 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import com.google.devtools.build.lib.actions.ActionExecutionContext;
+import com.google.devtools.build.lib.actions.ActionMetadata;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.ArtifactResolver;
+import com.google.devtools.build.lib.actions.Executor.ActionContext;
+
+import java.io.IOException;
+
+/**
+ * Context for actions that do include scanning.
+ */
+public interface IncludeScanningContext extends ActionContext {
+ /**
+ * Extracts the set of include files from a source file.
+ *
+ * @param actionExecutionContext the execution context
+ * @param resourceOwner the resource owner
+ * @param primaryInput the source file to be include scanned
+ * @param primaryOutput the output file where the results should be put
+ */
+ void extractIncludes(ActionExecutionContext actionExecutionContext,
+ ActionMetadata resourceOwner, Artifact primaryInput, Artifact primaryOutput)
+ throws IOException, InterruptedException;
+
+ /**
+ * Returns the artifact resolver.
+ */
+ ArtifactResolver getArtifactResolver();
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/Link.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/Link.java
new file mode 100644
index 0000000..26175eb
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/Link.java
@@ -0,0 +1,274 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import com.google.common.collect.AbstractIterator;
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.collect.CollectionUtils;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.rules.cpp.LinkerInputs.LibraryToLink;
+import com.google.devtools.build.lib.util.FileTypeSet;
+
+import java.util.Iterator;
+
+/**
+ * Utility types and methods for generating command lines for the linker, given
+ * a CppLinkAction or LinkConfiguration.
+ *
+ * <p>The linker commands, e.g. "ar", may not be functional, i.e.
+ * they may mutate the output file rather than overwriting it.
+ * To avoid this, we need to delete the output file before invoking the
+ * command. But that is not done by this class; deleting the output
+ * file is the responsibility of the classes derived from LinkStrategy.
+ */
+public abstract class Link {
+
+ private Link() {} // uninstantiable
+
+ /** The set of valid linker input files. */
+ public static final FileTypeSet VALID_LINKER_INPUTS = FileTypeSet.of(
+ CppFileTypes.ARCHIVE, CppFileTypes.PIC_ARCHIVE,
+ CppFileTypes.ALWAYS_LINK_LIBRARY, CppFileTypes.ALWAYS_LINK_PIC_LIBRARY,
+ CppFileTypes.OBJECT_FILE, CppFileTypes.PIC_OBJECT_FILE,
+ CppFileTypes.SHARED_LIBRARY, CppFileTypes.VERSIONED_SHARED_LIBRARY,
+ CppFileTypes.INTERFACE_SHARED_LIBRARY);
+
+ /**
+ * These file are supposed to be added using {@code addLibrary()} calls to {@link CppLinkAction}
+ * but will never be expanded to their constituent {@code .o} files. {@link CppLinkAction} checks
+ * that these files are never added as non-libraries.
+ */
+ public static final FileTypeSet SHARED_LIBRARY_FILETYPES = FileTypeSet.of(
+ CppFileTypes.SHARED_LIBRARY,
+ CppFileTypes.VERSIONED_SHARED_LIBRARY,
+ CppFileTypes.INTERFACE_SHARED_LIBRARY);
+
+ /**
+ * These need special handling when --thin_archive is true. {@link CppLinkAction} checks that
+ * these files are never added as non-libraries.
+ */
+ public static final FileTypeSet ARCHIVE_LIBRARY_FILETYPES = FileTypeSet.of(
+ CppFileTypes.ARCHIVE,
+ CppFileTypes.PIC_ARCHIVE,
+ CppFileTypes.ALWAYS_LINK_LIBRARY,
+ CppFileTypes.ALWAYS_LINK_PIC_LIBRARY);
+
+ public static final FileTypeSet ARCHIVE_FILETYPES = FileTypeSet.of(
+ CppFileTypes.ARCHIVE,
+ CppFileTypes.PIC_ARCHIVE);
+
+ public static final FileTypeSet LINK_LIBRARY_FILETYPES = FileTypeSet.of(
+ CppFileTypes.ALWAYS_LINK_LIBRARY,
+ CppFileTypes.ALWAYS_LINK_PIC_LIBRARY);
+
+
+ /** The set of object files */
+ public static final FileTypeSet OBJECT_FILETYPES = FileTypeSet.of(
+ CppFileTypes.OBJECT_FILE,
+ CppFileTypes.PIC_OBJECT_FILE);
+
+ /**
+ * Prefix that is prepended to command line entries that refer to the output
+ * of cc_fake_binary compile actions. This is a bad hack to signal to the code
+ * in {@code CppLinkAction#executeFake(Executor, FileOutErr)} that it needs
+ * special handling.
+ */
+ public static final String FAKE_OBJECT_PREFIX = "fake:";
+
+ /**
+ * Types of ELF files that can be created by the linker (.a, .so, .lo,
+ * executable).
+ */
+ public enum LinkTargetType {
+ /** A normal static archive. */
+ STATIC_LIBRARY(".a", true),
+
+ /** A static archive with .pic.o object files (compiled with -fPIC). */
+ PIC_STATIC_LIBRARY(".pic.a", true),
+
+ /** An interface dynamic library. */
+ INTERFACE_DYNAMIC_LIBRARY(".ifso", false),
+
+ /** A dynamic library. */
+ DYNAMIC_LIBRARY(".so", false),
+
+ /** A static archive without removal of unused object files. */
+ ALWAYS_LINK_STATIC_LIBRARY(".lo", true),
+
+ /** A PIC static archive without removal of unused object files. */
+ ALWAYS_LINK_PIC_STATIC_LIBRARY(".pic.lo", true),
+
+ /** An executable binary. */
+ EXECUTABLE("", false);
+
+ private final String extension;
+ private final boolean staticLibraryLink;
+
+ private LinkTargetType(String extension, boolean staticLibraryLink) {
+ this.extension = extension;
+ this.staticLibraryLink = staticLibraryLink;
+ }
+
+ public String getExtension() {
+ return extension;
+ }
+
+ public boolean isStaticLibraryLink() {
+ return staticLibraryLink;
+ }
+ }
+
+ /**
+ * The degree of "staticness" of symbol resolution during linking.
+ */
+ public enum LinkStaticness {
+ FULLY_STATIC, // Static binding of all symbols.
+ MOSTLY_STATIC, // Use dynamic binding only for symbols from glibc.
+ DYNAMIC, // Use dynamic binding wherever possible.
+ }
+
+ /**
+ * Types of archive.
+ */
+ public enum ArchiveType {
+ FAT, // Regular archive that includes its members.
+ THIN, // Thin archive that just points to its members.
+ START_END_LIB // A --start-lib ... --end-lib group in the command line.
+ }
+
+ static boolean useStartEndLib(LinkerInput linkerInput, ArchiveType archiveType) {
+ // TODO(bazel-team): Figure out if PicArchives are actually used. For it to be used, both
+ // linkingStatically and linkShared must me true, we must be in opt mode and cpu has to be k8.
+ return archiveType == ArchiveType.START_END_LIB
+ && ARCHIVE_FILETYPES.matches(linkerInput.getArtifact().getFilename())
+ && linkerInput.containsObjectFiles();
+ }
+
+ /**
+ * Replace always used archives with its members. This is used to build the linker cmd line.
+ */
+ public static Iterable<LinkerInput> mergeInputsCmdLine(NestedSet<LibraryToLink> inputs,
+ boolean globalNeedWholeArchive, ArchiveType archiveType) {
+ return new FilterMembersForLinkIterable(inputs, globalNeedWholeArchive, archiveType, false);
+ }
+
+ /**
+ * Add in any object files which are implicitly named as inputs by the linker.
+ */
+ public static Iterable<LinkerInput> mergeInputsDependencies(NestedSet<LibraryToLink> inputs,
+ boolean globalNeedWholeArchive, ArchiveType archiveType) {
+ return new FilterMembersForLinkIterable(inputs, globalNeedWholeArchive, archiveType, true);
+ }
+
+ /**
+ * On the fly implementation to filter the members.
+ */
+ private static final class FilterMembersForLinkIterable implements Iterable<LinkerInput> {
+ private final boolean globalNeedWholeArchive;
+ private final ArchiveType archiveType;
+ private final boolean deps;
+
+ private final Iterable<LibraryToLink> inputs;
+
+ private FilterMembersForLinkIterable(Iterable<LibraryToLink> inputs,
+ boolean globalNeedWholeArchive, ArchiveType archiveType, boolean deps) {
+ this.globalNeedWholeArchive = globalNeedWholeArchive;
+ this.archiveType = archiveType;
+ this.deps = deps;
+ this.inputs = CollectionUtils.makeImmutable(inputs);
+ }
+
+ @Override
+ public Iterator<LinkerInput> iterator() {
+ return new FilterMembersForLinkIterator(inputs.iterator(), globalNeedWholeArchive,
+ archiveType, deps);
+ }
+ }
+
+ /**
+ * On the fly implementation to filter the members.
+ */
+ private static final class FilterMembersForLinkIterator extends AbstractIterator<LinkerInput> {
+ private final boolean globalNeedWholeArchive;
+ private final ArchiveType archiveType;
+ private final boolean deps;
+
+ private final Iterator<LibraryToLink> inputs;
+ private Iterator<LinkerInput> delayList = ImmutableList.<LinkerInput>of().iterator();
+
+ private FilterMembersForLinkIterator(Iterator<LibraryToLink> inputs,
+ boolean globalNeedWholeArchive, ArchiveType archiveType, boolean deps) {
+ this.globalNeedWholeArchive = globalNeedWholeArchive;
+ this.archiveType = archiveType;
+ this.deps = deps;
+ this.inputs = inputs;
+ }
+
+ @Override
+ protected LinkerInput computeNext() {
+ if (delayList.hasNext()) {
+ return delayList.next();
+ }
+
+ while (inputs.hasNext()) {
+ LibraryToLink inputLibrary = inputs.next();
+ Artifact input = inputLibrary.getArtifact();
+ String name = input.getFilename();
+
+ // True if the linker might use the members of this file, i.e., if the file is a thin or
+ // start_end_lib archive (aka static library). Also check if the library contains object
+ // files - otherwise getObjectFiles returns null, which would lead to an NPE in
+ // simpleLinkerInputs.
+ boolean needMembersForLink = archiveType != ArchiveType.FAT
+ && ARCHIVE_LIBRARY_FILETYPES.matches(name) && inputLibrary.containsObjectFiles();
+
+ // True if we will pass the members instead of the original archive.
+ boolean passMembersToLinkCmd = needMembersForLink
+ && (globalNeedWholeArchive || LINK_LIBRARY_FILETYPES.matches(name));
+
+ // If deps is false (when computing the inputs to be passed on the command line), then it's
+ // an if-then-else, i.e., the passMembersToLinkCmd flag decides whether to pass the object
+ // files or the archive itself. This flag in turn is based on whether the archives are fat
+ // or not (thin archives or start_end_lib) - we never expand fat archives, but we do expand
+ // non-fat archives if we need whole-archives for the entire link, or for the specific
+ // library (i.e., if alwayslink=1).
+ //
+ // If deps is true (when computing the inputs to be passed to the action as inputs), then it
+ // becomes more complicated. We always need to pass the members for thin and start_end_lib
+ // archives (needMembersForLink). And we _also_ need to pass the archive file itself unless
+ // it's a start_end_lib archive (unless it's an alwayslink library).
+
+ // A note about ordering: the order in which the object files and the library are returned
+ // does not currently matter - this code results in the library returned first, and the
+ // object files returned after, but only if both are returned, which can only happen if
+ // deps is true, in which case this code only computes the list of inputs for the link
+ // action (so the order isn't critical).
+ if (passMembersToLinkCmd || (deps && needMembersForLink)) {
+ delayList = LinkerInputs.simpleLinkerInputs(inputLibrary.getObjectFiles()).iterator();
+ }
+
+ if (!(passMembersToLinkCmd || (deps && useStartEndLib(inputLibrary, archiveType)))) {
+ return inputLibrary;
+ }
+
+ if (delayList.hasNext()) {
+ return delayList.next();
+ }
+ }
+ return endOfData();
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/LinkCommandLine.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/LinkCommandLine.java
new file mode 100644
index 0000000..1dccafa
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/LinkCommandLine.java
@@ -0,0 +1,1121 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicates;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.devtools.build.lib.actions.ActionOwner;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.actions.CommandLine;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.collect.CollectionUtils;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.rules.cpp.Link.LinkStaticness;
+import com.google.devtools.build.lib.rules.cpp.Link.LinkTargetType;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.util.Pair;
+import com.google.devtools.build.lib.util.ShellEscaper;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.annotation.Nullable;
+
+/**
+ * Represents the command line of a linker invocation. It supports executables and dynamic
+ * libraries as well as static libraries.
+ */
+@Immutable
+public final class LinkCommandLine extends CommandLine {
+ private final BuildConfiguration configuration;
+ private final CppConfiguration cppConfiguration;
+ private final ActionOwner owner;
+ private final Artifact output;
+ @Nullable private final Artifact interfaceOutput;
+ @Nullable private final Artifact symbolCountsOutput;
+ private final ImmutableList<Artifact> buildInfoHeaderArtifacts;
+ private final Iterable<? extends LinkerInput> linkerInputs;
+ private final Iterable<? extends LinkerInput> runtimeInputs;
+ private final LinkTargetType linkTargetType;
+ private final LinkStaticness linkStaticness;
+ private final ImmutableList<String> linkopts;
+ private final ImmutableSet<String> features;
+ private final ImmutableMap<Artifact, Artifact> linkstamps;
+ private final ImmutableList<String> linkstampCompileOptions;
+ @Nullable private final PathFragment runtimeSolibDir;
+ private final boolean nativeDeps;
+ private final boolean useTestOnlyFlags;
+ private final boolean needWholeArchive;
+ private final boolean supportsParamFiles;
+ @Nullable private final Artifact interfaceSoBuilder;
+
+ private LinkCommandLine(
+ BuildConfiguration configuration,
+ ActionOwner owner,
+ Artifact output,
+ @Nullable Artifact interfaceOutput,
+ @Nullable Artifact symbolCountsOutput,
+ ImmutableList<Artifact> buildInfoHeaderArtifacts,
+ Iterable<? extends LinkerInput> linkerInputs,
+ Iterable<? extends LinkerInput> runtimeInputs,
+ LinkTargetType linkTargetType,
+ LinkStaticness linkStaticness,
+ ImmutableList<String> linkopts,
+ ImmutableSet<String> features,
+ ImmutableMap<Artifact, Artifact> linkstamps,
+ ImmutableList<String> linkstampCompileOptions,
+ @Nullable PathFragment runtimeSolibDir,
+ boolean nativeDeps,
+ boolean useTestOnlyFlags,
+ boolean needWholeArchive,
+ boolean supportsParamFiles,
+ Artifact interfaceSoBuilder) {
+ Preconditions.checkArgument(linkTargetType != LinkTargetType.INTERFACE_DYNAMIC_LIBRARY,
+ "you can't link an interface dynamic library directly");
+ if (linkTargetType != LinkTargetType.DYNAMIC_LIBRARY) {
+ Preconditions.checkArgument(interfaceOutput == null,
+ "interface output may only be non-null for dynamic library links");
+ }
+ if (linkTargetType.isStaticLibraryLink()) {
+ Preconditions.checkArgument(linkstamps.isEmpty(),
+ "linkstamps may only be present on dynamic library or executable links");
+ Preconditions.checkArgument(linkStaticness == LinkStaticness.FULLY_STATIC,
+ "static library link must be static");
+ Preconditions.checkArgument(buildInfoHeaderArtifacts.isEmpty(),
+ "build info headers may only be present on dynamic library or executable links");
+ Preconditions.checkArgument(symbolCountsOutput == null,
+ "the symbol counts output must be null for static links");
+ Preconditions.checkArgument(runtimeSolibDir == null,
+ "the runtime solib directory must be null for static links");
+ Preconditions.checkArgument(!nativeDeps,
+ "the native deps flag must be false for static links");
+ Preconditions.checkArgument(!needWholeArchive,
+ "the need whole archive flag must be false for static links");
+ }
+
+ this.configuration = Preconditions.checkNotNull(configuration);
+ this.cppConfiguration = configuration.getFragment(CppConfiguration.class);
+ this.owner = Preconditions.checkNotNull(owner);
+ this.output = Preconditions.checkNotNull(output);
+ this.interfaceOutput = interfaceOutput;
+ this.symbolCountsOutput = symbolCountsOutput;
+ this.buildInfoHeaderArtifacts = Preconditions.checkNotNull(buildInfoHeaderArtifacts);
+ this.linkerInputs = Preconditions.checkNotNull(linkerInputs);
+ this.runtimeInputs = Preconditions.checkNotNull(runtimeInputs);
+ this.linkTargetType = Preconditions.checkNotNull(linkTargetType);
+ this.linkStaticness = Preconditions.checkNotNull(linkStaticness);
+ // For now, silently ignore linkopts if this is a static library link.
+ this.linkopts = linkTargetType.isStaticLibraryLink()
+ ? ImmutableList.<String>of()
+ : Preconditions.checkNotNull(linkopts);
+ this.features = Preconditions.checkNotNull(features);
+ this.linkstamps = Preconditions.checkNotNull(linkstamps);
+ this.linkstampCompileOptions = linkstampCompileOptions;
+ this.runtimeSolibDir = runtimeSolibDir;
+ this.nativeDeps = nativeDeps;
+ this.useTestOnlyFlags = useTestOnlyFlags;
+ this.needWholeArchive = needWholeArchive;
+ this.supportsParamFiles = supportsParamFiles;
+ // For now, silently ignore interfaceSoBuilder if we don't build an interface dynamic library.
+ this.interfaceSoBuilder =
+ ((linkTargetType == LinkTargetType.DYNAMIC_LIBRARY) && (interfaceOutput != null))
+ ? Preconditions.checkNotNull(interfaceSoBuilder,
+ "cannot build interface dynamic library without builder")
+ : null;
+ }
+
+ /**
+ * Returns an interface shared object output artifact produced during linking. This only returns
+ * non-null if {@link #getLinkTargetType} is {@code DYNAMIC_LIBRARY} and an interface shared
+ * object was requested.
+ */
+ @Nullable public Artifact getInterfaceOutput() {
+ return interfaceOutput;
+ }
+
+ /**
+ * Returns an artifact containing the number of symbols used per object file passed to the linker.
+ * This is currently a gold only feature, and is only produced for executables. If another target
+ * is being linked, or if symbol counts output is disabled, this will be null.
+ */
+ @Nullable public Artifact getSymbolCountsOutput() {
+ return symbolCountsOutput;
+ }
+
+ /**
+ * Returns the (ordered, immutable) list of header files that contain build info.
+ */
+ public ImmutableList<Artifact> getBuildInfoHeaderArtifacts() {
+ return buildInfoHeaderArtifacts;
+ }
+
+ /**
+ * Returns the (ordered, immutable) list of paths to the linker's input files.
+ */
+ public Iterable<? extends LinkerInput> getLinkerInputs() {
+ return linkerInputs;
+ }
+
+ /**
+ * Returns the runtime inputs to the linker.
+ */
+ public Iterable<? extends LinkerInput> getRuntimeInputs() {
+ return runtimeInputs;
+ }
+
+ /**
+ * Returns the current type of link target set.
+ */
+ public LinkTargetType getLinkTargetType() {
+ return linkTargetType;
+ }
+
+ /**
+ * Returns the "staticness" of the link.
+ */
+ public LinkStaticness getLinkStaticness() {
+ return linkStaticness;
+ }
+
+ /**
+ * Returns the additional linker options for this link.
+ */
+ public ImmutableList<String> getLinkopts() {
+ return linkopts;
+ }
+
+ /**
+ * Returns a (possibly empty) mapping of (C++ source file, .o output file) pairs for source files
+ * that need to be compiled at link time.
+ *
+ * <p>This is used to embed various values from the build system into binaries to identify their
+ * provenance.
+ */
+ public ImmutableMap<Artifact, Artifact> getLinkstamps() {
+ return linkstamps;
+ }
+
+ /**
+ * Returns the location of the C++ runtime solib symlinks. If null, the C++ dynamic runtime
+ * libraries either do not exist (because they do not come from the depot) or they are in the
+ * regular solib directory.
+ */
+ @Nullable public PathFragment getRuntimeSolibDir() {
+ return runtimeSolibDir;
+ }
+
+ /**
+ * Returns true for libraries linked as native dependencies for other languages.
+ */
+ public boolean isNativeDeps() {
+ return nativeDeps;
+ }
+
+ /**
+ * Returns true if this link should use test-specific flags (e.g. $EXEC_ORIGIN as the root for
+ * finding shared libraries or lazy binding); false by default. See bug "Please use
+ * $EXEC_ORIGIN instead of $ORIGIN when linking cc_tests" for further context.
+ */
+ public boolean useTestOnlyFlags() {
+ return useTestOnlyFlags;
+ }
+
+ /**
+ * Splits the link command-line into a part to be written to a parameter file, and the remaining
+ * actual command line to be executed (which references the parameter file). Call {@link
+ * #canBeSplit} first to check if the command-line can be split.
+ *
+ * @throws IllegalStateException if the command-line cannot be split
+ */
+ @VisibleForTesting
+ final Pair<List<String>, List<String>> splitCommandline(PathFragment paramExecPath) {
+ Preconditions.checkState(canBeSplit());
+ List<String> args = getRawLinkArgv();
+ if (linkTargetType.isStaticLibraryLink()) {
+ // Ar link commands can also generate huge command lines.
+ List<String> paramFileArgs = args.subList(1, args.size());
+ List<String> commandlineArgs = new ArrayList<>();
+ commandlineArgs.add(args.get(0));
+
+ commandlineArgs.add("@" + paramExecPath.getPathString());
+ return Pair.of(commandlineArgs, paramFileArgs);
+ } else {
+ // Gcc link commands tend to generate humongous commandlines for some targets, which may
+ // not fit on some remote execution machines. To work around this we will employ the help of
+ // a parameter file and pass any linker options through it.
+ List<String> paramFileArgs = new ArrayList<>();
+ List<String> commandlineArgs = new ArrayList<>();
+ extractArgumentsForParamFile(args, commandlineArgs, paramFileArgs);
+
+ commandlineArgs.add("-Wl,@" + paramExecPath.getPathString());
+ return Pair.of(commandlineArgs, paramFileArgs);
+ }
+ }
+
+ boolean canBeSplit() {
+ if (!supportsParamFiles) {
+ return false;
+ }
+ switch (linkTargetType) {
+ // We currently can't split dynamic library links if they have interface outputs. That was
+ // probably an unintended side effect of the change that introduced interface outputs.
+ case DYNAMIC_LIBRARY:
+ return interfaceOutput == null;
+ case EXECUTABLE:
+ case STATIC_LIBRARY:
+ case PIC_STATIC_LIBRARY:
+ case ALWAYS_LINK_STATIC_LIBRARY:
+ case ALWAYS_LINK_PIC_STATIC_LIBRARY:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ private static void extractArgumentsForParamFile(List<String> args, List<String> commandlineArgs,
+ List<String> paramFileArgs) {
+ // Note, that it is not important that all linker arguments are extracted so that
+ // they can be moved into a parameter file, but the vast majority should.
+ commandlineArgs.add(args.get(0)); // gcc command, must not be moved!
+ int argsSize = args.size();
+ for (int i = 1; i < argsSize; i++) {
+ String arg = args.get(i);
+ if (arg.equals("-Wl,-no-whole-archive")) {
+ paramFileArgs.add("-no-whole-archive");
+ } else if (arg.equals("-Wl,-whole-archive")) {
+ paramFileArgs.add("-whole-archive");
+ } else if (arg.equals("-Wl,--start-group")) {
+ paramFileArgs.add("--start-group");
+ } else if (arg.equals("-Wl,--end-group")) {
+ paramFileArgs.add("--end-group");
+ } else if (arg.equals("-Wl,--start-lib")) {
+ paramFileArgs.add("--start-lib");
+ } else if (arg.equals("-Wl,--end-lib")) {
+ paramFileArgs.add("--end-lib");
+ } else if (arg.equals("--incremental-unchanged")) {
+ paramFileArgs.add(arg);
+ } else if (arg.equals("--incremental-changed")) {
+ paramFileArgs.add(arg);
+ } else if (arg.charAt(0) == '-') {
+ if (arg.startsWith("-l")) {
+ paramFileArgs.add(arg);
+ } else {
+ // Anything else starting with a '-' can stay on the commandline.
+ commandlineArgs.add(arg);
+ if (arg.equals("-o")) {
+ // Special case for '-o': add the following argument as well - it is the output file!
+ commandlineArgs.add(args.get(++i));
+ }
+ }
+ } else if (arg.endsWith(".a") || arg.endsWith(".lo") || arg.endsWith(".so")
+ || arg.endsWith(".ifso") || arg.endsWith(".o")
+ || CppFileTypes.VERSIONED_SHARED_LIBRARY.matches(arg)) {
+ // All objects of any kind go into the linker parameters.
+ paramFileArgs.add(arg);
+ } else {
+ // Everything that's left stays conservatively on the commandline.
+ commandlineArgs.add(arg);
+ }
+ }
+ }
+
+ /**
+ * Returns a raw link command for the given link invocation, including both command and
+ * arguments (argv). After any further usage-specific processing, this can be passed to
+ * {@link #finalizeWithLinkstampCommands} to give the final command line.
+ *
+ * @return raw link command line.
+ */
+ public List<String> getRawLinkArgv() {
+ List<String> argv = new ArrayList<>();
+ switch (linkTargetType) {
+ case EXECUTABLE:
+ addCppArgv(argv);
+ break;
+
+ case DYNAMIC_LIBRARY:
+ if (interfaceOutput != null) {
+ argv.add(configuration.getShExecutable().getPathString());
+ argv.add("-c");
+ argv.add("build_iface_so=\"$0\"; impl=\"$1\"; iface=\"$2\"; cmd=\"$3\"; shift 3; "
+ + "\"$cmd\" \"$@\" && \"$build_iface_so\" \"$impl\" \"$iface\"");
+ argv.add(interfaceSoBuilder.getExecPathString());
+ argv.add(output.getExecPathString());
+ argv.add(interfaceOutput.getExecPathString());
+ }
+ addCppArgv(argv);
+ // -pie is not compatible with -shared and should be
+ // removed when the latter is part of the link command. Should we need to further
+ // distinguish between shared libraries and executables, we could add additional
+ // command line / CROSSTOOL flags that distinguish them. But as long as this is
+ // the only relevant use case we're just special-casing it here.
+ Iterables.removeIf(argv, Predicates.equalTo("-pie"));
+ break;
+
+ case STATIC_LIBRARY:
+ case PIC_STATIC_LIBRARY:
+ case ALWAYS_LINK_STATIC_LIBRARY:
+ case ALWAYS_LINK_PIC_STATIC_LIBRARY:
+ // The static library link command follows this template:
+ // ar <cmd> <output_archive> <input_files...>
+ argv.add(cppConfiguration.getArExecutable().getPathString());
+ argv.addAll(
+ cppConfiguration.getArFlags(cppConfiguration.archiveType() == Link.ArchiveType.THIN));
+ argv.add(output.getExecPathString());
+ addInputFileLinkOptions(argv, /*needWholeArchive=*/false,
+ /*includeLinkopts=*/false);
+ break;
+
+ default:
+ throw new IllegalArgumentException();
+ }
+
+ // Fission mode: debug info is in .dwo files instead of .o files. Inform the linker of this.
+ if (!linkTargetType.isStaticLibraryLink() && cppConfiguration.useFission()) {
+ argv.add("-Wl,--gdb-index");
+ }
+
+ return argv;
+ }
+
+ @Override
+ public List<String> arguments() {
+ return finalizeWithLinkstampCommands(getRawLinkArgv());
+ }
+
+ /**
+ * Takes a raw link command line and gives the final link command that will
+ * also first compile any linkstamps necessary. Elements of rawLinkArgv are
+ * shell-escaped.
+ *
+ * @param rawLinkArgv raw link command line
+ *
+ * @return final link command line suitable for execution
+ */
+ public List<String> finalizeWithLinkstampCommands(List<String> rawLinkArgv) {
+ return addLinkstampingToCommand(getLinkstampCompileCommands(""), rawLinkArgv, true);
+ }
+
+ /**
+ * Takes a raw link command line and gives the final link command that will also first compile any
+ * linkstamps necessary. Elements of rawLinkArgv are not shell-escaped.
+ *
+ * @param rawLinkArgv raw link command line
+ * @param outputPrefix prefix to add before the linkstamp outputs' exec paths
+ *
+ * @return final link command line suitable for execution
+ */
+ public List<String> finalizeAlreadyEscapedWithLinkstampCommands(
+ List<String> rawLinkArgv, String outputPrefix) {
+ return addLinkstampingToCommand(getLinkstampCompileCommands(outputPrefix), rawLinkArgv, false);
+ }
+
+ /**
+ * Adds linkstamp compilation to the (otherwise) fully specified link
+ * command if {@link #getLinkstamps} is non-empty.
+ *
+ * <p>Linkstamps were historically compiled implicitly as part of the link
+ * command, but implicit compilation doesn't guarantee consistent outputs.
+ * For example, the command "gcc input.o input.o foo/linkstamp.cc -o myapp"
+ * causes gcc to implicitly run "gcc foo/linkstamp.cc -o /tmp/ccEtJHDB.o",
+ * for some internally decided output path /tmp/ccEtJHDB.o, then add that path
+ * to the linker's command line options. The name of this path can change
+ * even between equivalently specified gcc invocations.
+ *
+ * <p>So now we explicitly compile these files in their own command
+ * invocations before running the link command, thus giving us direct
+ * control over the naming of their outputs. This method adds those extra
+ * steps as necessary.
+ * @param linkstampCommands individual linkstamp compilation commands
+ * @param linkCommand the complete list of link command arguments (after
+ * .params file compacting) for an invocation
+ * @param escapeArgs if true, linkCommand arguments are shell escaped. if
+ * false, arguments are returned as-is
+ *
+ * @return The original argument list if no linkstamps compilation commands
+ * are given, otherwise an expanded list that adds the linkstamp
+ * compilation commands and funnels their outputs into the link step.
+ * Note that these outputs only need to persist for the duration of
+ * the link step.
+ */
+ private static List<String> addLinkstampingToCommand(
+ List<String> linkstampCommands,
+ List<String> linkCommand,
+ boolean escapeArgs) {
+ if (linkstampCommands.isEmpty()) {
+ return linkCommand;
+ } else {
+ List<String> batchCommand = Lists.newArrayListWithCapacity(3);
+ batchCommand.add("/bin/bash");
+ batchCommand.add("-c");
+ batchCommand.add(
+ Joiner.on(" && ").join(linkstampCommands) + " && "
+ + (escapeArgs
+ ? ShellEscaper.escapeJoinAll(linkCommand)
+ : Joiner.on(" ").join(linkCommand)));
+ return ImmutableList.copyOf(batchCommand);
+ }
+ }
+
+ /**
+ * Computes, for each C++ source file in
+ * {@link #getLinkstamps}, the command necessary to compile
+ * that file such that the output is correctly fed into the link command.
+ *
+ * <p>As these options (as well as all others) are taken into account when
+ * computing the action key, they do not directly contain volatile build
+ * information to avoid unnecessary relinking. Instead this information is
+ * passed as an additional header generated by
+ * {@link com.google.devtools.build.lib.rules.cpp.WriteBuildInfoHeaderAction}.
+ *
+ * @param outputPrefix prefix to add before the linkstamp outputs' exec paths
+ * @return a list of shell-escaped compiler commmands, one for each entry
+ * in {@link #getLinkstamps}
+ */
+ public List<String> getLinkstampCompileCommands(String outputPrefix) {
+ if (linkstamps.isEmpty()) {
+ return ImmutableList.of();
+ }
+
+ String compilerCommand = cppConfiguration.getCppExecutable().getPathString();
+ List<String> commands = Lists.newArrayListWithCapacity(linkstamps.size());
+
+ for (Map.Entry<Artifact, Artifact> linkstamp : linkstamps.entrySet()) {
+ List<String> optionList = new ArrayList<>();
+
+ // Defines related to the build info are read from generated headers.
+ for (Artifact header : buildInfoHeaderArtifacts) {
+ optionList.add("-include");
+ optionList.add(header.getExecPathString());
+ }
+
+ String labelReplacement = Matcher.quoteReplacement(
+ isSharedNativeLibrary() ? output.getExecPathString() : Label.print(owner.getLabel()));
+ String outputPathReplacement = Matcher.quoteReplacement(
+ output.getExecPathString());
+ for (String option : linkstampCompileOptions) {
+ optionList.add(option
+ .replaceAll(Pattern.quote("${LABEL}"), labelReplacement)
+ .replaceAll(Pattern.quote("${OUTPUT_PATH}"), outputPathReplacement));
+ }
+
+ optionList.add("-DGPLATFORM=\"" + cppConfiguration + "\"");
+
+ // Needed to find headers included from linkstamps.
+ optionList.add("-I.");
+
+ // Add sysroot.
+ PathFragment sysroot = cppConfiguration.getSysroot();
+ if (sysroot != null) {
+ optionList.add("--sysroot=" + sysroot.getPathString());
+ }
+
+ // Add toolchain compiler options.
+ optionList.addAll(cppConfiguration.getCompilerOptions(features));
+ optionList.addAll(cppConfiguration.getCOptions());
+ optionList.addAll(cppConfiguration.getUnfilteredCompilerOptions(features));
+
+ // For dynamic libraries, produce position independent code.
+ if (linkTargetType == LinkTargetType.DYNAMIC_LIBRARY
+ && cppConfiguration.toolchainNeedsPic()) {
+ optionList.add("-fPIC");
+ }
+
+ // Stamp FDO builds with FDO subtype string
+ String fdoBuildStamp = CppHelper.getFdoBuildStamp(cppConfiguration);
+ if (fdoBuildStamp != null) {
+ optionList.add("-D" + CppConfiguration.FDO_STAMP_MACRO + "=\"" + fdoBuildStamp + "\"");
+ }
+
+ // Add the compilation target.
+ optionList.add("-c");
+ optionList.add(linkstamp.getKey().getExecPathString());
+
+ // Assemble the final command, exempting outputPrefix from shell escaping.
+ commands.add(compilerCommand + " "
+ + ShellEscaper.escapeJoinAll(optionList)
+ + " -o "
+ + outputPrefix
+ + ShellEscaper.escapeString(linkstamp.getValue().getExecPathString()));
+ }
+
+ return commands;
+ }
+
+ /**
+ * Determine the arguments to pass to the C++ compiler when linking.
+ * Add them to the {@code argv} parameter.
+ */
+ private void addCppArgv(List<String> argv) {
+ argv.add(cppConfiguration.getCppExecutable().getPathString());
+
+ // When using gold to link an executable, output the number of used and unused symbols.
+ if (symbolCountsOutput != null) {
+ argv.add("-Wl,--print-symbol-counts=" + symbolCountsOutput.getExecPathString());
+ }
+
+ if (linkTargetType == LinkTargetType.DYNAMIC_LIBRARY) {
+ argv.add("-shared");
+ }
+
+ // Add the outputs of any associated linkstamp compilations.
+ for (Artifact linkstampOutput : linkstamps.values()) {
+ argv.add(linkstampOutput.getExecPathString());
+ }
+
+ boolean fullyStatic = (linkStaticness == LinkStaticness.FULLY_STATIC);
+ boolean mostlyStatic = (linkStaticness == LinkStaticness.MOSTLY_STATIC);
+ boolean sharedLinkopts =
+ linkTargetType == LinkTargetType.DYNAMIC_LIBRARY
+ || linkopts.contains("-shared")
+ || cppConfiguration.getLinkOptions().contains("-shared");
+
+ if (output != null) {
+ argv.add("-o");
+ String execpath = output.getExecPathString();
+ if (mostlyStatic
+ && linkTargetType == LinkTargetType.EXECUTABLE
+ && cppConfiguration.skipStaticOutputs()) {
+ // Linked binary goes to /dev/null; bogus dependency info in its place.
+ Collections.addAll(argv, "/dev/null", "-MMD", "-MF", execpath); // thanks Ambrose
+ } else {
+ argv.add(execpath);
+ }
+ }
+
+ addInputFileLinkOptions(argv, needWholeArchive, /*includeLinkopts=*/true);
+
+ // Extra toolchain link options based on the output's link staticness.
+ if (fullyStatic) {
+ argv.addAll(cppConfiguration.getFullyStaticLinkOptions(features, sharedLinkopts));
+ } else if (mostlyStatic) {
+ argv.addAll(cppConfiguration.getMostlyStaticLinkOptions(features, sharedLinkopts));
+ } else {
+ argv.addAll(cppConfiguration.getDynamicLinkOptions(features, sharedLinkopts));
+ }
+
+ // Extra test-specific link options.
+ if (useTestOnlyFlags) {
+ argv.addAll(cppConfiguration.getTestOnlyLinkOptions());
+ }
+
+ if (configuration.isCodeCoverageEnabled()) {
+ argv.add("-lgcov");
+ }
+
+ if (linkTargetType == LinkTargetType.EXECUTABLE && cppConfiguration.forcePic()) {
+ argv.add("-pie");
+ }
+
+ argv.addAll(cppConfiguration.getLinkOptions());
+ argv.addAll(cppConfiguration.getFdoSupport().getLinkOptions());
+ }
+
+ private static boolean isDynamicLibrary(LinkerInput linkInput) {
+ Artifact libraryArtifact = linkInput.getArtifact();
+ String name = libraryArtifact.getFilename();
+ return Link.SHARED_LIBRARY_FILETYPES.matches(name) && name.startsWith("lib");
+ }
+
+ private boolean isSharedNativeLibrary() {
+ return nativeDeps && cppConfiguration.shareNativeDeps();
+ }
+
+ /**
+ * When linking a shared library fully or mostly static then we need to link in
+ * *all* dependent files, not just what the shared library needs for its own
+ * code. This is done by wrapping all objects/libraries with
+ * -Wl,-whole-archive and -Wl,-no-whole-archive. For this case the
+ * globalNeedWholeArchive parameter must be set to true. Otherwise only
+ * library objects (.lo) need to be wrapped with -Wl,-whole-archive and
+ * -Wl,-no-whole-archive.
+ */
+ private void addInputFileLinkOptions(List<String> argv, boolean globalNeedWholeArchive,
+ boolean includeLinkopts) {
+ // The Apple ld doesn't support -whole-archive/-no-whole-archive. It
+ // does have -all_load/-noall_load, but -all_load is a global setting
+ // that affects all subsequent files, and -noall_load is simply ignored.
+ // TODO(bazel-team): Not sure what the implications of this are, other than
+ // bloated binaries.
+ boolean macosx = cppConfiguration.getTargetLibc().equals("macosx");
+ if (globalNeedWholeArchive) {
+ argv.add(macosx ? "-Wl,-all_load" : "-Wl,-whole-archive");
+ }
+
+ // Used to collect -L and -Wl,-rpath options, ensuring that each used only once.
+ Set<String> libOpts = new LinkedHashSet<>();
+
+ // List of command line parameters to link input files (either directly or using -l).
+ List<String> linkerInputs = new ArrayList<>();
+
+ // List of command line parameters that need to be placed *outside* of
+ // --whole-archive ... --no-whole-archive.
+ List<String> noWholeArchiveInputs = new ArrayList<>();
+
+ PathFragment solibDir = configuration.getBinDirectory().getExecPath()
+ .getRelative(cppConfiguration.getSolibDirectory());
+ String runtimeSolibName = runtimeSolibDir != null ? runtimeSolibDir.getBaseName() : null;
+ boolean runtimeRpath = runtimeSolibDir != null
+ && (linkTargetType == LinkTargetType.DYNAMIC_LIBRARY
+ || (linkTargetType == LinkTargetType.EXECUTABLE
+ && linkStaticness == LinkStaticness.DYNAMIC));
+
+ String rpathRoot = null;
+ List<String> runtimeRpathEntries = new ArrayList<>();
+
+ if (output != null) {
+ String origin =
+ useTestOnlyFlags && cppConfiguration.supportsExecOrigin() ? "$EXEC_ORIGIN/" : "$ORIGIN/";
+ if (runtimeRpath) {
+ runtimeRpathEntries.add("-Wl,-rpath," + origin + runtimeSolibName + "/");
+ }
+
+ // Calculate the correct relative value for the "-rpath" link option (which sets
+ // the search path for finding shared libraries).
+ if (isSharedNativeLibrary()) {
+ // For shared native libraries, special symlinking is applied to ensure C++
+ // runtimes are available under $ORIGIN/_solib_[arch]. So we set the RPATH to find
+ // them.
+ //
+ // Note that we have to do this because $ORIGIN points to different paths for
+ // different targets. In other words, blaze-bin/d1/d2/d3/a_shareddeps.so and
+ // blaze-bin/d4/b_shareddeps.so have different path depths. The first could
+ // reference a standard blaze-bin/_solib_[arch] via $ORIGIN/../../../_solib[arch],
+ // and the second could use $ORIGIN/../_solib_[arch]. But since this is a shared
+ // artifact, both are symlinks to the same place, so
+ // there's no *one* RPATH setting that fits all targets involved in the sharing.
+ rpathRoot = "-Wl,-rpath," + origin + ":"
+ + origin + cppConfiguration.getSolibDirectory() + "/";
+ if (runtimeRpath) {
+ runtimeRpathEntries.add("-Wl,-rpath," + origin + "../" + runtimeSolibName + "/");
+ }
+ } else {
+ // For all other links, calculate the relative path from the output file to _solib_[arch]
+ // (the directory where all shared libraries are stored, which resides under the blaze-bin
+ // directory. In other words, given blaze-bin/my/package/binary, rpathRoot would be
+ // "../../_solib_[arch]".
+ if (runtimeRpath) {
+ runtimeRpathEntries.add("-Wl,-rpath," + origin
+ + Strings.repeat("../", output.getRootRelativePath().segmentCount() - 1)
+ + runtimeSolibName + "/");
+ }
+
+ rpathRoot = "-Wl,-rpath,"
+ + origin + Strings.repeat("../", output.getRootRelativePath().segmentCount() - 1)
+ + cppConfiguration.getSolibDirectory() + "/";
+
+ if (nativeDeps) {
+ // We also retain the $ORIGIN/ path to solibs that are in _solib_<arch>, as opposed to
+ // the package directory)
+ if (runtimeRpath) {
+ runtimeRpathEntries.add("-Wl,-rpath," + origin + "../" + runtimeSolibName + "/");
+ }
+ rpathRoot += ":" + origin;
+ }
+ }
+ }
+
+ boolean includeSolibDir = false;
+
+ for (LinkerInput input : getLinkerInputs()) {
+ if (isDynamicLibrary(input)) {
+ PathFragment libDir = input.getArtifact().getExecPath().getParentDirectory();
+ Preconditions.checkState(
+ libDir.startsWith(solibDir),
+ "Artifact '%s' is not under directory '%s'.", input.getArtifact(), solibDir);
+ if (libDir.equals(solibDir)) {
+ includeSolibDir = true;
+ }
+ addDynamicInputLinkOptions(input, linkerInputs, libOpts, solibDir, rpathRoot);
+ } else {
+ addStaticInputLinkOptions(input, linkerInputs);
+ }
+ }
+
+ boolean includeRuntimeSolibDir = false;
+
+ for (LinkerInput input : runtimeInputs) {
+ List<String> optionsList = globalNeedWholeArchive
+ ? noWholeArchiveInputs
+ : linkerInputs;
+
+ if (isDynamicLibrary(input)) {
+ PathFragment libDir = input.getArtifact().getExecPath().getParentDirectory();
+ Preconditions.checkState(runtimeSolibDir != null && libDir.equals(runtimeSolibDir),
+ "Artifact '%s' is not under directory '%s'.", input.getArtifact(), solibDir);
+ includeRuntimeSolibDir = true;
+ addDynamicInputLinkOptions(input, optionsList, libOpts, solibDir, rpathRoot);
+ } else {
+ addStaticInputLinkOptions(input, optionsList);
+ }
+ }
+
+ // rpath ordering matters for performance; first add the one where most libraries are found.
+ if (includeSolibDir && rpathRoot != null) {
+ argv.add(rpathRoot);
+ }
+ if (includeRuntimeSolibDir) {
+ argv.addAll(runtimeRpathEntries);
+ }
+ argv.addAll(libOpts);
+
+ // Need to wrap static libraries with whole-archive option
+ for (String option : linkerInputs) {
+ if (!globalNeedWholeArchive && Link.LINK_LIBRARY_FILETYPES.matches(option)) {
+ argv.add(macosx ? "-Wl,-all_load" : "-Wl,-whole-archive");
+ argv.add(option);
+ argv.add(macosx ? "-Wl,-noall_load" : "-Wl,-no-whole-archive");
+ } else {
+ argv.add(option);
+ }
+ }
+
+ if (globalNeedWholeArchive) {
+ argv.add(macosx ? "-Wl,-noall_load" : "-Wl,-no-whole-archive");
+ argv.addAll(noWholeArchiveInputs);
+ }
+
+ if (includeLinkopts) {
+ /*
+ * For backwards compatibility, linkopts come _after_ inputFiles.
+ * This is needed to allow linkopts to contain libraries and
+ * positional library-related options such as
+ * -Wl,--begin-group -lfoo -lbar -Wl,--end-group
+ * or
+ * -Wl,--as-needed -lfoo -Wl,--no-as-needed
+ *
+ * As for the relative order of the three different flavours of linkopts
+ * (global defaults, per-target linkopts, and command-line linkopts),
+ * we have no idea what the right order should be, or if anyone cares.
+ */
+ argv.addAll(linkopts);
+ }
+ }
+
+ /**
+ * Adds command-line options for a dynamic library input file into
+ * options and libOpts.
+ */
+ private void addDynamicInputLinkOptions(LinkerInput input, List<String> options,
+ Set<String> libOpts, PathFragment solibDir, String rpathRoot) {
+ Preconditions.checkState(isDynamicLibrary(input));
+ Preconditions.checkState(
+ !Link.useStartEndLib(input, cppConfiguration.archiveType()));
+
+ Artifact inputArtifact = input.getArtifact();
+ PathFragment libDir = inputArtifact.getExecPath().getParentDirectory();
+ if (rpathRoot != null
+ && !libDir.equals(solibDir)
+ && (runtimeSolibDir == null || !runtimeSolibDir.equals(libDir))) {
+ String dotdots = "";
+ PathFragment commonParent = solibDir;
+ while (!libDir.startsWith(commonParent)) {
+ dotdots += "../";
+ commonParent = commonParent.getParentDirectory();
+ }
+
+ libOpts.add(rpathRoot + dotdots + libDir.relativeTo(commonParent).getPathString());
+ }
+
+ libOpts.add("-L" + inputArtifact.getExecPath().getParentDirectory().getPathString());
+
+ String name = inputArtifact.getFilename();
+ if (CppFileTypes.SHARED_LIBRARY.matches(name)) {
+ String libName = name.replaceAll("(^lib|\\.so$)", "");
+ options.add("-l" + libName);
+ } else {
+ // Interface shared objects have a non-standard extension
+ // that the linker won't be able to find. So use the
+ // filename directly rather than a -l option. Since the
+ // library has an SONAME attribute, this will work fine.
+ options.add(inputArtifact.getExecPathString());
+ }
+ }
+
+ /**
+ * Adds command-line options for a static library or non-library input
+ * into options.
+ */
+ private void addStaticInputLinkOptions(LinkerInput input, List<String> options) {
+ Preconditions.checkState(!isDynamicLibrary(input));
+
+ // start-lib/end-lib library: adds its input object files.
+ if (Link.useStartEndLib(input, cppConfiguration.archiveType())) {
+ Iterable<Artifact> archiveMembers = input.getObjectFiles();
+ if (!Iterables.isEmpty(archiveMembers)) {
+ options.add("-Wl,--start-lib");
+ for (Artifact member : archiveMembers) {
+ options.add(member.getExecPathString());
+ }
+ options.add("-Wl,--end-lib");
+ }
+ // For anything else, add the input directly.
+ } else {
+ Artifact inputArtifact = input.getArtifact();
+ if (input.isFake()) {
+ options.add(Link.FAKE_OBJECT_PREFIX + inputArtifact.getExecPathString());
+ } else {
+ options.add(inputArtifact.getExecPathString());
+ }
+ }
+ }
+
+ /**
+ * A builder for a {@link LinkCommandLine}.
+ */
+ public static final class Builder {
+ // TODO(bazel-team): Pass this in instead of having it here. Maybe move to cc_toolchain.
+ private static final ImmutableList<String> DEFAULT_LINKSTAMP_OPTIONS = ImmutableList.of(
+ // G3_VERSION_INFO and G3_TARGET_NAME are C string literals that normally
+ // contain the label of the target being linked. However, they are set
+ // differently when using shared native deps. In that case, a single .so file
+ // is shared by multiple targets, and its contents cannot depend on which
+ // target(s) were specified on the command line. So in that case we have
+ // to use the (obscure) name of the .so file instead, or more precisely
+ // the path of the .so file relative to the workspace root.
+ "-DG3_VERSION_INFO=\"${LABEL}\"",
+ "-DG3_TARGET_NAME=\"${LABEL}\"",
+
+ // G3_BUILD_TARGET is a C string literal containing the output of this
+ // link. (An undocumented and untested invariant is that G3_BUILD_TARGET is the location of
+ // the executable, either absolutely, or relative to the directory part of BUILD_INFO.)
+ "-DG3_BUILD_TARGET=\"${OUTPUT_PATH}\"");
+
+ private final BuildConfiguration configuration;
+ private final ActionOwner owner;
+
+ @Nullable private Artifact output;
+ @Nullable private Artifact interfaceOutput;
+ @Nullable private Artifact symbolCountsOutput;
+ private ImmutableList<Artifact> buildInfoHeaderArtifacts = ImmutableList.of();
+ private Iterable<? extends LinkerInput> linkerInputs = ImmutableList.of();
+ private Iterable<? extends LinkerInput> runtimeInputs = ImmutableList.of();
+ @Nullable private LinkTargetType linkTargetType;
+ private LinkStaticness linkStaticness = LinkStaticness.FULLY_STATIC;
+ private ImmutableList<String> linkopts = ImmutableList.of();
+ private ImmutableSet<String> features = ImmutableSet.of();
+ private ImmutableMap<Artifact, Artifact> linkstamps = ImmutableMap.of();
+ private List<String> linkstampCompileOptions = new ArrayList<>();
+ @Nullable private PathFragment runtimeSolibDir;
+ private boolean nativeDeps;
+ private boolean useTestOnlyFlags;
+ private boolean needWholeArchive;
+ private boolean supportsParamFiles;
+ @Nullable private Artifact interfaceSoBuilder;
+
+ public Builder(BuildConfiguration configuration, ActionOwner owner) {
+ this.configuration = configuration;
+ this.owner = owner;
+ }
+
+ public Builder(RuleContext ruleContext) {
+ this(ruleContext.getConfiguration(), ruleContext.getActionOwner());
+ }
+
+ public LinkCommandLine build() {
+ ImmutableList<String> actualLinkstampCompileOptions;
+ if (linkstampCompileOptions.isEmpty()) {
+ actualLinkstampCompileOptions = DEFAULT_LINKSTAMP_OPTIONS;
+ } else {
+ actualLinkstampCompileOptions = ImmutableList.copyOf(
+ Iterables.concat(DEFAULT_LINKSTAMP_OPTIONS, linkstampCompileOptions));
+ }
+ return new LinkCommandLine(configuration, owner, output, interfaceOutput,
+ symbolCountsOutput, buildInfoHeaderArtifacts, linkerInputs, runtimeInputs, linkTargetType,
+ linkStaticness, linkopts, features, linkstamps, actualLinkstampCompileOptions,
+ runtimeSolibDir, nativeDeps, useTestOnlyFlags, needWholeArchive, supportsParamFiles,
+ interfaceSoBuilder);
+ }
+
+ /**
+ * Sets the type of the link. It is an error to try to set this to {@link
+ * LinkTargetType#INTERFACE_DYNAMIC_LIBRARY}. Note that all the static target types (see {@link
+ * LinkTargetType#isStaticLibraryLink}) are equivalent, and there is no check that the output
+ * artifact matches the target type extension.
+ */
+ public Builder setLinkTargetType(LinkTargetType linkTargetType) {
+ Preconditions.checkArgument(linkTargetType != LinkTargetType.INTERFACE_DYNAMIC_LIBRARY);
+ this.linkTargetType = linkTargetType;
+ return this;
+ }
+
+ /**
+ * Sets the primary output artifact. This must be called before calling {@link #build}.
+ */
+ public Builder setOutput(Artifact output) {
+ this.output = output;
+ return this;
+ }
+
+ /**
+ * Sets a list of linker inputs. These get turned into linker options depending on the
+ * staticness and the target type. This call makes an immutable copy of the inputs, if the
+ * provided Iterable isn't already immutable (see {@link CollectionUtils#makeImmutable}).
+ */
+ public Builder setLinkerInputs(Iterable<LinkerInput> linkerInputs) {
+ this.linkerInputs = CollectionUtils.makeImmutable(linkerInputs);
+ return this;
+ }
+
+ public Builder setRuntimeInputs(ImmutableList<LinkerInput> runtimeInputs) {
+ this.runtimeInputs = runtimeInputs;
+ return this;
+ }
+
+ /**
+ * Sets the additional interface output artifact, which is only used for dynamic libraries. The
+ * {@link #build} method throws an exception if the target type is not {@link
+ * LinkTargetType#DYNAMIC_LIBRARY}.
+ */
+ public Builder setInterfaceOutput(Artifact interfaceOutput) {
+ this.interfaceOutput = interfaceOutput;
+ return this;
+ }
+
+ /**
+ * Sets an additional output artifact that contains symbol counts. The {@link #build} method
+ * throws an exception if this is non-null for a static link (see
+ * {@link LinkTargetType#isStaticLibraryLink}).
+ */
+ public Builder setSymbolCountsOutput(Artifact symbolCountsOutput) {
+ this.symbolCountsOutput = symbolCountsOutput;
+ return this;
+ }
+
+ /**
+ * Sets the linker options. These are passed to the linker in addition to the other linker
+ * options like linker inputs, symbol count options, etc. The {@link #build} method
+ * throws an exception if the linker options are non-empty for a static link (see {@link
+ * LinkTargetType#isStaticLibraryLink}).
+ */
+ public Builder setLinkopts(ImmutableList<String> linkopts) {
+ this.linkopts = linkopts;
+ return this;
+ }
+
+ /**
+ * Sets how static the link is supposed to be. For static target types (see {@link
+ * LinkTargetType#isStaticLibraryLink}), the {@link #build} method throws an exception if this
+ * is not {@link LinkStaticness#FULLY_STATIC}. The default setting is {@link
+ * LinkStaticness#FULLY_STATIC}.
+ */
+ public Builder setLinkStaticness(LinkStaticness linkStaticness) {
+ this.linkStaticness = linkStaticness;
+ return this;
+ }
+
+ /**
+ * Sets the binary that should be used to create the interface output for a dynamic library.
+ * This is ignored unless the target type is {@link LinkTargetType#DYNAMIC_LIBRARY} and an
+ * interface output artifact is specified.
+ */
+ public Builder setInterfaceSoBuilder(Artifact interfaceSoBuilder) {
+ this.interfaceSoBuilder = interfaceSoBuilder;
+ return this;
+ }
+
+ /**
+ * Sets the linkstamps. Linkstamps are additional C++ source files that are compiled as part of
+ * the link command. The {@link #build} method throws an exception if the linkstamps are
+ * non-empty for a static link (see {@link LinkTargetType#isStaticLibraryLink}).
+ */
+ public Builder setLinkstamps(ImmutableMap<Artifact, Artifact> linkstamps) {
+ this.linkstamps = linkstamps;
+ return this;
+ }
+
+ /**
+ * Adds the given C++ compiler options to the list of options passed to the linkstamp
+ * compilation.
+ */
+ public Builder addLinkstampCompileOptions(List<String> linkstampCompileOptions) {
+ this.linkstampCompileOptions.addAll(linkstampCompileOptions);
+ return this;
+ }
+
+ /**
+ * The build info header artifacts are generated header files that are used for link stamping.
+ * The {@link #build} method throws an exception if the build info header artifacts are
+ * non-empty for a static link (see {@link LinkTargetType#isStaticLibraryLink}).
+ */
+ public Builder setBuildInfoHeaderArtifacts(ImmutableList<Artifact> buildInfoHeaderArtifacts) {
+ this.buildInfoHeaderArtifacts = buildInfoHeaderArtifacts;
+ return this;
+ }
+
+ /**
+ * Sets the features enabled for the rule.
+ */
+ public Builder setFeatures(ImmutableSet<String> features) {
+ this.features = features;
+ return this;
+ }
+
+ /**
+ * Sets the directory of the dynamic runtime libraries, which is added to the rpath. The {@link
+ * #build} method throws an exception if the runtime dir is non-null for a static link (see
+ * {@link LinkTargetType#isStaticLibraryLink}).
+ */
+ public Builder setRuntimeSolibDir(PathFragment runtimeSolibDir) {
+ this.runtimeSolibDir = runtimeSolibDir;
+ return this;
+ }
+
+ /**
+ * Whether the resulting library is intended to be used as a native library from another
+ * programming language. This influences the rpath. The {@link #build} method throws an
+ * exception if this is true for a static link (see {@link LinkTargetType#isStaticLibraryLink}).
+ */
+ public Builder setNativeDeps(boolean nativeDeps) {
+ this.nativeDeps = nativeDeps;
+ return this;
+ }
+
+ /**
+ * Sets whether to use test-specific linker flags, e.g. {@code $EXEC_ORIGIN} instead of
+ * {@code $ORIGIN} in the rpath or lazy binding.
+ */
+ public Builder setUseTestOnlyFlags(boolean useTestOnlyFlags) {
+ this.useTestOnlyFlags = useTestOnlyFlags;
+ return this;
+ }
+
+ public Builder setNeedWholeArchive(boolean needWholeArchive) {
+ this.needWholeArchive = needWholeArchive;
+ return this;
+ }
+
+ public Builder setSupportsParamFiles(boolean supportsParamFiles) {
+ this.supportsParamFiles = supportsParamFiles;
+ return this;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/LinkStrategy.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/LinkStrategy.java
new file mode 100644
index 0000000..4f7673e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/LinkStrategy.java
@@ -0,0 +1,35 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+/**
+ * A strategy for executing {@link CppLinkAction}s.
+ *
+ * <p>The linker commands, e.g. "ar", are not necessary functional, i.e.
+ * they may mutate the output file rather than overwriting it.
+ * To avoid this, we need to delete the output file before invoking the
+ * command. That must be done by the classes that extend this class.
+ */
+public abstract class LinkStrategy implements CppLinkActionContext {
+ public LinkStrategy() {
+ }
+
+ /** The strategy name, preferably suitable for passing to --link_strategy. */
+ public abstract String linkStrategyName();
+
+ @Override
+ public String strategyLocality(CppLinkAction execOwner) {
+ return linkStrategyName();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/LinkerInput.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/LinkerInput.java
new file mode 100644
index 0000000..15a8b90
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/LinkerInput.java
@@ -0,0 +1,51 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import com.google.devtools.build.lib.actions.Artifact;
+
+/**
+ * Something that appears on the command line of the linker. Since we sometimes expand archive
+ * files to their constituent object files, we need to keep information whether a certain file
+ * contains embedded objects and if so, the list of the object files themselves.
+ */
+public interface LinkerInput {
+ /**
+ * Returns the artifact that is the input of the linker.
+ */
+ Artifact getArtifact();
+
+ /**
+ * Returns the original library to link. If this library is a solib symlink, returns the
+ * artifact the symlink points to, otherwise, the library itself.
+ */
+ Artifact getOriginalLibraryArtifact();
+
+ /**
+ * Whether the input artifact contains object files or is opaque.
+ */
+ boolean containsObjectFiles();
+
+ /**
+ * Returns whether the input artifact is a fake object file or not.
+ */
+ boolean isFake();
+
+ /**
+ * Return the list of object files included in the input artifact, if there are any. It is
+ * legal to call this only when {@link #containsObjectFiles()} returns true.
+ */
+ Iterable<Artifact> getObjectFiles();
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/LinkerInputs.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/LinkerInputs.java
new file mode 100644
index 0000000..24120ce
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/LinkerInputs.java
@@ -0,0 +1,353 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import com.google.common.base.Function;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.collect.CollectionUtils;
+import com.google.devtools.build.lib.concurrent.ThreadSafety;
+
+/**
+ * Factory for creating new {@link LinkerInput} objects.
+ */
+public abstract class LinkerInputs {
+ /**
+ * An opaque linker input that is not a library, for example a linker script or an individual
+ * object file.
+ */
+ @ThreadSafety.Immutable
+ public static class SimpleLinkerInput implements LinkerInput {
+ private final Artifact artifact;
+
+ public SimpleLinkerInput(Artifact artifact) {
+ this.artifact = Preconditions.checkNotNull(artifact);
+ }
+
+ @Override
+ public Artifact getArtifact() {
+ return artifact;
+ }
+
+ @Override
+ public Artifact getOriginalLibraryArtifact() {
+ return artifact;
+ }
+
+ @Override
+ public boolean containsObjectFiles() {
+ return false;
+ }
+
+ @Override
+ public boolean isFake() {
+ return false;
+ }
+
+ @Override
+ public Iterable<Artifact> getObjectFiles() {
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public boolean equals(Object that) {
+ if (this == that) {
+ return true;
+ }
+
+ if (!(that instanceof SimpleLinkerInput)) {
+ return false;
+ }
+
+ SimpleLinkerInput other = (SimpleLinkerInput) that;
+ return artifact.equals(other.artifact) && isFake() == other.isFake();
+ }
+
+ @Override
+ public int hashCode() {
+ return artifact.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return "SimpleLinkerInput(" + artifact.toString() + ")";
+ }
+ }
+
+ /**
+ * A linker input that is a fake object file generated by cc_fake_binary. The contained
+ * artifact must be an object file.
+ */
+ @ThreadSafety.Immutable
+ private static class FakeLinkerInput extends SimpleLinkerInput {
+ private FakeLinkerInput(Artifact artifact) {
+ super(artifact);
+ Preconditions.checkState(Link.OBJECT_FILETYPES.matches(artifact.getFilename()));
+ }
+
+ @Override
+ public boolean isFake() {
+ return true;
+ }
+ }
+
+ /**
+ * A library the user can link to. This is different from a simple linker input in that it also
+ * has a library identifier.
+ */
+ public interface LibraryToLink extends LinkerInput {
+ /**
+ * Returns whether the library is a solib symlink.
+ */
+ boolean isSolibSymlink();
+ }
+
+ /**
+ * This class represents a solib library symlink. Its library identifier is inherited from
+ * the library that it links to.
+ */
+ @ThreadSafety.Immutable
+ public static class SolibLibraryToLink implements LibraryToLink {
+ private final Artifact solibSymlinkArtifact;
+ private final Artifact libraryArtifact;
+
+ private SolibLibraryToLink(Artifact solibSymlinkArtifact, Artifact libraryArtifact) {
+ this.solibSymlinkArtifact = Preconditions.checkNotNull(solibSymlinkArtifact);
+ this.libraryArtifact = libraryArtifact;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("SolibLibraryToLink(%s -> %s",
+ solibSymlinkArtifact.toString(), libraryArtifact.toString());
+ }
+
+ @Override
+ public Artifact getArtifact() {
+ return solibSymlinkArtifact;
+ }
+
+ @Override
+ public boolean containsObjectFiles() {
+ return false;
+ }
+
+ @Override
+ public boolean isFake() {
+ return false;
+ }
+
+ @Override
+ public Iterable<Artifact> getObjectFiles() {
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public Artifact getOriginalLibraryArtifact() {
+ return libraryArtifact;
+ }
+
+ @Override
+ public boolean isSolibSymlink() {
+ return true;
+ }
+
+ @Override
+ public boolean equals(Object that) {
+ if (this == that) {
+ return true;
+ }
+
+ if (!(that instanceof SolibLibraryToLink)) {
+ return false;
+ }
+
+ SolibLibraryToLink thatSolib = (SolibLibraryToLink) that;
+ return
+ solibSymlinkArtifact.equals(thatSolib.solibSymlinkArtifact) &&
+ libraryArtifact.equals(thatSolib.libraryArtifact);
+ }
+
+ @Override
+ public int hashCode() {
+ return solibSymlinkArtifact.hashCode();
+ }
+ }
+
+ /**
+ * This class represents a library that may contain object files.
+ */
+ @ThreadSafety.Immutable
+ private static class CompoundLibraryToLink implements LibraryToLink {
+ private final Artifact libraryArtifact;
+ private final Iterable<Artifact> objectFiles;
+
+ private CompoundLibraryToLink(Artifact libraryArtifact, Iterable<Artifact> objectFiles) {
+ this.libraryArtifact = Preconditions.checkNotNull(libraryArtifact);
+ this.objectFiles = objectFiles == null ? null : CollectionUtils.makeImmutable(objectFiles);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("CompoundLibraryToLink(%s)", libraryArtifact.toString());
+ }
+
+ @Override
+ public Artifact getArtifact() {
+ return libraryArtifact;
+ }
+
+ @Override
+ public Artifact getOriginalLibraryArtifact() {
+ return libraryArtifact;
+ }
+
+ @Override
+ public boolean containsObjectFiles() {
+ return objectFiles != null;
+ }
+
+ @Override
+ public boolean isFake() {
+ return false;
+ }
+
+ @Override
+ public Iterable<Artifact> getObjectFiles() {
+ Preconditions.checkNotNull(objectFiles);
+ return objectFiles;
+ }
+
+ @Override
+ public boolean equals(Object that) {
+ if (this == that) {
+ return true;
+ }
+
+ if (!(that instanceof CompoundLibraryToLink)) {
+ return false;
+ }
+
+ return libraryArtifact.equals(((CompoundLibraryToLink) that).libraryArtifact);
+ }
+
+ @Override
+ public int hashCode() {
+ return libraryArtifact.hashCode();
+ }
+
+ @Override
+ public boolean isSolibSymlink() {
+ return false;
+ }
+ }
+
+ //////////////////////////////////////////////////////////////////////////////////////
+ // Public factory constructors:
+ //////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Creates linker input objects for non-library files.
+ */
+ public static Iterable<LinkerInput> simpleLinkerInputs(Iterable<Artifact> input) {
+ return Iterables.transform(input, new Function<Artifact, LinkerInput>() {
+ @Override
+ public LinkerInput apply(Artifact artifact) {
+ return simpleLinkerInput(artifact);
+ }
+ });
+ }
+
+ /**
+ * Creates a linker input for which we do not know what objects files it consists of.
+ */
+ public static LinkerInput simpleLinkerInput(Artifact artifact) {
+ // This precondition check was in place and *most* of the tests passed with them; the only
+ // exception is when you mention a generated .a file in the srcs of a cc_* rule.
+ // Preconditions.checkArgument(!ARCHIVE_LIBRARY_FILETYPES.contains(artifact.getFileType()));
+ return new SimpleLinkerInput(artifact);
+ }
+
+ /**
+ * Creates a fake linker input. The artifact must be an object file.
+ */
+ public static LinkerInput fakeLinkerInput(Artifact artifact) {
+ return new FakeLinkerInput(artifact);
+ }
+
+ /**
+ * Creates input libraries for which we do not know what objects files it consists of.
+ */
+ public static Iterable<LibraryToLink> opaqueLibrariesToLink(Iterable<Artifact> input) {
+ return Iterables.transform(input, new Function<Artifact, LibraryToLink>() {
+ @Override
+ public LibraryToLink apply(Artifact artifact) {
+ return opaqueLibraryToLink(artifact);
+ }
+ });
+ }
+
+ /**
+ * Creates a solib library symlink from the given artifact.
+ */
+ public static LibraryToLink solibLibraryToLink(Artifact solibSymlink, Artifact original) {
+ return new SolibLibraryToLink(solibSymlink, original);
+ }
+
+ /**
+ * Creates an input library for which we do not know what objects files it consists of.
+ */
+ public static LibraryToLink opaqueLibraryToLink(Artifact artifact) {
+ // This precondition check was in place and *most* of the tests passed with them; the only
+ // exception is when you mention a generated .a file in the srcs of a cc_* rule.
+ // It was very useful for proving that this actually works, though.
+ // Preconditions.checkArgument(
+ // !(artifact.getGeneratingAction() instanceof CppLinkAction) ||
+ // !Link.ARCHIVE_LIBRARY_FILETYPES.contains(artifact.getFileType()));
+ return new CompoundLibraryToLink(artifact, null);
+ }
+
+ /**
+ * Creates a library to link with the specified object files.
+ */
+ public static LibraryToLink newInputLibrary(Artifact library, Iterable<Artifact> objectFiles) {
+ return new CompoundLibraryToLink(library, objectFiles);
+ }
+
+ private static final Function<LibraryToLink, Artifact> LIBRARY_TO_NON_SOLIB =
+ new Function<LibraryToLink, Artifact>() {
+ @Override
+ public Artifact apply(LibraryToLink input) {
+ return input.getOriginalLibraryArtifact();
+ }
+ };
+
+ public static Iterable<Artifact> toNonSolibArtifacts(Iterable<LibraryToLink> libraries) {
+ return Iterables.transform(libraries, LIBRARY_TO_NON_SOLIB);
+ }
+
+ /**
+ * Returns the linker input artifacts from a collection of {@link LinkerInput} objects.
+ */
+ public static Iterable<Artifact> toLibraryArtifacts(Iterable<? extends LinkerInput> artifacts) {
+ return Iterables.transform(artifacts, new Function<LinkerInput, Artifact>() {
+ @Override
+ public Artifact apply(LinkerInput input) {
+ return input.getArtifact();
+ }
+ });
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/LinkingMode.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/LinkingMode.java
new file mode 100644
index 0000000..8018108
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/LinkingMode.java
@@ -0,0 +1,46 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+/**
+ * This class represents the different linking modes.
+ */
+public enum LinkingMode {
+
+ /**
+ * Everything is linked statically; e.g. {@code gcc -static x.o libfoo.a
+ * libbar.a -lm}. Specified by {@code -static} in linkopts.
+ */
+ FULLY_STATIC,
+
+ /**
+ * Link binaries statically except for system libraries
+ * e.g. {@code gcc x.o libfoo.a libbar.a -lm}. Specified by {@code linkstatic=1}.
+ *
+ * <p>This mode applies to executables.
+ */
+ MOSTLY_STATIC,
+
+ /**
+ * Same as MOSTLY_STATIC, but for shared libraries.
+ */
+ MOSTLY_STATIC_LIBRARIES,
+
+ /**
+ * All libraries are linked dynamically (if a dynamic version is available),
+ * e.g. {@code gcc x.o libfoo.so libbar.so -lm}. Specified by {@code
+ * linkstatic=0}.
+ */
+ DYNAMIC;
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/LipoContextProvider.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/LipoContextProvider.java
new file mode 100644
index 0000000..a9ffea8
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/LipoContextProvider.java
@@ -0,0 +1,58 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+
+import java.util.Map;
+
+/**
+ * Provides LIPO context information to the LIPO-enabled target configuration.
+ *
+ * <p>This is a rollup of the data collected in the LIPO context collector configuration.
+ * Each target in the LIPO context collector configuration has a {@link TransitiveLipoInfoProvider}
+ * which is used to transitively collect the data, then the {@code cc_binary} that is referred to
+ * in {@code --lipo_context} puts the collected data into {@link LipoContextProvider}, of which
+ * there is only one in any given build.
+ */
+@Immutable
+public final class LipoContextProvider implements TransitiveInfoProvider {
+
+ private final CppCompilationContext cppCompilationContext;
+
+ private final ImmutableMap<Artifact, IncludeScannable> includeScannables;
+ public LipoContextProvider(CppCompilationContext cppCompilationContext,
+ Map<Artifact, IncludeScannable> scannables) {
+ this.cppCompilationContext = cppCompilationContext;
+ this.includeScannables = ImmutableMap.copyOf(scannables);
+ }
+
+ /**
+ * Returns merged compilation context for the whole LIPO subtree.
+ */
+ public CppCompilationContext getLipoContext() {
+ return cppCompilationContext;
+ }
+
+ /**
+ * Returns the map from source artifact to the include scannable object representing
+ * the corresponding FDO source input file.
+ */
+ public ImmutableMap<Artifact, IncludeScannable> getIncludeScannables() {
+ return includeScannables;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/LocalGccStrategy.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/LocalGccStrategy.java
new file mode 100644
index 0000000..80ee23d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/LocalGccStrategy.java
@@ -0,0 +1,96 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.ActionExecutionContext;
+import com.google.devtools.build.lib.actions.ActionInput;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.BaseSpawn;
+import com.google.devtools.build.lib.actions.ExecException;
+import com.google.devtools.build.lib.actions.ExecutionStrategy;
+import com.google.devtools.build.lib.actions.ResourceSet;
+import com.google.devtools.common.options.OptionsClassProvider;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Run gcc locally by delegating to spawn.
+ */
+@ExecutionStrategy(name = { "local" },
+ contextType = CppCompileActionContext.class)
+public class LocalGccStrategy implements CppCompileActionContext {
+ private static final Reply CANNED_REPLY = new Reply() {
+ @Override
+ public byte[] getContents() {
+ throw new IllegalStateException("Remotely computed data requested for local action");
+ }
+ };
+
+ public LocalGccStrategy(OptionsClassProvider options) {
+ }
+
+ @Override
+ public String strategyLocality() {
+ return "local";
+ }
+
+ public static void updateEnv(CppCompileAction action, Map<String, String> env) {
+ // We cannot locally execute an action that does not expect to output a .d file, since we would
+ // have no way to tell what files that it included were used during compilation.
+ env.put("INTERCEPT_LOCALLY_EXECUTABLE", action.getDotdFile().artifact() == null ? "0" : "1");
+ }
+
+ @Override
+ public boolean needsIncludeScanning() {
+ return false;
+ }
+
+ @Override
+ public Collection<? extends ActionInput> findAdditionalInputs(CppCompileAction action,
+ ActionExecutionContext actionExecutionContext) throws ExecException, InterruptedException {
+ return ImmutableList.of();
+ }
+
+ @Override
+ public CppCompileActionContext.Reply execWithReply(
+ CppCompileAction action, ActionExecutionContext actionExecutionContext)
+ throws ExecException, InterruptedException {
+ Map<String, String> env = new HashMap<>();
+ env.putAll(action.getEnvironment());
+ updateEnv(action, env);
+ actionExecutionContext.getExecutor().getSpawnActionContext(action.getMnemonic())
+ .exec(new BaseSpawn.Local(action.getArgv(), env, action),
+ actionExecutionContext);
+ return null;
+ }
+
+ @Override
+ public ResourceSet estimateResourceConsumption(CppCompileAction action) {
+ return action.estimateResourceConsumptionLocal();
+ }
+
+ @Override
+ public Collection<Artifact> getScannedIncludeFiles(
+ CppCompileAction action, ActionExecutionContext actionExecutionContext) {
+ return ImmutableList.of();
+ }
+
+ @Override
+ public Reply getReplyFromException(ExecException e, CppCompileAction action) {
+ return CANNED_REPLY;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/LocalLinkStrategy.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/LocalLinkStrategy.java
new file mode 100644
index 0000000..3e7c863
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/LocalLinkStrategy.java
@@ -0,0 +1,62 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.actions.ActionExecutionContext;
+import com.google.devtools.build.lib.actions.ActionExecutionException;
+import com.google.devtools.build.lib.actions.BaseSpawn;
+import com.google.devtools.build.lib.actions.ExecException;
+import com.google.devtools.build.lib.actions.ExecutionStrategy;
+import com.google.devtools.build.lib.actions.Executor;
+import com.google.devtools.build.lib.actions.ResourceSet;
+
+import java.util.List;
+
+/**
+ * A link strategy that runs the linking step on the local host.
+ *
+ * <p>The set of input files necessary to successfully complete the link is the middleman-expanded
+ * set of the action's dependency inputs (which includes crosstool and libc dependencies, as
+ * defined by {@link com.google.devtools.build.lib.rules.cpp.CppHelper#getCrosstoolInputsForLink
+ * CppHelper.getCrosstoolInputsForLink}).
+ */
+@ExecutionStrategy(contextType = CppLinkActionContext.class, name = { "local" })
+public final class LocalLinkStrategy extends LinkStrategy {
+
+ public LocalLinkStrategy() {
+ }
+
+ @Override
+ public void exec(CppLinkAction action, ActionExecutionContext actionExecutionContext)
+ throws ExecException, ActionExecutionException, InterruptedException {
+ Executor executor = actionExecutionContext.getExecutor();
+ List<String> argv =
+ action.prepareCommandLine(executor.getExecRoot(), null);
+ executor.getSpawnActionContext(action.getMnemonic()).exec(
+ new BaseSpawn.Local(argv, ImmutableMap.<String, String>of(), action),
+ actionExecutionContext);
+ }
+
+ @Override
+ public String linkStrategyName() {
+ return "local";
+ }
+
+ @Override
+ public ResourceSet estimateResourceConsumption(CppLinkAction action) {
+ return action.estimateResourceConsumptionLocal();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/RemoteIncludeExtractor.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/RemoteIncludeExtractor.java
new file mode 100644
index 0000000..87a0712
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/RemoteIncludeExtractor.java
@@ -0,0 +1,52 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import com.google.devtools.build.lib.actions.ActionExecutionContext;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.Executor.ActionContext;
+import com.google.devtools.build.lib.rules.cpp.IncludeParser.Inclusion;
+import com.google.devtools.build.lib.vfs.Path;
+
+import java.io.IOException;
+import java.util.Collection;
+
+/** Parses a single file for its (direct) includes, possibly using a remote service. */
+public interface RemoteIncludeExtractor extends ActionContext {
+ /** Result of checking if this object should be used to parse a given file. */
+ interface RemoteParseData {
+ boolean shouldParseRemotely();
+ }
+
+ /**
+ * Returns whether to use this object to parse the given file for includes. The returned data
+ * should be passed to {@link #extractInclusions} to direct its behavior.
+ */
+ RemoteParseData shouldParseRemotely(Path file);
+
+ /**
+ * Extracts all inclusions from a given source file, possibly using a remote service.
+ *
+ * @param file the file from which to parse and extract inclusions.
+ * @param actionExecutionContext services in the scope of the action. Like the Err/Out stream
+ * outputs.
+ * @param remoteParseData the returned value of {@link #shouldParseRemotely}.
+ * @return a collection of inclusions, normalized to the cache
+ */
+ public Collection<Inclusion> extractInclusions(Artifact file,
+ ActionExecutionContext actionExecutionContext, RemoteParseData remoteParseData)
+ throws IOException, InterruptedException;
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/SolibSymlinkAction.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/SolibSymlinkAction.java
new file mode 100644
index 0000000..120ba86
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/SolibSymlinkAction.java
@@ -0,0 +1,234 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.AbstractAction;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.actions.ActionExecutionContext;
+import com.google.devtools.build.lib.actions.ActionExecutionException;
+import com.google.devtools.build.lib.actions.ActionOwner;
+import com.google.devtools.build.lib.actions.Actions;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.Executor;
+import com.google.devtools.build.lib.actions.ResourceSet;
+import com.google.devtools.build.lib.actions.Root;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.rules.cpp.LinkerInputs.LibraryToLink;
+import com.google.devtools.build.lib.util.Fingerprint;
+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;
+
+/**
+ * Creates mangled symlinks in the solib directory for all shared libraries.
+ * Libraries that have a potential to contain SONAME field rely on the mangled
+ * symlink to the parent directory instead.
+ *
+ * Such symlinks are used by the linker to ensure that all rpath entries can be
+ * specified relative to the $ORIGIN.
+ */
+public final class SolibSymlinkAction extends AbstractAction {
+
+ private final Artifact library;
+ private final Path target;
+ private final Artifact symlink;
+
+ private SolibSymlinkAction(ActionOwner owner, Artifact library, Artifact symlink) {
+ super(owner, ImmutableList.of(library), ImmutableList.of(symlink));
+
+ Preconditions.checkArgument(Link.SHARED_LIBRARY_FILETYPES.matches(library.getFilename()));
+ this.library = Preconditions.checkNotNull(library);
+ this.symlink = Preconditions.checkNotNull(symlink);
+ this.target = library.getPath();
+ }
+
+ @Override
+ protected void deleteOutputs(Path execRoot) throws IOException {
+ // Do not delete outputs if action does not intend to do anything.
+ if (target != null) {
+ super.deleteOutputs(execRoot);
+ }
+ }
+
+ @Override
+ public void execute(
+ ActionExecutionContext actionExecutionContext) throws ActionExecutionException {
+ Path mangledPath = symlink.getPath();
+ try {
+ FileSystemUtils.createDirectoryAndParents(mangledPath.getParentDirectory());
+ mangledPath.createSymbolicLink(target);
+ } catch (IOException e) {
+ throw new ActionExecutionException("failed to create _solib symbolic link '"
+ + symlink.prettyPrint() + "' to target '" + target + "'", e, this, false);
+ }
+ }
+
+ @Override
+ public Artifact getPrimaryInput() {
+ return library;
+ }
+
+ @Override
+ public Artifact getPrimaryOutput() {
+ return symlink;
+ }
+
+ @Override
+ public ResourceSet estimateResourceConsumption(Executor executor) {
+ return new ResourceSet(/*memoryMb=*/0, /*cpuUsage=*/0, /*ioUsage=*/0.0);
+ }
+
+ @Override
+ protected String computeKey() {
+ Fingerprint f = new Fingerprint();
+ f.addPath(symlink.getPath());
+ if (target != null) {
+ f.addPath(target);
+ }
+ return f.hexDigestAndReset();
+ }
+
+ @Override
+ public String getMnemonic() { return "SolibSymlink"; }
+
+ @Override
+ public String describeStrategy(Executor executor) {
+ return "local";
+ }
+
+ @Override
+ protected String getRawProgressMessage() { return null; }
+
+ /**
+ * Replaces shared library artifact with mangled symlink and creates related
+ * symlink action. For artifacts that should retain filename (e.g. libraries
+ * with SONAME tag), link is created to the parent directory instead.
+ *
+ * This action is performed to minimize number of -rpath entries used during
+ * linking process (by essentially "collecting" as many shared libraries as
+ * possible in the single directory), since we will be paying quadratic price
+ * for each additional entry on the -rpath.
+ *
+ * @param ruleContext rule context, that requested symlink.
+ * @param library Shared library artifact that needs to be mangled.
+ * @param preserveName whether to preserve the name of the library
+ * @param prefixConsumer whether to prefix the output artifact name with the label of the
+ * consumer
+ * @return mangled symlink artifact.
+ */
+ public static LibraryToLink getDynamicLibrarySymlink(final RuleContext ruleContext,
+ final Artifact library,
+ boolean preserveName,
+ boolean prefixConsumer,
+ BuildConfiguration configuration) {
+ PathFragment mangledName = getMangledName(
+ ruleContext, library.getRootRelativePath(), preserveName, prefixConsumer,
+ configuration.getFragment(CppConfiguration.class));
+ return getDynamicLibrarySymlinkInternal(ruleContext, library, mangledName, configuration);
+ }
+
+ /**
+ * Version of {@link #getDynamicLibrarySymlink} for the special case of C++ runtime libraries.
+ * These are handled differently than other libraries: neither their names nor directories are
+ * mangled, i.e. libstdc++.so.6 is symlinked from _solib_[arch]/libstdc++.so.6
+ */
+ public static LibraryToLink getCppRuntimeSymlink(RuleContext ruleContext, Artifact library,
+ String solibDirOverride, BuildConfiguration configuration) {
+ PathFragment solibDir = new PathFragment(solibDirOverride != null
+ ? solibDirOverride
+ : configuration.getFragment(CppConfiguration.class).getSolibDirectory());
+ PathFragment symlinkName = solibDir.getRelative(library.getRootRelativePath().getBaseName());
+ return getDynamicLibrarySymlinkInternal(ruleContext, library, symlinkName, configuration);
+ }
+
+ /**
+ * Internal implementation that takes a pre-determined symlink name; supports both the
+ * generic {@link #getDynamicLibrarySymlink} and the specialized {@link #getCppRuntimeSymlink}.
+ */
+ private static LibraryToLink getDynamicLibrarySymlinkInternal(RuleContext ruleContext,
+ Artifact library, PathFragment symlinkName, BuildConfiguration configuration) {
+ Preconditions.checkArgument(Link.SHARED_LIBRARY_FILETYPES.matches(library.getFilename()));
+ Preconditions.checkArgument(!library.getRootRelativePath().getSegment(0).startsWith("_solib_"));
+
+ // Ignore libraries that are already represented by the symlinks.
+ Root root = configuration.getBinDirectory();
+ Artifact symlink = ruleContext.getAnalysisEnvironment().getDerivedArtifact(symlinkName, root);
+ ruleContext.registerAction(
+ new SolibSymlinkAction(ruleContext.getActionOwner(), library, symlink));
+ return LinkerInputs.solibLibraryToLink(symlink, library);
+ }
+
+ /**
+ * Returns the name of the symlink that will be created for a library, given
+ * its name.
+ *
+ * @param ruleContext rule context that requests symlink
+ * @param libraryPath the root-relative path of the library
+ * @param preserveName true if filename should be preserved
+ * @param prefixConsumer true if the result should be prefixed with the label of the consumer
+ * @returns root relative path name
+ */
+ public static PathFragment getMangledName(RuleContext ruleContext,
+ PathFragment libraryPath,
+ boolean preserveName,
+ boolean prefixConsumer,
+ CppConfiguration cppConfiguration) {
+ String escapedRulePath = Actions.escapedPath(
+ "_" + ruleContext.getLabel());
+ String soname = getDynamicLibrarySoname(libraryPath, preserveName);
+ PathFragment solibDir = new PathFragment(cppConfiguration.getSolibDirectory());
+ if (preserveName) {
+ String escapedLibraryPath =
+ Actions.escapedPath("_" + libraryPath.getParentDirectory().getPathString());
+ PathFragment mangledDir = solibDir.getRelative(prefixConsumer
+ ? escapedRulePath + "__" + escapedLibraryPath
+ : escapedLibraryPath);
+ return mangledDir.getRelative(soname);
+ } else {
+ return solibDir.getRelative(prefixConsumer
+ ? escapedRulePath + "__" + soname
+ : soname);
+ }
+ }
+
+ /**
+ * Compute the SONAME to use for a dynamic library. This name is basically the
+ * name of the shared library in its final symlinked location.
+ *
+ * @param libraryPath name of the shared library that needs to be mangled
+ * @param preserveName true if filename should be preserved, false - mangled
+ * @return soname to embed in the dynamic library
+ */
+ public static String getDynamicLibrarySoname(PathFragment libraryPath,
+ boolean preserveName) {
+ String mangledName;
+ if (preserveName) {
+ mangledName = libraryPath.getBaseName();
+ } else {
+ mangledName = "lib" + Actions.escapedPath(libraryPath.getPathString());
+ }
+ return mangledName;
+ }
+
+ @Override
+ public boolean shouldReportPathPrefixConflict(Action action) {
+ return false; // Always ignore path prefix conflict for the SolibSymlinkAction.
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/TransitiveLipoInfoProvider.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/TransitiveLipoInfoProvider.java
new file mode 100644
index 0000000..4094124
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/TransitiveLipoInfoProvider.java
@@ -0,0 +1,51 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+
+/**
+ * A target that can contribute profiling information to LIPO C++ compilations.
+ *
+ * <p>This is used in the LIPO context collector tree to collect data from the transitive
+ * closure of the :lipo_context_collector target. It is eventually passed to the configured
+ * targets in the target configuration through {@link LipoContextProvider}.
+ */
+@Immutable
+public final class TransitiveLipoInfoProvider implements TransitiveInfoProvider {
+ public static final TransitiveLipoInfoProvider EMPTY =
+ new TransitiveLipoInfoProvider(
+ NestedSetBuilder.<IncludeScannable>emptySet(Order.STABLE_ORDER));
+
+ private final NestedSet<IncludeScannable> includeScannables;
+
+ public TransitiveLipoInfoProvider(NestedSet<IncludeScannable> includeScannables) {
+ this.includeScannables = includeScannables;
+ }
+
+ /**
+ * Returns the include scannables in the transitive closure.
+ *
+ * <p>This is used for constructing the path fragment -> include scannable map in the
+ * LIPO-enabled target configuration.
+ */
+ public NestedSet<IncludeScannable> getTransitiveIncludeScannables() {
+ return includeScannables;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/WriteBuildInfoHeaderAction.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/WriteBuildInfoHeaderAction.java
new file mode 100644
index 0000000..58b3330
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/WriteBuildInfoHeaderAction.java
@@ -0,0 +1,194 @@
+// Copyright 2014 Google Inc. 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.rules.cpp;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.Executor;
+import com.google.devtools.build.lib.analysis.BuildInfoHelper;
+import com.google.devtools.build.lib.analysis.WorkspaceStatusAction;
+import com.google.devtools.build.lib.analysis.actions.AbstractFileWriteAction;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.util.Fingerprint;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * An action that creates a C++ header containing the build information in the
+ * form of #define directives.
+ */
+public final class WriteBuildInfoHeaderAction extends AbstractFileWriteAction {
+ private static final String GUID = "b0798174-1352-4a54-854a-9785aaea491b";
+
+ private final ImmutableList<Artifact> valueArtifacts;
+
+ private final boolean writeVolatileInfo;
+ private final boolean writeStableInfo;
+
+ /**
+ * Creates an action that writes a C++ header with the build information.
+ *
+ * <p>It reads the set of build info keys from an action context that is usually contributed
+ * to Bazel by the workspace status module, and the value associated with said keys from the
+ * workspace status files (stable and volatile) written by the workspace status action.
+ *
+ * <p>Without input artifacts this action uses redacted build information.
+ * @param inputs Artifacts that contain build information, or an empty
+ * collection to use redacted build information
+ * @param output the C++ header Artifact created by this action
+ * @param writeVolatileInfo whether to write the volatile part of the build
+ * information to the generated header
+ * @param writeStableInfo whether to write the non-volatile part of the
+ * build information to the generated header
+ */
+ public WriteBuildInfoHeaderAction(Collection<Artifact> inputs,
+ Artifact output, boolean writeVolatileInfo, boolean writeStableInfo) {
+ super(BuildInfoHelper.BUILD_INFO_ACTION_OWNER,
+ inputs, output, /*makeExecutable=*/false);
+ valueArtifacts = ImmutableList.copyOf(inputs);
+ if (!inputs.isEmpty()) {
+ // With non-empty inputs we should not generate both volatile and non-volatile data
+ // in the same header file.
+ Preconditions.checkState(writeVolatileInfo ^ writeStableInfo);
+ }
+ Preconditions.checkState(
+ output.isConstantMetadata() == (writeVolatileInfo && !inputs.isEmpty()));
+
+ this.writeVolatileInfo = writeVolatileInfo;
+ this.writeStableInfo = writeStableInfo;
+ }
+
+ @Override
+ public DeterministicWriter newDeterministicWriter(EventHandler eventHandler, Executor executor)
+ throws IOException {
+ WorkspaceStatusAction.Context context =
+ executor.getContext(WorkspaceStatusAction.Context.class);
+
+ final Map<String, WorkspaceStatusAction.Key> keys = new LinkedHashMap<>();
+ if (writeVolatileInfo) {
+ keys.putAll(context.getVolatileKeys());
+ }
+
+ if (writeStableInfo) {
+ keys.putAll(context.getStableKeys());
+ }
+
+ final Map<String, String> values = new LinkedHashMap<>();
+ for (Artifact valueFile : valueArtifacts) {
+ values.putAll(WorkspaceStatusAction.parseValues(valueFile.getPath()));
+ }
+
+ final boolean redacted = valueArtifacts.isEmpty();
+
+ return new DeterministicWriter() {
+ @Override
+ public void writeOutputFile(OutputStream out) throws IOException {
+ Writer writer = new OutputStreamWriter(out, UTF_8);
+
+ for (Map.Entry<String, WorkspaceStatusAction.Key> key : keys.entrySet()) {
+ if (!key.getValue().isInLanguage("C++")) {
+ continue;
+ }
+
+ String value = redacted ? key.getValue().getRedactedValue()
+ : values.containsKey(key.getKey()) ? values.get(key.getKey())
+ : key.getValue().getDefaultValue();
+
+ switch (key.getValue().getType()) {
+ case VERBATIM:
+ case INTEGER:
+ break;
+
+ case STRING:
+ value = quote(value);
+ break;
+
+ default:
+ throw new IllegalStateException();
+ }
+ define(writer, key.getKey(), value);
+
+ }
+ writer.flush();
+ }
+ };
+ }
+
+ @Override
+ protected String computeKey() {
+ Fingerprint f = new Fingerprint();
+ f.addString(GUID);
+ f.addBoolean(writeStableInfo);
+ f.addBoolean(writeVolatileInfo);
+ return f.hexDigestAndReset();
+ }
+
+ @Override
+ public boolean executeUnconditionally() {
+ // Note: isVolatile must return true if executeUnconditionally can ever return true
+ // for this instance.
+ return isUnconditional();
+ }
+
+ @Override
+ public boolean isVolatile() {
+ return isUnconditional();
+ }
+
+ private boolean isUnconditional() {
+ // Because of special handling in the MetadataHandler, changed volatile build
+ // information does not trigger relinking of all libraries that have
+ // linkstamps. But we do want to regenerate the header in case libraries are
+ // relinked because of other reasons.
+ // Without inputs the contents of the header do not change, so there is no
+ // point in executing the action again in that case.
+ return writeVolatileInfo && !Iterables.isEmpty(getInputs());
+ }
+
+ /**
+ * Quote a string with double quotes.
+ */
+ private String quote(String string) {
+ // TODO(bazel-team): This is doesn't really work if the string contains quotes. Or a newline.
+ // Or a backslash. Or anything unusual, really.
+ return "\"" + string + "\"";
+ }
+
+ /**
+ * Write a preprocessor define directive to a Writer.
+ */
+ private void define(Writer writer, String name, String value) throws IOException {
+ writer.write("#define ");
+ writer.write(name);
+ writer.write(' ');
+ writer.write(value);
+ writer.write('\n');
+ }
+
+ @Override
+ protected String getRawProgressMessage() {
+ return null;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/extra/ActionListener.java b/src/main/java/com/google/devtools/build/lib/rules/extra/ActionListener.java
new file mode 100644
index 0000000..f3b302f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/extra/ActionListener.java
@@ -0,0 +1,85 @@
+// Copyright 2014 Google Inc. 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.rules.extra;
+
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Sets;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTargetBuilder;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.Runfiles;
+import com.google.devtools.build.lib.analysis.RunfilesProvider;
+import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
+import com.google.devtools.build.lib.collect.ImmutableSortedKeyListMultimap;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.rules.RuleConfiguredTargetFactory;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Implementation for the 'action_listener' rule.
+ */
+public final class ActionListener implements RuleConfiguredTargetFactory {
+ @Override
+ public ConfiguredTarget create(RuleContext ruleContext) {
+ // This rule doesn't produce any output when listed as a build target.
+ // Only when used via the --experimental_action_listener flag,
+ // this rule instructs the build system to add additional outputs.
+
+ List<ExtraActionSpec> extraActions;
+
+ Multimap<String, ExtraActionSpec> extraActionMap;
+
+ Set<String> mnemonics = Sets.newHashSet(
+ ruleContext.attributes().get("mnemonics", Type.STRING_LIST));
+ extraActions = retrieveAndValidateExtraActions(ruleContext);
+ ImmutableSortedKeyListMultimap.Builder<String, ExtraActionSpec>
+ extraActionMapBuilder = ImmutableSortedKeyListMultimap.builder();
+ for (String mnemonic : mnemonics) {
+ extraActionMapBuilder.putAll(mnemonic, extraActions);
+ }
+ extraActionMap = extraActionMapBuilder.build();
+ return new RuleConfiguredTargetBuilder(ruleContext)
+ .add(RunfilesProvider.class, RunfilesProvider.simple(Runfiles.EMPTY))
+ .add(ExtraActionMapProvider.class, new ExtraActionMapProvider(extraActionMap))
+ .build();
+ }
+
+ /**
+ * Loads the targets listed in the 'extra_actions' attribute of this rule.
+ * Validates these targets to be extra_actions indeed. And checks if the
+ * blaze version number is in the range of the blaze_version restrictions on the rule.
+ */
+ private List<ExtraActionSpec> retrieveAndValidateExtraActions(RuleContext ruleContext) {
+ List<ExtraActionSpec> extraActions = new ArrayList<>();
+ for (TransitiveInfoCollection prerequisite :
+ ruleContext.getPrerequisites("extra_actions", Mode.TARGET)) {
+ ExtraActionSpec spec = prerequisite.getProvider(ExtraActionSpec.class);
+ if (spec == null) {
+ ruleContext.attributeError("extra_actions", String.format("target %s is not an "
+ + "extra_action rule", prerequisite.getLabel().toString()));
+ } else {
+ extraActions.add(spec);
+ }
+ }
+ if (extraActions.size() == 0) {
+ ruleContext.attributeWarning("extra_actions",
+ "No extra_action is specified for this version of blaze.");
+ }
+ return extraActions;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/extra/ExtraAction.java b/src/main/java/com/google/devtools/build/lib/rules/extra/ExtraAction.java
new file mode 100644
index 0000000..2b53a1f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/extra/ExtraAction.java
@@ -0,0 +1,246 @@
+// Copyright 2014 Google Inc. 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.rules.extra;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.devtools.build.lib.actions.AbstractAction;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.actions.ActionExecutionContext;
+import com.google.devtools.build.lib.actions.ActionExecutionException;
+import com.google.devtools.build.lib.actions.ActionInput;
+import com.google.devtools.build.lib.actions.ActionOwner;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.ArtifactResolver;
+import com.google.devtools.build.lib.actions.DelegateSpawn;
+import com.google.devtools.build.lib.actions.ExecException;
+import com.google.devtools.build.lib.actions.Executor;
+import com.google.devtools.build.lib.actions.Spawn;
+import com.google.devtools.build.lib.actions.SpawnActionContext;
+import com.google.devtools.build.lib.actions.extra.ExtraActionInfo;
+import com.google.devtools.build.lib.analysis.actions.CommandLine;
+import com.google.devtools.build.lib.analysis.actions.SpawnAction;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Action used by extra_action rules to create an action that shadows an existing action. Runs a
+ * command-line using {@link SpawnActionContext} for executions.
+ */
+public final class ExtraAction extends SpawnAction {
+ private final Action shadowedAction;
+ private final boolean createDummyOutput;
+ private final Artifact extraActionInfoFile;
+ private final ImmutableMap<PathFragment, Artifact> runfilesManifests;
+ private final ImmutableSet<Artifact> extraActionInputs;
+ private boolean inputsKnown;
+
+ public ExtraAction(ActionOwner owner,
+ ImmutableSet<Artifact> extraActionInputs,
+ Map<PathFragment, Artifact> runfilesManifests,
+ Artifact extraActionInfoFile,
+ Collection<Artifact> outputs,
+ Action shadowedAction,
+ boolean createDummyOutput,
+ CommandLine argv,
+ Map<String, String> environment,
+ String progressMessage,
+ String mnemonic) {
+ super(owner,
+ createInputs(shadowedAction.getInputs(), extraActionInputs),
+ outputs,
+ AbstractAction.DEFAULT_RESOURCE_SET,
+ argv, environment, progressMessage, mnemonic);
+ this.extraActionInfoFile = extraActionInfoFile;
+ this.shadowedAction = shadowedAction;
+ this.runfilesManifests = ImmutableMap.copyOf(runfilesManifests);
+ this.createDummyOutput = createDummyOutput;
+
+ this.extraActionInputs = extraActionInputs;
+ inputsKnown = shadowedAction.inputsKnown();
+ if (createDummyOutput) {
+ // extra action file & dummy file
+ Preconditions.checkArgument(outputs.size() == 2);
+ }
+ }
+
+ @Override
+ public boolean discoversInputs() {
+ return shadowedAction.discoversInputs();
+ }
+
+ @Override
+ public void discoverInputs(ActionExecutionContext actionExecutionContext)
+ throws ActionExecutionException, InterruptedException {
+ Preconditions.checkState(discoversInputs(), this);
+ if (getContext(actionExecutionContext.getExecutor()).isRemotable(getMnemonic(),
+ isRemotable())) {
+ // If we're running remotely, we need to update our inputs to take account of any additional
+ // inputs the shadowed action may need to do its work.
+ if (shadowedAction.discoversInputs() && shadowedAction instanceof AbstractAction) {
+ updateInputs(
+ ((AbstractAction) shadowedAction).getInputFilesForExtraAction(actionExecutionContext));
+ }
+ }
+ }
+
+ @Override
+ public boolean inputsKnown() {
+ return inputsKnown;
+ }
+
+ private static NestedSet<Artifact> createInputs(
+ Iterable<Artifact> shadowedActionInputs, ImmutableSet<Artifact> extraActionInputs) {
+ NestedSetBuilder<Artifact> result = new NestedSetBuilder<>(Order.STABLE_ORDER);
+ if (shadowedActionInputs instanceof NestedSet) {
+ result.addTransitive((NestedSet<Artifact>) shadowedActionInputs);
+ } else {
+ result.addAll(shadowedActionInputs);
+ }
+ return result.addAll(extraActionInputs).build();
+ }
+
+ private void updateInputs(Iterable<Artifact> shadowedActionInputs) {
+ synchronized (this) {
+ setInputs(createInputs(shadowedActionInputs, extraActionInputs));
+ inputsKnown = true;
+ }
+ }
+
+ @Override
+ public void updateInputsFromCache(ArtifactResolver artifactResolver,
+ Collection<PathFragment> inputPaths) {
+ // We update the inputs directly from the shadowed action.
+ Set<PathFragment> extraActionPathFragments =
+ ImmutableSet.copyOf(Artifact.asPathFragments(extraActionInputs));
+ shadowedAction.updateInputsFromCache(artifactResolver,
+ Collections2.filter(inputPaths, Predicates.in(extraActionPathFragments)));
+ Preconditions.checkState(shadowedAction.inputsKnown(), "%s %s", this, shadowedAction);
+ updateInputs(shadowedAction.getInputs());
+ }
+
+ /**
+ * @InheritDoc
+ *
+ * This method calls in to {@link AbstractAction#getInputFilesForExtraAction} and
+ * {@link Action#getExtraActionInfo} of the action being shadowed from the thread executing this
+ * ExtraAction. It assumes these methods are safe to call from a different thread than the thread
+ * responsible for the execution of the action being shadowed.
+ */
+ @Override
+ public void execute(ActionExecutionContext actionExecutionContext)
+ throws ActionExecutionException, InterruptedException {
+ // PHASE 1: generate .xa file containing protocol buffer describing
+ // the action being shadowed
+
+ // We call the getExtraActionInfo command only at execution time
+ // so actions can store information only known at execution time into the
+ // protocol buffer.
+ ExtraActionInfo info = shadowedAction.getExtraActionInfo().build();
+ try (OutputStream out = extraActionInfoFile.getPath().getOutputStream()) {
+ info.writeTo(out);
+ } catch (IOException e) {
+ throw new ActionExecutionException(e.getMessage(), e, this, false);
+ }
+ Executor executor = actionExecutionContext.getExecutor();
+
+ // PHASE 2: execution of extra_action.
+
+ if (getContext(executor).isRemotable(getMnemonic(), isRemotable())) {
+ try {
+ getContext(executor).exec(getExtraActionSpawn(), actionExecutionContext);
+ } catch (ExecException e) {
+ throw e.toActionExecutionException(this);
+ }
+ } else {
+ super.execute(actionExecutionContext);
+ }
+
+ // PHASE 3: create dummy output.
+ // If the user didn't specify output, we need to create dummy output
+ // to make blaze schedule this action.
+ if (createDummyOutput) {
+ for (Artifact output : getOutputs()) {
+ try {
+ FileSystemUtils.touchFile(output.getPath());
+ } catch (IOException e) {
+ throw new ActionExecutionException(e.getMessage(), e, this, false);
+ }
+ }
+ }
+ synchronized (this) {
+ inputsKnown = true;
+ }
+ }
+
+ /**
+ * The spawn command for ExtraAction needs to be slightly modified from
+ * regular SpawnActions:
+ * -the extraActionInfo file needs to be added to the list of inputs.
+ * -the extraActionInfo file that is an output file of this task is created
+ * before the SpawnAction so should not be listed as one of its outputs.
+ */
+ // TODO(bazel-team): Add more tests that execute this code path!
+ private Spawn getExtraActionSpawn() {
+ final Spawn base = super.getSpawn();
+ return new DelegateSpawn(base) {
+ @Override public Iterable<? extends ActionInput> getInputFiles() {
+ return Iterables.concat(base.getInputFiles(), ImmutableSet.of(extraActionInfoFile));
+ }
+
+ @Override public List<? extends ActionInput> getOutputFiles() {
+ return Lists.newArrayList(
+ Iterables.filter(getOutputs(), new Predicate<Artifact>() {
+ @Override
+ public boolean apply(Artifact item) {
+ return item != extraActionInfoFile;
+ }
+ }));
+ }
+
+ @Override public ImmutableMap<PathFragment, Artifact> getRunfilesManifests() {
+ ImmutableMap.Builder<PathFragment, Artifact> builder = ImmutableMap.builder();
+ builder.putAll(super.getRunfilesManifests());
+ builder.putAll(runfilesManifests);
+ return builder.build();
+ }
+
+ @Override public String getMnemonic() { return ExtraAction.this.getMnemonic(); }
+ };
+ }
+
+ /**
+ * Returns the action this extra action is 'shadowing'.
+ */
+ public Action getShadowedAction() {
+ return shadowedAction;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/extra/ExtraActionFactory.java b/src/main/java/com/google/devtools/build/lib/rules/extra/ExtraActionFactory.java
new file mode 100644
index 0000000..8040ee0
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/extra/ExtraActionFactory.java
@@ -0,0 +1,91 @@
+// Copyright 2014 Google Inc. 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.rules.extra;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.CommandHelper;
+import com.google.devtools.build.lib.analysis.ConfigurationMakeVariableContext;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.FilesToRunProvider;
+import com.google.devtools.build.lib.analysis.MakeVariableExpander;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTargetBuilder;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.Runfiles;
+import com.google.devtools.build.lib.analysis.RunfilesProvider;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.rules.RuleConfiguredTargetFactory;
+import com.google.devtools.build.lib.syntax.Label;
+
+import java.util.List;
+
+/**
+ * Factory for 'extra_action'.
+ */
+public final class ExtraActionFactory implements RuleConfiguredTargetFactory {
+ @Override
+ public ConfiguredTarget create(RuleContext context) {
+ // This rule doesn't produce any output when listed as a build target.
+ // Only when used via the --experimental_action_listener flag,
+ // this rule instructs the build system to add additional outputs.
+ List<Artifact> resolvedData = Lists.newArrayList();
+
+ Iterable<FilesToRunProvider> tools =
+ context.getPrerequisites("tools", Mode.HOST, FilesToRunProvider.class);
+ CommandHelper commandHelper = new CommandHelper(
+ context, tools, ImmutableMap.<Label, Iterable<Artifact>>of());
+
+ resolvedData.addAll(context.getPrerequisiteArtifacts("data", Mode.DATA).list());
+ List<String>outputTemplates =
+ context.attributes().get("out_templates", Type.STRING_LIST);
+
+ String command = commandHelper.resolveCommandAndExpandLabels(false, true);
+ // This is a bit of a hack. We want to run the MakeVariableExpander first, so we expand $ on
+ // variables that are expanded below with $$, which gets reverted to $ by the
+ // MakeVariableExpander. This allows us to expand package-specific make variables in the
+ // package where the extra action is defined, and then later replace the owner-specific make
+ // variables when the extra action is instantiated.
+ command = command.replace("$(EXTRA_ACTION_FILE)", "$$(EXTRA_ACTION_FILE)");
+ command = command.replace("$(ACTION_ID)", "$$(ACTION_ID)");
+ command = command.replace("$(OWNER_LABEL_DIGEST)", "$$(OWNER_LABEL_DIGEST)");
+ command = command.replace("$(output ", "$$(output ");
+ try {
+ command = MakeVariableExpander.expand(
+ command, new ConfigurationMakeVariableContext(
+ context.getTarget().getPackage(), context.getConfiguration()));
+ } catch (MakeVariableExpander.ExpansionException e) {
+ context.ruleError(String.format("Unable to expand make variables: %s",
+ e.getMessage()));
+ }
+
+ boolean requiresActionOutput =
+ context.attributes().get("requires_action_output", Type.BOOLEAN);
+
+ ExtraActionSpec spec = new ExtraActionSpec(
+ commandHelper.getResolvedTools(),
+ commandHelper.getRemoteRunfileManifestMap(),
+ resolvedData,
+ outputTemplates,
+ command,
+ context.getLabel(),
+ requiresActionOutput);
+
+ return new RuleConfiguredTargetBuilder(context)
+ .addProvider(ExtraActionSpec.class, spec)
+ .add(RunfilesProvider.class, RunfilesProvider.simple(Runfiles.EMPTY))
+ .build();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/extra/ExtraActionMapProvider.java b/src/main/java/com/google/devtools/build/lib/rules/extra/ExtraActionMapProvider.java
new file mode 100644
index 0000000..ffeebe0
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/extra/ExtraActionMapProvider.java
@@ -0,0 +1,38 @@
+// Copyright 2014 Google Inc. 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.rules.extra;
+
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.Multimap;
+import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+
+/**
+ * Provides an action type -> set of extra actions to run map.
+ */
+@Immutable
+public final class ExtraActionMapProvider implements TransitiveInfoProvider {
+ private final ImmutableMultimap<String, ExtraActionSpec> extraActionMap;
+
+ public ExtraActionMapProvider(Multimap<String, ExtraActionSpec> extraActionMap) {
+ this.extraActionMap = ImmutableMultimap.copyOf(extraActionMap);
+ }
+
+ /**
+ * Returns the extra action map.
+ */
+ public ImmutableMultimap<String, ExtraActionSpec> getExtraActionMap() {
+ return extraActionMap;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/extra/ExtraActionSpec.java b/src/main/java/com/google/devtools/build/lib/rules/extra/ExtraActionSpec.java
new file mode 100644
index 0000000..40a063e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/extra/ExtraActionSpec.java
@@ -0,0 +1,220 @@
+// Copyright 2014 Google Inc. 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.rules.extra;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.actions.ActionOwner;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.CommandHelper;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
+import com.google.devtools.build.lib.analysis.actions.CommandLine;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.util.Fingerprint;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.Collection;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * The specification for a particular extra action type.
+ */
+@Immutable
+public final class ExtraActionSpec implements TransitiveInfoProvider {
+ private final ImmutableList<Artifact> resolvedTools;
+ private final ImmutableMap<PathFragment, Artifact> manifests;
+ private final ImmutableList<Artifact> resolvedData;
+ private final ImmutableList<String> outputTemplates;
+ private final String command;
+ private final boolean requiresActionOutput;
+ private final Label label;
+
+ ExtraActionSpec(
+ Iterable<Artifact> resolvedTools,
+ Map<PathFragment, Artifact> manifests,
+ Iterable<Artifact> resolvedData,
+ Iterable<String> outputTemplates,
+ String command,
+ Label label,
+ boolean requiresActionOutput) {
+ this.resolvedTools = ImmutableList.copyOf(resolvedTools);
+ this.manifests = ImmutableMap.copyOf(manifests);
+ this.resolvedData = ImmutableList.copyOf(resolvedData);
+ this.outputTemplates = ImmutableList.copyOf(outputTemplates);
+ this.command = command;
+ this.label = label;
+ this.requiresActionOutput = requiresActionOutput;
+ }
+
+ public Label getLabel() {
+ return label;
+ }
+
+ /**
+ * Adds an extra_action to the action graph based on the action to shadow.
+ */
+ public Collection<Artifact> addExtraAction(RuleContext owningRule,
+ Action actionToShadow) {
+ Collection<Artifact> extraActionOutputs = new LinkedHashSet<>();
+ ImmutableSet.Builder<Artifact> extraActionInputs = ImmutableSet.builder();
+
+ ActionOwner owner = actionToShadow.getOwner();
+ Label ownerLabel = owner.getLabel();
+ if (requiresActionOutput) {
+ extraActionInputs.addAll(actionToShadow.getOutputs());
+ }
+ extraActionInputs.addAll(resolvedTools);
+ extraActionInputs.addAll(resolvedData);
+
+ boolean createDummyOutput = false;
+
+ for (String outputTemplate : outputTemplates) {
+ // We create output for the extra_action based on the 'out_template' attribute.
+ // See {link #getExtraActionOutputArtifact} for supported variables.
+ extraActionOutputs.add(getExtraActionOutputArtifact(owningRule, actionToShadow,
+ owner, outputTemplate));
+ }
+ // extra_action has no output, we need to create some dummy output to keep the build up-to-date.
+ if (extraActionOutputs.size() == 0) {
+ createDummyOutput = true;
+ extraActionOutputs.add(getExtraActionOutputArtifact(owningRule, actionToShadow,
+ owner, "$(ACTION_ID).dummy"));
+ }
+
+ // We generate a file containing a protocol buffer describing the action that is being shadowed.
+ // It is up to each action being shadowed to decide what contents to store here.
+ Artifact extraActionInfoFile = getExtraActionOutputArtifact(owningRule, actionToShadow,
+ owner, "$(ACTION_ID).xa");
+ extraActionOutputs.add(extraActionInfoFile);
+
+ // Expand extra_action specific variables from the provided command-line.
+ // See {@link #createExpandedCommand} for list of supported variables.
+ String command = createExpandedCommand(owningRule, actionToShadow, owner, extraActionInfoFile);
+
+ Map<String, String> env = owningRule.getConfiguration().getDefaultShellEnvironment();
+
+ List<String> argv = CommandHelper.buildCommandLine(owningRule,
+ command, extraActionInputs, ".extra_action_script.sh");
+
+ String commandMessage = String.format("Executing extra_action %s on %s", label, ownerLabel);
+ owningRule.registerAction(new ExtraAction(
+ actionToShadow.getOwner(),
+ extraActionInputs.build(),
+ manifests,
+ extraActionInfoFile,
+ extraActionOutputs,
+ actionToShadow,
+ createDummyOutput,
+ CommandLine.of(argv, false),
+ env,
+ commandMessage,
+ label.getName()));
+
+ return extraActionOutputs;
+ }
+
+ /**
+ * Expand extra_action specific variables:
+ * $(EXTRA_ACTION_FILE): expands to a path of the file containing a protocol buffer
+ * describing the action being shadowed.
+ * $(output <out_template>): expands the output template to the execPath of the file.
+ * e.g. $(output $(ACTION_ID).out) ->
+ * <build_path>/extra_actions/bar/baz/devtools/build/test_A41234.out
+ */
+ private String createExpandedCommand(RuleContext owningRule,
+ Action action, ActionOwner owner, Artifact extraActionInfoFile) {
+ String realCommand = command.replace(
+ "$(EXTRA_ACTION_FILE)", extraActionInfoFile.getExecPathString());
+
+ for (String outputTemplate : outputTemplates) {
+ String outFile = getExtraActionOutputArtifact(owningRule, action, owner, outputTemplate)
+ .getExecPathString();
+ realCommand = realCommand.replace("$(output " + outputTemplate + ")", outFile);
+ }
+ return realCommand;
+ }
+
+ /**
+ * Creates an output artifact for the extra_action based on the output_template.
+ * The path will be in the following form:
+ * <output dir>/<target-configuration-specific-path>/extra_actions/<extra_action_label>/ +
+ * <configured_target_label>/<expanded_template>
+ *
+ * The template can use the following variables:
+ * $(ACTION_ID): a unique id for the extra_action.
+ *
+ * Sample:
+ * extra_action: foo/bar:extra
+ * template: $(ACTION_ID).analysis
+ * target: foo/bar:main
+ * expands to: output/configuration/extra_actions/\
+ * foo/bar/extra/foo/bar/4683026f7ac1dd1a873ccc8c3d764132.analysis
+ */
+ private Artifact getExtraActionOutputArtifact(RuleContext owningRule, Action action,
+ ActionOwner owner, String template) {
+ String actionId = getActionId(owner, action);
+
+ template = template.replace("$(ACTION_ID)", actionId);
+ template = template.replace("$(OWNER_LABEL_DIGEST)", getOwnerDigest(owner));
+
+ PathFragment rootRelativePath = getRootRelativePath(template, owner);
+ return owningRule.getAnalysisEnvironment().getDerivedArtifact(rootRelativePath,
+ owningRule.getConfiguration().getOutputDirectory());
+ }
+
+ private PathFragment getRootRelativePath(String template, ActionOwner owner) {
+ PathFragment extraActionPackageFragment = label.getPackageFragment();
+ PathFragment extraActionPrefix = extraActionPackageFragment.getRelative(label.getName());
+
+ PathFragment ownerFragment = owner.getLabel().getPackageFragment();
+ return new PathFragment("extra_actions").getRelative(extraActionPrefix)
+ .getRelative(ownerFragment).getRelative(template);
+ }
+
+ /**
+ * Calculates a digest representing the owner label. We use the digest instead of the
+ * original value as the original value might lead to a filename that is too long.
+ * By using a digest, tools can deterministically find all extra_action outputs for a given
+ * target, without having to open every file in the package.
+ */
+ private static String getOwnerDigest(ActionOwner owner) {
+ Fingerprint f = new Fingerprint();
+ f.addString(owner.getLabel().toString());
+ return f.hexDigestAndReset();
+ }
+
+ /**
+ * Creates a unique id for the action shadowed by this extra_action.
+ *
+ * We need to have a unique id for the extra_action to use. We build this
+ * from the owner's label and the shadowed action id (which is only
+ * guaranteed to be unique per target). Together with the subfolder
+ * matching the original target's package name, we believe this is enough
+ * of a uniqueness guarantee.
+ */
+ @VisibleForTesting
+ public static String getActionId(ActionOwner owner, Action action) {
+ Fingerprint f = new Fingerprint();
+ f.addString(owner.getLabel().toString());
+ f.addString(action.getKey());
+ return f.hexDigestAndReset();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/filegroup/Filegroup.java b/src/main/java/com/google/devtools/build/lib/rules/filegroup/Filegroup.java
new file mode 100644
index 0000000..cb297d8
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/filegroup/Filegroup.java
@@ -0,0 +1,103 @@
+// Copyright 2014 Google Inc. 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.rules.filegroup;
+
+import com.google.devtools.build.lib.actions.Actions;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.CompilationHelper;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.MiddlemanProvider;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTargetBuilder;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.Runfiles;
+import com.google.devtools.build.lib.analysis.RunfilesProvider;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.rules.RuleConfiguredTargetFactory;
+import com.google.devtools.build.lib.rules.test.InstrumentedFilesCollector;
+import com.google.devtools.build.lib.rules.test.InstrumentedFilesCollector.InstrumentationSpec;
+import com.google.devtools.build.lib.rules.test.InstrumentedFilesProvider;
+import com.google.devtools.build.lib.rules.test.InstrumentedFilesProviderImpl;
+import com.google.devtools.build.lib.util.FileTypeSet;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.Iterator;
+
+/**
+ * ConfiguredTarget for "filegroup".
+ */
+public class Filegroup implements RuleConfiguredTargetFactory {
+
+ @Override
+ public ConfiguredTarget create(RuleContext ruleContext) {
+ NestedSet<Artifact> filesToBuild = NestedSetBuilder.wrap(Order.STABLE_ORDER,
+ ruleContext.getPrerequisiteArtifacts("srcs", Mode.TARGET).list());
+ NestedSet<Artifact> middleman = CompilationHelper.getAggregatingMiddleman(
+ ruleContext, Actions.escapeLabel(ruleContext.getLabel()), filesToBuild);
+
+ InstrumentedFilesCollector instrumentedFilesCollector =
+ new InstrumentedFilesCollector(ruleContext,
+ // what do *we* know about whether this is a source file or not
+ new InstrumentationSpec(FileTypeSet.ANY_FILE, "srcs", "deps", "data"),
+ InstrumentedFilesCollector.NO_METADATA_COLLECTOR, filesToBuild);
+
+ RunfilesProvider runfilesProvider = RunfilesProvider.withData(
+ new Runfiles.Builder()
+ .addRunfiles(ruleContext, RunfilesProvider.DEFAULT_RUNFILES)
+ .build(),
+ // If you're visiting a filegroup as data, then we also visit its data as data.
+ new Runfiles.Builder().addTransitiveArtifacts(filesToBuild)
+ .addDataDeps(ruleContext).build());
+
+ return new RuleConfiguredTargetBuilder(ruleContext)
+ .add(RunfilesProvider.class, runfilesProvider)
+ .setFilesToBuild(filesToBuild)
+ .setRunfilesSupport(null, getExecutable(filesToBuild))
+ .add(InstrumentedFilesProvider.class, new InstrumentedFilesProviderImpl(
+ instrumentedFilesCollector))
+ .add(MiddlemanProvider.class, new MiddlemanProvider(middleman))
+ .add(FilegroupPathProvider.class,
+ new FilegroupPathProvider(getFilegroupPath(ruleContext)))
+ .build();
+ }
+
+ /*
+ * Returns the single executable output of this filegroup. Returns
+ * {@code null} if there are multiple outputs or the single output is not
+ * considered an executable.
+ */
+ private Artifact getExecutable(NestedSet<Artifact> filesToBuild) {
+ Iterator<Artifact> it = filesToBuild.iterator();
+ if (it.hasNext()) {
+ Artifact out = it.next();
+ if (!it.hasNext()) {
+ return out;
+ }
+ }
+ return null;
+ }
+
+ private PathFragment getFilegroupPath(RuleContext ruleContext) {
+ String attr = ruleContext.attributes().get("path", Type.STRING);
+ if (attr.isEmpty()) {
+ return PathFragment.EMPTY_FRAGMENT;
+ } else {
+ return ruleContext.getLabel().getPackageFragment().getRelative(attr);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/filegroup/FilegroupPathProvider.java b/src/main/java/com/google/devtools/build/lib/rules/filegroup/FilegroupPathProvider.java
new file mode 100644
index 0000000..370be07
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/filegroup/FilegroupPathProvider.java
@@ -0,0 +1,38 @@
+// Copyright 2014 Google Inc. 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.rules.filegroup;
+
+import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+/**
+ * A transitive info provider for dependent targets to query {@code path} attributes.
+ */
+@Immutable
+public final class FilegroupPathProvider implements TransitiveInfoProvider {
+ private final PathFragment pathFragment;
+
+ public FilegroupPathProvider(PathFragment pathFragment) {
+ this.pathFragment = pathFragment;
+ }
+
+ /**
+ * Returns the value of the {@code path} attribute or the empty fragment if it is not present.
+ */
+ public PathFragment getFilegroupPath() {
+ return pathFragment;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/fileset/FilesetActionContext.java b/src/main/java/com/google/devtools/build/lib/rules/fileset/FilesetActionContext.java
new file mode 100644
index 0000000..056b61e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/fileset/FilesetActionContext.java
@@ -0,0 +1,34 @@
+// Copyright 2014 Google Inc. 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.rules.fileset;
+
+import com.google.devtools.build.lib.actions.Executor.ActionContext;
+
+import java.util.concurrent.ThreadPoolExecutor;
+
+/**
+ * Action context for fileset collection actions.
+ */
+public interface FilesetActionContext extends ActionContext {
+
+ /**
+ * Returns a thread pool for fileset symlink tree creation.
+ */
+ ThreadPoolExecutor getFilesetPool();
+
+ /**
+ * Returns the name of the workspace the build is run in.
+ */
+ String getWorkspaceName();
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/fileset/FilesetActionContextImpl.java b/src/main/java/com/google/devtools/build/lib/rules/fileset/FilesetActionContextImpl.java
new file mode 100644
index 0000000..9c03129
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/fileset/FilesetActionContextImpl.java
@@ -0,0 +1,101 @@
+// Copyright 2014 Google Inc. 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.rules.fileset;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import com.google.devtools.build.lib.actions.ActionContextProvider;
+import com.google.devtools.build.lib.actions.ActionGraph;
+import com.google.devtools.build.lib.actions.ActionInputFileCache;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.BlazeExecutor;
+import com.google.devtools.build.lib.actions.ExecutionStrategy;
+import com.google.devtools.build.lib.actions.Executor.ActionContext;
+import com.google.devtools.build.lib.events.Reporter;
+
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Context for Fileset manifest actions. It currently only provides a ThreadPoolExecutor.
+ *
+ * <p>Fileset is a legacy, google-internal mechanism to make parts of the source tree appear as a
+ * tree in the output directory.
+ */
+@ExecutionStrategy(contextType = FilesetActionContext.class)
+public final class FilesetActionContextImpl implements FilesetActionContext {
+ // TODO(bazel-team): it would be nice if this weren't shipped in Bazel at all.
+
+ /**
+ * Factory class.
+ */
+ public static class Provider implements ActionContextProvider {
+ private FilesetActionContextImpl impl;
+ private final Reporter reporter;
+ private final ThreadPoolExecutor filesetPool;
+
+ public Provider(Reporter reporter, String workspaceName) {
+ this.reporter = reporter;
+ this.filesetPool = newFilesetPool(100);
+ this.impl = new FilesetActionContextImpl(filesetPool, workspaceName);
+ }
+
+ private static ThreadPoolExecutor newFilesetPool(int threads) {
+ ThreadPoolExecutor pool = new ThreadPoolExecutor(threads, threads, 3L, TimeUnit.SECONDS,
+ new LinkedBlockingQueue<Runnable>());
+ // Do not consume threads when not in use.
+ pool.allowCoreThreadTimeOut(true);
+ pool.setThreadFactory(new ThreadFactoryBuilder().setNameFormat("Fileset worker %d").build());
+ return pool;
+ }
+
+ @Override
+ public Iterable<ActionContext> getActionContexts() {
+ return ImmutableList.<ActionContext>of(impl);
+ }
+
+ @Override
+ public void executorCreated(Iterable<ActionContext> usedStrategies) {}
+
+ @Override
+ public void executionPhaseStarting(
+ ActionInputFileCache actionInputFileCache,
+ ActionGraph actionGraph,
+ Iterable<Artifact> topLevelArtifacts) {}
+
+ @Override
+ public void executionPhaseEnding() {
+ BlazeExecutor.shutdownHelperPool(reporter, filesetPool, "Fileset");
+ }
+ }
+
+ private final ThreadPoolExecutor filesetPool;
+ private final String workspaceName;
+
+ private FilesetActionContextImpl(ThreadPoolExecutor filesetPool, String workspaceName) {
+ this.filesetPool = filesetPool;
+ this.workspaceName = workspaceName;
+ }
+
+ @Override
+ public ThreadPoolExecutor getFilesetPool() {
+ return filesetPool;
+ }
+
+ @Override
+ public String getWorkspaceName() {
+ return workspaceName;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/fileset/FilesetLinks.java b/src/main/java/com/google/devtools/build/lib/rules/fileset/FilesetLinks.java
new file mode 100644
index 0000000..d523edc
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/fileset/FilesetLinks.java
@@ -0,0 +1,218 @@
+// Copyright 2014 Google Inc. 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.rules.fileset;
+
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.syntax.FilesetEntry;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * FilesetLinks manages the set of links added to a Fileset. If two links conflict, the first wins.
+ *
+ * <p>FilesetLinks is FileSystem-aware. For example, if you first create a link
+ * (a/b/c, foo), a subsequent call to link (a/b, bar) is a no-op.
+ * This is because the first link requires us to create a directory "a/b",
+ * so "a/b" cannot also link to "bar".
+ *
+ * <p>TODO(bazel-team): Consider warning if we have such a conflict; we don't do that currently.
+ */
+public interface FilesetLinks {
+
+ /**
+ * Get late directory information for a source.
+ *
+ * @param src The source to search for.
+ * @return The late directory info, or null if none was found.
+ */
+ public LateDirectoryInfo getLateDirectoryInfo(PathFragment src);
+
+ public boolean putLateDirectoryInfo(PathFragment src, LateDirectoryInfo lateDir);
+
+ /**
+ * Add specified file as a symlink.
+ *
+ * The behavior when the target file is a symlink depends on the
+ * symlinkBehavior parameter (see comments for FilesetEntry.SymlinkBehavior).
+ *
+ * @param src The root-relative symlink path.
+ * @param target The symlink target.
+ */
+ public void addFile(PathFragment src, Path target, String metadata,
+ FilesetEntry.SymlinkBehavior symlinkBehavior)
+ throws IOException;
+
+ /**
+ * Add all late directories as symlinks. This function should be called only
+ * after all recursions have completed, but before getData or getSymlinks are
+ * called.
+ */
+ public void addLateDirectories() throws IOException;
+
+ /**
+ * Adds the given symlink to the tree.
+ *
+ * @param fromFrag The root-relative symlink path.
+ * @param toFrag The symlink target.
+ * @return true iff the symlink was added.
+ */
+ public boolean addLink(PathFragment fromFrag, PathFragment toFrag, String dataVal);
+
+ /**
+ * @return The unmodifiable map of symlinks.
+ */
+ public Map<PathFragment, PathFragment> getSymlinks();
+
+ /**
+ * @return The unmodifiable map of metadata.
+ */
+ public Map<PathFragment, String> getData();
+
+ /**
+ * A data structure for containing all the information about a directory that
+ * is late-added. This means the directory is skipped unless we need to
+ * recurse into it later. If the directory is never recursed into, we will
+ * create a symlink directly to it.
+ */
+ public static final class LateDirectoryInfo {
+ // The constructors are private. Use the factory functions below to create
+ // instances of this class.
+
+ /** Construct a stub LateDirectoryInfo object. */
+ private LateDirectoryInfo() {
+ this.added = new AtomicBoolean(true);
+
+ // Shut up the compiler.
+ this.target = null;
+ this.src = null;
+ this.pkgMode = SubpackageMode.IGNORE;
+ this.metadata = null;
+ this.symlinkBehavior = null;
+ }
+
+ /** Construct a normal LateDirectoryInfo object. */
+ private LateDirectoryInfo(Path target, PathFragment src, SubpackageMode pkgMode,
+ String metadata, FilesetEntry.SymlinkBehavior symlinkBehavior) {
+ this.target = target;
+ this.src = src;
+ this.pkgMode = pkgMode;
+ this.metadata = metadata;
+ this.symlinkBehavior = symlinkBehavior;
+ this.added = new AtomicBoolean(false);
+ }
+
+ /** @return The target path for the symlink. The target is the referent. */
+ public Path getTarget() {
+ return target;
+ }
+
+ /**
+ * @return The source path for the symlink. The source is the place the
+ * symlink will be written. */
+ public PathFragment getSrc() {
+ return src;
+ }
+
+ /**
+ * @return Whether we should show a warning if we cross a package boundary
+ * when recursing into this directory.
+ */
+ public SubpackageMode getPkgMode() {
+ return pkgMode;
+ }
+
+ /**
+ * @return The metadata we will write into the manifest if we symlink to
+ * this directory.
+ */
+ public String getMetadata() {
+ return metadata;
+ }
+
+ /**
+ * @return How to perform the symlinking if the source happens to be a
+ * symlink itself.
+ */
+ public FilesetEntry.SymlinkBehavior getTargetSymlinkBehavior() {
+ return Preconditions.checkNotNull(symlinkBehavior,
+ "should not call this method on stub instances");
+ }
+
+ /**
+ * Atomically checks if the late directory has been added to the manifest
+ * and marks it as added. If this function returns true, it is the
+ * responsibility of the caller to recurse into the late directory.
+ * Otherwise, some other caller has already, or is in the process of
+ * recursing into it.
+ * @return Whether the caller should recurse into the late directory.
+ */
+ public boolean shouldAdd() {
+ return !added.getAndSet(true);
+ }
+
+ /**
+ * Create a stub LateDirectoryInfo that is already marked as added.
+ * @return The new LateDirectoryInfo object.
+ */
+ public static LateDirectoryInfo createStub() {
+ return new LateDirectoryInfo();
+ }
+
+ /**
+ * Create a LateDirectoryInfo object with the specified attributes.
+ * @param target The directory to which the symlinks will refer.
+ * @param src The location at which to create the symlink.
+ * @param pkgMode How to handle recursion into another package.
+ * @param metadata The metadata for the directory to write into the
+ * manifest if we symlink it directly.
+ * @return The new LateDirectoryInfo object.
+ */
+ public static LateDirectoryInfo create(Path target, PathFragment src, SubpackageMode pkgMode,
+ String metadata, FilesetEntry.SymlinkBehavior symlinkBehavior) {
+ return new LateDirectoryInfo(target, src, pkgMode, metadata, symlinkBehavior);
+ }
+
+ /**
+ * The target directory to which the symlink will point.
+ * Note this is a real path on the filesystem and can't be compared to src
+ * or any source (key) in the links map.
+ */
+ private final Path target;
+
+ /** The referent of the symlink. */
+ private final PathFragment src;
+
+ /** Whether to show cross package boundary warnings / errors. */
+ private final SubpackageMode pkgMode;
+
+ /** The metadata to write into the manifest file. */
+ private final String metadata;
+
+ /** How to perform the symlinking if the source happens to be a symlink itself. */
+ private final FilesetEntry.SymlinkBehavior symlinkBehavior;
+
+ /** Whether the directory has already been recursed into. */
+ private final AtomicBoolean added;
+ }
+
+ /** How to handle filesets that cross subpackages. */
+ public static enum SubpackageMode {
+ ERROR, WARNING, IGNORE;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/fileset/FilesetProvider.java b/src/main/java/com/google/devtools/build/lib/rules/fileset/FilesetProvider.java
new file mode 100644
index 0000000..6b70aab
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/fileset/FilesetProvider.java
@@ -0,0 +1,27 @@
+// Copyright 2014 Google Inc. 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.rules.fileset;
+
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+/**
+ * Information needed by a Fileset to do the right thing when it depends on another Fileset.
+ */
+public interface FilesetProvider extends TransitiveInfoProvider {
+ Artifact getFilesetInputManifest();
+ PathFragment getFilesetLinkDir();
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/fileset/SymlinkTraversal.java b/src/main/java/com/google/devtools/build/lib/rules/fileset/SymlinkTraversal.java
new file mode 100644
index 0000000..db13bdb
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/fileset/SymlinkTraversal.java
@@ -0,0 +1,54 @@
+// Copyright 2014 Google Inc. 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.rules.fileset;
+
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.util.Fingerprint;
+
+import java.io.IOException;
+import java.util.concurrent.ThreadPoolExecutor;
+
+/**
+ * An interface which contains a method to compute a symlink mapping.
+ */
+public interface SymlinkTraversal {
+
+ /**
+ * Adds symlinks to the given FilesetLinks.
+ *
+ * @throws IOException if a filesystem operation fails.
+ * @throws InterruptedException if the traversal is interrupted.
+ */
+ void addSymlinks(EventHandler eventHandler, FilesetLinks links, ThreadPoolExecutor filesetPool)
+ throws IOException, InterruptedException;
+
+ /**
+ * Add the traversal's fingerprint to the given Fingerprint.
+ * @param fp the Fingerprint to combine.
+ */
+ void fingerprint(Fingerprint fp);
+
+ /**
+ * @return true iff this traversal must be executed unconditionally.
+ */
+ boolean executeUnconditionally();
+
+ /**
+ * Returns true if it's ever possible that {@link #executeUnconditionally}
+ * could evaluate to true during the lifetime of this instance, false
+ * otherwise.
+ */
+ boolean isVolatile();
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/BaseJavaCompilationHelper.java b/src/main/java/com/google/devtools/build/lib/rules/java/BaseJavaCompilationHelper.java
new file mode 100644
index 0000000..69dc41b
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/BaseJavaCompilationHelper.java
@@ -0,0 +1,237 @@
+// Copyright 2014 Google Inc. 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.rules.java;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.ParameterFile.ParameterFileType;
+import com.google.devtools.build.lib.actions.Root;
+import com.google.devtools.build.lib.analysis.AnalysisEnvironment;
+import com.google.devtools.build.lib.analysis.AnalysisUtils;
+import com.google.devtools.build.lib.analysis.FilesToRunProvider;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.actions.CommandLine;
+import com.google.devtools.build.lib.analysis.actions.CustomCommandLine;
+import com.google.devtools.build.lib.analysis.actions.SpawnAction;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.Collection;
+
+/**
+ * A helper class for compiling Java targets. This helper does not rely on the
+ * presence of rule-specific attributes.
+ */
+public class BaseJavaCompilationHelper {
+ /**
+ * Also see DeployArchiveBuilder.SINGLEJAR_MAX_MEMORY. We don't expect that anyone has more
+ * than ~500,000 files in a source jar, so 256 MB of memory should be plenty.
+ */
+ private static final String SINGLEJAR_MAX_MEMORY = "-Xmx256m";
+
+ private final RuleContext ruleContext;
+
+ public BaseJavaCompilationHelper(RuleContext ruleContext) {
+ this.ruleContext = ruleContext;
+ }
+
+ /**
+ * Returns the artifacts required to invoke {@code javahome} relative binary
+ * in the action.
+ */
+ public static NestedSet<Artifact> getHostJavabaseInputs(RuleContext ruleContext) {
+ // This must have a different name than above, because the middleman creation uses the rule's
+ // configuration, although it should use the host configuration.
+ return AnalysisUtils.getMiddlemanFor(ruleContext, ":host_jdk");
+ }
+
+ private static final ImmutableList<String> SOURCE_JAR_COMMAND_LINE_ARGS = ImmutableList.of(
+ "--compression",
+ "--normalize",
+ "--exclude_build_data",
+ "--warn_duplicate_resources");
+
+ private CommandLine sourceJarCommandLine(JavaSemantics semantics, Artifact outputJar,
+ Iterable<Artifact> resources, Iterable<Artifact> resourceJars) {
+ CustomCommandLine.Builder args = CustomCommandLine.builder();
+ args.addExecPath("--output", outputJar);
+ args.add(SOURCE_JAR_COMMAND_LINE_ARGS);
+ args.addExecPaths("--sources", resourceJars);
+ args.add("--resources");
+ for (Artifact resource : resources) {
+ args.addPaths("%s:%s", resource.getExecPath(),
+ semantics.getJavaResourcePath(resource.getRootRelativePath()));
+ }
+ return args.build();
+ }
+
+ /**
+ * Creates an Action that packages files into a Jar file.
+ *
+ * @param semantics delegate semantics for java.
+ * @param resources the resources to put into the Jar.
+ * @param resourceJars the resource jars to merge into the jar
+ * @param outputJar the Jar to create
+ */
+ public void createSourceJarAction(JavaSemantics semantics, Collection<Artifact> resources,
+ Collection<Artifact> resourceJars, Artifact outputJar) {
+ ruleContext.registerAction(new SpawnAction.Builder()
+ .addOutput(outputJar)
+ .addInputs(resources)
+ .addInputs(resourceJars)
+ .addTransitiveInputs(JavaCompilationHelper.getHostJavabaseInputs(ruleContext))
+ .setJarExecutable(
+ ruleContext.getHostConfiguration().getFragment(Jvm.class).getJavaExecutable(),
+ ruleContext.getPrerequisiteArtifact("$singlejar", Mode.HOST),
+ ImmutableList.of("-client", SINGLEJAR_MAX_MEMORY))
+ .setCommandLine(sourceJarCommandLine(semantics, outputJar, resources, resourceJars))
+ .useParameterFile(ParameterFileType.SHELL_QUOTED)
+ .setProgressMessage("Building source jar " + outputJar.prettyPrint())
+ .setMnemonic("JavaSourceJar")
+ .build(ruleContext));
+ }
+
+ /**
+ * Returns the langtools jar Artifact.
+ */
+ protected final Artifact getLangtoolsJar() {
+ return ruleContext.getHostPrerequisiteArtifact("$java_langtools");
+ }
+
+ /**
+ * Returns the JavaBuilder jar Artifact.
+ */
+ protected final Artifact getJavaBuilderJar() {
+ return ruleContext.getPrerequisiteArtifact("$javabuilder", Mode.HOST);
+ }
+
+ /**
+ * Returns the javac bootclasspath artifacts.
+ */
+ protected final Iterable<Artifact> getBootClasspath() {
+ return ruleContext.getPrerequisiteArtifacts("$javac_bootclasspath", Mode.HOST).list();
+ }
+
+ private Artifact getIjarArtifact(Artifact jar, boolean addPrefix) {
+ if (addPrefix) {
+ PathFragment ruleBase = ruleContext.getLabel().getPackageFragment().getRelative(
+ ruleContext.getLabel().getName()).getRelative("_ijars");
+ PathFragment artifactDirFragment = jar.getRootRelativePath().getParentDirectory();
+ String ijarBasename = FileSystemUtils.removeExtension(jar.getFilename()) + "-ijar.jar";
+ return getAnalysisEnvironment().getDerivedArtifact(
+ ruleBase.getRelative(artifactDirFragment).getRelative(ijarBasename),
+ getConfiguration().getGenfilesDirectory());
+ } else {
+ return derivedArtifact(jar, "", "-ijar.jar");
+ }
+ }
+
+ /**
+ * Creates the Action that creates ijars from Jar files.
+ *
+ * @param inputJar the Jar to create the ijar for
+ * @param addPrefix whether to prefix the path of the generated ijar with the package and
+ * name of the current rule
+ * @return the Artifact to create with the Action
+ */
+ protected Artifact createIjarAction(final Artifact inputJar, boolean addPrefix) {
+ Artifact interfaceJar = getIjarArtifact(inputJar, addPrefix);
+ final FilesToRunProvider ijarTarget =
+ ruleContext.getExecutablePrerequisite("$ijar", Mode.HOST);
+ if (!ruleContext.hasErrors()) {
+ ruleContext.registerAction(new SpawnAction.Builder()
+ .addInput(inputJar)
+ .addOutput(interfaceJar)
+ .setExecutable(ijarTarget)
+ .addArgument(inputJar.getExecPathString())
+ .addArgument(interfaceJar.getExecPathString())
+ .setProgressMessage("Extracting interface " + ruleContext.getLabel())
+ .setMnemonic("JavaIjar")
+ .build(ruleContext));
+ }
+ return interfaceJar;
+ }
+
+ protected final JavaCompileAction.Builder createJavaCompileActionBuilder(
+ JavaSemantics semantics) {
+ JavaCompileAction.Builder builder = new JavaCompileAction.Builder(ruleContext, semantics);
+ builder.setJavaExecutable(
+ ruleContext.getHostConfiguration().getFragment(Jvm.class).getJavaExecutable());
+ builder.setJavaBaseInputs(BaseJavaCompilationHelper.getHostJavabaseInputs(ruleContext));
+ return builder;
+ }
+
+ public RuleContext getRuleContext() {
+ return ruleContext;
+ }
+
+ public AnalysisEnvironment getAnalysisEnvironment() {
+ return ruleContext.getAnalysisEnvironment();
+ }
+
+ protected BuildConfiguration getConfiguration() {
+ return ruleContext.getConfiguration();
+ }
+
+ protected JavaConfiguration getJavaConfiguration() {
+ return ruleContext.getFragment(JavaConfiguration.class);
+ }
+
+ protected PathFragment outputDir(Artifact outputJar) {
+ return workDir(outputJar, "_files");
+ }
+
+ /**
+ * Produces a derived directory where source files generated by annotation processors should be
+ * stored.
+ */
+ protected PathFragment sourceGenDir(Artifact outputJar) {
+ return workDir(outputJar, "_sourcegenfiles");
+ }
+
+ protected PathFragment tempDir(Artifact outputJar) {
+ return workDir(outputJar, "_temp");
+ }
+
+ /**
+ * For an output jar and a suffix, produces a derived directory under
+ * {@code bin} directory with a given suffix.
+ */
+ private PathFragment workDir(Artifact outputJar, String suffix) {
+ PathFragment path = outputJar.getRootRelativePath();
+ String basename = FileSystemUtils.removeExtension(path.getBaseName()) + suffix;
+ path = path.replaceName(basename);
+ return getConfiguration().getBinDirectory().getExecPath().getRelative(path);
+ }
+
+ /**
+ * Creates a derived artifact from the given artifact by adding the given
+ * prefix and removing the extension and replacing it by the given suffix.
+ * The new artifact will have the same root as the given one.
+ */
+ protected Artifact derivedArtifact(Artifact artifact, String prefix, String suffix) {
+ return derivedArtifact(artifact, prefix, suffix, artifact.getRoot());
+ }
+
+ protected Artifact derivedArtifact(Artifact artifact, String prefix, String suffix, Root root) {
+ PathFragment path = artifact.getRootRelativePath();
+ String basename = FileSystemUtils.removeExtension(path.getBaseName()) + suffix;
+ path = path.replaceName(prefix + basename);
+ return getAnalysisEnvironment().getDerivedArtifact(path, root);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/BuildInfoPropertiesTranslator.java b/src/main/java/com/google/devtools/build/lib/rules/java/BuildInfoPropertiesTranslator.java
new file mode 100644
index 0000000..053b9e7
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/BuildInfoPropertiesTranslator.java
@@ -0,0 +1,33 @@
+// Copyright 2014 Google Inc. 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.rules.java;
+
+import java.util.Map;
+import java.util.Properties;
+
+/**
+ * A class to describe how build information should be translated into the generated properties
+ * file.
+ */
+public interface BuildInfoPropertiesTranslator {
+
+ /** Translate build information into a property file. */
+ public void translate(Map<String, String> buildInfo, Properties properties);
+
+ /**
+ * Returns a unique key for this translator to be used by the
+ * {@link com.google.devtools.build.lib.actions.Action#getKey()} method
+ */
+ public String computeKey();
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/ClasspathConfiguredFragment.java b/src/main/java/com/google/devtools/build/lib/rules/java/ClasspathConfiguredFragment.java
new file mode 100644
index 0000000..6510a49
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/ClasspathConfiguredFragment.java
@@ -0,0 +1,96 @@
+// Copyright 2014 Google Inc. 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.rules.java;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+
+/**
+ * Represents common aspects of all JVM targeting configured targets.
+ */
+public final class ClasspathConfiguredFragment {
+
+ private final NestedSet<Artifact> runtimeClasspath;
+ private final NestedSet<Artifact> compileTimeClasspath;
+ private final ImmutableList<Artifact> bootClasspath;
+
+ /**
+ * Initializes the runtime and compile time classpaths for this target. This method
+ * should be called during {@code initializationHook()} once a {@link JavaTargetAttributes}
+ * object for this target is fully initialized.
+ *
+ * @param attributes the processed attributes of this Java target
+ * @param isNeverLink whether to leave runtimeClasspath empty
+ */
+ public ClasspathConfiguredFragment(JavaCompilationArtifacts javaArtifacts,
+ JavaTargetAttributes attributes, boolean isNeverLink) {
+ if (!isNeverLink) {
+ runtimeClasspath = getRuntimeClasspathList(attributes, javaArtifacts);
+ } else {
+ runtimeClasspath = NestedSetBuilder.emptySet(Order.NAIVE_LINK_ORDER);
+ }
+ compileTimeClasspath = attributes.getCompileTimeClassPath();
+ bootClasspath = attributes.getBootClassPath();
+ }
+
+ public ClasspathConfiguredFragment() {
+ runtimeClasspath = NestedSetBuilder.emptySet(Order.NAIVE_LINK_ORDER);
+ compileTimeClasspath = NestedSetBuilder.emptySet(Order.NAIVE_LINK_ORDER);
+ bootClasspath = ImmutableList.of();
+ }
+
+ /**
+ * Returns the runtime class path. It consists of the concatenation of the
+ * instrumentation class path, output jars and the runtime time class path of
+ * the transitive dependencies of this rule.
+ *
+ * @param attributes the processed attributes of this Java target
+ *
+ * @return a {@List} of artifacts that comprise the runtime class path.
+ */
+ private NestedSet<Artifact> getRuntimeClasspathList(
+ JavaTargetAttributes attributes, JavaCompilationArtifacts javaArtifacts) {
+ NestedSetBuilder<Artifact> builder = NestedSetBuilder.naiveLinkOrder();
+ builder.addAll(javaArtifacts.getRuntimeJars());
+ builder.addTransitive(attributes.getRuntimeClassPath());
+ return builder.build();
+ }
+
+ /**
+ * Returns the classpath to be passed to the JVM when running a target containing this fragment.
+ */
+ public NestedSet<Artifact> getRuntimeClasspath() {
+ return runtimeClasspath;
+ }
+
+ /**
+ * Returns the classpath to be passed to the Java compiler when compiling a target containing this
+ * fragment.
+ */
+ public NestedSet<Artifact> getCompileTimeClasspath() {
+ return compileTimeClasspath;
+ }
+
+ /**
+ * Returns the classpath to be passed as a boot classpath to the Java compiler when compiling
+ * a target containing this fragment.
+ */
+ public ImmutableList<Artifact> getBootClasspath() {
+ return bootClasspath;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/DeployArchiveBuilder.java b/src/main/java/com/google/devtools/build/lib/rules/java/DeployArchiveBuilder.java
new file mode 100644
index 0000000..b9fe186
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/DeployArchiveBuilder.java
@@ -0,0 +1,256 @@
+// Copyright 2014 Google Inc. 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.rules.java;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.ParameterFile.ParameterFileType;
+import com.google.devtools.build.lib.actions.ResourceSet;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.actions.CommandLine;
+import com.google.devtools.build.lib.analysis.actions.CustomCommandLine;
+import com.google.devtools.build.lib.analysis.actions.SpawnAction;
+import com.google.devtools.build.lib.collect.IterablesChain;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+/**
+ * Utility for configuring an action to generate a deploy archive.
+ */
+public class DeployArchiveBuilder {
+ /**
+ * Memory consumption of SingleJar is about 250 bytes per entry in the output file. Unfortunately,
+ * the JVM tends to kill the process with an OOM long before we're at the limit. In the most
+ * recent example, 400 MB of memory was enough for about 500,000 entries.
+ */
+ private static final String SINGLEJAR_MAX_MEMORY = "-Xmx1600m";
+
+ private final RuleContext ruleContext;
+
+ private final IterablesChain.Builder<Artifact> runtimeJarsBuilder = IterablesChain.builder();
+
+ private final JavaSemantics semantics;
+
+ private JavaTargetAttributes attributes;
+ private boolean includeBuildData;
+ private Compression compression = Compression.UNCOMPRESSED;
+ @Nullable private Artifact runfilesMiddleman;
+ private Artifact outputJar;
+ @Nullable private String javaStartClass;
+ private ImmutableList<String> deployManifestLines = ImmutableList.of();
+ @Nullable private Artifact launcher;
+
+ /**
+ * Type of compression to apply to output archive.
+ */
+ public enum Compression {
+
+ /** Output should be compressed */
+ COMPRESSED,
+
+ /** Output should not be compressed */
+ UNCOMPRESSED;
+ }
+
+ /**
+ * Creates a builder using the configuration of the rule as the action configuration.
+ */
+ public DeployArchiveBuilder(JavaSemantics semantics, RuleContext ruleContext) {
+ this.ruleContext = ruleContext;
+ this.semantics = semantics;
+ }
+
+ /**
+ * Sets the processed attributes of the rule generating the deploy archive.
+ */
+ public DeployArchiveBuilder setAttributes(JavaTargetAttributes attributes) {
+ this.attributes = attributes;
+ return this;
+ }
+
+ /**
+ * Sets whether to include build-data.properties in the deploy archive.
+ */
+ public DeployArchiveBuilder setIncludeBuildData(boolean includeBuildData) {
+ this.includeBuildData = includeBuildData;
+ return this;
+ }
+
+ /**
+ * Sets whether to enable compression of the output deploy archive.
+ */
+ public DeployArchiveBuilder setCompression(Compression compress) {
+ this.compression = Preconditions.checkNotNull(compress);
+ return this;
+ }
+
+ /**
+ * Sets additional dependencies to be added to the action that creates the
+ * deploy jar so that we force the runtime dependencies to be built.
+ */
+ public DeployArchiveBuilder setRunfilesMiddleman(@Nullable Artifact runfilesMiddleman) {
+ this.runfilesMiddleman = runfilesMiddleman;
+ return this;
+ }
+
+ /**
+ * Sets the artifact to create with the action.
+ */
+ public DeployArchiveBuilder setOutputJar(Artifact outputJar) {
+ this.outputJar = Preconditions.checkNotNull(outputJar);
+ return this;
+ }
+
+ /**
+ * Sets the class to launch the Java application.
+ */
+ public DeployArchiveBuilder setJavaStartClass(@Nullable String javaStartClass) {
+ this.javaStartClass = javaStartClass;
+ return this;
+ }
+
+ /**
+ * Adds additional jars that should be on the classpath at runtime.
+ */
+ public DeployArchiveBuilder addRuntimeJars(Iterable<Artifact> jars) {
+ this.runtimeJarsBuilder.add(jars);
+ return this;
+ }
+
+ /**
+ * Sets the list of extra lines to add to the archive's MANIFEST.MF file.
+ */
+ public DeployArchiveBuilder setDeployManifestLines(Iterable<String> deployManifestLines) {
+ this.deployManifestLines = ImmutableList.copyOf(deployManifestLines);
+ return this;
+ }
+
+ /**
+ * Sets the optional launcher to be used as the executable for this deploy
+ * JAR
+ */
+ public DeployArchiveBuilder setLauncher(@Nullable Artifact launcher) {
+ this.launcher = launcher;
+ return this;
+ }
+
+ public static CustomCommandLine.Builder defaultSingleJarCommandLine(Artifact outputJar,
+ String javaMainClass,
+ ImmutableList<String> deployManifestLines, Iterable<Artifact> buildInfoFiles,
+ ImmutableList<Artifact> classpathResources,
+ Iterable<Artifact> runtimeClasspath, boolean includeBuildData,
+ Compression compress, Artifact launcher) {
+
+ CustomCommandLine.Builder args = CustomCommandLine.builder();
+ args.addExecPath("--output", outputJar);
+ if (compress == Compression.COMPRESSED) {
+ args.add("--compression");
+ }
+ args.add("--normalize");
+ if (javaMainClass != null) {
+ args.add("--main_class");
+ args.add(javaMainClass);
+ }
+
+ if (!deployManifestLines.isEmpty()) {
+ args.add("--deploy_manifest_lines");
+ args.add(deployManifestLines);
+ }
+
+ if (buildInfoFiles != null) {
+ for (Artifact artifact : buildInfoFiles) {
+ args.addExecPath("--build_info_file", artifact);
+ }
+ }
+ if (!includeBuildData) {
+ args.add("--exclude_build_data");
+ }
+ if (launcher != null) {
+ args.add("--java_launcher");
+ args.add(launcher.getExecPathString());
+ }
+
+ args.addExecPaths("--classpath_resources", classpathResources);
+ args.addExecPaths("--sources", runtimeClasspath);
+ return args;
+ }
+
+ /**
+ * Builds the action as configured.
+ */
+ public void build() {
+ ImmutableList<Artifact> classpathResources = attributes.getClassPathResources();
+ Set<String> classPathResourceNames = new HashSet<>();
+ for (Artifact artifact : classpathResources) {
+ String name = artifact.getExecPath().getBaseName();
+ if (!classPathResourceNames.add(name)) {
+ ruleContext.attributeError("classpath_resources",
+ "entries must have different file names (duplicate: " + name + ")");
+ return;
+ }
+ }
+
+ IterablesChain<Artifact> runtimeJars = runtimeJarsBuilder.build();
+
+ IterablesChain.Builder<Artifact> inputs = IterablesChain.builder();
+ inputs.add(attributes.getArchiveInputs(true));
+
+ inputs.add(ImmutableList.copyOf(runtimeJars));
+ if (runfilesMiddleman != null) {
+ inputs.addElement(runfilesMiddleman);
+ }
+
+ final ImmutableList<Artifact> buildInfoArtifacts =
+ ruleContext.getAnalysisEnvironment().getBuildInfo(ruleContext, JavaBuildInfoFactory.KEY);
+ inputs.add(buildInfoArtifacts);
+
+ Iterable<Artifact> runtimeClasspath = Iterables.concat(
+ runtimeJars,
+ attributes.getRuntimeClassPathForArchive());
+
+ if (launcher != null) {
+ inputs.addElement(launcher);
+ }
+
+ CommandLine commandLine = semantics.buildSingleJarCommandLine(ruleContext.getConfiguration(),
+ outputJar, javaStartClass, deployManifestLines, buildInfoArtifacts, classpathResources,
+ runtimeClasspath, includeBuildData, compression, launcher);
+
+ List<String> jvmArgs = ImmutableList.of("-client", SINGLEJAR_MAX_MEMORY);
+ ResourceSet resourceSet =
+ new ResourceSet(/*memoryMb = */200.0, /*cpuUsage = */.2, /*ioUsage=*/.2);
+
+ ruleContext.registerAction(new SpawnAction.Builder()
+ .addInputs(inputs.build())
+ .addTransitiveInputs(JavaCompilationHelper.getHostJavabaseInputs(ruleContext))
+ .addOutput(outputJar)
+ .setResources(resourceSet)
+ .setJarExecutable(
+ ruleContext.getHostConfiguration().getFragment(Jvm.class).getJavaExecutable(),
+ ruleContext.getPrerequisiteArtifact("$singlejar", Mode.HOST),
+ jvmArgs)
+ .setCommandLine(commandLine)
+ .useParameterFile(ParameterFileType.SHELL_QUOTED)
+ .setProgressMessage("Building deploy jar " + outputJar.prettyPrint())
+ .setMnemonic("JavaDeployJar")
+ .build(ruleContext));
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/DirectDependencyProvider.java b/src/main/java/com/google/devtools/build/lib/rules/java/DirectDependencyProvider.java
new file mode 100644
index 0000000..5b2e106
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/DirectDependencyProvider.java
@@ -0,0 +1,64 @@
+// Copyright 2014 Google Inc. 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.rules.java;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.syntax.Label;
+
+/**
+ * A provider that returns the direct dependencies of a target. Used for strict dependency
+ * checking.
+ */
+@Immutable
+public final class DirectDependencyProvider implements TransitiveInfoProvider {
+
+ private final ImmutableList<Dependency> strictDependencies;
+
+ public DirectDependencyProvider(Iterable<Dependency> strictDependencies) {
+ this.strictDependencies = ImmutableList.copyOf(strictDependencies);
+ }
+
+ /**
+ * @returns the direct (strict) dependencies of this provider. All symbols that are directly
+ * reachable from the sources of the provider should be available in one these artifacts.
+ */
+ public Iterable<Dependency> getStrictDependencies() {
+ return strictDependencies;
+ }
+
+ /**
+ * A pair of label and its generated list of artifacts.
+ */
+ public static class Dependency {
+ private final Label label;
+
+ // TODO(bazel-team): change this to Artifacts
+ private final Iterable<String> fileExecPaths;
+
+ public Dependency(Label label, Iterable<String> fileExecPaths) {
+ this.label = label;
+ this.fileExecPaths = fileExecPaths;
+ }
+
+ public Label getLabel() {
+ return label;
+ }
+
+ public Iterable<String> getDependencyOutputs() {
+ return fileExecPaths;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/GenericBuildInfoPropertiesTranslator.java b/src/main/java/com/google/devtools/build/lib/rules/java/GenericBuildInfoPropertiesTranslator.java
new file mode 100644
index 0000000..df6a325
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/GenericBuildInfoPropertiesTranslator.java
@@ -0,0 +1,91 @@
+// Copyright 2014 Google Inc. 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.rules.java;
+
+import com.google.devtools.build.lib.util.Fingerprint;
+
+import java.util.Map;
+import java.util.Properties;
+
+/** The generic implementation of {@link BuildInfoPropertiesTranslator} */
+public class GenericBuildInfoPropertiesTranslator implements
+ BuildInfoPropertiesTranslator {
+
+ private static final String GUID = "e71fe4a8-11af-4ec0-9b38-1d3e7f542f51";
+
+ // syntax is %ID% for a property that depends on the ID key, %ID|default% to
+ // always add the property with the "default" key, %% is to add a percent sign
+ private final Map<String, String> translationKeys;
+
+ /**
+ * Create a generic translator, for each key,value pair in {@code translationKeys}, the key
+ * represents the property key that will be written and the value, its value. Inside value every
+ * %ID% is replaced by the corresponding build information with the same ID key. The property
+ * won't be added if it's depends on an unresolved build information. Adding a property can
+ * be forced even if a build information is missing by specifying a default value using the
+ * %ID|default% syntax. Finally to add a percent sign, just use the %% syntax.
+ */
+ public GenericBuildInfoPropertiesTranslator(Map<String, String> translationKeys) {
+ this.translationKeys = translationKeys;
+ }
+
+ @Override
+ public String computeKey() {
+ Fingerprint f = new Fingerprint();
+ f.addString(GUID);
+ f.addStringMap(translationKeys);
+ return f.hexDigestAndReset();
+ }
+
+ @Override
+ public void translate(Map<String, String> buildInfo, Properties properties) {
+ for (Map.Entry<String, String> entry : translationKeys.entrySet()) {
+ String translatedValue = translateValue(entry.getValue(), buildInfo);
+ if (translatedValue != null) {
+ properties.put(entry.getKey(), translatedValue);
+ }
+ }
+ }
+
+ private String translateValue(String valueDescription, Map<String, String> buildInfo) {
+ String[] split = valueDescription.split("%");
+ StringBuffer result = new StringBuffer();
+ boolean isInsideKey = false;
+ for (String key : split) {
+ if (isInsideKey) {
+ if (key.isEmpty()) {
+ result.append("%"); // empty key means %%
+ } else {
+ String defaultValue = null;
+ int i = key.lastIndexOf('|');
+ if (i >= 0) {
+ defaultValue = key.substring(i + 1);
+ key = key.substring(0, i);
+ }
+ if (buildInfo.containsKey(key)) {
+ result.append(buildInfo.get(key));
+ } else if (defaultValue != null) {
+ result.append(defaultValue);
+ } else { // we haven't found the requested key so we ignore the whole value
+ return null;
+ }
+ }
+ } else {
+ result.append(key);
+ }
+ isInsideKey = !isInsideKey;
+ }
+ return result.toString();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaBinary.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaBinary.java
new file mode 100644
index 0000000..e957f49
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaBinary.java
@@ -0,0 +1,359 @@
+// Copyright 2014 Google Inc. 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.rules.java;
+
+import static com.google.devtools.build.lib.rules.java.DeployArchiveBuilder.Compression.COMPRESSED;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.FileProvider;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTargetBuilder;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.Runfiles;
+import com.google.devtools.build.lib.analysis.RunfilesProvider;
+import com.google.devtools.build.lib.analysis.RunfilesSupport;
+import com.google.devtools.build.lib.analysis.TopLevelArtifactProvider;
+import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.rules.RuleConfiguredTargetFactory;
+import com.google.devtools.build.lib.rules.cpp.CppHelper;
+import com.google.devtools.build.lib.rules.cpp.LinkerInput;
+import com.google.devtools.build.lib.rules.java.JavaCompilationArgs.ClasspathType;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * An implementation of java_binary.
+ */
+public class JavaBinary implements RuleConfiguredTargetFactory {
+ private static final PathFragment CPP_RUNTIMES = new PathFragment("_cpp_runtimes");
+
+ private final JavaSemantics semantics;
+
+ protected JavaBinary(JavaSemantics semantics) {
+ this.semantics = semantics;
+ }
+
+ @Override
+ public ConfiguredTarget create(RuleContext ruleContext) {
+ final JavaCommon common = new JavaCommon(ruleContext, semantics);
+ DeployArchiveBuilder deployArchiveBuilder = new DeployArchiveBuilder(semantics, ruleContext);
+ Runfiles.Builder runfilesBuilder = new Runfiles.Builder();
+ List<String> jvmFlags = new ArrayList<>();
+
+ common.initializeJavacOpts();
+ JavaTargetAttributes.Builder attributesBuilder = common.initCommon();
+ attributesBuilder.addClassPathResources(
+ ruleContext.getPrerequisiteArtifacts("classpath_resources", Mode.TARGET).list());
+
+ List<String> userJvmFlags = common.getJvmFlags();
+
+ ruleContext.checkSrcsSamePackage(true);
+ boolean createExecutable = ruleContext.attributes().get("create_executable", Type.BOOLEAN);
+ List<TransitiveInfoCollection> deps =
+ Lists.newArrayList(common.targetsTreatedAsDeps(ClasspathType.COMPILE_ONLY));
+ semantics.checkRule(ruleContext, common);
+ String mainClass = semantics.getMainClass(ruleContext, common);
+ String originalMainClass = mainClass;
+ if (ruleContext.hasErrors()) {
+ return null;
+ }
+
+ // Collect the transitive dependencies.
+ JavaCompilationHelper helper = new JavaCompilationHelper(
+ ruleContext, semantics, common.getJavacOpts(), attributesBuilder);
+ helper.addLibrariesToAttributes(deps);
+ helper.addProvidersToAttributes(common.compilationArgsFromSources(), /* isNeverLink */ false);
+ attributesBuilder.addNativeLibraries(
+ collectNativeLibraries(common.targetsTreatedAsDeps(ClasspathType.BOTH)));
+
+ // deploy_env is valid for java_binary, but not for java_test.
+ if (ruleContext.getRule().isAttrDefined("deploy_env", Type.LABEL_LIST)) {
+ for (JavaRuntimeClasspathProvider envTarget : ruleContext.getPrerequisites(
+ "deploy_env", Mode.TARGET, JavaRuntimeClasspathProvider.class)) {
+ attributesBuilder.addExcludedArtifacts(envTarget.getRuntimeClasspath());
+ }
+ }
+
+ Artifact srcJar =
+ ruleContext.getImplicitOutputArtifact(JavaSemantics.JAVA_BINARY_SOURCE_JAR);
+
+ Artifact classJar =
+ ruleContext.getImplicitOutputArtifact(JavaSemantics.JAVA_BINARY_CLASS_JAR);
+
+ ImmutableList<Artifact> srcJars = ImmutableList.of(srcJar);
+
+ Artifact launcher = semantics.getLauncher(ruleContext, common, deployArchiveBuilder,
+ runfilesBuilder, jvmFlags, attributesBuilder);
+ JavaCompilationArtifacts.Builder javaArtifactsBuilder = new JavaCompilationArtifacts.Builder();
+ Artifact instrumentationMetadata =
+ helper.createInstrumentationMetadata(classJar, javaArtifactsBuilder);
+
+ NestedSetBuilder<Artifact> filesBuilder = NestedSetBuilder.stableOrder();
+ Artifact executable = null;
+ if (createExecutable) {
+ executable = ruleContext.createOutputArtifact(); // the artifact for the rule itself
+ filesBuilder.add(classJar).add(executable);
+
+ if (ruleContext.getConfiguration().isCodeCoverageEnabled()) {
+ mainClass = semantics.addCoverageSupport(helper, attributesBuilder,
+ executable, instrumentationMetadata, javaArtifactsBuilder, mainClass);
+ }
+ } else {
+ filesBuilder.add(classJar);
+ }
+
+ JavaTargetAttributes attributes = helper.getAttributes();
+ List<Artifact> nativeLibraries = attributes.getNativeLibraries();
+ if (!nativeLibraries.isEmpty()) {
+ jvmFlags.add("-Djava.library.path=" + JavaCommon.javaLibraryPath(nativeLibraries));
+ }
+
+ JavaConfiguration javaConfig = ruleContext.getFragment(JavaConfiguration.class);
+ if (attributes.hasMessages()) {
+ helper.addTranslations(semantics.translate(ruleContext, javaConfig,
+ attributes.getMessages()));
+ }
+
+ if (attributes.hasSourceFiles() || attributes.hasSourceJars()
+ || attributes.hasResources() || attributes.hasClassPathResources()) {
+ // We only want to add a jar to the classpath of a dependent rule if it has content.
+ javaArtifactsBuilder.addRuntimeJar(classJar);
+ }
+
+ // Any JAR files should be added to the collection of runtime jars.
+ javaArtifactsBuilder.addRuntimeJars(attributes.getJarFiles());
+
+ Artifact outputDepsProto = helper.createOutputDepsProtoArtifact(classJar, javaArtifactsBuilder);
+
+ common.setJavaCompilationArtifacts(javaArtifactsBuilder.build());
+
+ // The gensrcJar is only created if the target uses annotation processing. Otherwise,
+ // it is null, and the source jar action will not depend on the compile action.
+ Artifact gensrcJar = helper.createGensrcJar(classJar);
+
+ helper.createCompileAction(classJar, gensrcJar, outputDepsProto, instrumentationMetadata);
+ helper.createSourceJarAction(srcJar, gensrcJar);
+
+ common.setClassPathFragment(new ClasspathConfiguredFragment(
+ common.getJavaCompilationArtifacts(), attributes, false));
+
+ // Collect the action inputs for the runfiles collector here because we need to access the
+ // analysis environment, and that may no longer be safe when the runfiles collector runs.
+ Iterable<Artifact> dynamicRuntimeActionInputs =
+ CppHelper.getToolchain(ruleContext).getDynamicRuntimeLinkInputs();
+
+
+ Iterables.addAll(jvmFlags, semantics.getJvmFlags(ruleContext, common, launcher, userJvmFlags));
+ if (ruleContext.hasErrors()) {
+ return null;
+ }
+
+ if (createExecutable) {
+ // Create a shell stub for a Java application
+ semantics.createStubAction(ruleContext, common, jvmFlags, executable, mainClass,
+ common.getJavaBinSubstitution(launcher));
+ }
+
+ NestedSet<Artifact> transitiveSourceJars = collectTransitiveSourceJars(common, srcJar);
+
+ // TODO(bazel-team): if (getOptions().sourceJars) then make this a dummy prerequisite for the
+ // DeployArchiveAction ? Needs a few changes there as we can't pass inputs
+ helper.createSourceJarAction(semantics, ImmutableList.<Artifact>of(),
+ transitiveSourceJars.toCollection(),
+ ruleContext.getImplicitOutputArtifact(JavaSemantics.JAVA_BINARY_DEPLOY_SOURCE_JAR));
+
+ RuleConfiguredTargetBuilder builder =
+ new RuleConfiguredTargetBuilder(ruleContext);
+
+ semantics.addProviders(ruleContext, common, jvmFlags, classJar, srcJar, gensrcJar,
+ ImmutableMap.<Artifact, Artifact>of(), helper, filesBuilder, builder);
+
+ NestedSet<Artifact> filesToBuild = filesBuilder.build();
+
+ collectDefaultRunfiles(runfilesBuilder, ruleContext, common, filesToBuild, launcher,
+ dynamicRuntimeActionInputs);
+ Runfiles defaultRunfiles = runfilesBuilder.build();
+
+ RunfilesSupport runfilesSupport = createExecutable
+ ? runfilesSupport = RunfilesSupport.withExecutable(
+ ruleContext, defaultRunfiles, executable,
+ semantics.getExtraArguments(ruleContext, common))
+ : null;
+
+ RunfilesProvider runfilesProvider = RunfilesProvider.withData(
+ defaultRunfiles,
+ new Runfiles.Builder().merge(runfilesSupport).build());
+
+ ImmutableList<String> deployManifestLines =
+ getDeployManifestLines(ruleContext, originalMainClass);
+
+ Artifact deployJar =
+ ruleContext.getImplicitOutputArtifact(JavaSemantics.JAVA_BINARY_DEPLOY_JAR);
+
+ deployArchiveBuilder
+ .setOutputJar(deployJar)
+ .setJavaStartClass(mainClass)
+ .setDeployManifestLines(deployManifestLines)
+ .setAttributes(attributes)
+ .addRuntimeJars(common.getJavaCompilationArtifacts().getRuntimeJars())
+ .setIncludeBuildData(true)
+ .setRunfilesMiddleman(
+ runfilesSupport == null ? null : runfilesSupport.getRunfilesMiddleman())
+ .setCompression(COMPRESSED)
+ .setLauncher(launcher);
+
+ deployArchiveBuilder.build();
+
+ common.addTransitiveInfoProviders(builder, filesToBuild, classJar);
+
+ return builder
+ .setFilesToBuild(filesToBuild)
+ .add(RunfilesProvider.class, runfilesProvider)
+ .setRunfilesSupport(runfilesSupport, executable)
+ .add(JavaRuntimeClasspathProvider.class,
+ new JavaRuntimeClasspathProvider(common.getRuntimeClasspath()))
+ .add(JavaSourceJarsProvider.class,
+ new JavaSourceJarsProvider(transitiveSourceJars, srcJars))
+ .add(TopLevelArtifactProvider.class, new TopLevelArtifactProvider(
+ JavaSemantics.SOURCE_JARS_OUTPUT_GROUP, transitiveSourceJars))
+ .build();
+ }
+
+ // Create the deploy jar and make it dependent on the runfiles middleman if an executable is
+ // created. Do not add the deploy jar to files to build, so we will only build it when it gets
+ // requested.
+ private ImmutableList<String> getDeployManifestLines(RuleContext ruleContext,
+ String originalMainClass) {
+ ImmutableList.Builder<String> builder = ImmutableList.<String>builder()
+ .addAll(ruleContext.attributes().get("deploy_manifest_lines", Type.STRING_LIST));
+ if (ruleContext.getConfiguration().isCodeCoverageEnabled()) {
+ builder.add("Coverage-Main-Class: " + originalMainClass);
+ }
+ return builder.build();
+ }
+
+ private void collectDefaultRunfiles(Runfiles.Builder builder, RuleContext ruleContext,
+ JavaCommon common, NestedSet<Artifact> filesToBuild, Artifact launcher,
+ Iterable<Artifact> dynamicRuntimeActionInputs) {
+ // Convert to iterable: filesToBuild has a different order.
+ builder.addArtifacts((Iterable<Artifact>) filesToBuild);
+ builder.addArtifacts(common.getJavaCompilationArtifacts().getRuntimeJars());
+ if (launcher != null) {
+ final TransitiveInfoCollection defaultLauncher =
+ JavaHelper.launcherForTarget(semantics, ruleContext);
+ final Artifact defaultLauncherArtifact =
+ JavaHelper.launcherArtifactForTarget(semantics, ruleContext);
+ if (!defaultLauncherArtifact.equals(launcher)) {
+ builder.addArtifact(launcher);
+
+ // N.B. The "default launcher" referred to here is the launcher target specified through
+ // an attribute or flag. We wish to retain the runfiles of the default launcher, *except*
+ // for the original cc_binary artifact, because we've swapped it out with our custom
+ // launcher. Hence, instead of calling builder.addTarget(), or adding an odd method
+ // to Runfiles.Builder, we "unravel" the call and manually add things to the builder.
+ // Because the NestedSet representing each target's launcher runfiles is re-built here,
+ // we may see increased memory consumption for representing the target's runfiles.
+ Runfiles runfiles =
+ defaultLauncher.getProvider(RunfilesProvider.class)
+ .getDefaultRunfiles();
+ NestedSetBuilder<Artifact> unconditionalArtifacts = NestedSetBuilder.compileOrder();
+ for (Artifact a : runfiles.getUnconditionalArtifacts()) {
+ if (!a.equals(defaultLauncherArtifact)) {
+ unconditionalArtifacts.add(a);
+ }
+ }
+ builder.addTransitiveArtifacts(unconditionalArtifacts.build());
+ builder.addSymlinks(runfiles.getSymlinks());
+ builder.addRootSymlinks(runfiles.getRootSymlinks());
+ builder.addPruningManifests(runfiles.getPruningManifests());
+ } else {
+ builder.addTarget(defaultLauncher, RunfilesProvider.DEFAULT_RUNFILES);
+ }
+ }
+
+ semantics.addRunfilesForBinary(ruleContext, launcher, builder);
+ builder.addRunfiles(ruleContext, RunfilesProvider.DEFAULT_RUNFILES);
+ builder.add(ruleContext, JavaRunfilesProvider.TO_RUNFILES);
+
+ List<? extends TransitiveInfoCollection> runtimeDeps =
+ ruleContext.getPrerequisites("runtime_deps", Mode.TARGET);
+ builder.addTargets(runtimeDeps, JavaRunfilesProvider.TO_RUNFILES);
+ builder.addTargets(runtimeDeps, RunfilesProvider.DEFAULT_RUNFILES);
+ semantics.addDependenciesForRunfiles(ruleContext, builder);
+
+ if (ruleContext.getConfiguration().isCodeCoverageEnabled()) {
+ Artifact instrumentedJar = common.getJavaCompilationArtifacts().getInstrumentedJar();
+ if (instrumentedJar != null) {
+ builder.addArtifact(instrumentedJar);
+ }
+ }
+
+ builder.addArtifacts((Iterable<Artifact>) common.getRuntimeClasspath());
+
+ // Add the JDK files if it comes from the source repository (see java_stub_template.txt).
+ TransitiveInfoCollection javabaseTarget = ruleContext.getPrerequisite(":jvm", Mode.HOST);
+ if (javabaseTarget != null) {
+ builder.addArtifacts(
+ (Iterable<Artifact>) javabaseTarget.getProvider(FileProvider.class).getFilesToBuild());
+
+ // Add symlinks to the C++ runtime libraries under a path that can be built
+ // into the Java binary without having to embed the crosstool, gcc, and grte
+ // version information contained within the libraries' package paths.
+ for (Artifact lib : dynamicRuntimeActionInputs) {
+ PathFragment path = CPP_RUNTIMES.getRelative(lib.getExecPath().getBaseName());
+ builder.addSymlink(path, lib);
+ }
+ }
+ }
+
+ private NestedSet<Artifact> collectTransitiveSourceJars(JavaCommon common, Artifact srcJar) {
+ NestedSetBuilder<Artifact> builder = NestedSetBuilder.stableOrder();
+
+ builder.add(srcJar);
+ for (JavaSourceJarsProvider dep : common.getDependencies(JavaSourceJarsProvider.class)) {
+ builder.addTransitive(dep.getTransitiveSourceJars());
+ }
+ return builder.build();
+ }
+
+ /**
+ * Collects the native libraries in the transitive closure of the deps.
+ *
+ * @param deps the dependencies to be included as roots of the transitive closure.
+ * @return the native libraries found in the transitive closure of the deps.
+ */
+ public static Collection<Artifact> collectNativeLibraries(
+ Iterable<? extends TransitiveInfoCollection> deps) {
+ NestedSet<LinkerInput> linkerInputs = new NativeLibraryNestedSetBuilder()
+ .addJavaTargets(deps)
+ .build();
+ ImmutableList.Builder<Artifact> result = ImmutableList.builder();
+ for (LinkerInput linkerInput : linkerInputs) {
+ result.add(linkerInput.getArtifact());
+ }
+
+ return result.build();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaBuildInfoFactory.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaBuildInfoFactory.java
new file mode 100644
index 0000000..442b85b
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaBuildInfoFactory.java
@@ -0,0 +1,145 @@
+// Copyright 2014 Google Inc. 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.rules.java;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.Root;
+import com.google.devtools.build.lib.analysis.buildinfo.BuildInfoCollection;
+import com.google.devtools.build.lib.analysis.buildinfo.BuildInfoFactory;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.rules.java.WriteBuildInfoPropertiesAction.TimestampFormatter;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.joda.time.format.DateTimeFormat;
+import org.joda.time.format.DateTimeFormatter;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Java build info creation - generates properties file that contain the corresponding build-info
+ * data.
+ */
+public abstract class JavaBuildInfoFactory implements BuildInfoFactory {
+ public static final BuildInfoKey KEY = new BuildInfoKey("Java");
+
+ static final PathFragment BUILD_INFO_NONVOLATILE_PROPERTIES_NAME =
+ new PathFragment("build-info-nonvolatile.properties");
+ static final PathFragment BUILD_INFO_VOLATILE_PROPERTIES_NAME =
+ new PathFragment("build-info-volatile.properties");
+ static final PathFragment BUILD_INFO_REDACTED_PROPERTIES_NAME =
+ new PathFragment("build-info-redacted.properties");
+
+ private static final DateTimeFormatter DEFAULT_TIME_FORMAT =
+ DateTimeFormat.forPattern("EEE MMM d HH:mm:ss yyyy");
+
+ // A default formatter that returns a date in UTC format.
+ private static final TimestampFormatter DEFAULT_FORMATTER = new TimestampFormatter() {
+ @Override
+ public String format(long timestamp) {
+ return new DateTime(timestamp, DateTimeZone.UTC).toString(DEFAULT_TIME_FORMAT) + " ("
+ + timestamp / 1000 + ')';
+ }
+ };
+
+ @Override
+ public final BuildInfoCollection create(BuildInfoContext context, BuildConfiguration config,
+ Artifact stableStatus, Artifact volatileStatus) {
+ WriteBuildInfoPropertiesAction redactedInfo = getHeader(context,
+ config,
+ BUILD_INFO_REDACTED_PROPERTIES_NAME,
+ Artifact.NO_ARTIFACTS,
+ createRedactedTranslator(),
+ true,
+ true);
+ WriteBuildInfoPropertiesAction nonvolatileInfo = getHeader(context,
+ config,
+ BUILD_INFO_NONVOLATILE_PROPERTIES_NAME,
+ ImmutableList.of(stableStatus),
+ createNonVolatileTranslator(),
+ false,
+ true);
+ WriteBuildInfoPropertiesAction volatileInfo = getHeader(context,
+ config,
+ BUILD_INFO_VOLATILE_PROPERTIES_NAME,
+ ImmutableList.of(volatileStatus),
+ createVolatileTranslator(),
+ true,
+ false);
+ List<Action> actions = new ArrayList<Action>(3);
+ actions.add(redactedInfo);
+ actions.add(nonvolatileInfo);
+ actions.add(volatileInfo);
+ return new BuildInfoCollection(actions,
+ ImmutableList.of(nonvolatileInfo.getPrimaryOutput(), volatileInfo.getPrimaryOutput()),
+ ImmutableList.of(redactedInfo.getPrimaryOutput()));
+ }
+
+ /**
+ * Creates a {@link BuildInfoPropertiesTranslator} to use for volatile keys.
+ */
+ protected abstract BuildInfoPropertiesTranslator createVolatileTranslator();
+
+ /**
+ * Creates a {@link BuildInfoPropertiesTranslator} to use for non-volatile keys.
+ */
+ protected abstract BuildInfoPropertiesTranslator createNonVolatileTranslator();
+
+ /**
+ * Creates a {@link BuildInfoPropertiesTranslator} to use for redacted version of the build
+ * informations.
+ */
+ protected abstract BuildInfoPropertiesTranslator createRedactedTranslator();
+
+ /**
+ * Specifies the {@link TimestampFormatter} to use to output dates in the properties file.
+ */
+ protected TimestampFormatter getTimestampFormatter() {
+ return DEFAULT_FORMATTER;
+ }
+
+ private WriteBuildInfoPropertiesAction getHeader(BuildInfoContext context,
+ BuildConfiguration config,
+ PathFragment propertyFileName,
+ ImmutableList<Artifact> inputs,
+ BuildInfoPropertiesTranslator translator,
+ boolean includeVolatile,
+ boolean includeNonVolatile) {
+ Root outputPath = config.getIncludeDirectory();
+ final Artifact output = context.getBuildInfoArtifact(propertyFileName, outputPath,
+ includeVolatile && !inputs.isEmpty() ? BuildInfoType.NO_REBUILD
+ : BuildInfoType.FORCE_REBUILD_IF_CHANGED);
+ return new WriteBuildInfoPropertiesAction(inputs,
+ output,
+ translator,
+ includeVolatile,
+ includeNonVolatile,
+ getTimestampFormatter());
+ }
+
+ @Override
+ public final BuildInfoKey getKey() {
+ return KEY;
+ }
+
+ @Override
+ public boolean isEnabled(BuildConfiguration config) {
+ return config.hasFragment(JavaConfiguration.class);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaCcLinkParamsProvider.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaCcLinkParamsProvider.java
new file mode 100644
index 0000000..5218f3f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaCcLinkParamsProvider.java
@@ -0,0 +1,48 @@
+// Copyright 2014 Google Inc. 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.rules.java;
+
+import com.google.common.base.Function;
+import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
+import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.rules.cpp.CcLinkParamsStore;
+import com.google.devtools.build.lib.rules.cpp.CcLinkParamsStore.CcLinkParamsStoreImpl;
+
+/**
+ * A target that provides C++ libraries to be linked into Java targets.
+ */
+@Immutable
+public final class JavaCcLinkParamsProvider implements TransitiveInfoProvider {
+ private final CcLinkParamsStoreImpl store;
+
+ public JavaCcLinkParamsProvider(CcLinkParamsStore store) {
+ this.store = new CcLinkParamsStoreImpl(store);
+ }
+
+ public CcLinkParamsStore getLinkParams() {
+ return store;
+ }
+
+ public static final Function<TransitiveInfoCollection, CcLinkParamsStore> TO_LINK_PARAMS =
+ new Function<TransitiveInfoCollection, CcLinkParamsStore>() {
+ @Override
+ public CcLinkParamsStore apply(TransitiveInfoCollection input) {
+ JavaCcLinkParamsProvider provider = input.getProvider(
+ JavaCcLinkParamsProvider.class);
+ return provider == null ? null : provider.getLinkParams();
+ }
+ };
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaCommon.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaCommon.java
new file mode 100644
index 0000000..c55a74e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaCommon.java
@@ -0,0 +1,651 @@
+// Copyright 2014 Google Inc. 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.rules.java;
+
+import com.google.common.base.Function;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableList.Builder;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Multimap;
+import com.google.devtools.build.lib.Constants;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.AnalysisEnvironment;
+import com.google.devtools.build.lib.analysis.AnalysisUtils;
+import com.google.devtools.build.lib.analysis.FileProvider;
+import com.google.devtools.build.lib.analysis.FilesToCompileProvider;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTargetBuilder;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
+import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
+import com.google.devtools.build.lib.analysis.Util;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.packages.TargetUtils;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.rules.cpp.CppCompilationContext;
+import com.google.devtools.build.lib.rules.cpp.LinkerInput;
+import com.google.devtools.build.lib.rules.java.DirectDependencyProvider.Dependency;
+import com.google.devtools.build.lib.rules.java.JavaCompilationArgs.ClasspathType;
+import com.google.devtools.build.lib.rules.test.BaselineCoverageAction;
+import com.google.devtools.build.lib.rules.test.InstrumentedFilesCollector;
+import com.google.devtools.build.lib.rules.test.InstrumentedFilesCollector.LocalMetadataCollector;
+import com.google.devtools.build.lib.rules.test.InstrumentedFilesProvider;
+import com.google.devtools.build.lib.rules.test.InstrumentedFilesProviderImpl;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.util.FileType;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+/**
+ * A helper class to create configured targets for Java rules.
+ */
+public class JavaCommon {
+ private static final Function<TransitiveInfoCollection, Label> GET_COLLECTION_LABEL =
+ new Function<TransitiveInfoCollection, Label>() {
+ @Override
+ public Label apply(TransitiveInfoCollection collection) {
+ return collection.getLabel();
+ }
+ };
+
+ /**
+ * Collects all metadata files generated by Java compilation actions.
+ */
+ private static final LocalMetadataCollector JAVA_METADATA_COLLECTOR =
+ new LocalMetadataCollector() {
+ @Override
+ public void collectMetadataArtifacts(Iterable<Artifact> objectFiles,
+ AnalysisEnvironment analysisEnvironment, NestedSetBuilder<Artifact> metadataFilesBuilder) {
+ for (Artifact artifact : objectFiles) {
+ Action action = analysisEnvironment.getLocalGeneratingAction(artifact);
+ if (action instanceof JavaCompileAction) {
+ addOutputs(metadataFilesBuilder, action, JavaSemantics.COVERAGE_METADATA);
+ }
+ }
+ }
+ };
+
+ private ClasspathConfiguredFragment classpathFragment = new ClasspathConfiguredFragment();
+ private JavaCompilationArtifacts javaArtifacts = JavaCompilationArtifacts.EMPTY;
+ private ImmutableList<String> javacOpts;
+
+ // Targets treated as deps in compilation time, runtime time and both
+ private final ImmutableMap<ClasspathType, ImmutableList<TransitiveInfoCollection>>
+ targetsTreatedAsDeps;
+
+ private ImmutableList<Artifact> sources = ImmutableList.of();
+ private ImmutableList<JavaPluginInfoProvider> activePlugins = ImmutableList.of();
+
+ private final RuleContext ruleContext;
+ private final JavaSemantics semantics;
+
+ public JavaCommon(RuleContext ruleContext, JavaSemantics semantics) {
+ this(ruleContext, semantics,
+ collectTargetsTreatedAsDeps(ruleContext, semantics, ClasspathType.COMPILE_ONLY),
+ collectTargetsTreatedAsDeps(ruleContext, semantics, ClasspathType.RUNTIME_ONLY),
+ collectTargetsTreatedAsDeps(ruleContext, semantics, ClasspathType.BOTH));
+ }
+
+ public JavaCommon(RuleContext ruleContext,
+ JavaSemantics semantics,
+ ImmutableList<TransitiveInfoCollection> compileDeps,
+ ImmutableList<TransitiveInfoCollection> runtimeDeps,
+ ImmutableList<TransitiveInfoCollection> bothDeps) {
+ this.ruleContext = ruleContext;
+ this.semantics = semantics;
+ this.targetsTreatedAsDeps = ImmutableMap.of(
+ ClasspathType.COMPILE_ONLY, compileDeps,
+ ClasspathType.RUNTIME_ONLY, runtimeDeps,
+ ClasspathType.BOTH, bothDeps);
+ }
+
+ public void setClassPathFragment(ClasspathConfiguredFragment classpathFragment) {
+ this.classpathFragment = classpathFragment;
+ }
+
+ public void setJavaCompilationArtifacts(JavaCompilationArtifacts javaArtifacts) {
+ this.javaArtifacts = javaArtifacts;
+ }
+
+ public JavaCompilationArtifacts getJavaCompilationArtifacts() {
+ return javaArtifacts;
+ }
+
+ public ImmutableList<Artifact> getProcessorClasspathJars() {
+ Set<Artifact> processorClasspath = new LinkedHashSet<>();
+ for (JavaPluginInfoProvider plugin : activePlugins) {
+ for (Artifact classpathJar : plugin.getProcessorClasspath()) {
+ processorClasspath.add(classpathJar);
+ }
+ }
+ return ImmutableList.copyOf(processorClasspath);
+ }
+
+ public ImmutableList<String> getProcessorClassNames() {
+ Set<String> processorNames = new LinkedHashSet<>();
+ for (JavaPluginInfoProvider plugin : activePlugins) {
+ processorNames.addAll(plugin.getProcessorClasses());
+ }
+ return ImmutableList.copyOf(processorNames);
+ }
+
+ /**
+ * Creates the java.library.path from a list of the native libraries.
+ * Concatenates the parent directories of the shared libraries into a Java
+ * search path. Each relative path entry is prepended with "${JAVA_RUNFILES}/"
+ * so it can be resolved at runtime.
+ *
+ * @param sharedLibraries a collection of native libraries to create the java
+ * library path from
+ * @return a String containing the ":" separated java library path
+ */
+ public static String javaLibraryPath(Collection<Artifact> sharedLibraries) {
+ StringBuilder buffer = new StringBuilder();
+ Set<PathFragment> entries = new HashSet<>();
+ for (Artifact sharedLibrary : sharedLibraries) {
+ PathFragment entry = sharedLibrary.getRootRelativePath().getParentDirectory();
+ if (entries.add(entry)) {
+ if (buffer.length() > 0) {
+ buffer.append(':');
+ }
+ buffer.append("${JAVA_RUNFILES}/" + Constants.RUNFILES_PREFIX + "/");
+ buffer.append(entry.getPathString());
+ }
+ }
+ return buffer.toString();
+ }
+
+ /**
+ * Collects Java compilation arguments for this target.
+ *
+ * @param recursive Whether to scan dependencies recursively.
+ * @param isNeverLink Whether the target has the 'neverlink' attr.
+ */
+ JavaCompilationArgs collectJavaCompilationArgs(boolean recursive, boolean isNeverLink,
+ Iterable<SourcesJavaCompilationArgsProvider> compilationArgsFromSources) {
+ ClasspathType type = isNeverLink ? ClasspathType.COMPILE_ONLY : ClasspathType.BOTH;
+ JavaCompilationArgs.Builder builder = JavaCompilationArgs.builder()
+ .merge(getJavaCompilationArtifacts(), isNeverLink)
+ .addTransitiveTargets(getExports(ruleContext), recursive, type);
+ if (recursive) {
+ builder
+ .addTransitiveTargets(targetsTreatedAsDeps(ClasspathType.COMPILE_ONLY), recursive, type)
+ .addTransitiveTargets(getRuntimeDeps(ruleContext), recursive, ClasspathType.RUNTIME_ONLY)
+ .addSourcesTransitiveCompilationArgs(compilationArgsFromSources, recursive, type);
+ }
+ return builder.build();
+ }
+
+ /**
+ * Collects Java dependency artifacts for this target.
+ *
+ * @param outDeps output (compile-time) dependency artifact of this target
+ */
+ NestedSet<Artifact> collectCompileTimeDependencyArtifacts(Artifact outDeps) {
+ NestedSetBuilder<Artifact> builder = NestedSetBuilder.stableOrder();
+ if (outDeps != null) {
+ builder.add(outDeps);
+ }
+
+ for (JavaCompilationArgsProvider provider : AnalysisUtils.getProviders(
+ getExports(ruleContext), JavaCompilationArgsProvider.class)) {
+ builder.addTransitive(provider.getCompileTimeJavaDependencyArtifacts());
+ }
+ return builder.build();
+ }
+
+ public static List<TransitiveInfoCollection> getExports(RuleContext ruleContext) {
+ // We need to check here because there are classes inheriting from this class that implement
+ // rules that don't have this attribute.
+ if (ruleContext.getRule().getRuleClassObject().hasAttr("exports", Type.LABEL_LIST)) {
+ return ImmutableList.copyOf(ruleContext.getPrerequisites("exports", Mode.TARGET));
+ } else {
+ return ImmutableList.of();
+ }
+ }
+
+ /**
+ * Sanity checks the given runtime dependencies, and emits errors if there is a problem.
+ * Also called by {@link #initCommon()} for the current target's runtime dependencies.
+ */
+ public void checkRuntimeDeps(List<TransitiveInfoCollection> runtimeDepInfo) {
+ for (TransitiveInfoCollection c : runtimeDepInfo) {
+ JavaNeverlinkInfoProvider neverLinkedness =
+ c.getProvider(JavaNeverlinkInfoProvider.class);
+ if (neverLinkedness == null) {
+ continue;
+ }
+ boolean reportError = !ruleContext.getConfiguration().getAllowRuntimeDepsOnNeverLink();
+ if (neverLinkedness.isNeverlink()) {
+ String msg = String.format("neverlink dep %s not allowed in runtime deps", c.getLabel());
+ if (reportError) {
+ ruleContext.attributeError("runtime_deps", msg);
+ } else {
+ ruleContext.attributeWarning("runtime_deps", msg);
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns transitive Java native libraries.
+ *
+ * @see JavaNativeLibraryProvider
+ */
+ protected NestedSet<LinkerInput> collectTransitiveJavaNativeLibraries() {
+ NativeLibraryNestedSetBuilder builder = new NativeLibraryNestedSetBuilder();
+ builder.addJavaTargets(targetsTreatedAsDeps(ClasspathType.BOTH));
+
+ if (ruleContext.getRule().isAttrDefined("data", Type.LABEL_LIST)) {
+ builder.addJavaTargets(ruleContext.getPrerequisites("data", Mode.DATA));
+ }
+ return builder.build();
+ }
+
+ /**
+ * Collects transitive source jars for the current rule.
+ *
+ * @param targetSrcJar The source jar artifact corresponding to the output of the current rule.
+ * @return A nested set containing all of the source jar artifacts on which the current rule
+ * transitively depends.
+ */
+ public NestedSet<Artifact> collectTransitiveSourceJars(Artifact targetSrcJar) {
+ NestedSetBuilder<Artifact> builder = NestedSetBuilder.stableOrder();
+
+ builder.add(targetSrcJar);
+ for (JavaSourceJarsProvider dep : getDependencies(JavaSourceJarsProvider.class)) {
+ builder.addTransitive(dep.getTransitiveSourceJars());
+ }
+ return builder.build();
+ }
+
+ /**
+ * Collects transitive C++ dependencies.
+ */
+ protected CppCompilationContext collectTransitiveCppDeps() {
+ CppCompilationContext.Builder builder = new CppCompilationContext.Builder(ruleContext);
+ for (TransitiveInfoCollection dep : targetsTreatedAsDeps(ClasspathType.BOTH)) {
+ CppCompilationContext context =
+ dep.getProvider(CppCompilationContext.class);
+ if (context != null) {
+ builder.mergeDependentContext(context);
+ }
+ }
+ return builder.build();
+ }
+
+ /**
+ * Collects labels of targets and artifacts reached transitively via the "exports" attribute.
+ */
+ protected NestedSet<Label> collectTransitiveExports() {
+ NestedSetBuilder<Label> builder = NestedSetBuilder.stableOrder();
+ List<TransitiveInfoCollection> currentRuleExports = getExports(ruleContext);
+
+ builder.addAll(Iterables.transform(currentRuleExports, GET_COLLECTION_LABEL));
+
+ for (TransitiveInfoCollection dep : currentRuleExports) {
+ JavaExportsProvider exportsProvider = dep.getProvider(JavaExportsProvider.class);
+
+ if (exportsProvider != null) {
+ builder.addTransitive(exportsProvider.getTransitiveExports());
+ }
+ }
+
+ return builder.build();
+ }
+
+ public final void initializeJavacOpts() {
+ initializeJavacOpts(semantics.getExtraJavacOpts(ruleContext));
+ }
+
+ public final void initializeJavacOpts(Iterable<String> extraJavacOpts) {
+ javacOpts = ImmutableList.copyOf(Iterables.concat(
+ JavaToolchainProvider.getDefaultJavacOptions(ruleContext),
+ ruleContext.getTokenizedStringListAttr("javacopts"), extraJavacOpts));
+ }
+
+ /**
+ * Returns the string that the stub should use to determine the JVM
+ * @param launcher if non-null, the cc_binary used to launch the Java Virtual Machine
+ */
+ public String getJavaBinSubstitution(@Nullable Artifact launcher) {
+ PathFragment javaExecutable;
+
+ if (launcher != null) {
+ javaExecutable = launcher.getRootRelativePath();
+ } else {
+ javaExecutable = ruleContext.getFragment(Jvm.class).getJavaExecutable();
+ }
+
+ String pathPrefix =
+ javaExecutable.isAbsolute() ? "" : "${JAVA_RUNFILES}/" + Constants.RUNFILES_PREFIX + "/";
+ return "JAVABIN=${JAVABIN:-" + pathPrefix + javaExecutable.getPathString() + "}";
+ }
+
+ /**
+ * Heuristically determines the name of the primary Java class for this
+ * executable, based on the rule name and the "srcs" list.
+ *
+ * <p>(This is expected to be the class containing the "main" method for a
+ * java_binary, or a JUnit Test class for a java_test.)
+ *
+ * @param sourceFiles the source files for this rule
+ * @return a fully qualified Java class name, or null if none could be
+ * determined.
+ */
+ public String determinePrimaryClass(Collection<Artifact> sourceFiles) {
+ if (!sourceFiles.isEmpty()) {
+ String mainSource = ruleContext.getTarget().getName() + ".java";
+ for (Artifact sourceFile : sourceFiles) {
+ PathFragment path = sourceFile.getRootRelativePath();
+ if (path.getBaseName().equals(mainSource)) {
+ return JavaUtil.getJavaFullClassname(FileSystemUtils.removeExtension(path));
+ }
+ }
+ }
+ // Last resort: Use the name and package name of the target.
+ // TODO(bazel-team): this should be fixed to use a source file from the dependencies to
+ // determine the package of the Java class.
+ return JavaUtil.getJavaFullClassname(Util.getWorkspaceRelativePath(ruleContext.getTarget()));
+ }
+
+ /**
+ * Gets the value of the "jvm_flags" attribute combining it with the default
+ * options and expanding any make variables.
+ */
+ public List<String> getJvmFlags() {
+ List<String> jvmFlags = new ArrayList<>();
+ jvmFlags.addAll(ruleContext.getFragment(JavaConfiguration.class).getDefaultJvmFlags());
+ jvmFlags.addAll(ruleContext.expandedMakeVariablesList("jvm_flags"));
+ return jvmFlags;
+ }
+
+ private static List<TransitiveInfoCollection> getRuntimeDeps(RuleContext ruleContext) {
+ // We need to check here because there are classes inheriting from this class that implement
+ // rules that don't have this attribute.
+ if (ruleContext.getRule().getRuleClassObject().hasAttr("runtime_deps", Type.LABEL_LIST)) {
+ return ImmutableList.copyOf(ruleContext.getPrerequisites("runtime_deps", Mode.TARGET));
+ } else {
+ return ImmutableList.of();
+ }
+ }
+
+ public JavaTargetAttributes.Builder initCommon() {
+ return initCommon(Collections.<Artifact>emptySet());
+ }
+
+ /**
+ * Initialize the common actions and build various collections of artifacts
+ * for the initializationHook() methods of the subclasses.
+ *
+ * <p>Note that not all subclasses call this method.
+ *
+ * @return the processed attributes
+ */
+ public JavaTargetAttributes.Builder initCommon(Collection<Artifact> extraSrcs) {
+ Preconditions.checkState(javacOpts != null);
+ sources = ruleContext.getPrerequisiteArtifacts("srcs", Mode.TARGET).list();
+ activePlugins = collectPlugins();
+
+ JavaTargetAttributes.Builder javaTargetAttributes = new JavaTargetAttributes.Builder(semantics);
+ processSrcs(javaTargetAttributes, javacOpts);
+ javaTargetAttributes.addSourceArtifacts(extraSrcs);
+ processRuntimeDeps(javaTargetAttributes);
+
+ semantics.commonDependencyProcessing(ruleContext, javaTargetAttributes,
+ targetsTreatedAsDeps(ClasspathType.COMPILE_ONLY));
+
+ // Check that we have do not have both sources and jars.
+ if ((javaTargetAttributes.hasSourceFiles() || javaTargetAttributes.hasSourceJars())
+ && javaTargetAttributes.hasJarFiles()) {
+ ruleContext.attributeWarning("srcs", "cannot use both Java sources - source "
+ + "jars or source files - and precompiled jars");
+ }
+
+ if (disallowDepsWithoutSrcs(ruleContext.getRule().getRuleClass())
+ && ruleContext.attributes().get("srcs", Type.LABEL_LIST).isEmpty()
+ && ruleContext.getRule().isAttributeValueExplicitlySpecified("deps")) {
+ ruleContext.attributeError("deps", "deps not allowed without srcs; move to runtime_deps?");
+ }
+
+ javaTargetAttributes.addResources(semantics.collectResources(ruleContext));
+ addPlugins(javaTargetAttributes);
+
+ javaTargetAttributes.setRuleKind(ruleContext.getRule().getRuleClass());
+ javaTargetAttributes.setTargetLabel(ruleContext.getLabel());
+
+ return javaTargetAttributes;
+ }
+
+ private boolean disallowDepsWithoutSrcs(String ruleClass) {
+ return ruleClass.equals("java_library")
+ || ruleClass.equals("java_binary")
+ || ruleClass.equals("java_test");
+ }
+
+ public ImmutableList<? extends TransitiveInfoCollection> targetsTreatedAsDeps(
+ ClasspathType type) {
+ return targetsTreatedAsDeps.get(type);
+ }
+
+ private static ImmutableList<TransitiveInfoCollection> collectTargetsTreatedAsDeps(
+ RuleContext ruleContext, JavaSemantics semantics, ClasspathType type) {
+ ImmutableList.Builder<TransitiveInfoCollection> builder = new Builder<>();
+
+ if (!type.equals(ClasspathType.COMPILE_ONLY)) {
+ builder.addAll(getRuntimeDeps(ruleContext));
+ builder.addAll(getExports(ruleContext));
+ }
+ builder.addAll(ruleContext.getPrerequisites("deps", Mode.TARGET));
+
+ semantics.collectTargetsTreatedAsDeps(ruleContext, builder);
+
+ // Implicitly add dependency on java launcher cc_binary when --java_launcher= is enabled,
+ // or when launcher attribute is specified in a build rule.
+ TransitiveInfoCollection launcher = JavaHelper.launcherForTarget(semantics, ruleContext);
+ if (launcher != null) {
+ builder.add(launcher);
+ }
+
+ return builder.build();
+ }
+
+ public void addTransitiveInfoProviders(RuleConfiguredTargetBuilder builder,
+ NestedSet<Artifact> filesToBuild, @Nullable Artifact classJar) {
+ InstrumentedFilesCollector instrumentedFilesCollector =
+ new InstrumentedFilesCollector(ruleContext, semantics.getCoverageInstrumentationSpec(),
+ JAVA_METADATA_COLLECTOR, filesToBuild);
+
+ builder
+ .add(InstrumentedFilesProvider.class, new InstrumentedFilesProviderImpl(
+ instrumentedFilesCollector))
+ .add(FilesToCompileProvider.class,
+ new FilesToCompileProvider(getFilesToCompile(classJar)))
+ .add(JavaExportsProvider.class, new JavaExportsProvider(collectTransitiveExports()));
+
+ if (!TargetUtils.isTestRule(ruleContext.getTarget())) {
+ ImmutableList<Artifact> baselineCoverageArtifacts =
+ BaselineCoverageAction.getBaselineCoverageArtifacts(ruleContext,
+ instrumentedFilesCollector.getInstrumentedFiles());
+ builder.setBaselineCoverageArtifacts(baselineCoverageArtifacts);
+ }
+ }
+
+ /**
+ * Processes the sources of this target, adding them as messages, proper
+ * sources or to the list of targets treated as deps as required.
+ */
+ private void processSrcs(JavaTargetAttributes.Builder attributes,
+ ImmutableList<String> javacOpts) {
+ for (MessageBundleProvider srcItem : ruleContext.getPrerequisites(
+ "srcs", Mode.TARGET, MessageBundleProvider.class)) {
+ attributes.addMessages(srcItem.getMessages());
+ }
+
+ attributes.addSourceArtifacts(sources);
+
+ addCompileTimeClassPathEntriesMaybeThroughIjar(attributes, javacOpts);
+ }
+
+ /**
+ * Processes the transitive runtime_deps of this target.
+ */
+ private void processRuntimeDeps(JavaTargetAttributes.Builder attributes) {
+ List<TransitiveInfoCollection> runtimeDepInfo = getRuntimeDeps(ruleContext);
+ checkRuntimeDeps(runtimeDepInfo);
+ JavaCompilationArgs args = JavaCompilationArgs.builder()
+ .addTransitiveTargets(runtimeDepInfo, true, ClasspathType.RUNTIME_ONLY)
+ .build();
+ attributes.addRuntimeClassPathEntries(args.getRuntimeJars());
+ attributes.addInstrumentationMetadataEntries(args.getInstrumentationMetadata());
+ }
+
+ public Iterable<SourcesJavaCompilationArgsProvider> compilationArgsFromSources() {
+ return ruleContext.getPrerequisites("srcs", Mode.TARGET,
+ SourcesJavaCompilationArgsProvider.class);
+ }
+
+ /**
+ * Adds jars in the given group of entries to the compile time classpath after
+ * using ijar to create jar interfaces for the generated jars.
+ */
+ private void addCompileTimeClassPathEntriesMaybeThroughIjar(
+ JavaTargetAttributes.Builder attributes, ImmutableList<String> javacOpts) {
+ JavaCompilationHelper helper = new JavaCompilationHelper(
+ ruleContext, semantics, javacOpts, attributes);
+ for (FileProvider provider : ruleContext
+ .getPrerequisites("srcs", Mode.TARGET, FileProvider.class)) {
+ Iterable<Artifact> jarFiles = helper.filterGeneratedJarsThroughIjar(
+ FileType.filter(provider.getFilesToBuild(), JavaSemantics.JAR));
+ List<Artifact> jarsWithOwners = Lists.newArrayList(jarFiles);
+ attributes.addDirectCompileTimeClassPathEntries(jarsWithOwners);
+ attributes.addCompileTimeJarFiles(jarsWithOwners);
+ }
+ }
+
+ /**
+ * Adds information about the annotation processors that should be run for this java target to
+ * the target attributes.
+ */
+ private void addPlugins(JavaTargetAttributes.Builder attributes) {
+ for (JavaPluginInfoProvider plugin : activePlugins) {
+ for (String name : plugin.getProcessorClasses()) {
+ attributes.addProcessorName(name);
+ }
+ // Now get the plugin-libraries runtime classpath.
+ attributes.addProcessorPath(plugin.getProcessorClasspath());
+ }
+ }
+
+ private ImmutableList<JavaPluginInfoProvider> collectPlugins() {
+ List<JavaPluginInfoProvider> result = new ArrayList<>();
+ Iterables.addAll(result, getPluginInfoProvidersForAttribute(":java_plugins", Mode.HOST));
+ Iterables.addAll(result, getPluginInfoProvidersForAttribute("plugins", Mode.HOST));
+ Iterables.addAll(result, getPluginInfoProvidersForAttribute("deps", Mode.TARGET));
+ return ImmutableList.copyOf(result);
+ }
+
+ Iterable<JavaPluginInfoProvider> getPluginInfoProvidersForAttribute(String attribute,
+ Mode mode) {
+ if (ruleContext.getRule().getRuleClassObject().hasAttr(attribute, Type.LABEL_LIST)) {
+ return ruleContext.getPrerequisites(attribute, mode, JavaPluginInfoProvider.class);
+ }
+ return ImmutableList.of();
+ }
+
+ /**
+ * Gets all the deps.
+ */
+ public final Iterable<? extends TransitiveInfoCollection> getDependencies() {
+ return targetsTreatedAsDeps(ClasspathType.BOTH);
+ }
+
+ /**
+ * Gets all the deps that implement a particular provider.
+ */
+ public final <P extends TransitiveInfoProvider> Iterable<P> getDependencies(
+ Class<P> provider) {
+ return AnalysisUtils.getProviders(getDependencies(), provider);
+ }
+
+ /**
+ * Returns true if and only if this target has the neverlink attribute set to
+ * 1, or false if the neverlink attribute does not exist (for example, on
+ * *_binary targets)
+ *
+ * @return the value of the neverlink attribute.
+ */
+ public final boolean isNeverLink() {
+ return ruleContext.getRule().isAttrDefined("neverlink", Type.BOOLEAN) &&
+ ruleContext.attributes().get("neverlink", Type.BOOLEAN);
+ }
+
+ private ImmutableList<Artifact> getFilesToCompile(Artifact classJar) {
+ if (classJar == null) {
+ // Some subclasses don't produce jars
+ return ImmutableList.of();
+ }
+ return ImmutableList.of(classJar);
+ }
+
+ public ImmutableList<Dependency> computeStrictDepsFromJavaAttributes(
+ JavaTargetAttributes javaTargetAttributes) {
+ Multimap<Label, String> depMap = HashMultimap.<Label, String>create();
+ for (Artifact jar : javaTargetAttributes.getDirectJars()) {
+ depMap.put(Preconditions.checkNotNull(jar.getOwner()),
+ jar.getExecPathString());
+ }
+ ImmutableList.Builder<Dependency> depOuts = ImmutableList.builder();
+ for (Label label : depMap.keySet()) {
+ depOuts.add(new Dependency(label, depMap.get(label)));
+ }
+ return depOuts.build();
+ }
+
+ public ImmutableList<Artifact> getSrcsArtifacts() {
+ return sources;
+ }
+
+ public ImmutableList<String> getJavacOpts() {
+ return javacOpts;
+ }
+
+ public ImmutableList<Artifact> getBootClasspath() {
+ return classpathFragment.getBootClasspath();
+ }
+
+ public NestedSet<Artifact> getRuntimeClasspath() {
+ return classpathFragment.getRuntimeClasspath();
+ }
+
+ public NestedSet<Artifact> getCompileTimeClasspath() {
+ return classpathFragment.getCompileTimeClasspath();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaCompilationArgs.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaCompilationArgs.java
new file mode 100644
index 0000000..145d646
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaCompilationArgs.java
@@ -0,0 +1,301 @@
+// Copyright 2014 Google Inc. 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.rules.java;
+
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.FileProvider;
+import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.util.FileType;
+
+import java.util.Collection;
+
+/**
+ * A container of Java compilation artifacts.
+ */
+public final class JavaCompilationArgs {
+ // TODO(bazel-team): It would be desirable to use LinkOrderNestedSet here so that
+ // parents-before-deps is preserved for graphs that are not trees. However, the legacy
+ // JavaLibraryCollector implemented naive link ordering and many targets in the
+ // depot depend on the consistency of left-to-right ordering that is not provided by
+ // LinkOrderNestedSet. They simply list their local dependencies before
+ // other targets that may use conflicting dependencies, and the local deps
+ // appear earlier on the classpath, as desired. Behavior of LinkOrderNestedSet
+ // can be very unintuitive in case of conflicting orders, because the order is
+ // decided by the rightmost branch in such cases. For example, if A depends on {junit4,
+ // B}, B depends on {C, D}, C depends on {junit3}, and D depends on {junit4},
+ // the classpath of A will have junit3 before junit4.
+ private final NestedSet<Artifact> runtimeJars;
+ private final NestedSet<Artifact> compileTimeJars;
+ private final NestedSet<Artifact> instrumentationMetadata;
+
+ public static final JavaCompilationArgs EMPTY_ARGS = new JavaCompilationArgs(
+ NestedSetBuilder.<Artifact>create(Order.NAIVE_LINK_ORDER),
+ NestedSetBuilder.<Artifact>create(Order.NAIVE_LINK_ORDER),
+ NestedSetBuilder.<Artifact>create(Order.NAIVE_LINK_ORDER));
+
+ private JavaCompilationArgs(NestedSet<Artifact> runtimeJars,
+ NestedSet<Artifact> compileTimeJars,
+ NestedSet<Artifact> instrumentationMetadata) {
+ this.runtimeJars = runtimeJars;
+ this.compileTimeJars = compileTimeJars;
+ this.instrumentationMetadata = instrumentationMetadata;
+ }
+
+ /**
+ * Returns transitive runtime jars.
+ */
+ public NestedSet<Artifact> getRuntimeJars() {
+ return runtimeJars;
+ }
+
+ /**
+ * Returns transitive compile-time jars.
+ */
+ public NestedSet<Artifact> getCompileTimeJars() {
+ return compileTimeJars;
+ }
+
+ /**
+ * Returns transitive instrumentation metadata jars.
+ */
+ public NestedSet<Artifact> getInstrumentationMetadata() {
+ return instrumentationMetadata;
+ }
+
+ /**
+ * Returns a new builder instance.
+ */
+ public static final Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * Builder for {@link JavaCompilationArgs}.
+ *
+ *
+ */
+ public static final class Builder {
+ private final NestedSetBuilder<Artifact> runtimeJarsBuilder =
+ NestedSetBuilder.naiveLinkOrder();
+ private final NestedSetBuilder<Artifact> compileTimeJarsBuilder =
+ NestedSetBuilder.naiveLinkOrder();
+ private final NestedSetBuilder<Artifact> instrumentationMetadataBuilder =
+ NestedSetBuilder.naiveLinkOrder();
+
+ /**
+ * Use {@code TransitiveJavaCompilationArgs#builder()} to instantiate the builder.
+ */
+ private Builder() {
+ }
+
+ /**
+ * Legacy method for dealing with objects which construct
+ * {@link JavaCompilationArtifacts} objects.
+ */
+ // TODO(bazel-team): Remove when we get rid of JavaCompilationArtifacts.
+ public Builder merge(JavaCompilationArtifacts other, boolean isNeverLink) {
+ if (!isNeverLink) {
+ addRuntimeJars(other.getRuntimeJars());
+ }
+ addCompileTimeJars(other.getCompileTimeJars());
+ addInstrumentationMetadata(other.getInstrumentationMetadata());
+ return this;
+ }
+
+ /**
+ * Legacy method for dealing with objects which construct
+ * {@link JavaCompilationArtifacts} objects.
+ */
+ public Builder merge(JavaCompilationArtifacts other) {
+ return merge(other, false);
+ }
+
+ public Builder addRuntimeJar(Artifact runtimeJar) {
+ this.runtimeJarsBuilder.add(runtimeJar);
+ return this;
+ }
+
+ public Builder addRuntimeJars(Iterable<Artifact> runtimeJars) {
+ this.runtimeJarsBuilder.addAll(runtimeJars);
+ return this;
+ }
+
+ public Builder addCompileTimeJar(Artifact compileTimeJar) {
+ this.compileTimeJarsBuilder.add(compileTimeJar);
+ return this;
+ }
+
+ public Builder addCompileTimeJars(Iterable<Artifact> compileTimeJars) {
+ this.compileTimeJarsBuilder.addAll(compileTimeJars);
+ return this;
+ }
+
+ public Builder addInstrumentationMetadata(Artifact instrumentationMetadata) {
+ this.instrumentationMetadataBuilder.add(instrumentationMetadata);
+ return this;
+ }
+
+ public Builder addInstrumentationMetadata(Collection<Artifact> instrumentationMetadata) {
+ this.instrumentationMetadataBuilder.addAll(instrumentationMetadata);
+ return this;
+ }
+
+ public Builder addTransitiveCompilationArgs(
+ JavaCompilationArgsProvider dep, boolean recursive, ClasspathType type) {
+ JavaCompilationArgs args = recursive
+ ? dep.getRecursiveJavaCompilationArgs()
+ : dep.getJavaCompilationArgs();
+ addTransitiveArgs(args, type);
+ return this;
+ }
+
+ public Builder addTransitiveCompilationArgs(
+ SourcesJavaCompilationArgsProvider dep, boolean recursive, ClasspathType type) {
+ JavaCompilationArgs args;
+ if (recursive) {
+ args = dep.getRecursiveJavaCompilationArgs();
+ } else {
+ args = dep.getJavaCompilationArgs();
+ }
+ addTransitiveArgs(args, type);
+ return this;
+ }
+
+ public Builder addSourcesTransitiveCompilationArgs(
+ Iterable<? extends SourcesJavaCompilationArgsProvider> deps,
+ boolean recursive,
+ ClasspathType type) {
+ for (SourcesJavaCompilationArgsProvider dep : deps) {
+ addTransitiveCompilationArgs(dep, recursive, type);
+ }
+
+ return this;
+ }
+
+ /**
+ * Merges the artifacts of another target.
+ */
+ public Builder addTransitiveTarget(TransitiveInfoCollection dep, boolean recursive,
+ ClasspathType type) {
+ JavaCompilationArgsProvider provider = dep.getProvider(JavaCompilationArgsProvider.class);
+ if (provider != null) {
+ addTransitiveCompilationArgs(provider, recursive, type);
+ return this;
+ } else {
+ NestedSet<Artifact> filesToBuild =
+ dep.getProvider(FileProvider.class).getFilesToBuild();
+ for (Artifact jar : FileType.filter(filesToBuild, JavaSemantics.JAR)) {
+ addCompileTimeJar(jar);
+ addRuntimeJar(jar);
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Merges the artifacts of a collection of targets.
+ */
+ public Builder addTransitiveTargets(Iterable<? extends TransitiveInfoCollection> deps,
+ boolean recursive, ClasspathType type) {
+ for (TransitiveInfoCollection dep : deps) {
+ addTransitiveTarget(dep, recursive, type);
+ }
+ return this;
+ }
+
+ /**
+ * Merges the artifacts of a collection of targets.
+ */
+ public Builder addTransitiveTargets(Iterable<? extends TransitiveInfoCollection> deps,
+ boolean recursive) {
+ return addTransitiveTargets(deps, recursive, ClasspathType.BOTH);
+ }
+
+ /**
+ * Merges the artifacts of a collection of targets.
+ */
+ public Builder addTransitiveDependencies(Iterable<JavaCompilationArgsProvider> deps,
+ boolean recursive) {
+ for (JavaCompilationArgsProvider dep : deps) {
+ addTransitiveDependency(dep, recursive, ClasspathType.BOTH);
+ }
+ return this;
+ }
+
+ /**
+ * Merges the artifacts of another target.
+ */
+ private Builder addTransitiveDependency(JavaCompilationArgsProvider dep, boolean recursive,
+ ClasspathType type) {
+ JavaCompilationArgs args = recursive
+ ? dep.getRecursiveJavaCompilationArgs()
+ : dep.getJavaCompilationArgs();
+ addTransitiveArgs(args, type);
+ return this;
+ }
+
+ /**
+ * Merges the artifacts of a collection of targets.
+ */
+ public Builder addTransitiveTargets(Iterable<? extends TransitiveInfoCollection> deps) {
+ return addTransitiveTargets(deps, /*recursive=*/true, ClasspathType.BOTH);
+ }
+
+ /**
+ * Includes the contents of another instance of JavaCompilationArgs.
+ *
+ * @param args the JavaCompilationArgs instance
+ * @param type the classpath(s) to consider
+ */
+ public Builder addTransitiveArgs(JavaCompilationArgs args, ClasspathType type) {
+ if (!ClasspathType.RUNTIME_ONLY.equals(type)) {
+ compileTimeJarsBuilder.addTransitive(args.getCompileTimeJars());
+ }
+ if (!ClasspathType.COMPILE_ONLY.equals(type)) {
+ runtimeJarsBuilder.addTransitive(args.getRuntimeJars());
+ }
+ instrumentationMetadataBuilder.addTransitive(
+ args.getInstrumentationMetadata());
+ return this;
+ }
+
+ /**
+ * Builds a {@link JavaCompilationArgs} object.
+ */
+ public JavaCompilationArgs build() {
+ return new JavaCompilationArgs(
+ runtimeJarsBuilder.build(),
+ compileTimeJarsBuilder.build(),
+ instrumentationMetadataBuilder.build());
+ }
+ }
+
+ /**
+ * Enum to specify transitive compilation args traversal
+ */
+ public static enum ClasspathType {
+ /* treat the same for compile time and runtime */
+ BOTH,
+
+ /* Only include on compile classpath */
+ COMPILE_ONLY,
+
+ /* Only include on runtime classpath */
+ RUNTIME_ONLY;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaCompilationArgsProvider.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaCompilationArgsProvider.java
new file mode 100644
index 0000000..1958ada
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaCompilationArgsProvider.java
@@ -0,0 +1,94 @@
+// Copyright 2014 Google Inc. 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.rules.java;
+
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+
+/**
+ * An interface for objects that provide information on how to include them in
+ * Java builds.
+ */
+@Immutable
+public final class JavaCompilationArgsProvider implements TransitiveInfoProvider {
+ private final JavaCompilationArgs javaCompilationArgs;
+ private final JavaCompilationArgs recursiveJavaCompilationArgs;
+ private final NestedSet<Artifact> compileTimeJavaDepArtifacts;
+ private final NestedSet<Artifact> runTimeJavaDepArtifacts;
+
+ public JavaCompilationArgsProvider(JavaCompilationArgs javaCompilationArgs,
+ JavaCompilationArgs recursiveJavaCompilationArgs,
+ NestedSet<Artifact> compileTimeJavaDepArtifacts,
+ NestedSet<Artifact> runTimeJavaDepArtifacts) {
+ this.javaCompilationArgs = javaCompilationArgs;
+ this.recursiveJavaCompilationArgs = recursiveJavaCompilationArgs;
+ this.compileTimeJavaDepArtifacts = compileTimeJavaDepArtifacts;
+ this.runTimeJavaDepArtifacts = runTimeJavaDepArtifacts;
+ }
+
+ public JavaCompilationArgsProvider(JavaCompilationArgs javaCompilationArgs,
+ JavaCompilationArgs recursiveJavaCompilationArgs) {
+ this.javaCompilationArgs = javaCompilationArgs;
+ this.recursiveJavaCompilationArgs = recursiveJavaCompilationArgs;
+ this.compileTimeJavaDepArtifacts = NestedSetBuilder.<Artifact>emptySet(Order.STABLE_ORDER);
+ this.runTimeJavaDepArtifacts = NestedSetBuilder.<Artifact>emptySet(Order.STABLE_ORDER);
+ }
+
+ /**
+ * Returns non-recursively collected Java compilation information for
+ * building this target (called when strict_java_deps = 1).
+ *
+ * <p>Note that some of the parameters are still collected from the complete
+ * transitive closure. The non-recursive collection applies mainly to
+ * compile-time jars.
+ */
+ public JavaCompilationArgs getJavaCompilationArgs() {
+ return javaCompilationArgs;
+ }
+
+ /**
+ * Returns recursively collected Java compilation information for building
+ * this target (called when strict_java_deps = 0).
+ */
+ public JavaCompilationArgs getRecursiveJavaCompilationArgs() {
+ return recursiveJavaCompilationArgs;
+ }
+
+ /**
+ * Returns non-recursively collected Java dependency artifacts for
+ * computing a restricted classpath when building this target (called when
+ * strict_java_deps = 1).
+ *
+ * <p>Note that dependency artifacts are needed only when non-recursive
+ * compilation args do not provide a safe super-set of dependencies.
+ * Non-strict targets such as proto_library, always collecting their
+ * transitive closure of deps, do not need to provide dependency artifacts.
+ */
+ public NestedSet<Artifact> getCompileTimeJavaDependencyArtifacts() {
+ return compileTimeJavaDepArtifacts;
+ }
+
+ /**
+ * Returns Java dependency artifacts for computing a restricted run-time
+ * classpath (called when strict_java_deps = 1).
+ */
+ public NestedSet<Artifact> getRunTimeJavaDependencyArtifacts() {
+ return runTimeJavaDepArtifacts;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaCompilationArtifacts.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaCompilationArtifacts.java
new file mode 100644
index 0000000..98ccbac
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaCompilationArtifacts.java
@@ -0,0 +1,148 @@
+// Copyright 2014 Google Inc. 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.rules.java;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+/**
+ * A collection of artifacts for java compilations. It concisely describes the
+ * outputs of a java-related rule, with runtime jars, compile-time jars,
+ * unfiltered compile-time jars (these are run through ijar if they are
+ * dependent upon by another target), source ijars, and instrumentation
+ * manifests. Not all rules generate all kinds of artifacts. Each java-related
+ * rule should add both a runtime jar and either a compile-time jar or an
+ * unfiltered compile-time jar.
+ *
+ * <p>An instance of this class only collects the data for the current target,
+ * not for the transitive closure of targets, so these still need to be
+ * collected using some other mechanism, such as the {@link
+ * JavaCompilationArgsProvider}.
+ */
+@Immutable
+public final class JavaCompilationArtifacts {
+
+ public static final JavaCompilationArtifacts EMPTY = new Builder().build();
+
+ private final ImmutableList<Artifact> runtimeJars;
+ private final ImmutableList<Artifact> compileTimeJars;
+ private final ImmutableList<Artifact> instrumentationMetadata;
+ private final Artifact compileTimeDependencyArtifact;
+ private final Artifact runTimeDependencyArtifact;
+ private final Artifact instrumentedJar;
+
+ private JavaCompilationArtifacts(ImmutableList<Artifact> runtimeJars,
+ ImmutableList<Artifact> compileTimeJars,
+ ImmutableList<Artifact> instrumentationMetadata,
+ Artifact compileTimeDependencyArtifact, Artifact runTimeDependencyArtifact,
+ Artifact instrumentedJar) {
+ this.runtimeJars = runtimeJars;
+ this.compileTimeJars = compileTimeJars;
+ this.instrumentationMetadata = instrumentationMetadata;
+ this.compileTimeDependencyArtifact = compileTimeDependencyArtifact;
+ this.runTimeDependencyArtifact = runTimeDependencyArtifact;
+ this.instrumentedJar = instrumentedJar;
+ }
+
+ public ImmutableList<Artifact> getRuntimeJars() {
+ return runtimeJars;
+ }
+
+ public ImmutableList<Artifact> getCompileTimeJars() {
+ return compileTimeJars;
+ }
+
+ public ImmutableList<Artifact> getInstrumentationMetadata() {
+ return instrumentationMetadata;
+ }
+
+ public Artifact getCompileTimeDependencyArtifact() {
+ return compileTimeDependencyArtifact;
+ }
+
+ public Artifact getRunTimeDependencyArtifact() {
+ return runTimeDependencyArtifact;
+ }
+
+ public Artifact getInstrumentedJar() {
+ return instrumentedJar;
+ }
+
+ /**
+ * A builder for {@link JavaCompilationArtifacts}.
+ */
+ public static final class Builder {
+ private final Set<Artifact> runtimeJars = new LinkedHashSet<>();
+ private final Set<Artifact> compileTimeJars = new LinkedHashSet<>();
+ private final Set<Artifact> instrumentationMetadata = new LinkedHashSet<>();
+ private Artifact compileTimeDependencies;
+ private Artifact runTimeDependencies;
+ private Artifact instrumentedJar;
+
+ public JavaCompilationArtifacts build() {
+ return new JavaCompilationArtifacts(ImmutableList.copyOf(runtimeJars),
+ ImmutableList.copyOf(compileTimeJars),
+ ImmutableList.copyOf(instrumentationMetadata),
+ compileTimeDependencies, runTimeDependencies, instrumentedJar);
+ }
+
+ public Builder addRuntimeJar(Artifact jar) {
+ this.runtimeJars.add(jar);
+ return this;
+ }
+
+ public Builder addRuntimeJars(Iterable<Artifact> jars) {
+ Iterables.addAll(this.runtimeJars, jars);
+ return this;
+ }
+
+ public Builder addCompileTimeJar(Artifact jar) {
+ this.compileTimeJars.add(jar);
+ return this;
+ }
+
+ public Builder addCompileTimeJars(Iterable<Artifact> jars) {
+ Iterables.addAll(this.compileTimeJars, jars);
+ return this;
+ }
+
+ public Builder addInstrumentationMetadata(Artifact instrumentationMetadata) {
+ this.instrumentationMetadata.add(instrumentationMetadata);
+ return this;
+ }
+
+ public Builder setCompileTimeDependencies(@Nullable Artifact compileTimeDependencies) {
+ this.compileTimeDependencies = compileTimeDependencies;
+ return this;
+ }
+
+ public Builder setRunTimeDependencies(@Nullable Artifact runTimeDependencies) {
+ this.runTimeDependencies = runTimeDependencies;
+ return this;
+ }
+
+ public Builder setInstrumentedJar(@Nullable Artifact instrumentedJar) {
+ this.instrumentedJar = instrumentedJar;
+ return this;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaCompilationHelper.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaCompilationHelper.java
new file mode 100644
index 0000000..181dd12
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaCompilationHelper.java
@@ -0,0 +1,436 @@
+// Copyright 2014 Google Inc. 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.rules.java;
+
+import static com.google.devtools.build.lib.analysis.config.BuildConfiguration.StrictDepsMode.OFF;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.AnalysisUtils;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration.StrictDepsMode;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.rules.java.JavaCompilationArgs.ClasspathType;
+import com.google.devtools.build.lib.rules.java.JavaConfiguration.JavaClasspathMode;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+/**
+ * A helper class for compiling Java targets. It contains method to create the
+ * various intermediate Artifacts for using ijars and source ijars.
+ * <p>
+ * Also supports the creation of resource and source only Jars.
+ */
+public class JavaCompilationHelper extends BaseJavaCompilationHelper {
+ private Artifact outputDepsProtoArtifact;
+ private JavaTargetAttributes.Builder attributes;
+ private JavaTargetAttributes builtAttributes;
+ private final ImmutableList<String> customJavacOpts;
+ private final List<Artifact> translations = new ArrayList<>();
+ private boolean translationsFrozen = false;
+ private final JavaSemantics semantics;
+
+ public JavaCompilationHelper(RuleContext ruleContext, JavaSemantics semantics,
+ ImmutableList<String> javacOpts, JavaTargetAttributes.Builder attributes) {
+ super(ruleContext);
+ this.attributes = attributes;
+ this.customJavacOpts = javacOpts;
+ this.semantics = semantics;
+ }
+
+ public JavaCompilationHelper(RuleContext ruleContext, JavaSemantics semantics,
+ JavaTargetAttributes.Builder attributes) {
+ this(ruleContext, semantics, getDefaultJavacOptsFromRule(ruleContext), attributes);
+ }
+
+ public JavaTargetAttributes getAttributes() {
+ if (builtAttributes == null) {
+ builtAttributes = attributes.build();
+ }
+ return builtAttributes;
+ }
+
+ /**
+ * Creates the Action that compiles Java source files.
+ *
+ * @param outputJar the class jar Artifact to create with the Action
+ * @param gensrcOutputJar the generated sources jar Artifact to create with the Action
+ * (null if no sources will be generated).
+ * @param outputDepsProto the compiler-generated jdeps file to create with the Action
+ * (null if not requested)
+ * @param outputMetadata metadata file (null if no instrumentation is needed).
+ */
+ public void createCompileAction(Artifact outputJar, @Nullable Artifact gensrcOutputJar,
+ @Nullable Artifact outputDepsProto, @Nullable Artifact outputMetadata) {
+ JavaTargetAttributes attributes = getAttributes();
+ List<String> javacOpts = getJavacOpts();
+ JavaCompileAction.Builder builder = createJavaCompileActionBuilder(semantics);
+ builder.setClasspathEntries(attributes.getCompileTimeClassPath());
+ builder.addResources(attributes.getResources());
+ builder.addClasspathResources(attributes.getClassPathResources());
+ // Only add default bootclasspath entries if not explicitly set in attributes.
+ if (!attributes.getBootClassPath().isEmpty()) {
+ builder.setBootclasspathEntries(attributes.getBootClassPath());
+ } else {
+ builder.setBootclasspathEntries(getBootClasspath());
+ }
+ builder.setLangtoolsJar(getLangtoolsJar());
+ builder.setJavaBuilderJar(getJavaBuilderJar());
+ builder.addTranslations(getTranslations());
+ builder.setOutputJar(outputJar);
+ builder.setGensrcOutputJar(gensrcOutputJar);
+ builder.setOutputDepsProto(outputDepsProto);
+ builder.setMetadata(outputMetadata);
+ builder.addSourceFiles(attributes.getSourceFiles());
+ builder.addSourceJars(attributes.getSourceJars());
+ builder.setJavacOpts(javacOpts);
+ builder.setCompressJar(true);
+ builder.setClassDirectory(outputDir(outputJar));
+ builder.setSourceGenDirectory(sourceGenDir(outputJar));
+ builder.setTempDirectory(tempDir(outputJar));
+ builder.addProcessorPaths(attributes.getProcessorPath());
+ builder.addProcessorNames(attributes.getProcessorNames());
+ builder.setStrictJavaDeps(attributes.getStrictJavaDeps());
+ builder.addDirectJars(attributes.getDirectJars());
+ builder.addCompileTimeDependencyArtifacts(attributes.getCompileTimeDependencyArtifacts());
+ builder.setRuleKind(attributes.getRuleKind());
+ builder.setTargetLabel(attributes.getTargetLabel());
+ getAnalysisEnvironment().registerAction(builder.build());
+ }
+
+ /**
+ * Creates the Action that compiles Java source files and optionally instruments them for
+ * coverage.
+ *
+ * @param outputJar the class jar Artifact to create with the Action
+ * @param gensrcJar the generated sources jar Artifact to create with the Action
+ * @param outputDepsProto the compiler-generated jdeps file to create with the Action
+ * @param javaArtifactsBuilder the build to store the instrumentation metadata in
+ */
+ public void createCompileActionWithInstrumentation(Artifact outputJar, Artifact gensrcJar,
+ Artifact outputDepsProto, JavaCompilationArtifacts.Builder javaArtifactsBuilder) {
+ createCompileAction(outputJar, gensrcJar, outputDepsProto,
+ createInstrumentationMetadata(outputJar, javaArtifactsBuilder));
+ }
+
+ /**
+ * Creates the instrumentation metadata artifact if needed.
+ *
+ * @return the instrumentation metadata artifact or null if instrumentation is
+ * disabled
+ */
+ public Artifact createInstrumentationMetadata(Artifact outputJar,
+ JavaCompilationArtifacts.Builder javaArtifactsBuilder) {
+ // If we need to instrument the jar, add additional output (the coverage metadata file) to the
+ // JavaCompileAction.
+ Artifact instrumentationMetadata = null;
+ if (shouldInstrumentJar()) {
+ instrumentationMetadata = semantics.createInstrumentationMetadataArtifact(
+ getAnalysisEnvironment(), outputJar);
+
+ if (instrumentationMetadata != null) {
+ javaArtifactsBuilder.addInstrumentationMetadata(instrumentationMetadata);
+ }
+ }
+ return instrumentationMetadata;
+ }
+
+ private boolean shouldInstrumentJar() {
+ // TODO(bazel-team): What about source jars?
+ return getConfiguration().isCodeCoverageEnabled() && attributes.hasSourceFiles() &&
+ getConfiguration().getInstrumentationFilter().isIncluded(
+ getRuleContext().getLabel().toString());
+ }
+
+ /**
+ * Returns the artifact for a jar file containing source files that were generated by an
+ * annotation processor or null if no annotation processors are used.
+ */
+ public Artifact createGensrcJar(@Nullable Artifact outputJar) {
+ if (!usesAnnotationProcessing()) {
+ return null;
+ }
+ return getAnalysisEnvironment().getDerivedArtifact(
+ FileSystemUtils.appendWithoutExtension(outputJar.getRootRelativePath(), "-gensrc"),
+ outputJar.getRoot());
+ }
+
+ /**
+ * Returns whether this target uses annotation processing.
+ */
+ private boolean usesAnnotationProcessing() {
+ JavaTargetAttributes attributes = getAttributes();
+ return getJavacOpts().contains("-processor") || !attributes.getProcessorNames().isEmpty();
+ }
+
+ public Artifact getOutputDepsProtoArtifact() {
+ return outputDepsProtoArtifact;
+ }
+ /**
+ * Creates the jdeps file artifact if needed. Returns null if the target can't emit dependency
+ * information (i.e there is no compilation step, the target acts as an alias).
+ *
+ * @param outputJar output jar artifact used to derive the name
+ * @return the jdeps file artifact or null if the target can't generate such a file
+ */
+ public Artifact createOutputDepsProtoArtifact(Artifact outputJar,
+ JavaCompilationArtifacts.Builder builder) {
+ if (!generatesOutputDeps()) {
+ return null;
+ }
+
+ outputDepsProtoArtifact = getAnalysisEnvironment().getDerivedArtifact(
+ FileSystemUtils.replaceExtension(outputJar.getRootRelativePath(), ".jdeps"),
+ outputJar.getRoot());
+
+ builder.setRunTimeDependencies(outputDepsProtoArtifact);
+ return outputDepsProtoArtifact;
+ }
+
+ /**
+ * Returns whether this target emits dependency information. Compilation must occur, so certain
+ * targets acting as aliases have to be filtered out.
+ */
+ private boolean generatesOutputDeps() {
+ return getJavaConfiguration().getGenerateJavaDeps() &&
+ (attributes.hasSourceFiles() || attributes.hasSourceJars());
+ }
+
+ /**
+ * Creates an Action that packages all of the resources into a Jar. This
+ * includes the declared resources, the classpath resources and the translated
+ * messages.
+ *
+ * <p>The resource jar artifact is derived from the given original jar, by
+ * prepending the given prefix and appending the given suffix. The new jar
+ * uses the same root as the original jar.
+ */
+ // TODO(bazel-team): Extract this method to make it easier to create simple
+ // zip/jar archives without having to first create a JavaCompilationhelper and
+ // JavaTargetAttributes.
+ public Artifact createResourceJarAction(Artifact resourceJar) {
+ JavaTargetAttributes attributes = getAttributes();
+ JavaCompileAction.Builder builder = createJavaCompileActionBuilder(semantics);
+ builder.setOutputJar(resourceJar);
+ builder.addResources(attributes.getResources());
+ builder.addClasspathResources(attributes.getClassPathResources());
+ builder.setLangtoolsJar(getLangtoolsJar());
+ builder.addTranslations(getTranslations());
+ builder.setCompressJar(true);
+ builder.setClassDirectory(outputDir(resourceJar));
+ builder.setTempDirectory(tempDir(resourceJar));
+ builder.setJavaBuilderJar(getJavaBuilderJar());
+ getAnalysisEnvironment().registerAction(builder.build());
+ return resourceJar;
+ }
+
+ /**
+ * Creates an Action that packages the Java source files into a Jar. If {@code gensrcJar} is
+ * non-null, includes the contents of the {@code gensrcJar} with the output source jar.
+ *
+ * @param outputJar the Artifact to create with the Action
+ * @param gensrcJar the generated sources jar Artifact that should be included with the
+ * sources in the output Artifact. May be null.
+ */
+ public void createSourceJarAction(Artifact outputJar, @Nullable Artifact gensrcJar) {
+ JavaTargetAttributes attributes = getAttributes();
+ Collection<Artifact> resourceJars = new ArrayList<>(attributes.getSourceJars());
+ if (gensrcJar != null) {
+ resourceJars.add(gensrcJar);
+ }
+ createSourceJarAction(semantics, attributes.getSourceFiles(), resourceJars, outputJar);
+ }
+
+ /**
+ * Creates the actions that produce the interface jar. Adds the jar artifacts to the given
+ * JavaCompilationArtifacts builder.
+ */
+ public void createCompileTimeJarAction(Artifact runtimeJar,
+ @Nullable Artifact runtimeDeps, JavaCompilationArtifacts.Builder builder) {
+ Artifact jar = getJavaConfiguration().getUseIjars()
+ ? createIjarAction(runtimeJar, false)
+ : runtimeJar;
+ Artifact deps = runtimeDeps;
+
+ builder.addCompileTimeJar(jar);
+ builder.setCompileTimeDependencies(deps);
+ }
+
+ /**
+ * Creates actions that create ijars from generated jars that are an input to
+ * the Java target.
+ *
+ * @return the generated ijars or original jars that are not generated by a
+ * genrule
+ */
+ public Iterable<Artifact> filterGeneratedJarsThroughIjar(Iterable<Artifact> jars) {
+ if (!getJavaConfiguration().getUseIjars()) {
+ return jars;
+ }
+ // We need to copy this list in order to avoid generating a new action each time the iterator
+ // is enumerated
+ return ImmutableList.copyOf(Iterables.transform(jars, new Function<Artifact, Artifact>() {
+ @Override
+ public Artifact apply(Artifact jar) {
+ return !jar.isSourceArtifact() ? createIjarAction(jar, true) : jar;
+ }
+ }));
+ }
+
+ private void addArgsAndJarsToAttributes(JavaCompilationArgs args, Iterable<Artifact> directJars) {
+ // Can only be non-null when isStrict() returns true.
+ if (directJars != null) {
+ attributes.addDirectCompileTimeClassPathEntries(directJars);
+ attributes.addDirectJars(directJars);
+ }
+
+ attributes.merge(args);
+ }
+
+ private void addLibrariesToAttributesInternal(Iterable<? extends TransitiveInfoCollection> deps) {
+ JavaCompilationArgs args = JavaCompilationArgs.builder()
+ .addTransitiveTargets(deps).build();
+
+ NestedSet<Artifact> directJars = isStrict()
+ ? getNonRecursiveCompileTimeJarsFromCollection(deps)
+ : null;
+ addArgsAndJarsToAttributes(args, directJars);
+ }
+
+ private void addProvidersToAttributesInternal(
+ Iterable<? extends SourcesJavaCompilationArgsProvider> deps, boolean isNeverLink) {
+ JavaCompilationArgs args = JavaCompilationArgs.builder()
+ .addSourcesTransitiveCompilationArgs(deps, true,
+ isNeverLink ? ClasspathType.COMPILE_ONLY : ClasspathType.BOTH)
+ .build();
+
+ NestedSet<Artifact> directJars = isStrict()
+ ? getNonRecursiveCompileTimeJarsFromProvider(deps, isNeverLink)
+ : null;
+ addArgsAndJarsToAttributes(args, directJars);
+ }
+
+ private boolean isStrict() {
+ return getStrictJavaDeps() != OFF;
+ }
+
+ private NestedSet<Artifact> getNonRecursiveCompileTimeJarsFromCollection(
+ Iterable<? extends TransitiveInfoCollection> deps) {
+ JavaCompilationArgs.Builder builder = JavaCompilationArgs.builder();
+ builder.addTransitiveTargets(deps, /*recursive=*/false);
+ return builder.build().getCompileTimeJars();
+ }
+
+ private NestedSet<Artifact> getNonRecursiveCompileTimeJarsFromProvider(
+ Iterable<? extends SourcesJavaCompilationArgsProvider> deps, boolean isNeverLink) {
+ return JavaCompilationArgs.builder()
+ .addSourcesTransitiveCompilationArgs(deps, false,
+ isNeverLink ? ClasspathType.COMPILE_ONLY : ClasspathType.BOTH)
+ .build().getCompileTimeJars();
+ }
+
+ private void addDependencyArtifactsToAttributes(
+ Iterable<? extends TransitiveInfoCollection> deps) {
+ NestedSetBuilder<Artifact> compileTimeBuilder = NestedSetBuilder.stableOrder();
+ NestedSetBuilder<Artifact> runTimeBuilder = NestedSetBuilder.stableOrder();
+ for (JavaCompilationArgsProvider provider : AnalysisUtils.getProviders(
+ deps, JavaCompilationArgsProvider.class)) {
+ compileTimeBuilder.addTransitive(provider.getCompileTimeJavaDependencyArtifacts());
+ runTimeBuilder.addTransitive(provider.getRunTimeJavaDependencyArtifacts());
+ }
+ attributes.addCompileTimeDependencyArtifacts(compileTimeBuilder.build());
+ attributes.addRuntimeDependencyArtifacts(runTimeBuilder.build());
+ }
+
+ /**
+ * Adds the compile time and runtime Java libraries in the transitive closure
+ * of the deps to the attributes.
+ *
+ * @param deps the dependencies to be included as roots of the transitive
+ * closure
+ */
+ public void addLibrariesToAttributes(Iterable<? extends TransitiveInfoCollection> deps) {
+ // Enforcing strict Java dependencies: when the --strict_java_deps flag is
+ // WARN or ERROR, or is DEFAULT and strict_java_deps attribute is unset,
+ // we use a stricter javac compiler to perform direct deps checks.
+ attributes.setStrictJavaDeps(getStrictJavaDeps());
+ addLibrariesToAttributesInternal(deps);
+
+ JavaClasspathMode classpathMode = getJavaConfiguration().getReduceJavaClasspath();
+ if (isStrict() && classpathMode != JavaClasspathMode.OFF) {
+ addDependencyArtifactsToAttributes(deps);
+ }
+ }
+
+ public void addProvidersToAttributes(Iterable<? extends SourcesJavaCompilationArgsProvider> deps,
+ boolean isNeverLink) {
+ // see addLibrariesToAttributes() for explanation
+ attributes.setStrictJavaDeps(getStrictJavaDeps());
+ addProvidersToAttributesInternal(deps, isNeverLink);
+ }
+
+ /**
+ * Determines whether to enable strict_java_deps.
+ *
+ * @return filtered command line flag value, defaulting to ERROR
+ */
+ public StrictDepsMode getStrictJavaDeps() {
+ return getJavaConfiguration().getFilteredStrictJavaDeps();
+ }
+
+ /**
+ * Gets the value of the "javacopts" attribute combining them with the
+ * default options. If the current rule has no javacopts attribute, this
+ * method only returns the default options.
+ */
+ @VisibleForTesting
+ ImmutableList<String> getJavacOpts() {
+ return customJavacOpts;
+ }
+
+ /**
+ * Obtains the standard list of javac opts needed to build {@code rule}.
+ *
+ * This method must only be called during initialization.
+ *
+ * @param ruleContext a rule context
+ * @return a list of options to provide to javac
+ */
+ private static ImmutableList<String> getDefaultJavacOptsFromRule(RuleContext ruleContext) {
+ return ImmutableList.copyOf(Iterables.concat(
+ JavaToolchainProvider.getDefaultJavacOptions(ruleContext),
+ ruleContext.getTokenizedStringListAttr("javacopts")));
+ }
+
+ public void addTranslations(Collection<Artifact> translations) {
+ Preconditions.checkArgument(!translationsFrozen);
+ this.translations.addAll(translations);
+ }
+
+ private ImmutableList<Artifact> getTranslations() {
+ translationsFrozen = true;
+ return ImmutableList.copyOf(translations);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaCompileAction.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaCompileAction.java
new file mode 100644
index 0000000..655270b
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaCompileAction.java
@@ -0,0 +1,1021 @@
+// Copyright 2014 Google Inc. 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.rules.java;
+
+import static java.nio.charset.StandardCharsets.ISO_8859_1;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.AbstractAction;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.actions.ActionExecutionContext;
+import com.google.devtools.build.lib.actions.ActionExecutionException;
+import com.google.devtools.build.lib.actions.ActionInput;
+import com.google.devtools.build.lib.actions.ActionInputHelper;
+import com.google.devtools.build.lib.actions.ActionOwner;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.BaseSpawn;
+import com.google.devtools.build.lib.actions.EnvironmentalExecException;
+import com.google.devtools.build.lib.actions.ExecException;
+import com.google.devtools.build.lib.actions.Executor;
+import com.google.devtools.build.lib.actions.ParameterFile;
+import com.google.devtools.build.lib.actions.ResourceSet;
+import com.google.devtools.build.lib.actions.Spawn;
+import com.google.devtools.build.lib.actions.SpawnActionContext;
+import com.google.devtools.build.lib.actions.extra.ExtraActionInfo;
+import com.google.devtools.build.lib.actions.extra.JavaCompileInfo;
+import com.google.devtools.build.lib.analysis.AnalysisEnvironment;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.actions.CommandLine;
+import com.google.devtools.build.lib.analysis.actions.CustomCommandLine;
+import com.google.devtools.build.lib.analysis.actions.CustomCommandLine.CustomArgv;
+import com.google.devtools.build.lib.analysis.actions.CustomCommandLine.CustomMultiArgv;
+import com.google.devtools.build.lib.analysis.actions.ParameterFileWriteAction;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.collect.ImmutableIterable;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadCompatible;
+import com.google.devtools.build.lib.rules.java.JavaConfiguration.JavaClasspathMode;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.util.Fingerprint;
+import com.google.devtools.build.lib.util.ShellEscaper;
+import com.google.devtools.build.lib.util.StringCanonicalizer;
+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.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashSet;
+import java.util.List;
+
+/**
+ * Action that represents a Java compilation.
+ */
+@ThreadCompatible
+public class JavaCompileAction extends AbstractAction {
+
+ private static final String GUID = "786e174d-ed97-4e79-9f61-ae74430714cf";
+
+ private static final ResourceSet LOCAL_RESOURCES =
+ new ResourceSet(750 /*MB*/, 0.5 /*CPU*/, 0.0 /*IO*/);
+
+ private final CommandLine javaCompileCommandLine;
+ private final CommandLine commandLine;
+
+ /**
+ * The directory in which generated classfiles are placed.
+ * May be erased/created by the JavaBuilder.
+ */
+ private final PathFragment classDirectory;
+
+ private final Artifact outputJar;
+
+ /**
+ * The list of classpath entries to specify to javac.
+ */
+ private final NestedSet<Artifact> classpath;
+
+ /**
+ * The list of classpath entries to search for annotation processors.
+ */
+ private final ImmutableList<Artifact> processorPath;
+
+ /**
+ * The list of annotation processor classes to run.
+ */
+ private final ImmutableList<String> processorNames;
+
+ /**
+ * The translation messages.
+ */
+ private final ImmutableList<Artifact> messages;
+
+ /**
+ * The set of resources to put into the jar.
+ */
+ private final ImmutableList<Artifact> resources;
+
+ /**
+ * The set of classpath resources to put into the jar.
+ */
+ private final ImmutableList<Artifact> classpathResources;
+
+ /**
+ * The set of files which contain lists of additional Java source files to
+ * compile.
+ */
+ private final ImmutableList<Artifact> sourceJars;
+
+ /**
+ * The set of explicit Java source files to compile.
+ */
+ private final ImmutableList<Artifact> sourceFiles;
+
+ /**
+ * The compiler options to pass to javac.
+ */
+ private final ImmutableList<String> javacOpts;
+
+ /**
+ * The subset of classpath jars provided by direct dependencies.
+ */
+ private final ImmutableList<Artifact> directJars;
+
+ /**
+ * The level of strict dependency checks (off, warnings, or errors).
+ */
+ private final BuildConfiguration.StrictDepsMode strictJavaDeps;
+
+ /**
+ * The set of .deps artifacts provided by direct dependencies.
+ */
+ private final ImmutableList<Artifact> compileTimeDependencyArtifacts;
+
+ /**
+ * The java semantics to get the list of action outputs.
+ */
+ private final JavaSemantics semantics;
+
+ /**
+ * Constructs an action to compile a set of Java source files to class files.
+ *
+ * @param owner the action owner, typically a java_* RuleConfiguredTarget.
+ * @param baseInputs the set of the input artifacts of the compile action
+ * without the parameter file action;
+ * @param outputs the outputs of the action
+ * @param javaCompileCommandLine the command line for the java library
+ * builder - it's actually written to the parameter file, but other
+ * parts (for example, ide_build_info) need access to the data
+ * @param commandLine the actual invocation command line
+ */
+ private JavaCompileAction(ActionOwner owner,
+ Iterable<Artifact> baseInputs,
+ Collection<Artifact> outputs,
+ CommandLine javaCompileCommandLine,
+ CommandLine commandLine,
+ PathFragment classDirectory,
+ Artifact outputJar,
+ NestedSet<Artifact> classpath,
+ List<Artifact> processorPath,
+ Artifact langtoolsJar,
+ Artifact javaBuilderJar,
+ List<String> processorNames,
+ Collection<Artifact> messages,
+ Collection<Artifact> resources,
+ Collection<Artifact> classpathResources,
+ Collection<Artifact> sourceJars,
+ Collection<Artifact> sourceFiles,
+ List<String> javacOpts,
+ Collection<Artifact> directJars,
+ BuildConfiguration.StrictDepsMode strictJavaDeps,
+ Collection<Artifact> compileTimeDependencyArtifacts,
+ JavaSemantics semantics) {
+ super(owner, Iterables.concat(ImmutableList.of(
+ classpath, processorPath, messages, resources,
+ classpathResources, sourceJars, sourceFiles, compileTimeDependencyArtifacts,
+ ImmutableList.of(langtoolsJar, javaBuilderJar), baseInputs)),
+ outputs);
+ this.javaCompileCommandLine = javaCompileCommandLine;
+ this.commandLine = commandLine;
+
+ this.classDirectory = Preconditions.checkNotNull(classDirectory);
+ this.outputJar = outputJar;
+ this.classpath = classpath;
+ this.processorPath = ImmutableList.copyOf(processorPath);
+ this.processorNames = ImmutableList.copyOf(processorNames);
+ this.messages = ImmutableList.copyOf(messages);
+ this.resources = ImmutableList.copyOf(resources);
+ this.classpathResources = ImmutableList.copyOf(classpathResources);
+ this.sourceJars = ImmutableList.copyOf(sourceJars);
+ this.sourceFiles = ImmutableList.copyOf(sourceFiles);
+ this.javacOpts = ImmutableList.copyOf(javacOpts);
+ this.directJars = ImmutableList.copyOf(directJars);
+ this.strictJavaDeps = strictJavaDeps;
+ this.compileTimeDependencyArtifacts = ImmutableList.copyOf(compileTimeDependencyArtifacts);
+ this.semantics = semantics;
+ }
+
+ /**
+ * Returns the given (passed to constructor) source files.
+ */
+ @VisibleForTesting
+ public Collection<Artifact> getSourceFiles() {
+ return sourceFiles;
+ }
+
+ /**
+ * Returns the list of paths that represent the resources to be added to the
+ * jar.
+ */
+ @VisibleForTesting
+ public Collection<Artifact> getResources() {
+ return resources;
+ }
+
+ /**
+ * Returns the list of paths that represents the classpath.
+ */
+ @VisibleForTesting
+ public Iterable<Artifact> getClasspath() {
+ return classpath;
+ }
+
+ /**
+ * Returns the list of paths that represents the source jars.
+ */
+ @VisibleForTesting
+ public Collection<Artifact> getSourceJars() {
+ return sourceJars;
+ }
+
+ /**
+ * Returns the list of paths that represents the processor path.
+ */
+ @VisibleForTesting
+ public List<Artifact> getProcessorpath() {
+ return processorPath;
+ }
+
+ @VisibleForTesting
+ public List<String> getJavacOpts() {
+ return javacOpts;
+ }
+
+ @VisibleForTesting
+ public Collection<Artifact> getDirectJars() {
+ return directJars;
+ }
+
+ @VisibleForTesting
+ public Collection<Artifact> getCompileTimeDependencyArtifacts() {
+ return compileTimeDependencyArtifacts;
+ }
+
+ @VisibleForTesting
+ public BuildConfiguration.StrictDepsMode getStrictJavaDepsMode() {
+ return strictJavaDeps;
+ }
+
+ public PathFragment getClassDirectory() {
+ return classDirectory;
+ }
+
+ /**
+ * Returns the list of class names of processors that should
+ * be run.
+ */
+ @VisibleForTesting
+ public List<String> getProcessorNames() {
+ return processorNames;
+ }
+
+ /**
+ * Returns the output jar artifact that gets generated by archiving the
+ * results of the Java compilation and the declared resources.
+ */
+ public Artifact getOutputJar() {
+ return outputJar;
+ }
+
+ @Override
+ public Artifact getPrimaryOutput() {
+ return getOutputJar();
+ }
+
+ /**
+ * Constructs a command line that can be used to invoke the
+ * JavaBuilder.
+ *
+ * <p>Do not use this method, except for testing (and for the in-process
+ * strategy).
+ */
+ @VisibleForTesting
+ public Iterable<String> buildCommandLine() {
+ return javaCompileCommandLine.arguments();
+ }
+
+ /**
+ * Returns the command and arguments for a java compile action.
+ */
+ public List<String> getCommand() {
+ return ImmutableList.copyOf(commandLine.arguments());
+ }
+
+ @Override
+ @ThreadCompatible
+ public void execute(ActionExecutionContext actionExecutionContext)
+ throws ActionExecutionException, InterruptedException {
+ Executor executor = actionExecutionContext.getExecutor();
+ try {
+ List<ActionInput> outputs = new ArrayList<>();
+ outputs.addAll(getOutputs());
+ // Add a few useful side-effect output files to the list to retrieve.
+ // TODO(bazel-team): Just make these Artifacts.
+ PathFragment classDirectory = getClassDirectory();
+ outputs.addAll(semantics.getExtraJavaCompileOutputs(classDirectory));
+ outputs.add(ActionInputHelper.fromPath(classDirectory.getChild("srclist").getPathString()));
+
+ try {
+ // Make sure the directories exist, else the distributor will bomb.
+ Path classDirectoryPath = executor.getExecRoot().getRelative(getClassDirectory());
+ FileSystemUtils.createDirectoryAndParents(classDirectoryPath);
+ } catch (IOException e) {
+ throw new EnvironmentalExecException(e.getMessage());
+ }
+
+ final ImmutableList<ActionInput> finalOutputs = ImmutableList.copyOf(outputs);
+ Spawn spawn = new BaseSpawn(getCommand(), ImmutableMap.<String, String>of(),
+ ImmutableMap.<String, String>of(), this, LOCAL_RESOURCES) {
+ @Override
+ public Collection<? extends ActionInput> getOutputFiles() {
+ return finalOutputs;
+ }
+ };
+
+ executor.getSpawnActionContext(getMnemonic()).exec(spawn, actionExecutionContext);
+ } catch (ExecException e) {
+ throw e.toActionExecutionException("Java compilation in rule '" + getOwner().getLabel() + "'",
+ executor.getVerboseFailures(), this);
+ }
+ }
+
+ @Override
+ protected String computeKey() {
+ Fingerprint f = new Fingerprint();
+ f.addString(GUID);
+ f.addStrings(commandLine.arguments());
+ return f.hexDigestAndReset();
+ }
+
+ @Override
+ public String describeKey() {
+ StringBuilder message = new StringBuilder();
+ for (String arg : ShellEscaper.escapeAll(commandLine.arguments())) {
+ message.append(" Command-line argument: ");
+ message.append(arg);
+ message.append('\n');
+ }
+ return message.toString();
+ }
+
+ @Override
+ public String getMnemonic() {
+ return "Javac";
+ }
+
+ @Override
+ protected String getRawProgressMessage() {
+ int count = sourceFiles.size();
+ if (count == 0) { // nothing to compile, just bundling resources and messages
+ count = resources.size() + classpathResources.size() + messages.size();
+ }
+ return "Building " + outputJar.prettyPrint() + " (" + count + " files)";
+ }
+
+ @Override
+ public String describeStrategy(Executor executor) {
+ return getContext(executor).strategyLocality(getMnemonic(), true);
+ }
+
+ @Override
+ public ResourceSet estimateResourceConsumption(Executor executor) {
+ SpawnActionContext context = getContext(executor);
+ if (context.isRemotable(getMnemonic(), true)) {
+ return ResourceSet.ZERO;
+ }
+ return LOCAL_RESOURCES;
+ }
+
+ protected SpawnActionContext getContext(Executor executor) {
+ return executor.getSpawnActionContext(getMnemonic());
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder result = new StringBuilder();
+ result.append("JavaBuilder ");
+ Joiner.on(' ').appendTo(result, commandLine.arguments());
+ return result.toString();
+ }
+
+ @Override
+ public ExtraActionInfo.Builder getExtraActionInfo() {
+ JavaCompileInfo.Builder info = JavaCompileInfo.newBuilder();
+ info.addAllSourceFile(Artifact.toExecPaths(getSourceFiles()));
+ info.addAllClasspath(Artifact.toExecPaths(getClasspath()));
+ info.addClasspath(getClassDirectory().getPathString());
+ info.addAllSourcepath(Artifact.toExecPaths(getSourceJars()));
+ info.addAllJavacOpt(getJavacOpts());
+ info.addAllProcessor(getProcessorNames());
+ info.addAllProcessorpath(Artifact.toExecPaths(getProcessorpath()));
+ info.setOutputjar(getOutputJar().getExecPathString());
+
+ return super.getExtraActionInfo()
+ .setExtension(JavaCompileInfo.javaCompileInfo, info.build());
+ }
+
+ /**
+ * Creates an instance.
+ *
+ * @param configuration the build configuration, which provides the default options and the path
+ * to the compiler, etc.
+ * @param classDirectory the directory in which generated classfiles are placed relative to the
+ * exec root
+ * @param sourceGenDirectory the directory where source files generated by annotation processors
+ * should be stored.
+ * @param tempDirectory a directory in which the library builder can store temporary files
+ * relative to the exec root
+ * @param outputJar output jar
+ * @param compressJar if true compress the output jar
+ * @param outputDepsProto the proto file capturing dependency information
+ * @param classpath the complete classpath, the directory in which generated classfiles are placed
+ * @param processorPath the classpath where javac should search for annotation processors
+ * @param processorNames the classes that javac should use as annotation processors
+ * @param messages the message files for translation
+ * @param resources the set of resources to put into the jar
+ * @param classpathResources the set of classpath resources to put into the jar
+ * @param sourceJars the set of jars containing additional source files to compile
+ * @param sourceFiles the set of explicit Java source files to compile
+ * @param javacOpts the compiler options to pass to javac
+ */
+ private static CustomCommandLine.Builder javaCompileCommandLine(
+ final JavaSemantics semantics,
+ final BuildConfiguration configuration,
+ final PathFragment classDirectory,
+ final PathFragment sourceGenDirectory,
+ PathFragment tempDirectory,
+ Artifact outputJar,
+ Artifact gensrcOutputJar,
+ boolean compressJar,
+ Artifact outputDepsProto,
+ final NestedSet<Artifact> classpath,
+ List<Artifact> processorPath,
+ Artifact langtoolsJar,
+ Artifact javaBuilderJar,
+ List<String> processorNames,
+ Collection<Artifact> messages,
+ Collection<Artifact> resources,
+ Collection<Artifact> classpathResources,
+ Collection<Artifact> sourceJars,
+ Collection<Artifact> sourceFiles,
+ List<String> javacOpts,
+ final Collection<Artifact> directJars,
+ BuildConfiguration.StrictDepsMode strictJavaDeps,
+ Collection<Artifact> compileTimeDependencyArtifacts,
+ String ruleKind,
+ Label targetLabel) {
+ Preconditions.checkNotNull(classDirectory);
+ Preconditions.checkNotNull(tempDirectory);
+ Preconditions.checkNotNull(langtoolsJar);
+ Preconditions.checkNotNull(javaBuilderJar);
+
+ CustomCommandLine.Builder result = CustomCommandLine.builder();
+
+ result.add("--classdir").addPath(classDirectory);
+
+ result.add("--tempdir").addPath(tempDirectory);
+
+ if (outputJar != null) {
+ result.addExecPath("--output", outputJar);
+ }
+
+ if (gensrcOutputJar != null) {
+ result.add("--sourcegendir").addPath(sourceGenDirectory);
+ result.addExecPath("--generated_sources_output", gensrcOutputJar);
+ }
+
+ if (compressJar) {
+ result.add("--compress_jar");
+ }
+
+ if (outputDepsProto != null) {
+ result.addExecPath("--output_deps_proto", outputDepsProto);
+ }
+
+ result.add("--classpath").add(new CustomArgv() {
+ @Override
+ public String argv() {
+ List<PathFragment> classpathEntries = new ArrayList<>();
+ for (Artifact classpathArtifact : classpath) {
+ classpathEntries.add(classpathArtifact.getExecPath());
+ }
+ classpathEntries.add(classDirectory);
+ return Joiner.on(configuration.getHostPathSeparator()).join(classpathEntries);
+ }
+ });
+
+ if (!processorPath.isEmpty()) {
+ result.addJoinExecPaths("--processorpath",
+ configuration.getHostPathSeparator(), processorPath);
+ }
+
+ if (!processorNames.isEmpty()) {
+ result.add("--processors", processorNames);
+ }
+
+ if (!messages.isEmpty()) {
+ result.add("--messages");
+ for (Artifact message : messages) {
+ addAsResourcePrefixedExecPath(semantics, message, result);
+ }
+ }
+
+ if (!resources.isEmpty()) {
+ result.add("--resources");
+ for (Artifact resource : resources) {
+ addAsResourcePrefixedExecPath(semantics, resource, result);
+ }
+ }
+
+ if (!classpathResources.isEmpty()) {
+ result.addExecPaths("--classpath_resources", classpathResources);
+ }
+
+ if (!sourceJars.isEmpty()) {
+ result.addExecPaths("--source_jars", sourceJars);
+ }
+
+ result.addExecPaths("--sources", sourceFiles);
+
+ if (!javacOpts.isEmpty()) {
+ result.add("--javacopts", javacOpts);
+ }
+
+ // strict_java_deps controls whether the mapping from jars to targets is
+ // written out and whether we try to minimize the compile-time classpath.
+ if (strictJavaDeps != BuildConfiguration.StrictDepsMode.OFF) {
+ result.add("--strict_java_deps");
+ result.add((semantics.useStrictJavaDeps(configuration) ? strictJavaDeps
+ : BuildConfiguration.StrictDepsMode.OFF).toString());
+ result.add(new CustomMultiArgv() {
+ @Override
+ public Iterable<String> argv() {
+ return addJarsToTargets(classpath, directJars);
+ }
+ });
+
+ if (configuration.getFragment(JavaConfiguration.class).getReduceJavaClasspath()
+ == JavaClasspathMode.JAVABUILDER) {
+ result.add("--reduce_classpath");
+
+ if (!compileTimeDependencyArtifacts.isEmpty()) {
+ result.addExecPaths("--deps_artifacts", compileTimeDependencyArtifacts);
+ }
+ }
+ }
+
+ if (ruleKind != null) {
+ result.add("--rule_kind");
+ result.add(ruleKind);
+ }
+ if (targetLabel != null) {
+ result.add("--target_label");
+ if (targetLabel.getPackageIdentifier().getRepository().isDefault()) {
+ result.add(targetLabel.toString());
+ } else {
+ // @-prefixed strings will be assumed to be filenames and expanded by
+ // {@link JavaLibraryBuildRequest}, so add an extra &at; to escape it.
+ result.add("@" + targetLabel);
+ }
+ }
+
+ return result;
+ }
+
+ private static void addAsResourcePrefixedExecPath(JavaSemantics semantics,
+ Artifact artifact, CustomCommandLine.Builder builder) {
+ PathFragment execPath = artifact.getExecPath();
+ PathFragment resourcePath = semantics.getJavaResourcePath(artifact.getRootRelativePath());
+ if (execPath.equals(resourcePath)) {
+ builder.addPaths(":%s", resourcePath);
+ } else {
+ // execPath must end with resourcePath in all cases
+ PathFragment rootPrefix = trimTail(execPath, resourcePath);
+ builder.addPaths("%s:%s", rootPrefix, resourcePath);
+ }
+ }
+
+ /**
+ * Returns the root-part of a given path by trimming off the end specified by
+ * a given tail. Assumes that the tail is known to match, and simply relies on
+ * the segment lengths.
+ */
+ private static PathFragment trimTail(PathFragment path, PathFragment tail) {
+ return path.subFragment(0, path.segmentCount() - tail.segmentCount());
+ }
+
+ /**
+ * Builds the list of mappings between jars on the classpath and their
+ * originating targets names.
+ */
+ private static ImmutableList<String> addJarsToTargets(
+ NestedSet<Artifact> classpath, Collection<Artifact> directJars) {
+ ImmutableList.Builder<String> builder = ImmutableList.builder();
+ for (Artifact jar : classpath) {
+ builder.add(directJars.contains(jar)
+ ? "--direct_dependency"
+ : "--indirect_dependency");
+ builder.add(jar.getExecPathString());
+ Label label = getTargetName(jar);
+ builder.add(label.getPackageIdentifier().getRepository().isDefault()
+ ? label.toString()
+ : label.toPathFragment().toString());
+ }
+ return builder.build();
+ }
+
+ /**
+ * Gets the name of the target that produced the given jar artifact.
+ *
+ * When specifying jars directly in the "srcs" attribute of a rule (mostly
+ * for third_party libraries), there is no generating action, so we just
+ * return the jar name in label form.
+ */
+ private static Label getTargetName(Artifact jar) {
+ return Preconditions.checkNotNull(jar.getOwner(), jar);
+ }
+
+ /**
+ * The actual command line executed for a compile action.
+ */
+ private static CommandLine spawnCommandLine(PathFragment javaExecutable, Artifact javaBuilderJar,
+ Artifact langtoolsJar, Artifact paramFile, ImmutableList<String> javaBuilderJvmFlags) {
+ Preconditions.checkNotNull(langtoolsJar);
+ Preconditions.checkNotNull(javaBuilderJar);
+ return CustomCommandLine.builder()
+ .addPath(javaExecutable)
+ // Langtools jar is placed on the boot classpath so that it can override classes
+ // in the JRE. Typically this has no effect since langtools.jar does not have
+ // classes in common with rt.jar. However, it is necessary when using a version
+ // of javac.jar generated via ant from the langtools build.xml that is of a
+ // different version than AND has an overlap in contents with the default
+ // run-time (eg while upgrading the Java version).
+ .addPaths("-Xbootclasspath/p:%s", langtoolsJar.getExecPath())
+ .add(javaBuilderJvmFlags)
+ .addExecPath("-jar", javaBuilderJar)
+ .addPaths("@%s", paramFile.getExecPath())
+ .build();
+ }
+
+ /**
+ * Builder class to construct Java compile actions.
+ */
+ public static class Builder {
+ private final ActionOwner owner;
+ private final AnalysisEnvironment analysisEnvironment;
+ private final BuildConfiguration configuration;
+ private final JavaSemantics semantics;
+
+ private PathFragment javaExecutable;
+ private List<Artifact> javabaseInputs = ImmutableList.of();
+ private Artifact outputJar;
+ private Artifact gensrcOutputJar;
+ private Artifact outputDepsProto;
+ private Artifact paramFile;
+ private Artifact metadata;
+ private final Collection<Artifact> sourceFiles = new ArrayList<>();
+ private final Collection<Artifact> sourceJars = new ArrayList<>();
+ private final Collection<Artifact> resources = new ArrayList<>();
+ private final Collection<Artifact> classpathResources = new ArrayList<>();
+ private final Collection<Artifact> translations = new LinkedHashSet<>();
+ private BuildConfiguration.StrictDepsMode strictJavaDeps =
+ BuildConfiguration.StrictDepsMode.OFF;
+ private final Collection<Artifact> directJars = new ArrayList<>();
+ private final Collection<Artifact> compileTimeDependencyArtifacts = new ArrayList<>();
+ private List<String> javacOpts = new ArrayList<>();
+ private boolean compressJar;
+ private NestedSet<Artifact> classpathEntries =
+ NestedSetBuilder.emptySet(Order.NAIVE_LINK_ORDER);
+ private ImmutableList<Artifact> bootclasspathEntries = ImmutableList.of();
+ private Artifact javaBuilderJar;
+ private Artifact langtoolsJar;
+ private PathFragment classDirectory;
+ private PathFragment sourceGenDirectory;
+ private PathFragment tempDirectory;
+ private final List<Artifact> processorPath = new ArrayList<>();
+ private final List<String> processorNames = new ArrayList<>();
+ private String ruleKind;
+ private Label targetLabel;
+
+ /**
+ * Creates a Builder from an owner and a build configuration.
+ */
+ public Builder(ActionOwner owner, AnalysisEnvironment analysisEnvironment,
+ BuildConfiguration configuration, JavaSemantics semantics) {
+ this.owner = owner;
+ this.analysisEnvironment = analysisEnvironment;
+ this.configuration = configuration;
+ this.semantics = semantics;
+ }
+
+ /**
+ * Creates a Builder from an owner and a build configuration.
+ */
+ public Builder(RuleContext ruleContext, JavaSemantics semantics) {
+ this(ruleContext.getActionOwner(), ruleContext.getAnalysisEnvironment(),
+ ruleContext.getConfiguration(), semantics);
+ }
+
+ public JavaCompileAction build() {
+ // TODO(bazel-team): all the params should be calculated before getting here, and the various
+ // aggregation code below should go away.
+ List<String> jcopts = new ArrayList<>(javacOpts);
+ JavaConfiguration javaConfiguration = configuration.getFragment(JavaConfiguration.class);
+ if (javaConfiguration.getJavaWarns().size() > 0) {
+ jcopts.add("-Xlint:" + Joiner.on(',').join(javaConfiguration.getJavaWarns()));
+ }
+ if (!bootclasspathEntries.isEmpty()) {
+ jcopts.add("-bootclasspath");
+ jcopts.add(
+ Artifact.joinExecPaths(configuration.getHostPathSeparator(), bootclasspathEntries));
+ }
+ List<String> internedJcopts = new ArrayList<>();
+ for (String jcopt : jcopts) {
+ internedJcopts.add(StringCanonicalizer.intern(jcopt));
+ }
+
+ // Invariant: if strictJavaDeps is OFF, then directJars and
+ // dependencyArtifacts are ignored
+ if (strictJavaDeps == BuildConfiguration.StrictDepsMode.OFF) {
+ directJars.clear();
+ compileTimeDependencyArtifacts.clear();
+ }
+
+ // Invariant: if experimental_java_classpath is not set to 'javabuilder',
+ // dependencyArtifacts are ignored
+ if (javaConfiguration.getReduceJavaClasspath() != JavaClasspathMode.JAVABUILDER) {
+ compileTimeDependencyArtifacts.clear();
+ }
+
+ if (paramFile == null) {
+ paramFile = analysisEnvironment.getDerivedArtifact(
+ ParameterFile.derivePath(outputJar.getRootRelativePath()),
+ configuration.getBinDirectory());
+ }
+
+ // ImmutableIterable is safe to use here because we know that neither of the components of
+ // the Iterable.concat() will change. Without ImmutableIterable, AbstractAction will
+ // waste memory by making a preventive copy of the iterable.
+ Iterable<Artifact> baseInputs = ImmutableIterable.from(Iterables.concat(
+ javabaseInputs,
+ bootclasspathEntries,
+ ImmutableList.of(paramFile)));
+
+ Preconditions.checkState(javaExecutable != null, owner);
+ Preconditions.checkState(javaExecutable.isAbsolute() ^ !javabaseInputs.isEmpty(),
+ javaExecutable);
+
+ Collection<Artifact> outputs;
+ ImmutableList.Builder<Artifact> outputsBuilder = ImmutableList.builder();
+ outputsBuilder.add(outputJar);
+ if (metadata != null) {
+ outputsBuilder.add(metadata);
+ }
+ if (gensrcOutputJar != null) {
+ outputsBuilder.add(gensrcOutputJar);
+ }
+ if (outputDepsProto != null) {
+ outputsBuilder.add(outputDepsProto);
+ }
+ outputs = outputsBuilder.build();
+
+ CustomCommandLine.Builder paramFileContentsBuilder = javaCompileCommandLine(
+ semantics,
+ configuration,
+ classDirectory,
+ sourceGenDirectory,
+ tempDirectory,
+ outputJar,
+ gensrcOutputJar,
+ compressJar,
+ outputDepsProto,
+ classpathEntries,
+ processorPath,
+ langtoolsJar,
+ javaBuilderJar,
+ processorNames,
+ translations,
+ resources,
+ classpathResources,
+ sourceJars,
+ sourceFiles,
+ internedJcopts,
+ directJars,
+ strictJavaDeps,
+ compileTimeDependencyArtifacts,
+ ruleKind,
+ targetLabel);
+ semantics.buildJavaCommandLine(outputs, configuration, paramFileContentsBuilder);
+ CommandLine paramFileContents = paramFileContentsBuilder.build();
+ Action parameterFileWriteAction = new ParameterFileWriteAction(owner, paramFile,
+ paramFileContents, ParameterFile.ParameterFileType.UNQUOTED, ISO_8859_1);
+ analysisEnvironment.registerAction(parameterFileWriteAction);
+
+ CommandLine javaBuilderCommandLine = spawnCommandLine(
+ javaExecutable,
+ javaBuilderJar,
+ langtoolsJar,
+ paramFile,
+ javaConfiguration.getDefaultJavaBuilderJvmFlags());
+
+ return new JavaCompileAction(owner,
+ baseInputs,
+ outputs,
+ paramFileContents,
+ javaBuilderCommandLine,
+ classDirectory,
+ outputJar,
+ classpathEntries,
+ processorPath,
+ langtoolsJar,
+ javaBuilderJar,
+ processorNames,
+ translations,
+ resources,
+ classpathResources,
+ sourceJars,
+ sourceFiles,
+ internedJcopts,
+ directJars,
+ strictJavaDeps,
+ compileTimeDependencyArtifacts,
+
+ semantics);
+ }
+
+ public Builder setParameterFile(Artifact paramFile) {
+ this.paramFile = paramFile;
+ return this;
+ }
+
+ public Builder setJavaExecutable(PathFragment javaExecutable) {
+ this.javaExecutable = javaExecutable;
+ return this;
+ }
+
+ public Builder setJavaBaseInputs(Iterable<Artifact> javabaseInputs) {
+ this.javabaseInputs = ImmutableList.copyOf(javabaseInputs);
+ return this;
+ }
+
+ public Builder setOutputJar(Artifact outputJar) {
+ this.outputJar = outputJar;
+ return this;
+ }
+
+ public Builder setGensrcOutputJar(Artifact gensrcOutputJar) {
+ this.gensrcOutputJar = gensrcOutputJar;
+ return this;
+ }
+
+ public Builder setOutputDepsProto(Artifact outputDepsProto) {
+ this.outputDepsProto = outputDepsProto;
+ return this;
+ }
+
+ public Builder setMetadata(Artifact metadata) {
+ this.metadata = metadata;
+ return this;
+ }
+
+ public Builder addSourceFile(Artifact sourceFile) {
+ sourceFiles.add(sourceFile);
+ return this;
+ }
+
+ public Builder addSourceFiles(Collection<Artifact> sourceFiles) {
+ this.sourceFiles.addAll(sourceFiles);
+ return this;
+ }
+
+ public Builder addSourceJars(Collection<Artifact> sourceJars) {
+ this.sourceJars.addAll(sourceJars);
+ return this;
+ }
+
+ public Builder addResources(Collection<Artifact> resources) {
+ this.resources.addAll(resources);
+ return this;
+ }
+
+ public Builder addClasspathResources(Collection<Artifact> classpathResources) {
+ this.classpathResources.addAll(classpathResources);
+ return this;
+ }
+
+ public Builder addTranslations(Collection<Artifact> translations) {
+ this.translations.addAll(translations);
+ return this;
+ }
+
+ /**
+ * Sets the strictness of Java dependency checking, see {@link
+ * com.google.devtools.build.lib.analysis.config.BuildConfiguration.StrictDepsMode}.
+ */
+ public Builder setStrictJavaDeps(BuildConfiguration.StrictDepsMode strictDeps) {
+ strictJavaDeps = strictDeps;
+ return this;
+ }
+
+ /**
+ * Accumulates the given jar artifacts as being provided by direct dependencies.
+ */
+ public Builder addDirectJars(Collection<Artifact> directJars) {
+ Iterables.addAll(this.directJars, directJars);
+ return this;
+ }
+
+ public Builder addCompileTimeDependencyArtifacts(Collection<Artifact> dependencyArtifacts) {
+ Iterables.addAll(this.compileTimeDependencyArtifacts, dependencyArtifacts);
+ return this;
+ }
+
+ public Builder setJavacOpts(Iterable<String> copts) {
+ this.javacOpts = ImmutableList.copyOf(copts);
+ return this;
+ }
+
+ public Builder setCompressJar(boolean compressJar) {
+ this.compressJar = compressJar;
+ return this;
+ }
+
+ public Builder setClasspathEntries(NestedSet<Artifact> classpathEntries) {
+ this.classpathEntries = classpathEntries;
+ return this;
+ }
+
+ public Builder setBootclasspathEntries(Iterable<Artifact> bootclasspathEntries) {
+ this.bootclasspathEntries = ImmutableList.copyOf(bootclasspathEntries);
+ return this;
+ }
+
+ public Builder setClassDirectory(PathFragment classDirectory) {
+ this.classDirectory = classDirectory;
+ return this;
+ }
+
+ /**
+ * Sets the directory where source files generated by annotation processors should be stored.
+ */
+ public Builder setSourceGenDirectory(PathFragment sourceGenDirectory) {
+ this.sourceGenDirectory = sourceGenDirectory;
+ return this;
+ }
+
+ public Builder setTempDirectory(PathFragment tempDirectory) {
+ this.tempDirectory = tempDirectory;
+ return this;
+ }
+
+ public Builder addProcessorPaths(Collection<Artifact> processorPaths) {
+ this.processorPath.addAll(processorPaths);
+ return this;
+ }
+
+ public Builder addProcessorNames(Collection<String> processorNames) {
+ this.processorNames.addAll(processorNames);
+ return this;
+ }
+
+ public Builder setLangtoolsJar(Artifact langtoolsJar) {
+ this.langtoolsJar = langtoolsJar;
+ return this;
+ }
+
+ public Builder setJavaBuilderJar(Artifact javaBuilderJar) {
+ this.javaBuilderJar = javaBuilderJar;
+ return this;
+ }
+
+ public Builder setRuleKind(String ruleKind) {
+ this.ruleKind = ruleKind;
+ return this;
+ }
+
+ public Builder setTargetLabel(Label targetLabel) {
+ this.targetLabel = targetLabel;
+ return this;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaConfiguration.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaConfiguration.java
new file mode 100644
index 0000000..e1c6dc2
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaConfiguration.java
@@ -0,0 +1,260 @@
+// Copyright 2014 Google Inc. 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.rules.java;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap.Builder;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration.Fragment;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration.StrictDepsMode;
+import com.google.devtools.build.lib.analysis.config.BuildOptions;
+import com.google.devtools.build.lib.analysis.config.InvalidConfigurationException;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.syntax.Label.SyntaxException;
+import com.google.devtools.build.lib.syntax.SkylarkCallable;
+import com.google.devtools.build.lib.syntax.SkylarkModule;
+import com.google.devtools.common.options.TriState;
+
+import java.util.List;
+
+/**
+ * A java compiler configuration containing the flags required for compilation.
+ */
+@Immutable
+@SkylarkModule(name = "java_configuration", doc = "A java compiler configuration")
+public final class JavaConfiguration extends Fragment {
+ /**
+ * Values for the --experimental_java_classpath option
+ */
+ public static enum JavaClasspathMode {
+ /** Use full transitive classpaths, the default behavior. */
+ OFF,
+ /** JavaBuilder computes the reduced classpath before invoking javac. */
+ JAVABUILDER,
+ /** Blaze computes the reduced classpath before invoking JavaBuilder. */
+ BLAZE
+ }
+
+ private final ImmutableList<String> commandLineJavacFlags;
+ private final Label javaLauncherLabel;
+ private final Label javaBuilderTop;
+ private final ImmutableList<String> defaultJavaBuilderJvmOpts;
+ private final Label javaLangtoolsJar;
+ private final boolean useIjars;
+ private final boolean generateJavaDeps;
+ private final JavaClasspathMode experimentalJavaClasspath;
+ private final ImmutableList<String> javaWarns;
+ private final ImmutableList<String> defaultJvmFlags;
+ private final ImmutableList<String> checkedConstraints;
+ private final StrictDepsMode strictJavaDeps;
+ private final Label javacBootclasspath;
+ private final ImmutableList<String> javacOpts;
+ private final TriState bundleTranslations;
+ private final ImmutableList<Label> translationTargets;
+ private final String javaCpu;
+
+ private final String cacheKey;
+ private Label javaToolchain;
+
+ JavaConfiguration(boolean generateJavaDeps,
+ List<String> defaultJvmFlags, JavaOptions javaOptions, Label javaToolchain, String javaCpu,
+ ImmutableList<String> defaultJavaBuilderJvmOpts) throws InvalidConfigurationException {
+ this.commandLineJavacFlags =
+ ImmutableList.copyOf(JavaHelper.tokenizeJavaOptions(javaOptions.javacOpts));
+ this.javaLauncherLabel = javaOptions.javaLauncher;
+ this.javaBuilderTop = javaOptions.javaBuilderTop;
+ this.defaultJavaBuilderJvmOpts = defaultJavaBuilderJvmOpts;
+ this.javaLangtoolsJar = javaOptions.javaLangtoolsJar;
+ this.useIjars = javaOptions.useIjars;
+ this.generateJavaDeps = generateJavaDeps;
+ this.experimentalJavaClasspath = javaOptions.experimentalJavaClasspath;
+ this.javaWarns = ImmutableList.copyOf(javaOptions.javaWarns);
+ this.defaultJvmFlags = ImmutableList.copyOf(defaultJvmFlags);
+ this.checkedConstraints = ImmutableList.copyOf(javaOptions.checkedConstraints);
+ this.strictJavaDeps = javaOptions.strictJavaDeps;
+ this.javacBootclasspath = javaOptions.javacBootclasspath;
+ this.javacOpts = ImmutableList.copyOf(javaOptions.javacOpts);
+ this.bundleTranslations = javaOptions.bundleTranslations;
+ this.javaCpu = javaCpu;
+ this.javaToolchain = javaToolchain;
+
+ ImmutableList.Builder<Label> translationsBuilder = ImmutableList.builder();
+ for (String s : javaOptions.translationTargets) {
+ try {
+ Label label = Label.parseAbsolute(s);
+ translationsBuilder.add(label);
+ } catch (SyntaxException e) {
+ throw new InvalidConfigurationException("Invalid translations target '" + s + "', make " +
+ "sure it uses correct absolute path syntax.", e);
+ }
+ }
+ this.translationTargets = translationsBuilder.build();
+
+ this.cacheKey = Joiner.on(" ").join(commandLineJavacFlags);
+ }
+
+ @SkylarkCallable(name = "default_javac_flags", structField = true,
+ doc = "The default flags for the Java compiler.")
+ // TODO(bazel-team): this is the command-line passed options, we should remove from skylark
+ // probably.
+ public List<String> getDefaultJavacFlags() {
+ return commandLineJavacFlags;
+ }
+
+ @Override
+ public String cacheKey() {
+ return cacheKey;
+ }
+
+ @Override
+ public void reportInvalidOptions(EventHandler reporter, BuildOptions buildOptions) {
+ if ((bundleTranslations == TriState.YES) && translationTargets.isEmpty()) {
+ reporter.handle(Event.error("Translations enabled, but no message translations specified. " +
+ "Use '--message_translations' to select the message translations to use"));
+ }
+ }
+
+ @Override
+ public void addGlobalMakeVariables(Builder<String, String> globalMakeEnvBuilder) {
+ globalMakeEnvBuilder.put("JAVA_TRANSLATIONS", buildTranslations() ? "1" : "0");
+ globalMakeEnvBuilder.put("JAVA_CPU", javaCpu);
+ }
+
+ /**
+ * Returns the Java cpu.
+ */
+ public String getJavaCpu() {
+ return javaCpu;
+ }
+
+ /**
+ * Returns the default javabuilder jar
+ */
+ public Label getDefaultJavaBuilderJar() {
+ return javaBuilderTop;
+ }
+
+ /**
+ * Returns the default JVM flags to be used when invoking javabuilder.
+ */
+ public ImmutableList<String> getDefaultJavaBuilderJvmFlags() {
+ return defaultJavaBuilderJvmOpts;
+ }
+
+ /**
+ * Returns the default java langtools jar
+ */
+ public Label getDefaultJavaLangtoolsJar() {
+ return javaLangtoolsJar;
+ }
+
+ /**
+ * Returns true iff Java compilation should use ijars.
+ */
+ public boolean getUseIjars() {
+ return useIjars;
+ }
+
+ /**
+ * Returns true iff dependency information is generated after compilation.
+ */
+ public boolean getGenerateJavaDeps() {
+ return generateJavaDeps;
+ }
+
+ public JavaClasspathMode getReduceJavaClasspath() {
+ return experimentalJavaClasspath;
+ }
+
+ /**
+ * Returns the extra warnings enabled for Java compilation.
+ */
+ public List<String> getJavaWarns() {
+ return javaWarns;
+ }
+
+ public List<String> getDefaultJvmFlags() {
+ return defaultJvmFlags;
+ }
+
+ public List<String> getCheckedConstraints() {
+ return checkedConstraints;
+ }
+
+ public StrictDepsMode getStrictJavaDeps() {
+ return strictJavaDeps;
+ }
+
+ public StrictDepsMode getFilteredStrictJavaDeps() {
+ StrictDepsMode strict = getStrictJavaDeps();
+ switch (strict) {
+ case STRICT:
+ case DEFAULT:
+ return StrictDepsMode.ERROR;
+ default: // OFF, WARN, ERROR
+ return strict;
+ }
+ }
+
+ /**
+ * @return proper label only if --java_launcher= is specified, otherwise null.
+ */
+ public Label getJavaLauncherLabel() {
+ return javaLauncherLabel;
+ }
+
+ public Label getJavacBootclasspath() {
+ return javacBootclasspath;
+ }
+
+ public List<String> getJavacOpts() {
+ return javacOpts;
+ }
+
+ @Override
+ public String getName() {
+ return "Java";
+ }
+
+ /**
+ * Returns the raw translation targets.
+ */
+ public List<Label> getTranslationTargets() {
+ return translationTargets;
+ }
+
+ /**
+ * Returns true if the we should build translations.
+ */
+ public boolean buildTranslations() {
+ return (bundleTranslations != TriState.NO) && !translationTargets.isEmpty();
+ }
+
+ /**
+ * Returns whether translations were explicitly disabled.
+ */
+ public boolean isTranslationsDisabled() {
+ return bundleTranslations == TriState.NO;
+ }
+
+ /**
+ * Returns the label of the default java_toolchain rule
+ */
+ public Label getToolchainLabel() {
+ return javaToolchain;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaConfigurationLoader.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaConfigurationLoader.java
new file mode 100644
index 0000000..53fdfdf
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaConfigurationLoader.java
@@ -0,0 +1,76 @@
+// Copyright 2014 Google Inc. 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.rules.java;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.analysis.RedirectChaser;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration.Fragment;
+import com.google.devtools.build.lib.analysis.config.BuildOptions;
+import com.google.devtools.build.lib.analysis.config.ConfigurationEnvironment;
+import com.google.devtools.build.lib.analysis.config.ConfigurationFragmentFactory;
+import com.google.devtools.build.lib.analysis.config.InvalidConfigurationException;
+import com.google.devtools.build.lib.rules.java.JavaConfiguration.JavaClasspathMode;
+import com.google.devtools.build.lib.syntax.Label;
+
+/**
+ * A loader that creates JavaConfiguration instances based on JavaBuilder configurations and
+ * command-line options.
+ */
+public class JavaConfigurationLoader implements ConfigurationFragmentFactory {
+ private final JavaCpuSupplier cpuSupplier;
+
+ public JavaConfigurationLoader(JavaCpuSupplier cpuSupplier) {
+ this.cpuSupplier = cpuSupplier;
+ }
+
+ @Override
+ public JavaConfiguration create(ConfigurationEnvironment env, BuildOptions buildOptions)
+ throws InvalidConfigurationException {
+ JavaOptions javaOptions = buildOptions.get(JavaOptions.class);
+
+ Label javaToolchain = RedirectChaser.followRedirects(env, javaOptions.javaToolchain,
+ "java_toolchain");
+ return create(javaOptions, javaToolchain, cpuSupplier.getJavaCpu(buildOptions, env));
+ }
+
+ @Override
+ public Class<? extends Fragment> creates() {
+ return JavaConfiguration.class;
+ }
+
+ public JavaConfiguration create(JavaOptions javaOptions, Label javaToolchain, String javaCpu)
+ throws InvalidConfigurationException {
+
+ boolean generateJavaDeps = javaOptions.javaDeps ||
+ javaOptions.experimentalJavaClasspath != JavaClasspathMode.OFF;
+
+ ImmutableList<String> defaultJavaBuilderJvmOpts = ImmutableList.<String>builder()
+ .addAll(getJavacJvmOptions())
+ .addAll(JavaHelper.tokenizeJavaOptions(javaOptions.javaBuilderJvmOpts))
+ .build();
+
+ return new JavaConfiguration(generateJavaDeps, javaOptions.jvmOpts, javaOptions,
+ javaToolchain, javaCpu, defaultJavaBuilderJvmOpts);
+ }
+
+ /**
+ * This method returns the list of JVM options when invoking the java compiler.
+ *
+ * <p>TODO(bazel-team): Maybe we should put those options in the java_toolchain rule.
+ */
+ protected ImmutableList<String> getJavacJvmOptions() {
+ return ImmutableList.of("-client");
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaCpuSupplier.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaCpuSupplier.java
new file mode 100644
index 0000000..5492abf
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaCpuSupplier.java
@@ -0,0 +1,31 @@
+// Copyright 2014 Google Inc. 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.rules.java;
+
+import com.google.devtools.build.lib.analysis.config.BuildOptions;
+import com.google.devtools.build.lib.analysis.config.ConfigurationEnvironment;
+import com.google.devtools.build.lib.analysis.config.InvalidConfigurationException;
+
+/**
+ * Determines the CPU to be used for Java compilation from the build options and the
+ * configuration environment.
+ */
+public interface JavaCpuSupplier {
+ /**
+ * Returns the Java CPU based on the buiold options and the configuration environment.
+ */
+ String getJavaCpu(BuildOptions buildOptions, ConfigurationEnvironment env)
+ throws InvalidConfigurationException;
+}
\ No newline at end of file
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaExportsProvider.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaExportsProvider.java
new file mode 100644
index 0000000..52857f1
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaExportsProvider.java
@@ -0,0 +1,42 @@
+// Copyright 2014 Google Inc. 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.rules.java;
+
+import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.syntax.Label;
+
+/**
+ * The collection of labels of exported targets and artifacts reached via "exports" attribute
+ * transitively.
+ */
+@Immutable
+public final class JavaExportsProvider implements TransitiveInfoProvider {
+
+ private final NestedSet<Label> transitiveExports;
+
+ public JavaExportsProvider(NestedSet<Label> transitiveExports) {
+ this.transitiveExports = transitiveExports;
+ }
+
+ /**
+ * Returns the labels of exported targets and artifacts reached transitively through the "exports"
+ * attribute.
+ */
+ public NestedSet<Label> getTransitiveExports() {
+ return transitiveExports;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaHelper.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaHelper.java
new file mode 100644
index 0000000..b2a7ca0
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaHelper.java
@@ -0,0 +1,104 @@
+// Copyright 2014 Google Inc. 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.rules.java;
+
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.shell.ShellUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Utility methods for use by Java-related parts of Bazel.
+ */
+// TODO(bazel-team): Merge with JavaUtil.
+public abstract class JavaHelper {
+
+ private JavaHelper() {}
+
+ /**
+ * Returns the java launcher implementation for the given target, if any.
+ * A null return value means "use the JDK launcher".
+ */
+ public static TransitiveInfoCollection launcherForTarget(JavaSemantics semantics,
+ RuleContext ruleContext) {
+ String launcher = filterLauncherForTarget(semantics, ruleContext);
+ return (launcher == null) ? null : ruleContext.getPrerequisite(launcher, Mode.TARGET);
+ }
+
+ /**
+ * Returns the java launcher artifact for the given target, if any.
+ * A null return value means "use the JDK launcher".
+ */
+ public static Artifact launcherArtifactForTarget(JavaSemantics semantics,
+ RuleContext ruleContext) {
+ String launcher = filterLauncherForTarget(semantics, ruleContext);
+ return (launcher == null) ? null : ruleContext.getPrerequisiteArtifact(launcher, Mode.TARGET);
+ }
+
+ /**
+ * Control structure abstraction for safely extracting a prereq from the launcher attribute
+ * or --java_launcher flag.
+ */
+ private static String filterLauncherForTarget(JavaSemantics semantics, RuleContext ruleContext) {
+ // BUILD rule "launcher" attribute
+ if (ruleContext.getRule().isAttrDefined("launcher", Type.LABEL)
+ && ruleContext.attributes().get("launcher", Type.LABEL) != null) {
+ if (ruleContext.attributes().get("launcher", Type.LABEL)
+ .equals(JavaSemantics.JDK_LAUNCHER_LABEL)) {
+ return null;
+ }
+ return "launcher";
+ }
+ // Blaze flag --java_launcher
+ JavaConfiguration javaConfig = ruleContext.getFragment(JavaConfiguration.class);
+ if (ruleContext.getRule().isAttrDefined(":java_launcher", Type.LABEL)
+ && ((javaConfig.getJavaLauncherLabel() != null
+ && !javaConfig.getJavaLauncherLabel().equals(JavaSemantics.JDK_LAUNCHER_LABEL))
+ || semantics.forceUseJavaLauncherTarget(ruleContext))) {
+ return ":java_launcher";
+ }
+ return null;
+ }
+
+ /**
+ * Javac options require special processing - People use them and expect the
+ * options to be tokenized.
+ */
+ public static List<String> tokenizeJavaOptions(Iterable<String> inOpts) {
+ // Ideally, this would be in the options parser. Unfortunately,
+ // the options parser can't handle a converter that expands
+ // from a value X into a List<X> and allow-multiple at the
+ // same time.
+ List<String> result = new ArrayList<>();
+ for (String current : inOpts) {
+ try {
+ ShellUtils.tokenize(result, current);
+ } catch (ShellUtils.TokenizationException ex) {
+ // Tokenization failed; this likely means that the user
+ // did not want tokenization to happen on his argument.
+ // (Any tokenization where we should produce an error
+ // has already been done by the shell that invoked
+ // blaze). Therefore, pass the argument through to
+ // the tool, so that we can see the original error.
+ result.add(current);
+ }
+ }
+ return result;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaImport.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaImport.java
new file mode 100644
index 0000000..f978f98
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaImport.java
@@ -0,0 +1,201 @@
+// Copyright 2014 Google Inc. 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.rules.java;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.FileProvider;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTargetBuilder;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.Runfiles;
+import com.google.devtools.build.lib.analysis.RunfilesProvider;
+import com.google.devtools.build.lib.analysis.TopLevelArtifactProvider;
+import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.rules.RuleConfiguredTargetFactory;
+import com.google.devtools.build.lib.rules.cpp.CcLinkParams;
+import com.google.devtools.build.lib.rules.cpp.CcLinkParamsProvider;
+import com.google.devtools.build.lib.rules.cpp.CcLinkParamsStore;
+import com.google.devtools.build.lib.rules.cpp.CppCompilationContext;
+import com.google.devtools.build.lib.rules.cpp.LinkerInput;
+import com.google.devtools.build.lib.rules.java.JavaCompilationArgs.ClasspathType;
+
+/**
+ * An implementation for the "java_import" rule.
+ */
+public class JavaImport implements RuleConfiguredTargetFactory {
+ private final JavaSemantics semantics;
+
+ protected JavaImport(JavaSemantics semantics) {
+ this.semantics = semantics;
+ }
+
+ @Override
+ public ConfiguredTarget create(RuleContext ruleContext) {
+ ImmutableList<Artifact> srcJars = ImmutableList.of();
+ ImmutableList<Artifact> jars = collectJars(ruleContext);
+ Artifact srcJar = ruleContext.getPrerequisiteArtifact("srcjar", Mode.TARGET);
+
+ if (ruleContext.hasErrors()) {
+ return null;
+ }
+
+ ImmutableList<TransitiveInfoCollection> targets = ImmutableList.copyOf(
+ ruleContext.getPrerequisites("exports", Mode.TARGET));
+ final JavaCommon common = new JavaCommon(
+ ruleContext, semantics, targets, targets, targets);
+ semantics.checkRule(ruleContext, common);
+
+ // No need for javac options - no compilation happening here.
+ JavaCompilationHelper helper = new JavaCompilationHelper(ruleContext, semantics,
+ ImmutableList.<String>of(), new JavaTargetAttributes.Builder(semantics));
+ ImmutableMap.Builder<Artifact, Artifact> compilationToRuntimeJarMap = ImmutableMap.builder();
+ ImmutableList<Artifact> interfaceJars =
+ processWithIjar(jars, helper, compilationToRuntimeJarMap);
+
+ common.setJavaCompilationArtifacts(collectJavaArtifacts(jars, interfaceJars));
+
+ CppCompilationContext transitiveCppDeps = common.collectTransitiveCppDeps();
+ NestedSet<LinkerInput> transitiveJavaNativeLibraries =
+ common.collectTransitiveJavaNativeLibraries();
+
+ JavaCompilationArgs javaCompilationArgs = common.collectJavaCompilationArgs(
+ false, common.isNeverLink(), compilationArgsFromSources());
+ JavaCompilationArgs recursiveJavaCompilationArgs = common.collectJavaCompilationArgs(
+ true, common.isNeverLink(), compilationArgsFromSources());
+ NestedSet<Artifact> transitiveJavaSourceJars =
+ collectTransitiveJavaSourceJars(ruleContext, srcJar);
+ if (srcJar != null) {
+ srcJars = ImmutableList.of(srcJar);
+ }
+
+ // The "neverlink" attribute is transitive, so if it is enabled, we don't add any
+ // runfiles from this target or its dependencies.
+ Runfiles runfiles = common.isNeverLink() ?
+ Runfiles.EMPTY :
+ new Runfiles.Builder()
+ // add the jars to the runfiles
+ .addArtifacts(common.getJavaCompilationArtifacts().getRuntimeJars())
+ .addTargets(targets, RunfilesProvider.DEFAULT_RUNFILES)
+ .addRunfiles(ruleContext, RunfilesProvider.DEFAULT_RUNFILES)
+ .addTargets(targets, JavaRunfilesProvider.TO_RUNFILES)
+ .add(ruleContext, JavaRunfilesProvider.TO_RUNFILES)
+ .build();
+
+ CcLinkParamsStore ccLinkParamsStore = new CcLinkParamsStore() {
+ @Override
+ protected void collect(CcLinkParams.Builder builder, boolean linkingStatically,
+ boolean linkShared) {
+ Iterable<? extends TransitiveInfoCollection> deps =
+ common.targetsTreatedAsDeps(ClasspathType.BOTH);
+ builder.addTransitiveTargets(deps);
+ builder.addTransitiveLangTargets(deps, JavaCcLinkParamsProvider.TO_LINK_PARAMS);
+ }
+ };
+ RuleConfiguredTargetBuilder ruleBuilder =
+ new RuleConfiguredTargetBuilder(ruleContext);
+ NestedSetBuilder<Artifact> filesBuilder = NestedSetBuilder.stableOrder();
+ filesBuilder.addAll(jars);
+
+ semantics.addProviders(
+ ruleContext, common, ImmutableList.<String>of(), null,
+ srcJar, null, compilationToRuntimeJarMap.build(), helper, filesBuilder, ruleBuilder);
+
+ NestedSet<Artifact> filesToBuild = filesBuilder.build();
+
+ common.addTransitiveInfoProviders(ruleBuilder, filesToBuild, null);
+ return ruleBuilder
+ .setFilesToBuild(filesToBuild)
+ .add(JavaNeverlinkInfoProvider.class, new JavaNeverlinkInfoProvider(common.isNeverLink()))
+ .add(RunfilesProvider.class, RunfilesProvider.simple(runfiles))
+ .add(CcLinkParamsProvider.class, new CcLinkParamsProvider(ccLinkParamsStore))
+ .add(JavaCompilationArgsProvider.class, new JavaCompilationArgsProvider(
+ javaCompilationArgs, recursiveJavaCompilationArgs))
+ .add(JavaNativeLibraryProvider.class, new JavaNativeLibraryProvider(
+ transitiveJavaNativeLibraries))
+ .add(CppCompilationContext.class, transitiveCppDeps)
+ .add(JavaSourceJarsProvider.class, new JavaSourceJarsProvider(
+ transitiveJavaSourceJars, srcJars))
+ .add(TopLevelArtifactProvider.class, new TopLevelArtifactProvider(
+ JavaSemantics.SOURCE_JARS_OUTPUT_GROUP, transitiveJavaSourceJars))
+ .build();
+ }
+
+ private NestedSet<Artifact> collectTransitiveJavaSourceJars(RuleContext ruleContext,
+ Artifact srcJar) {
+ NestedSetBuilder<Artifact> transitiveJavaSourceJarBuilder =
+ NestedSetBuilder.stableOrder();
+ if (srcJar != null) {
+ transitiveJavaSourceJarBuilder.add(srcJar);
+ }
+ for (JavaSourceJarsProvider other :
+ ruleContext.getPrerequisites("exports", Mode.TARGET, JavaSourceJarsProvider.class)) {
+ transitiveJavaSourceJarBuilder.addTransitive(other.getTransitiveSourceJars());
+ }
+ return transitiveJavaSourceJarBuilder.build();
+ }
+
+ private JavaCompilationArtifacts collectJavaArtifacts(
+ ImmutableList<Artifact> jars,
+ ImmutableList<Artifact> interfaceJars) {
+ JavaCompilationArtifacts.Builder javaArtifactsBuilder = new JavaCompilationArtifacts.Builder();
+ javaArtifactsBuilder.addRuntimeJars(jars);
+ // interfaceJars Artifacts have proper owner labels
+ javaArtifactsBuilder.addCompileTimeJars(interfaceJars);
+ return javaArtifactsBuilder.build();
+ }
+
+ private ImmutableList<Artifact> collectJars(RuleContext ruleContext) {
+ ImmutableList.Builder<Artifact> jarsBuilder = ImmutableList.builder();
+ for (TransitiveInfoCollection info : ruleContext.getPrerequisites("jars", Mode.TARGET)) {
+ if (info.getProvider(JavaCompilationArgsProvider.class) != null) {
+ ruleContext.attributeError("jars", "should not refer to Java rules");
+ }
+ for (Artifact jar : info.getProvider(FileProvider.class).getFilesToBuild()) {
+ if (!JavaSemantics.JAR.matches(jar.getFilename())) {
+ ruleContext.attributeError("jars", jar.getFilename() + " is not a .jar file");
+ } else {
+ jarsBuilder.add(jar);
+ }
+ }
+ }
+ return jarsBuilder.build();
+ }
+
+ private ImmutableList<Artifact> processWithIjar(ImmutableList<Artifact> jars,
+ JavaCompilationHelper helper,
+ ImmutableMap.Builder<Artifact, Artifact> compilationToRuntimeJarMap) {
+ ImmutableList.Builder<Artifact> interfaceJarsBuilder = ImmutableList.builder();
+ for (Artifact jar : jars) {
+ Artifact ijar = helper.createIjarAction(jar, true);
+ interfaceJarsBuilder.add(ijar);
+ compilationToRuntimeJarMap.put(ijar, jar);
+ }
+ return interfaceJarsBuilder.build();
+ }
+
+ private Iterable<SourcesJavaCompilationArgsProvider> compilationArgsFromSources() {
+ return ImmutableList.of();
+ }
+
+ private ImmutableList<String> getJavaConstraints(RuleContext ruleContext) {
+ return ImmutableList.copyOf(ruleContext.attributes().get("constraints", Type.STRING_LIST));
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaImportBaseRule.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaImportBaseRule.java
new file mode 100644
index 0000000..b153b58
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaImportBaseRule.java
@@ -0,0 +1,91 @@
+// Copyright 2014 Google Inc. 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.rules.java;
+
+import static com.google.devtools.build.lib.packages.Attribute.ConfigurationTransition.HOST;
+import static com.google.devtools.build.lib.packages.Attribute.attr;
+import static com.google.devtools.build.lib.packages.Type.BOOLEAN;
+import static com.google.devtools.build.lib.packages.Type.LABEL;
+import static com.google.devtools.build.lib.packages.Type.LABEL_LIST;
+import static com.google.devtools.build.lib.packages.Type.STRING_LIST;
+
+import com.google.devtools.build.lib.analysis.BaseRuleClasses;
+import com.google.devtools.build.lib.analysis.BlazeRule;
+import com.google.devtools.build.lib.analysis.RuleDefinition;
+import com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.RuleClass.Builder;
+import com.google.devtools.build.lib.packages.RuleClass.Builder.RuleClassType;
+
+/**
+ * A base rule for building the java_import rule.
+ */
+@BlazeRule(name = "$java_import_base",
+ type = RuleClassType.ABSTRACT,
+ ancestors = { BaseRuleClasses.RuleBase.class })
+public class JavaImportBaseRule implements RuleDefinition {
+
+ @Override
+ public RuleClass build(Builder builder, RuleDefinitionEnvironment environment) {
+ return builder
+ .add(attr(":host_jdk", LABEL)
+ .cfg(HOST)
+ .value(JavaSemantics.HOST_JDK))
+ /* <!-- #BLAZE_RULE(java_import).ATTRIBUTE(jars) -->
+ The list of JAR files provided to Java targets that depend on this target.
+ ${SYNOPSIS}
+ <!-- #END_BLAZE_RULE.ATTRIBUTE --> */
+ .add(attr("jars", LABEL_LIST)
+ .mandatory()
+ .nonEmpty()
+ .allowedFileTypes(JavaSemantics.JAR))
+ /* <!-- #BLAZE_RULE(java_import).ATTRIBUTE(srcjar) -->
+ A JAR file that contains source code for the compiled JAR files.
+ ${SYNOPSIS}
+ <!-- #END_BLAZE_RULE.ATTRIBUTE --> */
+ .add(attr("srcjar", LABEL)
+ .allowedFileTypes(JavaSemantics.SOURCE_JAR, JavaSemantics.JAR)
+ .direct_compile_time_input())
+ .removeAttribute("deps") // only exports are allowed; nothing is compiled
+ /* <!-- #BLAZE_RULE(java_import).ATTRIBUTE(neverlink) -->
+ Only use this library for compilation and not at runtime.
+ ${SYNOPSIS}
+ Useful if the library will be provided by the runtime environment
+ during execution. Examples of libraries like this are IDE APIs
+ for IDE plug-ins or <code>tools.jar</code> for anything running on
+ a standard JDK.
+ <!-- #END_BLAZE_RULE.ATTRIBUTE --> */
+ .add(attr("neverlink", BOOLEAN).value(false))
+ /* <!-- #BLAZE_RULE(java_import).ATTRIBUTE(constraints) -->
+ Extra constraints imposed on this rule as a Java library.
+ ${SYNOPSIS}
+ See <a href="#java_library.constraints">java_library.constraints</a>.
+ <!-- #END_BLAZE_RULE.ATTRIBUTE --> */
+ .add(attr("constraints", STRING_LIST)
+ .orderIndependent()
+ .nonconfigurable("used in Attribute.validityPredicate implementations (loading time)"))
+ .build();
+ }
+}
+/*<!-- #BLAZE_RULE (NAME = java_import, TYPE = LIBRARY, FAMILY = Java) -->
+
+${ATTRIBUTE_SIGNATURE}
+
+ <p>This rule allows the use of precompiled JAR files as libraries for
+ <code><a href="#java_library">java_library</a></code> rules.</p>
+
+${ATTRIBUTE_DEFINITION}
+
+<!-- #END_BLAZE_RULE -->*/
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaLibrary.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaLibrary.java
new file mode 100644
index 0000000..1831ef0
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaLibrary.java
@@ -0,0 +1,244 @@
+// Copyright 2014 Google Inc. 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.rules.java;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTargetBuilder;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.Runfiles;
+import com.google.devtools.build.lib.analysis.RunfilesProvider;
+import com.google.devtools.build.lib.analysis.TopLevelArtifactProvider;
+import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.rules.RuleConfiguredTargetFactory;
+import com.google.devtools.build.lib.rules.cpp.CcLinkParams;
+import com.google.devtools.build.lib.rules.cpp.CcLinkParamsProvider;
+import com.google.devtools.build.lib.rules.cpp.CcLinkParamsStore;
+import com.google.devtools.build.lib.rules.cpp.CppCompilationContext;
+import com.google.devtools.build.lib.rules.cpp.LinkerInput;
+import com.google.devtools.build.lib.rules.java.JavaCompilationArgs.ClasspathType;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Implementation for the java_library rule.
+ */
+public class JavaLibrary implements RuleConfiguredTargetFactory {
+ private final JavaSemantics semantics;
+
+ protected JavaLibrary(JavaSemantics semantics) {
+ this.semantics = semantics;
+ }
+
+ @Override
+ public ConfiguredTarget create(RuleContext ruleContext) {
+ JavaCommon common = new JavaCommon(ruleContext, semantics);
+ RuleConfiguredTargetBuilder builder = init(ruleContext, common);
+ return builder != null ? builder.build() : null;
+ }
+
+ public RuleConfiguredTargetBuilder init(RuleContext ruleContext, final JavaCommon common) {
+ common.initializeJavacOpts();
+ JavaTargetAttributes.Builder attributesBuilder = common.initCommon();
+
+ // Collect the transitive dependencies.
+ JavaCompilationHelper helper = new JavaCompilationHelper(
+ ruleContext, semantics, common.getJavacOpts(), attributesBuilder);
+ helper.addLibrariesToAttributes(common.targetsTreatedAsDeps(ClasspathType.COMPILE_ONLY));
+ helper.addProvidersToAttributes(common.compilationArgsFromSources(), common.isNeverLink());
+
+ if (ruleContext.hasErrors()) {
+ return null;
+ }
+
+ semantics.checkRule(ruleContext, common);
+
+ JavaCompilationArtifacts.Builder javaArtifactsBuilder = new JavaCompilationArtifacts.Builder();
+
+ if (ruleContext.hasErrors()) {
+ common.setJavaCompilationArtifacts(JavaCompilationArtifacts.EMPTY);
+ return null;
+ }
+
+ JavaConfiguration javaConfig = ruleContext.getFragment(JavaConfiguration.class);
+ NestedSetBuilder<Artifact> filesBuilder = NestedSetBuilder.stableOrder();
+
+ JavaTargetAttributes attributes = helper.getAttributes();
+ if (attributes.hasJarFiles()) {
+ // This rule is repackaging some source jars as a java library.
+ Set<Artifact> jarFiles = attributes.getJarFiles();
+ javaArtifactsBuilder.addRuntimeJars(jarFiles);
+ javaArtifactsBuilder.addCompileTimeJars(attributes.getCompileTimeJarFiles());
+
+ filesBuilder.addAll(jarFiles);
+ }
+ if (attributes.hasMessages()) {
+ helper.addTranslations(semantics.translate(ruleContext, javaConfig,
+ attributes.getMessages()));
+ }
+
+ ruleContext.checkSrcsSamePackage(true);
+
+ Artifact jar = null;
+
+ Artifact srcJar = ruleContext.getImplicitOutputArtifact(
+ JavaSemantics.JAVA_LIBRARY_SOURCE_JAR);
+
+ Artifact classJar = ruleContext.getImplicitOutputArtifact(
+ JavaSemantics.JAVA_LIBRARY_CLASS_JAR);
+
+ if (attributes.hasSourceFiles() || attributes.hasSourceJars() || attributes.hasResources()
+ || attributes.hasMessages()) {
+ // We only want to add a jar to the classpath of a dependent rule if it has content.
+ javaArtifactsBuilder.addRuntimeJar(classJar);
+ jar = classJar;
+ }
+
+ filesBuilder.add(classJar);
+
+ // The gensrcJar is only created if the target uses annotation processing. Otherwise,
+ // it is null, and the source jar action will not depend on the compile action.
+ Artifact gensrcJar = helper.createGensrcJar(classJar);
+
+ Artifact outputDepsProto = helper.createOutputDepsProtoArtifact(classJar, javaArtifactsBuilder);
+
+ helper.createCompileActionWithInstrumentation(classJar, gensrcJar, outputDepsProto,
+ javaArtifactsBuilder);
+ helper.createSourceJarAction(srcJar, gensrcJar);
+
+ if ((attributes.hasSourceFiles() || attributes.hasSourceJars()) && jar != null) {
+ helper.createCompileTimeJarAction(jar, outputDepsProto,
+ javaArtifactsBuilder);
+ }
+
+ common.setJavaCompilationArtifacts(javaArtifactsBuilder.build());
+ common.setClassPathFragment(new ClasspathConfiguredFragment(
+ common.getJavaCompilationArtifacts(), attributes, common.isNeverLink()));
+ CppCompilationContext transitiveCppDeps = common.collectTransitiveCppDeps();
+
+ NestedSet<Artifact> transitiveSourceJars = common.collectTransitiveSourceJars(srcJar);
+
+ // If sources are empty, treat this library as a forwarding node for dependencies.
+ JavaCompilationArgs javaCompilationArgs = common.collectJavaCompilationArgs(
+ false, common.isNeverLink(), common.compilationArgsFromSources());
+ JavaCompilationArgs recursiveJavaCompilationArgs = common.collectJavaCompilationArgs(
+ true, common.isNeverLink(), common.compilationArgsFromSources());
+ NestedSet<Artifact> compileTimeJavaDepArtifacts = common.collectCompileTimeDependencyArtifacts(
+ common.getJavaCompilationArtifacts().getCompileTimeDependencyArtifact());
+ NestedSet<Artifact> runTimeJavaDepArtifacts = NestedSetBuilder.emptySet(Order.STABLE_ORDER);
+ NestedSet<LinkerInput> transitiveJavaNativeLibraries =
+ common.collectTransitiveJavaNativeLibraries();
+
+ ImmutableList<String> exportedProcessorClasses = ImmutableList.of();
+ NestedSet<Artifact> exportedProcessorClasspath =
+ NestedSetBuilder.emptySet(Order.NAIVE_LINK_ORDER);
+ ImmutableList.Builder<String> processorClasses = ImmutableList.builder();
+ NestedSetBuilder<Artifact> processorClasspath = NestedSetBuilder.naiveLinkOrder();
+ for (JavaPluginInfoProvider provider : Iterables.concat(
+ common.getPluginInfoProvidersForAttribute("exported_plugins", Mode.HOST),
+ common.getPluginInfoProvidersForAttribute("exports", Mode.TARGET))) {
+ processorClasses.addAll(provider.getProcessorClasses());
+ processorClasspath.addTransitive(provider.getProcessorClasspath());
+ }
+ exportedProcessorClasses = processorClasses.build();
+ exportedProcessorClasspath = processorClasspath.build();
+
+ CcLinkParamsStore ccLinkParamsStore = new CcLinkParamsStore() {
+ @Override
+ protected void collect(CcLinkParams.Builder builder, boolean linkingStatically,
+ boolean linkShared) {
+ Iterable<? extends TransitiveInfoCollection> deps =
+ common.targetsTreatedAsDeps(ClasspathType.BOTH);
+ builder.addTransitiveTargets(deps);
+ builder.addTransitiveLangTargets(deps, JavaCcLinkParamsProvider.TO_LINK_PARAMS);
+ }
+ };
+
+ // The "neverlink" attribute is transitive, so we don't add any
+ // runfiles from this target or its dependencies.
+ Runfiles runfiles = Runfiles.EMPTY;
+ if (!common.isNeverLink()) {
+ Runfiles.Builder runfilesBuilder = new Runfiles.Builder().addArtifacts(
+ common.getJavaCompilationArtifacts().getRuntimeJars());
+
+
+ runfilesBuilder.addRunfiles(ruleContext, RunfilesProvider.DEFAULT_RUNFILES);
+ runfilesBuilder.add(ruleContext, JavaRunfilesProvider.TO_RUNFILES);
+
+ List<TransitiveInfoCollection> depsForRunfiles = new ArrayList<>();
+ if (ruleContext.getRule().isAttrDefined("runtime_deps", Type.LABEL_LIST)) {
+ depsForRunfiles.addAll(ruleContext.getPrerequisites("runtime_deps", Mode.TARGET));
+ }
+ if (ruleContext.getRule().isAttrDefined("exports", Type.LABEL_LIST)) {
+ depsForRunfiles.addAll(ruleContext.getPrerequisites("exports", Mode.TARGET));
+ }
+
+ runfilesBuilder.addTargets(depsForRunfiles, RunfilesProvider.DEFAULT_RUNFILES);
+ runfilesBuilder.addTargets(depsForRunfiles, JavaRunfilesProvider.TO_RUNFILES);
+
+ TransitiveInfoCollection launcher = JavaHelper.launcherForTarget(semantics, ruleContext);
+ if (launcher != null) {
+ runfilesBuilder.addTarget(launcher, RunfilesProvider.DATA_RUNFILES);
+ }
+
+ semantics.addRunfilesForLibrary(ruleContext, runfilesBuilder);
+ runfiles = runfilesBuilder.build();
+ }
+
+ RuleConfiguredTargetBuilder builder =
+ new RuleConfiguredTargetBuilder(ruleContext);
+
+ semantics.addProviders(
+ ruleContext, common, ImmutableList.<String>of(), classJar, srcJar, gensrcJar,
+ ImmutableMap.<Artifact, Artifact>of(), helper, filesBuilder, builder);
+
+ NestedSet<Artifact> filesToBuild = filesBuilder.build();
+ common.addTransitiveInfoProviders(builder, filesToBuild, classJar);
+
+ builder
+ .add(RunfilesProvider.class, RunfilesProvider.simple(runfiles))
+ .setFilesToBuild(filesToBuild)
+ .add(JavaNeverlinkInfoProvider.class, new JavaNeverlinkInfoProvider(common.isNeverLink()))
+ .add(CppCompilationContext.class, transitiveCppDeps)
+ .add(JavaCompilationArgsProvider.class, new JavaCompilationArgsProvider(
+ javaCompilationArgs, recursiveJavaCompilationArgs,
+ compileTimeJavaDepArtifacts, runTimeJavaDepArtifacts))
+ .add(CcLinkParamsProvider.class, new CcLinkParamsProvider(ccLinkParamsStore))
+ .add(JavaNativeLibraryProvider.class, new JavaNativeLibraryProvider(
+ transitiveJavaNativeLibraries))
+ .add(JavaSourceJarsProvider.class, new JavaSourceJarsProvider(
+ transitiveSourceJars, ImmutableList.of(srcJar)))
+ .add(TopLevelArtifactProvider.class, new TopLevelArtifactProvider(
+ JavaSemantics.SOURCE_JARS_OUTPUT_GROUP, transitiveSourceJars))
+ // TODO(bazel-team): this should only happen for java_plugin
+ .add(JavaPluginInfoProvider.class, new JavaPluginInfoProvider(
+ exportedProcessorClasses, exportedProcessorClasspath));
+
+ if (ruleContext.hasErrors()) {
+ return null;
+ }
+
+ return builder;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaLibraryHelper.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaLibraryHelper.java
new file mode 100644
index 0000000..a28d7fd
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaLibraryHelper.java
@@ -0,0 +1,382 @@
+// Copyright 2014 Google Inc. 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.rules.java;
+
+import static com.google.devtools.build.lib.analysis.config.BuildConfiguration.StrictDepsMode.OFF;
+
+import com.google.common.base.Function;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.UnmodifiableIterator;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.FileProvider;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.Runfiles;
+import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
+import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration.StrictDepsMode;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.rules.cpp.CcLinkParams.Builder;
+import com.google.devtools.build.lib.rules.cpp.CcLinkParamsStore;
+import com.google.devtools.build.lib.rules.cpp.CcSpecificLinkParamsProvider;
+import com.google.devtools.build.lib.rules.java.JavaConfiguration.JavaClasspathMode;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.util.FileType;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A class to create Java compile actions in a way that is consistent with java_library. Rules that
+ * generate source files and emulate java_library on top of that should use this class
+ * instead of the lower-level API in JavaCompilationHelper.
+ *
+ * <p>Rules that want to use this class are required to have an implicit dependency on the
+ * Java compiler.
+ */
+public final class JavaLibraryHelper {
+ /**
+ * Function for extracting the {@link JavaCompilationArgs} - note that it also handles .jar files.
+ */
+ private static final Function<TransitiveInfoCollection, JavaCompilationArgsProvider>
+ TO_COMPILATION_ARGS = new Function<TransitiveInfoCollection, JavaCompilationArgsProvider>() {
+ @Override
+ public JavaCompilationArgsProvider apply(TransitiveInfoCollection target) {
+ return forTarget(target);
+ }
+ };
+
+ /**
+ * Contains the providers as well as the compilation outputs.
+ */
+ public static final class Info {
+ private final Map<Class<? extends TransitiveInfoProvider>, TransitiveInfoProvider> providers;
+ private final JavaCompilationArtifacts compilationArtifacts;
+
+ private Info(Map<Class<? extends TransitiveInfoProvider>, TransitiveInfoProvider> providers,
+ JavaCompilationArtifacts compilationArtifacts) {
+ this.providers = Collections.unmodifiableMap(providers);
+ this.compilationArtifacts = compilationArtifacts;
+ }
+
+ public Map<Class<? extends TransitiveInfoProvider>, TransitiveInfoProvider> getProviders() {
+ return providers;
+ }
+
+ public JavaCompilationArtifacts getCompilationArtifacts() {
+ return compilationArtifacts;
+ }
+ }
+
+ private final RuleContext ruleContext;
+ private final BuildConfiguration configuration;
+
+ private Artifact output;
+ private final List<Artifact> sourceJars = new ArrayList<>();
+ /**
+ * Contains all the dependencies; these are treated as both compile-time and runtime dependencies.
+ * Some of these may not be complete configured targets; for backwards compatibility with some
+ * existing code, we sometimes only have pretend dependencies that only have a single {@link
+ * JavaCompilationArgsProvider}.
+ */
+ private final List<TransitiveInfoCollection> deps = new ArrayList<>();
+ private ImmutableList<String> javacOpts = ImmutableList.of();
+
+ private StrictDepsMode strictDepsMode = StrictDepsMode.OFF;
+ private JavaClasspathMode classpathMode = JavaClasspathMode.OFF;
+ private boolean emitProviders = true;
+
+ public JavaLibraryHelper(RuleContext ruleContext) {
+ this.ruleContext = ruleContext;
+ this.configuration = ruleContext.getConfiguration();
+ this.classpathMode = ruleContext.getFragment(JavaConfiguration.class).getReduceJavaClasspath();
+ }
+
+ /**
+ * Sets the final output jar; if this is not set, then the {@link #build} method throws an {@link
+ * IllegalStateException}. Note that this class may generate not just the output itself, but also
+ * a number of additional intermediate files and outputs.
+ */
+ public JavaLibraryHelper setOutput(Artifact output) {
+ this.output = output;
+ return this;
+ }
+
+ /**
+ * Adds the given source jars. Any .java files in these jars will be compiled.
+ */
+ public JavaLibraryHelper addSourceJars(Iterable<Artifact> sourceJars) {
+ Iterables.addAll(this.sourceJars, sourceJars);
+ return this;
+ }
+
+ /**
+ * Adds the given source jars. Any .java files in these jars will be compiled.
+ */
+ public JavaLibraryHelper addSourceJars(Artifact... sourceJars) {
+ return this.addSourceJars(Arrays.asList(sourceJars));
+ }
+
+ /**
+ * Adds the given compilation args as deps. Avoid this method, and prefer {@link #addDeps}
+ * instead; this method only exists for backward compatibility and may be removed at any time.
+ */
+ public JavaLibraryHelper addProcessedDeps(JavaCompilationArgs... deps) {
+ for (JavaCompilationArgs dep : deps) {
+ this.deps.add(toTransitiveInfoCollection(dep));
+ }
+ return this;
+ }
+
+ private static TransitiveInfoCollection toTransitiveInfoCollection(
+ final JavaCompilationArgs args) {
+ return new TransitiveInfoCollection() {
+ @Override
+ public <P extends TransitiveInfoProvider> P getProvider(Class<P> provider) {
+ if (JavaCompilationArgsProvider.class.equals(provider)) {
+ return provider.cast(new JavaCompilationArgsProvider(args, args));
+ }
+ return null;
+ }
+
+ @Override
+ public Label getLabel() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public BuildConfiguration getConfiguration() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Object get(String providerKey) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public UnmodifiableIterator<TransitiveInfoProvider> iterator() {
+ throw new UnsupportedOperationException();
+ }
+ };
+ }
+
+ /**
+ * Adds the given targets as deps. These are used as both compile-time and runtime dependencies.
+ */
+ public JavaLibraryHelper addDeps(Iterable<? extends TransitiveInfoCollection> deps) {
+ for (TransitiveInfoCollection dep : deps) {
+ Preconditions.checkArgument(dep.getConfiguration() == null
+ || dep.getConfiguration().equals(configuration));
+ this.deps.add(dep);
+ }
+ return this;
+ }
+
+ /**
+ * Sets the compiler options.
+ */
+ public JavaLibraryHelper setJavacOpts(Iterable<String> javacOpts) {
+ this.javacOpts = ImmutableList.copyOf(javacOpts);
+ return this;
+ }
+
+ /**
+ * Sets the mode that determines how strictly dependencies are checked.
+ */
+ public JavaLibraryHelper setStrictDepsMode(StrictDepsMode strictDepsMode) {
+ this.strictDepsMode = strictDepsMode;
+ return this;
+ }
+
+ /**
+ * Disables all providers, i.e., the resulting {@link Info} object will not contain any providers.
+ * Avoid this method - having this class compute the providers ensures consistency among all
+ * clients of this code.
+ */
+ public JavaLibraryHelper noProviders() {
+ this.emitProviders = false;
+ return this;
+ }
+
+ /**
+ * Creates the compile actions and providers.
+ */
+ public Info build(JavaSemantics semantics) {
+ Preconditions.checkState(output != null, "must have an output file; use setOutput()");
+ JavaTargetAttributes.Builder attributes = new JavaTargetAttributes.Builder(semantics);
+ attributes.addSourceJars(sourceJars);
+ addDepsToAttributes(attributes);
+ attributes.setStrictJavaDeps(strictDepsMode);
+ attributes.setRuleKind(ruleContext.getRule().getRuleClass());
+ attributes.setTargetLabel(ruleContext.getLabel());
+
+ if (isStrict() && classpathMode != JavaClasspathMode.OFF) {
+ addDependencyArtifactsToAttributes(attributes);
+ }
+
+ JavaCompilationArtifacts.Builder artifactsBuilder = new JavaCompilationArtifacts.Builder();
+ JavaCompilationHelper helper =
+ new JavaCompilationHelper(ruleContext, semantics, javacOpts, attributes);
+ Artifact outputDepsProto = helper.createOutputDepsProtoArtifact(output, artifactsBuilder);
+ helper.createCompileAction(output, null, outputDepsProto, null);
+ helper.createCompileTimeJarAction(output, outputDepsProto, artifactsBuilder);
+ artifactsBuilder.addRuntimeJar(output);
+ JavaCompilationArtifacts compilationArtifacts = artifactsBuilder.build();
+
+ Map<Class<? extends TransitiveInfoProvider>, TransitiveInfoProvider> providers =
+ new LinkedHashMap<>();
+ if (emitProviders) {
+ providers.put(JavaCompilationArgsProvider.class,
+ collectJavaCompilationArgs(compilationArtifacts));
+ providers.put(JavaSourceJarsProvider.class,
+ new JavaSourceJarsProvider(collectTransitiveJavaSourceJars(), sourceJars));
+ providers.put(JavaRunfilesProvider.class, collectJavaRunfiles(compilationArtifacts));
+ providers.put(JavaCcLinkParamsProvider.class,
+ new JavaCcLinkParamsProvider(createJavaCcLinkParamsStore()));
+ }
+ return new Info(providers, compilationArtifacts);
+ }
+
+ private void addDepsToAttributes(JavaTargetAttributes.Builder attributes) {
+ NestedSet<Artifact> directJars = null;
+ if (isStrict()) {
+ directJars = getNonRecursiveCompileTimeJarsFromDeps();
+ if (directJars != null) {
+ attributes.addDirectCompileTimeClassPathEntries(directJars);
+ attributes.addDirectJars(directJars);
+ }
+ }
+
+ JavaCompilationArgs args = JavaCompilationArgs.builder()
+ .addTransitiveDependencies(transformDeps(), true).build();
+ attributes.addCompileTimeClassPathEntries(args.getCompileTimeJars());
+ attributes.addRuntimeClassPathEntries(args.getRuntimeJars());
+ attributes.addInstrumentationMetadataEntries(args.getInstrumentationMetadata());
+ }
+
+ private NestedSet<Artifact> getNonRecursiveCompileTimeJarsFromDeps() {
+ JavaCompilationArgs.Builder builder = JavaCompilationArgs.builder();
+ builder.addTransitiveDependencies(transformDeps(), false);
+ return builder.build().getCompileTimeJars();
+ }
+
+ private void addDependencyArtifactsToAttributes(JavaTargetAttributes.Builder attributes) {
+ NestedSetBuilder<Artifact> compileTimeBuilder = NestedSetBuilder.stableOrder();
+ NestedSetBuilder<Artifact> runTimeBuilder = NestedSetBuilder.stableOrder();
+ for (JavaCompilationArgsProvider dep : transformDeps()) {
+ compileTimeBuilder.addTransitive(dep.getCompileTimeJavaDependencyArtifacts());
+ runTimeBuilder.addTransitive(dep.getRunTimeJavaDependencyArtifacts());
+ }
+ attributes.addCompileTimeDependencyArtifacts(compileTimeBuilder.build());
+ attributes.addRuntimeDependencyArtifacts(runTimeBuilder.build());
+ }
+
+ private Iterable<JavaCompilationArgsProvider> transformDeps() {
+ return Iterables.transform(deps, TO_COMPILATION_ARGS);
+ }
+
+ private static JavaCompilationArgsProvider forTarget(TransitiveInfoCollection target) {
+ if (target.getProvider(JavaCompilationArgsProvider.class) != null) {
+ // If the target has JavaCompilationArgs, we use those.
+ return target.getProvider(JavaCompilationArgsProvider.class);
+ } else {
+ // Otherwise we look for any jar files. It would be good to remove this, and require
+ // intermediate java_import rules in these cases.
+ NestedSet<Artifact> filesToBuild =
+ target.getProvider(FileProvider.class).getFilesToBuild();
+ final List<Artifact> jars = new ArrayList<>();
+ Iterables.addAll(jars, FileType.filter(filesToBuild, JavaSemantics.JAR));
+ JavaCompilationArgs args = JavaCompilationArgs.builder()
+ .addCompileTimeJars(jars)
+ .addRuntimeJars(jars)
+ .build();
+ return new JavaCompilationArgsProvider(args, args);
+ }
+ }
+
+ private boolean isStrict() {
+ return strictDepsMode != OFF;
+ }
+
+ private JavaCompilationArgsProvider collectJavaCompilationArgs(
+ JavaCompilationArtifacts compilationArtifacts) {
+ JavaCompilationArgs javaCompilationArgs =
+ collectJavaCompilationArgs(compilationArtifacts, false);
+ JavaCompilationArgs recursiveJavaCompilationArgs =
+ collectJavaCompilationArgs(compilationArtifacts, true);
+ return new JavaCompilationArgsProvider(javaCompilationArgs, recursiveJavaCompilationArgs);
+ }
+
+ /**
+ * Get compilation arguments for java compilation action.
+ *
+ * @param recursive a boolean specifying whether to get transitive
+ * dependencies
+ * @return java compilation args
+ */
+ private JavaCompilationArgs collectJavaCompilationArgs(
+ JavaCompilationArtifacts compilationArtifacts, boolean recursive) {
+ return JavaCompilationArgs.builder()
+ .merge(compilationArtifacts)
+ .addTransitiveDependencies(transformDeps(), recursive)
+ .build();
+ }
+
+ private NestedSet<Artifact> collectTransitiveJavaSourceJars() {
+ NestedSetBuilder<Artifact> transitiveJavaSourceJarBuilder =
+ NestedSetBuilder.<Artifact>stableOrder();
+ transitiveJavaSourceJarBuilder.addAll(sourceJars);
+ for (JavaSourceJarsProvider other : ruleContext.getPrerequisites(
+ "deps", Mode.TARGET, JavaSourceJarsProvider.class)) {
+ transitiveJavaSourceJarBuilder.addTransitive(other.getTransitiveSourceJars());
+ }
+ return transitiveJavaSourceJarBuilder.build();
+ }
+
+ private JavaRunfilesProvider collectJavaRunfiles(
+ JavaCompilationArtifacts javaCompilationArtifacts) {
+ Runfiles runfiles = new Runfiles.Builder()
+ // Compiled templates as well, for API.
+ .addArtifacts(javaCompilationArtifacts.getRuntimeJars())
+ .addTargets(deps, JavaRunfilesProvider.TO_RUNFILES)
+ .build();
+ return new JavaRunfilesProvider(runfiles);
+ }
+
+ private CcLinkParamsStore createJavaCcLinkParamsStore() {
+ return new CcLinkParamsStore() {
+ @Override
+ protected void collect(Builder builder, boolean linkingStatically, boolean linkShared) {
+ builder.addTransitiveLangTargets(
+ deps,
+ JavaCcLinkParamsProvider.TO_LINK_PARAMS);
+ builder.addTransitiveTargets(deps);
+ // TODO(bazel-team): This may need to be optional for some clients of this class.
+ builder.addTransitiveLangTargets(
+ deps,
+ CcSpecificLinkParamsProvider.TO_LINK_PARAMS);
+ }
+ };
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaNativeLibraryProvider.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaNativeLibraryProvider.java
new file mode 100644
index 0000000..8be42c0
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaNativeLibraryProvider.java
@@ -0,0 +1,43 @@
+// Copyright 2014 Google Inc. 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.rules.java;
+
+import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.rules.cpp.LinkerInput;
+
+/**
+ * A target that provides native libraries in the transitive closure of its deps that are needed for
+ * executing Java code.
+ */
+@Immutable
+public final class JavaNativeLibraryProvider implements TransitiveInfoProvider {
+
+ private final NestedSet<LinkerInput> transitiveJavaNativeLibraries;
+
+ public JavaNativeLibraryProvider(
+ NestedSet<LinkerInput> transitiveJavaNativeLibraries) {
+ this.transitiveJavaNativeLibraries = transitiveJavaNativeLibraries;
+ }
+
+ /**
+ * Collects native libraries in the transitive closure of its deps that are needed for executing
+ * Java code.
+ */
+ public NestedSet<LinkerInput> getTransitiveJavaNativeLibraries() {
+ return transitiveJavaNativeLibraries;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaNeverlinkInfoProvider.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaNeverlinkInfoProvider.java
new file mode 100644
index 0000000..75b36c1
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaNeverlinkInfoProvider.java
@@ -0,0 +1,35 @@
+// Copyright 2014 Google Inc. 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.rules.java;
+
+import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+
+/**
+ * A {@link TransitiveInfoProvider} that provides information about whether a Java archive
+ * is neverlink.
+ */
+@Immutable
+public final class JavaNeverlinkInfoProvider implements TransitiveInfoProvider {
+ private final boolean isNeverLink;
+
+ public JavaNeverlinkInfoProvider(boolean isNeverLink) {
+ this.isNeverLink = isNeverLink;
+ }
+
+ public boolean isNeverlink() {
+ return isNeverLink;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaOptions.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaOptions.java
new file mode 100644
index 0000000..f7ef0c7
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaOptions.java
@@ -0,0 +1,350 @@
+// Copyright 2014 Google Inc. 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.rules.java;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Multimap;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration.LabelConverter;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration.StrictDepsConverter;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration.StrictDepsMode;
+import com.google.devtools.build.lib.analysis.config.DefaultsPackage;
+import com.google.devtools.build.lib.analysis.config.FragmentOptions;
+import com.google.devtools.build.lib.rules.java.JavaConfiguration.JavaClasspathMode;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.syntax.Label.SyntaxException;
+import com.google.devtools.common.options.Converters.StringSetConverter;
+import com.google.devtools.common.options.EnumConverter;
+import com.google.devtools.common.options.Option;
+import com.google.devtools.common.options.TriState;
+
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Command-line options for building Java targets
+ */
+public class JavaOptions extends FragmentOptions {
+ // Defaults value for options
+ static final String DEFAULT_LANGTOOLS_BOOTCLASSPATH = "//tools/jdk:bootclasspath";
+ static final String DEFAULT_LANGTOOLS = "//tools/jdk:langtools";
+ static final String DEFAULT_JAVABUILDER = "//tools:java/JavaBuilder_deploy.jar";
+ static final String DEFAULT_SINGLEJAR = "//tools:java/SingleJar_deploy.jar";
+ static final String DEFAULT_JAVABASE = "//tools/jdk:jdk";
+ static final String DEFAULT_IJAR = "//tools:java/ijar";
+ static final String DEFAULT_TOOLCHAIN = "//tools/jdk:toolchain";
+
+ /**
+ * Converter for the --javawarn option.
+ */
+ public static class JavacWarnConverter extends StringSetConverter {
+ public JavacWarnConverter() {
+ super("all",
+ "cast",
+ "-cast",
+ "deprecation",
+ "-deprecation",
+ "divzero",
+ "-divzero",
+ "empty",
+ "-empty",
+ "fallthrough",
+ "-fallthrough",
+ "finally",
+ "-finally",
+ "none",
+ "options",
+ "-options",
+ "overrides",
+ "-overrides",
+ "path",
+ "-path",
+ "processing",
+ "-processing",
+ "rawtypes",
+ "-rawtypes",
+ "serial",
+ "-serial",
+ "unchecked",
+ "-unchecked"
+ );
+ }
+ }
+
+ /**
+ * Converter for the --experimental_java_classpath option.
+ */
+ public static class JavaClasspathModeConverter extends EnumConverter<JavaClasspathMode> {
+ public JavaClasspathModeConverter() {
+ super(JavaClasspathMode.class, "Java classpath reduction strategy");
+ }
+ }
+
+ @Option(name = "javabase",
+ defaultValue = DEFAULT_JAVABASE,
+ category = "version",
+ help = "JAVABASE used for the JDK invoked by Blaze. This is the "
+ + "JAVABASE which will be used to execute external Java "
+ + "commands.")
+ public String javaBase;
+
+ @Option(name = "java_toolchain",
+ defaultValue = DEFAULT_TOOLCHAIN,
+ category = "version",
+ converter = LabelConverter.class,
+ help = "The name of the toolchain rule for Java. Default is " + DEFAULT_TOOLCHAIN)
+ public Label javaToolchain;
+
+ @Option(name = "host_javabase",
+ defaultValue = DEFAULT_JAVABASE,
+ category = "version",
+ help = "JAVABASE used for the host JDK. This is the JAVABASE which is used to execute "
+ + " tools during a build.")
+ public String hostJavaBase;
+
+ @Option(name = "javacopt",
+ allowMultiple = true,
+ defaultValue = "",
+ category = "flags",
+ help = "Additional options to pass to javac.")
+ public List<String> javacOpts;
+
+ @Option(name = "jvmopt",
+ allowMultiple = true,
+ defaultValue = "",
+ category = "flags",
+ help = "Additional options to pass to the Java VM. These options will get added to the "
+ + "VM startup options of each java_binary target.")
+ public List<String> jvmOpts;
+
+ @Option(name = "javawarn",
+ converter = JavacWarnConverter.class,
+ defaultValue = "",
+ category = "flags",
+ allowMultiple = true,
+ help = "Additional javac warnings to enable when compiling Java source files.")
+ public List<String> javaWarns;
+
+ @Option(name = "use_ijars",
+ defaultValue = "true",
+ category = "strategy",
+ help = "If enabled, this option causes Java compilation to use interface jars. "
+ + "This will result in faster incremental compilation, "
+ + "but error messages can be different.")
+ public boolean useIjars;
+
+ @Deprecated
+ @Option(name = "use_src_ijars",
+ defaultValue = "false",
+ category = "undocumented",
+ help = "No-op. Kept here for backwards compatibility.")
+ public boolean useSourceIjars;
+
+ @Deprecated
+ @Option(name = "experimental_incremental_ijars",
+ defaultValue = "false",
+ category = "undocumented",
+ help = "No-op. Kept here for backwards compatibility.")
+ public boolean incrementalIjars;
+
+ @Option(name = "java_deps",
+ defaultValue = "true",
+ category = "strategy",
+ help = "Generate dependency information (for now, compile-time classpath) per Java target.")
+ public boolean javaDeps;
+
+ @Option(name = "experimental_java_deps",
+ defaultValue = "false",
+ category = "experimental",
+ expansion = "--java_deps",
+ deprecationWarning = "Use --java_deps instead")
+ public boolean experimentalJavaDeps;
+
+ @Option(name = "experimental_java_classpath",
+ allowMultiple = false,
+ defaultValue = "javabuilder",
+ converter = JavaClasspathModeConverter.class,
+ category = "semantics",
+ help = "Enables reduced classpaths for Java compilations.")
+ public JavaClasspathMode experimentalJavaClasspath;
+
+ @Option(name = "java_cpu",
+ defaultValue = "null",
+ category = "semantics",
+ help = "The Java target CPU. Default is k8.")
+ public String javaCpu;
+
+ @Option(name = "java_debug",
+ defaultValue = "null",
+ category = "testing",
+ expansion = {"--test_arg=--wrapper_script_flag=--debug", "--test_output=streamed",
+ "--test_strategy=exclusive", "--test_timeout=9999", "--nocache_test_results"},
+ help = "Causes the Java virtual machine of a java test to wait for a connection from a "
+ + "JDWP-compliant debugger (such as jdb) before starting the test. Implies "
+ + "-test_output=streamed."
+ )
+ public Void javaTestDebug;
+
+ @Option(name = "strict_java_deps",
+ allowMultiple = false,
+ defaultValue = "default",
+ converter = StrictDepsConverter.class,
+ category = "semantics",
+ help = "If true, checks that a Java target explicitly declares all directly used "
+ + "targets as dependencies.")
+ public StrictDepsMode strictJavaDeps;
+
+ @Option(name = "javabuilder_top",
+ defaultValue = DEFAULT_JAVABUILDER,
+ category = "version",
+ converter = LabelConverter.class,
+ help = "Label of the filegroup that contains the JavaBuilder jar.")
+ public Label javaBuilderTop;
+
+ @Option(name = "javabuilder_jvmopt",
+ allowMultiple = true,
+ defaultValue = "",
+ category = "undocumented",
+ help = "Additional options to pass to the JVM when invoking JavaBuilder.")
+ public List<String> javaBuilderJvmOpts;
+
+ @Option(name = "singlejar_top",
+ defaultValue = DEFAULT_SINGLEJAR,
+ category = "version",
+ converter = LabelConverter.class,
+ help = "Label of the filegroup that contains the SingleJar jar.")
+ public Label singleJarTop;
+
+ @Option(name = "ijar_top",
+ defaultValue = DEFAULT_IJAR,
+ category = "version",
+ converter = LabelConverter.class,
+ help = "Label of the filegroup that contains the ijar binary.")
+ public Label iJarTop;
+
+ @Option(name = "java_langtools",
+ defaultValue = DEFAULT_LANGTOOLS,
+ category = "version",
+ converter = LabelConverter.class,
+ help = "Label of the rule that produces the Java langtools jar.")
+ public Label javaLangtoolsJar;
+
+ @Option(name = "javac_bootclasspath",
+ defaultValue = DEFAULT_LANGTOOLS_BOOTCLASSPATH,
+ category = "version",
+ converter = LabelConverter.class,
+ help = "Label of the rule that produces the bootclasspath jars for javac to use.")
+ public Label javacBootclasspath;
+
+ @Option(name = "java_launcher",
+ defaultValue = "null",
+ converter = LabelConverter.class,
+ category = "semantics",
+ help = "If enabled, a specific Java launcher is used. "
+ + "The \"launcher\" attribute overrides this flag. ")
+ public Label javaLauncher;
+
+ @Option(name = "translations",
+ defaultValue = "auto",
+ category = "semantics",
+ help = "Translate Java messages; bundle all translations into the jar "
+ + "for each affected rule.")
+ public TriState bundleTranslations;
+
+ @Option(name = "message_translations",
+ defaultValue = "",
+ category = "semantics",
+ allowMultiple = true,
+ help = "The message translations used for translating messages in Java targets.")
+ public List<String> translationTargets;
+
+ @Option(name = "check_constraint",
+ allowMultiple = true,
+ defaultValue = "",
+ category = "checking",
+ help = "Check the listed constraint.")
+ public List<String> checkedConstraints;
+
+ @Override
+ public FragmentOptions getHost(boolean fallback) {
+ JavaOptions host = (JavaOptions) getDefault();
+
+ host.javaBase = hostJavaBase;
+ host.jvmOpts = ImmutableList.of("-client", "-XX:ErrorFile=/dev/stderr");
+
+ host.javacOpts = javacOpts;
+ host.javaLangtoolsJar = javaLangtoolsJar;
+ host.javaBuilderTop = javaBuilderTop;
+ host.javaToolchain = javaToolchain;
+ host.singleJarTop = singleJarTop;
+ host.iJarTop = iJarTop;
+
+ // Java builds often contain complicated code generators for which
+ // incremental build performance is important.
+ host.useIjars = useIjars;
+
+ host.javaDeps = javaDeps;
+ host.experimentalJavaClasspath = experimentalJavaClasspath;
+
+ return host;
+ }
+
+ @Override
+ public void addAllLabels(Multimap<String, Label> labelMap) {
+ addOptionalLabel(labelMap, "jdk", javaBase);
+ addOptionalLabel(labelMap, "jdk", hostJavaBase);
+ if (javaLauncher != null) {
+ labelMap.put("java_launcher", javaLauncher);
+ }
+ labelMap.put("javabuilder", javaBuilderTop);
+ labelMap.put("singlejar", singleJarTop);
+ labelMap.put("ijar", iJarTop);
+ labelMap.put("java_toolchain", javaToolchain);
+ labelMap.putAll("translation", getTranslationLabels());
+ }
+
+ @Override
+ public Map<String, Set<Label>> getDefaultsLabels(BuildConfiguration.Options commonOptions) {
+ Set<Label> jdkLabels = new LinkedHashSet<>();
+ DefaultsPackage.parseAndAdd(jdkLabels, javaBase);
+ DefaultsPackage.parseAndAdd(jdkLabels, hostJavaBase);
+ Map<String, Set<Label>> result = new HashMap<>();
+ result.put("JDK", jdkLabels);
+ result.put("JAVA_LANGTOOLS", ImmutableSet.of(javaLangtoolsJar));
+ result.put("JAVAC_BOOTCLASSPATH", ImmutableSet.of(javacBootclasspath));
+ result.put("JAVABUILDER", ImmutableSet.of(javaBuilderTop));
+ result.put("SINGLEJAR", ImmutableSet.of(singleJarTop));
+ result.put("IJAR", ImmutableSet.of(iJarTop));
+ result.put("JAVA_TOOLCHAIN", ImmutableSet.of(javaToolchain));
+
+ return result;
+ }
+
+ private Set<Label> getTranslationLabels() {
+ Set<Label> result = new LinkedHashSet<>();
+ for (String s : translationTargets) {
+ try {
+ Label label = Label.parseAbsolute(s);
+ result.add(label);
+ } catch (SyntaxException e) {
+ // We ignore this exception here - it will cause an error message at a later time.
+ }
+ }
+ return result;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaPlugin.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaPlugin.java
new file mode 100644
index 0000000..526d52c
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaPlugin.java
@@ -0,0 +1,57 @@
+// Copyright 2014 Google Inc. 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.rules.java;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTargetBuilder;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.rules.RuleConfiguredTargetFactory;
+
+/**
+ * Implementation for the java_plugin rule.
+ */
+public class JavaPlugin implements RuleConfiguredTargetFactory {
+
+ private final JavaSemantics semantics;
+
+ protected JavaPlugin(JavaSemantics semantics) {
+ this.semantics = semantics;
+ }
+
+ @Override
+ public ConfiguredTarget create(RuleContext ruleContext) throws InterruptedException {
+ JavaLibrary javaLibrary = new JavaLibrary(semantics);
+ JavaCommon common = new JavaCommon(ruleContext, semantics);
+ RuleConfiguredTargetBuilder builder = javaLibrary.init(ruleContext, common);
+ if (builder == null) {
+ return null;
+ }
+ builder.add(JavaPluginInfoProvider.class, new JavaPluginInfoProvider(
+ getProcessorClasses(ruleContext), common.getRuntimeClasspath()));
+ return builder.build();
+ }
+
+ /**
+ * Returns the class that should be passed to javac in order
+ * to run the annotation processor this class represents.
+ */
+ private ImmutableList<String> getProcessorClasses(RuleContext ruleContext) {
+ if (ruleContext.getRule().isAttributeValueExplicitlySpecified("processor_class")) {
+ return ImmutableList.of(ruleContext.attributes().get("processor_class", Type.STRING));
+ }
+ return ImmutableList.of();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaPluginInfoProvider.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaPluginInfoProvider.java
new file mode 100644
index 0000000..520a228
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaPluginInfoProvider.java
@@ -0,0 +1,52 @@
+// Copyright 2014 Google Inc. 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.rules.java;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+
+/**
+ * Provider for users of Java plugins.
+ */
+@Immutable
+public final class JavaPluginInfoProvider implements TransitiveInfoProvider {
+
+ private final ImmutableList<String> processorClasses;
+ private final NestedSet<Artifact> processorClasspath;
+
+ public JavaPluginInfoProvider(ImmutableList<String> processorClasses,
+ NestedSet<Artifact> processorClasspath) {
+ this.processorClasses = processorClasses;
+ this.processorClasspath = processorClasspath;
+ }
+
+ /**
+ * Returns the class that should be passed to javac in order
+ * to run the annotation processor this class represents.
+ */
+ public ImmutableList<String> getProcessorClasses() {
+ return processorClasses;
+ }
+
+ /**
+ * Returns the artifacts to add to the runtime classpath for this plugin.
+ */
+ public NestedSet<Artifact> getProcessorClasspath() {
+ return processorClasspath;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaPrimaryClassProvider.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaPrimaryClassProvider.java
new file mode 100644
index 0000000..fd90011
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaPrimaryClassProvider.java
@@ -0,0 +1,42 @@
+// Copyright 2014 Google Inc. 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.rules.java;
+
+import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+
+/**
+ * Provides the fully qualified name of the primary class to invoke for java targets.
+ */
+@Immutable
+public final class JavaPrimaryClassProvider implements TransitiveInfoProvider {
+
+ private final String primaryClass;
+
+ public JavaPrimaryClassProvider(String primaryClass) {
+ this.primaryClass = primaryClass;
+ }
+
+ /**
+ * Returns either the Java class whose main() method is to be invoked (when
+ * use_testrunner=0) or the Java subclass of junit.framework.Test that
+ * is to be tested by the test runner class (when use_testrunner=1).
+ *
+ * @return a fully qualified Java class name, or null if none could be
+ * determined.
+ */
+ public String getPrimaryClass() {
+ return primaryClass;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaRunfilesProvider.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaRunfilesProvider.java
new file mode 100644
index 0000000..b742d62
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaRunfilesProvider.java
@@ -0,0 +1,52 @@
+// Copyright 2014 Google Inc. 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.rules.java;
+
+import com.google.common.base.Function;
+import com.google.devtools.build.lib.analysis.Runfiles;
+import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
+import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+
+/**
+ * A {@link TransitiveInfoProvider} that supplies runfiles for Java dependencies.
+ */
+@Immutable
+public final class JavaRunfilesProvider implements TransitiveInfoProvider {
+ private final Runfiles runfiles;
+
+ public JavaRunfilesProvider(Runfiles runfiles) {
+ this.runfiles = runfiles;
+ }
+
+ public Runfiles getRunfiles() {
+ return runfiles;
+ }
+
+ /**
+ * Returns a function that gets the Java runfiles from a {@link TransitiveInfoCollection} or
+ * the empty runfiles instance if it does not contain that provider.
+ */
+ public static final Function<TransitiveInfoCollection, Runfiles> TO_RUNFILES =
+ new Function<TransitiveInfoCollection, Runfiles>() {
+ @Override
+ public Runfiles apply(TransitiveInfoCollection input) {
+ JavaRunfilesProvider provider = input.getProvider(JavaRunfilesProvider.class);
+ return provider == null
+ ? Runfiles.EMPTY
+ : provider.getRunfiles();
+ }
+ };
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaRuntimeClasspathProvider.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaRuntimeClasspathProvider.java
new file mode 100644
index 0000000..c8090df
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaRuntimeClasspathProvider.java
@@ -0,0 +1,43 @@
+// Copyright 2014 Google Inc. 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.rules.java;
+
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+
+/**
+ * Provider for the runtime classpath contributions of a Java binary.
+ *
+ * Used to exclude already-available artifacts from related binaries
+ * (e.g. plugins).
+ */
+@Immutable
+public final class JavaRuntimeClasspathProvider implements TransitiveInfoProvider {
+
+ private final NestedSet<Artifact> runtimeClasspath;
+
+ public JavaRuntimeClasspathProvider(NestedSet<Artifact> runtimeClasspath) {
+ this.runtimeClasspath = runtimeClasspath;
+ }
+
+ /**
+ * Returns the artifacts included on the runtime classpath of this binary.
+ */
+ public NestedSet<Artifact> getRuntimeClasspath() {
+ return runtimeClasspath;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaSemantics.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaSemantics.java
new file mode 100644
index 0000000..64b6214
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaSemantics.java
@@ -0,0 +1,351 @@
+// Copyright 2014 Google Inc. 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.rules.java;
+
+import static com.google.devtools.build.lib.packages.ImplicitOutputsFunction.fromTemplates;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.actions.ActionInput;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.AnalysisEnvironment;
+import com.google.devtools.build.lib.analysis.LanguageDependentFragment.LibraryLanguage;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTargetBuilder;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.Runfiles;
+import com.google.devtools.build.lib.analysis.Runfiles.Builder;
+import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
+import com.google.devtools.build.lib.analysis.actions.CustomCommandLine;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.packages.Attribute.LateBoundLabel;
+import com.google.devtools.build.lib.packages.Attribute.LateBoundLabelList;
+import com.google.devtools.build.lib.packages.ImplicitOutputsFunction.SafeImplicitOutputsFunction;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.rules.java.DeployArchiveBuilder.Compression;
+import com.google.devtools.build.lib.rules.test.InstrumentedFilesCollector.InstrumentationSpec;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.util.FileType;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.Collection;
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+/**
+ * Pluggable Java compilation semantics.
+ */
+public interface JavaSemantics {
+
+ public static final LibraryLanguage LANGUAGE = new LibraryLanguage("Java");
+
+ public static final SafeImplicitOutputsFunction JAVA_LIBRARY_CLASS_JAR =
+ fromTemplates("lib%{name}.jar");
+ public static final SafeImplicitOutputsFunction JAVA_LIBRARY_SOURCE_JAR =
+ fromTemplates("lib%{name}-src.jar");
+ public static final SafeImplicitOutputsFunction JAVA_BINARY_CLASS_JAR =
+ fromTemplates("%{name}.jar");
+ public static final SafeImplicitOutputsFunction JAVA_BINARY_SOURCE_JAR =
+ fromTemplates("%{name}-src.jar");
+ public static final SafeImplicitOutputsFunction JAVA_BINARY_DEPLOY_JAR =
+ fromTemplates("%{name}_deploy.jar");
+ public static final SafeImplicitOutputsFunction JAVA_BINARY_DEPLOY_SOURCE_JAR =
+ fromTemplates("%{name}_deploy-src.jar");
+
+ public static final FileType JAVA_SOURCE = FileType.of(".java");
+ public static final FileType JAR = FileType.of(".jar");
+ public static final FileType PROPERTIES = FileType.of(".properties");
+ public static final FileType SOURCE_JAR = FileType.of(".srcjar");
+ // TODO(bazel-team): Rename this metadata extension to something meaningful.
+ public static final FileType COVERAGE_METADATA = FileType.of(".em");
+
+ /**
+ * Label to the Java Toolchain rule. It is resolved from a label given in the java options.
+ */
+ static final String JAVA_TOOLCHAIN_LABEL = "//tools/defaults:java_toolchain";
+
+ public static final LateBoundLabel<BuildConfiguration> JAVA_TOOLCHAIN =
+ new LateBoundLabel<BuildConfiguration>(JAVA_TOOLCHAIN_LABEL) {
+ @Override
+ public Label getDefault(Rule rule, BuildConfiguration configuration) {
+ return configuration.getFragment(JavaConfiguration.class).getToolchainLabel();
+ }
+ };
+
+ /**
+ * Name of the output group used for source jars.
+ */
+ public static final String SOURCE_JARS_OUTPUT_GROUP = "source_jars";
+
+ /**
+ * Label of a pseudo-filegroup that contains all jdk files for all
+ * configurations, as specified on the command-line.
+ */
+ public static final String JDK_LABEL = "//tools/defaults:jdk";
+
+ /**
+ * Label of a pseudo-filegroup that contains the boot-classpath entries.
+ */
+ public static final String JAVAC_BOOTCLASSPATH_LABEL = "//tools/defaults:javac_bootclasspath";
+
+ /**
+ * Label of the JavaBuilder JAR used for compiling Java source code.
+ */
+ public static final String JAVABUILDER_LABEL = "//tools/defaults:javabuilder";
+
+ /**
+ * Label of the SingleJar JAR used for creating deploy jars.
+ */
+ public static final String SINGLEJAR_LABEL = "//tools/defaults:singlejar";
+
+ /**
+ * Label of pseudo-cc_binary that tells Blaze a java target's JAVABIN is never to be replaced by
+ * the contents of --java_launcher; only the JDK's launcher will ever be used.
+ */
+ public static final Label JDK_LAUNCHER_LABEL =
+ Label.parseAbsoluteUnchecked("//third_party/java/jdk:jdk_launcher");
+
+ /**
+ * Implementation for the :jvm attribute.
+ */
+ public static final LateBoundLabel<BuildConfiguration> JVM =
+ new LateBoundLabel<BuildConfiguration>(JDK_LABEL) {
+ @Override
+ public Label getDefault(Rule rule, BuildConfiguration configuration) {
+ return configuration.getFragment(Jvm.class).getJvmLabel();
+ }
+ };
+
+ /**
+ * Implementation for the :host_jdk attribute.
+ */
+ public static final LateBoundLabel<BuildConfiguration> HOST_JDK =
+ new LateBoundLabel<BuildConfiguration>(JDK_LABEL) {
+ @Override
+ public boolean useHostConfiguration() {
+ return true;
+ }
+
+ @Override
+ public Label getDefault(Rule rule, BuildConfiguration configuration) {
+ return configuration.getFragment(Jvm.class).getJvmLabel();
+ }
+ };
+
+ /**
+ * Implementation for the :java_launcher attribute. Note that the Java launcher is disabled by
+ * default, so it returns null for the configuration-independent default value.
+ */
+ public static final LateBoundLabel<BuildConfiguration> JAVA_LAUNCHER =
+ new LateBoundLabel<BuildConfiguration>() {
+ @Override
+ public Label getDefault(Rule rule, BuildConfiguration configuration) {
+ return configuration.getFragment(JavaConfiguration.class).getJavaLauncherLabel();
+ }
+ };
+
+ public static final LateBoundLabelList<BuildConfiguration> JAVA_PLUGINS =
+ new LateBoundLabelList<BuildConfiguration>() {
+ @Override
+ public List<Label> getDefault(Rule rule, BuildConfiguration configuration) {
+ return ImmutableList.copyOf(configuration.getPlugins());
+ }
+ };
+
+ public static final String IJAR_LABEL = "//tools/defaults:ijar";
+
+ /**
+ * Verifies if the rule contains and errors.
+ *
+ * <p>Errors should be signaled through {@link RuleContext}.
+ */
+ void checkRule(RuleContext ruleContext, JavaCommon javaCommon);
+
+ /**
+ * Returns the main class of a Java binary.
+ */
+ String getMainClass(RuleContext ruleContext, JavaCommon javaCommon);
+
+ /**
+ * Returns the resources contributed by a Java rule (usually the contents of the
+ * {@code resources} attribute)
+ */
+ ImmutableList<Artifact> collectResources(RuleContext ruleContext);
+
+ /**
+ * Creates the instrumentation metadata artifact for the specified output .jar .
+ */
+ @Nullable Artifact createInstrumentationMetadataArtifact(
+ AnalysisEnvironment analysisEnvironment, Artifact outputJar);
+
+ /**
+ * May add extra command line options to the Java compile command line.
+ */
+ void buildJavaCommandLine(Collection<Artifact> outputs, BuildConfiguration configuration,
+ CustomCommandLine.Builder result);
+
+
+ /**
+ * Constructs the command line to call SingleJar to join all artifacts from
+ * {@code classpath} (java code) and {@code resources} into {@code output}.
+ */
+ CustomCommandLine buildSingleJarCommandLine(BuildConfiguration configuration,
+ Artifact output, String mainClass, ImmutableList<String> manifestLines,
+ Iterable<Artifact> buildInfoFiles, ImmutableList<Artifact> resources,
+ Iterable<Artifact> classpath, boolean includeBuildData,
+ Compression compression, Artifact launcher);
+
+ /**
+ * Creates the action that writes the Java executable stub script.
+ */
+ void createStubAction(RuleContext ruleContext, final JavaCommon javaCommon,
+ List<String> jvmFlags, Artifact executable, String javaStartClass,
+ String javaExecutable);
+
+ /**
+ * Adds extra runfiles for a {@code java_binary} rule.
+ */
+ void addRunfilesForBinary(RuleContext ruleContext, Artifact launcher,
+ Runfiles.Builder runfilesBuilder);
+
+ /**
+ * Adds extra runfiles for a {@code java_library} rule.
+ */
+ void addRunfilesForLibrary(RuleContext ruleContext, Runfiles.Builder runfilesBuilder);
+
+ /**
+ * Returns the coverage instrumentation specification to be used in Java rules.
+ */
+ InstrumentationSpec getCoverageInstrumentationSpec();
+
+ /**
+ * Returns the additional options to be passed to javac.
+ */
+ Iterable<String> getExtraJavacOpts(RuleContext ruleContext);
+
+ /**
+ * Add additional targets to be treated as direct dependencies.
+ */
+ void collectTargetsTreatedAsDeps(
+ RuleContext ruleContext, ImmutableList.Builder<TransitiveInfoCollection> builder);
+
+ /**
+ * Enables coverage support for the java target - adds instrumented jar to the classpath and
+ * modifies main class.
+ *
+ * @return new main class
+ */
+ String addCoverageSupport(JavaCompilationHelper helper,
+ JavaTargetAttributes.Builder attributes,
+ Artifact executable, Artifact instrumentationMetadata,
+ JavaCompilationArtifacts.Builder javaArtifactsBuilder, String mainClass);
+
+ /**
+ * Return the JVM flags to be used in a Java binary.
+ */
+ Iterable<String> getJvmFlags(RuleContext ruleContext, JavaCommon javaCommon,
+ Artifact launcher, List<String> userJvmFlags);
+
+ /**
+ * Adds extra providers to a Java target.
+ */
+ void addProviders(RuleContext ruleContext,
+ JavaCommon javaCommon,
+ List<String> jvmFlags,
+ Artifact classJar,
+ Artifact srcJar,
+ Artifact gensrcJar,
+ ImmutableMap<Artifact, Artifact> compilationToRuntimeJarMap,
+ JavaCompilationHelper helper,
+ NestedSetBuilder<Artifact> filesBuilder,
+ RuleConfiguredTargetBuilder ruleBuilder);
+
+ /**
+ * Tell if a build with the given configuration should use strict java dependencies. This method
+ * enforces strict java dependencies off if it returns false.
+ */
+ boolean useStrictJavaDeps(BuildConfiguration configuration);
+
+ /**
+ * Translates XMB messages to translations artifact suitable for Java targets.
+ */
+ Collection<Artifact> translate(RuleContext ruleContext, JavaConfiguration javaConfig,
+ List<Artifact> messages);
+
+ /**
+ * Get the launcher artifact for a java binary, creating the necessary actions for it.
+ *
+ * @param ruleContext The rule context
+ * @param common The common helper class.
+ * @param deployArchiveBuilder the builder to construct the deploy archive action (mutable).
+ * @param runfilesBuilder the builder to construct the list of runfiles (mutable).
+ * @param jvmFlags the list of flags to pass to the JVM when running the Java binary (mutable).
+ * @param attributesBuilder the builder to construct the list of attributes of this target
+ * (mutable).
+ * @return the launcher as an artifact
+ */
+ Artifact getLauncher(final RuleContext ruleContext, final JavaCommon common,
+ DeployArchiveBuilder deployArchiveBuilder, Runfiles.Builder runfilesBuilder,
+ List<String> jvmFlags, JavaTargetAttributes.Builder attributesBuilder);
+
+ /**
+ * Add extra dependencies for runfiles of a Java binary.
+ */
+ void addDependenciesForRunfiles(RuleContext ruleContext, Builder builder);
+
+ /**
+ * Determines if we should enforce the use of the :java_launcher target to determine the java
+ * launcher artifact even if the --java_launcher option was not specified.
+ */
+ boolean forceUseJavaLauncherTarget(RuleContext ruleContext);
+
+ /**
+ * Add a source artifact to a {@link JavaTargetAttributes.Builder}. It is called when a source
+ * artifact is processed but is not matched by default patterns in the
+ * {@link JavaTargetAttributes.Builder#addSourceArtifacts(Iterable)} method. The semantics can
+ * then detect its custom artifact types and add it to the builder.
+ */
+ void addArtifactToJavaTargetAttribute(JavaTargetAttributes.Builder builder, Artifact srcArtifact);
+
+ /**
+ * Works on the list of dependencies of a java target to builder the {@link JavaTargetAttributes}.
+ * This work is performed in {@link JavaCommon} for all java targets.
+ */
+ void commonDependencyProcessing(RuleContext ruleContext, JavaTargetAttributes.Builder attributes,
+ Collection<? extends TransitiveInfoCollection> deps);
+
+ /**
+ * Returns an list of {@link ActionInput} that the {@link JavaCompileAction} generates and
+ * that should be cached.
+ */
+ Collection<ActionInput> getExtraJavaCompileOutputs(PathFragment classDirectory);
+
+ /**
+ * Takes the path of a Java resource and tries to determine the Java
+ * root relative path of the resource.
+ *
+ * @param path the root relative path of the resource.
+ * @return the Java root relative path of the resource of the root
+ * relative path of the resource if no Java root relative path can be
+ * determined.
+ */
+ PathFragment getJavaResourcePath(PathFragment path);
+
+ /**
+ * @return a list of extra arguments to appends to the runfiles support.
+ */
+ List<String> getExtraArguments(RuleContext ruleContext, JavaCommon javaCommon);
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaSourceJarsProvider.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaSourceJarsProvider.java
new file mode 100644
index 0000000..2bb3597
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaSourceJarsProvider.java
@@ -0,0 +1,52 @@
+// Copyright 2014 Google Inc. 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.rules.java;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+
+/**
+ * The collection of source jars from the transitive closure.
+ */
+@Immutable
+public final class JavaSourceJarsProvider implements TransitiveInfoProvider {
+
+ private final NestedSet<Artifact> transitiveSourceJars;
+ private final ImmutableList<Artifact> sourceJars;
+
+ public JavaSourceJarsProvider(NestedSet<Artifact> transitiveSourceJars,
+ Iterable<Artifact> sourceJars) {
+ this.transitiveSourceJars = transitiveSourceJars;
+ this.sourceJars = ImmutableList.copyOf(sourceJars);
+ }
+
+ /**
+ * Returns all the source jars in the transitive closure, that can be reached by a chain of
+ * JavaSourceJarsProvider instances.
+ */
+ public NestedSet<Artifact> getTransitiveSourceJars() {
+ return transitiveSourceJars;
+ }
+
+ /**
+ * Return the source jars that are to be built when the target is on the command line.
+ */
+ public ImmutableList<Artifact> getSourceJars() {
+ return sourceJars;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaTargetAttributes.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaTargetAttributes.java
new file mode 100644
index 0000000..a7fc497
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaTargetAttributes.java
@@ -0,0 +1,603 @@
+// Copyright 2014 Google Inc. 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.rules.java;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicates;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.collect.IterablesChain;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.rules.cpp.CppFileTypes;
+import com.google.devtools.build.lib.syntax.Label;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * An object that captures the temporary state we need to pass around while
+ * the initialization hook for a java rule is running.
+ */
+public class JavaTargetAttributes {
+
+ private static void checkJar(Artifact classPathEntry) {
+ if (!JavaSemantics.JAR.matches(classPathEntry.getFilename())) {
+ throw new IllegalArgumentException(
+ "not a jar file: " + classPathEntry.prettyPrint());
+ }
+ }
+
+ /**
+ * A builder class for JavaTargetAttributes.
+ */
+ public static class Builder {
+
+ // The order of source files is important, and there must not be duplicates.
+ // Unfortunately, there is no interface in Java that represents a collection
+ // without duplicates that has a stable and deterministic iteration order,
+ // but is not sorted according to a property of the elements. Thus we are
+ // stuck with Set.
+ private final Set<Artifact> sourceFiles = new LinkedHashSet<>();
+ private final Set<Artifact> jarFiles = new LinkedHashSet<>();
+ private final Set<Artifact> compileTimeJarFiles = new LinkedHashSet<>();
+
+ private final NestedSetBuilder<Artifact> runtimeClassPath =
+ NestedSetBuilder.naiveLinkOrder();
+
+ private final NestedSetBuilder<Artifact> compileTimeClassPath =
+ NestedSetBuilder.naiveLinkOrder();
+
+ private final List<Artifact> bootClassPath = new ArrayList<>();
+ private final List<Artifact> nativeLibraries = new ArrayList<>();
+
+ private final Set<Artifact> processorPath = new LinkedHashSet<>();
+ private final Set<String> processorNames = new LinkedHashSet<>();
+
+ private final List<Artifact> resources = new ArrayList<>();
+ private final List<Artifact> messages = new ArrayList<>();
+ private final List<Artifact> instrumentationMetadata = new ArrayList<>();
+ private final List<Artifact> sourceJars = new ArrayList<>();
+
+ private final List<Artifact> classPathResources = new ArrayList<>();
+
+ private BuildConfiguration.StrictDepsMode strictJavaDeps =
+ BuildConfiguration.StrictDepsMode.OFF;
+ private final List<Artifact> directJars = new ArrayList<>();
+ private final List<Artifact> compileTimeDependencyArtifacts = new ArrayList<>();
+ private final List<Artifact> runtimeDependencyArtifacts = new ArrayList<>();
+ private String ruleKind;
+ private Label targetLabel;
+
+ private final NestedSetBuilder<Artifact> excludedArtifacts =
+ NestedSetBuilder.naiveLinkOrder();
+
+ private boolean built = false;
+
+ private final JavaSemantics semantics;
+
+ public Builder(JavaSemantics semantics) {
+ this.semantics = semantics;
+ }
+
+ public Builder addSourceArtifacts(Iterable<Artifact> sourceArtifacts) {
+ Preconditions.checkArgument(!built);
+ for (Artifact srcArtifact : sourceArtifacts) {
+ String srcFilename = srcArtifact.getExecPathString();
+ if (JavaSemantics.JAR.matches(srcFilename)) {
+ runtimeClassPath.add(srcArtifact);
+ jarFiles.add(srcArtifact);
+ } else if (JavaSemantics.SOURCE_JAR.matches(srcFilename)) {
+ sourceJars.add(srcArtifact);
+ } else if (JavaSemantics.PROPERTIES.matches(srcFilename)) {
+ // output files of the message compiler
+ resources.add(srcArtifact);
+ } else if (JavaSemantics.JAVA_SOURCE.matches(srcFilename)) {
+ sourceFiles.add(srcArtifact);
+ } else {
+ // try specific cases from the semantics.
+ semantics.addArtifactToJavaTargetAttribute(this, srcArtifact);
+ }
+ }
+ return this;
+ }
+
+ public Builder addSourceFiles(Iterable<Artifact> sourceFiles) {
+ Preconditions.checkArgument(!built);
+ for (Artifact artifact : sourceFiles) {
+ if (JavaSemantics.JAVA_SOURCE.matches(artifact.getFilename())) {
+ this.sourceFiles.add(artifact);
+ }
+ }
+ return this;
+ }
+
+ public Builder merge(JavaCompilationArgs context) {
+ Preconditions.checkArgument(!built);
+ addCompileTimeClassPathEntries(context.getCompileTimeJars());
+ addRuntimeClassPathEntries(context.getRuntimeJars());
+ addInstrumentationMetadataEntries(context.getInstrumentationMetadata());
+ return this;
+ }
+
+ public Builder addSourceJars(Collection<Artifact> sourceJars) {
+ Preconditions.checkArgument(!built);
+ this.sourceJars.addAll(sourceJars);
+ return this;
+ }
+
+ public Builder addSourceJar(Artifact sourceJar) {
+ Preconditions.checkArgument(!built);
+ this.sourceJars.add(sourceJar);
+ return this;
+ }
+
+ public Builder addCompileTimeJarFiles(Iterable<Artifact> jars) {
+ Preconditions.checkArgument(!built);
+ Iterables.addAll(compileTimeJarFiles, jars);
+ return this;
+ }
+
+ public Builder addRuntimeClassPathEntry(Artifact classPathEntry) {
+ Preconditions.checkArgument(!built);
+ checkJar(classPathEntry);
+ runtimeClassPath.add(classPathEntry);
+ return this;
+ }
+
+ public Builder addRuntimeClassPathEntries(NestedSet<Artifact> classPathEntries) {
+ Preconditions.checkArgument(!built);
+ runtimeClassPath.addTransitive(classPathEntries);
+ return this;
+ }
+
+ public Builder addCompileTimeClassPathEntries(NestedSet<Artifact> entries) {
+ Preconditions.checkArgument(!built);
+ compileTimeClassPath.addTransitive(entries);
+ return this;
+ }
+
+ public Builder addDirectCompileTimeClassPathEntries(Iterable<Artifact> entries) {
+ Preconditions.checkArgument(!built);
+ // The other version is preferred as it is more memory-efficient.
+ for (Artifact classPathEntry : entries) {
+ compileTimeClassPath.add(classPathEntry);
+ }
+ return this;
+ }
+
+ public Builder setRuleKind(String ruleKind) {
+ Preconditions.checkArgument(!built);
+ this.ruleKind = ruleKind;
+ return this;
+ }
+
+ public Builder setTargetLabel(Label targetLabel) {
+ Preconditions.checkArgument(!built);
+ this.targetLabel = targetLabel;
+ return this;
+ }
+
+ /**
+ * Sets the bootclasspath to be passed to the Java compiler.
+ *
+ * <p>If this method is called, then the bootclasspath specified in this JavaTargetAttributes
+ * instance overrides the default bootclasspath.
+ */
+ public Builder setBootClassPath(List<Artifact> jars) {
+ Preconditions.checkArgument(!built);
+ Preconditions.checkArgument(!jars.isEmpty());
+ Preconditions.checkState(bootClassPath.isEmpty());
+ bootClassPath.addAll(jars);
+ return this;
+ }
+
+ public Builder addExcludedArtifacts(NestedSet<Artifact> toExclude) {
+ Preconditions.checkArgument(!built);
+ excludedArtifacts.addTransitive(toExclude);
+ return this;
+ }
+
+ /**
+ * Controls how strict the javac compiler will be in checking correct use of
+ * direct dependencies.
+ *
+ * @param strictDeps one of WARN, ERROR or OFF
+ */
+ public Builder setStrictJavaDeps(BuildConfiguration.StrictDepsMode strictDeps) {
+ Preconditions.checkArgument(!built);
+ strictJavaDeps = strictDeps;
+ return this;
+ }
+
+ /**
+ * In tandem with strictJavaDeps, directJars represents a subset of the
+ * compile-time, classpath jars that were provided by direct dependencies.
+ * When strictJavaDeps is OFF, there is no need to provide directJars, and
+ * no extra information is passed to javac. When strictJavaDeps is set to
+ * WARN or ERROR, the compiler command line will include extra flags to
+ * indicate the warning/error policy and to map the classpath jars to direct
+ * or transitive dependencies, using the information in directJars. The extra
+ * flags are formatted like this (same for --indirect_dependency):
+ * --direct_dependency
+ * foo/bar/lib.jar
+ * //java/com/google/foo:bar
+ *
+ * @param directJars
+ */
+ public Builder addDirectJars(Iterable<Artifact> directJars) {
+ Preconditions.checkArgument(!built);
+ Iterables.addAll(this.directJars, directJars);
+ return this;
+ }
+
+ public Builder addCompileTimeDependencyArtifacts(Iterable<Artifact> dependencyArtifacts) {
+ Preconditions.checkArgument(!built);
+ Iterables.addAll(this.compileTimeDependencyArtifacts, dependencyArtifacts);
+ return this;
+ }
+
+ public Builder addRuntimeDependencyArtifacts(Iterable<Artifact> dependencyArtifacts) {
+ Preconditions.checkArgument(!built);
+ Iterables.addAll(this.runtimeDependencyArtifacts, dependencyArtifacts);
+ return this;
+ }
+
+ public Builder addInstrumentationMetadataEntries(Iterable<Artifact> metadataEntries) {
+ Preconditions.checkArgument(!built);
+ Iterables.addAll(instrumentationMetadata, metadataEntries);
+ return this;
+ }
+
+ public Builder addNativeLibrary(Artifact nativeLibrary) {
+ Preconditions.checkArgument(!built);
+ String name = nativeLibrary.getFilename();
+ if (CppFileTypes.INTERFACE_SHARED_LIBRARY.matches(name)) {
+ return this;
+ }
+ if (!(CppFileTypes.SHARED_LIBRARY.matches(name)
+ || CppFileTypes.VERSIONED_SHARED_LIBRARY.matches(name))) {
+ throw new IllegalArgumentException("not a shared library :" + nativeLibrary.prettyPrint());
+ }
+ nativeLibraries.add(nativeLibrary);
+ return this;
+ }
+
+ public Builder addNativeLibraries(Iterable<Artifact> nativeLibraries) {
+ Preconditions.checkArgument(!built);
+ for (Artifact nativeLibrary : nativeLibraries) {
+ addNativeLibrary(nativeLibrary);
+ }
+ return this;
+ }
+
+ public Builder addMessages(Collection<Artifact> messages) {
+ Preconditions.checkArgument(!built);
+ this.messages.addAll(messages);
+ return this;
+ }
+
+ public Builder addMessage(Artifact messagesArtifact) {
+ Preconditions.checkArgument(!built);
+ this.messages.add(messagesArtifact);
+ return this;
+ }
+
+ public Builder addResources(Collection<Artifact> resources) {
+ Preconditions.checkArgument(!built);
+ this.resources.addAll(resources);
+ return this;
+ }
+
+ public Builder addResource(Artifact resource) {
+ Preconditions.checkArgument(!built);
+ resources.add(resource);
+ return this;
+ }
+
+ public Builder addProcessorName(String processor) {
+ Preconditions.checkArgument(!built);
+ processorNames.add(processor);
+ return this;
+ }
+
+ public Builder addProcessorPath(Iterable<Artifact> jars) {
+ Preconditions.checkArgument(!built);
+ Iterables.addAll(processorPath, jars);
+ return this;
+ }
+
+ public Builder addClassPathResources(List<Artifact> classPathResources) {
+ Preconditions.checkArgument(!built);
+ this.classPathResources.addAll(classPathResources);
+ return this;
+ }
+
+ public Builder addClassPathResource(Artifact classPathResource) {
+ Preconditions.checkArgument(!built);
+ this.classPathResources.add(classPathResource);
+ return this;
+ }
+
+ public JavaTargetAttributes build() {
+ built = true;
+ return new JavaTargetAttributes(
+ sourceFiles,
+ jarFiles,
+ compileTimeJarFiles,
+ runtimeClassPath,
+ compileTimeClassPath,
+ bootClassPath,
+ nativeLibraries,
+ processorPath,
+ processorNames,
+ resources,
+ messages,
+ sourceJars,
+ classPathResources,
+ directJars,
+ compileTimeDependencyArtifacts,
+ ruleKind,
+ targetLabel,
+ excludedArtifacts,
+ strictJavaDeps);
+ }
+
+ // TODO(bazel-team): Remove these 5 methods.
+ @Deprecated
+ public Set<Artifact> getSourceFiles() {
+ return sourceFiles;
+ }
+
+ @Deprecated
+ public boolean hasSourceFiles() {
+ return !sourceFiles.isEmpty();
+ }
+
+ @Deprecated
+ public List<Artifact> getInstrumentationMetadata() {
+ return instrumentationMetadata;
+ }
+
+ @Deprecated
+ public boolean hasSourceJars() {
+ return !sourceJars.isEmpty();
+ }
+
+ @Deprecated
+ public boolean hasJarFiles() {
+ return !jarFiles.isEmpty();
+ }
+ }
+
+ //
+ // -------------------------- END OF BUILDER CLASS -------------------------
+ //
+
+ private final ImmutableSet<Artifact> sourceFiles;
+ private final ImmutableSet<Artifact> jarFiles;
+ private final ImmutableSet<Artifact> compileTimeJarFiles;
+
+ private final NestedSet<Artifact> runtimeClassPath;
+ private final NestedSet<Artifact> compileTimeClassPath;
+
+ private final ImmutableList<Artifact> bootClassPath;
+ private final ImmutableList<Artifact> nativeLibraries;
+
+ private final ImmutableSet<Artifact> processorPath;
+ private final ImmutableSet<String> processorNames;
+
+ private final ImmutableList<Artifact> resources;
+ private final ImmutableList<Artifact> messages;
+ private final ImmutableList<Artifact> sourceJars;
+
+ private final ImmutableList<Artifact> classPathResources;
+
+ private final ImmutableList<Artifact> directJars;
+ private final ImmutableList<Artifact> compileTimeDependencyArtifacts;
+ private final String ruleKind;
+ private final Label targetLabel;
+
+ private final NestedSet<Artifact> excludedArtifacts;
+ private final BuildConfiguration.StrictDepsMode strictJavaDeps;
+
+ /**
+ * Constructor of JavaTargetAttributes.
+ */
+ private JavaTargetAttributes(
+ Set<Artifact> sourceFiles,
+ Set<Artifact> jarFiles,
+ Set<Artifact> compileTimeJarFiles,
+ NestedSetBuilder<Artifact> runtimeClassPath,
+ NestedSetBuilder<Artifact> compileTimeClassPath,
+ List<Artifact> bootClassPath,
+ List<Artifact> nativeLibraries,
+ Set<Artifact> processorPath,
+ Set<String> processorNames,
+ List<Artifact> resources,
+ List<Artifact> messages,
+ List<Artifact> sourceJars,
+ List<Artifact> classPathResources,
+ List<Artifact> directJars,
+ List<Artifact> compileTimeDependencyArtifacts,
+ String ruleKind,
+ Label targetLabel,
+ NestedSetBuilder<Artifact> excludedArtifacts,
+ BuildConfiguration.StrictDepsMode strictJavaDeps) {
+ this.sourceFiles = ImmutableSet.copyOf(sourceFiles);
+ this.jarFiles = ImmutableSet.copyOf(jarFiles);
+ this.compileTimeJarFiles = ImmutableSet.copyOf(compileTimeJarFiles);
+ this.runtimeClassPath = runtimeClassPath.build();
+ this.compileTimeClassPath = compileTimeClassPath.build();
+ this.bootClassPath = ImmutableList.copyOf(bootClassPath);
+ this.nativeLibraries = ImmutableList.copyOf(nativeLibraries);
+ this.processorPath = ImmutableSet.copyOf(processorPath);
+ this.processorNames = ImmutableSet.copyOf(processorNames);
+ this.resources = ImmutableList.copyOf(resources);
+ this.messages = ImmutableList.copyOf(messages);
+ this.sourceJars = ImmutableList.copyOf(sourceJars);
+ this.classPathResources = ImmutableList.copyOf(classPathResources);
+ this.directJars = ImmutableList.copyOf(directJars);
+ this.compileTimeDependencyArtifacts = ImmutableList.copyOf(compileTimeDependencyArtifacts);
+ this.ruleKind = ruleKind;
+ this.targetLabel = targetLabel;
+ this.excludedArtifacts = excludedArtifacts.build();
+ this.strictJavaDeps = strictJavaDeps;
+ }
+
+ public List<Artifact> getDirectJars() {
+ return directJars;
+ }
+
+ public List<Artifact> getCompileTimeDependencyArtifacts() {
+ return compileTimeDependencyArtifacts;
+ }
+
+ public List<Artifact> getSourceJars() {
+ return sourceJars;
+ }
+
+ public Collection<Artifact> getResources() {
+ return resources;
+ }
+
+ public List<Artifact> getMessages() {
+ return messages;
+ }
+
+ public ImmutableList<Artifact> getClassPathResources() {
+ return classPathResources;
+ }
+
+ private NestedSet<Artifact> getExcludedArtifacts() {
+ return excludedArtifacts;
+ }
+
+ /**
+ * Returns the artifacts needed on the runtime classpath of this target.
+ *
+ * See also {@link #getRuntimeClassPathForArchive()}.
+ */
+ public NestedSet<Artifact> getRuntimeClassPath() {
+ return runtimeClassPath;
+ }
+
+ /**
+ * Returns the classpath artifacts needed in a deploy jar for this target.
+ *
+ * This excludes the artifacts made available by jars in the deployment
+ * environment.
+ */
+ public Iterable<Artifact> getRuntimeClassPathForArchive() {
+ Iterable<Artifact> runtimeClasspath = getRuntimeClassPath();
+
+ if (getExcludedArtifacts().isEmpty()) {
+ return runtimeClasspath;
+ } else {
+ return Iterables.filter(runtimeClasspath,
+ Predicates.not(Predicates.in(getExcludedArtifacts().toSet())));
+ }
+ }
+
+ public NestedSet<Artifact> getCompileTimeClassPath() {
+ return compileTimeClassPath;
+ }
+
+ public ImmutableList<Artifact> getBootClassPath() {
+ return bootClassPath;
+ }
+
+ public ImmutableSet<Artifact> getProcessorPath() {
+ return processorPath;
+ }
+
+ public Set<Artifact> getSourceFiles() {
+ return sourceFiles;
+ }
+
+ public Set<Artifact> getJarFiles() {
+ return jarFiles;
+ }
+
+ public Set<Artifact> getCompileTimeJarFiles() {
+ return compileTimeJarFiles;
+ }
+
+ public List<Artifact> getNativeLibraries() {
+ return nativeLibraries;
+ }
+
+ public Collection<String> getProcessorNames() {
+ return processorNames;
+ }
+
+ public boolean hasSourceFiles() {
+ return !sourceFiles.isEmpty();
+ }
+
+ public boolean hasSourceJars() {
+ return !sourceJars.isEmpty();
+ }
+
+ public boolean hasJarFiles() {
+ return !jarFiles.isEmpty();
+ }
+
+ public boolean hasResources() {
+ return !resources.isEmpty();
+ }
+
+ public boolean hasMessages() {
+ return !messages.isEmpty();
+ }
+
+ public boolean hasClassPathResources() {
+ return !classPathResources.isEmpty();
+ }
+
+ public Iterable<Artifact> getArchiveInputs(boolean includeClasspath) {
+ IterablesChain.Builder<Artifact> inputs = IterablesChain.builder();
+ if (includeClasspath) {
+ inputs.add(ImmutableList.copyOf(getRuntimeClassPathForArchive()));
+ }
+ inputs.add(getResources());
+ inputs.add(getClassPathResources());
+ if (getExcludedArtifacts().isEmpty()) {
+ return inputs.build();
+ } else {
+ Set<Artifact> excludedJars = Sets.newHashSet(getExcludedArtifacts());
+ return ImmutableList.copyOf(Iterables.filter(
+ inputs.build(), Predicates.not(Predicates.in(excludedJars))));
+ }
+ }
+
+ public String getRuleKind() {
+ return ruleKind;
+ }
+
+ public Label getTargetLabel() {
+ return targetLabel;
+ }
+
+ public BuildConfiguration.StrictDepsMode getStrictJavaDeps() {
+ return strictJavaDeps;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaToolchain.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaToolchain.java
new file mode 100644
index 0000000..65ed97a
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaToolchain.java
@@ -0,0 +1,53 @@
+// Copyright 2014 Google Inc. 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.rules.java;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTargetBuilder;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.Runfiles;
+import com.google.devtools.build.lib.analysis.RunfilesProvider;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.rules.RuleConfiguredTargetFactory;
+
+import java.util.List;
+
+/**
+ * Implementation for the {@code java_toolchain} rule.
+ */
+public final class JavaToolchain implements RuleConfiguredTargetFactory {
+
+ @Override
+ public ConfiguredTarget create(RuleContext ruleContext) {
+ final String source = ruleContext.attributes().get("source_version", Type.STRING);
+ final String target = ruleContext.attributes().get("target_version", Type.STRING);
+ final String encoding = ruleContext.attributes().get("encoding", Type.STRING);
+ final List<String> xlint = ruleContext.attributes().get("xlint", Type.STRING_LIST);
+ final List<String> misc = ruleContext.attributes().get("misc", Type.STRING_LIST);
+ final JavaConfiguration configuration = ruleContext.getFragment(JavaConfiguration.class);
+ JavaToolchainProvider provider = new JavaToolchainProvider(source, target, encoding,
+ ImmutableList.copyOf(xlint), ImmutableList.copyOf(misc),
+ configuration.getDefaultJavacFlags());
+ RuleConfiguredTargetBuilder builder = new RuleConfiguredTargetBuilder(ruleContext)
+ .add(JavaToolchainProvider.class, provider)
+ .setFilesToBuild(new NestedSetBuilder<Artifact>(Order.STABLE_ORDER).build())
+ .add(RunfilesProvider.class, RunfilesProvider.simple(Runfiles.EMPTY));
+
+ return builder.build();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaToolchainData.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaToolchainData.java
new file mode 100644
index 0000000..0338fb8
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaToolchainData.java
@@ -0,0 +1,55 @@
+// Copyright 2014 Google Inc. 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.rules.java;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableList.Builder;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+
+/**
+ * Information about the JDK used by the <code>java_*</code> rules.
+ *
+ * <p>This class contains the data of the {@code java_toolchain} rules, it is a separate object so
+ * it can be shared with other tools.
+ */
+@Immutable
+public class JavaToolchainData {
+ private final ImmutableList<String> options;
+
+ public JavaToolchainData(String source, String target, String encoding,
+ ImmutableList<String> xlint, ImmutableList<String> misc) {
+ Builder<String> builder = ImmutableList.<String>builder();
+ if (!source.isEmpty()) {
+ builder.add("-source", source);
+ }
+ if (!target.isEmpty()) {
+ builder.add("-target", target);
+ }
+ if (!encoding.isEmpty()) {
+ builder.add("-encoding", encoding);
+ }
+ if (!xlint.isEmpty()) {
+ builder.add("-Xlint:" + Joiner.on(",").join(xlint));
+ }
+ this.options = builder.addAll(misc).build();
+ }
+
+ /**
+ * @return the list of options as given by the {@code java_toolchain} rule.
+ */
+ public ImmutableList<String> getJavacOptions() {
+ return options;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaToolchainProvider.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaToolchainProvider.java
new file mode 100644
index 0000000..3e210d8
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaToolchainProvider.java
@@ -0,0 +1,68 @@
+// Copyright 2014 Google Inc. 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.rules.java;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+
+import java.util.List;
+
+/**
+ * Information about the JDK used by the <code>java_*</code> rules.
+ */
+@Immutable
+public final class JavaToolchainProvider implements TransitiveInfoProvider {
+
+ private final ImmutableList<String> javacOptions;
+
+ public JavaToolchainProvider(String source, String target, String encoding,
+ ImmutableList<String> xlint, ImmutableList<String> misc, List<String> defaultJavacFlags) {
+ super();
+ // merges the defaultJavacFlags from
+ // {@link JavaConfiguration} with the flags from the {@code java_toolchain} rule.
+ JavaToolchainData data = new JavaToolchainData(source, target, encoding, xlint, misc);
+ this.javacOptions = ImmutableList.<String>builder()
+ .addAll(data.getJavacOptions())
+ .addAll(defaultJavacFlags)
+ .build();
+ }
+
+ /**
+ * @return the list of default options for the java compiler
+ */
+ public ImmutableList<String> getJavacOptions() {
+ return javacOptions;
+ }
+
+ /**
+ * An helper method to construct the list of javac options.
+ *
+ * @param ruleContext The rule context of the current rule.
+ * @return the list of flags provided by the {@code java_toolchain} rule merged with the one
+ * provided by the {@link JavaConfiguration} fragment.
+ */
+ public static List<String> getDefaultJavacOptions(RuleContext ruleContext) {
+ JavaToolchainProvider javaToolchain =
+ ruleContext.getPrerequisite(":java_toolchain", Mode.TARGET, JavaToolchainProvider.class);
+ if (javaToolchain == null) {
+ ruleContext.ruleError("No java_toolchain implicit dependency found. This is probably because"
+ + " your java configuration is not up-to-date.");
+ return ImmutableList.of();
+ }
+ return javaToolchain.getJavacOptions();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaToolchainRule.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaToolchainRule.java
new file mode 100644
index 0000000..16801ee
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaToolchainRule.java
@@ -0,0 +1,92 @@
+// Copyright 2014 Google Inc. 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.rules.java;
+
+import static com.google.devtools.build.lib.packages.Attribute.attr;
+import static com.google.devtools.build.lib.packages.Type.STRING;
+import static com.google.devtools.build.lib.packages.Type.STRING_LIST;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.analysis.BaseRuleClasses;
+import com.google.devtools.build.lib.analysis.BlazeRule;
+import com.google.devtools.build.lib.analysis.RuleDefinition;
+import com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.RuleClass.Builder;
+
+/**
+ * Rule definition for {@code java_toolchain}
+ */
+@BlazeRule(name = "java_toolchain", ancestors = {BaseRuleClasses.BaseRule.class},
+ factoryClass = JavaToolchain.class)
+public final class JavaToolchainRule implements RuleDefinition {
+ @Override
+ public RuleClass build(Builder builder, RuleDefinitionEnvironment env) {
+ return builder.setUndocumented()
+ /* <!-- #BLAZE_RULE(java_toolchain).ATTRIBUTE(source_version) -->
+ The Java source version (e.g., '6' or '7'). It specifies which set of code structures
+ are allowed in the Java source code.
+ <!-- #END_BLAZE_RULE.ATTRIBUTE --> */
+ .add(attr("source_version", STRING).mandatory()) // javac -source flag value.
+ /* <!-- #BLAZE_RULE(java_toolchain).ATTRIBUTE(target_version) -->
+ The Java target version (e.g., '6' or '7'). It specifies for which Java runtime the class
+ should be build.
+ <!-- #END_BLAZE_RULE.ATTRIBUTE --> */
+ .add(attr("target_version", STRING).mandatory()) // javac -target flag value.
+ /* <!-- #BLAZE_RULE(java_toolchain).ATTRIBUTE(encoding) -->
+ The encoding of the java files (e.g., 'UTF-8').
+ <!-- #END_BLAZE_RULE.ATTRIBUTE --> */
+ .add(attr("encoding", STRING).mandatory()) // javac -encoding flag value.
+ /* <!-- #BLAZE_RULE(java_toolchain).ATTRIBUTE(xlint) -->
+ The list of warning to add or removes from default list. Precedes it with a dash to
+ removes it. Please see the Javac documentation on the -Xlint options for more information.
+ <!-- #END_BLAZE_RULE.ATTRIBUTE --> */
+ .add(attr("xlint", STRING_LIST).value(ImmutableList.<String>of()))
+ /* <!-- #BLAZE_RULE(java_toolchain).ATTRIBUTE(xlint) -->
+ The list of extra arguments for the Java compiler. Please refer to the Java compiler
+ documentation for the extensive list of possible Java compiler flags.
+ <!-- #END_BLAZE_RULE.ATTRIBUTE --> */
+ .add(attr("misc", STRING_LIST).value(ImmutableList.<String>of()))
+ .build();
+ }
+}
+/*<!-- #BLAZE_RULE (NAME = java_toolchain, TYPE = OTHER, FAMILY = Java) -->
+
+${ATTRIBUTE_SIGNATURE}
+
+<p>
+Specifies the configuration for the Java compiler. Which toolchain to be used can be changed through
+the --java_toolchain argument. Normally you should not write those kind of rules unless you want to
+tune your Java compiler.
+</p>
+
+${ATTRIBUTE_DEFINITION}
+
+<h4 id="java_binary_examples">Examples</h4>
+
+<p>A simple example would be:
+</p>
+
+<pre class="code">
+java_toolchain(
+ name = "toolchain",
+ source_version = "7",
+ target_version = "7",
+ encoding = "UTF-8",
+ xlint = [ "classfile", "divzero", "empty", "options", "path" ],
+ misc = [ "-g" ],
+)
+</pre>
+
+<!-- #END_BLAZE_RULE -->*/
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaUtil.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaUtil.java
new file mode 100644
index 0000000..b2a84ec
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaUtil.java
@@ -0,0 +1,147 @@
+// Copyright 2014 Google Inc. 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.rules.java;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+/**
+ * Utility methods for use by Java-related parts of the build system.
+ */
+public abstract class JavaUtil {
+
+ private JavaUtil() {}
+
+ //---------- Java related methods
+
+ /*
+ * TODO(bazel-team): (2009)
+ *
+ * This way of figuring out Java source roots is basically
+ * broken. I think we need to support these two use cases:
+ * (1) A user puts his / her shell into a directory named java.
+ * (2) Someplace in the tree, there's a package named java.
+ *
+ * (1) is more important than (2); and (2) cannot always be guaranteed
+ * due to sloppy implementations in the past; most notably the old
+ * tools/boilerplate_rules.mk code for compiling Java.
+ *
+ * Basically, to implement correct semantics, we will need to configure
+ * Java source roots based on the package path, plus some heuristics to
+ * support legacy code, maybe.
+ *
+ * Roughly:
+ * Given a path, find the source root that applies to it by
+ * - walk over the elements in the package path
+ * - add "java", "javatests" to them
+ * - find the first element that is a maximal prefix to the Java file
+ * - for experimental, some legacy support that basically has some
+ * arbitrary padding before the Java sourceroot.
+ */
+
+ /**
+ * Given the filename of a Java source file, returns the name of the toplevel Java class defined
+ * within it.
+ */
+ public static String getJavaClassName(PathFragment path) {
+ return FileSystemUtils.removeExtension(path.getBaseName());
+ }
+
+ /**
+ * Find the index of the "java" or "javatests" segment in a Java path fragment
+ * that precedes the source root.
+ *
+ * @param path a Java source dir or file path
+ * @return the index of the java segment or -1 iff no java segment was found.
+ */
+ private static int javaSegmentIndex(PathFragment path) {
+ if (path.isAbsolute()) {
+ throw new IllegalArgumentException("path must not be absolute: '" + path + "'");
+ }
+ return path.getFirstSegment(ImmutableSet.of("java", "javatests"));
+ }
+
+ /**
+ * Given the PathFragment of a Java source file, returns the Java package to which it belongs.
+ */
+ public static String getJavaPackageName(PathFragment path) {
+ int index = javaSegmentIndex(path) + 1;
+ path = path.subFragment(index, path.segmentCount() - 1);
+ return path.getPathString().replace('/', '.');
+ }
+
+ /**
+ * Given the PathFragment of a file without extension, returns the
+ * Java fully qualified class name based on the Java root relative path of the
+ * specified path or 'null' if no java root can be determined.
+ * <p>
+ * For example, "java/foo/bar/wiz" and "javatests/foo/bar/wiz" both
+ * result in "foo.bar.wiz".
+ *
+ * TODO(bazel-team): (2011) We need to have a more robust way to determine the Java root
+ * of a relative path rather than simply trying to find the "java" or
+ * "javatests" directory.
+ */
+ public static String getJavaFullClassname(PathFragment path) {
+ PathFragment javaPath = getJavaPath(path);
+ if (javaPath != null) {
+ return javaPath.getPathString().replace('/', '.');
+ }
+ return null;
+ }
+
+ /**
+ * Given the PathFragment of a Java source file, returns the Java root relative path or 'null' if
+ * no java root can be determined.
+ *
+ * <p>
+ * For example, "{workspace}/java/foo/bar/wiz" and "{workspace}/javatests/foo/bar/wiz"
+ * both result in "foo/bar/wiz".
+ *
+ * TODO(bazel-team): (2011) We need to have a more robust way to determine the Java root
+ * of a relative path rather than simply trying to find the "java" or
+ * "javatests" directory.
+ */
+ public static PathFragment getJavaPath(PathFragment path) {
+ int index = javaSegmentIndex(path);
+ if (index >= 0) {
+ return path.subFragment(index + 1, path.segmentCount());
+ }
+ return null;
+ }
+
+ /**
+ * Given the PathFragment of a Java source file, returns the
+ * Java root of the specified path or 'null' if no java root can be
+ * determined.
+ * <p>
+ * Example 1: "{workspace}/java/foo/bar/wiz" and "{workspace}/javatests/foo/bar/wiz"
+ * result in "{workspace}/java" and "{workspace}/javatests" Example 2:
+ * "java/foo/bar/wiz" and "javatests/foo/bar/wiz" result in "java" and
+ * "javatests"
+ *
+ * TODO(bazel-team): (2011) We need to have a more robust way to determine the Java root
+ * of a relative path rather than simply trying to find the "java" or
+ * "javatests" directory.
+ */
+ public static PathFragment getJavaRoot(PathFragment path) {
+ int index = javaSegmentIndex(path);
+ if (index >= 0) {
+ return path.subFragment(0, index + 1);
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/Jvm.java b/src/main/java/com/google/devtools/build/lib/rules/java/Jvm.java
new file mode 100644
index 0000000..eda7360
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/Jvm.java
@@ -0,0 +1,120 @@
+// Copyright 2014 Google Inc. 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.rules.java;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap.Builder;
+import com.google.common.collect.Multimap;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.syntax.SkylarkCallable;
+import com.google.devtools.build.lib.syntax.SkylarkModule;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+/**
+ * This class represents a Java virtual machine with a host system and a path.
+ * If the JVM comes from the client, it can optionally also contain a label
+ * pointing to a target that contains all the necessary files.
+ */
+@SkylarkModule(name = "jvm",
+ doc = "A configuration fragment representing the Java virtual machine.")
+@Immutable
+public final class Jvm extends BuildConfiguration.Fragment {
+ private final PathFragment javaHome;
+ private final Label jvmLabel;
+
+ /**
+ * Creates a Jvm instance. Either the {@code javaHome} parameter is absolute,
+ * or the {@code jvmLabel} parameter must be non-null. This restriction might
+ * be lifted in the future. Only the {@code jvmLabel} is optional.
+ */
+ public Jvm(PathFragment javaHome, Label jvmLabel) {
+ Preconditions.checkArgument(javaHome.isAbsolute() ^ (jvmLabel != null));
+ this.javaHome = javaHome;
+ this.jvmLabel = jvmLabel;
+ }
+
+ @Override
+ public String getName() {
+ return "Jvm";
+ }
+
+ @Override
+ public void addImplicitLabels(Multimap<String, Label> implicitLabels) {
+ if (jvmLabel != null) {
+ implicitLabels.put(getName(), jvmLabel);
+ }
+ }
+
+ /**
+ * Returns a path fragment that determines the path to the installation
+ * directory. It is either absolute or relative to the execution root.
+ */
+ public PathFragment getJavaHome() {
+ return javaHome;
+ }
+
+ /**
+ * Returns the path to the javac binary.
+ */
+ public PathFragment getJavacExecutable() {
+ return getJavaHome().getRelative("bin/javac");
+ }
+
+ /**
+ * Returns the path to the jar binary.
+ */
+ public PathFragment getJarExecutable() {
+ return getJavaHome().getRelative("bin/jar");
+ }
+
+ /**
+ * Returns the path to the java binary.
+ */
+ @SkylarkCallable(name = "java_executable", structField = true,
+ doc = "The the java executable, i.e. bin/java relative to the Java home.")
+ public PathFragment getJavaExecutable() {
+ return getJavaHome().getRelative("bin/java");
+ }
+
+ /**
+ * Returns a label. Adding this label to the dependencies of an action that
+ * depends on this JVM is sufficient to ensure that all the required files are
+ * present. Can be <code>null</code>, in which case nothing needs to be added
+ * to the dependencies of an action. We rely on convention to make sure that
+ * this case works, since we can't know which JVMs are installed on the build host.
+ */
+ public Label getJvmLabel() {
+ return jvmLabel;
+ }
+
+ /**
+ * Returns a string that uniquely identifies the JVM for the life time of this
+ * Blaze instance. This value is intended for analysis caching, so it need not
+ * reflect changes in the individual files making up the JVM.
+ */
+ @Override
+ public String cacheKey() {
+ return javaHome.getSafePathString();
+ }
+
+ @Override
+ public void addGlobalMakeVariables(Builder<String, String> globalMakeEnvBuilder) {
+ globalMakeEnvBuilder.put("JAVABASE", getJavaHome().getPathString());
+ globalMakeEnvBuilder.put("JAVA", getJavaExecutable().getPathString());
+ globalMakeEnvBuilder.put("JAVAC", getJavacExecutable().getPathString());
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JvmConfigurationLoader.java b/src/main/java/com/google/devtools/build/lib/rules/java/JvmConfigurationLoader.java
new file mode 100644
index 0000000..7483f88
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/JvmConfigurationLoader.java
@@ -0,0 +1,163 @@
+// Copyright 2014 Google Inc. 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.rules.java;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.devtools.build.lib.analysis.RedirectChaser;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration.Fragment;
+import com.google.devtools.build.lib.analysis.config.BuildOptions;
+import com.google.devtools.build.lib.analysis.config.ConfigurationEnvironment;
+import com.google.devtools.build.lib.analysis.config.ConfigurationFragmentFactory;
+import com.google.devtools.build.lib.analysis.config.InvalidConfigurationException;
+import com.google.devtools.build.lib.packages.NoSuchPackageException;
+import com.google.devtools.build.lib.packages.NoSuchTargetException;
+import com.google.devtools.build.lib.packages.RawAttributeMapper;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.syntax.Label.SyntaxException;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+/**
+ * A provider to load jvm configurations from the package path.
+ *
+ * <p>If the given {@code javaHome} is a label, i.e. starts with {@code "//"},
+ * then the loader will look at the target it refers to. If the target is a
+ * filegroup, then the loader will look in it's srcs for a filegroup that ends
+ * with {@code -<cpu>}. It will use that filegroup to construct the actual
+ * {@link Jvm} instance, using the filegroups {@code path} attribute to
+ * construct the new {@code javaHome} path.
+ *
+ * <p>The loader also supports legacy mode, where the JVM can be defined with an abolute path.
+ */
+public final class JvmConfigurationLoader implements ConfigurationFragmentFactory {
+ private final boolean forceLegacy;
+ private final JavaCpuSupplier cpuSupplier;
+
+ public JvmConfigurationLoader(boolean forceLegacy, JavaCpuSupplier cpuSupplier) {
+ this.forceLegacy = forceLegacy;
+ this.cpuSupplier = cpuSupplier;
+ }
+
+ public JvmConfigurationLoader(JavaCpuSupplier cpuSupplier) {
+ this(/*forceLegacy=*/ false, cpuSupplier);
+ }
+
+ @Override
+ public Jvm create(ConfigurationEnvironment env, BuildOptions buildOptions)
+ throws InvalidConfigurationException {
+ JavaOptions javaOptions = buildOptions.get(JavaOptions.class);
+ String javaHome = javaOptions.javaBase;
+ String cpu = cpuSupplier.getJavaCpu(buildOptions, env);
+ if (cpu == null) {
+ return null;
+ }
+
+ if (!forceLegacy && javaHome.startsWith("//")) {
+ return createDefault(env, javaHome, cpu);
+ } else {
+ return createLegacy(javaHome);
+ }
+ }
+
+ @Override
+ public Class<? extends Fragment> creates() {
+ return Jvm.class;
+ }
+
+ @Nullable
+ private Jvm createDefault(ConfigurationEnvironment lookup, String javaHome, String cpu)
+ throws InvalidConfigurationException {
+ try {
+ Label label = Label.parseAbsolute(javaHome);
+ label = RedirectChaser.followRedirects(lookup, label, "jdk");
+ if (label == null) {
+ return null;
+ }
+ Target javaHomeTarget = lookup.getTarget(label);
+ if (javaHomeTarget == null) {
+ return null;
+ }
+ if ((javaHomeTarget instanceof Rule) &&
+ "filegroup".equals(((Rule) javaHomeTarget).getRuleClass())) {
+ RawAttributeMapper javaHomeAttributes = RawAttributeMapper.of((Rule) javaHomeTarget);
+ if (javaHomeAttributes.isConfigurable("srcs", Type.LABEL_LIST)) {
+ throw new InvalidConfigurationException("\"srcs\" in " + javaHome
+ + " is configurable. JAVABASE targets don't support configurable attributes");
+ }
+ List<Label> labels = javaHomeAttributes.get("srcs", Type.LABEL_LIST);
+ for (Label jvmLabel : labels) {
+ if (jvmLabel.getName().endsWith("-" + cpu)) {
+ Target jvmTarget = lookup.getTarget(jvmLabel);
+ if (jvmTarget == null) {
+ return null;
+ }
+ PathFragment javaHomePath = jvmLabel.getPackageFragment();
+ if ((jvmTarget instanceof Rule) &&
+ "filegroup".equals(((Rule) jvmTarget).getRuleClass())) {
+ RawAttributeMapper jvmTargetAttributes = RawAttributeMapper.of((Rule) jvmTarget);
+ if (jvmTargetAttributes.isConfigurable("path", Type.STRING)) {
+ throw new InvalidConfigurationException("\"path\" in " + jvmTarget
+ + " is configurable. JVM targets don't support configurable attributes");
+ }
+ String path = jvmTargetAttributes.get("path", Type.STRING);
+ if (path != null) {
+ javaHomePath = javaHomePath.getRelative(path);
+ }
+ }
+ return new Jvm(javaHomePath, jvmLabel);
+ }
+ }
+ }
+ throw new InvalidConfigurationException("No JVM target found under " + javaHome
+ + " that would work for " + cpu);
+ } catch (NoSuchPackageException e) {
+ throw new InvalidConfigurationException(e.getMessage(), e);
+ } catch (NoSuchTargetException e) {
+ throw new InvalidConfigurationException("No such target: " + e.getMessage(), e);
+ } catch (SyntaxException e) {
+ throw new InvalidConfigurationException(e.getMessage(), e);
+ }
+ }
+
+ private Jvm createLegacy(String javaHome)
+ throws InvalidConfigurationException {
+ if (!javaHome.startsWith("/")) {
+ throw new InvalidConfigurationException("Illegal javabase value '" + javaHome +
+ "', javabase must be an absolute path or label");
+ }
+ return new Jvm(new PathFragment(javaHome), null);
+ }
+
+ /**
+ * Converts the cpu name to a GNU system name. If the cpu is not a known value, it returns
+ * <code>"unknown-unknown-linux-gnu"</code>.
+ */
+ @VisibleForTesting
+ static String convertCpuToGnuSystemName(String cpu) {
+ if ("piii".equals(cpu)) {
+ return "i686-unknown-linux-gnu";
+ } else if ("k8".equals(cpu)) {
+ return "x86_64-unknown-linux-gnu";
+ } else {
+ return "unknown-unknown-linux-gnu";
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/MessageBundleProvider.java b/src/main/java/com/google/devtools/build/lib/rules/java/MessageBundleProvider.java
new file mode 100644
index 0000000..f78e386
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/MessageBundleProvider.java
@@ -0,0 +1,41 @@
+// Copyright 2014 Google Inc. 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.rules.java;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+
+/**
+ * Marks configured targets that are able to supply message bundles to their
+ * dependents.
+ */
+@Immutable
+public final class MessageBundleProvider implements TransitiveInfoProvider {
+
+ private final ImmutableList<Artifact> messages;
+
+ public MessageBundleProvider(ImmutableList<Artifact> messages) {
+ this.messages = messages;
+ }
+
+ /**
+ * The set of XML source files containing the message definitions.
+ */
+ public ImmutableList<Artifact> getMessages() {
+ return messages;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/NativeLibraryNestedSetBuilder.java b/src/main/java/com/google/devtools/build/lib/rules/java/NativeLibraryNestedSetBuilder.java
new file mode 100644
index 0000000..09ac59f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/NativeLibraryNestedSetBuilder.java
@@ -0,0 +1,115 @@
+// Copyright 2014 Google Inc. 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.rules.java;
+
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.FileProvider;
+import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.rules.cpp.CcNativeLibraryProvider;
+import com.google.devtools.build.lib.rules.cpp.CppFileTypes;
+import com.google.devtools.build.lib.rules.cpp.LinkerInput;
+import com.google.devtools.build.lib.rules.cpp.LinkerInputs;
+import com.google.devtools.build.lib.util.FileType;
+
+/**
+ * A builder that helps construct nested sets of native libraries.
+ */
+public final class NativeLibraryNestedSetBuilder {
+
+ private final NestedSetBuilder<LinkerInput> builder = NestedSetBuilder.linkOrder();
+
+ /**
+ * Build a nested set of native libraries.
+ */
+ public NestedSet<LinkerInput> build() {
+ return builder.build();
+ }
+
+ /**
+ * Include specified artifacts as native libraries in the nested set.
+ */
+ public NativeLibraryNestedSetBuilder addAll(Iterable<Artifact> deps) {
+ for (Artifact dep : deps) {
+ builder.add(new LinkerInputs.SimpleLinkerInput(dep));
+ }
+ return this;
+ }
+
+ /**
+ * Include native libraries of specified dependencies into the nested set.
+ */
+ public NativeLibraryNestedSetBuilder addJavaTargets(
+ Iterable<? extends TransitiveInfoCollection> deps) {
+ for (TransitiveInfoCollection dep : deps) {
+ addJavaTarget(dep);
+ }
+ return this;
+ }
+
+ /**
+ * Include native Java libraries of a specified target into the nested set.
+ */
+ private void addJavaTarget(TransitiveInfoCollection dep) {
+ JavaNativeLibraryProvider javaProvider = dep.getProvider(JavaNativeLibraryProvider.class);
+ if (javaProvider != null) {
+ builder.addTransitive(javaProvider.getTransitiveJavaNativeLibraries());
+ return;
+ }
+
+ CcNativeLibraryProvider ccProvider = dep.getProvider(CcNativeLibraryProvider.class);
+ if (ccProvider != null) {
+ builder.addTransitive(ccProvider.getTransitiveCcNativeLibraries());
+ return;
+ }
+
+ addTarget(dep);
+ }
+
+ /**
+ * Include native C/C++ libraries of specified dependencies into the nested set.
+ */
+ public NativeLibraryNestedSetBuilder addCcTargets(
+ Iterable<? extends TransitiveInfoCollection> deps) {
+ for (TransitiveInfoCollection dep : deps) {
+ addCcTarget(dep);
+ }
+ return this;
+ }
+
+ /**
+ * Include native Java libraries of a specified target into the nested set.
+ */
+ private void addCcTarget(TransitiveInfoCollection dep) {
+ CcNativeLibraryProvider provider = dep.getProvider(CcNativeLibraryProvider.class);
+ if (provider != null) {
+ builder.addTransitive(provider.getTransitiveCcNativeLibraries());
+ } else {
+ addTarget(dep);
+ }
+ }
+
+ /**
+ * Include files and genrule artifacts.
+ */
+ private void addTarget(TransitiveInfoCollection dep) {
+ for (Artifact artifact : FileType.filterList(
+ dep.getProvider(FileProvider.class).getFilesToBuild(),
+ CppFileTypes.SHARED_LIBRARY)) {
+ builder.add(new LinkerInputs.SimpleLinkerInput(artifact));
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/SourcesJavaCompilationArgsProvider.java b/src/main/java/com/google/devtools/build/lib/rules/java/SourcesJavaCompilationArgsProvider.java
new file mode 100644
index 0000000..ff8507e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/SourcesJavaCompilationArgsProvider.java
@@ -0,0 +1,60 @@
+// Copyright 2014 Google Inc. 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.rules.java;
+
+import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+
+/**
+ * An interface that marks configured targets that can provide Java compilation arguments through
+ * the 'srcs' attribute of Java rules.
+ *
+ * <p>In a perfect world, this would not be necessary for a million reasons, but
+ * this world is far from perfect, thus, we need this.
+ *
+ * <p>Please do not implement this interface with configured target implementations.
+ */
+@Immutable
+public final class SourcesJavaCompilationArgsProvider implements TransitiveInfoProvider {
+ private final JavaCompilationArgs javaCompilationArgs;
+ private final JavaCompilationArgs recursiveJavaCompilationArgs;
+
+ public SourcesJavaCompilationArgsProvider(
+ JavaCompilationArgs javaCompilationArgs,
+ JavaCompilationArgs recursiveJavaCompilationArgs) {
+ this.javaCompilationArgs = javaCompilationArgs;
+ this.recursiveJavaCompilationArgs = recursiveJavaCompilationArgs;
+ }
+
+ /**
+ * Returns non-recursively collected Java compilation information for
+ * building this target (called when strict_java_deps = 1).
+ *
+ * <p>Note that some of the parameters are still collected from the complete
+ * transitive closure. The non-recursive collection applies mainly to
+ * compile-time jars.
+ */
+ public JavaCompilationArgs getJavaCompilationArgs() {
+ return javaCompilationArgs;
+ }
+
+ /**
+ * Returns recursively collected Java compilation information for building
+ * this target (called when strict_java_deps = 0).
+ */
+ public JavaCompilationArgs getRecursiveJavaCompilationArgs() {
+ return recursiveJavaCompilationArgs;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/WriteBuildInfoPropertiesAction.java b/src/main/java/com/google/devtools/build/lib/rules/java/WriteBuildInfoPropertiesAction.java
new file mode 100644
index 0000000..08dcbba
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/WriteBuildInfoPropertiesAction.java
@@ -0,0 +1,211 @@
+// Copyright 2014 Google Inc. 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.rules.java;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.Executor;
+import com.google.devtools.build.lib.analysis.BuildInfoHelper;
+import com.google.devtools.build.lib.analysis.WorkspaceStatusAction;
+import com.google.devtools.build.lib.analysis.WorkspaceStatusAction.Key;
+import com.google.devtools.build.lib.analysis.actions.AbstractFileWriteAction;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.util.Fingerprint;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Properties;
+
+/**
+ * An action that creates a Java properties file containing the build informations.
+ */
+public class WriteBuildInfoPropertiesAction extends AbstractFileWriteAction {
+ private static final String GUID = "922949ca-1391-4046-a300-74810618dcdc";
+
+ private final ImmutableList<Artifact> valueArtifacts;
+ private final BuildInfoPropertiesTranslator keyTranslations;
+ private final boolean includeVolatile;
+ private final boolean includeNonVolatile;
+
+ private final TimestampFormatter timestampFormatter;
+ /**
+ * An interface to format a timestamp. We are using our custom one to avoid external dependency.
+ */
+ public static interface TimestampFormatter {
+ /**
+ * Return a human readable string for the given {@code timestamp}. {@code timestamp} is given
+ * in milliseconds since 1st of January 1970 at 0am UTC.
+ */
+ public String format(long timestamp);
+ }
+
+ /**
+ * A wrapper around a {@link Writer} that skips the first line assuming the line is pure ASCII. It
+ * can be used to strip the timestamp comment that {@link Properties#store(Writer, String)} adds.
+ */
+ @VisibleForTesting
+ static class StripFirstLineWriter extends Writer {
+ private final Writer writer;
+ private boolean newlineFound = false;
+
+ StripFirstLineWriter(OutputStream out) {
+ this.writer = new OutputStreamWriter(out, UTF_8);
+ }
+
+ @Override
+ public void write(char[] cbuf, int off, int len) throws IOException {
+ if (!newlineFound) {
+ while (len > 0 && cbuf[off] != '\n') {
+ off++;
+ len--;
+ }
+ if (len > 0) {
+ newlineFound = true;
+ off++;
+ len--;
+ }
+ }
+ if (len > 0) {
+ writer.write(cbuf, off, len);
+ }
+ }
+
+ @Override
+ public void flush() throws IOException {
+ writer.flush();
+ }
+
+ @Override
+ public void close() throws IOException {
+ writer.close();
+ }
+
+ }
+
+ /**
+ * Creates an action that writes a Java property files with build information.
+ *
+ * <p>It reads the set of build info keys from an action context that is usually contributed to
+ * Blaze by the workspace status module, and the value associated with said keys from the
+ * workspace status files (stable and volatile) written by the workspace status action. The files
+ * generated by this action serve as input to the
+ * {@link com.google.devtools.build.singlejar.SingleJar} program.
+ *
+ * <p>Without input artifacts, this action uses redacted build information.
+ *
+ * @param inputs Artifacts that contain build information, or an empty collection to use redacted
+ * build information
+ * @param output output the properties file Artifact created by this action
+ * @param keyTranslations how to translates available keys. See
+ * {@link BuildInfoPropertiesTranslator}.
+ * @param includeVolatile whether the set of key to write are giving volatile keys or not
+ * @param includeNonVolatile whether the set of key to write are giving non-volatile keys or not
+ * @param timestampFormatter formats dates printed in the properties file
+ */
+ public WriteBuildInfoPropertiesAction(Collection<Artifact> inputs, Artifact output,
+ BuildInfoPropertiesTranslator keyTranslations, boolean includeVolatile,
+ boolean includeNonVolatile, TimestampFormatter timestampFormatter) {
+ super(BuildInfoHelper.BUILD_INFO_ACTION_OWNER, inputs, output, /* makeExecutable= */false);
+ this.keyTranslations = keyTranslations;
+ this.includeVolatile = includeVolatile;
+ this.includeNonVolatile = includeNonVolatile;
+ this.timestampFormatter = timestampFormatter;
+ valueArtifacts = ImmutableList.copyOf(inputs);
+
+ if (!inputs.isEmpty()) {
+ // With non-empty inputs we should not generate both volatile and non-volatile data
+ // in the same properties file.
+ Preconditions.checkState(includeVolatile ^ includeNonVolatile);
+ }
+ Preconditions.checkState(
+ output.isConstantMetadata() == (includeVolatile && !inputs.isEmpty()));
+ }
+
+ @Override
+ public DeterministicWriter newDeterministicWriter(EventHandler eventHandler,
+ final Executor executor) {
+ final long timestamp = System.currentTimeMillis();
+ return new DeterministicWriter() {
+ @Override
+ public void writeOutputFile(OutputStream out) throws IOException {
+ WorkspaceStatusAction.Context context =
+ executor.getContext(WorkspaceStatusAction.Context.class);
+ Map<String, String> values = new LinkedHashMap<>();
+ for (Artifact valueFile : valueArtifacts) {
+ values.putAll(WorkspaceStatusAction.parseValues(valueFile.getPath()));
+ }
+
+ Map<String, String> keys = new HashMap<>();
+ if (includeVolatile) {
+ addValues(keys, values, context.getVolatileKeys());
+ keys.put("BUILD_TIMESTAMP", Long.toString(timestamp / 1000));
+ keys.put("BUILD_TIME", timestampFormatter.format(timestamp));
+ }
+ addValues(keys, values, context.getStableKeys());
+ Properties properties = new Properties();
+ keyTranslations.translate(keys, properties);
+ properties.store(new StripFirstLineWriter(out), null);
+ }
+ };
+ }
+
+ private void addValues(Map<String, String> result, Map<String, String> values,
+ Map<String, Key> keys) {
+ boolean redacted = values.isEmpty();
+ for (Map.Entry<String, WorkspaceStatusAction.Key> key : keys.entrySet()) {
+ if (key.getValue().isInLanguage("Java")) {
+ result.put(key.getKey(), gePropertyValue(values, redacted, key));
+ }
+ }
+ }
+
+ private static String gePropertyValue(Map<String, String> values, boolean redacted,
+ Map.Entry<String, WorkspaceStatusAction.Key> key) {
+ return redacted ? key.getValue().getRedactedValue()
+ : values.containsKey(key.getKey()) ? values.get(key.getKey())
+ : key.getValue().getDefaultValue();
+ }
+
+ @Override
+ protected String computeKey() {
+ Fingerprint f = new Fingerprint();
+ f.addString(GUID);
+ f.addString(keyTranslations.computeKey());
+ f.addBoolean(includeVolatile);
+ f.addBoolean(includeNonVolatile);
+ return f.hexDigestAndReset();
+ }
+
+ @Override
+ public boolean executeUnconditionally() {
+ return isVolatile();
+ }
+
+ @Override
+ public boolean isVolatile() {
+ return includeVolatile && !Iterables.isEmpty(getInputs());
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/ApplicationSupport.java b/src/main/java/com/google/devtools/build/lib/rules/objc/ApplicationSupport.java
new file mode 100644
index 0000000..73554e5
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/ApplicationSupport.java
@@ -0,0 +1,588 @@
+// Copyright 2015 Google Inc. 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.rules.objc;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.devtools.build.lib.packages.ImplicitOutputsFunction.fromTemplates;
+import static com.google.devtools.build.xcode.common.TargetDeviceFamily.UI_DEVICE_FAMILY_VALUES;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.FilesToRunProvider;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.actions.BinaryFileWriteAction;
+import com.google.devtools.build.lib.analysis.actions.CustomCommandLine;
+import com.google.devtools.build.lib.analysis.actions.SpawnAction;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.packages.ImplicitOutputsFunction.SafeImplicitOutputsFunction;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.rules.objc.ObjcActionsBuilder.ExtraActoolArgs;
+import com.google.devtools.build.lib.shell.ShellUtils;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.xcode.common.InvalidFamilyNameException;
+import com.google.devtools.build.xcode.common.Platform;
+import com.google.devtools.build.xcode.common.RepeatedFamilyNameException;
+import com.google.devtools.build.xcode.common.TargetDeviceFamily;
+import com.google.devtools.build.xcode.xcodegen.proto.XcodeGenProtos.XcodeprojBuildSetting;
+
+import java.util.List;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+/**
+ * Support for application-generating ObjC rules. An application is generally composed of a
+ * top-level {@link BundleSupport bundle}, potentially signed, as well as some debug information, if
+ * {@link ObjcConfiguration#generateDebugSymbols() requested}.
+ *
+ * <p>Contains actions, validation logic and provider value generation.
+ *
+ * <p>Methods on this class can be called in any order without impacting the result.
+ */
+public final class ApplicationSupport {
+
+ /**
+ * Template for the containing application folder.
+ */
+ public static final SafeImplicitOutputsFunction IPA = fromTemplates("%{name}.ipa");
+
+ @VisibleForTesting
+ static final String NO_ASSET_CATALOG_ERROR_FORMAT =
+ "a value was specified (%s), but this app does not have any asset catalogs";
+ @VisibleForTesting
+ static final String INVALID_FAMILIES_ERROR =
+ "Expected one or two strings from the list 'iphone', 'ipad'";
+ @VisibleForTesting
+ static final String DEVICE_NO_PROVISIONING_PROFILE =
+ "Provisioning profile must be set for device build";
+
+ @VisibleForTesting
+ static final String PROVISIONING_PROFILE_BUNDLE_FILE = "embedded.mobileprovision";
+
+ private final Attributes attributes;
+ private final BundleSupport bundleSupport;
+ private final RuleContext ruleContext;
+ private final Bundling bundling;
+ private final ObjcProvider objcProvider;
+ private final LinkedBinary linkedBinary;
+ private final ImmutableSet<TargetDeviceFamily> families;
+ private final IntermediateArtifacts intermediateArtifacts;
+
+ /**
+ * Indicator as to whether this rule generates a binary directly or whether only dependencies
+ * should be considered.
+ */
+ enum LinkedBinary {
+ /**
+ * This rule generates its own binary which should be included as well as dependency-generated
+ * binaries.
+ */
+ LOCAL_AND_DEPENDENCIES,
+
+ /**
+ * This rule does not generate its own binary, only consider binaries from dependencies.
+ */
+ DEPENDENCIES_ONLY
+ }
+
+ /**
+ * Creates a new application support within the given rule context.
+ *
+ * @param ruleContext context for the application-generating rule
+ * @param objcProvider provider containing all dependencies' information as well as some of this
+ * rule's
+ * @param optionsProvider provider containing options and plist settings for this rule and its
+ * dependencies
+ * @param linkedBinary whether to look for a linked binary from this rule and dependencies or just
+ * the latter
+ */
+ ApplicationSupport(
+ RuleContext ruleContext, ObjcProvider objcProvider, OptionsProvider optionsProvider,
+ LinkedBinary linkedBinary) {
+ this.linkedBinary = linkedBinary;
+ this.attributes = new Attributes(ruleContext);
+ this.ruleContext = ruleContext;
+ this.objcProvider = objcProvider;
+ this.families = ImmutableSet.copyOf(attributes.families());
+ this.intermediateArtifacts = ObjcRuleClasses.intermediateArtifacts(ruleContext);
+ bundling = bundling(ruleContext, objcProvider, optionsProvider);
+ bundleSupport = new BundleSupport(ruleContext, families, bundling, extraActoolArgs());
+ }
+
+ /**
+ * Validates application-related attributes set on this rule and registers any errors with the
+ * rule context.
+ *
+ * @return this application support
+ */
+ ApplicationSupport validateAttributes() {
+ bundleSupport.validateAttributes();
+
+ // No asset catalogs. That means you cannot specify app_icon or
+ // launch_image attributes, since they must not exist. However, we don't
+ // run actool in this case, which means it does not do validity checks,
+ // and we MUST raise our own error somehow...
+ if (!objcProvider.hasAssetCatalogs()) {
+ if (attributes.appIcon() != null) {
+ ruleContext.attributeError("app_icon",
+ String.format(NO_ASSET_CATALOG_ERROR_FORMAT, attributes.appIcon()));
+ }
+ if (attributes.launchImage() != null) {
+ ruleContext.attributeError("launch_image",
+ String.format(NO_ASSET_CATALOG_ERROR_FORMAT, attributes.launchImage()));
+ }
+ }
+
+ if (families.isEmpty()) {
+ ruleContext.attributeError("families", INVALID_FAMILIES_ERROR);
+ }
+
+ return this;
+ }
+
+ /**
+ * Registers actions required to build an application. This includes any
+ * {@link BundleSupport#registerActions(ObjcProvider) bundle} and bundle merge actions, signing
+ * this application if appropriate and combining several single-architecture binaries into one
+ * multi-architecture binary.
+ *
+ * @return this application support
+ */
+ ApplicationSupport registerActions() {
+ bundleSupport.registerActions(objcProvider);
+
+ registerCombineArchitecturesAction();
+
+ ObjcConfiguration objcConfiguration = ObjcRuleClasses.objcConfiguration(ruleContext);
+ Artifact ipaOutput = ruleContext.getImplicitOutputArtifact(IPA);
+
+ Artifact maybeSignedIpa;
+ if (objcConfiguration.getPlatform() == Platform.SIMULATOR) {
+ maybeSignedIpa = ipaOutput;
+ } else if (attributes.provisioningProfile() == null) {
+ throw new IllegalStateException(DEVICE_NO_PROVISIONING_PROFILE);
+ } else {
+ maybeSignedIpa = registerBundleSigningActions(ipaOutput);
+ }
+
+ BundleMergeControlBytes bundleMergeControlBytes = new BundleMergeControlBytes(
+ bundling, maybeSignedIpa, objcConfiguration, families);
+ registerBundleMergeActions(
+ maybeSignedIpa, bundling.getBundleContentArtifacts(), bundleMergeControlBytes);
+
+ return this;
+ }
+
+ private Artifact registerBundleSigningActions(Artifact ipaOutput) {
+ PathFragment entitlementsDirectory = ruleContext.getUniqueDirectory("entitlements");
+ Artifact teamPrefixFile = ruleContext.getRelatedArtifact(
+ entitlementsDirectory, ".team_prefix_file");
+ registerExtractTeamPrefixAction(teamPrefixFile);
+
+ Artifact entitlementsNeedingSubstitution = attributes.entitlements();
+ if (entitlementsNeedingSubstitution == null) {
+ entitlementsNeedingSubstitution = ruleContext.getRelatedArtifact(
+ entitlementsDirectory, ".entitlements_with_variables");
+ registerExtractEntitlementsAction(entitlementsNeedingSubstitution);
+ }
+ Artifact entitlements = ruleContext.getRelatedArtifact(
+ entitlementsDirectory, ".entitlements");
+ registerEntitlementsVariableSubstitutionAction(
+ entitlementsNeedingSubstitution, entitlements, teamPrefixFile);
+ Artifact ipaUnsigned = ObjcRuleClasses.artifactByAppendingToRootRelativePath(
+ ruleContext, ipaOutput.getExecPath(), ".unsigned");
+ registerSignBundleAction(entitlements, ipaOutput, ipaUnsigned);
+ return ipaUnsigned;
+ }
+
+ /**
+ * Adds bundle- and application-related settings to the given Xcode provider builder.
+ *
+ * @return this application support
+ */
+ ApplicationSupport addXcodeSettings(XcodeProvider.Builder xcodeProviderBuilder) {
+ bundleSupport.addXcodeSettings(xcodeProviderBuilder);
+ xcodeProviderBuilder.addXcodeprojBuildSettings(buildSettings());
+
+ return this;
+ }
+
+ /**
+ * Adds any files to the given nested set builder that should be built if this application is the
+ * top level target in a blaze invocation.
+ *
+ * @return this application support
+ */
+ ApplicationSupport addFilesToBuild(NestedSetBuilder<Artifact> filesToBuild) {
+ NestedSetBuilder<Artifact> debugSymbolBuilder = NestedSetBuilder.<Artifact>stableOrder()
+ .addTransitive(objcProvider.get(ObjcProvider.DEBUG_SYMBOLS));
+
+ if (linkedBinary == LinkedBinary.LOCAL_AND_DEPENDENCIES
+ && ObjcRuleClasses.objcConfiguration(ruleContext).generateDebugSymbols()) {
+ IntermediateArtifacts intermediateArtifacts =
+ ObjcRuleClasses.intermediateArtifacts(ruleContext);
+ debugSymbolBuilder.add(intermediateArtifacts.dsymPlist())
+ .add(intermediateArtifacts.dsymSymbol())
+ .add(intermediateArtifacts.breakpadSym());
+ }
+
+ filesToBuild.add(ruleContext.getImplicitOutputArtifact(ApplicationSupport.IPA))
+ // TODO(bazel-team): Fat binaries may require some merging of these file rather than just
+ // making them available.
+ .addTransitive(debugSymbolBuilder.build());
+ return this;
+ }
+
+ /**
+ * Creates the {@link XcTestAppProvider} that can be used if this application is used as an
+ * {@code xctest_app}.
+ */
+ XcTestAppProvider xcTestAppProvider() {
+ // We want access to #import-able things from our test rig's dependency graph, but we don't
+ // want to link anything since that stuff is shared automatically by way of the
+ // -bundle_loader linker flag.
+ ObjcProvider partialObjcProvider = new ObjcProvider.Builder()
+ .addTransitiveAndPropagate(ObjcProvider.HEADER, objcProvider)
+ .addTransitiveAndPropagate(ObjcProvider.INCLUDE, objcProvider)
+ .addTransitiveAndPropagate(ObjcProvider.SDK_DYLIB, objcProvider)
+ .addTransitiveAndPropagate(ObjcProvider.SDK_FRAMEWORK, objcProvider)
+ .addTransitiveAndPropagate(ObjcProvider.WEAK_SDK_FRAMEWORK, objcProvider)
+ .addTransitiveAndPropagate(ObjcProvider.FRAMEWORK_DIR, objcProvider)
+ .addTransitiveAndPropagate(ObjcProvider.FRAMEWORK_FILE, objcProvider)
+ .build();
+ // TODO(bazel-team): Handle the FRAMEWORK_DIR key properly. We probably want to add it to
+ // framework search paths, but not actually link it with the -framework flag.
+ return new XcTestAppProvider(intermediateArtifacts.singleArchitectureBinary(),
+ ruleContext.getImplicitOutputArtifact(IPA), partialObjcProvider);
+ }
+
+ private ExtraActoolArgs extraActoolArgs() {
+ ImmutableList.Builder<String> extraArgs = ImmutableList.builder();
+ if (attributes.appIcon() != null) {
+ extraArgs.add("--app-icon", attributes.appIcon());
+ }
+ if (attributes.launchImage() != null) {
+ extraArgs.add("--launch-image", attributes.launchImage());
+ }
+ return new ExtraActoolArgs(extraArgs.build());
+ }
+
+ private Bundling bundling(
+ RuleContext ruleContext, ObjcProvider objcProvider, OptionsProvider optionsProvider) {
+ ImmutableList<BundleableFile> extraBundleFiles;
+ ObjcConfiguration objcConfiguration = ObjcRuleClasses.objcConfiguration(ruleContext);
+ if (objcConfiguration.getPlatform() == Platform.DEVICE) {
+ extraBundleFiles = ImmutableList.of(new BundleableFile(
+ attributes.provisioningProfile(),
+ PROVISIONING_PROFILE_BUNDLE_FILE));
+ } else {
+ extraBundleFiles = ImmutableList.of();
+ }
+
+ return new Bundling.Builder()
+ .setName(ruleContext.getLabel().getName())
+ .setBundleDirSuffix(".app")
+ .setExtraBundleFiles(extraBundleFiles)
+ .setObjcProvider(objcProvider)
+ .setInfoplistMerging(
+ BundleSupport.infoPlistMerging(ruleContext, objcProvider, optionsProvider))
+ .setIntermediateArtifacts(intermediateArtifacts)
+ .build();
+ }
+
+ private void registerCombineArchitecturesAction() {
+ Artifact resultingLinkedBinary = intermediateArtifacts.combinedArchitectureBinary(".app");
+ NestedSet<Artifact> linkedBinaries = linkedBinaries();
+
+ ruleContext.registerAction(ObjcActionsBuilder.spawnOnDarwinActionBuilder()
+ .setMnemonic("ObjcCombiningArchitectures")
+ .addTransitiveInputs(linkedBinaries)
+ .addOutput(resultingLinkedBinary)
+ .setExecutable(ObjcActionsBuilder.LIPO)
+ .setCommandLine(CustomCommandLine.builder()
+ .addExecPaths("-create", linkedBinaries)
+ .addExecPath("-o", resultingLinkedBinary)
+ .build())
+ .build(ruleContext));
+ }
+
+ private NestedSet<Artifact> linkedBinaries() {
+ NestedSetBuilder<Artifact> linkedBinariesBuilder = NestedSetBuilder.<Artifact>stableOrder()
+ .addTransitive(attributes.dependentLinkedBinaries());
+ if (linkedBinary == LinkedBinary.LOCAL_AND_DEPENDENCIES) {
+ linkedBinariesBuilder.add(intermediateArtifacts.singleArchitectureBinary());
+ }
+ return linkedBinariesBuilder.build();
+ }
+
+ /** Returns this target's Xcode build settings. */
+ private Iterable<XcodeprojBuildSetting> buildSettings() {
+ ImmutableList.Builder<XcodeprojBuildSetting> buildSettings = new ImmutableList.Builder<>();
+ if (attributes.appIcon() != null) {
+ buildSettings.add(XcodeprojBuildSetting.newBuilder()
+ .setName("ASSETCATALOG_COMPILER_APPICON_NAME")
+ .setValue(attributes.appIcon())
+ .build());
+ }
+ if (attributes.launchImage() != null) {
+ buildSettings.add(XcodeprojBuildSetting.newBuilder()
+ .setName("ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME")
+ .setValue(attributes.launchImage())
+ .build());
+ }
+
+ // Convert names to a sequence containing "1" and/or "2" for iPhone and iPad, respectively.
+ Iterable<Integer> familyIndexes =
+ families.isEmpty() ? ImmutableList.<Integer>of() : UI_DEVICE_FAMILY_VALUES.get(families);
+ buildSettings.add(XcodeprojBuildSetting.newBuilder()
+ .setName("TARGETED_DEVICE_FAMILY")
+ .setValue(Joiner.on(',').join(familyIndexes))
+ .build());
+
+ Artifact entitlements = attributes.entitlements();
+ if (entitlements != null) {
+ buildSettings.add(XcodeprojBuildSetting.newBuilder()
+ .setName("CODE_SIGN_ENTITLEMENTS")
+ .setValue("$(WORKSPACE_ROOT)/" + entitlements.getExecPathString())
+ .build());
+ }
+
+ return buildSettings.build();
+ }
+
+ private ApplicationSupport registerSignBundleAction(
+ Artifact entitlements, Artifact ipaOutput, Artifact ipaUnsigned) {
+ // TODO(bazel-team): Support variable substitution
+ ruleContext.registerAction(ObjcActionsBuilder.spawnOnDarwinActionBuilder()
+ .setMnemonic("IosSignBundle")
+ .setProgressMessage("Signing iOS bundle: " + ruleContext.getLabel())
+ .setExecutable(new PathFragment("/bin/bash"))
+ .addArgument("-c")
+ // TODO(bazel-team): Support --resource-rules for resources
+ .addArgument("set -e && "
+ + "t=$(mktemp -d -t signing_intermediate) && "
+ // Get an absolute path since we need to cd into the temp directory for zip.
+ + "signed_ipa=${PWD}/" + ipaOutput.getExecPathString() + " && "
+ + "unzip -qq " + ipaUnsigned.getExecPathString() + " -d ${t} && "
+ + codesignCommand(
+ attributes.provisioningProfile(),
+ entitlements,
+ String.format("${t}/Payload/%s.app", ruleContext.getLabel().getName())) + " && "
+ // Using zip since we need to preserve permissions
+ + "cd \"${t}\" && /usr/bin/zip -q -r \"${signed_ipa}\" .")
+ .addInput(ipaUnsigned)
+ .addInput(attributes.provisioningProfile())
+ .addInput(entitlements)
+ .addOutput(ipaOutput)
+ .build(ruleContext));
+
+ return this;
+ }
+
+ private void registerBundleMergeActions(Artifact ipaUnsigned,
+ NestedSet<Artifact> bundleContentArtifacts, BundleMergeControlBytes controlBytes) {
+ Artifact bundleMergeControlArtifact =
+ ObjcRuleClasses.artifactByAppendingToBaseName(ruleContext, ".ipa-control");
+
+ ruleContext.registerAction(
+ new BinaryFileWriteAction(
+ ruleContext.getActionOwner(), bundleMergeControlArtifact, controlBytes,
+ /*makeExecutable=*/false));
+
+ ruleContext.registerAction(new SpawnAction.Builder()
+ .setMnemonic("IosBundle")
+ .setProgressMessage("Bundling iOS application: " + ruleContext.getLabel())
+ .setExecutable(attributes.bundleMergeExecutable())
+ .addInputArgument(bundleMergeControlArtifact)
+ .addTransitiveInputs(bundleContentArtifacts)
+ .addOutput(ipaUnsigned)
+ .build(ruleContext));
+ }
+
+ private void registerExtractTeamPrefixAction(Artifact teamPrefixFile) {
+ ruleContext.registerAction(ObjcActionsBuilder.spawnOnDarwinActionBuilder()
+ .setMnemonic("ExtractIosTeamPrefix")
+ .setExecutable(new PathFragment("/bin/bash"))
+ .addArgument("-c")
+ .addArgument("set -e &&"
+ + " PLIST=$(" + extractPlistCommand(attributes.provisioningProfile()) + ") && "
+
+ // We think PlistBuddy uses PRead internally to seek through the file. Or possibly
+ // mmaps the file. Or something similar.
+ //
+ // Pipe FDs do not support PRead or mmap, though.
+ //
+ // <<< however does something magical like write to a temporary file or something
+ // like that internally, which means that this Just Works.
+ + " PREFIX=$(/usr/libexec/PlistBuddy -c 'Print ApplicationIdentifierPrefix:0'"
+ + " /dev/stdin <<< \"${PLIST}\") && "
+ + " echo ${PREFIX} > " + teamPrefixFile.getExecPathString())
+ .addInput(attributes.provisioningProfile())
+ .addOutput(teamPrefixFile)
+ .build(ruleContext));
+ }
+
+ private ApplicationSupport registerExtractEntitlementsAction(Artifact entitlements) {
+ // See Apple Glossary (http://goo.gl/EkhXOb)
+ // An Application Identifier is constructed as: TeamID.BundleID
+ // TeamID is extracted from the provisioning profile.
+ // BundleID consists of a reverse-DNS string to identify the app, where the last component
+ // is the application name, and is specified as an attribute.
+
+ ruleContext.registerAction(ObjcActionsBuilder.spawnOnDarwinActionBuilder()
+ .setMnemonic("ExtractIosEntitlements")
+ .setProgressMessage("Extracting entitlements: " + ruleContext.getLabel())
+ .setExecutable(new PathFragment("/bin/bash"))
+ .addArgument("-c")
+ .addArgument("set -e && "
+ + "PLIST=$("
+ + extractPlistCommand(attributes.provisioningProfile()) + ") && "
+
+ // We think PlistBuddy uses PRead internally to seek through the file. Or possibly
+ // mmaps the file. Or something similar.
+ //
+ // Pipe FDs do not support PRead or mmap, though.
+ //
+ // <<< however does something magical like write to a temporary file or something
+ // like that internally, which means that this Just Works.
+
+ + "/usr/libexec/PlistBuddy -x -c 'Print Entitlements' /dev/stdin <<< \"${PLIST}\" "
+ + "> " + entitlements.getExecPathString())
+ .addInput(attributes.provisioningProfile())
+ .addOutput(entitlements)
+ .build(ruleContext));
+
+ return this;
+ }
+
+ private void registerEntitlementsVariableSubstitutionAction(Artifact in, Artifact out,
+ Artifact prefix) {
+ String escapedBundleId = ShellUtils.shellEscape(attributes.bundleId());
+ ruleContext.registerAction(new SpawnAction.Builder()
+ .setMnemonic("SubstituteIosEntitlements")
+ .setExecutable(new PathFragment("/bin/bash"))
+ .addArgument("-c")
+ .addArgument("set -e && "
+ + "PREFIX=\"$(cat " + prefix.getExecPathString() + ")\" && "
+ + "sed " + in.getExecPathString() + " "
+ // Replace .* from default entitlements file with bundle ID where suitable.
+ + "-e \"s#${PREFIX}\\.\\*#${PREFIX}." + escapedBundleId + "#g\" "
+
+ // Replace some variables that people put in their own entitlements files
+ + "-e \"s#\\$(AppIdentifierPrefix)#${PREFIX}.#g\" "
+ + "-e \"s#\\$(CFBundleIdentifier)#" + escapedBundleId + "#g\" "
+
+ + "> " + out.getExecPathString())
+ .addInput(in)
+ .addInput(prefix)
+ .addOutput(out)
+ .build(ruleContext));
+ }
+
+
+ private String extractPlistCommand(Artifact provisioningProfile) {
+ return "security cms -D -i " + ShellUtils.shellEscape(provisioningProfile.getExecPathString());
+ }
+
+ private String codesignCommand(
+ Artifact provisioningProfile, Artifact entitlements, String appDir) {
+ String fingerprintCommand =
+ "/usr/libexec/PlistBuddy -c 'Print DeveloperCertificates:0' /dev/stdin <<< "
+ + "$(" + extractPlistCommand(provisioningProfile) + ") | "
+ + "openssl x509 -inform DER -noout -fingerprint | "
+ + "cut -d= -f2 | sed -e 's#:##g'";
+ return String.format(
+ "/usr/bin/codesign --force --sign $(%s) --entitlements %s %s",
+ fingerprintCommand,
+ entitlements.getExecPathString(),
+ appDir);
+ }
+
+ /**
+ * Logic to access attributes required by application support. Attributes are required and
+ * guaranteed to return a value or throw unless they are annotated with {@link Nullable} in which
+ * case they can return {@code null} if no value is defined.
+ */
+ private static class Attributes {
+ private final RuleContext ruleContext;
+
+ private Attributes(RuleContext ruleContext) {
+ this.ruleContext = ruleContext;
+ }
+
+ @Nullable
+ String appIcon() {
+ return stringAttribute("app_icon");
+ }
+
+ @Nullable
+ String launchImage() {
+ return stringAttribute("launch_image");
+ }
+
+ @Nullable
+ Artifact provisioningProfile() {
+ return ruleContext.getPrerequisiteArtifact("provisioning_profile", Mode.TARGET);
+ }
+
+ /**
+ * Returns the value of the {@code families} attribute in a form that is more useful than a list
+ * of strings. Returns an empty set for any invalid {@code families} attribute value, including
+ * an empty list.
+ */
+ Set<TargetDeviceFamily> families() {
+ List<String> rawFamilies = ruleContext.attributes().get("families", Type.STRING_LIST);
+ try {
+ return TargetDeviceFamily.fromNamesInRule(rawFamilies);
+ } catch (InvalidFamilyNameException | RepeatedFamilyNameException e) {
+ return ImmutableSet.of();
+ }
+ }
+
+ @Nullable
+ Artifact entitlements() {
+ return ruleContext.getPrerequisiteArtifact("entitlements", Mode.TARGET);
+ }
+
+ NestedSet<? extends Artifact> dependentLinkedBinaries() {
+ if (ruleContext.attributes().getAttributeDefinition("binary") == null) {
+ return NestedSetBuilder.emptySet(Order.STABLE_ORDER);
+ }
+
+ return ruleContext.getPrerequisite("binary", Mode.TARGET, ObjcProvider.class)
+ .get(ObjcProvider.LINKED_BINARY);
+ }
+
+ FilesToRunProvider bundleMergeExecutable() {
+ return checkNotNull(ruleContext.getExecutablePrerequisite("$bundlemerge", Mode.HOST));
+ }
+
+ String bundleId() {
+ return checkNotNull(stringAttribute("bundle_id"));
+ }
+
+ @Nullable
+ private String stringAttribute(String attribute) {
+ String value = ruleContext.attributes().get(attribute, Type.STRING);
+ return value.isEmpty() ? null : value;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/ArtifactListAttribute.java b/src/main/java/com/google/devtools/build/lib/rules/objc/ArtifactListAttribute.java
new file mode 100644
index 0000000..d6c993b
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/ArtifactListAttribute.java
@@ -0,0 +1,45 @@
+// Copyright 2014 Google Inc. 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.rules.objc;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.analysis.RuleContext;
+
+import java.util.Locale;
+
+/**
+ * Attributes containing one or more labels.
+ */
+public enum ArtifactListAttribute {
+ BUNDLE_IMPORTS;
+
+ public String attrName() {
+ return name().toLowerCase(Locale.US);
+ }
+
+ /**
+ * The artifacts specified by this attribute on the given rule. Returns an empty sequence if the
+ * attribute is omitted or not available on the rule type.
+ */
+ public Iterable<Artifact> get(RuleContext context) {
+ if (context.attributes().getAttributeDefinition(attrName()) == null) {
+ return ImmutableList.of();
+ } else {
+ return context.getPrerequisiteArtifacts(attrName(), Mode.TARGET).list();
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/BundleMergeControlBytes.java b/src/main/java/com/google/devtools/build/lib/rules/objc/BundleMergeControlBytes.java
new file mode 100644
index 0000000..695dffc
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/BundleMergeControlBytes.java
@@ -0,0 +1,121 @@
+// Copyright 2014 Google Inc. 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.rules.objc;
+
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.BUNDLE_FILE;
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.NESTED_BUNDLE;
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.XCDATAMODEL;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.io.ByteSource;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.xcode.bundlemerge.proto.BundleMergeProtos;
+import com.google.devtools.build.xcode.bundlemerge.proto.BundleMergeProtos.Control;
+import com.google.devtools.build.xcode.bundlemerge.proto.BundleMergeProtos.MergeZip;
+import com.google.devtools.build.xcode.bundlemerge.proto.BundleMergeProtos.VariableSubstitution;
+import com.google.devtools.build.xcode.common.TargetDeviceFamily;
+
+import java.io.InputStream;
+import java.util.Map;
+
+/**
+ * A byte source that can be used to generate a control file for the tool:
+ * {@code //java/com/google/devtools/build/xcode/bundlemerge}. Note that this generates the control
+ * proto and bytes on-the-fly rather than eagerly. This is to prevent a copy of the bundle files and
+ * .xcdatamodels from being stored for each {@code objc_binary} (or any bundle) being built.
+ */
+final class BundleMergeControlBytes extends ByteSource {
+ private final Bundling rootBundling;
+ private final Artifact mergedIpa;
+ private final ObjcConfiguration objcConfiguration;
+ private final ImmutableSet<TargetDeviceFamily> families;
+
+ public BundleMergeControlBytes(
+ Bundling rootBundling, Artifact mergedIpa, ObjcConfiguration objcConfiguration,
+ ImmutableSet<TargetDeviceFamily> families) {
+ this.rootBundling = Preconditions.checkNotNull(rootBundling);
+ this.mergedIpa = Preconditions.checkNotNull(mergedIpa);
+ this.objcConfiguration = Preconditions.checkNotNull(objcConfiguration);
+ this.families = Preconditions.checkNotNull(families);
+ }
+
+ @Override
+ public InputStream openStream() {
+ return control("Payload/", "Payload/", rootBundling)
+ .toByteString()
+ .newInput();
+ }
+
+ private Control control(String mergeZipPrefix, String bundleDirPrefix, Bundling bundling) {
+ ObjcProvider objcProvider = bundling.getObjcProvider();
+ String bundleDir = bundleDirPrefix + bundling.getBundleDir();
+ mergeZipPrefix += bundling.getBundleDir() + "/";
+
+ BundleMergeProtos.Control.Builder control = BundleMergeProtos.Control.newBuilder()
+ .addAllBundleFile(BundleableFile.toBundleFiles(bundling.getExtraBundleFiles()))
+ .addAllBundleFile(BundleableFile.toBundleFiles(objcProvider.get(BUNDLE_FILE)))
+ .addAllSourcePlistFile(Artifact.toExecPaths(
+ bundling.getInfoplistMerging().getPlistWithEverything().asSet()))
+ // TODO(bazel-team): Add rule attribute for specifying targeted device family
+ .setMinimumOsVersion(objcConfiguration.getMinimumOs())
+ .setSdkVersion(objcConfiguration.getIosSdkVersion())
+ .setPlatform(objcConfiguration.getPlatform().name())
+ .setBundleRoot(bundleDir);
+
+ for (Artifact mergeZip : bundling.getMergeZips()) {
+ control.addMergeZip(MergeZip.newBuilder()
+ .setEntryNamePrefix(mergeZipPrefix)
+ .setSourcePath(mergeZip.getExecPathString())
+ .build());
+ }
+
+ for (Xcdatamodel datamodel : objcProvider.get(XCDATAMODEL)) {
+ control.addMergeZip(MergeZip.newBuilder()
+ .setEntryNamePrefix(mergeZipPrefix)
+ .setSourcePath(datamodel.getOutputZip().getExecPathString())
+ .build());
+ }
+ for (TargetDeviceFamily targetDeviceFamily : families) {
+ control.addTargetDeviceFamily(targetDeviceFamily.name());
+ }
+
+ Map<String, String> variableSubstitutions = bundling.variableSubstitutions();
+ for (String variable : variableSubstitutions.keySet()) {
+ control.addVariableSubstitution(VariableSubstitution.newBuilder()
+ .setName(variable)
+ .setValue(variableSubstitutions.get(variable))
+ .build());
+ }
+
+ control.setOutFile(mergedIpa.getExecPathString());
+
+ for (Artifact linkedBinary : bundling.getCombinedArchitectureBinary().asSet()) {
+ control
+ .addBundleFile(BundleMergeProtos.BundleFile.newBuilder()
+ .setSourceFile(linkedBinary.getExecPathString())
+ .setBundlePath(bundling.getName())
+ .setExternalFileAttribute(BundleableFile.EXECUTABLE_EXTERNAL_FILE_ATTRIBUTE)
+ .build())
+ .setExecutableName(bundling.getName());
+ }
+
+ for (Bundling nestedBundling : bundling.getObjcProvider().get(NESTED_BUNDLE)) {
+ control.addNestedBundle(control(mergeZipPrefix, "", nestedBundling));
+ }
+
+ return control.build();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/BundleSupport.java b/src/main/java/com/google/devtools/build/lib/rules/objc/BundleSupport.java
new file mode 100644
index 0000000..5434ff7
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/BundleSupport.java
@@ -0,0 +1,184 @@
+// Copyright 2015 Google Inc. 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.rules.objc;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.rules.objc.ObjcActionsBuilder.ExtraActoolArgs;
+import com.google.devtools.build.lib.rules.objc.XcodeProvider.Builder;
+import com.google.devtools.build.xcode.common.TargetDeviceFamily;
+
+import java.util.Set;
+
+/**
+ * Support for generating iOS bundles which contain metadata (a plist file), assets, resources and
+ * optionally a binary: registers actions that assemble resources and merge plists, provides data
+ * to providers and validates bundle-related attributes.
+ *
+ * <p>Methods on this class can be called in any order without impacting the result.
+ */
+final class BundleSupport {
+
+ @VisibleForTesting
+ static final String NO_INFOPLIST_ERROR = "An infoplist must be specified either in the "
+ + "'infoplist' attribute or via the 'options' attribute, but none was found";
+
+ private final RuleContext ruleContext;
+ private final Set<TargetDeviceFamily> targetDeviceFamilies;
+ private final ExtraActoolArgs extraActoolArgs;
+ private final Bundling bundling;
+
+ /**
+ * Returns merging instructions for a bundle's {@code Info.plist}.
+ *
+ * @param ruleContext context this bundle is constructed in
+ * @param objcProvider provider containing all dependencies' information as well as some of this
+ * rule's
+ * @param optionsProvider provider containing options and plist settings for this rule and its
+ * dependencies
+ */
+ static InfoplistMerging infoPlistMerging(RuleContext ruleContext,
+ ObjcProvider objcProvider, OptionsProvider optionsProvider) {
+ IntermediateArtifacts intermediateArtifacts =
+ ObjcRuleClasses.intermediateArtifacts(ruleContext);
+
+ return new InfoplistMerging.Builder(ruleContext)
+ .setIntermediateArtifacts(intermediateArtifacts)
+ .setInputPlists(NestedSetBuilder.<Artifact>stableOrder()
+ .addTransitive(optionsProvider.getInfoplists())
+ .addAll(actoolPartialInfoplist(ruleContext, objcProvider).asSet())
+ .build())
+ .setPlmerge(ruleContext.getExecutablePrerequisite("$plmerge", Mode.HOST))
+ .build();
+ }
+
+ /**
+ * Creates a new bundle support with no special {@code actool} arguments.
+ *
+ * @param ruleContext context this bundle is constructed in
+ * @param targetDeviceFamilies device families used in asset catalogue construction
+ * @param bundling bundle information as configured for this rule
+ */
+ public BundleSupport(
+ RuleContext ruleContext, Set<TargetDeviceFamily> targetDeviceFamilies, Bundling bundling) {
+ this(ruleContext, targetDeviceFamilies, bundling, new ExtraActoolArgs());
+ }
+
+ /**
+ * Creates a new bundle support.
+ *
+ * @param ruleContext context this bundle is constructed in
+ * @param targetDeviceFamilies device families used in asset catalogue construction
+ * @param bundling bundle information as configured for this rule
+ * @param extraActoolArgs any additional parameters to be used for invoking {@code actool}
+ */
+ public BundleSupport(RuleContext ruleContext, Set<TargetDeviceFamily> targetDeviceFamilies,
+ Bundling bundling, ExtraActoolArgs extraActoolArgs) {
+ this.ruleContext = ruleContext;
+ this.targetDeviceFamilies = targetDeviceFamilies;
+ this.extraActoolArgs = extraActoolArgs;
+ this.bundling = bundling;
+ }
+
+ /**
+ * Registers actions required for constructing this bundle, namely merging all involved {@code
+ * Info.plist} files and generating asset catalogues.
+ *
+ * @param objcProvider source of information from this rule's attributes and its dependencies
+ *
+ * @return this bundle support
+ */
+ BundleSupport registerActions(ObjcProvider objcProvider) {
+ registerMergeInfoplistAction();
+ registerActoolActionIfNecessary(objcProvider);
+
+ return this;
+ }
+
+ /**
+ * Adds any Xcode settings related to this bundle to the given provider builder.
+ *
+ * @return this bundle support
+ */
+ BundleSupport addXcodeSettings(Builder xcodeProviderBuilder) {
+ xcodeProviderBuilder.setInfoplistMerging(bundling.getInfoplistMerging());
+ return this;
+ }
+
+ /**
+ * Validates any rule attributes and dependencies related to this bundle.
+ *
+ * @return this bundle support
+ */
+ BundleSupport validateAttributes() {
+ if (bundling.getInfoplistMerging().getInputPlists().isEmpty()) {
+ ruleContext.ruleError(NO_INFOPLIST_ERROR);
+ }
+ return this;
+ }
+
+ private void registerMergeInfoplistAction() {
+ // TODO(bazel-team): Move action implementation from InfoplistMerging to this class.
+ ruleContext.registerAction(bundling.getInfoplistMerging().getMergeAction());
+ }
+
+ private void registerActoolActionIfNecessary(ObjcProvider objcProvider) {
+ Optional<Artifact> actoolzipOutput = bundling.getActoolzipOutput();
+ if (!actoolzipOutput.isPresent()) {
+ return;
+ }
+
+ ObjcActionsBuilder actionsBuilder = ObjcRuleClasses.actionsBuilder(ruleContext);
+
+ Artifact actoolPartialInfoplist = actoolPartialInfoplist(ruleContext, objcProvider).get();
+ actionsBuilder.registerActoolzipAction(
+ new ObjcRuleClasses.Tools(ruleContext),
+ objcProvider,
+ actoolzipOutput.get(),
+ new ObjcActionsBuilder.ExtraActoolOutputs(actoolPartialInfoplist),
+ new ExtraActoolArgs(
+ new ImmutableList.Builder<String>()
+ .addAll(extraActoolArgs)
+ .add("--output-partial-info-plist", actoolPartialInfoplist.getExecPathString())
+ .build()),
+ targetDeviceFamilies);
+ }
+
+ /**
+ * Returns the artifact that is a plist file generated by an invocation of {@code actool} or
+ * {@link Optional#absent()} if no asset catalogues are present in this target and its
+ * dependencies.
+ *
+ * <p>All invocations of {@code actool} generate this kind of plist file, which contains metadata
+ * about the {@code app_icon} and {@code launch_image} if supplied. If neither an app icon or a
+ * launch image was supplied, the plist file generated is empty.
+ */
+ private static Optional<Artifact> actoolPartialInfoplist(
+ RuleContext ruleContext, ObjcProvider objcProvider) {
+ if (objcProvider.hasAssetCatalogs()) {
+ IntermediateArtifacts intermediateArtifacts =
+ ObjcRuleClasses.intermediateArtifacts(ruleContext);
+ return Optional.of(intermediateArtifacts.actoolPartialInfoplist());
+ } else {
+ return Optional.absent();
+ }
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/BundleableFile.java b/src/main/java/com/google/devtools/build/lib/rules/objc/BundleableFile.java
new file mode 100644
index 0000000..eea7bf0
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/BundleableFile.java
@@ -0,0 +1,149 @@
+// Copyright 2014 Google Inc. 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.rules.objc;
+
+import static com.google.devtools.build.lib.rules.objc.ArtifactListAttribute.BUNDLE_IMPORTS;
+import static com.google.devtools.build.lib.rules.objc.ObjcCommon.BUNDLE_CONTAINER_TYPE;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.xcode.bundlemerge.proto.BundleMergeProtos.BundleFile;
+import com.google.devtools.build.xcode.util.Value;
+
+/**
+ * Represents a file which is processed to another file and bundled. It contains the
+ * {@code Artifact} corresponding to the original file as well as the {@code Artifact} for the file
+ * converted to its bundled form. Examples of files that fit this pattern are .strings and .xib
+ * files.
+ */
+public final class BundleableFile extends Value<BundleableFile> {
+ static final int EXECUTABLE_EXTERNAL_FILE_ATTRIBUTE = 0100755 << 16;
+ static final int DEFAULT_EXTERNAL_FILE_ATTRIBUTE = 0100644 << 16;
+
+ private final Artifact bundled;
+ private final String bundlePath;
+ private final int zipExternalFileAttribute;
+
+ /**
+ * Creates an instance whose {@code zipExternalFileAttribute} value is
+ * {@link #DEFAULT_EXTERNAL_FILE_ATTRIBUTE}.
+ */
+ BundleableFile(Artifact bundled, String bundlePath) {
+ this(bundled, bundlePath, DEFAULT_EXTERNAL_FILE_ATTRIBUTE);
+ }
+
+ /**
+ * @param bundled the {@link Artifact} whose data is placed in the bundle
+ * @param bundlePath the path of the file in the bundle
+ * @param the external file attribute of the file in the central directory of the bundle (zip
+ * file). The lower 16 bits contain the MS-DOS file attributes. The upper 16 bits contain the
+ * Unix file attributes, for instance 0100755 (octal) for a regular file with permissions
+ * {@code rwxr-xr-x}.
+ */
+ BundleableFile(Artifact bundled, String bundlePath, int zipExternalFileAttribute) {
+ super(new ImmutableMap.Builder<String, Object>()
+ .put("bundled", bundled)
+ .put("bundlePath", bundlePath)
+ .put("zipExternalFileAttribute", zipExternalFileAttribute)
+ .build());
+ this.bundled = bundled;
+ this.bundlePath = bundlePath;
+ this.zipExternalFileAttribute = zipExternalFileAttribute;
+ }
+
+ static String bundlePath(PathFragment path) {
+ String containingDir = path.getParentDirectory().getBaseName();
+ return (containingDir.endsWith(".lproj") ? (containingDir + "/") : "") + path.getBaseName();
+ }
+
+ /**
+ * Given a sequence of non-compiled resource files, returns a sequence of the same length of
+ * instances of this class. Non-compiled resource files are resources which are not processed
+ * before placing them in the final bundle. This is different from (for example) {@code .strings}
+ * and {@code .xib} files, which must be converted to binary plist form or compiled.
+ *
+ * @param files a sequence of artifacts corresponding to non-compiled resource files
+ */
+ public static Iterable<BundleableFile> nonCompiledResourceFiles(Iterable<Artifact> files) {
+ ImmutableList.Builder<BundleableFile> result = new ImmutableList.Builder<>();
+ for (Artifact file : files) {
+ result.add(new BundleableFile(file, bundlePath(file.getExecPath())));
+ }
+ return result.build();
+ }
+
+ /**
+ * Returns an instance for every file in a bundle directory.
+ * <p>
+ * This uses the parent-most container matching {@code *.bundle} as the bundle root.
+ * TODO(bazel-team): add something like an import_root attribute to specify this explicitly, which
+ * will be helpful if a bundle that appears to be nested needs to be imported alone.
+ */
+ public static Iterable<BundleableFile> bundleImportsFromRule(RuleContext context) {
+ ImmutableList.Builder<BundleableFile> result = new ImmutableList.Builder<>();
+ for (Artifact artifact : BUNDLE_IMPORTS.get(context)) {
+ for (PathFragment container :
+ ObjcCommon.farthestContainerMatching(BUNDLE_CONTAINER_TYPE, artifact).asSet()) {
+ // TODO(bazel-team): Figure out if we need to remove symbols of architectures we aren't
+ // building for from the binary in the bundle.
+ result.add(new BundleableFile(
+ artifact,
+ // The path from the artifact's container (including the container), to the artifact
+ // itself. For instance, if artifact is foo/bar.bundle/baz, then this value
+ // is bar.bundle/baz.
+ artifact.getExecPath().relativeTo(container.getParentDirectory()).getSafePathString()));
+ }
+ }
+ return result.build();
+ }
+
+ /**
+ * The artifact that is ultimately bundled.
+ */
+ public Artifact getBundled() {
+ return bundled;
+ }
+
+ /**
+ * Returns bundle files for each given strings file. These are used to merge the strings files to
+ * the final application bundle.
+ */
+ public static Iterable<BundleFile> toBundleFiles(Iterable<BundleableFile> files) {
+ ImmutableList.Builder<BundleFile> result = new ImmutableList.Builder<>();
+ for (BundleableFile file : files) {
+ result.add(BundleFile.newBuilder()
+ .setBundlePath(file.bundlePath)
+ .setSourceFile(file.bundled.getExecPathString())
+ .setExternalFileAttribute(file.zipExternalFileAttribute)
+ .build());
+ }
+ return result.build();
+ }
+
+ /**
+ * Returns the artifacts for the bundled files. These can be used, for instance, as the input
+ * files to add to the bundlemerge action for a bundle that contains all the given files.
+ */
+ public static Iterable<Artifact> toArtifacts(Iterable<BundleableFile> files) {
+ ImmutableList.Builder<Artifact> result = new ImmutableList.Builder<>();
+ for (BundleableFile file : files) {
+ result.add(file.bundled);
+ }
+ return result.build();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/Bundling.java b/src/main/java/com/google/devtools/build/lib/rules/objc/Bundling.java
new file mode 100644
index 0000000..484c553
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/Bundling.java
@@ -0,0 +1,254 @@
+// Copyright 2014 Google Inc. 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.rules.objc;
+
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.ASSET_CATALOG;
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.BUNDLE_FILE;
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.IMPORTED_LIBRARY;
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.LIBRARY;
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.MERGE_ZIP;
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.NESTED_BUNDLE;
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.XCDATAMODEL;
+
+import com.google.common.base.Optional;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.xcode.util.Value;
+
+import java.util.Map;
+
+/**
+ * Contains information regarding the creation of an iOS bundle.
+ */
+@Immutable
+final class Bundling extends Value<Bundling> {
+ static final class Builder {
+ private String name;
+ private String bundleDirSuffix;
+ private ImmutableList<BundleableFile> extraBundleFiles = ImmutableList.of();
+ private ObjcProvider objcProvider;
+ private InfoplistMerging infoplistMerging;
+ private IntermediateArtifacts intermediateArtifacts;
+
+ public Builder setName(String name) {
+ this.name = name;
+ return this;
+ }
+
+ public Builder setBundleDirSuffix(String bundleDirSuffix) {
+ this.bundleDirSuffix = bundleDirSuffix;
+ return this;
+ }
+
+ public Builder setExtraBundleFiles(ImmutableList<BundleableFile> extraBundleFiles) {
+ this.extraBundleFiles = extraBundleFiles;
+ return this;
+ }
+
+ public Builder setObjcProvider(ObjcProvider objcProvider) {
+ this.objcProvider = objcProvider;
+ return this;
+ }
+
+ public Builder setInfoplistMerging(InfoplistMerging infoplistMerging) {
+ this.infoplistMerging = infoplistMerging;
+ return this;
+ }
+
+ public Builder setIntermediateArtifacts(IntermediateArtifacts intermediateArtifacts) {
+ this.intermediateArtifacts = intermediateArtifacts;
+ return this;
+ }
+
+ private static NestedSet<Artifact> nestedBundleContentArtifacts(Iterable<Bundling> bundles) {
+ NestedSetBuilder<Artifact> artifacts = NestedSetBuilder.stableOrder();
+ for (Bundling bundle : bundles) {
+ artifacts.addTransitive(bundle.getBundleContentArtifacts());
+ }
+ return artifacts.build();
+ }
+
+ public Bundling build() {
+ Preconditions.checkNotNull(intermediateArtifacts, "intermediateArtifacts");
+
+ Optional<Artifact> actoolzipOutput = Optional.absent();
+ if (!Iterables.isEmpty(objcProvider.get(ASSET_CATALOG))) {
+ actoolzipOutput = Optional.of(intermediateArtifacts.actoolzipOutput());
+ }
+
+ Optional<Artifact> combinedArchitectureBinary = Optional.absent();
+ if (!Iterables.isEmpty(objcProvider.get(LIBRARY))
+ || !Iterables.isEmpty(objcProvider.get(IMPORTED_LIBRARY))) {
+ combinedArchitectureBinary =
+ Optional.of(intermediateArtifacts.combinedArchitectureBinary(bundleDirSuffix));
+ }
+
+ NestedSet<Artifact> mergeZips = NestedSetBuilder.<Artifact>stableOrder()
+ .addAll(actoolzipOutput.asSet())
+ .addTransitive(objcProvider.get(MERGE_ZIP))
+ .build();
+ NestedSet<Artifact> bundleContentArtifacts = NestedSetBuilder.<Artifact>stableOrder()
+ .addTransitive(nestedBundleContentArtifacts(objcProvider.get(NESTED_BUNDLE)))
+ .addAll(combinedArchitectureBinary.asSet())
+ .addAll(infoplistMerging.getPlistWithEverything().asSet())
+ .addTransitive(mergeZips)
+ .addAll(BundleableFile.toArtifacts(extraBundleFiles))
+ .addAll(BundleableFile.toArtifacts(objcProvider.get(BUNDLE_FILE)))
+ .addAll(Xcdatamodel.outputZips(objcProvider.get(XCDATAMODEL)))
+ .build();
+
+ return new Bundling(name, bundleDirSuffix, combinedArchitectureBinary, extraBundleFiles,
+ objcProvider, infoplistMerging, actoolzipOutput, bundleContentArtifacts, mergeZips);
+ }
+ }
+
+ private final String name;
+ private final String bundleDirSuffix;
+ private final Optional<Artifact> combinedArchitectureBinary;
+ private final ImmutableList<BundleableFile> extraBundleFiles;
+ private final ObjcProvider objcProvider;
+ private final InfoplistMerging infoplistMerging;
+ private final Optional<Artifact> actoolzipOutput;
+ private final NestedSet<Artifact> bundleContentArtifacts;
+ private final NestedSet<Artifact> mergeZips;
+
+ private Bundling(
+ String name,
+ String bundleDirSuffix,
+ Optional<Artifact> combinedArchitectureBinary,
+ ImmutableList<BundleableFile> extraBundleFiles,
+ ObjcProvider objcProvider,
+ InfoplistMerging infoplistMerging,
+ Optional<Artifact> actoolzipOutput,
+ NestedSet<Artifact> bundleContentArtifacts,
+ NestedSet<Artifact> mergeZips) {
+ super(new ImmutableMap.Builder<String, Object>()
+ .put("name", name)
+ .put("bundleDirSuffix", bundleDirSuffix)
+ .put("combinedArchitectureBinary", combinedArchitectureBinary)
+ .put("extraBundleFiles", extraBundleFiles)
+ .put("objcProvider", objcProvider)
+ .put("infoplistMerging", infoplistMerging)
+ .put("actoolzipOutput", actoolzipOutput)
+ .put("bundleContentArtifacts", bundleContentArtifacts)
+ .put("mergeZips", mergeZips)
+ .build());
+ this.name = name;
+ this.bundleDirSuffix = bundleDirSuffix;
+ this.combinedArchitectureBinary = combinedArchitectureBinary;
+ this.extraBundleFiles = extraBundleFiles;
+ this.objcProvider = objcProvider;
+ this.infoplistMerging = infoplistMerging;
+ this.actoolzipOutput = actoolzipOutput;
+ this.bundleContentArtifacts = bundleContentArtifacts;
+ this.mergeZips = mergeZips;
+ }
+
+ /**
+ * The bundle directory. For apps, {@code "Payload/" + bundleDir} is the directory in the bundle
+ * zip archive in which every file is found including the linked binary, nested bundles, and
+ * everything returned by {@link #getExtraBundleFiles()}. In an application bundle, for instance,
+ * this function returns {@code "(name).app"}.
+ */
+ public String getBundleDir() {
+ return name + bundleDirSuffix;
+ }
+
+ /**
+ * The name of the bundle, from which the bundle root and the path of the linked binary in the
+ * bundle archive are derived.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * An {@link Optional} with the linked binary artifact, or {@link Optional#absent()} if it is
+ * empty and should not be included in the bundle.
+ */
+ public Optional<Artifact> getCombinedArchitectureBinary() {
+ return combinedArchitectureBinary;
+ }
+
+ /**
+ * Extra bundle files to include in the bundle which are not automatically deduced by the contents
+ * of the provider. These files are placed under the bundle root (possibly nested, of course,
+ * depending on the bundle path of the files).
+ */
+ public ImmutableList<BundleableFile> getExtraBundleFiles() {
+ return extraBundleFiles;
+ }
+
+ /**
+ * The {@link ObjcProvider} for this bundle.
+ */
+ public ObjcProvider getObjcProvider() {
+ return objcProvider;
+ }
+
+ /**
+ * Information on the Info.plist and its merge inputs for this bundle. Note that an infoplist is
+ * only included in the bundle if it has one or more merge inputs.
+ */
+ public InfoplistMerging getInfoplistMerging() {
+ return infoplistMerging;
+ }
+
+ /**
+ * The location of the actoolzip output for this bundle. This is non-absent only included in the
+ * bundle if there is at least one asset catalog artifact supplied by
+ * {@link ObjcProvider#ASSET_CATALOG}.
+ */
+ public Optional<Artifact> getActoolzipOutput() {
+ return actoolzipOutput;
+ }
+
+ /**
+ * Returns all zip files whose contents should be merged into this bundle under the main bundle
+ * directory. For instance, if a merge zip contains files a/b and c/d, then the resulting bundling
+ * would have additional files at:
+ * <ul>
+ * <li>{bundleDir}/a/b
+ * <li>{bundleDir}/c/d
+ * </ul>
+ */
+ public NestedSet<Artifact> getMergeZips() {
+ return mergeZips;
+ }
+
+ /**
+ * Returns the variable substitutions that should be used when merging the plist info file of
+ * this bundle.
+ */
+ public Map<String, String> variableSubstitutions() {
+ return ImmutableMap.of(
+ "EXECUTABLE_NAME", name,
+ "BUNDLE_NAME", name + bundleDirSuffix,
+ "PRODUCT_NAME", name);
+ }
+
+ /**
+ * Returns the artifacts that are required to generate this bundle.
+ */
+ public NestedSet<Artifact> getBundleContentArtifacts() {
+ return bundleContentArtifacts;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/CompilationArtifacts.java b/src/main/java/com/google/devtools/build/lib/rules/objc/CompilationArtifacts.java
new file mode 100644
index 0000000..5aac139
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/CompilationArtifacts.java
@@ -0,0 +1,98 @@
+// Copyright 2014 Google Inc. 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.rules.objc;
+
+import com.google.common.base.Optional;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.Artifact;
+
+/**
+ * Artifacts related to compilation. Any rule containing compilable sources will create an instance
+ * of this class.
+ */
+final class CompilationArtifacts {
+ static class Builder {
+ private Iterable<Artifact> srcs = ImmutableList.of();
+ private Iterable<Artifact> nonArcSrcs = ImmutableList.of();
+ private Optional<Artifact> pchFile;
+ private IntermediateArtifacts intermediateArtifacts;
+
+ Builder addSrcs(Iterable<Artifact> srcs) {
+ this.srcs = Iterables.concat(this.srcs, srcs);
+ return this;
+ }
+
+ Builder addNonArcSrcs(Iterable<Artifact> nonArcSrcs) {
+ this.nonArcSrcs = Iterables.concat(this.nonArcSrcs, nonArcSrcs);
+ return this;
+ }
+
+ Builder setPchFile(Optional<Artifact> pchFile) {
+ Preconditions.checkState(this.pchFile == null,
+ "pchFile is already set to: %s", this.pchFile);
+ this.pchFile = Preconditions.checkNotNull(pchFile);
+ return this;
+ }
+
+ Builder setIntermediateArtifacts(IntermediateArtifacts intermediateArtifacts) {
+ Preconditions.checkState(this.intermediateArtifacts == null,
+ "intermediateArtifacts is already set to: %s", this.intermediateArtifacts);
+ this.intermediateArtifacts = intermediateArtifacts;
+ return this;
+ }
+
+ CompilationArtifacts build() {
+ Optional<Artifact> archive = Optional.absent();
+ if (!Iterables.isEmpty(srcs) || !Iterables.isEmpty(nonArcSrcs)) {
+ archive = Optional.of(intermediateArtifacts.archive());
+ }
+ return new CompilationArtifacts(srcs, nonArcSrcs, archive, pchFile);
+ }
+ }
+
+ private final Iterable<Artifact> srcs;
+ private final Iterable<Artifact> nonArcSrcs;
+ private final Optional<Artifact> archive;
+ private final Optional<Artifact> pchFile;
+
+ private CompilationArtifacts(
+ Iterable<Artifact> srcs,
+ Iterable<Artifact> nonArcSrcs,
+ Optional<Artifact> archive,
+ Optional<Artifact> pchFile) {
+ this.srcs = Preconditions.checkNotNull(srcs);
+ this.nonArcSrcs = Preconditions.checkNotNull(nonArcSrcs);
+ this.archive = Preconditions.checkNotNull(archive);
+ this.pchFile = Preconditions.checkNotNull(pchFile);
+ }
+
+ public Iterable<Artifact> getSrcs() {
+ return srcs;
+ }
+
+ public Iterable<Artifact> getNonArcSrcs() {
+ return nonArcSrcs;
+ }
+
+ public Optional<Artifact> getArchive() {
+ return archive;
+ }
+
+ public Optional<Artifact> getPchFile() {
+ return pchFile;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/CompilationSupport.java b/src/main/java/com/google/devtools/build/lib/rules/objc/CompilationSupport.java
new file mode 100644
index 0000000..1e23798
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/CompilationSupport.java
@@ -0,0 +1,235 @@
+// Copyright 2015 Google Inc. 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.rules.objc;
+
+import static com.google.devtools.build.lib.rules.objc.ObjcRuleClasses.NON_ARC_SRCS_TYPE;
+import static com.google.devtools.build.lib.rules.objc.ObjcRuleClasses.SRCS_TYPE;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.actions.CustomCommandLine;
+import com.google.devtools.build.lib.analysis.actions.SpawnAction;
+import com.google.devtools.build.lib.rules.objc.ObjcActionsBuilder.ExtraLinkArgs;
+import com.google.devtools.build.lib.rules.objc.ObjcActionsBuilder.ExtraLinkInputs;
+import com.google.devtools.build.lib.rules.objc.ObjcCommon.CompilationAttributes;
+import com.google.devtools.build.lib.rules.objc.XcodeProvider.Builder;
+import com.google.devtools.build.lib.shell.ShellUtils;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+/**
+ * Support for rules that compile sources. Provides ways to determine files that should be output,
+ * registering Xcode settings and generating the various actions that might be needed for
+ * compilation.
+ *
+ * <p>Methods on this class can be called in any order without impacting the result.
+ */
+final class CompilationSupport {
+
+ @VisibleForTesting
+ static final String ABSOLUTE_INCLUDES_PATH_FORMAT =
+ "The path '%s' is absolute, but only relative paths are allowed.";
+
+ /**
+ * Returns information about the given rule's compilation artifacts.
+ */
+ // TODO(bazel-team): Remove this information from ObjcCommon and move it internal to this class.
+ static CompilationArtifacts compilationArtifacts(RuleContext ruleContext) {
+ return new CompilationArtifacts.Builder()
+ .addSrcs(ruleContext.getPrerequisiteArtifacts("srcs", Mode.TARGET)
+ .errorsForNonMatching(SRCS_TYPE)
+ .list())
+ .addNonArcSrcs(ruleContext.getPrerequisiteArtifacts("non_arc_srcs", Mode.TARGET)
+ .errorsForNonMatching(NON_ARC_SRCS_TYPE)
+ .list())
+ .setIntermediateArtifacts(ObjcRuleClasses.intermediateArtifacts(ruleContext))
+ .setPchFile(Optional.fromNullable(ruleContext.getPrerequisiteArtifact("pch", Mode.TARGET)))
+ .build();
+ }
+
+ private final RuleContext ruleContext;
+ private final CompilationAttributes attributes;
+
+ /**
+ * Creates a new compilation support for the given rule.
+ */
+ CompilationSupport(RuleContext ruleContext) {
+ this.ruleContext = ruleContext;
+ this.attributes = new CompilationAttributes(ruleContext);
+ }
+
+ /**
+ * Registers all actions necessary to compile this rule's sources and archive them.
+ *
+ * @param common common information about this rule and its dependencies
+ * @param optionsProvider option and plist information about this rule and its dependencies
+ *
+ * @return this compilation support
+ */
+ CompilationSupport registerCompileAndArchiveActions(
+ ObjcCommon common, OptionsProvider optionsProvider) {
+ if (common.getCompilationArtifacts().isPresent()) {
+ ObjcRuleClasses.actionsBuilder(ruleContext).registerCompileAndArchiveActions(
+ common.getCompilationArtifacts().get(), common.getObjcProvider(), optionsProvider);
+ }
+ return this;
+ }
+
+ /**
+ * Registers any actions necessary to link this rule and its dependencies. Debug symbols are
+ * generated if {@link ObjcConfiguration#generateDebugSymbols()} is set.
+ *
+ * @param objcProvider common information about this rule's attributes and its dependencies
+ * @param extraLinkArgs any additional arguments to pass to the linker
+ * @param extraLinkInputs any additional input artifacts to pass to the link action
+ *
+ * @return this compilation support
+ */
+ CompilationSupport registerLinkActions(ObjcProvider objcProvider, ExtraLinkArgs extraLinkArgs,
+ ExtraLinkInputs extraLinkInputs) {
+ IntermediateArtifacts intermediateArtifacts =
+ ObjcRuleClasses.intermediateArtifacts(ruleContext);
+ Optional<Artifact> dsymBundle;
+ if (ObjcRuleClasses.objcConfiguration(ruleContext).generateDebugSymbols()) {
+ registerDsymActions();
+ dsymBundle = Optional.of(intermediateArtifacts.dsymBundle());
+ } else {
+ dsymBundle = Optional.absent();
+ }
+
+ ObjcRuleClasses.actionsBuilder(ruleContext).registerLinkAction(
+ intermediateArtifacts.singleArchitectureBinary(), objcProvider, extraLinkArgs,
+ extraLinkInputs, dsymBundle);
+ return this;
+ }
+
+ /**
+ * Registers actions that compile and archive j2Objc dependencies of this rule.
+ *
+ * @param optionsProvider option and plist information about this rule and its dependencies
+ * @param objcProvider common information about this rule's attributes and its dependencies
+ *
+ * @return this compilation support
+ */
+ CompilationSupport registerJ2ObjcCompileAndArchiveActions(
+ OptionsProvider optionsProvider, ObjcProvider objcProvider) {
+ for (J2ObjcSource j2ObjcSource : ObjcRuleClasses.j2ObjcSrcsProvider(ruleContext).getSrcs()) {
+ IntermediateArtifacts intermediateArtifacts =
+ ObjcRuleClasses.j2objcIntermediateArtifacts(ruleContext, j2ObjcSource);
+ CompilationArtifacts compilationArtifact = new CompilationArtifacts.Builder()
+ .addNonArcSrcs(j2ObjcSource.getObjcSrcs())
+ .setIntermediateArtifacts(intermediateArtifacts)
+ .setPchFile(Optional.<Artifact>absent())
+ .build();
+ ObjcActionsBuilder actionBuilder = new ObjcActionsBuilder(
+ ruleContext,
+ intermediateArtifacts,
+ ObjcRuleClasses.objcConfiguration(ruleContext),
+ ruleContext.getConfiguration(),
+ ruleContext);
+ actionBuilder
+ .registerCompileAndArchiveActions(compilationArtifact, objcProvider, optionsProvider);
+ }
+
+ return this;
+ }
+
+ /**
+ * Sets compilation-related Xcode project information on the given provider builder.
+ *
+ * @param common common information about this rule's attributes and its dependencies
+ * @param optionsProvider option and plist information about this rule and its dependencies
+ * @return this compilation support
+ */
+ CompilationSupport addXcodeSettings(Builder xcodeProviderBuilder,
+ ObjcCommon common, OptionsProvider optionsProvider) {
+ ObjcConfiguration objcConfiguration = ObjcRuleClasses.objcConfiguration(ruleContext);
+ for (CompilationArtifacts artifacts : common.getCompilationArtifacts().asSet()) {
+ xcodeProviderBuilder.setCompilationArtifacts(artifacts);
+ }
+ xcodeProviderBuilder
+ .addHeaders(attributes.hdrs())
+ .addUserHeaderSearchPaths(ObjcCommon.userHeaderSearchPaths(ruleContext.getConfiguration()))
+ .addHeaderSearchPaths("$(WORKSPACE_ROOT)", attributes.headerSearchPaths())
+ .addHeaderSearchPaths("$(SDKROOT)/usr/include", attributes.sdkIncludes())
+ .addCompilationModeCopts(objcConfiguration.getCoptsForCompilationMode())
+ .addCopts(objcConfiguration.getCopts())
+ .addCopts(optionsProvider.getCopts());
+ return this;
+ }
+
+ /**
+ * Validates compilation-related attributes on this rule.
+ *
+ * @return this compilation support
+ */
+ CompilationSupport validateAttributes() {
+ for (PathFragment absoluteInclude :
+ Iterables.filter(attributes.includes(), PathFragment.IS_ABSOLUTE)) {
+ ruleContext.attributeError(
+ "includes", String.format(ABSOLUTE_INCLUDES_PATH_FORMAT, absoluteInclude));
+ }
+
+ return this;
+ }
+
+ private CompilationSupport registerDsymActions() {
+ IntermediateArtifacts intermediateArtifacts =
+ ObjcRuleClasses.intermediateArtifacts(ruleContext);
+ Artifact dsymBundle = intermediateArtifacts.dsymBundle();
+ Artifact debugSymbolFile = intermediateArtifacts.dsymSymbol();
+ ruleContext.registerAction(new SpawnAction.Builder()
+ .setMnemonic("UnzipDsym")
+ .setProgressMessage("Unzipping dSYM file: " + ruleContext.getLabel())
+ .setExecutable(new PathFragment("/usr/bin/unzip"))
+ .addInput(dsymBundle)
+ .setCommandLine(CustomCommandLine.builder()
+ .add(dsymBundle.getExecPathString())
+ .add("-d")
+ .add(stripSuffix(dsymBundle.getExecPathString(),
+ IntermediateArtifacts.TMP_DSYM_BUNDLE_SUFFIX) + ".app.dSYM")
+ .build())
+ .addOutput(intermediateArtifacts.dsymPlist())
+ .addOutput(debugSymbolFile)
+ .build(ruleContext));
+
+ Artifact dumpsyms = ruleContext.getPrerequisiteArtifact("$dumpsyms", Mode.HOST);
+ Artifact breakpadFile = intermediateArtifacts.breakpadSym();
+ ruleContext.registerAction(new SpawnAction.Builder()
+ .setMnemonic("GenBreakpad")
+ .setProgressMessage("Generating breakpad file: " + ruleContext.getLabel())
+ .setShellCommand(ImmutableList.of("/bin/bash", "-c"))
+ .setExecutionInfo(ImmutableMap.of(ExecutionRequirements.REQUIRES_DARWIN, ""))
+ .addInput(dumpsyms)
+ .addInput(debugSymbolFile)
+ .addArgument(String.format("%s %s > %s",
+ ShellUtils.shellEscape(dumpsyms.getExecPathString()),
+ ShellUtils.shellEscape(debugSymbolFile.getExecPathString()),
+ ShellUtils.shellEscape(breakpadFile.getExecPathString())))
+ .addOutput(breakpadFile)
+ .build(ruleContext));
+ return this;
+ }
+
+ private String stripSuffix(String str, String suffix) {
+ // TODO(bazel-team): Throw instead of returning null?
+ return str.endsWith(suffix) ? str.substring(0, str.length() - suffix.length()) : null;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/CompiledResourceFile.java b/src/main/java/com/google/devtools/build/lib/rules/objc/CompiledResourceFile.java
new file mode 100644
index 0000000..cd84710
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/CompiledResourceFile.java
@@ -0,0 +1,69 @@
+// Copyright 2014 Google Inc. 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.rules.objc;
+
+import com.google.common.base.Function;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.Artifact;
+
+/**
+ * Represents a strings file.
+ */
+public class CompiledResourceFile {
+ private final Artifact original;
+ private final BundleableFile bundled;
+
+ private CompiledResourceFile(Artifact original, BundleableFile bundled) {
+ this.original = Preconditions.checkNotNull(original);
+ this.bundled = Preconditions.checkNotNull(bundled);
+ }
+
+ /**
+ * The checked-in version of the bundled file.
+ */
+ public Artifact getOriginal() {
+ return original;
+ }
+
+ public BundleableFile getBundled() {
+ return bundled;
+ }
+
+ public static final Function<CompiledResourceFile, BundleableFile> TO_BUNDLED =
+ new Function<CompiledResourceFile, BundleableFile>() {
+ @Override
+ public BundleableFile apply(CompiledResourceFile input) {
+ return input.bundled;
+ }
+ };
+
+ /**
+ * Given a sequence of artifacts corresponding to {@code .strings} files, returns a sequence of
+ * the same length of instances of this class. The value returned by {@link #getBundled()} of each
+ * instance will be the plist file in binary form.
+ */
+ public static Iterable<CompiledResourceFile> fromStringsFiles(
+ IntermediateArtifacts intermediateArtifacts, Iterable<Artifact> strings) {
+ ImmutableList.Builder<CompiledResourceFile> result = new ImmutableList.Builder<>();
+ for (Artifact originalFile : strings) {
+ Artifact binaryFile = intermediateArtifacts.convertedStringsFile(originalFile);
+ result.add(new CompiledResourceFile(
+ originalFile,
+ new BundleableFile(binaryFile, BundleableFile.bundlePath(originalFile.getExecPath()))));
+ }
+ return result.build();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/ExecutionRequirements.java b/src/main/java/com/google/devtools/build/lib/rules/objc/ExecutionRequirements.java
new file mode 100644
index 0000000..2257136
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/ExecutionRequirements.java
@@ -0,0 +1,23 @@
+// Copyright 2014 Google Inc. 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.rules.objc;
+
+/**
+ * Strings used to express requirements on action execution environments.
+ */
+public class ExecutionRequirements {
+ /** If an action would not successfully run other than on Darwin. */
+ public static final String REQUIRES_DARWIN = "requires-darwin";
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/InfoplistMerging.java b/src/main/java/com/google/devtools/build/lib/rules/objc/InfoplistMerging.java
new file mode 100644
index 0000000..58369ad
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/InfoplistMerging.java
@@ -0,0 +1,142 @@
+// Copyright 2014 Google Inc. 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.rules.objc;
+
+import com.google.common.base.Optional;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.FilesToRunProvider;
+import com.google.devtools.build.lib.analysis.actions.ActionConstructionContext;
+import com.google.devtools.build.lib.analysis.actions.CommandLine;
+import com.google.devtools.build.lib.analysis.actions.SpawnAction;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.xcode.util.Interspersing;
+
+/**
+ * Supplies information regarding Infoplist merging for a particular binary. This includes:
+ * <ul>
+ * <li>the Info.plist which contains the fields from every source. If there is only one source
+ * plist, this is that plist.
+ * <li>the action to merge all the Infoplists into a single one. This is present even if there is
+ * only one Infoplist, to prevent a Bazel error when an Artifact does not have a generating
+ * action.
+ * </ul>
+ */
+class InfoplistMerging {
+ static class Builder {
+ private final ActionConstructionContext context;
+ private NestedSet<Artifact> inputPlists;
+ private FilesToRunProvider plmerge;
+ private IntermediateArtifacts intermediateArtifacts;
+
+ public Builder(ActionConstructionContext context) {
+ this.context = Preconditions.checkNotNull(context);
+ }
+
+ public Builder setInputPlists(NestedSet<Artifact> inputPlists) {
+ Preconditions.checkState(this.inputPlists == null);
+ this.inputPlists = inputPlists;
+ return this;
+ }
+
+ public Builder setPlmerge(FilesToRunProvider plmerge) {
+ Preconditions.checkState(this.plmerge == null);
+ this.plmerge = plmerge;
+ return this;
+ }
+
+ public Builder setIntermediateArtifacts(IntermediateArtifacts intermediateArtifacts) {
+ this.intermediateArtifacts = intermediateArtifacts;
+ return this;
+ }
+
+ /**
+ * This static factory method prevents retention of the outer {@link Builder} class reference by
+ * the anonymous {@link CommandLine} instance.
+ */
+ private static CommandLine mergeCommandLine(
+ final NestedSet<Artifact> inputPlists, final Artifact mergedInfoplist) {
+ return new CommandLine() {
+ @Override
+ public Iterable<String> arguments() {
+ return new ImmutableList.Builder<String>()
+ .addAll(Interspersing.beforeEach(
+ "--source_file", Artifact.toExecPaths(inputPlists)))
+ .add("--out_file", mergedInfoplist.getExecPathString())
+ .build();
+ }
+ };
+ }
+
+ public InfoplistMerging build() {
+ Preconditions.checkNotNull(intermediateArtifacts, "intermediateArtifacts");
+
+ Optional<Artifact> plistWithEverything = Optional.absent();
+ Action[] mergeActions = new Action[0];
+
+ int inputs = Iterables.size(inputPlists);
+ if (inputs == 1) {
+ plistWithEverything = Optional.of(Iterables.getOnlyElement(inputPlists));
+ } else if (inputs > 1) {
+ Artifact merged = intermediateArtifacts.mergedInfoplist();
+
+ plistWithEverything = Optional.of(merged);
+ mergeActions = new SpawnAction.Builder()
+ .setMnemonic("MergeInfoPlistFiles")
+ .setExecutable(plmerge)
+ .setCommandLine(mergeCommandLine(inputPlists, merged))
+ .addTransitiveInputs(inputPlists)
+ .addOutput(merged)
+ .build(context);
+ }
+
+ return new InfoplistMerging(plistWithEverything, mergeActions, inputPlists);
+ }
+ }
+
+ private final Optional<Artifact> plistWithEverything;
+ private final Action[] mergeActions;
+ private final NestedSet<Artifact> inputPlists;
+
+ private InfoplistMerging(Optional<Artifact> plistWithEverything, Action[] mergeActions,
+ NestedSet<Artifact> inputPlists) {
+ this.plistWithEverything = plistWithEverything;
+ this.mergeActions = mergeActions;
+ this.inputPlists = inputPlists;
+ }
+
+ /**
+ * Creates action to merge multiple Info.plist files of a binary into a single Info.plist. No
+ * action is necessary if there is only one source.
+ */
+ public Action[] getMergeAction() {
+ return mergeActions;
+ }
+
+ /**
+ * An {@link Optional} with the merged infoplist, or {@link Optional#absent()} if there are no
+ * merge inputs and it should not be included in the bundle.
+ */
+ public Optional<Artifact> getPlistWithEverything() {
+ return plistWithEverything;
+ }
+
+ public NestedSet<Artifact> getInputPlists() {
+ return inputPlists;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/IntermediateArtifacts.java b/src/main/java/com/google/devtools/build/lib/rules/objc/IntermediateArtifacts.java
new file mode 100644
index 0000000..b84bf03
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/IntermediateArtifacts.java
@@ -0,0 +1,220 @@
+// Copyright 2014 Google Inc. 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.rules.objc;
+
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.Root;
+import com.google.devtools.build.lib.analysis.AnalysisEnvironment;
+import com.google.devtools.build.lib.analysis.AnalysisUtils;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+/**
+ * Factory class for generating artifacts which are used as intermediate output.
+ */
+// TODO(bazel-team): This should really be named DerivedArtifacts as it contains methods for
+// final as well as intermediate artifacts.
+final class IntermediateArtifacts {
+
+ /**
+ * Extension used on the temporary dsym bundle location. Must end in {@code .dSYM} for dsymutil
+ * to generate a plist file.
+ */
+ static final String TMP_DSYM_BUNDLE_SUFFIX = ".temp.app.dSYM";
+
+ private final AnalysisEnvironment analysisEnvironment;
+ private final Root binDirectory;
+ private final Label ownerLabel;
+ private final String archiveFileNameSuffix;
+
+ IntermediateArtifacts(
+ AnalysisEnvironment analysisEnvironment, Root binDirectory, Label ownerLabel,
+ String archiveFileNameSuffix) {
+ this.analysisEnvironment = Preconditions.checkNotNull(analysisEnvironment);
+ this.binDirectory = Preconditions.checkNotNull(binDirectory);
+ this.ownerLabel = Preconditions.checkNotNull(ownerLabel);
+ this.archiveFileNameSuffix = Preconditions.checkNotNull(archiveFileNameSuffix);
+ }
+
+ /**
+ * Returns a derived artifact in the bin directory obtained by appending some extension to the end
+ * of the given {@link PathFragment}.
+ */
+ private Artifact appendExtension(PathFragment original, String extension) {
+ return analysisEnvironment.getDerivedArtifact(
+ FileSystemUtils.appendExtension(original, extension), binDirectory);
+ }
+
+ /**
+ * Returns a derived artifact in the bin directory obtained by appending some extension to the end
+ * of the {@link PathFragment} corresponding to the owner {@link Label}.
+ */
+ private Artifact appendExtension(String extension) {
+ return appendExtension(ownerLabel.toPathFragment(), extension);
+ }
+
+ /**
+ * The output of using {@code actooloribtoolzip} to run {@code actool} for a given bundle which is
+ * merged under the {@code .app} or {@code .bundle} directory root.
+ */
+ public Artifact actoolzipOutput() {
+ return appendExtension(".actool.zip");
+ }
+
+ /**
+ * Output of the partial infoplist generated by {@code actool} when given the
+ * {@code --output-partial-info-plist [path]} flag.
+ */
+ public Artifact actoolPartialInfoplist() {
+ return appendExtension(".actool-PartialInfo.plist");
+ }
+
+ /**
+ * The Info.plist file for a bundle which is comprised of more than one originating plist file.
+ * This is not needed for a bundle which has no source Info.plist files, or only one Info.plist
+ * file, since no merging occurs in that case.
+ */
+ public Artifact mergedInfoplist() {
+ return appendExtension("-MergedInfo.plist");
+ }
+
+ /**
+ * The .objlist file, which contains a list of paths of object files to archive and is read by
+ * libtool in the archive action.
+ */
+ public Artifact objList() {
+ return appendExtension(".objlist");
+ }
+
+ /**
+ * The artifact which is the binary (or library) which is comprised of one or more .a files linked
+ * together.
+ */
+ public Artifact singleArchitectureBinary() {
+ return appendExtension("_bin");
+ }
+
+ /**
+ * Lipo binary generated by combining one or more linked binaries. This binary is the one included
+ * in generated bundles and invoked as entry point to the application.
+ *
+ * @param bundleDirSuffix suffix of the bundle containing this binary
+ */
+ public Artifact combinedArchitectureBinary(String bundleDirSuffix) {
+ String baseName = ownerLabel.toPathFragment().getBaseName();
+ return appendExtension(bundleDirSuffix + "/" + baseName);
+ }
+
+ /**
+ * The {@code .a} file which contains all the compiled sources for a rule.
+ */
+ public Artifact archive() {
+ PathFragment labelPath = ownerLabel.toPathFragment();
+ PathFragment rootRelative = labelPath
+ .getParentDirectory()
+ .getRelative(String.format("lib%s%s.a", labelPath.getBaseName(), archiveFileNameSuffix));
+ return analysisEnvironment.getDerivedArtifact(rootRelative, binDirectory);
+ }
+
+ /**
+ * The debug symbol bundle file which contains debug symbols generated by dsymutil.
+ */
+ public Artifact dsymBundle() {
+ return appendExtension(TMP_DSYM_BUNDLE_SUFFIX);
+ }
+
+ /**
+ * The artifact for the .o file that should be generated when compiling the {@code source}
+ * artifact.
+ */
+ public Artifact objFile(Artifact source) {
+ return analysisEnvironment.getDerivedArtifact(
+ FileSystemUtils.replaceExtension(
+ AnalysisUtils.getUniqueDirectory(ownerLabel, new PathFragment("_objs"))
+ .getRelative(source.getRootRelativePath()),
+ ".o"),
+ binDirectory);
+ }
+
+ /**
+ * Returns the artifact corresponding to the pbxproj control file, which specifies the information
+ * required to generate the Xcode project file.
+ */
+ public Artifact pbxprojControlArtifact() {
+ return appendExtension(".xcodeproj-control");
+ }
+
+ /**
+ * The artifact which contains the zipped-up results of compiling the storyboard. This is merged
+ * into the final bundle under the {@code .app} or {@code .bundle} directory root.
+ */
+ public Artifact compiledStoryboardZip(Artifact input) {
+ return appendExtension("/" + BundleableFile.bundlePath(input.getExecPath()) + ".zip");
+ }
+
+ /**
+ * Returns the artifact which is the output of building an entire xcdatamodel[d] made of artifacts
+ * specified by a single rule.
+ *
+ * @param containerDir the containing *.xcdatamodeld or *.xcdatamodel directory
+ * @return the artifact for the zipped up compilation results.
+ */
+ public Artifact compiledMomZipArtifact(PathFragment containerDir) {
+ return appendExtension(
+ "/" + FileSystemUtils.replaceExtension(containerDir, ".zip").getBaseName());
+ }
+
+ /**
+ * Returns the compiled (i.e. converted to binary plist format) artifact corresponding to the
+ * given {@code .strings} file.
+ */
+ public Artifact convertedStringsFile(Artifact originalFile) {
+ return appendExtension(originalFile.getExecPath(), ".binary");
+ }
+
+ /**
+ * Returns the artifact corresponding to the zipped-up compiled form of the given {@code .xib}
+ * file.
+ */
+ public Artifact compiledXibFileZip(Artifact originalFile) {
+ return analysisEnvironment.getDerivedArtifact(
+ FileSystemUtils.replaceExtension(originalFile.getExecPath(), ".nib.zip"),
+ binDirectory);
+ }
+
+ /**
+ * Debug symbol plist generated for a linked binary.
+ */
+ public Artifact dsymPlist() {
+ return appendExtension(".app.dSYM/Contents/Info.plist");
+ }
+
+ /**
+ * Debug symbol file generated for a linked binary.
+ */
+ public Artifact dsymSymbol() {
+ return appendExtension(
+ String.format(".app.dSYM/Contents/Resources/DWARF/%s_bin", ownerLabel.getName()));
+ }
+
+ /**
+ * Breakpad debug symbol representation.
+ */
+ public Artifact breakpadSym() {
+ return appendExtension(".breakpad");
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/IosApplicationRule.java b/src/main/java/com/google/devtools/build/lib/rules/objc/IosApplicationRule.java
new file mode 100644
index 0000000..b81d9b8
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/IosApplicationRule.java
@@ -0,0 +1,117 @@
+// Copyright 2015 Google Inc. 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.rules.objc;
+
+import static com.google.devtools.build.lib.packages.Attribute.ConfigurationTransition.HOST;
+import static com.google.devtools.build.lib.packages.Attribute.attr;
+import static com.google.devtools.build.lib.packages.Type.LABEL;
+import static com.google.devtools.build.lib.packages.Type.STRING;
+import static com.google.devtools.build.lib.packages.Type.STRING_LIST;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.analysis.BaseRuleClasses;
+import com.google.devtools.build.lib.analysis.BlazeRule;
+import com.google.devtools.build.lib.analysis.RuleDefinition;
+import com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment;
+import com.google.devtools.build.lib.packages.Attribute;
+import com.google.devtools.build.lib.packages.AttributeMap;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.RuleClass.Builder;
+import com.google.devtools.build.lib.packages.RuleClass.Builder.RuleClassType;
+import com.google.devtools.build.lib.util.FileType;
+import com.google.devtools.build.xcode.common.TargetDeviceFamily;
+
+/**
+ * Rule definition for ios_application.
+ */
+@BlazeRule(name = "$ios_application",
+ ancestors = { BaseRuleClasses.BaseRule.class,
+ ObjcRuleClasses.ObjcBaseResourcesRule.class,
+ ObjcRuleClasses.ObjcHasInfoplistRule.class,
+ ObjcRuleClasses.ObjcHasEntitlementsRule.class },
+ type = RuleClassType.ABSTRACT) // TODO(bazel-team): Add factory once this becomes a real rule.
+public class IosApplicationRule implements RuleDefinition {
+
+ @Override
+ public RuleClass build(Builder builder, RuleDefinitionEnvironment env) {
+ return builder
+ /* <!-- #BLAZE_RULE($ios_application).ATTRIBUTE(app_icon) -->
+ The name of the application icon, which should be in one of the asset
+ catalogs of this target or a (transitive) dependency. In a new project,
+ this is initialized to "AppIcon" by Xcode.
+ <p>
+ If the application icon is not in an asset catalog, do not use this
+ attribute. Instead, add a CFBundleIcons entry to the Info.plist file.
+ <!-- #END_BLAZE_RULE.ATTRIBUTE -->*/
+ .add(attr("app_icon", STRING))
+ /* <!-- #BLAZE_RULE($ios_application).ATTRIBUTE(launch_image) -->
+ The name of the launch image, which should be in one of the asset
+ catalogs of this target or a (transitive) dependency. In a new project,
+ this is initialized to "LaunchImage" by Xcode.
+ <p>
+ If the launch image is not in an asset catalog, do not use this
+ attribute. Instead, add an appropriately-named image resource to the
+ bundle.
+ <!-- #END_BLAZE_RULE.ATTRIBUTE -->*/
+ .add(attr("launch_image", STRING))
+ /* <!-- #BLAZE_RULE($ios_application).ATTRIBUTE(bundle_id) -->
+ The bundle ID (reverse-DNS path followed by app name) of the binary. If none is specified, a
+ junk value will be used.
+ <!-- #END_BLAZE_RULE.ATTRIBUTE -->*/
+ .add(attr("bundle_id", STRING)
+ .value(new Attribute.ComputedDefault() {
+ @Override
+ public Object getDefault(AttributeMap rule) {
+ // For tests and similar, we don't want to force people to explicitly specify
+ // throw-away data.
+ return "example." + rule.getName();
+ }
+ }))
+ /* <!-- #BLAZE_RULE($ios_application).ATTRIBUTE(families) -->
+ The device families to which this binary is targeted. This is known as
+ the <code>TARGETED_DEVICE_FAMILY</code> build setting in Xcode project
+ files. It is a list of one or more of the strings <code>"iphone"</code>
+ and <code>"ipad"</code>.
+ <!-- #END_BLAZE_RULE.ATTRIBUTE -->*/
+ .add(attr("families", STRING_LIST)
+ .value(ImmutableList.of(TargetDeviceFamily.IPHONE.getNameInRule())))
+ /* <!-- #BLAZE_RULE($ios_application).ATTRIBUTE(provisioning_profile) -->
+ The provisioning profile (.mobileprovision file) to use when bundling
+ the application.
+ <p>
+ This is only used for non-simulator builds.
+ <!-- #END_BLAZE_RULE.ATTRIBUTE -->*/
+ .add(attr("provisioning_profile", LABEL)
+ .value(env.getLabel("//tools/objc:default_provisioning_profile"))
+ .allowedFileTypes(FileType.of(".mobileprovision")))
+ // TODO(bazel-team): Consider ways to trim dependencies so that changes to deps of these
+ // tools don't trigger all objc_* targets. Right now we check-in deploy jars, but we
+ // need a less painful and error-prone way.
+ /* <!-- #BLAZE_RULE($ios_application).ATTRIBUTE(binary) -->
+ The binary target included in the final bundle.
+ ${SYNOPSIS}
+ <!-- #END_BLAZE_RULE.ATTRIBUTE -->*/
+ .add(attr("binary", LABEL)
+ .allowedRuleClasses("objc_binary")
+ .allowedFileTypes()
+ .mandatory()
+ .direct_compile_time_input())
+ .add(attr("$bundlemerge", LABEL).cfg(HOST).exec()
+ .value(env.getLabel("//tools/objc:bundlemerge")))
+ .add(attr("$dumpsyms", LABEL).cfg(HOST).exec()
+ .value(env.getLabel("//tools/objc:dump_syms")))
+ .build();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/IosDevice.java b/src/main/java/com/google/devtools/build/lib/rules/objc/IosDevice.java
new file mode 100644
index 0000000..2f01633
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/IosDevice.java
@@ -0,0 +1,42 @@
+// Copyright 2015 Google Inc. 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.rules.objc;
+
+import static com.google.devtools.build.lib.packages.Type.STRING;
+
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTargetBuilder;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.RunfilesProvider;
+import com.google.devtools.build.lib.rules.RuleConfiguredTargetFactory;
+
+/**
+ * Implementation for the "ios_device" rule.
+ */
+public final class IosDevice implements RuleConfiguredTargetFactory {
+ @Override
+ public ConfiguredTarget create(RuleContext context) throws InterruptedException {
+ IosDeviceProvider provider = new IosDeviceProvider.Builder()
+ .setType(context.attributes().get("type", STRING))
+ .setIosVersion(context.attributes().get("ios_version", STRING))
+ .setLocale(context.attributes().get("locale", STRING))
+ .build();
+
+ return new RuleConfiguredTargetBuilder(context)
+ .add(RunfilesProvider.class, RunfilesProvider.EMPTY)
+ .add(IosDeviceProvider.class, provider)
+ .build();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/IosDeviceProvider.java b/src/main/java/com/google/devtools/build/lib/rules/objc/IosDeviceProvider.java
new file mode 100644
index 0000000..6f01e11
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/IosDeviceProvider.java
@@ -0,0 +1,73 @@
+// Copyright 2015 Google Inc. 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.rules.objc;
+
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+
+/**
+ * Provider that describes a simulator device.
+ */
+@Immutable
+public final class IosDeviceProvider implements TransitiveInfoProvider {
+ /** A builder of {@link IosDeviceProvider}s. */
+ public static final class Builder {
+ private String type;
+ private String iosVersion;
+ private String locale;
+
+ public Builder setType(String type) {
+ this.type = type;
+ return this;
+ }
+
+ public Builder setIosVersion(String iosVersion) {
+ this.iosVersion = iosVersion;
+ return this;
+ }
+
+ public Builder setLocale(String locale) {
+ this.locale = locale;
+ return this;
+ }
+
+ public IosDeviceProvider build() {
+ return new IosDeviceProvider(this);
+ }
+ }
+
+ private final String type;
+ private final String iosVersion;
+ private final String locale;
+
+ private IosDeviceProvider(Builder builder) {
+ this.type = Preconditions.checkNotNull(builder.type);
+ this.iosVersion = Preconditions.checkNotNull(builder.iosVersion);
+ this.locale = Preconditions.checkNotNull(builder.locale);
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public String getIosVersion() {
+ return iosVersion;
+ }
+
+ public String getLocale() {
+ return locale;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/IosDeviceRule.java b/src/main/java/com/google/devtools/build/lib/rules/objc/IosDeviceRule.java
new file mode 100644
index 0000000..e028e70
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/IosDeviceRule.java
@@ -0,0 +1,67 @@
+// Copyright 2015 Google Inc. 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.rules.objc;
+
+import static com.google.devtools.build.lib.packages.Attribute.attr;
+import static com.google.devtools.build.lib.packages.Type.STRING;
+
+import com.google.devtools.build.lib.analysis.BaseRuleClasses;
+import com.google.devtools.build.lib.analysis.BlazeRule;
+import com.google.devtools.build.lib.analysis.RuleDefinition;
+import com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.RuleClass.Builder;
+
+/**
+ * Rule definition for ios_device.
+ */
+@BlazeRule(name = "ios_device",
+ factoryClass = IosDevice.class,
+ ancestors = { BaseRuleClasses.RuleBase.class })
+public final class IosDeviceRule implements RuleDefinition {
+ @Override
+ public RuleClass build(Builder builder, RuleDefinitionEnvironment env) {
+ return builder
+ /* <!-- #BLAZE_RULE(ios_device).ATTRIBUTE(ios_version) -->
+ The operating system version of the device. This corresponds to the
+ <code>simctl</code> runtime.
+ ${SYNOPSIS}
+ <!-- #END_BLAZE_RULE.ATTRIBUTE -->*/
+ .add(attr("ios_version", STRING)
+ .mandatory())
+ /* <!-- #BLAZE_RULE(ios_device).ATTRIBUTE(type) -->
+ The hardware type. This corresponds to the <code>simctl</code> device
+ type.
+ ${SYNOPSIS}
+ <!-- #END_BLAZE_RULE.ATTRIBUTE -->*/
+ .add(attr("type", STRING)
+ .mandatory())
+ .add(attr("locale", STRING)
+ .undocumented("this is not yet supported by any test runner")
+ .value("en"))
+ .build();
+ }
+}
+
+/*<!-- #BLAZE_RULE (NAME = ios_device, TYPE = BINARY, FAMILY = Objective-C) -->
+
+${ATTRIBUTE_SIGNATURE}
+
+<p>This rule defines an iOS device profile that defines a simulator against
+which to run tests</p>.
+
+${ATTRIBUTE_DEFINITION}
+
+<!-- #END_BLAZE_RULE -->*/
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/IosSdkCommands.java b/src/main/java/com/google/devtools/build/lib/rules/objc/IosSdkCommands.java
new file mode 100644
index 0000000..0a9fb7b
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/IosSdkCommands.java
@@ -0,0 +1,155 @@
+// Copyright 2014 Google Inc. 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.rules.objc;
+
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.FRAMEWORK_DIR;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.xcode.common.Platform;
+import com.google.devtools.build.xcode.util.Interspersing;
+import com.google.devtools.build.xcode.xcodegen.proto.XcodeGenProtos.XcodeprojBuildSetting;
+
+import java.util.List;
+
+/**
+ * Utility code for use when generating iOS SDK commands.
+ */
+public class IosSdkCommands {
+ public static final String DEVELOPER_DIR = "/Applications/Xcode.app/Contents/Developer";
+ public static final String BIN_DIR =
+ DEVELOPER_DIR + "/Toolchains/XcodeDefault.xctoolchain/usr/bin";
+ public static final String ACTOOL_PATH = DEVELOPER_DIR + "/usr/bin/actool";
+ public static final String IBTOOL_PATH = DEVELOPER_DIR + "/usr/bin/ibtool";
+ public static final String MOMC_PATH = DEVELOPER_DIR + "/usr/bin/momc";
+
+ // There is a handy reference to many clang warning flags at
+ // http://nshipster.com/clang-diagnostics/
+ // There is also a useful narrative for many Xcode settings at
+ // http://www.xs-labs.com/en/blog/2011/02/04/xcode-build-settings/
+ @VisibleForTesting
+ static final ImmutableMap<String, String> DEFAULT_WARNINGS =
+ new ImmutableMap.Builder<String, String>()
+ .put("GCC_WARN_64_TO_32_BIT_CONVERSION", "-Wshorten-64-to-32")
+ .put("CLANG_WARN_BOOL_CONVERSION", "-Wbool-conversion")
+ .put("CLANG_WARN_CONSTANT_CONVERSION", "-Wconstant-conversion")
+ // Double-underscores are intentional - thanks Xcode.
+ .put("CLANG_WARN__DUPLICATE_METHOD_MATCH", "-Wduplicate-method-match")
+ .put("CLANG_WARN_EMPTY_BODY", "-Wempty-body")
+ .put("CLANG_WARN_ENUM_CONVERSION", "-Wenum-conversion")
+ .put("CLANG_WARN_INT_CONVERSION", "-Wint-conversion")
+ .put("CLANG_WARN_UNREACHABLE_CODE", "-Wunreachable-code")
+ .put("GCC_WARN_ABOUT_RETURN_TYPE", "-Wmismatched-return-types")
+ .put("GCC_WARN_UNDECLARED_SELECTOR", "-Wundeclared-selector")
+ .put("GCC_WARN_UNINITIALIZED_AUTOS", "-Wuninitialized")
+ .put("GCC_WARN_UNUSED_FUNCTION", "-Wunused-function")
+ .put("GCC_WARN_UNUSED_VARIABLE", "-Wunused-variable")
+ .build();
+
+ static final ImmutableList<String> DEFAULT_LINKER_FLAGS = ImmutableList.of("-ObjC");
+
+ private IosSdkCommands() {
+ throw new UnsupportedOperationException("static-only");
+ }
+
+ private static String platformDir(ObjcConfiguration configuration) {
+ return DEVELOPER_DIR + "/Platforms/" + configuration.getPlatform().getNameInPlist()
+ + ".platform";
+ }
+
+ public static String sdkDir(ObjcConfiguration configuration) {
+ return platformDir(configuration) + "/Developer/SDKs/"
+ + configuration.getPlatform().getNameInPlist() + configuration.getIosSdkVersion() + ".sdk";
+ }
+
+ public static String frameworkDir(ObjcConfiguration configuration) {
+ return platformDir(configuration) + "/Developer/Library/Frameworks";
+ }
+
+ private static Iterable<PathFragment> uniqueParentDirectories(Iterable<PathFragment> paths) {
+ ImmutableSet.Builder<PathFragment> parents = new ImmutableSet.Builder<>();
+ for (PathFragment path : paths) {
+ parents.add(path.getParentDirectory());
+ }
+ return parents.build();
+ }
+
+ public static List<String> commonLinkAndCompileArgsForClang(
+ ObjcProvider provider, ObjcConfiguration configuration) {
+ ImmutableList.Builder<String> builder = new ImmutableList.Builder<>();
+ if (configuration.getPlatform() == Platform.SIMULATOR) {
+ builder.add("-mios-simulator-version-min=" + configuration.getMinimumOs());
+ } else {
+ builder.add("-miphoneos-version-min=" + configuration.getMinimumOs());
+ }
+
+ if (configuration.generateDebugSymbols()) {
+ builder.add("-g");
+ }
+
+ return builder
+ .add("-arch", configuration.getIosCpu())
+ .add("-isysroot", sdkDir(configuration))
+ // TODO(bazel-team): Pass framework search paths to Xcodegen.
+ .add("-F", sdkDir(configuration) + "/Developer/Library/Frameworks")
+ // As of sdk8.1, XCTest is in a base Framework dir
+ .add("-F", frameworkDir(configuration))
+ // Add custom (non-SDK) framework search paths. For each framework foo/bar.framework,
+ // include "foo" as a search path.
+ .addAll(Interspersing.beforeEach(
+ "-F",
+ PathFragment.safePathStrings(uniqueParentDirectories(provider.get(FRAMEWORK_DIR)))))
+ .build();
+ }
+
+ public static Iterable<String> compileArgsForClang(ObjcConfiguration configuration) {
+ return Iterables.concat(
+ DEFAULT_WARNINGS.values(),
+ platformSpecificCompileArgsForClang(configuration)
+ );
+ }
+
+ private static List<String> platformSpecificCompileArgsForClang(ObjcConfiguration configuration) {
+ switch (configuration.getPlatform()) {
+ case DEVICE:
+ return ImmutableList.of();
+ case SIMULATOR:
+ // These are added by Xcode when building, because the simulator is built on OSX
+ // frameworks so we aim compile to match the OSX objc runtime.
+ return ImmutableList.of(
+ "-fexceptions",
+ "-fasm-blocks",
+ "-fobjc-abi-version=2",
+ "-fobjc-legacy-dispatch");
+ default:
+ throw new AssertionError();
+ }
+ }
+
+ public static Iterable<? extends XcodeprojBuildSetting> defaultWarningsForXcode() {
+ return Iterables.transform(DEFAULT_WARNINGS.keySet(),
+ new Function<String, XcodeprojBuildSetting>() {
+ @Override
+ public XcodeprojBuildSetting apply(String key) {
+ return XcodeprojBuildSetting.newBuilder().setName(key).setValue("YES").build();
+ }
+ });
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/IosTest.java b/src/main/java/com/google/devtools/build/lib/rules/objc/IosTest.java
new file mode 100644
index 0000000..9e9ddc4
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/IosTest.java
@@ -0,0 +1,163 @@
+// Copyright 2014 Google Inc. 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.rules.objc;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.rules.RuleConfiguredTargetFactory;
+import com.google.devtools.build.lib.rules.objc.ApplicationSupport.LinkedBinary;
+import com.google.devtools.build.lib.rules.objc.ObjcActionsBuilder.ExtraLinkArgs;
+import com.google.devtools.build.lib.rules.objc.ObjcActionsBuilder.ExtraLinkInputs;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Contains information needed to create a {@link RuleConfiguredTarget} and invoke test runners
+ * for some instantiation of this rule.
+ */
+// TODO(bazel-team): Extract a TestSupport class that takes on most of the logic in this class.
+public abstract class IosTest implements RuleConfiguredTargetFactory {
+ private static final ImmutableList<SdkFramework> AUTOMATIC_SDK_FRAMEWORKS_FOR_XCTEST =
+ ImmutableList.of(new SdkFramework("XCTest"));
+
+ public static final String TARGET_DEVICE = "target_device";
+ public static final String IS_XCTEST = "xctest";
+ public static final String XCTEST_APP = "xctest_app";
+
+ @VisibleForTesting
+ public static final String REQUIRES_SOURCE_ERROR =
+ "ios_test requires at least one source file in srcs or non_arc_srcs";
+
+ /**
+ * Creates a target, including registering actions, just as {@link #create(RuleContext)} does.
+ * The difference between {@link #create(RuleContext)} and this method is that this method does
+ * only what is needed to support tests on the environment besides generate the Xcodeproj file
+ * and build the app and test {@code .ipa}s. The {@link #create(RuleContext)} method delegates
+ * to this method.
+ */
+ protected abstract ConfiguredTarget create(RuleContext ruleContext, ObjcCommon common,
+ XcodeProvider xcodeProvider, NestedSet<Artifact> filesToBuild) throws InterruptedException;
+
+ @Override
+ public final ConfiguredTarget create(RuleContext ruleContext) throws InterruptedException {
+ ObjcCommon common = common(ruleContext);
+ OptionsProvider optionsProvider = optionsProvider(ruleContext);
+
+ if (!common.getCompilationArtifacts().get().getArchive().isPresent()) {
+ ruleContext.ruleError(REQUIRES_SOURCE_ERROR);
+ }
+
+ XcodeProvider.Builder xcodeProviderBuilder = new XcodeProvider.Builder();
+ NestedSetBuilder<Artifact> filesToBuild = NestedSetBuilder.<Artifact>stableOrder()
+ .addTransitive(common.getStoryboards().getOutputZips())
+ .addAll(Xcdatamodel.outputZips(common.getDatamodels()));
+
+ XcodeProductType productType;
+ ExtraLinkArgs extraLinkArgs;
+ ExtraLinkInputs extraLinkInputs;
+ if (!isXcTest(ruleContext)) {
+ productType = XcodeProductType.APPLICATION;
+ extraLinkArgs = new ExtraLinkArgs();
+ extraLinkInputs = new ExtraLinkInputs();
+ } else {
+ productType = XcodeProductType.UNIT_TEST;
+ XcodeProvider appIpaXcodeProvider =
+ ruleContext.getPrerequisite(XCTEST_APP, Mode.TARGET, XcodeProvider.class);
+ xcodeProviderBuilder
+ .setTestHost(appIpaXcodeProvider)
+ .setProductType(productType);
+
+ Artifact bundleLoader = xcTestAppProvider(ruleContext).getBundleLoader();
+
+ // -bundle causes this binary to be linked as a bundle and not require an entry point
+ // (i.e. main())
+ // -bundle_loader causes the code in this test to have access to the symbols in the test rig,
+ // or more specifically, the flag causes ld to consider the given binary when checking for
+ // missing symbols.
+ extraLinkArgs = new ExtraLinkArgs(
+ "-bundle",
+ "-bundle_loader", bundleLoader.getExecPathString());
+ extraLinkInputs = new ExtraLinkInputs(bundleLoader);
+ }
+
+ new CompilationSupport(ruleContext)
+ .registerLinkActions(
+ common.getObjcProvider(), extraLinkArgs, extraLinkInputs)
+ .registerJ2ObjcCompileAndArchiveActions(optionsProvider, common.getObjcProvider())
+ .registerCompileAndArchiveActions(common, optionsProvider)
+ .addXcodeSettings(xcodeProviderBuilder, common, optionsProvider)
+ .validateAttributes();
+
+ new ApplicationSupport(
+ ruleContext, common.getObjcProvider(), optionsProvider, LinkedBinary.LOCAL_AND_DEPENDENCIES)
+ .registerActions()
+ .addXcodeSettings(xcodeProviderBuilder)
+ .addFilesToBuild(filesToBuild)
+ .validateAttributes();
+
+ new ResourceSupport(ruleContext)
+ .registerActions(common.getStoryboards())
+ .validateAttributes()
+ .addXcodeSettings(xcodeProviderBuilder);
+
+ new XcodeSupport(ruleContext)
+ .addXcodeSettings(xcodeProviderBuilder, common.getObjcProvider(), productType)
+ .addDependencies(xcodeProviderBuilder)
+ .addFilesToBuild(filesToBuild)
+ .registerActions(xcodeProviderBuilder.build());
+
+ return create(ruleContext, common, xcodeProviderBuilder.build(), filesToBuild.build());
+ }
+
+ protected static boolean isXcTest(RuleContext ruleContext) {
+ return ruleContext.attributes().get(IS_XCTEST, Type.BOOLEAN);
+ }
+
+ private OptionsProvider optionsProvider(RuleContext ruleContext) {
+ return new OptionsProvider.Builder()
+ .addCopts(ruleContext.getTokenizedStringListAttr("copts"))
+ .addInfoplists(ruleContext.getPrerequisiteArtifacts("infoplist", Mode.TARGET).list())
+ .addTransitive(Optional.fromNullable(
+ ruleContext.getPrerequisite("options", Mode.TARGET, OptionsProvider.class)))
+ .build();
+ }
+
+ /** Returns the {@link XcTestAppProvider} of the {@code xctest_app} attribute. */
+ private static XcTestAppProvider xcTestAppProvider(RuleContext ruleContext) {
+ return ruleContext.getPrerequisite(XCTEST_APP, Mode.TARGET, XcTestAppProvider.class);
+ }
+
+ private ObjcCommon common(RuleContext ruleContext) {
+ ImmutableList<SdkFramework> extraSdkFrameworks = isXcTest(ruleContext)
+ ? AUTOMATIC_SDK_FRAMEWORKS_FOR_XCTEST : ImmutableList.<SdkFramework>of();
+ List<ObjcProvider> extraDepObjcProviders = new ArrayList<>();
+ if (isXcTest(ruleContext)) {
+ extraDepObjcProviders.add(xcTestAppProvider(ruleContext).getObjcProvider());
+ }
+
+ return ObjcLibrary.common(ruleContext, extraSdkFrameworks, /*alwayslink=*/false,
+ new ObjcLibrary.ExtraImportLibraries(), new ObjcLibrary.Defines(), extraDepObjcProviders);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/IterableWrapper.java b/src/main/java/com/google/devtools/build/lib/rules/objc/IterableWrapper.java
new file mode 100644
index 0000000..4bd7b0c
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/IterableWrapper.java
@@ -0,0 +1,40 @@
+// Copyright 2014 Google Inc. 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.rules.objc;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+
+import java.util.Iterator;
+
+/**
+ * Base class for tiny container types that encapsulate an iterable.
+ */
+abstract class IterableWrapper<E> implements Iterable<E> {
+ private final Iterable<E> contents;
+
+ IterableWrapper(Iterable<E> contents) {
+ this.contents = Preconditions.checkNotNull(contents);
+ }
+
+ IterableWrapper(E... contents) {
+ this.contents = ImmutableList.copyOf(contents);
+ }
+
+ @Override
+ public Iterator<E> iterator() {
+ return contents.iterator();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/J2ObjcHeaderMappingFileProvider.java b/src/main/java/com/google/devtools/build/lib/rules/objc/J2ObjcHeaderMappingFileProvider.java
new file mode 100644
index 0000000..93ff980
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/J2ObjcHeaderMappingFileProvider.java
@@ -0,0 +1,38 @@
+// Copyright 2014 Google Inc. 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.rules.objc;
+
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+
+/**
+ * This provider is exported by java_library rules to supply ObjC header to Java type mapping files
+ * for J2ObjC translation. J2ObjC needs the mapping files to be able to output translated files with
+ * correct header import paths in the same directories of the Java source files.
+ */
+@Immutable
+public final class J2ObjcHeaderMappingFileProvider implements TransitiveInfoProvider {
+ private final NestedSet<Artifact> mappingFiles;
+
+ public J2ObjcHeaderMappingFileProvider(NestedSet<Artifact> mappingFiles) {
+ this.mappingFiles = mappingFiles;
+ }
+
+ public NestedSet<Artifact> getMappingFiles() {
+ return mappingFiles;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/J2ObjcSource.java b/src/main/java/com/google/devtools/build/lib/rules/objc/J2ObjcSource.java
new file mode 100644
index 0000000..81ba292
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/J2ObjcSource.java
@@ -0,0 +1,124 @@
+// Copyright 2014 Google Inc. 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.rules.objc;
+
+import com.google.common.base.Objects;
+import com.google.common.collect.Iterators;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+/**
+ * An object that captures information of ObjC files generated by J2ObjC in a single target.
+ */
+public class J2ObjcSource {
+
+ /**
+ * Indicates the type of files from which the ObjC files included in {@link J2ObjcSource} are
+ * generated.
+ */
+ public enum SourceType {
+ /**
+ * Indicates the original file type is java source file.
+ */
+ JAVA,
+
+ /**
+ * Indicates the original file type is proto file.
+ */
+ PROTO;
+ }
+
+ private final Label targetLabel;
+ private final Iterable<Artifact> objcSrcs;
+ private final Iterable<Artifact> objcHdrs;
+ private final PathFragment objcFilePath;
+ private final SourceType sourceType;
+
+ /**
+ * Constructs a J2ObjcSource containing target information for j2objc transpilation.
+ *
+ * @param targetLabel the @{code Label} of the associated target.
+ * @param objcSrcs the {@code Iterable} containing objc source files generated by J2ObjC
+ * @param objcHdrs the {@code Iterable} containing objc header files generated by J2ObjC
+ * @param objcFilePath the {@code PathFragment} under which all the generated objc files are. It
+ * can be used as header search path for objc compilations.
+ * @param sourceType the type of files from which the ObjC files are generated.
+ */
+ public J2ObjcSource(Label targetLabel, Iterable<Artifact> objcSrcs,
+ Iterable<Artifact> objcHdrs, PathFragment objcFilePath, SourceType sourceType) {
+ this.targetLabel = targetLabel;
+ this.objcSrcs = objcSrcs;
+ this.objcHdrs = objcHdrs;
+ this.objcFilePath = objcFilePath;
+ this.sourceType = sourceType;
+ }
+
+ /**
+ * Returns the label of the associated target.
+ */
+ public Label getTargetLabel() {
+ return targetLabel;
+ }
+
+ /**
+ * Returns the objc source files generated by J2ObjC.
+ */
+ public Iterable<Artifact> getObjcSrcs() {
+ return objcSrcs;
+ }
+
+ /*
+ * Returns the objc header files generated by J2ObjC
+ */
+ public Iterable<Artifact> getObjcHdrs() {
+ return objcHdrs;
+ }
+
+ /**
+ * Returns the {@code PathFragment} which represents a directory where the generated ObjC files
+ * reside and which can also be used as header search path in ObjC compilation.
+ */
+ public PathFragment getObjcFilePath() {
+ return objcFilePath;
+ }
+
+ /**
+ * Returns the type of files from which the ObjC files inside this object are generated.
+ */
+ public SourceType getSourceType() {
+ return sourceType;
+ }
+
+ @Override
+ public final boolean equals(Object other) {
+ if (!(other instanceof J2ObjcSource)) {
+ return false;
+ }
+
+ J2ObjcSource that = (J2ObjcSource) other;
+ return Objects.equal(this.targetLabel, that.targetLabel)
+ && Iterators.elementsEqual(this.objcSrcs.iterator(), that.objcSrcs.iterator())
+ && Iterators.elementsEqual(this.objcHdrs.iterator(), that.objcHdrs.iterator())
+ && Objects.equal(this.objcFilePath, that.objcFilePath)
+ && this.sourceType == that.sourceType;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(targetLabel, objcSrcs, objcHdrs, objcFilePath, sourceType);
+ }
+}
+
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/J2ObjcSrcsProvider.java b/src/main/java/com/google/devtools/build/lib/rules/objc/J2ObjcSrcsProvider.java
new file mode 100644
index 0000000..1e577c5
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/J2ObjcSrcsProvider.java
@@ -0,0 +1,45 @@
+// Copyright 2014 Google Inc. 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.rules.objc;
+
+import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+
+/**
+ * This provider is exported by java_library rules to supply J2ObjC-translated ObjC sources to
+ * objc_binary for compilation and linking.
+ */
+@Immutable
+public final class J2ObjcSrcsProvider implements TransitiveInfoProvider {
+ private final NestedSet<J2ObjcSource> srcs;
+ private final boolean hasProtos;
+
+ public J2ObjcSrcsProvider(NestedSet<J2ObjcSource> srcs, boolean hasProtos) {
+ this.srcs = srcs;
+ this.hasProtos = hasProtos;
+ }
+
+ public NestedSet<J2ObjcSource> getSrcs() {
+ return srcs;
+ }
+
+ /**
+ * Returns whether the translated source files in the provider has proto files.
+ */
+ public boolean hasProtos() {
+ return hasProtos;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcActionsBuilder.java b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcActionsBuilder.java
new file mode 100644
index 0000000..26742d9
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcActionsBuilder.java
@@ -0,0 +1,599 @@
+// Copyright 2014 Google Inc. 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.rules.objc;
+
+import static com.google.devtools.build.lib.rules.objc.IosSdkCommands.BIN_DIR;
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.ASSET_CATALOG;
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.DEFINE;
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.FORCE_LOAD_LIBRARY;
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.FRAMEWORK_DIR;
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.FRAMEWORK_FILE;
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.Flag.USES_CPP;
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.HEADER;
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.IMPORTED_LIBRARY;
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.INCLUDE;
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.LIBRARY;
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.SDK_DYLIB;
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.SDK_FRAMEWORK;
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.WEAK_SDK_FRAMEWORK;
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.XCASSETS_DIR;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Joiner;
+import com.google.common.base.Optional;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.io.ByteSource;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.actions.ActionRegistry;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.actions.ActionConstructionContext;
+import com.google.devtools.build.lib.analysis.actions.BinaryFileWriteAction;
+import com.google.devtools.build.lib.analysis.actions.CommandLine;
+import com.google.devtools.build.lib.analysis.actions.CustomCommandLine;
+import com.google.devtools.build.lib.analysis.actions.FileWriteAction;
+import com.google.devtools.build.lib.analysis.actions.SpawnAction;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.util.LazyString;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.xcode.common.TargetDeviceFamily;
+import com.google.devtools.build.xcode.util.Interspersing;
+import com.google.devtools.build.xcode.xcodegen.proto.XcodeGenProtos;
+
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+
+/**
+ * Object that creates actions used by Objective-C rules.
+ */
+final class ObjcActionsBuilder {
+ private final ActionConstructionContext context;
+ private final IntermediateArtifacts intermediateArtifacts;
+ private final ObjcConfiguration objcConfiguration;
+ private final BuildConfiguration buildConfiguration;
+ private final ActionRegistry actionRegistry;
+
+ ObjcActionsBuilder(ActionConstructionContext context, IntermediateArtifacts intermediateArtifacts,
+ ObjcConfiguration objcConfiguration, BuildConfiguration buildConfiguration,
+ ActionRegistry actionRegistry) {
+ this.context = Preconditions.checkNotNull(context);
+ this.intermediateArtifacts = Preconditions.checkNotNull(intermediateArtifacts);
+ this.objcConfiguration = Preconditions.checkNotNull(objcConfiguration);
+ this.buildConfiguration = Preconditions.checkNotNull(buildConfiguration);
+ this.actionRegistry = Preconditions.checkNotNull(actionRegistry);
+ }
+
+ /**
+ * Creates a new spawn action builder that requires a darwin architecture to run.
+ */
+ // TODO(bazel-team): Use everywhere we currently set the execution info manually.
+ static SpawnAction.Builder spawnOnDarwinActionBuilder() {
+ return new SpawnAction.Builder()
+ .setExecutionInfo(ImmutableMap.of(ExecutionRequirements.REQUIRES_DARWIN, ""));
+ }
+
+ static final PathFragment JAVA = new PathFragment("/usr/bin/java");
+ static final PathFragment CLANG = new PathFragment(BIN_DIR + "/clang");
+ static final PathFragment CLANG_PLUSPLUS = new PathFragment(BIN_DIR + "/clang++");
+ static final PathFragment LIBTOOL = new PathFragment(BIN_DIR + "/libtool");
+ static final PathFragment IBTOOL = new PathFragment(IosSdkCommands.IBTOOL_PATH);
+ static final PathFragment DSYMUTIL = new PathFragment(BIN_DIR + "/dsymutil");
+ static final PathFragment LIPO = new PathFragment(BIN_DIR + "/lipo");
+
+ // TODO(bazel-team): Reference a rule target rather than a jar file when Darwin runfiles work
+ // better.
+ private static SpawnAction.Builder spawnJavaOnDarwinActionBuilder(Artifact deployJarArtifact) {
+ return spawnOnDarwinActionBuilder()
+ .setExecutable(JAVA)
+ .addExecutableArguments("-jar", deployJarArtifact.getExecPathString())
+ .addInput(deployJarArtifact);
+ }
+
+ private void registerCompileAction(
+ Artifact sourceFile,
+ Artifact objFile,
+ Optional<Artifact> pchFile,
+ ObjcProvider objcProvider,
+ Iterable<String> otherFlags,
+ OptionsProvider optionsProvider) {
+ CustomCommandLine.Builder commandLine = new CustomCommandLine.Builder();
+ if (ObjcRuleClasses.CPP_SOURCES.matches(sourceFile.getExecPath())) {
+ commandLine.add("-stdlib=libc++");
+ }
+ commandLine
+ .add(IosSdkCommands.compileArgsForClang(objcConfiguration))
+ .add(IosSdkCommands.commonLinkAndCompileArgsForClang(
+ objcProvider, objcConfiguration))
+ .add(objcConfiguration.getCoptsForCompilationMode())
+ .addBeforeEachPath("-iquote", ObjcCommon.userHeaderSearchPaths(buildConfiguration))
+ .addBeforeEachExecPath("-include", pchFile.asSet())
+ .addBeforeEachPath("-I", objcProvider.get(INCLUDE))
+ .add(otherFlags)
+ .addFormatEach("-D%s", objcProvider.get(DEFINE))
+ .add(objcConfiguration.getCopts())
+ .add(optionsProvider.getCopts())
+ .addExecPath("-c", sourceFile)
+ .addExecPath("-o", objFile);
+
+ register(spawnOnDarwinActionBuilder()
+ .setMnemonic("ObjcCompile")
+ .setExecutable(CLANG)
+ .setCommandLine(commandLine.build())
+ .addInput(sourceFile)
+ .addOutput(objFile)
+ .addTransitiveInputs(objcProvider.get(HEADER))
+ .addTransitiveInputs(objcProvider.get(FRAMEWORK_FILE))
+ .addInputs(pchFile.asSet())
+ .build(context));
+ }
+
+ private static final ImmutableList<String> ARC_ARGS = ImmutableList.of("-fobjc-arc");
+ private static final ImmutableList<String> NON_ARC_ARGS = ImmutableList.of("-fno-objc-arc");
+
+ /**
+ * Creates actions to compile each source file individually, and link all the compiled object
+ * files into a single archive library.
+ */
+ void registerCompileAndArchiveActions(CompilationArtifacts compilationArtifacts,
+ ObjcProvider objcProvider, OptionsProvider optionsProvider) {
+ ImmutableList.Builder<Artifact> objFiles = new ImmutableList.Builder<>();
+ for (Artifact sourceFile : compilationArtifacts.getSrcs()) {
+ Artifact objFile = intermediateArtifacts.objFile(sourceFile);
+ objFiles.add(objFile);
+ registerCompileAction(sourceFile, objFile, compilationArtifacts.getPchFile(),
+ objcProvider, ARC_ARGS, optionsProvider);
+ }
+ for (Artifact nonArcSourceFile : compilationArtifacts.getNonArcSrcs()) {
+ Artifact objFile = intermediateArtifacts.objFile(nonArcSourceFile);
+ objFiles.add(objFile);
+ registerCompileAction(nonArcSourceFile, objFile, compilationArtifacts.getPchFile(),
+ objcProvider, NON_ARC_ARGS, optionsProvider);
+ }
+ for (Artifact archive : compilationArtifacts.getArchive().asSet()) {
+ registerAll(archiveActions(context, objFiles.build(), archive, objcConfiguration,
+ intermediateArtifacts.objList()));
+ }
+ }
+
+ private static Iterable<Action> archiveActions(
+ ActionConstructionContext context,
+ final Iterable<Artifact> objFiles,
+ final Artifact archive,
+ final ObjcConfiguration objcConfiguration,
+ final Artifact objList) {
+
+ ImmutableList.Builder<Action> actions = new ImmutableList.Builder<>();
+
+ actions.add(new FileWriteAction(
+ context.getActionOwner(), objList, joinExecPaths(objFiles), /*makeExecutable=*/ false));
+
+ actions.add(spawnOnDarwinActionBuilder()
+ .setMnemonic("ObjcLink")
+ .setExecutable(LIBTOOL)
+ .setCommandLine(new CommandLine() {
+ @Override
+ public Iterable<String> arguments() {
+ return new ImmutableList.Builder<String>()
+ .add("-static")
+ .add("-filelist").add(objList.getExecPathString())
+ .add("-arch_only").add(objcConfiguration.getIosCpu())
+ .add("-syslibroot").add(IosSdkCommands.sdkDir(objcConfiguration))
+ .add("-o").add(archive.getExecPathString())
+ .build();
+ }
+ })
+ .addInputs(objFiles)
+ .addInput(objList)
+ .addOutput(archive)
+ .build(context));
+
+ return actions.build();
+ }
+
+ private void register(Action... action) {
+ actionRegistry.registerAction(action);
+ }
+
+ private void registerAll(Iterable<? extends Action> actions) {
+ for (Action action : actions) {
+ actionRegistry.registerAction(action);
+ }
+ }
+
+ private static ByteSource xcodegenControlFileBytes(
+ final Artifact pbxproj, final XcodeProvider.Project project, final String minimumOs) {
+ return new ByteSource() {
+ @Override
+ public InputStream openStream() {
+ return XcodeGenProtos.Control.newBuilder()
+ .setPbxproj(pbxproj.getExecPathString())
+ .addAllTarget(project.targets())
+ .addBuildSetting(XcodeGenProtos.XcodeprojBuildSetting.newBuilder()
+ .setName("IPHONEOS_DEPLOYMENT_TARGET")
+ .setValue(minimumOs)
+ .build())
+ .build()
+ .toByteString()
+ .newInput();
+ }
+ };
+ }
+
+ /**
+ * Generates actions needed to create an Xcode project file.
+ */
+ void registerXcodegenActions(
+ ObjcRuleClasses.Tools baseTools, Artifact pbxproj, XcodeProvider.Project project) {
+ Artifact controlFile = intermediateArtifacts.pbxprojControlArtifact();
+ register(new BinaryFileWriteAction(
+ context.getActionOwner(),
+ controlFile,
+ xcodegenControlFileBytes(pbxproj, project, objcConfiguration.getMinimumOs()),
+ /*makeExecutable=*/false));
+ register(new SpawnAction.Builder()
+ .setMnemonic("GenerateXcodeproj")
+ .setExecutable(baseTools.xcodegen())
+ .addArgument("--control")
+ .addInputArgument(controlFile)
+ .addOutput(pbxproj)
+ .addTransitiveInputs(project.getInputsToXcodegen())
+ .build(context));
+ }
+
+ /**
+ * Creates actions to convert all files specified by the strings attribute into binary format.
+ */
+ private static Iterable<Action> convertStringsActions(
+ ActionConstructionContext context,
+ ObjcRuleClasses.Tools baseTools,
+ StringsFiles stringsFiles) {
+ ImmutableList.Builder<Action> result = new ImmutableList.Builder<>();
+ for (CompiledResourceFile stringsFile : stringsFiles) {
+ final Artifact original = stringsFile.getOriginal();
+ final Artifact bundled = stringsFile.getBundled().getBundled();
+ result.add(new SpawnAction.Builder()
+ .setMnemonic("ConvertStringsPlist")
+ .setExecutable(baseTools.plmerge())
+ .setCommandLine(new CommandLine() {
+ @Override
+ public Iterable<String> arguments() {
+ return ImmutableList.of("--source_file", original.getExecPathString(),
+ "--out_file", bundled.getExecPathString());
+ }
+ })
+ .addInput(original)
+ .addOutput(bundled)
+ .build(context));
+ }
+ return result.build();
+ }
+
+ private Action[] ibtoolzipAction(ObjcRuleClasses.Tools baseTools, String mnemonic, Artifact input,
+ Artifact zipOutput, String archiveRoot) {
+ return spawnJavaOnDarwinActionBuilder(baseTools.actooloribtoolzipDeployJar())
+ .setMnemonic(mnemonic)
+ .setCommandLine(new CustomCommandLine.Builder()
+ // The next three arguments are positional, i.e. they don't have flags before them.
+ .addPath(zipOutput.getExecPath())
+ .add(archiveRoot)
+ .addPath(IBTOOL)
+
+ .add("--minimum-deployment-target").add(objcConfiguration.getMinimumOs())
+ .addPath(input.getExecPath())
+ .build())
+ .addOutput(zipOutput)
+ .addInput(input)
+ .build(context);
+ }
+
+ /**
+ * Creates actions to convert all files specified by the xibs attribute into nib format.
+ */
+ private Iterable<Action> convertXibsActions(ObjcRuleClasses.Tools baseTools, XibFiles xibFiles) {
+ ImmutableList.Builder<Action> result = new ImmutableList.Builder<>();
+ for (Artifact original : xibFiles) {
+ Artifact zipOutput = intermediateArtifacts.compiledXibFileZip(original);
+ String archiveRoot = BundleableFile.bundlePath(
+ FileSystemUtils.replaceExtension(original.getExecPath(), ".nib"));
+ result.add(ibtoolzipAction(baseTools, "XibCompile", original, zipOutput, archiveRoot));
+ }
+ return result.build();
+ }
+
+ /**
+ * Outputs of an {@code actool} action besides the zip file.
+ */
+ static final class ExtraActoolOutputs extends IterableWrapper<Artifact> {
+ ExtraActoolOutputs(Artifact... extraActoolOutputs) {
+ super(extraActoolOutputs);
+ }
+ }
+
+ static final class ExtraActoolArgs extends IterableWrapper<String> {
+ ExtraActoolArgs(Iterable<String> args) {
+ super(args);
+ }
+
+ ExtraActoolArgs(String... args) {
+ super(args);
+ }
+ }
+
+ void registerActoolzipAction(
+ ObjcRuleClasses.Tools tools,
+ ObjcProvider provider,
+ Artifact zipOutput,
+ ExtraActoolOutputs extraActoolOutputs,
+ ExtraActoolArgs extraActoolArgs,
+ Set<TargetDeviceFamily> families) {
+ // TODO(bazel-team): Do not use the deploy jar explicitly here. There is currently a bug where
+ // we cannot .setExecutable({java_binary target}) and set REQUIRES_DARWIN in the execution info.
+ // Note that below we set the archive root to the empty string. This means that the generated
+ // zip file will be rooted at the bundle root, and we have to prepend the bundle root to each
+ // entry when merging it with the final .ipa file.
+ register(spawnJavaOnDarwinActionBuilder(tools.actooloribtoolzipDeployJar())
+ .setMnemonic("AssetCatalogCompile")
+ .addTransitiveInputs(provider.get(ASSET_CATALOG))
+ .addOutput(zipOutput)
+ .addOutputs(extraActoolOutputs)
+ .setCommandLine(actoolzipCommandLine(
+ objcConfiguration,
+ provider,
+ zipOutput,
+ extraActoolArgs,
+ ImmutableSet.copyOf(families)))
+ .build(context));
+ }
+
+ private static CommandLine actoolzipCommandLine(
+ final ObjcConfiguration objcConfiguration,
+ final ObjcProvider provider,
+ final Artifact zipOutput,
+ final ExtraActoolArgs extraActoolArgs,
+ final ImmutableSet<TargetDeviceFamily> families) {
+ return new CommandLine() {
+ @Override
+ public Iterable<String> arguments() {
+ ImmutableList.Builder<String> args = new ImmutableList.Builder<String>()
+ // The next three arguments are positional, i.e. they don't have flags before them.
+ .add(zipOutput.getExecPathString())
+ .add("") // archive root
+ .add(IosSdkCommands.ACTOOL_PATH)
+ .add("--platform")
+ .add(objcConfiguration.getPlatform().getLowerCaseNameInPlist())
+ .add("--minimum-deployment-target").add(objcConfiguration.getMinimumOs());
+ for (TargetDeviceFamily targetDeviceFamily : families) {
+ args.add("--target-device").add(targetDeviceFamily.name().toLowerCase(Locale.US));
+ }
+ return args
+ .addAll(PathFragment.safePathStrings(provider.get(XCASSETS_DIR)))
+ .addAll(extraActoolArgs)
+ .build();
+ }
+ };
+ }
+
+ void registerIbtoolzipAction(ObjcRuleClasses.Tools tools, Artifact input, Artifact outputZip) {
+ String archiveRoot = BundleableFile.bundlePath(input.getExecPath()) + "c";
+ register(ibtoolzipAction(tools, "StoryboardCompile", input, outputZip, archiveRoot));
+ }
+
+ @VisibleForTesting
+ static Iterable<String> commonMomczipArguments(ObjcConfiguration configuration) {
+ return ImmutableList.of(
+ "-XD_MOMC_SDKROOT=" + IosSdkCommands.sdkDir(configuration),
+ "-XD_MOMC_IOS_TARGET_VERSION=" + configuration.getMinimumOs(),
+ "-MOMC_PLATFORMS", configuration.getPlatform().getLowerCaseNameInPlist(),
+ "-XD_MOMC_TARGET_VERSION=10.6");
+ }
+
+ private static Iterable<Action> momczipActions(ActionConstructionContext context,
+ ObjcRuleClasses.Tools baseTools, final ObjcConfiguration objcConfiguration,
+ Iterable<Xcdatamodel> datamodels) {
+ ImmutableList.Builder<Action> result = new ImmutableList.Builder<>();
+ for (Xcdatamodel datamodel : datamodels) {
+ final Artifact outputZip = datamodel.getOutputZip();
+ final String archiveRoot = datamodel.archiveRootForMomczip();
+ final String container = datamodel.getContainer().getSafePathString();
+ result.add(spawnJavaOnDarwinActionBuilder(baseTools.momczipDeployJar())
+ .setMnemonic("MomCompile")
+ .addOutput(outputZip)
+ .addInputs(datamodel.getInputs())
+ .setCommandLine(new CommandLine() {
+ @Override
+ public Iterable<String> arguments() {
+ return new ImmutableList.Builder<String>()
+ .add(outputZip.getExecPathString())
+ .add(archiveRoot)
+ .add(IosSdkCommands.MOMC_PATH)
+ .addAll(commonMomczipArguments(objcConfiguration))
+ .add(container)
+ .build();
+ }
+ })
+ .build(context));
+ }
+ return result.build();
+ }
+
+ private static final String FRAMEWORK_SUFFIX = ".framework";
+
+ /**
+ * All framework names to pass to the linker using {@code -framework} flags. For a framework in
+ * the directory foo/bar.framework, the name is "bar". Each framework is found without using the
+ * full path by means of the framework search paths. The search paths are added by
+ * {@link IosSdkCommands#commonLinkAndCompileArgsForClang(ObjcProvider, ObjcConfiguration)}).
+ *
+ * <p>It's awful that we can't pass the full path to the framework and avoid framework search
+ * paths, but this is imposed on us by clang. clang does not support passing the full path to the
+ * framework, so Bazel cannot do it either.
+ */
+ private static Iterable<String> frameworkNames(ObjcProvider provider) {
+ List<String> names = new ArrayList<>();
+ Iterables.addAll(names, SdkFramework.names(provider.get(SDK_FRAMEWORK)));
+ for (PathFragment frameworkDir : provider.get(FRAMEWORK_DIR)) {
+ String segment = frameworkDir.getBaseName();
+ Preconditions.checkState(segment.endsWith(FRAMEWORK_SUFFIX),
+ "expect %s to end with %s, but it does not", segment, FRAMEWORK_SUFFIX);
+ names.add(segment.substring(0, segment.length() - FRAMEWORK_SUFFIX.length()));
+ }
+ return names;
+ }
+
+ static final class ExtraLinkArgs extends IterableWrapper<String> {
+ ExtraLinkArgs(Iterable<String> args) {
+ super(args);
+ }
+
+ ExtraLinkArgs(String... args) {
+ super(args);
+ }
+ }
+
+ static final class ExtraLinkInputs extends IterableWrapper<Artifact> {
+ ExtraLinkInputs(Iterable<Artifact> inputs) {
+ super(inputs);
+ }
+
+ ExtraLinkInputs(Artifact... inputs) {
+ super(inputs);
+ }
+ }
+
+ private static final class LinkCommandLine extends CommandLine {
+ private static final Joiner commandJoiner = Joiner.on(' ');
+ private final ObjcProvider objcProvider;
+ private final ObjcConfiguration objcConfiguration;
+ private final Artifact linkedBinary;
+ private final Optional<Artifact> dsymBundle;
+ private final ExtraLinkArgs extraLinkArgs;
+
+ LinkCommandLine(ObjcConfiguration objcConfiguration, ExtraLinkArgs extraLinkArgs,
+ ObjcProvider objcProvider, Artifact linkedBinary, Optional<Artifact> dsymBundle) {
+ this.objcConfiguration = Preconditions.checkNotNull(objcConfiguration);
+ this.extraLinkArgs = Preconditions.checkNotNull(extraLinkArgs);
+ this.objcProvider = Preconditions.checkNotNull(objcProvider);
+ this.linkedBinary = Preconditions.checkNotNull(linkedBinary);
+ this.dsymBundle = Preconditions.checkNotNull(dsymBundle);
+ }
+
+ Iterable<String> dylibPaths() {
+ ImmutableList.Builder<String> args = new ImmutableList.Builder<>();
+ for (String dylib : objcProvider.get(SDK_DYLIB)) {
+ args.add(String.format(
+ "%s/usr/lib/%s.dylib", IosSdkCommands.sdkDir(objcConfiguration), dylib));
+ }
+ return args.build();
+ }
+
+ @Override
+ public Iterable<String> arguments() {
+ StringBuilder argumentStringBuilder = new StringBuilder();
+
+ Iterable<String> archiveExecPaths = Artifact.toExecPaths(
+ Iterables.concat(objcProvider.get(LIBRARY), objcProvider.get(IMPORTED_LIBRARY)));
+ commandJoiner.appendTo(argumentStringBuilder, new ImmutableList.Builder<String>()
+ .add(objcProvider.is(USES_CPP) ? CLANG_PLUSPLUS.toString() : CLANG.toString())
+ .addAll(objcProvider.is(USES_CPP)
+ ? ImmutableList.of("-stdlib=libc++") : ImmutableList.<String>of())
+ .addAll(IosSdkCommands.commonLinkAndCompileArgsForClang(objcProvider, objcConfiguration))
+ .add("-Xlinker", "-objc_abi_version")
+ .add("-Xlinker", "2")
+ .add("-fobjc-link-runtime")
+ .addAll(IosSdkCommands.DEFAULT_LINKER_FLAGS)
+ .addAll(Interspersing.beforeEach("-framework", frameworkNames(objcProvider)))
+ .addAll(Interspersing.beforeEach(
+ "-weak_framework", SdkFramework.names(objcProvider.get(WEAK_SDK_FRAMEWORK))))
+ .add("-o", linkedBinary.getExecPathString())
+ .addAll(archiveExecPaths)
+ .addAll(dylibPaths())
+ .addAll(extraLinkArgs)
+ .build());
+
+ // Call to dsymutil for debug symbol generation must happen in the link action.
+ // All debug symbol information is encoded in object files inside archive files. To generate
+ // the debug symbol bundle, dsymutil will look inside the linked binary for the encoded
+ // absolute paths to archive files, which are only valid in the link action.
+ for (Artifact justDsymBundle : dsymBundle.asSet()) {
+ argumentStringBuilder.append(" ");
+ commandJoiner.appendTo(argumentStringBuilder, new ImmutableList.Builder<String>()
+ .add("&&")
+ .add(DSYMUTIL.toString())
+ .add(linkedBinary.getExecPathString())
+ .add("-o").add(justDsymBundle.getExecPathString())
+ .build());
+ }
+
+ return ImmutableList.of(argumentStringBuilder.toString());
+ }
+ }
+
+ /**
+ * Generates an action to link a binary.
+ */
+ void registerLinkAction(Artifact linkedBinary, ObjcProvider objcProvider,
+ ExtraLinkArgs extraLinkArgs, ExtraLinkInputs extraLinkInputs, Optional<Artifact> dsymBundle) {
+ extraLinkArgs = new ExtraLinkArgs(Iterables.concat(
+ Interspersing.beforeEach(
+ "-force_load", Artifact.toExecPaths(objcProvider.get(FORCE_LOAD_LIBRARY))),
+ extraLinkArgs));
+ register(spawnOnDarwinActionBuilder()
+ .setMnemonic("ObjcLink")
+ .setShellCommand(ImmutableList.of("/bin/bash", "-c"))
+ .setCommandLine(
+ new LinkCommandLine(objcConfiguration, extraLinkArgs, objcProvider, linkedBinary,
+ dsymBundle))
+ .addOutput(linkedBinary)
+ .addOutputs(dsymBundle.asSet())
+ .addTransitiveInputs(objcProvider.get(LIBRARY))
+ .addTransitiveInputs(objcProvider.get(IMPORTED_LIBRARY))
+ .addTransitiveInputs(objcProvider.get(FRAMEWORK_FILE))
+ .addInputs(extraLinkInputs)
+ .build(context));
+ }
+
+ static final class StringsFiles extends IterableWrapper<CompiledResourceFile> {
+ StringsFiles(Iterable<CompiledResourceFile> files) {
+ super(files);
+ }
+ }
+
+ /**
+ * Registers actions for resource conversion that are needed by all rules that inherit from
+ * {@link ObjcBase}.
+ */
+ void registerResourceActions(ObjcRuleClasses.Tools baseTools, StringsFiles stringsFiles,
+ XibFiles xibFiles, Iterable<Xcdatamodel> datamodels) {
+ registerAll(convertStringsActions(context, baseTools, stringsFiles));
+ registerAll(convertXibsActions(baseTools, xibFiles));
+ registerAll(momczipActions(context, baseTools, objcConfiguration, datamodels));
+ }
+
+ static LazyString joinExecPaths(final Iterable<Artifact> artifacts) {
+ return new LazyString() {
+ @Override
+ public String toString() {
+ return Artifact.joinExecPaths("\n", artifacts);
+ }
+ };
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcBinary.java b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcBinary.java
new file mode 100644
index 0000000..52c897f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcBinary.java
@@ -0,0 +1,147 @@
+// Copyright 2014 Google Inc. 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.rules.objc;
+
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.IMPORTED_LIBRARY;
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.LIBRARY;
+import static com.google.devtools.build.lib.rules.objc.XcodeProductType.APPLICATION;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.rules.RuleConfiguredTargetFactory;
+import com.google.devtools.build.lib.rules.objc.ApplicationSupport.LinkedBinary;
+import com.google.devtools.build.lib.rules.objc.ObjcActionsBuilder.ExtraLinkArgs;
+import com.google.devtools.build.lib.rules.objc.ObjcActionsBuilder.ExtraLinkInputs;
+import com.google.devtools.build.lib.rules.objc.ObjcCommon.CompilationAttributes;
+import com.google.devtools.build.lib.rules.objc.ObjcCommon.ResourceAttributes;
+
+/**
+ * Implementation for the "objc_binary" rule.
+ */
+public class ObjcBinary implements RuleConfiguredTargetFactory {
+
+ @VisibleForTesting
+ static final String REQUIRES_AT_LEAST_ONE_LIBRARY_OR_SOURCE_FILE =
+ "At least one library dependency or source file is required.";
+
+ @Override
+ public ConfiguredTarget create(RuleContext ruleContext) throws InterruptedException {
+ ObjcCommon common = common(ruleContext);
+ OptionsProvider optionsProvider = optionsProvider(ruleContext);
+
+ ObjcProvider objcProvider = common.getObjcProvider();
+ if (!hasLibraryOrSources(objcProvider)) {
+ ruleContext.ruleError(REQUIRES_AT_LEAST_ONE_LIBRARY_OR_SOURCE_FILE);
+ return null;
+ }
+
+ XcodeProvider.Builder xcodeProviderBuilder = new XcodeProvider.Builder();
+ NestedSetBuilder<Artifact> filesToBuild = NestedSetBuilder.stableOrder();
+
+ new CompilationSupport(ruleContext)
+ .registerJ2ObjcCompileAndArchiveActions(optionsProvider, common.getObjcProvider())
+ .registerCompileAndArchiveActions(common, optionsProvider)
+ .addXcodeSettings(xcodeProviderBuilder, common, optionsProvider)
+ .registerLinkActions(common.getObjcProvider(), new ExtraLinkArgs(), new ExtraLinkInputs())
+ .validateAttributes();
+
+ // TODO(bazel-team): Remove once all bundle users are migrated to ios_application.
+ ApplicationSupport applicationSupport = new ApplicationSupport(
+ ruleContext, common.getObjcProvider(), optionsProvider, LinkedBinary.LOCAL_AND_DEPENDENCIES)
+ .registerActions()
+ .addXcodeSettings(xcodeProviderBuilder)
+ .addFilesToBuild(filesToBuild)
+ .validateAttributes();
+
+ new ResourceSupport(ruleContext)
+ .registerActions(common.getStoryboards())
+ .validateAttributes()
+ .addXcodeSettings(xcodeProviderBuilder);
+
+ XcodeSupport xcodeSupport = new XcodeSupport(ruleContext)
+ // TODO(bazel-team): Use LIBRARY_STATIC as parameter instead of APPLICATION once objc_binary
+ // no longer creates an application bundle
+ .addXcodeSettings(xcodeProviderBuilder, common.getObjcProvider(), APPLICATION)
+ .addDependencies(xcodeProviderBuilder)
+ .addFilesToBuild(filesToBuild);
+ XcodeProvider xcodeProvider = xcodeProviderBuilder.build();
+ xcodeSupport.registerActions(xcodeProvider);
+
+ // TODO(bazel-team): Stop exporting an XcTestAppProvider once objc_binary no longer creates an
+ // application bundle.
+ return common.configuredTarget(
+ filesToBuild.build(),
+ Optional.of(xcodeProvider),
+ Optional.<ObjcProvider>absent(),
+ Optional.of(applicationSupport.xcTestAppProvider()),
+ Optional.<J2ObjcSrcsProvider>absent());
+ }
+
+ private OptionsProvider optionsProvider(RuleContext ruleContext) {
+ return new OptionsProvider.Builder()
+ .addCopts(ruleContext.getTokenizedStringListAttr("copts"))
+ .addInfoplists(ruleContext.getPrerequisiteArtifacts("infoplist", Mode.TARGET).list())
+ .addTransitive(Optional.fromNullable(
+ ruleContext.getPrerequisite("options", Mode.TARGET, OptionsProvider.class)))
+ .build();
+ }
+
+ private boolean hasLibraryOrSources(ObjcProvider objcProvider) {
+ return !Iterables.isEmpty(objcProvider.get(LIBRARY)) // Includes sources from this target.
+ || !Iterables.isEmpty(objcProvider.get(IMPORTED_LIBRARY));
+ }
+
+ private ObjcCommon common(RuleContext ruleContext) {
+ IntermediateArtifacts intermediateArtifacts =
+ ObjcRuleClasses.intermediateArtifacts(ruleContext);
+ CompilationArtifacts compilationArtifacts =
+ CompilationSupport.compilationArtifacts(ruleContext);
+
+ return new ObjcCommon.Builder(ruleContext)
+ .setCompilationAttributes(new CompilationAttributes(ruleContext))
+ .setResourceAttributes(new ResourceAttributes(ruleContext))
+ .setCompilationArtifacts(compilationArtifacts)
+ .addDepObjcProviders(ruleContext.getPrerequisites("deps", Mode.TARGET, ObjcProvider.class))
+ .addDepObjcProviders(
+ ruleContext.getPrerequisites("bundles", Mode.TARGET, ObjcProvider.class))
+ .addNonPropagatedDepObjcProviders(
+ ruleContext.getPrerequisites("non_propagated_deps", Mode.TARGET, ObjcProvider.class))
+ .setIntermediateArtifacts(intermediateArtifacts)
+ .setAlwayslink(false)
+ .addExtraImportLibraries(j2ObjcLibraries(ruleContext))
+ .setLinkedBinary(intermediateArtifacts.singleArchitectureBinary())
+ .build();
+ }
+
+ private Iterable<Artifact> j2ObjcLibraries(RuleContext ruleContext) {
+ J2ObjcSrcsProvider j2ObjcSrcsProvider = ObjcRuleClasses.j2ObjcSrcsProvider(ruleContext);
+ ImmutableList.Builder<Artifact> j2objcLibraries = ImmutableList.builder();
+
+ // TODO(bazel-team): Refactor the code to stop flattening the nested set here.
+ for (J2ObjcSource j2ObjcSource : j2ObjcSrcsProvider.getSrcs()) {
+ j2objcLibraries.add(
+ ObjcRuleClasses.j2objcIntermediateArtifacts(ruleContext, j2ObjcSource).archive());
+ }
+
+ return j2objcLibraries.build();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcBinaryRule.java b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcBinaryRule.java
new file mode 100644
index 0000000..c9fc57e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcBinaryRule.java
@@ -0,0 +1,62 @@
+// Copyright 2014 Google Inc. 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.rules.objc;
+
+import com.google.devtools.build.lib.analysis.BlazeRule;
+import com.google.devtools.build.lib.analysis.RuleDefinition;
+import com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment;
+import com.google.devtools.build.lib.packages.ImplicitOutputsFunction;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.RuleClass.Builder;
+
+/**
+ * Rule definition for objc_binary.
+ */
+@BlazeRule(name = "objc_binary",
+ factoryClass = ObjcBinary.class,
+ ancestors = { ObjcLibraryRule.class, IosApplicationRule.class })
+public class ObjcBinaryRule implements RuleDefinition {
+
+ @Override
+ public RuleClass build(Builder builder, final RuleDefinitionEnvironment env) {
+ return builder
+ // TODO(bazel-team): Remove bundling functionality (dependency on IosApplicationRule).
+ /*<!-- #BLAZE_RULE(objc_binary).IMPLICIT_OUTPUTS -->
+ <ul>
+ <li><code><var>name</var>.ipa</code>: the application bundle as an <code>.ipa</code>
+ file</li>
+ <li><code><var>name</var>.xcodeproj/project.pbxproj</code>: An Xcode project file which
+ can be used to develop or build on a Mac.</li>
+ </ul>
+ <!-- #END_BLAZE_RULE.IMPLICIT_OUTPUTS -->*/
+ .setImplicitOutputsFunction(
+ ImplicitOutputsFunction.fromFunctions(ApplicationSupport.IPA, XcodeSupport.PBXPROJ))
+ .removeAttribute("binary")
+ .removeAttribute("alwayslink")
+ .build();
+ }
+}
+
+/*<!-- #BLAZE_RULE (NAME = objc_binary, TYPE = BINARY, FAMILY = Objective-C) -->
+
+${ATTRIBUTE_SIGNATURE}
+
+<p>This rule produces an application bundle by linking one or more Objective-C libraries.</p>
+
+${IMPLICIT_OUTPUTS}
+
+${ATTRIBUTE_DEFINITION}
+
+<!-- #END_BLAZE_RULE -->*/
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcBundle.java b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcBundle.java
new file mode 100644
index 0000000..6585cba
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcBundle.java
@@ -0,0 +1,51 @@
+// Copyright 2014 Google Inc. 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.rules.objc;
+
+import static com.google.devtools.build.lib.collect.nestedset.Order.STABLE_ORDER;
+
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.rules.RuleConfiguredTargetFactory;
+
+/**
+ * Implementation for {@code objc_bundle}.
+ */
+public class ObjcBundle implements RuleConfiguredTargetFactory {
+ @Override
+ public ConfiguredTarget create(RuleContext ruleContext) throws InterruptedException {
+ ObjcCommon common = new ObjcCommon.Builder(ruleContext).build();
+
+ ImmutableList<Artifact> bundleImports = ruleContext
+ .getPrerequisiteArtifacts("bundle_imports", Mode.TARGET).list();
+ Iterable<String> bundleImportErrors =
+ ObjcCommon.notInContainerErrors(bundleImports, ObjcCommon.BUNDLE_CONTAINER_TYPE);
+ for (String error : bundleImportErrors) {
+ ruleContext.attributeError("bundle_imports", error);
+ }
+
+ return common.configuredTarget(
+ /*filesToBuild=*/NestedSetBuilder.<Artifact>emptySet(STABLE_ORDER),
+ Optional.<XcodeProvider>absent(),
+ Optional.of(common.getObjcProvider()),
+ Optional.<XcTestAppProvider>absent(),
+ Optional.<J2ObjcSrcsProvider>absent());
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcBundleLibrary.java b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcBundleLibrary.java
new file mode 100644
index 0000000..0e7f6b0
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcBundleLibrary.java
@@ -0,0 +1,103 @@
+// Copyright 2014 Google Inc. 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.rules.objc;
+
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.NESTED_BUNDLE;
+import static com.google.devtools.build.lib.rules.objc.XcodeProductType.BUNDLE;
+
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.rules.RuleConfiguredTargetFactory;
+import com.google.devtools.build.lib.rules.objc.ObjcCommon.ResourceAttributes;
+import com.google.devtools.build.xcode.common.TargetDeviceFamily;
+
+/**
+ * Implementation for {@code objc_bundle_library}.
+ */
+public class ObjcBundleLibrary implements RuleConfiguredTargetFactory {
+
+ @Override
+ public ConfiguredTarget create(RuleContext ruleContext) throws InterruptedException {
+ ObjcCommon common = common(ruleContext);
+ OptionsProvider optionsProvider = optionsProvider(ruleContext);
+
+ Bundling bundling = bundling(ruleContext, common, optionsProvider);
+
+ XcodeProvider.Builder xcodeProviderBuilder = new XcodeProvider.Builder();
+ NestedSetBuilder<Artifact> filesToBuild = NestedSetBuilder.stableOrder();
+
+ // TODO(bazel-team): Figure out if the target device is important, and what to set it to. It may
+ // have to inherit this from the binary being built. As of this writing, this is only used for
+ // asset catalogs compilation (actool).
+ new BundleSupport(ruleContext, ImmutableSet.of(TargetDeviceFamily.IPHONE), bundling)
+ .registerActions(common.getObjcProvider())
+ .addXcodeSettings(xcodeProviderBuilder);
+
+ new ResourceSupport(ruleContext)
+ .validateAttributes()
+ .registerActions(common.getStoryboards())
+ .addXcodeSettings(xcodeProviderBuilder);
+
+ new XcodeSupport(ruleContext)
+ .addFilesToBuild(filesToBuild)
+ .addXcodeSettings(xcodeProviderBuilder, common.getObjcProvider(), BUNDLE)
+ .registerActions(xcodeProviderBuilder.build());
+
+ ObjcProvider nestedBundleProvider = new ObjcProvider.Builder()
+ .add(NESTED_BUNDLE, bundling)
+ .build();
+
+ return common.configuredTarget(
+ filesToBuild.build(),
+ Optional.of(xcodeProviderBuilder.build()),
+ Optional.of(nestedBundleProvider),
+ Optional.<XcTestAppProvider>absent(),
+ Optional.<J2ObjcSrcsProvider>absent());
+ }
+
+ private OptionsProvider optionsProvider(RuleContext ruleContext) {
+ return new OptionsProvider.Builder()
+ .addInfoplists(ruleContext.getPrerequisiteArtifacts("infoplist", Mode.TARGET).list())
+ .build();
+ }
+
+ private Bundling bundling(
+ RuleContext ruleContext, ObjcCommon common, OptionsProvider optionsProvider) {
+ IntermediateArtifacts intermediateArtifacts =
+ ObjcRuleClasses.intermediateArtifacts(ruleContext);
+ return new Bundling.Builder()
+ .setName(ruleContext.getLabel().getName())
+ .setBundleDirSuffix(".bundle")
+ .setObjcProvider(common.getObjcProvider())
+ .setInfoplistMerging(
+ BundleSupport.infoPlistMerging(ruleContext, common.getObjcProvider(), optionsProvider))
+ .setIntermediateArtifacts(intermediateArtifacts)
+ .build();
+ }
+
+ private ObjcCommon common(RuleContext ruleContext) {
+ return new ObjcCommon.Builder(ruleContext)
+ .setResourceAttributes(new ResourceAttributes(ruleContext))
+ .addDepObjcProviders(
+ ruleContext.getPrerequisites("bundles", Mode.TARGET, ObjcProvider.class))
+ .setIntermediateArtifacts(ObjcRuleClasses.intermediateArtifacts(ruleContext))
+ .build();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcBundleLibraryRule.java b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcBundleLibraryRule.java
new file mode 100644
index 0000000..b9405a6
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcBundleLibraryRule.java
@@ -0,0 +1,67 @@
+// Copyright 2014 Google Inc. 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.rules.objc;
+
+import static com.google.devtools.build.lib.packages.Attribute.attr;
+import static com.google.devtools.build.lib.packages.Type.LABEL_LIST;
+
+import com.google.devtools.build.lib.analysis.BlazeRule;
+import com.google.devtools.build.lib.analysis.RuleDefinition;
+import com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment;
+import com.google.devtools.build.lib.packages.ImplicitOutputsFunction;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.RuleClass.Builder;
+
+/**
+ * Rule definition for objc_bundle_library.
+ */
+@BlazeRule(name = "objc_bundle_library",
+ factoryClass = ObjcBundleLibrary.class,
+ ancestors = { ObjcRuleClasses.ObjcBaseResourcesRule.class,
+ ObjcRuleClasses.ObjcHasInfoplistRule.class })
+public class ObjcBundleLibraryRule implements RuleDefinition {
+ @Override
+ public RuleClass build(Builder builder, RuleDefinitionEnvironment env) {
+ return builder
+ /*<!-- #BLAZE_RULE(objc_bundle_library).IMPLICIT_OUTPUTS -->
+ <ul>
+ <li><code><var>name</var>.xcodeproj/project.pbxproj</code>: An Xcode project file which
+ can be used to develop or build on a Mac.</li>
+ </ul>
+ <!-- #END_BLAZE_RULE.IMPLICIT_OUTPUTS -->*/
+ .setImplicitOutputsFunction(ImplicitOutputsFunction.fromFunctions(XcodeSupport.PBXPROJ))
+ /* <!-- #BLAZE_RULE(objc_bundle_library).ATTRIBUTE(bundles) -->
+ The list of bundle targets that this target requires to be included in the final bundle.
+ ${SYNOPSIS}
+ <!-- #END_BLAZE_RULE.ATTRIBUTE -->*/
+ .add(attr("bundles", LABEL_LIST)
+ .direct_compile_time_input()
+ .allowedRuleClasses("objc_bundle", "objc_bundle_library")
+ .allowedFileTypes())
+ .build();
+ }
+}
+
+/*<!-- #BLAZE_RULE (NAME = objc_bundle_library, TYPE = LIBRARY, FAMILY = Objective-C) -->
+
+${ATTRIBUTE_SIGNATURE}
+
+<p>This rule encapsulates a library which is provided to dependers as a bundle.
+A <code>objc_bundle_library</code>'s resources are put in a nested bundle in
+the final iOS application.
+
+${ATTRIBUTE_DEFINITION}
+
+<!-- #END_BLAZE_RULE -->*/
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcBundleRule.java b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcBundleRule.java
new file mode 100644
index 0000000..9be0d04
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcBundleRule.java
@@ -0,0 +1,59 @@
+// Copyright 2014 Google Inc. 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.rules.objc;
+
+import static com.google.devtools.build.lib.packages.Attribute.attr;
+import static com.google.devtools.build.lib.packages.Type.LABEL_LIST;
+
+import com.google.devtools.build.lib.analysis.BaseRuleClasses;
+import com.google.devtools.build.lib.analysis.BlazeRule;
+import com.google.devtools.build.lib.analysis.RuleDefinition;
+import com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.RuleClass.Builder;
+import com.google.devtools.build.lib.util.FileTypeSet;
+
+/**
+ * Rule definition for objc_bundle.
+ */
+@BlazeRule(name = "objc_bundle",
+ factoryClass = ObjcBundle.class,
+ ancestors = { BaseRuleClasses.BaseRule.class })
+public class ObjcBundleRule implements RuleDefinition {
+ @Override
+ public RuleClass build(Builder builder, RuleDefinitionEnvironment environment) {
+ return builder
+ /* <!-- #BLAZE_RULE(objc_bundle).ATTRIBUTE(bundle_imports) -->
+ The list of files under a <code>.bundle</code> directory which are
+ provided to Objective-C targets that depend on this target.
+ ${SYNOPSIS}
+ <!-- #END_BLAZE_RULE.ATTRIBUTE --> */
+ .add(attr("bundle_imports", LABEL_LIST)
+ .allowedFileTypes(FileTypeSet.ANY_FILE)
+ .nonEmpty())
+ .build();
+ }
+}
+
+/*<!-- #BLAZE_RULE (NAME = objc_bundle, TYPE = LIBRARY, FAMILY = Objective-C) -->
+
+${ATTRIBUTE_SIGNATURE}
+
+<p>This rule encapsulates an already-built bundle. It is defined by a list of
+files in one or more <code>.bundle</code> directories.
+
+${ATTRIBUTE_DEFINITION}
+
+<!-- #END_BLAZE_RULE -->*/
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcCommandLineOptions.java b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcCommandLineOptions.java
new file mode 100644
index 0000000..f85609a
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcCommandLineOptions.java
@@ -0,0 +1,85 @@
+// Copyright 2014 Google Inc. 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.rules.objc;
+
+import static com.google.devtools.build.xcode.common.BuildOptionsUtil.DEFAULT_OPTIONS_NAME;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Multimap;
+import com.google.devtools.build.lib.analysis.config.FragmentOptions;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.common.options.Option;
+
+import java.util.List;
+
+/**
+ * Command-line options for building Objective-C targets.
+ */
+public class
+ ObjcCommandLineOptions extends FragmentOptions {
+ @Option(name = "ios_sdk_version",
+ defaultValue = DEFAULT_SDK_VERSION,
+ category = "undocumented",
+ help = "Specifies the version of the iOS SDK to use to build iOS applications."
+ )
+ public String iosSdkVersion;
+
+ @VisibleForTesting static final String DEFAULT_SDK_VERSION = "8.1";
+
+ @Option(name = "ios_simulator_version",
+ defaultValue = "7.1",
+ category = "undocumented",
+ help = "The version of iOS to run on the simulator when running tests. This is ignored if the"
+ + " ios_test rule specifies the target device.",
+ deprecationWarning = "This flag is deprecated in favor of the target_device attribute and"
+ + " will eventually removed.")
+ public String iosSimulatorVersion;
+
+ @Option(name = "ios_cpu",
+ defaultValue = "i386",
+ category = "undocumented",
+ help = "Specifies to target CPU of iOS compilation.")
+ public String iosCpu;
+
+ @Option(name = "xcode_options",
+ defaultValue = DEFAULT_OPTIONS_NAME,
+ category = "undocumented",
+ help = "Specifies the name of the build settings to use.")
+ public String xcodeOptions;
+
+ @Option(name = "objc_generate_debug_symbols",
+ defaultValue = "false",
+ category = "undocumented",
+ help = "Specifies whether to generate debug symbol(.dSYM) file.")
+ public boolean generateDebugSymbols;
+
+ @Option(name = "objccopt",
+ allowMultiple = true,
+ defaultValue = "",
+ category = "flags",
+ help = "Additional options to pass to Objective C compilation.")
+ public List<String> copts;
+
+ @Option(name = "ios_minimum_os",
+ defaultValue = DEFAULT_MINIMUM_IOS,
+ category = "flags",
+ help = "Minimum compatible iOS version for target simulators and devices.")
+ public String iosMinimumOs;
+
+ @VisibleForTesting static final String DEFAULT_MINIMUM_IOS = "7.0";
+
+ @Override
+ public void addAllLabels(Multimap<String, Label> labelMap) {}
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcCommon.java b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcCommon.java
new file mode 100644
index 0000000..ade86ee
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcCommon.java
@@ -0,0 +1,620 @@
+// Copyright 2014 Google Inc. 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.rules.objc;
+
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.ASSET_CATALOG;
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.BUNDLE_FILE;
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.BUNDLE_IMPORT_DIR;
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.DEFINE;
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.FLAG;
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.FORCE_LOAD_FOR_XCODEGEN;
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.FORCE_LOAD_LIBRARY;
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.FRAMEWORK_DIR;
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.FRAMEWORK_FILE;
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.Flag.USES_CPP;
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.GENERAL_RESOURCE_FILE;
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.HEADER;
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.IMPORTED_LIBRARY;
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.INCLUDE;
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.LIBRARY;
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.LINKED_BINARY;
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.MERGE_ZIP;
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.SDK_DYLIB;
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.SDK_FRAMEWORK;
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.WEAK_SDK_FRAMEWORK;
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.XCASSETS_DIR;
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.XCDATAMODEL;
+import static com.google.devtools.build.lib.vfs.PathFragment.TO_PATH_FRAGMENT;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Optional;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicates;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTargetBuilder;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.Runfiles;
+import com.google.devtools.build.lib.analysis.RunfilesProvider;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.rules.cpp.CcCommon;
+import com.google.devtools.build.lib.util.FileType;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.xcode.util.Interspersing;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Contains information common to multiple objc_* rules, and provides a unified API for extracting
+ * and accessing it.
+ */
+// TODO(bazel-team): Decompose and subsume area-specific logic and data into the various *Support
+// classes. Make sure to distinguish rule output (providers, runfiles, ...) from intermediate,
+// rule-internal information. Any provider created by a rule should not be read, only published.
+public final class ObjcCommon {
+ /**
+ * Provides a way to access attributes that are common to all compilation rules that inherit from
+ * {@link ObjcRuleClasses.ObjcCompilationRule}.
+ */
+ // TODO(bazel-team): Delete and move into support-specific attributes classes once ObjcCommon is
+ // gone.
+ static final class CompilationAttributes {
+ private final RuleContext ruleContext;
+ private final ObjcSdkFrameworks.Attributes sdkFrameworkAttributes;
+
+ CompilationAttributes(RuleContext ruleContext) {
+ this.ruleContext = Preconditions.checkNotNull(ruleContext);
+ this.sdkFrameworkAttributes = new ObjcSdkFrameworks.Attributes(ruleContext);
+ }
+
+ ImmutableList<Artifact> hdrs() {
+ return ImmutableList.copyOf(CcCommon.getHeaders(ruleContext));
+ }
+
+ Iterable<PathFragment> includes() {
+ return Iterables.transform(
+ ruleContext.attributes().get("includes", Type.STRING_LIST),
+ PathFragment.TO_PATH_FRAGMENT);
+ }
+
+ Iterable<PathFragment> sdkIncludes() {
+ return Iterables.transform(
+ ruleContext.attributes().get("sdk_includes", Type.STRING_LIST),
+ PathFragment.TO_PATH_FRAGMENT);
+ }
+
+ /**
+ * Returns the value of the sdk_frameworks attribute plus frameworks that are included
+ * automatically.
+ */
+ ImmutableSet<SdkFramework> sdkFrameworks() {
+ return sdkFrameworkAttributes.sdkFrameworks();
+ }
+
+ /**
+ * Returns the value of the weak_sdk_frameworks attribute.
+ */
+ ImmutableSet<SdkFramework> weakSdkFrameworks() {
+ return sdkFrameworkAttributes.weakSdkFrameworks();
+ }
+
+ /**
+ * Returns the value of the sdk_dylibs attribute.
+ */
+ ImmutableSet<String> sdkDylibs() {
+ return sdkFrameworkAttributes.sdkDylibs();
+ }
+
+ /**
+ * Returns the exec paths of all header search paths that should be added to this target and
+ * dependers on this target, obtained from the {@code includes} attribute.
+ */
+ ImmutableList<PathFragment> headerSearchPaths() {
+ ImmutableList.Builder<PathFragment> paths = new ImmutableList.Builder<>();
+ PathFragment packageFragment = ruleContext.getLabel().getPackageFragment();
+ List<PathFragment> rootFragments = ImmutableList.of(
+ packageFragment,
+ ruleContext.getConfiguration().getGenfilesFragment().getRelative(packageFragment));
+
+ Iterable<PathFragment> relativeIncludes =
+ Iterables.filter(includes(), Predicates.not(PathFragment.IS_ABSOLUTE));
+ for (PathFragment include : relativeIncludes) {
+ for (PathFragment rootFragment : rootFragments) {
+ paths.add(rootFragment.getRelative(include).normalize());
+ }
+ }
+ return paths.build();
+ }
+ }
+
+ /**
+ * Provides a way to access attributes that are common to all resources rules that inherit from
+ * {@link ObjcRuleClasses.ObjcBaseResourcesRule}.
+ */
+ // TODO(bazel-team): Delete and move into support-specific attributes classes once ObjcCommon is
+ // gone.
+ static final class ResourceAttributes {
+ private final RuleContext ruleContext;
+
+ ResourceAttributes(RuleContext ruleContext) {
+ this.ruleContext = ruleContext;
+ }
+
+ ImmutableList<Artifact> strings() {
+ return ruleContext.getPrerequisiteArtifacts("strings", Mode.TARGET).list();
+ }
+
+ ImmutableList<Artifact> xibs() {
+ return ruleContext.getPrerequisiteArtifacts("xibs", Mode.TARGET)
+ .errorsForNonMatching(ObjcRuleClasses.XIB_TYPE)
+ .list();
+ }
+
+ ImmutableList<Artifact> storyboards() {
+ return ruleContext.getPrerequisiteArtifacts("storyboards", Mode.TARGET).list();
+ }
+
+ ImmutableList<Artifact> resources() {
+ return ruleContext.getPrerequisiteArtifacts("resources", Mode.TARGET).list();
+ }
+
+ ImmutableList<Artifact> datamodels() {
+ return ruleContext.getPrerequisiteArtifacts("datamodels", Mode.TARGET).list();
+ }
+
+ ImmutableList<Artifact> assetCatalogs() {
+ return ruleContext.getPrerequisiteArtifacts("asset_catalogs", Mode.TARGET).list();
+ }
+ }
+
+ static class Builder {
+ private RuleContext context;
+ private Optional<CompilationAttributes> compilationAttributes = Optional.absent();
+ private Optional<ResourceAttributes> resourceAttributes = Optional.absent();
+ private Iterable<SdkFramework> extraSdkFrameworks = ImmutableList.of();
+ private Iterable<SdkFramework> extraWeakSdkFrameworks = ImmutableList.of();
+ private Iterable<String> extraSdkDylibs = ImmutableList.of();
+ private Iterable<Artifact> frameworkImports = ImmutableList.of();
+ private Optional<CompilationArtifacts> compilationArtifacts = Optional.absent();
+ private Iterable<ObjcProvider> depObjcProviders = ImmutableList.of();
+ private Iterable<ObjcProvider> directDepObjcProviders = ImmutableList.of();
+ private Iterable<String> defines = ImmutableList.of();
+ private Iterable<PathFragment> userHeaderSearchPaths = ImmutableList.of();
+ private Iterable<Artifact> headers = ImmutableList.of();
+ private IntermediateArtifacts intermediateArtifacts;
+ private boolean alwayslink;
+ private Iterable<Artifact> extraImportLibraries = ImmutableList.of();
+ private Optional<Artifact> linkedBinary = Optional.absent();
+
+ Builder(RuleContext context) {
+ this.context = Preconditions.checkNotNull(context);
+ }
+
+ public Builder setCompilationAttributes(CompilationAttributes baseCompilationAttributes) {
+ Preconditions.checkState(!this.compilationAttributes.isPresent(),
+ "compilationAttributes is already set to: %s", this.compilationAttributes);
+ this.compilationAttributes = Optional.of(baseCompilationAttributes);
+ return this;
+ }
+
+ public Builder setResourceAttributes(ResourceAttributes baseResourceAttributes) {
+ Preconditions.checkState(!this.resourceAttributes.isPresent(),
+ "resourceAttributes is already set to: %s", this.resourceAttributes);
+ this.resourceAttributes = Optional.of(baseResourceAttributes);
+ return this;
+ }
+
+ Builder addExtraSdkFrameworks(Iterable<SdkFramework> extraSdkFrameworks) {
+ this.extraSdkFrameworks = Iterables.concat(this.extraSdkFrameworks, extraSdkFrameworks);
+ return this;
+ }
+
+ Builder addExtraWeakSdkFrameworks(Iterable<SdkFramework> extraWeakSdkFrameworks) {
+ this.extraWeakSdkFrameworks =
+ Iterables.concat(this.extraWeakSdkFrameworks, extraWeakSdkFrameworks);
+ return this;
+ }
+
+ Builder addExtraSdkDylibs(Iterable<String> extraSdkDylibs) {
+ this.extraSdkDylibs = Iterables.concat(this.extraSdkDylibs, extraSdkDylibs);
+ return this;
+ }
+
+ Builder addFrameworkImports(Iterable<Artifact> frameworkImports) {
+ this.frameworkImports = Iterables.concat(this.frameworkImports, frameworkImports);
+ return this;
+ }
+
+ Builder setCompilationArtifacts(CompilationArtifacts compilationArtifacts) {
+ Preconditions.checkState(!this.compilationArtifacts.isPresent(),
+ "compilationArtifacts is already set to: %s", this.compilationArtifacts);
+ this.compilationArtifacts = Optional.of(compilationArtifacts);
+ return this;
+ }
+
+ /**
+ * Add providers which will be exposed both to the declaring rule and to any dependers on the
+ * declaring rule.
+ */
+ Builder addDepObjcProviders(Iterable<ObjcProvider> depObjcProviders) {
+ this.depObjcProviders = Iterables.concat(this.depObjcProviders, depObjcProviders);
+ return this;
+ }
+
+ /**
+ * Add providers which will only be used by the declaring rule, and won't be propagated to any
+ * dependers on the declaring rule.
+ */
+ Builder addNonPropagatedDepObjcProviders(Iterable<ObjcProvider> directDepObjcProviders) {
+ this.directDepObjcProviders = Iterables.concat(
+ this.directDepObjcProviders, directDepObjcProviders);
+ return this;
+ }
+
+ public Builder addUserHeaderSearchPaths(Iterable<PathFragment> userHeaderSearchPaths) {
+ this.userHeaderSearchPaths =
+ Iterables.concat(this.userHeaderSearchPaths, userHeaderSearchPaths);
+ return this;
+ }
+
+ public Builder addDefines(Iterable<String> defines) {
+ this.defines = Iterables.concat(this.defines, defines);
+ return this;
+ }
+
+ public Builder addHeaders(Iterable<Artifact> headers) {
+ this.headers = Iterables.concat(this.headers, headers);
+ return this;
+ }
+
+ Builder setIntermediateArtifacts(IntermediateArtifacts intermediateArtifacts) {
+ this.intermediateArtifacts = intermediateArtifacts;
+ return this;
+ }
+
+ Builder setAlwayslink(boolean alwayslink) {
+ this.alwayslink = alwayslink;
+ return this;
+ }
+
+ /**
+ * Adds additional static libraries to be linked into the final ObjC application bundle.
+ */
+ Builder addExtraImportLibraries(Iterable<Artifact> extraImportLibraries) {
+ this.extraImportLibraries = Iterables.concat(this.extraImportLibraries, extraImportLibraries);
+ return this;
+ }
+
+ /**
+ * Sets a linked binary generated by this rule to be propagated to dependers.
+ */
+ Builder setLinkedBinary(Artifact linkedBinary) {
+ this.linkedBinary = Optional.of(linkedBinary);
+ return this;
+ }
+
+ ObjcCommon build() {
+ Iterable<BundleableFile> bundleImports = BundleableFile.bundleImportsFromRule(context);
+
+ ObjcProvider.Builder objcProvider = new ObjcProvider.Builder()
+ .addAll(IMPORTED_LIBRARY, extraImportLibraries)
+ .addAll(BUNDLE_FILE, bundleImports)
+ .addAll(BUNDLE_IMPORT_DIR,
+ uniqueContainers(BundleableFile.toArtifacts(bundleImports), BUNDLE_CONTAINER_TYPE))
+ .addAll(SDK_FRAMEWORK, extraSdkFrameworks)
+ .addAll(WEAK_SDK_FRAMEWORK, extraWeakSdkFrameworks)
+ .addAll(SDK_DYLIB, extraSdkDylibs)
+ .addAll(FRAMEWORK_FILE, frameworkImports)
+ .addAll(FRAMEWORK_DIR, uniqueContainers(frameworkImports, FRAMEWORK_CONTAINER_TYPE))
+ .addAll(INCLUDE, userHeaderSearchPaths)
+ .addAll(DEFINE, defines)
+ .addAll(HEADER, headers)
+ .addTransitiveAndPropagate(depObjcProviders)
+ .addTransitiveWithoutPropagating(directDepObjcProviders);
+
+ Storyboards storyboards;
+ Iterable<Xcdatamodel> datamodels;
+ if (compilationAttributes.isPresent()) {
+ CompilationAttributes attributes = compilationAttributes.get();
+ ObjcConfiguration objcConfiguration = ObjcRuleClasses.objcConfiguration(context);
+ Iterable<PathFragment> sdkIncludes = Iterables.transform(
+ Interspersing.prependEach(
+ IosSdkCommands.sdkDir(objcConfiguration) + "/usr/include/",
+ PathFragment.safePathStrings(attributes.sdkIncludes())),
+ TO_PATH_FRAGMENT);
+ objcProvider
+ .addAll(HEADER, attributes.hdrs())
+ .addAll(INCLUDE, attributes.headerSearchPaths())
+ .addAll(INCLUDE, sdkIncludes)
+ .addAll(SDK_FRAMEWORK, attributes.sdkFrameworks())
+ .addAll(WEAK_SDK_FRAMEWORK, attributes.weakSdkFrameworks())
+ .addAll(SDK_DYLIB, attributes.sdkDylibs());
+ }
+
+ if (resourceAttributes.isPresent()) {
+ ResourceAttributes attributes = resourceAttributes.get();
+ storyboards = Storyboards.fromInputs(attributes.storyboards(), intermediateArtifacts);
+ datamodels = Xcdatamodels.xcdatamodels(intermediateArtifacts, attributes.datamodels());
+ Iterable<CompiledResourceFile> compiledResources =
+ CompiledResourceFile.fromStringsFiles(intermediateArtifacts, attributes.strings());
+ XibFiles xibFiles = new XibFiles(attributes.xibs());
+
+ objcProvider
+ .addTransitiveAndPropagate(MERGE_ZIP, storyboards.getOutputZips())
+ .addAll(MERGE_ZIP, xibFiles.compiledZips(intermediateArtifacts))
+ .addAll(GENERAL_RESOURCE_FILE, storyboards.getInputs())
+ .addAll(GENERAL_RESOURCE_FILE, attributes.resources())
+ .addAll(GENERAL_RESOURCE_FILE, attributes.strings())
+ .addAll(GENERAL_RESOURCE_FILE, attributes.xibs())
+ .addAll(BUNDLE_FILE, BundleableFile.nonCompiledResourceFiles(attributes.resources()))
+ .addAll(BUNDLE_FILE,
+ Iterables.transform(compiledResources, CompiledResourceFile.TO_BUNDLED))
+ .addAll(XCASSETS_DIR,
+ uniqueContainers(attributes.assetCatalogs(), ASSET_CATALOG_CONTAINER_TYPE))
+ .addAll(ASSET_CATALOG, attributes.assetCatalogs())
+ .addAll(XCDATAMODEL, datamodels);
+ } else {
+ storyboards = Storyboards.empty();
+ datamodels = ImmutableList.of();
+ }
+
+ for (CompilationArtifacts artifacts : compilationArtifacts.asSet()) {
+ objcProvider.addAll(LIBRARY, artifacts.getArchive().asSet());
+
+ boolean usesCpp = false;
+ for (Artifact sourceFile :
+ Iterables.concat(artifacts.getSrcs(), artifacts.getNonArcSrcs())) {
+ usesCpp = usesCpp || ObjcRuleClasses.CPP_SOURCES.matches(sourceFile.getExecPath());
+ }
+ if (usesCpp) {
+ objcProvider.add(FLAG, USES_CPP);
+ }
+ }
+
+ if (alwayslink) {
+ for (CompilationArtifacts artifacts : compilationArtifacts.asSet()) {
+ for (Artifact archive : artifacts.getArchive().asSet()) {
+ objcProvider.add(FORCE_LOAD_LIBRARY, archive);
+ objcProvider.add(FORCE_LOAD_FOR_XCODEGEN,
+ "$(BUILT_PRODUCTS_DIR)/" + archive.getExecPath().getBaseName());
+ }
+ }
+ for (Artifact archive : extraImportLibraries) {
+ objcProvider.add(FORCE_LOAD_LIBRARY, archive);
+ objcProvider.add(FORCE_LOAD_FOR_XCODEGEN,
+ "$(WORKSPACE_ROOT)/" + archive.getExecPath().getSafePathString());
+ }
+ }
+
+ objcProvider.addAll(LINKED_BINARY, linkedBinary.asSet());
+
+ return new ObjcCommon(
+ context, objcProvider.build(), storyboards, datamodels, compilationArtifacts);
+ }
+
+ }
+
+ static final FileType BUNDLE_CONTAINER_TYPE = FileType.of(".bundle");
+
+ static final FileType ASSET_CATALOG_CONTAINER_TYPE = FileType.of(".xcassets");
+
+ static final FileType FRAMEWORK_CONTAINER_TYPE = FileType.of(".framework");
+ private final RuleContext context;
+ private final ObjcProvider objcProvider;
+ private final Storyboards storyboards;
+ private final Iterable<Xcdatamodel> datamodels;
+
+ private final Optional<CompilationArtifacts> compilationArtifacts;
+
+ private ObjcCommon(
+ RuleContext context,
+ ObjcProvider objcProvider,
+ Storyboards storyboards,
+ Iterable<Xcdatamodel> datamodels,
+ Optional<CompilationArtifacts> compilationArtifacts) {
+ this.context = Preconditions.checkNotNull(context);
+ this.objcProvider = Preconditions.checkNotNull(objcProvider);
+ this.storyboards = Preconditions.checkNotNull(storyboards);
+ this.datamodels = Preconditions.checkNotNull(datamodels);
+ this.compilationArtifacts = Preconditions.checkNotNull(compilationArtifacts);
+ }
+
+ public ObjcProvider getObjcProvider() {
+ return objcProvider;
+ }
+
+ public Optional<CompilationArtifacts> getCompilationArtifacts() {
+ return compilationArtifacts;
+ }
+
+ /**
+ * Returns all storyboards declared in this rule (not including others in the transitive
+ * dependency tree).
+ */
+ public Storyboards getStoryboards() {
+ return storyboards;
+ }
+
+ /**
+ * Returns all datamodels declared in this rule (not including others in the transitive
+ * dependency tree).
+ */
+ public Iterable<Xcdatamodel> getDatamodels() {
+ return datamodels;
+ }
+
+ /**
+ * Returns an {@link Optional} containing the compiled {@code .a} file, or
+ * {@link Optional#absent()} if this object contains no {@link CompilationArtifacts} or the
+ * compilation information has no sources.
+ */
+ public Optional<Artifact> getCompiledArchive() {
+ for (CompilationArtifacts justCompilationArtifacts : compilationArtifacts.asSet()) {
+ return justCompilationArtifacts.getArchive();
+ }
+ return Optional.absent();
+ }
+
+ /**
+ * Reports any known errors to the {@link RuleContext}. This should be called exactly once for
+ * a target.
+ */
+ public void reportErrors() {
+
+ // TODO(bazel-team): Report errors for rules that are not actually useful (i.e. objc_library
+ // without sources or resources, empty objc_bundles)
+ }
+
+ static ImmutableList<PathFragment> userHeaderSearchPaths(BuildConfiguration configuration) {
+ return ImmutableList.of(
+ new PathFragment("."),
+ configuration.getGenfilesFragment());
+ }
+
+ /**
+ * Returns the first directory in the sequence of parents of the exec path of the given artifact
+ * that matches {@code type}. For instance, if {@code type} is FileType.of(".foo") and the exec
+ * path of {@code artifact} is {@code a/b/c/bar.foo/d/e}, then the return value is
+ * {@code a/b/c/bar.foo}.
+ */
+ static Optional<PathFragment> nearestContainerMatching(FileType type, Artifact artifact) {
+ PathFragment container = artifact.getExecPath();
+ do {
+ if (type.matches(container)) {
+ return Optional.of(container);
+ }
+ container = container.getParentDirectory();
+ } while (container != null);
+ return Optional.absent();
+ }
+
+ /**
+ * Similar to {@link #nearestContainerMatching(FileType, Artifact)}, but tries matching several
+ * file types in {@code types}, and returns a path for the first match in the sequence.
+ */
+ static Optional<PathFragment> nearestContainerMatching(
+ Iterable<FileType> types, Artifact artifact) {
+ for (FileType type : types) {
+ for (PathFragment container : nearestContainerMatching(type, artifact).asSet()) {
+ return Optional.of(container);
+ }
+ }
+ return Optional.absent();
+ }
+
+ /**
+ * Returns all directories matching {@code containerType} that contain the items in
+ * {@code artifacts}. This function ignores artifacts that are not in any directory matching
+ * {@code containerType}.
+ */
+ static Iterable<PathFragment> uniqueContainers(
+ Iterable<Artifact> artifacts, FileType containerType) {
+ ImmutableSet.Builder<PathFragment> containers = new ImmutableSet.Builder<>();
+ for (Artifact artifact : artifacts) {
+ containers.addAll(ObjcCommon.nearestContainerMatching(containerType, artifact).asSet());
+ }
+ return containers.build();
+ }
+
+ /**
+ * Similar to {@link #nearestContainerMatching(FileType, Artifact)}, but returns the container
+ * closest to the root that matches the given type.
+ */
+ static Optional<PathFragment> farthestContainerMatching(FileType type, Artifact artifact) {
+ PathFragment container = artifact.getExecPath();
+ Optional<PathFragment> lastMatch = Optional.absent();
+ do {
+ if (type.matches(container)) {
+ lastMatch = Optional.of(container);
+ }
+ container = container.getParentDirectory();
+ } while (container != null);
+ return lastMatch;
+ }
+
+ static Iterable<String> notInContainerErrors(
+ Iterable<Artifact> artifacts, FileType containerType) {
+ return notInContainerErrors(artifacts, ImmutableList.of(containerType));
+ }
+
+ @VisibleForTesting
+ static final String NOT_IN_CONTAINER_ERROR_FORMAT =
+ "File '%s' is not in a directory of one of these type(s): %s";
+
+ static Iterable<String> notInContainerErrors(
+ Iterable<Artifact> artifacts, Iterable<FileType> containerTypes) {
+ Set<String> errors = new HashSet<>();
+ for (Artifact artifact : artifacts) {
+ boolean inContainer = nearestContainerMatching(containerTypes, artifact).isPresent();
+ if (!inContainer) {
+ errors.add(String.format(NOT_IN_CONTAINER_ERROR_FORMAT,
+ artifact.getExecPath(), Iterables.toString(containerTypes)));
+ }
+ }
+ return errors;
+ }
+
+ /**
+ * @param filesToBuild files to build for this target. These also become the data runfiles. Note
+ * that this method may add more files to create the complete list of files to build for this
+ * target.
+ * @param maybeTargetProvider the provider for this target.
+ * @param maybeExportedProvider the {@link ObjcProvider} for this target. This should generally be
+ * present whenever {@code objc_} rules may depend on this target.
+ * @param maybeJ2ObjcSrcsProvider the {@link J2ObjcSrcsProvider} for this target.
+ */
+ public ConfiguredTarget configuredTarget(NestedSet<Artifact> filesToBuild,
+ Optional<XcodeProvider> maybeTargetProvider, Optional<ObjcProvider> maybeExportedProvider,
+ Optional<XcTestAppProvider> maybeXcTestAppProvider,
+ Optional<J2ObjcSrcsProvider> maybeJ2ObjcSrcsProvider) {
+ NestedSet<Artifact> allFilesToBuild = NestedSetBuilder.<Artifact>stableOrder()
+ .addTransitive(filesToBuild)
+ .addTransitive(storyboards.getOutputZips())
+ .addAll(Xcdatamodel.outputZips(datamodels))
+ .build();
+
+ RunfilesProvider runfilesProvider = RunfilesProvider.withData(
+ new Runfiles.Builder()
+ .addRunfiles(context, RunfilesProvider.DEFAULT_RUNFILES)
+ .build(),
+ new Runfiles.Builder().addTransitiveArtifacts(allFilesToBuild).build());
+
+ RuleConfiguredTargetBuilder target = new RuleConfiguredTargetBuilder(context)
+ .setFilesToBuild(allFilesToBuild)
+ .add(RunfilesProvider.class, runfilesProvider);
+ for (ObjcProvider exportedProvider : maybeExportedProvider.asSet()) {
+ target.addProvider(ObjcProvider.class, exportedProvider);
+ }
+ for (XcTestAppProvider xcTestAppProvider : maybeXcTestAppProvider.asSet()) {
+ target.addProvider(XcTestAppProvider.class, xcTestAppProvider);
+ }
+ for (XcodeProvider targetProvider : maybeTargetProvider.asSet()) {
+ target.addProvider(XcodeProvider.class, targetProvider);
+ }
+ for (J2ObjcSrcsProvider j2ObjcSrcsProvider : maybeJ2ObjcSrcsProvider.asSet()) {
+ target.addProvider(J2ObjcSrcsProvider.class, j2ObjcSrcsProvider);
+ }
+ return target.build();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcConfiguration.java b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcConfiguration.java
new file mode 100644
index 0000000..3f2e073
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcConfiguration.java
@@ -0,0 +1,136 @@
+// Copyright 2014 Google Inc. 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.rules.objc;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.analysis.config.CompilationMode;
+import com.google.devtools.build.xcode.common.Platform;
+
+import java.util.List;
+
+/**
+ * A compiler configuration containing flags required for Objective-C compilation.
+ */
+public class ObjcConfiguration extends BuildConfiguration.Fragment {
+ @VisibleForTesting
+ static final ImmutableList<String> DBG_COPTS = ImmutableList.of("-O0", "-DDEBUG=1",
+ "-fstack-protector", "-fstack-protector-all", "-D_GLIBCXX_DEBUG_PEDANTIC", "-D_GLIBCXX_DEBUG",
+ "-D_GLIBCPP_CONCEPT_CHECKS");
+
+ @VisibleForTesting
+ static final ImmutableList<String> FASTBUILD_COPTS = ImmutableList.of("-O0", "-DDEBUG=1");
+
+ @VisibleForTesting
+ static final ImmutableList<String> OPT_COPTS =
+ ImmutableList.of("-Os", "-DNDEBUG=1", "-Wno-unused-variable", "-Winit-self", "-Wno-extra");
+
+ private final String iosSdkVersion;
+ private final String iosMinimumOs;
+ private final String iosSimulatorVersion;
+ private final String iosCpu;
+ private final String xcodeOptions;
+ private final boolean generateDebugSymbols;
+ private final List<String> copts;
+ private final CompilationMode compilationMode;
+
+ ObjcConfiguration(ObjcCommandLineOptions objcOptions, BuildConfiguration.Options options) {
+ this.iosSdkVersion = Preconditions.checkNotNull(objcOptions.iosSdkVersion, "iosSdkVersion");
+ this.iosMinimumOs = Preconditions.checkNotNull(objcOptions.iosMinimumOs, "iosMinimumOs");
+ this.iosSimulatorVersion =
+ Preconditions.checkNotNull(objcOptions.iosSimulatorVersion, "iosSimulatorVersion");
+ this.iosCpu = Preconditions.checkNotNull(objcOptions.iosCpu, "iosCpu");
+ this.xcodeOptions = Preconditions.checkNotNull(objcOptions.xcodeOptions, "xcodeOptions");
+ this.generateDebugSymbols = objcOptions.generateDebugSymbols;
+ this.copts = ImmutableList.copyOf(objcOptions.copts);
+ this.compilationMode = Preconditions.checkNotNull(options.compilationMode, "compilationMode");
+ }
+
+ public String getIosSdkVersion() {
+ return iosSdkVersion;
+ }
+
+ /**
+ * Returns the minimum iOS version supported by binaries and libraries. Any dependencies on newer
+ * iOS version features or libraries will become weak dependencies which are only loaded if the
+ * runtime OS supports them.
+ */
+ public String getMinimumOs() {
+ return iosMinimumOs;
+ }
+
+ public String getIosSimulatorVersion() {
+ return iosSimulatorVersion;
+ }
+
+ public String getIosCpu() {
+ return iosCpu;
+ }
+
+ public Platform getPlatform() {
+ return Platform.forArch(getIosCpu());
+ }
+
+ public String getXcodeOptions() {
+ return xcodeOptions;
+ }
+
+ public boolean generateDebugSymbols() {
+ return generateDebugSymbols;
+ }
+
+ /**
+ * Returns the current compilation mode.
+ */
+ public CompilationMode getCompilationMode() {
+ return compilationMode;
+ }
+
+ /**
+ * Returns the default set of clang options for the current compilation mode.
+ */
+ public List<String> getCoptsForCompilationMode() {
+ switch (compilationMode) {
+ case DBG:
+ return DBG_COPTS;
+ case FASTBUILD:
+ return FASTBUILD_COPTS;
+ case OPT:
+ return OPT_COPTS;
+ default:
+ throw new AssertionError();
+ }
+ }
+
+ /**
+ * Returns options passed to (Apple) clang when compiling Objective C. These options should be
+ * applied after any default options but before options specified in the attributes of the rule.
+ */
+ public List<String> getCopts() {
+ return copts;
+ }
+
+ @Override
+ public String getName() {
+ return "Objective-C";
+ }
+
+ @Override
+ public String cacheKey() {
+ return iosSdkVersion;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcConfigurationLoader.java b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcConfigurationLoader.java
new file mode 100644
index 0000000..19713a3
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcConfigurationLoader.java
@@ -0,0 +1,39 @@
+// Copyright 2014 Google Inc. 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.rules.objc;
+
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.analysis.config.BuildOptions;
+import com.google.devtools.build.lib.analysis.config.ConfigurationEnvironment;
+import com.google.devtools.build.lib.analysis.config.ConfigurationFragmentFactory;
+import com.google.devtools.build.lib.analysis.config.InvalidConfigurationException;
+
+/**
+ * A loader that creates ObjcConfiguration instances based on Objective-C configurations and
+ * command-line options.
+ */
+public class ObjcConfigurationLoader implements ConfigurationFragmentFactory {
+ @Override
+ public ObjcConfiguration create(ConfigurationEnvironment env, BuildOptions buildOptions)
+ throws InvalidConfigurationException {
+ return new ObjcConfiguration(buildOptions.get(ObjcCommandLineOptions.class),
+ buildOptions.get(BuildConfiguration.Options.class));
+ }
+
+ @Override
+ public Class<? extends BuildConfiguration.Fragment> creates() {
+ return ObjcConfiguration.class;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcFramework.java b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcFramework.java
new file mode 100644
index 0000000..c6a0037
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcFramework.java
@@ -0,0 +1,60 @@
+// Copyright 2014 Google Inc. 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.rules.objc;
+
+import static com.google.devtools.build.lib.collect.nestedset.Order.STABLE_ORDER;
+
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.rules.RuleConfiguredTargetFactory;
+import com.google.devtools.build.lib.rules.objc.ObjcSdkFrameworks.Attributes;
+
+/**
+ * Implementation for the {@code objc_framework} rule.
+ */
+public class ObjcFramework implements RuleConfiguredTargetFactory {
+ @Override
+ public ConfiguredTarget create(RuleContext ruleContext) throws InterruptedException {
+ Attributes sdkFrameworkAttributes = new Attributes(ruleContext);
+
+ ImmutableList<Artifact> frameworkImports =
+ ruleContext.getPrerequisiteArtifacts("framework_imports", Mode.TARGET).list();
+ ObjcCommon common = new ObjcCommon.Builder(ruleContext)
+ .addFrameworkImports(
+ frameworkImports)
+ .addExtraSdkFrameworks(sdkFrameworkAttributes.sdkFrameworks())
+ .addExtraWeakSdkFrameworks(sdkFrameworkAttributes.weakSdkFrameworks())
+ .addExtraSdkDylibs(sdkFrameworkAttributes.sdkDylibs())
+ .build();
+
+ Iterable<String> containerErrors =
+ ObjcCommon.notInContainerErrors(frameworkImports, ObjcCommon.FRAMEWORK_CONTAINER_TYPE);
+ for (String error : containerErrors) {
+ ruleContext.attributeError("framework_imports", error);
+ }
+
+ return common.configuredTarget(
+ NestedSetBuilder.<Artifact>emptySet(STABLE_ORDER) /* filesToBuild */,
+ Optional.<XcodeProvider>absent(),
+ Optional.of(common.getObjcProvider()),
+ Optional.<XcTestAppProvider>absent(),
+ Optional.<J2ObjcSrcsProvider>absent());
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcFrameworkRule.java b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcFrameworkRule.java
new file mode 100644
index 0000000..7fcfdd3
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcFrameworkRule.java
@@ -0,0 +1,62 @@
+// Copyright 2014 Google Inc. 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.rules.objc;
+
+import static com.google.devtools.build.lib.packages.Attribute.attr;
+import static com.google.devtools.build.lib.packages.Type.LABEL_LIST;
+
+import com.google.devtools.build.lib.analysis.BaseRuleClasses;
+import com.google.devtools.build.lib.analysis.BlazeRule;
+import com.google.devtools.build.lib.analysis.RuleDefinition;
+import com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.RuleClass.Builder;
+import com.google.devtools.build.lib.rules.objc.ObjcRuleClasses.ObjcSdkFrameworksRule;
+import com.google.devtools.build.lib.util.FileTypeSet;
+
+/**
+ * Rule definition for objc_framework.
+ */
+@BlazeRule(name = "objc_framework",
+ factoryClass = ObjcFramework.class,
+ ancestors = { BaseRuleClasses.BaseRule.class, ObjcSdkFrameworksRule.class})
+public class ObjcFrameworkRule implements RuleDefinition {
+
+ @Override
+ public RuleClass build(Builder builder, RuleDefinitionEnvironment environment) {
+ return builder
+ /* <!-- #BLAZE_RULE(objc_framework).ATTRIBUTE(framework_imports) -->
+ The list of files under a <code>.framework</code> directory which are
+ provided to Objective-C targets that depend on this target.
+ <i>(List of <a href="build-ref.html#labels">labels</a>; required)</i>
+ <!-- #END_BLAZE_RULE.ATTRIBUTE --> */
+ .add(attr("framework_imports", LABEL_LIST)
+ .allowedFileTypes(FileTypeSet.ANY_FILE)
+ .mandatory()
+ .nonEmpty())
+ .build();
+ }
+}
+
+/*<!-- #BLAZE_RULE (NAME = objc_framework, TYPE = LIBRARY, FAMILY = Objective-C) -->
+
+${ATTRIBUTE_SIGNATURE}
+
+<p>This rule encapsulates an already-built framework. It is defined by a list
+of files in one or more <code>.framework</code> directories.
+
+${ATTRIBUTE_DEFINITION}
+
+<!-- #END_BLAZE_RULE -->*/
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcImport.java b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcImport.java
new file mode 100644
index 0000000..70743ed
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcImport.java
@@ -0,0 +1,69 @@
+// Copyright 2014 Google Inc. 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.rules.objc;
+
+import static com.google.devtools.build.lib.rules.objc.XcodeProductType.LIBRARY_STATIC;
+
+import com.google.common.base.Optional;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.rules.RuleConfiguredTargetFactory;
+import com.google.devtools.build.lib.rules.objc.ObjcCommon.CompilationAttributes;
+import com.google.devtools.build.lib.rules.objc.ObjcCommon.ResourceAttributes;
+
+/**
+ * Implementation for {@code objc_import}.
+ */
+public class ObjcImport implements RuleConfiguredTargetFactory {
+ @Override
+ public ConfiguredTarget create(RuleContext ruleContext) throws InterruptedException {
+ ObjcCommon common = new ObjcCommon.Builder(ruleContext)
+ .setCompilationAttributes(new CompilationAttributes(ruleContext))
+ .setResourceAttributes(new ResourceAttributes(ruleContext))
+ .setIntermediateArtifacts(ObjcRuleClasses.intermediateArtifacts(ruleContext))
+ .setAlwayslink(ruleContext.attributes().get("alwayslink", Type.BOOLEAN))
+ .addExtraImportLibraries(
+ ruleContext.getPrerequisiteArtifacts("archives", Mode.TARGET).list())
+ .build();
+
+ XcodeProvider.Builder xcodeProviderBuilder = new XcodeProvider.Builder();
+ NestedSetBuilder<Artifact> filesToBuild = NestedSetBuilder.stableOrder();
+
+ new CompilationSupport(ruleContext)
+ .addXcodeSettings(xcodeProviderBuilder, common, OptionsProvider.DEFAULT)
+ .validateAttributes();
+
+ new ResourceSupport(ruleContext)
+ .registerActions(common.getStoryboards())
+ .validateAttributes()
+ .addXcodeSettings(xcodeProviderBuilder);
+
+ new XcodeSupport(ruleContext)
+ .addXcodeSettings(xcodeProviderBuilder, common.getObjcProvider(), LIBRARY_STATIC)
+ .registerActions(xcodeProviderBuilder.build())
+ .addFilesToBuild(filesToBuild);
+
+ return common.configuredTarget(
+ filesToBuild.build(),
+ Optional.of(xcodeProviderBuilder.build()),
+ Optional.of(common.getObjcProvider()),
+ Optional.<XcTestAppProvider>absent(),
+ Optional.<J2ObjcSrcsProvider>absent());
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcImportRule.java b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcImportRule.java
new file mode 100644
index 0000000..24e9412
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcImportRule.java
@@ -0,0 +1,81 @@
+// Copyright 2014 Google Inc. 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.rules.objc;
+
+import static com.google.devtools.build.lib.packages.Attribute.attr;
+import static com.google.devtools.build.lib.packages.Type.BOOLEAN;
+import static com.google.devtools.build.lib.packages.Type.LABEL_LIST;
+
+import com.google.devtools.build.lib.analysis.BlazeRule;
+import com.google.devtools.build.lib.analysis.RuleDefinition;
+import com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.RuleClass.Builder;
+import com.google.devtools.build.lib.rules.objc.ObjcRuleClasses.ObjcCompilationRule;
+import com.google.devtools.build.lib.util.FileType;
+
+/**
+ * Rule definition for {@code objc_import}.
+ */
+@BlazeRule(name = "objc_import",
+ factoryClass = ObjcImport.class,
+ ancestors = { ObjcCompilationRule.class })
+public class ObjcImportRule implements RuleDefinition {
+ @Override
+ public RuleClass build(Builder builder, RuleDefinitionEnvironment environment) {
+ return builder
+ /*<!-- #BLAZE_RULE(objc_import).IMPLICIT_OUTPUTS -->
+ <ul>
+ <li><code><var>name</var>.xcodeproj/project.pbxproj</code>: An Xcode project file which
+ can be used to develop or build on a Mac.</li>
+ </ul>
+ <!-- #END_BLAZE_RULE.IMPLICIT_OUTPUTS -->*/
+ .setImplicitOutputsFunction(XcodeSupport.PBXPROJ)
+ /* <!-- #BLAZE_RULE(objc_import).ATTRIBUTE(archives) -->
+ The list of <code>.a</code> files provided to Objective-C targets that
+ depend on this target.
+ ${SYNOPSIS}
+ <!-- #END_BLAZE_RULE.ATTRIBUTE --> */
+ .add(attr("archives", LABEL_LIST)
+ .mandatory()
+ .nonEmpty()
+ .allowedFileTypes(FileType.of(".a")))
+ /* <!-- #BLAZE_RULE(objc_import).ATTRIBUTE(alwayslink) -->
+ If 1, any bundle or binary that depends (directly or indirectly) on this
+ library will link in all the archive files listed in
+ <code>archives</code>, even if some contain no symbols referenced by the
+ binary.
+ ${SYNOPSIS}
+ This is useful if your code isn't explicitly called by code in
+ the binary, e.g., if your code registers to receive some callback
+ provided by some service.
+ <!-- #END_BLAZE_RULE.ATTRIBUTE -->*/
+ .add(attr("alwayslink", BOOLEAN))
+ .removeAttribute("deps")
+ .build();
+ }
+}
+
+/*<!-- #BLAZE_RULE (NAME = objc_import, TYPE = LIBRARY, FAMILY = Objective-C) -->
+
+${ATTRIBUTE_SIGNATURE}
+
+<p>This rule encapsulates an already-compiled static library in the form of an
+<code>.a</code> file. It also allows exporting headers and resources using the same
+attributes supported by <code>objc_library</code>.</p>
+
+${ATTRIBUTE_DEFINITION}
+
+<!-- #END_BLAZE_RULE -->*/
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcLibrary.java b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcLibrary.java
new file mode 100644
index 0000000..4136ffe
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcLibrary.java
@@ -0,0 +1,132 @@
+// Copyright 2014 Google Inc. 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.rules.objc;
+
+import static com.google.devtools.build.lib.rules.objc.XcodeProductType.LIBRARY_STATIC;
+
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.rules.RuleConfiguredTargetFactory;
+import com.google.devtools.build.lib.rules.objc.ObjcCommon.CompilationAttributes;
+import com.google.devtools.build.lib.rules.objc.ObjcCommon.ResourceAttributes;
+
+/**
+ * Implementation for {@code objc_library}.
+ */
+public class ObjcLibrary implements RuleConfiguredTargetFactory {
+
+ /**
+ * An {@link IterableWrapper} containing extra library {@link Artifact}s to be linked into the
+ * final ObjC application bundle.
+ */
+ static final class ExtraImportLibraries extends IterableWrapper<Artifact> {
+ ExtraImportLibraries(Artifact... extraImportLibraries) {
+ super(extraImportLibraries);
+ }
+ }
+
+ /**
+ * An {@link IterableWrapper} containing defines as specified in the {@code defines} attribute to
+ * be applied to this target and all depending targets' compilation actions.
+ */
+ static final class Defines extends IterableWrapper<String> {
+ Defines(Iterable<String> defines) {
+ super(defines);
+ }
+
+ Defines(String... defines) {
+ super(defines);
+ }
+ }
+
+ /**
+ * Constructs an {@link ObjcCommon} instance based on the attributes of the given rule. The rule
+ * should inherit from {@link ObjcLibraryRule}..
+ */
+ static ObjcCommon common(RuleContext ruleContext, Iterable<SdkFramework> extraSdkFrameworks,
+ boolean alwayslink, ExtraImportLibraries extraImportLibraries, Defines defines,
+ Iterable<ObjcProvider> extraDepObjcProviders) {
+ CompilationArtifacts compilationArtifacts =
+ CompilationSupport.compilationArtifacts(ruleContext);
+
+ return new ObjcCommon.Builder(ruleContext)
+ .setCompilationAttributes(new CompilationAttributes(ruleContext))
+ .setResourceAttributes(new ResourceAttributes(ruleContext))
+ .addExtraSdkFrameworks(extraSdkFrameworks)
+ .addDefines(defines)
+ .setCompilationArtifacts(compilationArtifacts)
+ .addDepObjcProviders(ruleContext.getPrerequisites("deps", Mode.TARGET, ObjcProvider.class))
+ .addDepObjcProviders(
+ ruleContext.getPrerequisites("bundles", Mode.TARGET, ObjcProvider.class))
+ .addNonPropagatedDepObjcProviders(ruleContext.getPrerequisites("non_propagated_deps",
+ Mode.TARGET, ObjcProvider.class))
+ .setIntermediateArtifacts(ObjcRuleClasses.intermediateArtifacts(ruleContext))
+ .setAlwayslink(alwayslink)
+ .addExtraImportLibraries(extraImportLibraries)
+ .addDepObjcProviders(extraDepObjcProviders)
+ .build();
+ }
+
+ @Override
+ public ConfiguredTarget create(RuleContext ruleContext) throws InterruptedException {
+ ObjcCommon common = common(
+ ruleContext, ImmutableList.<SdkFramework>of(),
+ ruleContext.attributes().get("alwayslink", Type.BOOLEAN), new ExtraImportLibraries(),
+ new Defines(ruleContext.getTokenizedStringListAttr("defines")),
+ ImmutableList.<ObjcProvider>of());
+ OptionsProvider optionsProvider = optionsProvider(ruleContext);
+
+ XcodeProvider.Builder xcodeProviderBuilder = new XcodeProvider.Builder();
+ NestedSetBuilder<Artifact> filesToBuild = NestedSetBuilder.<Artifact>stableOrder()
+ .addAll(common.getCompiledArchive().asSet());
+
+ new CompilationSupport(ruleContext)
+ .registerCompileAndArchiveActions(common, optionsProvider)
+ .addXcodeSettings(xcodeProviderBuilder, common, optionsProvider)
+ .validateAttributes();
+
+ new ResourceSupport(ruleContext)
+ .registerActions(common.getStoryboards())
+ .validateAttributes()
+ .addXcodeSettings(xcodeProviderBuilder);
+
+ new XcodeSupport(ruleContext)
+ .addFilesToBuild(filesToBuild)
+ .addXcodeSettings(xcodeProviderBuilder, common.getObjcProvider(), LIBRARY_STATIC)
+ .addDependencies(xcodeProviderBuilder)
+ .registerActions(xcodeProviderBuilder.build());
+
+ return common.configuredTarget(
+ filesToBuild.build(),
+ Optional.of(xcodeProviderBuilder.build()),
+ Optional.of(common.getObjcProvider()),
+ Optional.<XcTestAppProvider>absent(),
+ Optional.of(ObjcRuleClasses.j2ObjcSrcsProvider(ruleContext)));
+ }
+
+ private OptionsProvider optionsProvider(RuleContext ruleContext) {
+ return new OptionsProvider.Builder()
+ .addCopts(ruleContext.getTokenizedStringListAttr("copts"))
+ .addTransitive(Optional.fromNullable(
+ ruleContext.getPrerequisite("options", Mode.TARGET, OptionsProvider.class)))
+ .build();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcLibraryRule.java b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcLibraryRule.java
new file mode 100644
index 0000000..f721492
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcLibraryRule.java
@@ -0,0 +1,157 @@
+// Copyright 2014 Google Inc. 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.rules.objc;
+
+import static com.google.devtools.build.lib.packages.Attribute.attr;
+import static com.google.devtools.build.lib.packages.Type.BOOLEAN;
+import static com.google.devtools.build.lib.packages.Type.LABEL;
+import static com.google.devtools.build.lib.packages.Type.LABEL_LIST;
+import static com.google.devtools.build.lib.packages.Type.STRING_LIST;
+import static com.google.devtools.build.lib.rules.objc.ObjcRuleClasses.NON_ARC_SRCS_TYPE;
+import static com.google.devtools.build.lib.rules.objc.ObjcRuleClasses.SRCS_TYPE;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.analysis.BlazeRule;
+import com.google.devtools.build.lib.analysis.RuleDefinition;
+import com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.RuleClass.Builder;
+import com.google.devtools.build.lib.rules.objc.ObjcRuleClasses.ObjcCompilationRule;
+import com.google.devtools.build.lib.util.FileType;
+
+/**
+ * Rule definition for objc_library.
+ */
+@BlazeRule(name = "objc_library",
+ factoryClass = ObjcLibrary.class,
+ ancestors = { ObjcCompilationRule.class,
+ ObjcRuleClasses.ObjcOptsRule.class })
+public class ObjcLibraryRule implements RuleDefinition {
+ private static final Iterable<String> ALLOWED_DEPS_RULE_CLASSES = ImmutableSet.of(
+ "objc_library",
+ "objc_import",
+ "objc_bundle",
+ "objc_framework",
+ "objc_bundle_library",
+ "objc_proto_library",
+ "j2objc_library");
+
+ @Override
+ public RuleClass build(Builder builder, RuleDefinitionEnvironment env) {
+ return builder
+ /*<!-- #BLAZE_RULE(objc_library).IMPLICIT_OUTPUTS -->
+ <ul>
+ <li><code><var>name</var>.xcodeproj/project.pbxproj</code>: An Xcode project file which
+ can be used to develop or build on a Mac.</li>
+ </ul>
+ <!-- #END_BLAZE_RULE.IMPLICIT_OUTPUTS -->*/
+ .setImplicitOutputsFunction(XcodeSupport.PBXPROJ)
+ /* <!-- #BLAZE_RULE(objc_library).ATTRIBUTE(srcs) -->
+ The list of C, C++, Objective-C, and Objective-C++ files that are
+ processed to create the library target.
+ ${SYNOPSIS}
+ These are your checked-in source files, plus any generated files.
+ These are compiled into .o files with Clang, so headers should not go
+ here (see the hdrs attribute).
+ <!-- #END_BLAZE_RULE.ATTRIBUTE -->*/
+ .add(attr("srcs", LABEL_LIST)
+ .direct_compile_time_input()
+ .allowedFileTypes(SRCS_TYPE))
+ /* <!-- #BLAZE_RULE(objc_library).ATTRIBUTE(non_arc_srcs) -->
+ The list of Objective-C files that are processed to create the
+ library target that DO NOT use ARC.
+ ${SYNOPSIS}
+ The files in this attribute are treated very similar to those in the
+ srcs attribute, but are compiled without ARC enabled.
+ <!-- #END_BLAZE_RULE.ATTRIBUTE -->*/
+ .add(attr("non_arc_srcs", LABEL_LIST)
+ .direct_compile_time_input()
+ .allowedFileTypes(NON_ARC_SRCS_TYPE))
+ /* <!-- #BLAZE_RULE(objc_library).ATTRIBUTE(pch) -->
+ Header file to prepend to every source file being compiled (both arc
+ and non-arc). Note that the file will not be precompiled - this is
+ simply a convenience, not a build-speed enhancement.
+ <!-- #END_BLAZE_RULE.ATTRIBUTE -->*/
+ .add(attr("pch", LABEL)
+ .direct_compile_time_input()
+ .allowedFileTypes(FileType.of(".pch")))
+ .add(attr("options", LABEL)
+ .undocumented("objc_options will probably be removed")
+ .allowedFileTypes()
+ .allowedRuleClasses("objc_options"))
+ /* <!-- #BLAZE_RULE(objc_library).ATTRIBUTE(alwayslink) -->
+ If 1, any bundle or binary that depends (directly or indirectly) on this
+ library will link in all the object files for the files listed in
+ <code>srcs</code> and <code>non_arc_srcs</code>, even if some contain no
+ symbols referenced by the binary.
+ ${SYNOPSIS}
+ This is useful if your code isn't explicitly called by code in
+ the binary, e.g., if your code registers to receive some callback
+ provided by some service.
+ <!-- #END_BLAZE_RULE.ATTRIBUTE -->*/
+ .add(attr("alwayslink", BOOLEAN))
+ /* <!-- #BLAZE_RULE(objc_library).ATTRIBUTE(deps) -->
+ The list of targets that are linked together to form the final bundle.
+ ${SYNOPSIS}
+ <!-- #END_BLAZE_RULE.ATTRIBUTE -->*/
+ .override(attr("deps", LABEL_LIST)
+ .direct_compile_time_input()
+ .allowedRuleClasses(ALLOWED_DEPS_RULE_CLASSES)
+ .allowedFileTypes())
+ /* <!-- #BLAZE_RULE(objc_library).ATTRIBUTE(bundles) -->
+ The list of bundle targets that this target requires to be included in the final bundle.
+ ${SYNOPSIS}
+ <!-- #END_BLAZE_RULE.ATTRIBUTE -->*/
+ .add(attr("bundles", LABEL_LIST)
+ .direct_compile_time_input()
+ .allowedRuleClasses("objc_bundle", "objc_bundle_library")
+ .allowedFileTypes())
+ /* <!-- #BLAZE_RULE(objc_library).ATTRIBUTE(non_propagated_deps) -->
+ The list of targets that are required in order to build this target,
+ but which are not included in the final bundle.
+ <br />
+ This attribute should only rarely be used, and probably only for proto
+ dependencies.
+ ${SYNOPSIS}
+ <!-- #END_BLAZE_RULE.ATTRIBUTE -->*/
+ .add(attr("non_propagated_deps", LABEL_LIST)
+ .direct_compile_time_input()
+ .allowedRuleClasses(ALLOWED_DEPS_RULE_CLASSES)
+ .allowedFileTypes())
+ /* <!-- #BLAZE_RULE(objc_library).ATTRIBUTE(defines) -->
+ Extra <code>-D</code> flags to pass to the compiler. They should be in
+ the form <code>KEY=VALUE</code> or simply <code>KEY</code> and are
+ passed not only the compiler for this target (as <code>copts</code>
+ are) but also to all <code>objc_</code> dependers of this target.
+ ${SYNOPSIS}
+ Subject to <a href="#make_variables">"Make variable"</a> substitution and
+ <a href="#sh-tokenization">Bourne shell tokenization</a>.
+ <!-- #END_BLAZE_RULE.ATTRIBUTE -->*/
+ .add(attr("defines", STRING_LIST))
+ .build();
+ }
+}
+
+/*<!-- #BLAZE_RULE (NAME = objc_library, TYPE = LIBRARY, FAMILY = Objective-C) -->
+
+${ATTRIBUTE_SIGNATURE}
+
+<p>This rule produces a static library from the given Objective-C source files.</p>
+
+${IMPLICIT_OUTPUTS}
+
+${ATTRIBUTE_DEFINITION}
+
+<!-- #END_BLAZE_RULE -->*/
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcOptions.java b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcOptions.java
new file mode 100644
index 0000000..a7e2b8f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcOptions.java
@@ -0,0 +1,40 @@
+// Copyright 2014 Google Inc. 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.rules.objc;
+
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTargetBuilder;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.RunfilesProvider;
+import com.google.devtools.build.lib.rules.RuleConfiguredTargetFactory;
+
+/**
+ * Implementation for the {@code objc_options} rule.
+ */
+public class ObjcOptions implements RuleConfiguredTargetFactory {
+ @Override
+ public ConfiguredTarget create(RuleContext ruleContext) {
+ return new RuleConfiguredTargetBuilder(ruleContext)
+ .add(RunfilesProvider.class, RunfilesProvider.EMPTY)
+ .add(OptionsProvider.class,
+ new OptionsProvider.Builder()
+ .addCopts(ruleContext.getTokenizedStringListAttr("copts"))
+ .addInfoplists(
+ ruleContext.getPrerequisiteArtifacts("infoplists", Mode.TARGET).list())
+ .build())
+ .build();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcOptionsRule.java b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcOptionsRule.java
new file mode 100644
index 0000000..7f26bfe
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcOptionsRule.java
@@ -0,0 +1,67 @@
+// Copyright 2014 Google Inc. 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.rules.objc;
+
+import static com.google.devtools.build.lib.packages.Attribute.attr;
+import static com.google.devtools.build.lib.rules.objc.ObjcRuleClasses.PLIST_TYPE;
+
+import com.google.devtools.build.lib.analysis.BaseRuleClasses.BaseRule;
+import com.google.devtools.build.lib.analysis.BlazeRule;
+import com.google.devtools.build.lib.analysis.RuleDefinition;
+import com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.RuleClass.Builder;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.rules.objc.ObjcRuleClasses.ObjcOptsRule;
+
+/**
+ * Rule definition for {@code objc_options}.
+ */
+@BlazeRule(name = "objc_options",
+ factoryClass = ObjcOptions.class,
+ ancestors = { BaseRule.class, ObjcOptsRule.class })
+public class ObjcOptionsRule implements RuleDefinition {
+ @Override
+ public RuleClass build(Builder builder, RuleDefinitionEnvironment env) {
+ return builder
+ // TODO(bazel-team): Figure out if we really need objc_options, and if
+ // we don't, delete it.
+ .setUndocumented()
+ /* <!-- #BLAZE_RULE(objc_options).ATTRIBUTE(xcode_name)[DEPRECATED] -->
+ This attribute is ignored and will be removed.
+ <!-- #END_BLAZE_RULE.ATTRIBUTE -->*/
+ .add(attr("xcode_name", Type.STRING))
+ /* <!-- #BLAZE_RULE(objc_options).ATTRIBUTE(infoplists) -->
+ infoplist files to merge with the final binary's infoplist. This
+ corresponds to a single file <i>appname</i>-Info.plist in Xcode
+ projects.
+ <i>(List of <a href="build-ref.html#labels">labels</a>; optional)</i>
+ <!-- #END_BLAZE_RULE.ATTRIBUTE -->*/
+ .add(attr("infoplists", Type.LABEL_LIST)
+ .allowedFileTypes(PLIST_TYPE))
+ .build();
+ }
+}
+
+/*<!-- #BLAZE_RULE (NAME = objc_options, TYPE = OTHER, FAMILY = Objective-C) -->
+
+${ATTRIBUTE_SIGNATURE}
+
+<p>This rule provides a nameable set of build settings to use when building
+Objective-C targets.</p>
+
+${ATTRIBUTE_DEFINITION}
+
+<!-- #END_BLAZE_RULE -->*/
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcProtoLibrary.java b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcProtoLibrary.java
new file mode 100644
index 0000000..647b221
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcProtoLibrary.java
@@ -0,0 +1,242 @@
+// Copyright 2014 Google Inc. 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.rules.objc;
+
+import static com.google.common.base.CaseFormat.LOWER_UNDERSCORE;
+import static com.google.common.base.CaseFormat.UPPER_CAMEL;
+import static com.google.devtools.build.lib.rules.objc.XcodeProductType.LIBRARY_STATIC;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.AnalysisUtils;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.actions.CustomCommandLine;
+import com.google.devtools.build.lib.analysis.actions.FileWriteAction;
+import com.google.devtools.build.lib.analysis.actions.SpawnAction;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.rules.RuleConfiguredTargetFactory;
+import com.google.devtools.build.lib.rules.proto.ProtoSourcesProvider;
+import com.google.devtools.build.lib.util.FileType;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import javax.annotation.Nullable;
+
+/**
+ * Implementation for the "objc_proto_library" rule.
+ */
+public class ObjcProtoLibrary implements RuleConfiguredTargetFactory {
+ private static final Function<Artifact, PathFragment> PARENT_PATHFRAGMENT =
+ new Function<Artifact, PathFragment>() {
+ @Override
+ public PathFragment apply(Artifact input) {
+ return input.getExecPath().getParentDirectory();
+ }
+ };
+
+ @VisibleForTesting
+ static final String NO_PROTOS_ERROR =
+ "no protos to compile - a non-empty deps attribute is required";
+
+ @Override
+ public ConfiguredTarget create(final RuleContext ruleContext) throws InterruptedException {
+ Artifact compileProtos = ruleContext.getPrerequisiteArtifact(
+ ObjcRuleClasses.ObjcProtoRule.COMPILE_PROTOS_ATTR, Mode.HOST);
+ Optional<Artifact> optionsFile = Optional.fromNullable(
+ ruleContext.getPrerequisiteArtifact(ObjcProtoLibraryRule.OPTIONS_FILE_ATTR, Mode.HOST));
+ NestedSet<Artifact> protos = NestedSetBuilder.<Artifact>stableOrder()
+ .addAll(ruleContext.getPrerequisiteArtifacts("deps", Mode.TARGET)
+ .filter(FileType.of(".proto"))
+ .list())
+ .addTransitive(maybeGetProtoSources(ruleContext))
+ .build();
+
+ if (Iterables.isEmpty(protos)) {
+ ruleContext.ruleError(NO_PROTOS_ERROR);
+ }
+
+ ImmutableList<Artifact> libProtobuf = ruleContext
+ .getPrerequisiteArtifacts(ObjcProtoLibraryRule.LIBPROTOBUF_ATTR, Mode.TARGET)
+ .list();
+ ImmutableList<Artifact> protoSupport = ruleContext
+ .getPrerequisiteArtifacts(ObjcRuleClasses.ObjcProtoRule.PROTO_SUPPORT_ATTR, Mode.HOST)
+ .list();
+
+ // Generate sources in a package-and-rule-scoped directory; adds both the
+ // package-and-rule-scoped directory and the header-containing-directory to the include path of
+ // dependers.
+ PathFragment rootRelativeOutputDir = new PathFragment(
+ ruleContext.getLabel().getPackageFragment(),
+ new PathFragment("_generated_protos_" + ruleContext.getLabel().getName()));
+ PathFragment workspaceRelativeOutputDir = new PathFragment(
+ ruleContext.getBinOrGenfilesDirectory().getExecPath(), rootRelativeOutputDir);
+ PathFragment generatedProtoDir =
+ new PathFragment(workspaceRelativeOutputDir, ruleContext.getLabel().getPackageFragment());
+
+ boolean outputCpp =
+ ruleContext.attributes().get(ObjcProtoLibraryRule.OUTPUT_CPP_ATTR, Type.BOOLEAN);
+
+ ImmutableList<Artifact> protoGeneratedSources = outputArtifacts(
+ ruleContext, rootRelativeOutputDir, protos, FileType.of(".pb." + (outputCpp ? "cc" : "m")),
+ outputCpp);
+ ImmutableList<Artifact> protoGeneratedHeaders = outputArtifacts(
+ ruleContext, rootRelativeOutputDir, protos, FileType.of(".pb.h"), outputCpp);
+
+ Artifact inputFileList = ruleContext.getAnalysisEnvironment().getDerivedArtifact(
+ AnalysisUtils.getUniqueDirectory(ruleContext.getLabel(), new PathFragment("_protos"))
+ .getRelative("_proto_input_files"),
+ ruleContext.getConfiguration().getGenfilesDirectory());
+
+ ruleContext.registerAction(new FileWriteAction(
+ ruleContext.getActionOwner(),
+ inputFileList,
+ ObjcActionsBuilder.joinExecPaths(protos),
+ false));
+
+ CustomCommandLine.Builder commandLineBuilder = new CustomCommandLine.Builder()
+ .add(compileProtos.getExecPathString())
+ .add("--input-file-list").add(inputFileList.getExecPathString())
+ .add("--output-dir").add(workspaceRelativeOutputDir.getSafePathString());
+ if (optionsFile.isPresent()) {
+ commandLineBuilder
+ .add("--compiler-options-path")
+ .add(optionsFile.get().getExecPathString());
+ }
+ if (outputCpp) {
+ commandLineBuilder.add("--generate-cpp");
+ }
+
+ if (!Iterables.isEmpty(protos)) {
+ ruleContext.registerAction(new SpawnAction.Builder()
+ .setMnemonic("GenObjcProtos")
+ .addInput(compileProtos)
+ .addInputs(optionsFile.asSet())
+ .addInputs(protos)
+ .addInput(inputFileList)
+ .addInputs(libProtobuf)
+ .addInputs(protoSupport)
+ .addOutputs(Iterables.concat(protoGeneratedSources, protoGeneratedHeaders))
+ .setExecutable(new PathFragment("/usr/bin/python"))
+ .setCommandLine(commandLineBuilder.build())
+ .setExecutionInfo(ImmutableMap.of(ExecutionRequirements.REQUIRES_DARWIN, ""))
+ .build(ruleContext));
+ }
+
+ IntermediateArtifacts intermediateArtifacts =
+ ObjcRuleClasses.intermediateArtifacts(ruleContext);
+ CompilationArtifacts compilationArtifacts = new CompilationArtifacts.Builder()
+ .addNonArcSrcs(protoGeneratedSources)
+ .setIntermediateArtifacts(intermediateArtifacts)
+ .setPchFile(Optional.<Artifact>absent())
+ .build();
+
+ ImmutableSet<PathFragment> searchPathEntries = new ImmutableSet.Builder<PathFragment>()
+ .add(workspaceRelativeOutputDir)
+ .add(generatedProtoDir)
+ .addAll(Iterables.transform(protoGeneratedHeaders, PARENT_PATHFRAGMENT))
+ .build();
+ ObjcCommon common = new ObjcCommon.Builder(ruleContext)
+ .setCompilationArtifacts(compilationArtifacts)
+ .addUserHeaderSearchPaths(searchPathEntries)
+ .addDepObjcProviders(ruleContext.getPrerequisites(
+ ObjcProtoLibraryRule.LIBPROTOBUF_ATTR, Mode.TARGET, ObjcProvider.class))
+ .setIntermediateArtifacts(intermediateArtifacts)
+ .addHeaders(protoGeneratedHeaders)
+ .addHeaders(protoGeneratedSources)
+ .build();
+
+ XcodeProvider xcodeProvider = new XcodeProvider.Builder()
+ .setLabel(ruleContext.getLabel())
+ .addUserHeaderSearchPaths(searchPathEntries)
+ .addDependencies(ruleContext.getPrerequisites(
+ ObjcProtoLibraryRule.LIBPROTOBUF_ATTR, Mode.TARGET, XcodeProvider.class))
+ .addCopts(ObjcRuleClasses.objcConfiguration(ruleContext).getCopts())
+ .setProductType(LIBRARY_STATIC)
+ .addHeaders(protoGeneratedHeaders)
+ .setCompilationArtifacts(common.getCompilationArtifacts().get())
+ .setObjcProvider(common.getObjcProvider())
+ .build();
+
+ ObjcActionsBuilder actionsBuilder = ObjcRuleClasses.actionsBuilder(ruleContext);
+ actionsBuilder
+ .registerCompileAndArchiveActions(
+ compilationArtifacts, common.getObjcProvider(), OptionsProvider.DEFAULT);
+ actionsBuilder.registerXcodegenActions(
+ new ObjcRuleClasses.Tools(ruleContext),
+ ruleContext.getImplicitOutputArtifact(XcodeSupport.PBXPROJ),
+ XcodeProvider.Project.fromTopLevelTarget(xcodeProvider));
+
+ return common.configuredTarget(
+ NestedSetBuilder.<Artifact>stableOrder()
+ .addAll(common.getCompiledArchive().asSet())
+ .addAll(protoGeneratedSources)
+ .addAll(protoGeneratedHeaders)
+ .add(ruleContext.getImplicitOutputArtifact(XcodeSupport.PBXPROJ))
+ .build(),
+ Optional.of(xcodeProvider),
+ Optional.of(common.getObjcProvider()),
+ Optional.<XcTestAppProvider>absent(),
+ Optional.<J2ObjcSrcsProvider>absent());
+ }
+
+ private NestedSet<Artifact> maybeGetProtoSources(RuleContext ruleContext) {
+ NestedSetBuilder<Artifact> artifacts = new NestedSetBuilder<>(Order.STABLE_ORDER);
+ Iterable<ProtoSourcesProvider> providers =
+ ruleContext.getPrerequisites("deps", Mode.TARGET, ProtoSourcesProvider.class);
+ for (ProtoSourcesProvider provider : providers) {
+ artifacts.addTransitive(provider.getTransitiveProtoSources());
+ }
+ return artifacts.build();
+ }
+
+ private ImmutableList<Artifact> outputArtifacts(RuleContext ruleContext,
+ PathFragment rootRelativeOutputDir, Iterable<Artifact> protos, FileType newFileType,
+ boolean outputCpp) {
+ ImmutableList.Builder<Artifact> builder = new ImmutableList.Builder<>();
+ for (Artifact proto : protos) {
+ String protoOutputName;
+ if (outputCpp) {
+ protoOutputName = proto.getFilename();
+ } else {
+ String lowerUnderscoreBaseName = proto.getFilename().replace('-', '_').toLowerCase();
+ protoOutputName = LOWER_UNDERSCORE.to(UPPER_CAMEL, lowerUnderscoreBaseName);
+ }
+ PathFragment rawFragment = new PathFragment(
+ rootRelativeOutputDir,
+ proto.getExecPath().getParentDirectory(),
+ new PathFragment(protoOutputName));
+ @Nullable PathFragment outputFile = FileSystemUtils.replaceExtension(
+ rawFragment,
+ newFileType.getExtensions().get(0),
+ ".proto");
+ if (outputFile != null) {
+ builder.add(ruleContext.getAnalysisEnvironment().getDerivedArtifact(
+ outputFile, ruleContext.getBinOrGenfilesDirectory()));
+ }
+ }
+ return builder.build();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcProtoLibraryRule.java b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcProtoLibraryRule.java
new file mode 100644
index 0000000..a25f96e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcProtoLibraryRule.java
@@ -0,0 +1,80 @@
+// Copyright 2014 Google Inc. 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.rules.objc;
+
+import static com.google.devtools.build.lib.packages.Attribute.ConfigurationTransition.HOST;
+import static com.google.devtools.build.lib.packages.Attribute.attr;
+import static com.google.devtools.build.lib.packages.Type.BOOLEAN;
+import static com.google.devtools.build.lib.packages.Type.LABEL;
+import static com.google.devtools.build.lib.packages.Type.LABEL_LIST;
+
+import com.google.devtools.build.lib.analysis.BaseRuleClasses;
+import com.google.devtools.build.lib.analysis.BlazeRule;
+import com.google.devtools.build.lib.analysis.RuleDefinition;
+import com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.RuleClass.Builder;
+
+/**
+ * Rule definition for objc_proto_library.
+ *
+ * This is a temporary rule until it is better known how to support proto_library rules.
+ */
+@BlazeRule(name = "objc_proto_library",
+ factoryClass = ObjcProtoLibrary.class,
+ ancestors = { BaseRuleClasses.RuleBase.class, ObjcRuleClasses.ObjcProtoRule.class })
+public class ObjcProtoLibraryRule implements RuleDefinition {
+ static final String OPTIONS_FILE_ATTR = "options_file";
+ static final String OUTPUT_CPP_ATTR = "output_cpp";
+ static final String LIBPROTOBUF_ATTR = "$lib_protobuf";
+
+ @Override
+ public RuleClass build(Builder builder, final RuleDefinitionEnvironment env) {
+ return builder
+ /* <!-- #BLAZE_RULE(objc_proto_library).ATTRIBUTE(deps) -->
+ The directly depended upon proto_library rules.
+ <!-- #END_BLAZE_RULE.ATTRIBUTE -->*/
+ .override(attr("deps", LABEL_LIST)
+ .allowedRuleClasses("proto_library", "filegroup")
+ .legacyAllowAnyFileType())
+ /* <!-- #BLAZE_RULE(objc_proto_library).ATTRIBUTE(options_file) -->
+ Optional options file to apply to protos which affects compilation (e.g. class
+ whitelist/blacklist settings).
+ <!-- #END_BLAZE_RULE.ATTRIBUTE -->*/
+ .add(attr(OPTIONS_FILE_ATTR, LABEL).legacyAllowAnyFileType().singleArtifact().cfg(HOST))
+ /* <!-- #BLAZE_RULE(objc_proto_library).ATTRIBUTE(output_cpp) -->
+ If true, output C++ rather than ObjC.
+ <!-- #END_BLAZE_RULE.ATTRIBUTE -->*/
+ .add(attr(OUTPUT_CPP_ATTR, BOOLEAN).value(false))
+ // TODO(bazel-team): Use //external:objc_proto_lib when bind() support is a little better
+ .add(attr(LIBPROTOBUF_ATTR, LABEL).allowedRuleClasses("objc_library")
+ .value(env.getLabel(
+ "//googlemac/ThirdParty/ProtocolBuffers2/objectivec:ProtocolBuffers_lib")))
+ .add(attr("$xcodegen", LABEL).cfg(HOST).exec()
+ .value(env.getLabel("//tools/objc:xcodegen")))
+ .build();
+ }
+}
+
+/*<!-- #BLAZE_RULE (NAME = objc_proto_library, TYPE = LIBRARY, FAMILY = Objective-C) -->
+
+${ATTRIBUTE_SIGNATURE}
+
+<p>This rule produces a static library from the given proto_library dependencies, after applying an
+options file.</p>
+
+${ATTRIBUTE_DEFINITION}
+
+<!-- #END_BLAZE_RULE -->*/
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcProvider.java b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcProvider.java
new file mode 100644
index 0000000..c48710e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcProvider.java
@@ -0,0 +1,313 @@
+// Copyright 2014 Google Inc. 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.rules.objc;
+
+import static com.google.devtools.build.lib.collect.nestedset.Order.LINK_ORDER;
+import static com.google.devtools.build.lib.collect.nestedset.Order.STABLE_ORDER;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.xcode.xcodegen.proto.XcodeGenProtos.TargetControl;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A provider that provides all compiling and linking information in the transitive closure of its
+ * deps that are needed for building Objective-C rules.
+ */
+@Immutable
+public final class ObjcProvider implements TransitiveInfoProvider {
+ /**
+ * Represents one of the things this provider can provide transitively. Things are provided as
+ * {@link NestedSet}s of type E.
+ */
+ public static class Key<E> {
+ private final Order order;
+
+ private Key(Order order) {
+ this.order = Preconditions.checkNotNull(order);
+ }
+ }
+
+ public static final Key<Artifact> LIBRARY = new Key<>(LINK_ORDER);
+ public static final Key<Artifact> IMPORTED_LIBRARY = new Key<>(LINK_ORDER);
+
+ /**
+ * Single-architecture linked binaries to be combined for the final multi-architecture binary.
+ */
+ public static final Key<Artifact> LINKED_BINARY = new Key<>(STABLE_ORDER);
+
+ /**
+ * Indicates which libraries to load with {@code -force_load}. This is a subset of the union of
+ * the {@link #LIBRARY} and {@link #IMPORTED_LIBRARY} sets.
+ */
+ public static final Key<Artifact> FORCE_LOAD_LIBRARY = new Key<>(LINK_ORDER);
+
+ /**
+ * Libraries to pass with -force_load flags when setting the linkopts in Xcodegen. This is needed
+ * in addition to {@link #FORCE_LOAD_LIBRARY} because that one, contains a mixture of import
+ * archives (which are not built by Xcode) and built-from-source library archives (which are built
+ * by Xcode). Archives that are built by Xcode are placed directly under
+ * {@code BUILT_PRODUCTS_DIR} while those not built by Xcode appear somewhere in the Bazel
+ * workspace under {@code WORKSPACE_ROOT}.
+ */
+ public static final Key<String> FORCE_LOAD_FOR_XCODEGEN = new Key<>(LINK_ORDER);
+
+ public static final Key<Artifact> HEADER = new Key<>(STABLE_ORDER);
+
+ /**
+ * Include search paths specified with {@code -I} on the command line. Also known as header search
+ * paths (and distinct from <em>user</em> header search paths).
+ */
+ public static final Key<PathFragment> INCLUDE = new Key<>(LINK_ORDER);
+
+ /**
+ * Key for values in {@code defines} attributes. These are passed as {@code -D} flags to all
+ * invocations of the compiler for this target and all depending targets.
+ */
+ public static final Key<String> DEFINE = new Key<>(STABLE_ORDER);
+
+ public static final Key<Artifact> ASSET_CATALOG = new Key<>(STABLE_ORDER);
+
+ /**
+ * Added to {@link TargetControl#getGeneralResourceFileList()} when running Xcodegen.
+ */
+ public static final Key<Artifact> GENERAL_RESOURCE_FILE = new Key<>(STABLE_ORDER);
+
+ /**
+ * Exec paths of {@code .bundle} directories corresponding to imported bundles to link.
+ * These are passed to Xcodegen.
+ */
+ public static final Key<PathFragment> BUNDLE_IMPORT_DIR = new Key<>(STABLE_ORDER);
+
+ /**
+ * Files that are plopped into the final bundle at some arbitrary bundle path. Note that these are
+ * not passed to Xcodegen, and these don't include information about where the file originated
+ * from.
+ */
+ public static final Key<BundleableFile> BUNDLE_FILE = new Key<>(STABLE_ORDER);
+
+ public static final Key<PathFragment> XCASSETS_DIR = new Key<>(STABLE_ORDER);
+ public static final Key<String> SDK_DYLIB = new Key<>(STABLE_ORDER);
+ public static final Key<SdkFramework> SDK_FRAMEWORK = new Key<>(STABLE_ORDER);
+ public static final Key<SdkFramework> WEAK_SDK_FRAMEWORK = new Key<>(STABLE_ORDER);
+ public static final Key<Xcdatamodel> XCDATAMODEL = new Key<>(STABLE_ORDER);
+ public static final Key<Flag> FLAG = new Key<>(STABLE_ORDER);
+
+ /**
+ * Merge zips to include in the bundle. The entries of these zip files are included in the final
+ * bundle with the same path. The entries in the merge zips should not include the bundle root
+ * path (e.g. {@code Foo.app}).
+ */
+ public static final Key<Artifact> MERGE_ZIP = new Key<>(STABLE_ORDER);
+
+ /**
+ * Exec paths of {@code .framework} directories corresponding to frameworks to link. These cause
+ * -F arguments (framework search paths) to be added to each compile action, and -framework (link
+ * framework) arguments to be added to each link action.
+ */
+ public static final Key<PathFragment> FRAMEWORK_DIR = new Key<>(LINK_ORDER);
+
+ /**
+ * Files in {@code .framework} directories that should be included as inputs when compiling and
+ * linking.
+ */
+ public static final Key<Artifact> FRAMEWORK_FILE = new Key<>(STABLE_ORDER);
+
+ /**
+ * Bundles which should be linked in as a nested bundle to the final application.
+ */
+ public static final Key<Bundling> NESTED_BUNDLE = new Key<>(STABLE_ORDER);
+
+ /**
+ * Artifact containing information on debug symbols
+ */
+ public static final Key<Artifact> DEBUG_SYMBOLS = new Key<>(STABLE_ORDER);
+
+ /**
+ * Flags that apply to a transitive build dependency tree. Each item in the enum corresponds to a
+ * flag. If the item is included in the key {@link #FLAG}, then the flag is considered set.
+ */
+ public enum Flag {
+ /**
+ * Indicates that C++ (or Objective-C++) is used in any source file. This affects how the linker
+ * is invoked.
+ */
+ USES_CPP;
+ }
+
+ private final ImmutableMap<Key<?>, NestedSet<?>> items;
+
+ // Items which should be passed to direct dependers, but not transitive dependers.
+ private final ImmutableMap<Key<?>, NestedSet<?>> nonPropagatedItems;
+
+ private ObjcProvider(
+ ImmutableMap<Key<?>, NestedSet<?>> items,
+ ImmutableMap<Key<?>, NestedSet<?>> nonPropagatedItems) {
+ this.items = Preconditions.checkNotNull(items);
+ this.nonPropagatedItems = Preconditions.checkNotNull(nonPropagatedItems);
+ }
+
+ /**
+ * All artifacts, bundleable files, etc. of the type specified by {@code key}.
+ */
+ @SuppressWarnings("unchecked")
+ public <E> NestedSet<E> get(Key<E> key) {
+ Preconditions.checkNotNull(key);
+ NestedSetBuilder<E> builder = new NestedSetBuilder<>(key.order);
+ if (nonPropagatedItems.containsKey(key)) {
+ builder.addTransitive((NestedSet<E>) nonPropagatedItems.get(key));
+ }
+ if (items.containsKey(key)) {
+ builder.addTransitive((NestedSet<E>) items.get(key));
+ }
+ return builder.build();
+ }
+
+ /**
+ * Indicates whether {@code flag} is set on this provider.
+ */
+ public boolean is(Flag flag) {
+ return Iterables.contains(get(FLAG), flag);
+ }
+
+ /**
+ * Indicates whether this provider has any asset catalogs. This is true whenever some target in
+ * its transitive dependency tree specifies a non-empty {@code asset_catalogs} attribute.
+ */
+ public boolean hasAssetCatalogs() {
+ return !get(XCASSETS_DIR).isEmpty();
+ }
+
+ /**
+ * A builder for this context with an API that is optimized for collecting information from
+ * several transitive dependencies.
+ */
+ public static final class Builder {
+ private final Map<Key<?>, NestedSetBuilder<?>> items = new HashMap<>();
+ private final Map<Key<?>, NestedSetBuilder<?>> nonPropagatedItems = new HashMap<>();
+
+ private static void maybeAddEmptyBuilder(Map<Key<?>, NestedSetBuilder<?>> set, Key<?> key) {
+ if (!set.containsKey(key)) {
+ set.put(key, new NestedSetBuilder<>(key.order));
+ }
+ }
+
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ private void uncheckedAddAll(Key key, Iterable toAdd) {
+ maybeAddEmptyBuilder(items, key);
+ items.get(key).addAll(toAdd);
+ }
+
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ private void uncheckedAddTransitive(Key key, NestedSet toAdd, boolean propagate) {
+ Map<Key<?>, NestedSetBuilder<?>> set = propagate ? items : nonPropagatedItems;
+ maybeAddEmptyBuilder(set, key);
+ set.get(key).addTransitive(toAdd);
+ }
+
+ /**
+ * Adds elements in items, and propagate them to any (transitive) dependers on this
+ * ObjcProvider.
+ */
+ public <E> Builder addTransitiveAndPropagate(Key<E> key, NestedSet<E> items) {
+ uncheckedAddTransitive(key, items, true);
+ return this;
+ }
+
+ /**
+ * Add all elements from provider, and propagate them to any (transitive) dependers on this
+ * ObjcProvider.
+ */
+ public Builder addTransitiveAndPropagate(ObjcProvider provider) {
+ for (Map.Entry<Key<?>, NestedSet<?>> typeEntry : provider.items.entrySet()) {
+ uncheckedAddTransitive(typeEntry.getKey(), typeEntry.getValue(), true);
+ }
+ return this;
+ }
+
+ /**
+ * Add all elements from a single key of the given provider, and propagate them to any
+ * (transitive) dependers on this ObjcProvider.
+ */
+ public <E> Builder addTransitiveAndPropagate(Key<E> key, ObjcProvider provider) {
+ addTransitiveAndPropagate(key, provider.get(key));
+ return this;
+ }
+
+ /**
+ * Add all elements from providers, and propagate them to any (transitive) dependers on this
+ * ObjcProvider.
+ */
+ public Builder addTransitiveAndPropagate(Iterable<ObjcProvider> providers) {
+ for (ObjcProvider provider : providers) {
+ addTransitiveAndPropagate(provider);
+ }
+ return this;
+ }
+
+ /**
+ * Add elements from providers, but don't propagate them to any dependers on this ObjcProvider.
+ * These elements will be exposed to {@link #get(Key)} calls, but not to any ObjcProviders
+ * which add this provider to themself.
+ */
+ public Builder addTransitiveWithoutPropagating(Iterable<ObjcProvider> providers) {
+ for (ObjcProvider provider : providers) {
+ for (Map.Entry<Key<?>, NestedSet<?>> typeEntry : provider.items.entrySet()) {
+ uncheckedAddTransitive(typeEntry.getKey(), typeEntry.getValue(), false);
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Add element, and propagate it to any (transitive) dependers on this ObjcProvider.
+ */
+ public <E> Builder add(Key<E> key, E toAdd) {
+ uncheckedAddAll(key, ImmutableList.of(toAdd));
+ return this;
+ }
+
+ /**
+ * Add elements in toAdd, and propagate them to any (transitive) dependers on this ObjcProvider.
+ */
+ public <E> Builder addAll(Key<E> key, Iterable<? extends E> toAdd) {
+ uncheckedAddAll(key, toAdd);
+ return this;
+ }
+
+ public ObjcProvider build() {
+ ImmutableMap.Builder<Key<?>, NestedSet<?>> propagated = new ImmutableMap.Builder<>();
+ for (Map.Entry<Key<?>, NestedSetBuilder<?>> typeEntry : items.entrySet()) {
+ propagated.put(typeEntry.getKey(), typeEntry.getValue().build());
+ }
+ ImmutableMap.Builder<Key<?>, NestedSet<?>> nonPropagated = new ImmutableMap.Builder<>();
+ for (Map.Entry<Key<?>, NestedSetBuilder<?>> typeEntry : nonPropagatedItems.entrySet()) {
+ nonPropagated.put(typeEntry.getKey(), typeEntry.getValue().build());
+ }
+ return new ObjcProvider(propagated.build(), nonPropagated.build());
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcRuleClasses.java b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcRuleClasses.java
new file mode 100644
index 0000000..ad1e4dc
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcRuleClasses.java
@@ -0,0 +1,531 @@
+// Copyright 2014 Google Inc. 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.rules.objc;
+
+import static com.google.devtools.build.lib.packages.Attribute.ConfigurationTransition.HOST;
+import static com.google.devtools.build.lib.packages.Attribute.attr;
+import static com.google.devtools.build.lib.packages.Type.BOOLEAN;
+import static com.google.devtools.build.lib.packages.Type.LABEL;
+import static com.google.devtools.build.lib.packages.Type.LABEL_LIST;
+import static com.google.devtools.build.lib.packages.Type.STRING_LIST;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Optional;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.BaseRuleClasses;
+import com.google.devtools.build.lib.analysis.BaseRuleClasses.BaseRule;
+import com.google.devtools.build.lib.analysis.BlazeRule;
+import com.google.devtools.build.lib.analysis.FilesToRunProvider;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.RuleDefinition;
+import com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.packages.Attribute;
+import com.google.devtools.build.lib.packages.AttributeMap;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.RuleClass.Builder;
+import com.google.devtools.build.lib.packages.RuleClass.Builder.RuleClassType;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.util.FileType;
+import com.google.devtools.build.lib.util.FileTypeSet;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+/**
+ * Shared utility code for Objective-C rules.
+ */
+public class ObjcRuleClasses {
+
+ private ObjcRuleClasses() {
+ throw new UnsupportedOperationException("static-only");
+ }
+
+ /**
+ * Returns a derived Artifact by appending a String to a root-relative path. This is similar to
+ * {@link RuleContext#getRelatedArtifact(PathFragment, String)}, except the existing extension is
+ * not removed.
+ */
+ static Artifact artifactByAppendingToRootRelativePath(
+ RuleContext ruleContext, PathFragment path, String suffix) {
+ return ruleContext.getAnalysisEnvironment().getDerivedArtifact(
+ path.replaceName(path.getBaseName() + suffix),
+ ruleContext.getBinOrGenfilesDirectory());
+ }
+
+ static IntermediateArtifacts intermediateArtifacts(RuleContext ruleContext) {
+ return new IntermediateArtifacts(
+ ruleContext.getAnalysisEnvironment(), ruleContext.getBinOrGenfilesDirectory(),
+ ruleContext.getLabel(), /*archiveFileNameSuffix=*/"");
+ }
+
+ /**
+ * Returns a {@link IntermediateArtifacts} to be used to compile and link the ObjC source files
+ * in {@code j2ObjcSource}.
+ */
+ static IntermediateArtifacts j2objcIntermediateArtifacts(RuleContext ruleContext,
+ J2ObjcSource j2ObjcSource) {
+ // We need to append "_j2objc" to the name of the generated archive file to distinguish it from
+ // the C/C++ archive file created by proto_library targets with attribute cc_api_version
+ // specified.
+ return new IntermediateArtifacts(
+ ruleContext.getAnalysisEnvironment(),
+ ruleContext.getConfiguration().getBinDirectory(),
+ j2ObjcSource.getTargetLabel(),
+ /*archiveFileNameSuffix=*/"_j2objc");
+ }
+
+ /**
+ * Returns a {@link J2ObjcSrcsProvider} with J2ObjC-generated ObjC file information from the
+ * current rule, and from rules that can be reached transitively through the "deps" attribute.
+ *
+ * @param ruleContext the rule context of the current rule
+ * @param currentSource J2ObjC-generated ObjC file information from the current rule to contribute
+ * to the returned provider
+ * @return a {@link J2ObjcSrcsProvider} containing {@code currentSources} and source information
+ * from the transitive closure.
+ */
+ public static J2ObjcSrcsProvider j2ObjcSrcsProvider(RuleContext ruleContext,
+ J2ObjcSource currentSource) {
+ return j2ObjcSrcsProvider(ruleContext, Optional.of(currentSource));
+ }
+
+ /**
+ * Returns a {@link J2ObjcSrcsProvider} with J2ObjC-generated ObjC file information from rules
+ * that can be reached transitively through the "deps" attribute.
+ *
+ * @param ruleContext the rule context of the current rule
+ * @return a {@link J2ObjcSrcsProvider} containing source information from the transitive closure.
+ */
+ public static J2ObjcSrcsProvider j2ObjcSrcsProvider(RuleContext ruleContext) {
+ return j2ObjcSrcsProvider(ruleContext, Optional.<J2ObjcSource>absent());
+ }
+
+ private static J2ObjcSrcsProvider j2ObjcSrcsProvider(RuleContext ruleContext,
+ Optional<J2ObjcSource> currentSource) {
+ NestedSetBuilder<J2ObjcSource> builder = NestedSetBuilder.stableOrder();
+ builder.addAll(currentSource.asSet());
+ boolean hasProtos = currentSource.isPresent()
+ && currentSource.get().getSourceType() == J2ObjcSource.SourceType.PROTO;
+
+ for (J2ObjcSrcsProvider provider :
+ ruleContext.getPrerequisites("deps", Mode.TARGET, J2ObjcSrcsProvider.class)) {
+ builder.addTransitive(provider.getSrcs());
+ hasProtos |= provider.hasProtos();
+ }
+
+ return new J2ObjcSrcsProvider(builder.build(), hasProtos);
+ }
+
+ public static Artifact artifactByAppendingToBaseName(RuleContext context, String suffix) {
+ return artifactByAppendingToRootRelativePath(
+ context, context.getLabel().toPathFragment(), suffix);
+ }
+
+ static ObjcActionsBuilder actionsBuilder(RuleContext ruleContext) {
+ return new ObjcActionsBuilder(
+ ruleContext,
+ intermediateArtifacts(ruleContext),
+ ObjcRuleClasses.objcConfiguration(ruleContext),
+ ruleContext.getConfiguration(),
+ ruleContext);
+ }
+
+ public static ObjcConfiguration objcConfiguration(RuleContext ruleContext) {
+ return ruleContext.getFragment(ObjcConfiguration.class);
+ }
+
+ @VisibleForTesting
+ static final Iterable<SdkFramework> AUTOMATIC_SDK_FRAMEWORKS = ImmutableList.of(
+ new SdkFramework("Foundation"), new SdkFramework("UIKit"));
+
+ /**
+ * Attributes for {@code objc_*} rules that have compiler (and in the future, possibly linker)
+ * options
+ */
+ @BlazeRule(name = "$objc_opts_rule",
+ type = RuleClassType.ABSTRACT)
+ public static class ObjcOptsRule implements RuleDefinition {
+ @Override
+ public RuleClass build(Builder builder, RuleDefinitionEnvironment environment) {
+ return builder
+ /* <!-- #BLAZE_RULE($objc_opts_rule).ATTRIBUTE(copts) -->
+ Extra flags to pass to the compiler.
+ ${SYNOPSIS}
+ Subject to <a href="#make_variables">"Make variable"</a> substitution and
+ <a href="#sh-tokenization">Bourne shell tokenization</a>.
+ These flags will only apply to this target, and not those upon which
+ it depends, or those which depend on it.
+ <!-- #END_BLAZE_RULE.ATTRIBUTE -->*/
+ .add(attr("copts", STRING_LIST))
+ .build();
+ }
+ }
+
+ /**
+ * Attributes for {@code objc_*} rules that can link in SDK frameworks.
+ */
+ @BlazeRule(name = "$objc_sdk_frameworks_rule",
+ type = RuleClassType.ABSTRACT)
+ public static class ObjcSdkFrameworksRule implements RuleDefinition {
+ @Override
+ public RuleClass build(Builder builder, RuleDefinitionEnvironment environment) {
+ return builder
+ /* <!-- #BLAZE_RULE($objc_sdk_frameworks_rule).ATTRIBUTE(sdk_frameworks) -->
+ Names of SDK frameworks to link with. For instance, "XCTest" or
+ "Cocoa". "UIKit" and "Foundation" are always included and do not mean
+ anything if you include them.
+ When linking a library, only those frameworks named in that library's
+ sdk_frameworks attribute are linked in. When linking a binary, all
+ SDK frameworks named in that binary's transitive dependency graph are
+ used.
+ <!-- #END_BLAZE_RULE.ATTRIBUTE -->*/
+ .add(attr("sdk_frameworks", STRING_LIST))
+ /* <!-- #BLAZE_RULE($objc_sdk_frameworks_rule).ATTRIBUTE(weak_sdk_frameworks) -->
+ Names of SDK frameworks to weakly link with. For instance,
+ "MediaAccessibility". In difference to regularly linked SDK
+ frameworks, symbols from weakly linked frameworks do not cause an
+ error if they are not present at runtime.
+ <!-- #END_BLAZE_RULE.ATTRIBUTE -->*/
+ .add(attr("weak_sdk_frameworks", STRING_LIST))
+ /* <!-- #BLAZE_RULE($objc_sdk_frameworks_rule).ATTRIBUTE(sdk_dylibs) -->
+ Names of SDK .dylib libraries to link with. For instance, "libz" or
+ "libarchive". "libc++" is included automatically if the binary has
+ any C++ or Objective-C++ sources in its dependency tree. When linking
+ a binary, all libraries named in that binary's transitive dependency
+ graph are used.
+ <!-- #END_BLAZE_RULE.ATTRIBUTE -->*/
+ .add(attr("sdk_dylibs", STRING_LIST))
+ .build();
+ }
+ }
+
+ /**
+ * Iff a file matches this type, it is considered to use C++.
+ */
+ static final FileType CPP_SOURCES = FileType.of(".cc", ".cpp", ".mm", ".cxx", ".C");
+
+ private static final FileType NON_CPP_SOURCES = FileType.of(".m", ".c");
+
+ static final FileTypeSet SRCS_TYPE = FileTypeSet.of(NON_CPP_SOURCES, CPP_SOURCES);
+
+ static final FileTypeSet NON_ARC_SRCS_TYPE = FileTypeSet.of(FileType.of(".m", ".mm"));
+
+ static final FileTypeSet PLIST_TYPE = FileTypeSet.of(FileType.of(".plist"));
+
+ static final FileTypeSet STORYBOARD_TYPE = FileTypeSet.of(FileType.of(".storyboard"));
+
+ static final FileType XIB_TYPE = FileType.of(".xib");
+
+ /**
+ * Common attributes for {@code objc_*} rules that allow the definition of resources such as
+ * storyboards.
+ */
+ @BlazeRule(name = "$objc_base_resources_rule",
+ type = RuleClassType.ABSTRACT,
+ ancestors = { BaseRule.class })
+ public static class ObjcBaseResourcesRule implements RuleDefinition {
+ @Override
+ public RuleClass build(Builder builder, RuleDefinitionEnvironment env) {
+ return builder
+ /* <!-- #BLAZE_RULE($objc_base_resources_rule).ATTRIBUTE(strings) -->
+ Files which are plists of strings, often localizable. These files
+ are converted to binary plists (if they are not already) and placed
+ in the bundle root of the final package. If this file's immediate
+ containing directory is named *.lproj (e.g. en.lproj, Base.lproj), it
+ will be placed under a directory of that name in the final bundle.
+ This allows for localizable strings.
+ <!-- #END_BLAZE_RULE.ATTRIBUTE -->*/
+ .add(attr("strings", LABEL_LIST).legacyAllowAnyFileType()
+ .direct_compile_time_input())
+ /* <!-- #BLAZE_RULE($objc_base_resources_rule).ATTRIBUTE(xibs) -->
+ Files which are .xib resources, possibly localizable. These files are
+ compiled to .nib files and placed the bundle root of the final
+ package. If this file's immediate containing directory is named
+ *.lproj (e.g. en.lproj, Base.lproj), it will be placed under a
+ directory of that name in the final bundle. This allows for
+ localizable UI.
+ <!-- #END_BLAZE_RULE.ATTRIBUTE -->*/
+ .add(attr("xibs", LABEL_LIST)
+ .direct_compile_time_input()
+ .allowedFileTypes(XIB_TYPE))
+ /* <!-- #BLAZE_RULE($objc_base_resources_rule).ATTRIBUTE(storyboards) -->
+ Files which are .storyboard resources, possibly localizable. These
+ files are compiled to .storyboardc directories, which are placed in
+ the bundle root of the final package. If the storyboards's immediate
+ containing directory is named *.lproj (e.g. en.lproj, Base.lproj), it
+ will be placed under a directory of that name in the final bundle.
+ This allows for localizable UI.
+ <!-- #END_BLAZE_RULE.ATTRIBUTE -->*/
+ .add(attr("storyboards", LABEL_LIST)
+ .allowedFileTypes(STORYBOARD_TYPE))
+ /* <!-- #BLAZE_RULE($objc_base_resources_rule).ATTRIBUTE(resources) -->
+ Files to include in the final application bundle. They are not
+ processed or compiled in any way besides the processing done by the
+ rules that actually generate them. These files are placed in the root
+ of the bundle (e.g. Payload/foo.app/...) in most cases. However, if
+ they appear to be localized (i.e. are contained in a directory called
+ *.lproj), they will be placed in a directory of the same name in the
+ app bundle.
+ <!-- #END_BLAZE_RULE.ATTRIBUTE -->*/
+ .add(attr("resources", LABEL_LIST).legacyAllowAnyFileType().direct_compile_time_input())
+ /* <!-- #BLAZE_RULE($objc_base_resources_rule).ATTRIBUTE(datamodels) -->
+ Files that comprise the data models of the final linked binary.
+ Each file must have a containing directory named *.xcdatamodel, which
+ is usually contained by another *.xcdatamodeld (note the added d)
+ directory.
+ <!-- #END_BLAZE_RULE.ATTRIBUTE -->*/
+ .add(attr("datamodels", LABEL_LIST).legacyAllowAnyFileType()
+ .direct_compile_time_input())
+ /* <!-- #BLAZE_RULE($objc_base_resources_rule).ATTRIBUTE(asset_catalogs) -->
+ Files that comprise the asset catalogs of the final linked binary.
+ Each file must have a containing directory named *.xcassets. This
+ containing directory becomes the root of one of the asset catalogs
+ linked with any binary that depends directly or indirectly on this
+ target.
+ <!-- #END_BLAZE_RULE.ATTRIBUTE -->*/
+ .add(attr("asset_catalogs", LABEL_LIST).legacyAllowAnyFileType()
+ .direct_compile_time_input())
+ .add(attr("$xcodegen", LABEL).cfg(HOST).exec()
+ .value(env.getLabel("//tools/objc:xcodegen")))
+ .add(attr("$plmerge", LABEL).cfg(HOST).exec()
+ .value(env.getLabel("//tools/objc:plmerge")))
+ .add(attr("$momczip_deploy", LABEL).cfg(HOST)
+ .value(env.getLabel("//tools/objc:momczip_deploy.jar")))
+ .add(attr("$actooloribtoolzip_deploy", LABEL).cfg(HOST)
+ .value(env.getLabel("//tools/objc:actooloribtoolzip_deploy.jar")))
+ .build();
+ }
+ }
+
+ /**
+ * Common attributes for {@code objc_*} rules that contain compilable content.
+ */
+ @BlazeRule(name = "$objc_compilation_rule",
+ type = RuleClassType.ABSTRACT,
+ ancestors = { BaseRuleClasses.RuleBase.class, ObjcSdkFrameworksRule.class,
+ ObjcBaseResourcesRule.class })
+ public static class ObjcCompilationRule implements RuleDefinition {
+ @Override
+ public RuleClass build(Builder builder, RuleDefinitionEnvironment env) {
+ return builder
+ /* <!-- #BLAZE_RULE($objc_compilation_rule).ATTRIBUTE(hdrs) -->
+ The list of Objective-C files that are included as headers by source
+ files in this rule or by users of this library.
+ ${SYNOPSIS}
+ <!-- #END_BLAZE_RULE.ATTRIBUTE -->*/
+ .add(attr("hdrs", LABEL_LIST)
+ .direct_compile_time_input()
+ .allowedFileTypes(FileTypeSet.ANY_FILE))
+ /* <!-- #BLAZE_RULE($objc_compilation_rule).ATTRIBUTE(includes) -->
+ List of <code>#include/#import</code> search paths to add to this target
+ and all depending targets. This is to support third party and
+ open-sourced libraries that do not specify the entire workspace path in
+ their <code>#import/#include</code> statements.
+ <p>
+ The paths are interpreted relative to the package directory, and the
+ genfiles and bin roots (e.g. <code>blaze-genfiles/pkg/includedir</code>
+ and <code>blaze-out/pkg/includedir</code>) are included in addition to the
+ actual client root.
+ <!-- #END_BLAZE_RULE.ATTRIBUTE -->*/
+ .add(attr("includes", Type.STRING_LIST))
+ /* <!-- #BLAZE_RULE($objc_compilation_rule).ATTRIBUTE(sdk_includes) -->
+ List of <code>#include/#import</code> search paths to add to this target
+ and all depending targets, where each path is relative to
+ <code>$(SDKROOT)/usr/include</code>.
+ <!-- #END_BLAZE_RULE.ATTRIBUTE -->*/
+ .add(attr("sdk_includes", Type.STRING_LIST))
+ .build();
+ }
+ }
+
+ /**
+ * Common attributes for rules that uses ObjC proto compiler.
+ */
+ @BlazeRule(name = "$objc_proto_rule",
+ type = RuleClassType.ABSTRACT)
+ public static class ObjcProtoRule implements RuleDefinition {
+
+ /**
+ * A Predicate that returns true if the ObjC proto compiler and its support deps are needed by
+ * the current rule.
+ *
+ * <p>For proto_library rules, this will return true if they have a j2objc_api_version
+ * attribute, and it is greater than 0. For other rules, this will return true by default.
+ */
+ public static final Predicate<AttributeMap> USE_PROTO_COMPILER = new Predicate<AttributeMap>() {
+ @Override
+ public boolean apply(AttributeMap rule) {
+ return rule.getAttributeDefinition("j2objc_api_version") == null
+ || rule.get("j2objc_api_version", Type.INTEGER) != 0;
+ }
+ };
+
+ public static final String COMPILE_PROTOS_ATTR = "$googlemac_proto_compiler";
+ public static final String PROTO_SUPPORT_ATTR = "$googlemac_proto_compiler_support";
+
+ @Override
+ public RuleClass build(Builder builder, RuleDefinitionEnvironment env) {
+ return builder
+ .add(attr(COMPILE_PROTOS_ATTR, LABEL)
+ .allowedFileTypes(FileType.of(".py"))
+ .cfg(HOST)
+ .singleArtifact()
+ .condition(USE_PROTO_COMPILER)
+ .value(env.getLabel("//tools/objc:compile_protos")))
+ .add(attr(PROTO_SUPPORT_ATTR, LABEL)
+ .legacyAllowAnyFileType()
+ .cfg(HOST)
+ .condition(USE_PROTO_COMPILER)
+ .value(env.getLabel("//tools/objc:proto_support")))
+ .build();
+ }
+ }
+
+ /**
+ * Base rule definition for iOS test rules.
+ */
+ @BlazeRule(name = "$ios_test_base_rule",
+ type = RuleClassType.ABSTRACT,
+ ancestors = { ObjcBinaryRule.class })
+ public static class IosTestBaseRule implements RuleDefinition {
+ @Override
+ public RuleClass build(Builder builder, final RuleDefinitionEnvironment env) {
+ return builder
+ /* <!-- #BLAZE_RULE($ios_test_base_rule).ATTRIBUTE(target_device) -->
+ The device against which to run the test.
+ ${SYNOPSIS}
+ <!-- #END_BLAZE_RULE.ATTRIBUTE -->*/
+ .add(attr(IosTest.TARGET_DEVICE, LABEL)
+ .allowedFileTypes()
+ .allowedRuleClasses("ios_device"))
+ /* <!-- #BLAZE_RULE($ios_test_base_rule).ATTRIBUTE(xctest) -->
+ Whether this target contains tests using the XCTest testing framework.
+ ${SYNOPSIS}
+ <!-- #END_BLAZE_RULE.ATTRIBUTE -->*/
+ .add(attr(IosTest.IS_XCTEST, BOOLEAN))
+ /* <!-- #BLAZE_RULE($ios_test_base_rule).ATTRIBUTE(xctest_app) -->
+ A <code>objc_binary</code> target that contains the app bundle to test against in XCTest.
+ This attribute is only valid if <code>xctest</code> is true.
+ ${SYNOPSIS}
+ <!-- #END_BLAZE_RULE.ATTRIBUTE -->*/
+ .add(attr(IosTest.XCTEST_APP, LABEL)
+ .value(new Attribute.ComputedDefault(IosTest.IS_XCTEST) {
+ @Override
+ public Object getDefault(AttributeMap rule) {
+ return rule.get(IosTest.IS_XCTEST, Type.BOOLEAN)
+ ? env.getLabel("//tools/objc:xctest_app")
+ : null;
+ }
+ })
+ .allowedFileTypes()
+ .allowedRuleClasses("objc_binary"))
+ .override(attr("infoplist", LABEL)
+ .value(new Attribute.ComputedDefault(IosTest.IS_XCTEST) {
+ @Override
+ public Object getDefault(AttributeMap rule) {
+ return rule.get(IosTest.IS_XCTEST, Type.BOOLEAN)
+ ? env.getLabel("//tools/objc:xctest_infoplist")
+ : null;
+ }
+ })
+ .allowedFileTypes(PLIST_TYPE))
+ .build();
+ }
+ }
+
+ /**
+ * Abstract rule type with the {@code infoplist} attribute.
+ */
+ @BlazeRule(name = "$objc_has_infoplist_rule",
+ type = RuleClassType.ABSTRACT)
+ public static class ObjcHasInfoplistRule implements RuleDefinition {
+ @Override
+ public RuleClass build(Builder builder, final RuleDefinitionEnvironment env) {
+ return builder
+ /* <!-- #BLAZE_RULE($objc_has_infoplist_rule).ATTRIBUTE(infoplist) -->
+ The infoplist file. This corresponds to <i>appname</i>-Info.plist in Xcode projects.
+ ${SYNOPSIS}
+ Blaze will perform variable substitution on the plist file for the following values:
+ <ul>
+ <li><code>${EXECUTABLE_NAME}</code>: The name of the executable generated and included
+ in the bundle by blaze, which can be used as the value for
+ <code>CFBundleExecutable</code> within the plist.
+ <li><code>${BUNDLE_NAME}</code>: This target's name and bundle suffix (.bundle or .app)
+ in the form<code><var>name</var></code>.<code>suffix</code>.
+ <li><code>${PRODUCT_NAME}</code>: This target's name.
+ </ul>
+ <!-- #END_BLAZE_RULE.ATTRIBUTE -->*/
+ .add(attr("infoplist", LABEL)
+ .allowedFileTypes(PLIST_TYPE))
+ .build();
+ }
+ }
+
+ /**
+ * Abstract rule type with the {@code entitlements} attribute.
+ */
+ @BlazeRule(name = "$objc_has_entitlements_rule",
+ type = RuleClassType.ABSTRACT)
+ public static class ObjcHasEntitlementsRule implements RuleDefinition {
+ @Override
+ public RuleClass build(Builder builder, final RuleDefinitionEnvironment env) {
+ return builder
+ /* <!-- #BLAZE_RULE($objc_has_entitlements_rule).ATTRIBUTE(entitlements) -->
+ The entitlements file required for device builds of this application. See
+ <a href="https://developer.apple.com/library/mac/documentation/Miscellaneous/Reference/EntitlementKeyReference/Chapters/AboutEntitlements.html">the apple documentation</a>
+ for more information. If absent, the default entitlements from the
+ provisioning profile will be used.
+ <p>
+ The following variables are substituted as per
+ <a href="https://developer.apple.com/library/ios/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html">their definitions in Apple's documentation</a>:
+ $(AppIdentifierPrefix) and $(CFBundleIdentifier).
+ <!-- #END_BLAZE_RULE.ATTRIBUTE -->*/
+ .add(attr("entitlements", LABEL).legacyAllowAnyFileType())
+ .build();
+ }
+ }
+
+ /**
+ * Object that supplies tools used by all rules which have the helper tools common to most rule
+ * implementations.
+ */
+ static final class Tools {
+ private final RuleContext ruleContext;
+
+ Tools(RuleContext ruleContext) {
+ this.ruleContext = Preconditions.checkNotNull(ruleContext);
+ }
+
+ Artifact actooloribtoolzipDeployJar() {
+ return ruleContext.getPrerequisiteArtifact("$actooloribtoolzip_deploy", Mode.HOST);
+ }
+
+ Artifact momczipDeployJar() {
+ return ruleContext.getPrerequisiteArtifact("$momczip_deploy", Mode.HOST);
+ }
+
+ FilesToRunProvider xcodegen() {
+ return ruleContext.getExecutablePrerequisite("$xcodegen", Mode.HOST);
+ }
+
+ FilesToRunProvider plmerge() {
+ return ruleContext.getExecutablePrerequisite("$plmerge", Mode.HOST);
+ }
+ }
+}
+
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcSdkFrameworks.java b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcSdkFrameworks.java
new file mode 100644
index 0000000..2ff3a7c
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcSdkFrameworks.java
@@ -0,0 +1,71 @@
+// Copyright 2014 Google Inc. 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.rules.objc;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.rules.objc.ObjcRuleClasses.ObjcSdkFrameworksRule;
+
+/**
+ * Common logic for rules that inherit from {@link ObjcSdkFrameworksRule}.
+ */
+public class ObjcSdkFrameworks {
+
+ /**
+ * Class that handles extraction and processing of attributes common to inheritors of {@link
+ * ObjcSdkFrameworksRule}.
+ */
+ public static class Attributes {
+
+ private final RuleContext ruleContext;
+
+ public Attributes(RuleContext ruleContext) {
+ this.ruleContext = ruleContext;
+ }
+
+ /**
+ * Returns the SDK frameworks defined on the rule's {@code sdk_frameworks} attribute as well as
+ * base frameworks defined in {@link ObjcRuleClasses#AUTOMATIC_SDK_FRAMEWORKS}.
+ */
+ ImmutableSet<SdkFramework> sdkFrameworks() {
+ ImmutableSet.Builder<SdkFramework> result = new ImmutableSet.Builder<>();
+ result.addAll(ObjcRuleClasses.AUTOMATIC_SDK_FRAMEWORKS);
+ for (String explicit : ruleContext.attributes().get("sdk_frameworks", Type.STRING_LIST)) {
+ result.add(new SdkFramework(explicit));
+ }
+ return result.build();
+ }
+
+ /**
+ * Returns all SDK frameworks defined on the rule's {@code weak_sdk_frameworks} attribute.
+ */
+ ImmutableSet<SdkFramework> weakSdkFrameworks() {
+ ImmutableSet.Builder<SdkFramework> result = new ImmutableSet.Builder<>();
+ for (String frameworkName :
+ ruleContext.attributes().get("weak_sdk_frameworks", Type.STRING_LIST)) {
+ result.add(new SdkFramework(frameworkName));
+ }
+ return result.build();
+ }
+
+ /**
+ * Returns all SDK dylibs defined on the rule's {@code sdk_dylibs} attribute.
+ */
+ ImmutableSet<String> sdkDylibs() {
+ return ImmutableSet.copyOf(ruleContext.attributes().get("sdk_dylibs", Type.STRING_LIST));
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcXcodeproj.java b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcXcodeproj.java
new file mode 100644
index 0000000..e167f89
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcXcodeproj.java
@@ -0,0 +1,47 @@
+// Copyright 2015 Google Inc. 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.rules.objc;
+
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTargetBuilder;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.RunfilesProvider;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.rules.RuleConfiguredTargetFactory;
+
+/**
+ * Implementation for {@code objc_xcodeproj}.
+ */
+public class ObjcXcodeproj implements RuleConfiguredTargetFactory {
+
+ @Override
+ public ConfiguredTarget create(RuleContext ruleContext) throws InterruptedException {
+ XcodeProvider.Project project = XcodeProvider.Project.fromTopLevelTargets(
+ ruleContext.getPrerequisites("deps", Mode.TARGET, XcodeProvider.class));
+ Artifact pbxproj = ruleContext.getImplicitOutputArtifact(XcodeSupport.PBXPROJ);
+
+ ObjcActionsBuilder actionsBuilder = ObjcRuleClasses.actionsBuilder(ruleContext);
+ actionsBuilder.registerXcodegenActions(
+ new ObjcRuleClasses.Tools(ruleContext), pbxproj, project);
+
+ return new RuleConfiguredTargetBuilder(ruleContext)
+ .setFilesToBuild(NestedSetBuilder.create(Order.STABLE_ORDER, pbxproj))
+ .addProvider(RunfilesProvider.class, RunfilesProvider.EMPTY)
+ .build();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcXcodeprojRule.java b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcXcodeprojRule.java
new file mode 100644
index 0000000..0a6c4ba
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcXcodeprojRule.java
@@ -0,0 +1,78 @@
+// Copyright 2015 Google Inc. 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.rules.objc;
+
+import static com.google.devtools.build.lib.packages.Attribute.ConfigurationTransition.HOST;
+import static com.google.devtools.build.lib.packages.Attribute.attr;
+import static com.google.devtools.build.lib.packages.Type.BOOLEAN;
+import static com.google.devtools.build.lib.packages.Type.LABEL;
+
+import com.google.devtools.build.lib.analysis.BaseRuleClasses;
+import com.google.devtools.build.lib.analysis.BlazeRule;
+import com.google.devtools.build.lib.analysis.RuleDefinition;
+import com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.RuleClass.Builder;
+
+/**
+ * Rule definition for {@code objc_xcodeproj}.
+ */
+@BlazeRule(name = "objc_xcodeproj",
+ factoryClass = ObjcXcodeproj.class,
+ ancestors = { BaseRuleClasses.RuleBase.class })
+public class ObjcXcodeprojRule implements RuleDefinition {
+ @Override
+ public RuleClass build(Builder builder, RuleDefinitionEnvironment env) {
+ return builder
+ /*<!-- #BLAZE_RULE(objc_xcodeproj).IMPLICIT_OUTPUTS -->
+ <ul>
+ <li><code><var>name</var>.xcodeproj/project.pbxproj</code>: A combined Xcode project file
+ containing all the included targets which can be used to develop or build on a Mac.</li>
+ </ul>
+ <!-- #END_BLAZE_RULE.IMPLICIT_OUTPUTS -->*/
+ .setImplicitOutputsFunction(XcodeSupport.PBXPROJ)
+ /* <!-- #BLAZE_RULE(objc_xcodeproj).ATTRIBUTE(deps) -->
+ The list of targets to include in the combined Xcode project file.
+ ${SYNOPSIS}
+ <!-- #END_BLAZE_RULE.ATTRIBUTE -->*/
+ .override(builder.copy("deps")
+ .nonEmpty()
+ .allowedRuleClasses(
+ "objc_binary",
+ "ios_test",
+ "objc_bundle_library",
+ "objc_import",
+ "objc_library"))
+ .override(attr("testonly", BOOLEAN)
+ .nonconfigurable("Must support test deps.")
+ .value(true))
+ .add(attr("$xcodegen", LABEL)
+ .cfg(HOST)
+ .exec()
+ .value(env.getLabel("//tools/objc:xcodegen")))
+ .build();
+ }
+}
+
+/*<!-- #BLAZE_RULE (NAME = objc_xcodeproj, TYPE = OTHER, FAMILY = Objective-C) -->
+
+${ATTRIBUTE_SIGNATURE}
+
+<p>This rule combines build information about several objc targets (and all their transitive
+dependencies) into a single Xcode project file, for use in developing on a Mac.</p>
+
+${ATTRIBUTE_DEFINITION}
+
+<!-- #END_BLAZE_RULE -->*/
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/OptionsProvider.java b/src/main/java/com/google/devtools/build/lib/rules/objc/OptionsProvider.java
new file mode 100644
index 0000000..f87c96a
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/OptionsProvider.java
@@ -0,0 +1,87 @@
+// Copyright 2014 Google Inc. 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.rules.objc;
+
+import com.google.common.base.Optional;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.xcode.util.Value;
+
+/**
+ * Provides information contained in a {@code objc_options} target.
+ */
+@Immutable
+final class OptionsProvider
+ extends Value<OptionsProvider>
+ implements TransitiveInfoProvider {
+ static final class Builder {
+ private Iterable<String> copts = ImmutableList.of();
+ private final NestedSetBuilder<Artifact> infoplists = NestedSetBuilder.stableOrder();
+
+ /**
+ * Adds copts to the end of the copts sequence.
+ */
+ public Builder addCopts(Iterable<String> copts) {
+ this.copts = Iterables.concat(this.copts, copts);
+ return this;
+ }
+
+ public Builder addInfoplists(Iterable<Artifact> infoplists) {
+ this.infoplists.addAll(infoplists);
+ return this;
+ }
+
+ /**
+ * Adds infoplists and copts from the given provider, if present. copts are added to the end of
+ * the sequence.
+ */
+ public Builder addTransitive(Optional<OptionsProvider> maybeProvider) {
+ for (OptionsProvider provider : maybeProvider.asSet()) {
+ this.copts = Iterables.concat(this.copts, provider.copts);
+ this.infoplists.addTransitive(provider.infoplists);
+ }
+ return this;
+ }
+
+ public OptionsProvider build() {
+ return new OptionsProvider(ImmutableList.copyOf(copts), infoplists.build());
+ }
+ }
+
+ public static final OptionsProvider DEFAULT = new Builder().build();
+
+ private final ImmutableList<String> copts;
+ private final NestedSet<Artifact> infoplists;
+
+ private OptionsProvider(ImmutableList<String> copts, NestedSet<Artifact> infoplists) {
+ super(copts, infoplists);
+ this.copts = Preconditions.checkNotNull(copts);
+ this.infoplists = Preconditions.checkNotNull(infoplists);
+ }
+
+ public ImmutableList<String> getCopts() {
+ return copts;
+ }
+
+ public NestedSet<Artifact> getInfoplists() {
+ return infoplists;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/ResourceSupport.java b/src/main/java/com/google/devtools/build/lib/rules/objc/ResourceSupport.java
new file mode 100644
index 0000000..d1a717c
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/ResourceSupport.java
@@ -0,0 +1,123 @@
+// Copyright 2015 Google Inc. 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.rules.objc;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.analysis.RuleContext;
+
+/**
+ * Support for resource processing on Objc rules.
+ *
+ * <p>Methods on this class can be called in any order without impacting the result.
+ */
+final class ResourceSupport {
+ private final RuleContext ruleContext;
+ private final Attributes attributes;
+ private final IntermediateArtifacts intermediateArtifacts;
+ private final Iterable<Xcdatamodel> datamodels;
+
+ /**
+ * Creates a new resource support for the given context.
+ */
+ ResourceSupport(RuleContext ruleContext) {
+ this.ruleContext = ruleContext;
+ this.attributes = new Attributes(ruleContext);
+ this.intermediateArtifacts = ObjcRuleClasses.intermediateArtifacts(ruleContext);
+ this.datamodels = Xcdatamodels.xcdatamodels(intermediateArtifacts, attributes.datamodels());
+ }
+
+ /**
+ * Registers resource generating actions (strings, storyboards, ...).
+ *
+ * @param storyboards storyboards defined by this rule
+ *
+ * @return this resource support
+ */
+ ResourceSupport registerActions(Storyboards storyboards) {
+ ObjcActionsBuilder actionsBuilder = ObjcRuleClasses.actionsBuilder(ruleContext);
+
+ ObjcRuleClasses.Tools tools = new ObjcRuleClasses.Tools(ruleContext);
+ actionsBuilder.registerResourceActions(
+ tools,
+ new ObjcActionsBuilder.StringsFiles(
+ CompiledResourceFile.fromStringsFiles(intermediateArtifacts, attributes.strings())),
+ new XibFiles(attributes.xibs()),
+ datamodels);
+ for (Artifact storyboardInput : storyboards.getInputs()) {
+ actionsBuilder.registerIbtoolzipAction(
+ tools, storyboardInput, intermediateArtifacts.compiledStoryboardZip(storyboardInput));
+ }
+ return this;
+ }
+
+ /**
+ * Adds common xcode settings to the given provider builder.
+ *
+ * @return this resource support
+ */
+ ResourceSupport addXcodeSettings(XcodeProvider.Builder xcodeProviderBuilder) {
+ xcodeProviderBuilder.addInputsToXcodegen(Xcdatamodel.inputsToXcodegen(datamodels));
+ return this;
+ }
+
+ /**
+ * Validates resource attributes on this rule.
+ *
+ * @return this resource support
+ */
+ ResourceSupport validateAttributes() {
+ Iterable<String> assetCatalogErrors = ObjcCommon.notInContainerErrors(
+ attributes.assetCatalogs(), ObjcCommon.ASSET_CATALOG_CONTAINER_TYPE);
+ for (String error : assetCatalogErrors) {
+ ruleContext.attributeError("asset_catalogs", error);
+ }
+
+ Iterable<String> dataModelErrors =
+ ObjcCommon.notInContainerErrors(attributes.datamodels(), Xcdatamodels.CONTAINER_TYPES);
+ for (String error : dataModelErrors) {
+ ruleContext.attributeError("datamodels", error);
+ }
+
+ return this;
+ }
+
+ private static class Attributes {
+ private final RuleContext ruleContext;
+
+ Attributes(RuleContext ruleContext) {
+ this.ruleContext = ruleContext;
+ }
+
+ ImmutableList<Artifact> datamodels() {
+ return ruleContext.getPrerequisiteArtifacts("datamodels", Mode.TARGET).list();
+ }
+
+ ImmutableList<Artifact> xibs() {
+ return ruleContext.getPrerequisiteArtifacts("xibs", Mode.TARGET)
+ .errorsForNonMatching(ObjcRuleClasses.XIB_TYPE)
+ .list();
+ }
+
+ ImmutableList<Artifact> strings() {
+ return ruleContext.getPrerequisiteArtifacts("strings", Mode.TARGET).list();
+ }
+
+ ImmutableList<Artifact> assetCatalogs() {
+ return ruleContext.getPrerequisiteArtifacts("asset_catalogs", Mode.TARGET).list();
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/SdkFramework.java b/src/main/java/com/google/devtools/build/lib/rules/objc/SdkFramework.java
new file mode 100644
index 0000000..c692fcd
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/SdkFramework.java
@@ -0,0 +1,48 @@
+// Copyright 2014 Google Inc. 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.rules.objc;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.xcode.util.Value;
+
+/**
+ * Represents the name of an SDK framework.
+ * <p>
+ * Besides being a glorified String, this class prevents you from adding framework names to an
+ * argument list without explicitly specifying how to prefix them.
+ */
+final class SdkFramework extends Value<SdkFramework> {
+ private final String name;
+
+ public SdkFramework(String name) {
+ super(name);
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns an iterable which contains the name of each given framework in the same order.
+ */
+ static Iterable<String> names(Iterable<SdkFramework> frameworks) {
+ ImmutableList.Builder<String> result = new ImmutableList.Builder<>();
+ for (SdkFramework framework : frameworks) {
+ result.add(framework.getName());
+ }
+ return result.build();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/Storyboards.java b/src/main/java/com/google/devtools/build/lib/rules/objc/Storyboards.java
new file mode 100644
index 0000000..204c22d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/Storyboards.java
@@ -0,0 +1,76 @@
+// Copyright 2014 Google Inc. 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.rules.objc;
+
+
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+
+/**
+ * Contains information about storyboards for a single target. This does not include information
+ * about the transitive closure. A storyboard:
+ * <ul>
+ * <li>Is a single file with an extension of {@code .storyboard} in its uncompiled, checked-in
+ * form.
+ * <li>Can be in a localized {@code .lproj} directory, including {@code Base.lproj}.
+ * <li>Compiles with {@code ibtool} to a directory with extension {@code .storyboardc} (note the
+ * added "c")
+ * </ul>
+ *
+ * <p>The {@link NestedSet}s stored in this class are only one level deep, and do not include the
+ * storyboards in the transitive closure. This is to facilitate structural sharing between copies
+ * of the sequences - the output zips can be added transitively to the inputs of the merge bundle
+ * action, as well as to the files to build set, and only one instance of the sequence exists for
+ * each set.
+ */
+final class Storyboards {
+ private final NestedSet<Artifact> outputZips;
+ private final NestedSet<Artifact> inputs;
+
+ private Storyboards(NestedSet<Artifact> outputZips, NestedSet<Artifact> inputs) {
+ this.outputZips = outputZips;
+ this.inputs = inputs;
+ }
+
+ public NestedSet<Artifact> getOutputZips() {
+ return outputZips;
+ }
+
+ public NestedSet<Artifact> getInputs() {
+ return inputs;
+ }
+
+ static Storyboards empty() {
+ return new Storyboards(
+ NestedSetBuilder.<Artifact>emptySet(Order.STABLE_ORDER),
+ NestedSetBuilder.<Artifact>emptySet(Order.STABLE_ORDER));
+ }
+
+ /**
+ * Generates a set of new instances given the raw storyboard inputs.
+ * @param inputs the {@code .storyboard} files.
+ * @param intermediateArtifacts the object used to determine the output zip {@link Artifact}s.
+ */
+ static Storyboards fromInputs(
+ Iterable<Artifact> inputs, IntermediateArtifacts intermediateArtifacts) {
+ NestedSetBuilder<Artifact> outputZips = NestedSetBuilder.stableOrder();
+ for (Artifact input : inputs) {
+ outputZips.add(intermediateArtifacts.compiledStoryboardZip(input));
+ }
+ return new Storyboards(outputZips.build(), NestedSetBuilder.wrap(Order.STABLE_ORDER, inputs));
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/XcTestAppProvider.java b/src/main/java/com/google/devtools/build/lib/rules/objc/XcTestAppProvider.java
new file mode 100644
index 0000000..1fc5d5d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/XcTestAppProvider.java
@@ -0,0 +1,57 @@
+// Copyright 2015 Google Inc. 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.rules.objc;
+
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+
+/**
+ * Supplies information needed when a dependency serves as an {@code xctest_app}.
+ */
+@Immutable
+final class XcTestAppProvider implements TransitiveInfoProvider {
+ private final Artifact bundleLoader;
+ private final Artifact ipa;
+ private final ObjcProvider objcProvider;
+
+ XcTestAppProvider(Artifact bundleLoader, Artifact ipa, ObjcProvider objcProvider) {
+ this.bundleLoader = Preconditions.checkNotNull(bundleLoader);
+ this.ipa = Preconditions.checkNotNull(ipa);
+ this.objcProvider = Preconditions.checkNotNull(objcProvider);
+ }
+
+ /**
+ * The bundle loader, which corresponds to the test app's binary.
+ */
+ public Artifact getBundleLoader() {
+ return bundleLoader;
+ }
+
+ public Artifact getIpa() {
+ return ipa;
+ }
+
+ /**
+ * An {@link ObjcProvider} that should be included by any test target that uses this app as its
+ * {@code xctest_app}. This is <strong>not</strong> a typical {@link ObjcProvider} - it has
+ * certain linker-releated keys omitted, such as {@link ObjcProvider#LIBRARY}, since XcTests have
+ * access to symbols in their test rig without linking them into the main test binary.
+ */
+ public ObjcProvider getObjcProvider() {
+ return objcProvider;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/Xcdatamodel.java b/src/main/java/com/google/devtools/build/lib/rules/objc/Xcdatamodel.java
new file mode 100644
index 0000000..5b29435
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/Xcdatamodel.java
@@ -0,0 +1,136 @@
+// Copyright 2014 Google Inc. 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.rules.objc;
+
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.util.FileType;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.xcode.util.Value;
+
+/**
+ * Represents an .xcdatamodel[d] directory - knowing all {@code Artifact}s contained therein - and
+ * the .zip file that it is compiled to which should be merged with the final application bundle.
+ * <p>
+ * An .xcdatamodel (here and below note that lack or presence of a d) directory contains the schema
+ * for a managed object, or a managed object model. It typically has two files: {@code layout} and
+ * {@code contents}, although this detail isn't addressed in Bazel code. Directories of this
+ * sort are compiled into a single .mom file. If the .xcdatamodel directory is inside a
+ * .xcdatamodeld directory, then the .mom file is placed inside a .momd directory. The .momd
+ * directory or .mom file is placed in the bundle root of the final bundle.
+ * <p>
+ * An .xcdatamodeld directory contains several .xcdatamodel directories, each corresponding to a
+ * different version. In addition the .xcdatamodeld directory contains a {@code .xccurrentversion}
+ * file which identifies the current version. (this file is also not handled explicitly by Bazel
+ * code).
+ * <p>
+ * When processing artifacts referenced by a {@code datamodels} attribute, we must determine if it
+ * is in a .xcdatamodeld directory or only a .xcdatamodel directory. We also must group the
+ * artifacts by their container, the container being an .xcdatamodeld directory if possible, and a
+ * .xcdatamodel directory otherwise. Every container is compiled with a single invocation of the
+ * Managed Object Model Compiler (momc) and corresponds to exactly one instance of this class. We
+ * invoke momc indirectly through the momczip tool (part of Bazel) which runs momc and zips the
+ * output. The files in this zip are placed in the bundle root of the final application, not unlike
+ * the zips generated by {@code actooloribtoolzip}.
+ */
+class Xcdatamodel extends Value<Xcdatamodel> {
+ private final Artifact outputZip;
+ private final ImmutableSet<Artifact> inputs;
+ private final PathFragment container;
+
+ Xcdatamodel(Artifact outputZip, ImmutableSet<Artifact> inputs, PathFragment container) {
+ super(ImmutableMap.of(
+ "outputZip", outputZip,
+ "inputs", inputs,
+ "container", container));
+ this.outputZip = outputZip;
+ this.inputs = inputs;
+ this.container = container;
+ }
+
+ /**
+ * Returns the files that should be supplied to Xcodegen when generating a project that includes
+ * all of the given xcdatamodels.
+ */
+ public static Iterable<Artifact> inputsToXcodegen(Iterable<Xcdatamodel> datamodels) {
+ ImmutableSet.Builder<Artifact> inputs = new ImmutableSet.Builder<>();
+ for (Xcdatamodel datamodel : datamodels) {
+ for (Artifact generalInput : datamodel.inputs) {
+ if (generalInput.getExecPath().getBaseName().equals(".xccurrentversion")) {
+ inputs.add(generalInput);
+ }
+ }
+ }
+ return inputs.build();
+ }
+
+ public Artifact getOutputZip() {
+ return outputZip;
+ }
+
+ /**
+ * Returns every known file in the container. This is every input file that is processed by momc.
+ */
+ public ImmutableSet<Artifact> getInputs() {
+ return inputs;
+ }
+
+ public PathFragment getContainer() {
+ return container;
+ }
+
+ /**
+ * The ARCHIVE_ROOT passed to momczip. The archive root is the name of the .mom file
+ * unversioned object models, and the name of the .momd directory for versioned object models.
+ */
+ public String archiveRootForMomczip() {
+ return name() + (container.getBaseName().endsWith(".xcdatamodeld") ? ".momd" : ".mom");
+ }
+
+ /**
+ * The name of the data model. This is the name of the container without the extension. For
+ * instance, if the container is "foo/Information.xcdatamodel" or "bar/Information.xcdatamodeld",
+ * then the name is "Information".
+ */
+ public String name() {
+ String baseContainerName = container.getBaseName();
+ int lastDot = baseContainerName.lastIndexOf('.');
+ return baseContainerName.substring(0, lastDot);
+ }
+
+ public static Iterable<Artifact> outputZips(Iterable<Xcdatamodel> models) {
+ return Iterables.transform(models, new Function<Xcdatamodel, Artifact>() {
+ @Override
+ public Artifact apply(Xcdatamodel model) {
+ return model.getOutputZip();
+ }
+ });
+ }
+
+ /**
+ * Returns a sequence of all unique *.xcdatamodel directories that contain all the artifacts of
+ * the given models. Note that this does not return any *.xcdatamodeld directories.
+ */
+ static Iterable<PathFragment> xcdatamodelDirs(Iterable<Xcdatamodel> models) {
+ ImmutableSet.Builder<PathFragment> result = new ImmutableSet.Builder<>();
+ for (Xcdatamodel model : models) {
+ result.addAll(ObjcCommon.uniqueContainers(model.getInputs(), FileType.of(".xcdatamodel")));
+ }
+ return result.build();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/Xcdatamodels.java b/src/main/java/com/google/devtools/build/lib/rules/objc/Xcdatamodels.java
new file mode 100644
index 0000000..32d48aa
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/Xcdatamodels.java
@@ -0,0 +1,71 @@
+// Copyright 2014 Google Inc. 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.rules.objc;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSetMultimap;
+import com.google.common.collect.Multimap;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.util.FileType;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.Collection;
+import java.util.Map;
+
+/**
+ * Utility code for getting information specific to xcdatamodels for a single rule.
+ */
+class Xcdatamodels {
+ private Xcdatamodels() {}
+
+ static final ImmutableList<FileType> CONTAINER_TYPES =
+ ImmutableList.of(FileType.of(".xcdatamodeld"), FileType.of(".xcdatamodel"));
+
+ static Iterable<Xcdatamodel> xcdatamodels(
+ IntermediateArtifacts intermediateArtifacts, Iterable<Artifact> xcdatamodels) {
+ ImmutableSet.Builder<Xcdatamodel> result = new ImmutableSet.Builder<>();
+ Multimap<PathFragment, Artifact> artifactsByContainer = byContainer(xcdatamodels);
+
+ for (Map.Entry<PathFragment, Collection<Artifact>> modelDirEntry :
+ artifactsByContainer.asMap().entrySet()) {
+ PathFragment container = modelDirEntry.getKey();
+ Artifact outputZip = intermediateArtifacts.compiledMomZipArtifact(container);
+ result.add(
+ new Xcdatamodel(outputZip, ImmutableSet.copyOf(modelDirEntry.getValue()), container));
+ }
+
+ return result.build();
+ }
+
+
+ /**
+ * Arrange a sequence of artifacts into entries of a multimap by their nearest container
+ * directory, preferring {@code .xcdatamodeld} over {@code .xcdatamodel}.
+ * If an artifact is not inside any containing directory, then it is not present in the returned
+ * map.
+ */
+ static Multimap<PathFragment, Artifact> byContainer(Iterable<Artifact> artifacts) {
+ ImmutableSetMultimap.Builder<PathFragment, Artifact> result =
+ new ImmutableSetMultimap.Builder<>();
+ for (Artifact artifact : artifacts) {
+ for (PathFragment modelDir :
+ ObjcCommon.nearestContainerMatching(CONTAINER_TYPES, artifact).asSet()) {
+ result.put(modelDir, artifact);
+ }
+ }
+ return result.build();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/XcodeProductType.java b/src/main/java/com/google/devtools/build/lib/rules/objc/XcodeProductType.java
new file mode 100644
index 0000000..1a68206
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/XcodeProductType.java
@@ -0,0 +1,41 @@
+// Copyright 2014 Google Inc. 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.rules.objc;
+
+/**
+ * Possible values that {@code objc_*} rules care about for what Xcode project files refer to as
+ * "product type."
+ */
+enum XcodeProductType {
+ LIBRARY_STATIC("com.apple.product-type.library.static"),
+ BUNDLE("com.apple.product-type.bundle"),
+ APPLICATION("com.apple.product-type.application"),
+ UNIT_TEST("com.apple.product-type.bundle.unit-test"),
+ EXTENSION("com.apple.product-type.app-extension");
+
+ private final String identifier;
+
+ XcodeProductType(String identifier) {
+ this.identifier = identifier;
+ }
+
+ /**
+ * Returns the string used to identify this product type in the {@code productType} field of
+ * {@code PBXNativeTarget} objects in Xcode project files.
+ */
+ public String getIdentifier() {
+ return identifier;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/XcodeProvider.java b/src/main/java/com/google/devtools/build/lib/rules/objc/XcodeProvider.java
new file mode 100644
index 0000000..b244cd9
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/XcodeProvider.java
@@ -0,0 +1,452 @@
+// Copyright 2014 Google Inc. 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.rules.objc;
+
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.BUNDLE_IMPORT_DIR;
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.DEFINE;
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.FORCE_LOAD_FOR_XCODEGEN;
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.FRAMEWORK_DIR;
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.GENERAL_RESOURCE_FILE;
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.IMPORTED_LIBRARY;
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.SDK_DYLIB;
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.SDK_FRAMEWORK;
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.WEAK_SDK_FRAMEWORK;
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.XCASSETS_DIR;
+import static com.google.devtools.build.lib.rules.objc.ObjcProvider.XCDATAMODEL;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
+import com.google.common.base.Optional;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.rules.objc.ObjcProvider.Flag;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.xcode.util.Interspersing;
+import com.google.devtools.build.xcode.xcodegen.proto.XcodeGenProtos.DependencyControl;
+import com.google.devtools.build.xcode.xcodegen.proto.XcodeGenProtos.TargetControl;
+import com.google.devtools.build.xcode.xcodegen.proto.XcodeGenProtos.XcodeprojBuildSetting;
+
+import java.util.EnumSet;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+/**
+ * Provider which provides transitive dependency information that is specific to Xcodegen. In
+ * particular, it provides a sequence of targets which can be used to create a self-contained
+ * {@code .xcodeproj} file.
+ */
+@Immutable
+public final class XcodeProvider implements TransitiveInfoProvider {
+ /**
+ * A builder for instances of {@link XcodeProvider}.
+ */
+ public static final class Builder {
+ private Label label;
+ private final NestedSetBuilder<String> userHeaderSearchPaths = NestedSetBuilder.stableOrder();
+ private final NestedSetBuilder<String> headerSearchPaths = NestedSetBuilder.stableOrder();
+ private Optional<InfoplistMerging> infoplistMerging = Optional.absent();
+ private final NestedSetBuilder<XcodeProvider> dependencies = NestedSetBuilder.stableOrder();
+ private final ImmutableList.Builder<XcodeprojBuildSetting> xcodeprojBuildSettings =
+ new ImmutableList.Builder<>();
+ private final ImmutableList.Builder<String> copts = new ImmutableList.Builder<>();
+ private final ImmutableList.Builder<String> compilationModeCopts =
+ new ImmutableList.Builder<>();
+ private XcodeProductType productType;
+ private final ImmutableList.Builder<Artifact> headers = new ImmutableList.Builder<>();
+ private Optional<CompilationArtifacts> compilationArtifacts = Optional.absent();
+ private ObjcProvider objcProvider;
+ private Optional<XcodeProvider> testHost = Optional.absent();
+ private final NestedSetBuilder<Artifact> inputsToXcodegen = NestedSetBuilder.stableOrder();
+
+ /**
+ * Sets the label of the build target which corresponds to this Xcode target.
+ */
+ public Builder setLabel(Label label) {
+ this.label = label;
+ return this;
+ }
+
+ /**
+ * Adds user header search paths for this target.
+ */
+ public Builder addUserHeaderSearchPaths(Iterable<PathFragment> userHeaderSearchPaths) {
+ this.userHeaderSearchPaths.addAll(rootEach("$(WORKSPACE_ROOT)", userHeaderSearchPaths));
+ return this;
+ }
+
+ /**
+ * Adds header search paths for this target. Each path is interpreted relative to the given
+ * root, such as {@code "$(WORKSPACE_ROOT)"}.
+ */
+ public Builder addHeaderSearchPaths(String root, Iterable<PathFragment> paths) {
+ this.headerSearchPaths.addAll(rootEach(root, paths));
+ return this;
+ }
+
+ /**
+ * Sets the Info.plist merging information. Used for applications. May be
+ * absent for other bundles.
+ */
+ public Builder setInfoplistMerging(InfoplistMerging infoplistMerging) {
+ this.infoplistMerging = Optional.of(infoplistMerging);
+ return this;
+ }
+
+ /**
+ * Adds items in the {@link NestedSet}s of the given target to the corresponding sets in this
+ * builder. This is useful if the given target is a dependency or like a dependency
+ * (e.g. a test host). The given provider is not registered as a dependency with this provider.
+ */
+ private void addTransitiveSets(XcodeProvider dependencyish) {
+ inputsToXcodegen.addTransitive(dependencyish.inputsToXcodegen);
+ userHeaderSearchPaths.addTransitive(dependencyish.userHeaderSearchPaths);
+ headerSearchPaths.addTransitive(dependencyish.headerSearchPaths);
+ }
+
+ /**
+ * Adds {@link XcodeProvider}s corresponding to direct dependencies of this target which should
+ * be added in the {@code .xcodeproj} file.
+ */
+ public Builder addDependencies(Iterable<XcodeProvider> dependencies) {
+ for (XcodeProvider dependency : dependencies) {
+ this.dependencies.add(dependency);
+ this.dependencies.addTransitive(dependency.dependencies);
+ this.addTransitiveSets(dependency);
+ }
+ return this;
+ }
+
+ /**
+ * Adds additional build settings of this target.
+ */
+ public Builder addXcodeprojBuildSettings(
+ Iterable<XcodeprojBuildSetting> xcodeprojBuildSettings) {
+ this.xcodeprojBuildSettings.addAll(xcodeprojBuildSettings);
+ return this;
+ }
+
+ /**
+ * Sets the copts to use when compiling the Xcode target.
+ */
+ public Builder addCopts(Iterable<String> copts) {
+ this.copts.addAll(copts);
+ return this;
+ }
+
+ /**
+ * Sets the copts derived from compilation mode to use when compiling the Xcode target. These
+ * will be included before the DEFINE options.
+ */
+ public Builder addCompilationModeCopts(Iterable<String> copts) {
+ this.compilationModeCopts.addAll(copts);
+ return this;
+ }
+
+ /**
+ * Sets the product type for the PBXTarget in the .xcodeproj file.
+ */
+ public Builder setProductType(XcodeProductType productType) {
+ this.productType = productType;
+ return this;
+ }
+
+ /**
+ * Adds to the header files of this target. It needs not to include the header files of
+ * dependencies.
+ */
+ public Builder addHeaders(Iterable<Artifact> headers) {
+ this.headers.addAll(headers);
+ return this;
+ }
+
+ /**
+ * The compilation artifacts for this target.
+ */
+ public Builder setCompilationArtifacts(CompilationArtifacts compilationArtifacts) {
+ this.compilationArtifacts = Optional.of(compilationArtifacts);
+ return this;
+ }
+
+ /**
+ * Sets the {@link ObjcProvider} corresponding to this target.
+ */
+ public Builder setObjcProvider(ObjcProvider objcProvider) {
+ this.objcProvider = objcProvider;
+ return this;
+ }
+
+ /**
+ * Sets the test host. This is used for xctest targets.
+ */
+ public Builder setTestHost(XcodeProvider testHost) {
+ Preconditions.checkState(!this.testHost.isPresent());
+ this.testHost = Optional.of(testHost);
+ this.addTransitiveSets(testHost);
+ return this;
+ }
+
+ /**
+ * Adds inputs that are passed to Xcodegen when generating the project file.
+ */
+ public Builder addInputsToXcodegen(Iterable<Artifact> inputsToXcodegen) {
+ this.inputsToXcodegen.addAll(inputsToXcodegen);
+ return this;
+ }
+
+ public XcodeProvider build() {
+ Preconditions.checkArgument(
+ !testHost.isPresent() || (productType == XcodeProductType.UNIT_TEST),
+ "%s product types cannot have a test host (test host: %s).", productType, testHost);
+ return new XcodeProvider(this);
+ }
+ }
+
+ /**
+ * A collection of top-level targets that can be used to create a complete project.
+ */
+ public static final class Project {
+ private final NestedSet<Artifact> inputsToXcodegen;
+ private final ImmutableList<XcodeProvider> topLevelTargets;
+
+ private Project(
+ NestedSet<Artifact> inputsToXcodegen, ImmutableList<XcodeProvider> topLevelTargets) {
+ this.inputsToXcodegen = inputsToXcodegen;
+ this.topLevelTargets = topLevelTargets;
+ }
+
+ public static Project fromTopLevelTarget(XcodeProvider topLevelTarget) {
+ return fromTopLevelTargets(ImmutableList.of(topLevelTarget));
+ }
+
+ public static Project fromTopLevelTargets(Iterable<XcodeProvider> topLevelTargets) {
+ NestedSetBuilder<Artifact> inputsToXcodegen = NestedSetBuilder.stableOrder();
+ for (XcodeProvider target : topLevelTargets) {
+ inputsToXcodegen.addTransitive(target.inputsToXcodegen);
+ }
+ return new Project(inputsToXcodegen.build(), ImmutableList.copyOf(topLevelTargets));
+ }
+
+ /**
+ * Returns artifacts that are passed to the Xcodegen action when generating a project file that
+ * contains all of the given targets.
+ */
+ public NestedSet<Artifact> getInputsToXcodegen() {
+ return inputsToXcodegen;
+ }
+
+ public ImmutableList<XcodeProvider> getTopLevelTargets() {
+ return topLevelTargets;
+ }
+
+ /**
+ * Returns all the target controls that must be added to the xcodegen control. No other target
+ * controls are needed to generate a functional project file. This method creates a new list
+ * whenever it is called.
+ */
+ public ImmutableList<TargetControl> targets() {
+ // Collect all the dependencies of all the providers, filtering out duplicates.
+ Set<XcodeProvider> providerSet = new LinkedHashSet<>();
+ for (XcodeProvider target : topLevelTargets) {
+ Iterables.addAll(providerSet, target.providers());
+ }
+
+ ImmutableList.Builder<TargetControl> controls = new ImmutableList.Builder<>();
+ for (XcodeProvider provider : providerSet) {
+ controls.add(provider.targetControl());
+ }
+ return controls.build();
+ }
+ }
+
+ private final Label label;
+ private final NestedSet<String> userHeaderSearchPaths;
+ private final NestedSet<String> headerSearchPaths;
+ private final Optional<InfoplistMerging> infoplistMerging;
+ private final NestedSet<XcodeProvider> dependencies;
+ private final ImmutableList<XcodeprojBuildSetting> xcodeprojBuildSettings;
+ private final ImmutableList<String> copts;
+ private final ImmutableList<String> compilationModeCopts;
+ private final XcodeProductType productType;
+ private final ImmutableList<Artifact> headers;
+ private final Optional<CompilationArtifacts> compilationArtifacts;
+ private final ObjcProvider objcProvider;
+ private final Optional<XcodeProvider> testHost;
+ private final NestedSet<Artifact> inputsToXcodegen;
+
+ private XcodeProvider(Builder builder) {
+ this.label = Preconditions.checkNotNull(builder.label);
+ this.userHeaderSearchPaths = builder.userHeaderSearchPaths.build();
+ this.headerSearchPaths = builder.headerSearchPaths.build();
+ this.infoplistMerging = builder.infoplistMerging;
+ this.dependencies = builder.dependencies.build();
+ this.xcodeprojBuildSettings = builder.xcodeprojBuildSettings.build();
+ this.copts = builder.copts.build();
+ this.compilationModeCopts = builder.compilationModeCopts.build();
+ this.productType = Preconditions.checkNotNull(builder.productType);
+ this.headers = builder.headers.build();
+ this.compilationArtifacts = builder.compilationArtifacts;
+ this.objcProvider = Preconditions.checkNotNull(builder.objcProvider);
+ this.testHost = Preconditions.checkNotNull(builder.testHost);
+ this.inputsToXcodegen = builder.inputsToXcodegen.build();
+ }
+
+ /**
+ * Creates a builder whose values are all initialized to this provider.
+ */
+ public Builder toBuilder() {
+ Builder builder = new Builder();
+ builder.label = label;
+ builder.userHeaderSearchPaths.addAll(userHeaderSearchPaths);
+ builder.headerSearchPaths.addTransitive(headerSearchPaths);
+ builder.infoplistMerging = infoplistMerging;
+ builder.dependencies.addTransitive(dependencies);
+ builder.xcodeprojBuildSettings.addAll(xcodeprojBuildSettings);
+ builder.copts.addAll(copts);
+ builder.productType = productType;
+ builder.headers.addAll(headers);
+ builder.compilationArtifacts = compilationArtifacts;
+ builder.objcProvider = objcProvider;
+ builder.testHost = testHost;
+ builder.inputsToXcodegen.addTransitive(inputsToXcodegen);
+ return builder;
+ }
+
+ /**
+ * Returns a list of this provider and all its transitive dependencies.
+ */
+ private Iterable<XcodeProvider> providers() {
+ Set<XcodeProvider> providers = new LinkedHashSet<>();
+ providers.add(this);
+ Iterables.addAll(providers, dependencies);
+ for (XcodeProvider justTestHost : testHost.asSet()) {
+ providers.add(justTestHost);
+ Iterables.addAll(providers, justTestHost.dependencies);
+ }
+ return ImmutableList.copyOf(providers);
+ }
+
+ private static final EnumSet<XcodeProductType> CAN_LINK_PRODUCT_TYPES = EnumSet.of(
+ XcodeProductType.APPLICATION, XcodeProductType.BUNDLE, XcodeProductType.UNIT_TEST);
+
+ private TargetControl targetControl() {
+ String buildFilePath = label.getPackageFragment().getSafePathString() + "/BUILD";
+ // TODO(bazel-team): Add provisioning profile information when Xcodegen supports it.
+ TargetControl.Builder targetControl = TargetControl.newBuilder()
+ .setName(label.getName())
+ .setLabel(label.toString())
+ .setProductType(productType.getIdentifier())
+ .addAllImportedLibrary(Artifact.toExecPaths(objcProvider.get(IMPORTED_LIBRARY)))
+ .addAllUserHeaderSearchPath(userHeaderSearchPaths)
+ .addAllHeaderSearchPath(headerSearchPaths)
+ .addAllSupportFile(Artifact.toExecPaths(headers))
+ .addAllCopt(compilationModeCopts)
+ .addAllCopt(Interspersing.prependEach("-D", objcProvider.get(DEFINE)))
+ .addAllCopt(copts)
+ .addAllLinkopt(
+ Interspersing.beforeEach("-force_load", objcProvider.get(FORCE_LOAD_FOR_XCODEGEN)))
+ .addAllLinkopt(IosSdkCommands.DEFAULT_LINKER_FLAGS)
+ .addAllLinkopt(Interspersing.beforeEach(
+ "-weak_framework", SdkFramework.names(objcProvider.get(WEAK_SDK_FRAMEWORK))))
+ .addAllBuildSetting(xcodeprojBuildSettings)
+ .addAllBuildSetting(IosSdkCommands.defaultWarningsForXcode())
+ .addAllSdkFramework(SdkFramework.names(objcProvider.get(SDK_FRAMEWORK)))
+ .addAllFramework(PathFragment.safePathStrings(objcProvider.get(FRAMEWORK_DIR)))
+ .addAllXcassetsDir(PathFragment.safePathStrings(objcProvider.get(XCASSETS_DIR)))
+ .addAllXcdatamodel(PathFragment.safePathStrings(
+ Xcdatamodel.xcdatamodelDirs(objcProvider.get(XCDATAMODEL))))
+ .addAllBundleImport(PathFragment.safePathStrings(objcProvider.get(BUNDLE_IMPORT_DIR)))
+ .addAllSdkDylib(objcProvider.get(SDK_DYLIB))
+ .addAllGeneralResourceFile(Artifact.toExecPaths(objcProvider.get(GENERAL_RESOURCE_FILE)))
+ .addSupportFile(buildFilePath);
+
+ if (CAN_LINK_PRODUCT_TYPES.contains(productType)) {
+ for (XcodeProvider dependency : dependencies) {
+ // Only add a library target to a binary's dependencies if it has source files to compile.
+ // Xcode cannot build targets without a source file in the PBXSourceFilesBuildPhase, so if
+ // such a target is present in the control file, it is only to get Xcodegen to put headers
+ // and resources not used by the final binary in the Project Navigator.
+ //
+ // The exception to this rule is the objc_bundle_library target. Bundles are generally used
+ // for resources and can lack a PBXSourceFilesBuildPhase in the project file and still be
+ // considered valid by Xcode.
+ boolean hasSources = dependency.compilationArtifacts.isPresent()
+ && dependency.compilationArtifacts.get().getArchive().isPresent();
+ if (hasSources || (dependency.productType == XcodeProductType.BUNDLE)) {
+ targetControl.addDependency(DependencyControl.newBuilder()
+ .setTargetLabel(dependency.label.toString())
+ .build());
+ }
+ }
+ for (XcodeProvider justTestHost : testHost.asSet()) {
+ targetControl.addDependency(DependencyControl.newBuilder()
+ .setTargetLabel(justTestHost.label.toString())
+ .setTestHost(true)
+ .build());
+ }
+ }
+
+ for (InfoplistMerging merging : infoplistMerging.asSet()) {
+ for (Artifact infoplist : merging.getPlistWithEverything().asSet()) {
+ targetControl.setInfoplist(infoplist.getExecPathString());
+ }
+ }
+ for (CompilationArtifacts artifacts : compilationArtifacts.asSet()) {
+ targetControl
+ .addAllSourceFile(Artifact.toExecPaths(artifacts.getSrcs()))
+ .addAllNonArcSourceFile(Artifact.toExecPaths(artifacts.getNonArcSrcs()));
+
+ for (Artifact pchFile : artifacts.getPchFile().asSet()) {
+ targetControl
+ .setPchPath(pchFile.getExecPathString())
+ .addSupportFile(pchFile.getExecPathString());
+ }
+ }
+
+ if (objcProvider.is(Flag.USES_CPP)) {
+ targetControl.addSdkDylib("libc++");
+ }
+
+ return targetControl.build();
+ }
+
+ /**
+ * Prepends the given path to each path in {@code paths}. Empty paths are
+ * transformed to the value of {@code variable} rather than {@code variable + "/."}
+ */
+ @VisibleForTesting
+ static Iterable<String> rootEach(final String prefix, Iterable<PathFragment> paths) {
+ Preconditions.checkArgument(prefix.startsWith("$"),
+ "prefix should start with a build setting variable like '$(NAME)': %s", prefix);
+ Preconditions.checkArgument(!prefix.endsWith("/"),
+ "prefix should not end with '/': %s", prefix);
+ return Iterables.transform(paths, new Function<PathFragment, String>() {
+ @Override
+ public String apply(PathFragment input) {
+ if (input.getSafePathString().equals(".")) {
+ return prefix;
+ } else {
+ return prefix + "/" + input.getSafePathString();
+ }
+ }
+ });
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/XcodeSupport.java b/src/main/java/com/google/devtools/build/lib/rules/objc/XcodeSupport.java
new file mode 100644
index 0000000..f64c6bd
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/XcodeSupport.java
@@ -0,0 +1,102 @@
+// Copyright 2015 Google Inc. 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.rules.objc;
+
+import static com.google.devtools.build.lib.packages.ImplicitOutputsFunction.fromTemplates;
+
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.packages.ImplicitOutputsFunction.SafeImplicitOutputsFunction;
+
+/**
+ * Support for Objc rule types that export an Xcode provider or generate xcode project files.
+ *
+ * <p>Methods on this class can be called in any order without impacting the result.
+ */
+public final class XcodeSupport {
+
+ /**
+ * Template for a target's xcode project.
+ */
+ public static final SafeImplicitOutputsFunction PBXPROJ =
+ fromTemplates("%{name}.xcodeproj/project.pbxproj");
+
+ private final RuleContext ruleContext;
+
+ /**
+ * Creates a new xcode support for the given context.
+ */
+ XcodeSupport(RuleContext ruleContext) {
+ this.ruleContext = ruleContext;
+ }
+
+ /**
+ * Adds xcode project files to the given builder.
+ *
+ * @return this xcode support
+ */
+ XcodeSupport addFilesToBuild(NestedSetBuilder<Artifact> filesToBuild) {
+ filesToBuild.add(ruleContext.getImplicitOutputArtifact(PBXPROJ));
+ return this;
+ }
+
+ /**
+ * Registers actions that generate the rule's Xcode project.
+ *
+ * @param xcodeProvider information about this rule's xcode settings and that of its dependencies
+ * @return this xcode support
+ */
+ XcodeSupport registerActions(XcodeProvider xcodeProvider) {
+ ObjcActionsBuilder actionsBuilder = ObjcRuleClasses.actionsBuilder(ruleContext);
+ actionsBuilder.registerXcodegenActions(
+ new ObjcRuleClasses.Tools(ruleContext),
+ ruleContext.getImplicitOutputArtifact(XcodeSupport.PBXPROJ),
+ XcodeProvider.Project.fromTopLevelTarget(xcodeProvider));
+ return this;
+ }
+
+ /**
+ * Adds common xcode settings to the given provider builder.
+ *
+ * @param objcProvider provider containing all dependencies' information as well as some of this
+ * rule's
+ * @param productType type of this rule's Xcode target
+ *
+ * @return this xcode support
+ */
+ XcodeSupport addXcodeSettings(XcodeProvider.Builder xcodeProviderBuilder,
+ ObjcProvider objcProvider, XcodeProductType productType) {
+ xcodeProviderBuilder
+ .setLabel(ruleContext.getLabel())
+ .setObjcProvider(objcProvider)
+ .setProductType(productType);
+ return this;
+ }
+
+ /**
+ * Adds dependencies to the given provider builder from the {@code deps} and {@code bundles}
+ * attributes.
+ *
+ * @return this xcode support
+ */
+ XcodeSupport addDependencies(XcodeProvider.Builder xcodeProviderBuilder) {
+ xcodeProviderBuilder
+ .addDependencies(ruleContext.getPrerequisites("deps", Mode.TARGET, XcodeProvider.class))
+ .addDependencies(ruleContext.getPrerequisites("bundles", Mode.TARGET, XcodeProvider.class));
+ return this;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/XibFiles.java b/src/main/java/com/google/devtools/build/lib/rules/objc/XibFiles.java
new file mode 100644
index 0000000..9be1d06
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/XibFiles.java
@@ -0,0 +1,42 @@
+// Copyright 2014 Google Inc. 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.rules.objc;
+
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.Artifact;
+
+/**
+ * A sequence of xib source files. Each {@code .xib} file can be compiled to a {@code .nib} file or
+ * directory. Because it might be a directory, we always use zip files to store the output and use
+ * the {@code actooloribtoolzip} utility to run ibtool and zip the output.
+ */
+public final class XibFiles extends IterableWrapper<Artifact> {
+ public XibFiles(Iterable<Artifact> artifacts) {
+ super(artifacts);
+ }
+
+ /**
+ * Returns a sequence where each element of this sequence is converted to the file which contains
+ * the compiled contents of the xib.
+ */
+ public ImmutableList<Artifact> compiledZips(IntermediateArtifacts intermediateArtifacts) {
+ ImmutableList.Builder<Artifact> zips = new ImmutableList.Builder<>();
+ for (Artifact xib : this) {
+ zips.add(intermediateArtifacts.compiledXibFileZip(xib));
+ }
+ return zips.build();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/proto/ProtoSourcesProvider.java b/src/main/java/com/google/devtools/build/lib/rules/proto/ProtoSourcesProvider.java
new file mode 100644
index 0000000..0663f62
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/proto/ProtoSourcesProvider.java
@@ -0,0 +1,66 @@
+// Copyright 2014 Google Inc. 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.rules.proto;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+
+/**
+ * Configured target classes that implement this class can contribute .proto files to the
+ * compilation of proto_library rules.
+ */
+@Immutable
+public final class ProtoSourcesProvider implements TransitiveInfoProvider {
+
+ private final NestedSet<Artifact> transitiveImports;
+ private final NestedSet<Artifact> transitiveProtoSources;
+ private final ImmutableList<Artifact> protoSources;
+
+ public ProtoSourcesProvider(NestedSet<Artifact> transitiveImports,
+ NestedSet<Artifact> transitiveProtoSources,
+ ImmutableList<Artifact> protoSources) {
+ this.transitiveImports = transitiveImports;
+ this.transitiveProtoSources = transitiveProtoSources;
+ this.protoSources = protoSources;
+ }
+
+ /**
+ * Transitive imports including weak dependencies
+ * This determines the order of "-I" arguments to the protocol compiler, and
+ * that is probably important
+ */
+ public NestedSet<Artifact> getTransitiveImports() {
+ return transitiveImports;
+ }
+
+ /**
+ * Returns the proto sources for this rule and all its dependent protocol
+ * buffer rules.
+ */
+ public NestedSet<Artifact> getTransitiveProtoSources() {
+ return transitiveProtoSources;
+ }
+
+ /**
+ * Returns the proto sources from the 'srcs' attribute. If the library is a proxy library
+ * that has no sources, return the sources from the direct deps.
+ */
+ public ImmutableList<Artifact> getProtoSources() {
+ return protoSources;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/test/BaselineCoverageAction.java b/src/main/java/com/google/devtools/build/lib/rules/test/BaselineCoverageAction.java
new file mode 100644
index 0000000..6a19f92
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/test/BaselineCoverageAction.java
@@ -0,0 +1,132 @@
+// Copyright 2014 Google Inc. 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.rules.test;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.eventbus.EventBus;
+import com.google.devtools.build.lib.actions.ActionOwner;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.Executor;
+import com.google.devtools.build.lib.actions.NotifyOnActionCacheHit;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.Util;
+import com.google.devtools.build.lib.analysis.actions.AbstractFileWriteAction;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.util.Fingerprint;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Generates baseline (empty) coverage for the given non-test target.
+ */
+public class BaselineCoverageAction extends AbstractFileWriteAction
+ implements NotifyOnActionCacheHit {
+ // TODO(bazel-team): Remove this list of languages by separately collecting offline and online
+ // instrumented files.
+ private static final List<String> OFFLINE_INSTRUMENTATION_SUFFIXES = ImmutableList.of(
+ ".c", ".cc", ".cpp", ".dart", ".go", ".h", ".java", ".py");
+ private final Iterable<Artifact> instrumentedFiles;
+
+ private BaselineCoverageAction(
+ ActionOwner owner, Iterable<Artifact> instrumentedFiles, Artifact output) {
+ super(owner, ImmutableList.<Artifact>of(), output, false);
+ this.instrumentedFiles = instrumentedFiles;
+ }
+
+ @Override
+ public String getMnemonic() {
+ return "BaselineCoverage";
+ }
+
+ @Override
+ public String computeKey() {
+ return new Fingerprint()
+ .addStrings(getInstrumentedFilePathStrings())
+ .hexDigestAndReset();
+ }
+
+ private Iterable<String> getInstrumentedFilePathStrings() {
+ List<String> result = new ArrayList<>();
+ for (Artifact instrumentedFile : instrumentedFiles) {
+ String pathString = instrumentedFile.getExecPathString();
+ for (String suffix : OFFLINE_INSTRUMENTATION_SUFFIXES) {
+ if (pathString.endsWith(suffix)) {
+ result.add(pathString);
+ break;
+ }
+ }
+ }
+
+ return result;
+ }
+
+ @Override
+ public DeterministicWriter newDeterministicWriter(EventHandler eventHandler,
+ Executor executor) {
+ return new DeterministicWriter() {
+ @Override
+ public void writeOutputFile(OutputStream out) throws IOException {
+ PrintWriter writer = new PrintWriter(out);
+ for (String execPath : getInstrumentedFilePathStrings()) {
+ writer.write("SF:" + execPath + "\n");
+ writer.write("end_of_record\n");
+ }
+ writer.flush();
+ }
+ };
+ }
+
+ @Override
+ protected void afterWrite(Executor executor) {
+ notifyAboutBaselineCoverage(executor.getEventBus());
+ }
+
+ @Override
+ public void actionCacheHit(Executor executor) {
+ notifyAboutBaselineCoverage(executor.getEventBus());
+ }
+
+ /**
+ * Notify interested parties about new baseline coverage data.
+ */
+ private void notifyAboutBaselineCoverage(EventBus eventBus) {
+ Artifact output = Iterables.getOnlyElement(getOutputs());
+ String ownerString = Label.print(getOwner().getLabel());
+ eventBus.post(new BaselineCoverageResult(output, ownerString));
+ }
+
+ /**
+ * Returns collection of baseline coverage artifacts associated with the given target.
+ * Will always return 0 or 1 elements.
+ */
+ public static ImmutableList<Artifact> getBaselineCoverageArtifacts(RuleContext ruleContext,
+ Iterable<Artifact> instrumentedFiles) {
+ // Baseline coverage artifacts will still go into "testlogs" directory.
+ Artifact coverageData = ruleContext.getAnalysisEnvironment().getDerivedArtifact(
+ Util.getWorkspaceRelativePath(ruleContext.getTarget()).getChild("baseline_coverage.dat"),
+ ruleContext.getConfiguration().getTestLogsDirectory());
+ ruleContext.registerAction(new BaselineCoverageAction(
+ ruleContext.getActionOwner(), instrumentedFiles, coverageData));
+
+ return ImmutableList.of(coverageData);
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/test/BaselineCoverageResult.java b/src/main/java/com/google/devtools/build/lib/rules/test/BaselineCoverageResult.java
new file mode 100644
index 0000000..4af2df0
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/test/BaselineCoverageResult.java
@@ -0,0 +1,40 @@
+// Copyright 2014 Google Inc. 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.rules.test;
+
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.actions.Artifact;
+
+/**
+ * This event is used to notify about a successfully built baseline coverage artifact.
+ */
+public class BaselineCoverageResult {
+
+ private final Artifact baselineCoverageData;
+ private final String ownerString;
+
+ public BaselineCoverageResult(Artifact baselineCoverageData, String ownerString) {
+ this.baselineCoverageData = Preconditions.checkNotNull(baselineCoverageData);
+ this.ownerString = Preconditions.checkNotNull(ownerString);
+ }
+
+ public Artifact getArtifact() {
+ return baselineCoverageData;
+ }
+
+ public String getOwnerString() {
+ return ownerString;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/test/CoverageReportActionFactory.java b/src/main/java/com/google/devtools/build/lib/rules/test/CoverageReportActionFactory.java
new file mode 100644
index 0000000..5f7571a
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/test/CoverageReportActionFactory.java
@@ -0,0 +1,41 @@
+// Copyright 2014 Google Inc. 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.rules.test;
+
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.ArtifactFactory;
+import com.google.devtools.build.lib.actions.ArtifactOwner;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+/**
+ * A factory class to create coverage report actions.
+ */
+public interface CoverageReportActionFactory {
+
+ /**
+ * Returns a coverage report Action. May return null if it's not necessary to create
+ * such an Action based on the input parameters and some other data available to
+ * the factory implementation, such as command line arguments.
+ */
+ @Nullable
+ public Action createCoverageReportAction(Iterable<ConfiguredTarget> targetsToTest,
+ Set<Artifact> baselineCoverageArtifacts,
+ ArtifactFactory artifactFactory, ArtifactOwner artifactOwner);
+}
\ No newline at end of file
diff --git a/src/main/java/com/google/devtools/build/lib/rules/test/ExclusiveTestStrategy.java b/src/main/java/com/google/devtools/build/lib/rules/test/ExclusiveTestStrategy.java
new file mode 100644
index 0000000..3cb5750
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/test/ExclusiveTestStrategy.java
@@ -0,0 +1,55 @@
+// Copyright 2014 Google Inc. 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.rules.test;
+
+import com.google.devtools.build.lib.actions.ActionExecutionContext;
+import com.google.devtools.build.lib.actions.ExecException;
+import com.google.devtools.build.lib.actions.ExecutionStrategy;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.view.test.TestStatus.TestResultData;
+
+import java.io.IOException;
+
+/**
+ * Test strategy wrapper called 'exclusive'. It should delegate to a test strategy for local
+ * execution. The name 'exclusive' triggers behavior it triggers behavior in
+ * SkyframeExecutor to schedule test execution sequentially after non-test actions. This
+ * ensures streamed test output is not polluted by other action output.
+ */
+@ExecutionStrategy(contextType = TestActionContext.class,
+ name = { "exclusive" })
+public class ExclusiveTestStrategy implements TestActionContext {
+ private TestActionContext parent;
+
+ public ExclusiveTestStrategy(TestActionContext parent) {
+ this.parent = parent;
+ }
+
+ @Override
+ public void exec(TestRunnerAction action,
+ ActionExecutionContext actionExecutionContext) throws ExecException, InterruptedException {
+ parent.exec(action, actionExecutionContext);
+ }
+
+ @Override
+ public TestResult newCachedTestResult(
+ Path execRoot, TestRunnerAction action, TestResultData cached) throws IOException {
+ return parent.newCachedTestResult(execRoot, action, cached);
+ }
+
+ @Override
+ public String strategyLocality(TestRunnerAction testRunnerAction) {
+ return "exclusive";
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/test/ExecutionInfoProvider.java b/src/main/java/com/google/devtools/build/lib/rules/test/ExecutionInfoProvider.java
new file mode 100644
index 0000000..6c0d73c
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/test/ExecutionInfoProvider.java
@@ -0,0 +1,43 @@
+// Copyright 2014 Google Inc. 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.rules.test;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+
+import java.util.Map;
+
+/**
+ * This provider can be implemented by rules which need special environments to run in (especially
+ * tests).
+ */
+@Immutable
+public final class ExecutionInfoProvider implements TransitiveInfoProvider {
+
+ private final ImmutableMap<String, String> executionInfo;
+
+ public ExecutionInfoProvider(Map<String, String> requirements) {
+ this.executionInfo = ImmutableMap.copyOf(requirements);
+ }
+
+ /**
+ * Returns a map to indicate special execution requirements, such as hardware
+ * platforms, web browsers, etc. Rule tags, such as "requires-XXX", may also be added
+ * as keys to the map.
+ */
+ public ImmutableMap<String, String> getExecutionInfo() {
+ return executionInfo;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/test/InstrumentedFileManifestAction.java b/src/main/java/com/google/devtools/build/lib/rules/test/InstrumentedFileManifestAction.java
new file mode 100644
index 0000000..e5ab219
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/test/InstrumentedFileManifestAction.java
@@ -0,0 +1,133 @@
+// Copyright 2014 Google Inc. 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.rules.test;
+
+import static java.nio.charset.StandardCharsets.ISO_8859_1;
+
+import com.google.common.base.Function;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.ActionOwner;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.Executor;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.Util;
+import com.google.devtools.build.lib.analysis.actions.AbstractFileWriteAction;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.util.Fingerprint;
+import com.google.devtools.build.lib.util.RegexFilter;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.util.Arrays;
+import java.util.Collection;
+
+/**
+ * Creates instrumented file manifest to list instrumented source files.
+ */
+class InstrumentedFileManifestAction extends AbstractFileWriteAction {
+
+ private static final String GUID = "d9ddb800-f9a1-01Da-238d-988311a8475b";
+
+ private final Collection<Artifact> collectedSourceFiles;
+ private final Collection<Artifact> metadataFiles;
+ private final RegexFilter instrumentationFilter;
+
+ private InstrumentedFileManifestAction(ActionOwner owner, Collection<Artifact> inputs,
+ Collection<Artifact> additionalSourceFiles, Collection<Artifact> gcnoFiles,
+ Artifact output, RegexFilter instrumentationFilter) {
+ super(owner, inputs, output, false);
+ this.collectedSourceFiles = additionalSourceFiles;
+ this.metadataFiles = gcnoFiles;
+ this.instrumentationFilter = instrumentationFilter;
+ }
+
+ @Override
+ public DeterministicWriter newDeterministicWriter(EventHandler eventHandler, Executor executor) {
+ return new DeterministicWriter() {
+ @Override
+ public void writeOutputFile(OutputStream out) throws IOException {
+ Writer writer = null;
+ try {
+ // Save exec paths for both instrumented source files and gcno files in the manifest
+ // in the naturally sorted order.
+ String[] fileNames = Iterables.toArray(Iterables.transform(
+ Iterables.concat(collectedSourceFiles, metadataFiles),
+ new Function<Artifact, String> () {
+ @Override
+ public String apply(Artifact artifact) { return artifact.getExecPathString(); }
+ }), String.class);
+ Arrays.sort(fileNames);
+ writer = new OutputStreamWriter(out, ISO_8859_1);
+ for (String name : fileNames) {
+ writer.write(name);
+ writer.write('\n');
+ }
+ } finally {
+ if (writer != null) {
+ writer.close();
+ }
+ }
+ }
+ };
+ }
+
+ @Override
+ protected String computeKey() {
+ Fingerprint f = new Fingerprint();
+ f.addString(GUID);
+ f.addString(instrumentationFilter.toString());
+ return f.hexDigestAndReset();
+ }
+
+ /**
+ * Instantiates instrumented file manifest for the given target.
+ *
+ * @param ruleContext context of the executable configured target
+ * @param additionalSourceFiles additional instrumented source files, as
+ * collected by the {@link InstrumentedFilesCollector}
+ * @param metadataFiles *.gcno/*.em files collected by the {@link InstrumentedFilesCollector}
+ * @return instrumented file manifest artifact
+ */
+ public static Artifact getInstrumentedFileManifest(final RuleContext ruleContext,
+ final Collection<Artifact> additionalSourceFiles, final Collection<Artifact> metadataFiles) {
+ // Instrumented manifest makes sense only for rules with binary output.
+ Preconditions.checkState(ruleContext.getRule().hasBinaryOutput());
+ final Artifact instrumentedFileManifest =
+ ruleContext.getAnalysisEnvironment().getDerivedArtifact(
+ // Do not use replaceExtension(), as we may get name conflicts (two target-names have the
+ // same base name and only differ by extension).
+ FileSystemUtils.appendExtension(
+ Util.getWorkspaceRelativePath(ruleContext.getTarget()), ".instrumented_files"),
+ ruleContext.getConfiguration().getBinDirectory());
+
+ // Instrumented manifest artifact might already exist in case when multiple test
+ // actions that use slightly different subsets of runfiles set are generated for the same rule.
+ // So check whether we need to create a new action instance.
+ ImmutableList<Artifact> inputs = ImmutableList.<Artifact>builder()
+ .addAll(additionalSourceFiles)
+ .addAll(metadataFiles)
+ .build();
+ ruleContext.registerAction(new InstrumentedFileManifestAction(
+ ruleContext.getActionOwner(), inputs, additionalSourceFiles, metadataFiles,
+ instrumentedFileManifest, ruleContext.getConfiguration().getInstrumentationFilter()));
+
+ return instrumentedFileManifest;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/test/InstrumentedFilesCollector.java b/src/main/java/com/google/devtools/build/lib/rules/test/InstrumentedFilesCollector.java
new file mode 100644
index 0000000..e62a3b8
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/test/InstrumentedFilesCollector.java
@@ -0,0 +1,211 @@
+// Copyright 2014 Google Inc. 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.rules.test;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.AnalysisEnvironment;
+import com.google.devtools.build.lib.analysis.FileProvider;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.util.FileType;
+import com.google.devtools.build.lib.util.FileTypeSet;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * A helper class for collecting instrumented files and metadata for a target.
+ */
+public final class InstrumentedFilesCollector {
+
+ /**
+ * The set of file types and attributes to visit to collect instrumented files for a certain rule
+ * type. The class is intentionally immutable, so that a single instance is sufficient for all
+ * rules of the same type (and in some cases all rules of related types, such as all {@code foo_*}
+ * rules).
+ */
+ @Immutable
+ public static final class InstrumentationSpec {
+ private final FileTypeSet instrumentedFileTypes;
+ private final Collection<String> instrumentedAttributes;
+
+ public InstrumentationSpec(FileTypeSet instrumentedFileTypes,
+ Collection<String> instrumentedAttributes) {
+ this.instrumentedFileTypes = instrumentedFileTypes;
+ this.instrumentedAttributes = ImmutableList.copyOf(instrumentedAttributes);
+ }
+
+ public InstrumentationSpec(FileTypeSet instrumentedFileTypes,
+ String... instrumentedAttributes) {
+ this(instrumentedFileTypes, ImmutableList.copyOf(instrumentedAttributes));
+ }
+
+ /**
+ * Returns a new instrumentation spec with the given attribute names replacing the ones
+ * stored in this object.
+ */
+ public InstrumentationSpec withAttributes(String... instrumentedAttributes) {
+ return new InstrumentationSpec(instrumentedFileTypes, instrumentedAttributes);
+ }
+ }
+
+ /**
+ * The implementation for the local metadata collection. The intention is that implementations
+ * recurse over the locally (i.e., for that configured target) created actions and collect
+ * metadata files.
+ */
+ public abstract static class LocalMetadataCollector {
+ /**
+ * Recursively runs over the local actions and add metadata files to the metadataFilesBuilder.
+ */
+ public abstract void collectMetadataArtifacts(
+ Iterable<Artifact> artifacts, AnalysisEnvironment analysisEnvironment,
+ NestedSetBuilder<Artifact> metadataFilesBuilder);
+
+ /**
+ * Adds action output of a particular type to metadata files.
+ *
+ * <p>Only adds the first output that matches the given file type.
+ *
+ * @param metadataFilesBuilder builder to collect metadata files
+ * @param action the action whose outputs to scan
+ * @param fileType the filetype of outputs which should be collected
+ */
+ protected void addOutputs(NestedSetBuilder<Artifact> metadataFilesBuilder,
+ Action action, FileType fileType) {
+ for (Artifact output : action.getOutputs()) {
+ if (fileType.matches(output.getFilename())) {
+ metadataFilesBuilder.add(output);
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * Only collects files transitively from srcs, deps, and data attributes.
+ */
+ public static final InstrumentationSpec TRANSITIVE_COLLECTION_SPEC = new InstrumentationSpec(
+ FileTypeSet.NO_FILE,
+ "srcs", "deps", "data");
+
+ /**
+ * An explicit constant for a {@link LocalMetadataCollector} that doesn't collect anything.
+ */
+ public static final LocalMetadataCollector NO_METADATA_COLLECTOR = null;
+
+ private final RuleContext ruleContext;
+ private final InstrumentationSpec spec;
+ private final LocalMetadataCollector localMetadataCollector;
+ private final NestedSet<Artifact> instrumentationMetadataFiles;
+ private final NestedSet<Artifact> instrumentedFiles;
+
+ public InstrumentedFilesCollector(RuleContext ruleContext, InstrumentationSpec spec,
+ LocalMetadataCollector localMetadataCollector, Iterable<Artifact> rootFiles) {
+ this.ruleContext = ruleContext;
+ this.spec = spec;
+ this.localMetadataCollector = localMetadataCollector;
+ Preconditions.checkNotNull(ruleContext, "RuleContext already cleared. That means that the"
+ + " collector data was already memoized. You do not have to call it again.");
+ if (!ruleContext.getConfiguration().isCodeCoverageEnabled()) {
+ instrumentedFiles = NestedSetBuilder.emptySet(Order.STABLE_ORDER);
+ instrumentationMetadataFiles = NestedSetBuilder.emptySet(Order.STABLE_ORDER);
+ } else {
+ NestedSetBuilder<Artifact> instrumentedFilesBuilder =
+ NestedSetBuilder.stableOrder();
+ NestedSetBuilder<Artifact> metadataFilesBuilder = NestedSetBuilder.stableOrder();
+ collect(ruleContext.getAnalysisEnvironment(), instrumentedFilesBuilder, metadataFilesBuilder,
+ rootFiles);
+ instrumentedFiles = instrumentedFilesBuilder.build();
+ instrumentationMetadataFiles = metadataFilesBuilder.build();
+ }
+ }
+
+ /**
+ * Returns instrumented source files for the target provided during construction.
+ */
+ public final NestedSet<Artifact> getInstrumentedFiles() {
+ return instrumentedFiles;
+ }
+
+ /**
+ * Returns instrumentation metadata files for the target provided during construction.
+ */
+ public final NestedSet<Artifact> getInstrumentationMetadataFiles() {
+ return instrumentationMetadataFiles;
+ }
+
+ /**
+ * Collects instrumented files and metadata files.
+ */
+ private void collect(AnalysisEnvironment analysisEnvironment,
+ NestedSetBuilder<Artifact> instrumentedFilesBuilder,
+ NestedSetBuilder<Artifact> metadataFilesBuilder,
+ Iterable<Artifact> rootFiles) {
+ for (TransitiveInfoCollection dep : getAllPrerequisites()) {
+ InstrumentedFilesProvider provider = dep.getProvider(InstrumentedFilesProvider.class);
+ if (provider != null) {
+ instrumentedFilesBuilder.addTransitive(provider.getInstrumentedFiles());
+ metadataFilesBuilder.addTransitive(provider.getInstrumentationMetadataFiles());
+ } else if (shouldIncludeLocalSources()) {
+ for (Artifact artifact : dep.getProvider(FileProvider.class).getFilesToBuild()) {
+ if (artifact.isSourceArtifact() &&
+ spec.instrumentedFileTypes.matches(artifact.getFilename())) {
+ instrumentedFilesBuilder.add(artifact);
+ }
+ }
+ }
+ }
+
+ if (localMetadataCollector != null) {
+ localMetadataCollector.collectMetadataArtifacts(rootFiles,
+ analysisEnvironment, metadataFilesBuilder);
+ }
+ }
+
+ /**
+ * Returns the list of attributes which should be (transitively) checked for sources and
+ * instrumentation metadata.
+ */
+ private Collection<String> getSourceAttributes() {
+ return spec.instrumentedAttributes;
+ }
+
+ private boolean shouldIncludeLocalSources() {
+ return ruleContext.getConfiguration().getInstrumentationFilter().isIncluded(
+ ruleContext.getLabel().toString());
+ }
+
+ private Iterable<TransitiveInfoCollection> getAllPrerequisites() {
+ List<TransitiveInfoCollection> prerequisites = new ArrayList<>();
+ for (String attr : getSourceAttributes()) {
+ if (ruleContext.getRule().isAttrDefined(attr, Type.LABEL_LIST) ||
+ ruleContext.getRule().isAttrDefined(attr, Type.LABEL)) {
+ Iterables.addAll(prerequisites, ruleContext.getPrerequisites(attr, Mode.DONT_CHECK));
+ }
+ }
+ return prerequisites;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/test/InstrumentedFilesProvider.java b/src/main/java/com/google/devtools/build/lib/rules/test/InstrumentedFilesProvider.java
new file mode 100644
index 0000000..b1f956c
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/test/InstrumentedFilesProvider.java
@@ -0,0 +1,35 @@
+// Copyright 2014 Google Inc. 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.rules.test;
+
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+
+/**
+ * A provider of instrumented file sources and instrumentation metadata.
+ */
+public interface InstrumentedFilesProvider extends TransitiveInfoProvider {
+
+ /**
+ * Returns a collection of source files for instrumented binaries.
+ */
+ NestedSet<Artifact> getInstrumentedFiles();
+
+ /**
+ * Returns a collection of instrumentation metadata files.
+ */
+ NestedSet<Artifact> getInstrumentationMetadataFiles();
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/test/InstrumentedFilesProviderImpl.java b/src/main/java/com/google/devtools/build/lib/rules/test/InstrumentedFilesProviderImpl.java
new file mode 100644
index 0000000..1452e2d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/test/InstrumentedFilesProviderImpl.java
@@ -0,0 +1,53 @@
+// Copyright 2014 Google Inc. 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.rules.test;
+
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+
+/**
+ * An implementation class for the InstrumentedFilesProvider interface.
+ */
+public final class InstrumentedFilesProviderImpl implements InstrumentedFilesProvider {
+ public static final InstrumentedFilesProvider EMPTY = new InstrumentedFilesProvider() {
+ @Override
+ public NestedSet<Artifact> getInstrumentedFiles() {
+ return NestedSetBuilder.<Artifact>emptySet(Order.STABLE_ORDER);
+ }
+ @Override
+ public NestedSet<Artifact> getInstrumentationMetadataFiles() {
+ return NestedSetBuilder.<Artifact>emptySet(Order.STABLE_ORDER);
+ }
+ };
+
+ private final NestedSet<Artifact> instrumentedFiles;
+ private final NestedSet<Artifact> instrumentationMetadataFiles;
+
+ public InstrumentedFilesProviderImpl(InstrumentedFilesCollector collector) {
+ this.instrumentedFiles = collector.getInstrumentedFiles();
+ this.instrumentationMetadataFiles = collector.getInstrumentationMetadataFiles();
+ }
+
+ @Override
+ public NestedSet<Artifact> getInstrumentedFiles() {
+ return instrumentedFiles;
+ }
+
+ @Override
+ public NestedSet<Artifact> getInstrumentationMetadataFiles() {
+ return instrumentationMetadataFiles;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/test/StandaloneTestStrategy.java b/src/main/java/com/google/devtools/build/lib/rules/test/StandaloneTestStrategy.java
new file mode 100644
index 0000000..006f789
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/test/StandaloneTestStrategy.java
@@ -0,0 +1,224 @@
+// Copyright 2014 Google Inc. 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.rules.test;
+
+import com.google.common.collect.Lists;
+import com.google.devtools.build.lib.actions.ActionExecutionContext;
+import com.google.devtools.build.lib.actions.BaseSpawn;
+import com.google.devtools.build.lib.actions.EnvironmentalExecException;
+import com.google.devtools.build.lib.actions.ExecException;
+import com.google.devtools.build.lib.actions.ExecutionStrategy;
+import com.google.devtools.build.lib.actions.Executor;
+import com.google.devtools.build.lib.actions.Spawn;
+import com.google.devtools.build.lib.actions.TestExecException;
+import com.google.devtools.build.lib.analysis.config.BinTools;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventKind;
+import com.google.devtools.build.lib.events.Reporter;
+import com.google.devtools.build.lib.util.io.FileOutErr;
+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 com.google.devtools.build.lib.view.test.TestStatus.BlazeTestStatus;
+import com.google.devtools.build.lib.view.test.TestStatus.TestCase;
+import com.google.devtools.build.lib.view.test.TestStatus.TestResultData;
+import com.google.devtools.common.options.OptionsClassProvider;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Runs TestRunnerAction actions.
+ */
+@ExecutionStrategy(contextType = TestActionContext.class,
+ name = { "standalone" })
+public class StandaloneTestStrategy extends TestStrategy {
+ /*
+ TODO(bazel-team):
+
+ * tests
+ * It would be nice to get rid of (cd $TEST_SRCDIR) in the test-setup script.
+ * test timeouts.
+ * parsing XML output.
+
+ */
+ protected final PathFragment runfilesPrefix;
+
+ public StandaloneTestStrategy(OptionsClassProvider requestOptions,
+ OptionsClassProvider startupOptions, BinTools binTools, PathFragment runfilesPrefix) {
+ super(requestOptions, startupOptions, binTools);
+
+ this.runfilesPrefix = runfilesPrefix;
+ }
+
+ private static final String TEST_SETUP = "tools/test/test-setup.sh";
+
+ @Override
+ public void exec(TestRunnerAction action, ActionExecutionContext actionExecutionContext)
+ throws ExecException, InterruptedException {
+ Path runfilesDir = null;
+ try {
+ runfilesDir = TestStrategy.getLocalRunfilesDirectory(
+ action, actionExecutionContext, binTools);
+ } catch (ExecException e) {
+ throw new TestExecException(e.getMessage());
+ }
+
+ Path workingDirectory = runfilesDir.getRelative(runfilesPrefix);
+ Map<String, String> env = getEnv(action, runfilesDir);
+ Spawn spawn = new BaseSpawn(getArgs(action), env,
+ action.getTestProperties().getExecutionInfo(),
+ action,
+ action.getTestProperties().getLocalResourceUsage());
+
+ Executor executor = actionExecutionContext.getExecutor();
+ try {
+ FileSystemUtils.createDirectoryAndParents(workingDirectory);
+ FileOutErr fileOutErr = new FileOutErr(action.getTestLog().getPath(),
+ action.resolve(actionExecutionContext.getExecutor().getExecRoot()).getTestStderr());
+ TestResultData data = execute(
+ actionExecutionContext.withFileOutErr(fileOutErr), spawn, action);
+ appendStderr(fileOutErr.getOutputFile(), fileOutErr.getErrorFile());
+ finalizeTest(actionExecutionContext, action, data);
+ } catch (IOException e) {
+ executor.getEventHandler().handle(Event.error("Caught I/O exception: " + e));
+ throw new EnvironmentalExecException("unexpected I/O exception", e);
+ }
+ }
+
+ private Map<String, String> getEnv(TestRunnerAction action, Path runfilesDir) {
+ Map<String, String> vars = getDefaultTestEnvironment(action);
+ BuildConfiguration config = action.getConfiguration();
+
+ vars.putAll(config.getDefaultShellEnvironment());
+ vars.putAll(config.getTestEnv());
+ vars.put("TEST_SRCDIR", runfilesDir.getRelative(runfilesPrefix).getPathString());
+
+ // TODO(bazel-team): set TEST_TMPDIR.
+
+ return vars;
+ }
+
+ private TestResultData execute(
+ ActionExecutionContext actionExecutionContext, Spawn spawn, TestRunnerAction action)
+ throws TestExecException, InterruptedException {
+ Executor executor = actionExecutionContext.getExecutor();
+ Closeable streamed = null;
+ Path testLogPath = action.getTestLog().getPath();
+ TestResultData.Builder builder = TestResultData.newBuilder();
+
+ try {
+ try {
+ if (executionOptions.testOutput.equals(TestOutputFormat.STREAMED)) {
+ streamed = new StreamedTestOutput(
+ Reporter.outErrForReporter(
+ actionExecutionContext.getExecutor().getEventHandler()), testLogPath);
+ }
+ executor.getSpawnActionContext(action.getMnemonic()).exec(spawn, actionExecutionContext);
+
+ builder.setTestPassed(true)
+ .setStatus(BlazeTestStatus.PASSED)
+ .setCachable(true);
+ } catch (ExecException e) {
+ // Execution failed, which we consider a test failure.
+
+ // TODO(bazel-team): set cachable==true for relevant statuses (failure, but not for
+ // timeout, etc.)
+ builder.setTestPassed(false)
+ .setStatus(BlazeTestStatus.FAILED);
+ } finally {
+ if (streamed != null) {
+ streamed.close();
+ }
+ }
+
+ TestCase details = parseTestResult(
+ action.resolve(actionExecutionContext.getExecutor().getExecRoot()).getXmlOutputPath());
+ if (details != null) {
+ builder.setTestCase(details);
+ }
+
+ return builder.build();
+ } catch (IOException e) {
+ throw new TestExecException(e.getMessage());
+ }
+ }
+
+ /**
+ * Outputs test result to the stdout after test has finished (e.g. for --test_output=all or
+ * --test_output=errors). Will also try to group output lines together (up to 10000 lines) so
+ * parallel test outputs will not get interleaved.
+ */
+ protected void processTestOutput(Executor executor, FileOutErr outErr, TestResult result)
+ throws IOException {
+ Path testOutput = executor.getExecRoot().getRelative(result.getTestLogPath().asFragment());
+ boolean isPassed = result.getData().getTestPassed();
+ try {
+ if (TestLogHelper.shouldOutputTestLog(executionOptions.testOutput, isPassed)) {
+ TestLogHelper.writeTestLog(testOutput, result.getTestName(), outErr.getOutputStream());
+ }
+ } finally {
+ if (isPassed) {
+ executor.getEventHandler().handle(new Event(EventKind.PASS, null, result.getTestName()));
+ } else {
+ if (result.getData().getStatus() == BlazeTestStatus.TIMEOUT) {
+ executor.getEventHandler().handle(
+ new Event(EventKind.TIMEOUT, null, result.getTestName()
+ + " (see " + testOutput + ")"));
+ } else {
+ executor.getEventHandler().handle(
+ new Event(EventKind.FAIL, null, result.getTestName() + " (see " + testOutput + ")"));
+ }
+ }
+ }
+ }
+
+ private final void finalizeTest(ActionExecutionContext actionExecutionContext,
+ TestRunnerAction action, TestResultData data) throws IOException, ExecException {
+ TestResult result = new TestResult(action, data, false);
+ postTestResult(actionExecutionContext.getExecutor(), result);
+
+ processTestOutput(actionExecutionContext.getExecutor(),
+ actionExecutionContext.getFileOutErr(), result);
+ // TODO(bazel-team): handle --test_output=errors, --test_output=all.
+
+ if (!executionOptions.testKeepGoing && data.getStatus() != BlazeTestStatus.PASSED) {
+ throw new TestExecException("Test failed: aborting");
+ }
+ }
+
+ private List<String> getArgs(TestRunnerAction action) {
+ List<String> args = Lists.newArrayList(TEST_SETUP);
+ TestTargetExecutionSettings execSettings = action.getExecutionSettings();
+
+ // Execute the test using the alias in the runfiles tree.
+ args.add(execSettings.getExecutable().getRootRelativePath().getPathString());
+ args.addAll(execSettings.getArgs());
+
+ return args;
+ }
+
+ @Override
+ public String strategyLocality(TestRunnerAction action) { return "standalone"; }
+
+ @Override
+ public TestResult newCachedTestResult(
+ Path execRoot, TestRunnerAction action, TestResultData data) {
+ return new TestResult(action, data, /*cached*/ true);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/test/TestActionBuilder.java b/src/main/java/com/google/devtools/build/lib/rules/test/TestActionBuilder.java
new file mode 100644
index 0000000..2ac9a0f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/test/TestActionBuilder.java
@@ -0,0 +1,270 @@
+// Copyright 2014 Google Inc. 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.rules.test;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.Root;
+import com.google.devtools.build.lib.analysis.AnalysisEnvironment;
+import com.google.devtools.build.lib.analysis.FileProvider;
+import com.google.devtools.build.lib.analysis.FilesToRunProvider;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.RunfilesSupport;
+import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
+import com.google.devtools.build.lib.analysis.Util;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.packages.TargetUtils;
+import com.google.devtools.build.lib.packages.TestSize;
+import com.google.devtools.build.lib.packages.TestTimeout;
+import com.google.devtools.build.lib.rules.test.TestProvider.TestParams;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.common.options.EnumConverter;
+
+import java.util.Collection;
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+/**
+ * Helper class to create test actions.
+ */
+public final class TestActionBuilder {
+
+ private final RuleContext ruleContext;
+ private RunfilesSupport runfilesSupport;
+ private Artifact executable;
+ private ExecutionInfoProvider executionRequirements;
+ private InstrumentedFilesProvider instrumentedFiles;
+ private int explicitShardCount;
+
+ public TestActionBuilder(RuleContext ruleContext) {
+ this.ruleContext = ruleContext;
+ }
+
+ /**
+ * Creates the test actions and artifacts using the previously set parameters.
+ *
+ * @return ordered list of test status artifacts
+ */
+ public TestParams build() {
+ Preconditions.checkState(runfilesSupport != null);
+ boolean local = TargetUtils.isTestRuleAndRunsLocally(ruleContext.getRule());
+ TestShardingStrategy strategy = ruleContext.getConfiguration().testShardingStrategy();
+ int shards = strategy.getNumberOfShards(
+ local, explicitShardCount, isTestShardingCompliant(),
+ TestSize.getTestSize(ruleContext.getRule()));
+ Preconditions.checkState(shards >= 0);
+ return createTestAction(Util.getWorkspaceRelativePath(ruleContext.getLabel()), shards);
+ }
+
+ private boolean isTestShardingCompliant() {
+ // See if it has a data dependency on the special target
+ // //tools:test_sharding_compliant. Test runners add this dependency
+ // to show they speak the sharding protocol.
+ // There are certain cases where this heuristic may fail, giving
+ // a "false positive" (where we shard the test even though the
+ // it isn't supported). We may want to refine this logic, but
+ // heuristically sharding is currently experimental. Also, we do detect
+ // false-positive cases and return an error.
+ return runfilesSupport.getRunfilesSymlinkNames().contains(
+ new PathFragment("tools/test_sharding_compliant"));
+ }
+
+ /**
+ * Set the runfiles and executable to be run as a test.
+ */
+ public TestActionBuilder setFilesToRunProvider(FilesToRunProvider provider) {
+ Preconditions.checkNotNull(provider.getRunfilesSupport());
+ Preconditions.checkNotNull(provider.getExecutable());
+ this.runfilesSupport = provider.getRunfilesSupport();
+ this.executable = provider.getExecutable();
+ return this;
+ }
+
+ public TestActionBuilder setInstrumentedFiles(
+ @Nullable InstrumentedFilesProvider instrumentedFiles) {
+ this.instrumentedFiles = instrumentedFiles;
+ return this;
+ }
+
+ public TestActionBuilder setExecutionRequirements(
+ @Nullable ExecutionInfoProvider executionRequirements) {
+ this.executionRequirements = executionRequirements;
+ return this;
+ }
+
+ /**
+ * Set the explicit shard count. Note that this may be overridden by the sharding strategy.
+ */
+ public TestActionBuilder setShardCount(int explicitShardCount) {
+ this.explicitShardCount = explicitShardCount;
+ return this;
+ }
+
+ /**
+ * Converts to {@link TestActionBuilder.TestShardingStrategy}.
+ */
+ public static class ShardingStrategyConverter extends EnumConverter<TestShardingStrategy> {
+ public ShardingStrategyConverter() {
+ super(TestShardingStrategy.class, "test sharding strategy");
+ }
+ }
+
+ /**
+ * A strategy for running the same tests in many processes.
+ */
+ public static enum TestShardingStrategy {
+ EXPLICIT {
+ @Override public int getNumberOfShards(boolean isLocal, int shardCountFromAttr,
+ boolean testShardingCompliant, TestSize testSize) {
+ return Math.max(shardCountFromAttr, 0);
+ }
+ },
+
+ EXPERIMENTAL_HEURISTIC {
+ @Override public int getNumberOfShards(boolean isLocal, int shardCountFromAttr,
+ boolean testShardingCompliant, TestSize testSize) {
+ if (shardCountFromAttr >= 0) {
+ return shardCountFromAttr;
+ }
+ if (isLocal || !testShardingCompliant) {
+ return 0;
+ }
+ return testSize.getDefaultShards();
+ }
+ },
+
+ DISABLED {
+ @Override public int getNumberOfShards(boolean isLocal, int shardCountFromAttr,
+ boolean testShardingCompliant, TestSize testSize) {
+ return 0;
+ }
+ };
+
+ public abstract int getNumberOfShards(boolean isLocal, int shardCountFromAttr,
+ boolean testShardingCompliant, TestSize testSize);
+ }
+
+ /**
+ * Creates a test action and artifacts for the given rule. The test action will
+ * use the specified executable and runfiles.
+ *
+ * @param targetName the relative path of the target to run
+ * @return ordered list of test artifacts, one per action. These are used to drive
+ * execution in Skyframe, and by AggregatingTestListener and
+ * TestResultAnalyzer to keep track of completed and pending test runs.
+ */
+ private TestParams createTestAction(PathFragment targetName, int shards) {
+ BuildConfiguration config = ruleContext.getConfiguration();
+ AnalysisEnvironment env = ruleContext.getAnalysisEnvironment();
+ Root root = config.getTestLogsDirectory();
+
+ NestedSetBuilder<Artifact> inputsBuilder = NestedSetBuilder.stableOrder();
+ inputsBuilder.addTransitive(
+ NestedSetBuilder.create(Order.STABLE_ORDER, runfilesSupport.getRunfilesMiddleman()));
+ for (TransitiveInfoCollection dep : ruleContext.getPrerequisites("$test_runtime", Mode.HOST)) {
+ inputsBuilder.addTransitive(dep.getProvider(FileProvider.class).getFilesToBuild());
+ }
+ TestTargetProperties testProperties = new TestTargetProperties(
+ ruleContext, executionRequirements);
+
+ // If the test rule does not provide InstrumentedFilesProvider, there's not much that we can do.
+ final boolean collectCodeCoverage = config.isCodeCoverageEnabled()
+ && instrumentedFiles != null;
+
+ TestTargetExecutionSettings executionSettings;
+ if (collectCodeCoverage) {
+ // Add instrumented file manifest artifact to the list of inputs. This file will contain
+ // exec paths of all source files that should be included into the code coverage output.
+ Collection<Artifact> metadataFiles =
+ ImmutableList.copyOf(instrumentedFiles.getInstrumentationMetadataFiles());
+ inputsBuilder.addTransitive(NestedSetBuilder.wrap(Order.STABLE_ORDER, metadataFiles));
+ for (TransitiveInfoCollection dep :
+ ruleContext.getPrerequisites(":coverage_support", Mode.HOST)) {
+ inputsBuilder.addTransitive(dep.getProvider(FileProvider.class).getFilesToBuild());
+ }
+ Artifact instrumentedFileManifest =
+ InstrumentedFileManifestAction.getInstrumentedFileManifest(ruleContext,
+ ImmutableList.copyOf(instrumentedFiles.getInstrumentedFiles()),
+ metadataFiles);
+ executionSettings = new TestTargetExecutionSettings(ruleContext, runfilesSupport,
+ executable, instrumentedFileManifest, shards);
+ inputsBuilder.add(instrumentedFileManifest);
+ } else {
+ executionSettings = new TestTargetExecutionSettings(ruleContext, runfilesSupport,
+ executable, null, shards);
+ }
+
+ if (config.getRunUnder() != null) {
+ Artifact runUnderExecutable = executionSettings.getRunUnderExecutable();
+ if (runUnderExecutable != null) {
+ inputsBuilder.add(runUnderExecutable);
+ }
+ }
+
+ int runsPerTest = config.getRunsPerTestForLabel(ruleContext.getLabel());
+
+ Iterable<Artifact> inputs = inputsBuilder.build();
+ int shardRuns = (shards > 0 ? shards : 1);
+ List<Artifact> results = Lists.newArrayListWithCapacity(runsPerTest * shardRuns);
+ ImmutableList.Builder<Artifact> coverageArtifacts = ImmutableList.builder();
+
+ for (int run = 0; run < runsPerTest; run++) {
+ // Use a 1-based index for user friendliness.
+ String runSuffix =
+ runsPerTest > 1 ? String.format("_run_%d_of_%d", run + 1, runsPerTest) : "";
+ for (int shard = 0; shard < shardRuns; shard++) {
+ String suffix = (shardRuns > 1 ? String.format("_shard_%d_of_%d", shard + 1, shards) : "")
+ + runSuffix;
+ Artifact testLog = env.getDerivedArtifact(
+ targetName.getChild("test" + suffix + ".log"), root);
+ Artifact cacheStatus = env.getDerivedArtifact(
+ targetName.getChild("test" + suffix + ".cache_status"), root);
+
+ Artifact coverageArtifact = null;
+ if (collectCodeCoverage) {
+ coverageArtifact =
+ env.getDerivedArtifact(targetName.getChild("coverage" + suffix + ".dat"), root);
+ coverageArtifacts.add(coverageArtifact);
+ }
+
+ Artifact microCoverageArtifact = null;
+ if (collectCodeCoverage && config.isMicroCoverageEnabled()) {
+ microCoverageArtifact =
+ env.getDerivedArtifact(targetName.getChild("coverage" + suffix + ".micro.dat"), root);
+ }
+
+ env.registerAction(new TestRunnerAction(
+ ruleContext.getActionOwner(), inputs,
+ testLog, cacheStatus,
+ coverageArtifact, microCoverageArtifact,
+ testProperties, executionSettings,
+ shard, run, config));
+ results.add(cacheStatus);
+ }
+ }
+ // TODO(bazel-team): Passing the reportGenerator to every TestParams is a bit strange.
+ Artifact reportGenerator = collectCodeCoverage
+ ? ruleContext.getPrerequisiteArtifact(":coverage_report_generator", Mode.HOST) : null;
+ return new TestParams(runsPerTest, shards, TestTimeout.getTestTimeout(ruleContext.getRule()),
+ ruleContext.getRule().getRuleClass(), ImmutableList.copyOf(results),
+ coverageArtifacts.build(), reportGenerator);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/test/TestActionContext.java b/src/main/java/com/google/devtools/build/lib/rules/test/TestActionContext.java
new file mode 100644
index 0000000..7f7a916
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/test/TestActionContext.java
@@ -0,0 +1,46 @@
+// Copyright 2014 Google Inc. 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.rules.test;
+
+import com.google.devtools.build.lib.actions.ActionExecutionContext;
+import com.google.devtools.build.lib.actions.ExecException;
+import com.google.devtools.build.lib.actions.Executor.ActionContext;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.view.test.TestStatus.TestResultData;
+
+import java.io.IOException;
+
+/**
+ * A context for the execution of test actions ({@link TestRunnerAction}).
+ */
+public interface TestActionContext extends ActionContext {
+
+ /**
+ * Executes the test command, directing standard out / err to {@code outErr}. The status of
+ * the test should be communicated by posting a {@link TestResult} object to the eventbus.
+ */
+ void exec(TestRunnerAction action,
+ ActionExecutionContext actionExecutionContext) throws ExecException, InterruptedException;
+
+ /**
+ * String describing where the action will run.
+ */
+ String strategyLocality(TestRunnerAction action);
+
+ /**
+ * Creates a cached test result.
+ */
+ TestResult newCachedTestResult(Path execRoot, TestRunnerAction action, TestResultData cached)
+ throws IOException;
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/test/TestLogHelper.java b/src/main/java/com/google/devtools/build/lib/rules/test/TestLogHelper.java
new file mode 100644
index 0000000..462c24c
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/test/TestLogHelper.java
@@ -0,0 +1,141 @@
+// Copyright 2014 Google Inc. 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.rules.test;
+
+import com.google.common.io.ByteStreams;
+import com.google.devtools.build.lib.rules.test.TestStrategy.TestOutputFormat;
+import com.google.devtools.build.lib.vfs.Path;
+
+import java.io.BufferedOutputStream;
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PrintStream;
+
+/**
+ * A helper class for test log handling. It determines whether the test log should
+ * be output and formats the test log for console display.
+ */
+public class TestLogHelper {
+
+ public static final String HEADER_DELIMITER =
+ "-----------------------------------------------------------------------------";
+
+ /**
+ * Determines whether the test log should be output from the current outputMode
+ * and whether the test has passed or not.
+ */
+ public static boolean shouldOutputTestLog(TestOutputFormat outputMode, boolean hasPassed) {
+ return (outputMode == TestOutputFormat.ALL) ||
+ (!hasPassed && (outputMode == TestOutputFormat.ERRORS));
+ }
+
+ /**
+ * Reads the contents of the test log from the provided testOutput file, adds
+ * header and footer and returns the result.
+ * This method also looks for a header delimiter and cuts off the text before it,
+ * except if the header is 50 lines or longer.
+ */
+ public static void writeTestLog(Path testOutput, String testName, OutputStream out)
+ throws IOException {
+ InputStream input = null;
+ PrintStream printOut = new PrintStream(new BufferedOutputStream(out));
+ try {
+ final String outputHeader =
+ "==================== Test output for " + testName + ":";
+ final String outputFooter =
+ "================================================================================";
+
+ printOut.println(outputHeader);
+ printOut.flush();
+
+ input = testOutput.getInputStream();
+ FilterTestHeaderOutputStream filteringOutputStream = getHeaderFilteringOutputStream(printOut);
+ ByteStreams.copy(input, filteringOutputStream);
+
+ if (!filteringOutputStream.foundHeader()) {
+ InputStream inputAgain = testOutput.getInputStream();
+ try {
+ ByteStreams.copy(inputAgain, out);
+ } finally {
+ inputAgain.close();
+ }
+ }
+
+ printOut.println(outputFooter);
+ } finally {
+ printOut.flush();
+ if (input != null) {
+ input.close();
+ }
+ }
+ }
+
+ /**
+ * Returns an output stream that doesn't write to original until it
+ * sees HEADER_DELIMITER by itself on a line.
+ */
+ public static FilterTestHeaderOutputStream getHeaderFilteringOutputStream(OutputStream original) {
+ return new FilterTestHeaderOutputStream(original);
+ }
+
+ private TestLogHelper() {
+ // Prevent Java from creating a public constructor.
+ }
+
+ /**
+ * Use this class to filter the streaming output of a test until we see the
+ * header delimiter.
+ */
+ public static class FilterTestHeaderOutputStream extends FilterOutputStream {
+
+ private boolean seenDelimiter = false;
+ private StringBuilder lineBuilder = new StringBuilder();
+
+ private static final int NEWLINE = '\n';
+
+ public FilterTestHeaderOutputStream(OutputStream out) {
+ super(out);
+ }
+
+ @Override
+ public void write(int b) throws IOException {
+ if (seenDelimiter) {
+ out.write(b);
+ } else if (b == NEWLINE) {
+ String line = lineBuilder.toString();
+ lineBuilder = new StringBuilder();
+ if (line.equals(TestLogHelper.HEADER_DELIMITER)) {
+ seenDelimiter = true;
+ }
+ } else if (lineBuilder.length() <= TestLogHelper.HEADER_DELIMITER.length()) {
+ lineBuilder.append((char) b);
+ }
+ }
+
+ @Override
+ public void write(byte b[], int off, int len) throws IOException {
+ if (seenDelimiter) {
+ out.write(b, off, len);
+ } else {
+ super.write(b, off, len);
+ }
+ }
+
+ public boolean foundHeader() {
+ return seenDelimiter;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/test/TestProvider.java b/src/main/java/com/google/devtools/build/lib/rules/test/TestProvider.java
new file mode 100644
index 0000000..d24fe6b
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/test/TestProvider.java
@@ -0,0 +1,143 @@
+// Copyright 2014 Google Inc. 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.rules.test;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
+import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.packages.TestTimeout;
+
+import java.util.List;
+
+/**
+ * A {@link TransitiveInfoProvider} for configured targets that implement test rules.
+ */
+@Immutable
+public final class TestProvider implements TransitiveInfoProvider {
+ private final TestParams testParams;
+ private final ImmutableList<String> testTags;
+
+ public TestProvider(TestParams testParams, ImmutableList<String> testTags) {
+ this.testParams = testParams;
+ this.testTags = testTags;
+ }
+
+ /**
+ * Returns the {@link TestParams} object for the test represented by the corresponding configured
+ * target.
+ */
+ public TestParams getTestParams() {
+ return testParams;
+ }
+
+ /**
+ * Temporary hack to allow dependencies on test_suite targets to continue to work for the time
+ * being.
+ */
+ public List<String> getTestTags() {
+ return testTags;
+ }
+
+ /**
+ * Returns the test status artifacts for a specified configured target
+ *
+ * @param target the configured target. Should belong to a test rule.
+ * @return the test status artifacts
+ */
+ public static ImmutableList<Artifact> getTestStatusArtifacts(TransitiveInfoCollection target) {
+ return target.getProvider(TestProvider.class).getTestParams().getTestStatusArtifacts();
+ }
+
+ /**
+ * A value class describing the properties of a test.
+ */
+ public static class TestParams {
+ private final int runs;
+ private final int shards;
+ private final TestTimeout timeout;
+ private final String testRuleClass;
+ private final ImmutableList<Artifact> testStatusArtifacts;
+ private final ImmutableList<Artifact> coverageArtifacts;
+ private final Artifact coverageReportGenerator;
+
+ /**
+ * Don't call this directly. Instead use {@link TestActionBuilder}.
+ */
+ TestParams(int runs, int shards, TestTimeout timeout, String testRuleClass,
+ ImmutableList<Artifact> testStatusArtifacts,
+ ImmutableList<Artifact> coverageArtifacts,
+ Artifact coverageReportGenerator) {
+ this.runs = runs;
+ this.shards = shards;
+ this.timeout = timeout;
+ this.testRuleClass = testRuleClass;
+ this.testStatusArtifacts = testStatusArtifacts;
+ this.coverageArtifacts = coverageArtifacts;
+ this.coverageReportGenerator = coverageReportGenerator;
+ }
+
+ /**
+ * Returns the number of times this test should be run.
+ */
+ public int getRuns() {
+ return runs;
+ }
+
+ /**
+ * Returns the number of shards for this test.
+ */
+ public int getShards() {
+ return shards;
+ }
+
+ /**
+ * Returns the timeout of this test.
+ */
+ public TestTimeout getTimeout() {
+ return timeout;
+ }
+
+ /**
+ * Returns the test rule class.
+ */
+ public String getTestRuleClass() {
+ return testRuleClass;
+ }
+
+ /**
+ * Returns a list of test status artifacts that represent serialized test status protobuffers
+ * produced by testing this target.
+ */
+ public ImmutableList<Artifact> getTestStatusArtifacts() {
+ return testStatusArtifacts;
+ }
+
+ /**
+ * Returns the coverageArtifacts
+ */
+ public ImmutableList<Artifact> getCoverageArtifacts() {
+ return coverageArtifacts;
+ }
+
+ /**
+ * Returns the coverage report generator tool.
+ */
+ public Artifact getCoverageReportGenerator() {
+ return coverageReportGenerator;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/test/TestResult.java b/src/main/java/com/google/devtools/build/lib/rules/test/TestResult.java
new file mode 100644
index 0000000..b0de6cd
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/test/TestResult.java
@@ -0,0 +1,133 @@
+// Copyright 2014 Google Inc. 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.rules.test;
+
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.lib.view.test.TestStatus.BlazeTestStatus;
+import com.google.devtools.build.lib.view.test.TestStatus.TestResultData;
+
+/**
+ * This is the event passed from the various test strategies to the {@code RecordingTestListener}
+ * upon test completion.
+ */
+@ThreadSafe
+@Immutable
+public class TestResult {
+
+ private final TestRunnerAction testAction;
+ private final TestResultData data;
+ private final boolean cached;
+
+ /**
+ * Construct the TestResult for the given test / status.
+ *
+ * @param testAction The test that was run.
+ * @param data test result protobuffer.
+ * @param cached true if this is a cached test result.
+ */
+ public TestResult(TestRunnerAction testAction, TestResultData data, boolean cached) {
+ this.testAction = Preconditions.checkNotNull(testAction);
+ this.data = data;
+ this.cached = cached;
+ }
+
+ public static boolean isBlazeTestStatusPassed(BlazeTestStatus status) {
+ return status == BlazeTestStatus.PASSED || status == BlazeTestStatus.FLAKY;
+ }
+
+ /**
+ * @return The test action.
+ */
+ public TestRunnerAction getTestAction() {
+ return testAction;
+ }
+
+ /**
+ * @return The test log path. Note, that actual log file may no longer
+ * correspond to this artifact - use getActualLogPath() method if
+ * you need log location.
+ */
+ public Path getTestLogPath() {
+ return testAction.getTestLog().getPath();
+ }
+
+ /**
+ * Return if result was loaded from local action cache.
+ */
+ public final boolean isCached() {
+ return cached;
+ }
+
+ /**
+ * @return Coverage data artifact, if available and null otherwise.
+ */
+ public PathFragment getCoverageData() {
+ if (data.getHasCoverage()) {
+ return testAction.getCoverageData().getExecPath();
+ }
+ return null;
+ }
+
+ /**
+ * @return The test status artifact.
+ */
+ public Artifact getTestStatusArtifact() {
+ // these artifacts are used to keep track of the number of pending and completed tests.
+ return testAction.getCacheStatusArtifact();
+ }
+
+
+ /**
+ * Gets the test name in a user-friendly format.
+ * Will generally include the target name and shard number, if applicable.
+ *
+ * @return The test name.
+ */
+ public String getTestName() {
+ return testAction.getTestName();
+ }
+
+ /**
+ * @return The test label.
+ */
+ public String getLabel() {
+ return Label.print(testAction.getOwner().getLabel());
+ }
+
+ /**
+ * @return The test shard number.
+ */
+ public int getShardNum() {
+ return testAction.getShardNum();
+ }
+
+ /**
+ * @return Total number of test shards. 0 means
+ * no sharding, whereas 1 means degenerate sharding.
+ */
+ public int getTotalShards() {
+ return testAction.getExecutionSettings().getTotalShards();
+ }
+
+ public TestResultData getData() {
+ return data;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/test/TestRunnerAction.java b/src/main/java/com/google/devtools/build/lib/rules/test/TestRunnerAction.java
new file mode 100644
index 0000000..28500a7
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/test/TestRunnerAction.java
@@ -0,0 +1,607 @@
+// Copyright 2014 Google Inc. 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.rules.test;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.AbstractAction;
+import com.google.devtools.build.lib.actions.ActionExecutionContext;
+import com.google.devtools.build.lib.actions.ActionExecutionException;
+import com.google.devtools.build.lib.actions.ActionOwner;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.ExecException;
+import com.google.devtools.build.lib.actions.Executor;
+import com.google.devtools.build.lib.actions.NotifyOnActionCacheHit;
+import com.google.devtools.build.lib.actions.ResourceSet;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.analysis.config.RunUnder;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.util.Fingerprint;
+import com.google.devtools.build.lib.util.LoggingUtil;
+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 com.google.devtools.build.lib.view.test.TestStatus.TestResultData;
+import com.google.devtools.common.options.TriState;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.logging.Level;
+
+import javax.annotation.Nullable;
+
+/**
+ * An Action representing a test with the associated environment (runfiles,
+ * environment variables, test result, etc). It consumes test executable and
+ * runfiles artifacts and produces test result and test status artifacts.
+ */
+// Not final so that we can mock it in tests.
+public class TestRunnerAction extends AbstractAction implements NotifyOnActionCacheHit {
+
+ private static final String GUID = "94857c93-f11c-4cbc-8c1b-e0a281633f9e";
+
+ private final BuildConfiguration configuration;
+ private final Artifact testLog;
+ private final Artifact cacheStatus;
+ private final PathFragment testWarningsPath;
+ private final PathFragment splitLogsPath;
+ private final PathFragment splitLogsDir;
+ private final PathFragment undeclaredOutputsDir;
+ private final PathFragment undeclaredOutputsZipPath;
+ private final PathFragment undeclaredOutputsAnnotationsDir;
+ private final PathFragment undeclaredOutputsManifestPath;
+ private final PathFragment undeclaredOutputsAnnotationsPath;
+ private final PathFragment xmlOutputPath;
+ @Nullable
+ private final PathFragment testShard;
+ private final PathFragment testExitSafe;
+ private final PathFragment testStderr;
+ private final PathFragment testInfrastructureFailure;
+ private final PathFragment baseDir;
+ private final String namePrefix;
+ private final Artifact coverageData;
+ private final Artifact microCoverageData;
+ private final TestTargetProperties testProperties;
+ private final TestTargetExecutionSettings executionSettings;
+ private final int shardNum;
+ private final int runNumber;
+
+ // Mutable state related to test caching.
+ private boolean checkedCaching = false;
+ private boolean unconditionalExecution = false;
+
+ private static ImmutableList<Artifact> list(Artifact... artifacts) {
+ ImmutableList.Builder<Artifact> builder = ImmutableList.builder();
+ for (Artifact artifact : artifacts) {
+ if (artifact != null) {
+ builder.add(artifact);
+ }
+ }
+ return builder.build();
+ }
+
+ /**
+ * Create new TestRunnerAction instance. Should not be called directly.
+ * Use {@link TestActionBuilder} instead.
+ *
+ * @param shardNum The shard number. Must be 0 if totalShards == 0
+ * (no sharding). Otherwise, must be >= 0 and < totalShards.
+ * @param runNumber test run number
+ */
+ TestRunnerAction(ActionOwner owner,
+ Iterable<Artifact> inputs,
+ Artifact testLog,
+ Artifact cacheStatus,
+ Artifact coverageArtifact,
+ Artifact microCoverageArtifact,
+ TestTargetProperties testProperties,
+ TestTargetExecutionSettings executionSettings,
+ int shardNum,
+ int runNumber,
+ BuildConfiguration configuration) {
+ super(owner, inputs, list(testLog, cacheStatus, coverageArtifact, microCoverageArtifact));
+ this.configuration = Preconditions.checkNotNull(configuration);
+ this.testLog = testLog;
+ this.cacheStatus = cacheStatus;
+ this.coverageData = coverageArtifact;
+ this.microCoverageData = microCoverageArtifact;
+ this.shardNum = shardNum;
+ this.runNumber = runNumber;
+ this.testProperties = Preconditions.checkNotNull(testProperties);
+ this.executionSettings = Preconditions.checkNotNull(executionSettings);
+
+ this.baseDir = cacheStatus.getExecPath().getParentDirectory();
+ this.namePrefix = FileSystemUtils.removeExtension(cacheStatus.getExecPath().getBaseName());
+
+ int totalShards = executionSettings.getTotalShards();
+ Preconditions.checkState((totalShards == 0 && shardNum == 0) ||
+ (totalShards > 0 && 0 <= shardNum && shardNum < totalShards));
+ this.testExitSafe = baseDir.getChild(namePrefix + ".exited_prematurely");
+ // testShard Path should be set only if sharding is enabled.
+ this.testShard = totalShards > 1
+ ? baseDir.getChild(namePrefix + ".shard")
+ : null;
+ this.xmlOutputPath = baseDir.getChild(namePrefix + ".xml");
+ this.testWarningsPath = baseDir.getChild(namePrefix + ".warnings");
+ this.testStderr = baseDir.getChild(namePrefix + ".err");
+ this.splitLogsDir = baseDir.getChild(namePrefix + ".raw_splitlogs");
+ // See note in {@link #getSplitLogsPath} on the choice of file name.
+ this.splitLogsPath = splitLogsDir.getChild("test.splitlogs");
+ this.undeclaredOutputsDir = baseDir.getChild(namePrefix + ".outputs");
+ this.undeclaredOutputsZipPath = undeclaredOutputsDir.getChild("outputs.zip");
+ this.undeclaredOutputsAnnotationsDir = baseDir.getChild(namePrefix + ".outputs_manifest");
+ this.undeclaredOutputsManifestPath = undeclaredOutputsAnnotationsDir.getChild("MANIFEST");
+ this.undeclaredOutputsAnnotationsPath = undeclaredOutputsAnnotationsDir.getChild("ANNOTATIONS");
+ this.testInfrastructureFailure = baseDir.getChild(namePrefix + ".infrastructure_failure");
+ }
+
+ public BuildConfiguration getConfiguration() {
+ return configuration;
+ }
+
+ public final PathFragment getBaseDir() {
+ return baseDir;
+ }
+
+ public final String getNamePrefix() {
+ return namePrefix;
+ }
+
+ @Override
+ public boolean showsOutputUnconditionally() {
+ return true;
+ }
+
+ @Override
+ public int getInputCount() {
+ return Iterables.size(getInputs());
+ }
+
+ @Override
+ protected String computeKey() {
+ Fingerprint f = new Fingerprint();
+ f.addString(GUID);
+ f.addStrings(executionSettings.getArgs());
+ f.addString(executionSettings.getTestFilter() == null ? "" : executionSettings.getTestFilter());
+ RunUnder runUnder = executionSettings.getRunUnder();
+ f.addString(runUnder == null ? "" : runUnder.getValue());
+ f.addStringMap(configuration.getTestEnv());
+ f.addString(testProperties.getSize().toString());
+ f.addString(testProperties.getTimeout().toString());
+ f.addStrings(testProperties.getTags());
+ f.addInt(testProperties.isLocal() ? 1 : 0);
+ f.addInt(shardNum);
+ f.addInt(executionSettings.getTotalShards());
+ f.addInt(runNumber);
+ f.addInt(configuration.getRunsPerTestForLabel(getOwner().getLabel()));
+ f.addInt(configuration.isCodeCoverageEnabled() ? 1 : 0);
+ return f.hexDigestAndReset();
+ }
+
+ @Override
+ public boolean executeUnconditionally() {
+ // Note: isVolatile must return true if executeUnconditionally can ever return true
+ // for this instance.
+ unconditionalExecution = updateExecuteUnconditionallyFromTestStatus();
+ checkedCaching = true;
+ return unconditionalExecution;
+ }
+
+ @Override
+ public boolean isVolatile() {
+ return true;
+ }
+
+ /**
+ * Saves cache status to disk.
+ */
+ public void saveCacheStatus(TestResultData data) throws IOException {
+ try (OutputStream out = cacheStatus.getPath().getOutputStream()) {
+ data.writeTo(out);
+ }
+ }
+
+ /**
+ * Returns the cache from disk, or null if there is an error.
+ */
+ @Nullable
+ private TestResultData readCacheStatus() {
+ try (InputStream in = cacheStatus.getPath().getInputStream()) {
+ return TestResultData.parseFrom(in);
+ } catch (IOException expected) {
+
+ }
+ return null;
+ }
+
+ private boolean updateExecuteUnconditionallyFromTestStatus() {
+ if (configuration.cacheTestResults() == TriState.NO || testProperties.isExternal()
+ || (configuration.cacheTestResults() == TriState.AUTO
+ && configuration.getRunsPerTestForLabel(getOwner().getLabel()) > 1)) {
+ return true;
+ }
+
+ // Test will not be executed unconditionally - check whether test result exists and is
+ // valid. If it is, method will return false and we will rely on the dependency checker
+ // to make a decision about test execution.
+ TestResultData status = readCacheStatus();
+ if (status != null) {
+ if (!status.getCachable()) {
+ return true;
+ }
+
+ return (configuration.cacheTestResults() == TriState.AUTO
+ && !status.getTestPassed());
+ }
+
+ // CacheStatus is an artifact, so if it does not exist, the dependency checker will rebuild
+ // it. We can't return "true" here, as it also signals to not accept cached remote results.
+ return false;
+ }
+
+ /**
+ * May only be called after the dependency checked called executeUnconditionally().
+ * Returns whether caching has been deemed safe by looking at the previous test run
+ * (for local caching). If the previous run is not present, return "true" here, as
+ * remote execution caching should be safe.
+ */
+ public boolean shouldCacheResult() {
+ Preconditions.checkState(checkedCaching);
+ return !unconditionalExecution;
+ }
+
+ @Override
+ public void actionCacheHit(Executor executor) {
+ checkedCaching = false;
+ try {
+ executor.getEventBus().post(
+ executor.getContext(TestActionContext.class).newCachedTestResult(
+ executor.getExecRoot(), this, readCacheStatus()));
+ } catch (IOException e) {
+ LoggingUtil.logToRemote(Level.WARNING, "Failed creating cached protocol buffer", e);
+ }
+ }
+
+ @Override
+ public ResourceSet estimateResourceConsumption(Executor executor) {
+ // return null here to indicate that resources would be managed manually
+ // during action execution.
+ return null;
+ }
+
+ @Override
+ protected String getRawProgressMessage() {
+ return "Testing " + getTestName();
+ }
+
+ @Override
+ public String describeStrategy(Executor executor) {
+ return executor.getContext(TestActionContext.class).strategyLocality(this);
+ }
+
+ /**
+ * Deletes <b>all</b> possible test outputs.
+ *
+ * TestRunnerAction potentially can create many more non-declared outputs - xml output,
+ * coverage data file and logs for failed attempts. All those outputs are uniquely
+ * identified by the test log base name with arbitrary prefix and extension.
+ */
+ @Override
+ protected void deleteOutputs(Path execRoot) throws IOException {
+ super.deleteOutputs(execRoot);
+
+ // We do not rely on globs, as it causes quadratic behavior in --runs_per_test and test
+ // shard count.
+
+ // We also need to remove *.(xml|data|shard|warnings|zip) files if they are present.
+ execRoot.getRelative(xmlOutputPath).delete();
+ execRoot.getRelative(testWarningsPath).delete();
+ // Note that splitLogsPath points to a file inside the splitLogsDir so
+ // it's not necessary to delete it explicitly.
+ FileSystemUtils.deleteTree(execRoot.getRelative(splitLogsDir));
+ FileSystemUtils.deleteTree(execRoot.getRelative(undeclaredOutputsDir));
+ FileSystemUtils.deleteTree(execRoot.getRelative(undeclaredOutputsAnnotationsDir));
+ execRoot.getRelative(testStderr).delete();
+ execRoot.getRelative(testExitSafe).delete();
+ if (testShard != null) {
+ execRoot.getRelative(testShard).delete();
+ }
+ execRoot.getRelative(testInfrastructureFailure).delete();
+
+ // Coverage files use "coverage" instead of "test".
+ String coveragePrefix = "coverage" + namePrefix.substring(4);
+
+ // We cannot use coverageData artifact since it may be null. Generate coverage name instead.
+ execRoot.getRelative(baseDir.getChild(coveragePrefix + ".dat")).delete();
+ // We cannot use microcoverageData artifact since it may be null. Generate filename instead.
+ execRoot.getRelative(baseDir.getChild(coveragePrefix + ".micro.dat")).delete();
+
+ // Delete files fetched from remote execution.
+ execRoot.getRelative(baseDir.getChild(namePrefix + ".zip")).delete();
+ deleteTestAttemptsDirMaybe(execRoot.getRelative(baseDir), namePrefix);
+ }
+
+ private void deleteTestAttemptsDirMaybe(Path outputDir, String namePrefix) throws IOException {
+ Path testAttemptsDir = outputDir.getChild(namePrefix + "_attempts");
+ if (testAttemptsDir.exists()) {
+ // Normally we should have used deleteTree(testAttemptsDir). However, if test output is
+ // in a FUSE filesystem implemented with the high-level API, there may be .fuse???????
+ // entries, which prevent removing the directory. As a workaround, code below will throw
+ // IOException if it will fail to remove something inside testAttemptsDir, but will
+ // silently suppress any exceptions when deleting testAttemptsDir itself.
+ FileSystemUtils.deleteTreesBelow(testAttemptsDir);
+ try {
+ testAttemptsDir.delete();
+ } catch (IOException e) {
+ // Do nothing.
+ }
+ }
+ }
+
+ /**
+ * Gets the test name in a user-friendly format.
+ * Will generally include the target name and run/shard numbers, if applicable.
+ */
+ public String getTestName() {
+ String suffix = getTestSuffix();
+ String label = Label.print(getOwner().getLabel());
+ return suffix.isEmpty() ? label : label + " " + suffix;
+ }
+
+ /**
+ * Gets the test suffix in a user-friendly format, eg "(shard 1 of 7)".
+ * Will include the target name and run/shard numbers, if applicable.
+ */
+ public String getTestSuffix() {
+ int totalShards = executionSettings.getTotalShards();
+ // Use a 1-based index for user friendliness.
+ int runsPerTest = configuration.getRunsPerTestForLabel(getOwner().getLabel());
+ if (totalShards > 1 && runsPerTest > 1) {
+ return String.format("(shard %d of %d, run %d of %d)", shardNum + 1, totalShards,
+ runNumber + 1, runsPerTest);
+ } else if (totalShards > 1) {
+ return String.format("(shard %d of %d)", shardNum + 1, totalShards);
+ } else if (runsPerTest > 1) {
+ return String.format("(run %d of %d)", runNumber + 1, runsPerTest);
+ } else {
+ return "";
+ }
+ }
+
+ public Artifact getTestLog() {
+ return testLog;
+ }
+
+ public ResolvedPaths resolve(Path execRoot) {
+ return new ResolvedPaths(execRoot);
+ }
+
+ public Artifact getCacheStatusArtifact() {
+ return cacheStatus;
+ }
+
+ public PathFragment getTestWarningsPath() {
+ return testWarningsPath;
+ }
+
+ public PathFragment getSplitLogsPath() {
+ return splitLogsPath;
+ }
+
+ /**
+ * @return path to the optional zip file of undeclared test outputs.
+ */
+ public PathFragment getUndeclaredOutputsZipPath() {
+ return undeclaredOutputsZipPath;
+ }
+
+ /**
+ * @return path to the undeclared output manifest file.
+ */
+ public PathFragment getUndeclaredOutputsManifestPath() {
+ return undeclaredOutputsManifestPath;
+ }
+
+ /**
+ * @return path to the undeclared output annotations file.
+ */
+ public PathFragment getUndeclaredOutputsAnnotationsPath() {
+ return undeclaredOutputsAnnotationsPath;
+ }
+
+ public PathFragment getTestShard() {
+ return testShard;
+ }
+
+ public PathFragment getExitSafeFile() {
+ return testExitSafe;
+ }
+
+ public PathFragment getInfrastructureFailureFile() {
+ return testInfrastructureFailure;
+ }
+
+ /**
+ * @return path to the optionally created XML output file created by the test.
+ */
+ public PathFragment getXmlOutputPath() {
+ return xmlOutputPath;
+ }
+
+ /**
+ * @return coverage data artifact or null if code coverage was not requested.
+ */
+ @Nullable public Artifact getCoverageData() {
+ return coverageData;
+ }
+
+ /**
+ * @return microcoverage data artifact or null if code coverage was not requested.
+ */
+ @Nullable public Artifact getMicroCoverageData() {
+ return microCoverageData;
+ }
+
+ public TestTargetProperties getTestProperties() {
+ return testProperties;
+ }
+
+ public TestTargetExecutionSettings getExecutionSettings() {
+ return executionSettings;
+ }
+
+ public boolean isSharded() {
+ return testShard != null;
+ }
+
+ /**
+ * @return the shard number for this action.
+ * If getTotalShards() > 0, must be >= 0 and < getTotalShards().
+ * Otherwise, must be 0.
+ */
+ public int getShardNum() {
+ return shardNum;
+ }
+
+ /**
+ * @return run number.
+ */
+ public int getRunNumber() {
+ return runNumber;
+ }
+
+ @Override
+ public Artifact getPrimaryOutput() {
+ return testLog;
+ }
+
+ @Override
+ public void execute(ActionExecutionContext actionExecutionContext)
+ throws ActionExecutionException, InterruptedException {
+ TestActionContext context =
+ actionExecutionContext.getExecutor().getContext(TestActionContext.class);
+ try {
+ context.exec(this, actionExecutionContext);
+ } catch (ExecException e) {
+ throw e.toActionExecutionException(this);
+ } finally {
+ checkedCaching = false;
+ }
+ }
+
+ @Override
+ public String getMnemonic() {
+ return "TestRunner";
+ }
+
+ /**
+ * The same set of paths as the parent test action, resolved against a given exec root.
+ */
+ public final class ResolvedPaths {
+ private final Path execRoot;
+
+ ResolvedPaths(Path execRoot) {
+ this.execRoot = Preconditions.checkNotNull(execRoot);
+ }
+
+ private Path getPath(PathFragment relativePath) {
+ return execRoot.getRelative(relativePath);
+ }
+
+ public final Path getBaseDir() {
+ return getPath(baseDir);
+ }
+
+ /**
+ * In rare cases, error messages will be printed to stderr instead of stdout. The test action is
+ * responsible for appending anything in the stderr file to the real test.log.
+ */
+ public Path getTestStderr() {
+ return getPath(testStderr);
+ }
+
+ public Path getTestWarningsPath() {
+ return getPath(testWarningsPath);
+ }
+
+ public Path getSplitLogsPath() {
+ return getPath(splitLogsPath);
+ }
+
+ /**
+ * @return path to the directory containing the split logs (raw and proto file).
+ */
+ public Path getSplitLogsDir() {
+ return getPath(splitLogsDir);
+ }
+
+ /**
+ * @return path to the optional zip file of undeclared test outputs.
+ */
+ public Path getUndeclaredOutputsZipPath() {
+ return getPath(undeclaredOutputsZipPath);
+ }
+
+ /**
+ * @return path to the directory to hold undeclared test outputs.
+ */
+ public Path getUndeclaredOutputsDir() {
+ return getPath(undeclaredOutputsDir);
+ }
+
+ /**
+ * @return path to the directory to hold undeclared output annotations parts.
+ */
+ public Path getUndeclaredOutputsAnnotationsDir() {
+ return getPath(undeclaredOutputsAnnotationsDir);
+ }
+
+ /**
+ * @return path to the undeclared output manifest file.
+ */
+ public Path getUndeclaredOutputsManifestPath() {
+ return getPath(undeclaredOutputsManifestPath);
+ }
+
+ /**
+ * @return path to the undeclared output annotations file.
+ */
+ public Path getUndeclaredOutputsAnnotationsPath() {
+ return getPath(undeclaredOutputsAnnotationsPath);
+ }
+
+ @Nullable
+ public Path getTestShard() {
+ return testShard == null ? null : getPath(testShard);
+ }
+
+ public Path getExitSafeFile() {
+ return getPath(testExitSafe);
+ }
+
+ public Path getInfrastructureFailureFile() {
+ return getPath(testInfrastructureFailure);
+ }
+
+ /**
+ * @return path to the optionally created XML output file created by the test.
+ */
+ public Path getXmlOutputPath() {
+ return getPath(xmlOutputPath);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/test/TestStrategy.java b/src/main/java/com/google/devtools/build/lib/rules/test/TestStrategy.java
new file mode 100644
index 0000000..4905e15
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/test/TestStrategy.java
@@ -0,0 +1,388 @@
+// Copyright 2014 Google Inc. 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.rules.test;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.io.ByteStreams;
+import com.google.common.io.Closeables;
+import com.google.devtools.build.lib.actions.ActionExecutionContext;
+import com.google.devtools.build.lib.actions.ExecException;
+import com.google.devtools.build.lib.actions.Executor;
+import com.google.devtools.build.lib.analysis.config.BinTools;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.exec.ExecutionOptions;
+import com.google.devtools.build.lib.exec.SymlinkTreeHelper;
+import com.google.devtools.build.lib.profiler.Profiler;
+import com.google.devtools.build.lib.profiler.ProfilerTask;
+import com.google.devtools.build.lib.runtime.BlazeServerStartupOptions;
+import com.google.devtools.build.lib.util.io.FileWatcher;
+import com.google.devtools.build.lib.util.io.OutErr;
+import com.google.devtools.build.lib.vfs.FileStatus;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.lib.view.test.TestStatus.TestCase;
+import com.google.devtools.common.options.Converters.RangeConverter;
+import com.google.devtools.common.options.EnumConverter;
+import com.google.devtools.common.options.OptionsClassProvider;
+import com.google.devtools.common.options.OptionsParsingException;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.annotation.Nullable;
+
+/**
+ * A strategy for executing a {@link TestRunnerAction}.
+ */
+public abstract class TestStrategy implements TestActionContext {
+ /**
+ * Converter for the --flaky_test_attempts option.
+ */
+ public static class TestAttemptsConverter extends RangeConverter {
+ public TestAttemptsConverter() {
+ super(1, 10);
+ }
+
+ @Override
+ public Integer convert(String input) throws OptionsParsingException {
+ if ("default".equals(input)) {
+ return -1;
+ } else {
+ return super.convert(input);
+ }
+ }
+
+ @Override
+ public String getTypeDescription() {
+ return super.getTypeDescription() + " or the string \"default\"";
+ }
+ }
+
+ public enum TestOutputFormat {
+ SUMMARY, // Provide summary output only.
+ ERRORS, // Print output from failed tests to the stderr after the test failure.
+ ALL, // Print output from all tests to the stderr after the test completion.
+ STREAMED; // Stream output for each test.
+
+ /**
+ * Converts to {@link TestOutputFormat}.
+ */
+ public static class Converter extends EnumConverter<TestOutputFormat> {
+ public Converter() {
+ super(TestOutputFormat.class, "test output");
+ }
+ }
+ }
+
+ public enum TestSummaryFormat {
+ SHORT, // Print information only about tests.
+ TERSE, // Like "SHORT", but even shorter: Do not print PASSED tests.
+ DETAILED, // Print information only about failed test cases.
+ NONE; // Do not print summary.
+
+ /**
+ * Converts to {@link TestSummaryFormat}.
+ */
+ public static class Converter extends EnumConverter<TestSummaryFormat> {
+ public Converter() {
+ super(TestSummaryFormat.class, "test summary");
+ }
+ }
+ }
+
+ public static final PathFragment TEST_TMP_ROOT = new PathFragment("_tmp");
+
+ // Used for selecting subset of testcase / testmethods.
+ private static final String TEST_BRIDGE_TEST_FILTER_ENV = "TESTBRIDGE_TEST_ONLY";
+
+ private final boolean statusServerRunning;
+ protected final ExecutionOptions executionOptions;
+ protected final BinTools binTools;
+
+ public TestStrategy(OptionsClassProvider requestOptionsProvider,
+ OptionsClassProvider startupOptionsProvider, BinTools binTools) {
+ this.executionOptions = requestOptionsProvider.getOptions(ExecutionOptions.class);
+ this.binTools = binTools;
+ BlazeServerStartupOptions startupOptions =
+ startupOptionsProvider.getOptions(BlazeServerStartupOptions.class);
+ statusServerRunning = startupOptions != null && startupOptions.useWebStatusServer > 0;
+ }
+
+ @Override
+ public abstract void exec(TestRunnerAction action, ActionExecutionContext actionExecutionContext)
+ throws ExecException, InterruptedException;
+
+ @Override
+ public abstract String strategyLocality(TestRunnerAction action);
+
+ /**
+ * Callback for determining the strategy locality.
+ *
+ * @param action the test action
+ * @param localRun whether to run it locally
+ */
+ protected String strategyLocality(TestRunnerAction action, boolean localRun) {
+ return strategyLocality(action);
+ }
+
+ /**
+ * Returns mutable map of default testing shell environment. By itself it is incomplete and is
+ * modified further by the specific test strategy implementations (mostly due to the fact that
+ * environments used locally and remotely are different).
+ */
+ protected Map<String, String> getDefaultTestEnvironment(TestRunnerAction action) {
+ Map<String, String> env = new HashMap<>();
+
+ env.putAll(action.getConfiguration().getDefaultShellEnvironment());
+ env.remove("LANG");
+ env.put("TZ", "UTC");
+ env.put("TEST_SIZE", action.getTestProperties().getSize().toString());
+ env.put("TEST_TIMEOUT", Integer.toString(getTimeout(action)));
+
+ if (action.isSharded()) {
+ env.put("TEST_SHARD_INDEX", Integer.toString(action.getShardNum()));
+ env.put("TEST_TOTAL_SHARDS",
+ Integer.toString(action.getExecutionSettings().getTotalShards()));
+ }
+
+ // When we run test multiple times, set different TEST_RANDOM_SEED values for each run.
+ if (action.getConfiguration().getRunsPerTestForLabel(action.getOwner().getLabel()) > 1) {
+ env.put("TEST_RANDOM_SEED", Integer.toString(action.getRunNumber() + 1));
+ }
+
+ String testFilter = action.getExecutionSettings().getTestFilter();
+ if (testFilter != null) {
+ env.put(TEST_BRIDGE_TEST_FILTER_ENV, testFilter);
+ }
+
+ return env;
+ }
+
+ /**
+ * Returns the number of attempts specific test action can be retried.
+ *
+ * <p>For rules with "flaky = 1" attribute, this method will return 3 unless --flaky_test_attempts
+ * option is given and specifies another value.
+ */
+ @VisibleForTesting /* protected */
+ public int getTestAttempts(TestRunnerAction action) {
+ if (executionOptions.testAttempts == -1) {
+ return action.getTestProperties().isFlaky() ? 3 : 1;
+ } else {
+ return executionOptions.testAttempts;
+ }
+ }
+
+ /**
+ * Returns timeout value in seconds that should be used for the given test action. We always use
+ * the "categorical timeouts" which are based on the --test_timeout flag. A rule picks its timeout
+ * but ends up with the same effective value as all other rules in that bucket.
+ */
+ protected final int getTimeout(TestRunnerAction testAction) {
+ return executionOptions.testTimeout.get(testAction.getTestProperties().getTimeout());
+ }
+
+ /**
+ * Returns a subset of the environment from the current shell.
+ *
+ * <p>Warning: Since these variables are not part of the configuration's fingerprint, they
+ * MUST NOT be used by any rule or action in such a way as to affect the semantics of that
+ * build step.
+ */
+ public Map<String, String> getAdmissibleShellEnvironment(BuildConfiguration config,
+ Iterable<String> variables) {
+ return getMapping(variables, config.getClientEnv());
+ }
+
+ /*
+ * Finalize test run: persist the result, and post on the event bus.
+ */
+ protected void postTestResult(Executor executor, TestResult result) throws IOException {
+ result.getTestAction().saveCacheStatus(result.getData());
+ executor.getEventBus().post(result);
+ }
+
+ /**
+ * Parse a test result XML file into a {@link TestCase}.
+ */
+ @Nullable
+ protected TestCase parseTestResult(Path resultFile) {
+ /* xml files. We avoid parsing it unnecessarily, since test results can potentially consume
+ a large amount of memory. */
+ if (executionOptions.testSummary != TestSummaryFormat.DETAILED && !statusServerRunning) {
+ return null;
+ }
+
+ try (InputStream fileStream = resultFile.getInputStream()) {
+ return new TestXmlOutputParser().parseXmlIntoTestResult(fileStream);
+ } catch (IOException | TestXmlOutputParserException e) {
+ return null;
+ }
+ }
+
+ /**
+ * For an given environment, returns a subset containing all variables in the given list if they
+ * are defined in the given environment.
+ */
+ @VisibleForTesting
+ public static Map<String, String> getMapping(Iterable<String> variables,
+ Map<String, String> environment) {
+ Map<String, String> result = new HashMap<>();
+ for (String var : variables) {
+ if (environment.containsKey(var)) {
+ result.put(var, environment.get(var));
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Returns the runfiles directory associated with the test executable,
+ * creating/updating it if necessary and --build_runfile_links is specified.
+ */
+ protected static Path getLocalRunfilesDirectory(TestRunnerAction testAction,
+ ActionExecutionContext actionExecutionContext, BinTools binTools) throws ExecException,
+ InterruptedException {
+ TestTargetExecutionSettings execSettings = testAction.getExecutionSettings();
+
+ // --nobuild_runfile_links disables runfiles generation only for C++ rules.
+ // In that case, getManifest returns the .runfiles_manifest (input) file,
+ // not the MANIFEST output file of the build-runfiles action. So the
+ // extension ".runfiles_manifest" indicates no runfiles tree.
+ if (!execSettings.getManifest().equals(execSettings.getInputManifest())) {
+ return execSettings.getManifest().getPath().getParentDirectory();
+ }
+
+ // We might need to build runfiles tree now, since it was not created yet
+ // local testing is needed.
+ Path program = execSettings.getExecutable().getPath();
+ Path runfilesDir = program.getParentDirectory().getChild(program.getBaseName() + ".runfiles");
+
+ // Synchronize runfiles tree generation on the runfiles manifest artifact.
+ // This is necessary, because we might end up with multiple test runner actions
+ // trying to generate same runfiles tree in case of --runs_per_test > 1 or
+ // local test sharding.
+ long startTime = Profiler.nanoTimeMaybe();
+ synchronized (execSettings.getManifest()) {
+ Profiler.instance().logSimpleTask(startTime, ProfilerTask.WAIT, testAction);
+ updateLocalRunfilesDirectory(testAction, runfilesDir, actionExecutionContext, binTools);
+ }
+
+ return runfilesDir;
+ }
+
+ /**
+ * Ensure the runfiles tree exists and is consistent with the TestAction's manifest
+ * ($0.runfiles_manifest), bringing it into consistency if not. The contents of the output file
+ * $0.runfiles/MANIFEST, if it exists, are used a proxy for the set of existing symlinks, to avoid
+ * the need for recursion.
+ */
+ private static void updateLocalRunfilesDirectory(TestRunnerAction testAction, Path runfilesDir,
+ ActionExecutionContext actionExecutionContext, BinTools binTools) throws ExecException,
+ InterruptedException {
+ Executor executor = actionExecutionContext.getExecutor();
+
+ TestTargetExecutionSettings execSettings = testAction.getExecutionSettings();
+ try {
+ if (Arrays.equals(runfilesDir.getRelative("MANIFEST").getMD5Digest(),
+ execSettings.getManifest().getPath().getMD5Digest())) {
+ return;
+ }
+ } catch (IOException e1) {
+ // Ignore it - we will just try to create runfiles directory.
+ }
+
+ executor.getEventHandler().handle(Event.progress(
+ "Building runfiles directory for '" + execSettings.getExecutable().prettyPrint() + "'."));
+
+ new SymlinkTreeHelper(execSettings.getManifest().getExecPath(),
+ runfilesDir.relativeTo(executor.getExecRoot()), /* filesetTree= */ false)
+ .createSymlinks(testAction, actionExecutionContext, binTools);
+
+ executor.getEventHandler().handle(Event.progress(testAction.getProgressMessage()));
+ }
+
+ /**
+ * In rare cases, we might write something to stderr. Append it to the real test.log.
+ */
+ protected static void appendStderr(Path stdOut, Path stdErr) throws IOException {
+ FileStatus stat = stdErr.statNullable();
+ OutputStream out = null;
+ InputStream in = null;
+ if (stat != null) {
+ try {
+ if (stat.getSize() > 0) {
+ if (stdOut.exists()) {
+ stdOut.setWritable(true);
+ }
+ out = stdOut.getOutputStream(true);
+ in = stdErr.getInputStream();
+ ByteStreams.copy(in, out);
+ }
+ } finally {
+ Closeables.close(out, true);
+ Closeables.close(in, true);
+ stdErr.delete();
+ }
+ }
+ }
+
+ /**
+ * Implements the --test_output=streamed option.
+ */
+ protected static class StreamedTestOutput implements Closeable {
+ private final TestLogHelper.FilterTestHeaderOutputStream headerFilter;
+ private final FileWatcher watcher;
+ private final Path testLogPath;
+ private final OutErr outErr;
+
+ public StreamedTestOutput(OutErr outErr, Path testLogPath) throws IOException {
+ this.testLogPath = testLogPath;
+ this.outErr = outErr;
+ this.headerFilter = TestLogHelper.getHeaderFilteringOutputStream(outErr.getOutputStream());
+ this.watcher = new FileWatcher(testLogPath, OutErr.create(headerFilter, headerFilter), false);
+ watcher.start();
+ }
+
+ @Override
+ public void close() throws IOException {
+ watcher.stopPumping();
+ try {
+ // The watcher thread might leak if the following call is interrupted.
+ // This is a relatively minor issue since the worst it could do is
+ // write one additional line from the test.log to the console later on
+ // in the build.
+ watcher.join();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ if (!headerFilter.foundHeader()) {
+ InputStream input = testLogPath.getInputStream();
+ try {
+ ByteStreams.copy(input, outErr.getOutputStream());
+ } finally {
+ input.close();
+ }
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/test/TestSuite.java b/src/main/java/com/google/devtools/build/lib/rules/test/TestSuite.java
new file mode 100644
index 0000000..ef795aa
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/test/TestSuite.java
@@ -0,0 +1,99 @@
+// Copyright 2014 Google Inc. 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.rules.test;
+
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTargetBuilder;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.Runfiles;
+import com.google.devtools.build.lib.analysis.RunfilesProvider;
+import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
+import com.google.devtools.build.lib.packages.TestTargetUtils;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.rules.RuleConfiguredTargetFactory;
+import com.google.devtools.build.lib.util.Pair;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Implementation for the "test_suite" rule.
+ */
+public class TestSuite implements RuleConfiguredTargetFactory {
+
+ @Override
+ public ConfiguredTarget create(RuleContext ruleContext) {
+ checkTestsAndSuites(ruleContext, "tests");
+ checkTestsAndSuites(ruleContext, "suites");
+ if (ruleContext.hasErrors()) {
+ return null;
+ }
+
+ //
+ // CAUTION! Keep this logic consistent with lib.query2.TestsExpression!
+ //
+
+ List<String> tagsAttribute = new ArrayList<>(
+ ruleContext.attributes().get("tags", Type.STRING_LIST));
+ tagsAttribute.remove("manual");
+ Pair<Collection<String>, Collection<String>> requiredExcluded =
+ TestTargetUtils.sortTagsBySense(tagsAttribute);
+
+ List<TransitiveInfoCollection> directTestsAndSuitesBuilder = new ArrayList<>();
+
+ // The set of implicit tests is determined in
+ // {@link com.google.devtools.build.lib.packages.Package}.
+ // Manual tests are already filtered out there. That is what $implicit_tests is about.
+ for (TransitiveInfoCollection dep :
+ Iterables.concat(
+ ruleContext.getPrerequisites("tests", Mode.TARGET),
+ ruleContext.getPrerequisites("suites", Mode.TARGET),
+ ruleContext.getPrerequisites("$implicit_tests", Mode.TARGET))) {
+ if (dep.getProvider(TestProvider.class) != null) {
+ List<String> tags = dep.getProvider(TestProvider.class).getTestTags();
+ if (!TestTargetUtils.testMatchesFilters(
+ tags, requiredExcluded.first, requiredExcluded.second, true)) {
+ // This test does not match our filter. Ignore it.
+ continue;
+ }
+ }
+ directTestsAndSuitesBuilder.add(dep);
+ }
+
+ Runfiles runfiles = new Runfiles.Builder()
+ .addTargets(directTestsAndSuitesBuilder, RunfilesProvider.DATA_RUNFILES)
+ .build();
+
+ return new RuleConfiguredTargetBuilder(ruleContext)
+ .add(RunfilesProvider.class,
+ RunfilesProvider.withData(Runfiles.EMPTY, runfiles))
+ .add(TransitiveTestsProvider.class, new TransitiveTestsProvider())
+ .build();
+ }
+
+ private void checkTestsAndSuites(RuleContext ruleContext, String attributeName) {
+ for (TransitiveInfoCollection dep : ruleContext.getPrerequisites(attributeName, Mode.TARGET)) {
+ // TODO(bazel-team): Maybe convert the TransitiveTestsProvider into an inner interface.
+ TransitiveTestsProvider provider = dep.getProvider(TransitiveTestsProvider.class);
+ TestProvider testProvider = dep.getProvider(TestProvider.class);
+ if (provider == null && testProvider == null) {
+ ruleContext.attributeError(attributeName,
+ "expecting a test or a test_suite rule but '" + dep.getLabel() + "' is not one");
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/test/TestTargetExecutionSettings.java b/src/main/java/com/google/devtools/build/lib/rules/test/TestTargetExecutionSettings.java
new file mode 100644
index 0000000..20ad8af
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/test/TestTargetExecutionSettings.java
@@ -0,0 +1,133 @@
+// Copyright 2014 Google Inc. 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.rules.test;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.FilesToRunProvider;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.RunfilesSupport;
+import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.analysis.config.RunUnder;
+import com.google.devtools.build.lib.packages.TargetUtils;
+
+import java.util.List;
+
+/**
+ * Container for common test execution settings shared by all
+ * all TestRunnerAction instances for the given test target.
+ */
+public final class TestTargetExecutionSettings {
+
+ private final List<String> testArguments;
+ private final String testFilter;
+ private final int totalShards;
+ private final RunUnder runUnder;
+ private final Artifact runUnderExecutable;
+ private final Artifact executable;
+ private final Artifact runfilesManifest;
+ private final Artifact runfilesInputManifest;
+ private final Artifact instrumentedFileManifest;
+
+ TestTargetExecutionSettings(RuleContext ruleContext, RunfilesSupport runfiles,
+ Artifact executable, Artifact instrumentedFileManifest, int shards) {
+ Preconditions.checkArgument(TargetUtils.isTestRule(ruleContext.getRule()));
+ Preconditions.checkArgument(shards >= 0);
+ BuildConfiguration config = ruleContext.getConfiguration();
+
+ List<String> targetArgs = runfiles.getArgs();
+ testArguments = targetArgs.isEmpty()
+ ? config.getTestArguments()
+ : ImmutableList.copyOf(Iterables.concat(targetArgs, config.getTestArguments()));
+
+ totalShards = shards;
+ runUnder = config.getRunUnder();
+ runUnderExecutable = getRunUnderExecutable(ruleContext);
+
+ this.testFilter = config.getTestFilter();
+ this.executable = executable;
+ this.runfilesManifest = runfiles.getRunfilesManifest();
+ this.runfilesInputManifest = runfiles.getRunfilesInputManifest();
+ this.instrumentedFileManifest = instrumentedFileManifest;
+ }
+
+ private static Artifact getRunUnderExecutable(RuleContext ruleContext) {
+ TransitiveInfoCollection runUnderTarget = ruleContext
+ .getPrerequisite(":run_under", Mode.DATA);
+ return runUnderTarget == null
+ ? null
+ : runUnderTarget.getProvider(FilesToRunProvider.class).getExecutable();
+ }
+
+ public List<String> getArgs() {
+ return testArguments;
+ }
+
+ public String getTestFilter() {
+ return testFilter;
+ }
+
+ public int getTotalShards() {
+ return totalShards;
+ }
+
+ public RunUnder getRunUnder() {
+ return runUnder;
+ }
+
+ public Artifact getRunUnderExecutable() {
+ return runUnderExecutable;
+ }
+
+ public Artifact getExecutable() {
+ return executable;
+ }
+
+ /**
+ * Returns the runfiles manifest for this test.
+ *
+ * <p>This returns either the input manifest outside of the runfiles tree,
+ * if blaze is run with --nobuild_runfile_links or the manifest inside the
+ * runfiles tree, if blaze is run with --build_runfile_links.
+ *
+ * @see com.google.devtools.build.lib.analysis.RunfilesSupport#getRunfilesManifest()
+ */
+ public Artifact getManifest() {
+ return runfilesManifest;
+ }
+
+ /**
+ * Returns the input runfiles manifest for this test.
+ *
+ * <p>This always returns the input manifest outside of the runfiles tree.
+ *
+ * @see com.google.devtools.build.lib.analysis.RunfilesSupport#getRunfilesInputManifest()
+ */
+ public Artifact getInputManifest() {
+ return runfilesInputManifest;
+ }
+
+ /**
+ * Returns instrumented file manifest or null if code coverage is not
+ * collected.
+ */
+ public Artifact getInstrumentedFileManifest() {
+ return instrumentedFileManifest;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/test/TestTargetProperties.java b/src/main/java/com/google/devtools/build/lib/rules/test/TestTargetProperties.java
new file mode 100644
index 0000000..8cf26b8
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/test/TestTargetProperties.java
@@ -0,0 +1,131 @@
+// Copyright 2014 Google Inc. 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.rules.test;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import com.google.devtools.build.lib.actions.ResourceSet;
+import com.google.devtools.build.lib.actions.Spawn;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.packages.TargetUtils;
+import com.google.devtools.build.lib.packages.TestSize;
+import com.google.devtools.build.lib.packages.TestTimeout;
+import com.google.devtools.build.lib.packages.Type;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Container for test target properties available to the
+ * TestRunnerAction instance.
+ */
+public class TestTargetProperties {
+
+ /**
+ * Resources used by local tests of various sizes.
+ */
+ private static final ResourceSet SMALL_RESOURCES = new ResourceSet(20, 0.9, 0.00);
+ private static final ResourceSet MEDIUM_RESOURCES = new ResourceSet(100, 0.9, 0.1);
+ private static final ResourceSet LARGE_RESOURCES = new ResourceSet(300, 0.8, 0.1);
+ private static final ResourceSet ENORMOUS_RESOURCES = new ResourceSet(800, 0.7, 0.4);
+
+ private static ResourceSet getResourceSetFromSize(TestSize size) {
+ switch (size) {
+ case SMALL: return SMALL_RESOURCES;
+ case MEDIUM: return MEDIUM_RESOURCES;
+ case LARGE: return LARGE_RESOURCES;
+ default: return ENORMOUS_RESOURCES;
+ }
+ }
+
+ private final TestSize size;
+ private final TestTimeout timeout;
+ private final List<String> tags;
+ private final boolean isLocal;
+ private final boolean isFlaky;
+ private final boolean isExternal;
+ private final String language;
+ private final ImmutableMap<String, String> executionInfo;
+
+ /**
+ * Creates test target properties instance. Constructor expects that it
+ * will be called only for test configured targets.
+ */
+ TestTargetProperties(RuleContext ruleContext,
+ ExecutionInfoProvider executionRequirements) {
+ Rule rule = ruleContext.getRule();
+
+ Preconditions.checkState(TargetUtils.isTestRule(rule));
+ size = TestSize.getTestSize(rule);
+ timeout = TestTimeout.getTestTimeout(rule);
+ tags = ruleContext.attributes().get("tags", Type.STRING_LIST);
+ isLocal = TargetUtils.isLocalTestRule(rule) || TargetUtils.isExclusiveTestRule(rule);
+
+ // We need to use method on ruleConfiguredTarget to perform validation.
+ isFlaky = ruleContext.attributes().get("flaky", Type.BOOLEAN);
+ isExternal = TargetUtils.isExternalTestRule(rule);
+
+ Map<String, String> executionInfo = Maps.newLinkedHashMap();
+ executionInfo.putAll(TargetUtils.getExecutionInfo(rule));
+ if (executionRequirements != null) {
+ // This will overwrite whatever TargetUtils put there, which might be confusing.
+ executionInfo.putAll(executionRequirements.getExecutionInfo());
+ }
+ this.executionInfo = ImmutableMap.copyOf(executionInfo);
+
+ language = TargetUtils.getRuleLanguage(rule);
+ }
+
+ public TestSize getSize() {
+ return size;
+ }
+
+ public TestTimeout getTimeout() {
+ return timeout;
+ }
+
+ public List<String> getTags() {
+ return tags;
+ }
+
+ public boolean isLocal() {
+ return isLocal;
+ }
+
+ public boolean isFlaky() {
+ return isFlaky;
+ }
+
+ public boolean isExternal() {
+ return isExternal;
+ }
+
+ public ResourceSet getLocalResourceUsage() {
+ return TestTargetProperties.getResourceSetFromSize(size);
+ }
+
+ /**
+ * Returns a map of execution info. See {@link Spawn#getExecutionInfo}.
+ */
+ public ImmutableMap<String, String> getExecutionInfo() {
+ return executionInfo;
+ }
+
+ public String getLanguage() {
+ return language;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/test/TestXmlOutputParser.java b/src/main/java/com/google/devtools/build/lib/rules/test/TestXmlOutputParser.java
new file mode 100644
index 0000000..8d660ec
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/test/TestXmlOutputParser.java
@@ -0,0 +1,345 @@
+// Copyright 2014 Google Inc. 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.rules.test;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.view.test.TestStatus.TestCase;
+import com.google.devtools.build.lib.view.test.TestStatus.TestCase.Type;
+import com.google.protobuf.UninitializedMessageException;
+
+import java.io.InputStream;
+import java.util.Collection;
+
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLStreamConstants;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+
+/**
+ * Parses a test.xml generated by jUnit or any testing framework
+ * into a protocol buffer. The schema of the test.xml is a bit hazy, so there is
+ * some guesswork involved.
+ */
+class TestXmlOutputParser {
+ // jUnit can use either "testsuites" or "testsuite".
+ private static final Collection<String> TOPLEVEL_ELEMENT_NAMES =
+ ImmutableSet.of("testsuites", "testsuite");
+
+ public TestCase parseXmlIntoTestResult(InputStream xmlStream)
+ throws TestXmlOutputParserException {
+ return parseXmlToTree(xmlStream);
+ }
+
+ /**
+ * Parses the a test result XML file into the corresponding protocol buffer.
+ * @param xmlStream the XML data stream
+ * @return the protocol buffer with the parsed data, or null if there was
+ * an error while parsing the file.
+ *
+ * @throws TestXmlOutputParserException when the XML file cannot be parsed
+ */
+ private TestCase parseXmlToTree(InputStream xmlStream)
+ throws TestXmlOutputParserException {
+ XMLStreamReader parser = null;
+
+ try {
+ parser = XMLInputFactory.newInstance().createXMLStreamReader(xmlStream);
+
+ while (true) {
+ int event = parser.next();
+ if (event == XMLStreamConstants.END_DOCUMENT) {
+ return null;
+ }
+
+ // First find the topmost node.
+ if (event == XMLStreamConstants.START_ELEMENT) {
+ String elementName = parser.getLocalName();
+ if (TOPLEVEL_ELEMENT_NAMES.contains(elementName)) {
+ TestCase result = parseTestSuite(parser, elementName);
+ return result;
+ }
+ }
+ }
+ } catch (XMLStreamException e) {
+ throw new TestXmlOutputParserException(e);
+ } catch (NumberFormatException e) {
+ // The parser is definitely != null here.
+ throw new TestXmlOutputParserException(
+ "Number could not be parsed at "
+ + parser.getLocation().getLineNumber() + ":"
+ + parser.getLocation().getColumnNumber(),
+ e);
+ } catch (UninitializedMessageException e) {
+ // This happens when the XML does not contain a field that is required
+ // in the protocol buffer
+ throw new TestXmlOutputParserException(e);
+ } catch (RuntimeException e) {
+
+ // Seems like that an XNIException can leak through, even though it is not
+ // specified anywhere.
+ //
+ // It's a bad idea to refer to XNIException directly because the Xerces
+ // documentation says that it may not be available here soon (and it
+ // results in a compile-time warning anyway), so we do it the roundabout
+ // way: check if the class name has something to do with Xerces, and if
+ // so, wrap it in our own exception type, otherwise, let the stack
+ // unwinding continue.
+ String name = e.getClass().getCanonicalName();
+ if (name != null && name.contains("org.apache.xerces")) {
+ throw new TestXmlOutputParserException(e);
+ } else {
+ throw e;
+ }
+ } finally {
+ if (parser != null) {
+ try {
+ parser.close();
+ } catch (XMLStreamException e) {
+
+ // Ignore errors during closure so that we do not interfere with an
+ // already propagating exception.
+ }
+ }
+ }
+ }
+
+ /**
+ * Creates an exception suitable to be thrown when and a bad end tag appears.
+ * The exception could also be thrown from here but that would result in an
+ * extra stack frame, whereas this way, the topmost frame shows the location
+ * where the error occurred.
+ */
+ private TestXmlOutputParserException createBadElementException(
+ String expected, XMLStreamReader parser) {
+ return new TestXmlOutputParserException("Expected end of XML element '"
+ + expected + "' , but got '" + parser.getLocalName() + "' at "
+ + parser.getLocation().getLineNumber() + ":"
+ + parser.getLocation().getColumnNumber());
+ }
+
+ /**
+ * Parses a 'testsuite' element.
+ *
+ * @throws TestXmlOutputParserException if the XML document is malformed
+ * @throws XMLStreamException if there was an error processing the XML
+ * @throws NumberFormatException if one of the numeric fields does not contain
+ * a valid number
+ */
+ private TestCase parseTestSuite(XMLStreamReader parser, String elementName)
+ throws XMLStreamException, TestXmlOutputParserException {
+ TestCase.Builder builder = TestCase.newBuilder();
+ builder.setType(Type.TEST_SUITE);
+ for (int i = 0; i < parser.getAttributeCount(); i++) {
+ String name = parser.getAttributeLocalName(i).intern();
+ String value = parser.getAttributeValue(i);
+
+ if (name.equals("name")) {
+ builder.setName(value);
+ } else if (name.equals("time")) {
+ builder.setRunDurationMillis(parseTime(value));
+ }
+ }
+
+ parseContainedElements(parser, elementName, builder);
+ return builder.build();
+ }
+
+ /**
+ * Parses a time in test.xml format.
+ *
+ * @throws NumberFormatException if the time is malformed (i.e. is neither an
+ * integer nor a decimal fraction with '.' as the fraction separator)
+ */
+ private long parseTime(String string) {
+
+ // This is ugly. For Historical Reasons, we have to check whether the number
+ // contains a decimal point or not. If it does, the number is expressed in
+ // milliseconds, otherwise, in seconds.
+ if (string.contains(".")) {
+ return Math.round(Float.parseFloat(string) * 1000);
+ } else {
+ return Long.parseLong(string);
+ }
+ }
+
+ /**
+ * Parses a 'decorator' element.
+ *
+ * @throws TestXmlOutputParserException if the XML document is malformed
+ * @throws XMLStreamException if there was an error processing the XML
+ * @throws NumberFormatException if one of the numeric fields does not contain
+ * a valid number
+ */
+ private TestCase parseTestDecorator(XMLStreamReader parser)
+ throws XMLStreamException, TestXmlOutputParserException {
+ TestCase.Builder builder = TestCase.newBuilder();
+ builder.setType(Type.TEST_DECORATOR);
+ for (int i = 0; i < parser.getAttributeCount(); i++) {
+ String name = parser.getAttributeLocalName(i);
+ String value = parser.getAttributeValue(i);
+
+ builder.setName(name);
+ if (name.equals("classname")) {
+ builder.setClassName(value);
+ } else if (name.equals("time")) {
+ builder.setRunDurationMillis(parseTime(value));
+ }
+ }
+
+ parseContainedElements(parser, "testdecorator", builder);
+ return builder.build();
+ }
+
+ /**
+ * Parses child elements of the specified tag. Strictly speaking, not every
+ * element can be a child of every other, but the HierarchicalTestResult can
+ * handle that, and (in this case) it does not hurt to be a bit more flexible
+ * than necessary.
+ *
+ * @throws TestXmlOutputParserException if the XML document is malformed
+ * @throws XMLStreamException if there was an error processing the XML
+ * @throws NumberFormatException if one of the numeric fields does not contain
+ * a valid number
+ */
+ private void parseContainedElements(
+ XMLStreamReader parser, String elementName, TestCase.Builder builder)
+ throws XMLStreamException, TestXmlOutputParserException {
+ int failures = 0;
+ int errors = 0;
+
+ while (true) {
+ int event = parser.next();
+ switch (event) {
+ case XMLStreamConstants.START_ELEMENT:
+ String childElementName = parser.getLocalName().intern();
+
+ // We are not parsing four elements here: system-out, system-err,
+ // failure and error. They potentially contain useful information, but
+ // they can be too big to fit in the memory. We add failure and error
+ // elements to the output without a message, so that there is a
+ // difference between passed and failed test cases.
+ if (childElementName.equals("testsuite")) {
+ builder.addChild(parseTestSuite(parser, childElementName));
+ } else if (childElementName.equals("testcase")) {
+ builder.addChild(parseTestCase(parser));
+ } else if (childElementName.equals("failure")) {
+ failures += 1;
+ skipCompleteElement(parser);
+ } else if (childElementName.equals("error")) {
+ errors += 1;
+ skipCompleteElement(parser);
+ } else if (childElementName.equals("testdecorator")) {
+ builder.addChild(parseTestDecorator(parser));
+ } else {
+
+ // Unknown element encountered. Since the schema of the input file
+ // is a bit hazy, just skip it and go merrily on our way. Ignorance
+ // is bliss.
+ skipCompleteElement(parser);
+ }
+ break;
+
+ case XMLStreamConstants.END_ELEMENT:
+ // Propagate errors/failures from children up to the current case
+ for (int i = 0; i < builder.getChildCount(); i += 1) {
+ if (builder.getChild(i).getStatus() == TestCase.Status.ERROR) {
+ errors += 1;
+ }
+ if (builder.getChild(i).getStatus() == TestCase.Status.FAILED) {
+ failures += 1;
+ }
+ }
+
+ if (errors > 0) {
+ builder.setStatus(TestCase.Status.ERROR);
+ } else if (failures > 0) {
+ builder.setStatus(TestCase.Status.FAILED);
+ } else {
+ builder.setStatus(TestCase.Status.PASSED);
+ }
+ // This is the end tag of the element we are supposed to parse.
+ // Hooray, tell our superiors that our mission is complete.
+ if (!parser.getLocalName().equals(elementName)) {
+ throw createBadElementException(elementName, parser);
+ }
+ return;
+ }
+ }
+ }
+
+
+ /**
+ * Parses a 'testcase' element.
+ *
+ * @throws TestXmlOutputParserException if the XML document is malformed
+ * @throws XMLStreamException if there was an error processing the XML
+ * @throws NumberFormatException if the time field does not contain a valid
+ * number
+ */
+ private TestCase parseTestCase(XMLStreamReader parser)
+ throws XMLStreamException, TestXmlOutputParserException {
+ TestCase.Builder builder = TestCase.newBuilder();
+ builder.setType(Type.TEST_CASE);
+ for (int i = 0; i < parser.getAttributeCount(); i++) {
+ String name = parser.getAttributeLocalName(i).intern();
+ String value = parser.getAttributeValue(i);
+
+ if (name.equals("name")) {
+ builder.setName(value);
+ } else if (name.equals("classname")) {
+ builder.setClassName(value);
+ } else if (name.equals("time")) {
+ builder.setRunDurationMillis(parseTime(value));
+ } else if (name.equals("result")) {
+ builder.setResult(value);
+ } else if (name.equals("status")) {
+ if (value.equals("notrun")) {
+ builder.setRun(false);
+ } else if (value.equals("run")) {
+ builder.setRun(true);
+ }
+ }
+ }
+
+ parseContainedElements(parser, "testcase", builder);
+ return builder.build();
+ }
+
+ /**
+ * Skips over a complete XML element on the input.
+ * Precondition: the cursor is at a START_ELEMENT.
+ * Postcondition: the cursor is at an END_ELEMENT.
+ *
+ * @throws XMLStreamException if the XML is malformed
+ */
+ private void skipCompleteElement(XMLStreamReader parser) throws XMLStreamException {
+ int depth = 1;
+ while (true) {
+ int event = parser.next();
+
+ switch (event) {
+ case XMLStreamConstants.START_ELEMENT:
+ depth++;
+ break;
+
+ case XMLStreamConstants.END_ELEMENT:
+ if (--depth == 0) {
+ return;
+ }
+ break;
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/test/TestXmlOutputParserException.java b/src/main/java/com/google/devtools/build/lib/rules/test/TestXmlOutputParserException.java
new file mode 100644
index 0000000..c27ca9d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/test/TestXmlOutputParserException.java
@@ -0,0 +1,33 @@
+// Copyright 2014 Google Inc. 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.rules.test;
+
+/**
+ * This exception gets thrown if there was a problem with parsing a test.xml
+ * file.
+ */
+class TestXmlOutputParserException extends Exception {
+ public TestXmlOutputParserException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public TestXmlOutputParserException(Throwable cause) {
+ super(cause);
+ }
+
+ public TestXmlOutputParserException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/test/TransitiveTestsProvider.java b/src/main/java/com/google/devtools/build/lib/rules/test/TransitiveTestsProvider.java
new file mode 100644
index 0000000..c46b2a7
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/test/TransitiveTestsProvider.java
@@ -0,0 +1,25 @@
+// Copyright 2014 Google Inc. 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.rules.test;
+
+import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+
+/**
+ * Marker transitive info provider for test_suite rules to recognize one another.
+ */
+@Immutable
+public final class TransitiveTestsProvider implements TransitiveInfoProvider {
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/workspace/Bind.java b/src/main/java/com/google/devtools/build/lib/rules/workspace/Bind.java
new file mode 100644
index 0000000..49f829a
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/workspace/Bind.java
@@ -0,0 +1,125 @@
+// Copyright 2014 Google Inc. 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.rules.workspace;
+
+import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.UnmodifiableIterator;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.FileProvider;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.rules.RuleConfiguredTargetFactory;
+import com.google.devtools.build.lib.syntax.ClassObject;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.syntax.SkylarkNestedSet;
+
+/**
+ * Implementation for the bind rule.
+ */
+public class Bind implements RuleConfiguredTargetFactory {
+
+ /**
+ * This configured target pretends to be whatever type of target "actual" is, returning its
+ * transitive info providers and target, but returning the label for the //external target.
+ */
+ private static class BindConfiguredTarget implements ConfiguredTarget, ClassObject {
+
+ private Label label;
+ private ConfiguredTarget configuredTarget;
+ private BuildConfiguration config;
+
+ BindConfiguredTarget(RuleContext ruleContext) {
+ label = ruleContext.getRule().getLabel();
+ config = ruleContext.getConfiguration();
+ // TODO(bazel-team): we should special case ConfiguredTargetFactory.createConfiguredTarget,
+ // not cast down here.
+ configuredTarget = (ConfiguredTarget) ruleContext.getPrerequisite("actual", Mode.TARGET);
+ }
+
+ @Override
+ public <P extends TransitiveInfoProvider> P getProvider(Class<P> provider) {
+ return configuredTarget.getProvider(provider);
+ }
+
+ @Override
+ public Label getLabel() {
+ return label;
+ }
+
+ @Override
+ public Object get(String providerKey) {
+ return configuredTarget.get(providerKey);
+ }
+
+ @Override
+ public UnmodifiableIterator<TransitiveInfoProvider> iterator() {
+ return configuredTarget.iterator();
+ }
+
+ @Override
+ public Target getTarget() {
+ return configuredTarget.getTarget();
+ }
+
+ @Override
+ public BuildConfiguration getConfiguration() {
+ return config;
+ }
+
+ /* ClassObject methods */
+
+ @Override
+ public Object getValue(String name) {
+ if (name.equals("label")) {
+ return getLabel();
+ } else if (name.equals("files")) {
+ // A shortcut for files to build in Skylark. FileConfiguredTarget and RunleConfiguredTarget
+ // always has FileProvider and Error- and PackageGroupConfiguredTarget-s shouldn't be
+ // accessible in Skylark.
+ return SkylarkNestedSet.of(
+ Artifact.class, getProvider(FileProvider.class).getFilesToBuild());
+ }
+ return configuredTarget.get(name);
+ }
+
+ @SuppressWarnings("cast")
+ @Override
+ public ImmutableCollection<String> getKeys() {
+ return new ImmutableList.Builder<String>()
+ .add("label", "files")
+ .addAll(configuredTarget.getProvider(RuleConfiguredTarget.SkylarkProviders.class)
+ .getKeys())
+ .build();
+ }
+
+ @Override
+ public String errorMessage(String name) {
+ // Use the default error message.
+ return null;
+ }
+ }
+
+ @Override
+ public ConfiguredTarget create(RuleContext ruleContext) throws InterruptedException {
+ return new BindConfiguredTarget(ruleContext);
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/workspace/BindRule.java b/src/main/java/com/google/devtools/build/lib/rules/workspace/BindRule.java
new file mode 100644
index 0000000..c3f2dd2
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/workspace/BindRule.java
@@ -0,0 +1,126 @@
+// Copyright 2014 Google Inc. 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.rules.workspace;
+
+import static com.google.devtools.build.lib.packages.Attribute.attr;
+import static com.google.devtools.build.lib.packages.Type.LABEL;
+
+import com.google.devtools.build.lib.analysis.BaseRuleClasses.BaseRule;
+import com.google.devtools.build.lib.analysis.BlazeRule;
+import com.google.devtools.build.lib.analysis.RuleDefinition;
+import com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.RuleClass.Builder.RuleClassType;
+
+/**
+ * Binds an existing target to a target in the virtual //external package.
+ */
+@BlazeRule(name = "bind",
+ type = RuleClassType.WORKSPACE,
+ ancestors = {BaseRule.class},
+ factoryClass = Bind.class)
+public final class BindRule implements RuleDefinition {
+
+ @Override
+ public RuleClass build(RuleClass.Builder builder, RuleDefinitionEnvironment environment) {
+ return builder
+ /* <!-- #BLAZE_RULE(bind).ATTRIBUTE(actual) -->
+ The target to be aliased.
+
+ <p>This target must exist, but can be any type of rule (including bind).</p>
+ <!-- #END_BLAZE_RULE.ATTRIBUTE --> */
+ .add(attr("actual", LABEL).allowedFileTypes())
+ .setWorkspaceOnly()
+ .build();
+ }
+}
+/*<!-- #BLAZE_RULE (NAME = bind, TYPE = OTHER, FAMILY = General)[GENERIC_RULE] -->
+
+${ATTRIBUTE_SIGNATURE}
+
+<p>Gives a target an alias in the <code>//external</code> package.</p>
+
+${ATTRIBUTE_DEFINITION}
+
+<p>The <code>//external</code> package is not a "normal" package: there is no external/ directory,
+ so it can be thought of as a "virtual package" that contains all bound targets.</p>
+
+<h4 id="bind_examples">Examples</h4>
+
+<p>To give a target an alias, bind it in the <i>WORKSPACE</i> file. For example, suppose there is
+ a <code>java_library</code> target called <code>//third_party/javacc-v2</code>. This could be
+ aliased by adding the following to the <i>WORKSPACE</i> file:</p>
+
+<pre class="code">
+bind(
+ name = "javacc-latest",
+ actual = "//third_party/javacc-v2",
+)
+</pre>
+
+<p>Now targets can depend on <code>//external:javacc-latest</code> instead of
+ <code>//third_party/javacc-v2</code>. If javacc-v3 is released, the binding can be updated and
+ all of the BUILD files depending on <code>//external:javacc-latest</code> will now depend on
+ javacc-v3 without needing to be edited.</p>
+
+<p>Bind can also be used to refer to external repositories' targets. For example, if there is a
+ remote repository named <code>@my-ssl</code> imported in the WORKSPACE file. If the
+ <code>@my-ssl</code> repository has a cc_library target <code>//src:openssl-lib</code>, you
+ could make this target accessible for your program to depend on by using <code>bind</code>:</p>
+
+<pre class="code">
+bind(
+ name = "openssl",
+ actual = "@my-ssl//src:openssl-lib",
+)
+</pre>
+
+<p>BUILD files cannot use labels that include a repository name
+ ("@repository-name//package-name:target-name"), so the only way to depend on a target from
+ another repository is to <code>bind</code> it in the WORKSPACE file and then refer to it by its
+ aliased name in <code>//external</code> from a BUILD file.</p>
+
+<p>For example, in a BUILD file, the bound target could be used as follows:</p>
+
+<pre class="code">
+cc_library(
+ name = "sign-in",
+ srcs = ["sign_in.cc"],
+ hdrs = ["sign_in.h"],
+ deps = ["//external:openssl"],
+)
+</pre>
+
+<p>Within <code>sign_in.cc</code> and <code>sign_in.h</code>, the header files exposed by
+ <code>//external:openssl</code> can be referred to by their path relative to their repository
+ root. For example, if the rule definition for <code>@my-ssl//src:openssl-lib</code> looks like
+ this:</p>
+
+<pre class="code">
+cc_library(
+ name = "openssl-lib",
+ srcs = ["openssl.cc"],
+ hdrs = ["openssl.h"],
+)
+</pre>
+
+<p>Then <code>sign_in.cc</code>'s first lines might look like this:</p>
+
+<pre class="code">
+#include "sign_in.h"
+#include "src/openssl.h"
+</pre>
+
+<!-- #END_BLAZE_RULE -->*/
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/AbstractCriticalPathComponent.java b/src/main/java/com/google/devtools/build/lib/runtime/AbstractCriticalPathComponent.java
new file mode 100644
index 0000000..9bf7a3f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/AbstractCriticalPathComponent.java
@@ -0,0 +1,120 @@
+// Copyright 2014 Google Inc. 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.runtime;
+
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadCompatible;
+
+import javax.annotation.Nullable;
+
+/**
+ * This class records the critical path for the graph of actions executed.
+ */
+@ThreadCompatible
+public class AbstractCriticalPathComponent<C extends AbstractCriticalPathComponent<C>> {
+
+ /** Wall time start time for the action. In milliseconds. */
+ private final long startTime;
+ /** Wall time finish time for the action. In milliseconds. */
+ private long finishTime = 0;
+ protected volatile boolean isRunning = true;
+
+ /** We keep here the critical path time for the most expensive child. */
+ private long childAggregatedWallTime = 0;
+
+ /** The action for which we are storing the stat. */
+ private final Action action;
+
+ /**
+ * Child with the maximum critical path.
+ */
+ @Nullable
+ private C child;
+
+ public AbstractCriticalPathComponent(Action action, long startTime) {
+ this.action = action;
+ this.startTime = startTime;
+ }
+
+ /** Sets the finish time for the action in milliseconds. */
+ public void setFinishTimeMillis(long finishTime) {
+ Preconditions.checkState(isRunning, "Already stopped! %s.", action);
+ this.finishTime = finishTime;
+ isRunning = false;
+ }
+
+ /** The action for which we are storing the stat. */
+ public Action getAction() {
+ return action;
+ }
+
+ /**
+ * Add statistics for one dependency of this action.
+ */
+ public void addDepInfo(C dep) {
+ Preconditions.checkState(!dep.isRunning,
+ "Cannot add critical path stats when the action is not finished. %s. %s", action,
+ dep.getAction());
+ long childAggregatedWallTime = dep.getAggregatedWallTime();
+ // Replace the child if its critical path had the maximum wall time.
+ if (child == null || childAggregatedWallTime > this.childAggregatedWallTime) {
+ this.childAggregatedWallTime = childAggregatedWallTime;
+ child = dep;
+ }
+ }
+
+ public long getActionWallTime() {
+ Preconditions.checkState(!isRunning, "Still running %s", action);
+ return finishTime - startTime;
+ }
+
+ /**
+ * Returns the current critical path for the action in milliseconds.
+ *
+ * <p>Critical path is defined as : action_execution_time + max(child_critical_path).
+ */
+ public long getAggregatedWallTime() {
+ Preconditions.checkState(!isRunning, "Still running %s", action);
+ return getActionWallTime() + childAggregatedWallTime;
+ }
+
+ /** Time when the action started to execute. Milliseconds since epoch time. */
+ public long getStartTime() {
+ return startTime;
+ }
+
+ /**
+ * Get the child critical path component.
+ *
+ * <p>The component dependency with the maximum total critical path time.
+ */
+ @Nullable
+ public C getChild() {
+ return child;
+ }
+
+ /**
+ * Returns a human readable representation of the critical path stats with all the details.
+ */
+ @Override
+ public String toString() {
+ String currentTime = "still running ";
+ if (!isRunning) {
+ currentTime = String.format("%.2f", getActionWallTime() / 1000.0) + "s ";
+ }
+ return currentTime + action.describe();
+ }
+}
+
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/AggregatedCriticalPath.java b/src/main/java/com/google/devtools/build/lib/runtime/AggregatedCriticalPath.java
new file mode 100644
index 0000000..dd70c35
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/AggregatedCriticalPath.java
@@ -0,0 +1,70 @@
+// Copyright 2014 Google Inc. 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.runtime;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+
+/**
+ * Aggregates all the critical path components in one object. This allows us to easily access the
+ * components data and have a proper toString().
+ */
+public class AggregatedCriticalPath<T extends AbstractCriticalPathComponent> {
+
+ private final long totalTime;
+ private final ImmutableList<T> criticalPathComponents;
+
+ protected AggregatedCriticalPath(long totalTime, ImmutableList<T> criticalPathComponents) {
+ this.totalTime = totalTime;
+ this.criticalPathComponents = criticalPathComponents;
+ }
+
+ /** Total wall time in ms spent running the critical path actions. */
+ public long totalTime() {
+ return totalTime;
+ }
+
+ /** Returns a list of all the component stats for the critical path. */
+ public ImmutableList<T> components() {
+ return criticalPathComponents;
+ }
+
+ @Override
+ public String toString() {
+ return toString(false);
+ }
+
+ /**
+ * Returns a summary version of the critical path stats that omits stats that are not useful
+ * to the user.
+ */
+ public String toStringSummary() {
+ return toString(true);
+ }
+
+ private String toString(boolean summary) {
+ StringBuilder sb = new StringBuilder("Critical Path: ");
+ double totalMillis = totalTime;
+ sb.append(String.format("%.2f", totalMillis / 1000.0));
+ sb.append("s");
+ if (summary || criticalPathComponents.isEmpty()) {
+ return sb.toString();
+ }
+ sb.append("\n ");
+ Joiner.on("\n ").appendTo(sb, criticalPathComponents);
+ return sb.toString();
+ }
+}
+
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/AggregatingTestListener.java b/src/main/java/com/google/devtools/build/lib/runtime/AggregatingTestListener.java
new file mode 100644
index 0000000..cc240c4
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/AggregatingTestListener.java
@@ -0,0 +1,255 @@
+// Copyright 2014 Google Inc. 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.runtime;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.MapMaker;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Sets;
+import com.google.common.eventbus.AllowConcurrentEvents;
+import com.google.common.eventbus.EventBus;
+import com.google.common.eventbus.Subscribe;
+import com.google.devtools.build.lib.actions.ActionOwner;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.AnalysisFailureEvent;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.LabelAndConfiguration;
+import com.google.devtools.build.lib.analysis.TargetCompleteEvent;
+import com.google.devtools.build.lib.buildtool.buildevent.BuildCompleteEvent;
+import com.google.devtools.build.lib.buildtool.buildevent.BuildInterruptedEvent;
+import com.google.devtools.build.lib.buildtool.buildevent.TestFilteringCompleteEvent;
+import com.google.devtools.build.lib.concurrent.ThreadSafety;
+import com.google.devtools.build.lib.events.ExceptionListener;
+import com.google.devtools.build.lib.rules.test.TestProvider;
+import com.google.devtools.build.lib.rules.test.TestResult;
+import com.google.devtools.build.lib.view.test.TestStatus.BlazeTestStatus;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import java.util.concurrent.ConcurrentMap;
+
+/**
+ * This class aggregates and reports target-wide test statuses in real-time.
+ * It must be public for EventBus invocation.
+ */
+@ThreadSafety.ThreadSafe
+public class AggregatingTestListener {
+ private final ConcurrentMap<Artifact, TestResult> statusMap = new MapMaker().makeMap();
+
+ private final TestResultAnalyzer analyzer;
+ private final EventBus eventBus;
+ private final EventHandlerPreconditions preconditionHelper;
+ private volatile boolean blazeHalted = false;
+
+
+ // summaryLock guards concurrent access to these two collections, which should be kept
+ // synchronized with each other.
+ private final Map<LabelAndConfiguration, TestSummary.Builder> summaries;
+ private final Multimap<LabelAndConfiguration, Artifact> remainingRuns;
+ private final Object summaryLock = new Object();
+
+ public AggregatingTestListener(TestResultAnalyzer analyzer,
+ EventBus eventBus,
+ ExceptionListener listener) {
+ this.analyzer = analyzer;
+ this.eventBus = eventBus;
+ this.preconditionHelper = new EventHandlerPreconditions(listener);
+
+ this.summaries = Maps.newHashMap();
+ this.remainingRuns = HashMultimap.create();
+ }
+
+ /**
+ * @return An unmodifiable copy of the map of test results.
+ */
+ public Map<Artifact, TestResult> getStatusMap() {
+ return ImmutableMap.copyOf(statusMap);
+ }
+
+ /**
+ * Populates the test summary map as soon as test filtering is complete.
+ * This is the earliest at which the final set of targets to test is known.
+ */
+ @Subscribe
+ @AllowConcurrentEvents
+ public void populateTests(TestFilteringCompleteEvent event) {
+ // Add all target runs to the map, assuming 1:1 status artifact <-> result.
+ synchronized (summaryLock) {
+ for (ConfiguredTarget target : event.getTestTargets()) {
+ Iterable<Artifact> statusArtifacts =
+ target.getProvider(TestProvider.class).getTestParams().getTestStatusArtifacts();
+ preconditionHelper.checkState(remainingRuns.putAll(asKey(target), statusArtifacts));
+
+ // And create an empty summary suitable for incremental analysis.
+ // Also has the nice side effect of mapping labels to RuleConfiguredTargets.
+ TestSummary.Builder summary = TestSummary.newBuilder()
+ .setTarget(target)
+ .setStatus(BlazeTestStatus.NO_STATUS);
+ preconditionHelper.checkState(summaries.put(asKey(target), summary) == null);
+ }
+ }
+ }
+
+ /**
+ * Records a new test run result and incrementally updates the target status.
+ * This event is sent upon completion of executed test runs.
+ */
+ @Subscribe
+ @AllowConcurrentEvents
+ public void testEvent(TestResult result) {
+ Preconditions.checkState(
+ statusMap.put(result.getTestStatusArtifact(), result) == null,
+ "Duplicate result reported for an individual test shard");
+
+ ActionOwner testOwner = result.getTestAction().getOwner();
+ LabelAndConfiguration targetLabel = LabelAndConfiguration.of(
+ testOwner.getLabel(), result.getTestAction().getConfiguration());
+
+ TestSummary finalTestSummary = null;
+ synchronized (summaryLock) {
+ TestSummary.Builder summary = summaries.get(targetLabel);
+ preconditionHelper.checkNotNull(summary);
+ if (!remainingRuns.remove(targetLabel, result.getTestStatusArtifact())) {
+ // This can happen if a buildCompleteEvent() was processed before this event reached us.
+ // This situation is likely to happen if --notest_keep_going is set with multiple targets.
+ return;
+ }
+
+ summary = analyzer.incrementalAnalyze(summary, result);
+
+ // If all runs are processed, the target is finished and ready to report.
+ if (!remainingRuns.containsKey(targetLabel)) {
+ finalTestSummary = summary.build();
+ }
+ }
+
+ // Report finished targets.
+ if (finalTestSummary != null) {
+ eventBus.post(finalTestSummary);
+ }
+ }
+
+ private void targetFailure(LabelAndConfiguration label) {
+ TestSummary finalSummary;
+ synchronized (summaryLock) {
+ if (!remainingRuns.containsKey(label)) {
+ // Blaze does not guarantee that BuildResult.getSuccessfulTargets() and posted TestResult
+ // events are in sync. Thus, it is possible that a test event was posted, but the target is
+ // not present in the set of successful targets.
+ return;
+ }
+
+ TestSummary.Builder summary = summaries.get(label);
+ if (summary == null) {
+ // Not a test target; nothing to do.
+ return;
+ }
+ finalSummary = analyzer.markUnbuilt(summary, blazeHalted).build();
+
+ // These are never going to run; removing them marks the target complete.
+ remainingRuns.removeAll(label);
+ }
+ eventBus.post(finalSummary);
+ }
+
+ @VisibleForTesting
+ void buildComplete(
+ Collection<ConfiguredTarget> actualTargets, Collection<ConfiguredTarget> successfulTargets) {
+ if (actualTargets == null || successfulTargets == null) {
+ return;
+ }
+
+ for (ConfiguredTarget target: Sets.difference(
+ ImmutableSet.copyOf(actualTargets), ImmutableSet.copyOf(successfulTargets))) {
+ targetFailure(asKey(target));
+ }
+ }
+
+ @Subscribe
+ public void buildCompleteEvent(BuildCompleteEvent event) {
+ if (event.getResult().wasCatastrophe()) {
+ blazeHalted = true;
+ }
+ buildComplete(event.getResult().getActualTargets(), event.getResult().getSuccessfulTargets());
+ }
+
+ @Subscribe
+ public void analysisFailure(AnalysisFailureEvent event) {
+ targetFailure(event.getFailedTarget());
+ }
+
+ @Subscribe
+ @AllowConcurrentEvents
+ public void buildInterrupted(BuildInterruptedEvent event) {
+ blazeHalted = true;
+ }
+
+ /**
+ * Called when a build action is not executed (e.g. because a dependency failed to build). We want
+ * to catch such events in order to determine when a test target has failed to build.
+ */
+ @Subscribe
+ @AllowConcurrentEvents
+ public void targetComplete(TargetCompleteEvent event) {
+ if (event.failed()) {
+ targetFailure(new LabelAndConfiguration(event.getTarget()));
+ }
+ }
+
+ /**
+ * Returns the known aggregate results for the given target at the current moment.
+ */
+ public TestSummary.Builder getCurrentSummary(ConfiguredTarget target) {
+ synchronized (summaryLock) {
+ return summaries.get(asKey(target));
+ }
+ }
+
+ /**
+ * Returns all test status artifacts associated with a given target
+ * whose runs have yet to finish.
+ */
+ public Collection<Artifact> getIncompleteRuns(ConfiguredTarget target) {
+ synchronized (summaryLock) {
+ return Collections.unmodifiableCollection(remainingRuns.get(asKey(target)));
+ }
+ }
+
+ /**
+ * Returns true iff all runs of the target are accounted for.
+ */
+ public boolean targetReported(ConfiguredTarget target) {
+ synchronized (summaryLock) {
+ return summaries.containsKey(asKey(target)) && !remainingRuns.containsKey(asKey(target));
+ }
+ }
+
+ /**
+ * Returns the {@link TestResultAnalyzer} associated with this listener.
+ */
+ public TestResultAnalyzer getAnalyzer() {
+ return analyzer;
+ }
+
+ private LabelAndConfiguration asKey(ConfiguredTarget target) {
+ return new LabelAndConfiguration(target);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/BlazeCommand.java b/src/main/java/com/google/devtools/build/lib/runtime/BlazeCommand.java
new file mode 100644
index 0000000..61f46a8
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/BlazeCommand.java
@@ -0,0 +1,63 @@
+// Copyright 2014 Google Inc. 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.runtime;
+
+import com.google.devtools.build.lib.util.AbruptExitException;
+import com.google.devtools.build.lib.util.ExitCode;
+import com.google.devtools.common.options.OptionsParser;
+import com.google.devtools.common.options.OptionsProvider;
+
+/**
+ * Interface implemented by Blaze commands. In addition to implementing this interface, each
+ * command must be annotated with a {@link Command} annotation.
+ */
+public interface BlazeCommand {
+ /**
+ * This method provides the imperative portion of the command. It takes
+ * a {@link OptionsProvider} instance {@code options}, which provides access
+ * to the options instances via {@link OptionsProvider#getOptions(Class)},
+ * and access to the residue (the remainder of the command line) via
+ * {@link OptionsProvider#getResidue()}. The framework parses and makes
+ * available exactly the options that the command class specifies via the
+ * annotation {@link Command#options()}. The command may write to standard
+ * out and standard error via {@code outErr}. It indicates success / failure
+ * via its return value, which becomes the Unix exit status of the Blaze
+ * client process. It may indicate a shutdown request by throwing
+ * {@link BlazeCommandDispatcher.ShutdownBlazeServerException}. In that case,
+ * the Blaze server process (the memory resident portion of Blaze) will
+ * shut down and the exit status will be 0 (in case the shutdown succeeds
+ * without error).
+ *
+ * @param runtime The Blaze runtime requesting the execution of the command
+ * @param options A parsed options instance initialized with the values for
+ * the options specified in {@link Command#options()}.
+ *
+ * @return The Unix exit status for the Blaze client.
+ * @throws BlazeCommandDispatcher.ShutdownBlazeServerException Indicates
+ * that the command wants to shutdown the Blaze server.
+ */
+ ExitCode exec(BlazeRuntime runtime, OptionsProvider options)
+ throws BlazeCommandDispatcher.ShutdownBlazeServerException;
+
+ /**
+ * Allows the command to provide command-specific option defaults and/or
+ * requirements. This method is called after all command-line and rc file options have been
+ * parsed.
+ *
+ * @param runtime The Blaze runtime requesting the execution of the command
+ *
+ * @throws AbruptExitException if something went wrong
+ */
+ void editOptions(BlazeRuntime runtime, OptionsParser optionsParser) throws AbruptExitException;
+}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/BlazeCommandDispatcher.java b/src/main/java/com/google/devtools/build/lib/runtime/BlazeCommandDispatcher.java
new file mode 100644
index 0000000..cee47ee
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/BlazeCommandDispatcher.java
@@ -0,0 +1,692 @@
+// Copyright 2014 Google Inc. 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.runtime;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Lists;
+import com.google.common.io.Flushables;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.events.Reporter;
+import com.google.devtools.build.lib.util.AbruptExitException;
+import com.google.devtools.build.lib.util.AnsiStrippingOutputStream;
+import com.google.devtools.build.lib.util.ExitCode;
+import com.google.devtools.build.lib.util.LoggingUtil;
+import com.google.devtools.build.lib.util.Pair;
+import com.google.devtools.build.lib.util.io.DelegatingOutErr;
+import com.google.devtools.build.lib.util.io.OutErr;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.common.options.OptionPriority;
+import com.google.devtools.common.options.OptionsBase;
+import com.google.devtools.common.options.OptionsParser;
+import com.google.devtools.common.options.OptionsParsingException;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.logging.Level;
+
+/**
+ * Dispatches to the Blaze commands; that is, given a command line, this
+ * abstraction looks up the appropriate command object, parses the options
+ * required by the object, and calls its exec method. Also, this object provides
+ * the runtime state (BlazeRuntime) to the commands.
+ */
+public class BlazeCommandDispatcher {
+
+ // Keep in sync with options added in OptionProcessor::AddRcfileArgsAndOptions()
+ private static final Set<String> INTERNAL_COMMAND_OPTIONS = ImmutableSet.of(
+ "rc_source", "default_override", "isatty", "terminal_columns", "ignore_client_env",
+ "client_env", "client_cwd");
+
+ private static final ImmutableList<String> HELP_COMMAND = ImmutableList.of("help");
+
+ private static final Set<String> ALL_HELP_OPTIONS = ImmutableSet.of("--help", "-help", "-h");
+
+ /**
+ * By throwing this exception, a command indicates that it wants to shutdown
+ * the Blaze server process.
+ * See {@link BlazeCommandDispatcher#exec(List, OutErr, long)}.
+ */
+ public static class ShutdownBlazeServerException extends Exception {
+ private final int exitStatus;
+
+ public ShutdownBlazeServerException(int exitStatus, Throwable cause) {
+ super(cause);
+ this.exitStatus = exitStatus;
+ }
+
+ public ShutdownBlazeServerException(int exitStatus) {
+ this.exitStatus = exitStatus;
+ }
+
+ public int getExitStatus() {
+ return exitStatus;
+ }
+ }
+
+ private final BlazeRuntime runtime;
+ private final Map<String, BlazeCommand> commandsByName = new LinkedHashMap<>();
+
+ private OutputStream logOutputStream = null;
+
+ /**
+ * Create a Blaze dispatcher that uses the specified {@code BlazeRuntime}
+ * instance, and no default options, and delegates to {@code commands} as
+ * appropriate.
+ */
+ @VisibleForTesting
+ public BlazeCommandDispatcher(BlazeRuntime runtime, BlazeCommand... commands) {
+ this(runtime, ImmutableList.copyOf(commands));
+ }
+
+ /**
+ * Create a Blaze dispatcher that uses the specified {@code BlazeRuntime}
+ * instance, and delegates to {@code commands} as appropriate.
+ */
+ public BlazeCommandDispatcher(BlazeRuntime runtime, Iterable<BlazeCommand> commands) {
+ this.runtime = runtime;
+ for (BlazeCommand command : commands) {
+ addCommandByName(command);
+ }
+
+ for (BlazeModule module : runtime.getBlazeModules()) {
+ for (BlazeCommand command : module.getCommands()) {
+ addCommandByName(command);
+ }
+ }
+
+ runtime.setCommandMap(commandsByName);
+ }
+
+ /**
+ * Adds the given command under the given name to the map of commands.
+ *
+ * @throws AssertionError if the name is already used by another command.
+ */
+ private void addCommandByName(BlazeCommand command) {
+ String name = command.getClass().getAnnotation(Command.class).name();
+ if (commandsByName.containsKey(name)) {
+ throw new IllegalStateException("Command name or alias " + name + " is already used.");
+ }
+ commandsByName.put(name, command);
+ }
+
+ /**
+ * Only some commands work if cwd != workspaceSuffix in Blaze. In that case, also check if Blaze
+ * was called from the output directory and fail if it was.
+ */
+ private ExitCode checkCwdInWorkspace(Command commandAnnotation, String commandName,
+ OutErr outErr) {
+ if (!commandAnnotation.mustRunInWorkspace()) {
+ return ExitCode.SUCCESS;
+ }
+
+ if (!runtime.inWorkspace()) {
+ outErr.printErrLn("The '" + commandName + "' command is only supported from within a "
+ + "workspace.");
+ return ExitCode.COMMAND_LINE_ERROR;
+ }
+
+ Path workspace = runtime.getWorkspace();
+ Path doNotBuild = workspace.getParentDirectory().getRelative(
+ BlazeRuntime.DO_NOT_BUILD_FILE_NAME);
+ if (doNotBuild.exists()) {
+ if (!commandAnnotation.canRunInOutputDirectory()) {
+ outErr.printErrLn(getNotInRealWorkspaceError(doNotBuild));
+ return ExitCode.COMMAND_LINE_ERROR;
+ } else {
+ outErr.printErrLn("WARNING: Blaze is run from output directory. This is unsound.");
+ }
+ }
+ return ExitCode.SUCCESS;
+ }
+
+ private CommonCommandOptions checkOptions(OptionsParser optionsParser,
+ Command commandAnnotation, List<String> args, List<String> rcfileNotes, OutErr outErr)
+ throws OptionsParsingException {
+ Function<String, String> commandOptionSourceFunction = new Function<String, String>() {
+ @Override
+ public String apply(String input) {
+ if (INTERNAL_COMMAND_OPTIONS.contains(input)) {
+ return "options generated by Blaze launcher";
+ } else {
+ return "command line options";
+ }
+ }
+ };
+
+ // Explicit command-line options:
+ List<String> cmdLineAfterCommand = args.subList(1, args.size());
+ optionsParser.parseWithSourceFunction(OptionPriority.COMMAND_LINE,
+ commandOptionSourceFunction, cmdLineAfterCommand);
+
+ // Command-specific options from .blazerc passed in via --default_override
+ // and --rc_source. A no-op if none are provided.
+ CommonCommandOptions rcFileOptions = optionsParser.getOptions(CommonCommandOptions.class);
+ List<Pair<String, ListMultimap<String, String>>> optionsMap =
+ getOptionsMap(outErr, rcFileOptions.rcSource, rcFileOptions.optionsOverrides,
+ commandsByName.keySet());
+
+ parseOptionsForCommand(rcfileNotes, commandAnnotation, optionsParser, optionsMap, null);
+
+ // Fix-point iteration until all configs are loaded.
+ List<String> configsLoaded = ImmutableList.of();
+ CommonCommandOptions commonOptions = optionsParser.getOptions(CommonCommandOptions.class);
+ while (!commonOptions.configs.equals(configsLoaded)) {
+ Set<String> missingConfigs = new LinkedHashSet<>(commonOptions.configs);
+ missingConfigs.removeAll(configsLoaded);
+ parseOptionsForCommand(rcfileNotes, commandAnnotation, optionsParser, optionsMap,
+ missingConfigs);
+ configsLoaded = commonOptions.configs;
+ commonOptions = optionsParser.getOptions(CommonCommandOptions.class);
+ }
+
+ return commonOptions;
+ }
+
+ /**
+ * Sends {@code EventKind.{STDOUT|STDERR}} messages to the given {@link OutErr}.
+ *
+ * <p>This is necessary because we cannot delete the output files from the previous Blaze run
+ * because there can be processes spawned by the previous invocation that are still processing
+ * them, in which case we need to print a warning message about that.
+ *
+ * <p>Thus, messages sent to {@link Reporter#getOutErr} get sent to this event handler, then
+ * to its {@link OutErr}. We need to go deeper!
+ */
+ private static class OutErrEventHandler implements EventHandler {
+ private final OutErr outErr;
+
+ private OutErrEventHandler(OutErr outErr) {
+ this.outErr = outErr;
+ }
+
+ @Override
+ public void handle(Event event) {
+ try {
+ switch (event.getKind()) {
+ case STDOUT:
+ outErr.getOutputStream().write(event.getMessageBytes());
+ break;
+ case STDERR:
+ outErr.getErrorStream().write(event.getMessageBytes());
+ break;
+ }
+ } catch (IOException e) {
+ // We cannot do too much here -- ErrorEventListener#handle does not provide us with ways to
+ // report an error.
+ }
+ }
+ }
+
+ /**
+ * Executes a single command. Returns the Unix exit status for the Blaze
+ * client process, or throws {@link ShutdownBlazeServerException} to
+ * indicate that a command wants to shutdown the Blaze server.
+ */
+ public int exec(List<String> args, OutErr originalOutErr, long firstContactTime)
+ throws ShutdownBlazeServerException {
+ // Record the start time for the profiler and the timestamp granularity monitor. Do not put
+ // anything before this!
+ long execStartTimeNanos = runtime.getClock().nanoTime();
+
+ // Record the command's starting time for use by the commands themselves.
+ runtime.recordCommandStartTime(firstContactTime);
+
+ // Record the command's starting time again, for use by
+ // TimestampGranularityMonitor.waitForTimestampGranularity().
+ // This should be done as close as possible to the start of
+ // the command's execution - that's why we do this separately,
+ // rather than in runtime.beforeCommand().
+ runtime.getTimestampGranularityMonitor().setCommandStartTime();
+ runtime.initEventBus();
+
+ // Give a chance for module.beforeCommand() to report an errors to stdout and stderr.
+ // Once we can close the old streams, this event handler is removed.
+ OutErrEventHandler originalOutErrEventHandler =
+ new OutErrEventHandler(originalOutErr);
+ runtime.getReporter().addHandler(originalOutErrEventHandler);
+ OutErr outErr = originalOutErr;
+ runtime.getReporter().removeHandler(originalOutErrEventHandler);
+
+ if (args.isEmpty()) { // Default to help command if no arguments specified.
+ args = HELP_COMMAND;
+ }
+ String commandName = args.get(0);
+
+ // Be gentle to users who want to find out about Blaze invocation.
+ if (ALL_HELP_OPTIONS.contains(commandName)) {
+ commandName = "help";
+ }
+
+ BlazeCommand command = commandsByName.get(commandName);
+ if (command == null) {
+ outErr.printErrLn("Command '" + commandName + "' not found. " + "Try 'blaze help'.");
+ return ExitCode.COMMAND_LINE_ERROR.getNumericExitCode();
+ }
+ Command commandAnnotation = command.getClass().getAnnotation(Command.class);
+
+ AbruptExitException exitCausingException = null;
+ for (BlazeModule module : runtime.getBlazeModules()) {
+ try {
+ module.beforeCommand(runtime, commandAnnotation);
+ } catch (AbruptExitException e) {
+ // Don't let one module's complaints prevent the other modules from doing necessary
+ // setup. We promised to call beforeCommand exactly once per-module before each command
+ // and will be calling afterCommand soon in the future - a module's afterCommand might
+ // rightfully assume its beforeCommand has already been called.
+ outErr.printErrLn(e.getMessage());
+ // It's not ideal but we can only return one exit code, so we just pick the code of the
+ // last exception.
+ exitCausingException = e;
+ }
+ }
+ if (exitCausingException != null) {
+ return exitCausingException.getExitCode().getNumericExitCode();
+ }
+
+ try {
+ Path commandLog = getCommandLogPath(runtime.getOutputBase());
+
+ // Unlink old command log from previous build, if present, so scripts
+ // reading it don't conflate it with the command log we're about to write.
+ commandLog.delete();
+
+ logOutputStream = commandLog.getOutputStream();
+ outErr = tee(originalOutErr, OutErr.create(logOutputStream, logOutputStream));
+ } catch (IOException ioException) {
+ LoggingUtil.logToRemote(
+ Level.WARNING, "Unable to delete or open command.log", ioException);
+ }
+
+ // Create the UUID for this command.
+ runtime.setCommandId(UUID.randomUUID());
+
+ ExitCode result = checkCwdInWorkspace(commandAnnotation, commandName, outErr);
+ if (result != ExitCode.SUCCESS) {
+ return result.getNumericExitCode();
+ }
+
+ OptionsParser optionsParser;
+ CommonCommandOptions commonOptions;
+ // Delay output of notes regarding the parsed rc file, so it's possible to disable this in the
+ // rc file.
+ List<String> rcfileNotes = new ArrayList<>();
+ try {
+ optionsParser = createOptionsParser(command);
+ commonOptions = checkOptions(optionsParser, commandAnnotation, args, rcfileNotes, outErr);
+ } catch (OptionsParsingException e) {
+ for (String note : rcfileNotes) {
+ outErr.printErrLn("INFO: " + note);
+ }
+ outErr.printErrLn(e.getMessage());
+ return ExitCode.COMMAND_LINE_ERROR.getNumericExitCode();
+ }
+
+ // Setup log filtering
+ BlazeCommandEventHandler.Options eventHandlerOptions =
+ optionsParser.getOptions(BlazeCommandEventHandler.Options.class);
+ if (!eventHandlerOptions.useColor()) {
+ if (!commandAnnotation.binaryStdOut()) {
+ outErr = ansiStripOut(outErr);
+ }
+
+ if (!commandAnnotation.binaryStdErr()) {
+ outErr = ansiStripErr(outErr);
+ }
+ }
+
+ BlazeRuntime.setupLogging(commonOptions.verbosity);
+
+ // Do this before an actual crash so we don't have to worry about
+ // allocating memory post-crash.
+ String[] crashData = runtime.getCrashData();
+ int numericExitCode = ExitCode.BLAZE_INTERNAL_ERROR.getNumericExitCode();
+ PrintStream savedOut = System.out;
+ PrintStream savedErr = System.err;
+
+ EventHandler handler = createEventHandler(outErr, eventHandlerOptions);
+ Reporter reporter = runtime.getReporter();
+ reporter.addHandler(handler);
+ try {
+ // While a Blaze command is active, direct all errors to the client's
+ // event handler (and out/err streams).
+ OutErr reporterOutErr = reporter.getOutErr();
+ System.setOut(new PrintStream(reporterOutErr.getOutputStream(), /*autoflush=*/true));
+ System.setErr(new PrintStream(reporterOutErr.getErrorStream(), /*autoflush=*/true));
+
+ if (commonOptions.announceRcOptions) {
+ for (String note : rcfileNotes) {
+ reporter.handle(Event.info(note));
+ }
+ }
+
+ try {
+ // Notify the BlazeRuntime, so it can do some initial setup.
+ runtime.beforeCommand(commandName, optionsParser, commonOptions, execStartTimeNanos);
+ // Allow the command to edit options after parsing:
+ command.editOptions(runtime, optionsParser);
+ } catch (AbruptExitException e) {
+ reporter.handle(Event.error(e.getMessage()));
+ return e.getExitCode().getNumericExitCode();
+ }
+
+ // Print warnings for odd options usage
+ for (String warning : optionsParser.getWarnings()) {
+ reporter.handle(Event.warn(warning));
+ }
+
+ ExitCode outcome = command.exec(runtime, optionsParser);
+ outcome = runtime.precompleteCommand(outcome);
+ numericExitCode = outcome.getNumericExitCode();
+ return numericExitCode;
+ } catch (ShutdownBlazeServerException e) {
+ numericExitCode = e.getExitStatus();
+ throw e;
+ } catch (Throwable e) {
+ BugReport.printBug(outErr, e);
+ BugReport.sendBugReport(e, args, crashData);
+ numericExitCode = e instanceof OutOfMemoryError
+ ? ExitCode.OOM_ERROR.getNumericExitCode()
+ : ExitCode.BLAZE_INTERNAL_ERROR.getNumericExitCode();
+ throw new ShutdownBlazeServerException(numericExitCode, e);
+ } finally {
+ runtime.afterCommand(numericExitCode);
+ // Swallow IOException, as we are already in a finally clause
+ Flushables.flushQuietly(outErr.getOutputStream());
+ Flushables.flushQuietly(outErr.getErrorStream());
+
+ System.setOut(savedOut);
+ System.setErr(savedErr);
+ reporter.removeHandler(handler);
+ releaseHandler(handler);
+ runtime.getTimestampGranularityMonitor().waitForTimestampGranularity(outErr);
+ }
+ }
+
+ /**
+ * For testing ONLY. Same as {@link #exec(List, OutErr, long)}, but automatically uses the current
+ * time.
+ */
+ @VisibleForTesting
+ public int exec(List<String> args, OutErr originalOutErr) throws ShutdownBlazeServerException {
+ return exec(args, originalOutErr, runtime.getClock().currentTimeMillis());
+ }
+
+ /**
+ * Parses the options from .rc files for a command invocation. It works in one of two modes;
+ * either it loads the non-config options, or the config options that are specified in the {@code
+ * configs} parameter.
+ *
+ * <p>This method adds every option pertaining to the specified command to the options parser. To
+ * do that, it needs the command -> option mapping that is generated from the .rc files.
+ *
+ * <p>It is not as trivial as simply taking the list of options for the specified command because
+ * commands can inherit arguments from each other, and we have to respect that (e.g. if an option
+ * is specified for 'build', it needs to take effect for the 'test' command, too).
+ *
+ * <p>Note that the order in which the options are parsed is well-defined: all options from the
+ * same rc file are parsed at the same time, and the rc files are handled in the order in which
+ * they were passed in from the client.
+ *
+ * @param rcfileNotes note message that would be printed during parsing
+ * @param commandAnnotation the command for which options should be parsed.
+ * @param optionsParser parser to receive parsed options.
+ * @param optionsMap .rc files in structured format: a list of pairs, where the first part is the
+ * name of the rc file, and the second part is a multimap of command name (plus config, if
+ * present) to the list of options for that command
+ * @param configs the configs for which to parse options; if {@code null}, non-config options are
+ * parsed
+ * @throws OptionsParsingException
+ */
+ protected static void parseOptionsForCommand(List<String> rcfileNotes, Command commandAnnotation,
+ OptionsParser optionsParser, List<Pair<String, ListMultimap<String, String>>> optionsMap,
+ Iterable<String> configs) throws OptionsParsingException {
+ for (String commandToParse : getCommandNamesToParse(commandAnnotation)) {
+ for (Pair<String, ListMultimap<String, String>> entry : optionsMap) {
+ List<String> allOptions = new ArrayList<>();
+ if (configs == null) {
+ allOptions.addAll(entry.second.get(commandToParse));
+ } else {
+ for (String config : configs) {
+ allOptions.addAll(entry.second.get(commandToParse + ":" + config));
+ }
+ }
+ processOptionList(optionsParser, commandToParse,
+ commandAnnotation.name(), rcfileNotes, entry.first, allOptions);
+ if (allOptions.isEmpty()) {
+ continue;
+ }
+ }
+ }
+ }
+
+ // Processes the option list for an .rc file - command pair.
+ private static void processOptionList(OptionsParser optionsParser, String commandToParse,
+ String originalCommand, List<String> rcfileNotes, String rcfile, List<String> rcfileOptions)
+ throws OptionsParsingException {
+ if (!rcfileOptions.isEmpty()) {
+ String inherited = commandToParse.equals(originalCommand) ? "" : "Inherited ";
+ rcfileNotes.add("Reading options for '" + originalCommand +
+ "' from " + rcfile + ":\n" +
+ " " + inherited + "'" + commandToParse + "' options: "
+ + Joiner.on(' ').join(rcfileOptions));
+ optionsParser.parse(OptionPriority.RC_FILE, rcfile, rcfileOptions);
+ }
+ }
+
+ private static List<String> getCommandNamesToParse(Command commandAnnotation) {
+ List<String> result = new ArrayList<>();
+ getCommandNamesToParseHelper(commandAnnotation, result);
+ result.add("common");
+ // TODO(bazel-team): This statement is a NO-OP: Lists.reverse(result);
+ return result;
+ }
+
+ private static void getCommandNamesToParseHelper(Command commandAnnotation,
+ List<String> accumulator) {
+ for (Class<? extends BlazeCommand> base : commandAnnotation.inherits()) {
+ getCommandNamesToParseHelper(base.getAnnotation(Command.class), accumulator);
+ }
+ accumulator.add(commandAnnotation.name());
+ }
+
+ private OutErr ansiStripOut(OutErr outErr) {
+ OutputStream wrappedOut = new AnsiStrippingOutputStream(outErr.getOutputStream());
+ return OutErr.create(wrappedOut, outErr.getErrorStream());
+ }
+
+ private OutErr ansiStripErr(OutErr outErr) {
+ OutputStream wrappedErr = new AnsiStrippingOutputStream(outErr.getErrorStream());
+ return OutErr.create(outErr.getOutputStream(), wrappedErr);
+ }
+
+ private String getNotInRealWorkspaceError(Path doNotBuildFile) {
+ String message = "Blaze should not be called from a Blaze output directory. ";
+ try {
+ String realWorkspace =
+ new String(FileSystemUtils.readContentAsLatin1(doNotBuildFile));
+ message += String.format("The pertinent workspace directory is: '%s'",
+ realWorkspace);
+ } catch (IOException e) {
+ // We are exiting anyway.
+ }
+
+ return message;
+ }
+
+ /**
+ * For a given output_base directory, returns the command log file path.
+ */
+ public static Path getCommandLogPath(Path outputBase) {
+ return outputBase.getRelative("command.log");
+ }
+
+ private OutErr tee(OutErr outErr1, OutErr outErr2) {
+ DelegatingOutErr outErr = new DelegatingOutErr();
+ outErr.addSink(outErr1);
+ outErr.addSink(outErr2);
+ return outErr;
+ }
+
+ private void closeSilently(OutputStream logOutputStream) {
+ if (logOutputStream != null) {
+ try {
+ logOutputStream.close();
+ } catch (IOException e) {
+ LoggingUtil.logToRemote(Level.WARNING, "Unable to close command.log", e);
+ }
+ }
+ }
+
+ /**
+ * Creates an option parser using the common options classes and the
+ * command-specific options classes.
+ *
+ * <p>An overriding method should first call this method and can then
+ * override default values directly or by calling {@link
+ * #parseOptionsForCommand} for command-specific options.
+ *
+ * @throws OptionsParsingException
+ */
+ protected OptionsParser createOptionsParser(BlazeCommand command)
+ throws OptionsParsingException {
+ Command annotation = command.getClass().getAnnotation(Command.class);
+ List<Class<? extends OptionsBase>> allOptions = Lists.newArrayList();
+ allOptions.addAll(BlazeCommandUtils.getOptions(
+ command.getClass(), getRuntime().getBlazeModules(), getRuntime().getRuleClassProvider()));
+ OptionsParser parser = OptionsParser.newOptionsParser(allOptions);
+ parser.setAllowResidue(annotation.allowResidue());
+ return parser;
+ }
+
+ /**
+ * Convert a list of option override specifications to a more easily digestible
+ * form.
+ *
+ * @param overrides list of option override specifications
+ */
+ @VisibleForTesting
+ static List<Pair<String, ListMultimap<String, String>>> getOptionsMap(
+ OutErr outErr,
+ List<String> rcFiles,
+ List<CommonCommandOptions.OptionOverride> overrides,
+ Set<String> validCommands) {
+ List<Pair<String, ListMultimap<String, String>>> result = new ArrayList<>();
+
+ String lastRcFile = null;
+ ListMultimap<String, String> lastMap = null;
+ for (CommonCommandOptions.OptionOverride override : overrides) {
+ if (override.blazeRc < 0 || override.blazeRc >= rcFiles.size()) {
+ outErr.printErrLn("WARNING: inconsistency in generated command line "
+ + "args. Ignoring bogus argument\n");
+ continue;
+ }
+ String rcFile = rcFiles.get(override.blazeRc);
+
+ String command = override.command;
+ int index = command.indexOf(':');
+ if (index > 0) {
+ command = command.substring(0, index);
+ }
+ if (!validCommands.contains(command) && !command.equals("common")) {
+ outErr.printErrLn("WARNING: while reading option defaults file '"
+ + rcFile + "':\n"
+ + " invalid command name '" + override.command + "'.");
+ continue;
+ }
+
+ if (!rcFile.equals(lastRcFile)) {
+ if (lastRcFile != null) {
+ result.add(Pair.of(lastRcFile, lastMap));
+ }
+ lastRcFile = rcFile;
+ lastMap = ArrayListMultimap.create();
+ }
+ lastMap.put(override.command, override.option);
+ }
+ if (lastRcFile != null) {
+ result.add(Pair.of(lastRcFile, lastMap));
+ }
+
+ return result;
+ }
+
+ /**
+ * Returns the event handler to use for this Blaze command.
+ */
+ private EventHandler createEventHandler(OutErr outErr,
+ BlazeCommandEventHandler.Options eventOptions) {
+ EventHandler eventHandler;
+ if ((eventOptions.useColor() || eventOptions.useCursorControl())) {
+ eventHandler = new FancyTerminalEventHandler(outErr, eventOptions);
+ } else {
+ eventHandler = new BlazeCommandEventHandler(outErr, eventOptions);
+ }
+
+ return RateLimitingEventHandler.create(eventHandler, eventOptions.showProgressRateLimit);
+ }
+
+ /**
+ * Unsets the event handler.
+ */
+ private void releaseHandler(EventHandler eventHandler) {
+ if (eventHandler instanceof FancyTerminalEventHandler) {
+ // Make sure that the terminal state of the old event handler is clear
+ // before creating a new one.
+ ((FancyTerminalEventHandler)eventHandler).resetTerminal();
+ }
+ }
+
+ /**
+ * Returns the runtime instance shared by the commands that this dispatcher
+ * dispatches to.
+ */
+ public BlazeRuntime getRuntime() {
+ return runtime;
+ }
+
+ /**
+ * The map from command names to commands that this dispatcher dispatches to.
+ */
+ Map<String, BlazeCommand> getCommandsByName() {
+ return Collections.unmodifiableMap(commandsByName);
+ }
+
+ /**
+ * Shuts down all the registered commands to give them a chance to cleanup or
+ * close resources. Should be called by the owner of this command dispatcher
+ * in all termination cases.
+ */
+ public void shutdown() {
+ closeSilently(logOutputStream);
+ logOutputStream = null;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/BlazeCommandEventHandler.java b/src/main/java/com/google/devtools/build/lib/runtime/BlazeCommandEventHandler.java
new file mode 100644
index 0000000..603b0be
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/BlazeCommandEventHandler.java
@@ -0,0 +1,246 @@
+// Copyright 2014 Google Inc. 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.runtime;
+
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.events.EventKind;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.util.io.OutErr;
+import com.google.devtools.common.options.EnumConverter;
+import com.google.devtools.common.options.Option;
+import com.google.devtools.common.options.OptionsBase;
+
+import org.joda.time.format.DateTimeFormat;
+import org.joda.time.format.DateTimeFormatter;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.util.EnumSet;
+import java.util.Set;
+
+/**
+ * BlazeCommandEventHandler: an event handler established for the duration of a
+ * single Blaze command.
+ */
+public class BlazeCommandEventHandler implements EventHandler {
+
+ public enum UseColor { YES, NO, AUTO }
+ public enum UseCurses { YES, NO, AUTO }
+
+ public static class UseColorConverter extends EnumConverter<UseColor> {
+ public UseColorConverter() {
+ super(UseColor.class, "--color setting");
+ }
+ }
+
+ public static class UseCursesConverter extends EnumConverter<UseCurses> {
+ public UseCursesConverter() {
+ super(UseCurses.class, "--curses setting");
+ }
+ }
+
+ public static class Options extends OptionsBase {
+
+ @Option(name = "show_progress",
+ defaultValue = "true",
+ category = "verbosity",
+ help = "Display progress messages during a build.")
+ public boolean showProgress;
+
+ @Option(name = "show_task_finish",
+ defaultValue = "false",
+ category = "verbosity",
+ help = "Display progress messages when tasks complete, not just when they start.")
+ public boolean showTaskFinish;
+
+ @Option(name = "show_progress_rate_limit",
+ defaultValue = "0.03", // A nice middle ground; snappy but not too spammy in logs.
+ category = "verbosity",
+ help = "Minimum number of seconds between progress messages in the output.")
+ public double showProgressRateLimit;
+
+ @Option(name = "color",
+ defaultValue = "auto",
+ converter = UseColorConverter.class,
+ category = "verbosity",
+ help = "Use terminal controls to colorize output.")
+ public UseColor useColorEnum;
+
+ @Option(name = "curses",
+ defaultValue = "auto",
+ converter = UseCursesConverter.class,
+ category = "verbosity",
+ help = "Use terminal cursor controls to minimize scrolling output")
+ public UseCurses useCursesEnum;
+
+ @Option(name = "terminal_columns",
+ defaultValue = "80",
+ category = "hidden",
+ help = "A system-generated parameter which specifies the terminal "
+ + " width in columns.")
+ public int terminalColumns;
+
+ @Option(name = "isatty",
+ defaultValue = "false",
+ category = "hidden",
+ help = "A system-generated parameter which is used to notify the "
+ + "server whether this client is running in a terminal. "
+ + "If this is set to false, then '--color=auto' will be treated as '--color=no'. "
+ + "If this is set to true, then '--color=auto' will be treated as '--color=yes'.")
+ public boolean isATty;
+
+ // This lives here (as opposed to the more logical BuildRequest.Options)
+ // because the client passes it to the server *always*. We don't want the
+ // client to have to figure out when it should or shouldn't to send it.
+ @Option(name = "emacs",
+ defaultValue = "false",
+ category = "undocumented",
+ help = "A system-generated parameter which is true iff EMACS=t in the environment of "
+ + "the client. This option controls certain display features.")
+ public boolean runningInEmacs;
+
+ @Option(name = "show_timestamps",
+ defaultValue = "false",
+ category = "verbosity",
+ help = "Include timestamps in messages")
+ public boolean showTimestamp;
+
+ @Option(name = "progress_in_terminal_title",
+ defaultValue = "false",
+ category = "verbosity",
+ help = "Show the command progress in the terminal title. "
+ + "Useful to see what blaze is doing when having multiple terminal tabs.")
+ public boolean progressInTermTitle;
+
+
+ public boolean useColor() {
+ return useColorEnum == UseColor.YES || (useColorEnum == UseColor.AUTO && isATty);
+ }
+
+ public boolean useCursorControl() {
+ return useCursesEnum == UseCurses.YES || (useCursesEnum == UseCurses.AUTO && isATty);
+ }
+ }
+
+ private static final DateTimeFormatter TIMESTAMP_FORMAT =
+ DateTimeFormat.forPattern("(MM-dd HH:mm:ss.SSS) ");
+
+ protected final OutErr outErr;
+
+ private final PrintStream errPrintStream;
+
+ protected final Set<EventKind> eventMask =
+ EnumSet.copyOf(EventKind.ERRORS_WARNINGS_AND_INFO_AND_OUTPUT);
+
+ protected final boolean showTimestamp;
+
+ public BlazeCommandEventHandler(OutErr outErr, Options eventOptions) {
+ this.outErr = outErr;
+ this.errPrintStream = new PrintStream(outErr.getErrorStream(), true);
+ if (eventOptions.showProgress) {
+ eventMask.add(EventKind.PROGRESS);
+ eventMask.add(EventKind.START);
+ } else {
+ // Skip PASS events if --noshow_progress is requested.
+ eventMask.remove(EventKind.PASS);
+ }
+ if (eventOptions.showTaskFinish) {
+ eventMask.add(EventKind.FINISH);
+ }
+ eventMask.add(EventKind.SUBCOMMAND);
+ this.showTimestamp = eventOptions.showTimestamp;
+ }
+
+ /** See EventHandler.handle. */
+ @Override
+ public void handle(Event event) {
+ if (!eventMask.contains(event.getKind())) {
+ return;
+ }
+ String prefix;
+ switch (event.getKind()) {
+ case STDOUT:
+ putOutput(outErr.getOutputStream(), event);
+ return;
+ case STDERR:
+ putOutput(outErr.getErrorStream(), event);
+ return;
+ case PASS:
+ case FAIL:
+ case TIMEOUT:
+ case ERROR:
+ case WARNING:
+ case DEPCHECKER:
+ prefix = event.getKind() + ": ";
+ break;
+ case SUBCOMMAND:
+ prefix = ">>>>>>>>> ";
+ break;
+ case INFO:
+ case PROGRESS:
+ case START:
+ case FINISH:
+ prefix = "____";
+ break;
+ default:
+ throw new IllegalStateException("" + event.getKind());
+ }
+ StringBuilder buf = new StringBuilder();
+ buf.append(prefix);
+
+ if (showTimestamp) {
+ buf.append(timestamp());
+ }
+
+ Location location = event.getLocation();
+ if (location != null) {
+ buf.append(location.print()).append(": ");
+ }
+
+ buf.append(event.getMessage());
+ if (event.getKind() == EventKind.FINISH) {
+ buf.append(" DONE");
+ }
+
+ // Add a trailing period for ERROR and WARNING messages, which are
+ // typically English sentences composed from exception messages.
+ if (event.getKind() == EventKind.WARNING ||
+ event.getKind() == EventKind.ERROR) {
+ buf.append('.');
+ }
+
+ // Event messages go to stderr; results (e.g. 'blaze query') go to stdout.
+ errPrintStream.println(buf);
+ }
+
+ private void putOutput(OutputStream out, Event event) {
+ try {
+ out.write(event.getMessageBytes());
+ out.flush();
+ } catch (IOException e) {
+ // This can happen in server mode if the blaze client has exited,
+ // or if output is redirected to a file and the disk is full, etc.
+ // Ignore.
+ }
+ }
+
+ /**
+ * @return a string representing the current time, eg "04-26 13:47:32.124".
+ */
+ protected String timestamp() {
+ return TIMESTAMP_FORMAT.print(System.currentTimeMillis());
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/BlazeCommandUtils.java b/src/main/java/com/google/devtools/build/lib/runtime/BlazeCommandUtils.java
new file mode 100644
index 0000000..ff738db
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/BlazeCommandUtils.java
@@ -0,0 +1,166 @@
+// Copyright 2014 Google Inc. 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.runtime;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider;
+import com.google.devtools.build.lib.util.ResourceFileLoader;
+import com.google.devtools.common.options.OptionsBase;
+import com.google.devtools.common.options.OptionsParser;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Utility class for functionality related to Blaze commands.
+ */
+public class BlazeCommandUtils {
+ /**
+ * Options classes used as startup options in Blaze core.
+ */
+ private static final List<Class<? extends OptionsBase>> DEFAULT_STARTUP_OPTIONS =
+ ImmutableList.<Class<? extends OptionsBase>>of(
+ BlazeServerStartupOptions.class,
+ HostJvmStartupOptions.class);
+
+ /**
+ * The set of option-classes that are common to all Blaze commands.
+ */
+ private static final Collection<Class<? extends OptionsBase>> COMMON_COMMAND_OPTIONS =
+ ImmutableList.of(CommonCommandOptions.class, BlazeCommandEventHandler.Options.class);
+
+
+ private BlazeCommandUtils() {}
+
+ public static ImmutableList<Class<? extends OptionsBase>> getStartupOptions(
+ Iterable<BlazeModule> modules) {
+ Set<Class<? extends OptionsBase>> options = new HashSet<>();
+ options.addAll(DEFAULT_STARTUP_OPTIONS);
+ for (BlazeModule blazeModule : modules) {
+ Iterables.addAll(options, blazeModule.getStartupOptions());
+ }
+
+ return ImmutableList.copyOf(options);
+ }
+
+ /**
+ * Returns the set of all options (including those inherited directly and
+ * transitively) for this AbstractCommand's @Command annotation.
+ *
+ * <p>Why does metaprogramming always seem like such a bright idea in the
+ * beginning?
+ */
+ public static ImmutableList<Class<? extends OptionsBase>> getOptions(
+ Class<? extends BlazeCommand> clazz,
+ Iterable<BlazeModule> modules,
+ ConfiguredRuleClassProvider ruleClassProvider) {
+ Command commandAnnotation = clazz.getAnnotation(Command.class);
+ if (commandAnnotation == null) {
+ throw new IllegalStateException("@Command missing for " + clazz.getName());
+ }
+
+ Set<Class<? extends OptionsBase>> options = new HashSet<>();
+ options.addAll(COMMON_COMMAND_OPTIONS);
+ Collections.addAll(options, commandAnnotation.options());
+
+ if (commandAnnotation.usesConfigurationOptions()) {
+ options.addAll(ruleClassProvider.getConfigurationOptions());
+ }
+
+ for (BlazeModule blazeModule : modules) {
+ Iterables.addAll(options, blazeModule.getCommandOptions(commandAnnotation));
+ }
+
+ for (Class<? extends BlazeCommand> base : commandAnnotation.inherits()) {
+ options.addAll(getOptions(base, modules, ruleClassProvider));
+ }
+ return ImmutableList.copyOf(options);
+ }
+
+ /**
+ * Returns the expansion of the specified help topic.
+ *
+ * @param topic the name of the help topic; used in %{command} expansion.
+ * @param help the text template of the help message. Certain %{x} variables
+ * will be expanded. A prefix of "resource:" means use the .jar
+ * resource of that name.
+ * @param categoryDescriptions a mapping from option category names to
+ * descriptions, passed to {@link OptionsParser#describeOptions}.
+ * @param helpVerbosity a tri-state verbosity option selecting between just
+ * names, names and syntax, and full description.
+ */
+ public static final String expandHelpTopic(String topic, String help,
+ Class<? extends BlazeCommand> commandClass,
+ Collection<Class<? extends OptionsBase>> options,
+ Map<String, String> categoryDescriptions,
+ OptionsParser.HelpVerbosity helpVerbosity) {
+ OptionsParser parser = OptionsParser.newOptionsParser(options);
+
+ String template;
+ if (help.startsWith("resource:")) {
+ String resourceName = help.substring("resource:".length());
+ try {
+ template = ResourceFileLoader.loadResource(commandClass, resourceName);
+ } catch (IOException e) {
+ throw new IllegalStateException("failed to load help resource '" + resourceName
+ + "' due to I/O error: " + e.getMessage(), e);
+ }
+ } else {
+ template = help;
+ }
+
+ if (!template.contains("%{options}")) {
+ throw new IllegalStateException("Help template for '" + topic + "' omits %{options}!");
+ }
+
+ return template.
+ replace("%{command}", topic).
+ replace("%{options}", parser.describeOptions(categoryDescriptions, helpVerbosity)).
+ trim()
+ + "\n\n"
+ + (helpVerbosity == OptionsParser.HelpVerbosity.MEDIUM
+ ? "(Use 'help --long' for full details or --short to just enumerate options.)\n"
+ : "");
+ }
+
+ /**
+ * The help page for this command.
+ *
+ * @param categoryDescriptions a mapping from option category names to
+ * descriptions, passed to {@link OptionsParser#describeOptions}.
+ * @param verbosity a tri-state verbosity option selecting between just names,
+ * names and syntax, and full description.
+ */
+ public static String getUsage(
+ Class<? extends BlazeCommand> commandClass,
+ Map<String, String> categoryDescriptions,
+ OptionsParser.HelpVerbosity verbosity,
+ Iterable<BlazeModule> blazeModules,
+ ConfiguredRuleClassProvider ruleClassProvider) {
+ Command commandAnnotation = commandClass.getAnnotation(Command.class);
+ return BlazeCommandUtils.expandHelpTopic(
+ commandAnnotation.name(),
+ commandAnnotation.help(),
+ commandClass,
+ BlazeCommandUtils.getOptions(commandClass, blazeModules, ruleClassProvider),
+ categoryDescriptions,
+ verbosity);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/BlazeModule.java b/src/main/java/com/google/devtools/build/lib/runtime/BlazeModule.java
new file mode 100644
index 0000000..6855cbd
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/BlazeModule.java
@@ -0,0 +1,420 @@
+// Copyright 2014 Google Inc. 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.runtime;
+
+import com.google.common.base.Predicate;
+import com.google.common.base.Supplier;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.actions.ActionContextConsumer;
+import com.google.devtools.build.lib.actions.ActionContextProvider;
+import com.google.devtools.build.lib.actions.ActionInputFileCache;
+import com.google.devtools.build.lib.analysis.BlazeDirectories;
+import com.google.devtools.build.lib.analysis.BlazeVersionInfo;
+import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider;
+import com.google.devtools.build.lib.analysis.WorkspaceStatusAction;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.exec.OutputService;
+import com.google.devtools.build.lib.packages.MakeEnvironment;
+import com.google.devtools.build.lib.packages.NoSuchThingException;
+import com.google.devtools.build.lib.packages.PackageFactory;
+import com.google.devtools.build.lib.packages.PackageFactory.PackageArgument;
+import com.google.devtools.build.lib.packages.Preprocessor;
+import com.google.devtools.build.lib.query2.engine.QueryEnvironment.QueryFunction;
+import com.google.devtools.build.lib.query2.output.OutputFormatter;
+import com.google.devtools.build.lib.rules.test.CoverageReportActionFactory;
+import com.google.devtools.build.lib.skyframe.DiffAwareness;
+import com.google.devtools.build.lib.skyframe.PrecomputedValue.Injected;
+import com.google.devtools.build.lib.skyframe.SkyframeExecutor;
+import com.google.devtools.build.lib.skyframe.SkyframeExecutorFactory;
+import com.google.devtools.build.lib.syntax.Environment;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.util.AbruptExitException;
+import com.google.devtools.build.lib.util.Clock;
+import com.google.devtools.build.lib.vfs.FileSystem;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyFunctionName;
+import com.google.devtools.common.options.OptionsBase;
+import com.google.devtools.common.options.OptionsProvider;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+/**
+ * A module Blaze can load at the beginning of its execution. Modules are supplied with extension
+ * points to augment the functionality at specific, well-defined places.
+ *
+ * <p>The constructors of individual Blaze modules should be empty. All work should be done in the
+ * methods (e.g. {@link #blazeStartup}).
+ */
+public abstract class BlazeModule {
+
+ /**
+ * Returns the extra startup options this module contributes.
+ *
+ * <p>This method will be called at the beginning of Blaze startup (before #blazeStartup).
+ */
+ public Iterable<Class<? extends OptionsBase>> getStartupOptions() {
+ return ImmutableList.of();
+ }
+
+ /**
+ * Called before {@link #getFileSystem} and {@link #blazeStartup}.
+ *
+ * <p>This method will be called at the beginning of Blaze startup.
+ */
+ @SuppressWarnings("unused")
+ public void globalInit(OptionsProvider startupOptions) throws AbruptExitException {
+ }
+
+ /**
+ * Returns the file system implementation used by Blaze. It is an error if more than one module
+ * returns a file system. If all return null, the default unix file system is used.
+ *
+ * <p>This method will be called at the beginning of Blaze startup (in-between #globalInit and
+ * #blazeStartup).
+ */
+ @SuppressWarnings("unused")
+ public FileSystem getFileSystem(OptionsProvider startupOptions, PathFragment outputPath) {
+ return null;
+ }
+
+ /**
+ * Called when Blaze starts up.
+ */
+ @SuppressWarnings("unused")
+ public void blazeStartup(OptionsProvider startupOptions,
+ BlazeVersionInfo versionInfo, UUID instanceId, BlazeDirectories directories,
+ Clock clock) throws AbruptExitException {
+ }
+
+ /**
+ * Returns the set of directories under which blaze may assume all files are immutable.
+ */
+ public Set<Path> getImmutableDirectories() {
+ return ImmutableSet.<Path>of();
+ }
+
+ /**
+ * May yield a supplier that provides factories for the Preprocessor to apply. Only one of the
+ * configured modules may return non-null.
+ *
+ * The factory yielded by the supplier will be checked with
+ * {@link Preprocessor.Factory#isStillValid} at the beginning of each incremental build. This
+ * allows modules to have preprocessors customizable by flags.
+ *
+ * <p>This method will be called during Blaze startup (after #blazeStartup).
+ */
+ public Preprocessor.Factory.Supplier getPreprocessorFactorySupplier() {
+ return null;
+ }
+
+ /**
+ * Adds the rule classes supported by this module.
+ *
+ * <p>This method will be called during Blaze startup (after #blazeStartup).
+ */
+ @SuppressWarnings("unused")
+ public void initializeRuleClasses(ConfiguredRuleClassProvider.Builder builder) {
+ }
+
+ /**
+ * Returns the list of commands this module contributes to Blaze.
+ *
+ * <p>This method will be called during Blaze startup (after #blazeStartup).
+ */
+ public Iterable<? extends BlazeCommand> getCommands() {
+ return ImmutableList.of();
+ }
+
+ /**
+ * Returns the list of query output formatters this module provides.
+ *
+ * <p>This method will be called during Blaze startup (after #blazeStartup).
+ */
+ public Iterable<OutputFormatter> getQueryOutputFormatters() {
+ return ImmutableList.of();
+ }
+
+ /**
+ * Returns the {@link DiffAwareness} strategies this module contributes. These will be used to
+ * determine which files, if any, changed between Blaze commands.
+ *
+ * <p>This method will be called during Blaze startup (after #blazeStartup).
+ */
+ @SuppressWarnings("unused")
+ public Iterable<? extends DiffAwareness.Factory> getDiffAwarenessFactories(boolean watchFS) {
+ return ImmutableList.of();
+ }
+
+ /**
+ * Returns the workspace status action factory contributed by this module.
+ *
+ * <p>There should always be exactly one of these in a Blaze instance.
+ */
+ public WorkspaceStatusAction.Factory getWorkspaceStatusActionFactory() {
+ return null;
+ }
+
+ /**
+ * PlatformSet is a group of platforms characterized by a regular expression. For example, the
+ * entry "oldlinux": "i[34]86-libc[345]-linux" might define a set of platforms representing
+ * certain older linux releases.
+ *
+ * <p>Platform-set names are used in BUILD files in the third argument to <tt>vardef</tt>, to
+ * define per-platform tweaks to variables such as CFLAGS.
+ *
+ * <p>vardef is a legacy mechanism: it needs explicit support in the rule implementations,
+ * and cannot express conditional dependencies, only conditional attribute values. This
+ * mechanism will be supplanted by configuration dependent attributes, and its effect can
+ * usually also be achieved with abi_deps.
+ *
+ * <p>This method will be called during Blaze startup (after #blazeStartup).
+ */
+ public Map<String, String> getPlatformSetRegexps() {
+ return ImmutableMap.<String, String>of();
+ }
+
+ /**
+ * Services provided for Blaze modules via BlazeRuntime.
+ */
+ public interface ModuleEnvironment {
+ /**
+ * Gets a file from the depot based on its label and returns the {@link Path} where it can
+ * be found.
+ */
+ Path getFileFromDepot(Label label)
+ throws NoSuchThingException, InterruptedException, IOException;
+
+ /**
+ * Exits Blaze as early as possible. This is currently a hack and should only be called in
+ * event handlers for {@code BuildStartingEvent}, {@code GotOptionsEvent} and
+ * {@code LoadingPhaseCompleteEvent}.
+ */
+ void exit(AbruptExitException exception);
+ }
+
+ /**
+ * Called before each command.
+ */
+ @SuppressWarnings("unused")
+ public void beforeCommand(BlazeRuntime blazeRuntime, Command command)
+ throws AbruptExitException {
+ }
+
+ /**
+ * Returns the output service to be used. It is an error if more than one module returns an
+ * output service.
+ *
+ * <p>This method will be called at the beginning of each command (after #beforeCommand).
+ */
+ @SuppressWarnings("unused")
+ public OutputService getOutputService() throws AbruptExitException {
+ return null;
+ }
+
+ /**
+ * Returns the extra options this module contributes to a specific command.
+ *
+ * <p>This method will be called at the beginning of each command (after #beforeCommand).
+ */
+ @SuppressWarnings("unused")
+ public Iterable<Class<? extends OptionsBase>> getCommandOptions(Command command) {
+ return ImmutableList.of();
+ }
+
+ /**
+ * Returns a map of option categories to descriptive strings. This is used by {@code HelpCommand}
+ * to show a more readable list of flags.
+ */
+ public Map<String, String> getOptionCategories() {
+ return ImmutableMap.of();
+ }
+
+ /**
+ * A item that is returned by "blaze info".
+ */
+ public interface InfoItem {
+ /**
+ * The name of the info key.
+ */
+ String getName();
+
+ /**
+ * The help description of the info key.
+ */
+ String getDescription();
+
+ /**
+ * Whether the key is printed when "blaze info" is invoked without arguments.
+ *
+ * <p>This is usually true for info keys that take multiple lines, thus, cannot really be
+ * included in the output of argumentless "blaze info".
+ */
+ boolean isHidden();
+
+ /**
+ * Returns the value of the info key. The return value is directly printed to stdout.
+ */
+ byte[] get(Supplier<BuildConfiguration> configurationSupplier) throws AbruptExitException;
+ }
+
+ /**
+ * Returns the additional information this module provides to "blaze info".
+ *
+ * <p>This method will be called at the beginning of each "blaze info" command (after
+ * #beforeCommand).
+ */
+ public Iterable<InfoItem> getInfoItems() {
+ return ImmutableList.of();
+ }
+
+ /**
+ * Returns the list of query functions this module provides to "blaze query".
+ *
+ * <p>This method will be called at the beginning of each "blaze query" command (after
+ * #beforeCommand).
+ */
+ public Iterable<QueryFunction> getQueryFunctions() {
+ return ImmutableList.of();
+ }
+
+ /**
+ * Returns the action context provider the module contributes to Blaze, if any.
+ *
+ * <p>This method will be called at the beginning of the execution phase, e.g. of the
+ * "blaze build" command.
+ */
+ public ActionContextProvider getActionContextProvider() {
+ return null;
+ }
+
+ /**
+ * Returns the action context consumer that pulls in action contexts required by this module,
+ * if any.
+ *
+ * <p>This method will be called at the beginning of the execution phase, e.g. of the
+ * "blaze build" command.
+ */
+ public ActionContextConsumer getActionContextConsumer() {
+ return null;
+ }
+
+ /**
+ * Called after each command.
+ */
+ public void afterCommand() {
+ }
+
+ /**
+ * Called when Blaze shuts down.
+ */
+ public void blazeShutdown() {
+ }
+
+ /**
+ * Action inputs are allowed to be missing for all inputs where this predicate returns true.
+ */
+ public Predicate<PathFragment> getAllowedMissingInputs() {
+ return null;
+ }
+
+ /**
+ * Optionally specializes the cache that ensures source files are looked at just once during
+ * a build. Only one module may do so.
+ */
+ public ActionInputFileCache createActionInputCache(String cwd, FileSystem fs) {
+ return null;
+ }
+
+ /**
+ * Returns the extensions this module contributes to the global namespace of the BUILD language.
+ */
+ public PackageFactory.EnvironmentExtension getPackageEnvironmentExtension() {
+ return new PackageFactory.EnvironmentExtension() {
+ @Override
+ public void update(
+ Environment environment, MakeEnvironment.Builder pkgMakeEnv, Label buildFileLabel) {
+ }
+
+ @Override
+ public Iterable<PackageArgument<?>> getPackageArguments() {
+ return ImmutableList.of();
+ }
+ };
+ }
+
+ /**
+ * Returns a factory for creating {@link SkyframeExecutor} objects. If the module does not
+ * provide any SkyframeExecutorFactory, it returns null. Note that only one factory per
+ * Bazel/Blaze runtime is allowed.
+ */
+ public SkyframeExecutorFactory getSkyframeExecutorFactory() {
+ return null;
+ }
+
+ /** Returns a map of "extra" SkyFunctions for SkyValues that this module may want to build. */
+ public ImmutableMap<SkyFunctionName, SkyFunction> getSkyFunctions(BlazeDirectories directories) {
+ return ImmutableMap.of();
+ }
+
+ /**
+ * Returns the extra precomputed values that the module makes available in Skyframe.
+ *
+ * <p>This method is called once per Blaze instance at the very beginning of its life.
+ * If it creates the injected values by using a {@code com.google.common.base.Supplier},
+ * that supplier is asked for the value it contains just before the loading phase begins. This
+ * functionality can be used to implement precomputed values that are not constant during the
+ * lifetime of a Blaze instance (naturally, they must be constant over the course of a build)
+ *
+ * <p>The following things must be done in order to define a new precomputed values:
+ * <ul>
+ * <li> Create a public static final variable of type
+ * {@link com.google.devtools.build.lib.skyframe.PrecomputedValue.Precomputed}
+ * <li> Set its value by adding an {@link Injected} in this method (it can be created using the
+ * aforementioned variable and the value or a supplier of the value)
+ * <li> Reference the value in Skyframe functions by calling get {@code get} method on the
+ * {@link com.google.devtools.build.lib.skyframe.PrecomputedValue.Precomputed} variable. This
+ * will never return null, because its value will have been injected before most of the Skyframe
+ * values are computed.
+ * </ul>
+ */
+ public Iterable<Injected> getPrecomputedSkyframeValues() {
+ return ImmutableList.of();
+ }
+
+ /**
+ * Optionally returns a provider for project files that can be used to bundle targets and
+ * command-line options.
+ */
+ @Nullable
+ public ProjectFile.Provider createProjectFileProvider() {
+ return null;
+ }
+
+ /**
+ * Optionally returns a factory to create coverage report actions.
+ */
+ @Nullable
+ public CoverageReportActionFactory getCoverageReportFactory() {
+ return null;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/BlazeRuntime.java b/src/main/java/com/google/devtools/build/lib/runtime/BlazeRuntime.java
new file mode 100644
index 0000000..0251e83
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/BlazeRuntime.java
@@ -0,0 +1,1795 @@
+// Copyright 2014 Google Inc. 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.runtime;
+
+import static java.nio.charset.StandardCharsets.ISO_8859_1;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSortedSet;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.common.eventbus.EventBus;
+import com.google.common.eventbus.SubscriberExceptionContext;
+import com.google.common.eventbus.SubscriberExceptionHandler;
+import com.google.common.util.concurrent.Uninterruptibles;
+import com.google.devtools.build.lib.Constants;
+import com.google.devtools.build.lib.actions.cache.ActionCache;
+import com.google.devtools.build.lib.actions.cache.CompactPersistentActionCache;
+import com.google.devtools.build.lib.actions.cache.NullActionCache;
+import com.google.devtools.build.lib.analysis.BlazeDirectories;
+import com.google.devtools.build.lib.analysis.BlazeVersionInfo;
+import com.google.devtools.build.lib.analysis.BuildView;
+import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider;
+import com.google.devtools.build.lib.analysis.WorkspaceStatusAction;
+import com.google.devtools.build.lib.analysis.config.BinTools;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.analysis.config.BuildConfigurationCollection;
+import com.google.devtools.build.lib.analysis.config.BuildConfigurationKey;
+import com.google.devtools.build.lib.analysis.config.BuildOptions;
+import com.google.devtools.build.lib.analysis.config.ConfigurationFactory;
+import com.google.devtools.build.lib.analysis.config.DefaultsPackage;
+import com.google.devtools.build.lib.analysis.config.InvalidConfigurationException;
+import com.google.devtools.build.lib.buildtool.BuildTool;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.OutputFilter;
+import com.google.devtools.build.lib.events.Reporter;
+import com.google.devtools.build.lib.exec.OutputService;
+import com.google.devtools.build.lib.packages.NoSuchThingException;
+import com.google.devtools.build.lib.packages.PackageFactory;
+import com.google.devtools.build.lib.packages.Preprocessor;
+import com.google.devtools.build.lib.packages.RuleClassProvider;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.pkgcache.LoadedPackageProvider;
+import com.google.devtools.build.lib.pkgcache.LoadingPhaseRunner;
+import com.google.devtools.build.lib.pkgcache.PackageCacheOptions;
+import com.google.devtools.build.lib.pkgcache.PackageManager;
+import com.google.devtools.build.lib.pkgcache.TargetPatternEvaluator;
+import com.google.devtools.build.lib.profiler.MemoryProfiler;
+import com.google.devtools.build.lib.profiler.ProfilePhase;
+import com.google.devtools.build.lib.profiler.Profiler;
+import com.google.devtools.build.lib.profiler.Profiler.ProfiledTaskKinds;
+import com.google.devtools.build.lib.profiler.ProfilerTask;
+import com.google.devtools.build.lib.query2.output.OutputFormatter;
+import com.google.devtools.build.lib.rules.test.CoverageReportActionFactory;
+import com.google.devtools.build.lib.runtime.commands.BuildCommand;
+import com.google.devtools.build.lib.runtime.commands.CanonicalizeCommand;
+import com.google.devtools.build.lib.runtime.commands.CleanCommand;
+import com.google.devtools.build.lib.runtime.commands.HelpCommand;
+import com.google.devtools.build.lib.runtime.commands.InfoCommand;
+import com.google.devtools.build.lib.runtime.commands.ProfileCommand;
+import com.google.devtools.build.lib.runtime.commands.QueryCommand;
+import com.google.devtools.build.lib.runtime.commands.RunCommand;
+import com.google.devtools.build.lib.runtime.commands.ShutdownCommand;
+import com.google.devtools.build.lib.runtime.commands.SkylarkCommand;
+import com.google.devtools.build.lib.runtime.commands.TestCommand;
+import com.google.devtools.build.lib.runtime.commands.VersionCommand;
+import com.google.devtools.build.lib.server.RPCServer;
+import com.google.devtools.build.lib.server.ServerCommand;
+import com.google.devtools.build.lib.server.signal.InterruptSignalHandler;
+import com.google.devtools.build.lib.skyframe.DiffAwareness;
+import com.google.devtools.build.lib.skyframe.PrecomputedValue;
+import com.google.devtools.build.lib.skyframe.SequencedSkyframeExecutorFactory;
+import com.google.devtools.build.lib.skyframe.SkyframeExecutor;
+import com.google.devtools.build.lib.skyframe.SkyframeExecutorFactory;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.util.AbruptExitException;
+import com.google.devtools.build.lib.util.BlazeClock;
+import com.google.devtools.build.lib.util.Clock;
+import com.google.devtools.build.lib.util.ExitCode;
+import com.google.devtools.build.lib.util.LoggingUtil;
+import com.google.devtools.build.lib.util.OS;
+import com.google.devtools.build.lib.util.OsUtils;
+import com.google.devtools.build.lib.util.ThreadUtils;
+import com.google.devtools.build.lib.util.io.OutErr;
+import com.google.devtools.build.lib.util.io.TimestampGranularityMonitor;
+import com.google.devtools.build.lib.vfs.FileSystem;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
+import com.google.devtools.build.lib.vfs.JavaIoFileSystem;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.lib.vfs.UnixFileSystem;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyFunctionName;
+import com.google.devtools.common.options.Option;
+import com.google.devtools.common.options.OptionPriority;
+import com.google.devtools.common.options.OptionsBase;
+import com.google.devtools.common.options.OptionsClassProvider;
+import com.google.devtools.common.options.OptionsParser;
+import com.google.devtools.common.options.OptionsParsingException;
+import com.google.devtools.common.options.OptionsProvider;
+import com.google.devtools.common.options.TriState;
+
+import java.io.BufferedOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.lang.management.ManagementFactory;
+import java.lang.management.MemoryMXBean;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.regex.Pattern;
+
+import javax.annotation.Nullable;
+
+/**
+ * The BlazeRuntime class encapsulates the runtime settings and services that
+ * are available to most parts of any Blaze application for the duration of the
+ * batch run or server lifetime. A single instance of this runtime will exist
+ * and will be passed around as needed.
+ */
+public final class BlazeRuntime {
+ /**
+ * The threshold for memory reserved by a 32-bit JVM before trouble may be expected.
+ *
+ * <p>After the JVM starts, it reserves memory for heap (controlled by -Xmx) and non-heap
+ * (code, PermGen, etc.). Furthermore, as Blaze spawns threads, each thread reserves memory
+ * for the stack (controlled by -Xss). Thus even if Blaze starts fine, with high memory settings
+ * it will die from a stack allocation failure in the middle of a build. We prefer failing
+ * upfront by setting a safe threshold.
+ *
+ * <p>This does not apply to 64-bit VMs.
+ */
+ private static final long MAX_BLAZE32_RESERVED_MEMORY = 3400 * 1048576L;
+
+ // Less than this indicates tampering with -Xmx settings.
+ private static final long MIN_BLAZE32_HEAP_SIZE = 3000 * 1000000L;
+
+ public static final String DO_NOT_BUILD_FILE_NAME = "DO_NOT_BUILD_HERE";
+
+ private static final Pattern suppressFromLog = Pattern.compile(".*(auth|pass|cookie).*",
+ Pattern.CASE_INSENSITIVE);
+
+ private static final Logger LOG = Logger.getLogger(BlazeRuntime.class.getName());
+
+ private final BlazeDirectories directories;
+ private Path workingDirectory;
+ private long commandStartTime;
+
+ // Application-specified constants
+ private final PathFragment runfilesPrefix;
+
+ private final SkyframeExecutor skyframeExecutor;
+
+ private final Reporter reporter;
+ private EventBus eventBus;
+ private final LoadingPhaseRunner loadingPhaseRunner;
+ private final PackageFactory packageFactory;
+ private final ConfigurationFactory configurationFactory;
+ private final ConfiguredRuleClassProvider ruleClassProvider;
+ private final BuildView view;
+ private ActionCache actionCache;
+ private final TimestampGranularityMonitor timestampGranularityMonitor;
+ private final Clock clock;
+ private final BuildTool buildTool;
+
+ private OutputService outputService;
+
+ private final Iterable<BlazeModule> blazeModules;
+ private final BlazeModule.ModuleEnvironment blazeModuleEnvironment;
+
+ private UUID commandId; // Unique identifier for the command being run
+
+ private final AtomicInteger storedExitCode = new AtomicInteger();
+
+ private final Map<String, String> clientEnv;
+
+ // We pass this through here to make it available to the MasterLogWriter.
+ private final OptionsProvider startupOptionsProvider;
+
+ private String outputFileSystem;
+ private Map<String, BlazeCommand> commandMap;
+
+ private AbruptExitException pendingException;
+
+ private final SubscriberExceptionHandler eventBusExceptionHandler;
+
+ private final BinTools binTools;
+
+ private final WorkspaceStatusAction.Factory workspaceStatusActionFactory;
+
+ private final ProjectFile.Provider projectFileProvider;
+
+ private class BlazeModuleEnvironment implements BlazeModule.ModuleEnvironment {
+ @Override
+ public Path getFileFromDepot(Label label)
+ throws NoSuchThingException, InterruptedException, IOException {
+ Target target = getPackageManager().getTarget(reporter, label);
+ return (outputService != null)
+ ? outputService.stageTool(target)
+ : target.getPackage().getPackageDirectory().getRelative(target.getName());
+ }
+
+ @Override
+ public void exit(AbruptExitException exception) {
+ Preconditions.checkState(pendingException == null);
+ pendingException = exception;
+ }
+ }
+
+ private BlazeRuntime(BlazeDirectories directories, Reporter reporter,
+ WorkspaceStatusAction.Factory workspaceStatusActionFactory,
+ final SkyframeExecutor skyframeExecutor,
+ PackageFactory pkgFactory, ConfiguredRuleClassProvider ruleClassProvider,
+ ConfigurationFactory configurationFactory, PathFragment runfilesPrefix, Clock clock,
+ OptionsProvider startupOptionsProvider, Iterable<BlazeModule> blazeModules,
+ Map<String, String> clientEnv,
+ TimestampGranularityMonitor timestampGranularityMonitor,
+ SubscriberExceptionHandler eventBusExceptionHandler,
+ BinTools binTools, ProjectFile.Provider projectFileProvider) {
+ this.workspaceStatusActionFactory = workspaceStatusActionFactory;
+ this.directories = directories;
+ this.workingDirectory = directories.getWorkspace();
+ this.reporter = reporter;
+ this.runfilesPrefix = runfilesPrefix;
+ this.packageFactory = pkgFactory;
+ this.binTools = binTools;
+ this.projectFileProvider = projectFileProvider;
+
+ this.skyframeExecutor = skyframeExecutor;
+ this.loadingPhaseRunner = new LoadingPhaseRunner(
+ skyframeExecutor.getPackageManager(),
+ pkgFactory.getRuleClassNames());
+
+ this.clientEnv = clientEnv;
+
+ this.blazeModules = blazeModules;
+ this.ruleClassProvider = ruleClassProvider;
+ this.configurationFactory = configurationFactory;
+ this.view = new BuildView(directories, getPackageManager(), ruleClassProvider,
+ skyframeExecutor, binTools, getCoverageReportActionFactory(blazeModules));
+ this.clock = clock;
+ this.timestampGranularityMonitor = Preconditions.checkNotNull(timestampGranularityMonitor);
+ this.startupOptionsProvider = startupOptionsProvider;
+
+ this.eventBusExceptionHandler = eventBusExceptionHandler;
+ this.blazeModuleEnvironment = new BlazeModuleEnvironment();
+ this.buildTool = new BuildTool(this);
+ initEventBus();
+
+ if (inWorkspace()) {
+ writeOutputBaseReadmeFile();
+ writeOutputBaseDoNotBuildHereFile();
+ }
+ setupExecRoot();
+ }
+
+ @Nullable private CoverageReportActionFactory getCoverageReportActionFactory(
+ Iterable<BlazeModule> blazeModules) {
+ CoverageReportActionFactory firstFactory = null;
+ for (BlazeModule module : blazeModules) {
+ CoverageReportActionFactory factory = module.getCoverageReportFactory();
+ if (factory != null) {
+ Preconditions.checkState(firstFactory == null,
+ "only one Blaze Module can have a Coverage Report Factory");
+ firstFactory = factory;
+ }
+ }
+ return firstFactory;
+ }
+
+ /**
+ * Figures out what file system we are writing output to. Here we use
+ * outputBase instead of outputPath because we need a file system to create the latter.
+ */
+ private String determineOutputFileSystem() {
+ if (getOutputService() != null) {
+ return getOutputService().getFilesSystemName();
+ }
+ long startTime = Profiler.nanoTimeMaybe();
+ String fileSystem = FileSystemUtils.getFileSystem(getOutputBase());
+ Profiler.instance().logSimpleTask(startTime, ProfilerTask.INFO, "Finding output file system");
+ return fileSystem;
+ }
+
+ public String getOutputFileSystem() {
+ return outputFileSystem;
+ }
+
+ @VisibleForTesting
+ public void initEventBus() {
+ setEventBus(new EventBus(eventBusExceptionHandler));
+ }
+
+ private void clearEventBus() {
+ // EventBus does not have an unregister() method, so this is how we release memory associated
+ // with handlers.
+ setEventBus(null);
+ }
+
+ private void setEventBus(EventBus eventBus) {
+ this.eventBus = eventBus;
+ skyframeExecutor.setEventBus(eventBus);
+ }
+
+ /**
+ * Conditionally enable profiling.
+ */
+ private final boolean initProfiler(CommonCommandOptions options,
+ UUID buildID, long execStartTimeNanos) {
+ OutputStream out = null;
+ boolean recordFullProfilerData = false;
+ ProfiledTaskKinds profiledTasks = ProfiledTaskKinds.NONE;
+
+ try {
+ if (options.profilePath != null) {
+ Path profilePath = getWorkspace().getRelative(options.profilePath);
+
+ recordFullProfilerData = options.recordFullProfilerData;
+ out = new BufferedOutputStream(profilePath.getOutputStream(), 1024 * 1024);
+ getReporter().handle(Event.info("Writing profile data to '" + profilePath + "'"));
+ profiledTasks = ProfiledTaskKinds.ALL;
+ } else if (options.alwaysProfileSlowOperations) {
+ recordFullProfilerData = false;
+ out = null;
+ profiledTasks = ProfiledTaskKinds.SLOWEST;
+ }
+ if (profiledTasks != ProfiledTaskKinds.NONE) {
+ Profiler.instance().start(profiledTasks, out,
+ "Blaze profile for " + getOutputBase() + " at " + new Date()
+ + ", build ID: " + buildID,
+ recordFullProfilerData, clock, execStartTimeNanos);
+ return true;
+ }
+ } catch (IOException e) {
+ getReporter().handle(Event.error("Error while creating profile file: " + e.getMessage()));
+ }
+ return false;
+ }
+
+ /**
+ * Generates a README file in the output base directory. This README file
+ * contains the name of the workspace directory, so that users can figure out
+ * which output base directory corresponds to which workspace.
+ */
+ private void writeOutputBaseReadmeFile() {
+ Preconditions.checkNotNull(getWorkspace());
+ Path outputBaseReadmeFile = getOutputBase().getRelative("README");
+ try {
+ FileSystemUtils.writeIsoLatin1(outputBaseReadmeFile, "WORKSPACE: " + getWorkspace(), "",
+ "The first line of this file is intentionally easy to parse for various",
+ "interactive scripting and debugging purposes. But please DO NOT write programs",
+ "that exploit it, as they will be broken by design: it is not possible to",
+ "reverse engineer the set of source trees or the --package_path from the output",
+ "tree, and if you attempt it, you will fail, creating subtle and",
+ "hard-to-diagnose bugs, that will no doubt get blamed on changes made by the",
+ "Blaze team.", "", "This directory was generated by Blaze.",
+ "Do not attempt to modify or delete any files in this directory.",
+ "Among other issues, Blaze's file system caching assumes that",
+ "only Blaze will modify this directory and the files in it,",
+ "so if you change anything here you may mess up Blaze's cache.");
+ } catch (IOException e) {
+ LOG.warning("Couldn't write to '" + outputBaseReadmeFile + "': " + e.getMessage());
+ }
+ }
+
+ private void writeOutputBaseDoNotBuildHereFile() {
+ Preconditions.checkNotNull(getWorkspace());
+ Path filePath = getOutputBase().getRelative(DO_NOT_BUILD_FILE_NAME);
+ try {
+ FileSystemUtils.writeContent(filePath, ISO_8859_1, getWorkspace().toString());
+ } catch (IOException e) {
+ LOG.warning("Couldn't write to '" + filePath + "': " + e.getMessage());
+ }
+ }
+
+ /**
+ * Creates the execRoot dir under outputBase.
+ */
+ private void setupExecRoot() {
+ try {
+ FileSystemUtils.createDirectoryAndParents(directories.getExecRoot());
+ } catch (IOException e) {
+ LOG.warning("failed to create execution root '" + directories.getExecRoot() + "': "
+ + e.getMessage());
+ }
+ }
+
+ public void recordCommandStartTime(long commandStartTime) {
+ this.commandStartTime = commandStartTime;
+ }
+
+ public long getCommandStartTime() {
+ return commandStartTime;
+ }
+
+ public String getWorkspaceName() {
+ Path workspace = directories.getWorkspace();
+ if (workspace == null) {
+ return "";
+ }
+ return workspace.getBaseName();
+ }
+
+ /**
+ * Returns any prefix to be inserted between relative source paths and the runfiles directory.
+ */
+ public PathFragment getRunfilesPrefix() {
+ return runfilesPrefix;
+ }
+
+ /**
+ * Returns the Blaze directories object for this runtime.
+ */
+ public BlazeDirectories getDirectories() {
+ return directories;
+ }
+
+ /**
+ * Returns the working directory of the server.
+ *
+ * <p>This is often the first entry on the {@code --package_path}, but not always.
+ * Callers should certainly not make this assumption. The Path returned may be null.
+ *
+ * @see #getWorkingDirectory()
+ */
+ public Path getWorkspace() {
+ return directories.getWorkspace();
+ }
+
+ /**
+ * Returns the working directory of the {@code blaze} client process.
+ *
+ * <p>This may be equal to {@code getWorkspace()}, or beneath it.
+ *
+ * @see #getWorkspace()
+ */
+ public Path getWorkingDirectory() {
+ return workingDirectory;
+ }
+
+ /**
+ * Returns if the client passed a valid workspace to be used for the build.
+ */
+ public boolean inWorkspace() {
+ return directories.inWorkspace();
+ }
+
+ /**
+ * Returns the output base directory associated with this Blaze server
+ * process. This is the base directory for shared Blaze state as well as tool
+ * and strategy specific subdirectories.
+ */
+ public Path getOutputBase() {
+ return directories.getOutputBase();
+ }
+
+ /**
+ * Returns the output path associated with this Blaze server process..
+ */
+ public Path getOutputPath() {
+ return directories.getOutputPath();
+ }
+
+ /**
+ * The directory in which blaze stores the server state - that is, the socket
+ * file and a log.
+ */
+ public Path getServerDirectory() {
+ return getOutputBase().getChild("server");
+ }
+
+ /**
+ * Returns the execution root directory associated with this Blaze server
+ * process. This is where all input and output files visible to the actual
+ * build reside.
+ */
+ public Path getExecRoot() {
+ return directories.getExecRoot();
+ }
+
+ /**
+ * Returns the reporter for events.
+ */
+ public Reporter getReporter() {
+ return reporter;
+ }
+
+ /**
+ * Returns the current event bus. Only valid within the scope of a single Blaze command.
+ */
+ public EventBus getEventBus() {
+ return eventBus;
+ }
+
+ public BinTools getBinTools() {
+ return binTools;
+ }
+
+ /**
+ * Returns the skyframe executor.
+ */
+ public SkyframeExecutor getSkyframeExecutor() {
+ return skyframeExecutor;
+ }
+
+ /**
+ * Returns the package factory.
+ */
+ public PackageFactory getPackageFactory() {
+ return packageFactory;
+ }
+
+ /**
+ * Returns the build tool.
+ */
+ public BuildTool getBuildTool() {
+ return buildTool;
+ }
+
+ public ImmutableList<OutputFormatter> getQueryOutputFormatters() {
+ ImmutableList.Builder<OutputFormatter> result = ImmutableList.builder();
+ result.addAll(OutputFormatter.getDefaultFormatters());
+ for (BlazeModule module : blazeModules) {
+ result.addAll(module.getQueryOutputFormatters());
+ }
+
+ return result.build();
+ }
+
+ /**
+ * Returns the package manager.
+ */
+ public PackageManager getPackageManager() {
+ return skyframeExecutor.getPackageManager();
+ }
+
+ public WorkspaceStatusAction.Factory getworkspaceStatusActionFactory() {
+ return workspaceStatusActionFactory;
+ }
+
+ public BlazeModule.ModuleEnvironment getBlazeModuleEnvironment() {
+ return blazeModuleEnvironment;
+ }
+
+ /**
+ * Returns the rule class provider.
+ */
+ public ConfiguredRuleClassProvider getRuleClassProvider() {
+ return ruleClassProvider;
+ }
+
+ public LoadingPhaseRunner getLoadingPhaseRunner() {
+ return loadingPhaseRunner;
+ }
+
+ /**
+ * Returns the build view.
+ */
+ public BuildView getView() {
+ return view;
+ }
+
+ public Iterable<BlazeModule> getBlazeModules() {
+ return blazeModules;
+ }
+
+ @SuppressWarnings("unchecked")
+ public <T extends BlazeModule> T getBlazeModule(Class<T> moduleClass) {
+ for (BlazeModule module : blazeModules) {
+ if (module.getClass() == moduleClass) {
+ return (T) module;
+ }
+ }
+
+ return null;
+ }
+
+ public ConfigurationFactory getConfigurationFactory() {
+ return configurationFactory;
+ }
+
+ /**
+ * Returns the target pattern parser.
+ */
+ public TargetPatternEvaluator getTargetPatternEvaluator() {
+ return loadingPhaseRunner.getTargetPatternEvaluator();
+ }
+
+ /**
+ * Returns reference to the lazily instantiated persistent action cache
+ * instance. Note, that method may recreate instance between different build
+ * requests, so return value should not be cached.
+ */
+ public ActionCache getPersistentActionCache() throws IOException {
+ if (actionCache == null) {
+ if (OS.getCurrent() == OS.WINDOWS) {
+ // TODO(bazel-team): Add support for a persistent action cache on Windows.
+ actionCache = new NullActionCache();
+ return actionCache;
+ }
+ long startTime = Profiler.nanoTimeMaybe();
+ try {
+ actionCache = new CompactPersistentActionCache(getCacheDirectory(), clock);
+ } catch (IOException e) {
+ LOG.log(Level.WARNING, "Failed to load action cache: " + e.getMessage(), e);
+ LoggingUtil.logToRemote(Level.WARNING, "Failed to load action cache: "
+ + e.getMessage(), e);
+ getReporter().handle(
+ Event.error("Error during action cache initialization: " + e.getMessage()
+ + ". Corrupted files were renamed to '" + getCacheDirectory() + "/*.bad'. "
+ + "Blaze will now reset action cache data, causing a full rebuild"));
+ actionCache = new CompactPersistentActionCache(getCacheDirectory(), clock);
+ } finally {
+ Profiler.instance().logSimpleTask(startTime, ProfilerTask.INFO, "Loading action cache");
+ }
+ }
+ return actionCache;
+ }
+
+ /**
+ * Removes in-memory caches.
+ */
+ public void clearCaches() throws IOException {
+ clearSkyframeRelevantCaches();
+ actionCache = null;
+ FileSystemUtils.deleteTree(getCacheDirectory());
+ }
+
+ /** Removes skyframe cache and other caches that must be kept synchronized with skyframe. */
+ private void clearSkyframeRelevantCaches() {
+ skyframeExecutor.resetEvaluator();
+ view.clear();
+ }
+
+ /**
+ * Returns the TimestampGranularityMonitor. The same monitor object is used
+ * across multiple Blaze commands, but it doesn't hold any persistent state
+ * across different commands.
+ */
+ public TimestampGranularityMonitor getTimestampGranularityMonitor() {
+ return timestampGranularityMonitor;
+ }
+
+ /**
+ * Returns path to the cache directory. Path must be inside output base to
+ * ensure that users can run concurrent instances of blaze in different
+ * clients without attempting to concurrently write to the same action cache
+ * on disk, which might not be safe.
+ */
+ private Path getCacheDirectory() {
+ return getOutputBase().getChild("action_cache");
+ }
+
+ /**
+ * Returns a provider for project file objects. Can be null if no such provider was set by any of
+ * the modules.
+ */
+ @Nullable
+ public ProjectFile.Provider getProjectFileProvider() {
+ return projectFileProvider;
+ }
+
+ /**
+ * Hook method called by the BlazeCommandDispatcher prior to the dispatch of
+ * each command.
+ *
+ * @param options The CommonCommandOptions used by every command.
+ * @throws AbruptExitException if this command is unsuitable to be run as specified
+ */
+ void beforeCommand(String commandName, OptionsParser optionsParser,
+ CommonCommandOptions options, long execStartTimeNanos)
+ throws AbruptExitException {
+ commandStartTime -= options.startupTime;
+
+ eventBus.post(new GotOptionsEvent(startupOptionsProvider,
+ optionsParser));
+ throwPendingException();
+
+ outputService = null;
+ BlazeModule outputModule = null;
+ for (BlazeModule module : blazeModules) {
+ OutputService moduleService = module.getOutputService();
+ if (moduleService != null) {
+ if (outputService != null) {
+ throw new IllegalStateException(String.format(
+ "More than one module (%s and %s) returns an output service",
+ module.getClass(), outputModule.getClass()));
+ }
+ outputService = moduleService;
+ outputModule = module;
+ }
+ }
+
+ skyframeExecutor.setBatchStatter(outputService == null
+ ? null
+ : outputService.getBatchStatter());
+
+ outputFileSystem = determineOutputFileSystem();
+
+ // Ensure that the working directory will be under the workspace directory.
+ Path workspace = getWorkspace();
+ if (inWorkspace()) {
+ workingDirectory = workspace.getRelative(options.clientCwd);
+ } else {
+ workspace = FileSystemUtils.getWorkingDirectory(directories.getFileSystem());
+ workingDirectory = workspace;
+ }
+ updateClientEnv(options.clientEnv, options.ignoreClientEnv);
+ loadingPhaseRunner.updatePatternEvaluator(workingDirectory.relativeTo(workspace));
+
+ // Fail fast in the case where a Blaze command forgets to install the package path correctly.
+ skyframeExecutor.setActive(false);
+ // Let skyframe figure out if it needs to store graph edges for this build.
+ skyframeExecutor.decideKeepIncrementalState(
+ startupOptionsProvider.getOptions(BlazeServerStartupOptions.class).batch,
+ optionsParser.getOptions(BuildView.Options.class));
+
+ // Conditionally enable profiling
+ // We need to compensate for launchTimeNanos (measurements taken outside of the jvm).
+ long startupTimeNanos = options.startupTime * 1000000L;
+ if (initProfiler(options, this.getCommandId(), execStartTimeNanos - startupTimeNanos)) {
+ Profiler profiler = Profiler.instance();
+
+ // Instead of logEvent() we're calling the low level function to pass the timings we took in
+ // the launcher. We're setting the INIT phase marker so that it follows immediately the LAUNCH
+ // phase.
+ profiler.logSimpleTaskDuration(execStartTimeNanos - startupTimeNanos, 0, ProfilerTask.PHASE,
+ ProfilePhase.LAUNCH.description);
+ profiler.logSimpleTaskDuration(execStartTimeNanos, 0, ProfilerTask.PHASE,
+ ProfilePhase.INIT.description);
+ }
+
+ if (options.memoryProfilePath != null) {
+ Path memoryProfilePath = getWorkingDirectory().getRelative(options.memoryProfilePath);
+ try {
+ MemoryProfiler.instance().start(memoryProfilePath.getOutputStream());
+ } catch (IOException e) {
+ getReporter().handle(
+ Event.error("Error while creating memory profile file: " + e.getMessage()));
+ }
+ }
+
+ eventBus.post(new CommandStartEvent(commandName, commandId, clientEnv, workingDirectory));
+ // Initialize exit code to dummy value for afterCommand.
+ storedExitCode.set(ExitCode.RESERVED.getNumericExitCode());
+ }
+
+ /**
+ * Hook method called by the BlazeCommandDispatcher right before the dispatch
+ * of each command ends (while its outcome can still be modified).
+ */
+ ExitCode precompleteCommand(ExitCode originalExit) {
+ eventBus.post(new CommandPrecompleteEvent(originalExit));
+ // If Blaze did not suffer an infrastructure failure, check for errors in modules.
+ ExitCode exitCode = originalExit;
+ if (!originalExit.isInfrastructureFailure()) {
+ if (pendingException != null) {
+ exitCode = pendingException.getExitCode();
+ }
+ }
+ pendingException = null;
+ return exitCode;
+ }
+
+ /**
+ * Posts the {@link CommandCompleteEvent}, so that listeners can tidy up. Called by {@link
+ * #afterCommand}, and by BugReport when crashing from an exception in an async thread.
+ */
+ public void notifyCommandComplete(int exitCode) {
+ if (!storedExitCode.compareAndSet(ExitCode.RESERVED.getNumericExitCode(), exitCode)) {
+ // This command has already been called, presumably because there is a race between the main
+ // thread and a worker thread that crashed. Don't try to arbitrate the dispute. If the main
+ // thread won the race (unlikely, but possible), this may be incorrectly logged as a success.
+ return;
+ }
+ eventBus.post(new CommandCompleteEvent(exitCode));
+ }
+
+ /**
+ * Hook method called by the BlazeCommandDispatcher after the dispatch of each
+ * command.
+ */
+ @VisibleForTesting
+ public void afterCommand(int exitCode) {
+ // Remove any filters that the command might have added to the reporter.
+ getReporter().setOutputFilter(OutputFilter.OUTPUT_EVERYTHING);
+
+ notifyCommandComplete(exitCode);
+
+ for (BlazeModule module : blazeModules) {
+ module.afterCommand();
+ }
+
+ clearEventBus();
+
+ try {
+ Profiler.instance().stop();
+ MemoryProfiler.instance().stop();
+ } catch (IOException e) {
+ getReporter().handle(Event.error("Error while writing profile file: " + e.getMessage()));
+ }
+ }
+
+ // Make sure we keep a strong reference to this logger, so that the
+ // configuration isn't lost when the gc kicks in.
+ private static Logger templateLogger = Logger.getLogger("com.google.devtools.build");
+
+ /**
+ * Configures "com.google.devtools.build.*" loggers to the given
+ * {@code level}. Note: This code relies on static state.
+ */
+ public static void setupLogging(Level level) {
+ templateLogger.setLevel(level);
+ templateLogger.info("Log level: " + templateLogger.getLevel());
+ }
+
+ /**
+ * Return an unmodifiable view of the blaze client's environment when it
+ * invoked the most recent command. Updates from future requests will be
+ * accessible from this view.
+ */
+ public Map<String, String> getClientEnv() {
+ return Collections.unmodifiableMap(clientEnv);
+ }
+
+ @VisibleForTesting
+ void updateClientEnv(List<Map.Entry<String, String>> clientEnvList, boolean ignoreClientEnv) {
+ clientEnv.clear();
+
+ Collection<Map.Entry<String, String>> env =
+ ignoreClientEnv ? System.getenv().entrySet() : clientEnvList;
+ for (Map.Entry<String, String> entry : env) {
+ clientEnv.put(entry.getKey(), entry.getValue());
+ }
+ }
+
+ /**
+ * Returns the Clock-instance used for the entire build. Before,
+ * individual classes (such as Profiler) used to specify the type
+ * of clock (e.g. EpochClock) they wanted to use. This made it
+ * difficult to get Blaze working on Windows as some of the clocks
+ * available for Linux aren't (directly) available on Windows.
+ * Setting the Blaze-wide clock upon construction of BlazeRuntime
+ * allows injecting whatever Clock instance should be used from
+ * BlazeMain.
+ *
+ * @return The Blaze-wide clock
+ */
+ public Clock getClock() {
+ return clock;
+ }
+
+ public OptionsProvider getStartupOptionsProvider() {
+ return startupOptionsProvider;
+ }
+
+ /**
+ * An array of String values useful if Blaze crashes.
+ * For now, just returns the size of the action cache and the build id.
+ */
+ public String[] getCrashData() {
+ return new String[]{
+ getFileSizeString(CompactPersistentActionCache.cacheFile(getCacheDirectory()),
+ "action cache"),
+ commandIdString(),
+ };
+ }
+
+ private String commandIdString() {
+ UUID uuid = getCommandId();
+ return (uuid == null)
+ ? "no build id"
+ : uuid + " (build id)";
+ }
+
+ /**
+ * @return the OutputService in use, or null if none.
+ */
+ public OutputService getOutputService() {
+ return outputService;
+ }
+
+ private String getFileSizeString(Path path, String type) {
+ try {
+ return String.format("%d bytes (%s)", path.getFileSize(), type);
+ } catch (IOException e) {
+ return String.format("unknown file size (%s)", type);
+ }
+ }
+
+ /**
+ * Returns the UUID that Blaze uses to identify everything
+ * logged from the current build command.
+ */
+ public UUID getCommandId() {
+ return commandId;
+ }
+
+ void setCommandMap(Map<String, BlazeCommand> commandMap) {
+ this.commandMap = ImmutableMap.copyOf(commandMap);
+ }
+
+ public Map<String, BlazeCommand> getCommandMap() {
+ return commandMap;
+ }
+
+ /**
+ * Sets the UUID that Blaze uses to identify everything
+ * logged from the current build command.
+ */
+ @VisibleForTesting
+ public void setCommandId(UUID runId) {
+ commandId = runId;
+ }
+
+ /**
+ * Constructs a build configuration key for the given options.
+ */
+ public BuildConfigurationKey getBuildConfigurationKey(BuildOptions buildOptions,
+ ImmutableSortedSet<String> multiCpu) {
+ return new BuildConfigurationKey(buildOptions, directories, clientEnv, multiCpu);
+ }
+
+ /**
+ * This method only exists for the benefit of InfoCommand, which needs to construct a {@link
+ * BuildConfigurationCollection} without running a full loading phase. Don't add any more clients;
+ * instead, we should change info so that it doesn't need the configuration.
+ */
+ public BuildConfigurationCollection getConfigurations(OptionsProvider optionsProvider)
+ throws InvalidConfigurationException, InterruptedException {
+ BuildConfigurationKey configurationKey = getBuildConfigurationKey(
+ createBuildOptions(optionsProvider), ImmutableSortedSet.<String>of());
+ boolean keepGoing = optionsProvider.getOptions(BuildView.Options.class).keepGoing;
+ LoadedPackageProvider loadedPackageProvider =
+ loadingPhaseRunner.loadForConfigurations(reporter,
+ ImmutableSet.copyOf(configurationKey.getLabelsToLoadUnconditionally().values()),
+ keepGoing);
+ if (loadedPackageProvider == null) {
+ throw new InvalidConfigurationException("Configuration creation failed");
+ }
+ return skyframeExecutor.createConfigurations(keepGoing, configurationFactory,
+ configurationKey);
+ }
+
+ /**
+ * Initializes the package cache using the given options, and syncs the package cache. Also
+ * injects a defaults package using the options for the {@link BuildConfiguration}.
+ *
+ * @see DefaultsPackage
+ */
+ public void setupPackageCache(PackageCacheOptions packageCacheOptions,
+ String defaultsPackageContents) throws InterruptedException, AbruptExitException {
+ if (!skyframeExecutor.hasIncrementalState()) {
+ clearSkyframeRelevantCaches();
+ }
+ skyframeExecutor.sync(packageCacheOptions, getWorkingDirectory(),
+ defaultsPackageContents, getCommandId());
+ }
+
+ public void shutdown() {
+ for (BlazeModule module : blazeModules) {
+ module.blazeShutdown();
+ }
+ }
+
+ /**
+ * Throws the exception currently queued by a Blaze module.
+ *
+ * <p>This should be called as often as is practical so that errors are reported as soon as
+ * possible. Ideally, we'd not need this, but the event bus swallows exceptions so we raise
+ * the exception this way.
+ */
+ public void throwPendingException() throws AbruptExitException {
+ if (pendingException != null) {
+ AbruptExitException exception = pendingException;
+ pendingException = null;
+ throw exception;
+ }
+ }
+
+ /**
+ * Returns the defaults package for the default settings. Should only be called by commands that
+ * do <i>not</i> process {@link BuildOptions}, since build options can alter the contents of the
+ * defaults package, which will not be reflected here.
+ */
+ public String getDefaultsPackageContent() {
+ return ruleClassProvider.getDefaultsPackageContent();
+ }
+
+ /**
+ * Returns the defaults package for the given options taken from an optionsProvider.
+ */
+ public String getDefaultsPackageContent(OptionsClassProvider optionsProvider) {
+ return ruleClassProvider.getDefaultsPackageContent(optionsProvider);
+ }
+
+ /**
+ * Creates a BuildOptions class for the given options taken from an optionsProvider.
+ */
+ public BuildOptions createBuildOptions(OptionsClassProvider optionsProvider) {
+ return ruleClassProvider.createBuildOptions(optionsProvider);
+ }
+
+ /**
+ * An EventBus exception handler that will report the exception to a remote server, if a
+ * handler is registered.
+ */
+ public static final class RemoteExceptionHandler implements SubscriberExceptionHandler {
+ @Override
+ public void handleException(Throwable exception, SubscriberExceptionContext context) {
+ LoggingUtil.logToRemote(Level.SEVERE, "Failure in EventBus subscriber.", exception);
+ }
+ }
+
+ /**
+ * An EventBus exception handler that will call BugReport.handleCrash exiting
+ * the current thread.
+ */
+ public static final class BugReportingExceptionHandler implements SubscriberExceptionHandler {
+ @Override
+ public void handleException(Throwable exception, SubscriberExceptionContext context) {
+ BugReport.handleCrash(exception);
+ }
+ }
+
+ /**
+ * Main method for the Blaze server startup. Note: This method logs
+ * exceptions to remote servers. Do not add this to a unittest.
+ */
+ public static void main(Iterable<Class<? extends BlazeModule>> moduleClasses, String[] args) {
+ setupUncaughtHandler(args);
+ List<BlazeModule> modules = createModules(moduleClasses);
+ if (args.length >= 1 && args[0].equals("--batch")) {
+ // Run Blaze in batch mode.
+ System.exit(batchMain(modules, args));
+ }
+ LOG.info("Starting Blaze server with args " + Arrays.toString(args));
+ try {
+ // Run Blaze in server mode.
+ System.exit(serverMain(modules, OutErr.SYSTEM_OUT_ERR, args));
+ } catch (RuntimeException | Error e) { // A definite bug...
+ BugReport.printBug(OutErr.SYSTEM_OUT_ERR, e);
+ BugReport.sendBugReport(e, Arrays.asList(args));
+ System.exit(ExitCode.BLAZE_INTERNAL_ERROR.getNumericExitCode());
+ throw e; // Shouldn't get here.
+ }
+ }
+
+ @VisibleForTesting
+ public static List<BlazeModule> createModules(
+ Iterable<Class<? extends BlazeModule>> moduleClasses) {
+ ImmutableList.Builder<BlazeModule> result = ImmutableList.builder();
+ for (Class<? extends BlazeModule> moduleClass : moduleClasses) {
+ try {
+ BlazeModule module = moduleClass.newInstance();
+ result.add(module);
+ } catch (Throwable e) {
+ throw new IllegalStateException("Cannot instantiate module " + moduleClass.getName(), e);
+ }
+ }
+
+ return result.build();
+ }
+
+ /**
+ * Generates a string form of a request to be written to the logs,
+ * filtering the user environment to remove anything that looks private.
+ * The current filter criteria removes any variable whose name includes
+ * "auth", "pass", or "cookie".
+ *
+ * @param requestStrings
+ * @return the filtered request to write to the log.
+ */
+ @VisibleForTesting
+ public static String getRequestLogString(List<String> requestStrings) {
+ StringBuilder buf = new StringBuilder();
+ buf.append('[');
+ String sep = "";
+ for (String s : requestStrings) {
+ buf.append(sep);
+ if (s.startsWith("--client_env")) {
+ int varStart = "--client_env=".length();
+ int varEnd = s.indexOf('=', varStart);
+ String varName = s.substring(varStart, varEnd);
+ if (suppressFromLog.matcher(varName).matches()) {
+ buf.append("--client_env=");
+ buf.append(varName);
+ buf.append("=__private_value_removed__");
+ } else {
+ buf.append(s);
+ }
+ } else {
+ buf.append(s);
+ }
+ sep = ", ";
+ }
+ buf.append(']');
+ return buf.toString();
+ }
+
+ /**
+ * Command line options split in to two parts: startup options and everything else.
+ */
+ @VisibleForTesting
+ static class CommandLineOptions {
+ private final List<String> startupArgs;
+ private final List<String> otherArgs;
+
+ CommandLineOptions(List<String> startupArgs, List<String> otherArgs) {
+ this.startupArgs = ImmutableList.copyOf(startupArgs);
+ this.otherArgs = ImmutableList.copyOf(otherArgs);
+ }
+
+ public List<String> getStartupArgs() {
+ return startupArgs;
+ }
+
+ public List<String> getOtherArgs() {
+ return otherArgs;
+ }
+ }
+
+ /**
+ * Splits given arguments into two lists - arguments matching options defined in this class
+ * and everything else, while preserving order in each list.
+ */
+ static CommandLineOptions splitStartupOptions(
+ Iterable<BlazeModule> modules, String... args) {
+ List<String> prefixes = new ArrayList<>();
+ List<Field> startupFields = Lists.newArrayList();
+ for (Class<? extends OptionsBase> defaultOptions
+ : BlazeCommandUtils.getStartupOptions(modules)) {
+ startupFields.addAll(ImmutableList.copyOf(defaultOptions.getFields()));
+ }
+
+ for (Field field : startupFields) {
+ if (field.isAnnotationPresent(Option.class)) {
+ prefixes.add("--" + field.getAnnotation(Option.class).name());
+ if (field.getType() == boolean.class || field.getType() == TriState.class) {
+ prefixes.add("--no" + field.getAnnotation(Option.class).name());
+ }
+ }
+ }
+
+ List<String> startupArgs = new ArrayList<>();
+ List<String> otherArgs = Lists.newArrayList(args);
+
+ for (Iterator<String> argi = otherArgs.iterator(); argi.hasNext(); ) {
+ String arg = argi.next();
+ if (!arg.startsWith("--")) {
+ break; // stop at command - all startup options would be specified before it.
+ }
+ for (String prefix : prefixes) {
+ if (arg.startsWith(prefix)) {
+ startupArgs.add(arg);
+ argi.remove();
+ break;
+ }
+ }
+ }
+ return new CommandLineOptions(startupArgs, otherArgs);
+ }
+
+ private static void captureSigint() {
+ final Thread mainThread = Thread.currentThread();
+ final AtomicInteger numInterrupts = new AtomicInteger();
+
+ final Runnable interruptWatcher = new Runnable() {
+ @Override
+ public void run() {
+ int count = 0;
+ // Not an actual infinite loop because it's run in a daemon thread.
+ while (true) {
+ count++;
+ Uninterruptibles.sleepUninterruptibly(10, TimeUnit.SECONDS);
+ LOG.warning("Slow interrupt number " + count + " in batch mode");
+ ThreadUtils.warnAboutSlowInterrupt();
+ }
+ }
+ };
+
+ new InterruptSignalHandler() {
+ @Override
+ public void run() {
+ LOG.info("User interrupt");
+ OutErr.SYSTEM_OUT_ERR.printErrLn("Blaze received an interrupt");
+ mainThread.interrupt();
+
+ int curNumInterrupts = numInterrupts.incrementAndGet();
+ if (curNumInterrupts == 1) {
+ Thread interruptWatcherThread = new Thread(interruptWatcher, "interrupt-watcher");
+ interruptWatcherThread.setDaemon(true);
+ interruptWatcherThread.start();
+ } else if (curNumInterrupts == 2) {
+ LOG.warning("Second --batch interrupt: Reverting to JVM SIGINT handler");
+ uninstall();
+ }
+ }
+ };
+ }
+
+ /**
+ * A main method that runs blaze commands in batch mode. The return value indicates the desired
+ * exit status of the program.
+ */
+ private static int batchMain(Iterable<BlazeModule> modules, String[] args) {
+ captureSigint();
+ CommandLineOptions commandLineOptions = splitStartupOptions(modules, args);
+ LOG.info("Running Blaze in batch mode with startup args "
+ + commandLineOptions.getStartupArgs());
+
+ String memoryWarning = validateJvmMemorySettings();
+ if (memoryWarning != null) {
+ OutErr.SYSTEM_OUT_ERR.printErrLn(memoryWarning);
+ }
+
+ BlazeRuntime runtime;
+ try {
+ runtime = newRuntime(modules, parseOptions(modules, commandLineOptions.getStartupArgs()));
+ } catch (OptionsParsingException e) {
+ OutErr.SYSTEM_OUT_ERR.printErr(e.getMessage());
+ return ExitCode.COMMAND_LINE_ERROR.getNumericExitCode();
+ } catch (AbruptExitException e) {
+ OutErr.SYSTEM_OUT_ERR.printErr(e.getMessage());
+ return e.getExitCode().getNumericExitCode();
+ }
+
+ BlazeCommandDispatcher dispatcher =
+ new BlazeCommandDispatcher(runtime, getBuiltinCommandList());
+
+ try {
+ LOG.info(getRequestLogString(commandLineOptions.getOtherArgs()));
+ return dispatcher.exec(commandLineOptions.getOtherArgs(), OutErr.SYSTEM_OUT_ERR,
+ runtime.getClock().currentTimeMillis());
+ } catch (BlazeCommandDispatcher.ShutdownBlazeServerException e) {
+ return e.getExitStatus();
+ } finally {
+ runtime.shutdown();
+ dispatcher.shutdown();
+ }
+ }
+
+ /**
+ * A main method that does not send email. The return value indicates the desired exit status of
+ * the program.
+ */
+ private static int serverMain(Iterable<BlazeModule> modules, OutErr outErr, String[] args) {
+ try {
+ createBlazeRPCServer(modules, Arrays.asList(args)).serve();
+ return ExitCode.SUCCESS.getNumericExitCode();
+ } catch (OptionsParsingException e) {
+ outErr.printErr(e.getMessage());
+ return ExitCode.COMMAND_LINE_ERROR.getNumericExitCode();
+ } catch (IOException e) {
+ outErr.printErr("I/O Error: " + e.getMessage());
+ return ExitCode.BUILD_FAILURE.getNumericExitCode();
+ } catch (AbruptExitException e) {
+ outErr.printErr(e.getMessage());
+ return e.getExitCode().getNumericExitCode();
+ }
+ }
+
+ private static FileSystem fileSystemImplementation() {
+ // The JNI-based UnixFileSystem is faster, but on Windows it is not available.
+ return OS.getCurrent() == OS.WINDOWS ? new JavaIoFileSystem() : new UnixFileSystem();
+ }
+
+ /**
+ * Creates and returns a new Blaze RPCServer. Call {@link RPCServer#serve()} to start the server.
+ */
+ private static RPCServer createBlazeRPCServer(Iterable<BlazeModule> modules, List<String> args)
+ throws IOException, OptionsParsingException, AbruptExitException {
+ OptionsProvider options = parseOptions(modules, args);
+ BlazeServerStartupOptions startupOptions = options.getOptions(BlazeServerStartupOptions.class);
+
+ final BlazeRuntime runtime = newRuntime(modules, options);
+ final BlazeCommandDispatcher dispatcher =
+ new BlazeCommandDispatcher(runtime, getBuiltinCommandList());
+ final String memoryWarning = validateJvmMemorySettings();
+
+ final ServerCommand blazeCommand;
+
+ // Adaptor from RPC mechanism to BlazeCommandDispatcher:
+ blazeCommand = new ServerCommand() {
+ private boolean shutdown = false;
+
+ @Override
+ public int exec(List<String> args, OutErr outErr, long firstContactTime) {
+ LOG.info(getRequestLogString(args));
+ if (memoryWarning != null) {
+ outErr.printErrLn(memoryWarning);
+ }
+
+ try {
+ return dispatcher.exec(args, outErr, firstContactTime);
+ } catch (BlazeCommandDispatcher.ShutdownBlazeServerException e) {
+ if (e.getCause() != null) {
+ StringWriter message = new StringWriter();
+ message.write("Shutting down due to exception:\n");
+ PrintWriter writer = new PrintWriter(message, true);
+ e.printStackTrace(writer);
+ writer.flush();
+ LOG.severe(message.toString());
+ }
+ shutdown = true;
+ runtime.shutdown();
+ dispatcher.shutdown();
+ return e.getExitStatus();
+ }
+ }
+
+ @Override
+ public boolean shutdown() {
+ return shutdown;
+ }
+ };
+
+ RPCServer server = RPCServer.newServerWith(runtime.getClock(), blazeCommand,
+ runtime.getServerDirectory(), runtime.getWorkspace(), startupOptions.maxIdleSeconds);
+ return server;
+ }
+
+ private static Function<String, String> sourceFunctionForMap(final Map<String, String> map) {
+ return new Function<String, String>() {
+ @Override
+ public String apply(String input) {
+ if (!map.containsKey(input)) {
+ return "default";
+ }
+
+ if (map.get(input).isEmpty()) {
+ return "command line";
+ }
+
+ return map.get(input);
+ }
+ };
+ }
+
+ /**
+ * Parses the command line arguments into a {@link OptionsParser} object.
+ *
+ * <p>This function needs to parse the --option_sources option manually so that the real option
+ * parser can set the source for every option correctly. If that cannot be parsed or is missing,
+ * we just report an unknown source for every startup option.
+ */
+ private static OptionsProvider parseOptions(
+ Iterable<BlazeModule> modules, List<String> args) throws OptionsParsingException {
+ Set<Class<? extends OptionsBase>> optionClasses = Sets.newHashSet();
+ optionClasses.addAll(BlazeCommandUtils.getStartupOptions(modules));
+ // First parse the command line so that we get the option_sources argument
+ OptionsParser parser = OptionsParser.newOptionsParser(optionClasses);
+ parser.setAllowResidue(false);
+ parser.parse(OptionPriority.COMMAND_LINE, null, args);
+ Function<? super String, String> sourceFunction =
+ sourceFunctionForMap(parser.getOptions(BlazeServerStartupOptions.class).optionSources);
+
+ // Then parse the command line again, this time with the correct option sources
+ parser = OptionsParser.newOptionsParser(optionClasses);
+ parser.setAllowResidue(false);
+ parser.parseWithSourceFunction(OptionPriority.COMMAND_LINE, sourceFunction, args);
+ return parser;
+ }
+
+ /**
+ * Creates a new blaze runtime, given the install and output base directories.
+ *
+ * <p>Note: This method can and should only be called once per startup, as it also creates the
+ * filesystem object that will be used for the runtime. So it should only ever be called from the
+ * main method of the Blaze program.
+ *
+ * @param options Blaze startup options.
+ *
+ * @return a new BlazeRuntime instance initialized with the given filesystem and directories, and
+ * an error string that, if not null, describes a fatal initialization failure that makes
+ * this runtime unsuitable for real commands
+ */
+ private static BlazeRuntime newRuntime(
+ Iterable<BlazeModule> blazeModules, OptionsProvider options) throws AbruptExitException {
+ for (BlazeModule module : blazeModules) {
+ module.globalInit(options);
+ }
+
+ BlazeServerStartupOptions startupOptions = options.getOptions(BlazeServerStartupOptions.class);
+ PathFragment workspaceDirectory = startupOptions.workspaceDirectory;
+ PathFragment installBase = startupOptions.installBase;
+ PathFragment outputBase = startupOptions.outputBase;
+
+ OsUtils.maybeForceJNI(installBase); // Must be before first use of JNI.
+
+ // From the point of view of the Java program --install_base and --output_base
+ // are mandatory options, despite the comment in their declarations.
+ if (installBase == null || !installBase.isAbsolute()) { // (includes "" default case)
+ throw new IllegalArgumentException(
+ "Bad --install_base option specified: '" + installBase + "'");
+ }
+ if (outputBase != null && !outputBase.isAbsolute()) { // (includes "" default case)
+ throw new IllegalArgumentException(
+ "Bad --output_base option specified: '" + outputBase + "'");
+ }
+
+ PathFragment outputPathFragment = BlazeDirectories.outputPathFromOutputBase(
+ outputBase, workspaceDirectory);
+ FileSystem fs = null;
+ for (BlazeModule module : blazeModules) {
+ FileSystem moduleFs = module.getFileSystem(options, outputPathFragment);
+ if (moduleFs != null) {
+ Preconditions.checkState(fs == null, "more than one module returns a file system");
+ fs = moduleFs;
+ }
+ }
+
+ if (fs == null) {
+ fs = fileSystemImplementation();
+ }
+ Path.setFileSystemForSerialization(fs);
+
+ Path installBasePath = fs.getPath(installBase);
+ Path outputBasePath = fs.getPath(outputBase);
+ Path workspaceDirectoryPath = null;
+ if (!workspaceDirectory.equals(PathFragment.EMPTY_FRAGMENT)) {
+ workspaceDirectoryPath = fs.getPath(workspaceDirectory);
+ }
+
+ BlazeDirectories directories =
+ new BlazeDirectories(installBasePath, outputBasePath, workspaceDirectoryPath);
+
+ Clock clock = BlazeClock.instance();
+
+ BinTools binTools;
+ try {
+ binTools = BinTools.forProduction(directories);
+ } catch (IOException e) {
+ throw new AbruptExitException(
+ "Cannot enumerate embedded binaries: " + e.getMessage(),
+ ExitCode.LOCAL_ENVIRONMENTAL_ERROR);
+ }
+
+ BlazeRuntime.Builder runtimeBuilder = new BlazeRuntime.Builder().setDirectories(directories)
+ .setStartupOptionsProvider(options)
+ .setBinTools(binTools)
+ .setClock(clock)
+ // TODO(bazel-team): Make BugReportingExceptionHandler the default.
+ // See bug "Make exceptions in EventBus subscribers fatal"
+ .setEventBusExceptionHandler(
+ startupOptions.fatalEventBusExceptions || !BlazeVersionInfo.instance().isReleasedBlaze()
+ ? new BlazeRuntime.BugReportingExceptionHandler()
+ : new BlazeRuntime.RemoteExceptionHandler());
+
+ runtimeBuilder.setRunfilesPrefix(new PathFragment(Constants.RUNFILES_PREFIX));
+ for (BlazeModule blazeModule : blazeModules) {
+ runtimeBuilder.addBlazeModule(blazeModule);
+ }
+
+ BlazeRuntime runtime = runtimeBuilder.build();
+ BugReport.setRuntime(runtime);
+ return runtime;
+ }
+
+ /**
+ * Returns null if JVM memory settings are considered safe, and an error string otherwise.
+ */
+ private static String validateJvmMemorySettings() {
+ boolean is64BitVM = "64".equals(System.getProperty("sun.arch.data.model"));
+ if (is64BitVM) {
+ return null;
+ }
+ MemoryMXBean mem = ManagementFactory.getMemoryMXBean();
+ long heapSize = mem.getHeapMemoryUsage().getMax();
+ long nonHeapSize = mem.getNonHeapMemoryUsage().getMax();
+ if (heapSize == -1 || nonHeapSize == -1) {
+ return null;
+ }
+
+ if (heapSize + nonHeapSize > MAX_BLAZE32_RESERVED_MEMORY) {
+ return String.format(
+ "WARNING: JVM reserved %d MB of virtual memory (above threshold of %d MB). "
+ + "This may result in OOMs at runtime. Use lower values of MaxPermSize "
+ + "or switch to blaze64.",
+ (heapSize + nonHeapSize) >> 20, MAX_BLAZE32_RESERVED_MEMORY >> 20);
+ } else if (heapSize < MIN_BLAZE32_HEAP_SIZE) {
+ return String.format(
+ "WARNING: JVM heap size is %d MB. You probably have a custom -Xmx setting in your "
+ + "local Blaze configuration. This may result in OOMs. Removing overrides of -Xmx "
+ + "settings is advised.",
+ heapSize >> 20);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Make sure async threads cannot be orphaned. This method makes sure bugs are reported to
+ * telemetry and the proper exit code is reported.
+ */
+ private static void setupUncaughtHandler(final String[] args) {
+ Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
+ @Override
+ public void uncaughtException(Thread thread, Throwable throwable) {
+ BugReport.handleCrash(throwable, args);
+ }
+ });
+ }
+
+
+ /**
+ * Returns an immutable list containing new instances of each Blaze command.
+ */
+ @VisibleForTesting
+ public static List<BlazeCommand> getBuiltinCommandList() {
+ return ImmutableList.of(
+ new BuildCommand(),
+ new CanonicalizeCommand(),
+ new CleanCommand(),
+ new HelpCommand(),
+ new SkylarkCommand(),
+ new InfoCommand(),
+ new ProfileCommand(),
+ new QueryCommand(),
+ new RunCommand(),
+ new ShutdownCommand(),
+ new TestCommand(),
+ new VersionCommand());
+ }
+
+ /**
+ * A builder for {@link BlazeRuntime} objects. The only required fields are the {@link
+ * BlazeDirectories}, and the {@link RuleClassProvider} (except for testing). All other fields
+ * have safe default values.
+ *
+ * <p>If a {@link ConfigurationFactory} is set, then the builder ignores the host system flag.
+ * <p>The default behavior of the BlazeRuntime's EventBus is to exit when a subscriber throws
+ * an exception. Please plan appropriately.
+ */
+ public static class Builder {
+
+ private PathFragment runfilesPrefix = PathFragment.EMPTY_FRAGMENT;
+ private BlazeDirectories directories;
+ private Reporter reporter;
+ private ConfigurationFactory configurationFactory;
+ private Clock clock;
+ private OptionsProvider startupOptionsProvider;
+ private final List<BlazeModule> blazeModules = Lists.newArrayList();
+ private SubscriberExceptionHandler eventBusExceptionHandler =
+ new RemoteExceptionHandler();
+ private BinTools binTools;
+ private UUID instanceId;
+
+ public BlazeRuntime build() throws AbruptExitException {
+ Preconditions.checkNotNull(directories);
+ Preconditions.checkNotNull(startupOptionsProvider);
+ Reporter reporter = (this.reporter == null) ? new Reporter() : this.reporter;
+
+ Clock clock = (this.clock == null) ? BlazeClock.instance() : this.clock;
+ UUID instanceId = (this.instanceId == null) ? UUID.randomUUID() : this.instanceId;
+
+ Preconditions.checkNotNull(clock);
+ Map<String, String> clientEnv = new HashMap<>();
+ TimestampGranularityMonitor timestampMonitor = new TimestampGranularityMonitor(clock);
+
+ Preprocessor.Factory.Supplier preprocessorFactorySupplier = null;
+ SkyframeExecutorFactory skyframeExecutorFactory = null;
+ for (BlazeModule module : blazeModules) {
+ module.blazeStartup(startupOptionsProvider,
+ BlazeVersionInfo.instance(), instanceId, directories, clock);
+ Preprocessor.Factory.Supplier modulePreprocessorFactorySupplier =
+ module.getPreprocessorFactorySupplier();
+ if (modulePreprocessorFactorySupplier != null) {
+ Preconditions.checkState(preprocessorFactorySupplier == null,
+ "more than one module defines a preprocessor factory supplier");
+ preprocessorFactorySupplier = modulePreprocessorFactorySupplier;
+ }
+ SkyframeExecutorFactory skyFactory = module.getSkyframeExecutorFactory();
+ if (skyFactory != null) {
+ Preconditions.checkState(skyframeExecutorFactory == null,
+ "At most one skyframe factory supported. But found two: %s and %s", skyFactory,
+ skyframeExecutorFactory);
+ skyframeExecutorFactory = skyFactory;
+ }
+ }
+ if (skyframeExecutorFactory == null) {
+ skyframeExecutorFactory = new SequencedSkyframeExecutorFactory();
+ }
+ if (preprocessorFactorySupplier == null) {
+ preprocessorFactorySupplier = Preprocessor.Factory.Supplier.NullSupplier.INSTANCE;
+ }
+
+ ConfiguredRuleClassProvider.Builder ruleClassBuilder =
+ new ConfiguredRuleClassProvider.Builder();
+ for (BlazeModule module : blazeModules) {
+ module.initializeRuleClasses(ruleClassBuilder);
+ }
+
+ Map<String, String> platformRegexps = null;
+ {
+ ImmutableMap.Builder<String, String> builder = new ImmutableMap.Builder<>();
+ for (BlazeModule module : blazeModules) {
+ builder.putAll(module.getPlatformSetRegexps());
+ }
+ platformRegexps = builder.build();
+ if (platformRegexps.isEmpty()) {
+ platformRegexps = null; // Use the default.
+ }
+ }
+
+ Set<Path> immutableDirectories = null;
+ {
+ ImmutableSet.Builder<Path> builder = new ImmutableSet.Builder<>();
+ for (BlazeModule module : blazeModules) {
+ builder.addAll(module.getImmutableDirectories());
+ }
+ immutableDirectories = builder.build();
+ }
+
+ Iterable<DiffAwareness.Factory> diffAwarenessFactories = null;
+ {
+ ImmutableList.Builder<DiffAwareness.Factory> builder = new ImmutableList.Builder<>();
+ boolean watchFS = startupOptionsProvider != null
+ && startupOptionsProvider.getOptions(BlazeServerStartupOptions.class).watchFS;
+ for (BlazeModule module : blazeModules) {
+ builder.addAll(module.getDiffAwarenessFactories(watchFS));
+ }
+ diffAwarenessFactories = builder.build();
+ }
+
+ // Merge filters from Blaze modules that allow some action inputs to be missing.
+ Predicate<PathFragment> allowedMissingInputs = null;
+ for (BlazeModule module : blazeModules) {
+ Predicate<PathFragment> modulePredicate = module.getAllowedMissingInputs();
+ if (modulePredicate != null) {
+ Preconditions.checkArgument(allowedMissingInputs == null,
+ "More than one Blaze module allows missing inputs.");
+ allowedMissingInputs = modulePredicate;
+ }
+ }
+ if (allowedMissingInputs == null) {
+ allowedMissingInputs = Predicates.alwaysFalse();
+ }
+
+ ConfiguredRuleClassProvider ruleClassProvider = ruleClassBuilder.build();
+ WorkspaceStatusAction.Factory workspaceStatusActionFactory = null;
+ for (BlazeModule module : blazeModules) {
+ WorkspaceStatusAction.Factory candidate = module.getWorkspaceStatusActionFactory();
+ if (candidate != null) {
+ Preconditions.checkState(workspaceStatusActionFactory == null,
+ "more than one module defines a workspace status action factory");
+ workspaceStatusActionFactory = candidate;
+ }
+ }
+
+ List<PackageFactory.EnvironmentExtension> extensions = new ArrayList<>();
+ for (BlazeModule module : blazeModules) {
+ extensions.add(module.getPackageEnvironmentExtension());
+ }
+
+ // We use an immutable map builder for the nice side effect that it throws if a duplicate key
+ // is inserted.
+ ImmutableMap.Builder<SkyFunctionName, SkyFunction> skyFunctions = ImmutableMap.builder();
+ for (BlazeModule module : blazeModules) {
+ skyFunctions.putAll(module.getSkyFunctions(directories));
+ }
+
+ ImmutableList.Builder<PrecomputedValue.Injected> precomputedValues = ImmutableList.builder();
+ for (BlazeModule module : blazeModules) {
+ precomputedValues.addAll(module.getPrecomputedSkyframeValues());
+ }
+
+ final PackageFactory pkgFactory =
+ new PackageFactory(ruleClassProvider, platformRegexps, extensions);
+ SkyframeExecutor skyframeExecutor = skyframeExecutorFactory.create(reporter, pkgFactory,
+ timestampMonitor, directories, workspaceStatusActionFactory,
+ ruleClassProvider.getBuildInfoFactories(), immutableDirectories, diffAwarenessFactories,
+ allowedMissingInputs, preprocessorFactorySupplier, skyFunctions.build(),
+ precomputedValues.build());
+
+ if (configurationFactory == null) {
+ configurationFactory = new ConfigurationFactory(
+ ruleClassProvider.getConfigurationCollectionFactory(),
+ ruleClassProvider.getConfigurationFragments());
+ }
+
+ ProjectFile.Provider projectFileProvider = null;
+ for (BlazeModule module : blazeModules) {
+ ProjectFile.Provider candidate = module.createProjectFileProvider();
+ if (candidate != null) {
+ Preconditions.checkState(projectFileProvider == null,
+ "more than one module defines a project file provider");
+ projectFileProvider = candidate;
+ }
+ }
+
+ return new BlazeRuntime(directories, reporter, workspaceStatusActionFactory, skyframeExecutor,
+ pkgFactory, ruleClassProvider, configurationFactory,
+ runfilesPrefix == null ? PathFragment.EMPTY_FRAGMENT : runfilesPrefix,
+ clock, startupOptionsProvider, ImmutableList.copyOf(blazeModules),
+ clientEnv, timestampMonitor,
+ eventBusExceptionHandler, binTools, projectFileProvider);
+ }
+
+ public Builder setRunfilesPrefix(PathFragment prefix) {
+ this.runfilesPrefix = prefix;
+ return this;
+ }
+
+ public Builder setBinTools(BinTools binTools) {
+ this.binTools = binTools;
+ return this;
+ }
+
+ public Builder setDirectories(BlazeDirectories directories) {
+ this.directories = directories;
+ return this;
+ }
+
+ /**
+ * Creates and sets a new {@link BlazeDirectories} instance with the given
+ * parameters.
+ */
+ public Builder setDirectories(Path installBase, Path outputBase,
+ Path workspace) {
+ this.directories = new BlazeDirectories(installBase, outputBase, workspace);
+ return this;
+ }
+
+ public Builder setReporter(Reporter reporter) {
+ this.reporter = reporter;
+ return this;
+ }
+
+ public Builder setConfigurationFactory(ConfigurationFactory configurationFactory) {
+ this.configurationFactory = configurationFactory;
+ return this;
+ }
+
+ public Builder setClock(Clock clock) {
+ this.clock = clock;
+ return this;
+ }
+
+ public Builder setStartupOptionsProvider(OptionsProvider startupOptionsProvider) {
+ this.startupOptionsProvider = startupOptionsProvider;
+ return this;
+ }
+
+ public Builder addBlazeModule(BlazeModule blazeModule) {
+ blazeModules.add(blazeModule);
+ return this;
+ }
+
+ public Builder setInstanceId(UUID id) {
+ instanceId = id;
+ return this;
+ }
+
+ @VisibleForTesting
+ public Builder setEventBusExceptionHandler(
+ SubscriberExceptionHandler eventBusExceptionHandler) {
+ this.eventBusExceptionHandler = eventBusExceptionHandler;
+ return this;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/BlazeServerStartupOptions.java b/src/main/java/com/google/devtools/build/lib/runtime/BlazeServerStartupOptions.java
new file mode 100644
index 0000000..1f9bcea
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/BlazeServerStartupOptions.java
@@ -0,0 +1,225 @@
+// Copyright 2014 Google Inc. 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.runtime;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.util.OptionsUtils;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.common.options.Converter;
+import com.google.devtools.common.options.Option;
+import com.google.devtools.common.options.OptionsBase;
+
+import java.util.Map;
+
+/**
+ * Options that will be evaluated by the blaze client startup code and passed
+ * to the blaze server upon startup.
+ *
+ * <h4>IMPORTANT</h4> These options and their defaults must be kept in sync with those in the
+ * source of the launcher. The latter define the actual default values; this class exists only to
+ * provide the help message, which displays the default values.
+ *
+ * The same relationship holds between {@link HostJvmStartupOptions} and the launcher.
+ */
+public class BlazeServerStartupOptions extends OptionsBase {
+ /**
+ * Converter for the <code>option_sources</code> option. Takes a string in the form of
+ * "option_name1:source1:option_name2:source2:.." and converts it into an option name to
+ * source map.
+ */
+ public static class OptionSourcesConverter implements Converter<Map<String, String>> {
+ private String unescape(String input) {
+ return input.replace("_C", ":").replace("_U", "_");
+ }
+
+ @Override
+ public Map<String, String> convert(String input) {
+ ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
+ if (input.isEmpty()) {
+ return builder.build();
+ }
+
+ String[] elements = input.split(":");
+ for (int i = 0; i < (elements.length + 1) / 2; i++) {
+ String name = elements[i * 2];
+ String value = "";
+ if (elements.length > i * 2 + 1) {
+ value = elements[i * 2 + 1];
+ }
+ builder.put(unescape(name), unescape(value));
+ }
+ return builder.build();
+ }
+
+ @Override
+ public String getTypeDescription() {
+ return "a list of option-source pairs";
+ }
+ }
+
+ /* Passed from the client to the server, specifies the installation
+ * location. The location should be of the form:
+ * $OUTPUT_BASE/_blaze_${USER}/install/${MD5_OF_INSTALL_MANIFEST}.
+ * The server code will only accept a non-empty path; it's the
+ * responsibility of the client to compute a proper default if
+ * necessary.
+ */
+ @Option(name = "install_base",
+ defaultValue = "", // NOTE: purely decorative! See class docstring.
+ category = "hidden",
+ converter = OptionsUtils.PathFragmentConverter.class,
+ help = "This launcher option is intended for use only by tests.")
+ public PathFragment installBase;
+
+ /* Note: The help string in this option applies to the client code; not
+ * the server code. The server code will only accept a non-empty path; it's
+ * the responsibility of the client to compute a proper default if
+ * necessary.
+ */
+ @Option(name = "output_base",
+ defaultValue = "null", // NOTE: purely decorative! See class docstring.
+ category = "server startup",
+ converter = OptionsUtils.PathFragmentConverter.class,
+ help = "If set, specifies the output location to which all build output will be written. "
+ + "Otherwise, the location will be "
+ + "${OUTPUT_ROOT}/_blaze_${USER}/${MD5_OF_WORKSPACE_ROOT}. Note: If you specify a "
+ + "different option from one to the next Blaze invocation for this value, you'll likely "
+ + "start up a new, additional Blaze server. Blaze starts exactly one server per "
+ + "specified output base. Typically there is one output base per workspace--however, "
+ + "with this option you may have multiple output bases per workspace and thereby run "
+ + "multiple builds for the same client on the same machine concurrently. See "
+ + "'blaze help shutdown' on how to shutdown a Blaze server.")
+ public PathFragment outputBase;
+
+ /* Note: This option is only used by the C++ client, never by the Java server.
+ * It is included here to make sure that the option is documented in the help
+ * output, which is auto-generated by Java code.
+ */
+ @Option(name = "output_user_root",
+ defaultValue = "null", // NOTE: purely decorative! See class docstring.
+ category = "server startup",
+ converter = OptionsUtils.PathFragmentConverter.class,
+ help = "The user-specific directory beneath which all build outputs are written; "
+ + "by default, this is a function of $USER, but by specifying a constant, build outputs "
+ + "can be shared between collaborating users.")
+ public PathFragment outputUserRoot;
+
+ @Option(name = "workspace_directory",
+ defaultValue = "",
+ category = "hidden",
+ converter = OptionsUtils.PathFragmentConverter.class,
+ help = "The root of the workspace, that is, the directory that Blaze uses as the root of the "
+ + "build. This flag is only to be set by the blaze client.")
+ public PathFragment workspaceDirectory;
+
+ @Option(name = "max_idle_secs",
+ defaultValue = "" + (3 * 3600), // NOTE: purely decorative! See class docstring.
+ category = "server startup",
+ help = "The number of seconds the build server will wait idling " +
+ "before shutting down. Note: Blaze will ignore this option " +
+ "unless you are starting a new instance. See also 'blaze help " +
+ "shutdown'.")
+ public int maxIdleSeconds;
+
+ @Option(name = "batch",
+ defaultValue = "false", // NOTE: purely decorative! See class docstring.
+ category = "server startup",
+ help = "If set, Blaze will be run in batch mode, instead of " +
+ "the standard client/server. Doing so may provide " +
+ "more predictable semantics with respect to signal handling and job control, " +
+ "Batch mode retains proper queueing semantics within the same output_base. " +
+ "That is, simultaneous invocations will be processed in order, without overlap. " +
+ "If a batch mode Blaze is run on a client with a running server, it first kills " +
+ "the server before processing the command." +
+ "Blaze will run slower in batch mode, compared to client/server mode. " +
+ "Among other things, the build file cache is memory-resident, so it is not " +
+ "preserved between sequential batch invocations. Therefore, using batch mode " +
+ "often makes more sense in cases where performance is less critical, " +
+ "such as continuous builds.")
+ public boolean batch;
+
+ @Option(name = "block_for_lock",
+ defaultValue = "true", // NOTE: purely decorative! See class docstring.
+ category = "server startup",
+ help = "If set, Blaze will exit immediately instead of waiting for other " +
+ "Blaze commands holding the server lock to complete.")
+ public boolean noblock_for_lock;
+
+ @Option(name = "io_nice_level",
+ defaultValue = "-1", // NOTE: purely decorative!
+ category = "server startup",
+ help = "Set a level from 0-7 for best-effort IO scheduling. 0 is highest priority, " +
+ "7 is lowest. The anticipatory scheduler may only honor up to priority 4. " +
+ "Negative values are ignored.")
+ public int ioNiceLevel;
+
+ @Option(name = "batch_cpu_scheduling",
+ defaultValue = "false", // NOTE: purely decorative!
+ category = "server startup",
+ help = "Use 'batch' CPU scheduling for Blaze. This policy is useful for workloads that " +
+ "are non-interactive, but do not want to lower their nice value. " +
+ "See 'man 2 sched_setscheduler'.")
+ public boolean batchCpuScheduling;
+
+ @Option(name = "blazerc",
+ // NOTE: purely decorative!
+ defaultValue = "In the current directory, then in the user's home directory, the file named "
+ + ".$(basename $0)rc (i.e. .bazelrc for Bazel or .blazerc for Blaze)",
+ category = "misc",
+ help = "The location of the .bazelrc/.blazerc file containing default values of "
+ + "Blaze command options. Use /dev/null to disable the search for a "
+ + "blazerc file, e.g. in release builds.")
+ public String blazerc;
+
+ @Option(name = "master_blazerc",
+ defaultValue = "true", // NOTE: purely decorative!
+ category = "misc",
+ help = "If this option is false, the master blazerc/bazelrc next to the binary "
+ + "is not read.")
+ public boolean masterBlazerc;
+
+ @Option(name = "skyframe",
+ defaultValue = "full",
+ category = "undocumented",
+ help = "Unused.")
+ public String unusedSkyframe;
+
+ @Option(name = "fatal_event_bus_exceptions",
+ defaultValue = "false", // NOTE: purely decorative!
+ category = "undocumented",
+ help = "Whether or not to allow EventBus exceptions to be fatal. Experimental.")
+ public boolean fatalEventBusExceptions;
+
+ @Option(name = "option_sources",
+ converter = OptionSourcesConverter.class,
+ defaultValue = "",
+ category = "hidden",
+ help = "")
+ public Map<String, String> optionSources;
+
+ // TODO(bazel-team): In order to make it easier to have local watchers in open source Bazel,
+ // turn this into a non-startup option.
+ @Option(name = "watchfs",
+ defaultValue = "false",
+ category = "undocumented",
+ help = "If true, Blaze tries to use the operating system's file watch service for local "
+ + "changes instead of scanning every file for a change.")
+ public boolean watchFS;
+
+ @Option(name = "use_webstatusserver",
+ defaultValue = "0",
+ category = "server startup",
+ help = "Specifies port to run web status server on (0 to disable, which is default).")
+ public int useWebStatusServer;
+}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/BugReport.java b/src/main/java/com/google/devtools/build/lib/runtime/BugReport.java
new file mode 100644
index 0000000..ee1e429
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/BugReport.java
@@ -0,0 +1,141 @@
+// Copyright 2014 Google Inc. 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.runtime;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.analysis.BlazeVersionInfo;
+import com.google.devtools.build.lib.util.ExitCode;
+import com.google.devtools.build.lib.util.LoggingUtil;
+import com.google.devtools.build.lib.util.io.OutErr;
+
+import java.io.PrintStream;
+import java.util.Arrays;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Utility methods for sending bug reports.
+ *
+ * <p> Note, code in this class must be extremely robust. There's nothing
+ * worse than a crash-handler that itself crashes!
+ */
+public abstract class BugReport {
+
+ private BugReport() {}
+
+ private static Logger LOG = Logger.getLogger(BugReport.class.getName());
+
+ private static BlazeVersionInfo versionInfo = BlazeVersionInfo.instance();
+
+ private static BlazeRuntime runtime = null;
+
+ public static void setRuntime(BlazeRuntime newRuntime) {
+ Preconditions.checkNotNull(newRuntime);
+ Preconditions.checkState(runtime == null, "runtime already set: %s, %s", runtime, newRuntime);
+ runtime = newRuntime;
+ }
+
+ /**
+ * Logs the unhandled exception with a special prefix signifying that this was a crash.
+ *
+ * @param exception the unhandled exception to display.
+ * @param args additional values to record in the message.
+ * @param values Additional string values to clarify the exception.
+ */
+ public static void sendBugReport(Throwable exception, List<String> args, String... values) {
+ if (!versionInfo.isReleasedBlaze()) {
+ LOG.info("(Not a released binary; not logged.)");
+ return;
+ }
+
+ logException(exception, filterClientEnv(args), values);
+ }
+
+ /**
+ * Print and send a bug report, and exit with the proper Blaze code.
+ */
+ public static void handleCrash(Throwable throwable, String... args) {
+ BugReport.sendBugReport(throwable, Arrays.asList(args));
+ BugReport.printBug(OutErr.SYSTEM_OUT_ERR, throwable);
+ System.err.println("Blaze crash in async thread:");
+ throwable.printStackTrace();
+ int exitCode =
+ (throwable instanceof OutOfMemoryError) ? ExitCode.OOM_ERROR.getNumericExitCode()
+ : ExitCode.BLAZE_INTERNAL_ERROR.getNumericExitCode();
+ if (runtime != null) {
+ runtime.notifyCommandComplete(exitCode);
+ // We don't call runtime#shutDown() here because all it does is shut down the modules, and who
+ // knows if they can be trusted.
+ }
+ System.exit(exitCode);
+ }
+
+ private static void printThrowableTo(OutErr outErr, Throwable e) {
+ PrintStream err = new PrintStream(outErr.getErrorStream());
+ e.printStackTrace(err);
+ err.flush();
+ LOG.log(Level.SEVERE, "Blaze crashed", e);
+ }
+
+ /**
+ * Print user-helpful information about the bug/crash to the output.
+ *
+ * @param outErr where to write the output
+ * @param e the exception thrown
+ */
+ public static void printBug(OutErr outErr, Throwable e) {
+ if (e instanceof OutOfMemoryError) {
+ outErr.printErr(e.getMessage() + "\n\n" +
+ "Blaze ran out of memory and crashed.\n");
+ } else {
+ printThrowableTo(outErr, e);
+ }
+ }
+
+ /**
+ * Filters {@code args} by removing any item that starts with "--client_env",
+ * then returns this as an immutable list.
+ *
+ * <p>The client's environment variables may contain sensitive data, so we filter it out.
+ */
+ private static List<String> filterClientEnv(Iterable<String> args) {
+ if (args == null) {
+ return null;
+ }
+
+ ImmutableList.Builder<String> filteredArgs = ImmutableList.builder();
+ for (String arg : args) {
+ if (arg != null && !arg.startsWith("--client_env")) {
+ filteredArgs.add(arg);
+ }
+ }
+ return filteredArgs.build();
+ }
+
+ // Log the exception. Because this method is only called in a blaze release,
+ // this will result in a report being sent to a remote logging service.
+ private static void logException(Throwable exception, List<String> args, String... values) {
+ // The preamble is used in the crash watcher, so don't change it
+ // unless you know what you're doing.
+ String preamble = exception instanceof OutOfMemoryError
+ ? "Blaze OOMError: "
+ : "Blaze crashed with args: ";
+
+ LoggingUtil.logToRemote(Level.SEVERE, preamble + Joiner.on(' ').join(args), exception,
+ values);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/BuildPhase.java b/src/main/java/com/google/devtools/build/lib/runtime/BuildPhase.java
new file mode 100644
index 0000000..5175a15
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/BuildPhase.java
@@ -0,0 +1,48 @@
+// Copyright 2014 Google Inc. 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.runtime;
+
+/**
+ * Represents how far into the build a given target has gone.
+ * Used primarily for master log status reporting and representation.
+ */
+public enum BuildPhase {
+ PARSING("parsing-failed", false),
+ LOADING("loading-failed", false),
+ ANALYSIS("analysis-failed", false),
+ TEST_FILTERING("test-filtered", true),
+ TARGET_FILTERING("target-filtered", true),
+ NOT_BUILT("not-built", false),
+ NOT_ANALYZED("not-analyzed", false),
+ EXECUTION("build-failed", false),
+ BLAZE_HALTED("blaze-halted", false),
+ COMPLETE("built", true);
+
+ private final String msg;
+ private final boolean success;
+
+ BuildPhase(String msg, boolean success) {
+ this.msg = msg;
+ this.success = success;
+ }
+
+ public String getMessage() {
+ return msg;
+ }
+
+ public boolean getSuccess() {
+ return success;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/BuildSummaryStatsModule.java b/src/main/java/com/google/devtools/build/lib/runtime/BuildSummaryStatsModule.java
new file mode 100644
index 0000000..8b072c7
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/BuildSummaryStatsModule.java
@@ -0,0 +1,88 @@
+// Copyright 2014 Google Inc. 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.runtime;
+
+import com.google.common.base.Joiner;
+import com.google.common.eventbus.EventBus;
+import com.google.common.eventbus.Subscribe;
+import com.google.devtools.build.lib.buildtool.buildevent.BuildCompleteEvent;
+import com.google.devtools.build.lib.buildtool.buildevent.ExecutionStartingEvent;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.Reporter;
+import com.google.devtools.build.lib.profiler.Profiler;
+import com.google.devtools.build.lib.profiler.ProfilerTask;
+import com.google.devtools.build.lib.util.BlazeClock;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Logger;
+
+/**
+ * Blaze module for the build summary message that reports various stats to the user.
+ */
+public class BuildSummaryStatsModule extends BlazeModule {
+
+ private static final Logger LOG = Logger.getLogger(BuildSummaryStatsModule.class.getName());
+
+ private SimpleCriticalPathComputer criticalPathComputer;
+ private EventBus eventBus;
+ private Reporter reporter;
+
+ @Override
+ public void beforeCommand(BlazeRuntime runtime, Command command) {
+ this.reporter = runtime.getReporter();
+ this.eventBus = runtime.getEventBus();
+ eventBus.register(this);
+ }
+
+ @Subscribe
+ public void executionPhaseStarting(ExecutionStartingEvent event) {
+ criticalPathComputer = new SimpleCriticalPathComputer(BlazeClock.instance());
+ eventBus.register(criticalPathComputer);
+ }
+
+ @Subscribe
+ public void buildComplete(BuildCompleteEvent event) {
+ try {
+ // We might want to make this conditional on a flag; it can sometimes be a bit of a nuisance.
+ List<String> items = new ArrayList<>();
+ items.add(String.format("Elapsed time: %.3fs", event.getResult().getElapsedSeconds()));
+
+ if (criticalPathComputer != null) {
+ Profiler.instance().startTask(ProfilerTask.CRITICAL_PATH, "Critical path");
+ AggregatedCriticalPath<SimpleCriticalPathComponent> criticalPath =
+ criticalPathComputer.aggregate();
+ items.add(criticalPath.toStringSummary());
+ LOG.info(criticalPath.toString());
+ LOG.info("Slowest actions:\n " + Joiner.on("\n ")
+ .join(criticalPathComputer.getSlowestComponents()));
+ // We reverse the critical path because the profiler expect events ordered by the time
+ // when the actions were executed while critical path computation is stored in the reverse
+ // way.
+ for (SimpleCriticalPathComponent stat : criticalPath.components().reverse()) {
+ Profiler.instance().logSimpleTaskDuration(
+ TimeUnit.MILLISECONDS.toNanos(stat.getStartTime()),
+ TimeUnit.MILLISECONDS.toNanos(stat.getActionWallTime()),
+ ProfilerTask.CRITICAL_PATH_COMPONENT, stat.getAction());
+ }
+ Profiler.instance().completeTask(ProfilerTask.CRITICAL_PATH);
+ }
+
+ reporter.handle(Event.info(Joiner.on(", ").join(items)));
+ } finally {
+ criticalPathComputer = null;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/Command.java b/src/main/java/com/google/devtools/build/lib/runtime/Command.java
new file mode 100644
index 0000000..1797cd3
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/Command.java
@@ -0,0 +1,108 @@
+// Copyright 2014 Google Inc. 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.runtime;
+
+import com.google.devtools.common.options.Option;
+import com.google.devtools.common.options.OptionsBase;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * An annotation that lets blaze commands specify their options and their help.
+ * The annotations are processed by {@link BlazeCommand}.
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Command {
+ /**
+ * The name of the command, as the user would type it.
+ */
+ String name();
+
+ /**
+ * Options processed by the command, indicated by options interfaces.
+ * These interfaces must contain methods annotated with {@link Option}.
+ */
+ Class<? extends OptionsBase>[] options() default {};
+
+ /**
+ * The set of other Blaze commands that this annotation's command "inherits"
+ * options from. These classes must be annotated with {@link Command}.
+ */
+ Class<? extends BlazeCommand>[] inherits() default {};
+
+ /**
+ * A short description, which appears in 'blaze help'.
+ */
+ String shortDescription();
+
+ /**
+ * True if the configuration-specific options should be available for this command.
+ */
+ boolean usesConfigurationOptions() default false;
+
+ /**
+ * True if the command runs a build.
+ */
+ boolean builds() default false;
+
+ /**
+ * True if the command should not be shown in the output of 'blaze help'.
+ */
+ boolean hidden() default false;
+
+ /**
+ * Specifies whether this command allows a residue after the parsed options.
+ * For example, a command might expect a list of targets to build in the
+ * residue.
+ */
+ boolean allowResidue() default false;
+
+ /**
+ * Returns true if this command wants to write binary data to stdout.
+ * Enabling this flag will disable ANSI escape stripping for this command.
+ */
+ boolean binaryStdOut() default false;
+
+ /**
+ * Returns true if this command wants to write binary data to stderr.
+ * Enabling this flag will disable ANSI escape stripping for this command.
+ */
+ boolean binaryStdErr() default false;
+
+ /**
+ * The help message for this command. If the value starts with "resource:",
+ * the remainder is interpreted as the name of a text file resource (in the
+ * .jar file that provides the Command implementation class).
+ */
+ String help();
+
+ /**
+ * Returns true iff this command may only be run from within a Blaze workspace. Broadly, this
+ * should be true for any command that interprets the package-path, since it's potentially
+ * confusing otherwise.
+ */
+ boolean mustRunInWorkspace() default true;
+
+ /**
+ * Returns true iff this command is allowed to run in the output directory,
+ * i.e. $OUTPUT_BASE/_blaze_$USER/$MD5/... . No command should be allowed to run here,
+ * but there are some legacy uses of 'blaze query'.
+ */
+ boolean canRunInOutputDirectory() default false;
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/CommandCompleteEvent.java b/src/main/java/com/google/devtools/build/lib/runtime/CommandCompleteEvent.java
new file mode 100644
index 0000000..fb92781
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/CommandCompleteEvent.java
@@ -0,0 +1,38 @@
+// Copyright 2014 Google Inc. 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.runtime;
+
+/**
+ * This event is fired when the Blaze command is complete
+ * (clean, build, test, etc.).
+ */
+public class CommandCompleteEvent extends CommandEvent {
+
+ private final int exitCode;
+
+ /**
+ * @param exitCode the exit code of the blaze command
+ */
+ public CommandCompleteEvent(int exitCode) {
+ this.exitCode = exitCode;
+ }
+
+ /**
+ * @return the exit code of the blaze command
+ */
+ public int getExitCode() {
+ return exitCode;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/CommandEvent.java b/src/main/java/com/google/devtools/build/lib/runtime/CommandEvent.java
new file mode 100644
index 0000000..3e59dce
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/CommandEvent.java
@@ -0,0 +1,68 @@
+// Copyright 2014 Google Inc. 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.runtime;
+
+import com.google.devtools.build.lib.util.BlazeClock;
+
+import java.lang.management.GarbageCollectorMXBean;
+import java.lang.management.ManagementFactory;
+import java.util.Date;
+
+/**
+ * Base class for Command events that includes some resource fields.
+ */
+public abstract class CommandEvent {
+
+ private final long eventTimeInNanos;
+ private final long eventTimeInEpochTime;
+ private final long gcTimeInMillis;
+
+ protected CommandEvent() {
+ eventTimeInNanos = BlazeClock.nanoTime();
+ eventTimeInEpochTime = new Date().getTime();
+ gcTimeInMillis = collectGcTimeInMillis();
+ }
+
+ /**
+ * Returns time spent in garbage collection since the start of the JVM process.
+ */
+ private static long collectGcTimeInMillis() {
+ long gcTime = 0;
+ for (GarbageCollectorMXBean gcBean : ManagementFactory.getGarbageCollectorMXBeans()) {
+ gcTime += gcBean.getCollectionTime();
+ }
+ return gcTime;
+ }
+
+ /**
+ * Get the time-stamp in ns for the event.
+ */
+ public long getEventTimeInNanos() {
+ return eventTimeInNanos;
+ }
+
+ /**
+ * Get the time-stamp as epoch-time for the event.
+ */
+ public long getEventTimeInEpochTime() {
+ return eventTimeInEpochTime;
+ }
+
+ /**
+ * Get the cumulative GC time for the event.
+ */
+ public long getGCTimeInMillis() {
+ return gcTimeInMillis;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/CommandPrecompleteEvent.java b/src/main/java/com/google/devtools/build/lib/runtime/CommandPrecompleteEvent.java
new file mode 100644
index 0000000..9a44086
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/CommandPrecompleteEvent.java
@@ -0,0 +1,38 @@
+// Copyright 2014 Google Inc. 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.runtime;
+
+import com.google.devtools.build.lib.util.ExitCode;
+
+/**
+ * This message is fired right before the Blaze command completes,
+ * and can be used to modify the command's exit code.
+ */
+public class CommandPrecompleteEvent {
+ private final ExitCode exitCode;
+
+ /**
+ * @param exitCode the exit code of the blaze command
+ */
+ public CommandPrecompleteEvent(ExitCode exitCode) {
+ this.exitCode = exitCode;
+ }
+
+ /**
+ * @return the exit code of the blaze command
+ */
+ public ExitCode getExitCode() {
+ return exitCode;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/CommandStartEvent.java b/src/main/java/com/google/devtools/build/lib/runtime/CommandStartEvent.java
new file mode 100644
index 0000000..32834a2
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/CommandStartEvent.java
@@ -0,0 +1,58 @@
+// Copyright 2014 Google Inc. 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.runtime;
+
+import com.google.devtools.build.lib.vfs.Path;
+
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * This event is fired when the Blaze command is started (clean, build, test,
+ * etc.).
+ */
+public class CommandStartEvent extends CommandEvent {
+ private final String commandName;
+ private final UUID commandId;
+ private final Map<String, String> clientEnv;
+ private final Path workingDirectory;
+
+ /**
+ * @param commandName the name of the command
+ */
+ public CommandStartEvent(String commandName, UUID commandId, Map<String, String> clientEnv,
+ Path workingDirectory) {
+ this.commandName = commandName;
+ this.commandId = commandId;
+ this.clientEnv = clientEnv;
+ this.workingDirectory = workingDirectory;
+ }
+
+ public String getCommandName() {
+ return commandName;
+ }
+
+ public UUID getCommandId() {
+ return commandId;
+ }
+
+ public Map<String, String> getClientEnv() {
+ return clientEnv;
+ }
+
+ public Path getWorkingDirectory() {
+ return workingDirectory;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/CommonCommandOptions.java b/src/main/java/com/google/devtools/build/lib/runtime/CommonCommandOptions.java
new file mode 100644
index 0000000..7054975
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/CommonCommandOptions.java
@@ -0,0 +1,250 @@
+// Copyright 2014 Google Inc. 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.runtime;
+
+import com.google.devtools.build.lib.util.OptionsUtils;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.common.options.Converter;
+import com.google.devtools.common.options.Converters;
+import com.google.devtools.common.options.Option;
+import com.google.devtools.common.options.OptionsBase;
+import com.google.devtools.common.options.OptionsParsingException;
+
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+
+/**
+ * Options common to all commands.
+ */
+public class CommonCommandOptions extends OptionsBase {
+ /**
+ * A class representing a blazerc option. blazeRc is serial number of the rc
+ * file this option came from, option is the name of the option and value is
+ * its value (or null if not specified).
+ */
+ public static class OptionOverride {
+ final int blazeRc;
+ final String command;
+ final String option;
+
+ public OptionOverride(int blazeRc, String command, String option) {
+ this.blazeRc = blazeRc;
+ this.command = command;
+ this.option = option;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%d:%s=%s", blazeRc, command, option);
+ }
+ }
+
+ /**
+ * Converter for --default_override. The format is:
+ * --default_override=blazerc:command=option.
+ */
+ public static class OptionOverrideConverter implements Converter<OptionOverride> {
+ static final String ERROR_MESSAGE = "option overrides must be in form "
+ + " rcfile:command=option, where rcfile is a nonzero integer";
+
+ public OptionOverrideConverter() {}
+
+ @Override
+ public OptionOverride convert(String input) throws OptionsParsingException {
+ int colonPos = input.indexOf(':');
+ int assignmentPos = input.indexOf('=');
+
+ if (colonPos < 0) {
+ throw new OptionsParsingException(ERROR_MESSAGE);
+ }
+
+ if (assignmentPos <= colonPos + 1) {
+ throw new OptionsParsingException(ERROR_MESSAGE);
+ }
+
+ int blazeRc;
+ try {
+ blazeRc = Integer.valueOf(input.substring(0, colonPos));
+ } catch (NumberFormatException e) {
+ throw new OptionsParsingException(ERROR_MESSAGE);
+ }
+
+ if (blazeRc < 0) {
+ throw new OptionsParsingException(ERROR_MESSAGE);
+ }
+
+ String command = input.substring(colonPos + 1, assignmentPos);
+ String option = input.substring(assignmentPos + 1);
+
+ return new OptionOverride(blazeRc, command, option);
+ }
+
+ @Override
+ public String getTypeDescription() {
+ return "blazerc option override";
+ }
+ }
+
+
+ @Option(name = "config",
+ defaultValue = "",
+ category = "misc",
+ allowMultiple = true,
+ help = "Selects additional config sections from the rc files; for every <command>, it "
+ + "also pulls in the options from <command>:<config> if such a section exists. "
+ + "Note that it is currently only possible to provide these options on the "
+ + "command line, not in the rc files. The config sections and flag combinations "
+ + "they are equivalent to are located in the tools/*.blazerc config files.")
+ public List<String> configs;
+
+ @Option(name = "logging",
+ defaultValue = "3", // Level.INFO
+ category = "verbosity",
+ converter = Converters.LogLevelConverter.class,
+ help = "The logging level.")
+ public Level verbosity;
+
+ @Option(name = "client_env",
+ defaultValue = "",
+ category = "hidden",
+ converter = Converters.AssignmentConverter.class,
+ allowMultiple = true,
+ help = "A system-generated parameter which specifies the client's environment")
+ public List<Map.Entry<String, String>> clientEnv;
+
+ @Option(name = "ignore_client_env",
+ defaultValue = "false",
+ category = "hidden",
+ help = "If true, ignore the '--client_env' flag, and use the JVM environment instead")
+ public boolean ignoreClientEnv;
+
+ @Option(name = "client_cwd",
+ defaultValue = "",
+ category = "hidden",
+ converter = OptionsUtils.PathFragmentConverter.class,
+ help = "A system-generated parameter which specifies the client's working directory")
+ public PathFragment clientCwd;
+
+ @Option(name = "announce_rc",
+ defaultValue = "false",
+ category = "verbosity",
+ help = "Whether to announce rc options.")
+ public boolean announceRcOptions;
+
+ /**
+ * These are the actual default overrides.
+ * Each value is a pair of (command name, value).
+ *
+ * For example: "--default_override=build=--cpu=piii"
+ */
+ @Option(name = "default_override",
+ defaultValue = "",
+ allowMultiple = true,
+ category = "hidden",
+ converter = OptionOverrideConverter.class,
+ help = "")
+ public List<OptionOverride> optionsOverrides;
+
+ /**
+ * This is the filename that the Blaze client parsed.
+ */
+ @Option(name = "rc_source",
+ defaultValue = "",
+ allowMultiple = true,
+ category = "hidden",
+ help = "")
+ public List<String> rcSource;
+
+ @Option(name = "always_profile_slow_operations",
+ defaultValue = "true",
+ category = "undocumented",
+ help = "Whether profiling slow operations is always turned on")
+ public boolean alwaysProfileSlowOperations;
+
+ @Option(name = "profile",
+ defaultValue = "null",
+ category = "misc",
+ converter = OptionsUtils.PathFragmentConverter.class,
+ help = "If set, profile Blaze and write data to the specified "
+ + "file. Use blaze analyze-profile to analyze the profile.")
+ public PathFragment profilePath;
+
+ @Option(name = "record_full_profiler_data",
+ defaultValue = "false",
+ category = "undocumented",
+ help = "By default, Blaze profiler will record only aggregated data for fast but numerous "
+ + "events (such as statting the file). If this option is enabled, profiler will record "
+ + "each event - resulting in more precise profiling data but LARGE performance "
+ + "hit. Option only has effect if --profile used as well.")
+ public boolean recordFullProfilerData;
+
+ @Option(name = "memory_profile",
+ defaultValue = "null",
+ category = "undocumented",
+ converter = OptionsUtils.PathFragmentConverter.class,
+ help = "If set, write memory usage data to the specified "
+ + "file at phase ends.")
+ public PathFragment memoryProfilePath;
+
+ @Option(name = "gc_watchdog",
+ defaultValue = "false",
+ category = "undocumented",
+ deprecationWarning = "Ignoring: this option is no longer supported",
+ help = "Deprecated.")
+ public boolean gcWatchdog;
+
+ @Option(name = "startup_time",
+ defaultValue = "0",
+ category = "hidden",
+ help = "The time in ms the launcher spends before sending the request to the blaze server.")
+ public long startupTime;
+
+ @Option(name = "extract_data_time",
+ defaultValue = "0",
+ category = "hidden",
+ help = "The time spend on extracting the new blaze version.")
+ public long extractDataTime;
+
+ @Option(name = "command_wait_time",
+ defaultValue = "0",
+ category = "hidden",
+ help = "The time in ms a command had to wait on a busy Blaze server process.")
+ public long waitTime;
+
+ @Option(name = "tool_tag",
+ defaultValue = "",
+ allowMultiple = true,
+ category = "misc",
+ help = "A tool name to attribute this Blaze invocation to.")
+ public List<String> toolTag;
+
+ @Option(name = "restart_reason",
+ defaultValue = "no_restart",
+ category = "hidden",
+ help = "The reason for the server restart.")
+ public String restartReason;
+
+ @Option(name = "binary_path",
+ defaultValue = "",
+ category = "hidden",
+ help = "The absolute path of the blaze binary.")
+ public String binaryPath;
+
+ @Option(name = "experimental_allow_project_files",
+ defaultValue = "false",
+ category = "hidden",
+ help = "Enable processing of +<file> parameters.")
+ public boolean allowProjectFiles;
+}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/CriticalPathComputer.java b/src/main/java/com/google/devtools/build/lib/runtime/CriticalPathComputer.java
new file mode 100644
index 0000000..2546492
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/CriticalPathComputer.java
@@ -0,0 +1,231 @@
+// Copyright 2014 Google Inc. 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.runtime;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Maps;
+import com.google.common.eventbus.Subscribe;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.actions.ActionCompletionEvent;
+import com.google.devtools.build.lib.actions.ActionMetadata;
+import com.google.devtools.build.lib.actions.ActionMiddlemanEvent;
+import com.google.devtools.build.lib.actions.ActionStartedEvent;
+import com.google.devtools.build.lib.actions.Actions;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.CachedActionEvent;
+import com.google.devtools.build.lib.util.Clock;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.PriorityQueue;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.TimeUnit;
+
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * Computes the critical path in the action graph based on events published to the event bus.
+ *
+ * <p>After instantiation, this object needs to be registered on the event bus to work.
+ */
+@ThreadSafe
+public abstract class CriticalPathComputer<C extends AbstractCriticalPathComponent<C>,
+ A extends AggregatedCriticalPath<C>> {
+
+ /** Number of top actions to record. */
+ static final int SLOWEST_COMPONENTS_SIZE = 30;
+ // outputArtifactToComponent is accessed from multiple event handlers.
+ protected final ConcurrentMap<Artifact, C> outputArtifactToComponent = Maps.newConcurrentMap();
+
+ /** Maximum critical path found. */
+ private C maxCriticalPath;
+ private final Clock clock;
+
+ /**
+ * The list of slowest individual components, ignoring the time to build dependencies.
+ *
+ * <p>This data is a useful metric when running non highly incremental builds, where multiple
+ * tasks could run un parallel and critical path would only record the longest path.
+ */
+ private final PriorityQueue<C> slowestComponents = new PriorityQueue<>(SLOWEST_COMPONENTS_SIZE,
+ new Comparator<C>() {
+ @Override
+ public int compare(C o1, C o2) {
+ return Long.compare(o1.getActionWallTime(), o2.getActionWallTime());
+ }
+ }
+ );
+
+ private final Object lock = new Object();
+
+ protected CriticalPathComputer(Clock clock) {
+ this.clock = clock;
+ maxCriticalPath = null;
+ }
+
+ /**
+ * Creates a critical path component for an action.
+ * @param action the action for the critical path component
+ * @param startTimeMillis time when the action started to run
+ */
+ protected abstract C createComponent(Action action, long startTimeMillis);
+
+ /**
+ * Return the critical path stats for the current command execution.
+ *
+ * <p>This method allows us to calculate lazily the aggregate statistics of the critical path,
+ * avoiding the memory and cpu penalty for doing it for all the actions executed.
+ */
+ public abstract A aggregate();
+
+ /**
+ * Record an action that has started to run.
+ *
+ * @param event information about the started action
+ */
+ @Subscribe
+ public void actionStarted(ActionStartedEvent event) {
+ Action action = event.getAction();
+ C component = createComponent(action, TimeUnit.NANOSECONDS.toMillis(event.getNanoTimeStart()));
+ for (Artifact output : action.getOutputs()) {
+ C old = outputArtifactToComponent.put(output, component);
+ Preconditions.checkState(old == null, "Duplicate output artifact found. This could happen"
+ + " if a previous event registered the action %s. Artifact: %s", action, output);
+ }
+ }
+
+ /**
+ * Record a middleman action execution. Even if middleman are almost instant, we record them
+ * because they depend on other actions and we need them for constructing the critical path.
+ *
+ * <p>For some rules with incorrect configuration transitions we might get notified several times
+ * for the same middleman. This should only happen if the actions are shared.
+ */
+ @Subscribe
+ public void middlemanAction(ActionMiddlemanEvent event) {
+ Action action = event.getAction();
+ C component = createComponent(action, TimeUnit.NANOSECONDS.toMillis(event.getNanoTimeStart()));
+ boolean duplicate = false;
+ for (Artifact output : action.getOutputs()) {
+ C old = outputArtifactToComponent.putIfAbsent(output, component);
+ if (old != null) {
+ if (!Actions.canBeShared(action, old.getAction())) {
+ throw new IllegalStateException("Duplicate output artifact found for middleman."
+ + "This could happen if a previous event registered the action.\n"
+ + "Old action: " + old.getAction() + "\n\n"
+ + "New action: " + action + "\n\n"
+ + "Artifact: " + output + "\n");
+ }
+ duplicate = true;
+ }
+ }
+ if (!duplicate) {
+ finalizeActionStat(action, component);
+ }
+ }
+
+ /**
+ * Record an action that was not executed because it was in the (disk) cache. This is needed so
+ * that we can calculate correctly the dependencies tree if we have some cached actions in the
+ * middle of the critical path.
+ */
+ @Subscribe
+ public void actionCached(CachedActionEvent event) {
+ Action action = event.getAction();
+ C component = createComponent(action, TimeUnit.NANOSECONDS.toMillis(event.getNanoTimeStart()));
+ for (Artifact output : action.getOutputs()) {
+ outputArtifactToComponent.put(output, component);
+ }
+ finalizeActionStat(action, component);
+ }
+
+ /**
+ * Records the elapsed time stats for the action. For each input artifact, it finds the real
+ * dependent artifacts and records the critical path stats.
+ */
+ @Subscribe
+ public void actionComplete(ActionCompletionEvent event) {
+ ActionMetadata action = event.getActionMetadata();
+ C component = Preconditions.checkNotNull(
+ outputArtifactToComponent.get(action.getPrimaryOutput()));
+ finalizeActionStat(action, component);
+ }
+
+ /** Maximum critical path component found during the build. */
+ protected C getMaxCriticalPath() {
+ synchronized (lock) {
+ return maxCriticalPath;
+ }
+ }
+
+ /**
+ * The list of slowest individual components, ignoring the time to build dependencies.
+ */
+ public ImmutableList<C> getSlowestComponents() {
+ ArrayList<C> list;
+ synchronized (lock) {
+ list = new ArrayList<>(slowestComponents);
+ Collections.sort(list, slowestComponents.comparator());
+ }
+ return ImmutableList.copyOf(list).reverse();
+ }
+
+ private void finalizeActionStat(ActionMetadata action, C component) {
+ component.setFinishTimeMillis(getTime());
+ for (Artifact input : action.getInputs()) {
+ addArtifactDependency(component, input);
+ }
+
+ synchronized (lock) {
+ if (isBiggestCriticalPath(component)) {
+ maxCriticalPath = component;
+ }
+
+ if (slowestComponents.size() == SLOWEST_COMPONENTS_SIZE) {
+ // The new component is faster than any of the slow components, avoid insertion.
+ if (slowestComponents.peek().getActionWallTime() >= component.getActionWallTime()) {
+ return;
+ }
+ // Remove the head element to make space (The fastest component in the queue).
+ slowestComponents.remove();
+ }
+ slowestComponents.add(component);
+ }
+ }
+
+ private long getTime() {
+ return TimeUnit.NANOSECONDS.toMillis(clock.nanoTime());
+ }
+
+ private boolean isBiggestCriticalPath(C newCriticalPath) {
+ synchronized (lock) {
+ return maxCriticalPath == null
+ || maxCriticalPath.getAggregatedWallTime() < newCriticalPath.getAggregatedWallTime();
+ }
+ }
+
+ /**
+ * If "input" is a generated artifact, link its critical path to the one we're building.
+ */
+ private void addArtifactDependency(C actionStats, Artifact input) {
+ C depComponent = outputArtifactToComponent.get(input);
+ if (depComponent != null) {
+ actionStats.addDepInfo(depComponent);
+ }
+ }
+}
+
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/EventHandlerPreconditions.java b/src/main/java/com/google/devtools/build/lib/runtime/EventHandlerPreconditions.java
new file mode 100644
index 0000000..f4ef8e3
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/EventHandlerPreconditions.java
@@ -0,0 +1,143 @@
+// Copyright 2014 Google Inc. 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.runtime;
+
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.events.ExceptionListener;
+import com.google.devtools.build.lib.util.LoggingUtil;
+
+import java.util.logging.Level;
+
+/**
+ * Reports precondition failures from within an event handler.
+ * Necessary because the EventBus silently ignores exceptions thrown from within a handler.
+ * This class logs the exceptions and creates some noise when a precondition check fails.
+ */
+public class EventHandlerPreconditions {
+
+ private final ExceptionListener listener;
+
+ /**
+ * Creates a new precondition helper which outputs errors to the given reporter.
+ */
+ public EventHandlerPreconditions(ExceptionListener listener) {
+ this.listener = listener;
+ }
+
+ /**
+ * Verifies that the given condition (a check on an argument) is true,
+ * throwing an IllegalArgumentException if not.
+ *
+ * @param condition a condition to check for truth.
+ * @throws IllegalArgumentException if the condition is false.
+ */
+ @SuppressWarnings("unused")
+ public void checkArgument(boolean condition) {
+ checkArgument(condition, null);
+ }
+
+ /**
+ * Verifies that the given condition (a check on an argument) is true,
+ * throwing an IllegalArgumentException with the given message if not.
+ *
+ * @param condition a condition to check for truth.
+ * @param message extra information to output if the condition is false.
+ * @throws IllegalArgumentException if the condition is false.
+ */
+ public void checkArgument(boolean condition, String message) {
+ try {
+ Preconditions.checkArgument(condition, message);
+ } catch (IllegalArgumentException iae) {
+ String error = "Event handler argument check failed";
+ LoggingUtil.logToRemote(Level.SEVERE, error, iae);
+ listener.error(null, error, iae);
+ throw iae; // Still terminate the handler.
+ }
+ }
+
+ /**
+ * Verifies that the given condition (a check against the program's current state) is true,
+ * throwing an IllegalStateException if not.
+ *
+ * @param condition a condition to check for truth.
+ * @throws IllegalStateException if the condition is false.
+ */
+ public void checkState(boolean condition) {
+ checkState(condition, null);
+ }
+
+ /**
+ * Verifies that the given condition (a check against the program's current state) is true,
+ * throwing an IllegalStateException with the given message if not.
+ *
+ * @param condition a condition to check for truth.
+ * @param message extra information to output if the condition is false.
+ * @throws IllegalStateException if the condition is false.
+ */
+ public void checkState(boolean condition, String message) {
+ try {
+ Preconditions.checkState(condition, message);
+ } catch (IllegalStateException ise) {
+ String error = "Event handler state check failed";
+ LoggingUtil.logToRemote(Level.SEVERE, error, ise);
+ listener.error(null, error, ise);
+ throw ise; // Still terminate the handler.
+ }
+ }
+
+ /**
+ * Fails with an IllegalStateException when invoked.
+ */
+ public void fail(String message) {
+ String error = "Event handler failed: " + message;
+ IllegalStateException ise = new IllegalStateException(message);
+ LoggingUtil.logToRemote(Level.SEVERE, error, ise);
+ listener.error(null, error, ise);
+ throw ise;
+ }
+
+ /**
+ * Verifies that the given argument is not null, throwing a NullPointerException if it is null.
+ * Returns the original argument or throws.
+ *
+ * @param object an object to test for null.
+ * @return the reference which was checked.
+ * @throws NullPointerException if the object is null.
+ */
+ public <T> T checkNotNull(T object) {
+ return checkNotNull(object, null);
+ }
+
+ /**
+ * Verifies that the given argument is not null, throwing a
+ * NullPointerException with the given message if it is null.
+ * Returns the original argument or throws.
+ *
+ * @param object an object to test for null.
+ * @param message extra information to output if the object is null.
+ * @return the reference which was checked.
+ * @throws NullPointerException if the object is null.
+ */
+ public <T> T checkNotNull(T object, String message) {
+ try {
+ return Preconditions.checkNotNull(object, message);
+ } catch (NullPointerException npe) {
+ String error = "Event handler not-null check failed";
+ LoggingUtil.logToRemote(Level.SEVERE, error, npe);
+ listener.error(null, error, npe);
+ throw npe;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/FancyTerminalEventHandler.java b/src/main/java/com/google/devtools/build/lib/runtime/FancyTerminalEventHandler.java
new file mode 100644
index 0000000..e55ad2f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/FancyTerminalEventHandler.java
@@ -0,0 +1,355 @@
+// Copyright 2014 Google Inc. 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.runtime;
+
+import com.google.common.base.Splitter;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.util.Pair;
+import com.google.devtools.build.lib.util.io.AnsiTerminal;
+import com.google.devtools.build.lib.util.io.OutErr;
+
+import java.io.IOException;
+import java.util.Iterator;
+import java.util.logging.Logger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * An event handler for ANSI terminals which uses control characters to
+ * provide eye-candy, reduce scrolling, and generally improve usability
+ * for users running directly from the shell.
+ *
+ * <p/>
+ * This event handler differs from a normal terminal because it only adds
+ * control characters to stderr, not stdout. All blaze status feedback
+ * is sent to stderr, so adding control characters just to that stream gives
+ * the benefits described above without modifying the normal output stream.
+ * For commands like build that don't generate stdout output this doesn't
+ * matter, but for commands like query and ide_build_info, inserting these
+ * control characters in stdout invalidated their output.
+ *
+ * <p/>
+ * The underlying streams may be either line-bufferred or unbuffered.
+ * Normally each event will write out a sequence of output to a single
+ * stream, and will end with a newline, which ensures a flush.
+ * But care is required when outputting incomplete lines, or when mixing
+ * output between the two different streams (stdout and stderr):
+ * it may be necessary to explicitly flush the output in those cases.
+ * However, we also don't want to flush too often; that can lead to
+ * a choppy UI experience.
+ */
+public class FancyTerminalEventHandler extends BlazeCommandEventHandler {
+ private static Logger LOG = Logger.getLogger(FancyTerminalEventHandler.class.getName());
+ private static final Pattern progressPattern = Pattern.compile(
+ // Match strings that look like they start with progress info:
+ // [42%] Compiling base/base.cc
+ // [1,442 / 23,476] Compiling base/base.cc
+ "^\\[(?:(?:\\d\\d?\\d?%)|(?:[\\d+,]+ / [\\d,]+))\\] ");
+ private static final Splitter LINEBREAK_SPLITTER = Splitter.on('\n');
+
+ private final AnsiTerminal terminal;
+
+ private final boolean useColor;
+ private final boolean useCursorControls;
+ private final boolean progressInTermTitle;
+ public final int terminalWidth;
+
+ private boolean terminalClosed = false;
+ private boolean previousLineErasable = false;
+ private int numLinesPreviousErasable = 0;
+
+ public FancyTerminalEventHandler(OutErr outErr, BlazeCommandEventHandler.Options options) {
+ super(outErr, options);
+ this.terminal = new AnsiTerminal(outErr.getErrorStream());
+ this.terminalWidth = (options.terminalColumns > 0 ? options.terminalColumns : 80);
+ useColor = options.useColor();
+ useCursorControls = options.useCursorControl();
+ progressInTermTitle = options.progressInTermTitle;
+ }
+
+ @Override
+ public void handle(Event event) {
+ if (terminalClosed) {
+ return;
+ }
+ if (!eventMask.contains(event.getKind())) {
+ return;
+ }
+
+ try {
+ boolean previousLineErased = false;
+ if (previousLineErasable) {
+ previousLineErased = maybeOverwritePreviousMessage();
+ }
+ switch (event.getKind()) {
+ case PROGRESS:
+ case START:
+ {
+ String message = event.getMessage();
+ Pair<String,String> progressPair = matchProgress(message);
+ if (progressPair != null) {
+ progress(progressPair.getFirst(), progressPair.getSecond());
+ } else {
+ progress("INFO: ", message);
+ }
+ break;
+ }
+ case FINISH:
+ {
+ String message = event.getMessage();
+ Pair<String,String> progressPair = matchProgress(message);
+ if (progressPair != null) {
+ String percentage = progressPair.getFirst();
+ String rest = progressPair.getSecond();
+ progress(percentage, rest + " DONE");
+ } else {
+ progress("INFO: ", message + " DONE");
+ }
+ break;
+ }
+ case PASS:
+ progress("PASS: ", event.getMessage());
+ break;
+ case INFO:
+ info(event);
+ break;
+ case ERROR:
+ case FAIL:
+ case TIMEOUT:
+ // For errors, scroll the message, so it appears above the status
+ // line, and highlight the word "ERROR" or "FAIL" in boldface red.
+ errorOrFail(event);
+ break;
+ case WARNING:
+ // For warnings, highlight the word "Warning" in boldface magenta,
+ // and scroll it.
+ warning(event);
+ break;
+ case SUBCOMMAND:
+ subcmd(event);
+ break;
+ case STDOUT:
+ if (previousLineErased) {
+ terminal.flush();
+ }
+ previousLineErasable = false;
+ super.handle(event);
+ // We don't need to flush stdout here, because
+ // super.handle(event) will take care of that.
+ break;
+ case STDERR:
+ putOutput(event);
+ break;
+ default:
+ // Ignore all other event types.
+ break;
+ }
+ } catch (IOException e) {
+ // The terminal shouldn't have IO errors, unless the shell is killed, which
+ // should also kill the blaze client. So this isn't something that should
+ // occur here; it will show up in the client/server interface as a broken
+ // pipe.
+ LOG.warning("Terminal was closed during build: " + e);
+ terminalClosed = true;
+ }
+ }
+
+ /**
+ * Displays a progress message that may be erased by subsequent messages.
+ *
+ * @param prefix a short string such as "[99%] " or "INFO: ", which will be highlighted
+ * @param rest the remainder of the message; may be multiple lines
+ */
+ private void progress(String prefix, String rest) throws IOException {
+ previousLineErasable = true;
+
+ if (progressInTermTitle) {
+ int newlinePos = rest.indexOf('\n');
+ if (newlinePos == -1) {
+ terminal.setTitle(prefix + rest);
+ } else {
+ terminal.setTitle(prefix + rest.substring(0, newlinePos));
+ }
+ }
+
+ if (useColor) {
+ terminal.textGreen();
+ }
+ int prefixWidth = prefix.length();
+ terminal.writeString(prefix);
+ terminal.resetTerminal();
+ if (showTimestamp) {
+ String timestamp = timestamp();
+ prefixWidth += timestamp.length();
+ terminal.writeString(timestamp);
+ }
+ int numLines = 0;
+ Iterator<String> lines = LINEBREAK_SPLITTER.split(rest).iterator();
+ String firstLine = lines.next();
+ terminal.writeString(firstLine);
+ // Subtract one, because when the line length is the same as the terminal
+ // width, the terminal doesn't line-advance, so we don't want to erase
+ // two lines.
+ numLines += (prefixWidth + firstLine.length() - 1) / terminalWidth + 1;
+ crlf();
+ while (lines.hasNext()) {
+ String line = lines.next();
+ terminal.writeString(line);
+ crlf();
+ numLines += (line.length() - 1) / terminalWidth + 1;
+ }
+ numLinesPreviousErasable = numLines;
+ }
+
+ /**
+ * Try to match a message against the "progress message" pattern. If it
+ * matches, return the progress percentage, and the rest of the message.
+ * @param message the message to match
+ * @return a pair containing the progress percentage, and the rest of the
+ * progress message, or null if the message isn't a progress message.
+ */
+ private Pair<String,String> matchProgress(String message) {
+ Matcher m = progressPattern.matcher(message);
+ if (m.find()) {
+ return Pair.of(message.substring(0, m.end()), message.substring(m.end()));
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Send the terminal controls that will put the cursor on the beginning
+ * of the same line if cursor control is on, or the next line if not.
+ * @returns True if it did any output; if so, caller is responsible for
+ * flushing the terminal if needed.
+ */
+ private boolean maybeOverwritePreviousMessage() throws IOException {
+ if (useCursorControls && numLinesPreviousErasable != 0) {
+ for (int i = 0; i < numLinesPreviousErasable; i++) {
+ terminal.cr();
+ terminal.cursorUp(1);
+ terminal.clearLine();
+ }
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ private void errorOrFail(Event event) throws IOException {
+ previousLineErasable = false;
+ if (useColor) {
+ terminal.textRed();
+ terminal.textBold();
+ }
+ terminal.writeString(event.getKind().toString() + ": ");
+ if (useColor) {
+ terminal.resetTerminal();
+ }
+ writeTimestampAndLocation(event);
+ terminal.writeString(event.getMessage());
+ terminal.writeString(".");
+ crlf();
+ }
+
+ private void warning(Event warning) throws IOException {
+ previousLineErasable = false;
+ if (useColor) {
+ terminal.textMagenta();
+ }
+ terminal.writeString("WARNING: ");
+ terminal.resetTerminal();
+ writeTimestampAndLocation(warning);
+ terminal.writeString(warning.getMessage());
+ terminal.writeString(".");
+ crlf();
+ }
+
+ private void info(Event event) throws IOException {
+ previousLineErasable = false;
+ if (useColor) {
+ terminal.textGreen();
+ }
+ terminal.writeString(event.getKind().toString() + ": ");
+ terminal.resetTerminal();
+ writeTimestampAndLocation(event);
+ terminal.writeString(event.getMessage());
+ // No period; info messages often end in '...'.
+ crlf();
+ }
+
+ private void subcmd(Event subcmd) throws IOException {
+ previousLineErasable = false;
+ if (useColor) {
+ terminal.textBlue();
+ }
+ terminal.writeString(">>>>> ");
+ terminal.resetTerminal();
+ writeTimestampAndLocation(subcmd);
+ terminal.writeString(subcmd.getMessage());
+ crlf();
+ }
+
+ /* Handle STDERR events. */
+ private void putOutput(Event event) throws IOException {
+ previousLineErasable = false;
+ terminal.writeBytes(event.getMessageBytes());
+/*
+ * The following code doesn't work because buildtool.TerminalTestNotifier
+ * writes ANSI-formatted text via this mechanism, one character at a time,
+ * and if we try to insert additional ANSI sequences in between the characters
+ * of another ANSI escape sequence, we screw things up. (?)
+ * TODO(bazel-team): (2009) fix this. TerminalTestNotifier should go via the Reporter
+ * rather than via an AnsiTerminalWriter.
+ */
+// terminal.resetTerminal();
+// writeTimestampAndLocation(event);
+// if (useColor) {
+// terminal.textNormal();
+// }
+// terminal.writeBytes(event.getMessageBytes());
+// terminal.resetTerminal();
+ }
+
+ /**
+ * Add a carriage return, shifting to the next line on the terminal, while
+ * guaranteeing that the terminal control codes don't cause any strange
+ * effects. Without the CR before the "\n", the "\n" can cause a line-break
+ * moving text to the next line, where the new message will be generated.
+ * Emitting a "CR" before means that the actual terminal controls generated
+ * here are CR+CR+LF; the double-CR resets the terminal line state, which
+ * prevents the potentially ugly formatting issue.
+ */
+ private void crlf() throws IOException {
+ terminal.cr();
+ terminal.writeString("\n");
+ }
+
+ private void writeTimestampAndLocation(Event event) throws IOException {
+ if (showTimestamp) {
+ terminal.writeString(timestamp());
+ }
+ if (event.getLocation() != null) {
+ terminal.writeString(event.getLocation() + ": ");
+ }
+ }
+
+ public void resetTerminal() {
+ try {
+ terminal.resetTerminal();
+ } catch (IOException e) {
+ LOG.warning("IO Error writing to user terminal: " + e);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/GCStatsRecorder.java b/src/main/java/com/google/devtools/build/lib/runtime/GCStatsRecorder.java
new file mode 100644
index 0000000..48e366d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/GCStatsRecorder.java
@@ -0,0 +1,85 @@
+// Copyright 2014 Google Inc. 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.runtime;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
+
+import java.lang.management.GarbageCollectorMXBean;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Record GC stats for a build.
+ */
+public class GCStatsRecorder {
+
+ private final Iterable<GarbageCollectorMXBean> mxBeans;
+ private final ImmutableMap<String, GCStat> initialData;
+
+ public GCStatsRecorder(Iterable<GarbageCollectorMXBean> mxBeans) {
+ this.mxBeans = mxBeans;
+ ImmutableMap.Builder<String, GCStat> initialData = ImmutableMap.builder();
+ for (GarbageCollectorMXBean mxBean : mxBeans) {
+ String name = mxBean.getName();
+ initialData.put(name, new GCStat(name, mxBean.getCollectionCount(),
+ mxBean.getCollectionTime()));
+ }
+ this.initialData = initialData.build();
+ }
+
+ public Iterable<GCStat> getCurrentGcStats() {
+ List<GCStat> stats = new ArrayList<>();
+ for (GarbageCollectorMXBean mxBean : mxBeans) {
+ String name = mxBean.getName();
+ GCStat initStat = Preconditions.checkNotNull(initialData.get(name));
+ stats.add(new GCStat(name,
+ mxBean.getCollectionCount() - initStat.getNumCollections(),
+ mxBean.getCollectionTime() - initStat.getTotalTimeInMs()));
+ }
+ return stats;
+ }
+
+ /** Represents the garbage collections statistics for one collector (For example CMS). */
+ public static class GCStat {
+
+ private final String name;
+ private final long numCollections;
+ private final long totalTimeInMs;
+
+ public GCStat(String name, long numCollections, long totalTimeInMs) {
+ this.name = name;
+ this.numCollections = numCollections;
+ this.totalTimeInMs = totalTimeInMs;
+ }
+
+ /** Name of the Collector. For example CMS. */
+ public String getName() { return name; }
+
+ /** Number of invocations for a build. */
+ public long getNumCollections() { return numCollections; }
+
+ /**
+ * Total time spend in GC for the collector. Note that the time does need to be exclusive (aka a
+ * stop-the-world GC).
+ */
+ public long getTotalTimeInMs() { return totalTimeInMs; }
+
+ @Override
+ public String toString() {
+ return "GC time for '" + name + "' collector: " + numCollections
+ + " collections using " + totalTimeInMs + "ms";
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/GotOptionsEvent.java b/src/main/java/com/google/devtools/build/lib/runtime/GotOptionsEvent.java
new file mode 100644
index 0000000..622d112
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/GotOptionsEvent.java
@@ -0,0 +1,51 @@
+// Copyright 2014 Google Inc. 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.runtime;
+
+import com.google.devtools.common.options.OptionsProvider;
+
+/**
+ * An event in which the command line options
+ * are discovered.
+ */
+public class GotOptionsEvent {
+
+ private final OptionsProvider startupOptions;
+ private final OptionsProvider options;
+
+ /**
+ * Construct the options event.
+ *
+ * @param startupOptions the parsed startup options
+ * @param options the parsed options
+ */
+ public GotOptionsEvent(OptionsProvider startupOptions, OptionsProvider options) {
+ this.startupOptions = startupOptions;
+ this.options = options;
+ }
+
+ /**
+ * @return the parsed startup options
+ */
+ public OptionsProvider getStartupOptions() {
+ return startupOptions;
+ }
+
+ /**
+ * @return the parsed options.
+ */
+ public OptionsProvider getOptions() {
+ return options;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/HostJvmStartupOptions.java b/src/main/java/com/google/devtools/build/lib/runtime/HostJvmStartupOptions.java
new file mode 100644
index 0000000..305c048
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/HostJvmStartupOptions.java
@@ -0,0 +1,54 @@
+// Copyright 2014 Google Inc. 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.runtime;
+
+import com.google.devtools.common.options.Option;
+import com.google.devtools.common.options.OptionsBase;
+
+/**
+ * Options that will be evaluated by the blaze client startup code only.
+ *
+ * The only reason we have this interface is that we'd like to print a nice
+ * help page for the client startup options. These options do not affect the
+ * server's behavior in any way.
+ */
+public class HostJvmStartupOptions extends OptionsBase {
+
+ @Option(name = "host_jvm_args",
+ defaultValue = "", // NOTE: purely decorative! See BlazeServerStartupOptions.
+ category = "host jvm startup",
+ help = "Flags to pass to the JVM executing Blaze. Note: Blaze " +
+ "will ignore this option unless you are starting a new " +
+ "instance. See also 'blaze help shutdown'.")
+ public String hostJvmArgs;
+
+ @Option(name = "host_jvm_profile",
+ defaultValue = "", // NOTE: purely decorative! See BlazeServerStartupOptions.
+ category = "host jvm startup",
+ help = "Run the JVM executing Blaze in the given profiler. " +
+ "Blaze will search for hardcoded paths based on the " +
+ "profiler. Note: Blaze will ignore this option unless you " +
+ "are starting a new instance. See also 'blaze help shutdown'.")
+ public String hostJvmProfile;
+
+ @Option(name = "host_jvm_debug",
+ defaultValue = "false", // NOTE: purely decorative! See BlazeServerStartupOptions.
+ category = "host jvm startup",
+ help = "Run the JVM executing Blaze so that it listens for a " +
+ "connection from a JDWP-compliant debugger. Note: Blaze " +
+ "will ignore this option unless you are starting a new " +
+ "instance. See also 'blaze help shutdown'.")
+ public boolean hostJvmDebug;
+}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/ProjectFile.java b/src/main/java/com/google/devtools/build/lib/runtime/ProjectFile.java
new file mode 100644
index 0000000..56747d8
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/ProjectFile.java
@@ -0,0 +1,59 @@
+// Copyright 2014 Google Inc. 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.runtime;
+
+import com.google.devtools.build.lib.util.AbruptExitException;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.List;
+
+/**
+ * A file that describes a project - for large source trees that are worked on by multiple
+ * independent teams, it is useful to have a larger unit than a package which combines a set of
+ * target patterns and a set of corresponding options.
+ */
+public interface ProjectFile {
+
+ /**
+ * A provider for a project file - we generally expect the provider to cache parsed files
+ * internally and return a cached version if it can ascertain that that is still correct.
+ *
+ * <p>Note in particular that packages may be moved between different package path entries, which
+ * should lead to cache invalidation.
+ */
+ public interface Provider {
+ /**
+ * Returns an (optionally cached) project file instance. If there is no such file, or if the
+ * file cannot be parsed, then it throws an exception.
+ */
+ ProjectFile getProjectFile(List<Path> packagePath, PathFragment path)
+ throws AbruptExitException;
+ }
+
+ /**
+ * A string name of the project file that is reported to the user. It should be in such a format
+ * that passing it back in on the command line works.
+ */
+ String getName();
+
+ /**
+ * A list of strings that are parsed into the options for the command.
+ *
+ * @param command An action from the command line, e.g. "build" or "test".
+ * @throws UnsupportedOperationException if an unknown command is passed.
+ */
+ List<String> getCommandLineFor(String command);
+}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/RateLimitingEventHandler.java b/src/main/java/com/google/devtools/build/lib/runtime/RateLimitingEventHandler.java
new file mode 100644
index 0000000..5e90f2e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/RateLimitingEventHandler.java
@@ -0,0 +1,71 @@
+// Copyright 2014 Google Inc. 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.runtime;
+
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.util.BlazeClock;
+import com.google.devtools.build.lib.util.Clock;
+
+/**
+ * An event handler that rate limits events.
+ */
+public class RateLimitingEventHandler implements EventHandler {
+
+ private final EventHandler outputHandler;
+ private final double intervalMillis;
+ private final Clock clock;
+ private long lastEventMillis = -1;
+
+ /**
+ * Creates a new Event handler that rate limits the events of type PROGRESS
+ * to one per event "rateLimitation" seconds. Events that arrive too quickly are dropped;
+ * all others are are forwarded to the handler "delegateTo".
+ *
+ * @param delegateTo The event handler that ultimately handles the events
+ * @param rateLimitation The minimum number of seconds between events that will be forwarded
+ * to the delegateTo-handler.
+ * If less than zero (or NaN), all events will be forwarded.
+ */
+ public static EventHandler create(EventHandler delegateTo, double rateLimitation) {
+ if (rateLimitation < 0.0 || Double.isNaN(rateLimitation)) {
+ return delegateTo;
+ }
+ return new RateLimitingEventHandler(delegateTo, rateLimitation);
+ }
+
+ private RateLimitingEventHandler(EventHandler delegateTo, double rateLimitation) {
+ clock = BlazeClock.instance();
+ outputHandler = delegateTo;
+ this.intervalMillis = rateLimitation * 1000;
+ }
+
+ @Override
+ public void handle(Event event) {
+ switch (event.getKind()) {
+ case PROGRESS:
+ case START:
+ case FINISH:
+ long currentTime = clock.currentTimeMillis();
+ if (lastEventMillis + intervalMillis <= currentTime) {
+ lastEventMillis = currentTime;
+ outputHandler.handle(event);
+ }
+ break;
+ default:
+ outputHandler.handle(event);
+ break;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/SimpleCriticalPathComponent.java b/src/main/java/com/google/devtools/build/lib/runtime/SimpleCriticalPathComponent.java
new file mode 100644
index 0000000..b8d5d45
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/SimpleCriticalPathComponent.java
@@ -0,0 +1,26 @@
+// Copyright 2014 Google Inc. 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.runtime;
+
+import com.google.devtools.build.lib.actions.Action;
+
+/**
+ * This class records the critical path for the graph of actions executed.
+ */
+public class SimpleCriticalPathComponent
+ extends AbstractCriticalPathComponent<SimpleCriticalPathComponent> {
+
+ public SimpleCriticalPathComponent(Action action, long startTime) { super(action, startTime); }
+}
+
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/SimpleCriticalPathComputer.java b/src/main/java/com/google/devtools/build/lib/runtime/SimpleCriticalPathComputer.java
new file mode 100644
index 0000000..65a9c95
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/SimpleCriticalPathComputer.java
@@ -0,0 +1,58 @@
+// Copyright 2014 Google Inc. 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.runtime;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.util.Clock;
+
+/**
+ * Computes the critical path during a build.
+ */
+public class SimpleCriticalPathComputer
+ extends CriticalPathComputer<SimpleCriticalPathComponent,
+ AggregatedCriticalPath<SimpleCriticalPathComponent>> {
+
+ public SimpleCriticalPathComputer(Clock clock) {
+ super(clock);
+ }
+
+ @Override
+ public SimpleCriticalPathComponent createComponent(Action action, long startTimeMillis) {
+ return new SimpleCriticalPathComponent(action, startTimeMillis);
+ }
+
+ /**
+ * Return the critical path stats for the current command execution.
+ *
+ * <p>This method allow us to calculate lazily the aggregate statistics of the critical path,
+ * avoiding the memory and cpu penalty for doing it for all the actions executed.
+ */
+ @Override
+ public AggregatedCriticalPath<SimpleCriticalPathComponent> aggregate() {
+ ImmutableList.Builder<SimpleCriticalPathComponent> components = ImmutableList.builder();
+ SimpleCriticalPathComponent maxCriticalPath = getMaxCriticalPath();
+ if (maxCriticalPath == null) {
+ return new AggregatedCriticalPath<>(0, components.build());
+ }
+ SimpleCriticalPathComponent child = maxCriticalPath;
+ while (child != null) {
+ components.add(child);
+ child = child.getChild();
+ }
+ return new AggregatedCriticalPath<>(maxCriticalPath.getAggregatedWallTime(),
+ components.build());
+ }
+}
+
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/TerminalTestResultNotifier.java b/src/main/java/com/google/devtools/build/lib/runtime/TerminalTestResultNotifier.java
new file mode 100644
index 0000000..0134f55
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/TerminalTestResultNotifier.java
@@ -0,0 +1,220 @@
+// Copyright 2014 Google Inc. 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.runtime;
+
+import com.google.devtools.build.lib.exec.ExecutionOptions;
+import com.google.devtools.build.lib.rules.test.TestLogHelper;
+import com.google.devtools.build.lib.rules.test.TestResult;
+import com.google.devtools.build.lib.rules.test.TestStrategy.TestOutputFormat;
+import com.google.devtools.build.lib.rules.test.TestStrategy.TestSummaryFormat;
+import com.google.devtools.build.lib.util.StringUtil;
+import com.google.devtools.build.lib.util.io.AnsiTerminalPrinter;
+import com.google.devtools.build.lib.view.test.TestStatus.BlazeTestStatus;
+import com.google.devtools.common.options.Option;
+import com.google.devtools.common.options.OptionsBase;
+import com.google.devtools.common.options.OptionsProvider;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Prints the test results to a terminal.
+ */
+public class TerminalTestResultNotifier implements TestResultNotifier {
+ private static class TestResultStats {
+ int numberOfTargets;
+ int passCount;
+ int failedToBuildCount;
+ int failedCount;
+ int failedRemotelyCount;
+ int failedLocallyCount;
+ int noStatusCount;
+ int numberOfExecutedTargets;
+ boolean wasUnreportedWrongSize;
+ }
+
+ /**
+ * Flags specific to test summary reporting.
+ */
+ public static class TestSummaryOptions extends OptionsBase {
+ @Option(name = "verbose_test_summary",
+ defaultValue = "true",
+ category = "verbosity",
+ help = "If true, print additional information (timing, number of failed runs, etc) in the"
+ + " test summary.")
+ public boolean verboseSummary;
+
+ @Option(name = "test_verbose_timeout_warnings",
+ defaultValue = "false",
+ category = "verbosity",
+ help = "If true, print additional warnings when the actual test execution time does not " +
+ "match the timeout defined by the test (whether implied or explicit).")
+ public boolean testVerboseTimeoutWarnings;
+ }
+
+ private final AnsiTerminalPrinter printer;
+ private final OptionsProvider options;
+ private final TestSummaryOptions summaryOptions;
+
+ /**
+ * @param printer The terminal to print to
+ */
+ public TerminalTestResultNotifier(AnsiTerminalPrinter printer, OptionsProvider options) {
+ this.printer = printer;
+ this.options = options;
+ this.summaryOptions = options.getOptions(TestSummaryOptions.class);
+ }
+
+ /**
+ * Prints a test result summary that contains only failed tests.
+ */
+ private void printDetailedTestResultSummary(Set<TestSummary> summaries) {
+ for (TestSummary entry : summaries) {
+ if (entry.getStatus() != BlazeTestStatus.PASSED) {
+ TestSummaryPrinter.print(entry, printer, summaryOptions.verboseSummary, true);
+ }
+ }
+ }
+
+ /**
+ * Prints a full test result summary.
+ */
+ private void printShortSummary(Set<TestSummary> summaries, boolean showPassingTests) {
+ for (TestSummary entry : summaries) {
+ if (entry.getStatus() != BlazeTestStatus.PASSED || showPassingTests) {
+ TestSummaryPrinter.print(entry, printer, summaryOptions.verboseSummary, false);
+ }
+ }
+ }
+
+ /**
+ * Returns true iff the --check_tests_up_to_date option is enabled.
+ */
+ private boolean optionCheckTestsUpToDate() {
+ return options.getOptions(ExecutionOptions.class).testCheckUpToDate;
+ }
+
+
+ /**
+ * Prints a test summary information for all tests to the terminal.
+ *
+ * @param summaries Summary of all targets that were ran
+ * @param numberOfExecutedTargets the number of targets that were actually ran
+ */
+ @Override
+ public void notify(Set<TestSummary> summaries, int numberOfExecutedTargets) {
+ TestResultStats stats = new TestResultStats();
+ stats.numberOfTargets = summaries.size();
+ stats.numberOfExecutedTargets = numberOfExecutedTargets;
+
+ TestOutputFormat testOutput = options.getOptions(ExecutionOptions.class).testOutput;
+
+ for (TestSummary summary : summaries) {
+ if (summary.isLocalActionCached()
+ && TestLogHelper.shouldOutputTestLog(testOutput,
+ TestResult.isBlazeTestStatusPassed(summary.getStatus()))) {
+ TestSummaryPrinter.printCachedOutput(summary, testOutput, printer);
+ }
+ }
+
+ for (TestSummary summary : summaries) {
+ if (TestResult.isBlazeTestStatusPassed(summary.getStatus())) {
+ stats.passCount++;
+ } else if (summary.getStatus() == BlazeTestStatus.FAILED_TO_BUILD) {
+ stats.failedToBuildCount++;
+ } else if (summary.ranRemotely()) {
+ stats.failedRemotelyCount++;
+ } else {
+ stats.failedLocallyCount++;
+ }
+
+ if (summary.getStatus() == BlazeTestStatus.NO_STATUS) {
+ stats.noStatusCount++;
+ }
+
+ if (summary.wasUnreportedWrongSize()) {
+ stats.wasUnreportedWrongSize = true;
+ }
+ }
+
+ stats.failedCount = summaries.size() - stats.passCount;
+
+ TestSummaryFormat testSummaryFormat = options.getOptions(ExecutionOptions.class).testSummary;
+ switch (testSummaryFormat) {
+ case DETAILED:
+ printDetailedTestResultSummary(summaries);
+ break;
+
+ case SHORT:
+ printShortSummary(summaries, /*printSuccess=*/true);
+ break;
+
+ case TERSE:
+ printShortSummary(summaries, /*printSuccess=*/false);
+ break;
+
+ case NONE:
+ break;
+ }
+
+ printStats(stats);
+ }
+
+ private void addToErrorList(List<String> list, String failureDescription, int count) {
+ if (count > 0) {
+ list.add(String.format("%s%d %s %s%s",
+ AnsiTerminalPrinter.Mode.ERROR,
+ count,
+ count == 1 ? "fails" : "fail",
+ failureDescription,
+ AnsiTerminalPrinter.Mode.DEFAULT));
+ }
+ }
+
+ private void printStats(TestResultStats stats) {
+ if (!optionCheckTestsUpToDate()) {
+ List<String> results = new ArrayList<>();
+ if (stats.passCount == 1) {
+ results.add(stats.passCount + " test passes");
+ } else if (stats.passCount > 0) {
+ results.add(stats.passCount + " tests pass");
+ }
+ addToErrorList(results, "to build", stats.failedToBuildCount);
+ addToErrorList(results, "locally", stats.failedLocallyCount);
+ addToErrorList(results, "remotely", stats.failedRemotelyCount);
+ printer.print(String.format("\nExecuted %d out of %d tests: %s.\n",
+ stats.numberOfExecutedTargets,
+ stats.numberOfTargets,
+ StringUtil.joinEnglishList(results, "and")));
+ } else {
+ int failingUpToDateCount = stats.failedCount - stats.noStatusCount;
+ printer.print(String.format(
+ "\nFinished with %d passing and %s%d failing%s tests up to date, %s%d out of date.%s\n",
+ stats.passCount,
+ failingUpToDateCount > 0 ? AnsiTerminalPrinter.Mode.ERROR : "",
+ failingUpToDateCount,
+ AnsiTerminalPrinter.Mode.DEFAULT,
+ stats.noStatusCount > 0 ? AnsiTerminalPrinter.Mode.ERROR : "",
+ stats.noStatusCount,
+ AnsiTerminalPrinter.Mode.DEFAULT));
+ }
+
+ if (stats.wasUnreportedWrongSize) {
+ printer.print("There were tests whose specified size is too big. Use the "
+ + "--test_verbose_timeout_warnings command line option to see which "
+ + "ones these are.\n");
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/TestResultAnalyzer.java b/src/main/java/com/google/devtools/build/lib/runtime/TestResultAnalyzer.java
new file mode 100644
index 0000000..ed9120b
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/TestResultAnalyzer.java
@@ -0,0 +1,349 @@
+// Copyright 2014 Google Inc. 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.runtime;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Sets;
+import com.google.common.eventbus.EventBus;
+import com.google.devtools.build.lib.Constants;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadCompatible;
+import com.google.devtools.build.lib.exec.ExecutionOptions;
+import com.google.devtools.build.lib.packages.TestSize;
+import com.google.devtools.build.lib.packages.TestTimeout;
+import com.google.devtools.build.lib.rules.test.TestProvider;
+import com.google.devtools.build.lib.rules.test.TestResult;
+import com.google.devtools.build.lib.runtime.TerminalTestResultNotifier.TestSummaryOptions;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.lib.view.test.TestStatus.BlazeTestStatus;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Prints results to the terminal, showing the results of each test target.
+ */
+@ThreadCompatible
+public class TestResultAnalyzer {
+ private final Path execRoot;
+ private final TestSummaryOptions summaryOptions;
+ private final ExecutionOptions executionOptions;
+ private final EventBus eventBus;
+
+ /**
+ * @param summaryOptions Parsed test summarization options.
+ * @param executionOptions Parsed build/test execution options.
+ * @param eventBus For reporting failed to build and cached tests.
+ */
+ public TestResultAnalyzer(Path execRoot,
+ TestSummaryOptions summaryOptions,
+ ExecutionOptions executionOptions,
+ EventBus eventBus) {
+ this.execRoot = execRoot;
+ this.summaryOptions = summaryOptions;
+ this.executionOptions = executionOptions;
+ this.eventBus = eventBus;
+ }
+
+ /**
+ * Prints out the results of the given tests, and returns true if they all passed.
+ * Posts any targets which weren't already completed by the listener to the EventBus.
+ * Reports all targets on the console via the given notifier.
+ * Run at the end of the build, run only once.
+ *
+ * @param testTargets The list of targets being run
+ * @param listener An aggregating listener with intermediate results
+ * @param notifier A console notifier to echo results to.
+ * @return true if all the tests passed, else false
+ */
+ public boolean differentialAnalyzeAndReport(
+ Collection<ConfiguredTarget> testTargets,
+ AggregatingTestListener listener,
+ TestResultNotifier notifier) {
+
+ Preconditions.checkNotNull(testTargets);
+ Preconditions.checkNotNull(listener);
+ Preconditions.checkNotNull(notifier);
+
+ // The natural ordering of the summaries defines their output order.
+ Set<TestSummary> summaries = Sets.newTreeSet();
+
+ int totalRun = 0; // Number of targets running at least one non-cached test.
+ int passCount = 0;
+
+ for (ConfiguredTarget testTarget : testTargets) {
+ TestSummary summary = aggregateAndReportSummary(testTarget, listener).build();
+ summaries.add(summary);
+
+ // Finished aggregating; build the final console output.
+ if (summary.actionRan()) {
+ totalRun++;
+ }
+
+ if (TestResult.isBlazeTestStatusPassed(summary.getStatus())) {
+ passCount++;
+ }
+ }
+
+ Preconditions.checkState(summaries.size() == testTargets.size());
+
+ notifier.notify(summaries, totalRun);
+ return passCount == testTargets.size();
+ }
+
+ private static BlazeTestStatus aggregateStatus(BlazeTestStatus status, BlazeTestStatus other) {
+ return status.ordinal() > other.ordinal() ? status : other;
+ }
+
+ /**
+ * Helper for differential analysis which aggregates the TestSummary
+ * for an individual target, reporting runs on the EventBus if necessary.
+ */
+ private TestSummary.Builder aggregateAndReportSummary(
+ ConfiguredTarget testTarget,
+ AggregatingTestListener listener) {
+
+ // If already reported by the listener, no work remains for this target.
+ TestSummary.Builder summary = listener.getCurrentSummary(testTarget);
+ Label testLabel = testTarget.getLabel();
+ Preconditions.checkNotNull(summary,
+ "%s did not complete test filtering, but has a test result", testLabel);
+ if (listener.targetReported(testTarget)) {
+ return summary;
+ }
+
+ Collection<Artifact> incompleteRuns = listener.getIncompleteRuns(testTarget);
+ Map<Artifact, TestResult> statusMap = listener.getStatusMap();
+
+ // We will get back multiple TestResult instances if test had to be retried several
+ // times before passing. Sharding and multiple runs of the same test without retries
+ // will be represented by separate artifacts and will produce exactly one TestResult.
+ for (Artifact testStatus : TestProvider.getTestStatusArtifacts(testTarget)) {
+ // When a build is interrupted ( eg. a broken target with --nokeep_going ) runResult could
+ // be null for an unrelated test because we were not able to even try to execute the test.
+ // In that case, for tests that were previously passing we return null ( == NO STATUS),
+ // because checking if the cached test target is up-to-date would require running the
+ // dependency checker transitively.
+ TestResult runResult = statusMap.get(testStatus);
+ boolean isIncompleteRun = incompleteRuns.contains(testStatus);
+ if (runResult == null) {
+ summary = markIncomplete(summary);
+ } else if (isIncompleteRun) {
+ // Only process results which were not recorded by the listener.
+
+ boolean newlyFetched = !statusMap.containsKey(testStatus);
+ summary = incrementalAnalyze(summary, runResult);
+ if (newlyFetched) {
+ eventBus.post(runResult);
+ }
+ Preconditions.checkState(
+ listener.getIncompleteRuns(testTarget).contains(testStatus) == isIncompleteRun,
+ "TestListener changed in differential analysis. Ensure it isn't still registered.");
+ }
+ }
+
+ // The target was not posted by the listener and must be posted now.
+ eventBus.post(summary.build());
+ return summary;
+ }
+
+ /**
+ * Incrementally updates a TestSummary given an existing summary
+ * and a new TestResult. Only call on built targets.
+ *
+ * @param summaryBuilder Existing unbuilt test summary associated with a target.
+ * @param result New test result to aggregate into the summary.
+ * @return The updated TestSummary.
+ */
+ public TestSummary.Builder incrementalAnalyze(TestSummary.Builder summaryBuilder,
+ TestResult result) {
+ // Cache retrieval should have been performed already.
+ Preconditions.checkNotNull(result);
+ Preconditions.checkNotNull(summaryBuilder);
+ TestSummary existingSummary = Preconditions.checkNotNull(summaryBuilder.peek());
+
+ TransitiveInfoCollection target = existingSummary.getTarget();
+ Preconditions.checkNotNull(
+ target, "The existing TestSummary must be associated with a target");
+
+ BlazeTestStatus status = existingSummary.getStatus();
+ int numCached = existingSummary.numCached();
+ int numLocalActionCached = existingSummary.numLocalActionCached();
+
+ if (!existingSummary.actionRan() && !result.isCached()) {
+ // At least one run of the test actually ran uncached.
+ summaryBuilder.setActionRan(true);
+
+ // Coverage data artifact will be identical for all test results - it is provided by the
+ // TestRunnerAction and all results in this collection associate with the same action.
+ PathFragment coverageData = result.getCoverageData();
+ if (coverageData != null) {
+ summaryBuilder.addCoverageFiles(
+ Collections.singletonList(execRoot.getRelative(coverageData)));
+ }
+ }
+
+ if (result.isCached() || result.getData().getRemotelyCached()) {
+ numCached++;
+ }
+ if (result.isCached()) {
+ numLocalActionCached++;
+ }
+
+ if (!executionOptions.runsPerTestDetectsFlakes) {
+ status = aggregateStatus(status, result.getData().getStatus());
+ } else {
+ int shardNumber = result.getShardNum();
+ int runsPerTestForLabel = target.getProvider(TestProvider.class).getTestParams().getRuns();
+ List<BlazeTestStatus> singleShardStatuses = summaryBuilder.addShardStatus(
+ shardNumber, result.getData().getStatus());
+ if (singleShardStatuses.size() == runsPerTestForLabel) {
+ BlazeTestStatus shardStatus = BlazeTestStatus.NO_STATUS;
+ int passes = 0;
+ for (BlazeTestStatus runStatusForShard : singleShardStatuses) {
+ shardStatus = aggregateStatus(shardStatus, runStatusForShard);
+ if (TestResult.isBlazeTestStatusPassed(shardStatus)) {
+ passes++;
+ }
+ }
+ // Under the RunsPerTestDetectsFlakes option, return flaky if 1 <= p < n shards pass.
+ // If all results pass or fail, aggregate the passing/failing shardStatus.
+ if (passes == 0 || passes == runsPerTestForLabel) {
+ status = aggregateStatus(status, shardStatus);
+ } else {
+ status = aggregateStatus(status, BlazeTestStatus.FLAKY);
+ }
+ }
+ }
+
+ List<String> filtered = new ArrayList<>();
+ warningLoop: for (String warning : result.getData().getWarningList()) {
+ for (String ignoredPrefix : Constants.IGNORED_TEST_WARNING_PREFIXES) {
+ if (warning.startsWith(ignoredPrefix)) {
+ continue warningLoop;
+ }
+ }
+
+ filtered.add(warning);
+ }
+
+ List<Path> passed = new ArrayList<>();
+ if (result.getData().hasPassedLog()) {
+ passed.add(result.getTestAction().getTestLog().getPath().getRelative(
+ result.getData().getPassedLog()));
+ }
+
+ List<Path> failed = new ArrayList<>();
+ for (String path : result.getData().getFailedLogsList()) {
+ failed.add(result.getTestAction().getTestLog().getPath().getRelative(path));
+ }
+
+ summaryBuilder
+ .addTestTimes(result.getData().getTestTimesList())
+ .addPassedLogs(passed)
+ .addFailedLogs(failed)
+ .addWarnings(filtered)
+ .collectFailedTests(result.getData().getTestCase())
+ .setRanRemotely(result.getData().getIsRemoteStrategy());
+
+ List<String> warnings = new ArrayList<>();
+ if (status == BlazeTestStatus.PASSED) {
+ if (shouldEmitTestSizeWarningInSummary(
+ summaryOptions.testVerboseTimeoutWarnings,
+ warnings, result.getData().getTestProcessTimesList(), target)) {
+ summaryBuilder.setWasUnreportedWrongSize(true);
+ }
+ }
+
+ return summaryBuilder
+ .setStatus(status)
+ .setNumCached(numCached)
+ .setNumLocalActionCached(numLocalActionCached)
+ .addWarnings(warnings);
+ }
+
+ private TestSummary.Builder markIncomplete(TestSummary.Builder summaryBuilder) {
+ // TODO(bazel-team): (2010) Make NotRunTestResult support both tests failed to built and
+ // tests with no status and post it here.
+ TestSummary summary = summaryBuilder.peek();
+ BlazeTestStatus status = summary.getStatus();
+ if (status != BlazeTestStatus.NO_STATUS) {
+ status = aggregateStatus(status, BlazeTestStatus.INCOMPLETE);
+ }
+
+ return summaryBuilder.setStatus(status);
+ }
+
+ TestSummary.Builder markUnbuilt(TestSummary.Builder summary, boolean blazeHalted) {
+ BlazeTestStatus runStatus = blazeHalted ? BlazeTestStatus.BLAZE_HALTED_BEFORE_TESTING
+ : (executionOptions.testCheckUpToDate
+ ? BlazeTestStatus.NO_STATUS
+ : BlazeTestStatus.FAILED_TO_BUILD);
+
+ return summary.setStatus(runStatus);
+ }
+
+ /**
+ * Checks whether the specified test timeout could have been smaller and adds
+ * a warning message if verbose is true.
+ *
+ * <p>Returns true if there was a test with the wrong timeout, but if was not
+ * reported.
+ */
+ private static boolean shouldEmitTestSizeWarningInSummary(boolean verbose,
+ List<String> warnings, List<Long> testTimes, TransitiveInfoCollection target) {
+
+ TestTimeout specifiedTimeout =
+ target.getProvider(TestProvider.class).getTestParams().getTimeout();
+ long maxTimeOfShard = 0;
+
+ for (Long shardTime : testTimes) {
+ if (shardTime != null) {
+ maxTimeOfShard = Math.max(maxTimeOfShard, shardTime);
+ }
+ }
+
+ int maxTimeInSeconds = (int) (maxTimeOfShard / 1000);
+
+ if (!specifiedTimeout.isInRangeFuzzy(maxTimeInSeconds)) {
+ TestTimeout expectedTimeout = TestTimeout.getSuggestedTestTimeout(maxTimeInSeconds);
+ TestSize expectedSize = TestSize.getTestSize(expectedTimeout);
+ if (verbose) {
+ StringBuilder builder = new StringBuilder(String.format(
+ "Test execution time (%.1fs excluding execution overhead) outside of "
+ + "range for %s tests. Consider setting timeout=\"%s\"",
+ maxTimeOfShard / 1000.0,
+ specifiedTimeout.prettyPrint(),
+ expectedTimeout));
+ if (expectedSize != null) {
+ builder.append(" or size=\"").append(expectedSize).append("\"");
+ }
+ builder.append(". You need not modify the size if you think it is correct.");
+ warnings.add(builder.toString());
+ return false;
+ }
+ return true;
+ } else {
+ return false;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/TestResultNotifier.java b/src/main/java/com/google/devtools/build/lib/runtime/TestResultNotifier.java
new file mode 100644
index 0000000..d7dbebb
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/TestResultNotifier.java
@@ -0,0 +1,30 @@
+// Copyright 2014 Google Inc. 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.runtime;
+
+import java.util.Set;
+
+/**
+ * Used to notify interested parties of test results.
+ */
+public interface TestResultNotifier {
+
+ /**
+ * @param summaries Summary of all targets that were supposed to be tested
+ * (regardless whether they actually were executed).
+ * @param numberOfExecutedTargets the number of targets that were actually run.
+ * Must not exceed summaries.size().
+ */
+ void notify(Set<TestSummary> summaries, int numberOfExecutedTargets);
+}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/TestSummary.java b/src/main/java/com/google/devtools/build/lib/runtime/TestSummary.java
new file mode 100644
index 0000000..171f150
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/TestSummary.java
@@ -0,0 +1,428 @@
+// Copyright 2014 Google Inc. 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.runtime;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Multimap;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.FilesToRunProvider;
+import com.google.devtools.build.lib.util.io.AnsiTerminalPrinter.Mode;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.view.test.TestStatus.BlazeTestStatus;
+import com.google.devtools.build.lib.view.test.TestStatus.FailedTestCasesStatus;
+import com.google.devtools.build.lib.view.test.TestStatus.TestCase;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * Test summary entry. Stores summary information for a single test rule.
+ * Also used to sort summary output by status.
+ *
+ * <p>Invariant:
+ * All TestSummary mutations should be performed through the Builder.
+ * No direct TestSummary methods (except the constructor) may mutate the object.
+ */
+@VisibleForTesting // Ideally package-scoped.
+public class TestSummary implements Comparable<TestSummary> {
+ /**
+ * Builder class responsible for creating and altering TestSummary objects.
+ */
+ public static class Builder {
+ private TestSummary summary;
+ private boolean built;
+
+ private Builder() {
+ summary = new TestSummary();
+ built = false;
+ }
+
+ private void mergeFrom(TestSummary existingSummary) {
+ // Yuck, manually fill in fields.
+ summary.shardRunStatuses = ArrayListMultimap.create(existingSummary.shardRunStatuses);
+ setTarget(existingSummary.target);
+ setStatus(existingSummary.status);
+ addCoverageFiles(existingSummary.coverageFiles);
+ addPassedLogs(existingSummary.passedLogs);
+ addFailedLogs(existingSummary.failedLogs);
+
+ if (existingSummary.failedTestCasesStatus != null) {
+ addFailedTestCases(existingSummary.getFailedTestCases(),
+ existingSummary.getFailedTestCasesStatus());
+ }
+
+ addTestTimes(existingSummary.testTimes);
+ addWarnings(existingSummary.warnings);
+ setActionRan(existingSummary.actionRan);
+ setNumCached(existingSummary.numCached);
+ setRanRemotely(existingSummary.ranRemotely);
+ setWasUnreportedWrongSize(existingSummary.wasUnreportedWrongSize);
+ }
+
+ // Implements copy on write logic, allowing reuse of the same builder.
+ private void checkMutation() {
+ // If mutating the builder after an object was built, create another copy.
+ if (built) {
+ built = false;
+ TestSummary lastSummary = summary;
+ summary = new TestSummary();
+ mergeFrom(lastSummary);
+ }
+ }
+
+ // This used to return a reference to the value on success.
+ // However, since it can alter the summary member, inlining it in an
+ // assignment to a property of summary was unsafe.
+ private void checkMutation(Object value) {
+ Preconditions.checkNotNull(value);
+ checkMutation();
+ }
+
+ public Builder setTarget(ConfiguredTarget target) {
+ checkMutation(target);
+ summary.target = target;
+ return this;
+ }
+
+ public Builder setStatus(BlazeTestStatus status) {
+ checkMutation(status);
+ summary.status = status;
+ return this;
+ }
+
+ public Builder addCoverageFiles(List<Path> coverageFiles) {
+ checkMutation(coverageFiles);
+ summary.coverageFiles.addAll(coverageFiles);
+ return this;
+ }
+
+ public Builder addPassedLogs(List<Path> passedLogs) {
+ checkMutation(passedLogs);
+ summary.passedLogs.addAll(passedLogs);
+ return this;
+ }
+
+ public Builder addFailedLogs(List<Path> failedLogs) {
+ checkMutation(failedLogs);
+ summary.failedLogs.addAll(failedLogs);
+ return this;
+ }
+
+ public Builder collectFailedTests(TestCase testCase) {
+ if (testCase == null) {
+ summary.failedTestCasesStatus = FailedTestCasesStatus.NOT_AVAILABLE;
+ return this;
+ }
+ summary.failedTestCasesStatus = FailedTestCasesStatus.FULL;
+ return collectFailedTestCases(testCase);
+ }
+
+ private Builder collectFailedTestCases(TestCase testCase) {
+ if (testCase.getChildCount() > 0) {
+ // This is a non-leaf result. Traverse its children, but do not add its
+ // name to the output list. It should not contain any 'failure' or
+ // 'error' tags, but we want to be lax here, because the syntax of the
+ // test.xml file is also lax.
+ for (TestCase child : testCase.getChildList()) {
+ collectFailedTestCases(child);
+ }
+ } else {
+ // This is a leaf result. If it passed, don't add it.
+ if (testCase.getStatus() == TestCase.Status.PASSED) {
+ return this;
+ }
+
+ String name = testCase.getName();
+ String className = testCase.getClassName();
+ if (name == null || className == null) {
+ // A test case detail is not really interesting if we cannot tell which
+ // one it is.
+ this.summary.failedTestCasesStatus = FailedTestCasesStatus.PARTIAL;
+ return this;
+ }
+
+ this.summary.failedTestCases.add(testCase);
+ }
+ return this;
+ }
+
+ public Builder addFailedTestCases(List<TestCase> testCases, FailedTestCasesStatus status) {
+ checkMutation(status);
+ checkMutation(testCases);
+
+ if (summary.failedTestCasesStatus == null) {
+ summary.failedTestCasesStatus = status;
+ } else if (summary.failedTestCasesStatus != status) {
+ summary.failedTestCasesStatus = FailedTestCasesStatus.PARTIAL;
+ }
+
+ if (testCases.isEmpty()) {
+ return this;
+ }
+
+ // union of summary.failedTestCases, testCases
+ Map<String, TestCase> allCases = new TreeMap<>();
+ if (summary.failedTestCases != null) {
+ for (TestCase detail : summary.failedTestCases) {
+ allCases.put(detail.getClassName() + "." + detail.getName(), detail);
+ }
+ }
+ for (TestCase detail : testCases) {
+ allCases.put(detail.getClassName() + "." + detail.getName(), detail);
+ }
+
+ summary.failedTestCases = new ArrayList<TestCase>(allCases.values());
+ return this;
+ }
+
+ public Builder addTestTimes(List<Long> testTimes) {
+ checkMutation(testTimes);
+ summary.testTimes.addAll(testTimes);
+ return this;
+ }
+
+ public Builder addWarnings(List<String> warnings) {
+ checkMutation(warnings);
+ summary.warnings.addAll(warnings);
+ return this;
+ }
+
+ public Builder setActionRan(boolean actionRan) {
+ checkMutation();
+ summary.actionRan = actionRan;
+ return this;
+ }
+
+ public Builder setNumCached(int numCached) {
+ checkMutation();
+ summary.numCached = numCached;
+ return this;
+ }
+
+ public Builder setNumLocalActionCached(int numLocalActionCached) {
+ checkMutation();
+ summary.numLocalActionCached = numLocalActionCached;
+ return this;
+ }
+
+ public Builder setRanRemotely(boolean ranRemotely) {
+ checkMutation();
+ summary.ranRemotely = ranRemotely;
+ return this;
+ }
+
+ public Builder setWasUnreportedWrongSize(boolean wasUnreportedWrongSize) {
+ checkMutation();
+ summary.wasUnreportedWrongSize = wasUnreportedWrongSize;
+ return this;
+ }
+
+ /**
+ * Records a new result for the given shard of the test.
+ *
+ * @return an immutable view of the statuses associated with the shard, with the new element.
+ */
+ public List<BlazeTestStatus> addShardStatus(int shardNumber, BlazeTestStatus status) {
+ Preconditions.checkState(summary.shardRunStatuses.put(shardNumber, status),
+ "shardRunStatuses must allow duplicate statuses");
+ return ImmutableList.copyOf(summary.shardRunStatuses.get(shardNumber));
+ }
+
+ /**
+ * Returns the created TestSummary object.
+ * Any actions following a build() will create another copy of the same values.
+ * Since no mutators are provided directly by TestSummary, a copy will not
+ * be produced if two builds are invoked in a row without calling a setter.
+ */
+ public TestSummary build() {
+ peek();
+ if (!built) {
+ makeSummaryImmutable();
+ // else: it is already immutable.
+ }
+ Preconditions.checkState(built, "Built flag was not set");
+ return summary;
+ }
+
+ /**
+ * Within-package, it is possible to read directly from an
+ * incompletely-built TestSummary. Used to pass Builders around directly.
+ */
+ TestSummary peek() {
+ Preconditions.checkNotNull(summary.target, "Target cannot be null");
+ Preconditions.checkNotNull(summary.status, "Status cannot be null");
+ return summary;
+ }
+
+ private void makeSummaryImmutable() {
+ // Once finalized, the list types are immutable.
+ summary.passedLogs = Collections.unmodifiableList(summary.passedLogs);
+ summary.failedLogs = Collections.unmodifiableList(summary.failedLogs);
+ summary.warnings = Collections.unmodifiableList(summary.warnings);
+ summary.coverageFiles = Collections.unmodifiableList(summary.coverageFiles);
+ summary.testTimes = Collections.unmodifiableList(summary.testTimes);
+
+ built = true;
+ }
+ }
+
+ private ConfiguredTarget target;
+ private BlazeTestStatus status;
+ // Currently only populated if --runs_per_test_detects_flakes is enabled.
+ private Multimap<Integer, BlazeTestStatus> shardRunStatuses = ArrayListMultimap.create();
+ private int numCached;
+ private int numLocalActionCached;
+ private boolean actionRan;
+ private boolean ranRemotely;
+ private boolean wasUnreportedWrongSize;
+ private List<TestCase> failedTestCases = new ArrayList<>();
+ private List<Path> passedLogs = new ArrayList<>();
+ private List<Path> failedLogs = new ArrayList<>();
+ private List<String> warnings = new ArrayList<>();
+ private List<Path> coverageFiles = new ArrayList<>();
+ private List<Long> testTimes = new ArrayList<>();
+ private FailedTestCasesStatus failedTestCasesStatus = null;
+
+ // Don't allow public instantiation; go through the Builder.
+ private TestSummary() {
+ }
+
+ /**
+ * Creates a new Builder allowing construction of a new TestSummary object.
+ */
+ public static Builder newBuilder() {
+ return new Builder();
+ }
+
+ /**
+ * Creates a new Builder initialized with a copy of the existing object's values.
+ */
+ public static Builder newBuilderFromExisting(TestSummary existing) {
+ Builder builder = new Builder();
+ builder.mergeFrom(existing);
+ return builder;
+ }
+
+ public ConfiguredTarget getTarget() {
+ return target;
+ }
+
+ public BlazeTestStatus getStatus() {
+ return status;
+ }
+
+ public boolean isCached() {
+ return numCached > 0;
+ }
+
+ public boolean isLocalActionCached() {
+ return numLocalActionCached > 0;
+ }
+
+ public int numLocalActionCached() {
+ return numLocalActionCached;
+ }
+
+ public int numCached() {
+ return numCached;
+ }
+
+ private int numUncached() {
+ return totalRuns() - numCached;
+ }
+
+ public boolean actionRan() {
+ return actionRan;
+ }
+
+ public boolean ranRemotely() {
+ return ranRemotely;
+ }
+
+ public boolean wasUnreportedWrongSize() {
+ return wasUnreportedWrongSize;
+ }
+
+ public List<TestCase> getFailedTestCases() {
+ return failedTestCases;
+ }
+
+ public List<Path> getCoverageFiles() {
+ return coverageFiles;
+ }
+
+ public List<Path> getPassedLogs() {
+ return passedLogs;
+ }
+
+ public List<Path> getFailedLogs() {
+ return failedLogs;
+ }
+
+ public FailedTestCasesStatus getFailedTestCasesStatus() {
+ return failedTestCasesStatus;
+ }
+
+ /**
+ * Returns an immutable view of the warnings associated with this test.
+ */
+ public List<String> getWarnings() {
+ return Collections.unmodifiableList(warnings);
+ }
+
+ private static int getSortKey(BlazeTestStatus status) {
+ return status == BlazeTestStatus.PASSED ? -1 : status.ordinal();
+ }
+
+ @Override
+ public int compareTo(TestSummary that) {
+ if (this.isCached() != that.isCached()) {
+ return this.isCached() ? -1 : 1;
+ } else if ((this.isCached() && that.isCached()) && (this.numUncached() != that.numUncached())) {
+ return this.numUncached() - that.numUncached();
+ } else if (this.status != that.status) {
+ return getSortKey(this.status) - getSortKey(that.status);
+ } else {
+ Artifact thisExecutable = this.target.getProvider(FilesToRunProvider.class).getExecutable();
+ Artifact thatExecutable = that.target.getProvider(FilesToRunProvider.class).getExecutable();
+ return thisExecutable.getPath().compareTo(thatExecutable.getPath());
+ }
+ }
+
+ public List<Long> getTestTimes() {
+ // The return result is unmodifiable (UnmodifiableList instance)
+ return testTimes;
+ }
+
+ public int getNumCached() {
+ return numCached;
+ }
+
+ public int totalRuns() {
+ return testTimes.size();
+ }
+
+ static Mode getStatusMode(BlazeTestStatus status) {
+ return status == BlazeTestStatus.PASSED
+ ? Mode.INFO
+ : (status == BlazeTestStatus.FLAKY ? Mode.WARNING : Mode.ERROR);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/TestSummaryPrinter.java b/src/main/java/com/google/devtools/build/lib/runtime/TestSummaryPrinter.java
new file mode 100644
index 0000000..91c1488
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/TestSummaryPrinter.java
@@ -0,0 +1,255 @@
+// Copyright 2014 Google Inc. 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.runtime;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Strings;
+import com.google.devtools.build.lib.rules.test.TestLogHelper;
+import com.google.devtools.build.lib.rules.test.TestStrategy.TestOutputFormat;
+import com.google.devtools.build.lib.util.LoggingUtil;
+import com.google.devtools.build.lib.util.io.AnsiTerminalPrinter;
+import com.google.devtools.build.lib.util.io.AnsiTerminalPrinter.Mode;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.view.test.TestStatus.BlazeTestStatus;
+import com.google.devtools.build.lib.view.test.TestStatus.FailedTestCasesStatus;
+import com.google.devtools.build.lib.view.test.TestStatus.TestCase;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
+
+/**
+ * Print test statistics in human readable form.
+ */
+public class TestSummaryPrinter {
+
+ /**
+ * Print the cached test log to the given printer.
+ */
+ public static void printCachedOutput(TestSummary summary,
+ TestOutputFormat testOutput,
+ AnsiTerminalPrinter printer) {
+
+ String testName = summary.getTarget().getLabel().toString();
+ List<String> allLogs = new ArrayList<>();
+ for (Path path : summary.getFailedLogs()) {
+ allLogs.add(path.getPathString());
+ }
+ for (Path path : summary.getPassedLogs()) {
+ allLogs.add(path.getPathString());
+ }
+ printer.printLn("" + TestSummary.getStatusMode(summary.getStatus()) + summary.getStatus() + ": "
+ + Mode.DEFAULT + testName + " (see " + Joiner.on(' ').join(allLogs) + ")");
+ printer.printLn(Mode.INFO + "INFO: " + Mode.DEFAULT + "From Testing " + testName);
+
+ // Whether to output the target at all was checked by the caller.
+ // Now check whether to output failing shards.
+ if (TestLogHelper.shouldOutputTestLog(testOutput, false)) {
+ for (Path path : summary.getFailedLogs()) {
+ try {
+ TestLogHelper.writeTestLog(path, testName, printer.getOutputStream());
+ } catch (IOException e) {
+ printer.printLn("==================== Could not read test output for " + testName);
+ LoggingUtil.logToRemote(Level.WARNING, "Error while reading test log", e);
+ }
+ }
+ }
+
+ // And passing shards, independently.
+ if (TestLogHelper.shouldOutputTestLog(testOutput, true)) {
+ for (Path path : summary.getPassedLogs()) {
+ try {
+ TestLogHelper.writeTestLog(path, testName, printer.getOutputStream());
+ } catch (Exception e) {
+ printer.printLn("==================== Could not read test output for " + testName);
+ LoggingUtil.logToRemote(Level.WARNING, "Error while reading test log", e);
+ }
+ }
+ }
+ }
+
+ private static String statusString(BlazeTestStatus status) {
+ return status.toString().replace('_', ' ');
+ }
+
+ /**
+ * Prints summary status for a single test.
+ * @param terminalPrinter The printer to print to
+ */
+ public static void print(
+ TestSummary summary,
+ AnsiTerminalPrinter terminalPrinter,
+ boolean verboseSummary, boolean printFailedTestCases) {
+ // Skip output for tests that failed to build.
+ if (summary.getStatus() == BlazeTestStatus.FAILED_TO_BUILD) {
+ return;
+ }
+ String message = getCacheMessage(summary) + statusString(summary.getStatus());
+ terminalPrinter.print(
+ Strings.padEnd(summary.getTarget().getLabel().toString(), 78 - message.length(), ' ')
+ + " " + TestSummary.getStatusMode(summary.getStatus()) + message + Mode.DEFAULT
+ + (verboseSummary ? getAttemptSummary(summary) + getTimeSummary(summary) : "") + "\n");
+
+ if (printFailedTestCases && summary.getStatus() == BlazeTestStatus.FAILED) {
+ if (summary.getFailedTestCasesStatus() == FailedTestCasesStatus.NOT_AVAILABLE) {
+ terminalPrinter.print(
+ Mode.WARNING + " (individual test case information not available) "
+ + Mode.DEFAULT + "\n");
+ } else {
+ for (TestCase testCase : summary.getFailedTestCases()) {
+ if (testCase.getStatus() != TestCase.Status.PASSED) {
+ TestSummaryPrinter.printTestCase(terminalPrinter, testCase);
+ }
+ }
+
+ if (summary.getFailedTestCasesStatus() != FailedTestCasesStatus.FULL) {
+ terminalPrinter.print(
+ Mode.WARNING
+ + " (some shards did not report details, list of failed test"
+ + " cases incomplete)\n"
+ + Mode.DEFAULT);
+ }
+ }
+ }
+
+ if (printFailedTestCases) {
+ // In this mode, test output and coverage files would just clutter up
+ // the output.
+ return;
+ }
+
+ for (String warning : summary.getWarnings()) {
+ terminalPrinter.print(" " + AnsiTerminalPrinter.Mode.WARNING + "WARNING: "
+ + AnsiTerminalPrinter.Mode.DEFAULT + warning + "\n");
+ }
+
+ for (Path path : summary.getFailedLogs()) {
+ if (path.exists()) {
+ // Don't use getPrettyPath() here - we want to print the absolute path,
+ // so that it cut and paste into a different terminal, and we don't
+ // want to use the blaze-bin etc. symbolic links because they could be changed
+ // by a subsequent build with different options.
+ terminalPrinter.print(" " + path.getPathString() + "\n");
+ }
+ }
+ for (Path path : summary.getCoverageFiles()) {
+ // Print only non-trivial coverage files.
+ try {
+ if (path.exists() && path.getFileSize() > 0) {
+ terminalPrinter.print(" " + path.getPathString() + "\n");
+ }
+ } catch (IOException e) {
+ LoggingUtil.logToRemote(Level.WARNING, "Error while reading coverage data file size",
+ e);
+ }
+ }
+ }
+
+ /**
+ * Prints the result of an individual test case. It is assumed not to have
+ * passed, since passed test cases are not reported.
+ */
+ static void printTestCase(
+ AnsiTerminalPrinter terminalPrinter, TestCase testCase) {
+ String timeSummary;
+ if (testCase.hasRunDurationMillis()) {
+ timeSummary = " ("
+ + timeInSec(testCase.getRunDurationMillis(), TimeUnit.MILLISECONDS)
+ + ")";
+ } else {
+ timeSummary = "";
+ }
+
+ terminalPrinter.print(
+ " "
+ + Mode.ERROR
+ + Strings.padEnd(testCase.getStatus().toString(), 8, ' ')
+ + Mode.DEFAULT
+ + testCase.getClassName()
+ + "."
+ + testCase.getName()
+ + timeSummary
+ + "\n");
+ }
+
+ /**
+ * Return the given time in seconds, to 1 decimal place,
+ * i.e. "32.1s".
+ */
+ static String timeInSec(long time, TimeUnit unit) {
+ double ms = TimeUnit.MILLISECONDS.convert(time, unit);
+ return String.format("%.1fs", ms / 1000.0);
+ }
+
+ static String getAttemptSummary(TestSummary summary) {
+ int attempts = summary.getPassedLogs().size() + summary.getFailedLogs().size();
+ if (attempts > 1) {
+ // Print number of failed runs for failed tests if testing was completed.
+ if (summary.getStatus() == BlazeTestStatus.FLAKY) {
+ return ", failed in " + summary.getFailedLogs().size() + " out of " + attempts;
+ }
+ if (summary.getStatus() == BlazeTestStatus.TIMEOUT
+ || summary.getStatus() == BlazeTestStatus.FAILED) {
+ return " in " + summary.getFailedLogs().size() + " out of " + attempts;
+ }
+ }
+ return "";
+ }
+
+ static String getCacheMessage(TestSummary summary) {
+ if (summary.getNumCached() == 0 || summary.getStatus() == BlazeTestStatus.INCOMPLETE) {
+ return "";
+ } else if (summary.getNumCached() == summary.totalRuns()) {
+ return "(cached) ";
+ } else {
+ return String.format("(%d/%d cached) ", summary.getNumCached(), summary.totalRuns());
+ }
+ }
+
+ static String getTimeSummary(TestSummary summary) {
+ if (summary.getTestTimes().isEmpty()) {
+ return "";
+ } else if (summary.getTestTimes().size() == 1) {
+ return " in " + timeInSec(summary.getTestTimes().get(0), TimeUnit.MILLISECONDS);
+ } else {
+ // We previously used com.google.math for this, which added about 1 MB of deps to the total
+ // size. If we re-introduce a dependency on that package, we could revert this change.
+ long min = summary.getTestTimes().get(0).longValue(), max = min, sum = 0;
+ double sumOfSquares = 0.0;
+ for (Long l : summary.getTestTimes()) {
+ long value = l.longValue();
+ min = value < min ? value : min;
+ max = value > max ? value : max;
+ sum += value;
+ sumOfSquares += ((double) value) * (double) value;
+ }
+ double mean = ((double) sum) / summary.getTestTimes().size();
+ double stddev = Math.sqrt((sumOfSquares - sum * mean) / summary.getTestTimes().size());
+ // For sharded tests, we print the max time on the same line as
+ // the test, and then print more detailed info about the
+ // distribution of times on the next line.
+ String maxTime = timeInSec(max, TimeUnit.MILLISECONDS);
+ return String.format(
+ " in %s\n Stats over %d runs: max = %s, min = %s, avg = %s, dev = %s",
+ maxTime,
+ summary.getTestTimes().size(),
+ maxTime,
+ timeInSec(min, TimeUnit.MILLISECONDS),
+ timeInSec((long) mean, TimeUnit.MILLISECONDS),
+ timeInSec((long) stddev, TimeUnit.MILLISECONDS));
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/BuildCommand.java b/src/main/java/com/google/devtools/build/lib/runtime/commands/BuildCommand.java
new file mode 100644
index 0000000..d6f61eb
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/BuildCommand.java
@@ -0,0 +1,69 @@
+// Copyright 2014 Google Inc. 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.runtime.commands;
+
+import com.google.devtools.build.lib.analysis.BuildView;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.buildtool.BuildRequest;
+import com.google.devtools.build.lib.buildtool.BuildRequest.BuildRequestOptions;
+import com.google.devtools.build.lib.exec.ExecutionOptions;
+import com.google.devtools.build.lib.pkgcache.LoadingPhaseRunner;
+import com.google.devtools.build.lib.pkgcache.PackageCacheOptions;
+import com.google.devtools.build.lib.runtime.BlazeCommand;
+import com.google.devtools.build.lib.runtime.BlazeRuntime;
+import com.google.devtools.build.lib.runtime.Command;
+import com.google.devtools.build.lib.util.AbruptExitException;
+import com.google.devtools.build.lib.util.ExitCode;
+import com.google.devtools.common.options.OptionsParser;
+import com.google.devtools.common.options.OptionsProvider;
+
+import java.util.List;
+
+/**
+ * Handles the 'build' command on the Blaze command line, including targets
+ * named by arguments passed to Blaze.
+ */
+@Command(name = "build",
+ builds = true,
+ options = { BuildRequestOptions.class,
+ ExecutionOptions.class,
+ PackageCacheOptions.class,
+ BuildView.Options.class,
+ LoadingPhaseRunner.Options.class,
+ BuildConfiguration.Options.class,
+ },
+ usesConfigurationOptions = true,
+ shortDescription = "Builds the specified targets.",
+ allowResidue = true,
+ help = "resource:build.txt")
+public final class BuildCommand implements BlazeCommand {
+
+ @Override
+ public void editOptions(BlazeRuntime runtime, OptionsParser optionsParser)
+ throws AbruptExitException {
+ ProjectFileSupport.handleProjectFiles(runtime, optionsParser, "build");
+ }
+
+ @Override
+ public ExitCode exec(BlazeRuntime runtime, OptionsProvider options) {
+ List<String> targets = ProjectFileSupport.getTargets(runtime, options);
+
+ BuildRequest request = BuildRequest.create(
+ getClass().getAnnotation(Command.class).name(), options,
+ runtime.getStartupOptionsProvider(),
+ targets,
+ runtime.getReporter().getOutErr(), runtime.getCommandId(), runtime.getCommandStartTime());
+ return runtime.getBuildTool().processRequest(request, null).getExitCondition();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/CanonicalizeCommand.java b/src/main/java/com/google/devtools/build/lib/runtime/commands/CanonicalizeCommand.java
new file mode 100644
index 0000000..0bb5a0e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/CanonicalizeCommand.java
@@ -0,0 +1,95 @@
+// Copyright 2014 Google Inc. 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.runtime.commands;
+
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.runtime.BlazeCommand;
+import com.google.devtools.build.lib.runtime.BlazeCommandUtils;
+import com.google.devtools.build.lib.runtime.BlazeRuntime;
+import com.google.devtools.build.lib.runtime.Command;
+import com.google.devtools.build.lib.util.ExitCode;
+import com.google.devtools.common.options.Converter;
+import com.google.devtools.common.options.Option;
+import com.google.devtools.common.options.OptionsBase;
+import com.google.devtools.common.options.OptionsParser;
+import com.google.devtools.common.options.OptionsParsingException;
+import com.google.devtools.common.options.OptionsProvider;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * The 'blaze canonicalize-flags' command.
+ */
+@Command(name = "canonicalize-flags",
+ options = { CanonicalizeCommand.Options.class },
+ allowResidue = true,
+ mustRunInWorkspace = false,
+ shortDescription = "Canonicalizes a list of Blaze options.",
+ help = "This command canonicalizes a list of Blaze options. Don't forget to prepend '--' "
+ + "to end option parsing before the flags to canonicalize.\n"
+ + "%{options}")
+public final class CanonicalizeCommand implements BlazeCommand {
+
+ public static class CommandConverter implements Converter<String> {
+
+ @Override
+ public String convert(String input) throws OptionsParsingException {
+ if (input.equals("build")) {
+ return input;
+ } else if (input.equals("test")) {
+ return input;
+ }
+ throw new OptionsParsingException("Not a valid command: '" + input + "' (should be "
+ + getTypeDescription() + ")");
+ }
+
+ @Override
+ public String getTypeDescription() {
+ return "build or test";
+ }
+ }
+
+ public static class Options extends OptionsBase {
+
+ @Option(name = "for_command",
+ defaultValue = "build",
+ category = "misc",
+ converter = CommandConverter.class,
+ help = "The command for which the options should be canonicalized.")
+ public String forCommand;
+ }
+
+ @Override
+ public ExitCode exec(BlazeRuntime runtime, OptionsProvider options) {
+ BlazeCommand command = runtime.getCommandMap().get(
+ options.getOptions(Options.class).forCommand);
+ Collection<Class<? extends OptionsBase>> optionsClasses =
+ BlazeCommandUtils.getOptions(
+ command.getClass(), runtime.getBlazeModules(), runtime.getRuleClassProvider());
+ try {
+ List<String> result = OptionsParser.canonicalize(optionsClasses, options.getResidue());
+ for (String piece : result) {
+ runtime.getReporter().getOutErr().printOutLn(piece);
+ }
+ } catch (OptionsParsingException e) {
+ runtime.getReporter().handle(Event.error(e.getMessage()));
+ return ExitCode.COMMAND_LINE_ERROR;
+ }
+ return ExitCode.SUCCESS;
+ }
+
+ @Override
+ public void editOptions(BlazeRuntime runtime, OptionsParser optionsParser) {}
+}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/CleanCommand.java b/src/main/java/com/google/devtools/build/lib/runtime/commands/CleanCommand.java
new file mode 100644
index 0000000..3fd300e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/CleanCommand.java
@@ -0,0 +1,185 @@
+// Copyright 2014 Google Inc. 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.runtime.commands;
+
+import com.google.devtools.build.lib.actions.ExecException;
+import com.google.devtools.build.lib.analysis.BlazeDirectories;
+import com.google.devtools.build.lib.buildtool.BuildRequest;
+import com.google.devtools.build.lib.buildtool.OutputDirectoryLinksUtils;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.runtime.BlazeCommand;
+import com.google.devtools.build.lib.runtime.BlazeCommandDispatcher.ShutdownBlazeServerException;
+import com.google.devtools.build.lib.runtime.BlazeRuntime;
+import com.google.devtools.build.lib.runtime.Command;
+import com.google.devtools.build.lib.shell.CommandException;
+import com.google.devtools.build.lib.util.CommandBuilder;
+import com.google.devtools.build.lib.util.ExitCode;
+import com.google.devtools.build.lib.util.ProcessUtils;
+import com.google.devtools.build.lib.util.ShellEscaper;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.common.options.Option;
+import com.google.devtools.common.options.OptionsBase;
+import com.google.devtools.common.options.OptionsParser;
+import com.google.devtools.common.options.OptionsProvider;
+
+import java.io.IOException;
+import java.util.logging.Logger;
+
+/**
+ * Implements 'blaze clean'.
+ */
+@Command(name = "clean",
+ builds = true, // Does not, but people expect build options to be there
+ options = { CleanCommand.Options.class },
+ help = "resource:clean.txt",
+ shortDescription = "Removes output files and optionally stops the server.",
+ // TODO(bazel-team): Remove this - we inherit a huge number of unused options.
+ inherits = { BuildCommand.class })
+public final class CleanCommand implements BlazeCommand {
+
+ /**
+ * An interface for special options for the clean command.
+ */
+ public static class Options extends OptionsBase {
+ @Option(name = "clean_style",
+ defaultValue = "",
+ category = "clean",
+ help = "Can be either 'expunge' or 'expunge_async'.")
+ public String cleanStyle;
+
+ @Option(name = "expunge",
+ defaultValue = "false",
+ category = "clean",
+ expansion = "--clean_style=expunge",
+ help = "If specified, clean will remove the entire working tree for this Blaze " +
+ "instance, which includes all Blaze-created temporary and build output " +
+ "files, and it will stop the Blaze server if it is running.")
+ public boolean expunge;
+
+ @Option(name = "expunge_async",
+ defaultValue = "false",
+ category = "clean",
+ expansion = "--clean_style=expunge_async",
+ help = "If specified, clean will asynchronously remove the entire working tree for " +
+ "this Blaze instance, which includes all Blaze-created temporary and build " +
+ "output files, and it will stop the Blaze server if it is running. When this " +
+ "command completes, it will be safe to execute new commands in the same client, " +
+ "even though the deletion may continue in the background.")
+ public boolean expunge_async;
+ }
+
+ private static Logger LOG = Logger.getLogger(CleanCommand.class.getName());
+
+ @Override
+ public ExitCode exec(BlazeRuntime runtime, OptionsProvider options)
+ throws ShutdownBlazeServerException {
+ Options cleanOptions = options.getOptions(Options.class);
+ cleanOptions.expunge_async = cleanOptions.cleanStyle.equals("expunge_async");
+ cleanOptions.expunge = cleanOptions.cleanStyle.equals("expunge");
+
+ if (cleanOptions.expunge == false && cleanOptions.expunge_async == false &&
+ !cleanOptions.cleanStyle.isEmpty()) {
+ runtime.getReporter().handle(Event.error(
+ null, "Invalid clean_style value '" + cleanOptions.cleanStyle + "'"));
+ return ExitCode.COMMAND_LINE_ERROR;
+ }
+
+ String cleanBanner = cleanOptions.expunge_async ?
+ "Starting clean." :
+ "Starting clean (this may take a while). " +
+ "Consider using --expunge_async if the clean takes more than several minutes.";
+
+ runtime.getReporter().handle(Event.info(null/*location*/, cleanBanner));
+ try {
+ String symlinkPrefix =
+ options.getOptions(BuildRequest.BuildRequestOptions.class).symlinkPrefix;
+ actuallyClean(runtime, runtime.getOutputBase(), cleanOptions, symlinkPrefix);
+ return ExitCode.SUCCESS;
+ } catch (IOException e) {
+ runtime.getReporter().handle(Event.error(e.getMessage()));
+ return ExitCode.LOCAL_ENVIRONMENTAL_ERROR;
+ } catch (CommandException e) {
+ runtime.getReporter().handle(Event.error(e.getMessage()));
+ return ExitCode.RUN_FAILURE;
+ } catch (ExecException e) {
+ runtime.getReporter().handle(Event.error(e.getMessage()));
+ return ExitCode.RUN_FAILURE;
+ } catch (InterruptedException e) {
+ runtime.getReporter().handle(Event.error("clean interrupted"));
+ return ExitCode.INTERRUPTED;
+ }
+ }
+
+ private void actuallyClean(BlazeRuntime runtime,
+ Path outputBase, Options cleanOptions, String symlinkPrefix) throws IOException,
+ ShutdownBlazeServerException, CommandException, ExecException, InterruptedException {
+ if (runtime.getOutputService() != null) {
+ runtime.getOutputService().clean();
+ }
+ if (cleanOptions.expunge) {
+ LOG.info("Expunging...");
+ // Delete the big subdirectories with the important content first--this
+ // will take the most time. Then quickly delete the little locks, logs
+ // and links right before we exit. Once the lock file is gone there will
+ // be a small possibility of a server race if a client is waiting, but
+ // all significant files will be gone by then.
+ FileSystemUtils.deleteTreesBelow(outputBase);
+ FileSystemUtils.deleteTree(outputBase);
+ } else if (cleanOptions.expunge_async) {
+ LOG.info("Expunging asynchronously...");
+ String tempBaseName = outputBase.getBaseName() + "_tmp_" + ProcessUtils.getpid();
+
+ // Keeping tempOutputBase in the same directory ensures it remains in the
+ // same file system, and therefore the mv will be atomic and fast.
+ Path tempOutputBase = outputBase.getParentDirectory().getChild(tempBaseName);
+ outputBase.renameTo(tempOutputBase);
+ runtime.getReporter().handle(Event.info(
+ null, "Output base moved to " + tempOutputBase + " for deletion"));
+
+ // Daemonize the shell and use the double-fork idiom to ensure that the shell
+ // exits even while the "rm -rf" command continues.
+ String command = String.format("exec >&- 2>&- <&- && (/usr/bin/setsid /bin/rm -rf %s &)&",
+ ShellEscaper.escapeString(tempOutputBase.getPathString()));
+
+ LOG.info("Executing shell commmand " + ShellEscaper.escapeString(command));
+
+ // Doesn't throw iff command exited and was successful.
+ new CommandBuilder().addArg(command).useShell(true)
+ .setWorkingDir(tempOutputBase.getParentDirectory())
+ .build().execute();
+ } else {
+ LOG.info("Output cleaning...");
+ runtime.clearCaches();
+ for (String directory : new String[] {
+ BlazeDirectories.RELATIVE_OUTPUT_PATH, runtime.getWorkspaceName() }) {
+ Path child = outputBase.getChild(directory);
+ if (child.exists()) {
+ LOG.finest("Cleaning " + child);
+ FileSystemUtils.deleteTreesBelow(child);
+ }
+ }
+ }
+ // remove convenience links
+ OutputDirectoryLinksUtils.removeOutputDirectoryLinks(
+ runtime.getWorkspaceName(), runtime.getWorkspace(), runtime.getReporter(), symlinkPrefix);
+ // shutdown on expunge cleans
+ if (cleanOptions.expunge || cleanOptions.expunge_async) {
+ throw new ShutdownBlazeServerException(0);
+ }
+ }
+
+ @Override
+ public void editOptions(BlazeRuntime runtime, OptionsParser optionsParser) {}
+}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/HelpCommand.java b/src/main/java/com/google/devtools/build/lib/runtime/commands/HelpCommand.java
new file mode 100644
index 0000000..5267e71
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/HelpCommand.java
@@ -0,0 +1,248 @@
+// Copyright 2014 Google Inc. 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.runtime.commands;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.docgen.BlazeRuleHelpPrinter;
+import com.google.devtools.build.lib.analysis.BlazeVersionInfo;
+import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.runtime.BlazeCommand;
+import com.google.devtools.build.lib.runtime.BlazeCommandUtils;
+import com.google.devtools.build.lib.runtime.BlazeModule;
+import com.google.devtools.build.lib.runtime.BlazeRuntime;
+import com.google.devtools.build.lib.runtime.Command;
+import com.google.devtools.build.lib.util.ExitCode;
+import com.google.devtools.build.lib.util.io.OutErr;
+import com.google.devtools.common.options.Converters;
+import com.google.devtools.common.options.Option;
+import com.google.devtools.common.options.OptionsBase;
+import com.google.devtools.common.options.OptionsParser;
+import com.google.devtools.common.options.OptionsProvider;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * The 'blaze help' command, which prints all available commands as well as
+ * specific help pages.
+ */
+@Command(name = "help",
+ options = { HelpCommand.Options.class },
+ allowResidue = true,
+ mustRunInWorkspace = false,
+ shortDescription = "Prints help for commands, or the index.",
+ help = "resource:help.txt")
+public final class HelpCommand implements BlazeCommand {
+ public static class Options extends OptionsBase {
+
+ @Option(name = "help_verbosity",
+ category = "help",
+ defaultValue = "medium",
+ converter = Converters.HelpVerbosityConverter.class,
+ help = "Select the verbosity of the help command.")
+ public OptionsParser.HelpVerbosity helpVerbosity;
+
+ @Option(name = "long",
+ abbrev = 'l',
+ defaultValue = "null",
+ category = "help",
+ expansion = {"--help_verbosity", "long"},
+ help = "Show full description of each option, instead of just its name.")
+ public Void showLongFormOptions;
+
+ @Option(name = "short",
+ defaultValue = "null",
+ category = "help",
+ expansion = {"--help_verbosity", "short"},
+ help = "Show only the names of the options, not their types or meanings.")
+ public Void showShortFormOptions;
+ }
+
+ /**
+ * Returns a map that maps option categories to descriptive help strings for categories that
+ * are not part of the Bazel core.
+ */
+ private ImmutableMap<String, String> getOptionCategories(BlazeRuntime runtime) {
+ ImmutableMap.Builder<String, String> optionCategoriesBuilder = ImmutableMap.builder();
+ optionCategoriesBuilder
+ .put("checking",
+ "Checking options, which control Blaze's error checking and/or warnings")
+ .put("coverage",
+ "Options that affect how Blaze generates code coverage information")
+ .put("experimental",
+ "Experimental options, which control experimental (and potentially risky) features")
+ .put("flags",
+ "Flags options, for passing options to other tools")
+ .put("help",
+ "Help options")
+ .put("host jvm startup",
+ "Options that affect the startup of the Blaze server's JVM")
+ .put("misc",
+ "Miscellaneous options")
+ .put("package loading",
+ "Options that specify how to locate packages")
+ .put("query",
+ "Options affecting the 'blaze query' dependency query command")
+ .put("run",
+ "Options specific to 'blaze run'")
+ .put("semantics",
+ "Semantics options, which affect the build commands and/or output file contents")
+ .put("server startup",
+ "Startup options, which affect the startup of the Blaze server")
+ .put("strategy",
+ "Strategy options, which affect how Blaze will execute the build")
+ .put("testing",
+ "Options that affect how Blaze runs tests")
+ .put("verbosity",
+ "Verbosity options, which control what Blaze prints")
+ .put("version",
+ "Version options, for selecting which version of other tools will be used")
+ .put("what",
+ "Output selection options, for determining what to build/test");
+ for (BlazeModule module : runtime.getBlazeModules()) {
+ optionCategoriesBuilder.putAll(module.getOptionCategories());
+ }
+ return optionCategoriesBuilder.build();
+ }
+
+ @Override
+ public void editOptions(BlazeRuntime runtime, OptionsParser optionsParser) {}
+
+ @Override
+ public ExitCode exec(BlazeRuntime runtime, OptionsProvider options) {
+ OutErr outErr = runtime.getReporter().getOutErr();
+ Options helpOptions = options.getOptions(Options.class);
+ if (options.getResidue().isEmpty()) {
+ emitBlazeVersionInfo(outErr);
+ emitGenericHelp(runtime, outErr);
+ return ExitCode.SUCCESS;
+ }
+ if (options.getResidue().size() != 1) {
+ runtime.getReporter().handle(Event.error("You must specify exactly one command"));
+ return ExitCode.COMMAND_LINE_ERROR;
+ }
+ String helpSubject = options.getResidue().get(0);
+ if (helpSubject.equals("startup_options")) {
+ emitBlazeVersionInfo(outErr);
+ emitStartupOptions(outErr, helpOptions.helpVerbosity, runtime, getOptionCategories(runtime));
+ return ExitCode.SUCCESS;
+ } else if (helpSubject.equals("target-syntax")) {
+ emitBlazeVersionInfo(outErr);
+ emitTargetSyntaxHelp(outErr, getOptionCategories(runtime));
+ return ExitCode.SUCCESS;
+ } else if (helpSubject.equals("info-keys")) {
+ emitInfoKeysHelp(runtime, outErr);
+ return ExitCode.SUCCESS;
+ }
+
+ BlazeCommand command = runtime.getCommandMap().get(helpSubject);
+ if (command == null) {
+ ConfiguredRuleClassProvider provider = runtime.getRuleClassProvider();
+ RuleClass ruleClass = provider.getRuleClassMap().get(helpSubject);
+ if (ruleClass != null && ruleClass.isDocumented()) {
+ // There is a rule with a corresponding name
+ outErr.printOut(BlazeRuleHelpPrinter.getRuleDoc(helpSubject, provider));
+ return ExitCode.SUCCESS;
+ } else {
+ runtime.getReporter().handle(Event.error(
+ null, "'" + helpSubject + "' is neither a command nor a build rule"));
+ return ExitCode.COMMAND_LINE_ERROR;
+ }
+ }
+ emitBlazeVersionInfo(outErr);
+ outErr.printOut(BlazeCommandUtils.getUsage(
+ command.getClass(),
+ getOptionCategories(runtime),
+ helpOptions.helpVerbosity,
+ runtime.getBlazeModules(),
+ runtime.getRuleClassProvider()));
+ return ExitCode.SUCCESS;
+ }
+
+ private void emitBlazeVersionInfo(OutErr outErr) {
+ String releaseInfo = BlazeVersionInfo.instance().getReleaseName();
+ String line = "[Blaze " + releaseInfo + "]";
+ outErr.printOut(String.format("%80s\n", line));
+ }
+
+ @SuppressWarnings("unchecked") // varargs generic array creation
+ private void emitStartupOptions(OutErr outErr, OptionsParser.HelpVerbosity helpVerbosity,
+ BlazeRuntime runtime, ImmutableMap<String, String> optionCategories) {
+ outErr.printOut(
+ BlazeCommandUtils.expandHelpTopic("startup_options",
+ "resource:startup_options.txt",
+ getClass(),
+ BlazeCommandUtils.getStartupOptions(runtime.getBlazeModules()),
+ optionCategories,
+ helpVerbosity));
+ }
+
+ private void emitTargetSyntaxHelp(OutErr outErr, ImmutableMap<String, String> optionCategories) {
+ outErr.printOut(BlazeCommandUtils.expandHelpTopic("target-syntax",
+ "resource:target-syntax.txt",
+ getClass(),
+ ImmutableList.<Class<? extends OptionsBase>>of(),
+ optionCategories,
+ OptionsParser.HelpVerbosity.MEDIUM));
+ }
+
+ private void emitInfoKeysHelp(BlazeRuntime runtime, OutErr outErr) {
+ for (InfoKey key : InfoKey.values()) {
+ outErr.printOut(String.format("%-23s %s\n", key.getName(), key.getDescription()));
+ }
+
+ for (BlazeModule.InfoItem item : InfoCommand.getInfoItemMap(runtime,
+ OptionsParser.newOptionsParser(
+ ImmutableList.<Class<? extends OptionsBase>>of())).values()) {
+ outErr.printOut(String.format("%-23s %s\n", item.getName(), item.getDescription()));
+ }
+ }
+
+ private void emitGenericHelp(BlazeRuntime runtime, OutErr outErr) {
+ outErr.printOut("Usage: blaze <command> <options> ...\n\n");
+
+ outErr.printOut("Available commands:\n");
+
+ Map<String, BlazeCommand> commandsByName = runtime.getCommandMap();
+ List<String> namesInOrder = new ArrayList<>(commandsByName.keySet());
+ Collections.sort(namesInOrder);
+
+ for (String name : namesInOrder) {
+ BlazeCommand command = commandsByName.get(name);
+ Command annotation = command.getClass().getAnnotation(Command.class);
+ if (annotation.hidden()) {
+ continue;
+ }
+
+ String shortDescription = annotation.shortDescription();
+ outErr.printOut(String.format(" %-19s %s\n", name, shortDescription));
+ }
+
+ outErr.printOut("\n");
+ outErr.printOut("Getting more help:\n");
+ outErr.printOut(" blaze help <command>\n");
+ outErr.printOut(" Prints help and options for <command>.\n");
+ outErr.printOut(" blaze help startup_options\n");
+ outErr.printOut(" Options for the JVM hosting Blaze.\n");
+ outErr.printOut(" blaze help target-syntax\n");
+ outErr.printOut(" Explains the syntax for specifying targets.\n");
+ outErr.printOut(" blaze help info-keys\n");
+ outErr.printOut(" Displays a list of keys used by the info command.\n");
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/InfoCommand.java b/src/main/java/com/google/devtools/build/lib/runtime/commands/InfoCommand.java
new file mode 100644
index 0000000..31aaeb1
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/InfoCommand.java
@@ -0,0 +1,448 @@
+// Copyright 2014 Google Inc. 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.runtime.commands;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+import com.google.common.base.Supplier;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.Constants;
+import com.google.devtools.build.lib.analysis.BlazeVersionInfo;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.analysis.config.InvalidConfigurationException;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.packages.Attribute;
+import com.google.devtools.build.lib.packages.ProtoUtils;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.RuleClassProvider;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.pkgcache.PackageCacheOptions;
+import com.google.devtools.build.lib.query2.proto.proto2api.Build.AllowedRuleClassInfo;
+import com.google.devtools.build.lib.query2.proto.proto2api.Build.AttributeDefinition;
+import com.google.devtools.build.lib.query2.proto.proto2api.Build.BuildLanguage;
+import com.google.devtools.build.lib.query2.proto.proto2api.Build.RuleDefinition;
+import com.google.devtools.build.lib.runtime.BlazeCommand;
+import com.google.devtools.build.lib.runtime.BlazeCommandDispatcher;
+import com.google.devtools.build.lib.runtime.BlazeModule;
+import com.google.devtools.build.lib.runtime.BlazeRuntime;
+import com.google.devtools.build.lib.runtime.Command;
+import com.google.devtools.build.lib.util.AbruptExitException;
+import com.google.devtools.build.lib.util.ExitCode;
+import com.google.devtools.build.lib.util.OsUtils;
+import com.google.devtools.build.lib.util.StringUtilities;
+import com.google.devtools.build.lib.util.io.OutErr;
+import com.google.devtools.common.options.Option;
+import com.google.devtools.common.options.OptionsBase;
+import com.google.devtools.common.options.OptionsParser;
+import com.google.devtools.common.options.OptionsProvider;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.lang.management.GarbageCollectorMXBean;
+import java.lang.management.ManagementFactory;
+import java.lang.management.MemoryMXBean;
+import java.lang.management.MemoryUsage;
+import java.nio.charset.StandardCharsets;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * Implementation of 'blaze info'.
+ */
+@Command(name = "info",
+ // TODO(bazel-team): this is not really a build command, but needs access to the
+ // configuration options to do its job
+ builds = true,
+ allowResidue = true,
+ binaryStdOut = true,
+ help = "resource:info.txt",
+ shortDescription = "Displays runtime info about the blaze server.",
+ options = { InfoCommand.Options.class },
+ // We have InfoCommand inherit from {@link BuildCommand} because we want all
+ // configuration defaults specified in ~/.blazerc for {@code build} to apply to
+ // {@code info} too, even though it doesn't actually do a build.
+ //
+ // (Ideally there would be a way to make {@code info} inherit just the bare
+ // minimum of relevant options from {@code build}, i.e. those that affect the
+ // values it prints. But there's no such mechanism.)
+ inherits = { BuildCommand.class })
+public class InfoCommand implements BlazeCommand {
+
+ public static class Options extends OptionsBase {
+ @Option(name = "show_make_env",
+ defaultValue = "false",
+ category = "misc",
+ help = "Include the \"Make\" environment in the output.")
+ public boolean showMakeEnvironment;
+ }
+
+ /**
+ * Unchecked variant of ExitCausingException. Below, we need to throw from the Supplier interface,
+ * which does not allow checked exceptions.
+ */
+ public static class ExitCausingRuntimeException extends RuntimeException {
+
+ private final ExitCode exitCode;
+
+ public ExitCausingRuntimeException(String message, ExitCode exitCode) {
+ super(message);
+ this.exitCode = exitCode;
+ }
+
+ public ExitCausingRuntimeException(ExitCode exitCode) {
+ this.exitCode = exitCode;
+ }
+
+ public ExitCode getExitCode() {
+ return exitCode;
+ }
+ }
+
+ private static class HardwiredInfoItem implements BlazeModule.InfoItem {
+ private final InfoKey key;
+ private final BlazeRuntime runtime;
+ private final OptionsProvider commandOptions;
+
+ private HardwiredInfoItem(InfoKey key, BlazeRuntime runtime, OptionsProvider commandOptions) {
+ this.key = key;
+ this.runtime = runtime;
+ this.commandOptions = commandOptions;
+ }
+
+ @Override
+ public String getName() {
+ return key.getName();
+ }
+
+ @Override
+ public String getDescription() {
+ return key.getDescription();
+ }
+
+ @Override
+ public boolean isHidden() {
+ return key.isHidden();
+ }
+
+ @Override
+ public byte[] get(Supplier<BuildConfiguration> configurationSupplier) {
+ return print(getInfoItem(runtime, key, configurationSupplier, commandOptions));
+ }
+ }
+
+ private static class MakeInfoItem implements BlazeModule.InfoItem {
+ private final String name;
+ private final String value;
+
+ private MakeInfoItem(String name, String value) {
+ this.name = name;
+ this.value = value;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public String getDescription() {
+ return "Make environment variable '" + name + "'";
+ }
+
+ @Override
+ public boolean isHidden() {
+ return false;
+ }
+
+ @Override
+ public byte[] get(Supplier<BuildConfiguration> configurationSupplier) {
+ return print(value);
+ }
+ }
+
+ @Override
+ public void editOptions(BlazeRuntime runtime, OptionsParser optionsParser) { }
+
+ @Override
+ public ExitCode exec(final BlazeRuntime runtime, final OptionsProvider optionsProvider) {
+ Options infoOptions = optionsProvider.getOptions(Options.class);
+
+ OutErr outErr = runtime.getReporter().getOutErr();
+ // Creating a BuildConfiguration is expensive and often unnecessary. Delay the creation until
+ // it is needed.
+ Supplier<BuildConfiguration> configurationSupplier = new Supplier<BuildConfiguration>() {
+ private BuildConfiguration configuration;
+ @Override
+ public BuildConfiguration get() {
+ if (configuration != null) {
+ return configuration;
+ }
+ try {
+ // In order to be able to answer configuration-specific queries, we need to setup the
+ // package path. Since info inherits all the build options, all the necessary information
+ // is available here.
+ runtime.setupPackageCache(
+ optionsProvider.getOptions(PackageCacheOptions.class),
+ runtime.getDefaultsPackageContent(optionsProvider));
+ // TODO(bazel-team): What if there are multiple configurations? [multi-config]
+ configuration = runtime
+ .getConfigurations(optionsProvider)
+ .getTargetConfigurations().get(0);
+ return configuration;
+ } catch (InvalidConfigurationException e) {
+ runtime.getReporter().handle(Event.error(e.getMessage()));
+ throw new ExitCausingRuntimeException(ExitCode.COMMAND_LINE_ERROR);
+ } catch (AbruptExitException e) {
+ throw new ExitCausingRuntimeException("unknown error: " + e.getMessage(),
+ e.getExitCode());
+ } catch (InterruptedException e) {
+ runtime.getReporter().handle(Event.error("interrupted"));
+ throw new ExitCausingRuntimeException(ExitCode.INTERRUPTED);
+ }
+ }
+ };
+
+ Map<String, BlazeModule.InfoItem> items = getInfoItemMap(runtime, optionsProvider);
+
+ try {
+ if (infoOptions.showMakeEnvironment) {
+ Map<String, String> makeEnv = configurationSupplier.get().getMakeEnvironment();
+ for (Map.Entry<String, String> entry : makeEnv.entrySet()) {
+ BlazeModule.InfoItem item = new MakeInfoItem(entry.getKey(), entry.getValue());
+ items.put(item.getName(), item);
+ }
+ }
+
+ List<String> residue = optionsProvider.getResidue();
+ if (residue.size() > 1) {
+ runtime.getReporter().handle(Event.error("at most one key may be specified"));
+ return ExitCode.COMMAND_LINE_ERROR;
+ }
+
+ String key = residue.size() == 1 ? residue.get(0) : null;
+ if (key != null) { // print just the value for the specified key:
+ byte[] value;
+ if (items.containsKey(key)) {
+ value = items.get(key).get(configurationSupplier);
+ } else {
+ runtime.getReporter().handle(Event.error("unknown key: '" + key + "'"));
+ return ExitCode.COMMAND_LINE_ERROR;
+ }
+ try {
+ outErr.getOutputStream().write(value);
+ outErr.getOutputStream().flush();
+ } catch (IOException e) {
+ runtime.getReporter().handle(Event.error("Cannot write info block: " + e.getMessage()));
+ return ExitCode.ANALYSIS_FAILURE;
+ }
+ } else { // print them all
+ configurationSupplier.get(); // We'll need this later anyway
+ for (BlazeModule.InfoItem infoItem : items.values()) {
+ if (infoItem.isHidden()) {
+ continue;
+ }
+ outErr.getOutputStream().write(
+ (infoItem.getName() + ": ").getBytes(StandardCharsets.UTF_8));
+ outErr.getOutputStream().write(infoItem.get(configurationSupplier));
+ }
+ }
+ } catch (AbruptExitException e) {
+ return e.getExitCode();
+ } catch (ExitCausingRuntimeException e) {
+ return e.getExitCode();
+ } catch (IOException e) {
+ return ExitCode.LOCAL_ENVIRONMENTAL_ERROR;
+ }
+ return ExitCode.SUCCESS;
+ }
+
+ /**
+ * Compute and return the info for the given key. Only keys that are not hidden are supported
+ * here.
+ */
+ private static Object getInfoItem(BlazeRuntime runtime, InfoKey key,
+ Supplier<BuildConfiguration> configurationSupplier, OptionsProvider options) {
+ switch (key) {
+ // directories
+ case WORKSPACE : return runtime.getWorkspace();
+ case INSTALL_BASE : return runtime.getDirectories().getInstallBase();
+ case OUTPUT_BASE : return runtime.getOutputBase();
+ case EXECUTION_ROOT : return runtime.getExecRoot();
+ case OUTPUT_PATH : return runtime.getDirectories().getOutputPath();
+ // These are the only (non-hidden) info items that require a configuration, because the
+ // corresponding paths contain the short name. Maybe we should recommend using the symlinks
+ // or make them hidden by default?
+ case BLAZE_BIN : return configurationSupplier.get().getBinDirectory().getPath();
+ case BLAZE_GENFILES : return configurationSupplier.get().getGenfilesDirectory().getPath();
+ case BLAZE_TESTLOGS : return configurationSupplier.get().getTestLogsDirectory().getPath();
+
+ // logs
+ case COMMAND_LOG : return BlazeCommandDispatcher.getCommandLogPath(runtime.getOutputBase());
+ case MESSAGE_LOG :
+ // NB: Duplicated in EventLogModule
+ return runtime.getOutputBase().getRelative("message.log");
+
+ // misc
+ case RELEASE : return BlazeVersionInfo.instance().getReleaseName();
+ case SERVER_PID : return OsUtils.getpid();
+ case PACKAGE_PATH : return getPackagePath(options);
+
+ // memory statistics
+ case GC_COUNT :
+ case GC_TIME :
+ // The documentation is not very clear on what it means to have more than
+ // one GC MXBean, so we just sum them up.
+ int gcCount = 0;
+ int gcTime = 0;
+ for (GarbageCollectorMXBean gcBean : ManagementFactory.getGarbageCollectorMXBeans()) {
+ gcCount += gcBean.getCollectionCount();
+ gcTime += gcBean.getCollectionTime();
+ }
+ if (key == InfoKey.GC_COUNT) {
+ return gcCount + "";
+ } else {
+ return gcTime + "ms";
+ }
+
+ case MAX_HEAP_SIZE :
+ return StringUtilities.prettyPrintBytes(getMemoryUsage().getMax());
+ case USED_HEAP_SIZE :
+ case COMMITTED_HEAP_SIZE :
+ return StringUtilities.prettyPrintBytes(key == InfoKey.USED_HEAP_SIZE ?
+ getMemoryUsage().getUsed() : getMemoryUsage().getCommitted());
+
+ case USED_HEAP_SIZE_AFTER_GC :
+ // Note that this info value is not printed by default, but only when explicitly requested.
+ System.gc();
+ return StringUtilities.prettyPrintBytes(getMemoryUsage().getUsed());
+
+ case DEFAULTS_PACKAGE:
+ return runtime.getDefaultsPackageContent();
+
+ case BUILD_LANGUAGE:
+ return getBuildLanguageDefinition(runtime.getRuleClassProvider());
+
+ case DEFAULT_PACKAGE_PATH:
+ return Joiner.on(":").join(Constants.DEFAULT_PACKAGE_PATH);
+
+ default:
+ throw new IllegalArgumentException("missing implementation for " + key);
+ }
+ }
+
+ private static MemoryUsage getMemoryUsage() {
+ MemoryMXBean memBean = ManagementFactory.getMemoryMXBean();
+ return memBean.getHeapMemoryUsage();
+ }
+
+ /**
+ * Get the package_path variable for the given set of options.
+ */
+ private static String getPackagePath(OptionsProvider options) {
+ PackageCacheOptions packageCacheOptions =
+ options.getOptions(PackageCacheOptions.class);
+ return Joiner.on(":").join(packageCacheOptions.packagePath);
+ }
+
+ private static AllowedRuleClassInfo getAllowedRuleClasses(
+ Collection<RuleClass> ruleClasses, Attribute attr) {
+ AllowedRuleClassInfo.Builder info = AllowedRuleClassInfo.newBuilder();
+ info.setPolicy(AllowedRuleClassInfo.AllowedRuleClasses.ANY);
+
+ if (attr.isStrictLabelCheckingEnabled()) {
+ if (attr.getAllowedRuleClassesPredicate() != Predicates.<RuleClass>alwaysTrue()) {
+ info.setPolicy(AllowedRuleClassInfo.AllowedRuleClasses.SPECIFIED);
+ Predicate<RuleClass> filter = attr.getAllowedRuleClassesPredicate();
+ for (RuleClass otherClass : Iterables.filter(
+ ruleClasses, filter)) {
+ if (otherClass.isDocumented()) {
+ info.addAllowedRuleClass(otherClass.getName());
+ }
+ }
+ }
+ }
+
+ return info.build();
+ }
+
+ /**
+ * Returns a byte array containing a proto-buffer describing the build language.
+ */
+ private static byte[] getBuildLanguageDefinition(RuleClassProvider provider) {
+ BuildLanguage.Builder resultPb = BuildLanguage.newBuilder();
+ Collection<RuleClass> ruleClasses = provider.getRuleClassMap().values();
+ for (RuleClass ruleClass : ruleClasses) {
+ if (!ruleClass.isDocumented()) {
+ continue;
+ }
+
+ RuleDefinition.Builder rulePb = RuleDefinition.newBuilder();
+ rulePb.setName(ruleClass.getName());
+ for (Attribute attr : ruleClass.getAttributes()) {
+ if (!attr.isDocumented()) {
+ continue;
+ }
+
+ AttributeDefinition.Builder attrPb = AttributeDefinition.newBuilder();
+ attrPb.setName(attr.getName());
+ // The protocol compiler, in its infinite wisdom, generates the field as one of the
+ // integer type and the getTypeEnum() method is missing. WTF?
+ attrPb.setType(ProtoUtils.getDiscriminatorFromType(attr.getType()));
+ attrPb.setMandatory(attr.isMandatory());
+
+ if (Type.isLabelType(attr.getType())) {
+ attrPb.setAllowedRuleClasses(getAllowedRuleClasses(ruleClasses, attr));
+ }
+
+ rulePb.addAttribute(attrPb);
+ }
+
+ resultPb.addRule(rulePb);
+ }
+
+ return resultPb.build().toByteArray();
+ }
+
+ private static byte[] print(Object value) {
+ if (value instanceof byte[]) {
+ return (byte[]) value;
+ }
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ PrintWriter writer = new PrintWriter(outputStream);
+ writer.print(value.toString() + "\n");
+ writer.flush();
+ return outputStream.toByteArray();
+ }
+
+ static Map<String, BlazeModule.InfoItem> getInfoItemMap(
+ BlazeRuntime runtime, OptionsProvider commandOptions) {
+ Map<String, BlazeModule.InfoItem> result = new TreeMap<>(); // order by key
+ for (BlazeModule module : runtime.getBlazeModules()) {
+ for (BlazeModule.InfoItem item : module.getInfoItems()) {
+ result.put(item.getName(), item);
+ }
+ }
+
+ for (InfoKey key : InfoKey.values()) {
+ BlazeModule.InfoItem item = new HardwiredInfoItem(key, runtime, commandOptions);
+ result.put(item.getName(), item);
+ }
+
+ return result;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/InfoKey.java b/src/main/java/com/google/devtools/build/lib/runtime/commands/InfoKey.java
new file mode 100644
index 0000000..d2e7bc0
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/InfoKey.java
@@ -0,0 +1,90 @@
+// Copyright 2014 Google Inc. 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.runtime.commands;
+
+
+/**
+ * An enumeration of all the valid info keys, excepting the make environment
+ * variables.
+ */
+public enum InfoKey {
+ // directories
+ WORKSPACE("workspace", "The working directory of the server."),
+ INSTALL_BASE("install_base", "The installation base directory."),
+ OUTPUT_BASE("output_base",
+ "A directory for shared Blaze state as well as tool and strategy specific subdirectories."),
+ EXECUTION_ROOT("execution_root",
+ "A directory that makes all input and output files visible to the build."),
+ OUTPUT_PATH("output_path", "Output directory"),
+ BLAZE_BIN("blaze-bin", "Configuration dependent directory for binaries."),
+ BLAZE_GENFILES("blaze-genfiles", "Configuration dependent directory for generated files."),
+ BLAZE_TESTLOGS("blaze-testlogs", "Configuration dependent directory for logs from a test run."),
+
+ // logs
+ COMMAND_LOG("command_log", "Location of the log containg the output from the build commands."),
+ MESSAGE_LOG("message_log" ,
+ "Location of a log containing machine readable message in LogMessage protobuf format."),
+
+ // misc
+ RELEASE("release", "Blaze release identifier"),
+ SERVER_PID("server_pid", "Blaze process id"),
+ PACKAGE_PATH("package_path", "The search path for resolving package labels."),
+
+ // memory statistics
+ USED_HEAP_SIZE("used-heap-size", "The amount of used memory in bytes. Note that this is not a "
+ + "good indicator of the actual memory use, as it includes any remaining inaccessible "
+ + "memory."),
+ USED_HEAP_SIZE_AFTER_GC("used-heap-size-after-gc",
+ "The amount of used memory in bytes after a call to System.gc().", true),
+ COMMITTED_HEAP_SIZE("committed-heap-size",
+ "The amount of memory in bytes that is committed for the Java virtual machine to use"),
+ MAX_HEAP_SIZE("max-heap-size",
+ "The maximum amount of memory in bytes that can be used for memory management."),
+ GC_COUNT("gc-count", "Number of garbage collection runs."),
+ GC_TIME("gc-time", "The approximate accumulated time spend on garbage collection."),
+
+ // These are deprecated, they still work, when explicitly requested, but are not shown by default
+
+ // These keys print multi-line messages and thus don't play well with grep. We don't print them
+ // unless explicitly requested
+ DEFAULTS_PACKAGE("defaults-package", "Default packages used as implicit dependencies", true),
+ BUILD_LANGUAGE("build-language", "A protobuffer with the build language structure", true),
+ DEFAULT_PACKAGE_PATH("default-package-path", "The default package path", true);
+
+ private final String name;
+ private final String description;
+ private final boolean hidden;
+
+ private InfoKey(String name, String description) {
+ this(name, description, false);
+ }
+
+ private InfoKey(String name, String description, boolean hidden) {
+ this.name = name;
+ this.description = description;
+ this.hidden = hidden;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public boolean isHidden() {
+ return hidden;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/ProfileCommand.java b/src/main/java/com/google/devtools/build/lib/runtime/commands/ProfileCommand.java
new file mode 100644
index 0000000..7b91dc7
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/ProfileCommand.java
@@ -0,0 +1,771 @@
+// Copyright 2014 Google Inc. 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.runtime.commands;
+
+import com.google.common.base.Function;
+import com.google.common.base.Functions;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Ordering;
+import com.google.common.collect.TreeMultimap;
+import com.google.devtools.build.lib.actions.MiddlemanAction;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.profiler.ProfileInfo;
+import com.google.devtools.build.lib.profiler.ProfileInfo.CriticalPathEntry;
+import com.google.devtools.build.lib.profiler.ProfileInfo.InfoListener;
+import com.google.devtools.build.lib.profiler.ProfilePhase;
+import com.google.devtools.build.lib.profiler.ProfilePhaseStatistics;
+import com.google.devtools.build.lib.profiler.ProfilerTask;
+import com.google.devtools.build.lib.profiler.chart.AggregatingChartCreator;
+import com.google.devtools.build.lib.profiler.chart.Chart;
+import com.google.devtools.build.lib.profiler.chart.ChartCreator;
+import com.google.devtools.build.lib.profiler.chart.DetailedChartCreator;
+import com.google.devtools.build.lib.profiler.chart.HtmlChartVisitor;
+import com.google.devtools.build.lib.runtime.BlazeCommand;
+import com.google.devtools.build.lib.runtime.BlazeRuntime;
+import com.google.devtools.build.lib.runtime.Command;
+import com.google.devtools.build.lib.util.ExitCode;
+import com.google.devtools.build.lib.util.StringUtil;
+import com.google.devtools.build.lib.util.TimeUtilities;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.common.options.Converters;
+import com.google.devtools.common.options.Option;
+import com.google.devtools.common.options.OptionsBase;
+import com.google.devtools.common.options.OptionsParser;
+import com.google.devtools.common.options.OptionsProvider;
+
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.EnumMap;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Command line wrapper for analyzing Blaze build profiles.
+ */
+@Command(name = "analyze-profile",
+ options = { ProfileCommand.ProfileOptions.class },
+ shortDescription = "Analyzes build profile data.",
+ help = "resource:analyze-profile.txt",
+ allowResidue = true,
+ mustRunInWorkspace = false)
+public final class ProfileCommand implements BlazeCommand {
+
+ private final String TWO_COLUMN_FORMAT = "%-37s %10s\n";
+ private final String THREE_COLUMN_FORMAT = "%-28s %10s %8s\n";
+
+ public static class DumpConverter extends Converters.StringSetConverter {
+ public DumpConverter() {
+ super("text", "raw", "text-unsorted", "raw-unsorted");
+ }
+ }
+
+ public static class ProfileOptions extends OptionsBase {
+ @Option(name = "dump",
+ abbrev='d',
+ converter = DumpConverter.class,
+ defaultValue = "null",
+ help = "output full profile data dump either in human-readable 'text' format or"
+ + " script-friendly 'raw' format, either sorted or unsorted.")
+ public String dumpMode;
+
+ @Option(name = "html",
+ defaultValue = "false",
+ help = "If present, an HTML file visualizing the tasks of the profiled build is created. "
+ + "The name of the html file is the name of the profile file plus '.html'.")
+ public boolean html;
+
+ @Option(name = "html_pixels_per_second",
+ defaultValue = "50",
+ help = "Defines the scale of the time axis of the task diagram. The unit is "
+ + "pixels per second. Default is 50 pixels per second. ")
+ public int htmlPixelsPerSecond;
+
+ @Option(name = "html_details",
+ defaultValue = "false",
+ help = "If --html_details is present, the task diagram contains all tasks of the profile. "
+ + "If --nohtml_details is present, an aggregated diagram is generated. The default is "
+ + "to generate an aggregated diagram.")
+ public boolean htmlDetails;
+
+ @Option(name = "vfs_stats",
+ defaultValue = "false",
+ help = "If present, include VFS path statistics.")
+ public boolean vfsStats;
+
+ @Option(name = "vfs_stats_limit",
+ defaultValue = "-1",
+ help = "Maximum number of VFS path statistics to print.")
+ public int vfsStatsLimit;
+ }
+
+ private Function<String, String> currentPathMapping = Functions.<String>identity();
+
+ private InfoListener getInfoListener(final BlazeRuntime runtime) {
+ return new InfoListener() {
+ private final EventHandler reporter = runtime.getReporter();
+
+ @Override
+ public void info(String text) {
+ reporter.handle(Event.info(text));
+ }
+
+ @Override
+ public void warn(String text) {
+ reporter.handle(Event.warn(text));
+ }
+ };
+ }
+
+ @Override
+ public void editOptions(BlazeRuntime runtime, OptionsParser optionsParser) {}
+
+ @Override
+ public ExitCode exec(final BlazeRuntime runtime, OptionsProvider options) {
+ ProfileOptions opts =
+ options.getOptions(ProfileOptions.class);
+
+ if (!opts.vfsStats) {
+ opts.vfsStatsLimit = 0;
+ }
+
+ currentPathMapping = new Function<String, String>() {
+ @Override
+ public String apply(String input) {
+ if (runtime.getWorkspaceName().isEmpty()) {
+ return input;
+ } else {
+ return input.substring(input.lastIndexOf("/" + runtime.getWorkspaceName()) + 1);
+ }
+ }
+ };
+
+ PrintStream out = new PrintStream(runtime.getReporter().getOutErr().getOutputStream());
+ try {
+ runtime.getReporter().handle(Event.warn(
+ null, "This information is intended for consumption by Blaze developers"
+ + " only, and may change at any time. Script against it at your own risk"));
+
+ for (String name : options.getResidue()) {
+ Path profileFile = runtime.getWorkingDirectory().getRelative(name);
+ try {
+ ProfileInfo info = ProfileInfo.loadProfileVerbosely(
+ profileFile, getInfoListener(runtime));
+ if (opts.dumpMode != null) {
+ dumpProfile(runtime, info, out, opts.dumpMode);
+ } else if (opts.html) {
+ createHtml(runtime, info, profileFile, opts);
+ } else {
+ createText(runtime, info, out, opts);
+ }
+ } catch (IOException e) {
+ runtime.getReporter().handle(Event.error(
+ null, "Failed to process file " + name + ": " + e.getMessage()));
+ }
+ }
+ } finally {
+ out.flush();
+ }
+ return ExitCode.SUCCESS;
+ }
+
+ private void createText(BlazeRuntime runtime, ProfileInfo info, PrintStream out,
+ ProfileOptions opts) {
+ List<ProfilePhaseStatistics> statistics = getStatistics(runtime, info, opts);
+
+ for (ProfilePhaseStatistics stat : statistics) {
+ String title = stat.getTitle();
+
+ if (!title.equals("")) {
+ out.println("\n=== " + title.toUpperCase() + " ===\n");
+ }
+ out.print(stat.getStatistics());
+ }
+ }
+
+ private void createHtml(BlazeRuntime runtime, ProfileInfo info, Path profileFile,
+ ProfileOptions opts)
+ throws IOException {
+ Path htmlFile =
+ profileFile.getParentDirectory().getChild(profileFile.getBaseName() + ".html");
+ List<ProfilePhaseStatistics> statistics = getStatistics(runtime, info, opts);
+
+ runtime.getReporter().handle(Event.info("Creating HTML output in " + htmlFile));
+
+ ChartCreator chartCreator =
+ opts.htmlDetails ? new DetailedChartCreator(info, statistics)
+ : new AggregatingChartCreator(info, statistics);
+ Chart chart = chartCreator.create();
+ OutputStream out = new BufferedOutputStream(htmlFile.getOutputStream());
+ try {
+ chart.accept(new HtmlChartVisitor(new PrintStream(out), opts.htmlPixelsPerSecond));
+ } finally {
+ try {
+ out.close();
+ } catch (IOException e) {
+ // Ignore
+ }
+ }
+ }
+
+ private List<ProfilePhaseStatistics> getStatistics(
+ BlazeRuntime runtime, ProfileInfo info, ProfileOptions opts) {
+ try {
+ ProfileInfo.aggregateProfile(info, getInfoListener(runtime));
+ runtime.getReporter().handle(Event.info("Analyzing relationships"));
+
+ info.analyzeRelationships();
+
+ List<ProfilePhaseStatistics> statistics = new ArrayList<>();
+
+ // Print phase durations and total execution time
+ ByteArrayOutputStream byteOutput = new ByteArrayOutputStream();
+ PrintStream out = new PrintStream(byteOutput, false, "UTF-8");
+ long duration = 0;
+ for (ProfilePhase phase : ProfilePhase.values()) {
+ ProfileInfo.Task phaseTask = info.getPhaseTask(phase);
+ if (phaseTask != null) {
+ duration += info.getPhaseDuration(phaseTask);
+ }
+ }
+ for (ProfilePhase phase : ProfilePhase.values()) {
+ ProfileInfo.Task phaseTask = info.getPhaseTask(phase);
+ if (phaseTask != null) {
+ long phaseDuration = info.getPhaseDuration(phaseTask);
+ out.printf(THREE_COLUMN_FORMAT, "Total " + phase.nick + " phase time",
+ TimeUtilities.prettyTime(phaseDuration), prettyPercentage(phaseDuration, duration));
+ }
+ }
+ out.printf(THREE_COLUMN_FORMAT, "Total run time", TimeUtilities.prettyTime(duration),
+ "100.00%");
+ statistics.add(new ProfilePhaseStatistics("Phase Summary Information",
+ new String(byteOutput.toByteArray(), "UTF-8")));
+
+ // Print details of major phases
+ if (duration > 0) {
+ statistics.add(formatInitPhaseStatistics(info, opts));
+ statistics.add(formatLoadingPhaseStatistics(info, opts));
+ statistics.add(formatAnalysisPhaseStatistics(info, opts));
+ ProfilePhaseStatistics stat = formatExecutionPhaseStatistics(info, opts);
+ if (stat != null) {
+ statistics.add(stat);
+ }
+ }
+
+ return statistics;
+ } catch (UnsupportedEncodingException e) {
+ throw new AssertionError("Should not happen since, UTF8 is available on all JVMs");
+ }
+ }
+
+ private void dumpProfile(
+ BlazeRuntime runtime, ProfileInfo info, PrintStream out, String dumpMode) {
+ if (!dumpMode.contains("unsorted")) {
+ ProfileInfo.aggregateProfile(info, getInfoListener(runtime));
+ }
+ if (dumpMode.contains("raw")) {
+ for (ProfileInfo.Task task : info.allTasksById) {
+ dumpRaw(task, out);
+ }
+ } else if (dumpMode.contains("unsorted")) {
+ for (ProfileInfo.Task task : info.allTasksById) {
+ dumpTask(task, out, 0);
+ }
+ } else {
+ for (ProfileInfo.Task task : info.rootTasksById) {
+ dumpTask(task, out, 0);
+ }
+ }
+ }
+
+ private void dumpTask(ProfileInfo.Task task, PrintStream out, int indent) {
+ StringBuilder builder = new StringBuilder(String.format(
+ "\n%s %s\nThread: %-6d Id: %-6d Parent: %d\nStart time: %-12s Duration: %s",
+ task.type, task.getDescription(), task.threadId, task.id, task.parentId,
+ TimeUtilities.prettyTime(task.startTime), TimeUtilities.prettyTime(task.duration)));
+ if (task.hasStats()) {
+ builder.append("\n");
+ ProfileInfo.AggregateAttr[] stats = task.getStatAttrArray();
+ for (ProfilerTask type : ProfilerTask.values()) {
+ ProfileInfo.AggregateAttr attr = stats[type.ordinal()];
+ if (attr != null) {
+ builder.append(type.toString().toLowerCase()).append("=(").
+ append(attr.count).append(", ").
+ append(TimeUtilities.prettyTime(attr.totalTime)).append(") ");
+ }
+ }
+ }
+ out.println(StringUtil.indent(builder.toString(), indent));
+ for (ProfileInfo.Task subtask : task.subtasks) {
+ dumpTask(subtask, out, indent + 1);
+ }
+ }
+
+ private void dumpRaw(ProfileInfo.Task task, PrintStream out) {
+ StringBuilder aggregateString = new StringBuilder();
+ ProfileInfo.AggregateAttr[] stats = task.getStatAttrArray();
+ for (ProfilerTask type : ProfilerTask.values()) {
+ ProfileInfo.AggregateAttr attr = stats[type.ordinal()];
+ if (attr != null) {
+ aggregateString.append(type.toString().toLowerCase()).append(",").
+ append(attr.count).append(",").append(attr.totalTime).append(" ");
+ }
+ }
+ out.println(
+ task.threadId + "|" + task.id + "|" + task.parentId + "|"
+ + task.startTime + "|" + task.duration + "|"
+ + aggregateString.toString().trim() + "|"
+ + task.type + "|" + task.getDescription());
+ }
+
+ /**
+ * Converts relative duration to the percentage string
+ * @return formatted percentage string or "N/A" if result is undefined.
+ */
+ private static String prettyPercentage(long duration, long total) {
+ if (total == 0) {
+ // Return "not available" string if total is 0 and result is undefined.
+ return "N/A";
+ }
+ return String.format("%5.2f%%", duration*100.0/total);
+ }
+
+ private void printCriticalPath(String title, PrintStream out, CriticalPathEntry path) {
+ out.println(String.format("\n%s (%s):", title,
+ TimeUtilities.prettyTime(path.cumulativeDuration)));
+
+ boolean lightCriticalPath = isLightCriticalPath(path);
+ out.println(lightCriticalPath ?
+ String.format("%6s %11s %8s %s", "Id", "Time", "Percentage", "Description")
+ : String.format("%6s %11s %8s %8s %s", "Id", "Time", "Share", "Critical", "Description"));
+
+ long totalPathTime = path.cumulativeDuration;
+ int middlemanCount = 0;
+ long middlemanDuration = 0L;
+ long middlemanCritTime = 0L;
+
+ for (; path != null ; path = path.next) {
+ if (path.task.id < 0) {
+ // Ignore fake actions.
+ continue;
+ } else if (path.task.getDescription().startsWith(MiddlemanAction.MIDDLEMAN_MNEMONIC + " ")
+ || path.task.getDescription().startsWith("TargetCompletionMiddleman")) {
+ // Aggregate middleman actions.
+ middlemanCount++;
+ middlemanDuration += path.duration;
+ middlemanCritTime += path.getCriticalTime();
+ } else {
+ String desc = path.task.getDescription().replace(':', ' ');
+ if (lightCriticalPath) {
+ out.println(String.format("%6d %11s %8s %s", path.task.id,
+ TimeUtilities.prettyTime(path.duration),
+ prettyPercentage(path.duration, totalPathTime),
+ desc));
+ } else {
+ out.println(String.format("%6d %11s %8s %8s %s", path.task.id,
+ TimeUtilities.prettyTime(path.duration),
+ prettyPercentage(path.duration, totalPathTime),
+ prettyPercentage(path.getCriticalTime(), totalPathTime), desc));
+ }
+ }
+ }
+ if (middlemanCount > 0) {
+ if (lightCriticalPath) {
+ out.println(String.format(" %11s %8s [%d middleman actions]",
+ TimeUtilities.prettyTime(middlemanDuration),
+ prettyPercentage(middlemanDuration, totalPathTime),
+ middlemanCount));
+ } else {
+ out.println(String.format(" %11s %8s %8s [%d middleman actions]",
+ TimeUtilities.prettyTime(middlemanDuration),
+ prettyPercentage(middlemanDuration, totalPathTime),
+ prettyPercentage(middlemanCritTime, totalPathTime), middlemanCount));
+ }
+ }
+ }
+
+ private boolean isLightCriticalPath(CriticalPathEntry path) {
+ return path.task.type == ProfilerTask.CRITICAL_PATH_COMPONENT;
+ }
+
+ private void printShortPhaseAnalysis(ProfileInfo info, PrintStream out, ProfilePhase phase) {
+ ProfileInfo.Task phaseTask = info.getPhaseTask(phase);
+ if (phaseTask != null) {
+ long phaseDuration = info.getPhaseDuration(phaseTask);
+ out.printf(TWO_COLUMN_FORMAT, "Total " + phase.nick + " phase time",
+ TimeUtilities.prettyTime(phaseDuration));
+ printTimeDistributionByType(info, out, phaseTask);
+ }
+ }
+
+ private void printTimeDistributionByType(ProfileInfo info, PrintStream out,
+ ProfileInfo.Task phaseTask) {
+ List<ProfileInfo.Task> taskList = info.getTasksForPhase(phaseTask);
+ long phaseDuration = info.getPhaseDuration(phaseTask);
+ long totalDuration = phaseDuration;
+ for (ProfileInfo.Task task : taskList) {
+ // Tasks on the phaseTask thread already accounted for in the phaseDuration.
+ if (task.threadId != phaseTask.threadId) {
+ totalDuration += task.duration;
+ }
+ }
+ boolean headerNeeded = true;
+ for (ProfilerTask type : ProfilerTask.values()) {
+ ProfileInfo.AggregateAttr stats = info.getStatsForType(type, taskList);
+ if (stats.count > 0 && stats.totalTime > 0) {
+ if (headerNeeded) {
+ out.println("\nTotal time (across all threads) spent on:");
+ out.println(String.format("%18s %8s %8s %11s", "Type", "Total", "Count", "Average"));
+ headerNeeded = false;
+ }
+ out.println(String.format("%18s %8s %8d %11s", type.toString(),
+ prettyPercentage(stats.totalTime, totalDuration), stats.count,
+ TimeUtilities.prettyTime(stats.totalTime / stats.count)));
+ }
+ }
+ }
+
+ static class Stat implements Comparable<Stat> {
+ public long duration;
+ public long frequency;
+
+ @Override
+ public int compareTo(Stat o) {
+ return this.duration == o.duration ? Long.compare(this.frequency, o.frequency)
+ : Long.compare(this.duration, o.duration);
+ }
+ }
+
+ /**
+ * Print the time spent on VFS operations on each path. Output is grouped by operation and sorted
+ * by descending duration. If multiple of the same VFS operation were logged for the same path,
+ * print the total duration.
+ *
+ * @param info profiling data.
+ * @param out output stream.
+ * @param phase build phase.
+ * @param limit maximum number of statistics to print, or -1 for no limit.
+ */
+ private void printVfsStatistics(ProfileInfo info, PrintStream out,
+ ProfilePhase phase, int limit) {
+ ProfileInfo.Task phaseTask = info.getPhaseTask(phase);
+ if (phaseTask == null) {
+ return;
+ }
+
+ if (limit == 0) {
+ return;
+ }
+
+ // Group into VFS operations and build maps from path to duration.
+
+ List<ProfileInfo.Task> taskList = info.getTasksForPhase(phaseTask);
+ EnumMap<ProfilerTask, Map<String, Stat>> stats = Maps.newEnumMap(ProfilerTask.class);
+
+ collectVfsEntries(stats, taskList);
+
+ if (!stats.isEmpty()) {
+ out.printf("\nVFS path statistics:\n");
+ out.printf("%15s %10s %10s %s\n", "Type", "Frequency", "Duration", "Path");
+ }
+
+ // Reverse the maps to get maps from duration to path. We use a TreeMultimap to sort by duration
+ // and because durations are not unique.
+
+ for (ProfilerTask type : stats.keySet()) {
+ Map<String, Stat> statsForType = stats.get(type);
+ TreeMultimap<Stat, String> sortedStats =
+ TreeMultimap.create(Ordering.natural().reverse(), Ordering.natural());
+
+ for (Map.Entry<String, Stat> stat : statsForType.entrySet()) {
+ sortedStats.put(stat.getValue(), stat.getKey());
+ }
+
+ int numPrinted = 0;
+ for (Map.Entry<Stat, String> stat : sortedStats.entries()) {
+ if (limit != -1 && numPrinted++ == limit) {
+ out.printf("... %d more ...\n", sortedStats.size() - limit);
+ break;
+ }
+ out.printf("%15s %10d %10s %s\n",
+ type.name(), stat.getKey().frequency, TimeUtilities.prettyTime(stat.getKey().duration),
+ stat.getValue());
+ }
+ }
+ }
+
+ private void collectVfsEntries(EnumMap<ProfilerTask, Map<String, Stat>> stats,
+ List<ProfileInfo.Task> taskList) {
+ for (ProfileInfo.Task task : taskList) {
+ collectVfsEntries(stats, Arrays.asList(task.subtasks));
+ if (!task.type.name().startsWith("VFS_")) {
+ continue;
+ }
+
+ Map<String, Stat> statsForType = stats.get(task.type);
+ if (statsForType == null) {
+ statsForType = Maps.newHashMap();
+ stats.put(task.type, statsForType);
+ }
+
+ String path = currentPathMapping.apply(task.getDescription());
+
+ Stat stat = statsForType.get(path);
+ if (stat == null) {
+ stat = new Stat();
+ }
+
+ stat.duration += task.duration;
+ stat.frequency++;
+ statsForType.put(path, stat);
+ }
+ }
+
+ /**
+ * Returns set of profiler tasks to be filtered from critical path.
+ * Also always filters out ACTION_LOCK and WAIT tasks to simulate
+ * unlimited resource critical path (see comments inside formatExecutionPhaseStatistics()
+ * method).
+ */
+ private EnumSet<ProfilerTask> getTypeFilter(ProfilerTask... tasks) {
+ EnumSet<ProfilerTask> filter = EnumSet.of(ProfilerTask.ACTION_LOCK, ProfilerTask.WAIT);
+ for (ProfilerTask task : tasks) {
+ filter.add(task);
+ }
+ return filter;
+ }
+
+ private ProfilePhaseStatistics formatInitPhaseStatistics(ProfileInfo info, ProfileOptions opts)
+ throws UnsupportedEncodingException {
+ return formatSimplePhaseStatistics(info, opts, "Init", ProfilePhase.INIT);
+ }
+
+ private ProfilePhaseStatistics formatLoadingPhaseStatistics(ProfileInfo info, ProfileOptions opts)
+ throws UnsupportedEncodingException {
+ return formatSimplePhaseStatistics(info, opts, "Loading", ProfilePhase.LOAD);
+ }
+
+ private ProfilePhaseStatistics formatAnalysisPhaseStatistics(ProfileInfo info,
+ ProfileOptions opts)
+ throws UnsupportedEncodingException {
+ return formatSimplePhaseStatistics(info, opts, "Analysis", ProfilePhase.ANALYZE);
+ }
+
+ private ProfilePhaseStatistics formatSimplePhaseStatistics(ProfileInfo info,
+ ProfileOptions opts,
+ String name,
+ ProfilePhase phase)
+ throws UnsupportedEncodingException {
+ ByteArrayOutputStream byteOutput = new ByteArrayOutputStream();
+ PrintStream out = new PrintStream(byteOutput, false, "UTF-8");
+
+ printShortPhaseAnalysis(info, out, phase);
+ printVfsStatistics(info, out, phase, opts.vfsStatsLimit);
+ return new ProfilePhaseStatistics(name + " Phase Information",
+ new String(byteOutput.toByteArray(), "UTF-8"));
+ }
+
+ private ProfilePhaseStatistics formatExecutionPhaseStatistics(ProfileInfo info,
+ ProfileOptions opts)
+ throws UnsupportedEncodingException {
+ ByteArrayOutputStream byteOutput = new ByteArrayOutputStream();
+ PrintStream out = new PrintStream(byteOutput, false, "UTF-8");
+
+ ProfileInfo.Task prepPhase = info.getPhaseTask(ProfilePhase.PREPARE);
+ ProfileInfo.Task execPhase = info.getPhaseTask(ProfilePhase.EXECUTE);
+ ProfileInfo.Task finishPhase = info.getPhaseTask(ProfilePhase.FINISH);
+ if (execPhase == null) {
+ return null;
+ }
+
+ List<ProfileInfo.Task> execTasks = info.getTasksForPhase(execPhase);
+ long graphTime = info.getStatsForType(ProfilerTask.ACTION_GRAPH, execTasks).totalTime;
+ long execTime = info.getPhaseDuration(execPhase) - graphTime;
+
+ if (prepPhase != null) {
+ out.printf(TWO_COLUMN_FORMAT, "Total preparation time",
+ TimeUtilities.prettyTime(info.getPhaseDuration(prepPhase)));
+ }
+ out.printf(TWO_COLUMN_FORMAT, "Total execution phase time",
+ TimeUtilities.prettyTime(info.getPhaseDuration(execPhase)));
+ if (finishPhase != null) {
+ out.printf(TWO_COLUMN_FORMAT, "Total time finalizing build",
+ TimeUtilities.prettyTime(info.getPhaseDuration(finishPhase)));
+ }
+ out.println("");
+ out.printf(TWO_COLUMN_FORMAT, "Action dependency map creation",
+ TimeUtilities.prettyTime(graphTime));
+ out.printf(TWO_COLUMN_FORMAT, "Actual execution time",
+ TimeUtilities.prettyTime(execTime));
+
+ EnumSet<ProfilerTask> typeFilter = EnumSet.noneOf(ProfilerTask.class);
+ CriticalPathEntry totalPath = info.getCriticalPath(typeFilter);
+ info.analyzeCriticalPath(typeFilter, totalPath);
+
+ typeFilter = getTypeFilter();
+ CriticalPathEntry optimalPath = info.getCriticalPath(typeFilter);
+ info.analyzeCriticalPath(typeFilter, optimalPath);
+
+ if (totalPath != null) {
+ printCriticalPathTimingBreakdown(info, totalPath, optimalPath, execTime, out);
+ } else {
+ out.println("\nCritical path not available because no action graph was generated.");
+ }
+
+ printTimeDistributionByType(info, out, execPhase);
+
+ if (totalPath != null) {
+ printCriticalPath("Critical path", out, totalPath);
+ // In light critical path we do not record scheduling delay data so it does not make sense
+ // to differentiate it.
+ if (!isLightCriticalPath(totalPath)) {
+ printCriticalPath("Critical path excluding scheduling delays", out, optimalPath);
+ }
+ }
+
+ if (info.getMissingActionsCount() > 0) {
+ out.println("\n" + info.getMissingActionsCount() + " action(s) are present in the"
+ + " action graph but missing instrumentation data. Most likely profile file"
+ + " has been created for the failed or aborted build.");
+ }
+
+ printVfsStatistics(info, out, ProfilePhase.EXECUTE, opts.vfsStatsLimit);
+
+ return new ProfilePhaseStatistics("Execution Phase Information",
+ new String(byteOutput.toByteArray(), "UTF-8"));
+ }
+
+ void printCriticalPathTimingBreakdown(ProfileInfo info, CriticalPathEntry totalPath,
+ CriticalPathEntry optimalPath, long execTime, PrintStream out) {
+ Preconditions.checkNotNull(totalPath);
+ Preconditions.checkNotNull(optimalPath);
+ // TODO(bazel-team): Print remote vs build stats recorded by CriticalPathStats
+ if (isLightCriticalPath(totalPath)) {
+ return;
+ }
+ out.println(totalPath.task.type);
+ // Worker thread pool scheduling delays for the actual critical path.
+ long workerWaitTime = 0;
+ long mainThreadWaitTime = 0;
+ for (ProfileInfo.CriticalPathEntry entry = totalPath; entry != null; entry = entry.next) {
+ workerWaitTime += info.getActionWaitTime(entry.task);
+ mainThreadWaitTime += info.getActionQueueTime(entry.task);
+ }
+ out.printf(TWO_COLUMN_FORMAT, "Worker thread scheduling delays",
+ TimeUtilities.prettyTime(workerWaitTime));
+ out.printf(TWO_COLUMN_FORMAT, "Main thread scheduling delays",
+ TimeUtilities.prettyTime(mainThreadWaitTime));
+
+ out.println("\nCritical path time:");
+ // Actual critical path.
+ long totalTime = totalPath.cumulativeDuration;
+ out.printf("%-37s %10s (%s of execution time)\n", "Actual time",
+ TimeUtilities.prettyTime(totalTime),
+ prettyPercentage(totalTime, execTime));
+ // Unlimited resource critical path. Essentially, we assume that if we
+ // remove all scheduling delays caused by resource semaphore contention,
+ // each action execution time would not change (even though load now would
+ // be substantially higher - so this assumption might be incorrect but it is
+ // still useful for modeling). Given those assumptions we calculate critical
+ // path excluding scheduling delays.
+ long optimalTime = optimalPath.cumulativeDuration;
+ out.printf("%-37s %10s (%s of execution time)\n", "Time excluding scheduling delays",
+ TimeUtilities.prettyTime(optimalTime),
+ prettyPercentage(optimalTime, execTime));
+
+ // Artificial critical path if we ignore all the time spent in all tasks,
+ // except time directly attributed to the ACTION tasks.
+ out.println("\nTime related to:");
+
+ EnumSet<ProfilerTask> typeFilter = EnumSet.allOf(ProfilerTask.class);
+ ProfileInfo.CriticalPathEntry path = info.getCriticalPath(typeFilter);
+ out.printf(TWO_COLUMN_FORMAT, "the builder overhead",
+ prettyPercentage(path.cumulativeDuration, totalTime));
+
+ typeFilter = getTypeFilter();
+ for (ProfilerTask task : ProfilerTask.values()) {
+ if (task.name().startsWith("VFS_")) {
+ typeFilter.add(task);
+ }
+ }
+ path = info.getCriticalPath(typeFilter);
+ out.printf(TWO_COLUMN_FORMAT, "the VFS calls",
+ prettyPercentage(optimalTime - path.cumulativeDuration, optimalTime));
+
+ typeFilter = getTypeFilter(ProfilerTask.ACTION_CHECK);
+ path = info.getCriticalPath(typeFilter);
+ out.printf(TWO_COLUMN_FORMAT, "the dependency checking",
+ prettyPercentage(optimalTime - path.cumulativeDuration, optimalTime));
+
+ typeFilter = getTypeFilter(ProfilerTask.ACTION_EXECUTE);
+ path = info.getCriticalPath(typeFilter);
+ out.printf(TWO_COLUMN_FORMAT, "the execution setup",
+ prettyPercentage(optimalTime - path.cumulativeDuration, optimalTime));
+
+ typeFilter = getTypeFilter(ProfilerTask.SPAWN, ProfilerTask.LOCAL_EXECUTION);
+ path = info.getCriticalPath(typeFilter);
+ out.printf(TWO_COLUMN_FORMAT, "local execution",
+ prettyPercentage(optimalTime - path.cumulativeDuration, optimalTime));
+
+ typeFilter = getTypeFilter(ProfilerTask.SCANNER);
+ path = info.getCriticalPath(typeFilter);
+ out.printf(TWO_COLUMN_FORMAT, "the include scanner",
+ prettyPercentage(optimalTime - path.cumulativeDuration, optimalTime));
+
+ typeFilter = getTypeFilter(ProfilerTask.REMOTE_EXECUTION, ProfilerTask.PROCESS_TIME,
+ ProfilerTask.LOCAL_PARSE, ProfilerTask.UPLOAD_TIME,
+ ProfilerTask.REMOTE_QUEUE, ProfilerTask.REMOTE_SETUP, ProfilerTask.FETCH);
+ path = info.getCriticalPath(typeFilter);
+ out.printf(TWO_COLUMN_FORMAT, "Remote execution (cumulative)",
+ prettyPercentage(optimalTime - path.cumulativeDuration, optimalTime));
+
+ typeFilter = getTypeFilter( ProfilerTask.UPLOAD_TIME, ProfilerTask.REMOTE_SETUP);
+ path = info.getCriticalPath(typeFilter);
+ out.printf(TWO_COLUMN_FORMAT, " file uploads",
+ prettyPercentage(optimalTime - path.cumulativeDuration, optimalTime));
+
+ typeFilter = getTypeFilter(ProfilerTask.FETCH);
+ path = info.getCriticalPath(typeFilter);
+ out.printf(TWO_COLUMN_FORMAT, " file fetching",
+ prettyPercentage(optimalTime - path.cumulativeDuration, optimalTime));
+
+ typeFilter = getTypeFilter(ProfilerTask.PROCESS_TIME);
+ path = info.getCriticalPath(typeFilter);
+ out.printf(TWO_COLUMN_FORMAT, " process time",
+ prettyPercentage(optimalTime - path.cumulativeDuration, optimalTime));
+
+ typeFilter = getTypeFilter(ProfilerTask.REMOTE_QUEUE);
+ path = info.getCriticalPath(typeFilter);
+ out.printf(TWO_COLUMN_FORMAT, " remote queueing",
+ prettyPercentage(optimalTime - path.cumulativeDuration, optimalTime));
+
+ typeFilter = getTypeFilter(ProfilerTask.LOCAL_PARSE);
+ path = info.getCriticalPath(typeFilter);
+ out.printf(TWO_COLUMN_FORMAT, " remote execution parse",
+ prettyPercentage(optimalTime - path.cumulativeDuration, optimalTime));
+
+ typeFilter = getTypeFilter(ProfilerTask.REMOTE_EXECUTION);
+ path = info.getCriticalPath(typeFilter);
+ out.printf(TWO_COLUMN_FORMAT, " other remote activities",
+ prettyPercentage(optimalTime - path.cumulativeDuration, optimalTime));
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/ProjectFileSupport.java b/src/main/java/com/google/devtools/build/lib/runtime/commands/ProjectFileSupport.java
new file mode 100644
index 0000000..2e5faf6
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/ProjectFileSupport.java
@@ -0,0 +1,93 @@
+// Copyright 2014 Google Inc. 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.runtime.commands;
+
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.pkgcache.PackageCacheOptions;
+import com.google.devtools.build.lib.pkgcache.PathPackageLocator;
+import com.google.devtools.build.lib.runtime.BlazeCommand;
+import com.google.devtools.build.lib.runtime.BlazeRuntime;
+import com.google.devtools.build.lib.runtime.CommonCommandOptions;
+import com.google.devtools.build.lib.runtime.ProjectFile;
+import com.google.devtools.build.lib.util.AbruptExitException;
+import com.google.devtools.build.lib.util.ExitCode;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.common.options.OptionPriority;
+import com.google.devtools.common.options.OptionsParser;
+import com.google.devtools.common.options.OptionsParsingException;
+import com.google.devtools.common.options.OptionsProvider;
+
+import java.util.List;
+
+/**
+ * Provides support for implementations for {@link BlazeCommand} to work with {@link ProjectFile}.
+ */
+public final class ProjectFileSupport {
+ static final String PROJECT_FILE_PREFIX = "+";
+
+ private ProjectFileSupport() {}
+
+ /**
+ * Reads any project files specified on the command line and updates the options parser
+ * accordingly. If project files cannot be read or if they contain unparsable options, or if they
+ * are not enabled, then it throws an exception instead.
+ */
+ public static void handleProjectFiles(BlazeRuntime runtime, OptionsParser optionsParser,
+ String command) throws AbruptExitException {
+ List<String> targets = optionsParser.getResidue();
+ ProjectFile.Provider projectFileProvider = runtime.getProjectFileProvider();
+ if (projectFileProvider != null && targets.size() > 0
+ && targets.get(0).startsWith(PROJECT_FILE_PREFIX)) {
+ if (targets.size() > 1) {
+ throw new AbruptExitException("Cannot handle more than one +<file> argument yet",
+ ExitCode.COMMAND_LINE_ERROR);
+ }
+ if (!optionsParser.getOptions(CommonCommandOptions.class).allowProjectFiles) {
+ throw new AbruptExitException("project file support is not enabled",
+ ExitCode.COMMAND_LINE_ERROR);
+ }
+ // TODO(bazel-team): This is currently treated as a path relative to the workspace - if the
+ // cwd is a subdirectory of the workspace, that will be surprising, and we should interpret it
+ // relative to the cwd instead.
+ PathFragment projectFilePath = new PathFragment(targets.get(0).substring(1));
+ List<Path> packagePath = PathPackageLocator.create(
+ optionsParser.getOptions(PackageCacheOptions.class).packagePath, runtime.getReporter(),
+ runtime.getWorkspace(), runtime.getWorkingDirectory()).getPathEntries();
+ ProjectFile projectFile = projectFileProvider.getProjectFile(packagePath, projectFilePath);
+ runtime.getReporter().handle(Event.info("Using " + projectFile.getName()));
+
+ try {
+ optionsParser.parse(
+ OptionPriority.RC_FILE, projectFile.getName(), projectFile.getCommandLineFor(command));
+ } catch (OptionsParsingException e) {
+ throw new AbruptExitException(e.getMessage(), ExitCode.COMMAND_LINE_ERROR);
+ }
+ }
+ }
+
+ /**
+ * Returns a list of targets from the options residue. If a project file is supplied as the first
+ * argument, it will be ignored, on the assumption that handleProjectFiles() has been called to
+ * process it.
+ */
+ public static List<String> getTargets(BlazeRuntime runtime, OptionsProvider options) {
+ List<String> targets = options.getResidue();
+ if (runtime.getProjectFileProvider() != null && targets.size() > 0
+ && targets.get(0).startsWith(PROJECT_FILE_PREFIX)) {
+ return targets.subList(1, targets.size());
+ }
+ return targets;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/QueryCommand.java b/src/main/java/com/google/devtools/build/lib/runtime/commands/QueryCommand.java
new file mode 100644
index 0000000..c5120cb
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/QueryCommand.java
@@ -0,0 +1,173 @@
+// Copyright 2014 Google Inc. 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.runtime.commands;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.pkgcache.PackageCacheOptions;
+import com.google.devtools.build.lib.query2.BlazeQueryEnvironment;
+import com.google.devtools.build.lib.query2.SkyframeQueryEnvironment;
+import com.google.devtools.build.lib.query2.engine.BlazeQueryEvalResult;
+import com.google.devtools.build.lib.query2.engine.QueryEnvironment.QueryFunction;
+import com.google.devtools.build.lib.query2.engine.QueryEnvironment.Setting;
+import com.google.devtools.build.lib.query2.engine.QueryException;
+import com.google.devtools.build.lib.query2.engine.QueryExpression;
+import com.google.devtools.build.lib.query2.output.OutputFormatter;
+import com.google.devtools.build.lib.query2.output.OutputFormatter.UnorderedFormatter;
+import com.google.devtools.build.lib.query2.output.QueryOptions;
+import com.google.devtools.build.lib.runtime.BlazeCommand;
+import com.google.devtools.build.lib.runtime.BlazeModule;
+import com.google.devtools.build.lib.runtime.BlazeRuntime;
+import com.google.devtools.build.lib.runtime.Command;
+import com.google.devtools.build.lib.util.AbruptExitException;
+import com.google.devtools.build.lib.util.ExitCode;
+import com.google.devtools.common.options.OptionsParser;
+import com.google.devtools.common.options.OptionsProvider;
+
+import java.io.IOException;
+import java.io.PrintStream;
+import java.nio.channels.ClosedByInterruptException;
+import java.util.Set;
+
+/**
+ * Command line wrapper for executing a query with blaze.
+ */
+@Command(name = "query",
+ options = { PackageCacheOptions.class,
+ QueryOptions.class },
+ help = "resource:query.txt",
+ shortDescription = "Executes a dependency graph query.",
+ allowResidue = true,
+ binaryStdOut = true,
+ canRunInOutputDirectory = true)
+public final class QueryCommand implements BlazeCommand {
+
+ @Override
+ public void editOptions(BlazeRuntime runtime, OptionsParser optionsParser) { }
+
+ /**
+ * Exit codes:
+ * 0 on successful evaluation.
+ * 1 if query evaluation did not complete.
+ * 2 if query parsing failed.
+ * 3 if errors were reported but evaluation produced a partial result
+ * (only when --keep_going is in effect.)
+ */
+ @Override
+ public ExitCode exec(BlazeRuntime runtime, OptionsProvider options) {
+ QueryOptions queryOptions = options.getOptions(QueryOptions.class);
+
+ try {
+ runtime.setupPackageCache(
+ options.getOptions(PackageCacheOptions.class),
+ runtime.getDefaultsPackageContent());
+ } catch (InterruptedException e) {
+ runtime.getReporter().handle(Event.error("query interrupted"));
+ return ExitCode.INTERRUPTED;
+ } catch (AbruptExitException e) {
+ runtime.getReporter().handle(Event.error(null, "Unknown error: " + e.getMessage()));
+ return e.getExitCode();
+ }
+
+ if (options.getResidue().isEmpty()) {
+ runtime.getReporter().handle(Event.error(
+ "missing query expression. Type 'blaze help query' for syntax and help"));
+ return ExitCode.COMMAND_LINE_ERROR;
+ }
+
+ Iterable<OutputFormatter> formatters = runtime.getQueryOutputFormatters();
+ OutputFormatter formatter =
+ OutputFormatter.getFormatter(formatters, queryOptions.outputFormat);
+ if (formatter == null) {
+ runtime.getReporter().handle(Event.error(
+ String.format("Invalid output format '%s'. Valid values are: %s",
+ queryOptions.outputFormat, OutputFormatter.formatterNames(formatters))));
+ return ExitCode.COMMAND_LINE_ERROR;
+ }
+
+ String query = Joiner.on(' ').join(options.getResidue());
+
+ Set<Setting> settings = queryOptions.toSettings();
+ BlazeQueryEnvironment env = newQueryEnvironment(
+ runtime,
+ queryOptions.keepGoing,
+ queryOptions.loadingPhaseThreads,
+ settings);
+
+ // 1. Parse query:
+ QueryExpression expr;
+ try {
+ expr = QueryExpression.parse(query, env);
+ } catch (QueryException e) {
+ runtime.getReporter().handle(Event.error(
+ null, "Error while parsing '" + query + "': " + e.getMessage()));
+ return ExitCode.COMMAND_LINE_ERROR;
+ }
+
+ // 2. Evaluate expression:
+ BlazeQueryEvalResult<Target> result;
+ try {
+ result = env.evaluateQuery(expr);
+ } catch (QueryException e) {
+ // Keep consistent with reportBuildFileError()
+ runtime.getReporter().handle(Event.error(e.getMessage()));
+ return ExitCode.ANALYSIS_FAILURE;
+ }
+
+ // 3. Output results:
+ OutputFormatter.UnorderedFormatter unorderedFormatter = null;
+ if (!queryOptions.orderResults && formatter instanceof UnorderedFormatter) {
+ unorderedFormatter = (UnorderedFormatter) formatter;
+ }
+
+ PrintStream output = new PrintStream(runtime.getReporter().getOutErr().getOutputStream());
+ try {
+ if (unorderedFormatter != null) {
+ unorderedFormatter.outputUnordered(queryOptions, result.getResultSet(), output);
+ } else {
+ formatter.output(queryOptions, result.getResultGraph(), output);
+ }
+ } catch (ClosedByInterruptException e) {
+ runtime.getReporter().handle(Event.error("query interrupted"));
+ return ExitCode.INTERRUPTED;
+ } catch (IOException e) {
+ runtime.getReporter().handle(Event.error("I/O error: " + e.getMessage()));
+ return ExitCode.LOCAL_ENVIRONMENTAL_ERROR;
+ } finally {
+ output.flush();
+ }
+ if (result.getResultSet().isEmpty()) {
+ runtime.getReporter().handle(Event.info("Empty results"));
+ }
+
+ return result.getSuccess() ? ExitCode.SUCCESS : ExitCode.PARTIAL_ANALYSIS_FAILURE;
+ }
+
+ @VisibleForTesting // for com.google.devtools.deps.gquery.test.QueryResultTestUtil
+ public static BlazeQueryEnvironment newQueryEnvironment(BlazeRuntime runtime,
+ boolean keepGoing, int loadingPhaseThreads, Set<Setting> settings) {
+ ImmutableList.Builder<QueryFunction> functions = ImmutableList.builder();
+ for (BlazeModule module : runtime.getBlazeModules()) {
+ functions.addAll(module.getQueryFunctions());
+ }
+ return new SkyframeQueryEnvironment(
+ runtime.getPackageManager().newTransitiveLoader(),
+ runtime.getPackageManager(),
+ runtime.getTargetPatternEvaluator(),
+ keepGoing, loadingPhaseThreads, runtime.getReporter(), settings, functions.build());
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/RunCommand.java b/src/main/java/com/google/devtools/build/lib/runtime/commands/RunCommand.java
new file mode 100644
index 0000000..b128d37
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/RunCommand.java
@@ -0,0 +1,519 @@
+// Copyright 2014 Google Inc. 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.runtime.commands;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.FilesToRunProvider;
+import com.google.devtools.build.lib.analysis.RunfilesSupport;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.analysis.config.RunUnder;
+import com.google.devtools.build.lib.buildtool.BuildRequest;
+import com.google.devtools.build.lib.buildtool.BuildRequest.BuildRequestOptions;
+import com.google.devtools.build.lib.buildtool.BuildResult;
+import com.google.devtools.build.lib.buildtool.OutputDirectoryLinksUtils;
+import com.google.devtools.build.lib.buildtool.TargetValidator;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.Reporter;
+import com.google.devtools.build.lib.exec.SymlinkTreeHelper;
+import com.google.devtools.build.lib.packages.NonconfigurableAttributeMapper;
+import com.google.devtools.build.lib.packages.OutputFile;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.packages.TargetUtils;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.pkgcache.LoadingFailedException;
+import com.google.devtools.build.lib.runtime.BlazeCommand;
+import com.google.devtools.build.lib.runtime.BlazeRuntime;
+import com.google.devtools.build.lib.runtime.Command;
+import com.google.devtools.build.lib.shell.AbnormalTerminationException;
+import com.google.devtools.build.lib.shell.BadExitStatusException;
+import com.google.devtools.build.lib.shell.CommandException;
+import com.google.devtools.build.lib.util.CommandBuilder;
+import com.google.devtools.build.lib.util.CommandDescriptionForm;
+import com.google.devtools.build.lib.util.CommandFailureUtils;
+import com.google.devtools.build.lib.util.ExitCode;
+import com.google.devtools.build.lib.util.FileType;
+import com.google.devtools.build.lib.util.OptionsUtils;
+import com.google.devtools.build.lib.util.ShellEscaper;
+import com.google.devtools.build.lib.util.io.OutErr;
+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 com.google.devtools.common.options.Option;
+import com.google.devtools.common.options.OptionsBase;
+import com.google.devtools.common.options.OptionsParser;
+import com.google.devtools.common.options.OptionsProvider;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Builds and run a target with the given command line arguments.
+ */
+@Command(name = "run",
+ builds = true,
+ options = { RunCommand.RunOptions.class },
+ inherits = { BuildCommand.class },
+ shortDescription = "Runs the specified target.",
+ help = "resource:run.txt",
+ allowResidue = true,
+ binaryStdOut = true,
+ binaryStdErr = true)
+public class RunCommand implements BlazeCommand {
+
+ public static class RunOptions extends OptionsBase {
+ @Option(name = "script_path",
+ category = "run",
+ defaultValue = "null",
+ converter = OptionsUtils.PathFragmentConverter.class,
+ help = "If set, write a shell script to the given file which invokes the "
+ + "target. If this option is set, the target is not run from Blaze. "
+ + "Use 'blaze run --script_path=foo //foo && foo' to invoke target '//foo' "
+ + "This differs from 'blaze run //foo' in that the Blaze lock is released "
+ + "and the executable is connected to the terminal's stdin.")
+ public PathFragment scriptPath;
+ }
+
+ @VisibleForTesting
+ public static final String SINGLE_TARGET_MESSAGE = "Blaze can only run a single target. "
+ + "Do not use wildcards that match more than one target";
+ @VisibleForTesting
+ public static final String NO_TARGET_MESSAGE = "No targets found to run";
+
+ private static final String PROCESS_WRAPPER = "process-wrapper";
+
+ // Value of --run_under as of the most recent command invocation.
+ private RunUnder currentRunUnder;
+
+ private static final FileType RUNFILES_MANIFEST = FileType.of(".runfiles_manifest");
+
+ @VisibleForTesting // productionVisibility = Visibility.PRIVATE
+ protected BuildResult processRequest(final BlazeRuntime runtime, BuildRequest request) {
+ return runtime.getBuildTool().processRequest(request, new TargetValidator() {
+ @Override
+ public void validateTargets(Collection<Target> targets, boolean keepGoing)
+ throws LoadingFailedException {
+ RunCommand.this.validateTargets(runtime.getReporter(), targets, keepGoing);
+ }
+ });
+ }
+
+ @Override
+ public void editOptions(BlazeRuntime runtime, OptionsParser optionsParser) { }
+
+ @Override
+ public ExitCode exec(BlazeRuntime runtime, OptionsProvider options) {
+ RunOptions runOptions = options.getOptions(RunOptions.class);
+ // This list should look like: ["//executable:target", "arg1", "arg2"]
+ List<String> targetAndArgs = options.getResidue();
+
+ // The user must at the least specify an executable target.
+ if (targetAndArgs.isEmpty()) {
+ runtime.getReporter().handle(Event.error("Must specify a target to run"));
+ return ExitCode.COMMAND_LINE_ERROR;
+ }
+ String targetString = targetAndArgs.get(0);
+ List<String> runTargetArgs = targetAndArgs.subList(1, targetAndArgs.size());
+ RunUnder runUnder = options.getOptions(BuildConfiguration.Options.class).runUnder;
+
+ OutErr outErr = runtime.getReporter().getOutErr();
+ List<String> targets = (runUnder != null) && (runUnder.getLabel() != null)
+ ? ImmutableList.of(targetString, runUnder.getLabel().toString())
+ : ImmutableList.of(targetString);
+ BuildRequest request = BuildRequest.create(
+ this.getClass().getAnnotation(Command.class).name(), options,
+ runtime.getStartupOptionsProvider(), targets, outErr,
+ runtime.getCommandId(), runtime.getCommandStartTime());
+ if (request.getBuildOptions().compileOnly) {
+ String message = "The '" + getClass().getAnnotation(Command.class).name() +
+ "' command is incompatible with the --compile_only option";
+ runtime.getReporter().handle(Event.error(message));
+ return ExitCode.COMMAND_LINE_ERROR;
+ }
+
+ currentRunUnder = runUnder;
+ BuildResult result;
+ try {
+ result = processRequest(runtime, request);
+ } finally {
+ currentRunUnder = null;
+ }
+
+ if (!result.getSuccess()) {
+ runtime.getReporter().handle(Event.error("Build failed. Not running target"));
+ return result.getExitCondition();
+ }
+
+ // Make sure that we have exactly 1 built target (excluding --run_under),
+ // and that it is executable.
+ // These checks should only fail if keepGoing is true, because we already did
+ // validation before the build began. See {@link #validateTargets()}.
+ Collection<ConfiguredTarget> targetsBuilt = result.getSuccessfulTargets();
+ ConfiguredTarget targetToRun = null;
+ ConfiguredTarget runUnderTarget = null;
+
+ if (targetsBuilt != null) {
+ int maxTargets = runUnder != null && runUnder.getLabel() != null ? 2 : 1;
+ if (targetsBuilt.size() > maxTargets) {
+ runtime.getReporter().handle(Event.error(SINGLE_TARGET_MESSAGE));
+ return ExitCode.COMMAND_LINE_ERROR;
+ }
+ for (ConfiguredTarget target : targetsBuilt) {
+ ExitCode targetValidation = fullyValidateTarget(runtime, target);
+ if (targetValidation != ExitCode.SUCCESS) {
+ return targetValidation;
+ }
+ if (runUnder != null && target.getLabel().equals(runUnder.getLabel())) {
+ if (runUnderTarget != null) {
+ runtime.getReporter().handle(Event.error(
+ null, "Can't identify the run_under target from multiple options?"));
+ return ExitCode.COMMAND_LINE_ERROR;
+ }
+ runUnderTarget = target;
+ } else if (targetToRun == null) {
+ targetToRun = target;
+ } else {
+ runtime.getReporter().handle(Event.error(SINGLE_TARGET_MESSAGE));
+ return ExitCode.COMMAND_LINE_ERROR;
+ }
+ }
+ }
+ // Handle target & run_under referring to the same target.
+ if ((targetToRun == null) && (runUnderTarget != null)) {
+ targetToRun = runUnderTarget;
+ }
+ if (targetToRun == null) {
+ runtime.getReporter().handle(Event.error(NO_TARGET_MESSAGE));
+ return ExitCode.COMMAND_LINE_ERROR;
+ }
+
+ Path executablePath = Preconditions.checkNotNull(
+ targetToRun.getProvider(FilesToRunProvider.class).getExecutable().getPath());
+ BuildConfiguration configuration = targetToRun.getConfiguration();
+ if (configuration == null) {
+ // The target may be an input file, which doesn't have a configuration. In that case, we
+ // choose any target configuration.
+ configuration = runtime.getBuildTool().getView().getConfigurationCollection()
+ .getTargetConfigurations().get(0);
+ }
+ Path workingDir;
+ try {
+ workingDir = ensureRunfilesBuilt(runtime, targetToRun);
+ } catch (CommandException e) {
+ runtime.getReporter().handle(Event.error("Error creating runfiles: " + e.getMessage()));
+ return ExitCode.LOCAL_ENVIRONMENTAL_ERROR;
+ }
+
+ List<String> args = runTargetArgs;
+
+ FilesToRunProvider provider = targetToRun.getProvider(FilesToRunProvider.class);
+ RunfilesSupport runfilesSupport = provider == null ? null : provider.getRunfilesSupport();
+ if (runfilesSupport != null && runfilesSupport.getArgs() != null) {
+ List<String> targetArgs = runfilesSupport.getArgs();
+ if (!targetArgs.isEmpty()) {
+ args = Lists.newArrayListWithCapacity(targetArgs.size() + runTargetArgs.size());
+ args.addAll(targetArgs);
+ args.addAll(runTargetArgs);
+ }
+ }
+
+ //
+ // We now have a unique executable ready to be run.
+ //
+ // We build up two different versions of the command to run: one with an absolute path, which
+ // we'll actually run, and a prettier one with the long absolute path to the executable
+ // replaced with a shorter relative path that uses the symlinks in the workspace.
+ PathFragment prettyExecutablePath =
+ OutputDirectoryLinksUtils.getPrettyPath(executablePath,
+ runtime.getWorkspaceName(), runtime.getWorkspace(),
+ options.getOptions(BuildRequestOptions.class).symlinkPrefix);
+ List<String> cmdLine = new ArrayList<>();
+ if (runOptions.scriptPath == null) {
+ cmdLine.add(runtime.getDirectories().getExecRoot()
+ .getRelative(runtime.getBinTools().getExecPath(PROCESS_WRAPPER)).getPathString());
+ cmdLine.add("-1");
+ cmdLine.add("15");
+ cmdLine.add("-");
+ cmdLine.add("-");
+ }
+ List<String> prettyCmdLine = new ArrayList<>();
+ // Insert the command prefix specified by the "--run_under=<command-prefix>" option
+ // at the start of the command line.
+ if (runUnder != null) {
+ String runUnderValue = runUnder.getValue();
+ if (runUnderTarget != null) {
+ // --run_under specifies a target. Get the corresponding executable.
+ // This must be an absolute path, because the run_under target is only
+ // in the runfiles of test targets.
+ runUnderValue = runUnderTarget
+ .getProvider(FilesToRunProvider.class).getExecutable().getPath().getPathString();
+ // If the run_under command contains any options, make sure to add them
+ // to the command line as well.
+ List<String> opts = runUnder.getOptions();
+ if (!opts.isEmpty()) {
+ runUnderValue += " " + ShellEscaper.escapeJoinAll(opts);
+ }
+ }
+ cmdLine.add(configuration.getShExecutable().getPathString());
+ cmdLine.add("-c");
+ cmdLine.add(runUnderValue + " " + executablePath.getPathString() + " " +
+ ShellEscaper.escapeJoinAll(args));
+ prettyCmdLine.add(configuration.getShExecutable().getPathString());
+ prettyCmdLine.add("-c");
+ prettyCmdLine.add(runUnderValue + " " + prettyExecutablePath.getPathString() + " " +
+ ShellEscaper.escapeJoinAll(args));
+ } else {
+ cmdLine.add(executablePath.getPathString());
+ cmdLine.addAll(args);
+ prettyCmdLine.add(prettyExecutablePath.getPathString());
+ prettyCmdLine.addAll(args);
+ }
+
+ // Add a newline between the blaze output and the binary's output.
+ outErr.printErrLn("");
+
+ if (runOptions.scriptPath != null) {
+ String unisolatedCommand = CommandFailureUtils.describeCommand(
+ CommandDescriptionForm.COMPLETE_UNISOLATED,
+ cmdLine, null, workingDir.getPathString());
+ if (writeScript(runtime, runOptions.scriptPath, unisolatedCommand)) {
+ return ExitCode.SUCCESS;
+ } else {
+ return ExitCode.RUN_FAILURE;
+ }
+ }
+
+ runtime.getReporter().handle(Event.info(
+ null, "Running command line: " + ShellEscaper.escapeJoinAll(prettyCmdLine)));
+
+ com.google.devtools.build.lib.shell.Command command = new CommandBuilder()
+ .addArgs(cmdLine).setEnv(runtime.getClientEnv()).setWorkingDir(workingDir).build();
+
+ try {
+ // The command API is a little strange in that the following statement
+ // will return normally only if the program exits with exit code 0.
+ // If it ends with any other code, we have to catch BadExitStatusException.
+ command.execute(com.google.devtools.build.lib.shell.Command.NO_INPUT,
+ com.google.devtools.build.lib.shell.Command.NO_OBSERVER,
+ outErr.getOutputStream(),
+ outErr.getErrorStream(),
+ true /* interruptible */).getTerminationStatus().getExitCode();
+ return ExitCode.SUCCESS;
+ } catch (BadExitStatusException e) {
+ String message = "Non-zero return code '"
+ + e.getResult().getTerminationStatus().getExitCode()
+ + "' from command: " + e.getMessage();
+ runtime.getReporter().handle(Event.error(message));
+ return ExitCode.RUN_FAILURE;
+ } catch (AbnormalTerminationException e) {
+ // The process was likely terminated by a signal in this case.
+ return ExitCode.INTERRUPTED;
+ } catch (CommandException e) {
+ runtime.getReporter().handle(Event.error("Error running program: " + e.getMessage()));
+ return ExitCode.RUN_FAILURE;
+ }
+ }
+
+ /**
+ * Ensures that runfiles are built for the specified target. If they already
+ * are, does nothing, otherwise builds them.
+ *
+ * @param target the target to build runfiles for.
+ * @return the path of the runfiles directory.
+ * @throws CommandException
+ */
+ private Path ensureRunfilesBuilt(BlazeRuntime runtime, ConfiguredTarget target)
+ throws CommandException {
+ FilesToRunProvider provider = target.getProvider(FilesToRunProvider.class);
+ RunfilesSupport runfilesSupport = provider == null ? null : provider.getRunfilesSupport();
+ if (runfilesSupport == null) {
+ return runtime.getWorkingDirectory();
+ }
+
+ Artifact manifest = runfilesSupport.getRunfilesManifest();
+ PathFragment runfilesDir = runfilesSupport.getRunfilesDirectoryExecPath();
+ Path workingDir = runtime.getExecRoot()
+ .getRelative(runfilesDir)
+ .getRelative(runtime.getRunfilesPrefix());
+
+ // When runfiles are not generated, getManifest() returns the
+ // .runfiles_manifest file, otherwise it returns the MANIFEST file. This is
+ // a handy way to check whether runfiles were built or not.
+ if (!RUNFILES_MANIFEST.matches(manifest.getFilename())) {
+ // Runfiles already built, nothing to do.
+ return workingDir;
+ }
+
+ SymlinkTreeHelper helper = new SymlinkTreeHelper(
+ manifest.getExecPath(),
+ runfilesDir,
+ false);
+ helper.createSymlinksUsingCommand(runtime.getExecRoot(), target.getConfiguration(),
+ runtime.getBinTools());
+ return workingDir;
+ }
+
+ private boolean writeScript(BlazeRuntime runtime, PathFragment scriptPathFrag, String cmd) {
+ final String SH_SHEBANG = "#!/bin/sh";
+ Path scriptPath = runtime.getWorkingDirectory().getRelative(scriptPathFrag);
+ try {
+ FileSystemUtils.writeContent(scriptPath, StandardCharsets.ISO_8859_1,
+ SH_SHEBANG + "\n" + cmd + " \"$@\"");
+ scriptPath.setExecutable(true);
+ } catch (IOException e) {
+ runtime.getReporter().handle(Event.error("Error writing run script:" + e.getMessage()));
+ return false;
+ }
+ return true;
+ }
+
+ // Make sure we are building exactly 1 binary target.
+ // If keepGoing, we'll build all the targets even if they are non-binary.
+ private void validateTargets(Reporter reporter, Collection<Target> targets, boolean keepGoing)
+ throws LoadingFailedException {
+ Target targetToRun = null;
+ Target runUnderTarget = null;
+
+ boolean singleTargetWarningWasOutput = false;
+ int maxTargets = currentRunUnder != null && currentRunUnder.getLabel() != null ? 2 : 1;
+ if (targets.size() > maxTargets) {
+ warningOrException(reporter, SINGLE_TARGET_MESSAGE, keepGoing);
+ singleTargetWarningWasOutput = true;
+ }
+ for (Target target : targets) {
+ String targetError = validateTarget(target);
+ if (targetError != null) {
+ warningOrException(reporter, targetError, keepGoing);
+ }
+
+ if (currentRunUnder != null && target.getLabel().equals(currentRunUnder.getLabel())) {
+ // It's impossible to have two targets with the same label.
+ Preconditions.checkState(runUnderTarget == null);
+ runUnderTarget = target;
+ } else if (targetToRun == null) {
+ targetToRun = target;
+ } else {
+ if (!singleTargetWarningWasOutput) {
+ warningOrException(reporter, SINGLE_TARGET_MESSAGE, keepGoing);
+ }
+ return;
+ }
+ }
+ // Handle target & run_under referring to the same target.
+ if ((targetToRun == null) && (runUnderTarget != null)) {
+ targetToRun = runUnderTarget;
+ }
+ if (targetToRun == null) {
+ warningOrException(reporter, NO_TARGET_MESSAGE, keepGoing);
+ }
+ }
+
+ // If keepGoing, print a warning and return the given collection.
+ // Otherwise, throw InvalidTargetException.
+ private void warningOrException(Reporter reporter, String message,
+ boolean keepGoing) throws LoadingFailedException {
+ if (keepGoing) {
+ reporter.handle(Event.warn(message + ". Will continue anyway"));
+ } else {
+ throw new LoadingFailedException(message);
+ }
+ }
+
+ private static String notExecutableError(Target target) {
+ return "Cannot run target " + target.getLabel() + ": Not executable";
+ }
+
+ /** Returns null if the target is a runnable rule, or an appropriate error message otherwise. */
+ private static String validateTarget(Target target) {
+ return isExecutable(target)
+ ? null
+ : notExecutableError(target);
+ }
+
+ /**
+ * Performs all available validation checks on an individual target.
+ *
+ * @param target ConfiguredTarget to validate
+ * @return ExitCode.SUCCESS if all checks succeeded, otherwise a different error code.
+ */
+ private ExitCode fullyValidateTarget(BlazeRuntime runtime, ConfiguredTarget target) {
+ String targetError = validateTarget(target.getTarget());
+
+ if (targetError != null) {
+ runtime.getReporter().handle(Event.error(targetError));
+ return ExitCode.COMMAND_LINE_ERROR;
+ }
+
+ Artifact executable = target.getProvider(FilesToRunProvider.class).getExecutable();
+ if (executable == null) {
+ runtime.getReporter().handle(Event.error(notExecutableError(target.getTarget())));
+ return ExitCode.COMMAND_LINE_ERROR;
+ }
+
+ // Shouldn't happen: We just validated the target.
+ Preconditions.checkState(executable != null,
+ "Could not find executable for target %s", target);
+ Path executablePath = executable.getPath();
+ try {
+ if (!executablePath.exists() || !executablePath.isExecutable()) {
+ runtime.getReporter().handle(Event.error(
+ null, "Non-existent or non-executable " + executablePath));
+ return ExitCode.BLAZE_INTERNAL_ERROR;
+ }
+ } catch (IOException e) {
+ runtime.getReporter().handle(Event.error(
+ "Error checking " + executablePath.getPathString() + ": " + e.getMessage()));
+ return ExitCode.LOCAL_ENVIRONMENTAL_ERROR;
+ }
+
+ return ExitCode.SUCCESS;
+ }
+
+ /**
+ * Return true iff {@code target} is a rule that has an executable file. This includes
+ * *_test rules, *_binary rules, and generated outputs.
+ */
+ private static boolean isExecutable(Target target) {
+ return isOutputFile(target) || isExecutableNonTestRule(target)
+ || TargetUtils.isTestRule(target);
+ }
+
+ /**
+ * Return true iff {@code target} is a rule that generates an executable file and is user-executed
+ * code.
+ */
+ private static boolean isExecutableNonTestRule(Target target) {
+ if (!(target instanceof Rule)) {
+ return false;
+ }
+ Rule rule = ((Rule) target);
+ if (rule.getRuleClassObject().hasAttr("$is_executable", Type.BOOLEAN)) {
+ return NonconfigurableAttributeMapper.of(rule).get("$is_executable", Type.BOOLEAN);
+ }
+ return false;
+ }
+
+ private static boolean isOutputFile(Target target) {
+ return (target instanceof OutputFile);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/ShutdownCommand.java b/src/main/java/com/google/devtools/build/lib/runtime/commands/ShutdownCommand.java
new file mode 100644
index 0000000..fb9ba39
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/ShutdownCommand.java
@@ -0,0 +1,71 @@
+// Copyright 2014 Google Inc. 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.runtime.commands;
+
+import com.google.devtools.build.lib.runtime.BlazeCommand;
+import com.google.devtools.build.lib.runtime.BlazeCommandDispatcher.ShutdownBlazeServerException;
+import com.google.devtools.build.lib.runtime.BlazeRuntime;
+import com.google.devtools.build.lib.runtime.Command;
+import com.google.devtools.build.lib.util.ExitCode;
+import com.google.devtools.common.options.Option;
+import com.google.devtools.common.options.OptionsBase;
+import com.google.devtools.common.options.OptionsParser;
+import com.google.devtools.common.options.OptionsProvider;
+
+/**
+ * The 'blaze shutdown' command.
+ */
+@Command(name = "shutdown",
+ options = { ShutdownCommand.Options.class },
+ allowResidue = false,
+ mustRunInWorkspace = false,
+ shortDescription = "Stops the Blaze server.",
+ help = "This command shuts down the memory resident Blaze server process.\n%{options}")
+public final class ShutdownCommand implements BlazeCommand {
+
+ public static class Options extends OptionsBase {
+
+ @Option(name="iff_heap_size_greater_than",
+ defaultValue = "0",
+ category = "misc",
+ help="Iff non-zero, then shutdown will only shut down the " +
+ "server if the total memory (in MB) consumed by the JVM " +
+ "exceeds this value.")
+ public int heapSizeLimit;
+ }
+
+ @Override
+ public void editOptions(BlazeRuntime runtime, OptionsParser optionsParser) {}
+
+ @Override
+ public ExitCode exec(BlazeRuntime runtime, OptionsProvider options)
+ throws ShutdownBlazeServerException {
+
+ int limit = options.getOptions(Options.class).heapSizeLimit;
+
+ // Iff limit is non-zero, shut down the server if total memory exceeds the
+ // limit. totalMemory is the actual heap size that the VM currently uses
+ // *from the OS perspective*. That is, it's not the size occupied by all
+ // objects (which is totalMemory() - freeMemory()), and not the -Xmx
+ // (which is maxMemory()). It's really how much memory this process
+ // currently consumes, in addition to the JVM code and C heap.
+
+ if (limit == 0 ||
+ Runtime.getRuntime().totalMemory() > limit * 1000L * 1000) {
+ throw new ShutdownBlazeServerException(0);
+ }
+ return ExitCode.SUCCESS;
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/SkylarkCommand.java b/src/main/java/com/google/devtools/build/lib/runtime/commands/SkylarkCommand.java
new file mode 100644
index 0000000..70082ef
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/SkylarkCommand.java
@@ -0,0 +1,82 @@
+// Copyright 2014 Google Inc. 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.runtime.commands;
+
+import com.google.devtools.build.docgen.SkylarkDocumentationProcessor;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.Reporter;
+import com.google.devtools.build.lib.runtime.BlazeCommand;
+import com.google.devtools.build.lib.runtime.BlazeCommandDispatcher.ShutdownBlazeServerException;
+import com.google.devtools.build.lib.runtime.BlazeRuntime;
+import com.google.devtools.build.lib.runtime.Command;
+import com.google.devtools.build.lib.util.ExitCode;
+import com.google.devtools.build.lib.util.io.OutErr;
+import com.google.devtools.common.options.OptionsParser;
+import com.google.devtools.common.options.OptionsProvider;
+
+import java.util.Map;
+
+/**
+ * The 'doc_ext' command, which prints the extension API doc.
+ */
+@Command(name = "doc_ext",
+allowResidue = true,
+mustRunInWorkspace = false,
+shortDescription = "Prints help for commands, or the index.",
+help = "resource:skylark.txt")
+public final class SkylarkCommand implements BlazeCommand {
+
+ @Override
+ public ExitCode exec(BlazeRuntime runtime, OptionsProvider options)
+ throws ShutdownBlazeServerException {
+ OutErr outErr = runtime.getReporter().getOutErr();
+ if (options.getResidue().isEmpty()) {
+ printTopLevelAPIDoc(outErr);
+ return ExitCode.SUCCESS;
+ }
+ if (options.getResidue().size() != 1) {
+ runtime.getReporter().handle(Event.error("Cannot specify more than one parameters"));
+ return ExitCode.COMMAND_LINE_ERROR;
+ }
+ return printAPIDoc(options.getResidue().get(0), outErr, runtime.getReporter());
+ }
+
+ private ExitCode printAPIDoc(String param, OutErr outErr, Reporter reporter) {
+ String params[] = param.split("\\.");
+ if (params.length > 2) {
+ reporter.handle(Event.error("Identifier not found: " + param));
+ return ExitCode.COMMAND_LINE_ERROR;
+ }
+ SkylarkDocumentationProcessor processor = new SkylarkDocumentationProcessor();
+ String doc = processor.getCommandLineAPIDoc(params);
+ if (doc == null) {
+ reporter.handle(Event.error("Identifier not found: " + param));
+ return ExitCode.COMMAND_LINE_ERROR;
+ }
+ outErr.printOut(doc);
+ return ExitCode.SUCCESS;
+ }
+
+ private void printTopLevelAPIDoc(OutErr outErr) {
+ SkylarkDocumentationProcessor processor = new SkylarkDocumentationProcessor();
+ outErr.printOut("Top level language modules, methods and objects:\n\n");
+ for (Map.Entry<String, String> entry : processor.collectTopLevelModules().entrySet()) {
+ outErr.printOut(entry.getKey() + ": " + entry.getValue());
+ }
+ }
+
+ @Override
+ public void editOptions(BlazeRuntime runtime, OptionsParser optionsParser) {}
+}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/TestCommand.java b/src/main/java/com/google/devtools/build/lib/runtime/commands/TestCommand.java
new file mode 100644
index 0000000..561c54a
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/TestCommand.java
@@ -0,0 +1,161 @@
+// Copyright 2014 Google Inc. 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.runtime.commands;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.buildtool.BuildRequest;
+import com.google.devtools.build.lib.buildtool.BuildResult;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.exec.ExecutionOptions;
+import com.google.devtools.build.lib.rules.test.TestStrategy;
+import com.google.devtools.build.lib.rules.test.TestStrategy.TestOutputFormat;
+import com.google.devtools.build.lib.runtime.AggregatingTestListener;
+import com.google.devtools.build.lib.runtime.BlazeCommand;
+import com.google.devtools.build.lib.runtime.BlazeCommandEventHandler;
+import com.google.devtools.build.lib.runtime.BlazeRuntime;
+import com.google.devtools.build.lib.runtime.Command;
+import com.google.devtools.build.lib.runtime.TerminalTestResultNotifier;
+import com.google.devtools.build.lib.runtime.TerminalTestResultNotifier.TestSummaryOptions;
+import com.google.devtools.build.lib.runtime.TestResultAnalyzer;
+import com.google.devtools.build.lib.runtime.TestResultNotifier;
+import com.google.devtools.build.lib.util.AbruptExitException;
+import com.google.devtools.build.lib.util.ExitCode;
+import com.google.devtools.build.lib.util.io.AnsiTerminalPrinter;
+import com.google.devtools.common.options.OptionPriority;
+import com.google.devtools.common.options.OptionsParser;
+import com.google.devtools.common.options.OptionsParsingException;
+import com.google.devtools.common.options.OptionsProvider;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Handles the 'test' command on the Blaze command line.
+ */
+@Command(name = "test",
+ builds = true,
+ inherits = { BuildCommand.class },
+ options = { TestSummaryOptions.class },
+ shortDescription = "Builds and runs the specified test targets.",
+ help = "resource:test.txt",
+ allowResidue = true)
+public class TestCommand implements BlazeCommand {
+ private AnsiTerminalPrinter printer;
+
+ @Override
+ public void editOptions(BlazeRuntime runtime, OptionsParser optionsParser)
+ throws AbruptExitException {
+ ProjectFileSupport.handleProjectFiles(runtime, optionsParser, "test");
+
+ TestOutputFormat testOutput = optionsParser.getOptions(ExecutionOptions.class).testOutput;
+
+ if (testOutput == TestStrategy.TestOutputFormat.STREAMED) {
+ runtime.getReporter().handle(Event.warn(
+ "Streamed test output requested so all tests will be run locally, without sharding, " +
+ "one at a time"));
+ try {
+ optionsParser.parse(OptionPriority.SOFTWARE_REQUIREMENT,
+ "streamed output requires locally run tests, without sharding",
+ ImmutableList.of("--test_sharding_strategy=disabled", "--test_strategy=exclusive"));
+ } catch (OptionsParsingException e) {
+ throw new IllegalStateException("Known options failed to parse", e);
+ }
+ }
+ }
+
+ @Override
+ public ExitCode exec(BlazeRuntime runtime, OptionsProvider options) {
+ TestResultAnalyzer resultAnalyzer = new TestResultAnalyzer(
+ runtime.getExecRoot(),
+ options.getOptions(TestSummaryOptions.class),
+ options.getOptions(ExecutionOptions.class),
+ runtime.getEventBus());
+
+ printer = new AnsiTerminalPrinter(runtime.getReporter().getOutErr().getOutputStream(),
+ options.getOptions(BlazeCommandEventHandler.Options.class).useColor());
+
+ // Initialize test handler.
+ AggregatingTestListener testListener = new AggregatingTestListener(
+ resultAnalyzer, runtime.getEventBus(), runtime.getReporter());
+
+ runtime.getEventBus().register(testListener);
+ return doTest(runtime, options, testListener);
+ }
+
+ private ExitCode doTest(BlazeRuntime runtime,
+ OptionsProvider options,
+ AggregatingTestListener testListener) {
+ // Run simultaneous build and test.
+ List<String> targets = ProjectFileSupport.getTargets(runtime, options);
+ BuildRequest request = BuildRequest.create(
+ getClass().getAnnotation(Command.class).name(), options,
+ runtime.getStartupOptionsProvider(), targets,
+ runtime.getReporter().getOutErr(), runtime.getCommandId(), runtime.getCommandStartTime());
+ if (request.getBuildOptions().compileOnly) {
+ String message = "The '" + getClass().getAnnotation(Command.class).name() +
+ "' command is incompatible with the --compile_only option";
+ runtime.getReporter().handle(Event.error(message));
+ return ExitCode.COMMAND_LINE_ERROR;
+ }
+ request.setRunTests();
+
+ BuildResult buildResult = runtime.getBuildTool().processRequest(request, null);
+
+ Collection<ConfiguredTarget> testTargets = buildResult.getTestTargets();
+ // TODO(bazel-team): don't handle isEmpty here or fix up a bunch of tests
+ if (buildResult.getSuccessfulTargets() == null) {
+ // This can happen if there were errors in the target parsing or loading phase
+ // (original exitcode=BUILD_FAILURE) or if there weren't but --noanalyze was given
+ // (original exitcode=SUCCESS).
+ runtime.getReporter().handle(Event.error("Couldn't start the build. Unable to run tests"));
+ return buildResult.getSuccess() ? ExitCode.PARSING_FAILURE : buildResult.getExitCondition();
+ }
+ // TODO(bazel-team): the check above shadows NO_TESTS_FOUND, but switching the conditions breaks
+ // more tests
+ if (testTargets.isEmpty()) {
+ runtime.getReporter().handle(Event.error(
+ null, "No test targets were found, yet testing was requested"));
+ return buildResult.getSuccess() ? ExitCode.NO_TESTS_FOUND : buildResult.getExitCondition();
+ }
+
+ boolean buildSuccess = buildResult.getSuccess();
+ boolean testSuccess = analyzeTestResults(testTargets, testListener, options);
+
+ if (testSuccess && !buildSuccess) {
+ // If all tests run successfully, test summary should include warning if
+ // there were build errors not associated with the test targets.
+ printer.printLn(AnsiTerminalPrinter.Mode.ERROR
+ + "One or more non-test targets failed to build.\n"
+ + AnsiTerminalPrinter.Mode.DEFAULT);
+ }
+
+ return buildSuccess ?
+ (testSuccess ? ExitCode.SUCCESS : ExitCode.TESTS_FAILED)
+ : buildResult.getExitCondition();
+ }
+
+ /**
+ * Analyzes test results and prints summary information.
+ * Returns true if and only if all tests were successful.
+ */
+ private boolean analyzeTestResults(Collection<ConfiguredTarget> testTargets,
+ AggregatingTestListener listener,
+ OptionsProvider options) {
+ TestResultNotifier notifier = new TerminalTestResultNotifier(printer, options);
+ return listener.getAnalyzer().differentialAnalyzeAndReport(
+ testTargets, listener, notifier);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/VersionCommand.java b/src/main/java/com/google/devtools/build/lib/runtime/commands/VersionCommand.java
new file mode 100644
index 0000000..0804cf6
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/VersionCommand.java
@@ -0,0 +1,49 @@
+// Copyright 2014 Google Inc. 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.runtime.commands;
+
+import com.google.devtools.build.lib.analysis.BlazeVersionInfo;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.runtime.BlazeCommand;
+import com.google.devtools.build.lib.runtime.BlazeRuntime;
+import com.google.devtools.build.lib.runtime.Command;
+import com.google.devtools.build.lib.util.ExitCode;
+import com.google.devtools.common.options.OptionsParser;
+import com.google.devtools.common.options.OptionsProvider;
+
+/**
+ * The 'blaze version' command, which informs users about the blaze version
+ * information.
+ */
+@Command(name = "version",
+ options = {},
+ allowResidue = false,
+ mustRunInWorkspace = false,
+ help = "resource:version.txt",
+ shortDescription = "Prints version information for Blaze.")
+public final class VersionCommand implements BlazeCommand {
+ @Override
+ public void editOptions(BlazeRuntime runtime, OptionsParser optionsParser) {}
+
+ @Override
+ public ExitCode exec(BlazeRuntime runtime, OptionsProvider options) {
+ BlazeVersionInfo info = BlazeVersionInfo.instance();
+ if (info.getSummary() == null) {
+ runtime.getReporter().handle(Event.error("Version information not available"));
+ return ExitCode.COMMAND_LINE_ERROR;
+ }
+ runtime.getReporter().getOutErr().printOutLn(info.getSummary());
+ return ExitCode.SUCCESS;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/analyze-profile.txt b/src/main/java/com/google/devtools/build/lib/runtime/commands/analyze-profile.txt
new file mode 100644
index 0000000..0ef55a8
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/analyze-profile.txt
@@ -0,0 +1,14 @@
+
+Usage: blaze %{command} <options> <profile-files> [<profile-file> ...]
+
+Analyzes build profile data for the given profile data files.
+
+Analyzes each specified profile data file and prints the results. The
+input files must have been produced by the 'blaze build
+--profile=file' command.
+
+By default, a summary of the analysis is printed. For post-processing
+with scripts, the --dump=raw option is recommended, causing this
+command to dump profile data in easily-parsed format.
+
+%{options}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/build.txt b/src/main/java/com/google/devtools/build/lib/runtime/commands/build.txt
new file mode 100644
index 0000000..5e8d88a
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/build.txt
@@ -0,0 +1,10 @@
+
+Usage: blaze %{command} <options> <targets>
+
+Builds the specified targets, using the options.
+
+See 'blaze help target-syntax' for details and examples on how to
+specify targets to build.
+
+%{options}
+
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/canonicalize.txt b/src/main/java/com/google/devtools/build/lib/runtime/commands/canonicalize.txt
new file mode 100644
index 0000000..11541ff
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/canonicalize.txt
@@ -0,0 +1,8 @@
+
+Usage: blaze canonicalize-flags <options> -- <options-to-canonicalize>
+
+Canonicalizes Blaze flags for the test and build commands. This command is
+intended to be used for tools that wish to check if two lists of options have
+the same effect at runtime.
+
+%{options}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/clean.txt b/src/main/java/com/google/devtools/build/lib/runtime/commands/clean.txt
new file mode 100644
index 0000000..7633888
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/clean.txt
@@ -0,0 +1,10 @@
+
+Usage: blaze %{command} [<option> ...]
+
+Removes Blaze-created output, including all object files, and Blaze
+metadata.
+
+If '--expunge' is specified, the entire working tree will be removed
+and the server stopped.
+
+%{options}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/help.txt b/src/main/java/com/google/devtools/build/lib/runtime/commands/help.txt
new file mode 100644
index 0000000..a2040c8
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/help.txt
@@ -0,0 +1,7 @@
+
+Usage: blaze help [<command>]
+
+Prints a help page for the given command, or, if no command is
+specified, prints the index of available commands.
+
+%{options}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/info.txt b/src/main/java/com/google/devtools/build/lib/runtime/commands/info.txt
new file mode 100644
index 0000000..9c8b552
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/info.txt
@@ -0,0 +1,23 @@
+
+Usage: blaze info <options> [key]
+
+Displays information about the state of the blaze process in the
+form of several "key: value" pairs. This includes the locations of
+several output directories. Because some of the
+values are affected by the options passed to 'blaze build', the
+info command accepts the same set of options.
+
+A single non-option argument may be specified (e.g. "blaze-bin"), in
+which case only the value for that key will be printed.
+
+If --show_make_env is specified, the output includes the set of key/value
+pairs in the "Make" environment, accessible within BUILD files.
+
+The full list of keys and the meaning of their values is documented in
+the Blaze User Manual, and can be programmatically obtained with
+'blaze help info-keys'.
+
+See also 'blaze version' for more detailed blaze version
+information.
+
+%{options}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/query.txt b/src/main/java/com/google/devtools/build/lib/runtime/commands/query.txt
new file mode 100644
index 0000000..ce10211
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/query.txt
@@ -0,0 +1,19 @@
+
+Usage: blaze %{command} <options> <query-expression>
+
+Executes a query language expression over a specified subgraph of the
+build dependency graph.
+
+For example, to show all C++ test rules in the strings package, use:
+
+ % blaze query 'kind("cc_.*test", strings:*)'
+
+or to find all dependencies of chubby lockserver, use:
+
+ % blaze query 'deps(//path/to/package:target)'
+
+or to find a dependency path between //path/to/package:target and //dependency:
+
+ % blaze query 'somepath(//path/to/package:target, //dependency)'
+
+%{options}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/run.txt b/src/main/java/com/google/devtools/build/lib/runtime/commands/run.txt
new file mode 100644
index 0000000..57283d5
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/run.txt
@@ -0,0 +1,12 @@
+
+Usage: blaze %{command} <options> -- <binary target> <flags to binary>
+
+Build the specified target and run it with the given arguments.
+
+'run' accepts any 'build' options, and will inherit any defaults
+provided by .blazerc.
+
+If your script needs stdin or execution not constrained by the Blaze lock,
+use 'blaze run --script_path' to write a script and then execute it.
+
+%{options}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/startup_options.txt b/src/main/java/com/google/devtools/build/lib/runtime/commands/startup_options.txt
new file mode 100644
index 0000000..5414707
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/startup_options.txt
@@ -0,0 +1,14 @@
+
+Startup options
+===============
+
+These options affect how Blaze starts up, or more specifically, how
+the virtual machine hosting Blaze starts up, and how the Blaze server
+starts up. These options must be specified to the left of the Blaze
+command (e.g. 'build'), and they must not contain any space between
+option name and value.
+
+Example:
+ % blaze --host_jvm_args=-Xmx1400m --output_base=/tmp/foo build //base
+
+%{options}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/target-syntax.txt b/src/main/java/com/google/devtools/build/lib/runtime/commands/target-syntax.txt
new file mode 100644
index 0000000..1fac498
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/target-syntax.txt
@@ -0,0 +1,64 @@
+
+Target pattern syntax
+=====================
+
+The BUILD file label syntax is used to specify a single target. Target
+patterns generalize this syntax to sets of targets, and also support
+working-directory-relative forms, recursion, subtraction and filtering.
+Examples:
+
+Specifying a single target:
+
+ //foo/bar:wiz The single target '//foo/bar:wiz'.
+ foo/bar/wiz Equivalent to the first existing one of these:
+ //foo/bar:wiz
+ //foo:bar/wiz
+ //foo/bar Equivalent to '//foo/bar:bar'.
+
+Specifying all rules in a package:
+
+ //foo/bar:all Matches all rules in package 'foo/bar'.
+
+Specifying all rules recursively beneath a package:
+
+ //foo/...:all Matches all rules in all packages beneath directory 'foo'.
+ //foo/... (ditto)
+
+Working-directory relative forms: (assume cwd = 'workspace/foo')
+
+ Target patterns which do not begin with '//' are taken relative to
+ the working directory. Patterns which begin with '//' are always
+ absolute.
+
+ ...:all Equivalent to '//foo/...:all'.
+ ... (ditto)
+
+ bar/...:all Equivalent to '//foo/bar/...:all'.
+ bar/... (ditto)
+
+ bar:wiz Equivalent to '//foo/bar:wiz'.
+ :foo Equivalent to '//foo:foo'.
+
+ bar:all Equivalent to '//foo/bar:all'.
+ :all Equivalent to '//foo:all'.
+
+Summary of target wildcards:
+
+ :all, Match all rules in the specified packages.
+ :*, :all-targets Match all targets (rules and files) in the specified
+ packages, including .par and _deploy.jar files.
+
+Subtractive patterns:
+
+ Target patterns may be preceded by '-', meaning they should be
+ subtracted from the set of targets accumulated by preceding
+ patterns. For example:
+
+ % blaze build -- foo/... -foo/contrib/...
+
+ builds everything in 'foo', except 'contrib'. In case a target not
+ under 'contrib' depends on something under 'contrib' though, in order to
+ build the former blaze has to build the latter too. As usual, the '--' is
+ required to prevent '-b' from being interpreted as an option.
+
+%{options}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/test.txt b/src/main/java/com/google/devtools/build/lib/runtime/commands/test.txt
new file mode 100644
index 0000000..a1f0523
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/test.txt
@@ -0,0 +1,15 @@
+
+Usage: blaze %{command} <options> <test-targets>
+
+Builds the specified targets and runs all test targets among them (test targets
+might also need to satisfy provided tag, size or language filters) using
+the specified options.
+
+This command accepts all valid options to 'build', and inherits
+defaults for 'build' from your .blazerc. If you don't use .blazerc,
+don't forget to pass all your 'build' options to '%{command}' too.
+
+See 'blaze help target-syntax' for details and examples on how to
+specify targets.
+
+%{options}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/version.txt b/src/main/java/com/google/devtools/build/lib/runtime/commands/version.txt
new file mode 100644
index 0000000..10e1df7
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/version.txt
@@ -0,0 +1,3 @@
+Prints the version information that was embedded when blaze was built.
+
+%{options}
diff --git a/src/main/java/com/google/devtools/build/lib/server/IdleServerTasks.java b/src/main/java/com/google/devtools/build/lib/server/IdleServerTasks.java
new file mode 100644
index 0000000..ad3e475
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/server/IdleServerTasks.java
@@ -0,0 +1,158 @@
+// Copyright 2014 Google Inc. 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.server;
+
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.util.LoggingUtil;
+import com.google.devtools.build.lib.util.ProcMeminfoParser;
+import com.google.devtools.build.lib.vfs.FileStatus;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.Symlinks;
+
+import java.io.IOException;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.annotation.Nullable;
+
+/**
+ * Run cleanup-related tasks during idle periods in the server.
+ * idle() and busy() must be called in that order, and only once.
+ */
+class IdleServerTasks {
+
+ private final Path workspaceDir;
+ private final ScheduledThreadPoolExecutor executor;
+ private static final Logger LOG = Logger.getLogger(IdleServerTasks.class.getName());
+
+ private static final long FIVE_MIN_MILLIS = 1000 * 60 * 5;
+
+ /**
+ * Must be called from the main thread.
+ */
+ public IdleServerTasks(@Nullable Path workspaceDir) {
+ this.executor = new ScheduledThreadPoolExecutor(1);
+ this.workspaceDir = workspaceDir;
+ }
+
+ /**
+ * Called when the server becomes idle. Should not block, but may invoke
+ * new threads.
+ */
+ public void idle() {
+ Preconditions.checkState(!executor.isShutdown());
+
+ // Do a GC cycle while the server is idle.
+ executor.schedule(new Runnable() {
+ @Override public void run() {
+ long before = System.currentTimeMillis();
+ System.gc();
+ LOG.info("Idle GC: " + (System.currentTimeMillis() - before) + "ms");
+ }
+ }, 10, TimeUnit.SECONDS);
+ }
+
+ /**
+ * Called by the main thread when the server gets to work.
+ * Should return quickly.
+ */
+ public void busy() {
+ Preconditions.checkState(!executor.isShutdown());
+
+ // Make sure tasks are finished after shutdown(), so they do not intefere
+ // with subsequent server invocations.
+ executor.shutdown();
+ executor.setContinueExistingPeriodicTasksAfterShutdownPolicy(false);
+ executor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
+
+ boolean interrupted = false;
+ while (true) {
+ try {
+ executor.awaitTermination(Long.MAX_VALUE, TimeUnit.HOURS);
+ break;
+ } catch (InterruptedException e) {
+ // It's unsafe to leak threads - just reset the interrupt bit later.
+ interrupted = true;
+ }
+ }
+
+ if (interrupted) {
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ /**
+ * Return true iff the server should continue processing requests.
+ * Called from the main thread, so it should return quickly.
+ */
+ public boolean continueProcessing(long idleMillis) {
+ if (!memoryHeuristic(idleMillis)) {
+ return false;
+ }
+ if (workspaceDir == null) {
+ return false;
+ }
+
+ FileStatus stat;
+ try {
+ stat = workspaceDir.statIfFound(Symlinks.FOLLOW);
+ } catch (IOException e) {
+ // Do not terminate the server if the workspace is temporarily inaccessible, for example,
+ // if it is on a network filesystem and the connection is down.
+ return true;
+ }
+ return stat != null && stat.isDirectory();
+ }
+
+ private boolean memoryHeuristic(long idleMillis) {
+ if (idleMillis < FIVE_MIN_MILLIS) {
+ // Don't check memory health until after five minutes.
+ return true;
+ }
+
+ ProcMeminfoParser memInfo = null;
+ try {
+ memInfo = new ProcMeminfoParser();
+ } catch (IOException e) {
+ LOG.info("Could not process /proc/meminfo: " + e);
+ return true;
+ }
+
+ long totalPhysical, totalFree;
+ try {
+ totalPhysical = memInfo.getTotalKb();
+ totalFree = memInfo.getFreeRamKb(); // See method javadoc.
+ } catch (IllegalArgumentException e) {
+ // Ugly capture of unchecked exception, similar to that in
+ // LocalHostCapacity.
+ LoggingUtil.logToRemote(Level.WARNING,
+ "Could not read memInfo during idle query", e);
+ return true;
+ }
+ double fractionFree = (double) totalFree / totalPhysical;
+
+ // If the system as a whole is low on memory, let this server die.
+ if (fractionFree < .1) {
+ LOG.info("Terminating due to memory constraints");
+ LOG.info(String.format("Total physical:%d\nTotal free: %d\n",
+ totalPhysical, totalFree));
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/server/RPCServer.java b/src/main/java/com/google/devtools/build/lib/server/RPCServer.java
new file mode 100644
index 0000000..a1e9982
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/server/RPCServer.java
@@ -0,0 +1,562 @@
+// Copyright 2014 Google Inc. 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.server;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
+import com.google.common.io.ByteStreams;
+import com.google.devtools.build.lib.server.RPCService.UnknownCommandException;
+import com.google.devtools.build.lib.server.signal.InterruptSignalHandler;
+import com.google.devtools.build.lib.unix.FilesystemUtils;
+import com.google.devtools.build.lib.unix.LocalClientSocket;
+import com.google.devtools.build.lib.unix.LocalServerSocket;
+import com.google.devtools.build.lib.unix.LocalSocketAddress;
+import com.google.devtools.build.lib.util.Clock;
+import com.google.devtools.build.lib.util.ThreadUtils;
+import com.google.devtools.build.lib.util.io.OutErr;
+import com.google.devtools.build.lib.util.io.StreamMultiplexer;
+import com.google.devtools.build.lib.vfs.Path;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.net.Socket;
+import java.net.SocketTimeoutException;
+import java.nio.charset.Charset;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.logging.Logger;
+
+/**
+ * An RPCServer server is a Java object that sits and waits for RPC requests
+ * (the sit-and-wait is implemented in {@link #serve()}). These requests
+ * arrive via UNIX file sockets. The RPCServer then calls the application
+ * (which implements ServerCommand) to handle the request. (Since the Blaze
+ * server may need to stat hundreds of directories during initialization, this
+ * is a significant speedup.) The server thread will terminate after idling
+ * for a user-specified time.
+ *
+ * Note: If you are contemplating to call into the RPCServer from
+ * within Java, consider using the {@link RPCService} class instead.
+ */
+// TODO(bazel-team): Signal handling.
+// TODO(bazel-team): Gives clients status information when the server is busy. One
+// way to do this is to put the server status in a file (pid, the current
+// target, etc) in the server directory. Alternatively, we can have a separate
+// thread taking care of the server socket and put the information into socket
+// handshakes.
+// TODO(bazel-team): Use Reporter for server-side messages.
+public final class RPCServer {
+
+ private final Clock clock;
+ private final RPCService rpcService;
+ private final LocalServerSocket serverSocket;
+ private final long maxIdleMillis;
+ private final long statusCheckMillis;
+ private final Path serverDirectory;
+ private final Path workspaceDir;
+ private static final Logger LOG = Logger.getLogger(RPCServer.class.getName());
+ private volatile boolean lameDuck;
+
+ private static final long STATUS_CHECK_PERIOD_MILLIS = 1000 * 60; // 1 minute.
+ private static final Splitter NULLTERMINATOR_SPLITTER = Splitter.on('\0');
+
+ /**
+ * Create a new server instance. After creating the server, you can start it
+ * by calling the {@link #serve()} method.
+ *
+ * @param clock The clock to take time measurements
+ * @param rpcService The underlying service object, which takes
+ * care of dispatching to the {@link ServerCommand}
+ * instances, as requests arrive.
+ * @param maxIdleMillis The maximum time the server will wait idly.
+ * @param statusCheckPeriodMillis How long to wait between system status checks.
+ * @param serverDirectory Directory to put file socket and pid files, etc.
+ * @param workspaceDir The workspace. Used solely to ensure it persists.
+ * @throws IOException
+ */
+ public RPCServer(Clock clock, RPCService rpcService,
+ long maxIdleMillis, long statusCheckPeriodMillis,
+ Path serverDirectory, Path workspaceDir)
+ throws IOException {
+ this.clock = clock;
+ this.rpcService = rpcService;
+ this.maxIdleMillis = maxIdleMillis;
+ this.statusCheckMillis = statusCheckPeriodMillis;
+ this.serverDirectory = serverDirectory;
+ this.workspaceDir = workspaceDir;
+
+ this.serverSocket = openServerSocket();
+ serverSocket.setSoTimeout(Math.min(maxIdleMillis, statusCheckMillis));
+ lameDuck = false;
+ }
+
+ /**
+ * Create a new server instance. After creating the server, you can start it
+ * by calling the {@link #serve()} method.
+ *
+ * @param clock The clock to take time measurements
+ * @param rpcService The underlying service object, which takes
+ * care of dispatching to the {@link ServerCommand}
+ * instances, as requests arrive.
+ * @param maxIdleMillis The maximum time the server will wait idly.
+ * @param serverDirectory Directory to put file socket and pid files, etc.
+ * @param workspaceDir The workspace. Used solely to ensure it persists.
+ * @throws IOException
+ */
+ public RPCServer(Clock clock, RPCService rpcService,
+ long maxIdleMillis, Path serverDirectory, Path workspaceDir)
+ throws IOException {
+ this(clock, rpcService, maxIdleMillis, STATUS_CHECK_PERIOD_MILLIS,
+ serverDirectory, workspaceDir);
+ }
+
+ private static void printStack(IOException e) {
+ /*
+ * Hopefully this never happens. It's not very nice to just write this
+ * to the user's console, but I'm not sure what better choice we have.
+ */
+ StringWriter err = new StringWriter();
+ PrintWriter printErr = new PrintWriter(err);
+ printErr.println("=======[BLAZE SERVER: ENCOUNTERED IO EXCEPTION]=======");
+ e.printStackTrace(printErr);
+ printErr.println("=====================================================");
+ LOG.severe(err.toString());
+ }
+
+ /**
+ * Wait on a socket for business (answer requests). Note that this
+ * method won't return until the server shuts down.
+ */
+ public void serve() {
+ // Register the signal handler.
+ final AtomicBoolean inAction = new AtomicBoolean(false);
+ final AtomicBoolean allowingInterrupt = new AtomicBoolean(true);
+ final AtomicLong cmdNum = new AtomicLong();
+ final Thread mainThread = Thread.currentThread();
+ final Object interruptLock = new Object();
+
+ InterruptSignalHandler sigintHandler = new InterruptSignalHandler() {
+ @Override
+ public void run() {
+ LOG.severe("User interrupt");
+
+ // Only interrupt during actions - otherwise we may end up setting the interrupt bit
+ // at the end of a build and responding to it at the beginning of the subsequent build.
+ synchronized (interruptLock) {
+ if (allowingInterrupt.get()) {
+ mainThread.interrupt();
+ }
+ }
+
+ Runnable interruptWatcher = new Runnable() {
+ @Override
+ public void run() {
+ try {
+ long originalCmd = cmdNum.get();
+ Thread.sleep(10 * 1000);
+ if (inAction.get() && cmdNum.get() == originalCmd) {
+ // We're still operating on the same command.
+ // Interrupt took too long.
+ ThreadUtils.warnAboutSlowInterrupt();
+ }
+ } catch (InterruptedException e) {
+ // Ignore.
+ }
+ }
+ };
+
+ if (inAction.get()) {
+ Thread interruptWatcherThread =
+ new Thread(interruptWatcher, "interrupt-watcher-" + cmdNum);
+ interruptWatcherThread.setDaemon(true);
+ interruptWatcherThread.start();
+ }
+ }
+ };
+
+ try {
+ while (!lameDuck) {
+ try {
+ IdleServerTasks idleChecker = new IdleServerTasks(workspaceDir);
+ idleChecker.idle();
+ RequestIo requestIo;
+
+ long startTime = clock.currentTimeMillis();
+ while (true) {
+ try {
+ allowingInterrupt.set(true);
+ Socket socket = serverSocket.accept();
+ long firstContactTime = clock.currentTimeMillis();
+ requestIo = new RequestIo(socket, firstContactTime);
+ break;
+ } catch (SocketTimeoutException e) {
+ long idleTime = clock.currentTimeMillis() - startTime;
+ if (lameDuck) {
+ closeServerSocket();
+ return;
+ } else if (idleTime > maxIdleMillis ||
+ (idleTime > statusCheckMillis && !idleChecker.continueProcessing(idleTime))) {
+ enterLameDuck();
+ }
+ }
+ }
+ idleChecker.busy();
+
+ try {
+ cmdNum.incrementAndGet();
+ inAction.set(true);
+ executeRequest(requestIo);
+ } finally {
+ inAction.set(false);
+ synchronized (interruptLock) {
+ allowingInterrupt.set(false);
+ Thread.interrupted(); // clears thread interrupted status
+ }
+ requestIo.shutdown();
+ if (rpcService.isShutdown()) {
+ return;
+ }
+ }
+ } catch (IOException e) {
+ if (e.getMessage().equals("Broken pipe")) {
+ LOG.info("Connection to the client lost: "
+ + e.getMessage());
+ } else {
+ // Other cases: print the stack for debugging.
+ printStack(e);
+ }
+ }
+ }
+ } finally {
+ rpcService.shutdown();
+ LOG.info("Logging finished");
+ sigintHandler.uninstall();
+ }
+ }
+
+ private void closeServerSocket() {
+ LOG.info("Closing serverSocket.");
+ try {
+ serverSocket.close();
+ } catch (IOException e) {
+ printStack(e);
+ }
+
+ if (!lameDuck) {
+ try {
+ getSocketPath().delete();
+ } catch (IOException e) {
+ printStack(e);
+ }
+ }
+ }
+
+ /**
+ * Allow one last request to be serviced.
+ */
+ private void enterLameDuck() {
+ lameDuck = true;
+ try {
+ getSocketPath().delete();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ serverSocket.setSoTimeout(1);
+ }
+
+ /**
+ * Returns the path of the socket file to be used.
+ */
+ public Path getSocketPath() {
+ return serverDirectory.getRelative("server.socket");
+ }
+
+ /**
+ * Ensures no other server is running for the current socket file. This
+ * guarantees that no two servers are running against the same output
+ * directory.
+ *
+ * @throws IOException if another server holds the lock for the socket file.
+ */
+ public static void ensureExclusiveAccess(Path socketFile) throws IOException {
+ LocalSocketAddress address =
+ new LocalSocketAddress(socketFile.getPathFile());
+ if (socketFile.exists()) {
+ try {
+ new LocalClientSocket(address).close();
+ } catch (IOException e) {
+ // The previous server process is dead--unlink the file:
+ socketFile.delete();
+ return;
+ }
+ // TODO(bazel-team): (2009) Read the previous server's pid from the "hello" message
+ // and add it to the message.
+ throw new IOException("Socket file " + socketFile.getPathString()
+ + " is locked by another server");
+ }
+ }
+
+ /**
+ * Schedule the specified file for (attempted) deletion at JVM exit.
+ */
+ private static void deleteAtExit(final Path socketFile, final boolean deleteParent) {
+ Runtime.getRuntime().addShutdownHook(new Thread() {
+ @Override
+ public void run() {
+ try {
+ socketFile.delete();
+ if (deleteParent) {
+ socketFile.getParentDirectory().delete();
+ }
+ } catch (IOException e) {
+ printStack(e);
+ }
+ }
+ });
+ }
+
+ /**
+ * Opens a UNIX local server socket.
+ * @throws IOException if the socket file is used by another server or can
+ * not be made exclusive.
+ */
+ private LocalServerSocket openServerSocket() throws IOException {
+ // This is the "well known" socket path via which the server is found...
+ Path socketFile = getSocketPath();
+
+ // ...but it may have a name that's too long for AF_UNIX, in which case we
+ // make it a symlink to /tmp/something. This typically only happens in
+ // tests where the --output_base is beneath a very deep temp dir.
+ // (All this extra complexity is just used in tests... *sigh*).
+ if (socketFile.toString().length() >= 108) { // = UNIX_PATH_MAX
+ Path socketLink = socketFile;
+ String tmpDir = System.getProperty("blaze.rpcserver.tmpdir", "/tmp");
+ socketFile = createTempSocketDirectory(socketFile.getRelative(tmpDir)).
+ getRelative("server.socket");
+ LOG.info("Using symlinked socket at " + socketFile);
+
+ socketLink.delete(); // Remove stale symlink, if any.
+ socketLink.createSymbolicLink(socketFile);
+
+ deleteAtExit(socketLink, /*deleteParent=*/false);
+ deleteAtExit(socketFile, /*deleteParent=*/true);
+ } else {
+ deleteAtExit(socketFile, /*deleteParent=*/false);
+ }
+
+ ensureExclusiveAccess(socketFile);
+
+ LocalServerSocket serverSocket = new LocalServerSocket();
+ serverSocket.bind(new LocalSocketAddress(socketFile.getPathFile()));
+ FilesystemUtils.chmod(socketFile.getPathFile(), 0600); // Lock it down.
+ serverSocket.listen(/*backlog=*/50);
+ return serverSocket;
+ }
+
+ // Atomically create a new directory in the (assumed sticky) /tmp directory for use with a
+ // Unix domain socket. The directory will be mode 0700. Retries indefinitely until it
+ // succeeds.
+ private static Path createTempSocketDirectory(Path tempDir) {
+ Random random = new Random();
+ while (true) {
+ Path socketDir = tempDir.getRelative(String.format("blaze-%d", random.nextInt()));
+ try {
+ if (socketDir.createDirectory()) {
+ // Make sure it's private; unfortunately, createDirectory() doesn't take a mode
+ // argument.
+ socketDir.chmod(0700);
+ return socketDir; // Created.
+ }
+ // Already existed; try again.
+ } catch (IOException e) {
+ // Failed; try again.
+ }
+ }
+ }
+
+ /**
+ * Read a string in platform default encoding and split it into a list of
+ * NUL-separated words.
+ *
+ * <p>Blaze consistently uses the platform default encoding (defined in
+ * blaze.cc) to interface with Unix APIs.
+ */
+ private static List<String> readRequest(InputStream input) throws IOException {
+ byte[] inputBytes = ByteStreams.toByteArray(input);
+ if (inputBytes.length == 0) {
+ return null;
+ }
+ String s = new String(inputBytes, Charset.defaultCharset());
+ return ImmutableList.copyOf(NULLTERMINATOR_SPLITTER.split(s));
+ }
+
+ private void executeRequest(RequestIo requestIo) {
+ int exitStatus = 2;
+ try {
+ List<String> request = readRequest(requestIo.in);
+ if (request == null) {
+ LOG.info("Short-circuiting empty request");
+ return;
+ }
+ exitStatus = rpcService.executeRequest(request, requestIo.requestOutErr,
+ requestIo.firstContactTime);
+ LOG.info("Finished executing request");
+ } catch (UnknownCommandException e) {
+ requestIo.requestOutErr.printErrLn("SERVER ERROR: " + e.getMessage());
+ LOG.severe("SERVER ERROR: " + e.getMessage());
+ } catch (Exception e) {
+ // Stacktrace for unknown exception.
+ StringWriter trace = new StringWriter();
+ e.printStackTrace(new PrintWriter(trace, true));
+ requestIo.requestOutErr.printErr("SERVER ERROR: " + trace);
+ LOG.severe("SERVER ERROR: " + trace);
+ }
+
+ if (rpcService.isShutdown()) {
+ // In case of shutdown, disable the listening socket *before* we write
+ // the last part of the response. Otherwise, a sufficiently fast client
+ // could read the response and exit, and a new client could make a
+ // connection to this server, which is still in the listening state, even
+ // though it is about to shut down imminently.
+ closeServerSocket();
+ }
+
+ requestIo.writeExitStatus(exitStatus);
+ }
+
+ /**
+ * Because it's a little complicated, this class factors out all the IO Hook
+ * up we need per request, that is, in
+ * {@link RPCServer#executeRequest(RequestIo)}.
+ * It's unfortunately complicated, so it's explained here.
+ */
+ private static class RequestIo {
+
+ // Used by the client code
+ private final InputStream in;
+ private final OutErr requestOutErr;
+ private final OutputStream controlChannel;
+
+ // just used by this class to keep the state around
+ private final Socket requestSocket;
+ private final OutputStream requestOut;
+ private final long firstContactTime;
+
+ RequestIo(Socket requestSocket, long firstContactTime) throws IOException {
+ this.requestSocket = requestSocket;
+ this.firstContactTime = firstContactTime;
+ this.in = requestSocket.getInputStream();
+ this.requestOut = requestSocket.getOutputStream();
+
+ // We encode the response sent to the client with a multiplexer so
+ // we can send three streams (out / err / control) over one wire stream
+ // (requestOut).
+ StreamMultiplexer multiplexer = new StreamMultiplexer(requestOut);
+
+ // We'll be writing control messages (exit code + out of date message)
+ // to this control channel.
+ controlChannel = multiplexer.createControl();
+
+ // This is the outErr part of the multiplexed output.
+ requestOutErr = OutErr.create(multiplexer.createStdout(),
+ multiplexer.createStderr());
+ // We hook up System.out / System.err to our IO object. Stuff written to
+ // System.out / System.err will show up on the user's screen, prefixed
+ // with "System.out "/"System.err ".
+ requestOutErr.addSystemOutErrAsSource();
+ }
+
+ public void writeExitStatus(int exitStatus) {
+ // Make sure to flush the output / error streams prior to writing the exit status.
+ // The client may stop reading that direction of the socket immediately upon reading the
+ // exit code.
+ flushOutErr();
+ try {
+ controlChannel.write(("" + exitStatus + "\n").getBytes(UTF_8));
+ controlChannel.flush();
+ LOG.info("" + exitStatus);
+ } catch (IOException ignored) {
+ // This exception is historically ignored.
+ }
+ }
+
+ private void flushOutErr() {
+ try {
+ requestOutErr.getOutputStream().flush();
+ } catch (IOException e) {
+ printStack(e);
+ }
+ try {
+ requestOutErr.getErrorStream().flush();
+ } catch (IOException e) {
+ printStack(e);
+ }
+ }
+
+ public void shutdown() {
+ try {
+ requestOut.close();
+ } catch (IOException e) {
+ printStack(e);
+ }
+ try {
+ in.close();
+ } catch (IOException e) {
+ printStack(e);
+ }
+ try {
+ requestSocket.close();
+ } catch (IOException e) {
+ printStack(e);
+ }
+ }
+ }
+
+ /**
+ * Creates and returns a new RPC server.
+ * Use {@link RPCServer#serve()} to start the server.
+ *
+ * @param appCommand The application's ServerCommand implementation.
+ * @param serverDirectory The directory for server-related files. The caller
+ * must ensure the directory has been created.
+ * @param workspaceDir The workspace, used solely to ensure it persists.
+ * @param maxIdleSeconds The idle time in seconds after which the rpc
+ * server will die unless it receives a request.
+ */
+ public static RPCServer newServerWith(Clock clock,
+ ServerCommand appCommand,
+ Path serverDirectory,
+ Path workspaceDir,
+ int maxIdleSeconds)
+ throws IOException {
+ if (!serverDirectory.exists()) {
+ serverDirectory.createDirectory();
+ }
+
+ // Creates and starts the RPC server.
+ RPCService service = new RPCService(appCommand);
+
+ return new RPCServer(clock, service, maxIdleSeconds * 1000L,
+ serverDirectory, workspaceDir);
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/server/RPCService.java b/src/main/java/com/google/devtools/build/lib/server/RPCService.java
new file mode 100644
index 0000000..379e83c
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/server/RPCService.java
@@ -0,0 +1,95 @@
+// Copyright 2014 Google Inc. 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.server;
+
+import com.google.devtools.build.lib.util.io.OutErr;
+
+import java.util.List;
+import java.util.logging.Logger;
+
+/**
+ * An RPCService is a Java object that can process RPC requests. Requests may
+ * be of the form:
+ * <pre>
+ * blaze <blaze-arguments>
+ * </pre>
+ * Requests are delegated to the ServerCommand instance provided
+ * to the constructor.
+ */
+public final class RPCService {
+
+ private boolean isShutdown;
+ private static final Logger LOG = Logger.getLogger(RPCService.class.getName());
+ private final ServerCommand appCommand;
+
+ public RPCService(ServerCommand appCommand) {
+ this.appCommand = appCommand;
+ }
+
+ /**
+ * The {@link #executeRequest(List, OutErr, long)} method may
+ * throw this exception if a command is unknown to the RPC service.
+ */
+ public static class UnknownCommandException extends Exception {
+ private static final long serialVersionUID = 1L;
+ UnknownCommandException(String command) {
+ super("Unknown command: " + command);
+ }
+ }
+
+ /**
+ * Executes the request; returns Unix like return codes (0 means success). May
+ * also throw arbitrary exceptions.
+ */
+ public int executeRequest(List<String> request,
+ OutErr outErr,
+ long firstContactTime) throws Exception {
+ if (isShutdown) {
+ throw new IllegalStateException("Received request after shutdown.");
+ }
+ String command = request.isEmpty() ? "" : request.get(0);
+ if (appCommand != null && command.equals("blaze")) { // an application request
+ int result = appCommand.exec(request.subList(1, request.size()), outErr, firstContactTime);
+ if (appCommand.shutdown()) { // an application shutdown request
+ shutdown();
+ }
+ return result;
+ } else {
+ throw new UnknownCommandException(command);
+ }
+ }
+
+ /**
+ * After executing this function, further requests will fail, and
+ * {@link #isShutdown()} will return true.
+ */
+ public void shutdown() {
+ if (isShutdown) {
+ return;
+ }
+ LOG.info("RPC Service: shutting down ...");
+ isShutdown = true;
+ }
+
+ /**
+ * Has this service been shutdown. If so, any call to
+ * {@link #executeRequest(List, OutErr, long)} will result in an
+ * {@link IllegalStateException}
+ */
+ public boolean isShutdown() {
+ return isShutdown;
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/server/ServerCommand.java b/src/main/java/com/google/devtools/build/lib/server/ServerCommand.java
new file mode 100644
index 0000000..972753c
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/server/ServerCommand.java
@@ -0,0 +1,38 @@
+// Copyright 2014 Google Inc. 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.server;
+
+import com.google.devtools.build.lib.util.io.OutErr;
+
+import java.util.List;
+
+/**
+ * The {@link RPCServer} calls an arbitrary command implementing this
+ * interface.
+ */
+public interface ServerCommand {
+
+ /**
+ * Executes the request, writing any output or error messages into err.
+ * Returns 0 on success; any other value or exception indicates an error.
+ */
+ int exec(List<String> args, OutErr outErr, long firstContactTime) throws Exception;
+
+ /**
+ * The implementation returns true from this method to initiate a shutdown.
+ * No further requests will be handled.
+ */
+ boolean shutdown();
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/server/ServerResponse.java b/src/main/java/com/google/devtools/build/lib/server/ServerResponse.java
new file mode 100644
index 0000000..e5ab930
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/server/ServerResponse.java
@@ -0,0 +1,114 @@
+// Copyright 2014 Google Inc. 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.server;
+
+import com.google.common.base.Preconditions;
+
+import java.io.ByteArrayOutputStream;
+import java.io.UnsupportedEncodingException;
+
+/**
+ * This class models a response from the {@link RPCServer}. This is a
+ * tuple of an error message and the exit status. The encoding of the response
+ * is extremely simple {@link #toString()}:
+ *
+ * <ul><li>Iff a message is present, the wire format is
+ * <pre>message + '\n' + exit code as string + '\n'</pre>
+ * </li>
+ * <li>Otherwise it's just the exit code as string + '\n'</li>
+ * </ul>
+ */
+final class ServerResponse {
+
+ /**
+ * Parses an input string into a {@link ServerResponse} object.
+ */
+ public static ServerResponse parseFrom(String input) {
+ if (input.charAt(input.length() - 1) != '\n') {
+ String msg = "Response must end with newline (" + input + ")";
+ throw new IllegalArgumentException(msg);
+ }
+ int newlineAt = input.lastIndexOf('\n', input.length() - 2);
+
+ final String exitStatusString;
+ final String errorMessage;
+ if (newlineAt == -1) {
+ errorMessage = "";
+ exitStatusString = input.substring(0, input.length() - 1);
+ } else {
+ errorMessage = input.substring(0, newlineAt);
+ exitStatusString = input.substring(newlineAt + 1, input.length() - 1);
+ }
+
+ return new ServerResponse(errorMessage, Integer.parseInt(exitStatusString));
+ }
+
+ /**
+ * Parses {@code bytes} into a {@link ServerResponse} instance, assuming
+ * Latin 1 encoding.
+ */
+ public static ServerResponse parseFrom(byte[] bytes) {
+ try {
+ return parseFrom(new String(bytes, "ISO-8859-1"));
+ } catch (UnsupportedEncodingException e) {
+ throw new AssertionError(e); // Latin 1 is everywhere.
+ }
+ }
+
+ /**
+ * Parses {@code bytes} into a {@link ServerResponse} instance, assuming
+ * Latin 1 encoding.
+ */
+ public static ServerResponse parseFrom(ByteArrayOutputStream bytes) {
+ return parseFrom(bytes.toByteArray());
+ }
+
+ private final String errorMessage;
+ private final int exitStatus;
+
+ /**
+ * Construct a new instance given an error message and an exit status.
+ */
+ public ServerResponse(String errorMessage, int exitStatus) {
+ Preconditions.checkNotNull(errorMessage);
+ this.errorMessage = errorMessage;
+ this.exitStatus = exitStatus;
+ }
+
+ /**
+ * The wire representation of this response object.
+ */
+ @Override
+ public String toString() {
+ if (errorMessage.length() == 0) {
+ return Integer.toString(exitStatus) + '\n';
+ }
+ return errorMessage + '\n' + Integer.toString(exitStatus) + '\n';
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == null || !(other instanceof ServerResponse)) return false;
+ ServerResponse otherResponse = (ServerResponse) other;
+ return exitStatus == otherResponse.exitStatus
+ && errorMessage.equals(otherResponse.errorMessage);
+ }
+
+ @Override
+ public int hashCode() {
+ return exitStatus * 31 ^ errorMessage.hashCode();
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/server/signal/InterruptSignalHandler.java b/src/main/java/com/google/devtools/build/lib/server/signal/InterruptSignalHandler.java
new file mode 100644
index 0000000..521dcef
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/server/signal/InterruptSignalHandler.java
@@ -0,0 +1,58 @@
+// Copyright 2014 Google Inc. 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.server.signal;
+
+
+import com.google.common.base.Preconditions;
+
+import sun.misc.Signal;
+import sun.misc.SignalHandler;
+
+/**
+ * A facade around sun.misc.Signal providing special-purpose SIGINT handling.
+ *
+ * We use this code in preference to using sun.misc directly since the latter
+ * is deprecated, and depending on it causes the jdk1.6 javac to emit an
+ * unsuppressable warning that sun.misc is "Sun proprietary API and may be
+ * removed in a future release".
+ */
+public abstract class InterruptSignalHandler implements Runnable {
+
+ private static final Signal SIGINT = new Signal("INT");
+
+ private SignalHandler oldHandler;
+
+ /**
+ * Constructs an InterruptSignalHandler instance. Until the uninstall()
+ * method is invoked, the delivery of a SIGINT signal to this process will
+ * cause the run() method to be invoked in another thread.
+ */
+ protected InterruptSignalHandler() {
+ this.oldHandler = Signal.handle(SIGINT, new SignalHandler() {
+ @Override
+ public void handle(Signal signal) {
+ run();
+ }
+ });
+ }
+
+ /**
+ * Disables SIGINT handling.
+ */
+ public synchronized final void uninstall() {
+ Preconditions.checkNotNull(oldHandler, "uninstall() already called");
+ Signal.handle(SIGINT, oldHandler);
+ oldHandler = null;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/shell/AbnormalTerminationException.java b/src/main/java/com/google/devtools/build/lib/shell/AbnormalTerminationException.java
new file mode 100644
index 0000000..30562c6
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/shell/AbnormalTerminationException.java
@@ -0,0 +1,52 @@
+// Copyright 2014 Google Inc. 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.shell;
+
+/**
+ * Thrown when a command's execution terminates abnormally -- for example,
+ * if it is killed, or if it terminates with a non-zero exit status.
+ */
+public class AbnormalTerminationException extends CommandException {
+
+ private final CommandResult result;
+
+ public AbnormalTerminationException(final Command command,
+ final CommandResult result,
+ final String message) {
+ super(command, message);
+ this.result = result;
+ }
+
+ public AbnormalTerminationException(final Command command,
+ final CommandResult result,
+ final Throwable cause) {
+ super(command, cause);
+ this.result = result;
+ }
+
+ public AbnormalTerminationException(final Command command,
+ final CommandResult result,
+ final String message,
+ final Throwable cause) {
+ super(command, message, cause);
+ this.result = result;
+ }
+
+ public CommandResult getResult() {
+ return result;
+ }
+
+ private static final long serialVersionUID = 2L;
+}
diff --git a/src/main/java/com/google/devtools/build/lib/shell/BadExitStatusException.java b/src/main/java/com/google/devtools/build/lib/shell/BadExitStatusException.java
new file mode 100644
index 0000000..324007a
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/shell/BadExitStatusException.java
@@ -0,0 +1,36 @@
+// Copyright 2014 Google Inc. 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.shell;
+
+/**
+ * Thrown when a command's execution terminates with a non-zero exit status.
+ */
+public final class BadExitStatusException extends AbnormalTerminationException {
+
+ public BadExitStatusException(final Command command,
+ final CommandResult result,
+ final String message) {
+ super(command, result, message);
+ }
+
+ public BadExitStatusException(final Command command,
+ final CommandResult result,
+ final String message,
+ final Throwable cause) {
+ super(command, result, message, cause);
+ }
+
+ private static final long serialVersionUID = 1L;
+}
diff --git a/src/main/java/com/google/devtools/build/lib/shell/Command.java b/src/main/java/com/google/devtools/build/lib/shell/Command.java
new file mode 100644
index 0000000..ab4a7fc
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/shell/Command.java
@@ -0,0 +1,960 @@
+// Copyright 2014 Google Inc. 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.shell;
+
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * <p>Represents an executable command, including its arguments and
+ * runtime environment (environment variables, working directory). This class
+ * lets a caller execute a command, get its results, and optionally try to kill
+ * the task during execution.</p>
+ *
+ * <p>The use of "shell" in the full name of this class is a misnomer. In
+ * terms of the way its arguments are interpreted, this class is closer to
+ * {@code execve(2)} than to {@code system(3)}. No Bourne shell is executed.
+ *
+ * <p>The most basic use-case for this class is as follows:
+ * <pre>
+ * String[] args = { "/bin/du", "-s", directory };
+ * CommandResult result = new Command(args).execute();
+ * String output = new String(result.getStdout());
+ * </pre>
+ * which writes the output of the {@code du(1)} command into {@code output}.
+ * More complex cases might inspect the stderr stream, kill the subprocess
+ * asynchronously, feed input to its standard input, handle the exceptions
+ * thrown if the command fails, or print the termination status (exit code or
+ * signal name).
+ *
+ * <h4>Invoking the Bourne shell</h4>
+ *
+ * <p>Perhaps the most common command invoked programmatically is the UNIX
+ * shell, {@code /bin/sh}. Because the shell is a general-purpose programming
+ * language, care must be taken to ensure that variable parts of the shell
+ * command (e.g. strings entered by the user) do not contain shell
+ * metacharacters, as this poses a correctness and/or security risk.
+ *
+ * <p>To execute a shell command directly, use the following pattern:
+ * <pre>
+ * String[] args = { "/bin/sh", "-c", shellCommand };
+ * CommandResult result = new Command(args).execute();
+ * </pre>
+ * {@code shellCommand} is a complete Bourne shell program, possibly containing
+ * all kinds of unescaped metacharacters. For example, here's a shell command
+ * that enumerates the working directories of all processes named "foo":
+ * <pre>ps auxx | grep foo | awk '{print $1}' |
+ * while read pid; do readlink /proc/$pid/cwd; done</pre>
+ * It is the responsibility of the caller to ensure that this string means what
+ * they intend.
+ *
+ * <p>Consider the risk posed by allowing the "foo" part of the previous
+ * command to be some arbitrary (untrusted) string called {@code processName}:
+ * <pre>
+ * // WARNING: unsafe!
+ * String shellCommand = "ps auxx | grep " + processName + " | awk '{print $1}' | "
+ * + "while read pid; do readlink /proc/$pid/cwd; done";</pre>
+ * </pre>
+ * Passing this string to {@link Command} is unsafe because if the string
+ * {@processName} contains shell metacharacters, the meaning of the command can
+ * be arbitrarily changed; consider:
+ * <pre>String processName = ". ; rm -fr $HOME & ";</pre>
+ *
+ * <p>To defend against this possibility, it is essential to properly quote the
+ * variable portions of the shell command so that shell metacharacters are
+ * escaped. Use {@link ShellUtils#shellEscape} for this purpose:
+ * <pre>
+ * // Safe.
+ * String shellCommand = "ps auxx | grep " + ShellUtils.shellEscape(processName)
+ * + " | awk '{print $1}' | while read pid; do readlink /proc/$pid/cwd; done";
+ * </pre>
+ *
+ * <p>Tip: if you are only invoking a single known command, and no shell
+ * features (e.g. $PATH lookup, output redirection, pipelines, etc) are needed,
+ * call it directly without using a shell, as in the {@code du(1)} example
+ * above.
+ *
+ * <h4>Other features</h4>
+ *
+ * <p>A caller can optionally specify bytes to be written to the process's
+ * "stdin". The returned {@link CommandResult} object gives the caller access to
+ * the exit status, as well as output from "stdout" and "stderr". To use
+ * this class with processes that generate very large amounts of input/output,
+ * consider
+ * {@link #execute(InputStream, KillableObserver, OutputStream, OutputStream)}
+ * and
+ * {@link #execute(byte[], KillableObserver, OutputStream, OutputStream)}.
+ * </p>
+ *
+ * <p>This class ensures that stdout and stderr streams are read promptly,
+ * avoiding potential deadlock if the output is large. See <a
+ * href="http://www.javaworld.com/javaworld/jw-12-2000/jw-1229-traps.html"> When
+ * <code>Runtime.exec()</code> won't</a>.</p>
+ *
+ * <p>This class is immutable and therefore thread-safe.</p>
+ */
+public final class Command {
+
+ private static final Logger log =
+ Logger.getLogger("com.google.devtools.build.lib.shell.Command");
+
+ /**
+ * Pass this value to {@link #execute(byte[])} to indicate that no input
+ * should be written to stdin.
+ */
+ public static final byte[] NO_INPUT = new byte[0];
+
+ private static final String[] EMPTY_STRING_ARRAY = new String[0];
+
+ /**
+ * Pass this to {@link #execute(byte[], KillableObserver, boolean)} to
+ * indicate that you do not wish to observe / kill the underlying
+ * process.
+ */
+ public static final KillableObserver NO_OBSERVER = new KillableObserver() {
+ @Override
+ public void startObserving(final Killable killable) {
+ // do nothing
+ }
+ @Override
+ public void stopObserving(final Killable killable) {
+ // do nothing
+ }
+ };
+
+ private final ProcessBuilder processBuilder;
+
+ // Start of public API -----------------------------------------------------
+
+ /**
+ * Creates a new {@link Command} that will execute a command line that
+ * is described by a {@link ProcessBuilder}. Command line elements,
+ * environment, and working directory are taken from this object. The
+ * command line is executed exactly as given, without a shell.
+ *
+ * @param processBuilder {@link ProcessBuilder} describing command line
+ * to execute
+ */
+ public Command(final ProcessBuilder processBuilder) {
+ this(processBuilder.command().toArray(EMPTY_STRING_ARRAY),
+ processBuilder.environment(),
+ processBuilder.directory());
+ }
+
+ /**
+ * Creates a new {@link Command} for the given command line elements. The
+ * command line is executed exactly as given, without a shell.
+ * Subsequent calls to {@link #execute()} will use the JVM's working
+ * directory and environment.
+ *
+ * @param commandLineElements elements of raw command line to execute
+ * @throws IllegalArgumentException if commandLine is null or empty
+ */
+ /* TODO(bazel-team): Use varargs here
+ */
+ public Command(final String[] commandLineElements) {
+ this(commandLineElements, null, null);
+ }
+
+ /**
+ * <p>Creates a new {@link Command} for the given command line elements.
+ * Subsequent calls to {@link #execute()} will use the JVM's working
+ * directory and environment.</p>
+ *
+ * <p>Note: be careful when setting useShell to <code>true</code>; you
+ * may inadvertently expose a security hole. See
+ * {@link #Command(String, Map, File)}.</p>
+ *
+ * @param commandLineElements elements of raw command line to execute
+ * @param useShell if true, command is executed using a shell interpreter
+ * (e.g. <code>/bin/sh</code> on Linux); if false, command is executed
+ * exactly as given
+ * @throws IllegalArgumentException if commandLine is null or empty
+ */
+ public Command(final String[] commandLineElements, final boolean useShell) {
+ this(commandLineElements, useShell, null, null);
+ }
+
+ /**
+ * Creates a new {@link Command} for the given command line elements. The
+ * command line is executed exactly as given, without a shell. The given
+ * environment variables and working directory are used in subsequent
+ * calls to {@link #execute()}.
+ *
+ * @param commandLineElements elements of raw command line to execute
+ * @param environmentVariables environment variables to replace JVM's
+ * environment variables; may be null
+ * @param workingDirectory working directory for execution; if null, current
+ * working directory is used
+ * @throws IllegalArgumentException if commandLine is null or empty
+ */
+ public Command(final String[] commandLineElements,
+ final Map<String, String> environmentVariables,
+ final File workingDirectory) {
+ this(commandLineElements, false, environmentVariables, workingDirectory);
+ }
+
+ /**
+ * <p>Creates a new {@link Command} for the given command line elements. The
+ * given environment variables and working directory are used in subsequent
+ * calls to {@link #execute()}.</p>
+ *
+ * <p>Note: be careful when setting useShell to <code>true</code>; you
+ * may inadvertently expose a security hole. See
+ * {@link #Command(String, Map, File)}.</p>
+ *
+ * @param commandLineElements elements of raw command line to execute
+ * @param useShell if true, command is executed using a shell interpreter
+ * (e.g. <code>/bin/sh</code> on Linux); if false, command is executed
+ * exactly as given
+ * @param environmentVariables environment variables to replace JVM's
+ * environment variables; may be null
+ * @param workingDirectory working directory for execution; if null, current
+ * working directory is used
+ * @throws IllegalArgumentException if commandLine is null or empty
+ */
+ public Command(final String[] commandLineElements,
+ final boolean useShell,
+ final Map<String, String> environmentVariables,
+ final File workingDirectory) {
+ if (commandLineElements == null || commandLineElements.length == 0) {
+ throw new IllegalArgumentException("command line is null or empty");
+ }
+ this.processBuilder =
+ new ProcessBuilder(maybeAddShell(commandLineElements, useShell));
+ if (environmentVariables != null) {
+ // TODO(bazel-team) remove next line eventually; it is here to mimic old
+ // Runtime.exec() behavior
+ this.processBuilder.environment().clear();
+ this.processBuilder.environment().putAll(environmentVariables);
+ }
+ this.processBuilder.directory(workingDirectory);
+ }
+
+ private static String[] maybeAddShell(final String[] commandLineElements,
+ final boolean useShell) {
+ if (useShell) {
+ final StringBuilder builder = new StringBuilder();
+ for (final String element : commandLineElements) {
+ if (builder.length() > 0) {
+ builder.append(' ');
+ }
+ builder.append(element);
+ }
+ return Shell.getPlatformShell().shellify(builder.toString());
+ } else {
+ return commandLineElements;
+ }
+ }
+
+ /**
+ * @return raw command line elements to be executed
+ */
+ public String[] getCommandLineElements() {
+ final List<String> elements = processBuilder.command();
+ return elements.toArray(new String[elements.size()]);
+ }
+
+ /**
+ * @return (unmodifiable) {@link Map} view of command's environment variables
+ */
+ public Map<String, String> getEnvironmentVariables() {
+ return Collections.unmodifiableMap(processBuilder.environment());
+ }
+
+ /**
+ * @return working directory used for execution, or null if the current
+ * working directory is used
+ */
+ public File getWorkingDirectory() {
+ return processBuilder.directory();
+ }
+
+ /**
+ * Execute this command with no input to stdin. This call will block until the
+ * process completes or an error occurs.
+ *
+ * @return {@link CommandResult} representing result of the execution
+ * @throws ExecFailedException if {@link Runtime#exec(String[])} fails for any
+ * reason
+ * @throws AbnormalTerminationException if an {@link IOException} is
+ * encountered while reading from the process, or the process was terminated
+ * due to a signal.
+ * @throws BadExitStatusException if the process exits with a
+ * non-zero status
+ */
+ public CommandResult execute() throws CommandException {
+ return execute(NO_INPUT);
+ }
+
+ /**
+ * Execute this command with given input to stdin. This call will block until
+ * the process completes or an error occurs.
+ *
+ * @param stdinInput bytes to be written to process's stdin
+ * @return {@link CommandResult} representing result of the execution
+ * @throws ExecFailedException if {@link Runtime#exec(String[])} fails for any
+ * reason
+ * @throws AbnormalTerminationException if an {@link IOException} is
+ * encountered while reading from the process, or the process was terminated
+ * due to a signal.
+ * @throws BadExitStatusException if the process exits with a
+ * non-zero status
+ * @throws NullPointerException if stdin is null
+ */
+ public CommandResult execute(final byte[] stdinInput)
+ throws CommandException {
+ nullCheck(stdinInput, "stdinInput");
+ return doExecute(new ByteArrayInputSource(stdinInput),
+ NO_OBSERVER,
+ Consumers.createAccumulatingConsumers(),
+ /*killSubprocess=*/false, /*closeOutput=*/false).get();
+ }
+
+ /**
+ * <p>Execute this command with given input to stdin. This call will block
+ * until the process completes or an error occurs. Caller may specify
+ * whether the method should ignore stdout/stderr output. If the
+ * given number of milliseconds elapses before the command has
+ * completed, this method will attempt to kill the command.</p>
+ *
+ * @param stdinInput bytes to be written to process's stdin, or
+ * {@link #NO_INPUT} if no bytes should be written
+ * @param timeout number of milliseconds to wait for command completion
+ * before attempting to kill the command
+ * @param ignoreOutput if true, method will ignore stdout/stderr output
+ * and return value will not contain this data
+ * @return {@link CommandResult} representing result of the execution
+ * @throws ExecFailedException if {@link Runtime#exec(String[])} fails for any
+ * reason
+ * @throws AbnormalTerminationException if an {@link IOException} is
+ * encountered while reading from the process, or the process was terminated
+ * due to a signal.
+ * @throws BadExitStatusException if the process exits with a
+ * non-zero status
+ * @throws NullPointerException if stdin is null
+ */
+ public CommandResult execute(final byte[] stdinInput,
+ final long timeout,
+ final boolean ignoreOutput)
+ throws CommandException {
+ return execute(stdinInput,
+ new TimeoutKillableObserver(timeout),
+ ignoreOutput);
+ }
+
+ /**
+ * <p>Execute this command with given input to stdin. This call will block
+ * until the process completes or an error occurs. Caller may specify
+ * whether the method should ignore stdout/stderr output. The given {@link
+ * KillableObserver} may also terminate the process early while running.</p>
+ *
+ * @param stdinInput bytes to be written to process's stdin, or
+ * {@link #NO_INPUT} if no bytes should be written
+ * @param observer {@link KillableObserver} that should observe the running
+ * process, or {@link #NO_OBSERVER} if caller does not wish to kill
+ * the process
+ * @param ignoreOutput if true, method will ignore stdout/stderr output
+ * and return value will not contain this data
+ * @return {@link CommandResult} representing result of the execution
+ * @throws ExecFailedException if {@link Runtime#exec(String[])} fails for any
+ * reason
+ * @throws AbnormalTerminationException if the process is interrupted (or
+ * killed) before completion, if an {@link IOException} is encountered while
+ * reading from the process, or the process was terminated due to a signal.
+ * @throws BadExitStatusException if the process exits with a
+ * non-zero status
+ * @throws NullPointerException if stdin is null
+ */
+ public CommandResult execute(final byte[] stdinInput,
+ final KillableObserver observer,
+ final boolean ignoreOutput)
+ throws CommandException {
+ // supporting "null" here for backwards compatibility
+ final KillableObserver theObserver =
+ observer == null ? NO_OBSERVER : observer;
+ return doExecute(new ByteArrayInputSource(stdinInput),
+ theObserver,
+ ignoreOutput ? Consumers.createDiscardingConsumers()
+ : Consumers.createAccumulatingConsumers(),
+ /*killSubprocess=*/false, /*closeOutput=*/false).get();
+ }
+
+ /**
+ * <p>Execute this command with given input to stdin. This call blocks
+ * until the process completes or an error occurs. The caller provides
+ * {@link OutputStream} instances into which the process writes its
+ * stdout/stderr output; these streams are <em>not</em> closed when the
+ * process terminates. The given {@link KillableObserver} may also
+ * terminate the process early while running.</p>
+ *
+ * <p>Note that stdout and stderr are written concurrently. If these are
+ * aliased to each other, it is the caller's duty to ensure thread safety.
+ * </p>
+ *
+ * @param stdinInput bytes to be written to process's stdin, or
+ * {@link #NO_INPUT} if no bytes should be written
+ * @param observer {@link KillableObserver} that should observe the running
+ * process, or {@link #NO_OBSERVER} if caller does not wish to kill the
+ * process
+ * @param stdOut the process will write its standard output into this stream.
+ * E.g., you could pass {@link System#out} as <code>stdOut</code>.
+ * @param stdErr the process will write its standard error into this stream.
+ * E.g., you could pass {@link System#err} as <code>stdErr</code>.
+ * @return {@link CommandResult} representing result of the execution. Note
+ * that {@link CommandResult#getStdout()} and
+ * {@link CommandResult#getStderr()} will yield {@link IllegalStateException}
+ * in this case, as the output is written to <code>stdOut/stdErr</code>
+ * instead.
+ * @throws ExecFailedException if {@link Runtime#exec(String[])} fails for any
+ * reason
+ * @throws AbnormalTerminationException if the process is interrupted (or
+ * killed) before completion, if an {@link IOException} is encountered while
+ * reading from the process, or the process was terminated due to a signal.
+ * @throws BadExitStatusException if the process exits with a
+ * non-zero status
+ * @throws NullPointerException if any argument is null.
+ */
+ public CommandResult execute(final byte[] stdinInput,
+ final KillableObserver observer,
+ final OutputStream stdOut,
+ final OutputStream stdErr)
+ throws CommandException {
+ return execute(stdinInput, observer, stdOut, stdErr, false);
+ }
+
+ /**
+ * Like {@link #execute(byte[], KillableObserver, OutputStream, OutputStream)}
+ * but enables setting of the killSubprocessOnInterrupt attribute.
+ *
+ * @param killSubprocessOnInterrupt if set to true, the execution of
+ * this command is <i>interruptible</i>: in other words, if this thread is
+ * interrupted during a call to execute, the subprocess will be terminated
+ * and the call will return in a timely manner. If false, the subprocess
+ * will run to completion; this is the default value use by all other
+ * constructors. The thread's interrupted status is preserved in all cases,
+ * however.
+ */
+ public CommandResult execute(final byte[] stdinInput,
+ final KillableObserver observer,
+ final OutputStream stdOut,
+ final OutputStream stdErr,
+ final boolean killSubprocessOnInterrupt)
+ throws CommandException {
+ nullCheck(stdinInput, "stdinInput");
+ nullCheck(observer, "observer");
+ nullCheck(stdOut, "stdOut");
+ nullCheck(stdErr, "stdErr");
+ return doExecute(new ByteArrayInputSource(stdinInput),
+ observer,
+ Consumers.createStreamingConsumers(stdOut, stdErr),
+ killSubprocessOnInterrupt, false).get();
+ }
+
+ /**
+ * <p>Execute this command with given input to stdin; this stream is closed
+ * when the process terminates, and exceptions raised when closing this
+ * stream are ignored. This call blocks
+ * until the process completes or an error occurs. The caller provides
+ * {@link OutputStream} instances into which the process writes its
+ * stdout/stderr output; these streams are <em>not</em> closed when the
+ * process terminates. The given {@link KillableObserver} may also
+ * terminate the process early while running.</p>
+ *
+ * @param stdinInput The input to this process's stdin
+ * @param observer {@link KillableObserver} that should observe the running
+ * process, or {@link #NO_OBSERVER} if caller does not wish to kill the
+ * process
+ * @param stdOut the process will write its standard output into this stream.
+ * E.g., you could pass {@link System#out} as <code>stdOut</code>.
+ * @param stdErr the process will write its standard error into this stream.
+ * E.g., you could pass {@link System#err} as <code>stdErr</code>.
+ * @return {@link CommandResult} representing result of the execution. Note
+ * that {@link CommandResult#getStdout()} and
+ * {@link CommandResult#getStderr()} will yield {@link IllegalStateException}
+ * in this case, as the output is written to <code>stdOut/stdErr</code>
+ * instead.
+ * @throws ExecFailedException if {@link Runtime#exec(String[])} fails for any
+ * reason
+ * @throws AbnormalTerminationException if the process is interrupted (or
+ * killed) before completion, if an {@link IOException} is encountered while
+ * reading from the process, or the process was terminated due to a signal.
+ * @throws BadExitStatusException if the process exits with a
+ * non-zero status
+ * @throws NullPointerException if any argument is null.
+ */
+ public CommandResult execute(final InputStream stdinInput,
+ final KillableObserver observer,
+ final OutputStream stdOut,
+ final OutputStream stdErr)
+ throws CommandException {
+ nullCheck(stdinInput, "stdinInput");
+ nullCheck(observer, "observer");
+ nullCheck(stdOut, "stdOut");
+ nullCheck(stdErr, "stdErr");
+ return doExecute(new InputStreamInputSource(stdinInput),
+ observer,
+ Consumers.createStreamingConsumers(stdOut, stdErr),
+ /*killSubprocess=*/false, /*closeOutput=*/false).get();
+ }
+
+ /**
+ * <p>Execute this command with given input to stdin; this stream is closed
+ * when the process terminates, and exceptions raised when closing this
+ * stream are ignored. This call blocks
+ * until the process completes or an error occurs. The caller provides
+ * {@link OutputStream} instances into which the process writes its
+ * stdout/stderr output; these streams are closed when the process terminates
+ * if closeOut is set. The given {@link KillableObserver} may also
+ * terminate the process early while running.</p>
+ *
+ * @param stdinInput The input to this process's stdin
+ * @param observer {@link KillableObserver} that should observe the running
+ * process, or {@link #NO_OBSERVER} if caller does not wish to kill the
+ * process
+ * @param stdOut the process will write its standard output into this stream.
+ * E.g., you could pass {@link System#out} as <code>stdOut</code>.
+ * @param stdErr the process will write its standard error into this stream.
+ * E.g., you could pass {@link System#err} as <code>stdErr</code>.
+ * @param closeOut whether to close the output streams when the subprocess
+ * terminates.
+ * @return {@link CommandResult} representing result of the execution. Note
+ * that {@link CommandResult#getStdout()} and
+ * {@link CommandResult#getStderr()} will yield {@link IllegalStateException}
+ * in this case, as the output is written to <code>stdOut/stdErr</code>
+ * instead.
+ * @throws ExecFailedException if {@link Runtime#exec(String[])} fails for any
+ * reason
+ * @throws AbnormalTerminationException if the process is interrupted (or
+ * killed) before completion, if an {@link IOException} is encountered while
+ * reading from the process, or the process was terminated due to a signal.
+ * @throws BadExitStatusException if the process exits with a
+ * non-zero status
+ * @throws NullPointerException if any argument is null.
+ */
+ public CommandResult execute(final InputStream stdinInput,
+ final KillableObserver observer,
+ final OutputStream stdOut,
+ final OutputStream stdErr,
+ boolean closeOut)
+ throws CommandException {
+ nullCheck(stdinInput, "stdinInput");
+ nullCheck(observer, "observer");
+ nullCheck(stdOut, "stdOut");
+ nullCheck(stdErr, "stdErr");
+ return doExecute(new InputStreamInputSource(stdinInput),
+ observer,
+ Consumers.createStreamingConsumers(stdOut, stdErr),
+ false, closeOut).get();
+ }
+
+ /**
+ * <p>Executes this command with the given stdinInput, but does not
+ * wait for it to complete. The caller may choose to observe the status
+ * of the launched process by calling methods on the returned object.
+ *
+ * @param stdinInput bytes to be written to process's stdin, or
+ * {@link #NO_INPUT} if no bytes should be written
+ * @return An object that can be used to check if the process terminated and
+ * obtain the process results.
+ * @throws ExecFailedException if {@link Runtime#exec(String[])} fails for any
+ * reason
+ * @throws NullPointerException if stdin is null
+ */
+ public FutureCommandResult executeAsynchronously(final byte[] stdinInput)
+ throws CommandException {
+ return executeAsynchronously(stdinInput, NO_OBSERVER);
+ }
+
+ /**
+ * <p>Executes this command with the given input to stdin, but does
+ * not wait for it to complete. The caller may choose to observe the
+ * status of the launched process by calling methods on the returned
+ * object. This method performs the minimum cleanup after the
+ * process terminates: It closes the input stream, and it ignores
+ * exceptions that result from closing it. The given {@link
+ * KillableObserver} may also terminate the process early while
+ * running.</p>
+ *
+ * <p>Note that in this case the {@link KillableObserver} will be assigned
+ * to start observing the process via
+ * {@link KillableObserver#startObserving(Killable)} but will only be
+ * unassigned via {@link KillableObserver#stopObserving(Killable)}, if
+ * {@link FutureCommandResult#get()} is called. If the
+ * {@link KillableObserver} implementation used with this method will
+ * not work correctly without calls to
+ * {@link KillableObserver#stopObserving(Killable)} then a new instance
+ * should be used for each call to this method.</p>
+ *
+ * @param stdinInput bytes to be written to process's stdin, or
+ * {@link #NO_INPUT} if no bytes should be written
+ * @param observer {@link KillableObserver} that should observe the running
+ * process, or {@link #NO_OBSERVER} if caller does not wish to kill
+ * the process
+ * @return An object that can be used to check if the process terminated and
+ * obtain the process results.
+ * @throws ExecFailedException if {@link Runtime#exec(String[])} fails for any
+ * reason
+ * @throws NullPointerException if stdin is null
+ */
+ public FutureCommandResult executeAsynchronously(final byte[] stdinInput,
+ final KillableObserver observer)
+ throws CommandException {
+ // supporting "null" here for backwards compatibility
+ final KillableObserver theObserver =
+ observer == null ? NO_OBSERVER : observer;
+ nullCheck(stdinInput, "stdinInput");
+ return doExecute(new ByteArrayInputSource(stdinInput),
+ theObserver,
+ Consumers.createDiscardingConsumers(),
+ /*killSubprocess=*/false, /*closeOutput=*/false);
+ }
+
+ /**
+ * <p>Executes this command with the given input to stdin, but does
+ * not wait for it to complete. The caller may choose to observe the
+ * status of the launched process by calling methods on the returned
+ * object. This method performs the minimum cleanup after the
+ * process terminates: It closes the input stream, and it ignores
+ * exceptions that result from closing it. The caller provides
+ * {@link OutputStream} instances into which the process writes its
+ * stdout/stderr output; these streams are <em>not</em> closed when
+ * the process terminates. The given {@link KillableObserver} may
+ * also terminate the process early while running.</p>
+ *
+ * <p>Note that stdout and stderr are written concurrently. If these are
+ * aliased to each other, or if the caller continues to write to these
+ * streams, it is the caller's duty to ensure thread safety.
+ * </p>
+ *
+ * <p>Note that in this case the {@link KillableObserver} will be assigned
+ * to start observing the process via
+ * {@link KillableObserver#startObserving(Killable)} but will only be
+ * unassigned via {@link KillableObserver#stopObserving(Killable)}, if
+ * {@link FutureCommandResult#get()} is called. If the
+ * {@link KillableObserver} implementation used with this method will
+ * not work correctly without calls to
+ * {@link KillableObserver#stopObserving(Killable)} then a new instance
+ * should be used for each call to this method.</p>
+ *
+ * @param stdinInput The input to this process's stdin
+ * @param observer {@link KillableObserver} that should observe the running
+ * process, or {@link #NO_OBSERVER} if caller does not wish to kill
+ * the process
+ * @param stdOut the process will write its standard output into this stream.
+ * E.g., you could pass {@link System#out} as <code>stdOut</code>.
+ * @param stdErr the process will write its standard error into this stream.
+ * E.g., you could pass {@link System#err} as <code>stdErr</code>.
+ * @return An object that can be used to check if the process terminated and
+ * obtain the process results.
+ * @throws ExecFailedException if {@link Runtime#exec(String[])} fails for any
+ * reason
+ * @throws NullPointerException if stdin is null
+ */
+ public FutureCommandResult executeAsynchronously(final InputStream stdinInput,
+ final KillableObserver observer,
+ final OutputStream stdOut,
+ final OutputStream stdErr)
+ throws CommandException {
+ // supporting "null" here for backwards compatibility
+ final KillableObserver theObserver =
+ observer == null ? NO_OBSERVER : observer;
+ nullCheck(stdinInput, "stdinInput");
+ return doExecute(new InputStreamInputSource(stdinInput),
+ theObserver,
+ Consumers.createStreamingConsumers(stdOut, stdErr),
+ /*killSubprocess=*/false, /*closeOutput=*/false);
+ }
+
+ // End of public API -------------------------------------------------------
+
+ private void nullCheck(Object argument, String argumentName) {
+ if (argument == null) {
+ String message = argumentName + " argument must not be null.";
+ throw new NullPointerException(message);
+ }
+ }
+
+ private FutureCommandResult doExecute(final InputSource stdinInput,
+ final KillableObserver observer,
+ final Consumers.OutErrConsumers outErrConsumers,
+ final boolean killSubprocessOnInterrupt,
+ final boolean closeOutputStreams)
+ throws CommandException {
+
+ logCommand();
+
+ final Process process = startProcess();
+
+ outErrConsumers.logConsumptionStrategy();
+
+ outErrConsumers.registerInputs(process.getInputStream(),
+ process.getErrorStream(),
+ closeOutputStreams);
+
+ processInput(stdinInput, process);
+
+ // TODO(bazel-team): if the input stream is unbounded, observers will not get start
+ // notification in a timely manner!
+ final Killable processKillable = observeProcess(process, observer);
+
+ return new FutureCommandResult() {
+ @Override
+ public CommandResult get() throws AbnormalTerminationException {
+ return waitForProcessToComplete(process,
+ observer,
+ processKillable,
+ outErrConsumers,
+ killSubprocessOnInterrupt);
+ }
+
+ @Override
+ public boolean isDone() {
+ try {
+ // exitValue seems to be the only non-blocking call for
+ // checking process liveness.
+ process.exitValue();
+ return true;
+ } catch (IllegalThreadStateException e) {
+ return false;
+ }
+ }
+ };
+ }
+
+ private Process startProcess()
+ throws ExecFailedException {
+ try {
+ return processBuilder.start();
+ } catch (IOException ioe) {
+ throw new ExecFailedException(this, ioe);
+ }
+ }
+
+ private static interface InputSource {
+ void copyTo(OutputStream out) throws IOException;
+ boolean isEmpty();
+ String toLogString(String sourceName);
+ }
+
+ private static class ByteArrayInputSource implements InputSource {
+ private byte[] bytes;
+ ByteArrayInputSource(byte[] bytes){
+ this.bytes = bytes;
+ }
+ @Override
+ public void copyTo(OutputStream out) throws IOException {
+ out.write(bytes);
+ out.flush();
+ }
+ @Override
+ public boolean isEmpty() {
+ return bytes.length == 0;
+ }
+ @Override
+ public String toLogString(String sourceName) {
+ if (isEmpty()) {
+ return "No input to " + sourceName;
+ } else {
+ return "Input to " + sourceName + ": " +
+ LogUtil.toTruncatedString(bytes);
+ }
+ }
+ }
+
+ private static class InputStreamInputSource implements InputSource {
+ private InputStream inputStream;
+ InputStreamInputSource(InputStream inputStream){
+ this.inputStream = inputStream;
+ }
+ @Override
+ public void copyTo(OutputStream out) throws IOException {
+ byte[] buf = new byte[4096];
+ int r;
+ while ((r = inputStream.read(buf)) != -1) {
+ out.write(buf, 0, r);
+ out.flush();
+ }
+ }
+ @Override
+ public boolean isEmpty() {
+ return false;
+ }
+ @Override
+ public String toLogString(String sourceName) {
+ return "Input to " + sourceName + " is a stream.";
+ }
+ }
+
+ private static void processInput(final InputSource stdinInput,
+ final Process process) {
+ if (log.isLoggable(Level.FINER)) {
+ log.finer(stdinInput.toLogString("stdin"));
+ }
+ try {
+ if (stdinInput.isEmpty()) {
+ return;
+ }
+ stdinInput.copyTo(process.getOutputStream());
+ } catch (IOException ioe) {
+ // Note: this is not an error! Perhaps the command just isn't hungry for
+ // our input and exited with success. Process.waitFor (later) will tell
+ // us.
+ //
+ // (Unlike out/err streams, which are read asynchronously, the input stream is written
+ // synchronously, in its entirety, before processInput returns. If the input is
+ // infinite, and is passed through e.g. "cat" subprocess and back into the
+ // ByteArrayOutputStream, that will eventually run out of memory, causing the output stream
+ // to be closed, "cat" to terminate with SIGPIPE, and processInput to receive an IOException.
+ } finally {
+ // if this statement is ever deleted, the process's outputStream
+ // must be closed elsewhere -- it is not closed automatically
+ Command.silentClose(process.getOutputStream());
+ }
+ }
+
+ private static Killable observeProcess(final Process process,
+ final KillableObserver observer) {
+ final Killable processKillable = new ProcessKillable(process);
+ observer.startObserving(processKillable);
+ return processKillable;
+ }
+
+ private CommandResult waitForProcessToComplete(
+ final Process process,
+ final KillableObserver observer,
+ final Killable processKillable,
+ final Consumers.OutErrConsumers outErr,
+ final boolean killSubprocessOnInterrupt)
+ throws AbnormalTerminationException {
+
+ log.finer("Waiting for process...");
+
+ TerminationStatus status =
+ waitForProcess(process, killSubprocessOnInterrupt);
+
+ observer.stopObserving(processKillable);
+
+ log.finer(status.toString());
+
+ try {
+ outErr.waitForCompletion();
+ } catch (IOException ioe) {
+ CommandResult noOutputResult =
+ new CommandResult(CommandResult.EMPTY_OUTPUT,
+ CommandResult.EMPTY_OUTPUT,
+ status);
+ if (status.success()) {
+ // If command was otherwise successful, throw an exception about this
+ throw new AbnormalTerminationException(this, noOutputResult, ioe);
+ } else {
+ // Otherwise, throw the more important exception -- command
+ // was not successful
+ String message = status
+ + "; also encountered an error while attempting to retrieve output";
+ throw status.exited()
+ ? new BadExitStatusException(this, noOutputResult, message, ioe)
+ : new AbnormalTerminationException(this,
+ noOutputResult, message, ioe);
+ }
+ }
+
+ CommandResult result = new CommandResult(outErr.getAccumulatedOut(),
+ outErr.getAccumulatedErr(),
+ status);
+ result.logThis();
+ if (status.success()) {
+ return result;
+ } else if (status.exited()) {
+ throw new BadExitStatusException(this, result, status.toString());
+ } else {
+ throw new AbnormalTerminationException(this, result, status.toString());
+ }
+ }
+
+ private static TerminationStatus waitForProcess(Process process,
+ boolean killSubprocessOnInterrupt) {
+ boolean wasInterrupted = false;
+ try {
+ while (true) {
+ try {
+ return new TerminationStatus(process.waitFor());
+ } catch (InterruptedException ie) {
+ wasInterrupted = true;
+ if (killSubprocessOnInterrupt) {
+ process.destroy();
+ }
+ }
+ }
+ } finally {
+ // Read this for detailed explanation:
+ // http://www-128.ibm.com/developerworks/java/library/j-jtp05236.html
+ if (wasInterrupted) {
+ Thread.currentThread().interrupt(); // preserve interrupted status
+ }
+ }
+ }
+
+ private void logCommand() {
+ if (!log.isLoggable(Level.FINE)) {
+ return;
+ }
+ log.fine(toDebugString());
+ }
+
+ /**
+ * A string representation of this command object which includes
+ * the arguments, the environment, and the working directory. Avoid
+ * relying on the specifics of this format. Note that the size
+ * of the result string will reflect the size of the command.
+ */
+ public String toDebugString() {
+ StringBuilder message = new StringBuilder(128);
+ message.append("Executing (without brackets):");
+ for (final String arg : processBuilder.command()) {
+ message.append(" [");
+ message.append(arg);
+ message.append(']');
+ }
+ message.append("; environment: ");
+ message.append(processBuilder.environment().toString());
+ final File workingDirectory = processBuilder.directory();
+ message.append("; working dir: ");
+ message.append(workingDirectory == null ?
+ "(current)" :
+ workingDirectory.toString());
+ return message.toString();
+ }
+
+ /**
+ * Close the <code>out</code> stream and log a warning if anything happens.
+ */
+ private static void silentClose(final OutputStream out) {
+ try {
+ out.close();
+ } catch (IOException ioe) {
+ String message = "Unexpected exception while closing output stream";
+ log.log(Level.WARNING, message, ioe);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/shell/CommandException.java b/src/main/java/com/google/devtools/build/lib/shell/CommandException.java
new file mode 100644
index 0000000..a11be97
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/shell/CommandException.java
@@ -0,0 +1,48 @@
+// Copyright 2014 Google Inc. 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.shell;
+
+/**
+ * Superclass of all exceptions that may be thrown during command execution.
+ * It exists to unify them. It also provides access to the command name
+ * and arguments for the failing command.
+ */
+public class CommandException extends Exception {
+
+ private final Command command;
+
+ /** Returns the command that failed. */
+ public Command getCommand() {
+ return command;
+ }
+
+ public CommandException(Command command, final String message) {
+ super(message);
+ this.command = command;
+ }
+
+ public CommandException(Command command, final Throwable cause) {
+ super(cause);
+ this.command = command;
+ }
+
+ public CommandException(Command command, final String message,
+ final Throwable cause) {
+ super(message, cause);
+ this.command = command;
+ }
+
+ private static final long serialVersionUID = 2L;
+}
diff --git a/src/main/java/com/google/devtools/build/lib/shell/CommandResult.java b/src/main/java/com/google/devtools/build/lib/shell/CommandResult.java
new file mode 100644
index 0000000..185f91d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/shell/CommandResult.java
@@ -0,0 +1,116 @@
+// Copyright 2014 Google Inc. 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.shell;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.io.ByteArrayOutputStream;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Encapsulates the results of a command execution, including exit status
+ * and output to stdout and stderr.
+ */
+public final class CommandResult {
+
+ private static final Logger log =
+ Logger.getLogger("com.google.devtools.build.lib.shell.Command");
+
+ private static final byte[] NO_BYTES = new byte[0];
+
+ static final ByteArrayOutputStream EMPTY_OUTPUT =
+ new ByteArrayOutputStream() {
+
+ @Override
+ public byte[] toByteArray() {
+ return NO_BYTES;
+ }
+ };
+
+ static final ByteArrayOutputStream NO_OUTPUT_COLLECTED =
+ new ByteArrayOutputStream(){
+
+ @Override
+ public byte[] toByteArray() {
+ throw new IllegalStateException("Output was not collected");
+ }
+ };
+
+ private final ByteArrayOutputStream stdout;
+ private final ByteArrayOutputStream stderr;
+ private final TerminationStatus terminationStatus;
+
+ CommandResult(final ByteArrayOutputStream stdout,
+ final ByteArrayOutputStream stderr,
+ final TerminationStatus terminationStatus) {
+ checkNotNull(stdout);
+ checkNotNull(stderr);
+ checkNotNull(terminationStatus);
+ this.stdout = stdout;
+ this.stderr = stderr;
+ this.terminationStatus = terminationStatus;
+ }
+
+ /**
+ * @return raw bytes that were written to stdout by the command, or
+ * null if caller did chose to ignore output
+ * @throws IllegalStateException if output was not collected
+ */
+ public byte[] getStdout() {
+ return stdout.toByteArray();
+ }
+
+ /**
+ * @return raw bytes that were written to stderr by the command, or
+ * null if caller did chose to ignore output
+ * @throws IllegalStateException if output was not collected
+ */
+ public byte[] getStderr() {
+ return stderr.toByteArray();
+ }
+
+ /**
+ * @return the result of Process.waitFor for the subprocess.
+ * @deprecated this returns the result of Process.waitFor, which is not
+ * precisely defined, and is not to be confused with the value passed to
+ * exit(2) by the subprocess. Use getTerminationStatus() instead.
+ */
+ @Deprecated
+ public int getExitStatus() {
+ return terminationStatus.getRawResult();
+ }
+
+ /**
+ * @return the termination status of the subprocess.
+ */
+ public TerminationStatus getTerminationStatus() {
+ return terminationStatus;
+ }
+
+ void logThis() {
+ if (!log.isLoggable(Level.FINER)) {
+ return;
+ }
+ log.finer(terminationStatus.toString());
+
+ if (stdout == NO_OUTPUT_COLLECTED) {
+ return;
+ }
+ log.finer("Stdout: " + LogUtil.toTruncatedString(stdout.toByteArray()));
+ log.finer("Stderr: " + LogUtil.toTruncatedString(stderr.toByteArray()));
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/shell/Consumers.java b/src/main/java/com/google/devtools/build/lib/shell/Consumers.java
new file mode 100644
index 0000000..3ed5b7e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/shell/Consumers.java
@@ -0,0 +1,359 @@
+// Copyright 2014 Google Inc. 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.shell;
+
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ThreadFactory;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * This class provides convenience methods for consuming (actively reading)
+ * output and error streams with different consumption policies:
+ * discarding ({@link #createDiscardingConsumers()},
+ * accumulating ({@link #createAccumulatingConsumers()},
+ * and streaming ({@link #createStreamingConsumers(OutputStream, OutputStream)}).
+ */
+class Consumers {
+
+ private static final Logger log =
+ Logger.getLogger("com.google.devtools.build.lib.shell.Command");
+
+ private Consumers() {}
+
+ private static final ExecutorService pool =
+ Executors.newCachedThreadPool(new AccumulatorThreadFactory());
+
+ static OutErrConsumers createDiscardingConsumers() {
+ return new OutErrConsumers(new DiscardingConsumer(),
+ new DiscardingConsumer());
+ }
+
+ static OutErrConsumers createAccumulatingConsumers() {
+ return new OutErrConsumers(new AccumulatingConsumer(),
+ new AccumulatingConsumer());
+ }
+
+ static OutErrConsumers createStreamingConsumers(OutputStream out,
+ OutputStream err) {
+ return new OutErrConsumers(new StreamingConsumer(out),
+ new StreamingConsumer(err));
+ }
+
+ static class OutErrConsumers {
+
+ private final OutputConsumer out;
+ private final OutputConsumer err;
+
+ private OutErrConsumers(final OutputConsumer out, final OutputConsumer err){
+ this.out = out;
+ this.err = err;
+ }
+
+ void registerInputs(InputStream outInput, InputStream errInput, boolean closeStreams){
+ out.registerInput(outInput, closeStreams);
+ err.registerInput(errInput, closeStreams);
+ }
+
+ void cancel() {
+ out.cancel();
+ err.cancel();
+ }
+
+ void waitForCompletion() throws IOException {
+ out.waitForCompletion();
+ err.waitForCompletion();
+ }
+
+ ByteArrayOutputStream getAccumulatedOut(){
+ return out.getAccumulatedOut();
+ }
+
+ ByteArrayOutputStream getAccumulatedErr() {
+ return err.getAccumulatedOut();
+ }
+
+ void logConsumptionStrategy() {
+ // The creation methods guarantee that the consumption strategy is
+ // the same for out and err - doesn't matter whether we call out or err,
+ // let's pick out.
+ out.logConsumptionStrategy();
+ }
+
+ }
+
+ /**
+ * This interface describes just one consumer, which consumes the
+ * InputStream provided by {@link #registerInput(InputStream, boolean)}.
+ * Implementations implement different consumption strategies.
+ */
+ private static interface OutputConsumer {
+ /**
+ * Returns whatever the consumer accumulated internally, or
+ * {@link CommandResult#NO_OUTPUT_COLLECTED} if it doesn't accumulate
+ * any output.
+ *
+ * @see AccumulatingConsumer
+ */
+ ByteArrayOutputStream getAccumulatedOut();
+
+ void logConsumptionStrategy();
+
+ void registerInput(InputStream in, boolean closeConsumer);
+
+ void cancel();
+
+ void waitForCompletion() throws IOException;
+ }
+
+ /**
+ * This consumer sends the input to a stream while consuming it.
+ */
+ private static class StreamingConsumer extends FutureConsumption
+ implements OutputConsumer {
+ private OutputStream out;
+
+ StreamingConsumer(OutputStream out) {
+ this.out = out;
+ }
+
+ @Override
+ public ByteArrayOutputStream getAccumulatedOut() {
+ return CommandResult.NO_OUTPUT_COLLECTED;
+ }
+
+ @Override
+ public void logConsumptionStrategy() {
+ log.finer("Output will be sent to streams provided by client");
+ }
+
+ @Override protected Runnable createConsumingAndClosingSink(InputStream in,
+ boolean closeConsumer) {
+ return new ClosingSink(in, out, closeConsumer);
+ }
+ }
+
+ /**
+ * This consumer sends the input to a {@link ByteArrayOutputStream}
+ * while consuming it. This accumulated stream can be obtained by
+ * calling {@link #getAccumulatedOut()}.
+ */
+ private static class AccumulatingConsumer extends FutureConsumption
+ implements OutputConsumer {
+ private ByteArrayOutputStream out = new ByteArrayOutputStream();
+
+ @Override
+ public ByteArrayOutputStream getAccumulatedOut() {
+ return out;
+ }
+
+ @Override
+ public void logConsumptionStrategy() {
+ log.finer("Output will be accumulated (promptly read off) and returned");
+ }
+
+ @Override public Runnable createConsumingAndClosingSink(InputStream in, boolean closeConsumer) {
+ return new ClosingSink(in, out);
+ }
+ }
+
+ /**
+ * This consumer just discards whatever it reads.
+ */
+ private static class DiscardingConsumer extends FutureConsumption
+ implements OutputConsumer {
+ private DiscardingConsumer() {
+ }
+
+ @Override
+ public ByteArrayOutputStream getAccumulatedOut() {
+ return CommandResult.NO_OUTPUT_COLLECTED;
+ }
+
+ @Override
+ public void logConsumptionStrategy() {
+ log.finer("Output will be ignored");
+ }
+
+ @Override public Runnable createConsumingAndClosingSink(InputStream in, boolean closeConsumer) {
+ return new ClosingSink(in);
+ }
+ }
+
+ /**
+ * A mixin that makes consumers active - this is where we kick of
+ * multithreading ({@link #registerInput(InputStream, boolean)}), cancel actions
+ * and wait for the consumers to complete.
+ */
+ private abstract static class FutureConsumption implements OutputConsumer {
+
+ private Future<?> future;
+
+ @Override
+ public void registerInput(InputStream in, boolean closeConsumer){
+ Runnable sink = createConsumingAndClosingSink(in, closeConsumer);
+ future = pool.submit(sink);
+ }
+
+ protected abstract Runnable createConsumingAndClosingSink(InputStream in, boolean close);
+
+ @Override
+ public void cancel() {
+ future.cancel(true);
+ }
+
+ @Override
+ public void waitForCompletion() throws IOException {
+ boolean wasInterrupted = false;
+ try {
+ while (true) {
+ try {
+ future.get();
+ break;
+ } catch (InterruptedException ie) {
+ wasInterrupted = true;
+ // continue waiting
+ } catch (ExecutionException ee) {
+ // Runnable threw a RuntimeException
+ Throwable nested = ee.getCause();
+ if (nested instanceof RuntimeException) {
+ final RuntimeException re = (RuntimeException) nested;
+ // The stream sink classes, unfortunately, tunnel IOExceptions
+ // out of run() in a RuntimeException. If that's the case,
+ // unpack and re-throw the IOException. Otherwise, re-throw
+ // this unexpected RuntimeException
+ final Throwable cause = re.getCause();
+ if (cause instanceof IOException) {
+ throw (IOException) cause;
+ } else {
+ throw re;
+ }
+ } else if (nested instanceof OutOfMemoryError) {
+ // OutOfMemoryError does not support exception chaining.
+ throw (OutOfMemoryError) nested;
+ } else if (nested instanceof Error) {
+ throw new Error("unhandled Error in worker thread", ee);
+ } else {
+ throw new RuntimeException("unknown execution problem", ee);
+ }
+ }
+ }
+ } finally {
+ // Read this for detailed explanation:
+ // http://www-128.ibm.com/developerworks/java/library/j-jtp05236.html
+ if (wasInterrupted) {
+ Thread.currentThread().interrupt(); // preserve interrupted status
+ }
+ }
+ }
+ }
+
+ /**
+ * Factory which produces threads with a 32K stack size.
+ */
+ private static class AccumulatorThreadFactory implements ThreadFactory {
+
+ private static final int THREAD_STACK_SIZE = 32 * 1024;
+
+ private static int threadInitNumber;
+
+ private static synchronized int nextThreadNum() {
+ return threadInitNumber++;
+ }
+
+ @Override
+ public Thread newThread(final Runnable runnable) {
+ final Thread t =
+ new Thread(null,
+ runnable,
+ "Command-Accumulator-Thread-" + nextThreadNum(),
+ THREAD_STACK_SIZE);
+ // Don't let this thread hold up JVM exit
+ t.setDaemon(true);
+ return t;
+ }
+
+ }
+
+ /**
+ * A sink that closes its input stream once its done.
+ */
+ private static class ClosingSink implements Runnable {
+
+ private final InputStream in;
+ private final OutputStream out;
+ private final Runnable sink;
+ private final boolean close;
+
+ /**
+ * Creates a sink that will pump InputStream <code>in</code>
+ * into OutputStream <code>out</code>.
+ */
+ ClosingSink(final InputStream in, OutputStream out) {
+ this(in, out, false);
+ }
+
+ /**
+ * Creates a sink that will read <code>in</code> and discard it.
+ */
+ ClosingSink(final InputStream in) {
+ this.sink = InputStreamSink.newRunnableSink(in);
+ this.in = in;
+ this.close = false;
+ this.out = null;
+ }
+
+ ClosingSink(final InputStream in, OutputStream out, boolean close){
+ this.sink = InputStreamSink.newRunnableSink(in, out);
+ this.in = in;
+ this.out = out;
+ this.close = close;
+ }
+
+
+ @Override
+ public void run() {
+ try {
+ sink.run();
+ } finally {
+ silentClose(in);
+ if (close && out != null) {
+ silentClose(out);
+ }
+ }
+ }
+
+ }
+
+ /**
+ * Close the <code>in</code> stream and log a warning if anything happens.
+ */
+ private static void silentClose(final Closeable closeable) {
+ try {
+ closeable.close();
+ } catch (IOException ioe) {
+ String message = "Unexpected exception while closing input stream";
+ log.log(Level.WARNING, message, ioe);
+ }
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/shell/ExecFailedException.java b/src/main/java/com/google/devtools/build/lib/shell/ExecFailedException.java
new file mode 100644
index 0000000..24f42a6
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/shell/ExecFailedException.java
@@ -0,0 +1,28 @@
+// Copyright 2014 Google Inc. 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.shell;
+
+/**
+ * Thrown when a command could not even be executed by the JVM --
+ * in particular, when {@link Runtime#exec(String[])} fails.
+ */
+public final class ExecFailedException extends CommandException {
+
+ public ExecFailedException(Command command, final Throwable cause) {
+ super(command, cause);
+ }
+
+ private static final long serialVersionUID = 2L;
+}
diff --git a/src/main/java/com/google/devtools/build/lib/shell/FutureCommandResult.java b/src/main/java/com/google/devtools/build/lib/shell/FutureCommandResult.java
new file mode 100644
index 0000000..3e1f5c9
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/shell/FutureCommandResult.java
@@ -0,0 +1,40 @@
+// Copyright 2014 Google Inc. 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.shell;
+
+/**
+ * Supplier of the command result which additionally allows to check if
+ * the command already terminated. Implementing full fledged Future would
+ * be a much harder undertaking, so a bare minimum that makes this class still
+ * useful for asynchronous command execution is implemented.
+ */
+public interface FutureCommandResult {
+ /**
+ * Returns the result of command execution. If the process is not finished
+ * yet (as reported by {@link #isDone()}, the call will block until that
+ * process terminates.
+ *
+ * @return non-null result of command execution
+ * @throws AbnormalTerminationException if command execution failed
+ */
+ CommandResult get() throws AbnormalTerminationException;
+
+ /**
+ * Returns true if the process terminated, the command result is available
+ * and the call to {@link #get()} will not block.
+ *
+ * @return true if the process terminated
+ */
+ boolean isDone();
+}
diff --git a/src/main/java/com/google/devtools/build/lib/shell/InputStreamSink.java b/src/main/java/com/google/devtools/build/lib/shell/InputStreamSink.java
new file mode 100644
index 0000000..c35552b
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/shell/InputStreamSink.java
@@ -0,0 +1,133 @@
+// Copyright 2014 Google Inc. 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.shell;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * Provides sinks for input streams. Continuously read an input stream
+ * until the end-of-file is encountered. The stream may be redirected to
+ * an {@link OutputStream}, or discarded.
+ * <p>
+ * This class is useful for handing the {@code stdout} and {@code stderr}
+ * streams from a {@link Process} started with {@link Runtime#exec(String)}.
+ * If these streams are not consumed, the Process may block resulting in a
+ * deadlock.
+ *
+ * @see <a href="http://www.javaworld.com/javaworld/jw-12-2000/jw-1229-traps.html">
+ * JavaWorld: When Runtime.exec() won't</a>
+ */
+public final class InputStreamSink {
+
+ /**
+ * Black hole into which bytes are sometimes discarded by {@link NullSink}.
+ * It is shared by all threads since the actual contents of the buffer
+ * are irrelevant.
+ */
+ private static final byte[] DISCARD = new byte[4096];
+
+ // Supresses default constructor; ensures non-instantiability
+ private InputStreamSink() {
+ }
+
+ /**
+ * A {@link Thread} which reads and discards data from an
+ * {@link InputStream}.
+ */
+ private static class NullSink implements Runnable {
+ private final InputStream in;
+
+ public NullSink(InputStream in) {
+ this.in = in;
+ }
+
+ @Override
+ public void run() {
+ try {
+ try {
+ // Attempt to just skip all input
+ do {
+ in.skip(Integer.MAX_VALUE);
+ } while (in.read() != -1); // Need to test for EOF
+ } catch (IOException ioe) {
+ // Some streams throw IOException when skip() is called;
+ // resort to reading off all input with read():
+ while (in.read(DISCARD) != -1) {
+ // no loop body
+ }
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ /**
+ * A {@link Thread} which reads data from an {@link InputStream},
+ * and translates it into an {@link OutputStream}.
+ */
+ private static class CopySink implements Runnable {
+
+ private final InputStream in;
+ private final OutputStream out;
+
+ public CopySink(InputStream in, OutputStream out) {
+ this.in = in;
+ this.out = out;
+ }
+
+ @Override
+ public void run() {
+ try {
+ byte[] buffer = new byte[2048];
+ int bytesRead;
+ while ((bytesRead = in.read(buffer)) >= 0) {
+ out.write(buffer, 0, bytesRead);
+ out.flush();
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ /**
+ * Creates a {@link Runnable} which consumes the provided
+ * {@link InputStream} 'in', discarding its contents.
+ */
+ public static Runnable newRunnableSink(InputStream in) {
+ if (in == null) {
+ throw new NullPointerException("in");
+ }
+ return new NullSink(in);
+ }
+
+ /**
+ * Creates a {@link Runnable} which copies everything from 'in'
+ * to 'out'. 'out' will be written to and flushed after each
+ * read from 'in'. However, 'out' will not be closed.
+ */
+ public static Runnable newRunnableSink(InputStream in, OutputStream out) {
+ if (in == null) {
+ throw new NullPointerException("in");
+ }
+ if (out == null) {
+ throw new NullPointerException("out");
+ }
+ return new CopySink(in, out);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/shell/Killable.java b/src/main/java/com/google/devtools/build/lib/shell/Killable.java
new file mode 100644
index 0000000..66d1146
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/shell/Killable.java
@@ -0,0 +1,31 @@
+// Copyright 2014 Google Inc. 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.shell;
+
+/**
+ * Implementations encapsulate a running process that can be killed.
+ * In particular, here, it is used to wrap up a {@link Process} object
+ * and expose it to a {@link KillableObserver}. It is wrapped in this way
+ * so that the actual {@link Process} object can't be altered by
+ * a {@link KillableObserver}.
+ */
+public interface Killable {
+
+ /**
+ * Kill this killable instance.
+ */
+ void kill();
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/shell/KillableObserver.java b/src/main/java/com/google/devtools/build/lib/shell/KillableObserver.java
new file mode 100644
index 0000000..62d9aa0
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/shell/KillableObserver.java
@@ -0,0 +1,49 @@
+// Copyright 2014 Google Inc. 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.shell;
+
+/**
+ * Implementations of this interface observe, and potentially kill,
+ * a {@link Killable} object. This is the mechanism by which "kill"
+ * functionality is exposed to callers in the
+ * {@link Command#execute(byte[], KillableObserver, boolean)} method.
+ *
+ */
+public interface KillableObserver {
+
+ /**
+ * <p>Begin observing the given {@link Killable}. This method must return
+ * promptly; until it returns, {@link Command#execute()} cannot complete.
+ * Implementations may wish to start a new {@link Thread} here to handle
+ * kill logic, and to interrupt or otherwise ask the thread to stop in the
+ * {@link #stopObserving(Killable)} method. See
+ * <a href="http://builder.com.com/5100-6370-5144546.html">
+ * Interrupting Java threads</a> for notes on how to implement this
+ * correctly.</p>
+ *
+ * <p>Implementations may or may not be able to observe more than
+ * one {@link Killable} at a time; see javadoc for details.</p>
+ *
+ * @param killable killable to observer
+ */
+ void startObserving(Killable killable);
+
+ /**
+ * Stop observing the given {@link Killable}, since it is
+ * no longer active.
+ */
+ void stopObserving(Killable killable);
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/shell/LogUtil.java b/src/main/java/com/google/devtools/build/lib/shell/LogUtil.java
new file mode 100644
index 0000000..ab646f6
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/shell/LogUtil.java
@@ -0,0 +1,54 @@
+// Copyright 2014 Google Inc. 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.shell;
+
+/**
+ * Utilities for logging.
+ */
+class LogUtil {
+
+ private LogUtil() {}
+
+ private final static int TRUNCATE_STRINGS_AT = 150;
+
+ /**
+ * Make a string out of a byte array, and truncate it to a reasonable length.
+ * Useful for preventing logs from becoming excessively large.
+ */
+ static String toTruncatedString(final byte[] bytes) {
+ if(bytes == null || bytes.length == 0) {
+ return "";
+ }
+ /*
+ * Yes, we'll use the platform encoding here, and this is one of the rare
+ * cases where it makes sense. You want the logs to be encoded so that
+ * your platform tools (vi, emacs, cat) can render them, don't you?
+ * In practice, this means ISO-8859-1 or UTF-8, I guess.
+ */
+ try {
+ if (bytes.length > TRUNCATE_STRINGS_AT) {
+ return new String(bytes, 0, TRUNCATE_STRINGS_AT)
+ + "[... truncated. original size was " + bytes.length + " bytes.]";
+ }
+ return new String(bytes);
+ } catch (Exception e) {
+ /*
+ * In case encoding a binary string doesn't work for some reason, we
+ * don't want to bring a logging server down - do we? So we're paranoid.
+ */
+ return "IOUtil.toTruncatedString: " + e.getMessage();
+ }
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/shell/ProcessKillable.java b/src/main/java/com/google/devtools/build/lib/shell/ProcessKillable.java
new file mode 100644
index 0000000..5d0cb8f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/shell/ProcessKillable.java
@@ -0,0 +1,36 @@
+// Copyright 2014 Google Inc. 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.shell;
+
+/**
+ * {@link Killable} implementation which simply wraps a
+ * {@link Process} instance.
+ */
+final class ProcessKillable implements Killable {
+
+ private final Process process;
+
+ ProcessKillable(final Process process) {
+ this.process = process;
+ }
+
+ /**
+ * Calls {@link Process#destroy()}.
+ */
+ @Override
+ public void kill() {
+ process.destroy();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/shell/Shell.java b/src/main/java/com/google/devtools/build/lib/shell/Shell.java
new file mode 100644
index 0000000..2cae24e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/shell/Shell.java
@@ -0,0 +1,132 @@
+// Copyright 2014 Google Inc. 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.shell;
+
+import java.util.logging.Logger;
+
+/**
+ * <p>Represents an OS shell, such as "cmd" on Windows or "sh" on Unix-like
+ * platforms. Currently, Linux and Windows XP are supported.</p>
+ *
+ * <p>This class encapsulates shell-specific logic, like how to
+ * create a command line that uses the shell to invoke another command.
+ */
+public abstract class Shell {
+
+ private static final Logger log =
+ Logger.getLogger("com.google.devtools.build.lib.shell.Shell");
+
+ private static final Shell platformShell;
+
+ static {
+ final String osName = System.getProperty("os.name");
+ if ("Linux".equals(osName)) {
+ platformShell = new SHShell();
+ } else if ("Windows XP".equals(osName)) {
+ platformShell = new WindowsCMDShell();
+ } else {
+ log.severe("OS not supported; will not be able to execute commands");
+ platformShell = null;
+ }
+ log.config("Loaded shell support '" + platformShell +
+ "' for OS '" + osName + "'");
+ }
+
+ private Shell() {
+ // do nothing
+ }
+
+ /**
+ * @return {@link Shell} subclass appropriate for the current platform
+ * @throws UnsupportedOperationException if no such subclass exists
+ */
+ public static Shell getPlatformShell() {
+ if (platformShell == null) {
+ throw new UnsupportedOperationException("OS is not supported");
+ }
+ return platformShell;
+ }
+
+ /**
+ * Creates a command line suitable for execution by
+ * {@link Runtime#exec(String[])} from the given command string,
+ * a command line which uses a shell appropriate for a particular
+ * platform to execute the command (e.g. "/bin/sh" on Linux).
+ *
+ * @param command command for which to create a command line
+ * @return String[] suitable for execution by
+ * {@link Runtime#exec(String[])}
+ */
+ public abstract String[] shellify(final String command);
+
+
+ /**
+ * Represents the <code>sh</code> shell commonly found on Unix-like
+ * operating systems, including Linux.
+ */
+ private static final class SHShell extends Shell {
+
+ /**
+ * <p>Returns a command line which uses <code>cmd</code> to execute
+ * the {@link Command}. Given the command <code>foo bar baz</code>,
+ * for example, this will return a String array corresponding
+ * to the command line:</p>
+ *
+ * <p><code>/bin/sh -c "foo bar baz"</code></p>
+ *
+ * <p>That is, it always returns a 3-element array.</p>
+ *
+ * @param command command for which to create a command line
+ * @return String[] suitable for execution by
+ * {@link Runtime#exec(String[])}
+ */
+ @Override public String[] shellify(final String command) {
+ if (command == null || command.length() == 0) {
+ throw new IllegalArgumentException("command is null or empty");
+ }
+ return new String[] { "/bin/sh", "-c", command };
+ }
+
+ }
+
+ /**
+ * Represents the Windows command shell <code>cmd</code>.
+ */
+ private static final class WindowsCMDShell extends Shell {
+
+ /**
+ * <p>Returns a command line which uses <code>cmd</code> to execute
+ * the {@link Command}. Given the command <code>foo bar baz</code>,
+ * for example, this will return a String array corresponding
+ * to the command line:</p>
+ *
+ * <p><code>cmd /S /C "foo bar baz"</code></p>
+ *
+ * <p>That is, it always returns a 4-element array.</p>
+ *
+ * @param command command for which to create a command line
+ * @return String[] suitable for execution by
+ * {@link Runtime#exec(String[])}
+ */
+ @Override public String[] shellify(final String command) {
+ if (command == null || command.length() == 0) {
+ throw new IllegalArgumentException("command is null or empty");
+ }
+ return new String[] { "cmd", "/S", "/C", command };
+ }
+
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/shell/ShellUtils.java b/src/main/java/com/google/devtools/build/lib/shell/ShellUtils.java
new file mode 100644
index 0000000..5157f34
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/shell/ShellUtils.java
@@ -0,0 +1,145 @@
+// Copyright 2014 Google Inc. 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.shell;
+
+import java.util.List;
+
+/**
+ * Utility functions for Bourne shell commands, including escaping and
+ * tokenizing.
+ */
+public abstract class ShellUtils {
+
+ private ShellUtils() {}
+
+ /**
+ * Characters that have no special meaning to the shell.
+ */
+ private static final String SAFE_PUNCTUATION = "@%-_+:,./";
+
+ /**
+ * Quotes a word so that it can be used, without further quoting,
+ * as an argument (or part of an argument) in a shell command.
+ */
+ public static String shellEscape(String word) {
+ int len = word.length();
+ if (len == 0) {
+ // Empty string is a special case: needs to be quoted to ensure that it gets
+ // treated as a separate argument.
+ return "''";
+ }
+ for (int ii = 0; ii < len; ii++) {
+ char c = word.charAt(ii);
+ // We do this positively so as to be sure we don't inadvertently forget
+ // any unsafe characters.
+ if (!Character.isLetterOrDigit(c) && SAFE_PUNCTUATION.indexOf(c) == -1) {
+ // replace() actually means "replace all".
+ return "'" + word.replace("'", "'\\''") + "'";
+ }
+ }
+ return word;
+ }
+
+ /**
+ * Given an argv array such as might be passed to execve(2), returns a string
+ * that can be copied and pasted into a Bourne shell for a similar effect.
+ */
+ public static String prettyPrintArgv(List<String> argv) {
+ StringBuilder buf = new StringBuilder();
+ for (String arg: argv) {
+ if (buf.length() > 0) {
+ buf.append(' ');
+ }
+ buf.append(shellEscape(arg));
+ }
+ return buf.toString();
+ }
+
+
+ /**
+ * Thrown by tokenize method if there is an error
+ */
+ public static class TokenizationException extends Exception {
+ TokenizationException(String message) {
+ super(message);
+ }
+ }
+
+ /**
+ * Populates the passed list of command-line options extracted from {@code
+ * optionString}, which is a string containing multiple options, delimited in
+ * a Bourne shell-like manner.
+ *
+ * @param options the list to be populated with tokens.
+ * @param optionString the string to be tokenized.
+ * @throws TokenizationException if there was an error (such as an
+ * unterminated quotation).
+ */
+ public static void tokenize(List<String> options, String optionString)
+ throws TokenizationException {
+ // See test suite for examples.
+ //
+ // Note: backslash escapes the following character, except within a
+ // single-quoted region where it is literal.
+
+ StringBuilder token = new StringBuilder();
+ boolean forceToken = false;
+ char quotation = '\0'; // NUL, '\'' or '"'
+ for (int ii = 0, len = optionString.length(); ii < len; ii++) {
+ char c = optionString.charAt(ii);
+ if (quotation != '\0') { // in quotation
+ if (c == quotation) { // end of quotation
+ quotation = '\0';
+ } else if (c == '\\' && quotation == '"') { // backslash in "-quotation
+ if (++ii == len) {
+ throw new TokenizationException("backslash at end of string");
+ }
+ c = optionString.charAt(ii);
+ if (c != '\\' && c != '"') {
+ token.append('\\');
+ }
+ token.append(c);
+ } else { // regular char, in quotation
+ token.append(c);
+ }
+ } else { // not in quotation
+ if (c == '\'' || c == '"') { // begin single/double quotation
+ quotation = c;
+ forceToken = true;
+ } else if (c == ' ' || c == '\t') { // space, not quoted
+ if (forceToken || token.length() > 0) {
+ options.add(token.toString());
+ token = new StringBuilder();
+ forceToken = false;
+ }
+ } else if (c == '\\') { // backslash, not quoted
+ if (++ii == len) {
+ throw new TokenizationException("backslash at end of string");
+ }
+ token.append(optionString.charAt(ii));
+ } else { // regular char, not quoted
+ token.append(c);
+ }
+ }
+ }
+ if (quotation != '\0') {
+ throw new TokenizationException("unterminated quotation");
+ }
+ if (forceToken || token.length() > 0) {
+ options.add(token.toString());
+ }
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/shell/SimpleKillableObserver.java b/src/main/java/com/google/devtools/build/lib/shell/SimpleKillableObserver.java
new file mode 100644
index 0000000..85794b8
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/shell/SimpleKillableObserver.java
@@ -0,0 +1,60 @@
+// Copyright 2014 Google Inc. 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.shell;
+
+/**
+ * <p>A simple implementation of {@link KillableObserver} which can be told
+ * explicitly to kill its {@link Killable} by calling {@link #kill()}. This
+ * is the sort of functionality that callers might expect to find available
+ * on the {@link Command} class.</p>
+ *
+ * <p>Note that this class can only observe one {@link Killable} at a time;
+ * multiple instances should be used for concurrent calls to
+ * {@link Command#execute(byte[], KillableObserver, boolean)}.</p>
+ */
+public final class SimpleKillableObserver implements KillableObserver {
+
+ private Killable killable;
+
+ /**
+ * Does nothing except store a reference to the given {@link Killable}.
+ *
+ * @param killable {@link Killable} to kill
+ */
+ public synchronized void startObserving(final Killable killable) {
+ this.killable = killable;
+ }
+
+ /**
+ * Forgets reference to {@link Killable} provided to
+ * {@link #startObserving(Killable)}
+ */
+ public synchronized void stopObserving(final Killable killable) {
+ if (!this.killable.equals(killable)) {
+ throw new IllegalStateException("start/stopObservering called with " +
+ "different Killables");
+ }
+ this.killable = null;
+ }
+
+ /**
+ * Calls {@link Killable#kill()} on the saved {@link Killable}.
+ */
+ public synchronized void kill() {
+ if (killable != null) {
+ killable.kill();
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/shell/TerminationStatus.java b/src/main/java/com/google/devtools/build/lib/shell/TerminationStatus.java
new file mode 100644
index 0000000..73616c4
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/shell/TerminationStatus.java
@@ -0,0 +1,162 @@
+// Copyright 2014 Google Inc. 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.shell;
+
+/**
+ * Represents the termination status of a command. {@link Process#waitFor} is
+ * not very precisely specified, so this class encapsulates the interpretation
+ * of values returned by it.
+ *
+ * Caveat: due to the lossy encoding, it's not always possible to accurately
+ * distinguish signal and exit cases. In particular, processes that exit with
+ * a value within the interval [129, 191] will be mistaken for having been
+ * terminated by a signal.
+ *
+ * Instances are immutable.
+ */
+public final class TerminationStatus {
+
+ private final int waitResult;
+
+ /**
+ * Values taken from the glibc strsignal(3) function.
+ */
+ private static final String[] SIGNAL_STRINGS = {
+ null,
+ "Hangup",
+ "Interrupt",
+ "Quit",
+ "Illegal instruction",
+ "Trace/breakpoint trap",
+ "Aborted",
+ "Bus error",
+ "Floating point exception",
+ "Killed",
+ "User defined signal 1",
+ "Segmentation fault",
+ "User defined signal 2",
+ "Broken pipe",
+ "Alarm clock",
+ "Terminated",
+ "Stack fault",
+ "Child exited",
+ "Continued",
+ "Stopped (signal)",
+ "Stopped",
+ "Stopped (tty input)",
+ "Stopped (tty output)",
+ "Urgent I/O condition",
+ "CPU time limit exceeded",
+ "File size limit exceeded",
+ "Virtual timer expired",
+ "Profiling timer expired",
+ "Window changed",
+ "I/O possible",
+ "Power failure",
+ "Bad system call",
+ };
+
+ private static String getSignalString(int signum) {
+ return signum > 0 && signum < SIGNAL_STRINGS.length
+ ? SIGNAL_STRINGS[signum]
+ : "Signal " + signum;
+ }
+
+ /**
+ * Construct a TerminationStatus instance from a Process waitFor code.
+ *
+ * @param waitResult the value returned by {@link java.lang.Process#waitFor}.
+ */
+ public TerminationStatus(int waitResult) {
+ this.waitResult = waitResult;
+ }
+
+ /**
+ * Returns the "raw" result returned by Process.waitFor.
+ */
+ int getRawResult() {
+ return waitResult;
+ }
+
+ /**
+ * Returns true iff the process exited with code 0.
+ */
+ public boolean success() {
+ return exited() && getExitCode() == 0;
+ }
+
+ // We're relying on undocumented behaviour of Process.waitFor, specifically
+ // that waitResult is the exit status when the process returns normally, or
+ // 128+signalnumber when the process is terminated by a signal. We further
+ // assume that value signal numbers fall in the interval [1, 63].
+ private static final int SIGNAL_1 = 128 + 1;
+ private static final int SIGNAL_63 = 128 + 63;
+
+ /**
+ * Returns true iff the process exited normally.
+ */
+ public boolean exited() {
+ return waitResult < SIGNAL_1 || waitResult > SIGNAL_63;
+ }
+
+ /**
+ * Returns the exit code of the subprocess. Undefined if exited() is false.
+ */
+ public int getExitCode() {
+ if (!exited()) {
+ throw new IllegalStateException("getExitCode() not defined");
+ }
+ return waitResult;
+ }
+
+ /**
+ * Returns the number of the signal that terminated the process. Undefined
+ * if exited() returns true.
+ */
+ public int getTerminatingSignal() {
+ if (exited()) {
+ throw new IllegalStateException("getTerminatingSignal() not defined");
+ }
+ return waitResult - SIGNAL_1 + 1;
+ }
+
+ /**
+ * Returns a short string describing the termination status.
+ * e.g. "Exit 1" or "Hangup".
+ */
+ public String toShortString() {
+ return exited()
+ ? ("Exit " + getExitCode())
+ : (getSignalString(getTerminatingSignal()));
+ }
+
+ @Override
+ public String toString() {
+ return exited()
+ ? ("Process exited with status " + getExitCode())
+ : ("Process terminated by signal " + getTerminatingSignal());
+ }
+
+ @Override
+ public int hashCode() {
+ return waitResult;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ return other instanceof TerminationStatus &&
+ ((TerminationStatus) other).waitResult == this.waitResult;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/shell/TimeoutKillableObserver.java b/src/main/java/com/google/devtools/build/lib/shell/TimeoutKillableObserver.java
new file mode 100644
index 0000000..c2ed033
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/shell/TimeoutKillableObserver.java
@@ -0,0 +1,102 @@
+// Copyright 2014 Google Inc. 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.shell;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * <p>{@link KillableObserver} implementation which will kill its observed
+ * {@link Killable} if it is still being observed after a given amount
+ * of time has elapsed.</p>
+ *
+ * <p>Note that this class can only observe one {@link Killable} at a time;
+ * multiple instances should be used for concurrent calls to
+ * {@link Command#execute(byte[], KillableObserver, boolean)}.</p>
+ */
+public final class TimeoutKillableObserver implements KillableObserver {
+
+ private static final Logger log =
+ Logger.getLogger(TimeoutKillableObserver.class.getCanonicalName());
+
+ private final long timeoutMS;
+ private Killable killable;
+ private SleeperThread sleeperThread;
+ private boolean timedOut;
+
+ // TODO(bazel-team): I'd like to use ThreadPool2, but it doesn't currently
+ // provide a way to interrupt a thread
+
+ public TimeoutKillableObserver(final long timeoutMS) {
+ this.timeoutMS = timeoutMS;
+ }
+
+ /**
+ * Starts a new {@link Thread} to wait for the timeout period. This is
+ * interrupted by the {@link #stopObserving(Killable)} method.
+ *
+ * @param killable killable to kill when the timeout period expires
+ */
+ @Override
+ public synchronized void startObserving(final Killable killable) {
+ this.timedOut = false;
+ this.killable = killable;
+ this.sleeperThread = new SleeperThread();
+ this.sleeperThread.start();
+ }
+
+ @Override
+ public synchronized void stopObserving(final Killable killable) {
+ if (!this.killable.equals(killable)) {
+ throw new IllegalStateException("start/stopObservering called with " +
+ "different Killables");
+ }
+ if (sleeperThread.isAlive()) {
+ sleeperThread.interrupt();
+ }
+ this.killable = null;
+ sleeperThread = null;
+ }
+
+ private final class SleeperThread extends Thread {
+ @Override public void run() {
+ try {
+ if (log.isLoggable(Level.FINE)) {
+ log.fine("Waiting for " + timeoutMS + "ms to kill process");
+ }
+ Thread.sleep(timeoutMS);
+ // timeout expired; kill it
+ synchronized (TimeoutKillableObserver.this) {
+ if (killable != null) {
+ log.fine("Killing process");
+ killable.kill();
+ timedOut = true;
+ }
+ }
+ } catch (InterruptedException ie) {
+ // continue -- process finished before timeout
+ log.fine("Wait interrupted since process finished; continuing...");
+ }
+ }
+ }
+
+ /**
+ * Returns true if the observed process was killed by this observer.
+ */
+ public synchronized boolean hasTimedOut() {
+ // synchronized needed for memory model visibility.
+ return timedOut;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ASTFileLookupFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/ASTFileLookupFunction.java
new file mode 100644
index 0000000..6dcc224
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ASTFileLookupFunction.java
@@ -0,0 +1,177 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.devtools.build.lib.packages.CachingPackageLocator;
+import com.google.devtools.build.lib.packages.RuleClassProvider;
+import com.google.devtools.build.lib.pkgcache.PathPackageLocator;
+import com.google.devtools.build.lib.syntax.BuildFileAST;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.lib.vfs.RootedPath;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyFunctionException;
+import com.google.devtools.build.skyframe.SkyFunctionException.Transience;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicReference;
+
+import javax.annotation.Nullable;
+
+/**
+ * A SkyFunction for {@link ASTFileLookupValue}s. Tries to locate a file and load it as a
+ * syntax tree and cache the resulting {@link BuildFileAST}. If the file doesn't exist
+ * the function doesn't fail but returns a specific NO_FILE ASTLookupValue.
+ */
+public class ASTFileLookupFunction implements SkyFunction {
+
+ private abstract static class FileLookupResult {
+ /** Returns whether the file lookup was successful. */
+ public abstract boolean lookupSuccessful();
+
+ /** If {@code lookupSuccessful()}, returns the {@link RootedPath} to the file. */
+ public abstract RootedPath rootedPath();
+
+ static FileLookupResult noFile() {
+ return UnsuccessfulFileResult.INSTANCE;
+ }
+
+ static FileLookupResult file(RootedPath rootedPath) {
+ return new SuccessfulFileResult(rootedPath);
+ }
+
+ private static class SuccessfulFileResult extends FileLookupResult {
+ private final RootedPath rootedPath;
+
+ private SuccessfulFileResult(RootedPath rootedPath) {
+ this.rootedPath = rootedPath;
+ }
+
+ @Override
+ public boolean lookupSuccessful() {
+ return true;
+ }
+
+ @Override
+ public RootedPath rootedPath() {
+ return rootedPath;
+ }
+ }
+
+ private static class UnsuccessfulFileResult extends FileLookupResult {
+ private static final UnsuccessfulFileResult INSTANCE = new UnsuccessfulFileResult();
+ private UnsuccessfulFileResult() {
+ }
+
+ @Override
+ public boolean lookupSuccessful() {
+ return false;
+ }
+
+ @Override
+ public RootedPath rootedPath() {
+ throw new IllegalStateException("unsucessful lookup");
+ }
+ }
+ }
+
+ private final AtomicReference<PathPackageLocator> pkgLocator;
+ private final RuleClassProvider ruleClassProvider;
+ private final CachingPackageLocator packageManager;
+
+ public ASTFileLookupFunction(AtomicReference<PathPackageLocator> pkgLocator,
+ CachingPackageLocator packageManager,
+ RuleClassProvider ruleClassProvider) {
+ this.pkgLocator = pkgLocator;
+ this.packageManager = packageManager;
+ this.ruleClassProvider = ruleClassProvider;
+ }
+
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env) throws SkyFunctionException,
+ InterruptedException {
+ PathFragment astFilePathFragment = (PathFragment) skyKey.argument();
+ FileLookupResult lookupResult = getASTFile(env, astFilePathFragment);
+ if (lookupResult == null) {
+ return null;
+ }
+
+ BuildFileAST ast = null;
+ if (!lookupResult.lookupSuccessful()) {
+ // Return the specific NO_FILE ASTLookupValue instance if no file was found.
+ return ASTFileLookupValue.NO_FILE;
+ } else {
+ Path path = lookupResult.rootedPath().asPath();
+ // Skylark files end with bzl.
+ boolean parseAsSkylark = astFilePathFragment.getPathString().endsWith(".bzl");
+ try {
+ ast = parseAsSkylark
+ ? BuildFileAST.parseSkylarkFile(path, env.getListener(),
+ packageManager, ruleClassProvider.getSkylarkValidationEnvironment().clone())
+ : BuildFileAST.parseBuildFile(path, env.getListener(),
+ packageManager, false);
+ } catch (IOException e) {
+ throw new ASTLookupFunctionException(new ErrorReadingSkylarkExtensionException(
+ e.getMessage()), Transience.TRANSIENT);
+ }
+ }
+
+ return new ASTFileLookupValue(ast);
+ }
+
+ private FileLookupResult getASTFile(Environment env, PathFragment astFilePathFragment)
+ throws ASTLookupFunctionException {
+ for (Path packagePathEntry : pkgLocator.get().getPathEntries()) {
+ RootedPath rootedPath = RootedPath.toRootedPath(packagePathEntry, astFilePathFragment);
+ SkyKey fileSkyKey = FileValue.key(rootedPath);
+ FileValue fileValue = null;
+ try {
+ fileValue = (FileValue) env.getValueOrThrow(fileSkyKey, IOException.class,
+ FileSymlinkCycleException.class, InconsistentFilesystemException.class);
+ } catch (IOException | FileSymlinkCycleException e) {
+ throw new ASTLookupFunctionException(new ErrorReadingSkylarkExtensionException(
+ e.getMessage()), Transience.PERSISTENT);
+ } catch (InconsistentFilesystemException e) {
+ throw new ASTLookupFunctionException(e, Transience.PERSISTENT);
+ }
+ if (fileValue == null) {
+ return null;
+ }
+ if (fileValue.isFile()) {
+ return FileLookupResult.file(rootedPath);
+ }
+ }
+ return FileLookupResult.noFile();
+ }
+
+ @Nullable
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return null;
+ }
+
+ private static final class ASTLookupFunctionException extends SkyFunctionException {
+ private ASTLookupFunctionException(ErrorReadingSkylarkExtensionException e,
+ Transience transience) {
+ super(e, transience);
+ }
+
+ private ASTLookupFunctionException(InconsistentFilesystemException e, Transience transience) {
+ super(e, transience);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ASTFileLookupValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/ASTFileLookupValue.java
new file mode 100644
index 0000000..1061c86
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ASTFileLookupValue.java
@@ -0,0 +1,61 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.devtools.build.lib.syntax.BuildFileAST;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import javax.annotation.Nullable;
+
+/**
+ * A value that represents an AST file lookup result.
+ */
+public class ASTFileLookupValue implements SkyValue {
+
+ static final ASTFileLookupValue NO_FILE = new ASTFileLookupValue(null);
+
+ @Nullable private final BuildFileAST ast;
+
+ public ASTFileLookupValue(@Nullable BuildFileAST ast) {
+ this.ast = ast;
+ }
+
+ /**
+ * Returns the original AST file.
+ */
+ @Nullable public BuildFileAST getAST() {
+ return ast;
+ }
+
+ static void checkInputArgument(PathFragment astFilePathFragment) throws ASTLookupInputException {
+ if (astFilePathFragment.isAbsolute()) {
+ throw new ASTLookupInputException(String.format(
+ "Input file '%s' cannot be an absolute path.", astFilePathFragment));
+ }
+ }
+
+ static SkyKey key(PathFragment astFilePathFragment) throws ASTLookupInputException {
+ checkInputArgument(astFilePathFragment);
+ return new SkyKey(SkyFunctions.AST_FILE_LOOKUP, astFilePathFragment);
+ }
+
+ static final class ASTLookupInputException extends Exception {
+ private ASTLookupInputException(String msg) {
+ super(msg);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/AbstractLabelCycleReporter.java b/src/main/java/com/google/devtools/build/lib/skyframe/AbstractLabelCycleReporter.java
new file mode 100644
index 0000000..797f158
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/AbstractLabelCycleReporter.java
@@ -0,0 +1,130 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Function;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.packages.NoSuchThingException;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.pkgcache.LoadedPackageProvider;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.skyframe.CycleInfo;
+import com.google.devtools.build.skyframe.CyclesReporter;
+import com.google.devtools.build.skyframe.SkyKey;
+
+/** Reports cycles between skyframe values whose keys contains {@link Label}s. */
+abstract class AbstractLabelCycleReporter implements CyclesReporter.SingleCycleReporter {
+
+ private final LoadedPackageProvider loadedPackageProvider;
+
+ AbstractLabelCycleReporter(LoadedPackageProvider loadedPackageProvider) {
+ this.loadedPackageProvider = loadedPackageProvider;
+ }
+
+ /** Returns the String representation of the {@code SkyKey}. */
+ protected abstract String prettyPrint(SkyKey key);
+
+ /** Returns the associated Label of the SkyKey. */
+ protected abstract Label getLabel(SkyKey key);
+
+ protected abstract boolean canReportCycle(SkyKey topLevelKey, CycleInfo cycleInfo);
+
+ protected String getAdditionalMessageAboutCycle(SkyKey topLevelKey, CycleInfo cycleInfo) {
+ return "";
+ }
+
+ @Override
+ public boolean maybeReportCycle(SkyKey topLevelKey, CycleInfo cycleInfo,
+ boolean alreadyReported, EventHandler eventHandler) {
+ Preconditions.checkNotNull(eventHandler);
+ if (!canReportCycle(topLevelKey, cycleInfo)) {
+ return false;
+ }
+
+ if (alreadyReported) {
+ Label label = getLabel(topLevelKey);
+ Target target = getTargetForLabel(label);
+ eventHandler.handle(Event.error(target.getLocation(),
+ "in " + target.getTargetKind() + " " + label +
+ ": cycle in dependency graph: target depends on an already-reported cycle"));
+ } else {
+ StringBuilder cycleMessage = new StringBuilder("cycle in dependency graph:");
+ ImmutableList<SkyKey> pathToCycle = cycleInfo.getPathToCycle();
+ ImmutableList<SkyKey> cycle = cycleInfo.getCycle();
+ for (SkyKey value : pathToCycle) {
+ cycleMessage.append("\n ");
+ cycleMessage.append(prettyPrint(value));
+ }
+
+ SkyKey cycleValue = printCycle(cycle, cycleMessage, new Function<SkyKey, String>() {
+ @Override
+ public String apply(SkyKey input) {
+ return prettyPrint(input);
+ }
+ });
+
+ cycleMessage.append(getAdditionalMessageAboutCycle(topLevelKey, cycleInfo));
+
+ Label label = getLabel(cycleValue);
+ Target target = getTargetForLabel(label);
+ eventHandler.handle(
+ Event.error(target.getLocation(), "in " + target.getTargetKind() + " " + label
+ + ": " + cycleMessage.toString()));
+ }
+
+ return true;
+ }
+
+ /**
+ * Prints the SkyKey-s in cycle into cycleMessage using the print function.
+ */
+ static SkyKey printCycle(ImmutableList<SkyKey> cycle, StringBuilder cycleMessage,
+ Function<SkyKey, String> printFunction) {
+ Iterable<SkyKey> valuesToPrint = cycle.size() > 1
+ ? Iterables.concat(cycle, ImmutableList.of(cycle.get(0))) : cycle;
+ SkyKey cycleValue = null;
+ for (SkyKey value : valuesToPrint) {
+ if (cycleValue == null) {
+ cycleValue = value;
+ }
+ if (value == cycleValue) {
+ cycleMessage.append("\n * ");
+ } else {
+ cycleMessage.append("\n ");
+ }
+ cycleMessage.append(printFunction.apply(value));
+ }
+
+ if (cycle.size() == 1) {
+ cycleMessage.append(" [self-edge]");
+ }
+
+ return cycleValue;
+ }
+
+ protected final Target getTargetForLabel(Label label) {
+ try {
+ return loadedPackageProvider.getLoadedTarget(label);
+ } catch (NoSuchThingException e) {
+ // This method is used for getting the target from a label in a circular dependency.
+ // If we have a cycle that means that we need to have accessed the target (to get its
+ // dependencies). So all the labels in a dependency cycle need to exist.
+ throw new IllegalStateException(e);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ActionArtifactCycleReporter.java b/src/main/java/com/google/devtools/build/lib/skyframe/ActionArtifactCycleReporter.java
new file mode 100644
index 0000000..3105539
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ActionArtifactCycleReporter.java
@@ -0,0 +1,77 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.analysis.LabelAndConfiguration;
+import com.google.devtools.build.lib.pkgcache.LoadedPackageProvider;
+import com.google.devtools.build.lib.skyframe.ArtifactValue.OwnedArtifact;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.skyframe.CycleInfo;
+import com.google.devtools.build.skyframe.SkyFunctionName;
+import com.google.devtools.build.skyframe.SkyKey;
+
+/**
+ * Reports cycles between Actions and Artifacts. These indicates cycles within a rule.
+ */
+public class ActionArtifactCycleReporter extends AbstractLabelCycleReporter {
+
+ private static final Predicate<SkyKey> IS_ARTIFACT_OR_ACTION_SKY_KEY = Predicates.or(
+ SkyFunctions.isSkyFunction(SkyFunctions.ARTIFACT),
+ SkyFunctions.isSkyFunction(SkyFunctions.ACTION_EXECUTION),
+ SkyFunctions.isSkyFunction(SkyFunctions.TARGET_COMPLETION));
+
+ ActionArtifactCycleReporter(LoadedPackageProvider loadedPackageProvider) {
+ super(loadedPackageProvider);
+ }
+
+ @Override
+ protected String prettyPrint(SkyKey key) {
+ return prettyPrint(key.functionName(), key.argument());
+ }
+
+ private String prettyPrint(SkyFunctionName skyFunctionName, Object arg) {
+ if (arg instanceof OwnedArtifact) {
+ return "file: " + ((OwnedArtifact) arg).getArtifact().getRootRelativePathString();
+ } else if (arg instanceof Action) {
+ return "action: " + ((Action) arg).getMnemonic();
+ } else if (arg instanceof LabelAndConfiguration
+ && skyFunctionName == SkyFunctions.TARGET_COMPLETION) {
+ return "configured target: " + ((LabelAndConfiguration) arg).getLabel().toString();
+ }
+ throw new IllegalStateException(
+ "Argument is not Action, TargetCompletion, or OwnedArtifact: " + arg);
+ }
+
+ @Override
+ protected Label getLabel(SkyKey key) {
+ Object arg = key.argument();
+ if (arg instanceof OwnedArtifact) {
+ return ((OwnedArtifact) arg).getArtifact().getOwner();
+ } else if (arg instanceof Action) {
+ return ((Action) arg).getOwner().getLabel();
+ }
+ throw new IllegalStateException("Argument is not Action or OwnedArtifact: " + arg);
+ }
+
+ @Override
+ protected boolean canReportCycle(SkyKey topLevelKey, CycleInfo cycleInfo) {
+ return IS_ARTIFACT_OR_ACTION_SKY_KEY.apply(topLevelKey)
+ && Iterables.all(cycleInfo.getCycle(), IS_ARTIFACT_OR_ACTION_SKY_KEY);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ActionExecutionFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/ActionExecutionFunction.java
new file mode 100644
index 0000000..1420860
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ActionExecutionFunction.java
@@ -0,0 +1,338 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.actions.ActionCacheChecker.Token;
+import com.google.devtools.build.lib.actions.ActionExecutionContext;
+import com.google.devtools.build.lib.actions.ActionExecutionException;
+import com.google.devtools.build.lib.actions.AlreadyReportedActionExecutionException;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.MissingInputFileException;
+import com.google.devtools.build.lib.actions.NotifyOnActionCacheHit;
+import com.google.devtools.build.lib.actions.cache.MetadataHandler;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.util.Pair;
+import com.google.devtools.build.lib.util.io.TimestampGranularityMonitor;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyFunctionException;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+import com.google.devtools.build.skyframe.ValueOrException2;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A builder for {@link ActionExecutionValue}s.
+ */
+public class ActionExecutionFunction implements SkyFunction {
+
+ private static final Predicate<Artifact> IS_SOURCE_ARTIFACT = new Predicate<Artifact>() {
+ @Override
+ public boolean apply(Artifact input) {
+ return input.isSourceArtifact();
+ }
+ };
+
+ private final SkyframeActionExecutor skyframeActionExecutor;
+ private final TimestampGranularityMonitor tsgm;
+
+ public ActionExecutionFunction(SkyframeActionExecutor skyframeActionExecutor,
+ TimestampGranularityMonitor tsgm) {
+ this.skyframeActionExecutor = skyframeActionExecutor;
+ this.tsgm = tsgm;
+ }
+
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env) throws ActionExecutionFunctionException,
+ InterruptedException {
+ Action action = (Action) skyKey.argument();
+ Map<Artifact, FileArtifactValue> inputArtifactData = null;
+ Map<Artifact, Collection<Artifact>> expandedMiddlemen = null;
+ boolean alreadyRan = skyframeActionExecutor.probeActionExecution(action);
+ try {
+ Pair<Map<Artifact, FileArtifactValue>, Map<Artifact, Collection<Artifact>>> checkedInputs =
+ checkInputs(env, action, alreadyRan); // Declare deps on known inputs to action.
+
+ if (checkedInputs != null) {
+ inputArtifactData = checkedInputs.first;
+ expandedMiddlemen = checkedInputs.second;
+ }
+ } catch (ActionExecutionException e) {
+ throw new ActionExecutionFunctionException(e);
+ }
+ // TODO(bazel-team): Non-volatile NotifyOnActionCacheHit actions perform worse in Skyframe than
+ // legacy when they are not at the top of the action graph. In legacy, they are stored
+ // separately, so notifying non-dirty actions is cheap. In Skyframe, they depend on the
+ // BUILD_ID, forcing invalidation of upward transitive closure on each build.
+ if (action.isVolatile() || action instanceof NotifyOnActionCacheHit) {
+ // Volatile build actions may need to execute even if none of their known inputs have changed.
+ // Depending on the buildID ensure that these actions have a chance to execute.
+ PrecomputedValue.BUILD_ID.get(env);
+ }
+ if (env.valuesMissing()) {
+ return null;
+ }
+
+ ActionExecutionValue result;
+ try {
+ result = checkCacheAndExecuteIfNeeded(action, inputArtifactData, expandedMiddlemen, env);
+ } catch (ActionExecutionException e) {
+ // In this case we do not report the error to the action reporter because we have already
+ // done it in SkyframeExecutor.reportErrorIfNotAbortingMode() method. That method
+ // prints the error in the top-level reporter and also dumps the recorded StdErr for the
+ // action. Label can be null in the case of, e.g., the SystemActionOwner (for build-info.txt).
+ throw new ActionExecutionFunctionException(new AlreadyReportedActionExecutionException(e));
+ } finally {
+ declareAdditionalDependencies(env, action);
+ }
+ if (env.valuesMissing()) {
+ return null;
+ }
+
+ return result;
+ }
+
+ private ActionExecutionValue checkCacheAndExecuteIfNeeded(
+ Action action,
+ Map<Artifact, FileArtifactValue> inputArtifactData,
+ Map<Artifact, Collection<Artifact>> expandedMiddlemen,
+ Environment env) throws ActionExecutionException, InterruptedException {
+ // Don't initialize the cache if the result has already been computed and this is just a
+ // rerun.
+ FileAndMetadataCache fileAndMetadataCache = null;
+ MetadataHandler metadataHandler = null;
+ Token token = null;
+ long actionStartTime = System.nanoTime();
+ // inputArtifactData is null exactly when we know that the execution result was already
+ // computed on a prior run of this SkyFunction. If it is null we don't need to initialize
+ // anything -- we will get the result directly from SkyframeActionExecutor's cache.
+ if (inputArtifactData != null) {
+ // Check action cache to see if we need to execute anything. Checking the action cache only
+ // needs to happen on the first run, since a cache hit means we'll return immediately, and
+ // there'll be no second run.
+ fileAndMetadataCache = new FileAndMetadataCache(
+ inputArtifactData,
+ expandedMiddlemen,
+ skyframeActionExecutor.getExecRoot(),
+ action.getOutputs(),
+ // Only give the metadata cache the ability to look up Skyframe values if the action
+ // might have undeclared inputs. If those undeclared inputs are generated, they are
+ // present in Skyframe, so we can save a stat by looking them up directly.
+ action.discoversInputs() ? env : null,
+ tsgm);
+ metadataHandler =
+ skyframeActionExecutor.constructMetadataHandler(fileAndMetadataCache);
+ token = skyframeActionExecutor.checkActionCache(action, metadataHandler, actionStartTime);
+ }
+ if (token == null && inputArtifactData != null) {
+ // We got a hit from the action cache -- no need to execute.
+ return new ActionExecutionValue(
+ fileAndMetadataCache.getOutputData(),
+ fileAndMetadataCache.getAdditionalOutputData());
+ } else {
+ ActionExecutionContext actionExecutionContext = null;
+ if (inputArtifactData != null) {
+ actionExecutionContext = skyframeActionExecutor.constructActionExecutionContext(
+ fileAndMetadataCache,
+ metadataHandler);
+ if (action.discoversInputs()) {
+ skyframeActionExecutor.discoverInputs(action, actionExecutionContext);
+ }
+ }
+ // If this is the second time we are here (because the action discovers inputs, and we had
+ // to restart the value builder after declaring our dependence on newly discovered inputs),
+ // the result returned here is the already-computed result from the first run.
+ // Similarly, if this is a shared action and the other action is the one that executed, we
+ // must use that other action's value, provided here, since it is populated with metadata
+ // for the outputs.
+ // If this action was not shared and this is the first run of the action, this returned
+ // result was computed during the call.
+ return skyframeActionExecutor.executeAction(action, fileAndMetadataCache, token,
+ actionStartTime, actionExecutionContext);
+ }
+ }
+
+ private static Iterable<SkyKey> toKeys(Iterable<Artifact> inputs,
+ Iterable<Artifact> mandatoryInputs) {
+ if (mandatoryInputs == null) {
+ // This is a non inputs-discovering action, so no need to distinguish mandatory from regular
+ // inputs.
+ return Iterables.transform(inputs, new Function<Artifact, SkyKey>() {
+ @Override
+ public SkyKey apply(Artifact artifact) {
+ return ArtifactValue.key(artifact, true);
+ }
+ });
+ } else {
+ Collection<SkyKey> discoveredArtifacts = new HashSet<>();
+ Set<Artifact> mandatory = Sets.newHashSet(mandatoryInputs);
+ for (Artifact artifact : inputs) {
+ discoveredArtifacts.add(ArtifactValue.key(artifact, mandatory.contains(artifact)));
+ }
+
+ // In case the action violates the invariant that getInputs() is a superset of
+ // getMandatoryInputs(), explicitly add the mandatory inputs. See bug about an
+ // "action not in canonical form" error message. Also note that we may add Skyframe edges on
+ // these potentially stale deps due to the way loading inputs from the action cache functions.
+ // In practice, this is safe since C++ actions (the only ones which discover inputs) only add
+ // possibly stale inputs on source artifacts, which we treat as non-mandatory.
+ for (Artifact artifact : mandatory) {
+ discoveredArtifacts.add(ArtifactValue.key(artifact, true));
+ }
+ return discoveredArtifacts;
+ }
+ }
+
+ /**
+ * Declare dependency on all known inputs of action. Throws exception if any are known to be
+ * missing. Some inputs may not yet be in the graph, in which case the builder should abort.
+ */
+ private Pair<Map<Artifact, FileArtifactValue>, Map<Artifact, Collection<Artifact>>> checkInputs(
+ Environment env, Action action, boolean alreadyRan) throws ActionExecutionException {
+ Map<SkyKey, ValueOrException2<MissingInputFileException, ActionExecutionException>> inputDeps =
+ env.getValuesOrThrow(toKeys(action.getInputs(), action.discoversInputs()
+ ? action.getMandatoryInputs() : null), MissingInputFileException.class,
+ ActionExecutionException.class);
+
+ // If the action was already run, then break out early. This avoids the cost of constructing the
+ // input map and expanded middlemen if they're not going to be used.
+ if (alreadyRan) {
+ return null;
+ }
+
+ int missingCount = 0;
+ int actionFailures = 0;
+ boolean catastrophe = false;
+ // Only populate input data if we have the input values, otherwise they'll just go unused.
+ // We still want to loop through the inputs to collect missing deps errors. During the
+ // evaluator "error bubbling", we may get one last chance at reporting errors even though
+ // some deps are stilling missing.
+ boolean populateInputData = !env.valuesMissing();
+ NestedSetBuilder<Label> rootCauses = NestedSetBuilder.stableOrder();
+ Map<Artifact, FileArtifactValue> inputArtifactData =
+ new HashMap<>(populateInputData ? inputDeps.size() : 0);
+ Map<Artifact, Collection<Artifact>> expandedMiddlemen =
+ new HashMap<>(populateInputData ? 128 : 0);
+
+ ActionExecutionException firstActionExecutionException = null;
+ for (Map.Entry<SkyKey, ValueOrException2<MissingInputFileException,
+ ActionExecutionException>> depsEntry : inputDeps.entrySet()) {
+ Artifact input = ArtifactValue.artifact(depsEntry.getKey());
+ try {
+ ArtifactValue value = (ArtifactValue) depsEntry.getValue().get();
+ if (populateInputData && value instanceof AggregatingArtifactValue) {
+ AggregatingArtifactValue aggregatingValue = (AggregatingArtifactValue) value;
+ for (Pair<Artifact, FileArtifactValue> entry : aggregatingValue.getInputs()) {
+ inputArtifactData.put(entry.first, entry.second);
+ }
+ // We have to cache the "digest" of the aggregating value itself, because the action cache
+ // checker may want it.
+ inputArtifactData.put(input, aggregatingValue.getSelfData());
+ expandedMiddlemen.put(input,
+ Collections2.transform(aggregatingValue.getInputs(),
+ Pair.<Artifact, FileArtifactValue>firstFunction()));
+ } else if (populateInputData && value instanceof FileArtifactValue) {
+ // TODO(bazel-team): Make sure middleman "virtual" artifact data is properly processed.
+ inputArtifactData.put(input, (FileArtifactValue) value);
+ }
+ } catch (MissingInputFileException e) {
+ missingCount++;
+ if (input.getOwner() != null) {
+ rootCauses.add(input.getOwner());
+ }
+ } catch (ActionExecutionException e) {
+ actionFailures++;
+ if (firstActionExecutionException == null) {
+ firstActionExecutionException = e;
+ }
+ catastrophe = catastrophe || e.isCatastrophe();
+ rootCauses.addTransitive(e.getRootCauses());
+ }
+ }
+ // We need to rethrow first exception because it can contain useful error message
+ if (firstActionExecutionException != null) {
+ if (missingCount == 0 && actionFailures == 1) {
+ // In the case a single action failed, just propagate the exception upward. This avoids
+ // having to copy the root causes to the upwards transitive closure.
+ throw firstActionExecutionException;
+ }
+ throw new ActionExecutionException(firstActionExecutionException.getMessage(),
+ firstActionExecutionException.getCause(), action, rootCauses.build(), catastrophe);
+ }
+
+ if (missingCount > 0) {
+ for (Label missingInput : rootCauses.build()) {
+ env.getListener().handle(Event.error(action.getOwner().getLocation(), String.format(
+ "%s: missing input file '%s'", action.getOwner().getLabel(), missingInput)));
+ }
+ throw new ActionExecutionException(missingCount + " input file(s) do not exist", action,
+ rootCauses.build(), /*catastrophe=*/false);
+ }
+ return Pair.of(
+ Collections.unmodifiableMap(inputArtifactData),
+ Collections.unmodifiableMap(expandedMiddlemen));
+ }
+
+ private static void declareAdditionalDependencies(Environment env, Action action) {
+ if (action.discoversInputs()) {
+ // TODO(bazel-team): Should this be all inputs, or just source files?
+ env.getValues(toKeys(Iterables.filter(action.getInputs(), IS_SOURCE_ARTIFACT),
+ action.getMandatoryInputs()));
+ }
+ }
+
+ /**
+ * All info/warning messages associated with actions should be always displayed.
+ */
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return null;
+ }
+
+ /**
+ * Used to declare all the exception types that can be wrapped in the exception thrown by
+ * {@link ActionExecutionFunction#compute}.
+ */
+ private static final class ActionExecutionFunctionException extends SkyFunctionException {
+
+ private final ActionExecutionException actionException;
+
+ public ActionExecutionFunctionException(ActionExecutionException e) {
+ // We conservatively assume that the error is transient. We don't have enough information to
+ // distinguish non-transient errors (e.g. compilation error from a deterministic compiler)
+ // from transient ones (e.g. IO error).
+ // TODO(bazel-team): Have ActionExecutionExceptions declare their transience.
+ super(e, Transience.TRANSIENT);
+ this.actionException = e;
+ }
+
+ @Override
+ public boolean isCatastrophic() {
+ return actionException.isCatastrophe();
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ActionExecutionInactivityWatchdog.java b/src/main/java/com/google/devtools/build/lib/skyframe/ActionExecutionInactivityWatchdog.java
new file mode 100644
index 0000000..87e3e0d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ActionExecutionInactivityWatchdog.java
@@ -0,0 +1,180 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.actions.ActionExecutionStatusReporter;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * An object that can monitor whether actions are getting completed in a timely manner.
+ *
+ * <p>If there's nothing happening for a while, a background thread will print (and update) the
+ * "Still waiting for N actions to complete..." message.
+ */
+public final class ActionExecutionInactivityWatchdog {
+
+ /** An object used in monitoring action execution inactivity. */
+ public interface InactivityMonitor {
+
+ /** Returns whether action execution has started. */
+ boolean hasStarted();
+
+ /** Returns the number of enqueued but not yet completed actions. */
+ int getPending();
+
+ /**
+ * Waits for any action to complete, or the timeout to elapse.
+ *
+ * <p>The thread must wait at least for the specified timeout, unless some action completes in
+ * the meantime. It's not allowed to return 0 too early.
+ *
+ * <p>Note that it's acceptable to return (any value) later than specified by the timeout.
+ *
+ * @return the number of actions completed during the wait
+ */
+ int waitForNextCompletion(int timeoutMilliseconds) throws InterruptedException;
+ }
+
+ /** An object that the watchdog can report inactivity to. */
+ public interface InactivityReporter {
+
+ /**
+ * Report that actions are not getting completed in a timely manner.
+ *
+ * <p>Inactivity is typically not reported if tests with streaming output are being run.
+ */
+ void maybeReportInactivity();
+ }
+
+ @VisibleForTesting
+ interface Sleep {
+ void sleep(int durationMilliseconds) throws InterruptedException;
+ }
+
+ private static final class WaitTime {
+ private final int progressIntervalFlagValue;
+ private int prev;
+
+ public WaitTime(int progressIntervalFlagValue) {
+ this.progressIntervalFlagValue = progressIntervalFlagValue;
+ }
+
+ public void reset() {
+ prev = 0;
+ }
+
+ public int next() {
+ prev = ActionExecutionStatusReporter.getWaitTime(progressIntervalFlagValue, prev);
+ return prev;
+ }
+ }
+
+ private final AtomicBoolean isRunning = new AtomicBoolean(false);
+ private final InactivityMonitor monitor;
+ private final InactivityReporter reporter;
+ private final Sleep sleeper;
+ private final Thread thread;
+ private final WaitTime waitTime;
+
+ public ActionExecutionInactivityWatchdog(InactivityMonitor monitor, InactivityReporter reporter,
+ int progressIntervalFlagValue) {
+ this(monitor, reporter, progressIntervalFlagValue, new Sleep() {
+ @Override
+ public void sleep(int durationMilliseconds) throws InterruptedException {
+ Thread.sleep(durationMilliseconds);
+ }
+ });
+ }
+
+ @VisibleForTesting
+ public ActionExecutionInactivityWatchdog(InactivityMonitor monitor, InactivityReporter reporter,
+ int progressIntervalFlagValue, Sleep sleeper) {
+ this.monitor = Preconditions.checkNotNull(monitor);
+ this.reporter = Preconditions.checkNotNull(reporter);
+ this.sleeper = Preconditions.checkNotNull(sleeper);
+ this.waitTime = new WaitTime(progressIntervalFlagValue);
+ this.thread = new Thread(new Runnable() {
+ @Override
+ public void run() {
+ enterWatchdogLoop();
+ }
+ });
+ this.thread.setDaemon(true);
+ this.thread.setName("action-execution-watchdog");
+ }
+
+ /** Starts the watchdog thread. This method should only be called once. */
+ public void start() {
+ Preconditions.checkState(!isRunning.getAndSet(true));
+ thread.start();
+ }
+
+ /**
+ * Stops the watchdog thread. This method should only be called once.
+ *
+ * <p>The method waits for the thread to terminate. If the caller thread is interrupted
+ * in the meantime, the interrupted status will be set.
+ */
+ public void stop() {
+ Preconditions.checkState(isRunning.getAndSet(false));
+ thread.interrupt();
+ try {
+ thread.join();
+ } catch (InterruptedException e) {
+ // When Thread.join throws, the interrupted status is cleared. We need to set it again.
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ private void enterWatchdogLoop() {
+ while (isRunning.get()) {
+ try {
+ // Wait a while for any SkyFunction to finish. The returned number indicates how many
+ // actions completed during the wait. It's possible that this is more than 1, since
+ // this thread may not immediately regain control.
+ int completedActions = monitor.waitForNextCompletion(waitTime.next() * 1000);
+ if (!isRunning.get()) {
+ break;
+ }
+
+ int pending = monitor.getPending();
+ if (!monitor.hasStarted() || completedActions > 0 || pending == 0) {
+ // If no keys have been enqueued yet (execution hasn't started), or some actions
+ // were completed since this thread was notified (we are making visible progress),
+ // or there are currently no enqueued actions waiting to be processed (perhaps all
+ // have completed and we are about to stop monitoring), then there's no need to
+ // display any messages.
+ waitTime.reset();
+
+ // Sleep a while before checking again. Actions might be executing at a nice rate, no
+ // need to worry about inactivity. This extra sleep isn't required but it's nice to
+ // have: without it we would, at times of high action completion rate, unnecessarily
+ // put the monitor into a fast sleep-wake cycle --- not a big problem but wasteful.
+ sleeper.sleep(1000);
+ } else {
+ // If actions are executing but we haven't made any progress in a while (no new
+ // action completion), then reassure the user that we're still running. Next time
+ // wait a little longer.
+ reporter.maybeReportInactivity();
+ }
+ } catch (InterruptedException ie) {
+ Thread.currentThread().interrupt();
+ return;
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ActionExecutionValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/ActionExecutionValue.java
new file mode 100644
index 0000000..de63c3b
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ActionExecutionValue.java
@@ -0,0 +1,117 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Objects;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.actions.Action.MiddlemanType;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.util.Map;
+
+import javax.annotation.Nullable;
+
+/**
+ * A value representing an executed action.
+ */
+@Immutable
+@ThreadSafe
+public class ActionExecutionValue implements SkyValue {
+ private final ImmutableMap<Artifact, FileValue> artifactData;
+ private final ImmutableMap<Artifact, FileArtifactValue> additionalOutputData;
+
+ /**
+ * @param artifactData Map from Artifacts to corresponding FileValues.
+ * @param additionalOutputData Map from Artifacts to values if the FileArtifactValue for this
+ * artifact cannot be derived from the corresponding FileValue (see {@link
+ * FileAndMetadataCache#getAdditionalOutputData} for when this is necessary).
+ */
+ ActionExecutionValue(Map<Artifact, FileValue> artifactData,
+ Map<Artifact, FileArtifactValue> additionalOutputData) {
+ this.artifactData = ImmutableMap.copyOf(artifactData);
+ this.additionalOutputData = ImmutableMap.copyOf(additionalOutputData);
+ }
+
+ /**
+ * Returns metadata for a given artifact, if that metadata cannot be inferred from the
+ * corresponding {@link #getData} call for that Artifact. See {@link
+ * FileAndMetadataCache#getAdditionalOutputData} for when that can happen.
+ */
+ @Nullable
+ FileArtifactValue getArtifactValue(Artifact artifact) {
+ return additionalOutputData.get(artifact);
+ }
+
+ /**
+ * @return The data for each non-middleman output of this action, in the form of the {@link
+ * FileValue} that would be created for the file if it were to be read from disk.
+ */
+ FileValue getData(Artifact artifact) {
+ Preconditions.checkState(!additionalOutputData.containsKey(artifact),
+ "Should not be requesting data for already-constructed FileArtifactValue: %s", artifact);
+ return artifactData.get(artifact);
+ }
+
+ /**
+ * @return The map from {@link Artifact} to the corresponding {@link FileValue} that would be
+ * returned by {@link #getData}. Should only be needed by {@link FilesystemValueChecker}.
+ */
+ ImmutableMap<Artifact, FileValue> getAllOutputArtifactData() {
+ return artifactData;
+ }
+
+ @ThreadSafe
+ @VisibleForTesting
+ public static SkyKey key(Action action) {
+ return new SkyKey(SkyFunctions.ACTION_EXECUTION, action);
+ }
+
+ /**
+ * Returns whether the key corresponds to a ActionExecutionValue worth reporting status about.
+ *
+ * <p>If an action can do real work, it's probably worth counting and reporting status about.
+ * Actions that don't really do any work (typically middleman actions) should not be counted
+ * towards enqueued and completed actions.
+ */
+ public static boolean isReportWorthyAction(SkyKey key) {
+ return key.functionName() == SkyFunctions.ACTION_EXECUTION
+ && isReportWorthyAction((Action) key.argument());
+ }
+
+ /**
+ * Returns whether the action is worth reporting status about.
+ *
+ * <p>If an action can do real work, it's probably worth counting and reporting status about.
+ * Actions that don't really do any work (typically middleman actions) should not be counted
+ * towards enqueued and completed actions.
+ */
+ public static boolean isReportWorthyAction(Action action) {
+ return action.getActionType() == MiddlemanType.NORMAL;
+ }
+
+ @Override
+ public String toString() {
+ return Objects.toStringHelper(this)
+ .add("artifactData", artifactData)
+ .add("additionalOutputData", additionalOutputData)
+ .toString();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ActionLookupValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/ActionLookupValue.java
new file mode 100644
index 0000000..1dfa722
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ActionLookupValue.java
@@ -0,0 +1,106 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.ArtifactOwner;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.skyframe.SkyFunctionName;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Base class for all values which can provide the generating action of an artifact. The primary
+ * instance of such lookup values is {@link ConfiguredTargetValue}. Values that hold the generating
+ * actions of target completion values and build info artifacts also fall into this category.
+ */
+public class ActionLookupValue implements SkyValue {
+ protected final ImmutableMap<Artifact, Action> generatingActionMap;
+
+ ActionLookupValue(Iterable<Action> actions) {
+ // Duplicate/shared actions get passed in all the time. Blaze is weird. We can't double-register
+ // the generated artifacts in an immutable map builder, so we double-register them in a more
+ // forgiving map, and then use that map to create the immutable one.
+ Map<Artifact, Action> generatingActions = new HashMap<>();
+ for (Action action : actions) {
+ for (Artifact artifact : action.getOutputs()) {
+ generatingActions.put(artifact, action);
+ }
+ }
+ generatingActionMap = ImmutableMap.copyOf(generatingActions);
+ }
+
+ ActionLookupValue(Action action) {
+ this(ImmutableList.of(action));
+ }
+
+ Action getGeneratingAction(Artifact artifact) {
+ return generatingActionMap.get(artifact);
+ }
+
+ /** To be used only when checking consistency of the action graph -- not by other values. */
+ ImmutableMap<Artifact, Action> getMapForConsistencyCheck() {
+ return generatingActionMap;
+ }
+
+ /**
+ * To be used only when setting the owners of deserialized artifacts whose owners were unknown at
+ * creation time -- not by other callers or values.
+ */
+ Iterable<Action> getActionsForFindingArtifactOwners() {
+ return generatingActionMap.values();
+ }
+
+ @VisibleForTesting
+ public static SkyKey key(ActionLookupKey ownerKey) {
+ return ownerKey.getSkyKey();
+ }
+
+ /**
+ * ArtifactOwner is not a SkyKey, but we wish to convert any ArtifactOwner into a SkyKey as
+ * simply as possible. To that end, all subclasses of ActionLookupValue "own" artifacts with
+ * ArtifactOwners that are subclasses of ActionLookupKey. This allows callers to easily find the
+ * value key, while remaining agnostic to what ActionLookupValues actually exist.
+ *
+ * <p>The methods of this class should only be called by {@link ActionLookupValue#key}.
+ */
+ protected abstract static class ActionLookupKey implements ArtifactOwner {
+ @Override
+ public Label getLabel() {
+ return null;
+ }
+
+ /**
+ * Subclasses must override this to specify their specific value type, unless they override
+ * {@link #getSkyKey}, in which case they are free not to implement this method.
+ */
+ abstract SkyFunctionName getType();
+
+ /**
+ * Prefer {@link ActionLookupValue#key} to calling this method directly.
+ *
+ * <p>Subclasses may override if the value key contents should not be the key itself.
+ */
+ SkyKey getSkyKey() {
+ return new SkyKey(getType(), this);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/AggregatingArtifactValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/AggregatingArtifactValue.java
new file mode 100644
index 0000000..8374efe
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/AggregatingArtifactValue.java
@@ -0,0 +1,42 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.util.Pair;
+
+import java.util.Collection;
+
+/** Value for aggregating artifacts, which must be expanded to a set of other artifacts. */
+class AggregatingArtifactValue extends ArtifactValue {
+ private final FileArtifactValue selfData;
+ private final ImmutableList<Pair<Artifact, FileArtifactValue>> inputs;
+
+ AggregatingArtifactValue(ImmutableList<Pair<Artifact, FileArtifactValue>> inputs,
+ FileArtifactValue selfData) {
+ this.inputs = inputs;
+ this.selfData = selfData;
+ }
+
+ /** Returns the artifacts that this artifact expands to, together with their data. */
+ Collection<Pair<Artifact, FileArtifactValue>> getInputs() {
+ return inputs;
+ }
+
+ /** Returns the data of the artifact for this value, as computed by the action cache checker. */
+ FileArtifactValue getSelfData() {
+ return selfData;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ArtifactFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/ArtifactFunction.java
new file mode 100644
index 0000000..e277476
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ArtifactFunction.java
@@ -0,0 +1,230 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.actions.Action.MiddlemanType;
+import com.google.devtools.build.lib.actions.ActionExecutionException;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.ArtifactOwner;
+import com.google.devtools.build.lib.actions.MissingInputFileException;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.skyframe.ActionLookupValue.ActionLookupKey;
+import com.google.devtools.build.lib.skyframe.ArtifactValue.OwnedArtifact;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.util.Pair;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.lib.vfs.RootedPath;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyFunctionException;
+import com.google.devtools.build.skyframe.SkyFunctionException.Transience;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.io.IOException;
+import java.util.Map;
+
+/**
+ * A builder for {@link ArtifactValue}s.
+ */
+class ArtifactFunction implements SkyFunction {
+
+ private final Predicate<PathFragment> allowedMissingInputs;
+
+ ArtifactFunction(Predicate<PathFragment> allowedMissingInputs) {
+ this.allowedMissingInputs = allowedMissingInputs;
+ }
+
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env) throws ArtifactFunctionException {
+ OwnedArtifact ownedArtifact = (OwnedArtifact) skyKey.argument();
+ Artifact artifact = ownedArtifact.getArtifact();
+ if (artifact.isSourceArtifact()) {
+ try {
+ return createSourceValue(artifact, ownedArtifact.isMandatory(), env);
+ } catch (MissingInputFileException e) {
+ // The error is not necessarily truly transient, but we mark it as such because we have
+ // the above side effect of posting an event to the EventBus. Importantly, that event
+ // is potentially used to report root causes.
+ throw new ArtifactFunctionException(e, Transience.TRANSIENT);
+ }
+ }
+
+ Action action = extractActionFromArtifact(artifact, env);
+ if (action == null) {
+ return null;
+ }
+
+ ActionExecutionValue actionValue =
+ (ActionExecutionValue) env.getValue(ActionExecutionValue.key(action));
+ if (actionValue == null) {
+ return null;
+ }
+
+ if (!isAggregatingValue(action)) {
+ try {
+ return createSimpleValue(artifact, actionValue);
+ } catch (IOException e) {
+ ActionExecutionException ex = new ActionExecutionException(e, action,
+ /*catastrophe=*/false);
+ env.getListener().handle(Event.error(ex.getLocation(), ex.getMessage()));
+ // This is a transient error since we did the work that led to the IOException.
+ throw new ArtifactFunctionException(ex, Transience.TRANSIENT);
+ }
+ } else {
+ return createAggregatingValue(artifact, action, actionValue.getArtifactValue(artifact), env);
+ }
+ }
+
+ private ArtifactValue createSourceValue(Artifact artifact, boolean mandatory, Environment env)
+ throws MissingInputFileException {
+ SkyKey fileSkyKey = FileValue.key(RootedPath.toRootedPath(artifact.getRoot().getPath(),
+ artifact.getPath()));
+ FileValue fileValue;
+ try {
+ fileValue = (FileValue) env.getValueOrThrow(fileSkyKey, IOException.class,
+ InconsistentFilesystemException.class, FileSymlinkCycleException.class);
+ } catch (IOException | InconsistentFilesystemException | FileSymlinkCycleException e) {
+ throw makeMissingInputFileExn(artifact, mandatory, e, env.getListener());
+ }
+ if (fileValue == null) {
+ return null;
+ }
+ if (!fileValue.exists()) {
+ if (allowedMissingInputs.apply(((RootedPath) fileSkyKey.argument()).getRelativePath())) {
+ return FileArtifactValue.MISSING_FILE_MARKER;
+ } else {
+ return missingInputFile(artifact, mandatory, null, env.getListener());
+ }
+ }
+ try {
+ return FileArtifactValue.create(artifact, fileValue);
+ } catch (IOException e) {
+ throw makeMissingInputFileExn(artifact, mandatory, e, env.getListener());
+ }
+ }
+
+ private static ArtifactValue missingInputFile(Artifact artifact, boolean mandatory,
+ Exception failure, EventHandler reporter) throws MissingInputFileException {
+ if (!mandatory) {
+ return FileArtifactValue.MISSING_FILE_MARKER;
+ }
+ throw makeMissingInputFileExn(artifact, mandatory, failure, reporter);
+ }
+
+ private static MissingInputFileException makeMissingInputFileExn(Artifact artifact,
+ boolean mandatory, Exception failure, EventHandler reporter) {
+ String extraMsg = (failure == null) ? "" : (":" + failure.getMessage());
+ MissingInputFileException ex = new MissingInputFileException(
+ constructErrorMessage(artifact) + extraMsg, null);
+ if (mandatory) {
+ reporter.handle(Event.error(ex.getLocation(), ex.getMessage()));
+ }
+ return ex;
+ }
+
+ // Non-aggregating artifact -- should contain at most one piece of artifact data.
+ // data may be null if and only if artifact is a middleman artifact.
+ private ArtifactValue createSimpleValue(Artifact artifact, ActionExecutionValue actionValue)
+ throws IOException {
+ ArtifactValue value = actionValue.getArtifactValue(artifact);
+ if (value != null) {
+ return value;
+ }
+ // Middleman artifacts have no corresponding files, so their ArtifactValues should have already
+ // been constructed during execution of the action.
+ Preconditions.checkState(!artifact.isMiddlemanArtifact(), artifact);
+ FileValue data = Preconditions.checkNotNull(actionValue.getData(artifact),
+ "%s %s", artifact, actionValue);
+ Preconditions.checkNotNull(data.getDigest(),
+ "Digest should already have been calculated for %s (%s)", artifact, data);
+ return FileArtifactValue.create(artifact, data);
+ }
+
+ private AggregatingArtifactValue createAggregatingValue(Artifact artifact, Action action,
+ FileArtifactValue value, SkyFunction.Environment env) {
+ // This artifact aggregates other artifacts. Keep track of them so callers can find them.
+ ImmutableList.Builder<Pair<Artifact, FileArtifactValue>> inputs = ImmutableList.builder();
+ for (Map.Entry<SkyKey, SkyValue> entry :
+ env.getValues(ArtifactValue.mandatoryKeys(action.getInputs())).entrySet()) {
+ Artifact input = ArtifactValue.artifact(entry.getKey());
+ ArtifactValue inputValue = (ArtifactValue) entry.getValue();
+ Preconditions.checkNotNull(inputValue, "%s has null dep %s", artifact, input);
+ if (!(inputValue instanceof FileArtifactValue)) {
+ // We do not recurse in aggregating middleman artifacts.
+ Preconditions.checkState(!(inputValue instanceof AggregatingArtifactValue),
+ "%s %s %s", artifact, action, inputValue);
+ continue;
+ }
+ inputs.add(Pair.of(input, (FileArtifactValue) inputValue));
+ }
+ return new AggregatingArtifactValue(inputs.build(), value);
+ }
+
+ /**
+ * Returns whether this value needs to contain the data of all its inputs. Currently only tests to
+ * see if the action is an aggregating middleman action. However, may include runfiles middleman
+ * actions and Fileset artifacts in the future.
+ */
+ private static boolean isAggregatingValue(Action action) {
+ return action.getActionType() == MiddlemanType.AGGREGATING_MIDDLEMAN;
+ }
+
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return Label.print(((OwnedArtifact) skyKey.argument()).getArtifact().getOwner());
+ }
+
+ private Action extractActionFromArtifact(Artifact artifact, SkyFunction.Environment env) {
+ ArtifactOwner artifactOwner = artifact.getArtifactOwner();
+
+ Preconditions.checkState(artifactOwner instanceof ActionLookupKey, "", artifact, artifactOwner);
+ SkyKey actionLookupKey = ActionLookupValue.key((ActionLookupKey) artifactOwner);
+ ActionLookupValue value = (ActionLookupValue) env.getValue(actionLookupKey);
+ if (value == null) {
+ Preconditions.checkState(artifactOwner == CoverageReportValue.ARTIFACT_OWNER,
+ "Not-yet-present artifact owner: %s", artifactOwner);
+ return null;
+ }
+ // The value should already exist (except for the coverage report action output artifacts):
+ // ConfiguredTargetValues were created during the analysis phase, and BuildInfo*Values
+ // were created during the first analysis of a configured target.
+ Preconditions.checkNotNull(value,
+ "Owner %s of %s not in graph %s", artifactOwner, artifact, actionLookupKey);
+ return Preconditions.checkNotNull(value.getGeneratingAction(artifact),
+ "Value %s does not contain generating action of %s", value, artifact);
+ }
+
+ private static final class ArtifactFunctionException extends SkyFunctionException {
+ ArtifactFunctionException(MissingInputFileException e, Transience transience) {
+ super(e, transience);
+ }
+
+ ArtifactFunctionException(ActionExecutionException e, Transience transience) {
+ super(e, transience);
+ }
+ }
+
+ private static String constructErrorMessage(Artifact artifact) {
+ if (artifact.getOwner() == null) {
+ return String.format("missing input file '%s'", artifact.getPath().getPathString());
+ } else {
+ return String.format("missing input file '%s'", artifact.getOwner());
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ArtifactValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/ArtifactValue.java
new file mode 100644
index 0000000..6139d2e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ArtifactValue.java
@@ -0,0 +1,160 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Function;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.util.Collection;
+
+/**
+ * A value representing an artifact. Source artifacts are checked for existence, while output
+ * artifacts imply creation of the output file.
+ *
+ * <p>There are effectively two kinds of output artifact values. The first corresponds to an
+ * ordinary artifact {@link FileArtifactValue}. It stores the relevant data for the artifact --
+ * digest/mtime and size. The second corresponds to an "aggregating" artifact -- the output of an
+ * aggregating middleman action. It stores the relevant data of all its inputs.
+ */
+@Immutable
+@ThreadSafe
+public abstract class ArtifactValue implements SkyValue {
+
+ @ThreadSafe
+ static SkyKey key(Artifact artifact, boolean isMandatory) {
+ return new SkyKey(SkyFunctions.ARTIFACT, artifact.isSourceArtifact()
+ ? new OwnedArtifact(artifact, isMandatory)
+ : new OwnedArtifact(artifact));
+ }
+
+ private static final Function<Artifact, SkyKey> TO_MANDATORY_KEY =
+ new Function<Artifact, SkyKey>() {
+ @Override
+ public SkyKey apply(Artifact artifact) {
+ return key(artifact, true);
+ }
+ };
+
+ @ThreadSafe
+ public static Iterable<SkyKey> mandatoryKeys(Iterable<Artifact> artifacts) {
+ return Iterables.transform(artifacts, TO_MANDATORY_KEY);
+ }
+
+ private static final Function<OwnedArtifact, Artifact> TO_ARTIFACT =
+ new Function<OwnedArtifact, Artifact>() {
+ @Override
+ public Artifact apply(OwnedArtifact key) {
+ return key.getArtifact();
+ }
+ };
+
+ public static Collection<Artifact> artifacts(Collection<? extends OwnedArtifact> keys) {
+ return Collections2.transform(keys, TO_ARTIFACT);
+ }
+
+ public static Artifact artifact(SkyKey key) {
+ return TO_ARTIFACT.apply((OwnedArtifact) key.argument());
+ }
+
+ /**
+ * Artifacts are compared using just their paths, but in Skyframe, the configured target that owns
+ * an artifact must also be part of the comparison. For example, suppose we build //foo:foo in
+ * configurationA, yielding artifact foo.out. If we change the configuration to configurationB in
+ * such a way that the path to the artifact does not change, requesting foo.out from the graph
+ * will result in the value entry for foo.out under configurationA being returned. This would
+ * prevent caching the graph in different configurations, and also causes big problems with change
+ * pruning, which assumes the invariant that a value's first dependency will always be the same.
+ * In this case, the value entry's old dependency on //foo:foo in configurationA would cause it to
+ * request (//foo:foo, configurationA) from the graph, causing an undesired re-analysis of
+ * (//foo:foo, configurationA).
+ *
+ * <p>In order to prevent that, instead of using Artifacts as keys in the graph, we use
+ * OwnedArtifacts, which compare for equality using both the Artifact, and the owner. The effect
+ * is functionally that of making Artifact.equals() check the owner, but only within Skyframe,
+ * since outside of Skyframe it is quite crucial that Artifacts with different owners be able to
+ * compare equal.
+ */
+ public static class OwnedArtifact {
+ private final Artifact artifact;
+ // Always true for derived artifacts.
+ private final boolean isMandatory;
+
+ /** Constructs an OwnedArtifact wrapper for a source artifact. */
+ private OwnedArtifact(Artifact sourceArtifact, boolean mandatory) {
+ Preconditions.checkArgument(sourceArtifact.isSourceArtifact());
+ this.artifact = Preconditions.checkNotNull(sourceArtifact);
+ this.isMandatory = mandatory;
+ }
+
+ /**
+ * Constructs an OwnedArtifact wrapper for a derived artifact. The mandatory attribute is
+ * not needed because a derived artifact must be a mandatory input for some action in order to
+ * ensure that it is built in the first place. If it fails to build, then that fact is cached
+ * in the node, so any action that has it as a non-mandatory input can retrieve that
+ * information from the node.
+ */
+ private OwnedArtifact(Artifact derivedArtifact) {
+ this.artifact = Preconditions.checkNotNull(derivedArtifact);
+ Preconditions.checkArgument(!derivedArtifact.isSourceArtifact(), derivedArtifact);
+ this.isMandatory = true; // Unused.
+ }
+
+ @Override
+ public int hashCode() {
+ int initialHash = artifact.hashCode() + artifact.getArtifactOwner().hashCode();
+ return isMandatory ? initialHash : 47 * initialHash + 1;
+ }
+
+ @Override
+ public boolean equals(Object that) {
+ if (this == that) {
+ return true;
+ }
+ if (!(that instanceof OwnedArtifact)) {
+ return false;
+ }
+ OwnedArtifact thatOwnedArtifact = ((OwnedArtifact) that);
+ Artifact thatArtifact = thatOwnedArtifact.artifact;
+ return artifact.equals(thatArtifact)
+ && artifact.getArtifactOwner().equals(thatArtifact.getArtifactOwner())
+ && isMandatory == thatOwnedArtifact.isMandatory;
+ }
+
+ Artifact getArtifact() {
+ return artifact;
+ }
+
+ /**
+ * Returns whether the artifact is a mandatory input of its requesting action. May only be
+ * called for source artifacts, since a derived artifact must be a mandatory input of some
+ * action in order to have been built in the first place.
+ */
+ public boolean isMandatory() {
+ Preconditions.checkState(artifact.isSourceArtifact(), artifact);
+ return isMandatory;
+ }
+
+ @Override
+ public String toString() {
+ return artifact.prettyPrint() + " " + artifact.getArtifactOwner();
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/AspectFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/AspectFunction.java
new file mode 100644
index 0000000..f1aa2f6e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/AspectFunction.java
@@ -0,0 +1,187 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ListMultimap;
+import com.google.devtools.build.lib.analysis.Aspect;
+import com.google.devtools.build.lib.analysis.CachingAnalysisEnvironment;
+import com.google.devtools.build.lib.analysis.ConfiguredAspectFactory;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget;
+import com.google.devtools.build.lib.analysis.TargetAndConfiguration;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.analysis.config.ConfigMatchingProvider;
+import com.google.devtools.build.lib.events.StoredEventHandler;
+import com.google.devtools.build.lib.packages.AspectFactory;
+import com.google.devtools.build.lib.packages.Attribute;
+import com.google.devtools.build.lib.packages.NoSuchTargetException;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.skyframe.AspectValue.AspectKey;
+import com.google.devtools.build.lib.skyframe.ConfiguredTargetFunction.DependencyEvaluationException;
+import com.google.devtools.build.lib.skyframe.SkyframeExecutor.BuildViewProvider;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyFunctionException;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+/**
+ * The Skyframe function that generates aspects.
+ */
+public final class AspectFunction implements SkyFunction {
+ private final BuildViewProvider buildViewProvider;
+
+ public AspectFunction(BuildViewProvider buildViewProvider) {
+ this.buildViewProvider = buildViewProvider;
+ }
+
+ @Nullable
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env)
+ throws AspectFunctionException {
+ SkyframeBuildView view = buildViewProvider.getSkyframeBuildView();
+ AspectKey key = (AspectKey) skyKey.argument();
+ ConfiguredAspectFactory aspectFactory =
+ (ConfiguredAspectFactory) AspectFactory.Util.create(key.getAspect());
+
+ PackageValue packageValue =
+ (PackageValue) env.getValue(PackageValue.key(key.getLabel().getPackageIdentifier()));
+ if (packageValue == null) {
+ return null;
+ }
+
+ Target target;
+ try {
+ target = packageValue.getPackage().getTarget(key.getLabel().getName());
+ } catch (NoSuchTargetException e) {
+ throw new AspectFunctionException(skyKey, e);
+ }
+
+ if (!(target instanceof Rule)) {
+ throw new AspectFunctionException(new AspectCreationException(
+ "aspects must be attached to rules"));
+ }
+
+ RuleConfiguredTarget associatedTarget = (RuleConfiguredTarget)
+ ((ConfiguredTargetValue) env.getValue(ConfiguredTargetValue.key(
+ key.getLabel(), key.getConfiguration()))).getConfiguredTarget();
+
+ if (associatedTarget == null) {
+ return null;
+ }
+
+ SkyframeDependencyResolver resolver = view.createDependencyResolver(env);
+ if (resolver == null) {
+ return null;
+ }
+
+ TargetAndConfiguration ctgValue =
+ new TargetAndConfiguration(target, key.getConfiguration());
+
+ try {
+ // Get the configuration targets that trigger this rule's configurable attributes.
+ Set<ConfigMatchingProvider> configConditions =
+ ConfiguredTargetFunction.getConfigConditions(target, env, resolver, ctgValue);
+ if (configConditions == null) {
+ // Those targets haven't yet been resolved.
+ return null;
+ }
+
+ ListMultimap<Attribute, ConfiguredTarget> depValueMap =
+ ConfiguredTargetFunction.computeDependencies(env, resolver, ctgValue,
+ aspectFactory.getDefinition(), configConditions);
+
+ return createAspect(env, key, associatedTarget, configConditions, depValueMap);
+ } catch (DependencyEvaluationException e) {
+ throw new AspectFunctionException(e.getRootCauseSkyKey(), e.getCause());
+ }
+ }
+
+ @Nullable
+ private AspectValue createAspect(Environment env, AspectKey key,
+ RuleConfiguredTarget associatedTarget, Set<ConfigMatchingProvider> configConditions,
+ ListMultimap<Attribute, ConfiguredTarget> directDeps)
+ throws AspectFunctionException {
+ SkyframeBuildView view = buildViewProvider.getSkyframeBuildView();
+ BuildConfiguration configuration = associatedTarget.getConfiguration();
+ boolean extendedSanityChecks = configuration != null && configuration.extendedSanityChecks();
+
+ StoredEventHandler events = new StoredEventHandler();
+ CachingAnalysisEnvironment analysisEnvironment = view.createAnalysisEnvironment(
+ key, false, extendedSanityChecks, events, env, true);
+ if (env.valuesMissing()) {
+ return null;
+ }
+
+ ConfiguredAspectFactory aspectFactory =
+ (ConfiguredAspectFactory) AspectFactory.Util.create(key.getAspect());
+ Aspect aspect = view.createAspect(
+ analysisEnvironment, associatedTarget, aspectFactory, directDeps, configConditions);
+
+ events.replayOn(env.getListener());
+ if (events.hasErrors()) {
+ analysisEnvironment.disable(associatedTarget.getTarget());
+ throw new AspectFunctionException(new AspectCreationException(
+ "Analysis of target '" + associatedTarget.getLabel() + "' failed; build aborted"));
+ }
+ Preconditions.checkState(!analysisEnvironment.hasErrors(),
+ "Analysis environment hasError() but no errors reported");
+
+ if (env.valuesMissing()) {
+ return null;
+ }
+
+ analysisEnvironment.disable(associatedTarget.getTarget());
+ Preconditions.checkNotNull(aspect);
+
+ return new AspectValue(
+ aspect, ImmutableList.copyOf(analysisEnvironment.getRegisteredActions()));
+ }
+
+ @Nullable
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return null;
+ }
+
+ /**
+ * An exception indicating that there was a problem creating an aspect.
+ */
+ public static final class AspectCreationException extends Exception {
+ public AspectCreationException(String message) {
+ super(message);
+ }
+ }
+
+ /**
+ * Used to indicate errors during the computation of an {@link AspectValue}.
+ */
+ private static final class AspectFunctionException extends SkyFunctionException {
+ public AspectFunctionException(Exception e) {
+ super(e, Transience.PERSISTENT);
+ }
+
+ /** Used to rethrow a child error that we cannot handle. */
+ public AspectFunctionException(SkyKey childKey, Exception transitiveError) {
+ super(transitiveError, childKey);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/AspectValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/AspectValue.java
new file mode 100644
index 0000000..9b863bd
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/AspectValue.java
@@ -0,0 +1,109 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Objects;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.analysis.Aspect;
+import com.google.devtools.build.lib.analysis.ConfiguredAspectFactory;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.skyframe.SkyFunctionName;
+import com.google.devtools.build.skyframe.SkyKey;
+
+/**
+ * An aspect in the context of the Skyframe graph.
+ */
+public final class AspectValue extends ActionLookupValue {
+ /**
+ * The key of an action that is generated by an aspect.
+ */
+ public static final class AspectKey extends ActionLookupKey {
+ private final Label label;
+ private final BuildConfiguration configuration;
+ // TODO(bazel-team): class objects are not really hashable or comparable for equality other than
+ // by reference. We should identify the aspect here in a way that does not rely on comparison
+ // by reference so that keys can be serialized and deserialized properly.
+ private final Class<? extends ConfiguredAspectFactory> aspectFactory;
+
+ private AspectKey(Label label, BuildConfiguration configuration,
+ Class<? extends ConfiguredAspectFactory> aspectFactory) {
+ this.label = label;
+ this.configuration = configuration;
+ this.aspectFactory = aspectFactory;
+ }
+
+ @Override
+ public Label getLabel() {
+ return label;
+ }
+
+ public BuildConfiguration getConfiguration() {
+ return configuration;
+ }
+
+ public Class<? extends ConfiguredAspectFactory> getAspect() {
+ return aspectFactory;
+ }
+
+ @Override
+ SkyFunctionName getType() {
+ return SkyFunctions.ASPECT;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(label, configuration, aspectFactory);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+
+ if (!(other instanceof AspectKey)) {
+ return false;
+ }
+
+ AspectKey that = (AspectKey) other;
+ return Objects.equal(label, that.label)
+ && Objects.equal(configuration, that.configuration)
+ && Objects.equal(aspectFactory, that.aspectFactory);
+ }
+
+ @Override
+ public String toString() {
+ return label + "#" + aspectFactory.getSimpleName() + " "
+ + (configuration == null ? "null" : configuration.shortCacheKey());
+ }
+ }
+
+ private final Aspect aspect;
+
+ public AspectValue(Aspect aspect, Iterable<Action> actions) {
+ super(actions);
+ this.aspect = aspect;
+ }
+
+ public Aspect get() {
+ return aspect;
+ }
+
+ public static SkyKey key(Label label, BuildConfiguration configuration,
+ Class<? extends ConfiguredAspectFactory> aspectFactory) {
+ return new SkyKey(SkyFunctions.ASPECT, new AspectKey(label, configuration, aspectFactory));
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/BrokenDiffAwarenessException.java b/src/main/java/com/google/devtools/build/lib/skyframe/BrokenDiffAwarenessException.java
new file mode 100644
index 0000000..a5b0272
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/BrokenDiffAwarenessException.java
@@ -0,0 +1,27 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Preconditions;
+
+/**
+ * Thrown on {@link DiffAwareness#getDiff} to indicate that something is wrong with the
+ * {@link DiffAwareness} instance and it should not be used again.
+ */
+public class BrokenDiffAwarenessException extends Exception {
+
+ public BrokenDiffAwarenessException(String msg) {
+ super(Preconditions.checkNotNull(msg));
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/BuildInfoCollectionFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/BuildInfoCollectionFunction.java
new file mode 100644
index 0000000..e717e51
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/BuildInfoCollectionFunction.java
@@ -0,0 +1,86 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Supplier;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.ArtifactFactory;
+import com.google.devtools.build.lib.actions.Root;
+import com.google.devtools.build.lib.analysis.buildinfo.BuildInfoFactory;
+import com.google.devtools.build.lib.analysis.buildinfo.BuildInfoFactory.BuildInfoContext;
+import com.google.devtools.build.lib.analysis.buildinfo.BuildInfoFactory.BuildInfoKey;
+import com.google.devtools.build.lib.analysis.buildinfo.BuildInfoFactory.BuildInfoType;
+import com.google.devtools.build.lib.skyframe.BuildInfoCollectionValue.BuildInfoKeyAndConfig;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.util.Map;
+
+/**
+ * Creates a {@link BuildInfoCollectionValue}. Only depends on the unique
+ * {@link WorkspaceStatusValue} and the constant {@link PrecomputedValue#BUILD_INFO_FACTORIES}
+ * injected value.
+ */
+public class BuildInfoCollectionFunction implements SkyFunction {
+ // Supplier only because the artifact factory has not yet been created at constructor time.
+ private final Supplier<ArtifactFactory> artifactFactory;
+ private final Root buildDataDirectory;
+
+ BuildInfoCollectionFunction(Supplier<ArtifactFactory> artifactFactory,
+ Root buildDataDirectory) {
+ this.artifactFactory = artifactFactory;
+ this.buildDataDirectory = buildDataDirectory;
+ }
+
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env) {
+ final BuildInfoKeyAndConfig keyAndConfig = (BuildInfoKeyAndConfig) skyKey.argument();
+ WorkspaceStatusValue infoArtifactValue =
+ (WorkspaceStatusValue) env.getValue(WorkspaceStatusValue.SKY_KEY);
+ if (infoArtifactValue == null) {
+ return null;
+ }
+ Map<BuildInfoKey, BuildInfoFactory> buildInfoFactories =
+ PrecomputedValue.BUILD_INFO_FACTORIES.get(env);
+ if (buildInfoFactories == null) {
+ return null;
+ }
+ final ArtifactFactory factory = artifactFactory.get();
+ BuildInfoContext context = new BuildInfoContext() {
+ @Override
+ public Artifact getBuildInfoArtifact(PathFragment rootRelativePath, Root root,
+ BuildInfoType type) {
+ return type == BuildInfoType.NO_REBUILD
+ ? factory.getConstantMetadataArtifact(rootRelativePath, root, keyAndConfig)
+ : factory.getDerivedArtifact(rootRelativePath, root, keyAndConfig);
+ }
+
+ @Override
+ public Root getBuildDataDirectory() {
+ return buildDataDirectory;
+ }
+ };
+
+ return new BuildInfoCollectionValue(buildInfoFactories.get(
+ keyAndConfig.getInfoKey()).create(context, keyAndConfig.getConfig(),
+ infoArtifactValue.getStableArtifact(), infoArtifactValue.getVolatileArtifact()));
+ }
+
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return null;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/BuildInfoCollectionValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/BuildInfoCollectionValue.java
new file mode 100644
index 0000000..8958e1b
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/BuildInfoCollectionValue.java
@@ -0,0 +1,97 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.analysis.buildinfo.BuildInfoCollection;
+import com.google.devtools.build.lib.analysis.buildinfo.BuildInfoFactory;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.skyframe.SkyFunctionName;
+
+import java.util.Objects;
+
+/**
+ * Value that stores {@link BuildInfoCollection}s generated by {@link BuildInfoFactory} instances.
+ * These collections are used during analysis (see {@code CachingAnalysisEnvironment}).
+ */
+public class BuildInfoCollectionValue extends ActionLookupValue {
+ private final BuildInfoCollection collection;
+
+ BuildInfoCollectionValue(BuildInfoCollection collection) {
+ super(collection.getActions());
+ this.collection = collection;
+ }
+
+ public BuildInfoCollection getCollection() {
+ return collection;
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public String toString() {
+ return com.google.common.base.Objects.toStringHelper(getClass())
+ .add("collection", collection)
+ .add("generatingActionMap", generatingActionMap).toString();
+ }
+
+ /** Key for BuildInfoCollectionValues. */
+ public static class BuildInfoKeyAndConfig extends ActionLookupKey {
+ private final BuildInfoFactory.BuildInfoKey infoKey;
+ private final BuildConfiguration config;
+
+ public BuildInfoKeyAndConfig(BuildInfoFactory.BuildInfoKey key, BuildConfiguration config) {
+ this.infoKey = Preconditions.checkNotNull(key, config);
+ this.config = Preconditions.checkNotNull(config, key);
+ }
+
+ @Override
+ SkyFunctionName getType() {
+ return SkyFunctions.BUILD_INFO_COLLECTION;
+ }
+
+ BuildInfoFactory.BuildInfoKey getInfoKey() {
+ return infoKey;
+ }
+
+ BuildConfiguration getConfig() {
+ return config;
+ }
+
+ @Override
+ public Label getLabel() {
+ return null;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(infoKey, config);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (other == null) {
+ return false;
+ }
+ if (this.getClass() != other.getClass()) {
+ return false;
+ }
+ BuildInfoKeyAndConfig that = (BuildInfoKeyAndConfig) other;
+ return Objects.equals(this.infoKey, that.infoKey) && Objects.equals(this.config, that.config);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/Builder.java b/src/main/java/com/google/devtools/build/lib/skyframe/Builder.java
new file mode 100644
index 0000000..7fdb55c
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/Builder.java
@@ -0,0 +1,75 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.BuildFailedException;
+import com.google.devtools.build.lib.actions.Executor;
+import com.google.devtools.build.lib.actions.TestExecException;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadCompatible;
+import com.google.devtools.build.lib.util.AbruptExitException;
+
+import java.util.Collection;
+import java.util.Set;
+
+/**
+ * A Builder consumes top-level artifacts, targets, and tests,, and executes them in some
+ * topological order, possibly concurrently, using some dependency-checking policy.
+ *
+ * <p> The methods of the Builder interface are typically long-running, but honor the
+ * {@link java.lang.Thread#interrupt} contract: if an interrupt is delivered to the thread in which
+ * a call to buildTargets or buildArtifacts is active, the Builder attempts to terminate the call
+ * prematurely, throwing InterruptedException. No guarantee is made about the timeliness of such
+ * termination, as it depends on the ability of the Actions being executed to be interrupted, but
+ * typically any running subprocesses will be quickly killed.
+ */
+public interface Builder {
+
+ /**
+ * Transitively build all given artifacts, targets, and tests, and all necessary prerequisites
+ * thereof. For sequential implementations of this interface, the top-level requests will be
+ * built in the iteration order of the Set provided; for concurrent implementations, the order
+ * is undefined.
+ *
+ * <p>This method should not be invoked more than once concurrently on the same Builder instance.
+ *
+ * @param artifacts the set of Artifacts to build
+ * @param parallelTests tests to execute in parallel with the other top-level targetsToBuild and
+ * artifacts.
+ * @param exclusiveTests are executed one at a time, only after all other tasks have completed
+ * @param targetsToBuild Set of targets which will be built
+ * @param executor an opaque application-specific value that will be
+ * passed down to the execute() method of any Action executed during
+ * this call
+ * @param builtTargets (out) set of successfully built subset of targetsToBuild. This set is
+ * populated immediately upon confirmation that artifact is built so it will be
+ * valid even if a future action throws ActionExecutionException
+ * @throws BuildFailedException if there were problems establishing the action execution
+ * environment, if the the metadata of any file during the build could not be obtained,
+ * if any input files are missing, or if an action fails during execution
+ * @throws InterruptedException if there was an asynchronous stop request
+ * @throws TestExecException if any test fails
+ */
+ @ThreadCompatible
+ void buildArtifacts(Set<Artifact> artifacts,
+ Set<ConfiguredTarget> parallelTests,
+ Set<ConfiguredTarget> exclusiveTests,
+ Collection<ConfiguredTarget> targetsToBuild,
+ Executor executor,
+ Set<ConfiguredTarget> builtTargets,
+ boolean explain)
+ throws BuildFailedException, AbruptExitException, InterruptedException, TestExecException;
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ConfigurationCollectionFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/ConfigurationCollectionFunction.java
new file mode 100644
index 0000000..89828c3
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ConfigurationCollectionFunction.java
@@ -0,0 +1,164 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Supplier;
+import com.google.devtools.build.lib.analysis.BlazeDirectories;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.analysis.config.BuildConfigurationCollection;
+import com.google.devtools.build.lib.analysis.config.BuildConfigurationKey;
+import com.google.devtools.build.lib.analysis.config.BuildOptions;
+import com.google.devtools.build.lib.analysis.config.ConfigurationFactory;
+import com.google.devtools.build.lib.analysis.config.InvalidConfigurationException;
+import com.google.devtools.build.lib.analysis.config.PackageProviderForConfigurations;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.events.StoredEventHandler;
+import com.google.devtools.build.lib.packages.Package;
+import com.google.devtools.build.lib.skyframe.ConfigurationCollectionValue.ConfigurationCollectionKey;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyFunctionException;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+/**
+ * A builder for {@link ConfigurationCollectionValue} instances.
+ */
+public class ConfigurationCollectionFunction implements SkyFunction {
+
+ private final Supplier<ConfigurationFactory> configurationFactory;
+ private final Supplier<Map<String, String>> clientEnv;
+ private final Supplier<Set<Package>> configurationPackages;
+
+ public ConfigurationCollectionFunction(
+ Supplier<ConfigurationFactory> configurationFactory,
+ Supplier<Map<String, String>> clientEnv,
+ Supplier<Set<Package>> configurationPackages) {
+ this.configurationFactory = configurationFactory;
+ this.clientEnv = clientEnv;
+ this.configurationPackages = configurationPackages;
+ }
+
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env) throws InterruptedException,
+ ConfigurationCollectionFunctionException {
+ ConfigurationCollectionKey collectionKey = (ConfigurationCollectionKey) skyKey.argument();
+ try {
+ // We are not using this value, because test_environment can be created from clientEnv. But
+ // we want ConfigurationCollection to be recomputed each time when test_environment changes.
+ PrecomputedValue.TEST_ENVIRONMENT_VARIABLES.get(env);
+ BlazeDirectories directories = PrecomputedValue.BLAZE_DIRECTORIES.get(env);
+ if (env.valuesMissing()) {
+ return null;
+ }
+
+ BuildConfigurationCollection result =
+ getConfigurations(env.getListener(),
+ new SkyframePackageLoaderWithValueEnvironment(env, configurationPackages.get()),
+ new BuildConfigurationKey(collectionKey.getBuildOptions(), directories, clientEnv.get(),
+ collectionKey.getMultiCpu()));
+
+ // BuildConfigurationCollection can be created, but dependencies to some files might be
+ // missing. In that case we need to build configurationCollection again.
+ if (env.valuesMissing()) {
+ return null;
+ }
+
+ for (BuildConfiguration config : result.getTargetConfigurations()) {
+ config.declareSkyframeDependencies(env);
+ }
+ if (env.valuesMissing()) {
+ return null;
+ }
+ return new ConfigurationCollectionValue(result, configurationPackages.get());
+ } catch (InvalidConfigurationException e) {
+ throw new ConfigurationCollectionFunctionException(e);
+ }
+ }
+
+ /** Create the build configurations with the given options. */
+ private BuildConfigurationCollection getConfigurations(EventHandler eventHandler,
+ PackageProviderForConfigurations loadedPackageProvider, BuildConfigurationKey key)
+ throws InvalidConfigurationException {
+ List<BuildConfiguration> targetConfigurations = new ArrayList<>();
+ if (!key.getMultiCpu().isEmpty()) {
+ for (String cpu : key.getMultiCpu()) {
+ BuildConfiguration targetConfiguration = createConfiguration(
+ eventHandler, loadedPackageProvider, key, cpu);
+ if (targetConfiguration == null || targetConfigurations.contains(targetConfiguration)) {
+ continue;
+ }
+ targetConfigurations.add(targetConfiguration);
+ }
+ if (loadedPackageProvider.valuesMissing()) {
+ return null;
+ }
+ } else {
+ BuildConfiguration targetConfiguration = createConfiguration(
+ eventHandler, loadedPackageProvider, key, null);
+ if (targetConfiguration == null) {
+ return null;
+ }
+ targetConfigurations.add(targetConfiguration);
+ }
+ return new BuildConfigurationCollection(targetConfigurations);
+ }
+
+ @Nullable
+ public BuildConfiguration createConfiguration(
+ EventHandler originalEventListener,
+ PackageProviderForConfigurations loadedPackageProvider,
+ BuildConfigurationKey key, String cpuOverride) throws InvalidConfigurationException {
+ StoredEventHandler errorEventListener = new StoredEventHandler();
+ BuildOptions buildOptions = key.getBuildOptions();
+ if (cpuOverride != null) {
+ // TODO(bazel-team): Options classes should be immutable. This is a bit of a hack.
+ buildOptions = buildOptions.clone();
+ buildOptions.get(BuildConfiguration.Options.class).cpu = cpuOverride;
+ }
+
+ BuildConfiguration targetConfig = configurationFactory.get().createConfiguration(
+ loadedPackageProvider, buildOptions, key, errorEventListener);
+ if (targetConfig == null) {
+ return null;
+ }
+ errorEventListener.replayOn(originalEventListener);
+ if (errorEventListener.hasErrors()) {
+ throw new InvalidConfigurationException("Build options are invalid");
+ }
+ return targetConfig;
+ }
+
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return null;
+ }
+
+ /**
+ * Used to declare all the exception types that can be wrapped in the exception thrown by
+ * {@link ConfigurationCollectionFunction#compute}.
+ */
+ private static final class ConfigurationCollectionFunctionException extends
+ SkyFunctionException {
+ public ConfigurationCollectionFunctionException(InvalidConfigurationException e) {
+ super(e, Transience.PERSISTENT);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ConfigurationCollectionValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/ConfigurationCollectionValue.java
new file mode 100644
index 0000000..30e4fd7
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ConfigurationCollectionValue.java
@@ -0,0 +1,100 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.analysis.config.BuildConfigurationCollection;
+import com.google.devtools.build.lib.analysis.config.BuildOptions;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.packages.Package;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.io.Serializable;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * A Skyframe value representing a build configuration collection.
+ */
+@Immutable
+@ThreadSafe
+public class ConfigurationCollectionValue implements SkyValue {
+
+ private final BuildConfigurationCollection configurationCollection;
+ private final ImmutableSet<Package> configurationPackages;
+
+ ConfigurationCollectionValue(BuildConfigurationCollection configurationCollection,
+ Set<Package> configurationPackages) {
+ this.configurationCollection = Preconditions.checkNotNull(configurationCollection);
+ this.configurationPackages = ImmutableSet.copyOf(configurationPackages);
+ }
+
+ public BuildConfigurationCollection getConfigurationCollection() {
+ return configurationCollection;
+ }
+
+ /**
+ * Returns set of packages required for configuration.
+ */
+ public Set<Package> getConfigurationPackages() {
+ return configurationPackages;
+ }
+
+ @ThreadSafe
+ public static SkyKey key(BuildOptions buildOptions, ImmutableSet<String> multiCpu) {
+ return new SkyKey(SkyFunctions.CONFIGURATION_COLLECTION,
+ new ConfigurationCollectionKey(buildOptions, multiCpu));
+ }
+
+ static final class ConfigurationCollectionKey implements Serializable {
+ private final BuildOptions buildOptions;
+ private final ImmutableSet<String> multiCpu;
+ private final int hashCode;
+
+ public ConfigurationCollectionKey(BuildOptions buildOptions, ImmutableSet<String> multiCpu) {
+ this.buildOptions = Preconditions.checkNotNull(buildOptions);
+ this.multiCpu = Preconditions.checkNotNull(multiCpu);
+ this.hashCode = Objects.hash(buildOptions, multiCpu);
+ }
+
+ public BuildOptions getBuildOptions() {
+ return buildOptions;
+ }
+
+ public ImmutableSet<String> getMultiCpu() {
+ return multiCpu;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof ConfigurationCollectionKey)) {
+ return false;
+ }
+ ConfigurationCollectionKey confObject = (ConfigurationCollectionKey) o;
+ return Objects.equals(multiCpu, confObject.multiCpu)
+ && Objects.equals(buildOptions, confObject.buildOptions);
+ }
+
+ @Override
+ public int hashCode() {
+ return hashCode;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ConfigurationFragmentFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/ConfigurationFragmentFunction.java
new file mode 100644
index 0000000..0393b16
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ConfigurationFragmentFunction.java
@@ -0,0 +1,146 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Supplier;
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.analysis.BlazeDirectories;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration.Fragment;
+import com.google.devtools.build.lib.analysis.config.BuildOptions;
+import com.google.devtools.build.lib.analysis.config.ConfigurationEnvironment;
+import com.google.devtools.build.lib.analysis.config.ConfigurationFragmentFactory;
+import com.google.devtools.build.lib.analysis.config.InvalidConfigurationException;
+import com.google.devtools.build.lib.analysis.config.PackageProviderForConfigurations;
+import com.google.devtools.build.lib.packages.NoSuchPackageException;
+import com.google.devtools.build.lib.packages.NoSuchTargetException;
+import com.google.devtools.build.lib.packages.Package;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.skyframe.ConfigurationFragmentValue.ConfigurationFragmentKey;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.syntax.Label.SyntaxException;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyFunctionException;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.io.IOException;
+import java.util.Set;
+
+/**
+ * A builder for {@link ConfigurationFragmentValue}s.
+ */
+public class ConfigurationFragmentFunction implements SkyFunction {
+
+ private final Supplier<ImmutableList<ConfigurationFragmentFactory>> configurationFragments;
+ private final Supplier<Set<Package>> configurationPackages;
+
+ public ConfigurationFragmentFunction(
+ Supplier<ImmutableList<ConfigurationFragmentFactory>> configurationFragments,
+ Supplier<Set<Package>> configurationPackages) {
+ this.configurationFragments = configurationFragments;
+ this.configurationPackages = configurationPackages;
+ }
+
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env) throws InterruptedException,
+ ConfigurationFragmentFunctionException {
+ ConfigurationFragmentKey configurationFragmentKey =
+ (ConfigurationFragmentKey) skyKey.argument();
+ BuildOptions buildOptions = configurationFragmentKey.getBuildOptions();
+ ConfigurationFragmentFactory factory = getFactory(configurationFragmentKey.getFragmentType());
+ try {
+ PackageProviderForConfigurations loadedPackageProvider =
+ new SkyframePackageLoaderWithValueEnvironment(env, configurationPackages.get());
+ ConfigurationEnvironment confEnv = new ConfigurationBuilderEnvironment(loadedPackageProvider);
+ Fragment fragment = factory.create(confEnv, buildOptions);
+
+ if (env.valuesMissing()) {
+ return null;
+ }
+ return new ConfigurationFragmentValue(fragment);
+ } catch (InvalidConfigurationException e) {
+ // TODO(bazel-team): Rework the control-flow here so that we're not actually throwing this
+ // exception with missing Skyframe dependencies.
+ if (env.valuesMissing()) {
+ return null;
+ }
+ throw new ConfigurationFragmentFunctionException(e);
+ }
+ }
+
+ private ConfigurationFragmentFactory getFactory(Class<? extends Fragment> fragmentType) {
+ for (ConfigurationFragmentFactory factory : configurationFragments.get()) {
+ if (factory.creates().equals(fragmentType)) {
+ return factory;
+ }
+ }
+ throw new IllegalStateException(
+ "There is no factory for fragment: " + fragmentType.getSimpleName());
+ }
+
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return null;
+ }
+
+ /**
+ * A {@link ConfigurationEnvironment} implementation that can create dependencies on files.
+ */
+ private final class ConfigurationBuilderEnvironment implements ConfigurationEnvironment {
+ private final PackageProviderForConfigurations loadedPackageProvider;
+
+ ConfigurationBuilderEnvironment(
+ PackageProviderForConfigurations loadedPackageProvider) {
+ this.loadedPackageProvider = loadedPackageProvider;
+ }
+
+ @Override
+ public Target getTarget(Label label) throws NoSuchPackageException, NoSuchTargetException {
+ return loadedPackageProvider.getLoadedTarget(label);
+ }
+
+ @Override
+ public Path getPath(Package pkg, String fileName) {
+ Path result = pkg.getPackageDirectory().getRelative(fileName);
+ try {
+ loadedPackageProvider.addDependency(pkg, fileName);
+ } catch (IOException | SyntaxException e) {
+ return null;
+ }
+ return result;
+ }
+
+ @Override
+ public <T extends Fragment> T getFragment(BuildOptions buildOptions, Class<T> fragmentType)
+ throws InvalidConfigurationException {
+ return loadedPackageProvider.getFragment(buildOptions, fragmentType);
+ }
+
+ @Override
+ public BlazeDirectories getBlazeDirectories() {
+ return loadedPackageProvider.getDirectories();
+ }
+ }
+
+ /**
+ * Used to declare all the exception types that can be wrapped in the exception thrown by
+ * {@link ConfigurationFragmentFunction#compute}.
+ */
+ private static final class ConfigurationFragmentFunctionException extends SkyFunctionException {
+ public ConfigurationFragmentFunctionException(InvalidConfigurationException e) {
+ super(e, Transience.PERSISTENT);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ConfigurationFragmentValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/ConfigurationFragmentValue.java
new file mode 100644
index 0000000..cc07216
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ConfigurationFragmentValue.java
@@ -0,0 +1,90 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration.Fragment;
+import com.google.devtools.build.lib.analysis.config.BuildOptions;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.io.Serializable;
+import java.util.Objects;
+
+import javax.annotation.Nullable;
+
+/**
+ * A Skyframe node representing a build configuration fragment.
+ */
+@Immutable
+@ThreadSafe
+public class ConfigurationFragmentValue implements SkyValue {
+
+ @Nullable
+ private final BuildConfiguration.Fragment fragment;
+
+ ConfigurationFragmentValue(BuildConfiguration.Fragment fragment) {
+ this.fragment = fragment;
+ }
+
+ public BuildConfiguration.Fragment getFragment() {
+ return fragment;
+ }
+
+ @ThreadSafe
+ public static SkyKey key(BuildOptions buildOptions, Class<? extends Fragment> fragmentType) {
+ return new SkyKey(SkyFunctions.CONFIGURATION_FRAGMENT,
+ new ConfigurationFragmentKey(buildOptions, fragmentType));
+ }
+
+ static final class ConfigurationFragmentKey implements Serializable {
+ private final BuildOptions buildOptions;
+ private final Class<? extends Fragment> fragmentType;
+
+ public ConfigurationFragmentKey(BuildOptions buildOptions,
+ Class<? extends Fragment> fragmentType) {
+ this.buildOptions = Preconditions.checkNotNull(buildOptions);
+ this.fragmentType = Preconditions.checkNotNull(fragmentType);
+ }
+
+ public BuildOptions getBuildOptions() {
+ return buildOptions;
+ }
+
+ public Class<? extends Fragment> getFragmentType() {
+ return fragmentType;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof ConfigurationFragmentKey)) {
+ return false;
+ }
+ ConfigurationFragmentKey confObject = (ConfigurationFragmentKey) o;
+ return Objects.equals(fragmentType, confObject.fragmentType)
+ && Objects.equals(buildOptions, confObject.buildOptions);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(buildOptions, fragmentType);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ConfiguredTargetFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/ConfiguredTargetFunction.java
new file mode 100644
index 0000000..9fc3df4
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ConfiguredTargetFunction.java
@@ -0,0 +1,578 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Function;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.ListMultimap;
+import com.google.devtools.build.lib.analysis.Aspect;
+import com.google.devtools.build.lib.analysis.CachingAnalysisEnvironment;
+import com.google.devtools.build.lib.analysis.ConfiguredAspectFactory;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.DependencyResolver.Dependency;
+import com.google.devtools.build.lib.analysis.LabelAndConfiguration;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget;
+import com.google.devtools.build.lib.analysis.TargetAndConfiguration;
+import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.analysis.config.ConfigMatchingProvider;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.StoredEventHandler;
+import com.google.devtools.build.lib.packages.AspectDefinition;
+import com.google.devtools.build.lib.packages.AspectFactory;
+import com.google.devtools.build.lib.packages.Attribute;
+import com.google.devtools.build.lib.packages.InputFile;
+import com.google.devtools.build.lib.packages.NoSuchPackageException;
+import com.google.devtools.build.lib.packages.NoSuchTargetException;
+import com.google.devtools.build.lib.packages.NoSuchThingException;
+import com.google.devtools.build.lib.packages.PackageGroup;
+import com.google.devtools.build.lib.packages.RawAttributeMapper;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.packages.TargetUtils;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.skyframe.AspectFunction.AspectCreationException;
+import com.google.devtools.build.lib.skyframe.SkyframeExecutor.BuildViewProvider;
+import com.google.devtools.build.lib.syntax.EvalException;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyFunctionException;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+import com.google.devtools.build.skyframe.ValueOrException2;
+import com.google.devtools.build.skyframe.ValueOrException3;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+/**
+ * SkyFunction for {@link ConfiguredTargetValue}s.
+ */
+final class ConfiguredTargetFunction implements SkyFunction {
+
+ /**
+ * Exception class that signals an error during the evaluation of a dependency.
+ */
+ public static class DependencyEvaluationException extends Exception {
+ private final SkyKey rootCauseSkyKey;
+
+ public DependencyEvaluationException(Exception cause) {
+ super(cause);
+ this.rootCauseSkyKey = null;
+ }
+
+ public DependencyEvaluationException(SkyKey rootCauseSkyKey, Exception cause) {
+ super(cause);
+ this.rootCauseSkyKey = rootCauseSkyKey;
+ }
+
+ /**
+ * Returns the key of the root cause or null if the problem was with this target.
+ */
+ public SkyKey getRootCauseSkyKey() {
+ return rootCauseSkyKey;
+ }
+
+ @Override
+ public Exception getCause() {
+ return (Exception) super.getCause();
+ }
+ }
+
+ private static final Function<Dependency, SkyKey> TO_KEYS =
+ new Function<Dependency, SkyKey>() {
+ @Override
+ public SkyKey apply(Dependency input) {
+ return ConfiguredTargetValue.key(input.getLabel(), input.getConfiguration());
+ }
+ };
+
+ private final BuildViewProvider buildViewProvider;
+
+ ConfiguredTargetFunction(BuildViewProvider buildViewProvider) {
+ this.buildViewProvider = buildViewProvider;
+ }
+
+ @Override
+ public SkyValue compute(SkyKey key, Environment env) throws ConfiguredTargetFunctionException,
+ InterruptedException {
+ SkyframeBuildView view = buildViewProvider.getSkyframeBuildView();
+
+ ConfiguredTargetKey configuredTargetKey = (ConfiguredTargetKey) key.argument();
+ LabelAndConfiguration lc = LabelAndConfiguration.of(
+ configuredTargetKey.getLabel(), configuredTargetKey.getConfiguration());
+
+ BuildConfiguration configuration = lc.getConfiguration();
+
+ PackageValue packageValue =
+ (PackageValue) env.getValue(PackageValue.key(lc.getLabel().getPackageIdentifier()));
+ if (packageValue == null) {
+ return null;
+ }
+
+ Target target;
+ try {
+ target = packageValue.getPackage().getTarget(lc.getLabel().getName());
+ } catch (NoSuchTargetException e1) {
+ throw new ConfiguredTargetFunctionException(new NoSuchTargetException(lc.getLabel(),
+ "No such target"));
+ }
+ // TODO(bazel-team): This is problematic - we create the right key, but then end up with a value
+ // that doesn't match; we can even have the same value multiple times. However, I think it's
+ // only triggered in tests (i.e., in normal operation, the configuration passed in is already
+ // null).
+ if (target instanceof InputFile) {
+ // InputFileConfiguredTarget expects its configuration to be null since it's not used.
+ configuration = null;
+ } else if (target instanceof PackageGroup) {
+ // Same for PackageGroupConfiguredTarget.
+ configuration = null;
+ }
+ TargetAndConfiguration ctgValue =
+ new TargetAndConfiguration(target, configuration);
+
+ SkyframeDependencyResolver resolver = view.createDependencyResolver(env);
+ if (resolver == null) {
+ return null;
+ }
+
+ try {
+ // Get the configuration targets that trigger this rule's configurable attributes.
+ Set<ConfigMatchingProvider> configConditions =
+ getConfigConditions(ctgValue.getTarget(), env, resolver, ctgValue);
+ if (configConditions == null) {
+ // Those targets haven't yet been resolved.
+ return null;
+ }
+
+ ListMultimap<Attribute, ConfiguredTarget> depValueMap =
+ computeDependencies(env, resolver, ctgValue, null, configConditions);
+ return createConfiguredTarget(
+ view, env, target, configuration, depValueMap, configConditions);
+ } catch (DependencyEvaluationException e) {
+ throw new ConfiguredTargetFunctionException(e.getRootCauseSkyKey(), e.getCause());
+ }
+ }
+
+ /**
+ * Computes the direct dependencies of a node in the configured target graph (a configured
+ * target or an aspect).
+ *
+ * <p>Returns null if Skyframe hasn't evaluated the required dependencies yet. In this case, the
+ * caller should also return null to Skyframe.
+ *
+ * @param env the Skyframe environment
+ * @param resolver The dependency resolver
+ * @param ctgValue The label and the configuration of the node
+ * @param aspectDefinition the aspect of the node (if null, the node is a configured target,
+ * otherwise it's an asect)
+ * @param configConditions the configuration conditions for evaluating the attributes of the node
+ * @return an attribute -> direct dependency multimap
+ * @throws ConfiguredTargetFunctionException
+ */
+ @Nullable
+ static ListMultimap<Attribute, ConfiguredTarget> computeDependencies(
+ Environment env, SkyframeDependencyResolver resolver, TargetAndConfiguration ctgValue,
+ AspectDefinition aspectDefinition, Set<ConfigMatchingProvider> configConditions)
+ throws DependencyEvaluationException {
+
+ // 1. Create the map from attributes to list of (target, configuration) pairs.
+ ListMultimap<Attribute, Dependency> depValueNames;
+ try {
+ depValueNames = resolver.dependentNodeMap(ctgValue, aspectDefinition, configConditions);
+ } catch (EvalException e) {
+ env.getListener().handle(Event.error(e.getLocation(), e.getMessage()));
+ throw new DependencyEvaluationException(new ConfiguredValueCreationException(e.print()));
+ }
+
+ // 2. Resolve configured target dependencies and handle errors.
+ Map<SkyKey, ConfiguredTarget> depValues =
+ resolveConfiguredTargetDependencies(env, depValueNames.values(), ctgValue.getTarget());
+ if (depValues == null) {
+ return null;
+ }
+
+ // 3. Resolve required aspects.
+ ListMultimap<SkyKey, Aspect> depAspects = resolveAspectDependencies(
+ env, depValues, depValueNames.values());
+ if (depAspects == null) {
+ return null;
+ }
+
+ // 3. Merge the dependent configured targets and aspects into a single map.
+ return mergeAspects(depValueNames, depValues, depAspects);
+ }
+
+ /**
+ * Merges the each direct dependency configured target with the aspects associated with it.
+ *
+ * <p>Note that the combination of a configured target and its associated aspects are not
+ * represented by a Skyframe node. This is because there can possibly be many different
+ * combinations of aspects for a particular configured target, so it would result in a
+ * combinatiorial explosion of Skyframe nodes.
+ */
+ private static ListMultimap<Attribute, ConfiguredTarget> mergeAspects(
+ ListMultimap<Attribute, Dependency> depValueNames,
+ Map<SkyKey, ConfiguredTarget> depConfiguredTargetMap,
+ ListMultimap<SkyKey, Aspect> depAspectMap) {
+ ListMultimap<Attribute, ConfiguredTarget> result = ArrayListMultimap.create();
+
+ for (Map.Entry<Attribute, Dependency> entry : depValueNames.entries()) {
+ Dependency dep = entry.getValue();
+ SkyKey depKey = TO_KEYS.apply(dep);
+ ConfiguredTarget depConfiguredTarget = depConfiguredTargetMap.get(depKey);
+ result.put(entry.getKey(),
+ RuleConfiguredTarget.mergeAspects(depConfiguredTarget, depAspectMap.get(depKey)));
+ }
+
+ return result;
+ }
+
+ /**
+ * Given a list of {@link Dependency} objects, returns a multimap from the {@link SkyKey} of the
+ * dependency to the {@link Aspect} instances that should be merged into it.
+ *
+ * <p>Returns null if the required aspects are not computed yet.
+ */
+ @Nullable
+ private static ListMultimap<SkyKey, Aspect> resolveAspectDependencies(Environment env,
+ Map<SkyKey, ConfiguredTarget> configuredTargetMap, Iterable<Dependency> deps)
+ throws DependencyEvaluationException {
+ ListMultimap<SkyKey, Aspect> result = ArrayListMultimap.create();
+ Set<SkyKey> aspectKeys = new HashSet<>();
+ for (Dependency dep : deps) {
+ for (Class<? extends ConfiguredAspectFactory> depAspect : dep.getAspects()) {
+ aspectKeys.add(AspectValue.key(dep.getLabel(), dep.getConfiguration(), depAspect));
+ }
+ }
+
+ Map<SkyKey, ValueOrException3<
+ AspectCreationException, NoSuchThingException, ConfiguredValueCreationException>>
+ depAspects = env.getValuesOrThrow(aspectKeys, AspectCreationException.class,
+ NoSuchThingException.class, ConfiguredValueCreationException.class);
+
+ for (Dependency dep : deps) {
+ SkyKey depKey = TO_KEYS.apply(dep);
+ ConfiguredTarget depConfiguredTarget = configuredTargetMap.get(depKey);
+ List<AspectValue> aspects = new ArrayList<>();
+ for (Class<? extends ConfiguredAspectFactory> depAspect : dep.getAspects()) {
+ if (!aspectMatchesConfiguredTarget(depConfiguredTarget, depAspect)) {
+ continue;
+ }
+
+ SkyKey aspectKey = AspectValue.key(dep.getLabel(), dep.getConfiguration(), depAspect);
+ AspectValue aspectValue = null;
+ try {
+ aspectValue = (AspectValue) depAspects.get(aspectKey).get();
+ } catch (ConfiguredValueCreationException e) {
+ // The configured target should have been created in resolveConfiguredTargetDependencies()
+ throw new IllegalStateException(e);
+ } catch (NoSuchThingException | AspectCreationException e) {
+ AspectFactory depAspectFactory = AspectFactory.Util.create(depAspect);
+ throw new DependencyEvaluationException(new ConfiguredValueCreationException(
+ String.format("Evaluation of aspect %s on %s failed: %s",
+ depAspectFactory.getDefinition().getName(), dep.getLabel(), e.toString())));
+ }
+
+ if (aspectValue == null) {
+ // Dependent aspect has either not been computed yet or is in error.
+ return null;
+ }
+ result.put(depKey, aspectValue.get());
+ }
+ }
+
+ return result;
+ }
+
+ private static boolean aspectMatchesConfiguredTarget(ConfiguredTarget dep,
+ Class<? extends ConfiguredAspectFactory> aspectFactory) {
+ AspectDefinition aspectDefinition = AspectFactory.Util.create(aspectFactory).getDefinition();
+ for (Class<?> provider : aspectDefinition.getRequiredProviders()) {
+ if (dep.getProvider((Class<? extends TransitiveInfoProvider>) provider) == null) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns which aspects are computable based on the precise set of providers direct dependencies
+ * publish (and not the upper estimate in their rule definition).
+ *
+ * <p>An aspect is computable for a particular configured target if the configured target supplies
+ * all the providers the aspect requires.
+ *
+ * @param upperEstimate a multimap from attribute to the upper estimates computed by
+ * {@link com.google.devtools.build.lib.analysis.DependencyResolver}.
+ * @param configuredTargetDeps a multimap from attribute to the directly dependent configured
+ * targets
+ * @return a multimap from attribute to the more precise {@link Dependency} objects
+ */
+ private static ListMultimap<Attribute, Dependency> getComputableAspects(
+ ListMultimap<Attribute, Dependency> upperEstimate,
+ Map<SkyKey, ConfiguredTarget> configuredTargetDeps) {
+ ListMultimap<Attribute, Dependency> result = ArrayListMultimap.create();
+ for (Map.Entry<Attribute, Dependency> entry : upperEstimate.entries()) {
+ ConfiguredTarget dep =
+ configuredTargetDeps.get(TO_KEYS.apply(entry.getValue()));
+ List<Class<? extends ConfiguredAspectFactory>> depAspects = new ArrayList<>();
+ for (Class<? extends ConfiguredAspectFactory> candidate : entry.getValue().getAspects()) {
+ boolean ok = true;
+ for (Class<?> requiredProvider :
+ AspectFactory.Util.create(candidate).getDefinition().getRequiredProviders()) {
+ if (dep.getProvider((Class<? extends TransitiveInfoProvider>) requiredProvider) == null) {
+ ok = false;
+ break;
+ }
+ }
+
+ if (ok) {
+ depAspects.add(candidate);
+ }
+ }
+
+ result.put(entry.getKey(), new Dependency(
+ entry.getValue().getLabel(), entry.getValue().getConfiguration(),
+ ImmutableSet.copyOf(depAspects)));
+ }
+
+ return result;
+ }
+
+ /**
+ * Returns the set of {@link ConfigMatchingProvider}s that key the configurable attributes
+ * used by this rule.
+ *
+ * <p>>If the configured targets supplying those providers aren't yet resolved by the
+ * dependency resolver, returns null.
+ */
+ @Nullable
+ static Set<ConfigMatchingProvider> getConfigConditions(Target target, Environment env,
+ SkyframeDependencyResolver resolver, TargetAndConfiguration ctgValue)
+ throws DependencyEvaluationException {
+ if (!(target instanceof Rule)) {
+ return ImmutableSet.of();
+ }
+
+ ImmutableSet.Builder<ConfigMatchingProvider> configConditions = ImmutableSet.builder();
+
+ // Collect the labels of the configured targets we need to resolve.
+ ListMultimap<Attribute, LabelAndConfiguration> configLabelMap = ArrayListMultimap.create();
+ RawAttributeMapper attributeMap = RawAttributeMapper.of(((Rule) target));
+ for (Attribute a : ((Rule) target).getAttributes()) {
+ for (Label configLabel : attributeMap.getConfigurabilityKeys(a.getName(), a.getType())) {
+ if (!Type.Selector.isReservedLabel(configLabel)) {
+ configLabelMap.put(a, LabelAndConfiguration.of(
+ configLabel, ctgValue.getConfiguration()));
+ }
+ }
+ }
+ if (configLabelMap.isEmpty()) {
+ return ImmutableSet.of();
+ }
+
+ // Collect the corresponding Skyframe configured target values. Abort early if they haven't
+ // been computed yet.
+ Collection<Dependency> configValueNames =
+ resolver.resolveRuleLabels(ctgValue, null, configLabelMap);
+ Map<SkyKey, ConfiguredTarget> configValues =
+ resolveConfiguredTargetDependencies(env, configValueNames, target);
+ if (configValues == null) {
+ return null;
+ }
+
+ // Get the configured targets as ConfigMatchingProvider interfaces.
+ for (Dependency entry : configValueNames) {
+ ConfiguredTarget value = configValues.get(TO_KEYS.apply(entry));
+ // The code above guarantees that value is non-null here.
+ ConfigMatchingProvider provider = value.getProvider(ConfigMatchingProvider.class);
+ if (provider != null) {
+ configConditions.add(provider);
+ } else {
+ // Not a valid provider for configuration conditions.
+ String message =
+ entry.getLabel() + " is not a valid configuration key for " + target.getLabel();
+ env.getListener().handle(Event.error(TargetUtils.getLocationMaybe(target), message));
+ throw new DependencyEvaluationException(new ConfiguredValueCreationException(message));
+ }
+ }
+
+ return configConditions.build();
+ }
+
+ /***
+ * Resolves the targets referenced in depValueNames and returns their ConfiguredTarget
+ * instances.
+ *
+ * <p>Returns null if not all instances are available yet.
+ *
+ */
+ @Nullable
+ private static Map<SkyKey, ConfiguredTarget> resolveConfiguredTargetDependencies(
+ Environment env, Collection<Dependency> deps, Target target)
+ throws DependencyEvaluationException {
+ boolean ok = !env.valuesMissing();
+ String message = null;
+ Iterable<SkyKey> depKeys = Iterables.transform(deps, TO_KEYS);
+ // TODO(bazel-team): maybe having a two-exception argument is better than typing a generic
+ // Exception here.
+ Map<SkyKey, ValueOrException2<NoSuchTargetException,
+ NoSuchPackageException>> depValuesOrExceptions = env.getValuesOrThrow(depKeys,
+ NoSuchTargetException.class, NoSuchPackageException.class);
+ Map<SkyKey, ConfiguredTarget> depValues = new HashMap<>(depValuesOrExceptions.size());
+ SkyKey childKey = null;
+ NoSuchThingException transitiveChildException = null;
+ for (Map.Entry<SkyKey, ValueOrException2<NoSuchTargetException, NoSuchPackageException>> entry
+ : depValuesOrExceptions.entrySet()) {
+ ConfiguredTargetKey depKey = (ConfiguredTargetKey) entry.getKey().argument();
+ LabelAndConfiguration depLabelAndConfiguration = LabelAndConfiguration.of(
+ depKey.getLabel(), depKey.getConfiguration());
+ Label depLabel = depLabelAndConfiguration.getLabel();
+ ConfiguredTargetValue depValue = null;
+ NoSuchThingException directChildException = null;
+ try {
+ depValue = (ConfiguredTargetValue) entry.getValue().get();
+ } catch (NoSuchTargetException e) {
+ if (depLabel.equals(e.getLabel())) {
+ directChildException = e;
+ } else {
+ childKey = entry.getKey();
+ transitiveChildException = e;
+ }
+ } catch (NoSuchPackageException e) {
+ if (depLabel.getPackageName().equals(e.getPackageName())) {
+ directChildException = e;
+ } else {
+ childKey = entry.getKey();
+ transitiveChildException = e;
+ }
+ }
+ // If an exception wasn't caused by a direct child target value, we'll treat it the same
+ // as any other missing dep by setting ok = false below, and returning null at the end.
+ if (directChildException != null) {
+ // Only update messages for missing targets we depend on directly.
+ message = TargetUtils.formatMissingEdge(target, depLabel, directChildException);
+ env.getListener().handle(Event.error(TargetUtils.getLocationMaybe(target), message));
+ }
+
+ if (depValue == null) {
+ ok = false;
+ } else {
+ depValues.put(entry.getKey(), depValue.getConfiguredTarget());
+ }
+ }
+ if (message != null) {
+ throw new DependencyEvaluationException(new NoSuchTargetException(message));
+ }
+ if (childKey != null) {
+ throw new DependencyEvaluationException(childKey, transitiveChildException);
+ }
+ if (!ok) {
+ return null;
+ } else {
+ return depValues;
+ }
+ }
+
+
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return Label.print(((ConfiguredTargetKey) skyKey.argument()).getLabel());
+ }
+
+ @Nullable
+ private ConfiguredTargetValue createConfiguredTarget(SkyframeBuildView view,
+ Environment env, Target target, BuildConfiguration configuration,
+ ListMultimap<Attribute, ConfiguredTarget> depValueMap,
+ Set<ConfigMatchingProvider> configConditions)
+ throws ConfiguredTargetFunctionException,
+ InterruptedException {
+ boolean extendedSanityChecks = configuration != null && configuration.extendedSanityChecks();
+
+ StoredEventHandler events = new StoredEventHandler();
+ BuildConfiguration ownerConfig = (configuration == null)
+ ? null : configuration.getArtifactOwnerConfiguration();
+ boolean allowRegisteringActions = configuration == null || configuration.isActionsEnabled();
+ CachingAnalysisEnvironment analysisEnvironment = view.createAnalysisEnvironment(
+ new ConfiguredTargetKey(target.getLabel(), ownerConfig), false,
+ extendedSanityChecks, events, env, allowRegisteringActions);
+ if (env.valuesMissing()) {
+ return null;
+ }
+
+ ConfiguredTarget configuredTarget = view.createConfiguredTarget(target, configuration,
+ analysisEnvironment, depValueMap, configConditions);
+
+ events.replayOn(env.getListener());
+ if (events.hasErrors()) {
+ analysisEnvironment.disable(target);
+ throw new ConfiguredTargetFunctionException(new ConfiguredValueCreationException(
+ "Analysis of target '" + target.getLabel() + "' failed; build aborted"));
+ }
+ Preconditions.checkState(!analysisEnvironment.hasErrors(),
+ "Analysis environment hasError() but no errors reported");
+ if (env.valuesMissing()) {
+ return null;
+ }
+
+ analysisEnvironment.disable(target);
+ Preconditions.checkNotNull(configuredTarget, target);
+
+ return new ConfiguredTargetValue(configuredTarget,
+ ImmutableList.copyOf(analysisEnvironment.getRegisteredActions()));
+ }
+
+ /**
+ * An exception indicating that there was a problem during the construction of
+ * a ConfiguredTargetValue.
+ */
+ public static final class ConfiguredValueCreationException extends Exception {
+
+ public ConfiguredValueCreationException(String message) {
+ super(message);
+ }
+ }
+
+ /**
+ * Used to declare all the exception types that can be wrapped in the exception thrown by
+ * {@link ConfiguredTargetFunction#compute}.
+ */
+ public static final class ConfiguredTargetFunctionException extends SkyFunctionException {
+ public ConfiguredTargetFunctionException(NoSuchTargetException e) {
+ super(e, Transience.PERSISTENT);
+ }
+
+ private ConfiguredTargetFunctionException(ConfiguredValueCreationException error) {
+ super(error, Transience.PERSISTENT);
+ };
+
+ private ConfiguredTargetFunctionException(
+ @Nullable SkyKey childKey, Exception transitiveError) {
+ super(transitiveError, childKey);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ConfiguredTargetKey.java b/src/main/java/com/google/devtools/build/lib/skyframe/ConfiguredTargetKey.java
new file mode 100644
index 0000000..ea744c1
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ConfiguredTargetKey.java
@@ -0,0 +1,96 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.skyframe.SkyFunctionName;
+
+import java.util.Objects;
+
+import javax.annotation.Nullable;
+
+/**
+ * A (Label, Configuration) pair. Note that this pair may be used to look up the generating action
+ * of an artifact. Callers may want to ensure that they have the correct configuration for this
+ * purpose by passing in {@link BuildConfiguration#getArtifactOwnerConfiguration} in preference to
+ * the raw configuration.
+ */
+public class ConfiguredTargetKey extends ActionLookupValue.ActionLookupKey {
+ private final Label label;
+ @Nullable
+ private final BuildConfiguration configuration;
+
+ public ConfiguredTargetKey(Label label, @Nullable BuildConfiguration configuration) {
+ this.label = Preconditions.checkNotNull(label);
+ this.configuration = configuration;
+ }
+
+ public ConfiguredTargetKey(ConfiguredTarget rule) {
+ this(rule.getTarget().getLabel(), rule.getConfiguration());
+ }
+
+ @Override
+ public Label getLabel() {
+ return label;
+ }
+
+ @Override
+ SkyFunctionName getType() {
+ return SkyFunctions.CONFIGURED_TARGET;
+ }
+
+ @Nullable
+ public BuildConfiguration getConfiguration() {
+ return configuration;
+ }
+
+ @Override
+ public int hashCode() {
+ int configVal = configuration == null ? 79 : configuration.hashCode();
+ return 31 * label.hashCode() + configVal;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (!(obj instanceof ConfiguredTargetKey)) {
+ return false;
+ }
+ ConfiguredTargetKey other = (ConfiguredTargetKey) obj;
+ return Objects.equals(label, other.label) && Objects.equals(configuration, other.configuration);
+ }
+
+ public String prettyPrint() {
+ if (label == null) {
+ return "null";
+ }
+ return (configuration != null && configuration.isHostConfiguration())
+ ? (label.toString() + " (host)") : label.toString();
+ }
+
+ @Override
+ public String toString() {
+ return label + " " + (configuration == null ? "null" : configuration.shortCacheKey());
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ConfiguredTargetValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/ConfiguredTargetValue.java
new file mode 100644
index 0000000..200e05f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ConfiguredTargetValue.java
@@ -0,0 +1,105 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.skyframe.SkyKey;
+
+import javax.annotation.Nullable;
+
+/**
+ * A configured target in the context of a Skyframe graph.
+ */
+@Immutable
+@ThreadSafe
+@VisibleForTesting
+public final class ConfiguredTargetValue extends ActionLookupValue {
+
+ // These variables are only non-final because they may be clear()ed to save memory. They are null
+ // only after they are cleared.
+ @Nullable private ConfiguredTarget configuredTarget;
+
+ // We overload this variable to check whether the value has been clear()ed. We don't use a
+ // separate variable in order to save memory.
+ @Nullable private volatile Iterable<Action> actions;
+
+ ConfiguredTargetValue(ConfiguredTarget configuredTarget, Iterable<Action> actions) {
+ super(actions);
+ this.configuredTarget = configuredTarget;
+ this.actions = actions;
+ }
+
+ @VisibleForTesting
+ public ConfiguredTarget getConfiguredTarget() {
+ Preconditions.checkNotNull(actions, configuredTarget);
+ return configuredTarget;
+ }
+
+ @VisibleForTesting
+ public Iterable<Action> getActions() {
+ return Preconditions.checkNotNull(actions, configuredTarget);
+ }
+
+ /**
+ * Clears configured target data from this value, leaving only the artifact->generating action
+ * map.
+ *
+ * <p>Should only be used when user specifies --discard_analysis_cache. Must be called at most
+ * once per value, after which {@link #getConfiguredTarget} and {@link #getActions} cannot be
+ * called.
+ */
+ public void clear() {
+ Preconditions.checkNotNull(actions, configuredTarget);
+ configuredTarget = null;
+ actions = null;
+ }
+
+ @VisibleForTesting
+ public static SkyKey key(Label label, BuildConfiguration configuration) {
+ return key(new ConfiguredTargetKey(label, configuration));
+ }
+
+ static ImmutableList<SkyKey> keys(Iterable<ConfiguredTargetKey> lacs) {
+ ImmutableList.Builder<SkyKey> keys = ImmutableList.builder();
+ for (ConfiguredTargetKey lac : lacs) {
+ keys.add(key(lac));
+ }
+ return keys.build();
+ }
+
+ /**
+ * Returns a label of ConfiguredTargetValue.
+ */
+ @ThreadSafe
+ static Label extractLabel(SkyKey value) {
+ Object valueName = value.argument();
+ Preconditions.checkState(valueName instanceof ConfiguredTargetKey, valueName);
+ return ((ConfiguredTargetKey) valueName).getLabel();
+ }
+
+ @Override
+ public String toString() {
+ return "ConfiguredTargetValue: "
+ + configuredTarget + ", actions: " + (actions == null ? null : Iterables.toString(actions));
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ContainingPackageLookupFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/ContainingPackageLookupFunction.java
new file mode 100644
index 0000000..58cb67d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ContainingPackageLookupFunction.java
@@ -0,0 +1,55 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.devtools.build.lib.packages.PackageIdentifier;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import javax.annotation.Nullable;
+
+/**
+ * SkyFunction for {@link ContainingPackageLookupValue}s.
+ */
+public class ContainingPackageLookupFunction implements SkyFunction {
+
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env) {
+ PackageIdentifier dir = (PackageIdentifier) skyKey.argument();
+ PackageLookupValue pkgLookupValue = null;
+ pkgLookupValue = (PackageLookupValue) env.getValue(PackageLookupValue.key(dir));
+ if (pkgLookupValue == null) {
+ return null;
+ }
+
+ if (pkgLookupValue.packageExists()) {
+ return ContainingPackageLookupValue.withContainingPackage(dir, pkgLookupValue.getRoot());
+ }
+
+ PathFragment parentDir = dir.getPackageFragment().getParentDirectory();
+ if (parentDir == null) {
+ return ContainingPackageLookupValue.noContainingPackage();
+ }
+ PackageIdentifier parentId = new PackageIdentifier(dir.getRepository(), parentDir);
+ return env.getValue(ContainingPackageLookupValue.key(parentId));
+ }
+
+ @Nullable
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return null;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ContainingPackageLookupValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/ContainingPackageLookupValue.java
new file mode 100644
index 0000000..16516b5
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ContainingPackageLookupValue.java
@@ -0,0 +1,111 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.packages.PackageIdentifier;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+/**
+ * A value that represents the result of looking for the existence of a package that owns a
+ * specific directory path. Compare with {@link PackageLookupValue}, which deals with existence of
+ * a specific package.
+ */
+public abstract class ContainingPackageLookupValue implements SkyValue {
+ /** Returns whether there is a containing package. */
+ public abstract boolean hasContainingPackage();
+
+ /** If there is a containing package, returns its name. */
+ abstract PackageIdentifier getContainingPackageName();
+
+ /** If there is a containing package, returns its package root */
+ public abstract Path getContainingPackageRoot();
+
+ public static SkyKey key(PackageIdentifier id) {
+ Preconditions.checkArgument(!id.getPackageFragment().isAbsolute(), id);
+ return new SkyKey(SkyFunctions.CONTAINING_PACKAGE_LOOKUP, id);
+ }
+
+ static ContainingPackageLookupValue noContainingPackage() {
+ return NoContainingPackage.INSTANCE;
+ }
+
+ static ContainingPackageLookupValue withContainingPackage(PackageIdentifier pkgId, Path root) {
+ return new ContainingPackage(pkgId, root);
+ }
+
+ private static class NoContainingPackage extends ContainingPackageLookupValue {
+ private static final NoContainingPackage INSTANCE = new NoContainingPackage();
+
+ @Override
+ public boolean hasContainingPackage() {
+ return false;
+ }
+
+ @Override
+ public PackageIdentifier getContainingPackageName() {
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public Path getContainingPackageRoot() {
+ throw new IllegalStateException();
+ }
+ }
+
+ private static class ContainingPackage extends ContainingPackageLookupValue {
+ private final PackageIdentifier containingPackage;
+ private final Path containingPackageRoot;
+
+ private ContainingPackage(PackageIdentifier pkgId, Path containingPackageRoot) {
+ this.containingPackage = pkgId;
+ this.containingPackageRoot = containingPackageRoot;
+ }
+
+ @Override
+ public boolean hasContainingPackage() {
+ return true;
+ }
+
+ @Override
+ public PackageIdentifier getContainingPackageName() {
+ return containingPackage;
+ }
+
+ @Override
+ public Path getContainingPackageRoot() {
+ return containingPackageRoot;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof ContainingPackage)) {
+ return false;
+ }
+ ContainingPackage other = (ContainingPackage) obj;
+ return containingPackage.equals(other.containingPackage)
+ && containingPackageRoot.equals(other.containingPackageRoot);
+ }
+
+ @Override
+ public int hashCode() {
+ return containingPackage.hashCode();
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/CoverageReportFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/CoverageReportFunction.java
new file mode 100644
index 0000000..8d6fe3d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/CoverageReportFunction.java
@@ -0,0 +1,49 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+/**
+ * A Skyframe function to calculate the coverage report Action and Artifacts.
+ */
+public class CoverageReportFunction implements SkyFunction {
+ CoverageReportFunction() {}
+
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env) {
+ Preconditions.checkState(
+ CoverageReportValue.SKY_KEY.equals(skyKey), String.format(
+ "Expected %s for SkyKey but got %s instead", CoverageReportValue.SKY_KEY, skyKey));
+
+ Action action = PrecomputedValue.COVERAGE_REPORT_KEY.get(env);
+ if (action == null) {
+ return null;
+ }
+
+ return new CoverageReportValue(
+ action.getOutputs(),
+ action);
+ }
+
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return null;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/CoverageReportValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/CoverageReportValue.java
new file mode 100644
index 0000000..862e381
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/CoverageReportValue.java
@@ -0,0 +1,55 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.ArtifactOwner;
+import com.google.devtools.build.skyframe.SkyFunctionName;
+import com.google.devtools.build.skyframe.SkyKey;
+
+/**
+ * A SkyValue to store the coverage report Action and Artifacts.
+ */
+public class CoverageReportValue extends ActionLookupValue {
+ private final ImmutableSet<Artifact> coverageReportArtifacts;
+
+ // There should only ever be one CoverageReportValue value in the graph.
+ public static final SkyKey SKY_KEY = new SkyKey(SkyFunctions.COVERAGE_REPORT, "COVERAGE_REPORT");
+ public static final ArtifactOwner ARTIFACT_OWNER = new CoverageReportKey();
+
+ public CoverageReportValue(ImmutableSet<Artifact> coverageReportArtifacts,
+ Action coverageReportAction) {
+ super(coverageReportAction);
+ this.coverageReportArtifacts = coverageReportArtifacts;
+ }
+
+ public ImmutableSet<Artifact> getCoverageReportArtifacts() {
+ return coverageReportArtifacts;
+ }
+
+ private static class CoverageReportKey extends ActionLookupKey {
+ @Override
+ SkyFunctionName getType() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ SkyKey getSkyKey() {
+ return SKY_KEY;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/DiffAwareness.java b/src/main/java/com/google/devtools/build/lib/skyframe/DiffAwareness.java
new file mode 100644
index 0000000..d0f4c99
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/DiffAwareness.java
@@ -0,0 +1,82 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.devtools.build.lib.vfs.ModifiedFileSet;
+import com.google.devtools.build.lib.vfs.Path;
+
+import java.io.Closeable;
+
+import javax.annotation.Nullable;
+
+/**
+ * Interface for computing modifications of files under a package path entry.
+ *
+ * <p> Skyframe has a {@link DiffAwareness} instance per package-path entry, and each instance is
+ * responsible for all files under its path entry. At the beginning of each incremental build,
+ * skyframe queries for changes using {@link #getDiff}. Ideally, {@link #getDiff} should be
+ * constant-time; if it were linear in the number of files of interest, we might as well just
+ * detect modifications manually.
+ */
+public interface DiffAwareness extends Closeable {
+
+ /** Factory for creating {@link DiffAwareness} instances. */
+ public interface Factory {
+ /**
+ * Returns a {@link DiffAwareness} instance suitable for managing changes to files under the
+ * given package path entry, or {@code null} if this factory cannot create such an instance.
+ *
+ * <p> Skyframe has a collection of factories, and will create a {@link DiffAwareness} instance
+ * per package path entry using one of the factories that returns a non-null value.
+ */
+ @Nullable
+ DiffAwareness maybeCreate(Path pathEntry);
+ }
+
+ /** Opaque view of the filesystem under a package path entry at a specific point in time. */
+ interface View {
+ }
+
+ /**
+ * Returns the live view of the filesystem under the package path entry.
+ *
+ * @throws BrokenDiffAwarenessException if something is wrong and the caller should discard this
+ * {@link DiffAwareness} instance. The {@link DiffAwareness} is expected to close itself in
+ * this case.
+ */
+ View getCurrentView() throws BrokenDiffAwarenessException;
+
+ /**
+ * Returns the set of files of interest that have been modified between the given two views.
+ *
+ * <p>The given views must have come from previous calls to {@link #getCurrentView} on the
+ * {@link DiffAwareness} instance (i.e. using a {@link View} from another instance is not
+ * supported).
+ *
+ * @throws IncompatibleViewException if the given views are not compatible with this
+ * {@link DiffAwareness} instance. This probably indicates a bug.
+ * @throws BrokenDiffAwarenessException if something is wrong and the caller should discard this
+ * {@link DiffAwareness} instance. The {@link DiffAwareness} is expected to close itself in
+ * this case.
+ */
+ ModifiedFileSet getDiff(View oldView, View newView)
+ throws IncompatibleViewException, BrokenDiffAwarenessException;
+
+ /**
+ * Must be called whenever the {@link DiffAwareness} object is to be discarded. Using a
+ * {@link DiffAwareness} instance after calling {@link #close} on it is unspecified behavior.
+ */
+ @Override
+ void close();
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/DiffAwarenessManager.java b/src/main/java/com/google/devtools/build/lib/skyframe/DiffAwarenessManager.java
new file mode 100644
index 0000000..d1bb81e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/DiffAwarenessManager.java
@@ -0,0 +1,188 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.Reporter;
+import com.google.devtools.build.lib.skyframe.DiffAwareness.View;
+import com.google.devtools.build.lib.vfs.ModifiedFileSet;
+import com.google.devtools.build.lib.vfs.Path;
+
+import java.util.Map;
+
+import javax.annotation.Nullable;
+
+/**
+ * Helper class to make it easier to correctly use the {@link DiffAwareness} interface in a
+ * sequential manner.
+ */
+public final class DiffAwarenessManager {
+
+ private final ImmutableSet<? extends DiffAwareness.Factory> diffAwarenessFactories;
+ private Map<Path, DiffAwarenessState> currentDiffAwarenessStates = Maps.newHashMap();
+ private final Reporter reporter;
+
+ public DiffAwarenessManager(Iterable<? extends DiffAwareness.Factory> diffAwarenessFactories,
+ Reporter reporter) {
+ this.diffAwarenessFactories = ImmutableSet.copyOf(diffAwarenessFactories);
+ this.reporter = reporter;
+ }
+
+ private static class DiffAwarenessState {
+ private final DiffAwareness diffAwareness;
+ /**
+ * The {@link View} that should be the baseline for the next {@link #getDiff} call, or
+ * {@code null} if the next {@link #getDiff} will be the first incremental one.
+ */
+ @Nullable
+ private View baselineView;
+
+ private DiffAwarenessState(DiffAwareness diffAwareness, @Nullable View baselineView) {
+ this.diffAwareness = diffAwareness;
+ this.baselineView = baselineView;
+ }
+ }
+
+ /** Reset internal {@link DiffAwareness} state. */
+ public void reset() {
+ for (DiffAwarenessState diffAwarenessState : currentDiffAwarenessStates.values()) {
+ diffAwarenessState.diffAwareness.close();
+ }
+ currentDiffAwarenessStates.clear();
+ }
+
+ /** A set of modified files that should be marked as processed. */
+ public interface ProcessableModifiedFileSet {
+ ModifiedFileSet getModifiedFileSet();
+
+ /**
+ * This should be called when the changes have been noted. Otherwise, the result from the next
+ * call to {@link #getDiff} will be from the baseline of the old, unprocessed, diff.
+ */
+ void markProcessed();
+ }
+
+ /**
+ * Gets the set of changed files since the last call with this path entry, or
+ * {@code ModifiedFileSet.EVERYTHING_MODIFIED} if this is the first such call.
+ */
+ public ProcessableModifiedFileSet getDiff(Path pathEntry) {
+ DiffAwarenessState diffAwarenessState = maybeGetDiffAwarenessState(pathEntry);
+ if (diffAwarenessState == null) {
+ return BrokenProcessableModifiedFileSet.INSTANCE;
+ }
+ DiffAwareness diffAwareness = diffAwarenessState.diffAwareness;
+ View newView;
+ try {
+ newView = diffAwareness.getCurrentView();
+ } catch (BrokenDiffAwarenessException e) {
+ handleBrokenDiffAwareness(pathEntry, e);
+ return BrokenProcessableModifiedFileSet.INSTANCE;
+ }
+
+ View baselineView = diffAwarenessState.baselineView;
+ if (baselineView == null) {
+ diffAwarenessState.baselineView = newView;
+ return BrokenProcessableModifiedFileSet.INSTANCE;
+ }
+
+ ModifiedFileSet diff;
+ try {
+ diff = diffAwareness.getDiff(baselineView, newView);
+ } catch (BrokenDiffAwarenessException e) {
+ handleBrokenDiffAwareness(pathEntry, e);
+ return BrokenProcessableModifiedFileSet.INSTANCE;
+ } catch (IncompatibleViewException e) {
+ throw new IllegalStateException(pathEntry + " " + baselineView + " " + newView, e);
+ }
+ ProcessableModifiedFileSet result = new ProcessableModifiedFileSetImpl(diff, pathEntry,
+ newView);
+ return result;
+ }
+
+ private void handleBrokenDiffAwareness(Path pathEntry, BrokenDiffAwarenessException e) {
+ currentDiffAwarenessStates.remove(pathEntry);
+ reporter.handle(Event.warn(e.getMessage() + "... temporarily falling back to manually "
+ + "checking files for changes"));
+ }
+
+ /**
+ * Returns the current diff awareness for the given path entry, or a fresh one if there is no
+ * current one, or otherwise {@code null} if no factory could make a fresh one.
+ */
+ @Nullable
+ private DiffAwarenessState maybeGetDiffAwarenessState(Path pathEntry) {
+ DiffAwarenessState diffAwarenessState = currentDiffAwarenessStates.get(pathEntry);
+ if (diffAwarenessState != null) {
+ return diffAwarenessState;
+ }
+ for (DiffAwareness.Factory factory : diffAwarenessFactories) {
+ DiffAwareness newDiffAwareness = factory.maybeCreate(pathEntry);
+ if (newDiffAwareness != null) {
+ diffAwarenessState = new DiffAwarenessState(newDiffAwareness, /*previousView=*/null);
+ currentDiffAwarenessStates.put(pathEntry, diffAwarenessState);
+ return diffAwarenessState;
+ }
+ }
+ return null;
+ }
+
+ private class ProcessableModifiedFileSetImpl implements ProcessableModifiedFileSet {
+
+ private final ModifiedFileSet modifiedFileSet;
+ private final Path pathEntry;
+ /**
+ * The {@link View} that should be the baseline on the next {@link #getDiff} call after
+ * {@link #markProcessed} is called.
+ */
+ private final View nextView;
+
+ private ProcessableModifiedFileSetImpl(ModifiedFileSet modifiedFileSet, Path pathEntry,
+ View nextView) {
+ this.modifiedFileSet = modifiedFileSet;
+ this.pathEntry = pathEntry;
+ this.nextView = nextView;
+ }
+
+ @Override
+ public ModifiedFileSet getModifiedFileSet() {
+ return modifiedFileSet;
+ }
+
+ @Override
+ public void markProcessed() {
+ DiffAwarenessState diffAwarenessState = currentDiffAwarenessStates.get(pathEntry);
+ if (diffAwarenessState != null) {
+ diffAwarenessState.baselineView = nextView;
+ }
+ }
+ }
+
+ private static class BrokenProcessableModifiedFileSet implements ProcessableModifiedFileSet {
+
+ private static final BrokenProcessableModifiedFileSet INSTANCE =
+ new BrokenProcessableModifiedFileSet();
+
+ @Override
+ public ModifiedFileSet getModifiedFileSet() {
+ return ModifiedFileSet.EVERYTHING_MODIFIED;
+ }
+
+ @Override
+ public void markProcessed() {
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/DirectoryListingFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/DirectoryListingFunction.java
new file mode 100644
index 0000000..93c3d75
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/DirectoryListingFunction.java
@@ -0,0 +1,72 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.devtools.build.lib.vfs.RootedPath;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyFunctionException;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import javax.annotation.Nullable;
+
+/**
+ * A {@link SkyFunction} for {@link DirectoryListingValue}s.
+ */
+final class DirectoryListingFunction implements SkyFunction {
+
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env)
+ throws DirectoryListingFunctionException {
+ RootedPath dirRootedPath = (RootedPath) skyKey.argument();
+
+ FileValue dirFileValue = (FileValue) env.getValue(FileValue.key(dirRootedPath));
+ if (dirFileValue == null) {
+ return null;
+ }
+
+ RootedPath realDirRootedPath = dirFileValue.realRootedPath();
+ if (!dirFileValue.isDirectory()) {
+ // Recall that the directory is assumed to exist (see DirectoryListingValue#key).
+ throw new DirectoryListingFunctionException(new InconsistentFilesystemException(
+ dirRootedPath.asPath() + " is no longer an existing directory. Did you delete it during "
+ + "the build?"));
+ }
+
+ DirectoryListingStateValue directoryListingStateValue =
+ (DirectoryListingStateValue) env.getValue(DirectoryListingStateValue.key(
+ realDirRootedPath));
+ if (directoryListingStateValue == null) {
+ return null;
+ }
+
+ return DirectoryListingValue.value(dirRootedPath, dirFileValue, directoryListingStateValue);
+ }
+
+ @Nullable
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return null;
+ }
+
+ /**
+ * Used to declare all the exception types that can be wrapped in the exception thrown by
+ * {@link DirectoryListingFunction#compute}.
+ */
+ private static final class DirectoryListingFunctionException extends SkyFunctionException {
+ public DirectoryListingFunctionException(InconsistentFilesystemException e) {
+ super(e, Transience.TRANSIENT);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/DirectoryListingStateFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/DirectoryListingStateFunction.java
new file mode 100644
index 0000000..6e47a2d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/DirectoryListingStateFunction.java
@@ -0,0 +1,68 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.devtools.build.lib.vfs.RootedPath;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyFunctionException;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.io.IOException;
+
+/**
+ * A {@link SkyFunction} for {@link DirectoryListingStateValue}s.
+ *
+ * <p>Merely calls DirectoryListingStateValue#create, but also has special handling for
+ * directories outside the package roots (see {@link ExternalFilesHelper}).
+ */
+public class DirectoryListingStateFunction implements SkyFunction {
+
+ private final ExternalFilesHelper externalFilesHelper;
+
+ public DirectoryListingStateFunction(ExternalFilesHelper externalFilesHelper) {
+ this.externalFilesHelper = externalFilesHelper;
+ }
+
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env)
+ throws DirectoryListingStateFunctionException {
+ RootedPath dirRootedPath = (RootedPath) skyKey.argument();
+ externalFilesHelper.maybeAddDepOnBuildId(dirRootedPath, env);
+ if (env.valuesMissing()) {
+ return null;
+ }
+ try {
+ return DirectoryListingStateValue.create(dirRootedPath);
+ } catch (IOException e) {
+ throw new DirectoryListingStateFunctionException(e);
+ }
+ }
+
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return null;
+ }
+
+ /**
+ * Used to declare all the exception types that can be wrapped in the exception thrown by
+ * {@link DirectoryListingStateFunction#compute}.
+ */
+ private static final class DirectoryListingStateFunctionException
+ extends SkyFunctionException {
+ public DirectoryListingStateFunctionException(IOException e) {
+ super(e, Transience.TRANSIENT);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/DirectoryListingStateValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/DirectoryListingStateValue.java
new file mode 100644
index 0000000..87b9748
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/DirectoryListingStateValue.java
@@ -0,0 +1,214 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.vfs.Dirent;
+import com.google.devtools.build.lib.vfs.Dirent.Type;
+import com.google.devtools.build.lib.vfs.RootedPath;
+import com.google.devtools.build.lib.vfs.Symlinks;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.BitSet;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.Objects;
+
+/**
+ * Encapsulates the filesystem operations needed to get the directory entries of a directory.
+ *
+ * <p>This class is an implementation detail of {@link DirectoryListingValue}.
+ */
+final class DirectoryListingStateValue implements SkyValue {
+
+ private final CompactSortedDirents compactSortedDirents;
+
+ private DirectoryListingStateValue(Collection<Dirent> dirents) {
+ this.compactSortedDirents = CompactSortedDirents.create(dirents);
+ }
+
+ @VisibleForTesting
+ public static DirectoryListingStateValue createForTesting(Collection<Dirent> dirents) {
+ return new DirectoryListingStateValue(dirents);
+ }
+
+ public static DirectoryListingStateValue create(RootedPath dirRootedPath) throws IOException {
+ Collection<Dirent> dirents = dirRootedPath.asPath().readdir(Symlinks.NOFOLLOW);
+ return new DirectoryListingStateValue(dirents);
+ }
+
+ @ThreadSafe
+ public static SkyKey key(RootedPath rootedPath) {
+ return new SkyKey(SkyFunctions.DIRECTORY_LISTING_STATE, rootedPath);
+ }
+
+ /**
+ * Returns the directory entries for this directory, in a stable order.
+ *
+ * <p>Symlinks are not expanded.
+ */
+ public Iterable<Dirent> getDirents() {
+ return compactSortedDirents;
+ }
+
+ @Override
+ public int hashCode() {
+ return compactSortedDirents.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof DirectoryListingStateValue)) {
+ return false;
+ }
+ DirectoryListingStateValue other = (DirectoryListingStateValue) obj;
+ return compactSortedDirents.equals(other.compactSortedDirents);
+ }
+
+ /** A space-efficient, sorted, immutable dirent structure. */
+ private static class CompactSortedDirents implements Iterable<Dirent>, Serializable {
+
+ private final String[] names;
+ private final BitSet packedTypes;
+
+ private CompactSortedDirents(String[] names, BitSet packedTypes) {
+ this.names = names;
+ this.packedTypes = packedTypes;
+ }
+
+ public static CompactSortedDirents create(Collection<Dirent> dirents) {
+ final Dirent[] direntArray = dirents.toArray(new Dirent[dirents.size()]);
+ Integer[] indices = new Integer[dirents.size()];
+ for (int i = 0; i < dirents.size(); i++) {
+ indices[i] = i;
+ }
+ Arrays.sort(indices,
+ new Comparator<Integer>() {
+ @Override
+ public int compare(Integer o1, Integer o2) {
+ return direntArray[o1].getName().compareTo(direntArray[o2].getName());
+ }
+ });
+ String[] names = new String[dirents.size()];
+ BitSet packedTypes = new BitSet(dirents.size() * 2);
+ for (int i = 0; i < dirents.size(); i++) {
+ Dirent dirent = direntArray[indices[i]];
+ names[i] = dirent.getName();
+ packType(packedTypes, dirent.getType(), i);
+ }
+ return new CompactSortedDirents(names, packedTypes);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof CompactSortedDirents)) {
+ return false;
+ }
+ if (this == obj) {
+ return true;
+ }
+ CompactSortedDirents other = (CompactSortedDirents) obj;
+ return Arrays.equals(names, other.names) && packedTypes.equals(other.packedTypes);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(Arrays.hashCode(names), packedTypes);
+ }
+
+ @Override
+ public Iterator<Dirent> iterator() {
+ return new Iterator<Dirent>() {
+
+ private int i = 0;
+
+ @Override
+ public boolean hasNext() {
+ return i < size();
+ }
+
+ @Override
+ public Dirent next() {
+ return direntAt(i++);
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+ };
+ }
+
+ private int size() {
+ return names.length;
+ }
+
+ /** Returns the type of the ith dirent. */
+ private Dirent.Type unpackType(int i) {
+ int start = i * 2;
+ boolean upper = packedTypes.get(start);
+ boolean lower = packedTypes.get(start + 1);
+ if (!upper && !lower) {
+ return Type.FILE;
+ } else if (!upper && lower){
+ return Type.DIRECTORY;
+ } else if (upper && !lower) {
+ return Type.SYMLINK;
+ } else {
+ return Type.UNKNOWN;
+ }
+ }
+
+ /** Sets the type of the ith dirent. */
+ private static void packType(BitSet bitSet, Dirent.Type type, int i) {
+ int start = i * 2;
+ switch (type) {
+ case FILE:
+ pack(bitSet, start, false, false);
+ break;
+ case DIRECTORY:
+ pack(bitSet, start, false, true);
+ break;
+ case SYMLINK:
+ pack(bitSet, start, true, false);
+ break;
+ case UNKNOWN:
+ pack(bitSet, start, true, true);
+ break;
+ default:
+ throw new IllegalStateException("Unknown dirent type: " + type);
+ }
+ }
+
+ private static void pack(BitSet bitSet, int start, boolean upper, boolean lower) {
+ bitSet.set(start, upper);
+ bitSet.set(start + 1, lower);
+ }
+
+ private Dirent direntAt(int i) {
+ Preconditions.checkState(i >= 0 && i < size(), "i: %s, size: %s", i, size());
+ return new Dirent(names[i], unpackType(i));
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/DirectoryListingValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/DirectoryListingValue.java
new file mode 100644
index 0000000..3fe6dba
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/DirectoryListingValue.java
@@ -0,0 +1,134 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.vfs.Dirent;
+import com.google.devtools.build.lib.vfs.RootedPath;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.util.Objects;
+
+/**
+ * A value that represents the list of files in a given directory under a given package path root.
+ * Anything in Skyframe that cares about the contents of a directory should have a dependency
+ * on the corresponding {@link DirectoryListingValue}.
+ *
+ * <p>This value only depends on the FileValue corresponding to the directory. In particular, note
+ * that it does not depend on any of its child entries.
+ *
+ * <p>Note that symlinks in dirents are <b>not</b> expanded. Dependents of the value are responsible
+ * for expanding the symlink entries by referring to FileValues that correspond to the symlinks.
+ * This is a little onerous, but correct: we do not need to reread the directory when a symlink
+ * inside it changes, therefore this value should not be invalidated in that case.
+ */
+@Immutable
+@ThreadSafe
+abstract class DirectoryListingValue implements SkyValue {
+
+ /**
+ * Returns the directory entries for this directory, in a stable order.
+ *
+ * <p>Symlinks are not expanded.
+ */
+ public abstract Iterable<Dirent> getDirents();
+
+ /**
+ * Returns a {@link SkyKey} for getting the directory entries of the given directory. The
+ * given path is assumed to be an existing directory (e.g. via {@link FileValue#isDirectory} or
+ * from a directory listing on its parent directory).
+ */
+ @ThreadSafe
+ static SkyKey key(RootedPath directoryUnderRoot) {
+ return new SkyKey(SkyFunctions.DIRECTORY_LISTING, directoryUnderRoot);
+ }
+
+ static DirectoryListingValue value(RootedPath dirRootedPath, FileValue dirFileValue,
+ DirectoryListingStateValue realDirectoryListingStateValue) {
+ return dirFileValue.realRootedPath().equals(dirRootedPath)
+ ? new RegularDirectoryListingValue(realDirectoryListingStateValue)
+ : new DifferentRealPathDirectoryListingValue(dirFileValue.realRootedPath(),
+ realDirectoryListingStateValue);
+ }
+
+ @ThreadSafe
+ private static final class RegularDirectoryListingValue extends DirectoryListingValue {
+
+ private final DirectoryListingStateValue directoryListingStateValue;
+
+ private RegularDirectoryListingValue(DirectoryListingStateValue directoryListingStateValue) {
+ this.directoryListingStateValue = directoryListingStateValue;
+ }
+
+ @Override
+ public Iterable<Dirent> getDirents() {
+ return directoryListingStateValue.getDirents();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof RegularDirectoryListingValue)) {
+ return false;
+ }
+ RegularDirectoryListingValue other = (RegularDirectoryListingValue) obj;
+ return directoryListingStateValue.equals(other.directoryListingStateValue);
+ }
+
+ @Override
+ public int hashCode() {
+ return directoryListingStateValue.hashCode();
+ }
+ }
+
+ @ThreadSafe
+ private static final class DifferentRealPathDirectoryListingValue extends DirectoryListingValue {
+
+ private final RootedPath realDirRootedPath;
+ private final DirectoryListingStateValue directoryListingStateValue;
+
+ private DifferentRealPathDirectoryListingValue(RootedPath realDirRootedPath,
+ DirectoryListingStateValue directoryListingStateValue) {
+ this.realDirRootedPath = realDirRootedPath;
+ this.directoryListingStateValue = directoryListingStateValue;
+ }
+
+ @Override
+ public Iterable<Dirent> getDirents() {
+ return directoryListingStateValue.getDirents();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof DifferentRealPathDirectoryListingValue)) {
+ return false;
+ }
+ DifferentRealPathDirectoryListingValue other = (DifferentRealPathDirectoryListingValue) obj;
+ return realDirRootedPath.equals(other.realDirRootedPath)
+ && directoryListingStateValue.equals(other.directoryListingStateValue);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(realDirRootedPath, directoryListingStateValue);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ErrorReadingSkylarkExtensionException.java b/src/main/java/com/google/devtools/build/lib/skyframe/ErrorReadingSkylarkExtensionException.java
new file mode 100644
index 0000000..8593afb
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ErrorReadingSkylarkExtensionException.java
@@ -0,0 +1,21 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+/** Indicates some sort of IO error while dealing with a Skylark extension. */
+public class ErrorReadingSkylarkExtensionException extends Exception {
+ public ErrorReadingSkylarkExtensionException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ExternalFilesHelper.java b/src/main/java/com/google/devtools/build/lib/skyframe/ExternalFilesHelper.java
new file mode 100644
index 0000000..ce858de
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ExternalFilesHelper.java
@@ -0,0 +1,97 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.pkgcache.PathPackageLocator;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.RootedPath;
+import com.google.devtools.build.skyframe.SkyFunction;
+
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicReference;
+
+/** Common utilities for dealing with files outside the package roots. */
+class ExternalFilesHelper {
+
+ private final AtomicReference<PathPackageLocator> pkgLocator;
+ private final Set<Path> immutableDirs;
+
+ ExternalFilesHelper(AtomicReference<PathPackageLocator> pkgLocator) {
+ this(pkgLocator, ImmutableSet.<Path>of());
+ }
+
+ ExternalFilesHelper(AtomicReference<PathPackageLocator> pkgLocator, Set<Path> immutableDirs) {
+ this.pkgLocator = pkgLocator;
+ this.immutableDirs = immutableDirs;
+ }
+
+ private enum FileType {
+ // A file inside the package roots.
+ INTERNAL_FILE,
+
+ // A file outside the package roots that we may pretends is immutable.
+ EXTERNAL_IMMUTABLE_FILE,
+
+ // A file outside the package roots about which we may make no other assumptions.
+ EXTERNAL_MUTABLE_FILE,
+ }
+
+ private FileType getFileType(RootedPath rootedPath) {
+ // TODO(bazel-team): This is inefficient when there are a lot of package roots or there are a
+ // lot of immutable directories. Consider either explicitly preventing this case or using a more
+ // efficient approach here (e.g. use a trie for determing if a file is under an immutable
+ // directory).
+ if (!pkgLocator.get().getPathEntries().contains(rootedPath.getRoot())) {
+ Path path = rootedPath.asPath();
+ for (Path immutableDir : immutableDirs) {
+ if (path.startsWith(immutableDir)) {
+ return FileType.EXTERNAL_IMMUTABLE_FILE;
+ }
+ }
+ return FileType.EXTERNAL_MUTABLE_FILE;
+ }
+ return FileType.INTERNAL_FILE;
+ }
+
+ public boolean shouldAssumeImmutable(RootedPath rootedPath) {
+ return getFileType(rootedPath) == FileType.EXTERNAL_IMMUTABLE_FILE;
+ }
+
+ public void maybeAddDepOnBuildId(RootedPath rootedPath, SkyFunction.Environment env) {
+ if (getFileType(rootedPath) == FileType.EXTERNAL_MUTABLE_FILE) {
+ // For files outside the package roots that are not assumed to be immutable, add a dependency
+ // on the build_id. This is sufficient for correctness; all other files will be handled by
+ // diff awareness of their respective package path, but these files need to be addressed
+ // separately.
+ //
+ // Using the build_id here seems to introduce a performance concern because the upward
+ // transitive closure of these external files will get eagerly invalidated on each
+ // incremental build (e.g. if every file had a transitive dependency on the filesystem root,
+ // then we'd have a big performance problem). But this a non-issue by design:
+ // - We don't add a dependency on the parent directory at the package root boundary, so the
+ // only transitive dependencies from files inside the package roots to external files are
+ // through symlinks. So the upwards transitive closure of external files is small.
+ // - The only way external source files get into the skyframe graph in the first place is
+ // through symlinks outside the package roots, which we neither want to encourage nor
+ // optimize for since it is not common. So the set of external files is small.
+ //
+ // The above reasoning doesn't hold for bazel, because external repositories
+ // (e.g. new_local_repository) cause lots of external symlinks to be present in the build.
+ // So bazel pretends that these external repositories are immutable to avoid the performance
+ // penalty described above.
+ PrecomputedValue.dependOnBuildId(env);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/FileAndMetadataCache.java b/src/main/java/com/google/devtools/build/lib/skyframe/FileAndMetadataCache.java
new file mode 100644
index 0000000..2a0de78
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/FileAndMetadataCache.java
@@ -0,0 +1,466 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Interner;
+import com.google.common.collect.Interners;
+import com.google.common.collect.Sets;
+import com.google.common.io.BaseEncoding;
+import com.google.devtools.build.lib.actions.ActionInput;
+import com.google.devtools.build.lib.actions.ActionInputFileCache;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.cache.Digest;
+import com.google.devtools.build.lib.actions.cache.DigestUtils;
+import com.google.devtools.build.lib.actions.cache.Metadata;
+import com.google.devtools.build.lib.actions.cache.MetadataHandler;
+import com.google.devtools.build.lib.util.io.TimestampGranularityMonitor;
+import com.google.devtools.build.lib.vfs.FileStatus;
+import com.google.devtools.build.lib.vfs.FileStatusWithDigest;
+import com.google.devtools.build.lib.vfs.FileStatusWithDigestAdapter;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.RootedPath;
+import com.google.devtools.build.lib.vfs.Symlinks;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.protobuf.ByteString;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import javax.annotation.Nullable;
+
+/**
+ * Cache provided by an {@link ActionExecutionFunction}, allowing Blaze to obtain data from the
+ * graph and to inject data (e.g. file digests) back into the graph.
+ *
+ * <p>Data for the action's inputs is injected into this cache on construction, using the graph as
+ * the source of truth.
+ *
+ * <p>As well, this cache collects data about the action's output files, which is used in three
+ * ways. First, it is served as requested during action execution, primarily by the {@code
+ * ActionCacheChecker} when determining if the action must be rerun, and then after the action is
+ * run, to gather information about the outputs. Second, it is accessed by {@link
+ * ArtifactFunction}s in order to construct {@link ArtifactValue}s. Third, the {@link
+ * FilesystemValueChecker} uses it to determine the set of output files to check for inter-build
+ * modifications. Because all these use cases are slightly different, we must occasionally store two
+ * versions of the data for a value (see {@link #getAdditionalOutputData} for more.
+ */
+@VisibleForTesting
+public class FileAndMetadataCache implements ActionInputFileCache, MetadataHandler {
+ /** This should never be read directly. Use {@link #getInputFileArtifactValue} instead. */
+ private final Map<Artifact, FileArtifactValue> inputArtifactData;
+ private final Map<Artifact, Collection<Artifact>> expandedInputMiddlemen;
+ private final File execRoot;
+ private final Map<ByteString, Artifact> reverseMap = new ConcurrentHashMap<>();
+ private final ConcurrentMap<Artifact, FileValue> outputArtifactData =
+ new ConcurrentHashMap<>();
+ // See #getAdditionalOutputData for documentation of this field.
+ private final ConcurrentMap<Artifact, FileArtifactValue> additionalOutputData =
+ new ConcurrentHashMap<>();
+ private final Set<Artifact> injectedArtifacts = Sets.newConcurrentHashSet();
+ private final ImmutableSet<Artifact> outputs;
+ @Nullable private final SkyFunction.Environment env;
+ private final TimestampGranularityMonitor tsgm;
+
+ private static final Interner<ByteString> BYTE_INTERNER = Interners.newWeakInterner();
+
+ public FileAndMetadataCache(Map<Artifact, FileArtifactValue> inputArtifactData,
+ Map<Artifact, Collection<Artifact>> expandedInputMiddlemen, File execRoot,
+ Iterable<Artifact> outputs, @Nullable SkyFunction.Environment env,
+ TimestampGranularityMonitor tsgm) {
+ this.inputArtifactData = Preconditions.checkNotNull(inputArtifactData);
+ this.expandedInputMiddlemen = Preconditions.checkNotNull(expandedInputMiddlemen);
+ this.execRoot = Preconditions.checkNotNull(execRoot);
+ this.outputs = ImmutableSet.copyOf(outputs);
+ this.env = env;
+ this.tsgm = tsgm;
+ }
+
+ @Override
+ public Metadata getMetadataMaybe(Artifact artifact) {
+ try {
+ return getMetadata(artifact);
+ } catch (IOException e) {
+ return null;
+ }
+ }
+
+ private static Metadata metadataFromValue(FileArtifactValue value) throws FileNotFoundException {
+ if (value == FileArtifactValue.MISSING_FILE_MARKER) {
+ throw new FileNotFoundException();
+ }
+ // If the file is empty or a directory, we need to return the mtime because the action cache
+ // uses mtime to determine if this artifact has changed. We do not optimize for this code
+ // path (by storing the mtime somewhere) because we eventually may be switching to use digests
+ // for empty files. We want this code path to go away somehow too for directories (maybe by
+ // implementing FileSet in Skyframe).
+ return value.getSize() > 0
+ ? new Metadata(value.getDigest())
+ : new Metadata(value.getModifiedTime());
+ }
+
+ @Override
+ public Metadata getMetadata(Artifact artifact) throws IOException {
+ Metadata metadata = getRealMetadata(artifact);
+ return artifact.isConstantMetadata() ? Metadata.CONSTANT_METADATA : metadata;
+ }
+
+ @Nullable
+ private FileArtifactValue getInputFileArtifactValue(ActionInput input) {
+ FileArtifactValue value = inputArtifactData.get(input);
+ if (value != null) {
+ return value;
+ }
+ if (outputs.contains(input)) {
+ // When this method is called to calculate the metadata of an artifact, the artifact may be an
+ // output artifact. Don't try to do anything then.
+ return null;
+ }
+ if (!(input instanceof Artifact)) {
+ // Maybe we're being asked for some strange constructed ActionInput coming from runfiles or
+ // similar. We have no information about such things.
+ return null;
+ }
+ // TODO(bazel-team): Remove this codepath once Skyframe has native input discovery, so all
+ // inputs will already have metadata known.
+ // ActionExecutionFunction may have passed in null environment if this action does not
+ // discover inputs. In which case we should not have gotten here.
+ Preconditions.checkNotNull(env, input);
+ Artifact artifact = (Artifact) input;
+ if (artifact.isSourceArtifact()) {
+ // We might have no artifact data for discovered source inputs, and it's not worth storing
+ // it in this cache, because it won't be reused across actions -- while we could request an
+ // artifact from the graph, we would have to be tolerant to it not yet being present in the
+ // graph yet, which adds complexity. Instead, we let the undeclared inputs handler stat it, so
+ // it can be reused.
+ return null;
+ } else {
+ // This getValue call is not expected to return null, because if the artifact is a
+ // transitive dependency of this action (as it should be), it will already have been built,
+ // so this call will return a value.
+ // This getValue call is theoretically less efficient for a subsequent incremental build
+ // than it would be to do a bulk getValues call after action execution, as is done for
+ // source inputs. However, since almost all nodes requested here are transitive deps of an
+ // already-declared dependency, change pruning will request these values serially, but they
+ // will already have been built. So the only penalty is restarting ParallelEvaluator#run, as
+ // opposed to traversing the entire downward transitive closure on a single thread.
+ value = (FileArtifactValue) env.getValue(
+ FileArtifactValue.key(artifact, /*argument ignored for derived artifacts*/false));
+ return value;
+ }
+ }
+
+ /**
+ * We cache data for constant-metadata artifacts, even though it is technically unnecessary,
+ * because the data stored in this cache is consumed by various parts of Blaze via the {@link
+ * ActionExecutionValue} (for now, {@link FilesystemValueChecker} and {@link ArtifactFunction}).
+ * It is simpler for those parts if every output of the action is present in the cache. However,
+ * we must not return the actual metadata for a constant-metadata artifact.
+ */
+ private Metadata getRealMetadata(Artifact artifact) throws IOException {
+ FileArtifactValue value = getInputFileArtifactValue(artifact);
+ if (value != null) {
+ return metadataFromValue(value);
+ }
+ if (artifact.isSourceArtifact()) {
+ // A discovered input we didn't have data for.
+ // TODO(bazel-team): Change this to an assertion once Skyframe has native input discovery, so
+ // all inputs will already have metadata known.
+ return null;
+ } else if (artifact.isMiddlemanArtifact()) {
+ // A middleman artifact's data was either already injected from the action cache checker using
+ // #setDigestForVirtualArtifact, or it has the default middleman value.
+ value = additionalOutputData.get(artifact);
+ if (value != null) {
+ return metadataFromValue(value);
+ }
+ value = FileArtifactValue.DEFAULT_MIDDLEMAN;
+ FileArtifactValue oldValue = additionalOutputData.putIfAbsent(artifact, value);
+ checkInconsistentData(artifact, oldValue, value);
+ return metadataFromValue(value);
+ }
+ FileValue fileValue = outputArtifactData.get(artifact);
+ if (fileValue != null) {
+ // Non-middleman artifacts should only have additionalOutputData if they have
+ // outputArtifactData. We don't assert this because of concurrency possibilities, but at least
+ // we don't check additionalOutputData unless we expect that we might see the artifact there.
+ value = additionalOutputData.get(artifact);
+ // If additional output data is present for this artifact, we use it in preference to the
+ // usual calculation.
+ if (value != null) {
+ return metadataFromValue(value);
+ }
+ if (!fileValue.exists()) {
+ throw new FileNotFoundException(artifact.prettyPrint() + " does not exist");
+ }
+ return new Metadata(Preconditions.checkNotNull(fileValue.getDigest(), artifact));
+ }
+ // We do not cache exceptions besides nonexistence here, because it is unlikely that the file
+ // will be requested from this cache too many times.
+ fileValue = fileValueFromArtifact(artifact, null, tsgm);
+ FileValue oldFileValue = outputArtifactData.putIfAbsent(artifact, fileValue);
+ checkInconsistentData(artifact, oldFileValue, value);
+ return maybeStoreAdditionalData(artifact, fileValue, null);
+ }
+
+ /** Expands one of the input middlemen artifacts of the corresponding action. */
+ public Collection<Artifact> expandInputMiddleman(Artifact middlemanArtifact) {
+ Preconditions.checkState(middlemanArtifact.isMiddlemanArtifact(), middlemanArtifact);
+ Collection<Artifact> result = expandedInputMiddlemen.get(middlemanArtifact);
+ // Note that result may be null for non-aggregating middlemen.
+ return result == null ? ImmutableSet.<Artifact>of() : result;
+ }
+
+ /**
+ * Check that the new {@code data} we just calculated for an {@code artifact} agrees with the
+ * {@code oldData} (presumably calculated concurrently), if it was present.
+ */
+ // Not private only because used by SkyframeActionExecutor's metadata handler.
+ static void checkInconsistentData(Artifact artifact,
+ @Nullable Object oldData, Object data) throws IOException {
+ if (oldData != null && !oldData.equals(data)) {
+ // Another thread checked this file since we looked at the map, and got a different answer
+ // than we did. Presumably the user modified the file between reads.
+ throw new IOException("Data for " + artifact.prettyPrint() + " changed to " + data
+ + " after it was calculated as " + oldData);
+ }
+ }
+
+ /**
+ * See {@link #getAdditionalOutputData} for why we sometimes need to store additional data, even
+ * for normal (non-middleman) artifacts.
+ */
+ @Nullable
+ private Metadata maybeStoreAdditionalData(Artifact artifact, FileValue data,
+ @Nullable byte[] injectedDigest) throws IOException {
+ if (!data.exists()) {
+ // Nonexistent files should only occur before executing an action.
+ throw new FileNotFoundException(artifact.prettyPrint() + " does not exist");
+ }
+ boolean isFile = data.isFile();
+ boolean useDigest = DigestUtils.useFileDigest(artifact, isFile, isFile ? data.getSize() : 0);
+ if (useDigest && data.getDigest() != null) {
+ // We do not need to store the FileArtifactValue separately -- the digest is in the file value
+ // and that is all that is needed for this file's metadata.
+ return new Metadata(data.getDigest());
+ }
+ // Unfortunately, the FileValue does not contain enough information for us to calculate the
+ // corresponding FileArtifactValue -- either the metadata must use the modified time, which we
+ // do not expose in the FileValue, or the FileValue didn't store the digest So we store the
+ // metadata separately.
+ // Use the FileValue's digest if no digest was injected, or if the file can't be digested.
+ injectedDigest = injectedDigest != null || !isFile ? injectedDigest : data.getDigest();
+ FileArtifactValue value =
+ FileArtifactValue.create(artifact, isFile, isFile ? data.getSize() : 0, injectedDigest);
+ FileArtifactValue oldValue = additionalOutputData.putIfAbsent(artifact, value);
+ checkInconsistentData(artifact, oldValue, value);
+ return metadataFromValue(value);
+ }
+
+ @Override
+ public void setDigestForVirtualArtifact(Artifact artifact, Digest digest) {
+ Preconditions.checkState(artifact.isMiddlemanArtifact(), artifact);
+ Preconditions.checkNotNull(digest, artifact);
+ additionalOutputData.put(artifact,
+ FileArtifactValue.createMiddleman(digest.asMetadata().digest));
+ }
+
+ @Override
+ public void injectDigest(ActionInput output, FileStatus statNoFollow, byte[] digest) {
+ if (output instanceof Artifact) {
+ Artifact artifact = (Artifact) output;
+ Preconditions.checkState(injectedArtifacts.add(artifact), artifact);
+ FileValue fileValue;
+ try {
+ // This call may do an unnecessary call to Path#getFastDigest to see if the digest is
+ // readily available. We cannot pass the digest in, though, because if it is not available
+ // from the filesystem, this FileValue will not compare equal to another one created for the
+ // same file, because the other one will be missing its digest.
+ fileValue = fileValueFromArtifact(artifact, FileStatusWithDigestAdapter.adapt(statNoFollow),
+ tsgm);
+ byte[] fileDigest = fileValue.getDigest();
+ Preconditions.checkState(fileDigest == null || Arrays.equals(digest, fileDigest),
+ "%s %s %s", artifact, digest, fileDigest);
+ outputArtifactData.put(artifact, fileValue);
+ } catch (IOException e) {
+ // Do nothing - we just failed to inject metadata. Real error handling will be done later,
+ // when somebody will try to access that file.
+ return;
+ }
+ // If needed, insert additional data. Note that this can only be true if the file is empty or
+ // the filesystem does not support fast digests. Since we usually only inject digests when
+ // running with a filesystem that supports fast digests, this is fairly unlikely.
+ try {
+ maybeStoreAdditionalData(artifact, fileValue, digest);
+ } catch (IOException e) {
+ if (fileValue.getSize() != 0) {
+ // Empty files currently have their mtimes examined, and so could throw. No other files
+ // should throw, since all filesystem access has already been done.
+ throw new IllegalStateException(
+ "Filesystem should not have been accessed while injecting data for "
+ + artifact.prettyPrint(), e);
+ }
+ // Ignore exceptions for empty files, as above.
+ }
+ }
+ }
+
+ @Override
+ public void discardMetadata(Collection<Artifact> artifactList) {
+ Preconditions.checkState(injectedArtifacts.isEmpty(),
+ "Artifacts cannot be injected before action execution: %s", injectedArtifacts);
+ outputArtifactData.keySet().removeAll(artifactList);
+ additionalOutputData.keySet().removeAll(artifactList);
+ }
+
+ @Override
+ public boolean artifactExists(Artifact artifact) {
+ return getMetadataMaybe(artifact) != null;
+ }
+
+ @Override
+ public boolean isRegularFile(Artifact artifact) {
+ // Currently this method is used only for genrule input directory checks. If we need to call
+ // this on output artifacts too, this could be more efficient.
+ FileArtifactValue value = getInputFileArtifactValue(artifact);
+ if (value != null && value.getDigest() != null) {
+ return true;
+ }
+ return artifact.getPath().isFile();
+ }
+
+ @Override
+ public boolean isInjected(Artifact artifact) {
+ return injectedArtifacts.contains(artifact);
+ }
+
+ /**
+ * @return data for output files that was computed during execution. Should include data for all
+ * non-middleman artifacts.
+ */
+ Map<Artifact, FileValue> getOutputData() {
+ return outputArtifactData;
+ }
+
+ /**
+ * Returns data for any output files whose metadata was not computable from the corresponding
+ * entry in {@link #getOutputData}.
+ *
+ * <p>There are three reasons why we might not be able to compute metadata for an artifact from
+ * the FileValue. First, middleman artifacts have no corresponding FileValues. Second, if
+ * computing a file's digest is not fast, the FileValue does not do so, so a file on a filesystem
+ * without fast digests has to have its metadata stored separately. Third, some files' metadata
+ * (directories, empty files) contain their mtimes, which the FileValue does not expose, so that
+ * has to be stored separately.
+ *
+ * <p>Note that for files that need digests, we can't easily inject the digest in the FileValue
+ * because it would complicate equality-checking on subsequent builds -- if our filesystem doesn't
+ * do fast digests, the comparison value would not have a digest.
+ */
+ Map<Artifact, FileArtifactValue> getAdditionalOutputData() {
+ return additionalOutputData;
+ }
+
+ @Override
+ public long getSizeInBytes(ActionInput input) throws IOException {
+ FileArtifactValue metadata = getInputFileArtifactValue(input);
+ if (metadata != null) {
+ return metadata.getSize();
+ }
+ return -1;
+ }
+
+ @Nullable
+ @Override
+ public File getFileFromDigest(ByteString digest) throws IOException {
+ Artifact artifact = reverseMap.get(digest);
+ if (artifact != null) {
+ String relPath = artifact.getExecPathString();
+ return relPath.startsWith("/") ? new File(relPath) : new File(execRoot, relPath);
+ }
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public ByteString getDigest(ActionInput input) throws IOException {
+ FileArtifactValue value = getInputFileArtifactValue(input);
+ if (value != null) {
+ byte[] bytes = value.getDigest();
+ if (bytes != null) {
+ ByteString digest = ByteString.copyFrom(BaseEncoding.base16().lowerCase().encode(bytes)
+ .getBytes(StandardCharsets.US_ASCII));
+ reverseMap.put(BYTE_INTERNER.intern(digest), (Artifact) input);
+ return digest;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public boolean contentsAvailableLocally(ByteString digest) {
+ return reverseMap.containsKey(digest);
+ }
+
+ static FileValue fileValueFromArtifact(Artifact artifact,
+ @Nullable FileStatusWithDigest statNoFollow, TimestampGranularityMonitor tsgm)
+ throws IOException {
+ Path path = artifact.getPath();
+ RootedPath rootedPath =
+ RootedPath.toRootedPath(artifact.getRoot().getPath(), artifact.getRootRelativePath());
+ if (statNoFollow == null) {
+ statNoFollow = FileStatusWithDigestAdapter.adapt(path.statIfFound(Symlinks.NOFOLLOW));
+ if (statNoFollow == null) {
+ return FileValue.value(rootedPath, FileStateValue.NONEXISTENT_FILE_STATE_NODE,
+ rootedPath, FileStateValue.NONEXISTENT_FILE_STATE_NODE);
+ }
+ }
+ Path realPath = path;
+ // We use FileStatus#isSymbolicLink over Path#isSymbolicLink to avoid the unnecessary stat
+ // done by the latter.
+ if (statNoFollow.isSymbolicLink()) {
+ realPath = path.resolveSymbolicLinks();
+ // We need to protect against symlink cycles since FileValue#value assumes it's dealing with a
+ // file that's not in a symlink cycle.
+ if (realPath.equals(path)) {
+ throw new IOException("symlink cycle");
+ }
+ }
+ RootedPath realRootedPath = RootedPath.toRootedPathMaybeUnderRoot(realPath,
+ ImmutableList.of(artifact.getRoot().getPath()));
+ FileStateValue fileStateValue;
+ FileStateValue realFileStateValue;
+ try {
+ fileStateValue = FileStateValue.createWithStatNoFollow(rootedPath, statNoFollow, tsgm);
+ // TODO(bazel-team): consider avoiding a 'stat' here when the symlink target hasn't changed
+ // and is a source file (since changes to those are checked separately).
+ realFileStateValue = realPath.equals(path) ? fileStateValue
+ : FileStateValue.create(realRootedPath, tsgm);
+ } catch (InconsistentFilesystemException e) {
+ throw new IOException(e);
+ }
+ return FileValue.value(rootedPath, fileStateValue, realRootedPath, realFileStateValue);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/FileArtifactValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/FileArtifactValue.java
new file mode 100644
index 0000000..7acc38c
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/FileArtifactValue.java
@@ -0,0 +1,148 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.cache.DigestUtils;
+import com.google.devtools.build.lib.vfs.FileStatus;
+import com.google.devtools.build.lib.vfs.Path;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+import javax.annotation.Nullable;
+
+/**
+ * Stores the data of an artifact corresponding to a file. This file may be an ordinary file, in
+ * which case we would expect to see a digest and size; a directory, in which case we would expect
+ * to see an mtime; or an empty file, where we would expect to see a size (=0), mtime, and digest
+ */
+public class FileArtifactValue extends ArtifactValue {
+ /** Data for Middleman artifacts that did not have data specified. */
+ static final FileArtifactValue DEFAULT_MIDDLEMAN = new FileArtifactValue(null, 0, 0);
+ /** Data that marks that a file is not present on the filesystem. */
+ static final FileArtifactValue MISSING_FILE_MARKER = new FileArtifactValue(null, 1, 0);
+
+ @Nullable private final byte[] digest;
+ private final long mtime;
+ private final long size;
+
+ private FileArtifactValue(byte[] digest, long size) {
+ Preconditions.checkState(size >= 0, "size must be non-negative: %s %s", digest, size);
+ this.digest = Preconditions.checkNotNull(digest, size);
+ this.size = size;
+ this.mtime = -1;
+ }
+
+ // Only used by empty files (non-null digest) and directories (null digest).
+ private FileArtifactValue(byte[] digest, long mtime, long size) {
+ Preconditions.checkState(mtime >= 0, "mtime must be non-negative: %s %s", mtime, size);
+ Preconditions.checkState(size == 0, "size must be zero: %s %s", mtime, size);
+ this.digest = digest;
+ this.size = size;
+ this.mtime = mtime;
+ }
+
+ static FileArtifactValue create(Artifact artifact) throws IOException {
+ Path path = artifact.getPath();
+ FileStatus stat = path.stat();
+ boolean isFile = stat.isFile();
+ return create(artifact, isFile, isFile ? stat.getSize() : 0, null);
+ }
+
+ static FileArtifactValue create(Artifact artifact, FileValue fileValue) throws IOException {
+ boolean isFile = fileValue.isFile();
+ return create(artifact, isFile, isFile ? fileValue.getSize() : 0,
+ isFile ? fileValue.getDigest() : null);
+ }
+
+ static FileArtifactValue create(Artifact artifact, boolean isFile, long size,
+ @Nullable byte[] digest) throws IOException {
+ if (isFile && digest == null) {
+ digest = DigestUtils.getDigestOrFail(artifact.getPath(), size);
+ }
+ if (!DigestUtils.useFileDigest(artifact, isFile, size)) {
+ // In this case, we need to store the mtime because the action cache uses mtime to determine
+ // if this artifact has changed. This is currently true for empty files and directories. We
+ // do not optimize for this code path (by storing the mtime in a FileValue) because we do not
+ // like it and may remove this special-casing for empty files in the future. We want this code
+ // path to go away somehow too for directories (maybe by implementing FileSet
+ // in Skyframe)
+ return new FileArtifactValue(digest, artifact.getPath().getLastModifiedTime(), size);
+ }
+ Preconditions.checkState(digest != null, artifact);
+ return new FileArtifactValue(digest, size);
+ }
+
+ static FileArtifactValue createMiddleman(byte[] digest) {
+ Preconditions.checkNotNull(digest);
+ // The Middleman artifact values have size 1 because we want their digests to be used. This hack
+ // can be removed once empty files are digested.
+ return new FileArtifactValue(digest, /*size=*/1);
+ }
+
+ @Nullable
+ byte[] getDigest() {
+ return digest;
+ }
+
+ /** Gets the size of the file. Directories have size 0. */
+ long getSize() {
+ return size;
+ }
+
+ /**
+ * Gets last modified time of file. Should only be called if {@link DigestUtils#useFileDigest} was
+ * false for this artifact -- namely, either it is a directory or an empty file. Note that since
+ * we store directory sizes as 0, all files for which this method can be called have size 0.
+ */
+ long getModifiedTime() {
+ Preconditions.checkState(size == 0, "%s %s %s", digest, mtime, size);
+ return mtime;
+ }
+
+ @Override
+ public int hashCode() {
+ // Hash digest by content, not reference. Note that digest is the only array in this array.
+ return Arrays.deepHashCode(new Object[] {size, mtime, digest});
+ }
+
+ /**
+ * Two FileArtifactValues will only compare equal if they have the same content. This differs
+ * from the {@code Metadata#equivalence} method, which allows for comparison using mtime if
+ * one object does not have a digest available.
+ */
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof FileArtifactValue)) {
+ return false;
+ }
+ FileArtifactValue that = (FileArtifactValue) other;
+ return this.mtime == that.mtime && this.size == that.size
+ && Arrays.equals(this.digest, that.digest);
+ }
+
+ @Override
+ public String toString() {
+ return Objects.toStringHelper(FileArtifactValue.class)
+ .add("digest", digest)
+ .add("mtime", mtime)
+ .add("size", size).toString();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/FileContentsProxy.java b/src/main/java/com/google/devtools/build/lib/skyframe/FileContentsProxy.java
new file mode 100644
index 0000000..344c364
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/FileContentsProxy.java
@@ -0,0 +1,66 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import java.io.Serializable;
+import java.util.Objects;
+
+/**
+ * In case we can't get a fast digest from the filesystem, we store this metadata as a proxy to
+ * the file contents. Currently it is a pair of the mtime and "value id" (which is right now just
+ * the ivalue number). We may wish to augment this object with the following data:
+ * a. the device number
+ * b. the ctime, which cannot be tampered with in userspace
+ *
+ * <p>For an example of why mtime alone is insufficient, note that 'mv' preserves timestamps. So if
+ * files 'a' and 'b' initially have the same timestamp, then we would think 'b' is unchanged after
+ * the user executes `mv a b` between two builds.
+ */
+public final class FileContentsProxy implements Serializable {
+ private final long mtime;
+ private final long valueId;
+
+ private FileContentsProxy(long mtime, long valueId) {
+ this.mtime = mtime;
+ this.valueId = valueId;
+ }
+
+ public static FileContentsProxy create(long mtime, long valueId) {
+ return new FileContentsProxy(mtime, valueId);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+
+ if (!(other instanceof FileContentsProxy)) {
+ return false;
+ }
+
+ FileContentsProxy that = (FileContentsProxy) other;
+ return mtime == that.mtime && valueId == that.valueId;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mtime, valueId);
+ }
+
+ @Override
+ public String toString() {
+ return "mtime: " + mtime + " valueId: " + valueId;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/FileFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/FileFunction.java
new file mode 100644
index 0000000..ad3cb74
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/FileFunction.java
@@ -0,0 +1,217 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Sets;
+import com.google.devtools.build.lib.pkgcache.PathPackageLocator;
+import com.google.devtools.build.lib.util.Pair;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.lib.vfs.RootedPath;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyFunctionException;
+import com.google.devtools.build.skyframe.SkyFunctionException.Transience;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.util.LinkedHashSet;
+import java.util.concurrent.atomic.AtomicReference;
+
+import javax.annotation.Nullable;
+
+/**
+ * A {@link SkyFunction} for {@link FileValue}s.
+ *
+ * <p>Most of the complexity in the implementation is associated to handling symlinks. Namely,
+ * this class makes sure that {@code FileValue}s corresponding to symlinks are correctly invalidated
+ * if the destination of the symlink is invalidated. Directory symlinks are also covered.
+ */
+public class FileFunction implements SkyFunction {
+
+ private final AtomicReference<PathPackageLocator> pkgLocator;
+ private final ExternalFilesHelper externalFilesHelper;
+
+ public FileFunction(AtomicReference<PathPackageLocator> pkgLocator,
+ ExternalFilesHelper externalFilesHelper) {
+ this.pkgLocator = pkgLocator;
+ this.externalFilesHelper = externalFilesHelper;
+ }
+
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env) throws FileFunctionException {
+ RootedPath rootedPath = (RootedPath) skyKey.argument();
+ RootedPath realRootedPath = rootedPath;
+ FileStateValue realFileStateValue = null;
+ PathFragment relativePath = rootedPath.getRelativePath();
+
+ // Resolve ancestor symlinks, but only if the current file is not the filesystem root (has no
+ // parent) or a package path root (treated opaquely and handled by skyframe's DiffAwareness
+ // interface) or otherwise assumed to be immutable (handling ancestors would add dependencies
+ // too aggressively). Note that this is the first thing we do - if an ancestor is part of a
+ // symlink cycle, we want to detect that quickly as it gives a more informative error message
+ // than we'd get doing bogus filesystem operations.
+ if (!relativePath.equals(PathFragment.EMPTY_FRAGMENT)
+ && !externalFilesHelper.shouldAssumeImmutable(rootedPath)) {
+ Pair<RootedPath, FileStateValue> resolvedState =
+ resolveFromAncestors(rootedPath, env);
+ if (resolvedState == null) {
+ return null;
+ }
+ realRootedPath = resolvedState.getFirst();
+ realFileStateValue = resolvedState.getSecond();
+ }
+
+ FileStateValue fileStateValue = (FileStateValue) env.getValue(FileStateValue.key(rootedPath));
+ if (fileStateValue == null) {
+ return null;
+ }
+ if (realFileStateValue == null) {
+ realFileStateValue = fileStateValue;
+ }
+
+ LinkedHashSet<RootedPath> seenPaths = Sets.newLinkedHashSet();
+ while (realFileStateValue.getType().equals(FileStateValue.Type.SYMLINK)) {
+ if (!seenPaths.add(realRootedPath)) {
+ FileSymlinkCycleException fileSymlinkCycleException =
+ makeFileSymlinkCycleException(realRootedPath, seenPaths);
+ if (env.getValue(FileSymlinkCycleUniquenessValue.key(fileSymlinkCycleException.getCycle()))
+ == null) {
+ // Note that this dependency is merely to ensure that each unique cycle gets reported
+ // exactly once.
+ return null;
+ }
+ throw new FileFunctionException(fileSymlinkCycleException);
+ }
+ Pair<RootedPath, FileStateValue> resolvedState = getSymlinkTargetRootedPath(realRootedPath,
+ realFileStateValue.getSymlinkTarget(), env);
+ if (resolvedState == null) {
+ return null;
+ }
+ realRootedPath = resolvedState.getFirst();
+ realFileStateValue = resolvedState.getSecond();
+ }
+ return FileValue.value(rootedPath, fileStateValue, realRootedPath, realFileStateValue);
+ }
+
+ /**
+ * Returns the path and file state of {@code rootedPath}, accounting for ancestor symlinks, or
+ * {@code null} if there was a missing dep.
+ */
+ @Nullable
+ private Pair<RootedPath, FileStateValue> resolveFromAncestors(RootedPath rootedPath,
+ Environment env) throws FileFunctionException {
+ PathFragment relativePath = rootedPath.getRelativePath();
+ RootedPath realRootedPath = rootedPath;
+ FileValue parentFileValue = null;
+ if (!relativePath.equals(PathFragment.EMPTY_FRAGMENT)) {
+ RootedPath parentRootedPath = RootedPath.toRootedPath(rootedPath.getRoot(),
+ relativePath.getParentDirectory());
+ parentFileValue = (FileValue) env.getValue(FileValue.key(parentRootedPath));
+ if (parentFileValue == null) {
+ return null;
+ }
+ PathFragment baseName = new PathFragment(relativePath.getBaseName());
+ RootedPath parentRealRootedPath = parentFileValue.realRootedPath();
+ realRootedPath = RootedPath.toRootedPath(parentRealRootedPath.getRoot(),
+ parentRealRootedPath.getRelativePath().getRelative(baseName));
+ }
+ FileStateValue realFileStateValue =
+ (FileStateValue) env.getValue(FileStateValue.key(realRootedPath));
+ if (realFileStateValue == null) {
+ return null;
+ }
+ if (realFileStateValue.getType() != FileStateValue.Type.NONEXISTENT
+ && parentFileValue != null && !parentFileValue.isDirectory()) {
+ String type = realFileStateValue.getType().toString().toLowerCase();
+ String message = type + " " + rootedPath.asPath() + " exists but its parent "
+ + "directory " + parentFileValue.realRootedPath().asPath() + " doesn't exist.";
+ throw new FileFunctionException(new InconsistentFilesystemException(message),
+ Transience.TRANSIENT);
+ }
+ return Pair.of(realRootedPath, realFileStateValue);
+ }
+
+ /**
+ * Returns the symlink target and file state of {@code rootedPath}'s symlink to
+ * {@code symlinkTarget}, accounting for ancestor symlinks, or {@code null} if there was a
+ * missing dep.
+ */
+ @Nullable
+ private Pair<RootedPath, FileStateValue> getSymlinkTargetRootedPath(RootedPath rootedPath,
+ PathFragment symlinkTarget, Environment env) throws FileFunctionException {
+ if (symlinkTarget.isAbsolute()) {
+ Path path = rootedPath.asPath().getFileSystem().getRootDirectory().getRelative(
+ symlinkTarget);
+ return resolveFromAncestors(
+ RootedPath.toRootedPathMaybeUnderRoot(path, pkgLocator.get().getPathEntries()), env);
+ }
+ Path path = rootedPath.asPath();
+ Path symlinkTargetPath;
+ if (path.getParentDirectory() != null) {
+ RootedPath parentRootedPath = RootedPath.toRootedPathMaybeUnderRoot(
+ path.getParentDirectory(), pkgLocator.get().getPathEntries());
+ FileValue parentFileValue = (FileValue) env.getValue(FileValue.key(parentRootedPath));
+ if (parentFileValue == null) {
+ return null;
+ }
+ symlinkTargetPath = parentFileValue.realRootedPath().asPath().getRelative(symlinkTarget);
+ } else {
+ // This means '/' is a symlink to 'symlinkTarget'.
+ symlinkTargetPath = path.getRelative(symlinkTarget);
+ }
+ RootedPath symlinkTargetRootedPath = RootedPath.toRootedPathMaybeUnderRoot(symlinkTargetPath,
+ pkgLocator.get().getPathEntries());
+ return resolveFromAncestors(symlinkTargetRootedPath, env);
+ }
+
+ private FileSymlinkCycleException makeFileSymlinkCycleException(RootedPath startOfCycle,
+ Iterable<RootedPath> symlinkPaths) {
+ boolean inPathToCycle = true;
+ ImmutableList.Builder<RootedPath> pathToCycleBuilder = ImmutableList.builder();
+ ImmutableList.Builder<RootedPath> cycleBuilder = ImmutableList.builder();
+ for (RootedPath path : symlinkPaths) {
+ if (path.equals(startOfCycle)) {
+ inPathToCycle = false;
+ }
+ if (inPathToCycle) {
+ pathToCycleBuilder.add(path);
+ } else {
+ cycleBuilder.add(path);
+ }
+ }
+ return new FileSymlinkCycleException(pathToCycleBuilder.build(), cycleBuilder.build());
+ }
+
+ @Nullable
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return null;
+ }
+
+ /**
+ * Used to declare all the exception types that can be wrapped in the exception thrown by
+ * {@link FileFunction#compute}.
+ */
+ private static final class FileFunctionException extends SkyFunctionException {
+
+ public FileFunctionException(InconsistentFilesystemException e, Transience transience) {
+ super(e, transience);
+ }
+
+ public FileFunctionException(FileSymlinkCycleException e) {
+ super(e, Transience.PERSISTENT);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/FileStateFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/FileStateFunction.java
new file mode 100644
index 0000000..ec2e871
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/FileStateFunction.java
@@ -0,0 +1,76 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.devtools.build.lib.util.io.TimestampGranularityMonitor;
+import com.google.devtools.build.lib.vfs.RootedPath;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyFunctionException;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.io.IOException;
+
+/**
+ * A {@link SkyFunction} for {@link FileStateValue}s.
+ *
+ * <p>Merely calls FileStateValue#create, but also has special handling for files outside the
+ * package roots (see {@link ExternalFilesHelper}).
+ */
+public class FileStateFunction implements SkyFunction {
+
+ private final TimestampGranularityMonitor tsgm;
+ private final ExternalFilesHelper externalFilesHelper;
+
+ public FileStateFunction(TimestampGranularityMonitor tsgm,
+ ExternalFilesHelper externalFilesHelper) {
+ this.tsgm = tsgm;
+ this.externalFilesHelper = externalFilesHelper;
+ }
+
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env) throws FileStateFunctionException {
+ RootedPath rootedPath = (RootedPath) skyKey.argument();
+ externalFilesHelper.maybeAddDepOnBuildId(rootedPath, env);
+ if (env.valuesMissing()) {
+ return null;
+ }
+ try {
+ return FileStateValue.create(rootedPath, tsgm);
+ } catch (IOException e) {
+ throw new FileStateFunctionException(e);
+ } catch (InconsistentFilesystemException e) {
+ throw new FileStateFunctionException(e);
+ }
+ }
+
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return null;
+ }
+
+ /**
+ * Used to declare all the exception types that can be wrapped in the exception thrown by
+ * {@link FileStateFunction#compute}.
+ */
+ private static final class FileStateFunctionException extends SkyFunctionException {
+ public FileStateFunctionException(IOException e) {
+ super(e, Transience.TRANSIENT);
+ }
+
+ public FileStateFunctionException(InconsistentFilesystemException e) {
+ super(e, Transience.TRANSIENT);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/FileStateValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/FileStateValue.java
new file mode 100644
index 0000000..8631ff3
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/FileStateValue.java
@@ -0,0 +1,317 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.util.io.TimestampGranularityMonitor;
+import com.google.devtools.build.lib.vfs.FileStatus;
+import com.google.devtools.build.lib.vfs.FileStatusWithDigest;
+import com.google.devtools.build.lib.vfs.FileStatusWithDigestAdapter;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.lib.vfs.RootedPath;
+import com.google.devtools.build.lib.vfs.Symlinks;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Objects;
+
+import javax.annotation.Nullable;
+
+/**
+ * Encapsulates the filesystem operations needed to get state for a path. This is at least a
+ * 'lstat' to determine what type of file the path is.
+ * <ul>
+ * <li> For a non-existent file, the non existence is noted.
+ * <li> For a symlink, the symlink target is noted.
+ * <li> For a directory, the existence is noted.
+ * <li> For a file, the existence is noted, along with metadata about the file (e.g.
+ * file digest). See {@link FileFileStateValue}.
+ * <ul>
+ *
+ * <p>This class is an implementation detail of {@link FileValue} and should not be used outside of
+ * {@link FileFunction}. Instead, {@link FileValue} should be used by consumers that care about
+ * files.
+ *
+ * <p>All subclasses must implement {@link #equals} and {@link #hashCode} properly.
+ */
+abstract class FileStateValue implements SkyValue {
+
+ public static final FileStateValue DIRECTORY_FILE_STATE_NODE = DirectoryFileStateValue.INSTANCE;
+ public static final FileStateValue NONEXISTENT_FILE_STATE_NODE =
+ NonexistentFileStateValue.INSTANCE;
+
+ public enum Type {
+ FILE,
+ DIRECTORY,
+ SYMLINK,
+ NONEXISTENT,
+ }
+
+ protected FileStateValue() {
+ }
+
+ public static FileStateValue create(RootedPath rootedPath,
+ @Nullable TimestampGranularityMonitor tsgm) throws InconsistentFilesystemException,
+ IOException {
+ Path path = rootedPath.asPath();
+ // Stat, but don't throw an exception for the common case of a nonexistent file. This still
+ // throws an IOException in case any other IO error is encountered.
+ FileStatus stat = path.statIfFound(Symlinks.NOFOLLOW);
+ if (stat == null) {
+ return NONEXISTENT_FILE_STATE_NODE;
+ }
+ return createWithStatNoFollow(rootedPath, FileStatusWithDigestAdapter.adapt(stat), tsgm);
+ }
+
+ public static FileStateValue createWithStatNoFollow(RootedPath rootedPath,
+ FileStatusWithDigest statNoFollow, @Nullable TimestampGranularityMonitor tsgm)
+ throws InconsistentFilesystemException, IOException {
+ Path path = rootedPath.asPath();
+ if (statNoFollow.isFile()) {
+ return FileFileStateValue.fromPath(path, statNoFollow, tsgm);
+ } else if (statNoFollow.isDirectory()) {
+ return DIRECTORY_FILE_STATE_NODE;
+ } else if (statNoFollow.isSymbolicLink()) {
+ return new SymlinkFileStateValue(path.readSymbolicLink());
+ }
+ throw new InconsistentFilesystemException("according to stat, existing path " + path + " is "
+ + "neither a file nor directory nor symlink.");
+ }
+
+ @ThreadSafe
+ static SkyKey key(RootedPath rootedPath) {
+ return new SkyKey(SkyFunctions.FILE_STATE, rootedPath);
+ }
+
+ abstract Type getType();
+
+ PathFragment getSymlinkTarget() {
+ throw new IllegalStateException();
+ }
+
+ long getSize() {
+ throw new IllegalStateException();
+ }
+
+ @Nullable
+ byte[] getDigest() {
+ throw new IllegalStateException();
+ }
+
+ /**
+ * Implementation of {@link FileStateValue} for files that exist.
+ *
+ * <p>A union of (digest, mtime). We use digests only if a fast digest lookup is available from
+ * the filesystem. If not, we fall back to mtime-based digests. This avoids the case where Blaze
+ * must read all files involved in the build in order to check for modifications in the case
+ * where fast digest lookups are not available.
+ */
+ @ThreadSafe
+ private static final class FileFileStateValue extends FileStateValue {
+ private final long size;
+ // Only needed for empty-file equality-checking. Otherwise is always -1.
+ // TODO(bazel-team): Consider getting rid of this special case for empty files.
+ private final long mtime;
+ @Nullable private final byte[] digest;
+ @Nullable private final FileContentsProxy contentsProxy;
+
+ private FileFileStateValue(long size, long mtime, byte[] digest,
+ FileContentsProxy contentsProxy) {
+ Preconditions.checkState((digest == null) != (contentsProxy == null));
+ this.size = size;
+ // mtime is forced to be -1 so that we do not accidentally depend on it for non-empty files,
+ // which should only be compared using digests.
+ this.mtime = size == 0 ? mtime : -1;
+ this.digest = digest;
+ this.contentsProxy = contentsProxy;
+ }
+
+ /**
+ * Create a FileFileStateValue instance corresponding to the given existing file.
+ * @param stat must be of type "File". (Not a symlink).
+ */
+ public static FileFileStateValue fromPath(Path path, FileStatusWithDigest stat,
+ @Nullable TimestampGranularityMonitor tsgm)
+ throws InconsistentFilesystemException {
+ Preconditions.checkState(stat.isFile(), path);
+ try {
+ byte[] digest = stat.getDigest();
+ if (digest == null) {
+ digest = path.getFastDigest();
+ }
+ if (digest == null) {
+ long mtime = stat.getLastModifiedTime();
+ // Note that TimestampGranularityMonitor#notifyDependenceOnFileTime is a thread-safe
+ // method.
+ if (tsgm != null) {
+ tsgm.notifyDependenceOnFileTime(mtime);
+ }
+ return new FileFileStateValue(stat.getSize(), stat.getLastModifiedTime(), null,
+ FileContentsProxy.create(mtime, stat.getNodeId()));
+ } else {
+ // We are careful here to avoid putting the value ID into FileMetadata if we already have
+ // a digest. Arbitrary filesystems may do weird things with the value ID; a digest is more
+ // robust.
+ return new FileFileStateValue(stat.getSize(), stat.getLastModifiedTime(), digest, null);
+ }
+ } catch (IOException e) {
+ String errorMessage = e.getMessage() != null
+ ? "error '" + e.getMessage() + "'" : "an error";
+ throw new InconsistentFilesystemException("'stat' said " + path + " is a file but then we "
+ + "later encountered " + errorMessage + " which indicates that " + path + " no longer "
+ + "exists. Did you delete it during the build?");
+ }
+ }
+
+ @Override
+ public Type getType() {
+ return Type.FILE;
+ }
+
+ @Override
+ public long getSize() {
+ return size;
+ }
+
+ @Override
+ @Nullable
+ public byte[] getDigest() {
+ return digest;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof FileFileStateValue) {
+ FileFileStateValue other = (FileFileStateValue) obj;
+ return size == other.size && mtime == other.mtime && Arrays.equals(digest, other.digest)
+ && Objects.equals(contentsProxy, other.contentsProxy);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(size, mtime, Arrays.hashCode(digest), contentsProxy);
+ }
+
+ @Override
+ public String toString() {
+ return "[size: " + size + " " + (mtime != -1 ? "mtime: " + mtime : "")
+ + (digest != null ? "digest: " + Arrays.toString(digest) : contentsProxy) + "]";
+ }
+ }
+
+ /** Implementation of {@link FileStateValue} for directories that exist. */
+ private static final class DirectoryFileStateValue extends FileStateValue {
+
+ public static final DirectoryFileStateValue INSTANCE = new DirectoryFileStateValue();
+
+ private DirectoryFileStateValue() {
+ }
+
+ @Override
+ public Type getType() {
+ return Type.DIRECTORY;
+ }
+
+ @Override
+ public String toString() {
+ return "directory";
+ }
+
+ // This object is normally a singleton, but deserialization produces copies.
+ @Override
+ public boolean equals(Object obj) {
+ return obj instanceof DirectoryFileStateValue;
+ }
+
+ @Override
+ public int hashCode() {
+ return 7654321;
+ }
+ }
+
+ /** Implementation of {@link FileStateValue} for symlinks. */
+ private static final class SymlinkFileStateValue extends FileStateValue {
+
+ private final PathFragment symlinkTarget;
+
+ private SymlinkFileStateValue(PathFragment symlinkTarget) {
+ this.symlinkTarget = symlinkTarget;
+ }
+
+ @Override
+ public Type getType() {
+ return Type.SYMLINK;
+ }
+
+ @Override
+ public PathFragment getSymlinkTarget() {
+ return symlinkTarget;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof SymlinkFileStateValue)) {
+ return false;
+ }
+ SymlinkFileStateValue other = (SymlinkFileStateValue) obj;
+ return symlinkTarget.equals(other.symlinkTarget);
+ }
+
+ @Override
+ public int hashCode() {
+ return symlinkTarget.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return "symlink to " + symlinkTarget;
+ }
+ }
+
+ /** Implementation of {@link FileStateValue} for nonexistent files. */
+ private static final class NonexistentFileStateValue extends FileStateValue {
+
+ public static final NonexistentFileStateValue INSTANCE = new NonexistentFileStateValue();
+
+ private NonexistentFileStateValue() {
+ }
+
+ @Override
+ public Type getType() {
+ return Type.NONEXISTENT;
+ }
+
+ @Override
+ public String toString() {
+ return "nonexistent";
+ }
+
+ // This object is normally a singleton, but deserialization produces copies.
+ @Override
+ public boolean equals(Object obj) {
+ return obj instanceof NonexistentFileStateValue;
+ }
+
+ @Override
+ public int hashCode() {
+ return 8765432;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/FileSymlinkCycleException.java b/src/main/java/com/google/devtools/build/lib/skyframe/FileSymlinkCycleException.java
new file mode 100644
index 0000000..d57fc42
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/FileSymlinkCycleException.java
@@ -0,0 +1,49 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.vfs.RootedPath;
+
+/** Exception indicating that a cycle was found in the filesystem. */
+public class FileSymlinkCycleException extends Exception {
+
+ private final ImmutableList<RootedPath> pathToCycle;
+ private final ImmutableList<RootedPath> cycle;
+
+ FileSymlinkCycleException(ImmutableList<RootedPath> pathToCycle,
+ ImmutableList<RootedPath> cycle) {
+ // The cycle itself has already been reported by FileSymlinkCycleUniquenessValue, but we still
+ // want to have a readable #getMessage.
+ super("Symlink cycle");
+ this.pathToCycle = pathToCycle;
+ this.cycle = cycle;
+ }
+
+ /**
+ * The symlink path to the symlink cycle. For example, suppose 'a' -> 'b' -> 'c' -> 'd' -> 'c'.
+ * The path to the cycle is 'a', 'b'.
+ */
+ ImmutableList<RootedPath> getPathToCycle() {
+ return pathToCycle;
+ }
+
+ /**
+ * The symlink cycle. For example, suppose 'a' -> 'b' -> 'c' -> 'd' -> 'c'.
+ * The cycle is 'c', 'd'.
+ */
+ ImmutableList<RootedPath> getCycle() {
+ return cycle;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/FileSymlinkCycleUniquenessFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/FileSymlinkCycleUniquenessFunction.java
new file mode 100644
index 0000000..a0604b5
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/FileSymlinkCycleUniquenessFunction.java
@@ -0,0 +1,45 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.vfs.RootedPath;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+/** A value builder that has the side effect of reporting a file symlink cycle. */
+public class FileSymlinkCycleUniquenessFunction implements SkyFunction {
+
+ @SuppressWarnings("unchecked") // Cast from Object to ImmutableList<RootedPath>.
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env) {
+ StringBuilder cycleMessage = new StringBuilder("circular symlinks detected\n");
+ cycleMessage.append("[start of symlink cycle]\n");
+ for (RootedPath rootedPath : (ImmutableList<RootedPath>) skyKey.argument()) {
+ cycleMessage.append(rootedPath.asPath() + "\n");
+ }
+ cycleMessage.append("[end of symlink cycle]");
+ // The purpose of this value builder is the side effect of emitting an error message exactly
+ // once per build per unique cycle.
+ env.getListener().handle(Event.error(cycleMessage.toString()));
+ return FileSymlinkCycleUniquenessValue.INSTANCE;
+ }
+
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return null;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/FileSymlinkCycleUniquenessValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/FileSymlinkCycleUniquenessValue.java
new file mode 100644
index 0000000..627276d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/FileSymlinkCycleUniquenessValue.java
@@ -0,0 +1,57 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.vfs.RootedPath;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+/**
+ * A value for ensuring that a file symlink cycle is reported exactly once. This is achieved by
+ * forcing the same value key for two logically equivalent cycles (e.g. ['a' -> 'b' -> 'c' -> 'a']
+ * and ['b' -> 'c' -> 'a' -> 'b'], and letting Skyframe do its magic.
+ */
+class FileSymlinkCycleUniquenessValue implements SkyValue {
+
+ public static final FileSymlinkCycleUniquenessValue INSTANCE =
+ new FileSymlinkCycleUniquenessValue();
+
+ private FileSymlinkCycleUniquenessValue() {
+ }
+
+ static SkyKey key(ImmutableList<RootedPath> cycle) {
+ Preconditions.checkState(!cycle.isEmpty());
+ return new SkyKey(SkyFunctions.FILE_SYMLINK_CYCLE_UNIQUENESS, canonicalize(cycle));
+ }
+
+ private static ImmutableList<RootedPath> canonicalize(ImmutableList<RootedPath> cycle) {
+ int minPos = 0;
+ String minString = cycle.get(0).toString();
+ for (int i = 1; i < cycle.size(); i++) {
+ String candidateString = cycle.get(i).toString();
+ if (candidateString.compareTo(minString) < 0) {
+ minPos = i;
+ minString = candidateString;
+ }
+ }
+ ImmutableList.Builder<RootedPath> builder = ImmutableList.builder();
+ for (int i = 0; i < cycle.size(); i++) {
+ int pos = (minPos + i) % cycle.size();
+ builder.add(cycle.get(pos));
+ }
+ return builder.build();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/FileValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/FileValue.java
new file mode 100644
index 0000000..1850fd9
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/FileValue.java
@@ -0,0 +1,279 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.skyframe.FileStateValue.Type;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.lib.vfs.RootedPath;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.util.Objects;
+
+import javax.annotation.Nullable;
+
+/**
+ * A value that corresponds to a file (or directory or symlink or non-existent file), fully
+ * accounting for symlinks (e.g. proper dependencies on ancestor symlinks so as to be incrementally
+ * correct). Anything in Skyframe that cares about the fully resolved path of a file (e.g. anything
+ * that cares about the contents of a file) should have a dependency on the corresponding
+ * {@link FileValue}.
+ *
+ * <p>
+ * Note that the existence of a file value does not imply that the file exists on the filesystem.
+ * File values for missing files will be created on purpose in order to facilitate incremental
+ * builds in the case those files have reappeared.
+ *
+ * <p>
+ * This class contains the relevant metadata for a file, although not the contents. Note that
+ * since a FileValue doesn't store its corresponding SkyKey, it's possible for the FileValues for
+ * two different paths to be the same.
+ *
+ * <p>
+ * This should not be used for build outputs; use {@link ArtifactValue} for those.
+ */
+@Immutable
+@ThreadSafe
+public abstract class FileValue implements SkyValue {
+
+ boolean exists() {
+ return realFileStateValue().getType() != Type.NONEXISTENT;
+ }
+
+ public boolean isSymlink() {
+ return false;
+ }
+
+ /**
+ * Returns true if this value corresponds to a file or symlink to an existing file. If so, its
+ * parent directory is guaranteed to exist.
+ */
+ public boolean isFile() {
+ return realFileStateValue().getType() == Type.FILE;
+ }
+
+ /**
+ * Returns true if the file is a directory or a symlink to an existing directory. If so, its
+ * parent directory is guaranteed to exist.
+ */
+ public boolean isDirectory() {
+ return realFileStateValue().getType() == Type.DIRECTORY;
+ }
+
+ /**
+ * Returns the real rooted path of the file, taking ancestor symlinks into account. For example,
+ * the rooted path ['root']/['a/b'] is really ['root']/['c/b'] if 'a' is a symlink to 'b'. Note
+ * that ancestor symlinks outside the root boundary are not taken into consideration.
+ */
+ public abstract RootedPath realRootedPath();
+
+ abstract FileStateValue realFileStateValue();
+
+ /**
+ * Returns the unresolved link target if {@link #isSymlink()}.
+ *
+ * <p>This is useful if the caller wants to, for example, duplicate a relative symlink. An actual
+ * example could be a build rule that copies a set of input files to the output directory, but
+ * upon encountering symbolic links it can decide between copying or following them.
+ */
+ PathFragment getUnresolvedLinkTarget() {
+ throw new IllegalStateException(this.toString());
+ }
+
+ long getSize() {
+ Preconditions.checkState(isFile(), this);
+ return realFileStateValue().getSize();
+ }
+
+ @Nullable
+ byte[] getDigest() {
+ Preconditions.checkState(isFile(), this);
+ return realFileStateValue().getDigest();
+ }
+
+ /**
+ * Returns a key for building a file value for the given root-relative path.
+ */
+ @ThreadSafe
+ public static SkyKey key(RootedPath rootedPath) {
+ return new SkyKey(SkyFunctions.FILE, rootedPath);
+ }
+
+ /**
+ * Only intended to be used by {@link FileFunction}. Should not be used for symlink cycles.
+ */
+ static FileValue value(RootedPath rootedPath, FileStateValue fileStateValue,
+ RootedPath realRootedPath, FileStateValue realFileStateValue) {
+ if (rootedPath.equals(realRootedPath)) {
+ Preconditions.checkState(fileStateValue.getType() != FileStateValue.Type.SYMLINK,
+ "rootedPath: %s, fileStateValue: %s, realRootedPath: %s, realFileStateValue: %s",
+ rootedPath, fileStateValue, realRootedPath, realFileStateValue);
+ return new RegularFileValue(rootedPath, fileStateValue);
+ } else {
+ if (fileStateValue.getType() == FileStateValue.Type.SYMLINK) {
+ return new SymlinkFileValue(realRootedPath, realFileStateValue,
+ fileStateValue.getSymlinkTarget());
+ } else {
+ return new DifferentRealPathFileValue(realRootedPath, realFileStateValue);
+ }
+ }
+ }
+
+ /**
+ * Implementation of {@link FileValue} for files whose fully resolved path is the same as the
+ * requested path. For example, this is the case for the path "foo/bar/baz" if neither 'foo' nor
+ * 'foo/bar' nor 'foo/bar/baz' are symlinks.
+ */
+ private static final class RegularFileValue extends FileValue {
+
+ private final RootedPath rootedPath;
+ private final FileStateValue fileStateValue;
+
+ private RegularFileValue(RootedPath rootedPath, FileStateValue fileState) {
+ this.rootedPath = Preconditions.checkNotNull(rootedPath);
+ this.fileStateValue = Preconditions.checkNotNull(fileState);
+ }
+
+ @Override
+ public RootedPath realRootedPath() {
+ return rootedPath;
+ }
+
+ @Override
+ FileStateValue realFileStateValue() {
+ return fileStateValue;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (obj.getClass() != RegularFileValue.class) {
+ return false;
+ }
+ RegularFileValue other = (RegularFileValue) obj;
+ return rootedPath.equals(other.rootedPath) && fileStateValue.equals(other.fileStateValue);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(rootedPath, fileStateValue);
+ }
+
+ @Override
+ public String toString() {
+ return rootedPath + ", " + fileStateValue;
+ }
+ }
+
+ /**
+ * Base class for {@link FileValue}s for files whose fully resolved path is different than the
+ * requested path. For example, this is the case for the path "foo/bar/baz" if at least one of
+ * 'foo', 'foo/bar', or 'foo/bar/baz' is a symlink.
+ */
+ private static class DifferentRealPathFileValue extends FileValue {
+
+ protected final RootedPath realRootedPath;
+ protected final FileStateValue realFileStateValue;
+
+ private DifferentRealPathFileValue(RootedPath realRootedPath,
+ FileStateValue realFileStateValue) {
+ this.realRootedPath = Preconditions.checkNotNull(realRootedPath);
+ this.realFileStateValue = Preconditions.checkNotNull(realFileStateValue);
+ }
+
+ @Override
+ public RootedPath realRootedPath() {
+ return realRootedPath;
+ }
+
+ @Override
+ FileStateValue realFileStateValue() {
+ return realFileStateValue;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (obj.getClass() != DifferentRealPathFileValue.class) {
+ return false;
+ }
+ DifferentRealPathFileValue other = (DifferentRealPathFileValue) obj;
+ return realRootedPath.equals(other.realRootedPath)
+ && realFileStateValue.equals(other.realFileStateValue);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(realRootedPath, realFileStateValue);
+ }
+
+ @Override
+ public String toString() {
+ return realRootedPath + ", " + realFileStateValue + " (symlink ancestor)";
+ }
+ }
+
+ /** Implementation of {@link FileValue} for files that are symlinks. */
+ private static final class SymlinkFileValue extends DifferentRealPathFileValue {
+ private final PathFragment linkValue;
+
+ private SymlinkFileValue(RootedPath realRootedPath, FileStateValue realFileState,
+ PathFragment linkTarget) {
+ super(realRootedPath, realFileState);
+ this.linkValue = linkTarget;
+ }
+
+ @Override
+ public boolean isSymlink() {
+ return true;
+ }
+
+ @Override
+ public PathFragment getUnresolvedLinkTarget() {
+ return linkValue;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (obj.getClass() != SymlinkFileValue.class) {
+ return false;
+ }
+ SymlinkFileValue other = (SymlinkFileValue) obj;
+ return realRootedPath.equals(other.realRootedPath)
+ && realFileStateValue.equals(other.realFileStateValue)
+ && linkValue.equals(other.linkValue);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(realRootedPath, realFileStateValue, linkValue, Boolean.TRUE);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("symlink (real_path=%s, real_state=%s, link_value=%s)",
+ realRootedPath, realFileStateValue, linkValue);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/FilesetEntryFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/FilesetEntryFunction.java
new file mode 100644
index 0000000..a559206
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/FilesetEntryFunction.java
@@ -0,0 +1,320 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Function;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
+import com.google.devtools.build.lib.actions.FilesetOutputSymlink;
+import com.google.devtools.build.lib.actions.FilesetTraversalParams;
+import com.google.devtools.build.lib.actions.FilesetTraversalParams.DirectTraversal;
+import com.google.devtools.build.lib.skyframe.RecursiveFilesystemTraversalFunction.DanglingSymlinkException;
+import com.google.devtools.build.lib.skyframe.RecursiveFilesystemTraversalFunction.RecursiveFilesystemTraversalException;
+import com.google.devtools.build.lib.skyframe.RecursiveFilesystemTraversalValue.ResolvedFile;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyFunctionException;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.TreeMap;
+
+/** SkyFunction for {@link FilesetEntryValue}. */
+public final class FilesetEntryFunction implements SkyFunction {
+
+ private static final class MissingDepException extends Exception {}
+
+ private static final class FilesetEntryFunctionException extends SkyFunctionException {
+ FilesetEntryFunctionException(RecursiveFilesystemTraversalException e) {
+ super(e, Transience.PERSISTENT);
+ }
+ }
+
+ @Override
+ public SkyValue compute(SkyKey key, Environment env) throws FilesetEntryFunctionException {
+ FilesetTraversalParams t = (FilesetTraversalParams) key.argument();
+ Preconditions.checkState(
+ t.getNestedTraversal().isPresent() != t.getDirectTraversal().isPresent(),
+ "Exactly one of the nested and direct traversals must be specified: %s", t);
+
+ // Create the set of excluded files. Only top-level files can be excluded, i.e. ones that are
+ // directly under the root if the root is a directory.
+ Set<String> exclusions = createExclusionSet(t.getExcludedFiles());
+
+ // The map of output symlinks. Each key is the path of a output symlink that the Fileset must
+ // create, relative to the Fileset.out directory, and each value specifies extra information
+ // about the link (its target, associated metadata and again its name).
+ Map<PathFragment, FilesetOutputSymlink> outputSymlinks = new LinkedHashMap<>();
+
+ if (t.getNestedTraversal().isPresent()) {
+ // The "nested" traversal parameters are present if and only if FilesetEntry.srcdir specifies
+ // another Fileset (a "nested" one).
+ FilesetEntryValue nested = (FilesetEntryValue) env.getValue(
+ FilesetEntryValue.key(t.getNestedTraversal().get()));
+ if (env.valuesMissing()) {
+ return null;
+ }
+
+ for (FilesetOutputSymlink s : nested.getSymlinks()) {
+ maybeStoreSymlink(s, t.getDestPath(), exclusions, outputSymlinks);
+ }
+ } else {
+ // The "nested" traversal params are absent if and only if the "direct" traversal params are
+ // present, which is the case when the FilesetEntry specifies a package's BUILD file, a
+ // directory or a list of files.
+
+ // The root of the direct traversal is defined as follows.
+ //
+ // If FilesetEntry.files is specified, then a TraversalRequest is created for each entry, the
+ // root being the respective entry itself. These are all traversed for they may be
+ // directories or symlinks to directories, and we need to establish Skyframe dependencies on
+ // their contents for incremental correctness. If an entry is indeed a directory (but not when
+ // it's a symlink to one) then we have to create symlinks to each of their childen.
+ // (NB: there seems to be no good reason for this, it's just how legacy Fileset works. We may
+ // want to consider creating a symlink just for the directory and not for its child elements.)
+ //
+ // If FilesetEntry.files is not specified, then srcdir refers to either a BUILD file or a
+ // directory. For the former, the root will be the parent of the BUILD file. For the latter,
+ // the root will be srcdir itself.
+ DirectTraversal direct = t.getDirectTraversal().get();
+
+ RecursiveFilesystemTraversalValue rftv;
+ try {
+ // Traverse the filesystem to establish skyframe dependencies.
+ rftv = traverse(env, createErrorInfo(t), direct);
+ } catch (MissingDepException e) {
+ return null;
+ }
+
+ // The root can only be absent for the EMPTY rftv instance.
+ if (!rftv.getResolvedRoot().isPresent()) {
+ return FilesetEntryValue.EMPTY;
+ }
+
+ ResolvedFile resolvedRoot = rftv.getResolvedRoot().get();
+
+ // Handle dangling symlinks gracefully be returning empty results.
+ if (!resolvedRoot.type.exists()) {
+ return FilesetEntryValue.EMPTY;
+ }
+
+ // The prefix to remove is the entire path of the root. This is OK:
+ // - when the root is a file, this removes the entire path, but the traversal's destination
+ // path is actually the name of the output symlink, so this works out correctly
+ // - when the root is a directory or a symlink to one then we need to strip off the
+ // directory's path from every result (this is how the output symlinks must be created)
+ // before making them relative to the destination path
+ PathFragment prefixToRemove = direct.getRoot().getRelativePart();
+
+ Iterable<ResolvedFile> results = null;
+
+ if (direct.isRecursive()
+ || (resolvedRoot.type.isDirectory() && !resolvedRoot.type.isSymlink())) {
+ // The traversal is recursive (requested for an entire FilesetEntry.srcdir) or it was
+ // requested for a FilesetEntry.files entry which turned out to be a directory. We need to
+ // create an output symlink for every file in it and all of its subdirectories. Only
+ // exception is when the subdirectory is really a symlink to a directory -- no output
+ // shall be created for the contents of those.
+ // Now we create Dir objects to model the filesystem tree. The object employs a trick to
+ // find directory symlinks: directory symlinks have corresponding ResolvedFile entries and
+ // are added as files too, while their children, also added as files, contain the path of
+ // the parent. Finding and discarding the children is easy if we traverse the tree from
+ // root to leaf.
+ DirectoryTree root = new DirectoryTree();
+ for (ResolvedFile f : rftv.getTransitiveFiles().toCollection()) {
+ PathFragment path = f.getNameInSymlinkTree().relativeTo(prefixToRemove);
+ if (path.segmentCount() > 0) {
+ path = t.getDestPath().getRelative(path);
+ DirectoryTree dir = root;
+ for (int i = 0; i < path.segmentCount() - 1; ++i) {
+ dir = dir.addOrGetSubdir(path.getSegment(i));
+ }
+ dir.maybeAddFile(f);
+ }
+ }
+ // Here's where the magic happens. The returned iterable will yield all files in the
+ // directory that are not under symlinked directories, as well as all directory symlinks.
+ results = root.iterateFiles();
+ } else {
+ // If we're on this branch then the traversal was done for just one entry in
+ // FilesetEntry.files (which was not a directory, so it was either a file, a symlink to one
+ // or a symlink to a directory), meaning we'll have only one output symlink.
+ results = ImmutableList.of(resolvedRoot);
+ }
+
+ // Create one output symlink for each entry in the results.
+ for (ResolvedFile f : results) {
+ PathFragment targetName;
+ try {
+ targetName = f.getTargetInSymlinkTree(direct.isFollowingSymlinks());
+ } catch (DanglingSymlinkException e) {
+ throw new FilesetEntryFunctionException(e);
+ }
+
+ // Metadata field must be present. It can only be absent when stripped by tests.
+ String metadata = Integer.toHexString(f.metadata.get().hashCode());
+
+ // The linkName has to be under the traversal's root, which is also the prefix to remove.
+ PathFragment linkName = f.getNameInSymlinkTree().relativeTo(prefixToRemove);
+ maybeStoreSymlink(linkName, targetName, metadata, t.getDestPath(), exclusions,
+ outputSymlinks);
+ }
+ }
+
+ return FilesetEntryValue.of(ImmutableSet.copyOf(outputSymlinks.values()));
+ }
+
+ /** Stores an output symlink unless it's excluded or would overwrite an existing one. */
+ private static void maybeStoreSymlink(FilesetOutputSymlink nestedLink, PathFragment destPath,
+ Set<String> exclusions, Map<PathFragment, FilesetOutputSymlink> result) {
+ maybeStoreSymlink(nestedLink.name, nestedLink.target, nestedLink.metadata, destPath,
+ exclusions, result);
+ }
+
+ /** Stores an output symlink unless it's excluded or would overwrite an existing one. */
+ private static void maybeStoreSymlink(PathFragment linkName, PathFragment linkTarget,
+ String metadata, PathFragment destPath, Set<String> exclusions,
+ Map<PathFragment, FilesetOutputSymlink> result) {
+ if (!exclusions.contains(linkName.getPathString())) {
+ linkName = destPath.getRelative(linkName);
+ if (!result.containsKey(linkName)) {
+ result.put(linkName, new FilesetOutputSymlink(linkName, linkTarget, metadata));
+ }
+ }
+ }
+
+ private static Set<String> createExclusionSet(Set<String> input) {
+ return Sets.filter(input, new Predicate<String>() {
+ @Override
+ public boolean apply(String e) {
+ // Keep the top-level exclusions only. Do not look for "/" but count the path segments
+ // instead, in anticipation of future Windows support.
+ return new PathFragment(e).segmentCount() == 1;
+ }
+ });
+ }
+
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return null;
+ }
+
+ private RecursiveFilesystemTraversalValue traverse(Environment env, String errorInfo,
+ DirectTraversal traversal) throws MissingDepException {
+ SkyKey depKey = RecursiveFilesystemTraversalValue.key(
+ new RecursiveFilesystemTraversalValue.TraversalRequest(traversal.getRoot().asRootedPath(),
+ traversal.isGenerated(), traversal.getCrossPackageBoundary(), traversal.isPackage(),
+ errorInfo));
+ RecursiveFilesystemTraversalValue v = (RecursiveFilesystemTraversalValue) env.getValue(depKey);
+ if (env.valuesMissing()) {
+ throw new MissingDepException();
+ }
+ return v;
+ }
+
+ private static String createErrorInfo(FilesetTraversalParams t) {
+ if (t.getDirectTraversal().isPresent()) {
+ DirectTraversal direct = t.getDirectTraversal().get();
+ return String.format("Fileset '%s' traversing %s %s", t.getOwnerLabel(),
+ direct.isPackage() ? "package" : "file (or directory)",
+ direct.getRoot().getRelativePart().getPathString());
+ } else {
+ return String.format("Fileset '%s' traversing another Fileset", t.getOwnerLabel());
+ }
+ }
+
+ /**
+ * Models a FilesetEntryFunction's portion of the symlink output tree created by a Fileset rule.
+ *
+ * <p>A Fileset rule's output is computed by zero or more {@link FilesetEntryFunction}s, resulting
+ * in one {@link FilesetEntryValue} for each. Each of those represents a portion of the grand
+ * output tree of the Fileset. These portions are later merged and written to the fileset manifest
+ * file, which is then consumed by a tool that ultimately creates the symlinks in the filesystem.
+ *
+ * <p>Because the Fileset doesn't process the lists in the FilesetEntryValues any further than
+ * merging them, they have to adhere to the conventions of the manifest file. One of these is that
+ * files are alphabetically ordered (enables the consumer of the manifest to work faster than
+ * otherwise) and another is that the contents of regular directories are listed, but contents
+ * of directory symlinks are not, only the symlinks are. (Other details of the manifest file are
+ * not relevant here.)
+ *
+ * <p>See {@link DirectoryTree#iterateFiles()} for more details.
+ */
+ private static final class DirectoryTree {
+ // Use TreeMaps for the benefit of alphabetically ordered iteration.
+ public final Map<String, ResolvedFile> files = new TreeMap<>();
+ public final Map<String, DirectoryTree> dirs = new TreeMap<>();
+
+ DirectoryTree addOrGetSubdir(String name) {
+ DirectoryTree result = dirs.get(name);
+ if (result == null) {
+ result = new DirectoryTree();
+ dirs.put(name, result);
+ }
+ return result;
+ }
+
+ void maybeAddFile(ResolvedFile r) {
+ String name = r.getNameInSymlinkTree().getBaseName();
+ if (!files.containsKey(name)) {
+ files.put(name, r);
+ }
+ }
+
+ /**
+ * Lazily yields all files in this directory and all of its subdirectories.
+ *
+ * <p>The function first yields all the files directly under this directory, in alphabetical
+ * order. Then come the contents of subdirectories, processed recursively in the same fashion
+ * as this directory, and also in alphabetical order.
+ *
+ * <p>If a directory symlink is encountered its contents are not listed, only the symlink is.
+ */
+ Iterable<ResolvedFile> iterateFiles() {
+ // 1. Filter directory symlinks. If the symlink target contains files, those were added
+ // as normal files so their parent directory (the symlink) would show up in the dirs map
+ // (as a directory) as well as in the files map (as a symlink to a directory).
+ final Set<String> fileNames = files.keySet();
+ Iterable<Map.Entry<String, DirectoryTree>> noDirSymlinkes = Iterables.filter(dirs.entrySet(),
+ new Predicate<Map.Entry<String, DirectoryTree>>() {
+ @Override
+ public boolean apply(Map.Entry<String, DirectoryTree> input) {
+ return !fileNames.contains(input.getKey());
+ }
+ });
+
+ // 2. Extract the iterables of the true subdirectories.
+ Iterable<Iterable<ResolvedFile>> subdirIters = Iterables.transform(noDirSymlinkes,
+ new Function<Map.Entry<String, DirectoryTree>, Iterable<ResolvedFile>>() {
+ @Override
+ public Iterable<ResolvedFile> apply(Entry<String, DirectoryTree> input) {
+ return input.getValue().iterateFiles();
+ }
+ });
+
+ // 3. Just concat all subdirectory iterations for one, seamless iteration.
+ Iterable<ResolvedFile> dirsIter = Iterables.concat(subdirIters);
+
+ return Iterables.concat(files.values(), dirsIter);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/FilesetEntryValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/FilesetEntryValue.java
new file mode 100644
index 0000000..e7b6580
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/FilesetEntryValue.java
@@ -0,0 +1,49 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.actions.FilesetOutputSymlink;
+import com.google.devtools.build.lib.actions.FilesetTraversalParams;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+/** Output symlinks produced by a whole FilesetEntry or by a single file in FilesetEntry.files. */
+public final class FilesetEntryValue implements SkyValue {
+ static final FilesetEntryValue EMPTY =
+ new FilesetEntryValue(ImmutableSet.<FilesetOutputSymlink>of());
+
+ private final ImmutableSet<FilesetOutputSymlink> symlinks;
+
+ private FilesetEntryValue(ImmutableSet<FilesetOutputSymlink> symlinks) {
+ this.symlinks = symlinks;
+ }
+
+ static FilesetEntryValue of(ImmutableSet<FilesetOutputSymlink> symlinks) {
+ if (symlinks.isEmpty()) {
+ return EMPTY;
+ } else {
+ return new FilesetEntryValue(symlinks);
+ }
+ }
+
+ /** Returns the list of output symlinks. */
+ public ImmutableSet<FilesetOutputSymlink> getSymlinks() {
+ return symlinks;
+ }
+
+ public static SkyKey key(FilesetTraversalParams params) {
+ return new SkyKey(SkyFunctions.FILESET_ENTRY, params);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/FilesystemValueChecker.java b/src/main/java/com/google/devtools/build/lib/skyframe/FilesystemValueChecker.java
new file mode 100644
index 0000000..be4f4e8
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/FilesystemValueChecker.java
@@ -0,0 +1,398 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.base.Supplier;
+import com.google.common.base.Suppliers;
+import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.concurrent.ExecutorShutdownUtil;
+import com.google.devtools.build.lib.concurrent.Sharder;
+import com.google.devtools.build.lib.concurrent.ThrowableRecordingRunnableWrapper;
+import com.google.devtools.build.lib.util.LoggingUtil;
+import com.google.devtools.build.lib.util.Pair;
+import com.google.devtools.build.lib.util.io.TimestampGranularityMonitor;
+import com.google.devtools.build.lib.vfs.BatchStat;
+import com.google.devtools.build.lib.vfs.FileStatusWithDigest;
+import com.google.devtools.build.lib.vfs.RootedPath;
+import com.google.devtools.build.skyframe.Differencer;
+import com.google.devtools.build.skyframe.MemoizingEvaluator;
+import com.google.devtools.build.skyframe.SkyFunctionName;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.annotation.Nullable;
+
+/**
+ * A helper class to find dirty values by accessing the filesystem directly (contrast with
+ * {@link DiffAwareness}).
+ */
+class FilesystemValueChecker {
+
+ private static final int DIRTINESS_CHECK_THREADS = 50;
+ private static final Logger LOG = Logger.getLogger(FilesystemValueChecker.class.getName());
+
+ private static final Predicate<SkyKey> FILE_STATE_AND_DIRECTORY_LISTING_STATE_FILTER =
+ SkyFunctionName.functionIsIn(ImmutableSet.of(SkyFunctions.FILE_STATE,
+ SkyFunctions.DIRECTORY_LISTING_STATE));
+ private static final Predicate<SkyKey> ACTION_FILTER =
+ SkyFunctionName.functionIs(SkyFunctions.ACTION_EXECUTION);
+
+ private final TimestampGranularityMonitor tsgm;
+ private final Supplier<Map<SkyKey, SkyValue>> valuesSupplier;
+ private AtomicInteger modifiedOutputFilesCounter = new AtomicInteger(0);
+
+ FilesystemValueChecker(final MemoizingEvaluator evaluator, TimestampGranularityMonitor tsgm) {
+ this.tsgm = tsgm;
+
+ // Construct the full map view of the entire graph at most once ("memoized"), lazily. If
+ // getDirtyFilesystemValues(Iterable<SkyKey>) is called on an empty Iterable, we avoid having
+ // to create the Map of value keys to values. This is useful in the case where the graph
+ // getValues() method could be slow.
+ this.valuesSupplier = Suppliers.memoize(new Supplier<Map<SkyKey, SkyValue>>() {
+ @Override
+ public Map<SkyKey, SkyValue> get() {
+ return evaluator.getValues();
+ }
+ });
+ }
+
+ Iterable<SkyKey> getFilesystemSkyKeys() {
+ return Iterables.filter(valuesSupplier.get().keySet(),
+ FILE_STATE_AND_DIRECTORY_LISTING_STATE_FILTER);
+ }
+
+ Differencer.Diff getDirtyFilesystemSkyKeys() throws InterruptedException {
+ return getDirtyFilesystemValues(getFilesystemSkyKeys());
+ }
+
+ /**
+ * Check the given file and directory values for modifications. {@code values} is assumed to only
+ * have {@link FileValue}s and {@link DirectoryListingStateValue}s.
+ */
+ Differencer.Diff getDirtyFilesystemValues(Iterable<SkyKey> values)
+ throws InterruptedException {
+ return getDirtyValues(values, FILE_STATE_AND_DIRECTORY_LISTING_STATE_FILTER,
+ new DirtyChecker() {
+ @Override
+ public DirtyResult check(SkyKey key, SkyValue oldValue, TimestampGranularityMonitor tsgm) {
+ if (key.functionName() == SkyFunctions.FILE_STATE) {
+ return checkFileStateValue((RootedPath) key.argument(), (FileStateValue) oldValue,
+ tsgm);
+ } else if (key.functionName() == SkyFunctions.DIRECTORY_LISTING_STATE) {
+ return checkDirectoryListingStateValue((RootedPath) key.argument(),
+ (DirectoryListingStateValue) oldValue);
+ } else {
+ throw new IllegalStateException("Unexpected key type " + key);
+ }
+ }
+ });
+ }
+
+ /**
+ * Return a collection of action values which have output files that are not in-sync with
+ * the on-disk file value (were modified externally).
+ */
+ public Collection<SkyKey> getDirtyActionValues(@Nullable final BatchStat batchStatter)
+ throws InterruptedException {
+ // CPU-bound (usually) stat() calls, plus a fudge factor.
+ LOG.info("Accumulating dirty actions");
+ final int numOutputJobs = Runtime.getRuntime().availableProcessors() * 4;
+ final Set<SkyKey> actionSkyKeys =
+ Sets.filter(valuesSupplier.get().keySet(), ACTION_FILTER);
+ final Sharder<Pair<SkyKey, ActionExecutionValue>> outputShards =
+ new Sharder<>(numOutputJobs, actionSkyKeys.size());
+
+ for (SkyKey key : actionSkyKeys) {
+ outputShards.add(Pair.of(key, (ActionExecutionValue) valuesSupplier.get().get(key)));
+ }
+ LOG.info("Sharded action values for batching");
+
+ ExecutorService executor = Executors.newFixedThreadPool(
+ numOutputJobs,
+ new ThreadFactoryBuilder().setNameFormat("FileSystem Output File Invalidator %d").build());
+
+ Collection<SkyKey> dirtyKeys = Sets.newConcurrentHashSet();
+ ThrowableRecordingRunnableWrapper wrapper =
+ new ThrowableRecordingRunnableWrapper("FileSystemValueChecker#getDirtyActionValues");
+
+ modifiedOutputFilesCounter.set(0);
+ for (List<Pair<SkyKey, ActionExecutionValue>> shard : outputShards) {
+ Runnable job = (batchStatter == null)
+ ? outputStatJob(dirtyKeys, shard)
+ : batchStatJob(dirtyKeys, shard, batchStatter);
+ executor.submit(wrapper.wrap(job));
+ }
+
+ boolean interrupted = ExecutorShutdownUtil.interruptibleShutdown(executor);
+ Throwables.propagateIfPossible(wrapper.getFirstThrownError());
+ LOG.info("Completed output file stat checks");
+ if (interrupted) {
+ throw new InterruptedException();
+ }
+ return dirtyKeys;
+ }
+
+ private Runnable batchStatJob(final Collection<SkyKey> dirtyKeys,
+ final List<Pair<SkyKey, ActionExecutionValue>> shard,
+ final BatchStat batchStatter) {
+ return new Runnable() {
+ @Override
+ public void run() {
+ Map<Artifact, Pair<SkyKey, ActionExecutionValue>> artifactToKeyAndValue = new HashMap<>();
+ for (Pair<SkyKey, ActionExecutionValue> keyAndValue : shard) {
+ ActionExecutionValue actionValue = keyAndValue.getSecond();
+ if (actionValue == null) {
+ dirtyKeys.add(keyAndValue.getFirst());
+ } else {
+ for (Artifact artifact : actionValue.getAllOutputArtifactData().keySet()) {
+ artifactToKeyAndValue.put(artifact, keyAndValue);
+ }
+ }
+ }
+
+ List<Artifact> artifacts = ImmutableList.copyOf(artifactToKeyAndValue.keySet());
+ List<FileStatusWithDigest> stats;
+ try {
+ stats = batchStatter.batchStat(/*includeDigest=*/true, /*includeLinks=*/true,
+ Artifact.asPathFragments(artifacts));
+ } catch (IOException e) {
+ // Batch stat did not work. Log an exception and fall back on system calls.
+ LoggingUtil.logToRemote(Level.WARNING, "Unable to process batch stat", e);
+ outputStatJob(dirtyKeys, shard).run();
+ return;
+ } catch (InterruptedException e) {
+ // We handle interrupt in the main thread.
+ return;
+ }
+
+ Preconditions.checkState(artifacts.size() == stats.size(),
+ "artifacts.size() == %s stats.size() == %s", artifacts.size(), stats.size());
+ for (int i = 0; i < artifacts.size(); i++) {
+ Artifact artifact = artifacts.get(i);
+ FileStatusWithDigest stat = stats.get(i);
+ Pair<SkyKey, ActionExecutionValue> keyAndValue = artifactToKeyAndValue.get(artifact);
+ ActionExecutionValue actionValue = keyAndValue.getSecond();
+ SkyKey key = keyAndValue.getFirst();
+ FileValue lastKnownData = actionValue.getAllOutputArtifactData().get(artifact);
+ try {
+ FileValue newData = FileAndMetadataCache.fileValueFromArtifact(artifact, stat, tsgm);
+ if (!newData.equals(lastKnownData)) {
+ modifiedOutputFilesCounter.getAndIncrement();
+ dirtyKeys.add(key);
+ }
+ } catch (IOException e) {
+ // This is an unexpected failure getting a digest or symlink target.
+ modifiedOutputFilesCounter.getAndIncrement();
+ dirtyKeys.add(key);
+ }
+ }
+ }
+ };
+ }
+
+ private Runnable outputStatJob(final Collection<SkyKey> dirtyKeys,
+ final List<Pair<SkyKey, ActionExecutionValue>> shard) {
+ return new Runnable() {
+ @Override
+ public void run() {
+ for (Pair<SkyKey, ActionExecutionValue> keyAndValue : shard) {
+ ActionExecutionValue value = keyAndValue.getSecond();
+ if (value == null || actionValueIsDirtyWithDirectSystemCalls(value)) {
+ dirtyKeys.add(keyAndValue.getFirst());
+ }
+ }
+ }
+ };
+ }
+
+ /**
+ * Returns number of modified output files inside of dirty actions.
+ */
+ int getNumberOfModifiedOutputFiles() {
+ return modifiedOutputFilesCounter.get();
+ }
+
+ private boolean actionValueIsDirtyWithDirectSystemCalls(ActionExecutionValue actionValue) {
+ boolean isDirty = false;
+ for (Map.Entry<Artifact, FileValue> entry :
+ actionValue.getAllOutputArtifactData().entrySet()) {
+ Artifact artifact = entry.getKey();
+ FileValue lastKnownData = entry.getValue();
+ try {
+ if (!FileAndMetadataCache.fileValueFromArtifact(artifact, null, tsgm).equals(
+ lastKnownData)) {
+ modifiedOutputFilesCounter.getAndIncrement();
+ isDirty = true;
+ }
+ } catch (IOException e) {
+ // This is an unexpected failure getting a digest or symlink target.
+ modifiedOutputFilesCounter.getAndIncrement();
+ isDirty = true;
+ }
+ }
+ return isDirty;
+ }
+
+ private BatchDirtyResult getDirtyValues(Iterable<SkyKey> values,
+ Predicate<SkyKey> keyFilter,
+ final DirtyChecker checker) throws InterruptedException {
+ ExecutorService executor = Executors.newFixedThreadPool(DIRTINESS_CHECK_THREADS,
+ new ThreadFactoryBuilder().setNameFormat("FileSystem Value Invalidator %d").build());
+
+ final BatchDirtyResult batchResult = new BatchDirtyResult();
+ ThrowableRecordingRunnableWrapper wrapper =
+ new ThrowableRecordingRunnableWrapper("FilesystemValueChecker#getDirtyValues");
+ for (final SkyKey key : values) {
+ Preconditions.checkState(keyFilter.apply(key), key);
+ final SkyValue value = valuesSupplier.get().get(key);
+ executor.execute(wrapper.wrap(new Runnable() {
+ @Override
+ public void run() {
+ if (value == null) {
+ // value will be null if the value is in error or part of a cycle.
+ // TODO(bazel-team): This is overly conservative.
+ batchResult.add(key, /*newValue=*/null);
+ return;
+ }
+ DirtyResult result = checker.check(key, value, tsgm);
+ if (result.isDirty()) {
+ batchResult.add(key, result.getNewValue());
+ }
+ }
+ }));
+ }
+
+ boolean interrupted = ExecutorShutdownUtil.interruptibleShutdown(executor);
+ Throwables.propagateIfPossible(wrapper.getFirstThrownError());
+ if (interrupted) {
+ throw new InterruptedException();
+ }
+ return batchResult;
+ }
+
+ private static DirtyResult checkFileStateValue(RootedPath rootedPath,
+ FileStateValue fileStateValue, TimestampGranularityMonitor tsgm) {
+ try {
+ FileStateValue newValue = FileStateValue.create(rootedPath, tsgm);
+ return newValue.equals(fileStateValue)
+ ? DirtyResult.NOT_DIRTY : DirtyResult.dirtyWithNewValue(newValue);
+ } catch (InconsistentFilesystemException | IOException e) {
+ // TODO(bazel-team): An IOException indicates a failure to get a file digest or a symlink
+ // target, not a missing file. Such a failure really shouldn't happen, so failing early
+ // may be better here.
+ return DirtyResult.DIRTY;
+ }
+ }
+
+ private static DirtyResult checkDirectoryListingStateValue(RootedPath dirRootedPath,
+ DirectoryListingStateValue directoryListingStateValue) {
+ try {
+ DirectoryListingStateValue newValue = DirectoryListingStateValue.create(dirRootedPath);
+ return newValue.equals(directoryListingStateValue)
+ ? DirtyResult.NOT_DIRTY : DirtyResult.dirtyWithNewValue(newValue);
+ } catch (IOException e) {
+ return DirtyResult.DIRTY;
+ }
+ }
+
+ /**
+ * Result of a batch call to {@link DirtyChecker#check}. Partitions the dirty values based on
+ * whether we have a new value available for them or not.
+ */
+ private static class BatchDirtyResult implements Differencer.Diff {
+
+ private final Set<SkyKey> concurrentDirtyKeysWithoutNewValues =
+ Collections.newSetFromMap(new ConcurrentHashMap<SkyKey, Boolean>());
+ private final ConcurrentHashMap<SkyKey, SkyValue> concurrentDirtyKeysWithNewValues =
+ new ConcurrentHashMap<>();
+
+ private void add(SkyKey key, @Nullable SkyValue newValue) {
+ if (newValue == null) {
+ concurrentDirtyKeysWithoutNewValues.add(key);
+ } else {
+ concurrentDirtyKeysWithNewValues.put(key, newValue);
+ }
+ }
+
+ @Override
+ public Iterable<SkyKey> changedKeysWithoutNewValues() {
+ return concurrentDirtyKeysWithoutNewValues;
+ }
+
+ @Override
+ public Map<SkyKey, ? extends SkyValue> changedKeysWithNewValues() {
+ return concurrentDirtyKeysWithNewValues;
+ }
+ }
+
+ private static class DirtyResult {
+
+ static final DirtyResult NOT_DIRTY = new DirtyResult(false, null);
+ static final DirtyResult DIRTY = new DirtyResult(true, null);
+
+ private final boolean isDirty;
+ @Nullable private final SkyValue newValue;
+
+ private DirtyResult(boolean isDirty, @Nullable SkyValue newValue) {
+ this.isDirty = isDirty;
+ this.newValue = newValue;
+ }
+
+ boolean isDirty() {
+ return isDirty;
+ }
+
+ /**
+ * If {@code isDirty()}, then either returns the new value for the value or {@code null} if
+ * the new value wasn't computed. In the case where the value is dirty and a new value is
+ * available, then the new value can be injected into the skyframe graph. Otherwise, the value
+ * should simply be invalidated.
+ */
+ @Nullable
+ SkyValue getNewValue() {
+ Preconditions.checkState(isDirty());
+ return newValue;
+ }
+
+ static DirtyResult dirtyWithNewValue(SkyValue newValue) {
+ return new DirtyResult(true, newValue);
+ }
+ }
+
+ private static interface DirtyChecker {
+ DirtyResult check(SkyKey key, SkyValue oldValue, TimestampGranularityMonitor tsgm);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/GlobDescriptor.java b/src/main/java/com/google/devtools/build/lib/skyframe/GlobDescriptor.java
new file mode 100644
index 0000000..5baeae8
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/GlobDescriptor.java
@@ -0,0 +1,113 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.packages.PackageIdentifier;
+import com.google.devtools.build.lib.util.StringCanonicalizer;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.io.Serializable;
+import java.util.Objects;
+
+/**
+ * A descriptor for a glob request.
+ *
+ * <p>{@code subdir} must be empty or point to an existing directory.</p>
+ *
+ * <p>{@code pattern} must be valid, as indicated by {@code UnixGlob#checkPatternForError}.
+ */
+@ThreadSafe
+public final class GlobDescriptor implements Serializable {
+ final PackageIdentifier packageId;
+ final PathFragment subdir;
+ final String pattern;
+ final boolean excludeDirs;
+
+ /**
+ * Constructs a GlobDescriptor.
+ *
+ * @param packageId the name of the owner package (must be an existing package)
+ * @param subdir the subdirectory being looked at (must exist and must be a directory. It's
+ * assumed that there are no other packages between {@code packageName} and
+ * {@code subdir}.
+ * @param pattern a valid glob pattern
+ * @param excludeDirs true if directories should be excluded from results
+ */
+ GlobDescriptor(PackageIdentifier packageId, PathFragment subdir, String pattern,
+ boolean excludeDirs) {
+ this.packageId = Preconditions.checkNotNull(packageId);
+ this.subdir = Preconditions.checkNotNull(subdir);
+ this.pattern = Preconditions.checkNotNull(StringCanonicalizer.intern(pattern));
+ this.excludeDirs = excludeDirs;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("<GlobDescriptor packageName=%s subdir=%s pattern=%s excludeDirs=%s>",
+ packageId, subdir, pattern, excludeDirs);
+ }
+
+ /**
+ * Returns the package that "owns" this glob.
+ *
+ * <p>The glob evaluation code ensures that the boundaries of this package are not crossed.
+ */
+ public PackageIdentifier getPackageId() {
+ return packageId;
+ }
+
+ /**
+ * Returns the subdirectory of the package under consideration.
+ */
+ PathFragment getSubdir() {
+ return subdir;
+ }
+
+ /**
+ * Returns the glob pattern under consideration. May contain wildcards.
+ *
+ * <p>As the glob evaluator traverses deeper into the file tree, components are added at the
+ * beginning of {@code subdir} and removed from the beginning of {@code pattern}.
+ */
+ String getPattern() {
+ return pattern;
+ }
+
+ /**
+ * Returns true if directories should be excluded from results.
+ */
+ boolean excludeDirs() {
+ return excludeDirs;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(packageId, subdir, pattern, excludeDirs);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof GlobDescriptor)) {
+ return false;
+ }
+ GlobDescriptor other = (GlobDescriptor) obj;
+ return packageId.equals(other.packageId) && subdir.equals(other.subdir)
+ && pattern.equals(other.pattern) && excludeDirs == other.excludeDirs;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/GlobFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/GlobFunction.java
new file mode 100644
index 0000000..a1fdcb2
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/GlobFunction.java
@@ -0,0 +1,251 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Preconditions;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.vfs.Dirent;
+import com.google.devtools.build.lib.vfs.Dirent.Type;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.lib.vfs.RootedPath;
+import com.google.devtools.build.lib.vfs.UnixGlob;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyFunctionException;
+import com.google.devtools.build.skyframe.SkyFunctionException.Transience;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.util.regex.Pattern;
+
+import javax.annotation.Nullable;
+
+/**
+ * A {@link SkyFunction} for {@link GlobValue}s.
+ *
+ * <p>This code drives the glob matching process.
+ */
+final class GlobFunction implements SkyFunction {
+
+ private final Cache<String, Pattern> regexPatternCache =
+ CacheBuilder.newBuilder().concurrencyLevel(4).build();
+
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env) throws GlobFunctionException {
+ GlobDescriptor glob = (GlobDescriptor) skyKey.argument();
+
+ PackageLookupValue globPkgLookupValue = (PackageLookupValue)
+ env.getValue(PackageLookupValue.key(glob.getPackageId()));
+ if (globPkgLookupValue == null) {
+ return null;
+ }
+ Preconditions.checkState(globPkgLookupValue.packageExists(), "%s isn't an existing package",
+ glob.getPackageId());
+ // Note that this implies that the package's BUILD file exists which implies that the
+ // package's directory exists.
+
+ PathFragment globSubdir = glob.getSubdir();
+ if (!globSubdir.equals(PathFragment.EMPTY_FRAGMENT)) {
+ PackageLookupValue globSubdirPkgLookupValue = (PackageLookupValue) env.getValue(
+ PackageLookupValue.key(glob.getPackageId().getPackageFragment()
+ .getRelative(globSubdir)));
+ if (globSubdirPkgLookupValue == null) {
+ return null;
+ }
+ if (globSubdirPkgLookupValue.packageExists()) {
+ // We crossed the package boundary, that is, pkg/subdir contains a BUILD file and thus
+ // defines another package, so glob expansion should not descend into that subdir.
+ return GlobValue.EMPTY;
+ }
+ }
+
+ String pattern = glob.getPattern();
+ // Split off the first path component of the pattern.
+ int slashPos = pattern.indexOf("/");
+ String patternHead;
+ String patternTail;
+ if (slashPos == -1) {
+ patternHead = pattern;
+ patternTail = null;
+ } else {
+ // Substrings will share the backing array of the original glob string. That should be fine.
+ patternHead = pattern.substring(0, slashPos);
+ patternTail = pattern.substring(slashPos + 1);
+ }
+
+ NestedSetBuilder<PathFragment> matches = NestedSetBuilder.stableOrder();
+
+ // "**" also matches an empty segment, so try the case where it is not present.
+ if ("**".equals(patternHead)) {
+ if (patternTail == null) {
+ if (!glob.excludeDirs()) {
+ matches.add(globSubdir);
+ }
+ } else {
+ SkyKey globKey = GlobValue.internalKey(
+ glob.getPackageId(), globSubdir, patternTail, glob.excludeDirs());
+ GlobValue globValue = (GlobValue) env.getValue(globKey);
+ if (globValue == null) {
+ return null;
+ }
+ matches.addTransitive(globValue.getMatches());
+ }
+ }
+
+ PathFragment dirPathFragment = glob.getPackageId().getPackageFragment().getRelative(globSubdir);
+ RootedPath dirRootedPath = RootedPath.toRootedPath(globPkgLookupValue.getRoot(),
+ dirPathFragment);
+ if (containsGlobs(patternHead)) {
+ // Pattern contains globs, so a directory listing is required.
+ //
+ // Note that we have good reason to believe the directory exists: if this is the
+ // top-level directory of the package, the package's existence implies the directory's
+ // existence; if this is a lower-level directory in the package, then we got here from
+ // previous directory listings. Filesystem operations concurrent with build could mean the
+ // directory no longer exists, but DirectoryListingFunction handles that gracefully.
+ DirectoryListingValue listingValue = (DirectoryListingValue)
+ env.getValue(DirectoryListingValue.key(dirRootedPath));
+ if (listingValue == null) {
+ return null;
+ }
+
+ for (Dirent dirent : listingValue.getDirents()) {
+ Type direntType = dirent.getType();
+ String fileName = dirent.getName();
+
+ boolean isDirectory = (direntType == Dirent.Type.DIRECTORY);
+
+ if (!UnixGlob.matches(patternHead, fileName, regexPatternCache)) {
+ continue;
+ }
+
+ if (direntType == Dirent.Type.SYMLINK) {
+ // TODO(bazel-team): Consider extracting the symlink resolution logic.
+ // For symlinks, look up the corresponding FileValue. This ensures that if the symlink
+ // changes and "switches types" (say, from a file to a directory), this value will be
+ // invalidated.
+ RootedPath symlinkRootedPath = RootedPath.toRootedPath(globPkgLookupValue.getRoot(),
+ dirPathFragment.getRelative(fileName));
+ FileValue symlinkFileValue = (FileValue) env.getValue(FileValue.key(symlinkRootedPath));
+ if (symlinkFileValue == null) {
+ continue;
+ }
+ if (!symlinkFileValue.isSymlink()) {
+ throw new GlobFunctionException(new InconsistentFilesystemException(
+ "readdir and stat disagree about whether " + symlinkRootedPath.asPath()
+ + " is a symlink."), Transience.TRANSIENT);
+ }
+ isDirectory = symlinkFileValue.isDirectory();
+ }
+
+ String subdirPattern = "**".equals(patternHead) ? glob.getPattern() : patternTail;
+ addFile(fileName, glob, subdirPattern, patternTail == null, isDirectory,
+ matches, env);
+ }
+ } else {
+ // Pattern does not contain globs, so a direct stat is enough.
+ String fileName = patternHead;
+ RootedPath fileRootedPath = RootedPath.toRootedPath(globPkgLookupValue.getRoot(),
+ dirPathFragment.getRelative(fileName));
+ FileValue fileValue = (FileValue) env.getValue(FileValue.key(fileRootedPath));
+ if (fileValue == null) {
+ return null;
+ }
+ if (fileValue.exists()) {
+ addFile(fileName, glob, patternTail, patternTail == null,
+ fileValue.isDirectory(), matches, env);
+ }
+ }
+
+ if (env.valuesMissing()) {
+ return null;
+ }
+
+ NestedSet<PathFragment> matchesBuilt = matches.build();
+ // Use the same value to represent that we did not match anything.
+ if (matchesBuilt.isEmpty()) {
+ return GlobValue.EMPTY;
+ }
+ return new GlobValue(matchesBuilt);
+ }
+
+ /**
+ * Returns true if the given pattern contains globs.
+ */
+ private boolean containsGlobs(String pattern) {
+ return pattern.contains("*") || pattern.contains("?");
+ }
+
+ /**
+ * Includes the given file/directory in the glob.
+ *
+ * <p>{@code fileName} must exist.
+ *
+ * <p>{@code isDirectory} must be true iff the file is a directory.
+ *
+ * <p>{@code directResult} must be set if the file should be included in the result set
+ * directly rather than recursed into if it is a directory.
+ */
+ private void addFile(String fileName, GlobDescriptor glob, String subdirPattern,
+ boolean directResult, boolean isDirectory, NestedSetBuilder<PathFragment> matches,
+ Environment env) {
+ if (isDirectory && subdirPattern != null) {
+ // This is a directory, and the pattern covers files under that directory.
+ SkyKey subdirGlobKey = GlobValue.internalKey(glob.getPackageId(),
+ glob.getSubdir().getRelative(fileName), subdirPattern, glob.excludeDirs());
+ GlobValue subdirGlob = (GlobValue) env.getValue(subdirGlobKey);
+ if (subdirGlob == null) {
+ return;
+ }
+ matches.addTransitive(subdirGlob.getMatches());
+ }
+
+ if (directResult && !(isDirectory && glob.excludeDirs())) {
+ if (isDirectory) {
+ // TODO(bazel-team): Refactor. This is basically inlined code from the next recursion level.
+ // Ensure that subdirectories that contain other packages are not picked up.
+ PathFragment directory = glob.getPackageId().getPackageFragment()
+ .getRelative(glob.getSubdir()).getRelative(fileName);
+ PackageLookupValue pkgLookupValue = (PackageLookupValue)
+ env.getValue(PackageLookupValue.key(directory));
+ if (pkgLookupValue == null) {
+ return;
+ }
+ if (pkgLookupValue.packageExists()) {
+ // The file is a directory and contains another package.
+ return;
+ }
+ }
+ matches.add(glob.getSubdir().getRelative(fileName));
+ }
+ }
+
+ @Nullable
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return null;
+ }
+
+ /**
+ * Used to declare all the exception types that can be wrapped in the exception thrown by
+ * {@link GlobFunction#compute}.
+ */
+ private static final class GlobFunctionException extends SkyFunctionException {
+ public GlobFunctionException(InconsistentFilesystemException e, Transience transience) {
+ super(e, transience);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/GlobValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/GlobValue.java
new file mode 100644
index 0000000..6de0fbd
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/GlobValue.java
@@ -0,0 +1,132 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.packages.PackageIdentifier;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.lib.vfs.UnixGlob;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+/**
+ * A value corresponding to a glob.
+ */
+@Immutable
+@ThreadSafe
+final class GlobValue implements SkyValue {
+
+ static final GlobValue EMPTY = new GlobValue(
+ NestedSetBuilder.<PathFragment>emptySet(Order.STABLE_ORDER));
+
+ private final NestedSet<PathFragment> matches;
+
+ GlobValue(NestedSet<PathFragment> matches) {
+ this.matches = Preconditions.checkNotNull(matches);
+ }
+
+ /**
+ * Returns glob matches.
+ */
+ NestedSet<PathFragment> getMatches() {
+ return matches;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+ if (!(other instanceof GlobValue)) {
+ return false;
+ }
+ // shallowEquals() may fail to detect that two equivalent (according to toString())
+ // NestedSets are equal, but will always detect when two NestedSets are different.
+ // This makes this implementation of equals() overly strict, but we only call this
+ // method when doing change pruning, which can accept false negatives.
+ return getMatches().shallowEquals(((GlobValue) other).getMatches());
+ }
+
+ @Override
+ public int hashCode() {
+ return matches.shallowHashCode();
+ }
+
+ /**
+ * Constructs a {@link SkyKey} for a glob lookup. {@code packageName} is assumed to be an
+ * existing package. Trying to glob into a non-package is undefined behavior.
+ *
+ * @throws InvalidGlobPatternException if the pattern is not valid.
+ */
+ @ThreadSafe
+ static SkyKey key(PackageIdentifier packageId, String pattern, boolean excludeDirs)
+ throws InvalidGlobPatternException {
+ if (pattern.indexOf('?') != -1) {
+ throw new InvalidGlobPatternException(pattern, "wildcard ? forbidden");
+ }
+
+ String error = UnixGlob.checkPatternForError(pattern);
+ if (error != null) {
+ throw new InvalidGlobPatternException(pattern, error);
+ }
+
+ return internalKey(packageId, PathFragment.EMPTY_FRAGMENT, pattern, excludeDirs);
+ }
+
+ /**
+ * Constructs a {@link SkyKey} for a glob lookup.
+ *
+ * <p>Do not use outside {@code GlobFunction}.
+ */
+ @ThreadSafe
+ static SkyKey internalKey(PackageIdentifier packageId, PathFragment subdir, String pattern,
+ boolean excludeDirs) {
+ return new SkyKey(SkyFunctions.GLOB,
+ new GlobDescriptor(packageId, subdir, pattern, excludeDirs));
+ }
+
+ /**
+ * Constructs a {@link SkyKey} for a glob lookup.
+ *
+ * <p>Do not use outside {@code GlobFunction}.
+ */
+ @ThreadSafe
+ static SkyKey internalKey(GlobDescriptor glob, String subdirName) {
+ return internalKey(glob.packageId, glob.subdir.getRelative(subdirName),
+ glob.pattern, glob.excludeDirs);
+ }
+
+ /**
+ * An exception that indicates that a glob pattern is syntactically invalid.
+ */
+ @ThreadSafe
+ static final class InvalidGlobPatternException extends Exception {
+ private final String pattern;
+
+ InvalidGlobPatternException(String pattern, String error) {
+ super(error);
+ this.pattern = pattern;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("invalid glob pattern '%s': %s", pattern, getMessage());
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/IncompatibleViewException.java b/src/main/java/com/google/devtools/build/lib/skyframe/IncompatibleViewException.java
new file mode 100644
index 0000000..9d6f550
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/IncompatibleViewException.java
@@ -0,0 +1,26 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Preconditions;
+
+/**
+ * Thrown on {@link DiffAwareness#getDiff} to indicate that the given {@link DiffAwareness.View}s
+ * are incompatible with the {@link DiffAwareness} instance.
+ */
+public class IncompatibleViewException extends Exception {
+ public IncompatibleViewException(String msg) {
+ super(Preconditions.checkNotNull(msg));
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/InconsistentFilesystemException.java b/src/main/java/com/google/devtools/build/lib/skyframe/InconsistentFilesystemException.java
new file mode 100644
index 0000000..26cb02f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/InconsistentFilesystemException.java
@@ -0,0 +1,27 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+/**
+ * Used to indicate a filesystem inconsistency, e.g. file 'a/b' exists but directory 'a' doesn't
+ * exist. This generally means the result of the build is undefined but we shouldn't crash hard.
+ */
+public class InconsistentFilesystemException extends Exception {
+ public InconsistentFilesystemException(String inconsistencyMessage) {
+ super("Inconsistent filesystem operations. " + inconsistencyMessage + " The results of the "
+ + "build are not guaranteed to be correct. You should probably run 'blaze clean' and "
+ + "investigate the filesystem inconsistency (likely due to filesytem updates concurrent "
+ + "with the build)");
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/LocalDiffAwareness.java b/src/main/java/com/google/devtools/build/lib/skyframe/LocalDiffAwareness.java
new file mode 100644
index 0000000..861f89ac
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/LocalDiffAwareness.java
@@ -0,0 +1,329 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Function;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.HashBiMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.Constants;
+import com.google.devtools.build.lib.vfs.ModifiedFileSet;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.io.IOException;
+import java.nio.file.ClosedWatchServiceException;
+import java.nio.file.FileSystems;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.StandardWatchEventKinds;
+import java.nio.file.WatchEvent;
+import java.nio.file.WatchEvent.Kind;
+import java.nio.file.WatchKey;
+import java.nio.file.WatchService;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * File system watcher for local filesystems. It's able to provide a list of changed
+ * files between two consecutive calls. Uses the standard Java WatchService, which uses
+ * 'inotify' on Linux.
+ */
+public class LocalDiffAwareness implements DiffAwareness {
+
+ /** Factory for creating {@link LocalDiffAwareness} instances. */
+ public static class Factory implements DiffAwareness.Factory {
+ @Override
+ public DiffAwareness maybeCreate(com.google.devtools.build.lib.vfs.Path pathEntry) {
+ com.google.devtools.build.lib.vfs.Path resolvedPathEntry;
+ try {
+ resolvedPathEntry = pathEntry.resolveSymbolicLinks();
+ } catch (IOException e) {
+ return null;
+ }
+ PathFragment resolvedPathEntryFragment = resolvedPathEntry.asFragment();
+ // There's no good way to automatically detect network file systems. We rely on a blacklist
+ // for now (and maybe add a command-line option in the future?).
+ for (String prefix : Constants.WATCHFS_BLACKLIST) {
+ if (resolvedPathEntryFragment.startsWith(new PathFragment(prefix))) {
+ return null;
+ }
+ }
+
+ WatchService watchService;
+ try {
+ watchService = FileSystems.getDefault().newWatchService();
+ } catch (IOException e) {
+ return null;
+ }
+ return new LocalDiffAwareness(resolvedPathEntryFragment.toString(),
+ watchService);
+ }
+ }
+
+ private int numGetCurrentViewCalls = 0;
+
+ /**
+ * Bijection from WatchKey to the (absolute) Path being watched. WatchKeys don't have this
+ * functionality built-in so we do it ourselves.
+ */
+ private final HashBiMap<WatchKey, Path> watchKeyToDirBiMap = HashBiMap.create();
+
+ /** Root directory to watch. This is an absolute path. */
+ private final Path watchRootPath;
+
+ /** Every directory is registered under this watch service. */
+ private WatchService watchService;
+
+ private LocalDiffAwareness(String watchRoot, WatchService watchService) {
+ this.watchRootPath = FileSystems.getDefault().getPath(watchRoot);
+ this.watchService = watchService;
+ }
+
+ /**
+ * The WatchService is inherently sequential and side-effectful, so we enforce this by only
+ * supporting {@link #getDiff} calls that happen to be sequential.
+ */
+ private static class SequentialView implements DiffAwareness.View {
+ private final LocalDiffAwareness owner;
+ private final int position;
+ private final Set<Path> modifiedAbsolutePaths;
+
+ public SequentialView(LocalDiffAwareness owner, int position, Set<Path> modifiedAbsolutePaths) {
+ this.owner = owner;
+ this.position = position;
+ this.modifiedAbsolutePaths = modifiedAbsolutePaths;
+ }
+
+ public static boolean areInSequence(SequentialView oldView, SequentialView newView) {
+ return oldView.owner == newView.owner && (oldView.position + 1) == newView.position;
+ }
+ }
+
+ @Override
+ public SequentialView getCurrentView() throws BrokenDiffAwarenessException {
+ Set<Path> modifiedAbsolutePaths;
+ if (numGetCurrentViewCalls++ == 0) {
+ try {
+ registerSubDirectoriesAndReturnContents(watchRootPath);
+ } catch (IOException e) {
+ close();
+ throw new BrokenDiffAwarenessException(
+ "Error encountered with local file system watcher " + e);
+ }
+ modifiedAbsolutePaths = ImmutableSet.of();
+ } else {
+ try {
+ modifiedAbsolutePaths = collectChanges();
+ } catch (BrokenDiffAwarenessException e) {
+ close();
+ throw e;
+ } catch (IOException e) {
+ close();
+ throw new BrokenDiffAwarenessException(
+ "Error encountered with local file system watcher " + e);
+ } catch (ClosedWatchServiceException e) {
+ throw new BrokenDiffAwarenessException(
+ "Internal error with the local file system watcher " + e);
+ }
+ }
+ return new SequentialView(this, numGetCurrentViewCalls, modifiedAbsolutePaths);
+ }
+
+ @Override
+ public ModifiedFileSet getDiff(View oldView, View newView)
+ throws IncompatibleViewException, BrokenDiffAwarenessException {
+ SequentialView oldSequentialView;
+ SequentialView newSequentialView;
+ try {
+ oldSequentialView = (SequentialView) oldView;
+ newSequentialView = (SequentialView) newView;
+ } catch (ClassCastException e) {
+ throw new IncompatibleViewException("Given views are not from LocalDiffAwareness");
+ }
+ if (!SequentialView.areInSequence(oldSequentialView, newSequentialView)) {
+ return ModifiedFileSet.EVERYTHING_MODIFIED;
+ }
+ return ModifiedFileSet.builder()
+ .modifyAll(Iterables.transform(newSequentialView.modifiedAbsolutePaths,
+ nioAbsolutePathToPathFragment))
+ .build();
+ }
+
+ @Override
+ public void close() {
+ try {
+ watchService.close();
+ } catch (IOException ignored) {
+ // Nothing we can do here.
+ }
+ }
+
+ /** Converts java.nio.file.Path objects to vfs.PathFragment. */
+ private final Function<Path, PathFragment> nioAbsolutePathToPathFragment =
+ new Function<Path, PathFragment>() {
+ @Override
+ public PathFragment apply(Path input) {
+ Preconditions.checkArgument(input.startsWith(watchRootPath), "%s %s", input,
+ watchRootPath);
+ return new PathFragment(watchRootPath.relativize(input).toString());
+ }
+ };
+
+ /** Returns the changed files caught by the watch service. */
+ private Set<Path> collectChanges() throws BrokenDiffAwarenessException, IOException {
+ Set<Path> createdFilesAndDirectories = new HashSet<Path>();
+ Set<Path> deletedOrModifiedFilesAndDirectories = new HashSet<Path>();
+ Set<Path> deletedTrackedDirectories = new HashSet<Path>();
+
+ WatchKey watchKey;
+ while ((watchKey = watchService.poll()) != null) {
+ Path dir = watchKeyToDirBiMap.get(watchKey);
+ Preconditions.checkArgument(dir != null);
+
+ // We replay all the events for this watched directory in chronological order and
+ // construct the diff of this directory since the last #collectChanges call.
+ for (WatchEvent<?> event : watchKey.pollEvents()) {
+ Kind<?> kind = event.kind();
+ if (kind == StandardWatchEventKinds.OVERFLOW) {
+ // TODO(bazel-team): find out when an overflow might happen, and maybe handle it more
+ // gently.
+ throw new BrokenDiffAwarenessException("Overflow when watching local filesystem for "
+ + "changes");
+ }
+ if (event.context() == null) {
+ // The WatchService documentation mentions that WatchEvent#context may return null, but
+ // doesn't explain how/why it would do so. Looking at the implementation, it only
+ // happens on an overflow event. But we make no assumptions about that implementation
+ // detail here.
+ throw new BrokenDiffAwarenessException("Insufficient information from local file system "
+ + "watcher");
+ }
+ // For the events we've registered, the context given is a relative path.
+ Path relativePath = (Path) event.context();
+ Path path = dir.resolve(relativePath);
+ Preconditions.checkState(path.isAbsolute(), path);
+ if (kind == StandardWatchEventKinds.ENTRY_CREATE) {
+ createdFilesAndDirectories.add(path);
+ deletedOrModifiedFilesAndDirectories.remove(path);
+ } else if (kind == StandardWatchEventKinds.ENTRY_DELETE) {
+ createdFilesAndDirectories.remove(path);
+ deletedOrModifiedFilesAndDirectories.add(path);
+ if (watchKeyToDirBiMap.containsValue(path)) {
+ // If the deleted directory has children, then there will also be events for the
+ // WatchKey of the directory itself. WatchService#poll doesn't specify the order in
+ // which WatchKeys are returned, so the key for the directory itself may be processed
+ // *after* the current key (the parent of the deleted directory), and so we don't want
+ // to remove the deleted directory from our bimap just yet.
+ //
+ // For example, suppose we have the file '/root/a/foo.txt' and are watching the
+ // directories '/root' and '/root/a'. If the directory '/root/a' gets deleted then the
+ // following is a valid sequence of events by key.
+ //
+ // WatchKey '/root/'
+ // WatchEvent EVENT_MODIFY 'a'
+ // WatchEvent EVENT_DELETE 'a'
+ // WatchKey '/root/a'
+ // WatchEvent EVENT_DELETE 'foo.txt'
+ deletedTrackedDirectories.add(path);
+ }
+ } else if (kind == StandardWatchEventKinds.ENTRY_MODIFY) {
+ // If a file was created and then modified, then the net diff is that it was
+ // created.
+ if (!createdFilesAndDirectories.contains(path)) {
+ deletedOrModifiedFilesAndDirectories.add(path);
+ }
+ }
+ }
+
+ if (!watchKey.reset()) {
+ // Watcher got deleted, directory no longer valid.
+ watchKeyToDirBiMap.remove(watchKey);
+ }
+ }
+
+ for (Path path : deletedTrackedDirectories) {
+ WatchKey staleKey = watchKeyToDirBiMap.inverse().get(path);
+ watchKeyToDirBiMap.remove(staleKey);
+ }
+ if (watchKeyToDirBiMap.isEmpty()) {
+ // No more directories to watch, something happened the root directory being watched.
+ throw new IOException("Root directory " + watchRootPath + " became inaccessible.");
+ }
+
+ Set<Path> changedPaths = new HashSet<Path>();
+ for (Path path : createdFilesAndDirectories) {
+ if (Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) {
+ // This is a new directory, so changes to it since its creation have not been watched.
+ // We manually traverse the directory tree to register all the new subdirectories and find
+ // all the new subdirectories and files.
+ changedPaths.addAll(registerSubDirectoriesAndReturnContents(path));
+ } else {
+ changedPaths.add(path);
+ }
+ }
+ changedPaths.addAll(deletedOrModifiedFilesAndDirectories);
+ return changedPaths;
+ }
+
+ /**
+ * Traverses directory tree to register subdirectories. Returns all paths traversed (as absolute
+ * paths).
+ */
+ private Set<Path> registerSubDirectoriesAndReturnContents(Path rootDir) throws IOException {
+ Set<Path> visitedAbsolutePaths = new HashSet<Path>();
+ // Note that this does not follow symlinks.
+ Files.walkFileTree(rootDir, new WatcherFileVisitor(visitedAbsolutePaths));
+ return visitedAbsolutePaths;
+ }
+
+ /** File visitor used by Files.walkFileTree() upon traversing subdirectories. */
+ private class WatcherFileVisitor extends SimpleFileVisitor<Path> {
+
+ private final Set<Path> visitedAbsolutePaths;
+
+ private WatcherFileVisitor(Set<Path> visitedPaths) {
+ this.visitedAbsolutePaths = visitedPaths;
+ }
+
+ @Override
+ public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) {
+ Preconditions.checkState(path.isAbsolute(), path);
+ visitedAbsolutePaths.add(path);
+ return FileVisitResult.CONTINUE;
+ }
+
+ @Override
+ public FileVisitResult preVisitDirectory(Path path, BasicFileAttributes attrs)
+ throws IOException {
+ // It's important that we register the directory before we visit its children. This way we
+ // are guaranteed to see new files/directories either on this #getDiff or the next one.
+ // Otherwise, e.g., an intra-build creation of a child directory will be forever missed if it
+ // happens before the directory is listed as part of the visitation.
+ WatchKey key = path.register(watchService,
+ StandardWatchEventKinds.ENTRY_CREATE,
+ StandardWatchEventKinds.ENTRY_MODIFY,
+ StandardWatchEventKinds.ENTRY_DELETE);
+ Preconditions.checkState(path.isAbsolute(), path);
+ visitedAbsolutePaths.add(path);
+ watchKeyToDirBiMap.put(key, path);
+ return FileVisitResult.CONTINUE;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/MutableSupplier.java b/src/main/java/com/google/devtools/build/lib/skyframe/MutableSupplier.java
new file mode 100644
index 0000000..86de11d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/MutableSupplier.java
@@ -0,0 +1,46 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Supplier;
+
+/**
+ * Supplier whose value can be changed by clients who have a reference to it as a MutableSupplier.
+ * Unlike an {@code AtomicReference}, clients who are passed a MutableSupplier as a Supplier cannot
+ * change its value without a reckless cast.
+ */
+public class MutableSupplier<T> implements Supplier<T> {
+ private T val;
+
+ @Override
+ public T get() {
+ return val;
+ }
+
+ /**
+ * Sets the value of the object supplied. Do not cast a Supplier to a MutableSupplier in order to
+ * call this method!
+ */
+ public void set(T newVal) {
+ val = newVal;
+ }
+
+ @SuppressWarnings("deprecation") // MoreObjects.toStringHelper() is not in Guava
+ @Override
+ public String toString() {
+ return Objects.toStringHelper(getClass())
+ .add("val", val).toString();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/PackageFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/PackageFunction.java
new file mode 100644
index 0000000..2404b99
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/PackageFunction.java
@@ -0,0 +1,809 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.common.eventbus.EventBus;
+import com.google.devtools.build.lib.Constants;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.events.Reporter;
+import com.google.devtools.build.lib.events.StoredEventHandler;
+import com.google.devtools.build.lib.packages.BuildFileContainsErrorsException;
+import com.google.devtools.build.lib.packages.BuildFileNotFoundException;
+import com.google.devtools.build.lib.packages.CachingPackageLocator;
+import com.google.devtools.build.lib.packages.InvalidPackageNameException;
+import com.google.devtools.build.lib.packages.NoSuchPackageException;
+import com.google.devtools.build.lib.packages.Package;
+import com.google.devtools.build.lib.packages.PackageFactory;
+import com.google.devtools.build.lib.packages.PackageFactory.Globber;
+import com.google.devtools.build.lib.packages.PackageIdentifier;
+import com.google.devtools.build.lib.packages.PackageIdentifier.RepositoryName;
+import com.google.devtools.build.lib.packages.PackageLoadedEvent;
+import com.google.devtools.build.lib.packages.Preprocessor;
+import com.google.devtools.build.lib.packages.RuleVisibility;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.profiler.Profiler;
+import com.google.devtools.build.lib.profiler.ProfilerTask;
+import com.google.devtools.build.lib.skyframe.ASTFileLookupValue.ASTLookupInputException;
+import com.google.devtools.build.lib.skyframe.GlobValue.InvalidGlobPatternException;
+import com.google.devtools.build.lib.skyframe.SkylarkImportLookupFunction.SkylarkImportFailedException;
+import com.google.devtools.build.lib.syntax.BuildFileAST;
+import com.google.devtools.build.lib.syntax.EvalException;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.syntax.ParserInputSource;
+import com.google.devtools.build.lib.syntax.SkylarkEnvironment;
+import com.google.devtools.build.lib.syntax.Statement;
+import com.google.devtools.build.lib.util.Clock;
+import com.google.devtools.build.lib.util.JavaClock;
+import com.google.devtools.build.lib.util.Pair;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.lib.vfs.RootedPath;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyFunctionException;
+import com.google.devtools.build.skyframe.SkyFunctionException.Transience;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+import com.google.devtools.build.skyframe.ValueOrException3;
+import com.google.devtools.build.skyframe.ValueOrException4;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+import javax.annotation.Nullable;
+
+/**
+ * A SkyFunction for {@link PackageValue}s.
+ */
+public class PackageFunction implements SkyFunction {
+
+ private final EventHandler reporter;
+ private final PackageFactory packageFactory;
+ private final CachingPackageLocator packageLocator;
+ private final ConcurrentMap<PackageIdentifier, Package.LegacyBuilder> packageFunctionCache;
+ private final AtomicBoolean showLoadingProgress;
+ private final AtomicReference<EventBus> eventBus;
+ private final AtomicInteger numPackagesLoaded;
+ private final Profiler profiler = Profiler.instance();
+
+ private static final PathFragment PRELUDE_FILE_FRAGMENT =
+ new PathFragment(Constants.PRELUDE_FILE_DEPOT_RELATIVE_PATH);
+
+ static final String DEFAULTS_PACKAGE_NAME = "tools/defaults";
+ public static final String EXTERNAL_PACKAGE_NAME = "external";
+
+ static {
+ Preconditions.checkArgument(!PRELUDE_FILE_FRAGMENT.isAbsolute());
+ }
+
+ public PackageFunction(Reporter reporter, PackageFactory packageFactory,
+ CachingPackageLocator pkgLocator, AtomicBoolean showLoadingProgress,
+ ConcurrentMap<PackageIdentifier, Package.LegacyBuilder> packageFunctionCache,
+ AtomicReference<EventBus> eventBus, AtomicInteger numPackagesLoaded) {
+ this.reporter = reporter;
+
+ this.packageFactory = packageFactory;
+ this.packageLocator = pkgLocator;
+ this.showLoadingProgress = showLoadingProgress;
+ this.packageFunctionCache = packageFunctionCache;
+ this.eventBus = eventBus;
+ this.numPackagesLoaded = numPackagesLoaded;
+ }
+
+ private static void maybeThrowFilesystemInconsistency(String packageName,
+ Exception skyframeException, boolean packageWasInError)
+ throws InternalInconsistentFilesystemException {
+ if (!packageWasInError) {
+ throw new InternalInconsistentFilesystemException(packageName, "Encountered error '"
+ + skyframeException.getMessage() + "' but didn't encounter it when doing the same thing "
+ + "earlier in the build");
+ }
+ }
+
+ /**
+ * Marks the given dependencies, and returns those already present. Ignores any exception
+ * thrown while building the dependency, except for filesystem inconsistencies.
+ *
+ * <p>We need to mark dependencies implicitly used by the legacy package loading code, but we
+ * don't care about any skyframe errors since the package knows whether it's in error or not.
+ */
+ private static Pair<? extends Map<PathFragment, PackageLookupValue>, Boolean>
+ getPackageLookupDepsAndPropagateInconsistentFilesystemExceptions(String packageName,
+ Iterable<SkyKey> depKeys, Environment env, boolean packageWasInError)
+ throws InternalInconsistentFilesystemException {
+ Preconditions.checkState(
+ Iterables.all(depKeys, SkyFunctions.isSkyFunction(SkyFunctions.PACKAGE_LOOKUP)), depKeys);
+ boolean packageShouldBeInError = packageWasInError;
+ ImmutableMap.Builder<PathFragment, PackageLookupValue> builder = ImmutableMap.builder();
+ for (Map.Entry<SkyKey, ValueOrException3<BuildFileNotFoundException,
+ InconsistentFilesystemException, FileSymlinkCycleException>> entry :
+ env.getValuesOrThrow(depKeys, BuildFileNotFoundException.class,
+ InconsistentFilesystemException.class,
+ FileSymlinkCycleException.class).entrySet()) {
+ PathFragment pkgName = ((PackageIdentifier) entry.getKey().argument()).getPackageFragment();
+ try {
+ PackageLookupValue value = (PackageLookupValue) entry.getValue().get();
+ if (value != null) {
+ builder.put(pkgName, value);
+ }
+ } catch (BuildFileNotFoundException e) {
+ maybeThrowFilesystemInconsistency(packageName, e, packageWasInError);
+ } catch (InconsistentFilesystemException e) {
+ throw new InternalInconsistentFilesystemException(packageName, e);
+ } catch (FileSymlinkCycleException e) {
+ // Legacy doesn't detect symlink cycles.
+ packageShouldBeInError = true;
+ }
+ }
+ return Pair.of(builder.build(), packageShouldBeInError);
+ }
+
+ private static boolean markFileDepsAndPropagateInconsistentFilesystemExceptions(
+ String packageName, Iterable<SkyKey> depKeys, Environment env, boolean packageWasInError)
+ throws InternalInconsistentFilesystemException {
+ Preconditions.checkState(
+ Iterables.all(depKeys, SkyFunctions.isSkyFunction(SkyFunctions.FILE)), depKeys);
+ boolean packageShouldBeInError = packageWasInError;
+ for (Map.Entry<SkyKey, ValueOrException3<IOException, FileSymlinkCycleException,
+ InconsistentFilesystemException>> entry : env.getValuesOrThrow(depKeys, IOException.class,
+ FileSymlinkCycleException.class, InconsistentFilesystemException.class).entrySet()) {
+ try {
+ entry.getValue().get();
+ } catch (IOException e) {
+ maybeThrowFilesystemInconsistency(packageName, e, packageWasInError);
+ } catch (FileSymlinkCycleException e) {
+ // Legacy doesn't detect symlink cycles.
+ packageShouldBeInError = true;
+ } catch (InconsistentFilesystemException e) {
+ throw new InternalInconsistentFilesystemException(packageName, e);
+ }
+ }
+ return packageShouldBeInError;
+ }
+
+ private static boolean markGlobDepsAndPropagateInconsistentFilesystemExceptions(
+ String packageName, Iterable<SkyKey> depKeys, Environment env, boolean packageWasInError)
+ throws InternalInconsistentFilesystemException {
+ Preconditions.checkState(
+ Iterables.all(depKeys, SkyFunctions.isSkyFunction(SkyFunctions.GLOB)), depKeys);
+ boolean packageShouldBeInError = packageWasInError;
+ for (Map.Entry<SkyKey, ValueOrException4<IOException, BuildFileNotFoundException,
+ FileSymlinkCycleException, InconsistentFilesystemException>> entry :
+ env.getValuesOrThrow(depKeys, IOException.class, BuildFileNotFoundException.class,
+ FileSymlinkCycleException.class, InconsistentFilesystemException.class).entrySet()) {
+ try {
+ entry.getValue().get();
+ } catch (IOException | BuildFileNotFoundException e) {
+ maybeThrowFilesystemInconsistency(packageName, e, packageWasInError);
+ } catch (FileSymlinkCycleException e) {
+ // Legacy doesn't detect symlink cycles.
+ packageShouldBeInError = true;
+ } catch (InconsistentFilesystemException e) {
+ throw new InternalInconsistentFilesystemException(packageName, e);
+ }
+ }
+ return packageShouldBeInError;
+ }
+
+ /**
+ * Marks dependencies implicitly used by legacy package loading code, after the fact. Note that
+ * the given package might already be in error.
+ *
+ * <p>Any skyframe exceptions encountered here are ignored, as similar errors should have
+ * already been encountered by legacy package loading (if not, then the filesystem is
+ * inconsistent).
+ */
+ private static boolean markDependenciesAndPropagateInconsistentFilesystemExceptions(
+ Package pkg, Environment env, Collection<Pair<String, Boolean>> globPatterns,
+ Map<Label, Path> subincludes) throws InternalInconsistentFilesystemException {
+ boolean packageShouldBeInError = pkg.containsErrors();
+
+ // TODO(bazel-team): This means that many packages will have to be preprocessed twice. Ouch!
+ // We need a better continuation mechanism to avoid repeating work. [skyframe-loading]
+
+ // TODO(bazel-team): It would be preferable to perform I/O from the package preprocessor via
+ // Skyframe rather than add (potentially incomplete) dependencies after the fact.
+ // [skyframe-loading]
+
+ Set<SkyKey> subincludePackageLookupDepKeys = Sets.newHashSet();
+ for (Label label : pkg.getSubincludeLabels()) {
+ // Declare a dependency on the package lookup for the package giving access to the label.
+ subincludePackageLookupDepKeys.add(PackageLookupValue.key(label.getPackageFragment()));
+ }
+ Pair<? extends Map<PathFragment, PackageLookupValue>, Boolean> subincludePackageLookupResult =
+ getPackageLookupDepsAndPropagateInconsistentFilesystemExceptions(pkg.getName(),
+ subincludePackageLookupDepKeys, env, pkg.containsErrors());
+ Map<PathFragment, PackageLookupValue> subincludePackageLookupDeps =
+ subincludePackageLookupResult.getFirst();
+ packageShouldBeInError = subincludePackageLookupResult.getSecond();
+ List<SkyKey> subincludeFileDepKeys = Lists.newArrayList();
+ for (Entry<Label, Path> subincludeEntry : subincludes.entrySet()) {
+ // Ideally, we would have a direct dependency on the target with the given label, but then
+ // subincluding a file from the same package will cause a dependency cycle, since targets
+ // depend on their containing packages.
+ Label label = subincludeEntry.getKey();
+ PackageLookupValue subincludePackageLookupValue =
+ subincludePackageLookupDeps.get(label.getPackageFragment());
+ if (subincludePackageLookupValue != null) {
+ // Declare a dependency on the actual file that was subincluded.
+ Path subincludeFilePath = subincludeEntry.getValue();
+ if (subincludeFilePath != null) {
+ if (!subincludePackageLookupValue.packageExists()) {
+ // Legacy blaze puts a non-null path when only when the package does indeed exist.
+ throw new InternalInconsistentFilesystemException(pkg.getName(), String.format(
+ "Unexpected package in %s. Was it modified during the build?", subincludeFilePath));
+ }
+ // Sanity check for consistency of Skyframe and legacy blaze.
+ Path subincludeFilePathSkyframe =
+ subincludePackageLookupValue.getRoot().getRelative(label.toPathFragment());
+ if (!subincludeFilePathSkyframe.equals(subincludeFilePath)) {
+ throw new InternalInconsistentFilesystemException(pkg.getName(), String.format(
+ "Inconsistent package location for %s: '%s' vs '%s'. "
+ + "Was the source tree modified during the build?",
+ label.getPackageFragment(), subincludeFilePathSkyframe, subincludeFilePath));
+ }
+ // The actual file may be under a different package root than the package being
+ // constructed.
+ SkyKey subincludeSkyKey =
+ FileValue.key(RootedPath.toRootedPath(subincludePackageLookupValue.getRoot(),
+ subincludeFilePath));
+ subincludeFileDepKeys.add(subincludeSkyKey);
+ }
+ }
+ }
+ packageShouldBeInError = markFileDepsAndPropagateInconsistentFilesystemExceptions(
+ pkg.getName(), subincludeFileDepKeys, env, pkg.containsErrors());
+ // Another concern is a subpackage cutting off the subinclude label, but this is already
+ // handled by the legacy package loading code which calls into our SkyframePackageLocator.
+
+ // TODO(bazel-team): In the long term, we want to actually resolve the glob patterns within
+ // Skyframe. For now, just logging the glob requests provides correct incrementality and
+ // adequate performance.
+ PackageIdentifier packageId = pkg.getPackageIdentifier();
+ List<SkyKey> globDepKeys = Lists.newArrayList();
+ for (Pair<String, Boolean> globPattern : globPatterns) {
+ String pattern = globPattern.getFirst();
+ boolean excludeDirs = globPattern.getSecond();
+ SkyKey globSkyKey;
+ try {
+ globSkyKey = GlobValue.key(packageId, pattern, excludeDirs);
+ } catch (InvalidGlobPatternException e) {
+ // Globs that make it to pkg.getGlobPatterns() should already be filtered for errors.
+ throw new IllegalStateException(e);
+ }
+ globDepKeys.add(globSkyKey);
+ }
+ packageShouldBeInError = markGlobDepsAndPropagateInconsistentFilesystemExceptions(
+ pkg.getName(), globDepKeys, env, pkg.containsErrors());
+ return packageShouldBeInError;
+ }
+
+ /**
+ * Adds a dependency on the WORKSPACE file, representing it as a special type of package.
+ * @throws PackageFunctionException if there is an error computing the workspace file or adding
+ * its rules to the //external package.
+ */
+ private SkyValue getExternalPackage(Environment env, Path packageLookupPath)
+ throws PackageFunctionException {
+ RootedPath workspacePath = RootedPath.toRootedPath(
+ packageLookupPath, new PathFragment("WORKSPACE"));
+ SkyKey workspaceKey = WorkspaceFileValue.key(workspacePath);
+ WorkspaceFileValue workspace = null;
+ try {
+ workspace = (WorkspaceFileValue) env.getValueOrThrow(workspaceKey, IOException.class,
+ FileSymlinkCycleException.class, InconsistentFilesystemException.class,
+ EvalException.class);
+ } catch (IOException | FileSymlinkCycleException | InconsistentFilesystemException
+ | EvalException e) {
+ throw new PackageFunctionException(new BadWorkspaceFileException(e.getMessage()),
+ Transience.PERSISTENT);
+ }
+ if (workspace == null) {
+ return null;
+ }
+
+ Package pkg = workspace.getPackage();
+ Event.replayEventsOn(env.getListener(), pkg.getEvents());
+ if (pkg.containsErrors()) {
+ throw new PackageFunctionException(new BuildFileContainsErrorsException("external",
+ "Package 'external' contains errors"),
+ pkg.containsTemporaryErrors() ? Transience.TRANSIENT : Transience.PERSISTENT);
+ }
+
+ return new PackageValue(pkg);
+ }
+
+ @Override
+ public SkyValue compute(SkyKey key, Environment env) throws PackageFunctionException,
+ InterruptedException {
+ PackageIdentifier packageId = (PackageIdentifier) key.argument();
+ PathFragment packageNameFragment = packageId.getPackageFragment();
+ String packageName = packageNameFragment.getPathString();
+
+ SkyKey packageLookupKey = PackageLookupValue.key(packageId);
+ PackageLookupValue packageLookupValue;
+ try {
+ packageLookupValue = (PackageLookupValue)
+ env.getValueOrThrow(packageLookupKey, BuildFileNotFoundException.class,
+ InconsistentFilesystemException.class);
+ } catch (BuildFileNotFoundException e) {
+ throw new PackageFunctionException(e, Transience.PERSISTENT);
+ } catch (InconsistentFilesystemException e) {
+ // This error is not transient from the perspective of the PackageFunction.
+ throw new PackageFunctionException(
+ new InternalInconsistentFilesystemException(packageName, e), Transience.PERSISTENT);
+ }
+ if (packageLookupValue == null) {
+ return null;
+ }
+
+ if (!packageLookupValue.packageExists()) {
+ switch (packageLookupValue.getErrorReason()) {
+ case NO_BUILD_FILE:
+ case DELETED_PACKAGE:
+ case NO_EXTERNAL_PACKAGE:
+ throw new PackageFunctionException(new BuildFileNotFoundException(packageName,
+ packageLookupValue.getErrorMsg()), Transience.PERSISTENT);
+ case INVALID_PACKAGE_NAME:
+ throw new PackageFunctionException(new InvalidPackageNameException(packageName,
+ packageLookupValue.getErrorMsg()), Transience.PERSISTENT);
+ default:
+ // We should never get here.
+ Preconditions.checkState(false);
+ }
+ }
+
+ if (packageName.equals(EXTERNAL_PACKAGE_NAME)) {
+ return getExternalPackage(env, packageLookupValue.getRoot());
+ }
+
+ RootedPath buildFileRootedPath = RootedPath.toRootedPath(packageLookupValue.getRoot(),
+ packageNameFragment.getChild("BUILD"));
+ FileValue buildFileValue;
+ try {
+ buildFileValue = (FileValue) env.getValueOrThrow(FileValue.key(buildFileRootedPath),
+ IOException.class, FileSymlinkCycleException.class,
+ InconsistentFilesystemException.class);
+ } catch (IOException | FileSymlinkCycleException | InconsistentFilesystemException e) {
+ throw new IllegalStateException("Package lookup succeeded but encountered error when "
+ + "getting FileValue for BUILD file directly.", e);
+ }
+ if (buildFileValue == null) {
+ return null;
+ }
+ Preconditions.checkState(buildFileValue.exists(),
+ "Package lookup succeeded but BUILD file doesn't exist");
+ Path buildFilePath = buildFileRootedPath.asPath();
+
+ String replacementContents = null;
+ if (packageName.equals(DEFAULTS_PACKAGE_NAME)) {
+ replacementContents = PrecomputedValue.DEFAULTS_PACKAGE_CONTENTS.get(env);
+ if (replacementContents == null) {
+ return null;
+ }
+ }
+
+ RuleVisibility defaultVisibility = PrecomputedValue.DEFAULT_VISIBILITY.get(env);
+ if (defaultVisibility == null) {
+ return null;
+ }
+
+ ASTFileLookupValue astLookupValue = null;
+ SkyKey astLookupKey = null;
+ try {
+ astLookupKey = ASTFileLookupValue.key(PRELUDE_FILE_FRAGMENT);
+ } catch (ASTLookupInputException e) {
+ // There's a static check ensuring that PRELUDE_FILE_FRAGMENT is relative.
+ throw new IllegalStateException(e);
+ }
+ try {
+ astLookupValue = (ASTFileLookupValue) env.getValueOrThrow(astLookupKey,
+ ErrorReadingSkylarkExtensionException.class, InconsistentFilesystemException.class);
+ } catch (ErrorReadingSkylarkExtensionException | InconsistentFilesystemException e) {
+ throw new PackageFunctionException(new BadPreludeFileException(packageName, e.getMessage()),
+ Transience.PERSISTENT);
+ }
+ if (astLookupValue == null) {
+ return null;
+ }
+ List<Statement> preludeStatements = astLookupValue == ASTFileLookupValue.NO_FILE
+ ? ImmutableList.<Statement>of() : astLookupValue.getAST().getStatements();
+
+ // Load the BUILD file AST and handle Skylark dependencies. This way BUILD files are
+ // only loaded twice if there are unavailable Skylark or package dependencies or an
+ // IOException occurs. Note that the BUILD files are still parsed two times.
+ ParserInputSource inputSource;
+ try {
+ if (showLoadingProgress.get() && !packageFunctionCache.containsKey(packageId)) {
+ // TODO(bazel-team): don't duplicate the loading message if there are unavailable
+ // Skylark dependencies.
+ reporter.handle(Event.progress("Loading package: " + packageName));
+ }
+ inputSource = ParserInputSource.create(buildFilePath);
+ } catch (IOException e) {
+ env.getListener().handle(Event.error(Location.fromFile(buildFilePath), e.getMessage()));
+ // Note that we did this work, so we should conservatively report this error as transient.
+ throw new PackageFunctionException(new BuildFileContainsErrorsException(
+ packageName, e.getMessage()), Transience.TRANSIENT);
+ }
+ SkylarkImportResult importResult = fetchImportsFromBuildFile(
+ buildFilePath, packageId.getRepository(), preludeStatements, inputSource, packageName, env);
+ if (importResult == null) {
+ return null;
+ }
+
+ Package.LegacyBuilder legacyPkgBuilder = loadPackage(inputSource, replacementContents,
+ packageId, buildFilePath, defaultVisibility, preludeStatements, importResult);
+ legacyPkgBuilder.buildPartial();
+ try {
+ handleLabelsCrossingSubpackagesAndPropagateInconsistentFilesystemExceptions(
+ packageLookupValue.getRoot(), packageId, legacyPkgBuilder, env);
+ } catch (InternalInconsistentFilesystemException e) {
+ packageFunctionCache.remove(packageId);
+ throw new PackageFunctionException(e,
+ e.isTransient() ? Transience.TRANSIENT : Transience.PERSISTENT);
+ }
+ if (env.valuesMissing()) {
+ // The package we just loaded will be in the {@code packageFunctionCache} next when this
+ // SkyFunction is called again.
+ return null;
+ }
+ Collection<Pair<String, Boolean>> globPatterns = legacyPkgBuilder.getGlobPatterns();
+ Map<Label, Path> subincludes = legacyPkgBuilder.getSubincludes();
+ Package pkg = legacyPkgBuilder.finishBuild();
+ Event.replayEventsOn(env.getListener(), pkg.getEvents());
+ boolean packageShouldBeConsideredInError = pkg.containsErrors();
+ try {
+ packageShouldBeConsideredInError =
+ markDependenciesAndPropagateInconsistentFilesystemExceptions(pkg, env,
+ globPatterns, subincludes);
+ } catch (InternalInconsistentFilesystemException e) {
+ packageFunctionCache.remove(packageId);
+ throw new PackageFunctionException(e,
+ e.isTransient() ? Transience.TRANSIENT : Transience.PERSISTENT);
+ }
+
+ if (env.valuesMissing()) {
+ return null;
+ }
+ // We know this SkyFunction will not be called again, so we can remove the cache entry.
+ packageFunctionCache.remove(packageId);
+
+ if (packageShouldBeConsideredInError) {
+ throw new PackageFunctionException(new BuildFileContainsErrorsException(pkg,
+ "Package '" + packageName + "' contains errors"),
+ pkg.containsTemporaryErrors() ? Transience.TRANSIENT : Transience.PERSISTENT);
+ }
+ return new PackageValue(pkg);
+ }
+
+ private SkylarkImportResult fetchImportsFromBuildFile(Path buildFilePath, RepositoryName repo,
+ List<Statement> preludeStatements, ParserInputSource inputSource,
+ String packageName, Environment env) throws PackageFunctionException {
+ StoredEventHandler eventHandler = new StoredEventHandler();
+ BuildFileAST buildFileAST = BuildFileAST.parseBuildFile(
+ inputSource, preludeStatements, eventHandler, null, true);
+
+ if (eventHandler.hasErrors()) {
+ // In case of Python preprocessing, errors have already been reported (see checkSyntax).
+ // In other cases, errors will be reported later.
+ // TODO(bazel-team): maybe we could get rid of checkSyntax and always report errors here?
+ return new SkylarkImportResult(
+ ImmutableMap.<PathFragment, SkylarkEnvironment>of(),
+ ImmutableList.<Label>of());
+ }
+
+ ImmutableCollection<PathFragment> imports = buildFileAST.getImports();
+ Map<PathFragment, SkylarkEnvironment> importMap = new HashMap<>();
+ ImmutableList.Builder<SkylarkFileDependency> fileDependencies = ImmutableList.builder();
+ try {
+ for (PathFragment importFile : imports) {
+ SkyKey importsLookupKey = SkylarkImportLookupValue.key(repo, importFile);
+ SkylarkImportLookupValue importLookupValue = (SkylarkImportLookupValue)
+ env.getValueOrThrow(importsLookupKey, SkylarkImportFailedException.class,
+ InconsistentFilesystemException.class, ASTLookupInputException.class,
+ BuildFileNotFoundException.class);
+ if (importLookupValue != null) {
+ importMap.put(importFile, importLookupValue.getImportedEnvironment());
+ fileDependencies.add(importLookupValue.getDependency());
+ }
+ }
+ } catch (SkylarkImportFailedException e) {
+ env.getListener().handle(Event.error(Location.fromFile(buildFilePath), e.getMessage()));
+ throw new PackageFunctionException(new BuildFileContainsErrorsException(packageName,
+ e.getMessage()), Transience.PERSISTENT);
+ } catch (InconsistentFilesystemException e) {
+ throw new PackageFunctionException(new InternalInconsistentFilesystemException(packageName,
+ e), Transience.PERSISTENT);
+ } catch (ASTLookupInputException e) {
+ // The load syntax is bad in the BUILD file so BuildFileContainsErrorsException is OK.
+ throw new PackageFunctionException(new BuildFileContainsErrorsException(packageName,
+ e.getMessage()), Transience.PERSISTENT);
+ } catch (BuildFileNotFoundException e) {
+ throw new PackageFunctionException(e, Transience.PERSISTENT);
+ }
+ if (env.valuesMissing()) {
+ // There are unavailable Skylark dependencies.
+ return null;
+ }
+ return new SkylarkImportResult(importMap, transitiveClosureOfLabels(fileDependencies.build()));
+ }
+
+ private ImmutableList<Label> transitiveClosureOfLabels(
+ ImmutableList<SkylarkFileDependency> immediateDeps) {
+ Set<Label> transitiveClosure = Sets.newHashSet();
+ transitiveClosureOfLabels(immediateDeps, transitiveClosure);
+ return ImmutableList.copyOf(transitiveClosure);
+ }
+
+ private void transitiveClosureOfLabels(
+ ImmutableList<SkylarkFileDependency> immediateDeps, Set<Label> transitiveClosure) {
+ for (SkylarkFileDependency dep : immediateDeps) {
+ if (!transitiveClosure.contains(dep.getLabel())) {
+ transitiveClosure.add(dep.getLabel());
+ transitiveClosureOfLabels(dep.getDependencies(), transitiveClosure);
+ }
+ }
+ }
+
+ @Nullable
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return null;
+ }
+
+ private static void handleLabelsCrossingSubpackagesAndPropagateInconsistentFilesystemExceptions(
+ Path pkgRoot, PackageIdentifier pkgId, Package.LegacyBuilder pkgBuilder, Environment env)
+ throws InternalInconsistentFilesystemException {
+ Set<SkyKey> containingPkgLookupKeys = Sets.newHashSet();
+ Map<Target, SkyKey> targetToKey = new HashMap<>();
+ for (Target target : pkgBuilder.getTargets()) {
+ PathFragment dir = target.getLabel().toPathFragment().getParentDirectory();
+ PackageIdentifier dirId = new PackageIdentifier(pkgId.getRepository(), dir);
+ if (dir.equals(pkgId.getPackageFragment())) {
+ continue;
+ }
+ SkyKey key = ContainingPackageLookupValue.key(dirId);
+ targetToKey.put(target, key);
+ containingPkgLookupKeys.add(key);
+ }
+ Map<Label, SkyKey> subincludeToKey = new HashMap<>();
+ for (Label subincludeLabel : pkgBuilder.getSubincludeLabels()) {
+ PathFragment dir = subincludeLabel.toPathFragment().getParentDirectory();
+ PackageIdentifier dirId = new PackageIdentifier(pkgId.getRepository(), dir);
+ if (dir.equals(pkgId.getPackageFragment())) {
+ continue;
+ }
+ SkyKey key = ContainingPackageLookupValue.key(dirId);
+ subincludeToKey.put(subincludeLabel, key);
+ containingPkgLookupKeys.add(ContainingPackageLookupValue.key(dirId));
+ }
+ Map<SkyKey, ValueOrException3<BuildFileNotFoundException, InconsistentFilesystemException,
+ FileSymlinkCycleException>> containingPkgLookupValues = env.getValuesOrThrow(
+ containingPkgLookupKeys, BuildFileNotFoundException.class,
+ InconsistentFilesystemException.class, FileSymlinkCycleException.class);
+ if (env.valuesMissing()) {
+ return;
+ }
+ for (Target target : ImmutableSet.copyOf(pkgBuilder.getTargets())) {
+ SkyKey key = targetToKey.get(target);
+ if (!containingPkgLookupValues.containsKey(key)) {
+ continue;
+ }
+ ContainingPackageLookupValue containingPackageLookupValue =
+ getContainingPkgLookupValueAndPropagateInconsistentFilesystemExceptions(
+ pkgId.getPackageFragment().getPathString(), containingPkgLookupValues.get(key), env);
+ if (maybeAddEventAboutLabelCrossingSubpackage(pkgBuilder, pkgRoot, target.getLabel(),
+ target.getLocation(), containingPackageLookupValue)) {
+ pkgBuilder.removeTarget(target);
+ pkgBuilder.setContainsErrors();
+ }
+ }
+ for (Label subincludeLabel : pkgBuilder.getSubincludeLabels()) {
+ SkyKey key = subincludeToKey.get(subincludeLabel);
+ if (!containingPkgLookupValues.containsKey(key)) {
+ continue;
+ }
+ ContainingPackageLookupValue containingPackageLookupValue =
+ getContainingPkgLookupValueAndPropagateInconsistentFilesystemExceptions(
+ pkgId.getPackageFragment().getPathString(), containingPkgLookupValues.get(key), env);
+ if (maybeAddEventAboutLabelCrossingSubpackage(pkgBuilder, pkgRoot, subincludeLabel,
+ /*location=*/null, containingPackageLookupValue)) {
+ pkgBuilder.setContainsErrors();
+ }
+ }
+ }
+
+ @Nullable
+ private static ContainingPackageLookupValue
+ getContainingPkgLookupValueAndPropagateInconsistentFilesystemExceptions(String packageName,
+ ValueOrException3<BuildFileNotFoundException, InconsistentFilesystemException,
+ FileSymlinkCycleException> containingPkgLookupValueOrException, Environment env)
+ throws InternalInconsistentFilesystemException {
+ try {
+ return (ContainingPackageLookupValue) containingPkgLookupValueOrException.get();
+ } catch (BuildFileNotFoundException | FileSymlinkCycleException e) {
+ env.getListener().handle(Event.error(null, e.getMessage()));
+ return null;
+ } catch (InconsistentFilesystemException e) {
+ throw new InternalInconsistentFilesystemException(packageName, e);
+ }
+ }
+
+ private static boolean maybeAddEventAboutLabelCrossingSubpackage(
+ Package.LegacyBuilder pkgBuilder, Path pkgRoot, Label label, @Nullable Location location,
+ @Nullable ContainingPackageLookupValue containingPkgLookupValue) {
+ if (containingPkgLookupValue == null) {
+ return true;
+ }
+ if (!containingPkgLookupValue.hasContainingPackage()) {
+ // The missing package here is a problem, but it's not an error from the perspective of
+ // PackageFunction.
+ return false;
+ }
+ PackageIdentifier containingPkg = containingPkgLookupValue.getContainingPackageName();
+ if (containingPkg.equals(label.getPackageIdentifier())) {
+ // The label does not cross a subpackage boundary.
+ return false;
+ }
+ if (!containingPkg.getPackageFragment().startsWith(label.getPackageFragment())) {
+ // This label is referencing an imaginary package, because the containing package should
+ // extend the label's package: if the label is //a/b:c/d, the containing package could be
+ // //a/b/c or //a/b, but should never be //a. Usually such errors will be caught earlier, but
+ // in some exceptional cases (such as a Python-aware BUILD file catching its own io
+ // exceptions), it reaches here, and we tolerate it.
+ return false;
+ }
+ PathFragment labelNameFragment = new PathFragment(label.getName());
+ String message = String.format("Label '%s' crosses boundary of subpackage '%s'",
+ label, containingPkg);
+ Path containingRoot = containingPkgLookupValue.getContainingPackageRoot();
+ if (pkgRoot.equals(containingRoot)) {
+ PathFragment labelNameInContainingPackage = labelNameFragment.subFragment(
+ containingPkg.getPackageFragment().segmentCount()
+ - label.getPackageFragment().segmentCount(),
+ labelNameFragment.segmentCount());
+ message += " (perhaps you meant to put the colon here: "
+ + "'//" + containingPkg + ":" + labelNameInContainingPackage + "'?)";
+ } else {
+ message += " (have you deleted " + containingPkg + "/BUILD? "
+ + "If so, use the --deleted_packages=" + containingPkg + " option)";
+ }
+ pkgBuilder.addEvent(Event.error(location, message));
+ return true;
+ }
+
+ /**
+ * Constructs a {@link Package} object for the given package using legacy package loading.
+ * Note that the returned package may be in error.
+ */
+ private Package.LegacyBuilder loadPackage(ParserInputSource inputSource,
+ @Nullable String replacementContents,
+ PackageIdentifier packageId, Path buildFilePath, RuleVisibility defaultVisibility,
+ List<Statement> preludeStatements, SkylarkImportResult importResult)
+ throws InterruptedException {
+ ParserInputSource replacementSource = replacementContents == null ? null
+ : ParserInputSource.create(replacementContents, buildFilePath);
+ Package.LegacyBuilder pkgBuilder = packageFunctionCache.get(packageId);
+ if (pkgBuilder == null) {
+ Clock clock = new JavaClock();
+ long startTime = clock.nanoTime();
+ profiler.startTask(ProfilerTask.CREATE_PACKAGE, packageId.toString());
+ try {
+ Globber globber = packageFactory.createLegacyGlobber(buildFilePath.getParentDirectory(),
+ packageId, packageLocator);
+ StoredEventHandler localReporter = new StoredEventHandler();
+ Preprocessor.Result preprocessingResult = replacementSource == null
+ ? packageFactory.preprocess(packageId, buildFilePath, inputSource, globber,
+ localReporter)
+ : Preprocessor.Result.noPreprocessing(replacementSource);
+ pkgBuilder = packageFactory.createPackageFromPreprocessingResult(packageId, buildFilePath,
+ preprocessingResult, localReporter.getEvents(), preludeStatements,
+ importResult.importMap, importResult.fileDependencies, packageLocator,
+ defaultVisibility, globber);
+ if (eventBus.get() != null) {
+ eventBus.get().post(new PackageLoadedEvent(packageId.toString(),
+ (clock.nanoTime() - startTime) / (1000 * 1000),
+ // It's impossible to tell if the package was loaded before, so we always pass false.
+ /*reloading=*/false,
+ // This isn't completely correct since we may encounter errors later (e.g. filesystem
+ // inconsistencies)
+ !pkgBuilder.containsErrors()));
+ }
+ numPackagesLoaded.incrementAndGet();
+ packageFunctionCache.put(packageId, pkgBuilder);
+ } finally {
+ profiler.completeTask(ProfilerTask.CREATE_PACKAGE);
+ }
+ }
+ return pkgBuilder;
+ }
+
+ private static class InternalInconsistentFilesystemException extends NoSuchPackageException {
+ private boolean isTransient;
+
+ /**
+ * Used to represent a filesystem inconsistency discovered outside the
+ * {@link PackageFunction}.
+ */
+ public InternalInconsistentFilesystemException(String packageName,
+ InconsistentFilesystemException e) {
+ super(packageName, e.getMessage(), e);
+ // This is not a transient error from the perspective of the PackageFunction.
+ this.isTransient = false;
+ }
+
+ /** Used to represent a filesystem inconsistency discovered by the {@link PackageFunction}. */
+ public InternalInconsistentFilesystemException(String packageName,
+ String inconsistencyMessage) {
+ this(packageName, new InconsistentFilesystemException(inconsistencyMessage));
+ this.isTransient = true;
+ }
+
+ public boolean isTransient() {
+ return isTransient;
+ }
+ }
+
+ private static class BadWorkspaceFileException extends NoSuchPackageException {
+ private BadWorkspaceFileException(String message) {
+ super("external", "Error encountered while dealing with the WORKSPACE file: " + message);
+ }
+ }
+
+ private static class BadPreludeFileException extends NoSuchPackageException {
+ private BadPreludeFileException(String packageName, String message) {
+ super(packageName, "Error encountered while reading the prelude file: " + message);
+ }
+ }
+
+ /**
+ * Used to declare all the exception types that can be wrapped in the exception thrown by
+ * {@link PackageFunction#compute}.
+ */
+ private static class PackageFunctionException extends SkyFunctionException {
+ public PackageFunctionException(NoSuchPackageException e, Transience transience) {
+ super(e, transience);
+ }
+ }
+
+ /** A simple value class to store the result of the Skylark imports.*/
+ private static final class SkylarkImportResult {
+ private final Map<PathFragment, SkylarkEnvironment> importMap;
+ private final ImmutableList<Label> fileDependencies;
+ private SkylarkImportResult(Map<PathFragment, SkylarkEnvironment> importMap,
+ ImmutableList<Label> fileDependencies) {
+ this.importMap = importMap;
+ this.fileDependencies = fileDependencies;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/PackageLookupFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/PackageLookupFunction.java
new file mode 100644
index 0000000..ae4ee55
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/PackageLookupFunction.java
@@ -0,0 +1,180 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.cmdline.LabelValidator;
+import com.google.devtools.build.lib.packages.BuildFileContainsErrorsException;
+import com.google.devtools.build.lib.packages.BuildFileNotFoundException;
+import com.google.devtools.build.lib.packages.NoSuchPackageException;
+import com.google.devtools.build.lib.packages.PackageIdentifier;
+import com.google.devtools.build.lib.pkgcache.PathPackageLocator;
+import com.google.devtools.build.lib.syntax.EvalException;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.lib.vfs.RootedPath;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyFunctionException;
+import com.google.devtools.build.skyframe.SkyFunctionException.Transience;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicReference;
+
+import javax.annotation.Nullable;
+
+/**
+ * SkyFunction for {@link PackageLookupValue}s.
+ */
+class PackageLookupFunction implements SkyFunction {
+
+ private final AtomicReference<ImmutableSet<String>> deletedPackages;
+
+ PackageLookupFunction(AtomicReference<ImmutableSet<String>> deletedPackages) {
+ this.deletedPackages = deletedPackages;
+ }
+
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env) throws PackageLookupFunctionException {
+ PathPackageLocator pkgLocator = PrecomputedValue.PATH_PACKAGE_LOCATOR.get(env);
+ PackageIdentifier packageKey = (PackageIdentifier) skyKey.argument();
+ if (!packageKey.getRepository().isDefault()) {
+ return computeExternalPackageLookupValue(skyKey, env);
+ }
+ PathFragment pkg = packageKey.getPackageFragment();
+
+ // This represents a package lookup at the package root.
+ if (pkg.equals(PathFragment.EMPTY_FRAGMENT)) {
+ return PackageLookupValue.invalidPackageName("The empty package name is invalid");
+ }
+
+ String pkgName = pkg.getPathString();
+ String packageNameErrorMsg = LabelValidator.validatePackageName(pkgName);
+ if (packageNameErrorMsg != null) {
+ return PackageLookupValue.invalidPackageName("Invalid package name '" + pkgName + "': "
+ + packageNameErrorMsg);
+ }
+
+ if (deletedPackages.get().contains(pkg.getPathString())) {
+ return PackageLookupValue.deletedPackage();
+ }
+
+ // TODO(bazel-team): The following is O(n^2) on the number of elements on the package path due
+ // to having restart the SkyFunction after every new dependency. However, if we try to batch
+ // the missing value keys, more dependencies than necessary will be declared. This wart can be
+ // fixed once we have nicer continuation support [skyframe-loading]
+ for (Path packagePathEntry : pkgLocator.getPathEntries()) {
+ PackageLookupValue value = getPackageLookupValue(env, packagePathEntry, pkg);
+ if (value == null || value.packageExists()) {
+ return value;
+ }
+ }
+ return PackageLookupValue.noBuildFile();
+ }
+
+ @Nullable
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return null;
+ }
+
+ private PackageLookupValue getPackageLookupValue(Environment env, Path packagePathEntry,
+ PathFragment pkgFragment) throws PackageLookupFunctionException {
+ PathFragment buildFileFragment;
+ if (pkgFragment.getPathString().equals(PackageFunction.EXTERNAL_PACKAGE_NAME)) {
+ buildFileFragment = new PathFragment("WORKSPACE");
+ } else {
+ buildFileFragment = pkgFragment.getChild("BUILD");
+ }
+ RootedPath buildFileRootedPath = RootedPath.toRootedPath(packagePathEntry,
+ buildFileFragment);
+ String basename = buildFileRootedPath.asPath().getBaseName();
+ SkyKey fileSkyKey = FileValue.key(buildFileRootedPath);
+ FileValue fileValue = null;
+ try {
+ fileValue = (FileValue) env.getValueOrThrow(fileSkyKey, IOException.class,
+ FileSymlinkCycleException.class, InconsistentFilesystemException.class);
+ } catch (IOException e) {
+ String pkgName = pkgFragment.getPathString();
+ // TODO(bazel-team): throw an IOException here and let PackageFunction wrap that into a
+ // BuildFileNotFoundException.
+ throw new PackageLookupFunctionException(new BuildFileNotFoundException(pkgName,
+ "IO errors while looking for " + basename + " file reading "
+ + buildFileRootedPath.asPath() + ": " + e.getMessage(), e),
+ Transience.PERSISTENT);
+ } catch (FileSymlinkCycleException e) {
+ String pkgName = buildFileRootedPath.asPath().getPathString();
+ throw new PackageLookupFunctionException(new BuildFileNotFoundException(pkgName,
+ "Symlink cycle detected while trying to find " + basename + " file "
+ + buildFileRootedPath.asPath()),
+ Transience.PERSISTENT);
+ } catch (InconsistentFilesystemException e) {
+ // This error is not transient from the perspective of the PackageLookupFunction.
+ throw new PackageLookupFunctionException(e, Transience.PERSISTENT);
+ }
+ if (fileValue == null) {
+ return null;
+ }
+ if (fileValue.isFile()) {
+ return PackageLookupValue.success(buildFileRootedPath.getRoot());
+ }
+ return PackageLookupValue.noBuildFile();
+ }
+
+ /**
+ * Gets a PackageLookupValue from a different Bazel repository.
+ *
+ * To do this, it looks up the "external" package and finds a path mapping for the repository
+ * name.
+ */
+ private PackageLookupValue computeExternalPackageLookupValue(
+ SkyKey skyKey, Environment env) throws PackageLookupFunctionException {
+ PackageIdentifier id = (PackageIdentifier) skyKey.argument();
+ SkyKey repositoryKey = RepositoryValue.key(id.getRepository());
+ RepositoryValue repositoryValue = null;
+ try {
+ repositoryValue = (RepositoryValue) env.getValueOrThrow(
+ repositoryKey, NoSuchPackageException.class, IOException.class, EvalException.class);
+ if (repositoryValue == null) {
+ return null;
+ }
+ } catch (NoSuchPackageException e) {
+ throw new PackageLookupFunctionException(e, Transience.PERSISTENT);
+ } catch (IOException e) {
+ throw new PackageLookupFunctionException(new BuildFileContainsErrorsException(
+ PackageFunction.EXTERNAL_PACKAGE_NAME, e.getMessage()), Transience.PERSISTENT);
+ } catch (EvalException e) {
+ throw new PackageLookupFunctionException(new BuildFileContainsErrorsException(
+ PackageFunction.EXTERNAL_PACKAGE_NAME, e.getMessage()), Transience.PERSISTENT);
+ }
+
+ return getPackageLookupValue(env, repositoryValue.getPath(), id.getPackageFragment());
+ }
+
+ /**
+ * Used to declare all the exception types that can be wrapped in the exception thrown by
+ * {@link PackageLookupFunction#compute}.
+ */
+ private static final class PackageLookupFunctionException extends SkyFunctionException {
+ public PackageLookupFunctionException(NoSuchPackageException e, Transience transience) {
+ super(e, transience);
+ }
+
+ public PackageLookupFunctionException(InconsistentFilesystemException e,
+ Transience transience) {
+ super(e, transience);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/PackageLookupValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/PackageLookupValue.java
new file mode 100644
index 0000000..c877d38
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/PackageLookupValue.java
@@ -0,0 +1,249 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.packages.PackageIdentifier;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+/**
+ * A value that represents a package lookup result.
+ *
+ * <p>Package lookups will always produce a value. On success, the {@code #getRoot} returns the
+ * package path root under which the package resides and the package's BUILD file is guaranteed to
+ * exist; on failure, {@code #getErrorReason} and {@code #getErrorMsg} describe why the package
+ * doesn't exist.
+ *
+ * <p>Implementation detail: we use inheritance here to optimize for memory usage.
+ */
+abstract class PackageLookupValue implements SkyValue {
+
+ enum ErrorReason {
+ // There is no BUILD file.
+ NO_BUILD_FILE,
+
+ // The package name is invalid.
+ INVALID_PACKAGE_NAME,
+
+ // The package is considered deleted because of --deleted_packages.
+ DELETED_PACKAGE,
+
+ // The //external package could not be loaded, either because the WORKSPACE file could not be
+ // parsed or the packages it references cannot be loaded.
+ NO_EXTERNAL_PACKAGE
+ }
+
+ protected PackageLookupValue() {
+ }
+
+ public static PackageLookupValue success(Path root) {
+ return new SuccessfulPackageLookupValue(root);
+ }
+
+ public static PackageLookupValue noBuildFile() {
+ return NoBuildFilePackageLookupValue.INSTANCE;
+ }
+
+ public static PackageLookupValue noExternalPackage() {
+ return NoExternalPackageLookupValue.INSTANCE;
+ }
+
+ public static PackageLookupValue invalidPackageName(String errorMsg) {
+ return new InvalidNamePackageLookupValue(errorMsg);
+ }
+
+ public static PackageLookupValue deletedPackage() {
+ return DeletedPackageLookupValue.INSTANCE;
+ }
+
+ /**
+ * For a successful package lookup, returns the root (package path entry) that the package
+ * resides in.
+ */
+ public abstract Path getRoot();
+
+ /**
+ * Returns whether the package lookup was successful.
+ */
+ public abstract boolean packageExists();
+
+ /**
+ * For an unsuccessful package lookup, gets the reason why {@link #packageExists} returns
+ * {@code false}.
+ */
+ abstract ErrorReason getErrorReason();
+
+ /**
+ * For an unsuccessful package lookup, gets a detailed error message for {@link #getErrorReason}
+ * that is suitable for reporting to a user.
+ */
+ abstract String getErrorMsg();
+
+ static SkyKey key(PathFragment directory) {
+ Preconditions.checkArgument(!directory.isAbsolute(), directory);
+ return key(PackageIdentifier.createInDefaultRepo(directory));
+ }
+
+ static SkyKey key(PackageIdentifier pkgIdentifier) {
+ return new SkyKey(SkyFunctions.PACKAGE_LOOKUP, pkgIdentifier);
+ }
+
+ private static class SuccessfulPackageLookupValue extends PackageLookupValue {
+
+ private final Path root;
+
+ private SuccessfulPackageLookupValue(Path root) {
+ this.root = root;
+ }
+
+ @Override
+ public boolean packageExists() {
+ return true;
+ }
+
+ @Override
+ public Path getRoot() {
+ return root;
+ }
+
+ @Override
+ ErrorReason getErrorReason() {
+ throw new IllegalStateException();
+ }
+
+ @Override
+ String getErrorMsg() {
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof SuccessfulPackageLookupValue)) {
+ return false;
+ }
+ SuccessfulPackageLookupValue other = (SuccessfulPackageLookupValue) obj;
+ return root.equals(other.root);
+ }
+
+ @Override
+ public int hashCode() {
+ return root.hashCode();
+ }
+ }
+
+ private abstract static class UnsuccessfulPackageLookupValue extends PackageLookupValue {
+
+ @Override
+ public boolean packageExists() {
+ return false;
+ }
+
+ @Override
+ public Path getRoot() {
+ throw new IllegalStateException();
+ }
+ }
+
+ private static class NoBuildFilePackageLookupValue extends UnsuccessfulPackageLookupValue {
+
+ public static final NoBuildFilePackageLookupValue INSTANCE =
+ new NoBuildFilePackageLookupValue();
+
+ private NoBuildFilePackageLookupValue() {
+ }
+
+ @Override
+ ErrorReason getErrorReason() {
+ return ErrorReason.NO_BUILD_FILE;
+ }
+
+ @Override
+ String getErrorMsg() {
+ return "BUILD file not found on package path";
+ }
+ }
+
+ private static class NoExternalPackageLookupValue extends UnsuccessfulPackageLookupValue {
+
+ public static final NoExternalPackageLookupValue INSTANCE =
+ new NoExternalPackageLookupValue();
+
+ private NoExternalPackageLookupValue() {
+ }
+
+ @Override
+ ErrorReason getErrorReason() {
+ return ErrorReason.NO_EXTERNAL_PACKAGE;
+ }
+
+ @Override
+ String getErrorMsg() {
+ return "Error loading the //external package";
+ }
+ }
+
+ private static class InvalidNamePackageLookupValue extends UnsuccessfulPackageLookupValue {
+
+ private final String errorMsg;
+
+ private InvalidNamePackageLookupValue(String errorMsg) {
+ this.errorMsg = errorMsg;
+ }
+
+ @Override
+ ErrorReason getErrorReason() {
+ return ErrorReason.INVALID_PACKAGE_NAME;
+ }
+
+ @Override
+ String getErrorMsg() {
+ return errorMsg;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof InvalidNamePackageLookupValue)) {
+ return false;
+ }
+ InvalidNamePackageLookupValue other = (InvalidNamePackageLookupValue) obj;
+ return errorMsg.equals(other.errorMsg);
+ }
+
+ @Override
+ public int hashCode() {
+ return errorMsg.hashCode();
+ }
+ }
+
+ private static class DeletedPackageLookupValue extends UnsuccessfulPackageLookupValue {
+
+ public static final DeletedPackageLookupValue INSTANCE = new DeletedPackageLookupValue();
+
+ private DeletedPackageLookupValue() {
+ }
+
+ @Override
+ ErrorReason getErrorReason() {
+ return ErrorReason.DELETED_PACKAGE;
+ }
+
+ @Override
+ String getErrorMsg() {
+ return "Package is considered deleted due to --deleted_packages";
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/PackageValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/PackageValue.java
new file mode 100644
index 0000000..65fd2af
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/PackageValue.java
@@ -0,0 +1,55 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.packages.Package;
+import com.google.devtools.build.lib.packages.PackageIdentifier;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+/**
+ * A Skyframe value representing a package.
+ */
+@Immutable
+@ThreadSafe
+public class PackageValue implements SkyValue {
+
+ private final Package pkg;
+
+ PackageValue(Package pkg) {
+ this.pkg = Preconditions.checkNotNull(pkg);
+ }
+
+ public Package getPackage() {
+ return pkg;
+ }
+
+ @Override
+ public String toString() {
+ return "<PackageValue name=" + pkg.getName() + ">";
+ }
+
+ @ThreadSafe
+ public static SkyKey key(PathFragment pkgName) {
+ return key(PackageIdentifier.createInDefaultRepo(pkgName));
+ }
+
+ public static SkyKey key(PackageIdentifier pkgIdentifier) {
+ return new SkyKey(SkyFunctions.PACKAGE, pkgIdentifier);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/PerBuildSyscallCache.java b/src/main/java/com/google/devtools/build/lib/skyframe/PerBuildSyscallCache.java
new file mode 100644
index 0000000..5116a6f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/PerBuildSyscallCache.java
@@ -0,0 +1,131 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.devtools.build.lib.util.Pair;
+import com.google.devtools.build.lib.vfs.Dirent;
+import com.google.devtools.build.lib.vfs.FileStatus;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.Symlinks;
+import com.google.devtools.build.lib.vfs.UnixGlob;
+
+import java.io.IOException;
+import java.util.Collection;
+
+/**
+ * A per-build cache of filesystem operations for Skyframe invocations of legacy package loading.
+ */
+class PerBuildSyscallCache implements UnixGlob.FilesystemCalls {
+
+ private final LoadingCache<Pair<Path, Symlinks>, FileStatus> statCache =
+ newStatMap();
+ private final LoadingCache<Pair<Path, Symlinks>, Pair<Collection<Dirent>, IOException>>
+ readdirCache = newReaddirMap();
+
+ private static final FileStatus NO_STATUS = new FakeFileStatus();
+
+ @Override
+ public Collection<Dirent> readdir(Path path, Symlinks symlinks) throws IOException {
+ Pair<Collection<Dirent>, IOException> result =
+ readdirCache.getUnchecked(Pair.of(path, symlinks));
+ Collection<Dirent> entries = result.getFirst();
+ if (entries != null) {
+ return entries;
+ }
+ throw result.getSecond();
+ }
+
+ @Override
+ public FileStatus statNullable(Path path, Symlinks symlinks) {
+ FileStatus status = statCache.getUnchecked(Pair.of(path, symlinks));
+ return (status == NO_STATUS) ? null : status;
+ }
+
+ // This is used because the cache implementations don't allow null.
+ private static final class FakeFileStatus implements FileStatus {
+ @Override
+ public long getLastChangeTime() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public long getNodeId() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public long getLastModifiedTime() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public long getSize() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isDirectory() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isFile() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isSymbolicLink() {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ /**
+ * A cache of stat calls.
+ * Input: (path, following_symlinks)
+ * Output: FileStatus
+ */
+ private static LoadingCache<Pair<Path, Symlinks>, FileStatus> newStatMap() {
+ return CacheBuilder.newBuilder().build(
+ new CacheLoader<Pair<Path, Symlinks>, FileStatus>() {
+ @Override
+ public FileStatus load(Pair<Path, Symlinks> p) {
+ FileStatus f = p.first.statNullable(p.second);
+ return (f == null) ? NO_STATUS : f;
+ }
+ });
+ }
+
+ /**
+ * A cache of readdir calls.
+ * Input: (path, following_symlinks)
+ * Output: A union of (Dirents, IOException).
+ */
+ private static
+ LoadingCache<Pair<Path, Symlinks>, Pair<Collection<Dirent>, IOException>> newReaddirMap() {
+ return CacheBuilder.newBuilder().build(
+ new CacheLoader<Pair<Path, Symlinks>, Pair<Collection<Dirent>, IOException>>() {
+ @Override
+ public Pair<Collection<Dirent>, IOException> load(Pair<Path, Symlinks> p) {
+ try {
+ return Pair.of(p.first.readdir(p.second), null);
+ } catch (IOException e) {
+ return Pair.of(null, e);
+ }
+ }
+ });
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/PostConfiguredTargetFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/PostConfiguredTargetFunction.java
new file mode 100644
index 0000000..03920b7
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/PostConfiguredTargetFunction.java
@@ -0,0 +1,145 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Function;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.DependencyResolver.Dependency;
+import com.google.devtools.build.lib.analysis.LabelAndConfiguration;
+import com.google.devtools.build.lib.analysis.TargetAndConfiguration;
+import com.google.devtools.build.lib.analysis.config.ConfigMatchingProvider;
+import com.google.devtools.build.lib.packages.Attribute;
+import com.google.devtools.build.lib.packages.RawAttributeMapper;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.skyframe.SkyframeActionExecutor.ConflictException;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyFunctionException;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.util.Collection;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+/**
+ * Build a post-processed ConfiguredTarget, vetting it for action conflict issues.
+ */
+public class PostConfiguredTargetFunction implements SkyFunction {
+ private static final Function<Dependency, SkyKey> TO_KEYS =
+ new Function<Dependency, SkyKey>() {
+ @Override
+ public SkyKey apply(Dependency input) {
+ return PostConfiguredTargetValue.key(
+ new ConfiguredTargetKey(input.getLabel(), input.getConfiguration()));
+ }
+ };
+
+ private final SkyframeExecutor.BuildViewProvider buildViewProvider;
+
+ public PostConfiguredTargetFunction(
+ SkyframeExecutor.BuildViewProvider buildViewProvider) {
+ this.buildViewProvider = Preconditions.checkNotNull(buildViewProvider);
+ }
+
+ @Nullable
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env) throws SkyFunctionException {
+ ImmutableMap<Action, ConflictException> badActions = PrecomputedValue.BAD_ACTIONS.get(env);
+ ConfiguredTargetValue ctValue = (ConfiguredTargetValue)
+ env.getValue(ConfiguredTargetValue.key((ConfiguredTargetKey) skyKey.argument()));
+ SkyframeDependencyResolver resolver =
+ buildViewProvider.getSkyframeBuildView().createDependencyResolver(env);
+ if (env.valuesMissing()) {
+ return null;
+ }
+
+ for (Action action : ctValue.getActions()) {
+ if (badActions.containsKey(action)) {
+ throw new ActionConflictFunctionException(badActions.get(action));
+ }
+ }
+
+ ConfiguredTarget ct = ctValue.getConfiguredTarget();
+ TargetAndConfiguration ctgValue =
+ new TargetAndConfiguration(ct.getTarget(), ct.getConfiguration());
+
+ Set<ConfigMatchingProvider> configConditions =
+ getConfigurableAttributeConditions(ctgValue, env);
+ if (configConditions == null) {
+ return null;
+ }
+
+ Collection<Dependency> deps = resolver.dependentNodes(ctgValue, configConditions);
+ env.getValues(Iterables.transform(deps, TO_KEYS));
+ if (env.valuesMissing()) {
+ return null;
+ }
+
+ return new PostConfiguredTargetValue(ct);
+ }
+
+ /**
+ * Returns the configurable attribute conditions necessary to evaluate the given configured
+ * target, or null if not all dependencies have yet been SkyFrame-evaluated.
+ */
+ @Nullable
+ private Set<ConfigMatchingProvider> getConfigurableAttributeConditions(
+ TargetAndConfiguration ctg, Environment env) {
+ if (!(ctg.getTarget() instanceof Rule)) {
+ return ImmutableSet.of();
+ }
+ Rule rule = (Rule) ctg.getTarget();
+ RawAttributeMapper mapper = RawAttributeMapper.of(rule);
+ Set<SkyKey> depKeys = new LinkedHashSet<>();
+ for (Attribute attribute : rule.getAttributes()) {
+ for (Label label : mapper.getConfigurabilityKeys(attribute.getName(), attribute.getType())) {
+ if (!Type.Selector.isReservedLabel(label)) {
+ depKeys.add(ConfiguredTargetValue.key(label, ctg.getConfiguration()));
+ }
+ }
+ }
+ Map<SkyKey, SkyValue> cts = env.getValues(depKeys);
+ if (env.valuesMissing()) {
+ return null;
+ }
+ ImmutableSet.Builder<ConfigMatchingProvider> conditions = ImmutableSet.builder();
+ for (SkyValue ctValue : cts.values()) {
+ ConfiguredTarget ct = ((ConfiguredTargetValue) ctValue).getConfiguredTarget();
+ conditions.add(Preconditions.checkNotNull(ct.getProvider(ConfigMatchingProvider.class)));
+ }
+ return conditions.build();
+ }
+
+ @Nullable
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return Label.print(((LabelAndConfiguration) skyKey.argument()).getLabel());
+ }
+
+ private static class ActionConflictFunctionException extends SkyFunctionException {
+ public ActionConflictFunctionException(ConflictException e) {
+ super(e, Transience.PERSISTENT);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/PostConfiguredTargetValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/PostConfiguredTargetValue.java
new file mode 100644
index 0000000..42d2b38
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/PostConfiguredTargetValue.java
@@ -0,0 +1,49 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+/**
+ * A post-processed ConfiguredTarget which is known to be transitively error-free from action
+ * conflict issues.
+ */
+class PostConfiguredTargetValue implements SkyValue {
+
+ private final ConfiguredTarget ct;
+
+ public PostConfiguredTargetValue(ConfiguredTarget ct) {
+ this.ct = Preconditions.checkNotNull(ct);
+ }
+
+ public static ImmutableList<SkyKey> keys(Iterable<ConfiguredTargetKey> lacs) {
+ ImmutableList.Builder<SkyKey> keys = ImmutableList.builder();
+ for (ConfiguredTargetKey lac : lacs) {
+ keys.add(key(lac));
+ }
+ return keys.build();
+ }
+
+ public static SkyKey key(ConfiguredTargetKey lac) {
+ return new SkyKey(SkyFunctions.POST_CONFIGURED_TARGET, lac);
+ }
+
+ public ConfiguredTarget getCt() {
+ return ct;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/PrecomputedFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/PrecomputedFunction.java
new file mode 100644
index 0000000..8254987
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/PrecomputedFunction.java
@@ -0,0 +1,38 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyFunctionException;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+/**
+ * Builder for {@link PrecomputedValue}s.
+ *
+ * <p>Always throws an error, because the values aren't computed inside the skyframe framework.
+ */
+public class PrecomputedFunction implements SkyFunction {
+
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env) throws SkyFunctionException,
+ InterruptedException {
+ throw new IllegalStateException(skyKey + " not set");
+ }
+
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return null;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/PrecomputedValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/PrecomputedValue.java
new file mode 100644
index 0000000..bb2656d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/PrecomputedValue.java
@@ -0,0 +1,182 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Supplier;
+import com.google.common.base.Suppliers;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.analysis.BlazeDirectories;
+import com.google.devtools.build.lib.analysis.TopLevelArtifactContext;
+import com.google.devtools.build.lib.analysis.WorkspaceStatusAction;
+import com.google.devtools.build.lib.analysis.buildinfo.BuildInfoFactory;
+import com.google.devtools.build.lib.analysis.buildinfo.BuildInfoFactory.BuildInfoKey;
+import com.google.devtools.build.lib.packages.RuleVisibility;
+import com.google.devtools.build.lib.pkgcache.PathPackageLocator;
+import com.google.devtools.build.lib.skyframe.SkyframeActionExecutor.ConflictException;
+import com.google.devtools.build.skyframe.Injectable;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.util.Map;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+/**
+ * A value that represents something computed outside of the skyframe framework. These values are
+ * "precomputed" from skyframe's perspective and so the graph needs to be prepopulated with them
+ * (e.g. via injection).
+ */
+public class PrecomputedValue implements SkyValue {
+ /**
+ * An externally-injected precomputed value. Exists so that modules can inject precomputed values
+ * into Skyframe's graph.
+ *
+ * <p>{@see com.google.devtools.build.lib.blaze.BlazeModule#getPrecomputedValues}.
+ */
+ public static final class Injected {
+ private final Precomputed<?> precomputed;
+ private final Supplier<? extends Object> supplier;
+
+ private Injected(Precomputed<?> precomputed, Supplier<? extends Object> supplier) {
+ this.precomputed = precomputed;
+ this.supplier = supplier;
+ }
+
+ void inject(Injectable injectable) {
+ injectable.inject(ImmutableMap.of(precomputed.key, new PrecomputedValue(supplier.get())));
+ }
+ }
+
+ public static <T> Injected injected(Precomputed<T> precomputed, Supplier<T> value) {
+ return new Injected(precomputed, value);
+ }
+
+ public static <T> Injected injected(Precomputed<T> precomputed, T value) {
+ return new Injected(precomputed, Suppliers.ofInstance(value));
+ }
+
+ static final Precomputed<String> DEFAULTS_PACKAGE_CONTENTS =
+ new Precomputed<>(new SkyKey(SkyFunctions.PRECOMPUTED, "default_pkg"));
+
+ static final Precomputed<RuleVisibility> DEFAULT_VISIBILITY =
+ new Precomputed<>(new SkyKey(SkyFunctions.PRECOMPUTED, "default_visibility"));
+
+ static final Precomputed<UUID> BUILD_ID =
+ new Precomputed<>(new SkyKey(SkyFunctions.PRECOMPUTED, "build_id"));
+
+ static final Precomputed<WorkspaceStatusAction> WORKSPACE_STATUS_KEY =
+ new Precomputed<>(new SkyKey(SkyFunctions.PRECOMPUTED, "workspace_status_action"));
+
+ static final Precomputed<Action> COVERAGE_REPORT_KEY =
+ new Precomputed<>(new SkyKey(SkyFunctions.PRECOMPUTED, "coverage_report_action"));
+
+ static final Precomputed<TopLevelArtifactContext> TOP_LEVEL_CONTEXT =
+ new Precomputed<>(new SkyKey(SkyFunctions.PRECOMPUTED, "top_level_context"));
+
+ static final Precomputed<Map<BuildInfoKey, BuildInfoFactory>> BUILD_INFO_FACTORIES =
+ new Precomputed<>(new SkyKey(SkyFunctions.PRECOMPUTED, "build_info_factories"));
+
+ static final Precomputed<Map<String, String>> TEST_ENVIRONMENT_VARIABLES =
+ new Precomputed<>(new SkyKey(SkyFunctions.PRECOMPUTED, "test_environment"));
+
+ static final Precomputed<BlazeDirectories> BLAZE_DIRECTORIES =
+ new Precomputed<>(new SkyKey(SkyFunctions.PRECOMPUTED, "blaze_directories"));
+
+ static final Precomputed<ImmutableMap<Action, ConflictException>> BAD_ACTIONS =
+ new Precomputed<>(new SkyKey(SkyFunctions.PRECOMPUTED, "bad_actions"));
+
+ public static final Precomputed<PathPackageLocator> PATH_PACKAGE_LOCATOR =
+ new Precomputed<>(new SkyKey(SkyFunctions.PRECOMPUTED, "path_package_locator"));
+
+ private final Object value;
+
+ public PrecomputedValue(Object value) {
+ this.value = Preconditions.checkNotNull(value);
+ }
+
+ /**
+ * Returns the value of the variable.
+ */
+ public Object get() {
+ return value;
+ }
+
+ @Override
+ public int hashCode() {
+ return value.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof PrecomputedValue)) {
+ return false;
+ }
+ PrecomputedValue other = (PrecomputedValue) obj;
+ return value.equals(other.value);
+ }
+
+ @Override
+ public String toString() {
+ return "<BuildVariable " + value + ">";
+ }
+
+ public static final void dependOnBuildId(SkyFunction.Environment env) {
+ BUILD_ID.get(env);
+ }
+
+ /**
+ * A helper object corresponding to a variable in Skyframe.
+ *
+ * <p>Instances do not have internal state.
+ */
+ public static final class Precomputed<T> {
+ private final SkyKey key;
+
+ public Precomputed(SkyKey key) {
+ this.key = key;
+ }
+
+ @VisibleForTesting
+ SkyKey getKeyForTesting() {
+ return key;
+ }
+
+ /**
+ * Retrieves the value of this variable from Skyframe.
+ *
+ * <p>If the value was not set, an exception will be raised.
+ */
+ @Nullable
+ @SuppressWarnings("unchecked")
+ public T get(SkyFunction.Environment env) {
+ PrecomputedValue value = (PrecomputedValue) env.getValue(key);
+ if (value == null) {
+ return null;
+ }
+ return (T) value.get();
+ }
+
+ /**
+ * Injects a new variable value.
+ */
+ void set(Injectable injectable, T value) {
+ injectable.inject(ImmutableMap.of(key, new PrecomputedValue(value)));
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/RecursiveFilesystemTraversalFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/RecursiveFilesystemTraversalFunction.java
new file mode 100644
index 0000000..d54e98f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/RecursiveFilesystemTraversalFunction.java
@@ -0,0 +1,454 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Function;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Verify;
+import com.google.common.collect.Collections2;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventKind;
+import com.google.devtools.build.lib.skyframe.RecursiveFilesystemTraversalValue.ResolvedFile;
+import com.google.devtools.build.lib.skyframe.RecursiveFilesystemTraversalValue.TraversalRequest;
+import com.google.devtools.build.lib.vfs.Dirent;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.lib.vfs.RootedPath;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyFunctionException;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nullable;
+
+/** A {@link SkyFunction} to build {@link RecursiveFilesystemTraversalValue}s. */
+public final class RecursiveFilesystemTraversalFunction implements SkyFunction {
+
+ private static final class MissingDepException extends Exception {}
+
+ /** Base class for exceptions that {@link RecursiveFilesystemTraversalFunctionException} wraps. */
+ public abstract static class RecursiveFilesystemTraversalException extends Exception {
+ protected RecursiveFilesystemTraversalException(String message) {
+ super(message);
+ }
+ }
+
+ /** Thrown when a generated directory's root-relative path conflicts with a package's path. */
+ public static final class GeneratedPathConflictException extends
+ RecursiveFilesystemTraversalException {
+ GeneratedPathConflictException(TraversalRequest traversal) {
+ super(String.format(
+ "Generated directory %s conflicts with package under the same path. Additional info: %s",
+ traversal.path.getRelativePath().getPathString(),
+ traversal.errorInfo != null ? traversal.errorInfo : traversal.toString()));
+ }
+ }
+
+ /**
+ * Thrown when the traversal encounters a subdirectory with a BUILD file but is not allowed to
+ * recurse into it.
+ */
+ public static final class CannotCrossPackageBoundaryException extends
+ RecursiveFilesystemTraversalException {
+ CannotCrossPackageBoundaryException(String message) {
+ super(message);
+ }
+ }
+
+ /**
+ * Thrown when a dangling symlink is attempted to be dereferenced.
+ *
+ * <p>Note: this class is not identical to the one in com.google.devtools.build.lib.view.fileset
+ * and it's not easy to merge the two because of the dependency structure. The other one will
+ * probably be removed along with the rest of the legacy Fileset code.
+ */
+ public static final class DanglingSymlinkException extends RecursiveFilesystemTraversalException {
+ public final String path;
+ public final String unresolvedLink;
+
+ public DanglingSymlinkException(String path, String unresolvedLink) {
+ super("Found dangling symlink: " + path + ", unresolved path: ");
+ Preconditions.checkArgument(path != null && !path.isEmpty());
+ Preconditions.checkArgument(unresolvedLink != null && !unresolvedLink.isEmpty());
+ this.path = path;
+ this.unresolvedLink = unresolvedLink;
+ }
+
+ public String getPath() {
+ return path;
+ }
+ }
+
+ /** Exception type thrown by {@link RecursiveFilesystemTraversalFunction#compute}. */
+ private static final class RecursiveFilesystemTraversalFunctionException extends
+ SkyFunctionException {
+ RecursiveFilesystemTraversalFunctionException(RecursiveFilesystemTraversalException e) {
+ super(e, Transience.PERSISTENT);
+ }
+ }
+
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env)
+ throws RecursiveFilesystemTraversalFunctionException {
+ TraversalRequest traversal = (TraversalRequest) skyKey.argument();
+ try {
+ // Stat the traversal root.
+ FileInfo rootInfo = lookUpFileInfo(env, traversal);
+
+ if (!rootInfo.type.exists()) {
+ // May be a dangling symlink or a non-existent file. Handle gracefully.
+ if (rootInfo.type.isSymlink()) {
+ return resultForDanglingSymlink(traversal.path, rootInfo);
+ } else {
+ return RecursiveFilesystemTraversalValue.EMPTY;
+ }
+ }
+
+ if (rootInfo.type.isFile()) {
+ // The root is a file or a symlink to one.
+ return resultForFileRoot(traversal.path, rootInfo);
+ }
+
+ // Otherwise the root is a directory or a symlink to one.
+ PkgLookupResult pkgLookupResult = checkIfPackage(env, traversal, rootInfo);
+ traversal = pkgLookupResult.traversal;
+
+ if (pkgLookupResult.isConflicting()) {
+ // The traversal was requested for an output directory whose root-relative path conflicts
+ // with a source package. We can't handle that, bail out.
+ throw new RecursiveFilesystemTraversalFunctionException(
+ new GeneratedPathConflictException(traversal));
+ } else if (pkgLookupResult.isPackage() && !traversal.skipTestingForSubpackage) {
+ // The traversal was requested for a directory that defines a package.
+ if (traversal.crossPkgBoundaries) {
+ // We are free to traverse the subpackage but we need to display a warning.
+ String msg = traversal.errorInfo + " crosses package boundary into package rooted at "
+ + traversal.path.getRelativePath().getPathString();
+ env.getListener().handle(new Event(EventKind.WARNING, null, msg));
+ } else {
+ // We cannot traverse the subpackage and should skip it silently. Return empty results.
+ return RecursiveFilesystemTraversalValue.EMPTY;
+ }
+ }
+
+ // We are free to traverse this directory.
+ Collection<SkyKey> dependentKeys = createRecursiveTraversalKeys(env, traversal);
+ return resultForDirectory(traversal, rootInfo, traverseChildren(env, dependentKeys));
+ } catch (MissingDepException e) {
+ return null;
+ }
+ }
+
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return null;
+ }
+
+ private static final class FileInfo {
+ final FileType type;
+ final FileStateValue metadata;
+ @Nullable final RootedPath realPath;
+ @Nullable final PathFragment unresolvedSymlinkTarget;
+
+ FileInfo(FileType type, FileStateValue metadata, @Nullable RootedPath realPath,
+ @Nullable PathFragment unresolvedSymlinkTarget) {
+ this.type = Preconditions.checkNotNull(type);
+ this.metadata = Preconditions.checkNotNull(metadata);
+ this.realPath = realPath;
+ this.unresolvedSymlinkTarget = unresolvedSymlinkTarget;
+ }
+
+ @Override
+ public String toString() {
+ if (type.isSymlink()) {
+ return String.format("(%s: link_value=%s, real_path=%s)", type,
+ unresolvedSymlinkTarget.getPathString(), realPath);
+ } else {
+ return String.format("(%s: real_path=%s)", type, realPath);
+ }
+ }
+ }
+
+ private static FileInfo lookUpFileInfo(Environment env, TraversalRequest traversal)
+ throws MissingDepException {
+ // Stat the file.
+ FileValue fileValue = (FileValue) getDependentSkyValue(env, FileValue.key(traversal.path));
+ if (fileValue.exists()) {
+ // If it exists, it may either be a symlink or a file/directory.
+ PathFragment unresolvedLinkTarget = null;
+ FileType type = null;
+ if (fileValue.isSymlink()) {
+ unresolvedLinkTarget = fileValue.getUnresolvedLinkTarget();
+ type = fileValue.isDirectory() ? FileType.SYMLINK_TO_DIRECTORY : FileType.SYMLINK_TO_FILE;
+ } else {
+ type = fileValue.isDirectory() ? FileType.DIRECTORY : FileType.FILE;
+ }
+ return new FileInfo(type, fileValue.realFileStateValue(),
+ fileValue.realRootedPath(), unresolvedLinkTarget);
+ } else {
+ // If it doesn't exist, or it's a dangling symlink, we still want to handle that gracefully.
+ return new FileInfo(
+ fileValue.isSymlink() ? FileType.DANGLING_SYMLINK : FileType.NONEXISTENT,
+ fileValue.realFileStateValue(), null,
+ fileValue.isSymlink() ? fileValue.getUnresolvedLinkTarget() : null);
+ }
+ }
+
+ private static final class PkgLookupResult {
+ private enum Type {
+ CONFLICT, DIRECTORY, PKG
+ }
+
+ private final Type type;
+ final TraversalRequest traversal;
+ final FileInfo rootInfo;
+
+ /** Result for a generated directory that conflicts with a source package. */
+ static PkgLookupResult conflict(TraversalRequest traversal, FileInfo rootInfo) {
+ return new PkgLookupResult(Type.CONFLICT, traversal, rootInfo);
+ }
+
+ /** Result for a source or generated directory (not a package). */
+ static PkgLookupResult directory(TraversalRequest traversal, FileInfo rootInfo) {
+ return new PkgLookupResult(Type.DIRECTORY, traversal, rootInfo);
+ }
+
+ /** Result for a package, i.e. a directory with a BUILD file. */
+ static PkgLookupResult pkg(TraversalRequest traversal, FileInfo rootInfo) {
+ return new PkgLookupResult(Type.PKG, traversal, rootInfo);
+ }
+
+ private PkgLookupResult(Type type, TraversalRequest traversal, FileInfo rootInfo) {
+ this.type = Preconditions.checkNotNull(type);
+ this.traversal = Preconditions.checkNotNull(traversal);
+ this.rootInfo = Preconditions.checkNotNull(rootInfo);
+ }
+
+ boolean isPackage() {
+ return type == Type.PKG;
+ }
+
+ boolean isConflicting() {
+ return type == Type.CONFLICT;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("(%s: info=%s, traversal=%s)", type, rootInfo, traversal);
+ }
+ }
+
+ /**
+ * Checks whether the {@code traversal}'s path refers to a package directory.
+ *
+ * @return the result of the lookup; it contains potentially new {@link TraversalRequest} and
+ * {@link FileInfo} so the caller should use these instead of the old ones (this happens when
+ * a package is found, but under a different root than expected)
+ */
+ private static PkgLookupResult checkIfPackage(Environment env, TraversalRequest traversal,
+ FileInfo rootInfo) throws MissingDepException {
+ Preconditions.checkArgument(rootInfo.type.exists() && !rootInfo.type.isFile(),
+ "{%s} {%s}", traversal, rootInfo);
+ PackageLookupValue pkgLookup = (PackageLookupValue) getDependentSkyValue(env,
+ PackageLookupValue.key(traversal.path.getRelativePath()));
+
+ if (pkgLookup.packageExists()) {
+ if (traversal.isGenerated) {
+ // The traversal's root was a generated directory, but its root-relative path conflicts with
+ // an existing package.
+ return PkgLookupResult.conflict(traversal, rootInfo);
+ } else {
+ // The traversal's root was a source directory and it defines a package.
+ Path pkgRoot = pkgLookup.getRoot();
+ if (!pkgRoot.equals(traversal.path.getRoot())) {
+ // However the root of this package is different from what we expected. stat() the real
+ // BUILD file of that package.
+ traversal = traversal.forChangedRootPath(pkgRoot);
+ rootInfo = lookUpFileInfo(env, traversal);
+ Verify.verify(rootInfo.type.exists(), "{%s} {%s}", traversal, rootInfo);
+ }
+ return PkgLookupResult.pkg(traversal, rootInfo);
+ }
+ } else {
+ // The traversal's root was a directory (source or generated one), no package exists under the
+ // same root-relative path.
+ return PkgLookupResult.directory(traversal, rootInfo);
+ }
+ }
+
+ /**
+ * List the directory and create {@code SkyKey}s to request contents of its children recursively.
+ *
+ * <p>The returned keys are of type {@link SkyFunctions#RECURSIVE_FILESYSTEM_TRAVERSAL}.
+ */
+ private static Collection<SkyKey> createRecursiveTraversalKeys(Environment env,
+ TraversalRequest traversal) throws MissingDepException {
+ // Use the traversal's path, even if it's a symlink. The contents of the directory, as listed
+ // in the result, must be relative to it.
+ DirectoryListingValue dirListing = (DirectoryListingValue) getDependentSkyValue(env,
+ DirectoryListingValue.key(traversal.path));
+
+ List<SkyKey> result = new ArrayList<>();
+ for (Dirent dirent : dirListing.getDirents()) {
+ RootedPath childPath = RootedPath.toRootedPath(traversal.path.getRoot(),
+ traversal.path.getRelativePath().getRelative(dirent.getName()));
+ TraversalRequest childTraversal = traversal.forChildEntry(childPath);
+ result.add(RecursiveFilesystemTraversalValue.key(childTraversal));
+ }
+ return result;
+ }
+
+ /**
+ * Creates result for a dangling symlink.
+ *
+ * @param linkName path to the symbolic link
+ * @param info the {@link FileInfo} associated with the link file
+ */
+ private static RecursiveFilesystemTraversalValue resultForDanglingSymlink(RootedPath linkName,
+ FileInfo info) {
+ Preconditions.checkState(info.type.isSymlink() && !info.type.exists(), "{%s} {%s}", linkName,
+ info.type);
+ return RecursiveFilesystemTraversalValue.of(
+ ResolvedFile.danglingSymlink(linkName, info.unresolvedSymlinkTarget, info.metadata));
+ }
+
+ /**
+ * Creates results for a file or for a symlink that points to one.
+ *
+ * <p>A symlink may be direct (points to a file) or transitive (points at a direct or transitive
+ * symlink).
+ */
+ private static RecursiveFilesystemTraversalValue resultForFileRoot(RootedPath path,
+ FileInfo info) {
+ Preconditions.checkState(info.type.isFile() && info.type.exists(), "{%s} {%s}", path,
+ info.type);
+ if (info.type.isSymlink()) {
+ return RecursiveFilesystemTraversalValue.of(ResolvedFile.symlinkToFile(info.realPath, path,
+ info.unresolvedSymlinkTarget, info.metadata));
+ } else {
+ return RecursiveFilesystemTraversalValue.of(ResolvedFile.regularFile(path, info.metadata));
+ }
+ }
+
+ private static RecursiveFilesystemTraversalValue resultForDirectory(TraversalRequest traversal,
+ FileInfo rootInfo, Collection<RecursiveFilesystemTraversalValue> subdirTraversals) {
+ // Collect transitive closure of files in subdirectories.
+ NestedSetBuilder<ResolvedFile> paths = NestedSetBuilder.stableOrder();
+ for (RecursiveFilesystemTraversalValue child : subdirTraversals) {
+ paths.addTransitive(child.getTransitiveFiles());
+ }
+ ResolvedFile root;
+ if (rootInfo.type.isSymlink()) {
+ root = ResolvedFile.symlinkToDirectory(rootInfo.realPath, traversal.path,
+ rootInfo.unresolvedSymlinkTarget, rootInfo.metadata);
+ paths.add(root);
+ } else {
+ root = ResolvedFile.directory(rootInfo.realPath);
+ }
+ return RecursiveFilesystemTraversalValue.of(root, paths.build());
+ }
+
+ private static SkyValue getDependentSkyValue(Environment env, SkyKey key)
+ throws MissingDepException {
+ SkyValue value = env.getValue(key);
+ if (env.valuesMissing()) {
+ throw new MissingDepException();
+ }
+ return value;
+ }
+
+ /**
+ * Requests Skyframe to compute the dependent values and returns them.
+ *
+ * <p>The keys must all be {@link SkyFunctions#RECURSIVE_FILESYSTEM_TRAVERSAL} keys.
+ */
+ private static Collection<RecursiveFilesystemTraversalValue> traverseChildren(
+ Environment env, Iterable<SkyKey> keys)
+ throws MissingDepException {
+ Map<SkyKey, SkyValue> values = env.getValues(keys);
+ if (env.valuesMissing()) {
+ throw new MissingDepException();
+ }
+ return Collections2.transform(values.values(),
+ new Function<SkyValue, RecursiveFilesystemTraversalValue>() {
+ @Override
+ public RecursiveFilesystemTraversalValue apply(SkyValue input) {
+ return (RecursiveFilesystemTraversalValue) input;
+ }
+ });
+ }
+
+ /** Type information about the filesystem entry residing at a path. */
+ enum FileType {
+ /** A regular file. */
+ FILE {
+ @Override boolean isFile() { return true; }
+ @Override boolean exists() { return true; }
+ @Override public String toString() { return "<f>"; }
+ },
+ /**
+ * A symlink to a regular file.
+ *
+ * <p>The symlink may be direct (points to a non-symlink (here a file)) or it may be transitive
+ * (points to a direct or transitive symlink).
+ */
+ SYMLINK_TO_FILE {
+ @Override boolean isFile() { return true; }
+ @Override boolean isSymlink() { return true; }
+ @Override boolean exists() { return true; }
+ @Override public String toString() { return "<lf>"; }
+ },
+ /** A directory. */
+ DIRECTORY {
+ @Override boolean isDirectory() { return true; }
+ @Override boolean exists() { return true; }
+ @Override public String toString() { return "<d>"; }
+ },
+ /**
+ * A symlink to a directory.
+ *
+ * <p>The symlink may be direct (points to a non-symlink (here a directory)) or it may be
+ * transitive (points to a direct or transitive symlink).
+ */
+ SYMLINK_TO_DIRECTORY {
+ @Override boolean isDirectory() { return true; }
+ @Override boolean isSymlink() { return true; }
+ @Override boolean exists() { return true; }
+ @Override public String toString() { return "<ld>"; }
+ },
+ /** A dangling symlink, i.e. one whose target is known not to exist. */
+ DANGLING_SYMLINK {
+ @Override boolean isFile() { throw new UnsupportedOperationException(); }
+ @Override boolean isDirectory() { throw new UnsupportedOperationException(); }
+ @Override boolean isSymlink() { return true; }
+ @Override public String toString() { return "<l?>"; }
+ },
+ /** A path that does not exist or should be ignored. */
+ NONEXISTENT {
+ @Override public String toString() { return "<?>"; }
+ };
+
+ boolean isFile() { return false; }
+ boolean isDirectory() { return false; }
+ boolean isSymlink() { return false; }
+ boolean exists() { return false; }
+ @Override public abstract String toString();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/RecursiveFilesystemTraversalValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/RecursiveFilesystemTraversalValue.java
new file mode 100644
index 0000000..023b1cf
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/RecursiveFilesystemTraversalValue.java
@@ -0,0 +1,597 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Objects;
+import com.google.common.base.Optional;
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.skyframe.RecursiveFilesystemTraversalFunction.DanglingSymlinkException;
+import com.google.devtools.build.lib.skyframe.RecursiveFilesystemTraversalFunction.FileType;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.lib.vfs.RootedPath;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import javax.annotation.Nullable;
+
+/**
+ * Collection of files found while recursively traversing a path.
+ *
+ * <p>The path may refer to files, symlinks or directories that may or may not exist.
+ *
+ * <p>Traversing a file or a symlink results in a single {@link ResolvedFile} corresponding to the
+ * file or symlink.
+ *
+ * <p>Traversing a directory results in a collection of {@link ResolvedFile}s for all files and
+ * symlinks under it, and in all of its subdirectories. The {@link TraversalRequest} can specify
+ * whether to traverse source subdirectories that are packages (have BUILD files in them).
+ *
+ * <p>Traversing a symlink that points to a directory is the same as traversing a normal directory.
+ * The paths in the result will not be resolved; the files will be listed under the symlink, as if
+ * it was the actual directory they reside in.
+ *
+ * <p>Editing a file that is part of this traversal, or adding or removing a file in a directory
+ * that is part of this traversal, will invalidate this {@link SkyValue}. This also applies to
+ * directories that are symlinked to.
+ */
+public final class RecursiveFilesystemTraversalValue implements SkyValue {
+ static final RecursiveFilesystemTraversalValue EMPTY = new RecursiveFilesystemTraversalValue(
+ Optional.<ResolvedFile>absent(),
+ NestedSetBuilder.<ResolvedFile>emptySet(Order.STABLE_ORDER));
+
+ /** The root of the traversal. May only be absent for the {@link #EMPTY} instance. */
+ private final Optional<ResolvedFile> resolvedRoot;
+
+ /** The transitive closure of {@link ResolvedFile}s. */
+ private final NestedSet<ResolvedFile> resolvedPaths;
+
+ private RecursiveFilesystemTraversalValue(Optional<ResolvedFile> resolvedRoot,
+ NestedSet<ResolvedFile> resolvedPaths) {
+ this.resolvedRoot = Preconditions.checkNotNull(resolvedRoot);
+ this.resolvedPaths = Preconditions.checkNotNull(resolvedPaths);
+ }
+
+ static RecursiveFilesystemTraversalValue of(ResolvedFile resolvedRoot,
+ NestedSet<ResolvedFile> resolvedPaths) {
+ if (resolvedPaths.isEmpty()) {
+ return EMPTY;
+ } else {
+ return new RecursiveFilesystemTraversalValue(Optional.of(resolvedRoot), resolvedPaths);
+ }
+ }
+
+ static RecursiveFilesystemTraversalValue of(ResolvedFile singleMember) {
+ return new RecursiveFilesystemTraversalValue(Optional.of(singleMember),
+ NestedSetBuilder.<ResolvedFile>create(Order.STABLE_ORDER, singleMember));
+ }
+
+ /** Returns the root of the traversal; absent only for the {@link #EMPTY} instance. */
+ public Optional<ResolvedFile> getResolvedRoot() {
+ return resolvedRoot;
+ }
+
+ /**
+ * Retrieves the set of {@link ResolvedFile}s that were found by this traversal.
+ *
+ * <p>The returned set may be empty if no files were found, or the ones found were to be
+ * considered non-existent. Unless it's empty, the returned set always includes the
+ * {@link #getResolvedRoot() resolved root}.
+ *
+ * <p>The returned set also includes symlinks. If a symlink points to a directory, its contents
+ * are also included in this set, and their path will start with the symlink's path, just like on
+ * a usual Unix file system.
+ */
+ public NestedSet<ResolvedFile> getTransitiveFiles() {
+ return resolvedPaths;
+ }
+
+ public static SkyKey key(TraversalRequest traversal) {
+ return new SkyKey(SkyFunctions.RECURSIVE_FILESYSTEM_TRAVERSAL, traversal);
+ }
+
+ /** The parameters of a file or directory traversal. */
+ public static final class TraversalRequest {
+
+ /** The path to start the traversal from; may be a file, a directory or a symlink. */
+ final RootedPath path;
+
+ /**
+ * Whether the path is in the output tree.
+ *
+ * <p>Such paths and all their subdirectories are assumed not to define packages, so package
+ * lookup for them is skipped.
+ */
+ final boolean isGenerated;
+
+ /** Whether traversal should descend into directories that are roots of subpackages. */
+ final boolean crossPkgBoundaries;
+
+ /**
+ * Whether to skip checking if the root (if it's a directory) contains a BUILD file.
+ *
+ * <p>Such directories are not considered to be packages when this flag is true. This needs to
+ * be true in order to traverse directories of packages, but should be false for <i>their</i>
+ * subdirectories.
+ */
+ final boolean skipTestingForSubpackage;
+
+ /** Information to be attached to any error messages that may be reported. */
+ @Nullable final String errorInfo;
+
+ public TraversalRequest(RootedPath path, boolean isRootGenerated,
+ boolean crossPkgBoundaries, boolean skipTestingForSubpackage,
+ @Nullable String errorInfo) {
+ this.path = path;
+ this.isGenerated = isRootGenerated;
+ this.crossPkgBoundaries = crossPkgBoundaries;
+ this.skipTestingForSubpackage = skipTestingForSubpackage;
+ this.errorInfo = errorInfo;
+ }
+
+ private TraversalRequest duplicate(RootedPath newRoot, boolean newSkipTestingForSubpackage) {
+ return new TraversalRequest(newRoot, isGenerated, crossPkgBoundaries,
+ newSkipTestingForSubpackage, errorInfo);
+ }
+
+ /** Creates a new request to traverse a child element in the current directory (the root). */
+ TraversalRequest forChildEntry(RootedPath newPath) {
+ return duplicate(newPath, false);
+ }
+
+ /**
+ * Creates a new request for a changed root.
+ *
+ * <p>This method can be used when a package is found out to be under a different root path than
+ * originally assumed.
+ */
+ TraversalRequest forChangedRootPath(Path newRoot) {
+ return duplicate(RootedPath.toRootedPath(newRoot, path.getRelativePath()),
+ skipTestingForSubpackage);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof TraversalRequest)) {
+ return false;
+ }
+ TraversalRequest o = (TraversalRequest) obj;
+ return path.equals(o.path) && isGenerated == o.isGenerated
+ && crossPkgBoundaries == o.crossPkgBoundaries
+ && skipTestingForSubpackage == o.skipTestingForSubpackage;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(path, isGenerated, crossPkgBoundaries, skipTestingForSubpackage);
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "TraversalParams(root=%s, is_generated=%d, skip_testing_for_subpkg=%d,"
+ + " pkg_boundaries=%d)", path, isGenerated ? 1 : 0, skipTestingForSubpackage ? 1 : 0,
+ crossPkgBoundaries ? 1 : 0);
+ }
+ }
+
+ /**
+ * Path and type information about a single file or symlink.
+ *
+ * <p>The object stores things such as the absolute path of the file or symlink, its exact type
+ * and, if it's a symlink, the resolved and unresolved link target paths.
+ */
+ public abstract static class ResolvedFile {
+ private static final class Symlink {
+ private final RootedPath linkName;
+ private final PathFragment unresolvedLinkTarget;
+ // The resolved link target is stored in ResolvedFile.path
+
+ private Symlink(RootedPath linkName, PathFragment unresolvedLinkTarget) {
+ this.linkName = Preconditions.checkNotNull(linkName);
+ this.unresolvedLinkTarget = Preconditions.checkNotNull(unresolvedLinkTarget);
+ }
+
+ PathFragment getNameInSymlinkTree() {
+ return linkName.getRelativePath();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof Symlink)) {
+ return false;
+ }
+ Symlink o = (Symlink) obj;
+ return linkName.equals(o.linkName) && unresolvedLinkTarget.equals(o.unresolvedLinkTarget);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(linkName, unresolvedLinkTarget);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("Symlink(link_name=%s, unresolved_target=%s)",
+ linkName, unresolvedLinkTarget);
+ }
+ }
+
+ private static final class RegularFile extends ResolvedFile {
+ private RegularFile(RootedPath path) {
+ super(FileType.FILE, Optional.of(path), Optional.<FileStateValue>absent());
+ }
+
+ RegularFile(RootedPath path, FileStateValue metadata) {
+ super(FileType.FILE, Optional.of(path), Optional.of(metadata));
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof RegularFile)) {
+ return false;
+ }
+ return super.isEqualTo((RegularFile) obj);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("RegularFile(%s)", super.toString());
+ }
+
+ @Override
+ ResolvedFile stripMetadataForTesting() {
+ return new RegularFile(path.get());
+ }
+
+ @Override
+ public PathFragment getNameInSymlinkTree() {
+ return path.get().getRelativePath();
+ }
+
+ @Override
+ public PathFragment getTargetInSymlinkTree(boolean followSymlinks) {
+ return path.get().asPath().asFragment();
+ }
+ }
+
+ private static final class Directory extends ResolvedFile {
+ Directory(RootedPath path) {
+ super(FileType.DIRECTORY, Optional.of(path), Optional.of(
+ FileStateValue.DIRECTORY_FILE_STATE_NODE));
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof Directory)) {
+ return false;
+ }
+ return super.isEqualTo((Directory) obj);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("Directory(%s)", super.toString());
+ }
+
+ @Override
+ ResolvedFile stripMetadataForTesting() {
+ return this;
+ }
+
+ @Override
+ public PathFragment getNameInSymlinkTree() {
+ return path.get().getRelativePath();
+ }
+
+ @Override
+ public PathFragment getTargetInSymlinkTree(boolean followSymlinks) {
+ return path.get().asPath().asFragment();
+ }
+ }
+
+ private static final class DanglingSymlink extends ResolvedFile {
+ private final Symlink symlink;
+
+ private DanglingSymlink(Symlink symlink) {
+ super(FileType.DANGLING_SYMLINK, Optional.<RootedPath>absent(),
+ Optional.<FileStateValue>absent());
+ this.symlink = symlink;
+ }
+
+ DanglingSymlink(RootedPath linkNamePath, PathFragment linkTargetPath,
+ FileStateValue metadata) {
+ super(FileType.DANGLING_SYMLINK, Optional.<RootedPath>absent(), Optional.of(metadata));
+ this.symlink = new Symlink(linkNamePath, linkTargetPath);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof DanglingSymlink)) {
+ return false;
+ }
+ DanglingSymlink o = (DanglingSymlink) obj;
+ return super.isEqualTo(o) && symlink.equals(o.symlink);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(super.hashCode(), symlink);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("DanglingSymlink(%s, %s)", super.toString(), symlink);
+ }
+
+ @Override
+ ResolvedFile stripMetadataForTesting() {
+ return new DanglingSymlink(symlink);
+ }
+
+ @Override
+ public PathFragment getNameInSymlinkTree() {
+ return symlink.getNameInSymlinkTree();
+ }
+
+ @Override
+ public PathFragment getTargetInSymlinkTree(boolean followSymlinks)
+ throws DanglingSymlinkException {
+ if (followSymlinks) {
+ throw new DanglingSymlinkException(symlink.linkName.asPath().getPathString(),
+ symlink.unresolvedLinkTarget.getPathString());
+ } else {
+ return symlink.unresolvedLinkTarget;
+ }
+ }
+ }
+
+ private static final class SymlinkToFile extends ResolvedFile {
+ private final Symlink symlink;
+
+ private SymlinkToFile(RootedPath targetPath, Symlink symlink) {
+ super(FileType.SYMLINK_TO_FILE, Optional.of(targetPath), Optional.<FileStateValue>absent());
+ this.symlink = symlink;
+ }
+
+ SymlinkToFile(RootedPath targetPath, RootedPath linkNamePath,
+ PathFragment linkTargetPath, FileStateValue metadata) {
+ super(FileType.SYMLINK_TO_FILE, Optional.of(targetPath), Optional.of(metadata));
+ this.symlink = new Symlink(linkNamePath, linkTargetPath);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof SymlinkToFile)) {
+ return false;
+ }
+ SymlinkToFile o = (SymlinkToFile) obj;
+ return super.isEqualTo(o) && symlink.equals(o.symlink);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(super.hashCode(), symlink);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("SymlinkToFile(%s, %s)", super.toString(), symlink);
+ }
+
+ @Override
+ ResolvedFile stripMetadataForTesting() {
+ return new SymlinkToFile(path.get(), symlink);
+ }
+
+ @Override
+ public PathFragment getNameInSymlinkTree() {
+ return symlink.getNameInSymlinkTree();
+ }
+
+ @Override
+ public PathFragment getTargetInSymlinkTree(boolean followSymlinks) {
+ return followSymlinks ? path.get().asPath().asFragment() : symlink.unresolvedLinkTarget;
+ }
+ }
+
+ private static final class SymlinkToDirectory extends ResolvedFile {
+ private final Symlink symlink;
+
+ private SymlinkToDirectory(RootedPath targetPath, Symlink symlink) {
+ super(FileType.SYMLINK_TO_DIRECTORY, Optional.of(targetPath),
+ Optional.<FileStateValue>absent());
+ this.symlink = symlink;
+ }
+
+ SymlinkToDirectory(RootedPath targetPath, RootedPath linkNamePath,
+ PathFragment linkValue, FileStateValue metadata) {
+ super(FileType.SYMLINK_TO_DIRECTORY, Optional.of(targetPath), Optional.of(metadata));
+ this.symlink = new Symlink(linkNamePath, linkValue);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof SymlinkToDirectory)) {
+ return false;
+ }
+ SymlinkToDirectory o = (SymlinkToDirectory) obj;
+ return super.isEqualTo(o) && symlink.equals(o.symlink);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(super.hashCode(), symlink);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("SymlinkToDirectory(%s, %s)", super.toString(), symlink);
+ }
+
+ @Override
+ ResolvedFile stripMetadataForTesting() {
+ return new SymlinkToDirectory(path.get(), symlink);
+ }
+
+ @Override
+ public PathFragment getNameInSymlinkTree() {
+ return symlink.getNameInSymlinkTree();
+ }
+
+ @Override
+ public PathFragment getTargetInSymlinkTree(boolean followSymlinks) {
+ return followSymlinks ? path.get().asPath().asFragment() : symlink.unresolvedLinkTarget;
+ }
+ }
+
+ /** Type of the entity under {@link #path}. */
+ final FileType type;
+
+ /**
+ * Path of the file, directory or resolved target of the symlink.
+ *
+ * <p>May only be absent for dangling symlinks.
+ */
+ protected final Optional<RootedPath> path;
+
+ /**
+ * Associated metadata.
+ *
+ * <p>This field must be stored so that this {@link ResolvedFile} is (also) the function of the
+ * stat() of the file, but otherwise it is likely not something the consumer of the
+ * {@link ResolvedFile} is directly interested in.
+ *
+ * <p>May only be absent if stripped for tests.
+ */
+ final Optional<FileStateValue> metadata;
+
+ private ResolvedFile(FileType type, Optional<RootedPath> path,
+ Optional<FileStateValue> metadata) {
+ this.type = Preconditions.checkNotNull(type);
+ this.path = Preconditions.checkNotNull(path);
+ this.metadata = Preconditions.checkNotNull(metadata);
+ }
+
+ static ResolvedFile regularFile(RootedPath path, FileStateValue metadata) {
+ return new RegularFile(path, metadata);
+ }
+
+ static ResolvedFile directory(RootedPath path) {
+ return new Directory(path);
+ }
+
+ static ResolvedFile symlinkToFile(RootedPath targetPath, RootedPath linkNamePath,
+ PathFragment linkTargetPath, FileStateValue metadata) {
+ return new SymlinkToFile(targetPath, linkNamePath, linkTargetPath, metadata);
+ }
+
+ static ResolvedFile symlinkToDirectory(RootedPath targetPath,
+ RootedPath linkNamePath, PathFragment linkValue, FileStateValue metadata) {
+ return new SymlinkToDirectory(targetPath, linkNamePath, linkValue, metadata);
+ }
+
+ static ResolvedFile danglingSymlink(RootedPath linkNamePath, PathFragment linkValue,
+ FileStateValue metadata) {
+ return new DanglingSymlink(linkNamePath, linkValue, metadata);
+ }
+
+ private boolean isEqualTo(ResolvedFile o) {
+ return type.equals(o.type) && path.equals(o.path) && metadata.equals(o.metadata);
+ }
+
+ @Override
+ public abstract boolean equals(Object obj);
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(type, path, metadata);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("type=%s, path=%s, metadata=%s", type, path,
+ metadata.isPresent() ? Integer.toHexString(metadata.get().hashCode()) : "(stripped)");
+ }
+
+ /**
+ * Returns the path of the Fileset-output symlink relative to the output directory.
+ *
+ * <p>The path should contain the FilesetEntry-specific destination directory (if any) and
+ * should have necessary prefixes stripped (if any).
+ */
+ public abstract PathFragment getNameInSymlinkTree();
+
+ /**
+ * Returns the path of the symlink target.
+ *
+ * @throws DanglingSymlinkException if the target cannot be resolved because the symlink is
+ * dangling
+ */
+ public abstract PathFragment getTargetInSymlinkTree(boolean followSymlinks)
+ throws DanglingSymlinkException;
+
+ /**
+ * Returns a copy of this object with the metadata stripped away.
+ *
+ * <p>This method should only be used by tests that wish to assert that this
+ * {@link ResolvedFile} refers to the expected absolute path and has the expected type, without
+ * asserting its actual contents (which the metadata is a function of).
+ */
+ @VisibleForTesting
+ abstract ResolvedFile stripMetadataForTesting();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof RecursiveFilesystemTraversalValue)) {
+ return false;
+ }
+ RecursiveFilesystemTraversalValue o = (RecursiveFilesystemTraversalValue) obj;
+ return resolvedRoot.equals(o.resolvedRoot) && resolvedPaths.equals(o.resolvedPaths);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(resolvedRoot, resolvedPaths);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/RecursivePkgFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/RecursivePkgFunction.java
new file mode 100644
index 0000000..11ed3be
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/RecursivePkgFunction.java
@@ -0,0 +1,151 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.collect.Lists;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.packages.NoSuchPackageException;
+import com.google.devtools.build.lib.packages.PackageIdentifier;
+import com.google.devtools.build.lib.pkgcache.PathPackageLocator;
+import com.google.devtools.build.lib.vfs.Dirent;
+import com.google.devtools.build.lib.vfs.Dirent.Type;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.lib.vfs.RootedPath;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nullable;
+
+/**
+ * RecursivePkgFunction builds up the set of packages underneath a given directory
+ * transitively.
+ *
+ * <p>Example: foo/BUILD, foo/sub/x, foo/subpkg/BUILD would yield transitive packages "foo" and
+ * "foo/subpkg".
+ */
+public class RecursivePkgFunction implements SkyFunction {
+
+ private static final Order ORDER = Order.STABLE_ORDER;
+
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env) {
+ RootedPath rootedPath = (RootedPath) skyKey.argument();
+ Path root = rootedPath.getRoot();
+ PathFragment rootRelativePath = rootedPath.getRelativePath();
+
+ SkyKey fileKey = FileValue.key(rootedPath);
+ FileValue fileValue = (FileValue) env.getValue(fileKey);
+ if (fileValue == null) {
+ return null;
+ }
+
+ if (!fileValue.isDirectory()) {
+ return new RecursivePkgValue(NestedSetBuilder.<String>emptySet(ORDER));
+ }
+
+ if (fileValue.isSymlink()) {
+ // We do not follow directory symlinks when we look recursively for packages. It also
+ // prevents symlink loops.
+ return new RecursivePkgValue(NestedSetBuilder.<String>emptySet(ORDER));
+ }
+
+ PackageIdentifier packageId = PackageIdentifier.createInDefaultRepo(
+ rootRelativePath.getPathString());
+ PackageLookupValue pkgLookupValue =
+ (PackageLookupValue) env.getValue(PackageLookupValue.key(packageId));
+ if (pkgLookupValue == null) {
+ return null;
+ }
+
+ NestedSetBuilder<String> packages = new NestedSetBuilder<>(ORDER);
+
+ if (pkgLookupValue.packageExists()) {
+ if (pkgLookupValue.getRoot().equals(root)) {
+ try {
+ PackageValue pkgValue = (PackageValue)
+ env.getValueOrThrow(PackageValue.key(packageId),
+ NoSuchPackageException.class);
+ if (pkgValue == null) {
+ return null;
+ }
+ packages.add(pkgValue.getPackage().getName());
+ } catch (NoSuchPackageException e) {
+ // The package had errors, but don't fail-fast as there might subpackages below the
+ // current directory.
+ env.getListener().handle(Event.error(
+ "package contains errors: " + rootRelativePath.getPathString()));
+ if (e.getPackage() != null) {
+ packages.add(e.getPackage().getName());
+ }
+ }
+ }
+ // The package lookup succeeded, but was under a different root. We still, however, need to
+ // recursively consider subdirectories. For example:
+ //
+ // Pretend --package_path=rootA/workspace:rootB/workspace and these are the only files:
+ // rootA/workspace/foo/
+ // rootA/workspace/foo/bar/BUILD
+ // rootB/workspace/foo/BUILD
+ // If we're doing a recursive package lookup under 'rootA/workspace' starting at 'foo', note
+ // that even though the package 'foo' is under 'rootB/workspace', there is still a package
+ // 'foo/bar' under 'rootA/workspace'.
+ }
+
+ DirectoryListingValue dirValue = (DirectoryListingValue)
+ env.getValue(DirectoryListingValue.key(rootedPath));
+ if (dirValue == null) {
+ return null;
+ }
+
+ List<SkyKey> childDeps = Lists.newArrayList();
+ for (Dirent dirent : dirValue.getDirents()) {
+ if (dirent.getType() != Type.DIRECTORY) {
+ // Non-directories can never host packages, and we do not follow symlinks (see above).
+ continue;
+ }
+ String basename = dirent.getName();
+ if (rootRelativePath.equals(PathFragment.EMPTY_FRAGMENT)
+ && PathPackageLocator.DEFAULT_TOP_LEVEL_EXCLUDES.contains(basename)) {
+ continue;
+ }
+ SkyKey req = RecursivePkgValue.key(RootedPath.toRootedPath(root,
+ rootRelativePath.getRelative(basename)));
+ childDeps.add(req);
+ }
+ Map<SkyKey, SkyValue> childValueMap = env.getValues(childDeps);
+ if (env.valuesMissing()) {
+ return null;
+ }
+ // Aggregate the transitive subpackages.
+ for (SkyValue childValue : childValueMap.values()) {
+ if (childValue != null) {
+ packages.addTransitive(((RecursivePkgValue) childValue).getPackages());
+ }
+ }
+ return new RecursivePkgValue(packages.build());
+ }
+
+ @Nullable
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return null;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/RecursivePkgValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/RecursivePkgValue.java
new file mode 100644
index 0000000..4013b90
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/RecursivePkgValue.java
@@ -0,0 +1,48 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.vfs.RootedPath;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+/**
+ * This value represents the result of looking up all the packages under a given package path root,
+ * starting at a given directory.
+ */
+@Immutable
+@ThreadSafe
+public class RecursivePkgValue implements SkyValue {
+
+ private final NestedSet<String> packages;
+
+ public RecursivePkgValue(NestedSet<String> packages) {
+ this.packages = packages;
+ }
+
+ /**
+ * Create a transitive package lookup request.
+ */
+ @ThreadSafe
+ public static SkyKey key(RootedPath rootedPath) {
+ return new SkyKey(SkyFunctions.RECURSIVE_PKG, rootedPath);
+ }
+
+ public NestedSet<String> getPackages() {
+ return packages;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/RepositoryValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/RepositoryValue.java
new file mode 100644
index 0000000..3183953
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/RepositoryValue.java
@@ -0,0 +1,75 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Objects;
+import com.google.devtools.build.lib.packages.PackageIdentifier.RepositoryName;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+/**
+ * A local view of an external repository.
+ */
+public class RepositoryValue implements SkyValue {
+ private final Path path;
+
+ /**
+ * If path is a symlink, this will keep track of what the symlink actually points to (for
+ * checking equality).
+ */
+ private final FileValue details;
+
+ public RepositoryValue(Path path, FileValue repositoryDirectory) {
+ this.path = path;
+ this.details = repositoryDirectory;
+ }
+
+ /**
+ * Returns the path to the directory containing the repository's contents. This directory is
+ * guaranteed to exist. It may contain a full Bazel repository (with a WORKSPACE file,
+ * directories, and BUILD files) or simply contain a file (or set of files) for, say, a jar from
+ * Maven.
+ */
+ public Path getPath() {
+ return path;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+
+ if (other instanceof RepositoryValue) {
+ RepositoryValue otherValue = (RepositoryValue) other;
+ return path.equals(otherValue.path) && details.equals(otherValue.details);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(path, details);
+ }
+
+ /**
+ * Creates a key from the given repository name.
+ */
+ public static SkyKey key(RepositoryName repository) {
+ return new SkyKey(SkyFunctions.REPOSITORY, repository);
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SequencedSkyframeExecutor.java b/src/main/java/com/google/devtools/build/lib/skyframe/SequencedSkyframeExecutor.java
new file mode 100644
index 0000000..e547166
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SequencedSkyframeExecutor.java
@@ -0,0 +1,580 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSetMultimap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Sets;
+import com.google.devtools.build.lib.analysis.BlazeDirectories;
+import com.google.devtools.build.lib.analysis.BuildView;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.WorkspaceStatusAction;
+import com.google.devtools.build.lib.analysis.WorkspaceStatusAction.Factory;
+import com.google.devtools.build.lib.analysis.buildinfo.BuildInfoFactory;
+import com.google.devtools.build.lib.events.Reporter;
+import com.google.devtools.build.lib.packages.Package;
+import com.google.devtools.build.lib.packages.PackageFactory;
+import com.google.devtools.build.lib.packages.PackageIdentifier;
+import com.google.devtools.build.lib.packages.Preprocessor;
+import com.google.devtools.build.lib.pkgcache.PackageCacheOptions;
+import com.google.devtools.build.lib.pkgcache.PathPackageLocator;
+import com.google.devtools.build.lib.util.AbruptExitException;
+import com.google.devtools.build.lib.util.Pair;
+import com.google.devtools.build.lib.util.ResourceUsage;
+import com.google.devtools.build.lib.util.io.TimestampGranularityMonitor;
+import com.google.devtools.build.lib.vfs.ModifiedFileSet;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.lib.vfs.RootedPath;
+import com.google.devtools.build.skyframe.BuildDriver;
+import com.google.devtools.build.skyframe.Differencer;
+import com.google.devtools.build.skyframe.ImmutableDiff;
+import com.google.devtools.build.skyframe.InMemoryMemoizingEvaluator;
+import com.google.devtools.build.skyframe.Injectable;
+import com.google.devtools.build.skyframe.MemoizingEvaluator.EvaluatorSupplier;
+import com.google.devtools.build.skyframe.RecordingDifferencer;
+import com.google.devtools.build.skyframe.SequentialBuildDriver;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyFunctionName;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.Callable;
+
+/**
+ * A SkyframeExecutor that implicitly assumes that builds can be done incrementally from the most
+ * recent build. In other words, builds are "sequenced".
+ */
+public final class SequencedSkyframeExecutor extends SkyframeExecutor {
+ /** Lower limit for number of loaded packages to consider clearing CT values. */
+ private int valueCacheEvictionLimit = -1;
+
+ /** Union of labels of loaded packages since the last eviction of CT values. */
+ private Set<PackageIdentifier> allLoadedPackages = ImmutableSet.of();
+ private boolean lastAnalysisDiscarded = false;
+
+ // Can only be set once (to false) over the lifetime of this object. If false, the graph will not
+ // store edges, saving memory but making incremental builds impossible.
+ private boolean keepGraphEdges = true;
+
+ private RecordingDifferencer recordingDiffer;
+ private final DiffAwarenessManager diffAwarenessManager;
+
+ private SequencedSkyframeExecutor(Reporter reporter, EvaluatorSupplier evaluatorSupplier,
+ PackageFactory pkgFactory, TimestampGranularityMonitor tsgm,
+ BlazeDirectories directories, Factory workspaceStatusActionFactory,
+ ImmutableList<BuildInfoFactory> buildInfoFactories,
+ Set<Path> immutableDirectories,
+ Iterable<? extends DiffAwareness.Factory> diffAwarenessFactories,
+ Predicate<PathFragment> allowedMissingInputs,
+ Preprocessor.Factory.Supplier preprocessorFactorySupplier,
+ ImmutableMap<SkyFunctionName, SkyFunction> extraSkyFunctions,
+ ImmutableList<PrecomputedValue.Injected> extraPrecomputedValues) {
+ super(reporter, evaluatorSupplier, pkgFactory, tsgm, directories,
+ workspaceStatusActionFactory, buildInfoFactories, immutableDirectories,
+ allowedMissingInputs, preprocessorFactorySupplier,
+ extraSkyFunctions, extraPrecomputedValues);
+ this.diffAwarenessManager = new DiffAwarenessManager(diffAwarenessFactories, reporter);
+ }
+
+ private SequencedSkyframeExecutor(Reporter reporter, PackageFactory pkgFactory,
+ TimestampGranularityMonitor tsgm, BlazeDirectories directories,
+ Factory workspaceStatusActionFactory,
+ ImmutableList<BuildInfoFactory> buildInfoFactories,
+ Set<Path> immutableDirectories,
+ Iterable<? extends DiffAwareness.Factory> diffAwarenessFactories,
+ Predicate<PathFragment> allowedMissingInputs,
+ Preprocessor.Factory.Supplier preprocessorFactorySupplier,
+ ImmutableMap<SkyFunctionName, SkyFunction> extraSkyFunctions,
+ ImmutableList<PrecomputedValue.Injected> extraPrecomputedValues) {
+ this(reporter, InMemoryMemoizingEvaluator.SUPPLIER, pkgFactory, tsgm,
+ directories, workspaceStatusActionFactory, buildInfoFactories, immutableDirectories,
+ diffAwarenessFactories, allowedMissingInputs, preprocessorFactorySupplier,
+ extraSkyFunctions, extraPrecomputedValues);
+ }
+
+ private static SequencedSkyframeExecutor create(Reporter reporter,
+ EvaluatorSupplier evaluatorSupplier, PackageFactory pkgFactory,
+ TimestampGranularityMonitor tsgm, BlazeDirectories directories,
+ Factory workspaceStatusActionFactory, ImmutableList<BuildInfoFactory> buildInfoFactories,
+ Set<Path> immutableDirectories,
+ Iterable<? extends DiffAwareness.Factory> diffAwarenessFactories,
+ Predicate<PathFragment> allowedMissingInputs,
+ Preprocessor.Factory.Supplier preprocessorFactorySupplier,
+ ImmutableMap<SkyFunctionName, SkyFunction> extraSkyFunctions,
+ ImmutableList<PrecomputedValue.Injected> extraPrecomputedValues) {
+ SequencedSkyframeExecutor skyframeExecutor = new SequencedSkyframeExecutor(reporter,
+ evaluatorSupplier, pkgFactory, tsgm, directories, workspaceStatusActionFactory,
+ buildInfoFactories, immutableDirectories, diffAwarenessFactories, allowedMissingInputs,
+ preprocessorFactorySupplier,
+ extraSkyFunctions, extraPrecomputedValues);
+ skyframeExecutor.init();
+ return skyframeExecutor;
+ }
+
+ public static SequencedSkyframeExecutor create(Reporter reporter, PackageFactory pkgFactory,
+ TimestampGranularityMonitor tsgm, BlazeDirectories directories,
+ Factory workspaceStatusActionFactory,
+ ImmutableList<BuildInfoFactory> buildInfoFactories,
+ Set<Path> immutableDirectories,
+ Iterable<? extends DiffAwareness.Factory> diffAwarenessFactories,
+ Predicate<PathFragment> allowedMissingInputs,
+ Preprocessor.Factory.Supplier preprocessorFactorySupplier,
+ ImmutableMap<SkyFunctionName, SkyFunction> extraSkyFunctions,
+ ImmutableList<PrecomputedValue.Injected> extraPrecomputedValues) {
+ return create(reporter, InMemoryMemoizingEvaluator.SUPPLIER, pkgFactory, tsgm,
+ directories, workspaceStatusActionFactory, buildInfoFactories, immutableDirectories,
+ diffAwarenessFactories, allowedMissingInputs, preprocessorFactorySupplier,
+ extraSkyFunctions, extraPrecomputedValues);
+ }
+
+ @VisibleForTesting
+ public static SequencedSkyframeExecutor create(Reporter reporter, PackageFactory pkgFactory,
+ TimestampGranularityMonitor tsgm, BlazeDirectories directories,
+ WorkspaceStatusAction.Factory workspaceStatusActionFactory,
+ ImmutableList<BuildInfoFactory> buildInfoFactories,
+ Set<Path> immutableDirectories,
+ Iterable<? extends DiffAwareness.Factory> diffAwarenessFactories) {
+ return create(reporter, pkgFactory, tsgm, directories, workspaceStatusActionFactory,
+ buildInfoFactories, immutableDirectories, diffAwarenessFactories,
+ Predicates.<PathFragment>alwaysFalse(),
+ Preprocessor.Factory.Supplier.NullSupplier.INSTANCE,
+ ImmutableMap.<SkyFunctionName, SkyFunction>of(),
+ ImmutableList.<PrecomputedValue.Injected>of());
+ }
+
+ @Override
+ protected BuildDriver newBuildDriver() {
+ return new SequentialBuildDriver(memoizingEvaluator);
+ }
+
+ @Override
+ protected void init() {
+ // Note that we need to set recordingDiffer first since SkyframeExecutor#init calls
+ // SkyframeExecutor#evaluatorDiffer.
+ recordingDiffer = new RecordingDifferencer();
+ super.init();
+ }
+
+ @Override
+ public void resetEvaluator() {
+ super.resetEvaluator();
+ diffAwarenessManager.reset();
+ }
+
+ @Override
+ protected Differencer evaluatorDiffer() {
+ return recordingDiffer;
+ }
+
+ @Override
+ protected Injectable injectable() {
+ return recordingDiffer;
+ }
+
+ @VisibleForTesting
+ public RecordingDifferencer getDifferencerForTesting() {
+ return recordingDiffer;
+ }
+
+ @Override
+ public void sync(PackageCacheOptions packageCacheOptions, Path workingDirectory,
+ String defaultsPackageContents, UUID commandId)
+ throws InterruptedException, AbruptExitException {
+ this.valueCacheEvictionLimit = packageCacheOptions.minLoadedPkgCountForCtNodeEviction;
+ super.sync(packageCacheOptions, workingDirectory, defaultsPackageContents, commandId);
+ handleDiffs();
+ }
+
+ /**
+ * The value types whose builders have direct access to the package locator, rather than accessing
+ * it via an explicit Skyframe dependency. They need to be invalidated if the package locator
+ * changes.
+ */
+ private static final Set<SkyFunctionName> PACKAGE_LOCATOR_DEPENDENT_VALUES = ImmutableSet.of(
+ SkyFunctions.FILE_STATE,
+ SkyFunctions.FILE,
+ SkyFunctions.DIRECTORY_LISTING_STATE,
+ SkyFunctions.TARGET_PATTERN,
+ SkyFunctions.WORKSPACE_FILE);
+
+ @Override
+ protected void onNewPackageLocator(PathPackageLocator oldLocator, PathPackageLocator pkgLocator) {
+ invalidate(SkyFunctionName.functionIsIn(PACKAGE_LOCATOR_DEPENDENT_VALUES));
+ }
+
+ @Override
+ protected void invalidate(Predicate<SkyKey> pred) {
+ recordingDiffer.invalidate(Iterables.filter(memoizingEvaluator.getValues().keySet(), pred));
+ }
+
+ private void invalidateDeletedPackages(Iterable<String> deletedPackages) {
+ ArrayList<SkyKey> packagesToInvalidate = Lists.newArrayList();
+ for (String deletedPackage : deletedPackages) {
+ PathFragment pathFragment = new PathFragment(deletedPackage);
+ packagesToInvalidate.add(PackageLookupValue.key(pathFragment));
+ }
+ recordingDiffer.invalidate(packagesToInvalidate);
+ }
+
+ /**
+ * Sets the packages that should be treated as deleted and ignored.
+ */
+ @Override
+ @VisibleForTesting // productionVisibility = Visibility.PRIVATE
+ public void setDeletedPackages(Iterable<String> pkgs) {
+ // Invalidate the old deletedPackages as they may exist now.
+ invalidateDeletedPackages(deletedPackages.get());
+ deletedPackages.set(ImmutableSet.copyOf(pkgs));
+ // Invalidate the new deletedPackages as we need to pretend that they don't exist now.
+ invalidateDeletedPackages(deletedPackages.get());
+ }
+
+ /**
+ * Uses diff awareness on all the package paths to invalidate changed files.
+ */
+ @VisibleForTesting
+ public void handleDiffs() throws InterruptedException {
+ if (lastAnalysisDiscarded) {
+ // Values were cleared last build, but they couldn't be deleted because they were needed for
+ // the execution phase. We can delete them now.
+ dropConfiguredTargetsNow();
+ lastAnalysisDiscarded = false;
+ }
+ modifiedFiles = 0;
+ Map<Path, DiffAwarenessManager.ProcessableModifiedFileSet> modifiedFilesByPathEntry =
+ Maps.newHashMap();
+ Set<Pair<Path, DiffAwarenessManager.ProcessableModifiedFileSet>>
+ pathEntriesWithoutDiffInformation = Sets.newHashSet();
+ for (Path pathEntry : pkgLocator.get().getPathEntries()) {
+ DiffAwarenessManager.ProcessableModifiedFileSet modifiedFileSet =
+ diffAwarenessManager.getDiff(pathEntry);
+ if (modifiedFileSet.getModifiedFileSet().treatEverythingAsModified()) {
+ pathEntriesWithoutDiffInformation.add(Pair.of(pathEntry, modifiedFileSet));
+ } else {
+ modifiedFilesByPathEntry.put(pathEntry, modifiedFileSet);
+ }
+ }
+ handleDiffsWithCompleteDiffInformation(modifiedFilesByPathEntry);
+ handleDiffsWithMissingDiffInformation(pathEntriesWithoutDiffInformation);
+ }
+
+ /**
+ * Invalidates files under path entries whose corresponding {@link DiffAwareness} gave an exact
+ * diff. Removes entries from the given map as they are processed. All of the files need to be
+ * invalidated, so the map should be empty upon completion of this function.
+ */
+ private void handleDiffsWithCompleteDiffInformation(
+ Map<Path, DiffAwarenessManager.ProcessableModifiedFileSet> modifiedFilesByPathEntry) {
+ // It's important that the below code be uninterruptible, since we already promised to
+ // invalidate these files.
+ for (Path pathEntry : ImmutableSet.copyOf(modifiedFilesByPathEntry.keySet())) {
+ DiffAwarenessManager.ProcessableModifiedFileSet processableModifiedFileSet =
+ modifiedFilesByPathEntry.get(pathEntry);
+ ModifiedFileSet modifiedFileSet = processableModifiedFileSet.getModifiedFileSet();
+ Preconditions.checkState(!modifiedFileSet.treatEverythingAsModified(), pathEntry);
+ Iterable<SkyKey> dirtyValues = getSkyKeysPotentiallyAffected(
+ modifiedFileSet.modifiedSourceFiles(), pathEntry);
+ handleChangedFiles(new ImmutableDiff(dirtyValues, ImmutableMap.<SkyKey, SkyValue>of()));
+ processableModifiedFileSet.markProcessed();
+ }
+ }
+
+ /**
+ * Finds and invalidates changed files under path entries whose corresponding
+ * {@link DiffAwareness} said all files may have been modified.
+ */
+ private void handleDiffsWithMissingDiffInformation(
+ Set<Pair<Path, DiffAwarenessManager.ProcessableModifiedFileSet>>
+ pathEntriesWithoutDiffInformation) throws InterruptedException {
+ if (pathEntriesWithoutDiffInformation.isEmpty()) {
+ return;
+ }
+ // Before running the FilesystemValueChecker, ensure that all values marked for invalidation
+ // have actually been invalidated (recall that invalidation happens at the beginning of the
+ // next evaluate() call), because checking those is a waste of time.
+ buildDriver.evaluate(ImmutableList.<SkyKey>of(), false,
+ DEFAULT_THREAD_COUNT, reporter);
+ FilesystemValueChecker fsnc = new FilesystemValueChecker(memoizingEvaluator, tsgm);
+ // We need to manually check for changes to known files. This entails finding all dirty file
+ // system values under package roots for which we don't have diff information. If at least
+ // one path entry doesn't have diff information, then we're going to have to iterate over
+ // the skyframe values at least once no matter what so we might as well do so now and avoid
+ // doing so more than once.
+ Iterable<SkyKey> filesystemSkyKeys = fsnc.getFilesystemSkyKeys();
+ // Partition by package path entry.
+ Multimap<Path, SkyKey> skyKeysByPathEntry = partitionSkyKeysByPackagePathEntry(
+ ImmutableSet.copyOf(pkgLocator.get().getPathEntries()), filesystemSkyKeys);
+ // Contains all file system values that we need to check for dirtiness.
+ List<Iterable<SkyKey>> valuesToCheckManually = Lists.newArrayList();
+ for (Pair<Path, DiffAwarenessManager.ProcessableModifiedFileSet> pair :
+ pathEntriesWithoutDiffInformation) {
+ Path pathEntry = pair.getFirst();
+ valuesToCheckManually.add(skyKeysByPathEntry.get(pathEntry));
+ }
+ Differencer.Diff diff = fsnc.getDirtyFilesystemValues(Iterables.concat(valuesToCheckManually));
+ handleChangedFiles(diff);
+ for (Pair<Path, DiffAwarenessManager.ProcessableModifiedFileSet> pair :
+ pathEntriesWithoutDiffInformation) {
+ DiffAwarenessManager.ProcessableModifiedFileSet processableModifiedFileSet = pair.getSecond();
+ processableModifiedFileSet.markProcessed();
+ }
+ }
+
+ /**
+ * Partitions the given filesystem values based on which package path root they are under.
+ * Returns a {@link Multimap} {@code m} such that {@code m.containsEntry(k, pe)} is true for
+ * each filesystem valuekey {@code k} under a package path root {@code pe}. Note that values not
+ * under a package path root are not present in the returned {@link Multimap}; these values are
+ * unconditionally checked for changes on each incremental build.
+ */
+ private static Multimap<Path, SkyKey> partitionSkyKeysByPackagePathEntry(
+ Set<Path> pkgRoots, Iterable<SkyKey> filesystemSkyKeys) {
+ ImmutableSetMultimap.Builder<Path, SkyKey> multimapBuilder =
+ ImmutableSetMultimap.builder();
+ for (SkyKey key : filesystemSkyKeys) {
+ Preconditions.checkState(key.functionName() == SkyFunctions.FILE_STATE
+ || key.functionName() == SkyFunctions.DIRECTORY_LISTING_STATE, key);
+ Path root = ((RootedPath) key.argument()).getRoot();
+ if (pkgRoots.contains(root)) {
+ multimapBuilder.put(root, key);
+ }
+ // We don't need to worry about FileStateValues for external files because they have a
+ // dependency on the build_id and so they get invalidated each build.
+ }
+ return multimapBuilder.build();
+ }
+
+ private void handleChangedFiles(Differencer.Diff diff) {
+ recordingDiffer.invalidate(diff.changedKeysWithoutNewValues());
+ recordingDiffer.inject(diff.changedKeysWithNewValues());
+ modifiedFiles += getNumberOfModifiedFiles(diff.changedKeysWithoutNewValues());
+ modifiedFiles += getNumberOfModifiedFiles(diff.changedKeysWithNewValues().keySet());
+ incrementalBuildMonitor.accrue(diff.changedKeysWithoutNewValues());
+ incrementalBuildMonitor.accrue(diff.changedKeysWithNewValues().keySet());
+ }
+
+ private static int getNumberOfModifiedFiles(Iterable<SkyKey> modifiedValues) {
+ // We are searching only for changed files, DirectoryListingValues don't depend on
+ // child values, that's why they are invalidated separately
+ return Iterables.size(Iterables.filter(modifiedValues,
+ SkyFunctionName.functionIs(SkyFunctions.FILE_STATE)));
+ }
+
+ @Override
+ public void decideKeepIncrementalState(boolean batch, BuildView.Options viewOptions) {
+ Preconditions.checkState(!active);
+ if (viewOptions == null) {
+ // Some blaze commands don't include the view options. Don't bother with them.
+ return;
+ }
+ if (batch && viewOptions.keepGoing && viewOptions.discardAnalysisCache) {
+ Preconditions.checkState(keepGraphEdges, "May only be called once if successful");
+ keepGraphEdges = false;
+ // Graph will be recreated on next sync.
+ }
+ }
+
+ @Override
+ public boolean hasIncrementalState() {
+ // TODO(bazel-team): Combine this method with clearSkyframeRelevantCaches() once legacy
+ // execution is removed [skyframe-execution].
+ return keepGraphEdges;
+ }
+
+ @Override
+ public void invalidateFilesUnderPathForTesting(ModifiedFileSet modifiedFileSet, Path pathEntry)
+ throws InterruptedException {
+ if (lastAnalysisDiscarded) {
+ // Values were cleared last build, but they couldn't be deleted because they were needed for
+ // the execution phase. We can delete them now.
+ dropConfiguredTargetsNow();
+ lastAnalysisDiscarded = false;
+ }
+ Iterable<SkyKey> keys;
+ if (modifiedFileSet.treatEverythingAsModified()) {
+ Differencer.Diff diff =
+ new FilesystemValueChecker(memoizingEvaluator, tsgm).getDirtyFilesystemSkyKeys();
+ keys = diff.changedKeysWithoutNewValues();
+ recordingDiffer.inject(diff.changedKeysWithNewValues());
+ } else {
+ keys = getSkyKeysPotentiallyAffected(modifiedFileSet.modifiedSourceFiles(), pathEntry);
+ }
+ syscalls.set(new PerBuildSyscallCache());
+ recordingDiffer.invalidate(keys);
+ // Blaze invalidates transient errors on every build.
+ invalidateTransientErrors();
+ }
+
+ @Override
+ public void invalidateTransientErrors() {
+ checkActive();
+ recordingDiffer.invalidateTransientErrors();
+ }
+
+ @Override
+ protected void invalidateDirtyActions(Iterable<SkyKey> dirtyActionValues) {
+ recordingDiffer.invalidate(dirtyActionValues);
+ }
+
+ /**
+ * Save memory by removing references to configured targets and actions in Skyframe.
+ *
+ * <p>These values must be recreated on subsequent builds. We do not clear the top-level target
+ * values, since their configured targets are needed for the target completion middleman values.
+ *
+ * <p>The values are not deleted during this method call, because they are needed for the
+ * execution phase. Instead, their data is cleared. The next build will delete the values (and
+ * recreate them if necessary).
+ */
+ private void discardAnalysisCache(Collection<ConfiguredTarget> topLevelTargets) {
+ lastAnalysisDiscarded = true;
+ for (Map.Entry<SkyKey, SkyValue> entry : memoizingEvaluator.getValues().entrySet()) {
+ if (!entry.getKey().functionName().equals(SkyFunctions.CONFIGURED_TARGET)) {
+ continue;
+ }
+ ConfiguredTargetValue ctValue = (ConfiguredTargetValue) entry.getValue();
+ // ctValue may be null if target was not successfully analyzed.
+ if (ctValue != null && !topLevelTargets.contains(ctValue.getConfiguredTarget())) {
+ ctValue.clear();
+ }
+ }
+ }
+
+ @Override
+ public void clearAnalysisCache(Collection<ConfiguredTarget> topLevelTargets) {
+ discardAnalysisCache(topLevelTargets);
+ }
+
+ @Override
+ public void dropConfiguredTargets() {
+ if (skyframeBuildView != null) {
+ skyframeBuildView.clearInvalidatedConfiguredTargets();
+ }
+ memoizingEvaluator.delete(
+ // We delete any value that can hold an action -- all subclasses of ActionLookupValue -- as
+ // well as ActionExecutionValues, since they do not depend on ActionLookupValues.
+ SkyFunctionName.functionIsIn(ImmutableSet.of(
+ SkyFunctions.CONFIGURED_TARGET,
+ SkyFunctions.ACTION_LOOKUP,
+ SkyFunctions.BUILD_INFO,
+ SkyFunctions.TARGET_COMPLETION,
+ SkyFunctions.BUILD_INFO_COLLECTION,
+ SkyFunctions.ACTION_EXECUTION))
+ );
+ }
+
+ /**
+ * Deletes all ConfiguredTarget values from the Skyframe cache.
+ *
+ * <p>After the execution of this method all invalidated and marked for deletion values
+ * (and the values depending on them) will be deleted from the cache.
+ *
+ * <p>WARNING: Note that a call to this method leaves legacy data inconsistent with Skyframe.
+ * The next build should clear the legacy caches.
+ */
+ private void dropConfiguredTargetsNow() {
+ dropConfiguredTargets();
+ // Run the invalidator to actually delete the values.
+ try {
+ progressReceiver.ignoreInvalidations = true;
+ callUninterruptibly(new Callable<Void>() {
+ @Override
+ public Void call() throws InterruptedException {
+ buildDriver.evaluate(ImmutableList.<SkyKey>of(), false,
+ ResourceUsage.getAvailableProcessors(), reporter);
+ return null;
+ }
+ });
+ } catch (Exception e) {
+ throw new IllegalStateException(e);
+ } finally {
+ progressReceiver.ignoreInvalidations = false;
+ }
+ }
+
+ /**
+ * Returns true if the old set of Packages is a subset or superset of the new one.
+ *
+ * <p>Compares the names of packages instead of the Package objects themselves (Package doesn't
+ * yet override #equals). Since packages store their names as a String rather than a Label, it's
+ * easier to use strings here.
+ */
+ @VisibleForTesting
+ static boolean isBuildSubsetOrSupersetOfPreviousBuild(Set<PackageIdentifier> oldPackages,
+ Set<PackageIdentifier> newPackages) {
+ if (newPackages.size() <= oldPackages.size()) {
+ return Sets.difference(newPackages, oldPackages).isEmpty();
+ } else if (oldPackages.size() < newPackages.size()) {
+ // No need to check for <= here, since the first branch does that already.
+ // If size(A) = size(B), then then A\B = 0 iff B\A = 0
+ return Sets.difference(oldPackages, newPackages).isEmpty();
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public void updateLoadedPackageSet(Set<PackageIdentifier> loadedPackages) {
+ Preconditions.checkState(valueCacheEvictionLimit >= 0,
+ "should have called setMinLoadedPkgCountForCtValueEviction earlier");
+
+ // Make a copy to avoid nesting SetView objects. It also computes size(), which we need below.
+ Set<PackageIdentifier> union = ImmutableSet.copyOf(
+ Sets.union(allLoadedPackages, loadedPackages));
+
+ if (union.size() < valueCacheEvictionLimit
+ || isBuildSubsetOrSupersetOfPreviousBuild(allLoadedPackages, loadedPackages)) {
+ allLoadedPackages = union;
+ } else {
+ dropConfiguredTargets();
+ allLoadedPackages = loadedPackages;
+ }
+ }
+
+ @Override
+ public void deleteOldNodes(long versionWindowForDirtyGc) {
+ // TODO(bazel-team): perhaps we should come up with a separate GC class dedicated to maintaining
+ // value garbage. If we ever do so, this logic should be moved there.
+ memoizingEvaluator.deleteDirty(versionWindowForDirtyGc);
+ }
+
+ @Override
+ public void dumpPackages(PrintStream out) {
+ Iterable<SkyKey> packageSkyKeys = Iterables.filter(memoizingEvaluator.getValues().keySet(),
+ SkyFunctions.isSkyFunction(SkyFunctions.PACKAGE));
+ out.println(Iterables.size(packageSkyKeys) + " packages");
+ for (SkyKey packageSkyKey : packageSkyKeys) {
+ Package pkg = ((PackageValue) memoizingEvaluator.getValues().get(packageSkyKey)).getPackage();
+ pkg.dump(out);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SequencedSkyframeExecutorFactory.java b/src/main/java/com/google/devtools/build/lib/skyframe/SequencedSkyframeExecutorFactory.java
new file mode 100644
index 0000000..7e277a7
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SequencedSkyframeExecutorFactory.java
@@ -0,0 +1,53 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.analysis.BlazeDirectories;
+import com.google.devtools.build.lib.analysis.WorkspaceStatusAction.Factory;
+import com.google.devtools.build.lib.analysis.buildinfo.BuildInfoFactory;
+import com.google.devtools.build.lib.events.Reporter;
+import com.google.devtools.build.lib.packages.PackageFactory;
+import com.google.devtools.build.lib.packages.Preprocessor;
+import com.google.devtools.build.lib.util.io.TimestampGranularityMonitor;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyFunctionName;
+
+import java.util.Set;
+
+/**
+ * A factory of SkyframeExecutors that returns SequencedSkyframeExecutor.
+ */
+public class SequencedSkyframeExecutorFactory implements SkyframeExecutorFactory {
+
+ @Override
+ public SkyframeExecutor create(Reporter reporter, PackageFactory pkgFactory,
+ TimestampGranularityMonitor tsgm, BlazeDirectories directories,
+ Factory workspaceStatusActionFactory, ImmutableList<BuildInfoFactory> buildInfoFactories,
+ Set<Path> immutableDirectories,
+ Iterable<? extends DiffAwareness.Factory> diffAwarenessFactories,
+ Predicate<PathFragment> allowedMissingInputs,
+ Preprocessor.Factory.Supplier preprocessorFactorySupplier,
+ ImmutableMap<SkyFunctionName, SkyFunction> extraSkyFunctions,
+ ImmutableList<PrecomputedValue.Injected> extraPrecomputedValues) {
+ return SequencedSkyframeExecutor.create(reporter, pkgFactory, tsgm, directories,
+ workspaceStatusActionFactory, buildInfoFactories, immutableDirectories,
+ diffAwarenessFactories, allowedMissingInputs, preprocessorFactorySupplier,
+ extraSkyFunctions, extraPrecomputedValues);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkyFunctions.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkyFunctions.java
new file mode 100644
index 0000000..316d27d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkyFunctions.java
@@ -0,0 +1,81 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Predicate;
+import com.google.devtools.build.skyframe.SkyFunctionName;
+import com.google.devtools.build.skyframe.SkyKey;
+
+/**
+ * Value types in Skyframe.
+ */
+public final class SkyFunctions {
+ public static final SkyFunctionName PRECOMPUTED = new SkyFunctionName("PRECOMPUTED", false);
+ public static final SkyFunctionName FILE_STATE = new SkyFunctionName("FILE_STATE", false);
+ public static final SkyFunctionName DIRECTORY_LISTING_STATE =
+ new SkyFunctionName("DIRECTORY_LISTING_STATE", false);
+ public static final SkyFunctionName FILE_SYMLINK_CYCLE_UNIQUENESS =
+ SkyFunctionName.computed("FILE_SYMLINK_CYCLE_UNIQUENESS_NODE");
+ public static final SkyFunctionName FILE = SkyFunctionName.computed("FILE");
+ public static final SkyFunctionName DIRECTORY_LISTING =
+ SkyFunctionName.computed("DIRECTORY_LISTING");
+ public static final SkyFunctionName PACKAGE_LOOKUP = SkyFunctionName.computed("PACKAGE_LOOKUP");
+ public static final SkyFunctionName CONTAINING_PACKAGE_LOOKUP =
+ SkyFunctionName.computed("CONTAINING_PACKAGE_LOOKUP");
+ public static final SkyFunctionName AST_FILE_LOOKUP = SkyFunctionName.computed("AST_FILE_LOOKUP");
+ public static final SkyFunctionName SKYLARK_IMPORTS_LOOKUP =
+ SkyFunctionName.computed("SKYLARK_IMPORTS_LOOKUP");
+ public static final SkyFunctionName GLOB = SkyFunctionName.computed("GLOB");
+ public static final SkyFunctionName PACKAGE = SkyFunctionName.computed("PACKAGE");
+ public static final SkyFunctionName TARGET_MARKER = SkyFunctionName.computed("TARGET_MARKER");
+ public static final SkyFunctionName TARGET_PATTERN = SkyFunctionName.computed("TARGET_PATTERN");
+ public static final SkyFunctionName RECURSIVE_PKG = SkyFunctionName.computed("RECURSIVE_PKG");
+ public static final SkyFunctionName TRANSITIVE_TARGET =
+ SkyFunctionName.computed("TRANSITIVE_TARGET");
+ public static final SkyFunctionName CONFIGURED_TARGET =
+ SkyFunctionName.computed("CONFIGURED_TARGET");
+ public static final SkyFunctionName ASPECT = SkyFunctionName.computed("ASPECT");
+ public static final SkyFunctionName POST_CONFIGURED_TARGET =
+ SkyFunctionName.computed("POST_CONFIGURED_TARGET");
+ public static final SkyFunctionName TARGET_COMPLETION =
+ SkyFunctionName.computed("TARGET_COMPLETION");
+ public static final SkyFunctionName TEST_COMPLETION =
+ SkyFunctionName.computed("TEST_COMPLETION");
+ public static final SkyFunctionName CONFIGURATION_FRAGMENT =
+ SkyFunctionName.computed("CONFIGURATION_FRAGMENT");
+ public static final SkyFunctionName CONFIGURATION_COLLECTION =
+ SkyFunctionName.computed("CONFIGURATION_COLLECTION");
+ public static final SkyFunctionName ARTIFACT = SkyFunctionName.computed("ARTIFACT");
+ public static final SkyFunctionName ACTION_EXECUTION =
+ SkyFunctionName.computed("ACTION_EXECUTION");
+ public static final SkyFunctionName ACTION_LOOKUP = SkyFunctionName.computed("ACTION_LOOKUP");
+ public static final SkyFunctionName RECURSIVE_FILESYSTEM_TRAVERSAL =
+ SkyFunctionName.computed("RECURSIVE_DIRECTORY_TRAVERSAL");
+ public static final SkyFunctionName FILESET_ENTRY = SkyFunctionName.computed("FILESET_ENTRY");
+ public static final SkyFunctionName BUILD_INFO_COLLECTION =
+ SkyFunctionName.computed("BUILD_INFO_COLLECTION");
+ public static final SkyFunctionName BUILD_INFO = SkyFunctionName.computed("BUILD_INFO");
+ public static final SkyFunctionName WORKSPACE_FILE = SkyFunctionName.computed("WORKSPACE_FILE");
+ public static final SkyFunctionName COVERAGE_REPORT = SkyFunctionName.computed("COVERAGE_REPORT");
+ public static final SkyFunctionName REPOSITORY = SkyFunctionName.computed("REPOSITORY");
+
+ public static Predicate<SkyKey> isSkyFunction(final SkyFunctionName functionName) {
+ return new Predicate<SkyKey>() {
+ @Override
+ public boolean apply(SkyKey key) {
+ return key.functionName() == functionName;
+ }
+ };
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeActionExecutor.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeActionExecutor.java
new file mode 100644
index 0000000..bd591e1
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeActionExecutor.java
@@ -0,0 +1,1152 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import static com.google.devtools.build.lib.vfs.FileSystemUtils.createDirectoryAndParents;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import com.google.common.eventbus.EventBus;
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.actions.ActionCacheChecker;
+import com.google.devtools.build.lib.actions.ActionCacheChecker.Token;
+import com.google.devtools.build.lib.actions.ActionCompletionEvent;
+import com.google.devtools.build.lib.actions.ActionExecutedEvent;
+import com.google.devtools.build.lib.actions.ActionExecutionContext;
+import com.google.devtools.build.lib.actions.ActionExecutionException;
+import com.google.devtools.build.lib.actions.ActionExecutionStatusReporter;
+import com.google.devtools.build.lib.actions.ActionGraph;
+import com.google.devtools.build.lib.actions.ActionInput;
+import com.google.devtools.build.lib.actions.ActionInputFileCache;
+import com.google.devtools.build.lib.actions.ActionLogBufferPathGenerator;
+import com.google.devtools.build.lib.actions.ActionMiddlemanEvent;
+import com.google.devtools.build.lib.actions.ActionStartedEvent;
+import com.google.devtools.build.lib.actions.Actions;
+import com.google.devtools.build.lib.actions.AlreadyReportedActionExecutionException;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.Artifact.MiddlemanExpander;
+import com.google.devtools.build.lib.actions.ArtifactPrefixConflictException;
+import com.google.devtools.build.lib.actions.CachedActionEvent;
+import com.google.devtools.build.lib.actions.Executor;
+import com.google.devtools.build.lib.actions.MapBasedActionGraph;
+import com.google.devtools.build.lib.actions.MutableActionGraph;
+import com.google.devtools.build.lib.actions.MutableActionGraph.ActionConflictException;
+import com.google.devtools.build.lib.actions.NotifyOnActionCacheHit;
+import com.google.devtools.build.lib.actions.ResourceManager;
+import com.google.devtools.build.lib.actions.ResourceSet;
+import com.google.devtools.build.lib.actions.TargetOutOfDateException;
+import com.google.devtools.build.lib.actions.cache.Digest;
+import com.google.devtools.build.lib.actions.cache.DigestUtils;
+import com.google.devtools.build.lib.actions.cache.Metadata;
+import com.google.devtools.build.lib.actions.cache.MetadataHandler;
+import com.google.devtools.build.lib.concurrent.ExecutorShutdownUtil;
+import com.google.devtools.build.lib.concurrent.Sharder;
+import com.google.devtools.build.lib.concurrent.ThrowableRecordingRunnableWrapper;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.Reporter;
+import com.google.devtools.build.lib.profiler.Profiler;
+import com.google.devtools.build.lib.profiler.ProfilerTask;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.util.Pair;
+import com.google.devtools.build.lib.util.io.FileOutErr;
+import com.google.devtools.build.lib.util.io.OutErr;
+import com.google.devtools.build.lib.vfs.FileStatus;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.lib.vfs.Symlinks;
+import com.google.protobuf.ByteString;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedMap;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.ConcurrentNavigableMap;
+import java.util.concurrent.ConcurrentSkipListMap;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.atomic.AtomicReference;
+
+import javax.annotation.Nullable;
+
+/**
+ * Action executor: takes care of preparing an action for execution, executing it, validating that
+ * all output artifacts were created, error reporting, etc.
+ */
+public final class SkyframeActionExecutor {
+ private final Reporter reporter;
+ private final AtomicReference<EventBus> eventBus;
+ private final ResourceManager resourceManager;
+ private Executor executorEngine;
+ private ActionLogBufferPathGenerator actionLogBufferPathGenerator;
+ private ActionCacheChecker actionCacheChecker;
+ private ConcurrentMap<Artifact, Metadata> undeclaredInputsMetadata = new ConcurrentHashMap<>();
+ private final Profiler profiler = Profiler.instance();
+ private boolean explain;
+
+ // We keep track of actions already executed this build in order to avoid executing a shared
+ // action twice. Note that we may still unnecessarily re-execute the action on a subsequent
+ // build: say actions A and B are shared. If A is requested on the first build and then B is
+ // requested on the second build, we will execute B even though its output files are up to date.
+ // However, we will not re-execute A on a subsequent build.
+ // We do not allow the shared action to re-execute in the same build, even after the first
+ // action has finished execution, because a downstream action might be reading the output file
+ // at the same time as the shared action was writing to it.
+ // This map is also used for Actions that try to execute twice because they have discovered
+ // headers -- the SkyFunction tries to declare a dep on the missing headers and has to restart.
+ // We don't want to execute the action again on the second entry to the SkyFunction.
+ // In both cases, we store the already-computed ActionExecutionValue to avoid having to compute it
+ // again.
+ private ConcurrentMap<Artifact, Pair<Action, FutureTask<ActionExecutionValue>>> buildActionMap;
+
+ // Errors found when examining all actions in the graph are stored here, so that they can be
+ // thrown when execution of the action is requested. This field is set during each call to
+ // findAndStoreArtifactConflicts, and is preserved across builds otherwise.
+ private ImmutableMap<Action, ConflictException> badActionMap = ImmutableMap.of();
+ private boolean keepGoing;
+ private boolean hadExecutionError;
+ private ActionInputFileCache perBuildFileCache;
+ private ProgressSupplier progressSupplier;
+ private ActionCompletedReceiver completionReceiver;
+ private final AtomicReference<ActionExecutionStatusReporter> statusReporterRef;
+
+ SkyframeActionExecutor(Reporter reporter, ResourceManager resourceManager,
+ AtomicReference<EventBus> eventBus,
+ AtomicReference<ActionExecutionStatusReporter> statusReporterRef) {
+ this.reporter = reporter;
+ this.resourceManager = resourceManager;
+ this.eventBus = eventBus;
+ this.statusReporterRef = statusReporterRef;
+ }
+
+ /**
+ * A typed union of {@link ActionConflictException}, which indicates two actions that generate
+ * the same {@link Artifact}, and {@link ArtifactPrefixConflictException}, which indicates that
+ * the path of one {@link Artifact} is a prefix of another.
+ */
+ public static class ConflictException extends Exception {
+ @Nullable private final ActionConflictException ace;
+ @Nullable private final ArtifactPrefixConflictException apce;
+
+ public ConflictException(ActionConflictException e) {
+ super(e);
+ this.ace = e;
+ this.apce = null;
+ }
+
+ public ConflictException(ArtifactPrefixConflictException e) {
+ super(e);
+ this.ace = null;
+ this.apce = e;
+ }
+
+ void rethrowTyped() throws ActionConflictException, ArtifactPrefixConflictException {
+ if (ace == null) {
+ throw Preconditions.checkNotNull(apce);
+ }
+ if (apce == null) {
+ throw Preconditions.checkNotNull(ace);
+ }
+ throw new IllegalStateException();
+ }
+ }
+
+ /**
+ * Return the map of mostly recently executed bad actions to their corresponding exception.
+ * See {#findAndStoreArtifactConflicts()}.
+ */
+ public ImmutableMap<Action, ConflictException> badActions() {
+ // TODO(bazel-team): Move badActions() and findAndStoreArtifactConflicts() to SkyframeBuildView
+ // now that it's done in the analysis phase.
+ return badActionMap;
+ }
+
+ /**
+ * Basic implementation of {@link MetadataHandler} that delegates to Skyframe for metadata and
+ * caches missing source artifacts (which must be undeclared inputs: discovered headers) to avoid
+ * excessive filesystem access. The discovered-header cache is available across actions.
+ */
+ // TODO(bazel-team): remove when include scanning is skyframe-native.
+ private static class UndeclaredInputHandler implements MetadataHandler {
+ private final ConcurrentMap<Artifact, Metadata> undeclaredInputsMetadata;
+ private final MetadataHandler perActionHandler;
+
+ UndeclaredInputHandler(MetadataHandler perActionHandler,
+ ConcurrentMap<Artifact, Metadata> undeclaredInputsMetadata) {
+ // Shared across all UndeclaredInputHandlers in this build.
+ this.undeclaredInputsMetadata = undeclaredInputsMetadata;
+ this.perActionHandler = perActionHandler;
+ }
+
+ @Override
+ public Metadata getMetadataMaybe(Artifact artifact) {
+ try {
+ return getMetadata(artifact);
+ } catch (IOException e) {
+ return null;
+ }
+ }
+
+ @Override
+ public Metadata getMetadata(Artifact artifact) throws IOException {
+ Metadata metadata = perActionHandler.getMetadata(artifact);
+ if (metadata != null) {
+ return metadata;
+ }
+ // Skyframe stats all generated artifacts, because either they are outputs of the action being
+ // executed or they are generated files already present in the graph.
+ Preconditions.checkState(artifact.isSourceArtifact(), artifact);
+ metadata = undeclaredInputsMetadata.get(artifact);
+ if (metadata != null) {
+ return metadata;
+ }
+ FileStatus stat = artifact.getPath().stat();
+ if (DigestUtils.useFileDigest(artifact, stat.isFile(), stat.getSize())) {
+ metadata = new Metadata(Preconditions.checkNotNull(
+ DigestUtils.getDigestOrFail(artifact.getPath(), stat.getSize()), artifact));
+ } else {
+ metadata = new Metadata(stat.getLastModifiedTime());
+ }
+ // Cache for other actions that may also include without declaring.
+ Metadata oldMetadata = undeclaredInputsMetadata.put(artifact, metadata);
+ FileAndMetadataCache.checkInconsistentData(artifact, oldMetadata, metadata);
+ return metadata;
+ }
+
+ @Override
+ public void setDigestForVirtualArtifact(Artifact artifact, Digest digest) {
+ perActionHandler.setDigestForVirtualArtifact(artifact, digest);
+ }
+
+ @Override
+ public void injectDigest(ActionInput output, FileStatus statNoFollow, byte[] digest) {
+ perActionHandler.injectDigest(output, statNoFollow, digest);
+ }
+
+ @Override
+ public boolean artifactExists(Artifact artifact) {
+ return perActionHandler.artifactExists(artifact);
+ }
+
+ @Override
+ public boolean isRegularFile(Artifact artifact) {
+ return perActionHandler.isRegularFile(artifact);
+ }
+
+ @Override
+ public boolean isInjected(Artifact artifact) throws IOException {
+ return perActionHandler.isInjected(artifact);
+ }
+
+ @Override
+ public void discardMetadata(Collection<Artifact> artifactList) {
+ // This input handler only caches undeclared inputs, which never need to be discarded
+ // intra-build.
+ perActionHandler.discardMetadata(artifactList);
+ }
+ }
+
+ /**
+ * Find conflicts between generated artifacts. There are two ways to have conflicts. First, if
+ * two (unshareable) actions generate the same output artifact, this will result in an {@link
+ * ActionConflictException}. Second, if one action generates an artifact whose path is a prefix of
+ * another artifact's path, those two artifacts cannot exist simultaneously in the output tree.
+ * This causes an {@link ArtifactPrefixConflictException}. The relevant exceptions are stored in
+ * the executor in {@code badActionMap}, and will be thrown immediately when that action is
+ * executed. Those exceptions persist, so that even if the action is not executed this build, the
+ * first time it is executed, the correct exception will be thrown.
+ *
+ * <p>This method must be called if a new action was added to the graph this build, so
+ * whenever a new configured target was analyzed this build. It is somewhat expensive (~1s
+ * range for a medium build as of 2014), so it should only be called when necessary.
+ *
+ * <p>Conflicts found may not be requested this build, and so we may overzealously throw an error.
+ * For instance, if actions A and B generate the same artifact foo, and the user first requests
+ * A' depending on A, and then in a subsequent build B' depending on B, we will fail the second
+ * build, even though it would have succeeded if it had been the only build. However, since
+ * Skyframe does not know the transitive dependencies of the request, we err on the conservative
+ * side.
+ *
+ * <p>If the user first runs one action on the first build, and on the second build adds a
+ * conflicting action, only the second action's error may be reported (because the first action
+ * will be cached), whereas if both actions were requested for the first time, both errors would
+ * be reported. However, the first time an action is added to the build, we are guaranteed to find
+ * any conflicts it has, since this method will compare it against all other actions. So there is
+ * no sequence of builds that can evade the error.
+ */
+ void findAndStoreArtifactConflicts(Iterable<ActionLookupValue> actionLookupValues)
+ throws InterruptedException {
+ ConcurrentMap<Action, ConflictException> temporaryBadActionMap = new ConcurrentHashMap<>();
+ Pair<ActionGraph, SortedMap<PathFragment, Artifact>> result;
+ result = constructActionGraphAndPathMap(actionLookupValues, temporaryBadActionMap);
+ ActionGraph actionGraph = result.first;
+ SortedMap<PathFragment, Artifact> artifactPathMap = result.second;
+
+ // Report an error for every derived artifact which is a prefix of another.
+ // If x << y << z (where x << y means "y starts with x"), then we only report (x,y), (x,z), but
+ // not (y,z).
+ Iterator<PathFragment> iter = artifactPathMap.keySet().iterator();
+ if (!iter.hasNext()) {
+ // No actions in graph -- currently happens only in tests. Special-cased because .next() call
+ // below is unconditional.
+ this.badActionMap = ImmutableMap.of();
+ return;
+ }
+ for (PathFragment pathJ = iter.next(); iter.hasNext(); ) {
+ // For each comparison, we have a prefix candidate (pathI) and a suffix candidate (pathJ).
+ // At the beginning of the loop, we set pathI to the last suffix candidate, since it has not
+ // yet been tested as a prefix candidate, and then set pathJ to the paths coming after pathI,
+ // until we come to one that does not contain pathI as a prefix. pathI is then verified not to
+ // be the prefix of any path, so we start the next run of the loop.
+ PathFragment pathI = pathJ;
+ // Compare pathI to the paths coming after it.
+ while (iter.hasNext()) {
+ pathJ = iter.next();
+ if (pathJ.startsWith(pathI)) { // prefix conflict.
+ Artifact artifactI = Preconditions.checkNotNull(artifactPathMap.get(pathI), pathI);
+ Artifact artifactJ = Preconditions.checkNotNull(artifactPathMap.get(pathJ), pathJ);
+ Action actionI =
+ Preconditions.checkNotNull(actionGraph.getGeneratingAction(artifactI), artifactI);
+ Action actionJ =
+ Preconditions.checkNotNull(actionGraph.getGeneratingAction(artifactJ), artifactJ);
+ if (actionI.shouldReportPathPrefixConflict(actionJ)) {
+ ArtifactPrefixConflictException exception = new ArtifactPrefixConflictException(pathI,
+ pathJ, actionI.getOwner().getLabel(), actionJ.getOwner().getLabel());
+ temporaryBadActionMap.put(actionI, new ConflictException(exception));
+ temporaryBadActionMap.put(actionJ, new ConflictException(exception));
+ }
+ } else { // pathJ didn't have prefix pathI, so no conflict possible for pathI.
+ break;
+ }
+ }
+ }
+ this.badActionMap = ImmutableMap.copyOf(temporaryBadActionMap);
+ }
+
+ /**
+ * Simultaneously construct an action graph for all the actions in Skyframe and a map from
+ * {@link PathFragment}s to their respective {@link Artifact}s. We do this in a threadpool to save
+ * around 1.5 seconds on a mid-sized build versus a single-threaded operation.
+ */
+ private static Pair<ActionGraph, SortedMap<PathFragment, Artifact>>
+ constructActionGraphAndPathMap(
+ Iterable<ActionLookupValue> values,
+ ConcurrentMap<Action, ConflictException> badActionMap) throws InterruptedException {
+ MutableActionGraph actionGraph = new MapBasedActionGraph();
+ ConcurrentNavigableMap<PathFragment, Artifact> artifactPathMap = new ConcurrentSkipListMap<>();
+ // Action graph construction is CPU-bound.
+ int numJobs = Runtime.getRuntime().availableProcessors();
+ // No great reason for expecting 5000 action lookup values, but not worth counting size of
+ // values.
+ Sharder<ActionLookupValue> actionShards = new Sharder<>(numJobs, 5000);
+ for (ActionLookupValue value : values) {
+ actionShards.add(value);
+ }
+
+ ThrowableRecordingRunnableWrapper wrapper = new ThrowableRecordingRunnableWrapper(
+ "SkyframeActionExecutor#constructActionGraphAndPathMap");
+
+ ExecutorService executor = Executors.newFixedThreadPool(
+ numJobs,
+ new ThreadFactoryBuilder().setNameFormat("ActionLookupValue Processor %d").build());
+ for (List<ActionLookupValue> shard : actionShards) {
+ executor.execute(
+ wrapper.wrap(actionRegistration(shard, actionGraph, artifactPathMap, badActionMap)));
+ }
+ boolean interrupted = ExecutorShutdownUtil.interruptibleShutdown(executor);
+ Throwables.propagateIfPossible(wrapper.getFirstThrownError());
+ if (interrupted) {
+ throw new InterruptedException();
+ }
+ return Pair.<ActionGraph, SortedMap<PathFragment, Artifact>>of(actionGraph, artifactPathMap);
+ }
+
+ private static Runnable actionRegistration(
+ final List<ActionLookupValue> values,
+ final MutableActionGraph actionGraph,
+ final ConcurrentMap<PathFragment, Artifact> artifactPathMap,
+ final ConcurrentMap<Action, ConflictException> badActionMap) {
+ return new Runnable() {
+ @Override
+ public void run() {
+ for (ActionLookupValue value : values) {
+ Set<Action> registeredActions = new HashSet<>();
+ for (Map.Entry<Artifact, Action> entry : value.getMapForConsistencyCheck().entrySet()) {
+ Action action = entry.getValue();
+ // We have an entry for each <action, artifact> pair. Only try to register each action
+ // once.
+ if (registeredActions.add(action)) {
+ try {
+ actionGraph.registerAction(action);
+ } catch (ActionConflictException e) {
+ Exception oldException = badActionMap.put(action, new ConflictException(e));
+ Preconditions.checkState(oldException == null,
+ "%s | %s | %s", action, e, oldException);
+ // We skip the rest of the loop, and do not add the path->artifact mapping for this
+ // artifact below -- we don't need to check it since this action is already in
+ // error.
+ continue;
+ }
+ }
+ artifactPathMap.put(entry.getKey().getExecPath(), entry.getKey());
+ }
+ }
+ }
+ };
+ }
+
+ void prepareForExecution(Executor executor, boolean keepGoing,
+ boolean explain, ActionCacheChecker actionCacheChecker) {
+ this.executorEngine = Preconditions.checkNotNull(executor);
+
+ // Start with a new map each build so there's no issue with internal resizing.
+ this.buildActionMap = Maps.newConcurrentMap();
+ this.keepGoing = keepGoing;
+ this.hadExecutionError = false;
+ this.actionCacheChecker = Preconditions.checkNotNull(actionCacheChecker);
+ // Don't cache possibly stale data from the last build.
+ undeclaredInputsMetadata = new ConcurrentHashMap<>();
+ this.explain = explain;
+ }
+
+ public void setActionLogBufferPathGenerator(
+ ActionLogBufferPathGenerator actionLogBufferPathGenerator) {
+ this.actionLogBufferPathGenerator = actionLogBufferPathGenerator;
+ }
+
+ void executionOver() {
+ // This transitively holds a bunch of heavy objects, so it's important to clear it at the
+ // end of a build.
+ this.executorEngine = null;
+ }
+
+ File getExecRoot() {
+ return executorEngine.getExecRoot().getPathFile();
+ }
+
+ boolean probeActionExecution(Action action) {
+ return buildActionMap.containsKey(action.getPrimaryOutput());
+ }
+
+ /**
+ * Executes the provided action on the current thread. Returns the ActionExecutionValue with the
+ * result, either computed here or already computed on another thread.
+ *
+ * <p>For use from {@link ArtifactFunction} only.
+ */
+ ActionExecutionValue executeAction(Action action, FileAndMetadataCache graphFileCache,
+ Token token, long actionStartTime,
+ ActionExecutionContext actionExecutionContext)
+ throws ActionExecutionException, InterruptedException {
+ Exception exception = badActionMap.get(action);
+ if (exception != null) {
+ // If action had a conflict with some other action in the graph, report it now.
+ reportError(exception.getMessage(), exception, action, null);
+ }
+ Artifact primaryOutput = action.getPrimaryOutput();
+ FutureTask<ActionExecutionValue> actionTask =
+ new FutureTask<>(new ActionRunner(action, graphFileCache, token,
+ actionStartTime, actionExecutionContext));
+ // Check to see if another action is already executing/has executed this value.
+ Pair<Action, FutureTask<ActionExecutionValue>> oldAction =
+ buildActionMap.putIfAbsent(primaryOutput, Pair.of(action, actionTask));
+
+ if (oldAction == null) {
+ actionTask.run();
+ } else if (action == oldAction.first) {
+ // We only allow the same action to be executed twice if it discovers inputs. We allow that
+ // because we need to declare additional dependencies on those new inputs.
+ Preconditions.checkState(action.discoversInputs(),
+ "Same action shouldn't execute twice in build: %s", action);
+ actionTask = oldAction.second;
+ } else {
+ Preconditions.checkState(Actions.canBeShared(oldAction.first, action),
+ "Actions cannot be shared: %s %s", oldAction.first, action);
+ // Wait for other action to finish, so any actions that depend on its outputs can execute.
+ actionTask = oldAction.second;
+ }
+ try {
+ return actionTask.get();
+ } catch (ExecutionException e) {
+ Throwables.propagateIfPossible(e.getCause(),
+ ActionExecutionException.class, InterruptedException.class);
+ throw new IllegalStateException(e);
+ } finally {
+ String message = action.getProgressMessage();
+ if (message != null) {
+ // Tell the receiver that the action has completed *before* telling the reporter.
+ // This way the latter will correctly show the number of completed actions when task
+ // completion messages are enabled (--show_task_finish).
+ if (completionReceiver != null) {
+ completionReceiver.actionCompleted(action);
+ }
+ reporter.finishTask(null, prependExecPhaseStats(message));
+ }
+ }
+ }
+
+ /**
+ * Returns an ActionExecutionContext suitable for executing a particular action. The caller should
+ * pass the returned context to {@link #executeAction}, and any other method that needs to execute
+ * tasks related to that action.
+ */
+ ActionExecutionContext constructActionExecutionContext(final FileAndMetadataCache graphFileCache,
+ MetadataHandler metadataHandler) {
+ FileOutErr fileOutErr = actionLogBufferPathGenerator.generate();
+ return new ActionExecutionContext(
+ executorEngine,
+ new DelegatingPairFileCache(graphFileCache, perBuildFileCache),
+ metadataHandler,
+ fileOutErr,
+ new MiddlemanExpander() {
+ @Override
+ public void expand(Artifact middlemanArtifact,
+ Collection<? super Artifact> output) {
+ // Legacy code is more permissive regarding "mm" in that it expands any middleman,
+ // not just inputs of this action. Skyframe doesn't have access to a global action
+ // graph, therefore this implementation can't expand any middleman, only the
+ // inputs of this action.
+ // This is fine though: actions should only hold references to their input
+ // artifacts, otherwise hermeticity would be violated.
+ output.addAll(graphFileCache.expandInputMiddleman(middlemanArtifact));
+ }
+ });
+ }
+
+ /**
+ * Returns a MetadataHandler for use when executing a particular action. The caller can pass the
+ * returned handler in whenever a MetadataHandler is needed in the course of executing the action.
+ */
+ MetadataHandler constructMetadataHandler(MetadataHandler graphFileCache) {
+ return new UndeclaredInputHandler(graphFileCache, undeclaredInputsMetadata);
+ }
+
+ /**
+ * Checks the action cache to see if {@code action} needs to be executed, or is up to date.
+ * Returns a token with the semantics of {@link ActionCacheChecker#getTokenIfNeedToExecute}: null
+ * if the action is up to date, and non-null if it needs to be executed, in which case that token
+ * should be provided to the ActionCacheChecker after execution.
+ */
+ Token checkActionCache(Action action, MetadataHandler metadataHandler, long actionStartTime) {
+ profiler.startTask(ProfilerTask.ACTION_CHECK, action);
+ Token token = actionCacheChecker.getTokenIfNeedToExecute(
+ action, explain ? reporter : null, metadataHandler);
+ profiler.completeTask(ProfilerTask.ACTION_CHECK);
+ if (token == null) {
+ boolean eventPosted = false;
+ // Notify BlazeRuntimeStatistics about the action middleman 'execution'.
+ if (action.getActionType().isMiddleman()) {
+ postEvent(new ActionMiddlemanEvent(action, actionStartTime));
+ eventPosted = true;
+ }
+
+ if (action instanceof NotifyOnActionCacheHit) {
+ NotifyOnActionCacheHit notify = (NotifyOnActionCacheHit) action;
+ notify.actionCacheHit(executorEngine);
+ }
+
+ // We still need to check the outputs so that output file data is available to the value.
+ checkOutputs(action, metadataHandler);
+ if (!eventPosted) {
+ postEvent(new CachedActionEvent(action, actionStartTime));
+ }
+ }
+ return token;
+ }
+
+ /**
+ * Perform dependency discovery for action, which must discover its inputs.
+ *
+ * <p>This method is just a wrapper around {@link Action#discoverInputs} that properly processes
+ * any ActionExecutionException thrown before rethrowing it to the caller.
+ */
+ void discoverInputs(Action action, ActionExecutionContext actionExecutionContext)
+ throws ActionExecutionException, InterruptedException {
+ try {
+ action.discoverInputs(actionExecutionContext);
+ } catch (ActionExecutionException e) {
+ processAndThrow(e, action, actionExecutionContext.getFileOutErr());
+ }
+ }
+
+ /**
+ * This method should be called if the builder encounters an error during
+ * execution. This allows the builder to record that it encountered at
+ * least one error, and may make it swallow its output to prevent
+ * spamming the user any further.
+ */
+ private void recordExecutionError() {
+ hadExecutionError = true;
+ }
+
+ /**
+ * Returns true if the Builder is winding down (i.e. cancelling outstanding
+ * actions and preparing to abort.)
+ * The builder is winding down iff:
+ * <ul>
+ * <li>we had an execution error
+ * <li>we are not running with --keep_going
+ * </ul>
+ */
+ private boolean isBuilderAborting() {
+ return hadExecutionError && !keepGoing;
+ }
+
+ void setFileCache(ActionInputFileCache fileCache) {
+ this.perBuildFileCache = fileCache;
+ }
+
+ private class ActionRunner implements Callable<ActionExecutionValue> {
+ private final Action action;
+ private final FileAndMetadataCache graphFileCache;
+ private Token token;
+ private long actionStartTime;
+ private ActionExecutionContext actionExecutionContext;
+
+ ActionRunner(Action action, FileAndMetadataCache graphFileCache, Token token,
+ long actionStartTime,
+ ActionExecutionContext actionExecutionContext) {
+ this.action = action;
+ this.graphFileCache = graphFileCache;
+ this.token = token;
+ this.actionStartTime = actionStartTime;
+ this.actionExecutionContext = actionExecutionContext;
+ }
+
+ @Override
+ public ActionExecutionValue call() throws ActionExecutionException, InterruptedException {
+ profiler.startTask(ProfilerTask.ACTION, action);
+ try {
+ if (actionCacheChecker.isActionExecutionProhibited(action)) {
+ // We can't execute an action (e.g. because --check_???_up_to_date option was used). Fail
+ // the build instead.
+ synchronized (reporter) {
+ TargetOutOfDateException e = new TargetOutOfDateException(action);
+ reporter.handle(Event.error(e.getMessage()));
+ recordExecutionError();
+ throw e;
+ }
+ }
+
+ String message = action.getProgressMessage();
+ if (message != null) {
+ reporter.startTask(null, prependExecPhaseStats(message));
+ }
+ statusReporterRef.get().setPreparing(action);
+
+ createOutputDirectories(action);
+
+ prepareScheduleExecuteAndCompleteAction(action, token,
+ actionExecutionContext, actionStartTime);
+ return new ActionExecutionValue(
+ graphFileCache.getOutputData(), graphFileCache.getAdditionalOutputData());
+ } finally {
+ profiler.completeTask(ProfilerTask.ACTION);
+ }
+ }
+ }
+
+ private void createOutputDirectories(Action action) throws ActionExecutionException {
+ try {
+ Set<Path> done = new HashSet<>(); // avoid redundant calls for the same directory.
+ for (Artifact outputFile : action.getOutputs()) {
+ Path outputDir = outputFile.getPath().getParentDirectory();
+ if (done.add(outputDir)) {
+ try {
+ createDirectoryAndParents(outputDir);
+ continue;
+ } catch (IOException e) {
+ /* Fall through to plan B. */
+ }
+
+ // Possibly some direct ancestors are not directories. In that case, we unlink all the
+ // ancestors until we reach a directory, then try again. This handles the case where a
+ // file becomes a directory, either from one build to another, or within a single build.
+ //
+ // Symlinks should not be followed so in order to clean up symlinks pointing to Fileset
+ // outputs from previous builds. See bug [incremental build of Fileset fails if
+ // Fileset.out was changed to be a subdirectory of the old value].
+ try {
+ for (Path p = outputDir; !p.isDirectory(Symlinks.NOFOLLOW);
+ p = p.getParentDirectory()) {
+ // p may be a file or dangling symlink, or a symlink to an old Fileset output
+ p.delete(); // throws IOException
+ }
+ createDirectoryAndParents(outputDir);
+ } catch (IOException e) {
+ throw new ActionExecutionException(
+ "failed to create output directory '" + outputDir + "'", e, action, false);
+ }
+ }
+ }
+ } catch (ActionExecutionException ex) {
+ printError(ex.getMessage(), action, null);
+ throw ex;
+ }
+ }
+
+ private String prependExecPhaseStats(String message) {
+ if (progressSupplier != null) {
+ // Prints a progress message like:
+ // [2608/6445] Compiling foo/bar.cc [host]
+ return progressSupplier.getProgressString() + " " + message;
+ } else {
+ // progressSupplier may be null in tests
+ return message;
+ }
+ }
+
+ /**
+ * Prepare, schedule, execute, and then complete the action.
+ * When this function is called, we know that this action needs to be executed.
+ * This function will prepare for the action's execution (i.e. delete the outputs);
+ * schedule its execution; execute the action;
+ * and then do some post-execution processing to complete the action:
+ * set the outputs readonly and executable, and insert the action results in the
+ * action cache.
+ *
+ * @param action The action to execute
+ * @param token The non-null token returned by dependencyChecker.getTokenIfNeedToExecute()
+ * @param context services in the scope of the action
+ * @param actionStartTime time when we started the first phase of the action execution.
+ * @throws ActionExecutionException if the execution of the specified action
+ * failed for any reason.
+ * @throws InterruptedException if the thread was interrupted.
+ */
+ private void prepareScheduleExecuteAndCompleteAction(Action action, Token token,
+ ActionExecutionContext context, long actionStartTime)
+ throws ActionExecutionException, InterruptedException {
+ Preconditions.checkNotNull(token, action);
+ // Delete the metadataHandler's cache of the action's outputs, since they are being deleted.
+ context.getMetadataHandler().discardMetadata(action.getOutputs());
+ // Delete the outputs before executing the action, just to ensure that
+ // the action really does produce the outputs.
+ try {
+ action.prepare(context.getExecutor().getExecRoot());
+ } catch (IOException e) {
+ reportError("failed to delete output files before executing action", e, action, null);
+ }
+
+ postEvent(new ActionStartedEvent(action, actionStartTime));
+ ResourceSet estimate = action.estimateResourceConsumption(executorEngine);
+ ActionExecutionStatusReporter statusReporter = statusReporterRef.get();
+ try {
+ if (estimate == null || estimate == ResourceSet.ZERO) {
+ statusReporter.setRunningFromBuildData(action);
+ } else {
+ // If estimated resource consumption is null, action will manually call
+ // resource manager when it knows what resources are needed.
+ resourceManager.acquireResources(action, estimate);
+ }
+ boolean outputDumped = executeActionTask(action, context);
+ completeAction(action, token, context.getMetadataHandler(),
+ context.getFileOutErr(), outputDumped);
+ } finally {
+ if (estimate != null) {
+ resourceManager.releaseResources(action, estimate);
+ }
+ statusReporter.remove(action);
+ postEvent(new ActionCompletionEvent(action));
+ }
+ }
+
+ private ActionExecutionException processAndThrow(
+ ActionExecutionException e, Action action, FileOutErr outErrBuffer)
+ throws ActionExecutionException {
+ reportActionExecution(action, e, outErrBuffer);
+ boolean reported = reportErrorIfNotAbortingMode(e, outErrBuffer);
+
+ ActionExecutionException toThrow = e;
+ if (reported){
+ // If we already printed the error for the exception we mark it as already reported
+ // so that we do not print it again in upper levels.
+ // Note that we need to report it here since we want immediate feedback of the errors
+ // and in some cases the upper-level printing mechanism only prints one of the errors.
+ toThrow = new AlreadyReportedActionExecutionException(e);
+ }
+
+ // Now, rethrow the exception.
+ // This can have two effects:
+ // If we're still building, the exception will get retrieved by the
+ // completor and rethrown.
+ // If we're aborting, the exception will never be retrieved from the
+ // completor, since the completor is waiting for all outstanding jobs
+ // to finish. After they have finished, it will only rethrow the
+ // exception that initially caused it to abort will and not check the
+ // exit status of any actions that had finished in the meantime.
+ throw toThrow;
+ }
+
+ /**
+ * Execute the specified action, in a profiler task.
+ * The caller is responsible for having already checked that we need to
+ * execute it and for acquiring/releasing any scheduling locks needed.
+ *
+ * <p>This is thread-safe so long as you don't try to execute the same action
+ * twice at the same time (or overlapping times).
+ * May execute in a worker thread.
+ *
+ * @throws ActionExecutionException if the execution of the specified action
+ * failed for any reason.
+ * @throws InterruptedException if the thread was interrupted.
+ * @return true if the action output was dumped, false otherwise.
+ */
+ private boolean executeActionTask(Action action, ActionExecutionContext actionExecutionContext)
+ throws ActionExecutionException, InterruptedException {
+ profiler.startTask(ProfilerTask.ACTION_EXECUTE, action);
+ // ActionExecutionExceptions that occur as the thread is interrupted are
+ // assumed to be a result of that, so we throw InterruptedException
+ // instead.
+ FileOutErr outErrBuffer = actionExecutionContext.getFileOutErr();
+ try {
+ action.execute(actionExecutionContext);
+
+ // Action terminated fine, now report the output.
+ // The .showOutput() method is not necessarily a quick check: in its
+ // current implementation it uses regular expression matching.
+ if (outErrBuffer.hasRecordedOutput()
+ && (action.showsOutputUnconditionally()
+ || reporter.showOutput(Label.print(action.getOwner().getLabel())))) {
+ dumpRecordedOutErr(action, outErrBuffer);
+ return true;
+ }
+ // Defer reporting action success until outputs are checked
+ } catch (ActionExecutionException e) {
+ processAndThrow(e, action, outErrBuffer);
+ } finally {
+ profiler.completeTask(ProfilerTask.ACTION_EXECUTE);
+ }
+ return false;
+ }
+
+ private void completeAction(Action action, Token token, MetadataHandler metadataHandler,
+ FileOutErr fileOutErr, boolean outputAlreadyDumped) throws ActionExecutionException {
+ try {
+ Preconditions.checkState(action.inputsKnown(),
+ "Action %s successfully executed, but inputs still not known", action);
+
+ profiler.startTask(ProfilerTask.ACTION_COMPLETE, action);
+ try {
+ if (!checkOutputs(action, metadataHandler)) {
+ reportError("not all outputs were created", null, action,
+ outputAlreadyDumped ? null : fileOutErr);
+ }
+ // Prevent accidental stomping on files.
+ // This will also throw a FileNotFoundException
+ // if any of the output files doesn't exist.
+ try {
+ setOutputsReadOnlyAndExecutable(action, metadataHandler);
+ } catch (IOException e) {
+ reportError("failed to set outputs read-only", e, action, null);
+ }
+ try {
+ actionCacheChecker.afterExecution(action, token, metadataHandler);
+ } catch (IOException e) {
+ // Skyframe does all the filesystem access needed during the previous calls, and if those
+ // calls failed, we should already have thrown. So an IOException is impossible here.
+ throw new IllegalStateException(
+ "failed to update action cache for " + action.prettyPrint()
+ + ", but all outputs should already have been checked", e);
+ }
+ } finally {
+ profiler.completeTask(ProfilerTask.ACTION_COMPLETE);
+ }
+ reportActionExecution(action, null, fileOutErr);
+ } catch (ActionExecutionException actionException) {
+ // Success in execution but failure in completion.
+ reportActionExecution(action, actionException, fileOutErr);
+ throw actionException;
+ } catch (IllegalStateException exception) {
+ // More serious internal error, but failure still reported.
+ reportActionExecution(action,
+ new ActionExecutionException(exception, action, true), fileOutErr);
+ throw exception;
+ }
+ }
+
+ /**
+ * For each of the action's outputs that is a regular file (not a symbolic
+ * link or directory), make it read-only and executable.
+ *
+ * <p>Making the outputs read-only helps preventing accidental editing of
+ * them (e.g. in case of generated source code), while making them executable
+ * helps running generated files (such as generated shell scripts) on the
+ * command line.
+ *
+ * <p>May execute in a worker thread.
+ *
+ * <p>Note: setting these bits maintains transparency regarding the locality of the build;
+ * because the remote execution engine sets them, they should be set for local builds too.
+ *
+ * @throws IOException if an I/O error occurred.
+ */
+ private final void setOutputsReadOnlyAndExecutable(Action action, MetadataHandler metadataHandler)
+ throws IOException {
+ Preconditions.checkState(!action.getActionType().isMiddleman());
+
+ for (Artifact output : action.getOutputs()) {
+ Path path = output.getPath();
+ if (metadataHandler.isInjected(output)) {
+ // We trust the files created by the execution-engine to be non symlinks with expected
+ // chmod() settings already applied. The follow stanza implies a total of 6 system calls,
+ // since the UnixFileSystem implementation of setWritable() and setExecutable() both
+ // do a stat() internally.
+ continue;
+ }
+ if (path.isFile(Symlinks.NOFOLLOW)) { // i.e. regular files only.
+ path.setWritable(false);
+ path.setExecutable(true);
+ }
+ }
+ }
+
+ private void reportMissingOutputFile(Action action, Artifact output, Reporter reporter,
+ boolean isSymlink) {
+ boolean genrule = action.getMnemonic().equals("Genrule");
+ String prefix = (genrule ? "declared output '" : "output '") + output.prettyPrint() + "' ";
+ if (isSymlink) {
+ reporter.handle(Event.error(
+ action.getOwner().getLocation(), prefix + "is a dangling symbolic link"));
+ } else {
+ String suffix = genrule ? " by genrule. This is probably "
+ + "because the genrule actually didn't create this output, or because the output was a "
+ + "directory and the genrule was run remotely (note that only the contents of "
+ + "declared file outputs are copied from genrules run remotely)" : "";
+ reporter.handle(Event.error(
+ action.getOwner().getLocation(), prefix + "was not created" + suffix));
+ }
+ }
+
+ /**
+ * Validates that all action outputs were created.
+ *
+ * @return false if some outputs are missing, true - otherwise.
+ */
+ private boolean checkOutputs(Action action, MetadataHandler metadataHandler) {
+ boolean success = true;
+ for (Artifact output : action.getOutputs()) {
+ if (!metadataHandler.artifactExists(output)) {
+ reportMissingOutputFile(action, output, reporter, output.getPath().isSymbolicLink());
+ success = false;
+ }
+ }
+ return success;
+ }
+
+ private void postEvent(Object event) {
+ EventBus bus = eventBus.get();
+ if (bus != null) {
+ bus.post(event);
+ }
+ }
+
+ /**
+ * Convenience function for reporting that the action failed due to a
+ * the exception cause, if there is an additional explanatory message that
+ * clarifies the message of the exception. Combines the user-provided message
+ * and the exceptions' message and reports the combination as error.
+ * Then, throws an ActionExecutionException with the reported error as
+ * message and the provided exception as the cause.
+ *
+ * @param message A small text that explains why the action failed
+ * @param cause The exception that caused the action to fail
+ * @param action The action that failed
+ * @param actionOutput The output of the failed Action.
+ * May be null, if there is no output to display
+ */
+ private void reportError(String message, Throwable cause, Action action, FileOutErr actionOutput)
+ throws ActionExecutionException {
+ ActionExecutionException ex;
+ if (cause == null) {
+ ex = new ActionExecutionException(message, action, false);
+ } else {
+ ex = new ActionExecutionException(message, cause, action, false);
+ }
+ printError(ex.getMessage(), action, actionOutput);
+ throw ex;
+ }
+
+ /**
+ * For the action 'action' that failed due to 'ex' with the output
+ * 'actionOutput', notify the user about the error. To notify the user, the
+ * method first displays the output of the action and then reports an error
+ * via the reporter. The method ensures that the two messages appear next to
+ * each other by locking the outErr object where the output is displayed.
+ *
+ * @param message The reason why the action failed
+ * @param action The action that failed, must not be null.
+ * @param actionOutput The output of the failed Action.
+ * May be null, if there is no output to display
+ */
+ private void printError(String message, Action action, FileOutErr actionOutput) {
+ synchronized (reporter) {
+ if (actionOutput != null && actionOutput.hasRecordedOutput()) {
+ dumpRecordedOutErr(action, actionOutput);
+ }
+ if (keepGoing) {
+ message = "Couldn't " + describeAction(action) + ": " + message;
+ }
+ reporter.handle(Event.error(action.getOwner().getLocation(), message));
+ recordExecutionError();
+ }
+ }
+
+ /** Describe an action, for use in error messages. */
+ private static String describeAction(Action action) {
+ if (action.getOutputs().isEmpty()) {
+ return "run " + action.prettyPrint();
+ } else if (action.getActionType().isMiddleman()) {
+ return "build " + action.prettyPrint();
+ } else {
+ return "build file " + action.getPrimaryOutput().prettyPrint();
+ }
+ }
+
+ /**
+ * Dump the output from the action.
+ *
+ * @param action The action whose output is being dumped
+ * @param outErrBuffer The OutErr that recorded the actions output
+ */
+ private void dumpRecordedOutErr(Action action, FileOutErr outErrBuffer) {
+ StringBuilder message = new StringBuilder("");
+ message.append("From ");
+ message.append(action.describe());
+ message.append(":");
+
+ // Synchronize this on the reporter, so that the output from multiple
+ // actions will not be interleaved.
+ synchronized (reporter) {
+ // Only print the output if we're not winding down.
+ if (isBuilderAborting()) {
+ return;
+ }
+ reporter.handle(Event.info(message.toString()));
+
+ OutErr outErr = this.reporter.getOutErr();
+ outErrBuffer.dumpOutAsLatin1(outErr.getOutputStream());
+ outErrBuffer.dumpErrAsLatin1(outErr.getErrorStream());
+ }
+ }
+
+ private void reportActionExecution(Action action,
+ ActionExecutionException exception, FileOutErr outErr) {
+ String stdout = null;
+ String stderr = null;
+
+ if (outErr.hasRecordedStdout()) {
+ stdout = outErr.getOutputFile().toString();
+ }
+ if (outErr.hasRecordedStderr()) {
+ stderr = outErr.getErrorFile().toString();
+ }
+ postEvent(new ActionExecutedEvent(action, exception, stdout, stderr));
+ }
+
+ /**
+ * Returns true if the exception was reported. False otherwise. Currently this is a copy of what
+ * we did in pre-Skyframe execution. The main implication is that we are printing the error to the
+ * top level reporter instead of the action reporter. Because of that Skyframe values do not know
+ * about the errors happening in the execution phase. Even if we change in the future to log to
+ * the action reporter (that would be done in ActionExecutionFunction.compute() when we get an
+ * ActionExecutionException), we probably do not want to also store the StdErr output, so
+ * dumpRecordedOutErr() should still be called here.
+ */
+ private boolean reportErrorIfNotAbortingMode(ActionExecutionException ex,
+ FileOutErr outErrBuffer) {
+ // For some actions (e.g. many local actions) the pollInterruptedStatus()
+ // won't notice that we had an interrupted job. It will continue.
+ // For that reason we must take care to NOT report errors if we're
+ // in the 'aborting' mode: Any cancelled action would show up here.
+ // For some actions (e.g. many local actions) the pollInterruptedStatus()
+ // won't notice that we had an interrupted job. It will continue.
+ // For that reason we must take care to NOT report errors if we're
+ // in the 'aborting' mode: Any cancelled action would show up here.
+ synchronized (this.reporter) {
+ if (!isBuilderAborting()) {
+ // Oops. The action aborted. Report the problem.
+ printError(ex.getMessage(), ex.getAction(), outErrBuffer);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** An object supplying data for action execution progress reporting. */
+ public interface ProgressSupplier {
+ /** Returns the progress string to prefix action execution messages with. */
+ String getProgressString();
+ }
+
+ /** An object that can be notified about action completion. */
+ public interface ActionCompletedReceiver {
+ /** Receives a completed action. */
+ void actionCompleted(Action action);
+ }
+
+ public void setActionExecutionProgressReportingObjects(
+ @Nullable ProgressSupplier progressSupplier,
+ @Nullable ActionCompletedReceiver completionReceiver) {
+ this.progressSupplier = progressSupplier;
+ this.completionReceiver = completionReceiver;
+ }
+
+ private static class DelegatingPairFileCache implements ActionInputFileCache {
+ private final ActionInputFileCache perActionCache;
+ private final ActionInputFileCache perBuildFileCache;
+
+ private DelegatingPairFileCache(ActionInputFileCache mainCache,
+ ActionInputFileCache perBuildFileCache) {
+ this.perActionCache = mainCache;
+ this.perBuildFileCache = perBuildFileCache;
+ }
+
+ @Override
+ public ByteString getDigest(ActionInput actionInput) throws IOException {
+ ByteString digest = perActionCache.getDigest(actionInput);
+ return digest != null ? digest : perBuildFileCache.getDigest(actionInput);
+ }
+
+ @Override
+ public long getSizeInBytes(ActionInput actionInput) throws IOException {
+ long size = perActionCache.getSizeInBytes(actionInput);
+ return size > -1 ? size : perBuildFileCache.getSizeInBytes(actionInput);
+ }
+
+ @Override
+ public boolean contentsAvailableLocally(ByteString digest) {
+ return perActionCache.contentsAvailableLocally(digest)
+ || perBuildFileCache.contentsAvailableLocally(digest);
+ }
+
+ @Nullable
+ @Override
+ public File getFileFromDigest(ByteString digest) throws IOException {
+ File file = perActionCache.getFileFromDigest(digest);
+ return file != null ? file : perBuildFileCache.getFileFromDigest(digest);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeBuildView.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeBuildView.java
new file mode 100644
index 0000000..857c231
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeBuildView.java
@@ -0,0 +1,510 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.common.eventbus.EventBus;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.ArtifactFactory;
+import com.google.devtools.build.lib.actions.ArtifactOwner;
+import com.google.devtools.build.lib.actions.ArtifactPrefixConflictException;
+import com.google.devtools.build.lib.actions.MutableActionGraph;
+import com.google.devtools.build.lib.analysis.AnalysisEnvironment;
+import com.google.devtools.build.lib.analysis.AnalysisFailureEvent;
+import com.google.devtools.build.lib.analysis.Aspect;
+import com.google.devtools.build.lib.analysis.CachingAnalysisEnvironment;
+import com.google.devtools.build.lib.analysis.ConfiguredAspectFactory;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.ConfiguredTargetFactory;
+import com.google.devtools.build.lib.analysis.LabelAndConfiguration;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget;
+import com.google.devtools.build.lib.analysis.ViewCreationFailedException;
+import com.google.devtools.build.lib.analysis.buildinfo.BuildInfoFactory;
+import com.google.devtools.build.lib.analysis.buildinfo.BuildInfoFactory.BuildInfoKey;
+import com.google.devtools.build.lib.analysis.config.BinTools;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.analysis.config.BuildConfigurationCollection;
+import com.google.devtools.build.lib.analysis.config.ConfigMatchingProvider;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.packages.Attribute;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.skyframe.ActionLookupValue.ActionLookupKey;
+import com.google.devtools.build.lib.skyframe.BuildInfoCollectionValue.BuildInfoKeyAndConfig;
+import com.google.devtools.build.lib.skyframe.ConfiguredTargetFunction.ConfiguredValueCreationException;
+import com.google.devtools.build.lib.skyframe.SkyframeActionExecutor.ConflictException;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.skyframe.CycleInfo;
+import com.google.devtools.build.skyframe.ErrorInfo;
+import com.google.devtools.build.skyframe.EvaluationProgressReceiver;
+import com.google.devtools.build.skyframe.EvaluationResult;
+import com.google.devtools.build.skyframe.SkyFunction.Environment;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+/**
+ * Skyframe-based driver of analysis.
+ *
+ * <p>Covers enough functionality to work as a substitute for {@code BuildView#configureTargets}.
+ */
+public final class SkyframeBuildView {
+
+ private final ConfiguredTargetFactory factory;
+ private final ArtifactFactory artifactFactory;
+ @Nullable private EventHandler warningListener;
+ private final SkyframeExecutor skyframeExecutor;
+ private final Runnable legacyDataCleaner;
+ private final BinTools binTools;
+ private boolean enableAnalysis = false;
+
+ // This hack allows us to see when a configured target has been invalidated, and thus when the set
+ // of artifact conflicts needs to be recomputed (whenever a configured target has been invalidated
+ // or newly evaluated).
+ private final EvaluationProgressReceiver invalidationReceiver =
+ new ConfiguredTargetValueInvalidationReceiver();
+ private final Set<SkyKey> evaluatedConfiguredTargets = Sets.newConcurrentHashSet();
+ // Used to see if checks of graph consistency need to be done after analysis.
+ private volatile boolean someConfiguredTargetEvaluated = false;
+
+ // We keep the set of invalidated configuration targets so that we can know if something
+ // has been invalidated after graph pruning has been executed.
+ private Set<ConfiguredTargetValue> dirtyConfiguredTargets = Sets.newConcurrentHashSet();
+ private volatile boolean anyConfiguredTargetDeleted = false;
+ private SkyKey configurationKey = null;
+
+ public SkyframeBuildView(ConfiguredTargetFactory factory,
+ ArtifactFactory artifactFactory,
+ SkyframeExecutor skyframeExecutor, Runnable legacyDataCleaner, BinTools binTools) {
+ this.factory = factory;
+ this.artifactFactory = artifactFactory;
+ this.skyframeExecutor = skyframeExecutor;
+ this.legacyDataCleaner = legacyDataCleaner;
+ this.binTools = binTools;
+ skyframeExecutor.setArtifactFactoryAndBinTools(artifactFactory, binTools);
+ }
+
+ public void setWarningListener(@Nullable EventHandler warningListener) {
+ this.warningListener = warningListener;
+ }
+
+ public void setConfigurationSkyKey(SkyKey skyKey) {
+ this.configurationKey = skyKey;
+ }
+
+ public void resetEvaluatedConfiguredTargetKeysSet() {
+ evaluatedConfiguredTargets.clear();
+ }
+
+ public Set<SkyKey> getEvaluatedTargetKeys() {
+ return ImmutableSet.copyOf(evaluatedConfiguredTargets);
+ }
+
+ private void setDeserializedArtifactOwners() throws ViewCreationFailedException {
+ Map<PathFragment, Artifact> deserializedArtifactMap =
+ artifactFactory.getDeserializedArtifacts();
+ Set<Artifact> deserializedArtifacts = new HashSet<>();
+ for (Artifact artifact : deserializedArtifactMap.values()) {
+ if (!artifact.getExecPath().getBaseName().endsWith(".gcda")) {
+ // gcda files are classified as generated artifacts, but are not actually generated. All
+ // others need owners.
+ deserializedArtifacts.add(artifact);
+ }
+ }
+ if (deserializedArtifacts.isEmpty()) {
+ // If there are no deserialized artifacts to process, don't pay the price of iterating over
+ // the graph.
+ return;
+ }
+ for (Map.Entry<SkyKey, ActionLookupValue> entry :
+ skyframeExecutor.getActionLookupValueMap().entrySet()) {
+ for (Action action : entry.getValue().getActionsForFindingArtifactOwners()) {
+ for (Artifact output : action.getOutputs()) {
+ Artifact deserializedArtifact = deserializedArtifactMap.get(output.getExecPath());
+ if (deserializedArtifact != null) {
+ deserializedArtifact.setArtifactOwner((ActionLookupKey) entry.getKey().argument());
+ deserializedArtifacts.remove(deserializedArtifact);
+ }
+ }
+ }
+ }
+ if (!deserializedArtifacts.isEmpty()) {
+ throw new ViewCreationFailedException("These artifacts were read in from the FDO profile but"
+ + " have no generating action that could be found. If you are confident that your profile was"
+ + " collected from the same source state at which you're building, please report this:\n"
+ + Artifact.asExecPaths(deserializedArtifacts));
+ }
+ artifactFactory.clearDeserializedArtifacts();
+ }
+
+ /**
+ * Analyzes the specified targets using Skyframe as the driving framework.
+ *
+ * @return the configured targets that should be built
+ */
+ public Collection<ConfiguredTarget> configureTargets(List<ConfiguredTargetKey> values,
+ EventBus eventBus, boolean keepGoing)
+ throws InterruptedException, ViewCreationFailedException {
+ enableAnalysis(true);
+ EvaluationResult<ConfiguredTargetValue> result;
+ try {
+ result = skyframeExecutor.configureTargets(values, keepGoing);
+ } finally {
+ enableAnalysis(false);
+ }
+ // For Skyframe m1, note that we already reported action conflicts during action registration
+ // in the legacy action graph.
+ ImmutableMap<Action, ConflictException> badActions = skyframeExecutor.findArtifactConflicts();
+
+ // Filter out all CTs that have a bad action and convert to a list of configured targets. This
+ // code ensures that the resulting list of configured targets has the same order as the incoming
+ // list of values, i.e., that the order is deterministic.
+ Collection<ConfiguredTarget> goodCts = Lists.newArrayListWithCapacity(values.size());
+ for (ConfiguredTargetKey value : values) {
+ ConfiguredTargetValue ctValue = result.get(ConfiguredTargetValue.key(value));
+ if (ctValue == null) {
+ continue;
+ }
+ goodCts.add(ctValue.getConfiguredTarget());
+ }
+
+ if (!result.hasError() && badActions.isEmpty()) {
+ setDeserializedArtifactOwners();
+ return goodCts;
+ }
+
+ // --nokeep_going so we fail with an exception for the first error.
+ // TODO(bazel-team): We might want to report the other errors through the event bus but
+ // for keeping this code in parity with legacy we just report the first error for now.
+ if (!keepGoing) {
+ for (Map.Entry<Action, ConflictException> bad : badActions.entrySet()) {
+ ConflictException ex = bad.getValue();
+ try {
+ ex.rethrowTyped();
+ } catch (MutableActionGraph.ActionConflictException ace) {
+ ace.reportTo(skyframeExecutor.getReporter());
+ String errorMsg = "Analysis of target '" + bad.getKey().getOwner().getLabel()
+ + "' failed; build aborted";
+ throw new ViewCreationFailedException(errorMsg);
+ } catch (ArtifactPrefixConflictException apce) {
+ skyframeExecutor.getReporter().handle(Event.error(apce.getMessage()));
+ }
+ throw new ViewCreationFailedException(ex.getMessage());
+ }
+
+ Map.Entry<SkyKey, ErrorInfo> error = result.errorMap().entrySet().iterator().next();
+ SkyKey topLevel = error.getKey();
+ ErrorInfo errorInfo = error.getValue();
+ assertSaneAnalysisError(errorInfo, topLevel);
+ skyframeExecutor.getCyclesReporter().reportCycles(errorInfo.getCycleInfo(), topLevel,
+ skyframeExecutor.getReporter());
+ Throwable cause = errorInfo.getException();
+ Preconditions.checkState(cause != null || !Iterables.isEmpty(errorInfo.getCycleInfo()),
+ errorInfo);
+ String errorMsg = "Analysis of target '" + ConfiguredTargetValue.extractLabel(topLevel)
+ + "' failed; build aborted";
+ throw new ViewCreationFailedException(errorMsg);
+ }
+
+ // --keep_going : We notify the error and return a ConfiguredTargetValue
+ for (Map.Entry<SkyKey, ErrorInfo> errorEntry : result.errorMap().entrySet()) {
+ if (values.contains(errorEntry.getKey().argument())) {
+ SkyKey errorKey = errorEntry.getKey();
+ ConfiguredTargetKey label = (ConfiguredTargetKey) errorKey.argument();
+ ErrorInfo errorInfo = errorEntry.getValue();
+ assertSaneAnalysisError(errorInfo, errorKey);
+
+ skyframeExecutor.getCyclesReporter().reportCycles(errorInfo.getCycleInfo(), errorKey,
+ skyframeExecutor.getReporter());
+ // We try to get the root cause key first from ErrorInfo rootCauses. If we don't have one
+ // we try to use the cycle culprit if the error is a cycle. Otherwise we use the top-level
+ // error key.
+ Label root;
+ if (!Iterables.isEmpty(errorEntry.getValue().getRootCauses())) {
+ SkyKey culprit = Preconditions.checkNotNull(Iterables.getFirst(
+ errorEntry.getValue().getRootCauses(), null));
+ root = ((ConfiguredTargetKey) culprit.argument()).getLabel();
+ } else {
+ root = maybeGetConfiguredTargetCycleCulprit(errorInfo.getCycleInfo());
+ }
+ if (warningListener != null) {
+ warningListener.handle(Event.warn("errors encountered while analyzing target '"
+ + label + "': it will not be built"));
+ }
+ eventBus.post(new AnalysisFailureEvent(
+ LabelAndConfiguration.of(label.getLabel(), label.getConfiguration()), root));
+ }
+ }
+
+ Collection<Exception> reportedExceptions = Sets.newHashSet();
+ for (Map.Entry<Action, ConflictException> bad : badActions.entrySet()) {
+ ConflictException ex = bad.getValue();
+ try {
+ ex.rethrowTyped();
+ } catch (MutableActionGraph.ActionConflictException ace) {
+ ace.reportTo(skyframeExecutor.getReporter());
+ if (warningListener != null) {
+ warningListener.handle(Event.warn("errors encountered while analyzing target '"
+ + bad.getKey().getOwner().getLabel() + "': it will not be built"));
+ }
+ } catch (ArtifactPrefixConflictException apce) {
+ if (reportedExceptions.add(apce)) {
+ skyframeExecutor.getReporter().handle(Event.error(apce.getMessage()));
+ }
+ }
+ }
+
+ if (!badActions.isEmpty()) {
+ // In order to determine the set of configured targets transitively error free from action
+ // conflict issues, we run a post-processing update() that uses the bad action map.
+ EvaluationResult<PostConfiguredTargetValue> actionConflictResult =
+ skyframeExecutor.postConfigureTargets(values, keepGoing, badActions);
+
+ goodCts = Lists.newArrayListWithCapacity(values.size());
+ for (ConfiguredTargetKey value : values) {
+ PostConfiguredTargetValue postCt =
+ actionConflictResult.get(PostConfiguredTargetValue.key(value));
+ if (postCt != null) {
+ goodCts.add(postCt.getCt());
+ }
+ }
+ }
+ setDeserializedArtifactOwners();
+ return goodCts;
+ }
+
+ @Nullable
+ Label maybeGetConfiguredTargetCycleCulprit(Iterable<CycleInfo> cycleInfos) {
+ for (CycleInfo cycleInfo : cycleInfos) {
+ SkyKey culprit = Iterables.getFirst(cycleInfo.getCycle(), null);
+ if (culprit == null) {
+ continue;
+ }
+ if (culprit.functionName().equals(SkyFunctions.CONFIGURED_TARGET)) {
+ return ((LabelAndConfiguration) culprit.argument()).getLabel();
+ }
+ }
+ return null;
+ }
+
+ private static void assertSaneAnalysisError(ErrorInfo errorInfo, SkyKey key) {
+ Throwable cause = errorInfo.getException();
+ if (cause != null) {
+ // We should only be trying to configure targets when the loading phase succeeds, meaning
+ // that the only errors should be analysis errors.
+ Preconditions.checkState(cause instanceof ConfiguredValueCreationException,
+ "%s -> %s", key, errorInfo);
+ }
+ }
+
+ ArtifactFactory getArtifactFactory() {
+ return artifactFactory;
+ }
+
+ @Nullable
+ EventHandler getWarningListener() {
+ return warningListener;
+ }
+
+ /**
+ * Because we don't know what build-info artifacts this configured target may request, we
+ * conservatively register a dep on all of them.
+ */
+ // TODO(bazel-team): Allow analysis to return null so the value builder can exit and wait for a
+ // restart deps are not present.
+ private boolean getWorkspaceStatusValues(Environment env) {
+ env.getValue(WorkspaceStatusValue.SKY_KEY);
+ Map<BuildInfoKey, BuildInfoFactory> buildInfoFactories =
+ PrecomputedValue.BUILD_INFO_FACTORIES.get(env);
+ if (buildInfoFactories == null) {
+ return false;
+ }
+ BuildConfigurationCollection configurations = getBuildConfigurationCollection(env);
+ if (configurations == null) {
+ return false;
+ }
+ // These factories may each create their own build info artifacts, all depending on the basic
+ // build-info.txt and build-changelist.txt.
+ List<SkyKey> depKeys = Lists.newArrayList();
+ for (BuildInfoKey key : buildInfoFactories.keySet()) {
+ for (BuildConfiguration config : configurations.getAllConfigurations()) {
+ if (buildInfoFactories.get(key).isEnabled(config)) {
+ depKeys.add(BuildInfoCollectionValue.key(new BuildInfoKeyAndConfig(key, config)));
+ }
+ }
+ }
+ env.getValues(depKeys);
+ return !env.valuesMissing();
+ }
+
+ /** Returns null if any build-info values are not ready. */
+ @Nullable
+ CachingAnalysisEnvironment createAnalysisEnvironment(ArtifactOwner owner,
+ boolean isSystemEnv, boolean extendedSanityChecks, EventHandler eventHandler,
+ Environment env, boolean allowRegisteringActions) {
+ if (!getWorkspaceStatusValues(env)) {
+ return null;
+ }
+ return new CachingAnalysisEnvironment(
+ artifactFactory, owner, isSystemEnv, extendedSanityChecks, eventHandler, env,
+ allowRegisteringActions, binTools);
+ }
+
+ /**
+ * Invokes the appropriate constructor to create a {@link ConfiguredTarget} instance.
+ *
+ * <p>For use in {@code ConfiguredTargetFunction}.
+ *
+ * <p>Returns null if Skyframe deps are missing or upon certain errors.
+ */
+ @Nullable
+ ConfiguredTarget createConfiguredTarget(Target target, BuildConfiguration configuration,
+ CachingAnalysisEnvironment analysisEnvironment,
+ ListMultimap<Attribute, ConfiguredTarget> prerequisiteMap,
+ Set<ConfigMatchingProvider> configConditions)
+ throws InterruptedException {
+ Preconditions.checkState(enableAnalysis,
+ "Already in execution phase %s %s", target, configuration);
+ return factory.createConfiguredTarget(analysisEnvironment, artifactFactory, target,
+ configuration, prerequisiteMap, configConditions);
+ }
+
+ @Nullable
+ public Aspect createAspect(
+ AnalysisEnvironment env, RuleConfiguredTarget associatedTarget,
+ ConfiguredAspectFactory aspectFactory,
+ ListMultimap<Attribute, ConfiguredTarget> prerequisiteMap,
+ Set<ConfigMatchingProvider> configConditions) {
+ return factory.createAspect(
+ env, associatedTarget, aspectFactory, prerequisiteMap, configConditions);
+ }
+
+ @Nullable
+ private BuildConfigurationCollection getBuildConfigurationCollection(Environment env) {
+ ConfigurationCollectionValue configurationsValue =
+ (ConfigurationCollectionValue) env.getValue(configurationKey);
+ return configurationsValue == null ? null : configurationsValue.getConfigurationCollection();
+ }
+
+ @Nullable
+ SkyframeDependencyResolver createDependencyResolver(Environment env) {
+ BuildConfigurationCollection configurations = getBuildConfigurationCollection(env);
+ return configurations == null ? null : new SkyframeDependencyResolver(env);
+ }
+
+ /**
+ * Workaround to clear all legacy data, like the action graph and the artifact factory. We need
+ * to clear them to avoid conflicts.
+ * TODO(bazel-team): Remove this workaround. [skyframe-execution]
+ */
+ void clearLegacyData() {
+ legacyDataCleaner.run();
+ }
+
+ /**
+ * Hack to invalidate actions in legacy action graph when their values are invalidated in
+ * skyframe.
+ */
+ EvaluationProgressReceiver getInvalidationReceiver() {
+ return invalidationReceiver;
+ }
+
+ /** Clear the invalidated configured targets detected during loading and analysis phases. */
+ public void clearInvalidatedConfiguredTargets() {
+ dirtyConfiguredTargets = Sets.newConcurrentHashSet();
+ anyConfiguredTargetDeleted = false;
+ }
+
+ public boolean isSomeConfiguredTargetInvalidated() {
+ return anyConfiguredTargetDeleted || !dirtyConfiguredTargets.isEmpty();
+ }
+
+ /**
+ * Called from SkyframeExecutor to see whether the graph needs to be checked for artifact
+ * conflicts. Returns true if some configured target has been evaluated since the last time the
+ * graph was checked for artifact conflicts (with that last time marked by a call to
+ * {@link #resetEvaluatedConfiguredTargetFlag()}).
+ */
+ boolean isSomeConfiguredTargetEvaluated() {
+ Preconditions.checkState(!enableAnalysis);
+ return someConfiguredTargetEvaluated;
+ }
+
+ /**
+ * Called from SkyframeExecutor after the graph is checked for artifact conflicts so that
+ * the next time {@link #isSomeConfiguredTargetEvaluated} is called, it will return true only if
+ * some configured target has been evaluated since the last check for artifact conflicts.
+ */
+ void resetEvaluatedConfiguredTargetFlag() {
+ someConfiguredTargetEvaluated = false;
+ }
+
+ /**
+ * {@link #createConfiguredTarget} will only create configured targets if this is set to true. It
+ * should be set to true before any Skyframe update call that might call into {@link
+ * #createConfiguredTarget}, and false immediately after the call. Use it to fail-fast in the case
+ * that a target is requested for analysis not during the analysis phase.
+ */
+ void enableAnalysis(boolean enable) {
+ this.enableAnalysis = enable;
+ }
+
+ private class ConfiguredTargetValueInvalidationReceiver implements EvaluationProgressReceiver {
+ @Override
+ public void invalidated(SkyValue value, InvalidationState state) {
+ if (value instanceof ConfiguredTargetValue) {
+ ConfiguredTargetValue ctValue = (ConfiguredTargetValue) value;
+ // If the value was just dirtied and not deleted, then it may not be truly invalid, since
+ // it may later get re-validated.
+ if (state == InvalidationState.DELETED) {
+ anyConfiguredTargetDeleted = true;
+ } else {
+ dirtyConfiguredTargets.add(ctValue);
+ }
+ }
+ }
+
+ @Override
+ public void enqueueing(SkyKey skyKey) {}
+
+ @Override
+ public void evaluated(SkyKey skyKey, SkyValue value, EvaluationState state) {
+ if (skyKey.functionName() == SkyFunctions.CONFIGURED_TARGET) {
+ if (state == EvaluationState.BUILT) {
+ evaluatedConfiguredTargets.add(skyKey);
+ // During multithreaded operation, this is only set to true, so no concurrency issues.
+ someConfiguredTargetEvaluated = true;
+ }
+ Preconditions.checkNotNull(value, "%s %s", skyKey, state);
+ ConfiguredTargetValue ctValue = (ConfiguredTargetValue) value;
+ dirtyConfiguredTargets.remove(ctValue);
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeDependencyResolver.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeDependencyResolver.java
new file mode 100644
index 0000000..1c7cbfa
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeDependencyResolver.java
@@ -0,0 +1,68 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.devtools.build.lib.analysis.DependencyResolver;
+import com.google.devtools.build.lib.analysis.TargetAndConfiguration;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.packages.NoSuchThingException;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.packages.TargetUtils;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.skyframe.SkyFunction.Environment;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import javax.annotation.Nullable;
+
+/**
+ * A dependency resolver for use within Skyframe. Loads packages lazily when possible.
+ */
+public final class SkyframeDependencyResolver extends DependencyResolver {
+
+ private final Environment env;
+
+ public SkyframeDependencyResolver(Environment env) {
+ this.env = env;
+ }
+
+ @Override
+ protected void invalidVisibilityReferenceHook(TargetAndConfiguration value, Label label) {
+ env.getListener().handle(
+ Event.error(TargetUtils.getLocationMaybe(value.getTarget()), String.format(
+ "Label '%s' in visibility attribute does not refer to a package group", label)));
+ }
+
+ @Override
+ protected void invalidPackageGroupReferenceHook(TargetAndConfiguration value, Label label) {
+ env.getListener().handle(
+ Event.error(TargetUtils.getLocationMaybe(value.getTarget()), String.format(
+ "label '%s' does not refer to a package group", label)));
+ }
+
+ @Nullable
+ @Override
+ protected Target getTarget(Label label) throws NoSuchThingException {
+ if (env.getValue(TargetMarkerValue.key(label)) == null) {
+ return null;
+ }
+ SkyKey key = PackageValue.key(label.getPackageIdentifier());
+ SkyValue value = env.getValue(key);
+ if (value == null) {
+ return null;
+ }
+ PackageValue packageValue = (PackageValue) value;
+ return packageValue.getPackage().getTarget(label.getName());
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java
new file mode 100644
index 0000000..29b4c23
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java
@@ -0,0 +1,1476 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.common.eventbus.EventBus;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.actions.ActionCacheChecker;
+import com.google.devtools.build.lib.actions.ActionExecutionStatusReporter;
+import com.google.devtools.build.lib.actions.ActionInputFileCache;
+import com.google.devtools.build.lib.actions.ActionLogBufferPathGenerator;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.ArtifactFactory;
+import com.google.devtools.build.lib.actions.ArtifactOwner;
+import com.google.devtools.build.lib.actions.Executor;
+import com.google.devtools.build.lib.actions.ResourceManager;
+import com.google.devtools.build.lib.actions.Root;
+import com.google.devtools.build.lib.analysis.Aspect;
+import com.google.devtools.build.lib.analysis.BlazeDirectories;
+import com.google.devtools.build.lib.analysis.BuildView.Options;
+import com.google.devtools.build.lib.analysis.ConfiguredAspectFactory;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.DependencyResolver.Dependency;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget;
+import com.google.devtools.build.lib.analysis.TopLevelArtifactContext;
+import com.google.devtools.build.lib.analysis.WorkspaceStatusAction;
+import com.google.devtools.build.lib.analysis.WorkspaceStatusAction.Factory;
+import com.google.devtools.build.lib.analysis.buildinfo.BuildInfoFactory;
+import com.google.devtools.build.lib.analysis.buildinfo.BuildInfoFactory.BuildInfoKey;
+import com.google.devtools.build.lib.analysis.config.BinTools;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.analysis.config.BuildConfigurationCollection;
+import com.google.devtools.build.lib.analysis.config.BuildConfigurationKey;
+import com.google.devtools.build.lib.analysis.config.BuildOptions;
+import com.google.devtools.build.lib.analysis.config.ConfigurationFactory;
+import com.google.devtools.build.lib.analysis.config.ConfigurationFragmentFactory;
+import com.google.devtools.build.lib.analysis.config.InvalidConfigurationException;
+import com.google.devtools.build.lib.concurrent.ThreadSafety;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadCompatible;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.events.Reporter;
+import com.google.devtools.build.lib.packages.BuildFileContainsErrorsException;
+import com.google.devtools.build.lib.packages.NoSuchPackageException;
+import com.google.devtools.build.lib.packages.NoSuchThingException;
+import com.google.devtools.build.lib.packages.Package;
+import com.google.devtools.build.lib.packages.PackageFactory;
+import com.google.devtools.build.lib.packages.PackageIdentifier;
+import com.google.devtools.build.lib.packages.Preprocessor;
+import com.google.devtools.build.lib.packages.RuleVisibility;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.pkgcache.PackageCacheOptions;
+import com.google.devtools.build.lib.pkgcache.PackageManager;
+import com.google.devtools.build.lib.pkgcache.PathPackageLocator;
+import com.google.devtools.build.lib.pkgcache.TransitivePackageLoader;
+import com.google.devtools.build.lib.skyframe.SkyframeActionExecutor.ActionCompletedReceiver;
+import com.google.devtools.build.lib.skyframe.SkyframeActionExecutor.ProgressSupplier;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.util.AbruptExitException;
+import com.google.devtools.build.lib.util.ExitCode;
+import com.google.devtools.build.lib.util.ResourceUsage;
+import com.google.devtools.build.lib.util.io.TimestampGranularityMonitor;
+import com.google.devtools.build.lib.vfs.BatchStat;
+import com.google.devtools.build.lib.vfs.ModifiedFileSet;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.lib.vfs.RootedPath;
+import com.google.devtools.build.lib.vfs.UnixGlob;
+import com.google.devtools.build.skyframe.BuildDriver;
+import com.google.devtools.build.skyframe.CycleInfo;
+import com.google.devtools.build.skyframe.CyclesReporter;
+import com.google.devtools.build.skyframe.Differencer;
+import com.google.devtools.build.skyframe.ErrorInfo;
+import com.google.devtools.build.skyframe.EvaluationProgressReceiver;
+import com.google.devtools.build.skyframe.EvaluationResult;
+import com.google.devtools.build.skyframe.Injectable;
+import com.google.devtools.build.skyframe.MemoizingEvaluator;
+import com.google.devtools.build.skyframe.MemoizingEvaluator.EvaluatorSupplier;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyFunctionName;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.io.IOException;
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.logging.Logger;
+
+import javax.annotation.Nullable;
+
+/**
+ * A helper object to support Skyframe-driven execution.
+ *
+ * <p>This object is mostly used to inject external state, such as the executor engine or
+ * some additional artifacts (workspace status and build info artifacts) into SkyFunctions
+ * for use during the build.
+ */
+public abstract class SkyframeExecutor {
+ private final EvaluatorSupplier evaluatorSupplier;
+ protected MemoizingEvaluator memoizingEvaluator;
+ private final MemoizingEvaluator.EmittedEventState emittedEventState =
+ new MemoizingEvaluator.EmittedEventState();
+ protected final Reporter reporter;
+ private final PackageFactory pkgFactory;
+ private final WorkspaceStatusAction.Factory workspaceStatusActionFactory;
+ private final BlazeDirectories directories;
+ @Nullable
+ private BatchStat batchStatter;
+
+ // TODO(bazel-team): Figure out how to handle value builders that block internally. Blocking
+ // operations may need to be handled in another (bigger?) thread pool. Also, we should detect
+ // the number of cores and use that as the thread-pool size for CPU-bound operations.
+ // I just bumped this to 200 to get reasonable execution phase performance; that may cause
+ // significant overhead for CPU-bound processes (i.e. analysis). [skyframe-analysis]
+ @VisibleForTesting
+ public static final int DEFAULT_THREAD_COUNT = 200;
+
+ // Stores Packages between reruns of the PackageFunction (because of missing dependencies,
+ // within the same evaluate() run) to avoid loading the same package twice (first time loading
+ // to find subincludes and declare value dependencies).
+ // TODO(bazel-team): remove this cache once we have skyframe-native package loading
+ // [skyframe-loading]
+ private final ConcurrentMap<PackageIdentifier, Package.LegacyBuilder> packageFunctionCache =
+ Maps.newConcurrentMap();
+ private final AtomicInteger numPackagesLoaded = new AtomicInteger(0);
+
+ protected SkyframeBuildView skyframeBuildView;
+ private EventHandler errorEventListener;
+ private ActionLogBufferPathGenerator actionLogBufferPathGenerator;
+
+ protected BuildDriver buildDriver;
+
+ // AtomicReferences are used here as mutable boxes shared with value builders.
+ private final AtomicBoolean showLoadingProgress = new AtomicBoolean();
+ protected final AtomicReference<UnixGlob.FilesystemCalls> syscalls =
+ new AtomicReference<>(UnixGlob.DEFAULT_SYSCALLS);
+ protected final AtomicReference<PathPackageLocator> pkgLocator =
+ new AtomicReference<>();
+ protected final AtomicReference<ImmutableSet<String>> deletedPackages =
+ new AtomicReference<>(ImmutableSet.<String>of());
+ private final AtomicReference<EventBus> eventBus = new AtomicReference<>();
+
+ private final ImmutableList<BuildInfoFactory> buildInfoFactories;
+ // Under normal circumstances, the artifact factory persists for the life of a Blaze server, but
+ // since it is not yet created when we create the value builders, we have to use a supplier,
+ // initialized when the build view is created.
+ private final MutableSupplier<ArtifactFactory> artifactFactory = new MutableSupplier<>();
+ // Used to give to WriteBuildInfoAction via a supplier. Relying on BuildVariableValue.BUILD_ID
+ // would be preferable, but we have no way to have the Action depend on that value directly.
+ // Having the BuildInfoFunction own the supplier is currently not possible either, because then
+ // it would be invalidated on every build, since it would depend on the build id value.
+ private MutableSupplier<UUID> buildId = new MutableSupplier<>();
+
+ protected boolean active = true;
+ private final PackageManager packageManager;
+
+ private final Preprocessor.Factory.Supplier preprocessorFactorySupplier;
+ private Preprocessor.Factory preprocessorFactory;
+
+ protected final TimestampGranularityMonitor tsgm;
+
+ private final ResourceManager resourceManager;
+
+ /** Used to lock evaluator on legacy calls to get existing values. */
+ private final Object valueLookupLock = new Object();
+ private final AtomicReference<ActionExecutionStatusReporter> statusReporterRef =
+ new AtomicReference<>();
+ private final SkyframeActionExecutor skyframeActionExecutor;
+ protected SkyframeProgressReceiver progressReceiver;
+ private final AtomicReference<CyclesReporter> cyclesReporter = new AtomicReference<>();
+
+ private final Set<Path> immutableDirectories;
+
+ private BinTools binTools = null;
+ private boolean needToInjectEmbeddedArtifacts = true;
+ private boolean needToInjectPrecomputedValuesForAnalysis = true;
+ protected int modifiedFiles;
+ private final Predicate<PathFragment> allowedMissingInputs;
+
+ private final ImmutableMap<SkyFunctionName, SkyFunction> extraSkyFunctions;
+ private final ImmutableList<PrecomputedValue.Injected> extraPrecomputedValues;
+
+ protected SkyframeIncrementalBuildMonitor incrementalBuildMonitor =
+ new SkyframeIncrementalBuildMonitor();
+
+ private MutableSupplier<ConfigurationFactory> configurationFactory = new MutableSupplier<>();
+ private MutableSupplier<Map<String, String>> clientEnv = new MutableSupplier<>();
+ private MutableSupplier<ImmutableList<ConfigurationFragmentFactory>> configurationFragments =
+ new MutableSupplier<>();
+ private MutableSupplier<Set<Package>> configurationPackages = new MutableSupplier<>();
+ private SkyKey configurationSkyKey = null;
+
+ private static final Logger LOG = Logger.getLogger(SkyframeExecutor.class.getName());
+
+ protected SkyframeExecutor(
+ Reporter reporter,
+ EvaluatorSupplier evaluatorSupplier,
+ PackageFactory pkgFactory,
+ TimestampGranularityMonitor tsgm,
+ BlazeDirectories directories,
+ Factory workspaceStatusActionFactory,
+ ImmutableList<BuildInfoFactory> buildInfoFactories,
+ Set<Path> immutableDirectories,
+ Predicate<PathFragment> allowedMissingInputs,
+ Preprocessor.Factory.Supplier preprocessorFactorySupplier,
+ ImmutableMap<SkyFunctionName, SkyFunction> extraSkyFunctions,
+ ImmutableList<PrecomputedValue.Injected> extraPrecomputedValues) {
+ // Strictly speaking, these arguments are not required for initialization, but all current
+ // callsites have them at hand, so we might as well set them during construction.
+ this.reporter = Preconditions.checkNotNull(reporter);
+ this.evaluatorSupplier = evaluatorSupplier;
+ this.pkgFactory = pkgFactory;
+ this.pkgFactory.setSyscalls(syscalls);
+ this.tsgm = tsgm;
+ this.workspaceStatusActionFactory = workspaceStatusActionFactory;
+ this.packageManager = new SkyframePackageManager(
+ new SkyframePackageLoader(), new SkyframeTransitivePackageLoader(),
+ new SkyframeTargetPatternEvaluator(this), syscalls, cyclesReporter, pkgLocator,
+ numPackagesLoaded, this);
+ this.errorEventListener = this.reporter;
+ this.resourceManager = ResourceManager.instance();
+ this.skyframeActionExecutor = new SkyframeActionExecutor(reporter, resourceManager, eventBus,
+ statusReporterRef);
+ this.directories = Preconditions.checkNotNull(directories);
+ this.buildInfoFactories = buildInfoFactories;
+ this.immutableDirectories = immutableDirectories;
+ this.allowedMissingInputs = allowedMissingInputs;
+ this.preprocessorFactorySupplier = preprocessorFactorySupplier;
+ this.extraSkyFunctions = extraSkyFunctions;
+ this.extraPrecomputedValues = extraPrecomputedValues;
+ }
+
+ private ImmutableMap<SkyFunctionName, SkyFunction> skyFunctions(
+ Root buildDataDirectory,
+ PackageFactory pkgFactory,
+ Predicate<PathFragment> allowedMissingInputs) {
+ ExternalFilesHelper externalFilesHelper = new ExternalFilesHelper(pkgLocator,
+ immutableDirectories);
+ // We use an immutable map builder for the nice side effect that it throws if a duplicate key
+ // is inserted.
+ ImmutableMap.Builder<SkyFunctionName, SkyFunction> map = ImmutableMap.builder();
+ map.put(SkyFunctions.PRECOMPUTED, new PrecomputedFunction());
+ map.put(SkyFunctions.FILE_STATE, new FileStateFunction(tsgm, externalFilesHelper));
+ map.put(SkyFunctions.DIRECTORY_LISTING_STATE,
+ new DirectoryListingStateFunction(externalFilesHelper));
+ map.put(SkyFunctions.FILE_SYMLINK_CYCLE_UNIQUENESS,
+ new FileSymlinkCycleUniquenessFunction());
+ map.put(SkyFunctions.FILE, new FileFunction(pkgLocator, externalFilesHelper));
+ map.put(SkyFunctions.DIRECTORY_LISTING, new DirectoryListingFunction());
+ map.put(SkyFunctions.PACKAGE_LOOKUP, new PackageLookupFunction(deletedPackages));
+ map.put(SkyFunctions.CONTAINING_PACKAGE_LOOKUP, new ContainingPackageLookupFunction());
+ map.put(SkyFunctions.AST_FILE_LOOKUP, new ASTFileLookupFunction(
+ pkgLocator, packageManager, pkgFactory.getRuleClassProvider()));
+ map.put(SkyFunctions.SKYLARK_IMPORTS_LOOKUP, new SkylarkImportLookupFunction(
+ pkgFactory.getRuleClassProvider(), pkgFactory));
+ map.put(SkyFunctions.GLOB, new GlobFunction());
+ map.put(SkyFunctions.TARGET_PATTERN, new TargetPatternFunction(pkgLocator));
+ map.put(SkyFunctions.RECURSIVE_PKG, new RecursivePkgFunction());
+ map.put(SkyFunctions.PACKAGE, new PackageFunction(
+ reporter, pkgFactory, packageManager, showLoadingProgress, packageFunctionCache,
+ eventBus, numPackagesLoaded));
+ map.put(SkyFunctions.TARGET_MARKER, new TargetMarkerFunction());
+ map.put(SkyFunctions.TRANSITIVE_TARGET, new TransitiveTargetFunction());
+ map.put(SkyFunctions.CONFIGURED_TARGET,
+ new ConfiguredTargetFunction(new BuildViewProvider()));
+ map.put(SkyFunctions.ASPECT, new AspectFunction(new BuildViewProvider()));
+ map.put(SkyFunctions.POST_CONFIGURED_TARGET,
+ new PostConfiguredTargetFunction(new BuildViewProvider()));
+ map.put(SkyFunctions.CONFIGURATION_COLLECTION, new ConfigurationCollectionFunction(
+ configurationFactory, clientEnv, configurationPackages));
+ map.put(SkyFunctions.CONFIGURATION_FRAGMENT, new ConfigurationFragmentFunction(
+ configurationFragments, configurationPackages));
+ map.put(SkyFunctions.WORKSPACE_FILE, new WorkspaceFileFunction(pkgFactory));
+ map.put(SkyFunctions.TARGET_COMPLETION, new TargetCompletionFunction(eventBus));
+ map.put(SkyFunctions.TEST_COMPLETION, new TestCompletionFunction());
+ map.put(SkyFunctions.ARTIFACT, new ArtifactFunction(allowedMissingInputs));
+ map.put(SkyFunctions.BUILD_INFO_COLLECTION, new BuildInfoCollectionFunction(artifactFactory,
+ buildDataDirectory));
+ map.put(SkyFunctions.BUILD_INFO, new WorkspaceStatusFunction());
+ map.put(SkyFunctions.COVERAGE_REPORT, new CoverageReportFunction());
+ map.put(SkyFunctions.ACTION_EXECUTION,
+ new ActionExecutionFunction(skyframeActionExecutor, tsgm));
+ map.put(SkyFunctions.RECURSIVE_FILESYSTEM_TRAVERSAL,
+ new RecursiveFilesystemTraversalFunction());
+ map.put(SkyFunctions.FILESET_ENTRY, new FilesetEntryFunction());
+ map.putAll(extraSkyFunctions);
+ return map.build();
+ }
+
+ @ThreadCompatible
+ public void setActive(boolean active) {
+ this.active = active;
+ }
+
+ protected void checkActive() {
+ Preconditions.checkState(active);
+ }
+
+ public void setFileCache(ActionInputFileCache fileCache) {
+ this.skyframeActionExecutor.setFileCache(fileCache);
+ }
+
+ public void dump(boolean summarize, PrintStream out) {
+ memoizingEvaluator.dump(summarize, out);
+ }
+
+ public abstract void dumpPackages(PrintStream out);
+
+ public void setBatchStatter(@Nullable BatchStat batchStatter) {
+ this.batchStatter = batchStatter;
+ }
+
+ /**
+ * Notify listeners about changed files, and release any associated memory afterwards.
+ */
+ public void drainChangedFiles() {
+ incrementalBuildMonitor.alertListeners(getEventBus());
+ incrementalBuildMonitor = null;
+ }
+
+ @VisibleForTesting
+ public BuildDriver getDriverForTesting() {
+ return buildDriver;
+ }
+
+ /**
+ * This method exists only to allow a module to make a top-level Skyframe call during the
+ * transition to making it fully Skyframe-compatible. Do not add additional callers!
+ */
+ public <E extends Exception> SkyValue evaluateSkyKeyForCodeMigration(final SkyKey key,
+ final Class<E> clazz) throws E {
+ try {
+ return callUninterruptibly(new Callable<SkyValue>() {
+ @Override
+ public SkyValue call() throws E, InterruptedException {
+ synchronized (valueLookupLock) {
+ // We evaluate in keepGoing mode because in the case that the graph does not store its
+ // edges, nokeepGoing builds are not allowed, whereas keepGoing builds are always
+ // permitted.
+ EvaluationResult<ActionLookupValue> result = buildDriver.evaluate(
+ ImmutableList.of(key), true, ResourceUsage.getAvailableProcessors(),
+ errorEventListener);
+ if (!result.hasError()) {
+ return Preconditions.checkNotNull(result.get(key), "%s %s", result, key);
+ }
+ ErrorInfo errorInfo = Preconditions.checkNotNull(result.getError(key),
+ "%s %s", key, result);
+ Throwables.propagateIfPossible(errorInfo.getException(), clazz);
+ if (errorInfo.getException() != null) {
+ throw new IllegalStateException(errorInfo.getException());
+ }
+ throw new IllegalStateException(errorInfo.toString());
+ }
+ }
+ });
+ } catch (Exception e) {
+ Throwables.propagateIfPossible(e, clazz);
+ throw new IllegalStateException(e);
+ }
+ }
+
+ class BuildViewProvider {
+ /**
+ * Returns the current {@link SkyframeBuildView} instance.
+ */
+ SkyframeBuildView getSkyframeBuildView() {
+ return skyframeBuildView;
+ }
+ }
+
+ /**
+ * Must be called before the {@link SkyframeExecutor} can be used (should only be called in
+ * factory methods and as an implementation detail of {@link #resetEvaluator}).
+ */
+ protected void init() {
+ progressReceiver = new SkyframeProgressReceiver();
+ Map<SkyFunctionName, SkyFunction> skyFunctions = skyFunctions(
+ directories.getBuildDataDirectory(), pkgFactory, allowedMissingInputs);
+ memoizingEvaluator = evaluatorSupplier.create(
+ skyFunctions, evaluatorDiffer(), progressReceiver, emittedEventState,
+ hasIncrementalState());
+ buildDriver = newBuildDriver();
+ }
+
+ /**
+ * Reinitializes the Skyframe evaluator, dropping all previously computed values.
+ *
+ * <p>Be careful with this method as it also deletes all injected values. You need to make sure
+ * that any necessary precomputed values are reinjected before the next build. Constants can be
+ * put in {@link #reinjectConstantValuesLazily}.
+ */
+ public void resetEvaluator() {
+ init();
+ emittedEventState.clear();
+ if (skyframeBuildView != null) {
+ skyframeBuildView.clearLegacyData();
+ }
+ reinjectConstantValuesLazily();
+ }
+
+ protected abstract Differencer evaluatorDiffer();
+
+ protected abstract BuildDriver newBuildDriver();
+
+ /**
+ * Values whose values are known at startup and guaranteed constant are still wiped from the
+ * evaluator when we create a new one, so they must be re-injected each time we create a new
+ * evaluator.
+ */
+ private void reinjectConstantValuesLazily() {
+ needToInjectEmbeddedArtifacts = true;
+ needToInjectPrecomputedValuesForAnalysis = true;
+ }
+
+ /**
+ * Deletes all ConfiguredTarget values from the Skyframe cache. This is done to save memory (e.g.
+ * on a configuration change); since the configuration is part of the key, these key/value pairs
+ * will be sitting around doing nothing until the configuration changes back to the previous
+ * value.
+ *
+ * <p>The next evaluation will delete all invalid values.
+ */
+ public abstract void dropConfiguredTargets();
+
+ /**
+ * Removes ConfigurationFragmentValuess and ConfigurationCollectionValues from the cache.
+ */
+ @VisibleForTesting
+ public void invalidateConfigurationCollection() {
+ invalidate(SkyFunctionName.functionIsIn(ImmutableSet.of(SkyFunctions.CONFIGURATION_FRAGMENT,
+ SkyFunctions.CONFIGURATION_COLLECTION)));
+ }
+
+ /**
+ * Decides if graph edges should be stored for this build. If not, re-creates the graph to not
+ * store graph edges. Necessary conditions to not store graph edges are:
+ * (1) batch (since incremental builds are not possible);
+ * (2) skyframe build (since otherwise the memory savings are too slight to bother);
+ * (3) keep-going (since otherwise bubbling errors up may require edges of done nodes);
+ * (4) discard_analysis_cache (since otherwise user isn't concerned about saving memory this way).
+ */
+ public void decideKeepIncrementalState(boolean batch, Options viewOptions) {
+ // Assume incrementality.
+ }
+
+ public boolean hasIncrementalState() {
+ return true;
+ }
+
+ @VisibleForTesting
+ protected abstract Injectable injectable();
+
+ /**
+ * Saves memory by clearing analysis objects from Skyframe. If using legacy execution, actually
+ * deletes the relevant values. If using Skyframe execution, clears their data without deleting
+ * them (they will be deleted on the next build).
+ */
+ public abstract void clearAnalysisCache(Collection<ConfiguredTarget> topLevelTargets);
+
+ /**
+ * Injects the contents of the computed tools/defaults package.
+ */
+ @VisibleForTesting
+ public void setupDefaultPackage(String defaultsPackageContents) {
+ PrecomputedValue.DEFAULTS_PACKAGE_CONTENTS.set(injectable(), defaultsPackageContents);
+ }
+
+ /**
+ * Injects the top-level artifact options.
+ */
+ public void injectTopLevelContext(TopLevelArtifactContext options) {
+ PrecomputedValue.TOP_LEVEL_CONTEXT.set(injectable(), options);
+ }
+
+ public void injectWorkspaceStatusData() {
+ PrecomputedValue.WORKSPACE_STATUS_KEY.set(injectable(),
+ workspaceStatusActionFactory.createWorkspaceStatusAction(
+ artifactFactory.get(), WorkspaceStatusValue.ARTIFACT_OWNER, buildId));
+ }
+
+ public void injectCoverageReportData(Action action) {
+ PrecomputedValue.COVERAGE_REPORT_KEY.set(injectable(), action);
+ }
+
+ /**
+ * Sets the default visibility.
+ */
+ private void setDefaultVisibility(RuleVisibility defaultVisibility) {
+ PrecomputedValue.DEFAULT_VISIBILITY.set(injectable(), defaultVisibility);
+ }
+
+ private void maybeInjectPrecomputedValuesForAnalysis() {
+ if (needToInjectPrecomputedValuesForAnalysis) {
+ injectBuildInfoFactories();
+ injectExtraPrecomputedValues();
+ needToInjectPrecomputedValuesForAnalysis = false;
+ }
+ }
+
+ private void injectExtraPrecomputedValues() {
+ for (PrecomputedValue.Injected injected : extraPrecomputedValues) {
+ injected.inject(injectable());
+ }
+ }
+
+ /**
+ * Injects the build info factory map that will be used when constructing build info
+ * actions/artifacts. Unchanged across the life of the Blaze server, although it must be injected
+ * each time the evaluator is created.
+ */
+ private void injectBuildInfoFactories() {
+ ImmutableMap.Builder<BuildInfoKey, BuildInfoFactory> factoryMapBuilder =
+ ImmutableMap.builder();
+ for (BuildInfoFactory factory : buildInfoFactories) {
+ factoryMapBuilder.put(factory.getKey(), factory);
+ }
+ PrecomputedValue.BUILD_INFO_FACTORIES.set(injectable(), factoryMapBuilder.build());
+ }
+
+ private void setShowLoadingProgress(boolean showLoadingProgressValue) {
+ showLoadingProgress.set(showLoadingProgressValue);
+ }
+
+ @VisibleForTesting
+ public void setCommandId(UUID commandId) {
+ PrecomputedValue.BUILD_ID.set(injectable(), commandId);
+ buildId.set(commandId);
+ }
+
+ /** Returns the build-info.txt and build-changelist.txt artifacts. */
+ public Collection<Artifact> getWorkspaceStatusArtifacts() throws InterruptedException {
+ // Should already be present, unless the user didn't request any targets for analysis.
+ EvaluationResult<WorkspaceStatusValue> result = buildDriver.evaluate(
+ ImmutableList.of(WorkspaceStatusValue.SKY_KEY), /*keepGoing=*/true, /*numThreads=*/1,
+ reporter);
+ WorkspaceStatusValue value =
+ Preconditions.checkNotNull(result.get(WorkspaceStatusValue.SKY_KEY));
+ return ImmutableList.of(value.getStableArtifact(), value.getVolatileArtifact());
+ }
+
+ // TODO(bazel-team): Make this take a PackageIdentifier.
+ public Map<PathFragment, Root> getArtifactRoots(Iterable<PathFragment> execPaths) {
+ final List<SkyKey> packageKeys = new ArrayList<>();
+ for (PathFragment execPath : execPaths) {
+ Preconditions.checkArgument(!execPath.isAbsolute(), execPath);
+ packageKeys.add(ContainingPackageLookupValue.key(
+ PackageIdentifier.createInDefaultRepo(execPath)));
+ }
+
+ EvaluationResult<ContainingPackageLookupValue> result;
+ try {
+ result = callUninterruptibly(new Callable<EvaluationResult<ContainingPackageLookupValue>>() {
+ @Override
+ public EvaluationResult<ContainingPackageLookupValue> call() throws InterruptedException {
+ return buildDriver.evaluate(packageKeys, /*keepGoing=*/true, /*numThreads=*/1, reporter);
+ }
+ });
+ } catch (Exception e) {
+ throw new IllegalStateException(e); // Should never happen.
+ }
+
+ Map<PathFragment, Root> roots = new HashMap<>();
+ for (PathFragment execPath : execPaths) {
+ ContainingPackageLookupValue value = result.get(ContainingPackageLookupValue.key(
+ PackageIdentifier.createInDefaultRepo(execPath)));
+ if (value.hasContainingPackage()) {
+ roots.put(execPath, Root.asSourceRoot(value.getContainingPackageRoot()));
+ } else {
+ roots.put(execPath, null);
+ }
+ }
+ return roots;
+ }
+
+ @VisibleForTesting
+ public WorkspaceStatusAction getLastWorkspaceStatusActionForTesting() {
+ PrecomputedValue value = (PrecomputedValue) buildDriver.getGraphForTesting()
+ .getExistingValueForTesting(PrecomputedValue.WORKSPACE_STATUS_KEY.getKeyForTesting());
+ return (WorkspaceStatusAction) value.get();
+ }
+
+ /**
+ * Informs user about number of modified files (source and output files).
+ */
+ // Note, that number of modified files in some cases can be bigger than actual number of
+ // modified files for targets in current request. Skyframe may check for modification all files
+ // from previous requests.
+ protected void informAboutNumberOfModifiedFiles() {
+ LOG.info(String.format("Found %d modified files from last build", modifiedFiles));
+ }
+
+ public Reporter getReporter() {
+ return reporter;
+ }
+
+ public EventBus getEventBus() {
+ return eventBus.get();
+ }
+
+ /**
+ * The map from package names to the package root where each package was found; this is used to
+ * set up the symlink tree.
+ */
+ public ImmutableMap<PackageIdentifier, Path> getPackageRoots() {
+ // Make a map of the package names to their root paths.
+ ImmutableMap.Builder<PackageIdentifier, Path> packageRoots = ImmutableMap.builder();
+ for (Package pkg : configurationPackages.get()) {
+ packageRoots.put(pkg.getPackageIdentifier(), pkg.getSourceRoot());
+ }
+ return packageRoots.build();
+ }
+
+ @VisibleForTesting
+ ImmutableList<Path> getPathEntries() {
+ return pkgLocator.get().getPathEntries();
+ }
+
+ protected abstract void invalidate(Predicate<SkyKey> pred);
+
+ protected static Iterable<SkyKey> getSkyKeysPotentiallyAffected(
+ Iterable<PathFragment> modifiedSourceFiles, final Path pathEntry) {
+ // TODO(bazel-team): change ModifiedFileSet to work with RootedPaths instead of PathFragments.
+ Iterable<SkyKey> fileStateSkyKeys = Iterables.transform(modifiedSourceFiles,
+ new Function<PathFragment, SkyKey>() {
+ @Override
+ public SkyKey apply(PathFragment pathFragment) {
+ Preconditions.checkState(!pathFragment.isAbsolute(),
+ "found absolute PathFragment: %s", pathFragment);
+ return FileStateValue.key(RootedPath.toRootedPath(pathEntry, pathFragment));
+ }
+ });
+ // TODO(bazel-team): Strictly speaking, we only need to invalidate directory values when a file
+ // has been created or deleted, not when it has been modified. Unfortunately we
+ // do not have that information here, although fancy filesystems could provide it with a
+ // hypothetically modified DiffAwareness interface.
+ // TODO(bazel-team): Even if we don't have that information, we could avoid invalidating
+ // directories when the state of a file does not change by statting them and comparing
+ // the new filetype (nonexistent/file/symlink/directory) with the old one.
+ Iterable<SkyKey> dirListingStateSkyKeys = Iterables.transform(
+ modifiedSourceFiles,
+ new Function<PathFragment, SkyKey>() {
+ @Override
+ public SkyKey apply(PathFragment pathFragment) {
+ Preconditions.checkState(!pathFragment.isAbsolute(),
+ "found absolute PathFragment: %s", pathFragment);
+ return DirectoryListingStateValue.key(RootedPath.toRootedPath(pathEntry,
+ pathFragment.getParentDirectory()));
+ }
+ });
+ return Iterables.concat(fileStateSkyKeys, dirListingStateSkyKeys);
+ }
+
+ protected static SkyKey createFileStateKey(RootedPath rootedPath) {
+ return FileStateValue.key(rootedPath);
+ }
+
+ protected static SkyKey createDirectoryListingStateKey(RootedPath rootedPath) {
+ return DirectoryListingStateValue.key(rootedPath);
+ }
+
+ /**
+ * Creates a FileValue pointing of type directory. No matter that the rootedPath points to a
+ * symlink.
+ *
+ * <p> Use it with caution as it would prevent invalidation when the destination file in the
+ * symlink changes.
+ */
+ protected static FileValue createFileDirValue(RootedPath rootedPath) {
+ return FileValue.value(rootedPath, FileStateValue.DIRECTORY_FILE_STATE_NODE,
+ rootedPath, FileStateValue.DIRECTORY_FILE_STATE_NODE);
+ }
+
+ /**
+ * Sets the packages that should be treated as deleted and ignored.
+ */
+ @VisibleForTesting // productionVisibility = Visibility.PRIVATE
+ public abstract void setDeletedPackages(Iterable<String> pkgs);
+
+ /**
+ * Prepares the evaluator for loading.
+ *
+ * <p>MUST be run before every incremental build.
+ */
+ @VisibleForTesting // productionVisibility = Visibility.PRIVATE
+ public void preparePackageLoading(PathPackageLocator pkgLocator, RuleVisibility defaultVisibility,
+ boolean showLoadingProgress,
+ String defaultsPackageContents, UUID commandId) {
+ Preconditions.checkNotNull(pkgLocator);
+ setActive(true);
+
+ maybeInjectPrecomputedValuesForAnalysis();
+ setCommandId(commandId);
+ setShowLoadingProgress(showLoadingProgress);
+ setDefaultVisibility(defaultVisibility);
+ setupDefaultPackage(defaultsPackageContents);
+ setPackageLocator(pkgLocator);
+
+ syscalls.set(new PerBuildSyscallCache());
+ checkPreprocessorFactory();
+ emittedEventState.clear();
+
+ // If the PackageFunction was interrupted, there may be stale entries here.
+ packageFunctionCache.clear();
+ numPackagesLoaded.set(0);
+
+ // Reset the stateful SkyframeCycleReporter, which contains cycles from last run.
+ cyclesReporter.set(createCyclesReporter());
+ }
+
+ @SuppressWarnings("unchecked")
+ private void setPackageLocator(PathPackageLocator pkgLocator) {
+ PathPackageLocator oldLocator = this.pkgLocator.getAndSet(pkgLocator);
+ PrecomputedValue.PATH_PACKAGE_LOCATOR.set(injectable(), pkgLocator);
+
+ if (!pkgLocator.equals(oldLocator)) {
+ // The package path is read not only by SkyFunctions but also by some other code paths.
+ // We need to take additional steps to keep the corresponding data structures in sync.
+ // (Some of the additional steps are carried out by ConfiguredTargetValueInvalidationListener,
+ // and some by BuildView#buildHasIncompatiblePackageRoots and #updateSkyframe.)
+ onNewPackageLocator(oldLocator, pkgLocator);
+ }
+ }
+
+ protected abstract void onNewPackageLocator(PathPackageLocator oldLocator,
+ PathPackageLocator pkgLocator);
+
+ private void checkPreprocessorFactory() {
+ if (preprocessorFactory == null) {
+ Preprocessor.Factory newPreprocessorFactory = preprocessorFactorySupplier.getFactory(
+ packageManager);
+ pkgFactory.setPreprocessorFactory(newPreprocessorFactory);
+ preprocessorFactory = newPreprocessorFactory;
+ } else if (!preprocessorFactory.isStillValid()) {
+ Preprocessor.Factory newPreprocessorFactory = preprocessorFactorySupplier.getFactory(
+ packageManager);
+ invalidate(SkyFunctionName.functionIs(SkyFunctions.PACKAGE));
+ pkgFactory.setPreprocessorFactory(newPreprocessorFactory);
+ preprocessorFactory = newPreprocessorFactory;
+ }
+ }
+
+ /**
+ * Specifies the current {@link SkyframeBuildView} instance. This should only be set once over the
+ * lifetime of the Blaze server, except in tests.
+ */
+ public void setSkyframeBuildView(SkyframeBuildView skyframeBuildView) {
+ this.skyframeBuildView = skyframeBuildView;
+ setConfigurationSkyKey(configurationSkyKey);
+ this.artifactFactory.set(skyframeBuildView.getArtifactFactory());
+ if (skyframeBuildView.getWarningListener() != null) {
+ setErrorEventListener(skyframeBuildView.getWarningListener());
+ }
+ }
+
+ /**
+ * Sets the eventBus to use for posting events.
+ */
+ public void setEventBus(EventBus eventBus) {
+ this.eventBus.set(eventBus);
+ }
+
+ /**
+ * Sets the eventHandler to use for reporting errors.
+ */
+ public void setErrorEventListener(EventHandler eventHandler) {
+ this.errorEventListener = eventHandler;
+ }
+
+ /**
+ * Sets the path for action log buffers.
+ */
+ public void setActionOutputRoot(Path actionOutputRoot) {
+ Preconditions.checkNotNull(actionOutputRoot);
+ this.actionLogBufferPathGenerator = new ActionLogBufferPathGenerator(actionOutputRoot);
+ this.skyframeActionExecutor.setActionLogBufferPathGenerator(actionLogBufferPathGenerator);
+ }
+
+ private void setConfigurationSkyKey(SkyKey skyKey) {
+ this.configurationSkyKey = skyKey;
+ if (skyframeBuildView != null) {
+ skyframeBuildView.setConfigurationSkyKey(skyKey);
+ }
+ }
+
+ @VisibleForTesting
+ public void setConfigurationDataForTesting(BuildOptions options,
+ BlazeDirectories directories, ConfigurationFactory configurationFactory) {
+ SkyKey skyKey = ConfigurationCollectionValue.key(options, ImmutableSet.<String>of());
+ setConfigurationSkyKey(skyKey);
+ PrecomputedValue.BLAZE_DIRECTORIES.set(injectable(), directories);
+ this.configurationFactory.set(configurationFactory);
+ this.configurationFragments.set(ImmutableList.copyOf(configurationFactory.getFactories()));
+ this.configurationPackages.set(Sets.<Package>newConcurrentHashSet());
+ }
+
+ @VisibleForTesting
+ public BuildConfigurationCollection createConfigurations(
+ ConfigurationFactory configurationFactory, BuildConfigurationKey configurationKey)
+ throws InvalidConfigurationException, InterruptedException {
+ return createConfigurations(false, configurationFactory, configurationKey);
+ }
+
+ /**
+ * Asks the Skyframe evaluator to build the value for BuildConfigurationCollection and
+ * returns result. Also invalidates {@link PrecomputedValue#TEST_ENVIRONMENT_VARIABLES} and
+ * {@link PrecomputedValue#BLAZE_DIRECTORIES} if they have changed.
+ */
+ public BuildConfigurationCollection createConfigurations(boolean keepGoing,
+ ConfigurationFactory configurationFactory, BuildConfigurationKey configurationKey)
+ throws InvalidConfigurationException, InterruptedException {
+
+ this.configurationPackages.set(Sets.<Package>newConcurrentHashSet());
+ this.clientEnv.set(configurationKey.getClientEnv());
+ this.configurationFactory.set(configurationFactory);
+ this.configurationFragments.set(ImmutableList.copyOf(configurationFactory.getFactories()));
+ BuildOptions buildOptions = configurationKey.getBuildOptions();
+ Map<String, String> testEnv = BuildConfiguration.getTestEnv(
+ buildOptions.get(BuildConfiguration.Options.class).testEnvironment,
+ configurationKey.getClientEnv());
+ // TODO(bazel-team): find a way to use only BuildConfigurationKey instead of
+ // TestEnvironmentVariables and BlazeDirectories. There is a problem only with
+ // TestEnvironmentVariables because BuildConfigurationKey stores client environment variables
+ // and we don't want to rebuild everything when any variable changes.
+ PrecomputedValue.TEST_ENVIRONMENT_VARIABLES.set(injectable(), testEnv);
+ PrecomputedValue.BLAZE_DIRECTORIES.set(injectable(), configurationKey.getDirectories());
+
+ SkyKey skyKey = ConfigurationCollectionValue.key(configurationKey.getBuildOptions(),
+ configurationKey.getMultiCpu());
+ setConfigurationSkyKey(skyKey);
+ EvaluationResult<ConfigurationCollectionValue> result = buildDriver.evaluate(
+ Arrays.asList(skyKey), keepGoing, DEFAULT_THREAD_COUNT, errorEventListener);
+ if (result.hasError()) {
+ Throwable e = result.getError(skyKey).getException();
+ // Wrap loading failed exceptions
+ if (e instanceof NoSuchThingException) {
+ e = new InvalidConfigurationException(e);
+ }
+ Throwables.propagateIfInstanceOf(e, InvalidConfigurationException.class);
+ throw new IllegalStateException(
+ "Unknown error during ConfigurationCollectionValue evaluation", e);
+ }
+ Preconditions.checkState(result.values().size() == 1,
+ "Result of evaluate() must contain exactly one value %s", result);
+ ConfigurationCollectionValue configurationValue =
+ Iterables.getOnlyElement(result.values());
+ this.configurationPackages.set(
+ Sets.newConcurrentHashSet(configurationValue.getConfigurationPackages()));
+ return configurationValue.getConfigurationCollection();
+ }
+
+ private Iterable<ActionLookupValue> getActionLookupValues() {
+ // This filter keeps subclasses of ActionLookupValue.
+ return Iterables.filter(memoizingEvaluator.getDoneValues().values(), ActionLookupValue.class);
+ }
+
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ Map<SkyKey, ActionLookupValue> getActionLookupValueMap() {
+ return (Map) Maps.filterValues(memoizingEvaluator.getDoneValues(),
+ Predicates.instanceOf(ActionLookupValue.class));
+ }
+
+ /**
+ * Checks the actions in Skyframe for conflicts between their output artifacts. Delegates to
+ * {@link SkyframeActionExecutor#findAndStoreArtifactConflicts} to do the work, since any
+ * conflicts found will only be reported during execution.
+ */
+ ImmutableMap<Action, SkyframeActionExecutor.ConflictException> findArtifactConflicts()
+ throws InterruptedException {
+ if (skyframeBuildView.isSomeConfiguredTargetEvaluated()
+ || skyframeBuildView.isSomeConfiguredTargetInvalidated()) {
+ // This operation is somewhat expensive, so we only do it if the graph might have changed in
+ // some way -- either we analyzed a new target or we invalidated an old one.
+ skyframeActionExecutor.findAndStoreArtifactConflicts(getActionLookupValues());
+ skyframeBuildView.resetEvaluatedConfiguredTargetFlag();
+ // The invalidated configured targets flag will be reset later in the evaluate() call.
+ }
+ return skyframeActionExecutor.badActions();
+ }
+
+ /**
+ * Asks the Skyframe evaluator to build the given artifacts and targets, and to test the
+ * given test targets.
+ */
+ public EvaluationResult<?> buildArtifacts(
+ Executor executor,
+ Set<Artifact> artifactsToBuild,
+ Collection<ConfiguredTarget> targetsToBuild,
+ Collection<ConfiguredTarget> targetsToTest,
+ boolean exclusiveTesting,
+ boolean keepGoing,
+ boolean explain,
+ int numJobs,
+ ActionCacheChecker actionCacheChecker,
+ @Nullable EvaluationProgressReceiver executionProgressReceiver) throws InterruptedException {
+ checkActive();
+ Preconditions.checkState(actionLogBufferPathGenerator != null);
+
+ skyframeActionExecutor.prepareForExecution(executor, keepGoing, explain, actionCacheChecker);
+
+ resourceManager.resetResourceUsage();
+ try {
+ progressReceiver.executionProgressReceiver = executionProgressReceiver;
+ Iterable<SkyKey> artifactKeys = ArtifactValue.mandatoryKeys(artifactsToBuild);
+ Iterable<SkyKey> targetKeys = TargetCompletionValue.keys(targetsToBuild);
+ Iterable<SkyKey> testKeys = TestCompletionValue.keys(targetsToTest, exclusiveTesting);
+ return buildDriver.evaluate(Iterables.concat(artifactKeys, targetKeys, testKeys), keepGoing,
+ numJobs, errorEventListener);
+ } finally {
+ progressReceiver.executionProgressReceiver = null;
+ // Also releases thread locks.
+ resourceManager.resetResourceUsage();
+ skyframeActionExecutor.executionOver();
+ }
+ }
+
+ @VisibleForTesting
+ public void prepareBuildingForTestingOnly(Executor executor, boolean keepGoing, boolean explain,
+ ActionCacheChecker checker) {
+ skyframeActionExecutor.prepareForExecution(executor, keepGoing, explain, checker);
+ }
+
+ EvaluationResult<TargetPatternValue> targetPatterns(Iterable<SkyKey> patternSkyKeys,
+ boolean keepGoing, EventHandler eventHandler) throws InterruptedException {
+ checkActive();
+ return buildDriver.evaluate(patternSkyKeys, keepGoing, DEFAULT_THREAD_COUNT,
+ eventHandler);
+ }
+
+ /**
+ * Returns the {@link ConfiguredTarget}s corresponding to the given keys.
+ *
+ * <p>For use for legacy support from {@code BuildView} only.
+ *
+ * <p>If a requested configured target is in error, the corresponding value is omitted from the
+ * returned list.
+ */
+ @ThreadSafety.ThreadSafe
+ public ImmutableList<ConfiguredTarget> getConfiguredTargets(Iterable<Dependency> keys) {
+ checkActive();
+ if (skyframeBuildView == null) {
+ // If build view has not yet been initialized, no configured targets can have been created.
+ // This is most likely to happen after a failed loading phase.
+ return ImmutableList.of();
+ }
+ final List<SkyKey> skyKeys = new ArrayList<>();
+ for (Dependency key : keys) {
+ skyKeys.add(ConfiguredTargetValue.key(key.getLabel(), key.getConfiguration()));
+ for (Class<? extends ConfiguredAspectFactory> aspect : key.getAspects()) {
+ skyKeys.add(AspectValue.key(key.getLabel(), key.getConfiguration(), aspect));
+ }
+ }
+
+ EvaluationResult<SkyValue> result;
+ try {
+ result = callUninterruptibly(new Callable<EvaluationResult<SkyValue>>() {
+ @Override
+ public EvaluationResult<SkyValue> call() throws Exception {
+ synchronized (valueLookupLock) {
+ try {
+ skyframeBuildView.enableAnalysis(true);
+ return buildDriver.evaluate(skyKeys, false, DEFAULT_THREAD_COUNT,
+ errorEventListener);
+ } finally {
+ skyframeBuildView.enableAnalysis(false);
+ }
+ }
+ }
+ });
+ } catch (Exception e) {
+ throw new IllegalStateException(e); // Should never happen.
+ }
+
+ ImmutableList.Builder<ConfiguredTarget> cts = ImmutableList.builder();
+
+ DependentNodeLoop:
+ for (Dependency key : keys) {
+ SkyKey configuredTargetKey = ConfiguredTargetValue.key(
+ key.getLabel(), key.getConfiguration());
+ if (result.get(configuredTargetKey) == null) {
+ continue;
+ }
+
+ ConfiguredTarget configuredTarget =
+ ((ConfiguredTargetValue) result.get(configuredTargetKey)).getConfiguredTarget();
+ List<Aspect> aspects = new ArrayList<>();
+
+ for (Class<? extends ConfiguredAspectFactory> aspect : key.getAspects()) {
+ SkyKey aspectKey = AspectValue.key(key.getLabel(), key.getConfiguration(), aspect);
+ if (result.get(aspectKey) == null) {
+ continue DependentNodeLoop;
+ }
+
+ aspects.add(((AspectValue) result.get(aspectKey)).get());
+ }
+
+ cts.add(RuleConfiguredTarget.mergeAspects(configuredTarget, aspects));
+ }
+
+ return cts.build();
+ }
+
+ /**
+ * Returns a particular configured target.
+ *
+ * <p>Used only for testing.
+ */
+ @VisibleForTesting
+ @Nullable
+ public ConfiguredTarget getConfiguredTargetForTesting(
+ Label label, BuildConfiguration configuration) {
+ if (memoizingEvaluator.getExistingValueForTesting(
+ PrecomputedValue.WORKSPACE_STATUS_KEY.getKeyForTesting()) == null) {
+ injectWorkspaceStatusData();
+ }
+ return Iterables.getFirst(getConfiguredTargets(ImmutableList.of(
+ new Dependency(label, configuration))), null);
+ }
+
+ /**
+ * Invalidates Skyframe values corresponding to the given set of modified files under the given
+ * path entry.
+ *
+ * <p>May throw an {@link InterruptedException}, which means that no values have been invalidated.
+ */
+ @VisibleForTesting
+ public abstract void invalidateFilesUnderPathForTesting(ModifiedFileSet modifiedFileSet,
+ Path pathEntry) throws InterruptedException;
+
+ /**
+ * Invalidates SkyFrame values that may have failed for transient reasons.
+ */
+ public abstract void invalidateTransientErrors();
+
+ @VisibleForTesting
+ public TimestampGranularityMonitor getTimestampGranularityMonitorForTesting() {
+ return tsgm;
+ }
+
+ /**
+ * Configures a given set of configured targets.
+ */
+ public EvaluationResult<ConfiguredTargetValue> configureTargets(
+ List<ConfiguredTargetKey> values, boolean keepGoing) throws InterruptedException {
+ checkActive();
+
+ // Make sure to not run too many analysis threads. This can cause memory thrashing.
+ return buildDriver.evaluate(ConfiguredTargetValue.keys(values), keepGoing,
+ ResourceUsage.getAvailableProcessors(), errorEventListener);
+ }
+
+ /**
+ * Post-process the targets. Values in the EvaluationResult are known to be transitively
+ * error-free from action conflicts.
+ */
+ public EvaluationResult<PostConfiguredTargetValue> postConfigureTargets(
+ List<ConfiguredTargetKey> values, boolean keepGoing,
+ ImmutableMap<Action, SkyframeActionExecutor.ConflictException> badActions)
+ throws InterruptedException {
+ checkActive();
+ PrecomputedValue.BAD_ACTIONS.set(injectable(), badActions);
+ // Make sure to not run too many analysis threads. This can cause memory thrashing.
+ EvaluationResult<PostConfiguredTargetValue> result =
+ buildDriver.evaluate(PostConfiguredTargetValue.keys(values), keepGoing,
+ ResourceUsage.getAvailableProcessors(), errorEventListener);
+
+ // Remove all post-configured target values immediately for memory efficiency. We are OK with
+ // this mini-phase being non-incremental as the failure mode of action conflict is rare.
+ memoizingEvaluator.delete(SkyFunctionName.functionIs(SkyFunctions.POST_CONFIGURED_TARGET));
+
+ return result;
+ }
+
+ /**
+ * Returns a Skyframe-based {@link SkyframeTransitivePackageLoader} implementation.
+ */
+ @VisibleForTesting
+ public TransitivePackageLoader pkgLoader() {
+ checkActive();
+ return new SkyframeLabelVisitor(new SkyframeTransitivePackageLoader(), cyclesReporter);
+ }
+
+ class SkyframeTransitivePackageLoader {
+ /**
+ * Loads the specified {@link TransitiveTargetValue}s.
+ */
+ EvaluationResult<TransitiveTargetValue> loadTransitiveTargets(
+ Iterable<Target> targetsToVisit, Iterable<Label> labelsToVisit, boolean keepGoing)
+ throws InterruptedException {
+ List<SkyKey> valueNames = new ArrayList<>();
+ for (Target target : targetsToVisit) {
+ valueNames.add(TransitiveTargetValue.key(target.getLabel()));
+ }
+ for (Label label : labelsToVisit) {
+ valueNames.add(TransitiveTargetValue.key(label));
+ }
+
+ return buildDriver.evaluate(valueNames, keepGoing, DEFAULT_THREAD_COUNT,
+ errorEventListener);
+ }
+
+ public Set<Package> retrievePackages(Set<PackageIdentifier> packageIds) {
+ final List<SkyKey> valueNames = new ArrayList<>();
+ for (PackageIdentifier pkgId : packageIds) {
+ valueNames.add(PackageValue.key(pkgId));
+ }
+
+ try {
+ return callUninterruptibly(new Callable<Set<Package>>() {
+ @Override
+ public Set<Package> call() throws Exception {
+ EvaluationResult<PackageValue> result = buildDriver.evaluate(
+ valueNames, false, ResourceUsage.getAvailableProcessors(), errorEventListener);
+ Preconditions.checkState(!result.hasError(),
+ "unexpected errors: %s", result.errorMap());
+ Set<Package> packages = Sets.newHashSet();
+ for (PackageValue value : result.values()) {
+ packages.add(value.getPackage());
+ }
+ return packages;
+ }
+ });
+ } catch (Exception e) {
+ throw new IllegalStateException(e);
+ }
+
+ }
+ }
+
+ /**
+ * Returns the generating {@link Action} of the given {@link Artifact}.
+ *
+ * <p>For use for legacy support from {@code BuildView} only.
+ */
+ @ThreadSafety.ThreadSafe
+ public Action getGeneratingAction(final Artifact artifact) {
+ if (artifact.isSourceArtifact()) {
+ return null;
+ }
+
+ try {
+ return callUninterruptibly(new Callable<Action>() {
+ @Override
+ public Action call() throws InterruptedException {
+ ArtifactOwner artifactOwner = artifact.getArtifactOwner();
+ Preconditions.checkState(artifactOwner instanceof ActionLookupValue.ActionLookupKey,
+ "%s %s", artifact, artifactOwner);
+ SkyKey actionLookupKey =
+ ActionLookupValue.key((ActionLookupValue.ActionLookupKey) artifactOwner);
+
+ synchronized (valueLookupLock) {
+ // Note that this will crash (attempting to run a configured target value builder after
+ // analysis) after a failed --nokeep_going analysis in which the configured target that
+ // failed was a (transitive) dependency of the configured target that should generate
+ // this action. We don't expect callers to query generating actions in such cases.
+ EvaluationResult<ActionLookupValue> result = buildDriver.evaluate(
+ ImmutableList.of(actionLookupKey), false, ResourceUsage.getAvailableProcessors(),
+ errorEventListener);
+ return result.hasError()
+ ? null
+ : result.get(actionLookupKey).getGeneratingAction(artifact);
+ }
+ }
+ });
+ } catch (Exception e) {
+ throw new IllegalStateException("Error getting generating action: " + artifact.prettyPrint(),
+ e);
+ }
+ }
+
+ public PackageManager getPackageManager() {
+ return packageManager;
+ }
+
+ class SkyframePackageLoader {
+ /**
+ * Looks up a particular package (mostly used after the loading phase, so packages should
+ * already be present, but occasionally used pre-loading phase). Use should be discouraged,
+ * since this cannot be used inside a Skyframe evaluation, and concurrent calls are
+ * synchronized.
+ *
+ * <p>Note that this method needs to be synchronized since InMemoryMemoizingEvaluator.evaluate()
+ * method does not support concurrent calls.
+ */
+ Package getPackage(EventHandler eventHandler, PackageIdentifier pkgName)
+ throws InterruptedException, NoSuchPackageException {
+ synchronized (valueLookupLock) {
+ SkyKey key = PackageValue.key(pkgName);
+ // Any call to this method post-loading phase should either be error-free or be in a
+ // keep_going build, since otherwise the build would have failed during loading. Thus
+ // we set keepGoing=true unconditionally.
+ EvaluationResult<PackageValue> result =
+ buildDriver.evaluate(ImmutableList.of(key), /*keepGoing=*/true,
+ DEFAULT_THREAD_COUNT, eventHandler);
+ if (result.hasError()) {
+ ErrorInfo error = result.getError();
+ if (!Iterables.isEmpty(error.getCycleInfo())) {
+ reportCycles(result.getError().getCycleInfo(), key);
+ // This can only happen if a package is freshly loaded outside of the target parsing
+ // or loading phase
+ throw new BuildFileContainsErrorsException(pkgName.toString(),
+ "Cycle encountered while loading package " + pkgName);
+ }
+ Throwable e = error.getException();
+ // PackageFunction should be catching, swallowing, and rethrowing all transitive
+ // errors as NoSuchPackageExceptions.
+ Throwables.propagateIfInstanceOf(e, NoSuchPackageException.class);
+ throw new IllegalStateException("Unexpected Exception type from PackageValue for '"
+ + pkgName + "'' with root causes: " + Iterables.toString(error.getRootCauses()), e);
+ }
+ return result.get(key).getPackage();
+ }
+ }
+
+ Package getLoadedPackage(final PackageIdentifier pkgName) throws NoSuchPackageException {
+ // Note that in Skyframe there is no way to tell if the package has been loaded before or not,
+ // so this will never throw for packages that are not loaded. However, no code currently
+ // relies on having the exception thrown.
+ try {
+ return callUninterruptibly(new Callable<Package>() {
+ @Override
+ public Package call() throws Exception {
+ return getPackage(errorEventListener, pkgName);
+ }
+ });
+ } catch (NoSuchPackageException e) {
+ if (e.getPackage() != null) {
+ return e.getPackage();
+ }
+ throw e;
+ } catch (Exception e) {
+ throw new IllegalStateException(e); // Should never happen.
+ }
+ }
+
+ /**
+ * Returns whether the given package should be consider deleted and thus should be ignored.
+ */
+ public boolean isPackageDeleted(String packageName) {
+ return deletedPackages.get().contains(packageName);
+ }
+
+ /** Same as {@link PackageManager#partiallyClear}. */
+ void partiallyClear() {
+ packageFunctionCache.clear();
+ }
+ }
+
+ /**
+ * Calls the given callable uninterruptibly.
+ *
+ * <p>If the callable throws {@link InterruptedException}, calls it again, until the callable
+ * returns a result. Sets the {@code currentThread().interrupted()} bit if the callable threw
+ * {@link InterruptedException} at least once.
+ *
+ * <p>This is almost identical to {@code Uninterruptibles#getUninterruptibly}.
+ */
+ protected static final <T> T callUninterruptibly(Callable<T> callable) throws Exception {
+ boolean interrupted = false;
+ try {
+ while (true) {
+ try {
+ return callable.call();
+ } catch (InterruptedException e) {
+ interrupted = true;
+ }
+ }
+ } finally {
+ if (interrupted) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+
+ @VisibleForTesting
+ public MemoizingEvaluator getEvaluatorForTesting() {
+ return memoizingEvaluator;
+ }
+
+ /**
+ * Stores the set of loaded packages and, if needed, evicts ConfiguredTarget values.
+ *
+ * <p>The set represents all packages from the transitive closure of the top-level targets from
+ * the latest build.
+ */
+ @ThreadCompatible
+ public abstract void updateLoadedPackageSet(Set<PackageIdentifier> loadedPackages);
+
+ public void sync(PackageCacheOptions packageCacheOptions, Path workingDirectory,
+ String defaultsPackageContents, UUID commandId) throws InterruptedException,
+ AbruptExitException{
+
+ preparePackageLoading(
+ createPackageLocator(packageCacheOptions, directories.getWorkspace(), workingDirectory),
+ packageCacheOptions.defaultVisibility, packageCacheOptions.showLoadingProgress,
+ defaultsPackageContents, commandId);
+ setDeletedPackages(ImmutableSet.copyOf(packageCacheOptions.deletedPackages));
+
+ incrementalBuildMonitor = new SkyframeIncrementalBuildMonitor();
+ invalidateTransientErrors();
+ }
+
+ protected PathPackageLocator createPackageLocator(PackageCacheOptions packageCacheOptions,
+ Path workspace, Path workingDirectory) throws AbruptExitException{
+ return PathPackageLocator.create(
+ packageCacheOptions.packagePath, getReporter(), workspace, workingDirectory);
+ }
+
+ private CyclesReporter createCyclesReporter() {
+ return new CyclesReporter(
+ new TransitiveTargetCycleReporter(packageManager),
+ new ActionArtifactCycleReporter(packageManager),
+ new SkylarkModuleCycleReporter());
+ }
+
+ CyclesReporter getCyclesReporter() {
+ return cyclesReporter.get();
+ }
+
+ /** Convenience method with same semantics as {@link CyclesReporter#reportCycles}. */
+ public void reportCycles(Iterable<CycleInfo> cycles, SkyKey topLevelKey) {
+ getCyclesReporter().reportCycles(cycles, topLevelKey, errorEventListener);
+ }
+
+ public void setActionExecutionProgressReportingObjects(@Nullable ProgressSupplier supplier,
+ @Nullable ActionCompletedReceiver completionReceiver,
+ @Nullable ActionExecutionStatusReporter statusReporter) {
+ skyframeActionExecutor.setActionExecutionProgressReportingObjects(supplier, completionReceiver);
+ this.statusReporterRef.set(statusReporter);
+ }
+
+ /**
+ * This should be called at most once in the lifetime of the SkyframeExecutor (except for
+ * tests), and it should be called before the execution phase.
+ */
+ void setArtifactFactoryAndBinTools(ArtifactFactory artifactFactory, BinTools binTools) {
+ this.artifactFactory.set(artifactFactory);
+ this.binTools = binTools;
+ }
+
+ public void prepareExecution(boolean checkOutputFiles) throws AbruptExitException,
+ InterruptedException {
+ maybeInjectEmbeddedArtifacts();
+
+ if (checkOutputFiles) {
+ // Detect external modifications in the output tree.
+ FilesystemValueChecker fsnc = new FilesystemValueChecker(memoizingEvaluator, tsgm);
+ invalidateDirtyActions(fsnc.getDirtyActionValues(batchStatter));
+ modifiedFiles += fsnc.getNumberOfModifiedOutputFiles();
+ }
+ informAboutNumberOfModifiedFiles();
+ }
+
+ protected abstract void invalidateDirtyActions(Iterable<SkyKey> dirtyActionValues);
+
+ @VisibleForTesting void maybeInjectEmbeddedArtifacts() throws AbruptExitException {
+ // The blaze client already ensures that the contents of the embedded binaries never change,
+ // so we just need to make sure that the appropriate artifacts are present in the skyframe
+ // graph.
+
+ if (!needToInjectEmbeddedArtifacts) {
+ return;
+ }
+
+ Preconditions.checkNotNull(artifactFactory.get());
+ Preconditions.checkNotNull(binTools);
+ Map<SkyKey, SkyValue> values = Maps.newHashMap();
+ // Blaze separately handles the symlinks that target these binaries. See BinTools#setupTool.
+ for (Artifact artifact : binTools.getAllEmbeddedArtifacts(artifactFactory.get())) {
+ FileArtifactValue fileArtifactValue;
+ try {
+ fileArtifactValue = FileArtifactValue.create(artifact);
+ } catch (IOException e) {
+ // See ExtractData in blaze.cc.
+ String message = "Error: corrupt installation: file " + artifact.getPath() + " missing. "
+ + "Please remove '" + directories.getInstallBase() + "' and try again.";
+ throw new AbruptExitException(message, ExitCode.LOCAL_ENVIRONMENTAL_ERROR, e);
+ }
+ values.put(ArtifactValue.key(artifact, /*isMandatory=*/true), fileArtifactValue);
+ }
+ injectable().inject(values);
+ needToInjectEmbeddedArtifacts = false;
+ }
+
+ /**
+ * Mark dirty values for deletion if they've been dirty for longer than N versions.
+ *
+ * <p>Specifying a value N means, if the current version is V and a value was dirtied (and
+ * has remained so) in version U, and U + N <= V, then the value will be marked for deletion
+ * and purged in version V+1.
+ */
+ public abstract void deleteOldNodes(long versionWindowForDirtyGc);
+
+ /**
+ * A progress received to track analysis invalidation and update progress messages.
+ */
+ protected class SkyframeProgressReceiver implements EvaluationProgressReceiver {
+ /**
+ * This flag is needed in order to avoid invalidating legacy data when we clear the
+ * analysis cache because of --discard_analysis_cache flag. For that case we want to keep
+ * the legacy data but get rid of the Skyframe data.
+ */
+ protected boolean ignoreInvalidations = false;
+ /** This receiver is only needed for execution, so it is null otherwise. */
+ @Nullable EvaluationProgressReceiver executionProgressReceiver = null;
+
+ @Override
+ public void invalidated(SkyValue value, InvalidationState state) {
+ if (ignoreInvalidations) {
+ return;
+ }
+ if (skyframeBuildView != null) {
+ skyframeBuildView.getInvalidationReceiver().invalidated(value, state);
+ }
+ }
+
+ @Override
+ public void enqueueing(SkyKey skyKey) {
+ if (ignoreInvalidations) {
+ return;
+ }
+ if (skyframeBuildView != null) {
+ skyframeBuildView.getInvalidationReceiver().enqueueing(skyKey);
+ }
+ if (executionProgressReceiver != null) {
+ executionProgressReceiver.enqueueing(skyKey);
+ }
+ }
+
+ @Override
+ public void evaluated(SkyKey skyKey, SkyValue value, EvaluationState state) {
+ if (ignoreInvalidations) {
+ return;
+ }
+ if (skyframeBuildView != null) {
+ skyframeBuildView.getInvalidationReceiver().evaluated(skyKey, value, state);
+ }
+ if (executionProgressReceiver != null) {
+ executionProgressReceiver.evaluated(skyKey, value, state);
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutorFactory.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutorFactory.java
new file mode 100644
index 0000000..a1615cf
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutorFactory.java
@@ -0,0 +1,68 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.analysis.BlazeDirectories;
+import com.google.devtools.build.lib.analysis.WorkspaceStatusAction.Factory;
+import com.google.devtools.build.lib.analysis.buildinfo.BuildInfoFactory;
+import com.google.devtools.build.lib.events.Reporter;
+import com.google.devtools.build.lib.packages.PackageFactory;
+import com.google.devtools.build.lib.packages.Preprocessor;
+import com.google.devtools.build.lib.util.AbruptExitException;
+import com.google.devtools.build.lib.util.io.TimestampGranularityMonitor;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyFunctionName;
+
+import java.util.Set;
+
+/**
+* A factory that creates instances of SkyframeExecutor.
+*/
+public interface SkyframeExecutorFactory {
+
+ /**
+ * Creates an instance of SkyframeExecutor
+ *
+ * @param reporter the reporter to be used by the executor
+ * @param pkgFactory the package factory
+ * @param skyframeBuild use Skyframe for the build phase. Should be always true after we are in
+ * the skyframe full mode.
+ * @param tsgm timestamp granularity monitor
+ * @param directories Blaze directories
+ * @param workspaceStatusActionFactory a factory for creating WorkspaceStatusAction objects
+ * @param buildInfoFactories list of BuildInfoFactories
+ * @param diffAwarenessFactories
+ * @param allowedMissingInputs
+ * @param preprocessorFactorySupplier
+ * @param extraSkyFunctions
+ * @param extraPrecomputedValues
+ * @return an instance of the SkyframeExecutor
+ * @throws AbruptExitException if the executor cannot be created
+ */
+ SkyframeExecutor create(Reporter reporter, PackageFactory pkgFactory,
+ TimestampGranularityMonitor tsgm, BlazeDirectories directories,
+ Factory workspaceStatusActionFactory,
+ ImmutableList<BuildInfoFactory> buildInfoFactories,
+ Set<Path> immutableDirectories,
+ Iterable<? extends DiffAwareness.Factory> diffAwarenessFactories,
+ Predicate<PathFragment> allowedMissingInputs,
+ Preprocessor.Factory.Supplier preprocessorFactorySupplier,
+ ImmutableMap<SkyFunctionName, SkyFunction> extraSkyFunctions,
+ ImmutableList<PrecomputedValue.Injected> extraPrecomputedValues) throws AbruptExitException;
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeIncrementalBuildMonitor.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeIncrementalBuildMonitor.java
new file mode 100644
index 0000000..c0fea26
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeIncrementalBuildMonitor.java
@@ -0,0 +1,59 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.eventbus.EventBus;
+import com.google.devtools.build.lib.actions.ChangedFilesMessage;
+import com.google.devtools.build.lib.concurrent.ThreadSafety;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.lib.vfs.RootedPath;
+import com.google.devtools.build.skyframe.SkyKey;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * A package-private class intended to track a small number of modified files during the build. This
+ * class should stop recording changed files if there are too many of them, instead of holding onto
+ * a large collection of files.
+ */
+@ThreadSafety.ThreadCompatible
+class SkyframeIncrementalBuildMonitor {
+ private Set<PathFragment> files = new HashSet<>();
+ private static final int MAX_FILES = 100;
+
+ public void accrue(Iterable<SkyKey> invalidatedValues) {
+ for (SkyKey skyKey : invalidatedValues) {
+ if (skyKey.functionName() == SkyFunctions.FILE_STATE) {
+ RootedPath file = (RootedPath) skyKey.argument();
+ maybeAddFile(file.getRelativePath());
+ }
+ }
+ }
+
+ private void maybeAddFile(PathFragment path) {
+ if (files != null) {
+ files.add(path);
+ if (files.size() >= MAX_FILES) {
+ files = null;
+ }
+ }
+ }
+
+ public void alertListeners(EventBus eventBus) {
+ if (files != null) {
+ eventBus.post(new ChangedFilesMessage(files));
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeLabelVisitor.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeLabelVisitor.java
new file mode 100644
index 0000000..2844cc0
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeLabelVisitor.java
@@ -0,0 +1,262 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Multimaps;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.packages.Package;
+import com.google.devtools.build.lib.packages.PackageIdentifier;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.pkgcache.TransitivePackageLoader;
+import com.google.devtools.build.lib.skyframe.SkyframeExecutor.SkyframeTransitivePackageLoader;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.skyframe.CycleInfo;
+import com.google.devtools.build.skyframe.CyclesReporter;
+import com.google.devtools.build.skyframe.ErrorInfo;
+import com.google.devtools.build.skyframe.EvaluationResult;
+import com.google.devtools.build.skyframe.SkyKey;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Map.Entry;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicReference;
+
+import javax.annotation.Nullable;
+
+/**
+ * Skyframe-based transitive package loader.
+ */
+final class SkyframeLabelVisitor implements TransitivePackageLoader {
+
+ private final SkyframeTransitivePackageLoader transitivePackageLoader;
+ private final AtomicReference<CyclesReporter> skyframeCyclesReporter;
+
+ private Set<PackageIdentifier> allVisitedPackages;
+ private Set<PackageIdentifier> errorFreeVisitedPackages;
+ private Set<Label> visitedTargets;
+ private Set<TransitiveTargetValue> previousBuildTargetValueSet = null;
+ private boolean lastBuildKeepGoing = false;
+ private final Multimap<Label, Label> rootCauses = HashMultimap.create();
+
+ SkyframeLabelVisitor(SkyframeTransitivePackageLoader transitivePackageLoader,
+ AtomicReference<CyclesReporter> skyframeCyclesReporter) {
+ this.transitivePackageLoader = transitivePackageLoader;
+ this.skyframeCyclesReporter = skyframeCyclesReporter;
+ }
+
+ @Override
+ public boolean sync(EventHandler eventHandler, Set<Target> targetsToVisit,
+ Set<Label> labelsToVisit, boolean keepGoing, int parallelThreads, int maxDepth)
+ throws InterruptedException {
+ rootCauses.clear();
+ lastBuildKeepGoing = false;
+ EvaluationResult<TransitiveTargetValue> result =
+ transitivePackageLoader.loadTransitiveTargets(targetsToVisit, labelsToVisit, keepGoing);
+ updateVisitedValues(result.values());
+ lastBuildKeepGoing = keepGoing;
+
+ if (!hasErrors(result)) {
+ return true;
+ }
+
+ Set<Entry<SkyKey, ErrorInfo>> errors = result.errorMap().entrySet();
+ if (!keepGoing) {
+ // We may have multiple errors, but in non keep_going builds, we're obligated to print only
+ // one of them.
+ Preconditions.checkState(!errors.isEmpty(), result);
+ Entry<SkyKey, ErrorInfo> error = errors.iterator().next();
+ ErrorInfo errorInfo = error.getValue();
+ SkyKey topLevel = error.getKey();
+ Label topLevelLabel = (Label) topLevel.argument();
+ if (!Iterables.isEmpty(errorInfo.getCycleInfo())) {
+ skyframeCyclesReporter.get().reportCycles(errorInfo.getCycleInfo(), topLevel, eventHandler);
+ errorAboutLoadingFailure(topLevelLabel, null, eventHandler);
+ } else if (isDirectErrorFromTopLevelLabel(topLevelLabel, labelsToVisit, errorInfo)) {
+ // An error caused by a non-top-level label has already been reported during error
+ // bubbling but an error caused by the top-level non-target label itself hasn't been
+ // reported yet. Note that errors from top-level targets have already been reported
+ // during target parsing.
+ errorAboutLoadingFailure(topLevelLabel, errorInfo.getException(), eventHandler);
+ }
+ return false;
+ }
+
+ for (Entry<SkyKey, ErrorInfo> errorEntry : errors) {
+ SkyKey key = errorEntry.getKey();
+ ErrorInfo errorInfo = errorEntry.getValue();
+ Preconditions.checkState(key.functionName().equals(SkyFunctions.TRANSITIVE_TARGET), errorEntry);
+ Label topLevelLabel = (Label) key.argument();
+ if (!Iterables.isEmpty(errorInfo.getCycleInfo())) {
+ skyframeCyclesReporter.get().reportCycles(errorInfo.getCycleInfo(), key, eventHandler);
+ for (Label rootCause : getRootCausesOfCycles(topLevelLabel, errorInfo.getCycleInfo())) {
+ rootCauses.put(topLevelLabel, rootCause);
+ }
+ }
+ if (isDirectErrorFromTopLevelLabel(topLevelLabel, labelsToVisit, errorInfo)) {
+ // Unlike top-level targets, which have already gone through target parsing,
+ // errors directly coming from top-level labels have not been reported yet.
+ //
+ // See the note in the --nokeep_going case above.
+ eventHandler.handle(Event.error(errorInfo.getException().getMessage()));
+ }
+ warnAboutLoadingFailure(topLevelLabel, eventHandler);
+ for (SkyKey badKey : errorInfo.getRootCauses()) {
+ Preconditions.checkState(badKey.argument() instanceof Label,
+ "%s %s %s", key, errorInfo, badKey);
+ rootCauses.put(topLevelLabel, (Label) badKey.argument());
+ }
+ }
+ for (Label topLevelLabel : result.<Label>keyNames()) {
+ SkyKey topLevelTransitiveTargetKey = TransitiveTargetValue.key(topLevelLabel);
+ TransitiveTargetValue topLevelTransitiveTargetValue = result.get(topLevelTransitiveTargetKey);
+ if (topLevelTransitiveTargetValue.getTransitiveRootCauses() != null) {
+ for (Label rootCause : topLevelTransitiveTargetValue.getTransitiveRootCauses()) {
+ rootCauses.put(topLevelLabel, rootCause);
+ }
+ warnAboutLoadingFailure(topLevelLabel, eventHandler);
+ }
+ }
+ return false;
+ }
+
+ private static boolean hasErrors(EvaluationResult<TransitiveTargetValue> result) {
+ if (result.hasError()) {
+ return true;
+ }
+ for (TransitiveTargetValue transitiveTargetValue : result.values()) {
+ if (transitiveTargetValue.getTransitiveRootCauses() != null) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static boolean isDirectErrorFromTopLevelLabel(Label label, Set<Label> topLevelLabels,
+ ErrorInfo errorInfo) {
+ return errorInfo.getException() != null && topLevelLabels.contains(label)
+ && Iterables.contains(errorInfo.getRootCauses(), TransitiveTargetValue.key(label));
+ }
+
+ private static void errorAboutLoadingFailure(Label topLevelLabel, @Nullable Throwable throwable,
+ EventHandler eventHandler) {
+ eventHandler.handle(Event.error(
+ "Loading of target '" + topLevelLabel + "' failed; build aborted" +
+ (throwable == null ? "" : ": " + throwable.getMessage())));
+ }
+
+ private static void warnAboutLoadingFailure(Label label, EventHandler eventHandler) {
+ eventHandler.handle(Event.warn(
+ // TODO(bazel-team): We use 'analyzing' here so that we print the same message as legacy
+ // Blaze. Once we get rid of legacy we should be able to change to 'loading' or
+ // similar.
+ "errors encountered while analyzing target '" + label + "': it will not be built"));
+ }
+
+ private static Set<Label> getRootCausesOfCycles(Label labelToLoad, Iterable<CycleInfo> cycles) {
+ ImmutableSet.Builder<Label> builder = ImmutableSet.builder();
+ for (CycleInfo cycleInfo : cycles) {
+ // The root cause of a cycle depends on the type of a cycle.
+
+ SkyKey culprit = Iterables.getFirst(cycleInfo.getCycle(), null);
+ if (culprit == null) {
+ continue;
+ }
+ if (culprit.functionName().equals(SkyFunctions.TRANSITIVE_TARGET)) {
+ // For a cycle between build targets, the root cause is the first element of the cycle.
+ builder.add((Label) culprit.argument());
+ } else {
+ // For other types of cycles (e.g. file symlink cycles), the root cause is the furthest
+ // target dependency that itself depended on the cycle.
+ Label furthestTarget = labelToLoad;
+ for (SkyKey skyKey : cycleInfo.getPathToCycle()) {
+ if (skyKey.functionName().equals(SkyFunctions.TRANSITIVE_TARGET)) {
+ furthestTarget = (Label) skyKey.argument();
+ } else {
+ break;
+ }
+ }
+ builder.add(furthestTarget);
+ }
+ }
+ return builder.build();
+ }
+
+ // Unfortunately we have to do an effective O(TC) visitation after the eval() call above to
+ // determine all of the packages in the closure.
+ private void updateVisitedValues(Collection<TransitiveTargetValue> targetValues) {
+ Set<TransitiveTargetValue> currentBuildTargetValueSet = new HashSet<>(targetValues);
+ if (Objects.equals(previousBuildTargetValueSet, currentBuildTargetValueSet)) {
+ // The next stanza is slow (and scales with the edge count of the target graph), so avoid
+ // the computation if the previous build already did it.
+ return;
+ }
+ NestedSetBuilder<PackageIdentifier> nestedAllPkgsBuilder = NestedSetBuilder.stableOrder();
+ NestedSetBuilder<PackageIdentifier> nestedErrorFreePkgsBuilder = NestedSetBuilder.stableOrder();
+ NestedSetBuilder<Label> nestedTargetBuilder = NestedSetBuilder.stableOrder();
+ for (TransitiveTargetValue value : targetValues) {
+ nestedAllPkgsBuilder.addTransitive(value.getTransitiveSuccessfulPackages());
+ nestedAllPkgsBuilder.addTransitive(value.getTransitiveUnsuccessfulPackages());
+ nestedErrorFreePkgsBuilder.addTransitive(value.getTransitiveSuccessfulPackages());
+ nestedTargetBuilder.addTransitive(value.getTransitiveTargets());
+ }
+ allVisitedPackages = nestedAllPkgsBuilder.build().toSet();
+ errorFreeVisitedPackages = nestedErrorFreePkgsBuilder.build().toSet();
+ visitedTargets = nestedTargetBuilder.build().toSet();
+ previousBuildTargetValueSet = currentBuildTargetValueSet;
+ }
+
+
+ @Override
+ public Set<PackageIdentifier> getVisitedPackageNames() {
+ return allVisitedPackages;
+ }
+
+ @Override
+ public Set<Package> getErrorFreeVisitedPackages() {
+ return transitivePackageLoader.retrievePackages(errorFreeVisitedPackages);
+ }
+
+ /**
+ * Doesn't necessarily include all top-level targets visited in error, because of issues with
+ * skyframe semantics (e.g. impossible to load a target if it transitively depends on a file
+ * symlink cycle). This is actually fine for the non-test usages of this method since such bad
+ * targets get filtered out.
+ */
+ @Override
+ public Set<Label> getVisitedTargets() {
+ return visitedTargets;
+ }
+
+ @Override
+ public Multimap<Label, Label> getRootCauses(final Collection<Label> targetsToLoad) {
+ Preconditions.checkState(lastBuildKeepGoing);
+ return Multimaps.filterKeys(rootCauses,
+ new Predicate<Label>() {
+ @Override
+ public boolean apply(Label label) {
+ return targetsToLoad.contains(label);
+ }
+ });
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframePackageLoaderWithValueEnvironment.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframePackageLoaderWithValueEnvironment.java
new file mode 100644
index 0000000..e467ae0
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframePackageLoaderWithValueEnvironment.java
@@ -0,0 +1,120 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.devtools.build.lib.analysis.BlazeDirectories;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration.Fragment;
+import com.google.devtools.build.lib.analysis.config.BuildOptions;
+import com.google.devtools.build.lib.analysis.config.InvalidConfigurationException;
+import com.google.devtools.build.lib.analysis.config.PackageProviderForConfigurations;
+import com.google.devtools.build.lib.packages.NoSuchPackageException;
+import com.google.devtools.build.lib.packages.NoSuchTargetException;
+import com.google.devtools.build.lib.packages.Package;
+import com.google.devtools.build.lib.packages.PackageIdentifier;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.skyframe.SkyframeExecutor.SkyframePackageLoader;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.syntax.Label.SyntaxException;
+import com.google.devtools.build.lib.vfs.RootedPath;
+import com.google.devtools.build.skyframe.MemoizingEvaluator;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyKey;
+
+import java.io.IOException;
+import java.util.Set;
+
+/**
+ * Repeats functionality of {@link SkyframePackageLoader} but uses
+ * {@link SkyFunction.Environment#getValue} instead of {@link MemoizingEvaluator#evaluate}
+ * for node evaluation
+ */
+class SkyframePackageLoaderWithValueEnvironment implements
+ PackageProviderForConfigurations {
+ private final SkyFunction.Environment env;
+ private final Set<Package> packages;
+
+ public SkyframePackageLoaderWithValueEnvironment(SkyFunction.Environment env,
+ Set<Package> packages) {
+ this.env = env;
+ this.packages = packages;
+ }
+
+ private Package getPackage(PackageIdentifier pkgIdentifier) throws NoSuchPackageException{
+ SkyKey key = PackageValue.key(pkgIdentifier);
+ PackageValue value = (PackageValue) env.getValueOrThrow(key, NoSuchPackageException.class);
+ if (value != null) {
+ packages.add(value.getPackage());
+ return value.getPackage();
+ }
+ return null;
+ }
+
+ @Override
+ public Package getLoadedPackage(final PackageIdentifier pkgIdentifier)
+ throws NoSuchPackageException {
+ try {
+ return getPackage(pkgIdentifier);
+ } catch (NoSuchPackageException e) {
+ if (e.getPackage() != null) {
+ return e.getPackage();
+ }
+ throw e;
+ }
+ }
+
+ @Override
+ public Target getLoadedTarget(Label label) throws NoSuchPackageException,
+ NoSuchTargetException {
+ Package pkg = getLoadedPackage(label.getPackageIdentifier());
+ return pkg == null ? null : pkg.getTarget(label.getName());
+ }
+
+ @Override
+ public boolean isTargetCurrent(Target target) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void addDependency(Package pkg, String fileName) throws SyntaxException, IOException {
+ RootedPath fileRootedPath = RootedPath.toRootedPath(pkg.getSourceRoot(),
+ pkg.getNameFragment().getRelative(fileName));
+ FileValue result = (FileValue) env.getValue(FileValue.key(fileRootedPath));
+ if (result != null && !result.exists()) {
+ throw new IOException();
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public <T extends Fragment> T getFragment(BuildOptions buildOptions, Class<T> fragmentType)
+ throws InvalidConfigurationException {
+ ConfigurationFragmentValue fragmentNode = (ConfigurationFragmentValue) env.getValueOrThrow(
+ ConfigurationFragmentValue.key(buildOptions, fragmentType),
+ InvalidConfigurationException.class);
+ if (fragmentNode == null) {
+ return null;
+ }
+ return (T) fragmentNode.getFragment();
+ }
+
+ @Override
+ public BlazeDirectories getDirectories() {
+ return PrecomputedValue.BLAZE_DIRECTORIES.get(env);
+ }
+
+ @Override
+ public boolean valuesMissing() {
+ return env.valuesMissing();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframePackageManager.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframePackageManager.java
new file mode 100644
index 0000000..cc32bf8
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframePackageManager.java
@@ -0,0 +1,177 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.devtools.build.lib.cmdline.LabelValidator;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.packages.NoSuchPackageException;
+import com.google.devtools.build.lib.packages.NoSuchTargetException;
+import com.google.devtools.build.lib.packages.Package;
+import com.google.devtools.build.lib.packages.PackageIdentifier;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.pkgcache.PackageManager;
+import com.google.devtools.build.lib.pkgcache.PathPackageLocator;
+import com.google.devtools.build.lib.pkgcache.TargetPatternEvaluator;
+import com.google.devtools.build.lib.pkgcache.TransitivePackageLoader;
+import com.google.devtools.build.lib.skyframe.SkyframeExecutor.SkyframePackageLoader;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.UnixGlob;
+import com.google.devtools.build.skyframe.CyclesReporter;
+
+import java.io.PrintStream;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Skyframe-based package manager.
+ *
+ * <p>This is essentially a compatibility shim between the native Skyframe and non-Skyframe
+ * parts of Blaze and should not be long-lived.
+ */
+class SkyframePackageManager implements PackageManager {
+
+ private final SkyframePackageLoader packageLoader;
+ private final SkyframeExecutor.SkyframeTransitivePackageLoader transitiveLoader;
+ private final TargetPatternEvaluator patternEvaluator;
+ private final AtomicReference<UnixGlob.FilesystemCalls> syscalls;
+ private final AtomicReference<CyclesReporter> skyframeCyclesReporter;
+ private final AtomicReference<PathPackageLocator> pkgLocator;
+ private final AtomicInteger numPackagesLoaded;
+ private final SkyframeExecutor skyframeExecutor;
+
+ public SkyframePackageManager(SkyframePackageLoader packageLoader,
+ SkyframeExecutor.SkyframeTransitivePackageLoader transitiveLoader,
+ TargetPatternEvaluator patternEvaluator,
+ AtomicReference<UnixGlob.FilesystemCalls> syscalls,
+ AtomicReference<CyclesReporter> skyframeCyclesReporter,
+ AtomicReference<PathPackageLocator> pkgLocator,
+ AtomicInteger numPackagesLoaded,
+ SkyframeExecutor skyframeExecutor) {
+ this.packageLoader = packageLoader;
+ this.transitiveLoader = transitiveLoader;
+ this.patternEvaluator = patternEvaluator;
+ this.skyframeCyclesReporter = skyframeCyclesReporter;
+ this.pkgLocator = pkgLocator;
+ this.syscalls = syscalls;
+ this.numPackagesLoaded = numPackagesLoaded;
+ this.skyframeExecutor = skyframeExecutor;
+ }
+
+ @Override
+ public Package getLoadedPackage(PackageIdentifier pkgIdentifier) throws NoSuchPackageException {
+ return packageLoader.getLoadedPackage(pkgIdentifier);
+ }
+
+ @ThreadSafe
+ @Override
+ public Package getPackage(EventHandler eventHandler, PackageIdentifier packageIdentifier)
+ throws NoSuchPackageException, InterruptedException {
+ try {
+ return packageLoader.getPackage(eventHandler, packageIdentifier);
+ } catch (NoSuchPackageException e) {
+ if (e.getPackage() != null) {
+ return e.getPackage();
+ }
+ throw e;
+ }
+ }
+
+ @Override
+ public Target getLoadedTarget(Label label) throws NoSuchPackageException, NoSuchTargetException {
+ return getLoadedPackage(label.getPackageIdentifier()).getTarget(label.getName());
+ }
+
+ @Override
+ public Target getTarget(EventHandler eventHandler, Label label)
+ throws NoSuchPackageException, NoSuchTargetException, InterruptedException {
+ return getPackage(eventHandler, label.getPackageIdentifier()).getTarget(label.getName());
+ }
+
+ @Override
+ public boolean isTargetCurrent(Target target) {
+ Package pkg = target.getPackage();
+ try {
+ return getLoadedPackage(target.getLabel().getPackageIdentifier()) == pkg;
+ } catch (NoSuchPackageException e) {
+ return false;
+ }
+ }
+
+ @Override
+ public void partiallyClear() {
+ packageLoader.partiallyClear();
+ }
+
+ @Override
+ public PackageManagerStatistics getStatistics() {
+ return new PackageManagerStatistics() {
+ @Override
+ public int getPackagesLoaded() {
+ return numPackagesLoaded.get();
+ }
+
+ @Override
+ public int getPackagesLookedUp() {
+ return -1;
+ }
+
+ @Override
+ public int getCacheSize() {
+ return -1;
+ }
+ };
+ }
+
+ @Override
+ public boolean isPackage(String packageName) {
+ return getBuildFileForPackage(packageName) != null;
+ }
+
+ @Override
+ public void dump(PrintStream printStream) {
+ skyframeExecutor.dumpPackages(printStream);
+ }
+
+ @ThreadSafe
+ @Override
+ public Path getBuildFileForPackage(String packageName) {
+ // Note that this method needs to be thread-safe, as it is currently used concurrently by
+ // legacy blaze code.
+ if (packageLoader.isPackageDeleted(packageName)
+ || LabelValidator.validatePackageName(packageName) != null) {
+ return null;
+ }
+ // TODO(bazel-team): Use a PackageLookupValue here [skyframe-loading]
+ // TODO(bazel-team): The implementation in PackageCache also checks for duplicate packages, see
+ // BuildFileCache#getBuildFile [skyframe-loading]
+ return pkgLocator.get().getPackageBuildFileNullable(packageName, syscalls);
+ }
+
+ @Override
+ public PathPackageLocator getPackagePath() {
+ return pkgLocator.get();
+ }
+
+ @Override
+ public TransitivePackageLoader newTransitiveLoader() {
+ return new SkyframeLabelVisitor(transitiveLoader, skyframeCyclesReporter);
+ }
+
+ @Override
+ public TargetPatternEvaluator getTargetPatternEvaluator() {
+ return patternEvaluator;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeTargetPatternEvaluator.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeTargetPatternEvaluator.java
new file mode 100644
index 0000000..9e619e3
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeTargetPatternEvaluator.java
@@ -0,0 +1,146 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicates;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Maps;
+import com.google.devtools.build.lib.cmdline.ResolvedTargets;
+import com.google.devtools.build.lib.cmdline.TargetParsingException;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.pkgcache.FilteringPolicies;
+import com.google.devtools.build.lib.pkgcache.FilteringPolicy;
+import com.google.devtools.build.lib.pkgcache.ParseFailureListener;
+import com.google.devtools.build.lib.pkgcache.TargetPatternEvaluator;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.skyframe.ErrorInfo;
+import com.google.devtools.build.skyframe.EvaluationResult;
+import com.google.devtools.build.skyframe.SkyKey;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Skyframe-based target pattern parsing.
+ */
+final class SkyframeTargetPatternEvaluator implements TargetPatternEvaluator {
+ private final SkyframeExecutor skyframeExecutor;
+ private String offset = "";
+
+ SkyframeTargetPatternEvaluator(SkyframeExecutor skyframeExecutor) {
+ this.skyframeExecutor = skyframeExecutor;
+ }
+
+ @Override
+ public ResolvedTargets<Target> parseTargetPatternList(EventHandler eventHandler,
+ List<String> targetPatterns, FilteringPolicy policy, boolean keepGoing)
+ throws TargetParsingException, InterruptedException {
+ return parseTargetPatternList(offset, eventHandler, targetPatterns, policy, keepGoing);
+ }
+
+ @Override
+ public ResolvedTargets<Target> parseTargetPattern(EventHandler eventHandler,
+ String pattern, boolean keepGoing) throws TargetParsingException, InterruptedException {
+ return parseTargetPatternList(eventHandler, ImmutableList.of(pattern),
+ FilteringPolicies.NO_FILTER, keepGoing);
+ }
+
+ @Override
+ public void updateOffset(PathFragment relativeWorkingDirectory) {
+ offset = relativeWorkingDirectory.getPathString();
+ }
+
+ @Override
+ public String getOffset() {
+ return offset;
+ }
+
+ @Override
+ public Map<String, ResolvedTargets<Target>> preloadTargetPatterns(EventHandler eventHandler,
+ Collection<String> patterns, boolean keepGoing)
+ throws TargetParsingException, InterruptedException {
+ // TODO(bazel-team): This is used only in "blaze query". There are plans to dramatically change
+ // how query works on Skyframe, in which case this method is likely to go away.
+ // We cannot use an ImmutableMap here because there may be null values.
+ Map<String, ResolvedTargets<Target>> result = Maps.newHashMapWithExpectedSize(patterns.size());
+ for (String pattern : patterns) {
+ // TODO(bazel-team): This could be parallelized to improve performance. [skyframe-loading]
+ result.put(pattern, parseTargetPattern(eventHandler, pattern, keepGoing));
+ }
+ return result;
+ }
+
+ /**
+ * Loads a list of target patterns (eg, "foo/...").
+ */
+ ResolvedTargets<Target> parseTargetPatternList(String offset, EventHandler eventHandler,
+ List<String> targetPatterns, FilteringPolicy policy, boolean keepGoing)
+ throws InterruptedException, TargetParsingException {
+ Iterable<SkyKey> patternSkyKeys = TargetPatternValue.keys(targetPatterns, policy, offset);
+ EvaluationResult<TargetPatternValue> result =
+ skyframeExecutor.targetPatterns(patternSkyKeys, keepGoing, eventHandler);
+
+ String errorMessage = null;
+ ResolvedTargets.Builder<Target> builder = ResolvedTargets.builder();
+ for (SkyKey key : patternSkyKeys) {
+ TargetPatternValue resultValue = result.get(key);
+ if (resultValue != null) {
+ ResolvedTargets<Target> results = resultValue.getTargets();
+ if (((TargetPatternValue.TargetPattern) key.argument()).isNegative()) {
+ builder.filter(Predicates.not(Predicates.in(results.getTargets())));
+ } else {
+ builder.merge(results);
+ }
+ } else {
+ TargetPatternValue.TargetPattern pattern =
+ (TargetPatternValue.TargetPattern) key.argument();
+ String rawPattern = pattern.getPattern();
+ ErrorInfo error = result.errorMap().get(key);
+ if (error == null) {
+ Preconditions.checkState(!keepGoing);
+ continue;
+ }
+ if (error.getException() != null) {
+ errorMessage = error.getException().getMessage();
+ } else if (!Iterables.isEmpty(error.getCycleInfo())) {
+ errorMessage = "cycles detected during target parsing";
+ skyframeExecutor.getCyclesReporter().reportCycles(
+ error.getCycleInfo(), key, eventHandler);
+ } else {
+ throw new IllegalStateException(error.toString());
+ }
+ if (keepGoing) {
+ eventHandler.handle(Event.error("Skipping '" + rawPattern + "': " + errorMessage));
+ }
+ builder.setError();
+
+ if (eventHandler instanceof ParseFailureListener) {
+ ParseFailureListener parseListener = (ParseFailureListener) eventHandler;
+ parseListener.parsingError(rawPattern, errorMessage);
+ }
+ }
+ }
+
+ if (!keepGoing && result.hasError()) {
+ Preconditions.checkState(errorMessage != null, "unexpected errors: %s", result.errorMap());
+ throw new TargetParsingException(errorMessage);
+ }
+ return builder.build();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkylarkFileDependency.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkylarkFileDependency.java
new file mode 100644
index 0000000..02d2e91
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkylarkFileDependency.java
@@ -0,0 +1,48 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.syntax.Label;
+
+/**
+ * A simple value class to store the direct Skylark file dependencies of a Skylark
+ * extension file. It also contains a Label identifying the extension file.
+ */
+class SkylarkFileDependency {
+
+ private final Label label;
+ private final ImmutableList<SkylarkFileDependency> dependencies;
+
+ SkylarkFileDependency(Label label, ImmutableList<SkylarkFileDependency> dependencies) {
+ this.label = label;
+ this.dependencies = dependencies;
+ }
+
+ /**
+ * Returns the list of direct Skylark file dependencies of the Skylark extension file
+ * corresponding to this object.
+ */
+ ImmutableList<SkylarkFileDependency> getDependencies() {
+ return dependencies;
+ }
+
+ /**
+ * Returns the Label of the Skylark extension file corresponding to this object.
+ */
+ Label getLabel() {
+ return label;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkylarkImportLookupFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkylarkImportLookupFunction.java
new file mode 100644
index 0000000..02d41e6
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkylarkImportLookupFunction.java
@@ -0,0 +1,238 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.StoredEventHandler;
+import com.google.devtools.build.lib.packages.BuildFileNotFoundException;
+import com.google.devtools.build.lib.packages.PackageFactory;
+import com.google.devtools.build.lib.packages.PackageIdentifier;
+import com.google.devtools.build.lib.packages.PackageIdentifier.RepositoryName;
+import com.google.devtools.build.lib.packages.RuleClassProvider;
+import com.google.devtools.build.lib.skyframe.ASTFileLookupValue.ASTLookupInputException;
+import com.google.devtools.build.lib.syntax.BuildFileAST;
+import com.google.devtools.build.lib.syntax.Function;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.syntax.Label.SyntaxException;
+import com.google.devtools.build.lib.syntax.SkylarkEnvironment;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyFunctionException;
+import com.google.devtools.build.skyframe.SkyFunctionException.Transience;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A Skyframe function to look up and import a single Skylark extension.
+ */
+public class SkylarkImportLookupFunction implements SkyFunction {
+
+ private final RuleClassProvider ruleClassProvider;
+ private final ImmutableList<Function> nativeRuleFunctions;
+
+ public SkylarkImportLookupFunction(
+ RuleClassProvider ruleClassProvider, PackageFactory packageFactory) {
+ this.ruleClassProvider = ruleClassProvider;
+ this.nativeRuleFunctions = packageFactory.collectNativeRuleFunctions();
+ }
+
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env) throws SkyFunctionException,
+ InterruptedException {
+ PackageIdentifier arg = (PackageIdentifier) skyKey.argument();
+ PathFragment file = arg.getPackageFragment();
+ ASTFileLookupValue astLookupValue = null;
+ try {
+ SkyKey astLookupKey = ASTFileLookupValue.key(file);
+ astLookupValue = (ASTFileLookupValue) env.getValueOrThrow(astLookupKey,
+ ErrorReadingSkylarkExtensionException.class, InconsistentFilesystemException.class);
+ } catch (ErrorReadingSkylarkExtensionException e) {
+ throw new SkylarkImportLookupFunctionException(SkylarkImportFailedException.errorReadingFile(
+ file, e.getMessage()));
+ } catch (InconsistentFilesystemException e) {
+ throw new SkylarkImportLookupFunctionException(e, Transience.PERSISTENT);
+ } catch (ASTLookupInputException e) {
+ throw new SkylarkImportLookupFunctionException(e, Transience.PERSISTENT);
+ }
+ if (astLookupValue == null) {
+ return null;
+ }
+ if (astLookupValue == ASTFileLookupValue.NO_FILE) {
+ // Skylark import files have to exist.
+ throw new SkylarkImportLookupFunctionException(SkylarkImportFailedException.noFile(file));
+ }
+
+ Map<PathFragment, SkylarkEnvironment> importMap = new HashMap<>();
+ ImmutableList.Builder<SkylarkFileDependency> fileDependencies = ImmutableList.builder();
+ BuildFileAST ast = astLookupValue.getAST();
+ // TODO(bazel-team): Refactor this code and PackageFunction to reduce code duplications.
+ for (PathFragment importFile : ast.getImports()) {
+ try {
+ SkyKey importsLookupKey = SkylarkImportLookupValue.key(arg.getRepository(), importFile);
+ SkylarkImportLookupValue importsLookupValue;
+ importsLookupValue = (SkylarkImportLookupValue) env.getValueOrThrow(
+ importsLookupKey, ASTLookupInputException.class);
+ if (importsLookupValue != null) {
+ importMap.put(importFile, importsLookupValue.getImportedEnvironment());
+ fileDependencies.add(importsLookupValue.getDependency());
+ }
+ } catch (ASTLookupInputException e) {
+ throw new SkylarkImportLookupFunctionException(e, Transience.PERSISTENT);
+ }
+ }
+ Label label = pathFragmentToLabel(arg.getRepository(), file, env);
+ if (env.valuesMissing()) {
+ // This means some imports are unavailable.
+ return null;
+ }
+
+ if (ast.containsErrors()) {
+ throw new SkylarkImportLookupFunctionException(SkylarkImportFailedException.skylarkErrors(
+ file));
+ }
+
+ SkylarkEnvironment extensionEnv = createEnv(ast, importMap, env);
+ // Skylark UserDefinedFunctions are sharing function definition Environments, so it's extremely
+ // important not to modify them from this point. Ideally they should be only used to import
+ // symbols and serve as global Environments of UserDefinedFunctions.
+ return new SkylarkImportLookupValue(
+ extensionEnv, new SkylarkFileDependency(label, fileDependencies.build()));
+ }
+
+ /**
+ * Converts the PathFragment of the Skylark file to a Label using the BUILD file closest to the
+ * Skylark file in its directory hierarchy - finds the package to which the Skylark file belongs.
+ * Throws an exception if no such BUILD file exists.
+ */
+ private Label pathFragmentToLabel(RepositoryName repo, PathFragment file, Environment env)
+ throws SkylarkImportLookupFunctionException {
+ ContainingPackageLookupValue containingPackageLookupValue = null;
+ try {
+ PackageIdentifier newPkgId = new PackageIdentifier(repo, file.getParentDirectory());
+ containingPackageLookupValue = (ContainingPackageLookupValue) env.getValueOrThrow(
+ ContainingPackageLookupValue.key(newPkgId),
+ BuildFileNotFoundException.class, InconsistentFilesystemException.class);
+ } catch (BuildFileNotFoundException e) {
+ // Thrown when there are IO errors looking for BUILD files.
+ throw new SkylarkImportLookupFunctionException(e, Transience.PERSISTENT);
+ } catch (InconsistentFilesystemException e) {
+ throw new SkylarkImportLookupFunctionException(e, Transience.PERSISTENT);
+ }
+
+ if (containingPackageLookupValue == null) {
+ return null;
+ }
+
+ if (!containingPackageLookupValue.hasContainingPackage()) {
+ throw new SkylarkImportLookupFunctionException(
+ SkylarkImportFailedException.noBuildFile(file));
+ }
+
+ PathFragment pkgName =
+ containingPackageLookupValue.getContainingPackageName().getPackageFragment();
+ PathFragment fileInPkg = file.relativeTo(pkgName);
+
+ try {
+ // This code relies on PackageIdentifier.RepositoryName.toString()
+ return Label.parseRepositoryLabel(repo + "//" + pkgName.getPathString() + ":" + fileInPkg);
+ } catch (SyntaxException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ /**
+ * Creates the SkylarkEnvironment to be imported. After it's returned, the Environment
+ * must not be modified.
+ */
+ private SkylarkEnvironment createEnv(BuildFileAST ast,
+ Map<PathFragment, SkylarkEnvironment> importMap, Environment env)
+ throws InterruptedException {
+ StoredEventHandler eventHandler = new StoredEventHandler();
+ // TODO(bazel-team): this method overestimates the changes which can affect the
+ // Skylark RuleClass. For example changes to comments or unused functions can modify the hash.
+ // A more accurate - however much more complicated - way would be to calculate a hash based on
+ // the transitive closure of the accessible AST nodes.
+ SkylarkEnvironment extensionEnv = ruleClassProvider
+ .createSkylarkRuleClassEnvironment(eventHandler, ast.getContentHashCode());
+ // Adding native rules module for build extensions.
+ // TODO(bazel-team): this might not be the best place to do this.
+ extensionEnv.update("native", ruleClassProvider.getNativeModule());
+ for (Function function : nativeRuleFunctions) {
+ extensionEnv.registerFunction(
+ ruleClassProvider.getNativeModule().getClass(), function.getName(), function);
+ }
+ extensionEnv.setImportedExtensions(importMap);
+ ast.exec(extensionEnv, eventHandler);
+ // Don't fail just replay the events so the original package lookup can fail.
+ Event.replayEventsOn(env.getListener(), eventHandler.getEvents());
+ return extensionEnv;
+ }
+
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return null;
+ }
+
+ static final class SkylarkImportFailedException extends Exception {
+ private SkylarkImportFailedException(String errorMessage) {
+ super(errorMessage);
+ }
+
+ static SkylarkImportFailedException errorReadingFile(PathFragment file, String error) {
+ return new SkylarkImportFailedException(
+ String.format("Encountered error while reading extension file '%s': %s", file, error));
+ }
+
+ static SkylarkImportFailedException noFile(PathFragment file) {
+ return new SkylarkImportFailedException(
+ String.format("Extension file not found: '%s'", file));
+ }
+
+ static SkylarkImportFailedException noBuildFile(PathFragment file) {
+ return new SkylarkImportFailedException(
+ String.format("Every .bzl file must have a corresponding package, but '%s' "
+ + "does not have one. Please create a BUILD file in the same or any parent directory."
+ + " Note that this BUILD file does not need to do anything except exist.", file));
+ }
+
+ static SkylarkImportFailedException skylarkErrors(PathFragment file) {
+ return new SkylarkImportFailedException(String.format("Extension '%s' has errors", file));
+ }
+ }
+
+ private static final class SkylarkImportLookupFunctionException extends SkyFunctionException {
+ private SkylarkImportLookupFunctionException(SkylarkImportFailedException cause) {
+ super(cause, Transience.PERSISTENT);
+ }
+
+ private SkylarkImportLookupFunctionException(InconsistentFilesystemException e,
+ Transience transience) {
+ super(e, transience);
+ }
+
+ private SkylarkImportLookupFunctionException(ASTLookupInputException e,
+ Transience transience) {
+ super(e, transience);
+ }
+
+ private SkylarkImportLookupFunctionException(BuildFileNotFoundException e,
+ Transience transience) {
+ super(e, transience);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkylarkImportLookupValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkylarkImportLookupValue.java
new file mode 100644
index 0000000..3c87431
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkylarkImportLookupValue.java
@@ -0,0 +1,70 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.packages.PackageIdentifier;
+import com.google.devtools.build.lib.packages.PackageIdentifier.RepositoryName;
+import com.google.devtools.build.lib.skyframe.ASTFileLookupValue.ASTLookupInputException;
+import com.google.devtools.build.lib.syntax.SkylarkEnvironment;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+/**
+ * A value that represents a Skylark import lookup result. The lookup value corresponds to
+ * exactly one Skylark file, identified by the PathFragment SkyKey argument.
+ */
+public class SkylarkImportLookupValue implements SkyValue {
+
+ private final SkylarkEnvironment importedEnvironment;
+ /**
+ * The immediate Skylark file dependency descriptor class corresponding to this value.
+ * Using this reference it's possible to reach the transitive closure of Skylark files
+ * on which this Skylark file depends.
+ */
+ private final SkylarkFileDependency dependency;
+
+ public SkylarkImportLookupValue(
+ SkylarkEnvironment importedEnvironment, SkylarkFileDependency dependency) {
+ this.importedEnvironment = Preconditions.checkNotNull(importedEnvironment);
+ this.dependency = Preconditions.checkNotNull(dependency);
+ }
+
+ /**
+ * Returns the imported SkylarkEnvironment.
+ */
+ public SkylarkEnvironment getImportedEnvironment() {
+ return importedEnvironment;
+ }
+
+ /**
+ * Returns the immediate Skylark file dependency corresponding to this import lookup value.
+ */
+ public SkylarkFileDependency getDependency() {
+ return dependency;
+ }
+
+ static SkyKey key(PackageIdentifier pkgIdentifier) throws ASTLookupInputException {
+ return key(pkgIdentifier.getRepository(), pkgIdentifier.getPackageFragment());
+ }
+
+ static SkyKey key(RepositoryName repo, PathFragment fileToImport) throws ASTLookupInputException {
+ // Skylark import lookup keys need to be valid AST file lookup keys.
+ ASTFileLookupValue.checkInputArgument(fileToImport);
+ return new SkyKey(
+ SkyFunctions.SKYLARK_IMPORTS_LOOKUP,
+ new PackageIdentifier(repo, fileToImport));
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkylarkModuleCycleReporter.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkylarkModuleCycleReporter.java
new file mode 100644
index 0000000..a0f37a9
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkylarkModuleCycleReporter.java
@@ -0,0 +1,71 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.packages.PackageIdentifier;
+import com.google.devtools.build.skyframe.CycleInfo;
+import com.google.devtools.build.skyframe.CyclesReporter;
+import com.google.devtools.build.skyframe.SkyKey;
+
+/**
+ * Reports cycles of recursive import of Skylark files.
+ */
+public class SkylarkModuleCycleReporter implements CyclesReporter.SingleCycleReporter {
+
+ private static final Predicate<SkyKey> IS_SKYLARK_MODULE_SKY_KEY =
+ SkyFunctions.isSkyFunction(SkyFunctions.SKYLARK_IMPORTS_LOOKUP);
+
+ private static final Predicate<SkyKey> IS_PACKAGE_SKY_KEY =
+ SkyFunctions.isSkyFunction(SkyFunctions.PACKAGE);
+
+ @Override
+ public boolean maybeReportCycle(SkyKey topLevelKey, CycleInfo cycleInfo, boolean alreadyReported,
+ EventHandler eventHandler) {
+ ImmutableList<SkyKey> pathToCycle = cycleInfo.getPathToCycle();
+ if (pathToCycle.size() == 0) {
+ return false;
+ }
+ SkyKey lastPathElement = cycleInfo.getPathToCycle().get(pathToCycle.size() - 1);
+ if (alreadyReported) {
+ return true;
+ } else if (Iterables.all(cycleInfo.getCycle(), IS_SKYLARK_MODULE_SKY_KEY)
+ // The last element of the path to the cycle has to be a PackageFunction.
+ && IS_PACKAGE_SKY_KEY.apply(lastPathElement)) {
+ StringBuilder cycleMessage = new StringBuilder()
+ .append(((PackageIdentifier) lastPathElement.argument()).toString() + "/BUILD: ")
+ .append("cycle in referenced extension files: ");
+
+ AbstractLabelCycleReporter.printCycle(cycleInfo.getCycle(), cycleMessage,
+ new Function<SkyKey, String>() {
+ @Override
+ public String apply(SkyKey input) {
+ return ((PackageIdentifier) input.argument()).toString();
+ }
+ });
+
+ // TODO(bazel-team): it would be nice to pass the Location of the load Statement in the
+ // BUILD file.
+ eventHandler.handle(Event.error(null, cycleMessage.toString()));
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/TargetCompletionFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/TargetCompletionFunction.java
new file mode 100644
index 0000000..2e4aea9
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/TargetCompletionFunction.java
@@ -0,0 +1,138 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.eventbus.EventBus;
+import com.google.devtools.build.lib.actions.ActionExecutionException;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.MissingInputFileException;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.LabelAndConfiguration;
+import com.google.devtools.build.lib.analysis.TargetCompleteEvent;
+import com.google.devtools.build.lib.analysis.TopLevelArtifactContext;
+import com.google.devtools.build.lib.analysis.TopLevelArtifactHelper;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyFunctionException;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+import com.google.devtools.build.skyframe.ValueOrException2;
+
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+
+import javax.annotation.Nullable;
+
+/**
+ * TargetCompletionFunction builds the artifactsToBuild collection of a {@link ConfiguredTarget}.
+ */
+public final class TargetCompletionFunction implements SkyFunction {
+
+ private final AtomicReference<EventBus> eventBusRef;
+
+ public TargetCompletionFunction(AtomicReference<EventBus> eventBusRef) {
+ this.eventBusRef = eventBusRef;
+ }
+
+ @Nullable
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env) throws TargetCompletionFunctionException {
+ LabelAndConfiguration lac = (LabelAndConfiguration) skyKey.argument();
+ ConfiguredTargetValue ctValue = (ConfiguredTargetValue)
+ env.getValue(ConfiguredTargetValue.key(lac.getLabel(), lac.getConfiguration()));
+ TopLevelArtifactContext topLevelContext = PrecomputedValue.TOP_LEVEL_CONTEXT.get(env);
+ if (env.valuesMissing()) {
+ return null;
+ }
+
+ Map<SkyKey, ValueOrException2<MissingInputFileException, ActionExecutionException>> inputDeps =
+ env.getValuesOrThrow(ArtifactValue.mandatoryKeys(
+ TopLevelArtifactHelper.getAllArtifactsToBuild(
+ ctValue.getConfiguredTarget(), topLevelContext)),
+ MissingInputFileException.class, ActionExecutionException.class);
+
+ int missingCount = 0;
+ ActionExecutionException firstActionExecutionException = null;
+ MissingInputFileException missingInputException = null;
+ NestedSetBuilder<Label> rootCausesBuilder = NestedSetBuilder.stableOrder();
+ for (Map.Entry<SkyKey, ValueOrException2<MissingInputFileException,
+ ActionExecutionException>> depsEntry : inputDeps.entrySet()) {
+ Artifact input = ArtifactValue.artifact(depsEntry.getKey());
+ try {
+ depsEntry.getValue().get();
+ } catch (MissingInputFileException e) {
+ missingCount++;
+ if (input.getOwner() != null) {
+ rootCausesBuilder.add(input.getOwner());
+ env.getListener().handle(Event.error(
+ ctValue.getConfiguredTarget().getTarget().getLocation(),
+ String.format("%s: missing input file '%s'",
+ lac.getLabel(), input.getOwner())));
+ }
+ } catch (ActionExecutionException e) {
+ rootCausesBuilder.addTransitive(e.getRootCauses());
+ if (firstActionExecutionException == null) {
+ firstActionExecutionException = e;
+ }
+ }
+ }
+
+ if (missingCount > 0) {
+ missingInputException = new MissingInputFileException(
+ ctValue.getConfiguredTarget().getTarget().getLocation() + " " + missingCount
+ + " input file(s) do not exist", ctValue.getConfiguredTarget().getTarget().getLocation());
+ }
+
+ NestedSet<Label> rootCauses = rootCausesBuilder.build();
+ if (!rootCauses.isEmpty()) {
+ eventBusRef.get().post(
+ TargetCompleteEvent.createFailed(ctValue.getConfiguredTarget(), rootCauses));
+ if (firstActionExecutionException != null) {
+ throw new TargetCompletionFunctionException(firstActionExecutionException);
+ } else {
+ throw new TargetCompletionFunctionException(missingInputException);
+ }
+ }
+
+ return env.valuesMissing() ? null : new TargetCompletionValue(ctValue.getConfiguredTarget());
+ }
+
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return Label.print(((LabelAndConfiguration) skyKey.argument()).getLabel());
+ }
+
+ private static final class TargetCompletionFunctionException extends SkyFunctionException {
+
+ private final ActionExecutionException actionException;
+
+ public TargetCompletionFunctionException(ActionExecutionException e) {
+ super(e, Transience.PERSISTENT);
+ this.actionException = e;
+ }
+
+ public TargetCompletionFunctionException(MissingInputFileException e) {
+ super(e, Transience.TRANSIENT);
+ this.actionException = null;
+ }
+
+ @Override
+ public boolean isCatastrophic() {
+ return actionException != null && actionException.isCatastrophe();
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/TargetCompletionValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/TargetCompletionValue.java
new file mode 100644
index 0000000..5d7153d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/TargetCompletionValue.java
@@ -0,0 +1,51 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.LabelAndConfiguration;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.util.Collection;
+
+/**
+ * The value of a TargetCompletion. Currently this just stores a ConfiguredTarget.
+ */
+public class TargetCompletionValue implements SkyValue {
+ private final ConfiguredTarget ct;
+
+ TargetCompletionValue(ConfiguredTarget ct) {
+ this.ct = ct;
+ }
+
+ public ConfiguredTarget getConfiguredTarget() {
+ return ct;
+ }
+
+ public static SkyKey key(LabelAndConfiguration labelAndConfiguration) {
+ return new SkyKey(SkyFunctions.TARGET_COMPLETION, labelAndConfiguration);
+ }
+
+ public static Iterable<SkyKey> keys(Collection<ConfiguredTarget> targets) {
+ return Iterables.transform(targets, new Function<ConfiguredTarget, SkyKey>() {
+ @Override
+ public SkyKey apply(ConfiguredTarget ct) {
+ return new SkyKey(SkyFunctions.TARGET_COMPLETION, new LabelAndConfiguration(ct));
+ }
+ });
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/TargetMarkerFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/TargetMarkerFunction.java
new file mode 100644
index 0000000..3f9f22f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/TargetMarkerFunction.java
@@ -0,0 +1,134 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.devtools.build.lib.packages.BuildFileNotFoundException;
+import com.google.devtools.build.lib.packages.NoSuchPackageException;
+import com.google.devtools.build.lib.packages.NoSuchTargetException;
+import com.google.devtools.build.lib.packages.Package;
+import com.google.devtools.build.lib.packages.PackageIdentifier;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyFunctionException;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+/**
+ * A SkyFunction for {@link TargetMarkerValue}s.
+ */
+public final class TargetMarkerFunction implements SkyFunction {
+
+ public TargetMarkerFunction() {
+ }
+
+ @Override
+ public SkyValue compute(SkyKey key, Environment env) throws TargetMarkerFunctionException {
+ Label label = (Label) key.argument();
+ PathFragment pkgForLabel = label.getPackageFragment();
+
+ if (label.getName().contains("/")) {
+ // This target is in a subdirectory, therefore it could potentially be invalidated by
+ // a new BUILD file appearing in the hierarchy.
+ PathFragment containingDirectory = label.toPathFragment().getParentDirectory();
+ ContainingPackageLookupValue containingPackageLookupValue = null;
+ try {
+ PackageIdentifier newPkgId = new PackageIdentifier(
+ label.getPackageIdentifier().getRepository(), containingDirectory);
+ containingPackageLookupValue = (ContainingPackageLookupValue) env.getValueOrThrow(
+ ContainingPackageLookupValue.key(newPkgId),
+ BuildFileNotFoundException.class, InconsistentFilesystemException.class);
+ } catch (BuildFileNotFoundException e) {
+ // Thrown when there are IO errors looking for BUILD files.
+ throw new TargetMarkerFunctionException(e);
+ } catch (InconsistentFilesystemException e) {
+ throw new TargetMarkerFunctionException(new NoSuchTargetException(label,
+ e.getMessage()));
+ }
+ if (containingPackageLookupValue == null) {
+ return null;
+ }
+ if (!containingPackageLookupValue.hasContainingPackage()) {
+ // This means the label's package doesn't exist. E.g. there is no package 'a' and we are
+ // trying to build the target for label 'a:b/foo'.
+ throw new TargetMarkerFunctionException(new BuildFileNotFoundException(
+ pkgForLabel.getPathString(), "BUILD file not found on package path for '"
+ + pkgForLabel.getPathString() + "'"));
+ }
+ if (!containingPackageLookupValue.getContainingPackageName().equals(
+ label.getPackageIdentifier())) {
+ throw new TargetMarkerFunctionException(new NoSuchTargetException(label,
+ String.format("Label '%s' crosses boundary of subpackage '%s'", label,
+ containingPackageLookupValue.getContainingPackageName())));
+ }
+ }
+
+ SkyKey pkgSkyKey = PackageValue.key(label.getPackageIdentifier());
+ NoSuchPackageException nspe = null;
+ Package pkg;
+ try {
+ PackageValue value = (PackageValue)
+ env.getValueOrThrow(pkgSkyKey, NoSuchPackageException.class);
+ if (value == null) {
+ return null;
+ }
+ pkg = value.getPackage();
+ } catch (NoSuchPackageException e) {
+ // For consistency with pre-Skyframe Blaze, we can return a valid Target from a Package
+ // containing errors.
+ pkg = e.getPackage();
+ if (pkg == null) {
+ // Re-throw this exception with our key because root causes should be targets, not packages.
+ throw new TargetMarkerFunctionException(e);
+ }
+ nspe = e;
+ }
+
+ Target target;
+ try {
+ target = pkg.getTarget(label.getName());
+ } catch (NoSuchTargetException e) {
+ throw new TargetMarkerFunctionException(e);
+ }
+
+ if (nspe != null) {
+ // There is a target, but its package is in error. We rethrow so that the root cause is the
+ // target, not the package. Note that targets are only in error when their package is
+ // "in error" (because a package is in error if there was an error evaluating the package, or
+ // if one of its targets was in error).
+ throw new TargetMarkerFunctionException(new NoSuchTargetException(target, nspe));
+ }
+ return TargetMarkerValue.TARGET_MARKER_INSTANCE;
+ }
+
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return Label.print((Label) skyKey.argument());
+ }
+
+ /**
+ * Used to declare all the exception types that can be wrapped in the exception thrown by
+ * {@link TargetMarkerFunction#compute}.
+ */
+ private static final class TargetMarkerFunctionException extends SkyFunctionException {
+ public TargetMarkerFunctionException(NoSuchTargetException e) {
+ super(e, Transience.PERSISTENT);
+ }
+
+ public TargetMarkerFunctionException(NoSuchPackageException e) {
+ super(e, Transience.PERSISTENT);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/TargetMarkerValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/TargetMarkerValue.java
new file mode 100644
index 0000000..eef7084
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/TargetMarkerValue.java
@@ -0,0 +1,38 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+/**
+ * Value represents visited target in the Skyframe graph after error checking.
+ */
+@Immutable
+@ThreadSafe
+public final class TargetMarkerValue implements SkyValue {
+
+ static final TargetMarkerValue TARGET_MARKER_INSTANCE = new TargetMarkerValue();
+
+ private TargetMarkerValue() {
+ }
+
+ @ThreadSafe
+ public static SkyKey key(Label label) {
+ return new SkyKey(SkyFunctions.TARGET_MARKER, label);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/TargetPatternFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/TargetPatternFunction.java
new file mode 100644
index 0000000..6e631ed
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/TargetPatternFunction.java
@@ -0,0 +1,278 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.devtools.build.lib.cmdline.LabelValidator;
+import com.google.devtools.build.lib.cmdline.ResolvedTargets;
+import com.google.devtools.build.lib.cmdline.TargetParsingException;
+import com.google.devtools.build.lib.cmdline.TargetPattern;
+import com.google.devtools.build.lib.cmdline.TargetPatternResolver;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.packages.NoSuchPackageException;
+import com.google.devtools.build.lib.packages.NoSuchThingException;
+import com.google.devtools.build.lib.packages.Package;
+import com.google.devtools.build.lib.packages.PackageIdentifier;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.pkgcache.FilteringPolicies;
+import com.google.devtools.build.lib.pkgcache.FilteringPolicy;
+import com.google.devtools.build.lib.pkgcache.PathPackageLocator;
+import com.google.devtools.build.lib.pkgcache.TargetPatternResolverUtil;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.lib.vfs.RootedPath;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyFunctionException;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import javax.annotation.Nullable;
+
+/**
+ * TargetPatternFunction translates a target pattern (eg, "foo/...") into a set of resolved
+ * Targets.
+ */
+public class TargetPatternFunction implements SkyFunction {
+
+ private final AtomicReference<PathPackageLocator> pkgPath;
+
+ public TargetPatternFunction(AtomicReference<PathPackageLocator> pkgPath) {
+ this.pkgPath = pkgPath;
+ }
+
+ @Override
+ public SkyValue compute(SkyKey key, Environment env) throws TargetPatternFunctionException,
+ InterruptedException {
+ TargetPatternValue.TargetPattern patternKey =
+ ((TargetPatternValue.TargetPattern) key.argument());
+
+ TargetPattern.Parser parser = new TargetPattern.Parser(patternKey.getOffset());
+ try {
+ Resolver resolver = new Resolver(env, patternKey.getPolicy(), pkgPath);
+ TargetPattern resolvedPattern = parser.parse(patternKey.getPattern());
+ return new TargetPatternValue(resolvedPattern.eval(resolver));
+ } catch (TargetParsingException e) {
+ throw new TargetPatternFunctionException(e);
+ } catch (TargetPatternResolver.MissingDepException e) {
+ return null;
+ }
+ }
+
+ @Nullable
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return null;
+ }
+
+ private static class Resolver implements TargetPatternResolver<Target> {
+ private final Environment env;
+ private final FilteringPolicy policy;
+ private final AtomicReference<PathPackageLocator> pkgPath;
+
+ public Resolver(Environment env, FilteringPolicy policy,
+ AtomicReference<PathPackageLocator> pkgPath) {
+ this.policy = policy;
+ this.env = env;
+ this.pkgPath = pkgPath;
+ }
+
+ @Override
+ public void warn(String msg) {
+ env.getListener().handle(Event.warn(msg));
+ }
+
+ /**
+ * Gets a Package via the Skyframe env. May return a Package that has errors.
+ */
+ private Package getPackage(PackageIdentifier pkgIdentifier)
+ throws MissingDepException, NoSuchThingException {
+ SkyKey pkgKey = PackageValue.key(pkgIdentifier);
+ Package pkg;
+ try {
+ PackageValue pkgValue =
+ (PackageValue) env.getValueOrThrow(pkgKey, NoSuchThingException.class);
+ if (pkgValue == null) {
+ throw new MissingDepException();
+ }
+ pkg = pkgValue.getPackage();
+ } catch (NoSuchPackageException e) {
+ pkg = e.getPackage();
+ if (pkg == null) {
+ throw e;
+ }
+ }
+ return pkg;
+ }
+
+ @Override
+ public Target getTargetOrNull(String targetName) throws InterruptedException,
+ MissingDepException {
+ try {
+ Label label = Label.parseAbsolute(targetName);
+ if (!isPackage(label.getPackageName())) {
+ return null;
+ }
+ Package pkg = getPackage(label.getPackageIdentifier());
+ return pkg.getTarget(label.getName());
+ } catch (Label.SyntaxException | NoSuchThingException e) {
+ return null;
+ }
+ }
+
+ @Override
+ public ResolvedTargets<Target> getExplicitTarget(String targetName)
+ throws TargetParsingException, InterruptedException, MissingDepException {
+ Label label = TargetPatternResolverUtil.label(targetName);
+ try {
+ Package pkg = getPackage(label.getPackageIdentifier());
+ Target target = pkg.getTarget(label.getName());
+ return policy.shouldRetain(target, true)
+ ? ResolvedTargets.of(target)
+ : ResolvedTargets.<Target>empty();
+ } catch (NoSuchThingException e) {
+ throw new TargetParsingException(e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public ResolvedTargets<Target> getTargetsInPackage(String originalPattern, String packageName,
+ boolean rulesOnly)
+ throws TargetParsingException, InterruptedException, MissingDepException {
+ FilteringPolicy actualPolicy = rulesOnly
+ ? FilteringPolicies.and(FilteringPolicies.RULES_ONLY, policy)
+ : policy;
+ return getTargetsInPackage(originalPattern, packageName, actualPolicy);
+ }
+
+ private ResolvedTargets<Target> getTargetsInPackage(String originalPattern, String packageName,
+ FilteringPolicy policy)
+ throws TargetParsingException, MissingDepException {
+ // Normalise, e.g "foo//bar" -> "foo/bar"; "foo/" -> "foo":
+ PathFragment packageNameFragment = new PathFragment(packageName);
+ packageName = packageNameFragment.toString();
+
+ // It's possible for this check to pass, but for
+ // Label.validatePackageNameFull to report an error because the
+ // package name is illegal. That's a little weird, but we can live with
+ // that for now--see test case: testBadPackageNameButGoodEnoughForALabel.
+ // (BTW I tried duplicating that validation logic in Label but it was
+ // extremely tricky.)
+ if (LabelValidator.validatePackageName(packageName) != null) {
+ throw new TargetParsingException("'" + packageName + "' is not a valid package name");
+ }
+ if (!isPackage(packageName)) {
+ throw new TargetParsingException(
+ TargetPatternResolverUtil.getParsingErrorMessage(
+ "no such package '" + packageName + "': BUILD file not found on package path",
+ originalPattern));
+ }
+
+ try {
+ Package pkg = getPackage(
+ PackageIdentifier.createInDefaultRepo(packageNameFragment.toString()));
+ return TargetPatternResolverUtil.resolvePackageTargets(pkg, policy);
+ } catch (NoSuchThingException e) {
+ String message = TargetPatternResolverUtil.getParsingErrorMessage(
+ "package contains errors", originalPattern);
+ throw new TargetParsingException(message, e);
+ }
+ }
+
+ @Override
+ public boolean isPackage(String packageName) throws MissingDepException {
+ SkyKey packageLookupKey;
+ packageLookupKey = PackageLookupValue.key(new PathFragment(packageName));
+ PackageLookupValue packageLookupValue = (PackageLookupValue) env.getValue(packageLookupKey);
+ if (packageLookupValue == null) {
+ throw new MissingDepException();
+ }
+ return packageLookupValue.packageExists();
+ }
+
+ @Override
+ public String getTargetKind(Target target) {
+ return target.getTargetKind();
+ }
+
+ @Override
+ public ResolvedTargets<Target> findTargetsBeneathDirectory(
+ String originalPattern, String pathPrefix, boolean rulesOnly)
+ throws TargetParsingException, MissingDepException {
+ FilteringPolicy actualPolicy = rulesOnly
+ ? FilteringPolicies.and(FilteringPolicies.RULES_ONLY, policy)
+ : policy;
+
+ PathFragment directory = new PathFragment(pathPrefix);
+ if (directory.containsUplevelReferences()) {
+ throw new TargetParsingException("up-level references are not permitted: '"
+ + directory.getPathString() + "'");
+ }
+ if (!pathPrefix.isEmpty() && (LabelValidator.validatePackageName(pathPrefix) != null)) {
+ throw new TargetParsingException("'" + pathPrefix + "' is not a valid package name");
+ }
+
+ ResolvedTargets.Builder<Target> builder = ResolvedTargets.builder();
+
+ List<RecursivePkgValue> lookupValues = new ArrayList<>();
+ for (Path root : pkgPath.get().getPathEntries()) {
+ SkyKey key = RecursivePkgValue.key(RootedPath.toRootedPath(root, directory));
+ RecursivePkgValue lookup = (RecursivePkgValue) env.getValue(key);
+ if (lookup != null) {
+ lookupValues.add(lookup);
+ }
+ }
+ if (env.valuesMissing()) {
+ throw new MissingDepException();
+ }
+
+ for (RecursivePkgValue value : lookupValues) {
+ for (String pkg : value.getPackages()) {
+ builder.merge(getTargetsInPackage(originalPattern, pkg, FilteringPolicies.NO_FILTER));
+ }
+ }
+
+ if (builder.isEmpty()) {
+ throw new TargetParsingException("no targets found beneath '" + directory + "'");
+ }
+
+ // Apply the transform after the check so we only return the
+ // error if the tree really contains no targets.
+ ResolvedTargets<Target> intermediateResult = builder.build();
+ ResolvedTargets.Builder<Target> filteredBuilder = ResolvedTargets.builder();
+ if (intermediateResult.hasError()) {
+ filteredBuilder.setError();
+ }
+ for (Target target : intermediateResult.getTargets()) {
+ if (actualPolicy.shouldRetain(target, false)) {
+ filteredBuilder.add(target);
+ }
+ }
+ return filteredBuilder.build();
+ }
+ }
+
+ /**
+ * Used to declare all the exception types that can be wrapped in the exception thrown by
+ * {@link TargetPatternFunction#compute}.
+ */
+ private static final class TargetPatternFunctionException extends SkyFunctionException {
+ public TargetPatternFunctionException(TargetParsingException e) {
+ super(e, Transience.PERSISTENT);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/TargetPatternValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/TargetPatternValue.java
new file mode 100644
index 0000000..c97194f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/TargetPatternValue.java
@@ -0,0 +1,212 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Lists;
+import com.google.devtools.build.lib.cmdline.ResolvedTargets;
+import com.google.devtools.build.lib.cmdline.ResolvedTargets.Builder;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.packages.NoSuchTargetException;
+import com.google.devtools.build.lib.packages.Package;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.pkgcache.FilteringPolicies;
+import com.google.devtools.build.lib.pkgcache.FilteringPolicy;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * A value referring to a computed set of resolved targets. This is used for the results of target
+ * pattern parsing.
+ */
+@Immutable
+@ThreadSafe
+public final class TargetPatternValue implements SkyValue {
+
+ private ResolvedTargets<Target> targets;
+
+ TargetPatternValue(ResolvedTargets<Target> targets) {
+ this.targets = Preconditions.checkNotNull(targets);
+ }
+
+ private void writeObject(ObjectOutputStream out) throws IOException {
+ Set<Package> packages = new LinkedHashSet<>();
+ List<String> ts = new ArrayList<>();
+ List<String> filteredTs = new ArrayList<>();
+ for (Target target : targets.getTargets()) {
+ packages.add(target.getPackage());
+ ts.add(target.getLabel().toString());
+ }
+ for (Target target : targets.getFilteredTargets()) {
+ packages.add(target.getPackage());
+ filteredTs.add(target.getLabel().toString());
+ }
+
+ out.writeObject(packages);
+ out.writeObject(ts);
+ out.writeObject(filteredTs);
+ }
+
+ @SuppressWarnings("unchecked")
+ private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
+ Set<Package> packages = (Set<Package>) in.readObject();
+ List<String> ts = (List<String>) in.readObject();
+ List<String> filteredTs = (List<String>) in.readObject();
+
+ Map<String, Package> packageMap = new HashMap<>();
+ for (Package p : packages) {
+ packageMap.put(p.getName(), p);
+ }
+
+ Builder<Target> builder = ResolvedTargets.<Target>builder();
+ for (String labelString : ts) {
+ builder.add(lookupTarget(packageMap, labelString));
+ }
+
+ for (String labelString : filteredTs) {
+ builder.remove(lookupTarget(packageMap, labelString));
+ }
+ this.targets = builder.build();
+ }
+
+ private static Target lookupTarget(Map<String, Package> packageMap, String labelString) {
+ Label label = Label.parseAbsoluteUnchecked(labelString);
+ Package p = packageMap.get(label.getPackageName());
+ try {
+ return p.getTarget(label.getName());
+ } catch (NoSuchTargetException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ private void readObjectNoData() {
+ throw new IllegalStateException();
+ }
+
+ /**
+ * Create a target pattern value key.
+ *
+ * @param pattern The pattern, eg "-foo/biz...". If the first character is "-", the pattern
+ * is treated as a negative pattern.
+ * @param policy The filtering policy, eg "only return test targets"
+ * @param offset The offset to apply to relative target patterns.
+ */
+ @ThreadSafe
+ public static SkyKey key(String pattern,
+ FilteringPolicy policy,
+ String offset) {
+ return new SkyKey(SkyFunctions.TARGET_PATTERN,
+ pattern.startsWith("-")
+ // Don't apply filters to negative patterns.
+ ? new TargetPattern(pattern.substring(1), FilteringPolicies.NO_FILTER, true, offset)
+ : new TargetPattern(pattern, policy, false, offset));
+ }
+
+ /**
+ * Like above, but accepts a collection of target patterns for the same filtering policy.
+ *
+ * @param patterns The collection of patterns, eg "-foo/biz...". If the first character is "-",
+ * the pattern is treated as a negative pattern.
+ * @param policy The filtering policy, eg "only return test targets"
+ * @param offset The offset to apply to relative target patterns.
+ */
+ @ThreadSafe
+ public static Iterable<SkyKey> keys(Collection<String> patterns,
+ FilteringPolicy policy,
+ String offset) {
+ List<SkyKey> keys = Lists.newArrayListWithCapacity(patterns.size());
+ for (String pattern : patterns) {
+ keys.add(key(pattern, policy, offset));
+ }
+ return keys;
+ }
+
+ public ResolvedTargets<Target> getTargets() {
+ return targets;
+ }
+
+ /**
+ * A TargetPattern is a tuple of pattern (eg, "foo/..."), filtering policy, a relative pattern
+ * offset, and whether it is a positive or negative match.
+ */
+ @ThreadSafe
+ public static class TargetPattern implements Serializable {
+ private final String pattern;
+ private final FilteringPolicy policy;
+ private final boolean isNegative;
+
+ private final String offset;
+
+ public TargetPattern(String pattern, FilteringPolicy policy,
+ boolean isNegative, String offset) {
+ this.pattern = Preconditions.checkNotNull(pattern);
+ this.policy = Preconditions.checkNotNull(policy);
+ this.isNegative = isNegative;
+ this.offset = offset;
+ }
+
+ public String getPattern() {
+ return pattern;
+ }
+
+ public boolean isNegative() {
+ return isNegative;
+ }
+
+ public FilteringPolicy getPolicy() {
+ return policy;
+ }
+
+ public String getOffset() {
+ return offset;
+ }
+
+ @Override
+ public String toString() {
+ return (isNegative ? "-" : "") + pattern;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(pattern, isNegative, policy, offset);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof TargetPattern)) {
+ return false;
+ }
+ TargetPattern other = (TargetPattern) obj;
+
+ return other.isNegative == this.isNegative && other.pattern.equals(this.pattern) &&
+ other.offset.equals(this.offset) && other.policy.equals(this.policy);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/TestCompletionFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/TestCompletionFunction.java
new file mode 100644
index 0000000..b6f9606
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/TestCompletionFunction.java
@@ -0,0 +1,71 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.LabelAndConfiguration;
+import com.google.devtools.build.lib.rules.test.TestProvider;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+/**
+ * TargetCompletionFunction builds all relevant test artifacts of a {@link
+ * com.google.devtools.build.lib.analysis.ConfiguredTarget}. This includes test shards and repeated
+ * runs.
+ */
+public final class TestCompletionFunction implements SkyFunction {
+
+ public TestCompletionFunction() {
+ }
+
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env) {
+ TestCompletionValue.TestCompletionKey key =
+ (TestCompletionValue.TestCompletionKey) skyKey.argument();
+ LabelAndConfiguration lac = key.getLabelAndConfiguration();
+ if (env.getValue(TargetCompletionValue.key(lac)) == null) {
+ return null;
+ }
+
+ ConfiguredTargetValue ctValue = (ConfiguredTargetValue)
+ env.getValue(ConfiguredTargetValue.key(lac.getLabel(), lac.getConfiguration()));
+ if (ctValue == null) {
+ return null;
+ }
+
+ ConfiguredTarget ct = ctValue.getConfiguredTarget();
+ if (key.isExclusiveTesting()) {
+ // Request test artifacts iteratively if testing exclusively.
+ for (Artifact testArtifact : TestProvider.getTestStatusArtifacts(ct)) {
+ if (env.getValue(ArtifactValue.key(testArtifact, /*isMandatory=*/true)) == null) {
+ return null;
+ }
+ }
+ } else {
+ env.getValues(ArtifactValue.mandatoryKeys(TestProvider.getTestStatusArtifacts(ct)));
+ if (env.valuesMissing()) {
+ return null;
+ }
+ }
+ return TestCompletionValue.TEST_COMPLETION_MARKER;
+ }
+
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return Label.print(((LabelAndConfiguration) skyKey.argument()).getLabel());
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/TestCompletionValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/TestCompletionValue.java
new file mode 100644
index 0000000..da944ed
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/TestCompletionValue.java
@@ -0,0 +1,66 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.LabelAndConfiguration;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.util.Collection;
+
+/**
+ * A test completion value represents the completion of a test target. This includes the execution
+ * of all test shards and repeated runs, if applicable.
+ */
+public class TestCompletionValue implements SkyValue {
+ static final TestCompletionValue TEST_COMPLETION_MARKER = new TestCompletionValue();
+
+ private TestCompletionValue() { }
+
+ public static SkyKey key(LabelAndConfiguration lac, boolean exclusive) {
+ return new SkyKey(SkyFunctions.TEST_COMPLETION, new TestCompletionKey(lac, exclusive));
+ }
+
+ public static Iterable<SkyKey> keys(Collection<ConfiguredTarget> targets,
+ final boolean exclusive) {
+ return Iterables.transform(targets, new Function<ConfiguredTarget, SkyKey>() {
+ @Override
+ public SkyKey apply(ConfiguredTarget ct) {
+ return new SkyKey(SkyFunctions.TEST_COMPLETION,
+ new TestCompletionKey(new LabelAndConfiguration(ct), exclusive));
+ }
+ });
+ }
+
+ static class TestCompletionKey {
+ private final LabelAndConfiguration lac;
+ private final boolean exclusiveTesting;
+
+ TestCompletionKey(LabelAndConfiguration lac, boolean exclusive) {
+ this.lac = lac;
+ this.exclusiveTesting = exclusive;
+ }
+
+ public LabelAndConfiguration getLabelAndConfiguration() {
+ return lac;
+ }
+
+ public boolean isExclusiveTesting() {
+ return exclusiveTesting;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/TransitiveTargetCycleReporter.java b/src/main/java/com/google/devtools/build/lib/skyframe/TransitiveTargetCycleReporter.java
new file mode 100644
index 0000000..03bbd25
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/TransitiveTargetCycleReporter.java
@@ -0,0 +1,86 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.devtools.build.lib.packages.PackageGroup;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.pkgcache.LoadedPackageProvider;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.skyframe.CycleInfo;
+import com.google.devtools.build.skyframe.SkyKey;
+
+import java.util.List;
+
+/**
+ * Reports cycles between {@link TransitiveTargetValue}s. These indicates cycles between targets
+ * (e.g. '//a:foo' depends on '//b:bar' and '//b:bar' depends on '//a:foo').
+ */
+class TransitiveTargetCycleReporter extends AbstractLabelCycleReporter {
+
+ private static final Predicate<SkyKey> IS_TRANSITIVE_TARGET_SKY_KEY =
+ SkyFunctions.isSkyFunction(SkyFunctions.TRANSITIVE_TARGET);
+
+ TransitiveTargetCycleReporter(LoadedPackageProvider loadedPackageProvider) {
+ super(loadedPackageProvider);
+ }
+
+ @Override
+ protected boolean canReportCycle(SkyKey topLevelKey, CycleInfo cycleInfo) {
+ return Iterables.all(Iterables.concat(ImmutableList.of(topLevelKey),
+ cycleInfo.getPathToCycle(), cycleInfo.getCycle()),
+ IS_TRANSITIVE_TARGET_SKY_KEY);
+ }
+
+ @Override
+ public String prettyPrint(SkyKey key) {
+ return getLabel(key).toString();
+ }
+
+ @Override
+ protected Label getLabel(SkyKey key) {
+ return (Label) key.argument();
+ }
+
+ @Override
+ protected String getAdditionalMessageAboutCycle(SkyKey topLevelKey, CycleInfo cycleInfo) {
+ Target currentTarget = getTargetForLabel(getLabel(topLevelKey));
+ List<SkyKey> keys = Lists.newArrayList();
+ if (!cycleInfo.getPathToCycle().isEmpty()) {
+ keys.add(topLevelKey);
+ keys.addAll(cycleInfo.getPathToCycle());
+ }
+ keys.addAll(cycleInfo.getCycle());
+ // Make sure we check the edge from the last element of the cycle to the first element of the
+ // cycle.
+ keys.add(cycleInfo.getCycle().get(0));
+ for (SkyKey nextKey : keys) {
+ Label nextLabel = getLabel(nextKey);
+ Target nextTarget = getTargetForLabel(nextLabel);
+ // This is inefficient but it's no big deal since we only do this when there's a cycle.
+ if (currentTarget.getVisibility().getDependencyLabels().contains(nextLabel)
+ && !nextTarget.getTargetKind().equals(PackageGroup.targetKind())) {
+ return "\nThe cycle is caused by a visibility edge from " + currentTarget.getLabel()
+ + " to the non-package-group target " + nextTarget.getLabel() + " . Note that "
+ + "visibility labels are supposed to be package group targets (which prevents cycles "
+ + "of this form)";
+ }
+ currentTarget = nextTarget;
+ }
+ return "";
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/TransitiveTargetFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/TransitiveTargetFunction.java
new file mode 100644
index 0000000..417cfca
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/TransitiveTargetFunction.java
@@ -0,0 +1,234 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.collect.Lists;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.packages.InputFile;
+import com.google.devtools.build.lib.packages.NoSuchPackageException;
+import com.google.devtools.build.lib.packages.NoSuchTargetException;
+import com.google.devtools.build.lib.packages.NoSuchThingException;
+import com.google.devtools.build.lib.packages.OutputFile;
+import com.google.devtools.build.lib.packages.PackageGroup;
+import com.google.devtools.build.lib.packages.PackageIdentifier;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.packages.TargetUtils;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyFunctionException;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+import com.google.devtools.build.skyframe.ValueOrException;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * This class builds transitive Target values such that evaluating a Target value is similar to
+ * running it through the LabelVisitor.
+ */
+public class TransitiveTargetFunction implements SkyFunction {
+
+ @Override
+ public SkyValue compute(SkyKey key, Environment env) throws TransitiveTargetFunctionException {
+ Label label = (Label) key.argument();
+ SkyKey packageKey = PackageValue.key(label.getPackageIdentifier());
+ SkyKey targetKey = TargetMarkerValue.key(label);
+ Target target;
+ boolean packageLoadedSuccessfully;
+ boolean successfulTransitiveLoading = true;
+ NestedSetBuilder<Label> transitiveRootCauses = NestedSetBuilder.stableOrder();
+ NoSuchTargetException errorLoadingTarget = null;
+ try {
+ TargetMarkerValue targetValue = (TargetMarkerValue) env.getValueOrThrow(targetKey,
+ NoSuchThingException.class);
+ if (targetValue == null) {
+ return null;
+ }
+ PackageValue packageValue = (PackageValue) env.getValueOrThrow(packageKey,
+ NoSuchThingException.class);
+ if (packageValue == null) {
+ return null;
+ }
+
+ packageLoadedSuccessfully = true;
+ target = packageValue.getPackage().getTarget(label.getName());
+ } catch (NoSuchTargetException e) {
+ target = e.getTarget();
+ if (target == null) {
+ throw new TransitiveTargetFunctionException(e);
+ }
+ successfulTransitiveLoading = false;
+ transitiveRootCauses.add(label);
+ errorLoadingTarget = e;
+ packageLoadedSuccessfully = e.getPackageLoadedSuccessfully();
+ } catch (NoSuchPackageException e) {
+ throw new TransitiveTargetFunctionException(e);
+ } catch (NoSuchThingException e) {
+ throw new IllegalStateException(e
+ + " not NoSuchTargetException or NoSuchPackageException");
+ }
+
+ NestedSetBuilder<PackageIdentifier> transitiveSuccessfulPkgs = NestedSetBuilder.stableOrder();
+ NestedSetBuilder<PackageIdentifier> transitiveUnsuccessfulPkgs = NestedSetBuilder.stableOrder();
+ NestedSetBuilder<Label> transitiveTargets = NestedSetBuilder.stableOrder();
+
+ PackageIdentifier packageId = target.getPackage().getPackageIdentifier();
+ if (packageLoadedSuccessfully) {
+ transitiveSuccessfulPkgs.add(packageId);
+ } else {
+ transitiveUnsuccessfulPkgs.add(packageId);
+ }
+ transitiveTargets.add(target.getLabel());
+ for (Map.Entry<SkyKey, ValueOrException<NoSuchThingException>> entry :
+ env.getValuesOrThrow(getLabelDepKeys(target), NoSuchThingException.class).entrySet()) {
+ Label depLabel = (Label) entry.getKey().argument();
+ TransitiveTargetValue transitiveTargetValue;
+ try {
+ transitiveTargetValue = (TransitiveTargetValue) entry.getValue().get();
+ if (transitiveTargetValue == null) {
+ continue;
+ }
+ } catch (NoSuchPackageException | NoSuchTargetException e) {
+ successfulTransitiveLoading = false;
+ transitiveRootCauses.add(depLabel);
+ maybeReportErrorAboutMissingEdge(target, depLabel, e, env.getListener());
+ continue;
+ } catch (NoSuchThingException e) {
+ throw new IllegalStateException("Unexpected Exception type from TransitiveTargetValue.", e);
+ }
+ transitiveSuccessfulPkgs.addTransitive(
+ transitiveTargetValue.getTransitiveSuccessfulPackages());
+ transitiveUnsuccessfulPkgs.addTransitive(
+ transitiveTargetValue.getTransitiveUnsuccessfulPackages());
+ transitiveTargets.addTransitive(transitiveTargetValue.getTransitiveTargets());
+ NestedSet<Label> rootCauses = transitiveTargetValue.getTransitiveRootCauses();
+ if (rootCauses != null) {
+ successfulTransitiveLoading = false;
+ transitiveRootCauses.addTransitive(rootCauses);
+ if (transitiveTargetValue.getErrorLoadingTarget() != null) {
+ maybeReportErrorAboutMissingEdge(target, depLabel,
+ transitiveTargetValue.getErrorLoadingTarget(), env.getListener());
+ }
+ }
+ }
+
+ if (env.valuesMissing()) {
+ return null;
+ }
+
+ NestedSet<PackageIdentifier> successfullyLoadedPackages = transitiveSuccessfulPkgs.build();
+ NestedSet<PackageIdentifier> unsuccessfullyLoadedPackages = transitiveUnsuccessfulPkgs.build();
+ NestedSet<Label> loadedTargets = transitiveTargets.build();
+ if (successfulTransitiveLoading) {
+ return TransitiveTargetValue.successfulTransitiveLoading(successfullyLoadedPackages,
+ unsuccessfullyLoadedPackages, loadedTargets);
+ } else {
+ NestedSet<Label> rootCauses = transitiveRootCauses.build();
+ return TransitiveTargetValue.unsuccessfulTransitiveLoading(successfullyLoadedPackages,
+ unsuccessfullyLoadedPackages, loadedTargets, rootCauses, errorLoadingTarget);
+ }
+ }
+
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return Label.print(((Label) skyKey.argument()));
+ }
+
+ private static void maybeReportErrorAboutMissingEdge(Target target, Label depLabel,
+ NoSuchThingException e, EventHandler eventHandler) {
+ if (e instanceof NoSuchTargetException) {
+ NoSuchTargetException nste = (NoSuchTargetException) e;
+ if (depLabel.equals(nste.getLabel())) {
+ eventHandler.handle(Event.error(TargetUtils.getLocationMaybe(target),
+ TargetUtils.formatMissingEdge(target, depLabel, e)));
+ }
+ } else if (e instanceof NoSuchPackageException) {
+ NoSuchPackageException nspe = (NoSuchPackageException) e;
+ if (nspe.getPackageName().equals(depLabel.getPackageName())) {
+ eventHandler.handle(Event.error(TargetUtils.getLocationMaybe(target),
+ TargetUtils.formatMissingEdge(target, depLabel, e)));
+ }
+ }
+ }
+
+ private static Iterable<SkyKey> getLabelDepKeys(Target target) {
+ List<SkyKey> depKeys = Lists.newArrayList();
+ for (Label depLabel : getLabelDeps(target)) {
+ depKeys.add(TransitiveTargetValue.key(depLabel));
+ }
+ return depKeys;
+ }
+
+ // TODO(bazel-team): Unify this logic with that in LabelVisitor, and possibly DependencyResolver.
+ private static Iterable<Label> getLabelDeps(Target target) {
+ final Set<Label> labels = new HashSet<>();
+ if (target instanceof OutputFile) {
+ Rule rule = ((OutputFile) target).getGeneratingRule();
+ labels.add(rule.getLabel());
+ visitTargetVisibility(target, labels);
+ } else if (target instanceof InputFile) {
+ visitTargetVisibility(target, labels);
+ } else if (target instanceof Rule) {
+ visitTargetVisibility(target, labels);
+ labels.addAll(((Rule) target).getLabels(Rule.NO_NODEP_ATTRIBUTES));
+ } else if (target instanceof PackageGroup) {
+ visitPackageGroup((PackageGroup) target, labels);
+ }
+ return labels;
+ }
+
+ private static void visitTargetVisibility(Target target, Set<Label> labels) {
+ for (Label label : target.getVisibility().getDependencyLabels()) {
+ labels.add(label);
+ }
+ }
+
+ private static void visitPackageGroup(PackageGroup packageGroup, Set<Label> labels) {
+ for (final Label include : packageGroup.getIncludes()) {
+ labels.add(include);
+ }
+ }
+
+ /**
+ * Used to declare all the exception types that can be wrapped in the exception thrown by
+ * {@link TransitiveTargetFunction#compute}.
+ */
+ private static class TransitiveTargetFunctionException extends SkyFunctionException {
+ /**
+ * Used to propagate an error from a direct target dependency to the
+ * target that depended on it.
+ */
+ public TransitiveTargetFunctionException(NoSuchPackageException e) {
+ super(e, Transience.PERSISTENT);
+ }
+
+ /**
+ * In nokeep_going mode, used to propagate an error from a direct target dependency to the
+ * target that depended on it.
+ *
+ * In keep_going mode, used the same way, but only for targets that could not be loaded at all
+ * (we proceed with transitive loading on targets that contain errors).
+ */
+ public TransitiveTargetFunctionException(NoSuchTargetException e) {
+ super(e, Transience.PERSISTENT);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/TransitiveTargetValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/TransitiveTargetValue.java
new file mode 100644
index 0000000..69b9638
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/TransitiveTargetValue.java
@@ -0,0 +1,142 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.packages.NoSuchTargetException;
+import com.google.devtools.build.lib.packages.PackageIdentifier;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+/**
+ * A <i>transitive</i> target reference that, when built in skyframe, loads the entire
+ * transitive closure of a target.
+ *
+ * This will probably be unnecessary once other refactorings occur throughout the codebase
+ * which make loading/analysis interleaving more feasible, or we will migrate "blaze query" to
+ * use this to evaluate its Target graph.
+ */
+@Immutable
+@ThreadSafe
+public class TransitiveTargetValue implements SkyValue {
+
+ // Non-final for serialization purposes.
+ private NestedSet<PackageIdentifier> transitiveSuccessfulPkgs;
+ private NestedSet<PackageIdentifier> transitiveUnsuccessfulPkgs;
+ private NestedSet<Label> transitiveTargets;
+ @Nullable private NestedSet<Label> transitiveRootCauses;
+ @Nullable private NoSuchTargetException errorLoadingTarget;
+
+ private TransitiveTargetValue(NestedSet<PackageIdentifier> transitiveSuccessfulPkgs,
+ NestedSet<PackageIdentifier> transitiveUnsuccessfulPkgs, NestedSet<Label> transitiveTargets,
+ @Nullable NestedSet<Label> transitiveRootCauses,
+ @Nullable NoSuchTargetException errorLoadingTarget) {
+ this.transitiveSuccessfulPkgs = transitiveSuccessfulPkgs;
+ this.transitiveUnsuccessfulPkgs = transitiveUnsuccessfulPkgs;
+ this.transitiveTargets = transitiveTargets;
+ this.transitiveRootCauses = transitiveRootCauses;
+ this.errorLoadingTarget = errorLoadingTarget;
+ }
+
+ private void writeObject(ObjectOutputStream out) throws IOException {
+ // It helps to flatten the transitiveSuccessfulPkgs nested set as it has lots of duplicates.
+ Set<PackageIdentifier> successfulPkgs = transitiveSuccessfulPkgs.toSet();
+ out.writeInt(successfulPkgs.size());
+ for (PackageIdentifier pkg : successfulPkgs) {
+ out.writeObject(pkg);
+ }
+
+ out.writeObject(transitiveUnsuccessfulPkgs);
+ // Deliberately do not write out transitiveTargets. There is a lot of those and they drive
+ // serialization costs through the roof, both in terms of space and of time.
+ // TODO(bazel-team): Deal with this properly once we have efficient serialization of NestedSets.
+ out.writeObject(transitiveRootCauses);
+ out.writeObject(errorLoadingTarget);
+ }
+
+ @SuppressWarnings("unchecked")
+ private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
+ int successfulPkgCount = in.readInt();
+ NestedSetBuilder<PackageIdentifier> pkgs = NestedSetBuilder.stableOrder();
+ for (int i = 0; i < successfulPkgCount; i++) {
+ pkgs.add((PackageIdentifier) in.readObject());
+ }
+ transitiveSuccessfulPkgs = pkgs.build();
+ transitiveUnsuccessfulPkgs = (NestedSet<PackageIdentifier>) in.readObject();
+ // TODO(bazel-team): Deal with transitiveTargets properly.
+ transitiveTargets = NestedSetBuilder.emptySet(Order.STABLE_ORDER);
+ transitiveRootCauses = (NestedSet<Label>) in.readObject();
+ errorLoadingTarget = (NoSuchTargetException) in.readObject();
+ }
+
+ static TransitiveTargetValue unsuccessfulTransitiveLoading(
+ NestedSet<PackageIdentifier> transitiveSuccessfulPkgs,
+ NestedSet<PackageIdentifier> transitiveUnsuccessfulPkgs, NestedSet<Label> transitiveTargets,
+ NestedSet<Label> rootCauses, @Nullable NoSuchTargetException errorLoadingTarget) {
+ return new TransitiveTargetValue(transitiveSuccessfulPkgs, transitiveUnsuccessfulPkgs,
+ transitiveTargets, rootCauses, errorLoadingTarget);
+ }
+
+ static TransitiveTargetValue successfulTransitiveLoading(
+ NestedSet<PackageIdentifier> transitiveSuccessfulPkgs,
+ NestedSet<PackageIdentifier> transitiveUnsuccessfulPkgs,
+ NestedSet<Label> transitiveTargets) {
+ return new TransitiveTargetValue(transitiveSuccessfulPkgs, transitiveUnsuccessfulPkgs,
+ transitiveTargets, null, null);
+ }
+
+ /** Returns the error, if any, from loading the target. */
+ @Nullable
+ public NoSuchTargetException getErrorLoadingTarget() {
+ return errorLoadingTarget;
+ }
+
+ /** Returns the packages that were transitively successfully loaded. */
+ public NestedSet<PackageIdentifier> getTransitiveSuccessfulPackages() {
+ return transitiveSuccessfulPkgs;
+ }
+
+ /** Returns the packages that were transitively successfully loaded. */
+ public NestedSet<PackageIdentifier> getTransitiveUnsuccessfulPackages() {
+ return transitiveUnsuccessfulPkgs;
+ }
+
+ /** Returns the targets that were transitively loaded. */
+ public NestedSet<Label> getTransitiveTargets() {
+ return transitiveTargets;
+ }
+
+ /** Returns the root causes, if any, of why targets weren't loaded. */
+ @Nullable
+ public NestedSet<Label> getTransitiveRootCauses() {
+ return transitiveRootCauses;
+ }
+
+ @ThreadSafe
+ public static SkyKey key(Label label) {
+ return new SkyKey(SkyFunctions.TRANSITIVE_TARGET, label);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/WorkspaceFileFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/WorkspaceFileFunction.java
new file mode 100644
index 0000000..f518b8a
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/WorkspaceFileFunction.java
@@ -0,0 +1,224 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import static com.google.devtools.build.lib.syntax.Environment.NONE;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.cmdline.LabelValidator;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.StoredEventHandler;
+import com.google.devtools.build.lib.packages.ExternalPackage.Binding;
+import com.google.devtools.build.lib.packages.ExternalPackage.ExternalPackageBuilder;
+import com.google.devtools.build.lib.packages.ExternalPackage.ExternalPackageBuilder.NoSuchBindingException;
+import com.google.devtools.build.lib.packages.Package.NameConflictException;
+import com.google.devtools.build.lib.packages.PackageFactory;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.RuleFactory;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.packages.Type.ConversionException;
+import com.google.devtools.build.lib.syntax.AbstractFunction;
+import com.google.devtools.build.lib.syntax.BuildFileAST;
+import com.google.devtools.build.lib.syntax.EvalException;
+import com.google.devtools.build.lib.syntax.FuncallExpression;
+import com.google.devtools.build.lib.syntax.Function;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.syntax.Label.SyntaxException;
+import com.google.devtools.build.lib.syntax.MixedModeFunction;
+import com.google.devtools.build.lib.syntax.ParserInputSource;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.RootedPath;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyFunctionException;
+import com.google.devtools.build.skyframe.SkyFunctionException.Transience;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A SkyFunction to parse WORKSPACE files.
+ */
+public class WorkspaceFileFunction implements SkyFunction {
+
+ private static final String BIND = "bind";
+
+ private final PackageFactory packageFactory;
+
+ WorkspaceFileFunction(PackageFactory packageFactory) {
+ this.packageFactory = packageFactory;
+ }
+
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env) throws WorkspaceFileFunctionException,
+ InterruptedException {
+ RootedPath workspaceRoot = (RootedPath) skyKey.argument();
+ // Explicitly make skyframe load this file.
+ if (env.getValue(FileValue.key(workspaceRoot)) == null) {
+ return null;
+ }
+ Path workspaceFilePath = workspaceRoot.getRoot().getRelative(workspaceRoot.getRelativePath());
+ WorkspaceNameHolder holder = new WorkspaceNameHolder();
+ ExternalPackageBuilder builder = new ExternalPackageBuilder(workspaceFilePath);
+ StoredEventHandler localReporter = new StoredEventHandler();
+ BuildFileAST buildFileAST;
+ ParserInputSource inputSource = null;
+
+ try {
+ inputSource = ParserInputSource.create(workspaceFilePath);
+ } catch (IOException e) {
+ throw new WorkspaceFileFunctionException(e, Transience.TRANSIENT);
+ }
+ buildFileAST = BuildFileAST.parseBuildFile(inputSource, localReporter, null, false);
+ if (buildFileAST.containsErrors()) {
+ localReporter.handle(Event.error("WORKSPACE file could not be parsed"));
+ } else {
+ try {
+ if (!evaluateWorkspaceFile(buildFileAST, holder, builder)) {
+ localReporter.handle(
+ Event.error("Error evaluating WORKSPACE file " + workspaceFilePath));
+ }
+ } catch (EvalException e) {
+ throw new WorkspaceFileFunctionException(e);
+ }
+ }
+
+ builder.addEvents(localReporter.getEvents());
+ if (localReporter.hasErrors()) {
+ builder.setContainsErrors();
+ }
+ return new WorkspaceFileValue(holder.workspaceName, builder.build());
+ }
+
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return null;
+ }
+
+ private static Function newWorkspaceNameFunction(final WorkspaceNameHolder holder) {
+ List<String> params = ImmutableList.of("name");
+ return new MixedModeFunction("workspace", params, 1, true) {
+ @Override
+ public Object call(Object[] namedArgs, FuncallExpression ast) throws EvalException,
+ ConversionException, InterruptedException {
+ String name = Type.STRING.convert(namedArgs[0], "'name' argument");
+ String errorMessage = LabelValidator.validateTargetName(name);
+ if (errorMessage != null) {
+ throw new EvalException(ast.getLocation(), errorMessage);
+ }
+ holder.workspaceName = name;
+ return NONE;
+ }
+ };
+ }
+
+ private static Function newBindFunction(final ExternalPackageBuilder builder) {
+ List<String> params = ImmutableList.of("name", "actual");
+ return new MixedModeFunction(BIND, params, 2, true) {
+ @Override
+ public Object call(Object[] namedArgs, FuncallExpression ast)
+ throws EvalException, ConversionException {
+ String name = Type.STRING.convert(namedArgs[0], "'name' argument");
+ String actual = Type.STRING.convert(namedArgs[1], "'actual' argument");
+
+ Label nameLabel = null;
+ try {
+ nameLabel = Label.parseAbsolute("//external:" + name);
+ builder.addBinding(
+ nameLabel, new Binding(Label.parseRepositoryLabel(actual), ast.getLocation()));
+ } catch (SyntaxException e) {
+ throw new EvalException(ast.getLocation(), e.getMessage());
+ }
+
+ return NONE;
+ }
+ };
+ }
+
+ /**
+ * Returns a function-value implementing the build rule "ruleClass" (e.g. cc_library) in the
+ * specified package context.
+ */
+ private static Function newRuleFunction(final RuleFactory ruleFactory,
+ final ExternalPackageBuilder builder, final String ruleClassName) {
+ return new AbstractFunction(ruleClassName) {
+ @Override
+ public Object call(List<Object> args, Map<String, Object> kwargs, FuncallExpression ast,
+ com.google.devtools.build.lib.syntax.Environment env)
+ throws EvalException {
+ if (!args.isEmpty()) {
+ throw new EvalException(ast.getLocation(),
+ "build rules do not accept positional parameters");
+ }
+
+ try {
+ RuleClass ruleClass = ruleFactory.getRuleClass(ruleClassName);
+ builder.createAndAddRepositoryRule(ruleClass, kwargs, ast);
+ } catch (RuleFactory.InvalidRuleException | NameConflictException | SyntaxException e) {
+ throw new EvalException(ast.getLocation(), e.getMessage());
+ }
+ return NONE;
+ }
+ };
+ }
+
+ public boolean evaluateWorkspaceFile(BuildFileAST buildFileAST, WorkspaceNameHolder holder,
+ ExternalPackageBuilder builder)
+ throws InterruptedException, EvalException, WorkspaceFileFunctionException {
+ // Environment is defined in SkyFunction and the syntax package.
+ com.google.devtools.build.lib.syntax.Environment workspaceEnv =
+ new com.google.devtools.build.lib.syntax.Environment();
+
+ RuleFactory ruleFactory = new RuleFactory(packageFactory.getRuleClassProvider());
+ for (String ruleClass : ruleFactory.getRuleClassNames()) {
+ Function ruleFunction = newRuleFunction(ruleFactory, builder, ruleClass);
+ workspaceEnv.update(ruleClass, ruleFunction);
+ }
+
+ workspaceEnv.update(BIND, newBindFunction(builder));
+ workspaceEnv.update("workspace", newWorkspaceNameFunction(holder));
+
+ StoredEventHandler eventHandler = new StoredEventHandler();
+ if (!buildFileAST.exec(workspaceEnv, eventHandler)) {
+ return false;
+ }
+ try {
+ builder.resolveBindTargets(packageFactory.getRuleClass(BIND));
+ } catch (NoSuchBindingException e) {
+ throw new WorkspaceFileFunctionException(e);
+ }
+ return true;
+ }
+
+ private static final class WorkspaceNameHolder {
+ String workspaceName;
+ }
+
+ private static final class WorkspaceFileFunctionException extends SkyFunctionException {
+ public WorkspaceFileFunctionException(IOException e, Transience transience) {
+ super(e, transience);
+ }
+
+ public WorkspaceFileFunctionException(NoSuchBindingException e) {
+ super(e, Transience.PERSISTENT);
+ }
+
+ public WorkspaceFileFunctionException(EvalException e) {
+ super(e, Transience.PERSISTENT);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/WorkspaceFileValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/WorkspaceFileValue.java
new file mode 100644
index 0000000..200f23f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/WorkspaceFileValue.java
@@ -0,0 +1,59 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.devtools.build.lib.packages.ExternalPackage;
+import com.google.devtools.build.lib.vfs.RootedPath;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import javax.annotation.Nullable;
+
+/**
+ * Holds the contents of a WORKSPACE file as the //external package.
+ */
+public class WorkspaceFileValue implements SkyValue {
+
+ private final String workspace;
+ private final ExternalPackage pkg;
+
+ public WorkspaceFileValue(String workspace, ExternalPackage pkg) {
+ this.workspace = workspace;
+ this.pkg = pkg;
+ }
+
+ /**
+ * Returns the name of this workspace (or null for the default workspace).
+ */
+ @Nullable
+ public String getWorkspace() {
+ return workspace;
+ }
+
+ /**
+ * Returns the //external package.
+ */
+ public ExternalPackage getPackage() {
+ return pkg;
+ }
+
+ /**
+ * Generates a SkyKey based on the path to the WORKSPACE file.
+ */
+ public static SkyKey key(RootedPath workspacePath) {
+ return new SkyKey(SkyFunctions.WORKSPACE_FILE, workspacePath);
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/WorkspaceStatusFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/WorkspaceStatusFunction.java
new file mode 100644
index 0000000..b1c5ef3
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/WorkspaceStatusFunction.java
@@ -0,0 +1,48 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.analysis.WorkspaceStatusAction;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+/** Creates the workspace status artifacts and action. */
+public class WorkspaceStatusFunction implements SkyFunction {
+ WorkspaceStatusFunction() {
+ }
+
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env) {
+ Preconditions.checkState(
+ WorkspaceStatusValue.SKY_KEY.equals(skyKey), WorkspaceStatusValue.SKY_KEY);
+
+ WorkspaceStatusAction action = PrecomputedValue.WORKSPACE_STATUS_KEY.get(env);
+ if (action == null) {
+ return null;
+ }
+
+ return new WorkspaceStatusValue(
+ action.getStableStatus(),
+ action.getVolatileStatus(),
+ action);
+ }
+
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return null;
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/WorkspaceStatusValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/WorkspaceStatusValue.java
new file mode 100644
index 0000000..21b9215
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/WorkspaceStatusValue.java
@@ -0,0 +1,62 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.ArtifactOwner;
+import com.google.devtools.build.lib.analysis.WorkspaceStatusAction;
+import com.google.devtools.build.skyframe.SkyFunctionName;
+import com.google.devtools.build.skyframe.SkyKey;
+
+/**
+ * Value that stores the workspace status artifacts and their generating action. There should be
+ * only one of these values in the graph at any time.
+ */
+// TODO(bazel-team): This seems to be superfluous now, but it cannot be removed without making
+// PrecomputedValue public instead of package-private
+public class WorkspaceStatusValue extends ActionLookupValue {
+ private final Artifact stableArtifact;
+ private final Artifact volatileArtifact;
+
+ // There should only ever be one BuildInfo value in the graph.
+ public static final SkyKey SKY_KEY = new SkyKey(SkyFunctions.BUILD_INFO, "BUILD_INFO");
+ static final ArtifactOwner ARTIFACT_OWNER = new BuildInfoKey();
+
+ public WorkspaceStatusValue(Artifact stableArtifact, Artifact volatileArtifact,
+ WorkspaceStatusAction action) {
+ super(action);
+ this.stableArtifact = stableArtifact;
+ this.volatileArtifact = volatileArtifact;
+ }
+
+ public Artifact getStableArtifact() {
+ return stableArtifact;
+ }
+
+ public Artifact getVolatileArtifact() {
+ return volatileArtifact;
+ }
+
+ private static class BuildInfoKey extends ActionLookupKey {
+ @Override
+ SkyFunctionName getType() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ SkyKey getSkyKey() {
+ return SKY_KEY;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/standalone/LinuxSandboxedStrategy.java b/src/main/java/com/google/devtools/build/lib/standalone/LinuxSandboxedStrategy.java
new file mode 100644
index 0000000..dc32ddc
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/standalone/LinuxSandboxedStrategy.java
@@ -0,0 +1,227 @@
+// Copyright 2014 Google Inc. 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.standalone;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.ActionExecutionContext;
+import com.google.devtools.build.lib.actions.ActionInput;
+import com.google.devtools.build.lib.actions.ActionInputHelper;
+import com.google.devtools.build.lib.actions.Actions;
+import com.google.devtools.build.lib.actions.ExecException;
+import com.google.devtools.build.lib.actions.ExecutionStrategy;
+import com.google.devtools.build.lib.actions.Executor;
+import com.google.devtools.build.lib.actions.Spawn;
+import com.google.devtools.build.lib.actions.SpawnActionContext;
+import com.google.devtools.build.lib.actions.UserExecException;
+import com.google.devtools.build.lib.analysis.BlazeDirectories;
+import com.google.devtools.build.lib.rules.cpp.CppCompileAction;
+import com.google.devtools.build.lib.shell.CommandException;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.unix.FilesystemUtils;
+import com.google.devtools.build.lib.util.CommandFailureUtils;
+import com.google.devtools.build.lib.util.DependencySet;
+import com.google.devtools.build.lib.util.io.FileOutErr;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.TreeSet;
+
+/**
+ * Strategy that uses sandboxing to execute a process.
+ */
+@ExecutionStrategy(name = {"sandboxed"},
+ contextType = SpawnActionContext.class)
+public class LinuxSandboxedStrategy implements SpawnActionContext {
+ private final boolean verboseFailures;
+ private final BlazeDirectories directories;
+
+ public LinuxSandboxedStrategy(BlazeDirectories blazeDirectories, boolean verboseFailures) {
+ this.directories = blazeDirectories;
+ this.verboseFailures = verboseFailures;
+ }
+
+ /**
+ * Executes the given {@code spawn}.
+ */
+ @Override
+ public void exec(Spawn spawn, ActionExecutionContext actionExecutionContext)
+ throws ExecException {
+ Executor executor = actionExecutionContext.getExecutor();
+ if (executor.reportsSubcommands()) {
+ executor.reportSubcommand(Label.print(spawn.getOwner().getLabel()),
+ spawn.asShellCommand(executor.getExecRoot()));
+ }
+ boolean processHeaders = spawn.getResourceOwner() instanceof CppCompileAction;
+
+ Path execPath = this.directories.getExecRoot();
+ List<String> spawnArguments = new ArrayList<>();
+
+ for (String arg : spawn.getArguments()) {
+ if (arg.startsWith(execPath.getPathString())) {
+ // make all paths relative for the sandbox
+ spawnArguments.add(arg.substring(execPath.getPathString().length()));
+ } else {
+ spawnArguments.add(arg);
+ }
+ }
+
+ List<? extends ActionInput> expandedInputs =
+ ActionInputHelper.expandMiddlemen(spawn.getInputFiles(),
+ actionExecutionContext.getMiddlemanExpander());
+
+ String cwd = executor.getExecRoot().getPathString();
+
+ FileOutErr outErr = actionExecutionContext.getFileOutErr();
+ try {
+ PathFragment includePrefix = null; // null when there's no include mangling to do
+ List<PathFragment> includeDirectories = ImmutableList.of();
+ if (processHeaders) {
+ CppCompileAction cppAction = (CppCompileAction) spawn.getResourceOwner();
+ // headers are mounted in the sandbox in a separate include dir, so their names are mangled
+ // when running the compilation and will have to be unmangled after it's done in the *.pic.d
+ includeDirectories = extractIncludeDirs(execPath, cppAction, spawnArguments);
+ includePrefix = getSandboxIncludeDir(cppAction);
+ }
+
+ NamespaceSandboxRunner runner = new NamespaceSandboxRunner(directories, spawn, includePrefix,
+ includeDirectories, spawn.getRunfilesManifests());
+ runner.setupSandbox(expandedInputs, spawn.getOutputFiles());
+ runner.run(spawnArguments, spawn.getEnvironment(), new File(cwd), outErr);
+ runner.copyOutputs(spawn.getOutputFiles(), outErr);
+ if (processHeaders) {
+ CppCompileAction cppAction = (CppCompileAction) spawn.getResourceOwner();
+ unmangleHeaderFiles(cppAction);
+ }
+ runner.cleanup();
+ } catch (CommandException e) {
+ String message = CommandFailureUtils.describeCommandFailure(verboseFailures,
+ spawn.getArguments(), spawn.getEnvironment(), cwd);
+ throw new UserExecException(String.format("%s: %s", message, e));
+ } catch (IOException e) {
+ throw new UserExecException(e.getMessage());
+ }
+ }
+
+ private void unmangleHeaderFiles(CppCompileAction cppCompileAction) throws IOException {
+ Path execPath = this.directories.getExecRoot();
+ CppCompileAction.DotdFile dotdfile = cppCompileAction.getDotdFile();
+ DependencySet depset = new DependencySet(execPath).read(dotdfile.getPath());
+ DependencySet unmangled = new DependencySet(execPath);
+ PathFragment sandboxIncludeDir = getSandboxIncludeDir(cppCompileAction);
+ PathFragment prefix = sandboxIncludeDir.getRelative(execPath.asFragment().relativeTo("/"));
+ for (PathFragment dep : depset.getDependencies()) {
+ if (dep.startsWith(prefix)) {
+ dep = dep.relativeTo(prefix);
+ }
+ unmangled.addDependency(dep);
+ }
+ unmangled.write(execPath.getRelative(depset.getOutputFileName()), ".d");
+ }
+
+ private PathFragment getSandboxIncludeDir(CppCompileAction cppCompileAction) {
+ return new PathFragment(
+ "include-" + Actions.escapedPath(cppCompileAction.getPrimaryOutput().toString()));
+ }
+
+ private ImmutableList<PathFragment> extractIncludeDirs(Path execPath,
+ CppCompileAction cppCompileAction, List<String> spawnArguments) throws IOException {
+ List<PathFragment> includes = new ArrayList<>();
+ includes.addAll(cppCompileAction.getQuoteIncludeDirs());
+ includes.addAll(cppCompileAction.getIncludeDirs());
+ includes.addAll(cppCompileAction.getSystemIncludeDirs());
+
+ // gcc implicitly includes headers in the same dir as .cc file
+ PathFragment sourceDirectory =
+ cppCompileAction.getSourceFile().getPath().getParentDirectory().asFragment();
+ includes.add(sourceDirectory);
+ spawnArguments.add("-iquote");
+ spawnArguments.add(sourceDirectory.toString());
+
+ TreeSet<PathFragment> processedIncludes = new TreeSet<>();
+ for (int i = 0; i < includes.size(); i++) {
+ PathFragment absolutePath;
+ if (!includes.get(i).isAbsolute()) {
+ absolutePath = execPath.getRelative(includes.get(i)).asFragment();
+ } else {
+ absolutePath = includes.get(i);
+ }
+ // CppCompileAction may provide execPath as one of the include directories. This is a big
+ // overestimation of what is actually needed and doesn't make for very hermetic sandbox
+ // (since everything from the workspace will be somehow accessed in the sandbox). To have
+ // some more hermeticity in this situation we mount all the include dirs in:
+ // sandbox-directory/include-prefix/actual-include-dir
+ // (where include-prefix is obtained from this.getSandboxIncludeDir(cppCompileAction))
+ // and make so gcc looks there for includes. This should prevent the user from accessing
+ // files that technically should not be in the sandbox.
+ // TODO(bazel-team): change CppCompileAction so that include dirs contain only subsets of the
+ // execPath
+ if (absolutePath.equals(execPath.asFragment())) {
+ // we can't mount execPath because it will lead to a circular mount; instead mount its
+ // subdirs inside (other than the ones containing sandbox)
+ String[] subdirs = FilesystemUtils.readdir(absolutePath.toString());
+ for (String dirName : subdirs) {
+ if (dirName.equals("_bin") || dirName.equals("bazel-out")) {
+ continue;
+ }
+ PathFragment child = absolutePath.getChild(dirName);
+ processedIncludes.add(child);
+ }
+ } else {
+ processedIncludes.add(absolutePath);
+ }
+ }
+
+ // pseudo random name for include directory inside sandbox, so it won't be accessed by accident
+ String prefix = getSandboxIncludeDir(cppCompileAction).toString();
+
+ // change names in the invocation
+ for (int i = 0; i < spawnArguments.size(); i++) {
+ if (spawnArguments.get(i).startsWith("-I")) {
+ String argument = spawnArguments.get(i).substring(2);
+ spawnArguments.set(i, setIncludeDirSandboxPath(execPath, argument, "-I" + prefix));
+ }
+ if (spawnArguments.get(i).equals("-iquote") || spawnArguments.get(i).equals("-isystem")) {
+ spawnArguments.set(i + 1, setIncludeDirSandboxPath(execPath,
+ spawnArguments.get(i + 1), prefix));
+ }
+ }
+ return ImmutableList.copyOf(processedIncludes);
+ }
+
+ private String setIncludeDirSandboxPath(Path execPath, String argument, String prefix) {
+ StringBuilder builder = new StringBuilder(prefix);
+ if (argument.charAt(0) != '/') {
+ // relative path
+ builder.append(execPath);
+ builder.append('/');
+ }
+ builder.append(argument);
+
+ return builder.toString();
+ }
+
+ @Override
+ public String strategyLocality(String mnemonic, boolean remotable) {
+ return "linux-sandboxing";
+ }
+
+ @Override
+ public boolean isRemotable(String mnemonic, boolean remotable) {
+ return false;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/standalone/LocalSpawnStrategy.java b/src/main/java/com/google/devtools/build/lib/standalone/LocalSpawnStrategy.java
new file mode 100644
index 0000000..fc2387c
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/standalone/LocalSpawnStrategy.java
@@ -0,0 +1,111 @@
+// Copyright 2014 Google Inc. 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.standalone;
+
+import com.google.devtools.build.lib.actions.ActionExecutionContext;
+import com.google.devtools.build.lib.actions.ExecException;
+import com.google.devtools.build.lib.actions.ExecutionStrategy;
+import com.google.devtools.build.lib.actions.Executor;
+import com.google.devtools.build.lib.actions.Spawn;
+import com.google.devtools.build.lib.actions.SpawnActionContext;
+import com.google.devtools.build.lib.actions.UserExecException;
+import com.google.devtools.build.lib.shell.Command;
+import com.google.devtools.build.lib.shell.CommandException;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.util.CommandFailureUtils;
+import com.google.devtools.build.lib.util.OS;
+import com.google.devtools.build.lib.util.OsUtils;
+import com.google.devtools.build.lib.util.io.FileOutErr;
+import com.google.devtools.build.lib.vfs.Path;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Strategy that uses subprocessing to execute a process.
+ */
+@ExecutionStrategy(name = { "standalone" }, contextType = SpawnActionContext.class)
+public class LocalSpawnStrategy implements SpawnActionContext {
+ private final boolean verboseFailures;
+
+ private final Path processWrapper;
+
+ public LocalSpawnStrategy(Path execRoot, boolean verboseFailures) {
+ this.verboseFailures = verboseFailures;
+ this.processWrapper = execRoot.getRelative(
+ "_bin/process-wrapper" + OsUtils.executableExtension());
+ }
+
+ /**
+ * Executes the given {@code spawn}.
+ */
+ @Override
+ public void exec(Spawn spawn,
+ ActionExecutionContext actionExecutionContext)
+ throws ExecException {
+ Executor executor = actionExecutionContext.getExecutor();
+ if (executor.reportsSubcommands()) {
+ executor.reportSubcommand(Label.print(spawn.getOwner().getLabel()),
+ spawn.asShellCommand(executor.getExecRoot()));
+ }
+
+ // We must wrap the subprocess with process-wrapper to kill the process tree.
+ // All actions therefore depend on the process-wrapper file. Since it's embedded,
+ // we don't bother with declaring it as an input.
+ List<String> args = new ArrayList<>();
+ if (OS.getCurrent() != OS.WINDOWS) {
+ // TODO(bazel-team): process-wrapper seems to work on Windows, but requires
+ // additional setup as it is an msys2 binary, so it needs msys2 DLLs on %PATH%.
+ // Disable it for now to make the setup easier and to avoid further PATH hacks.
+ // Ideally we should have a native implementation of process-wrapper for Windows.
+ args.add(processWrapper.getPathString());
+ args.add("-1"); /* timeout */
+ args.add("0"); /* kill delay. */
+
+ // TODO(bazel-team): use process-wrapper redirection so we don't have to
+ // pass test logs through the Java heap.
+ args.add("-"); /* stdout. */
+ args.add("-"); /* stderr. */
+ }
+ args.addAll(spawn.getArguments());
+
+ String cwd = executor.getExecRoot().getPathString();
+ Command cmd = new Command(args.toArray(new String[]{}), spawn.getEnvironment(), new File(cwd));
+
+ FileOutErr outErr = actionExecutionContext.getFileOutErr();
+ try {
+ cmd.execute(
+ /* stdin */ new byte[]{},
+ Command.NO_OBSERVER,
+ outErr.getOutputStream(),
+ outErr.getErrorStream(),
+ /*killSubprocessOnInterrupt*/ true);
+ } catch (CommandException e) {
+ String message = CommandFailureUtils.describeCommandFailure(
+ verboseFailures, spawn.getArguments(), spawn.getEnvironment(), cwd);
+ throw new UserExecException(String.format("%s: %s", message, e));
+ }
+ }
+
+ @Override
+ public String strategyLocality(String mnemonic, boolean remotable) {
+ return "standalone";
+ }
+
+ @Override
+ public boolean isRemotable(String mnemonic, boolean remotable) {
+ return false;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/standalone/NamespaceSandboxRunner.java b/src/main/java/com/google/devtools/build/lib/standalone/NamespaceSandboxRunner.java
new file mode 100644
index 0000000..3c7a8e0
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/standalone/NamespaceSandboxRunner.java
@@ -0,0 +1,267 @@
+// Copyright 2014 Google Inc. 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.standalone;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.io.Files;
+import com.google.devtools.build.lib.actions.ActionInput;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.Spawn;
+import com.google.devtools.build.lib.analysis.BlazeDirectories;
+import com.google.devtools.build.lib.shell.Command;
+import com.google.devtools.build.lib.shell.CommandException;
+import com.google.devtools.build.lib.unix.FilesystemUtils;
+import com.google.devtools.build.lib.util.Fingerprint;
+import com.google.devtools.build.lib.util.io.FileOutErr;
+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.File;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map.Entry;
+
+/**
+ * Helper class for running the namespace sandbox. This runner prepares environment inside the
+ * sandbox (copies inputs, creates file structure), handles sandbox output, performs cleanup and
+ * changes invocation if necessary.
+ */
+public class NamespaceSandboxRunner {
+ private final boolean debug = true;
+ private final PathFragment sandboxDirectory;
+ private final Path sandboxPath;
+ private final List<String> mounts;
+ private final Path embeddedBinaries;
+ private final Path tools;
+ private final ImmutableList<PathFragment> includeDirectories;
+ private final PathFragment includePrefix;
+ private final ImmutableMap<PathFragment, Artifact> manifests;
+ private final Path execRoot;
+
+ public NamespaceSandboxRunner(BlazeDirectories directories, Spawn spawn,
+ PathFragment includePrefix, List<PathFragment> includeDirectories,
+ ImmutableMap<PathFragment, Artifact> manifests) {
+ String md5sum = Fingerprint.md5Digest(spawn.getResourceOwner().getPrimaryOutput().toString());
+ this.sandboxDirectory = new PathFragment("sandbox-root-" + md5sum);
+ this.sandboxPath =
+ directories.getExecRoot().getRelative("sandboxes").getRelative(sandboxDirectory);
+ this.mounts = new ArrayList<>();
+ this.tools = directories.getExecRoot().getChild("tools");
+ this.embeddedBinaries = directories.getEmbeddedBinariesRoot();
+ this.includePrefix = includePrefix;
+ this.includeDirectories = ImmutableList.copyOf(includeDirectories);
+ this.manifests = manifests;
+ this.execRoot = directories.getExecRoot();
+ }
+
+ private void createFileSystem(Collection<? extends ActionInput> outputs) throws IOException {
+ // create the sandboxes' parent directory if needed
+ // TODO(bazel-team): create this with rest of the workspace dirs
+ if (!sandboxPath.getParentDirectory().isDirectory()) {
+ FilesystemUtils.mkdir(sandboxPath.getParentDirectory().getPathString(), 0755);
+ }
+
+ FilesystemUtils.mkdir(sandboxPath.getPathString(), 0755);
+ String[] dirs = { "bin", "etc" };
+ for (String dir : dirs) {
+ FilesystemUtils.mkdir(sandboxPath.getChild(dir).getPathString(), 0755);
+ mounts.add("/" + dir);
+ }
+
+ // usr
+ String[] dirsUsr = { "bin", "include" };
+ FilesystemUtils.mkdir(sandboxPath.getChild("usr").getPathString(), 0755);
+ Path usr = sandboxPath.getChild("usr");
+ for (String dir : dirsUsr) {
+ FilesystemUtils.mkdir(usr.getChild(dir).getPathString(), 0755);
+ mounts.add("/usr/" + dir);
+ }
+ FileSystemUtils.createDirectoryAndParents(usr.getChild("local").getChild("include"));
+ mounts.add("/usr/local/include");
+
+ // shared libs
+ String[] rootDirs = FilesystemUtils.readdir("/");
+ for (String entry : rootDirs) {
+ if (entry.startsWith("lib")) {
+ FilesystemUtils.mkdir(sandboxPath.getChild(entry).getPathString(), 0755);
+ mounts.add("/" + entry);
+ }
+ }
+
+ String[] usrDirs = FilesystemUtils.readdir("/usr/");
+ for (String entry : usrDirs) {
+ if (entry.startsWith("lib")) {
+ String lib = usr.getChild(entry).getPathString();
+ FilesystemUtils.mkdir(lib, 0755);
+ mounts.add("/usr/" + entry);
+ }
+ }
+
+ if (this.includePrefix != null) {
+ FilesystemUtils.mkdir(sandboxPath.getRelative(includePrefix).getPathString(), 0755);
+
+ for (PathFragment fullPath : includeDirectories) {
+ // includeDirectories should be absolute paths like /usr/include/foo.h. we want to combine
+ // them into something like sandbox/include-prefix/usr/include/foo.h - for that we remove
+ // the leading '/' from the path string and concatenate with sandbox/include/prefix
+ FileSystemUtils.createDirectoryAndParents(sandboxPath.getRelative(includePrefix)
+ .getRelative(fullPath.getPathString().substring(1)));
+ }
+ }
+
+ // output directories
+ for (ActionInput output : outputs) {
+ PathFragment parentDirectory =
+ new PathFragment(output.getExecPathString()).getParentDirectory();
+ FileSystemUtils.createDirectoryAndParents(sandboxPath.getRelative(parentDirectory));
+ }
+ }
+
+ public void setupSandbox(List<? extends ActionInput> inputs,
+ Collection<? extends ActionInput> outputs) throws IOException {
+ createFileSystem(outputs);
+ setupBlazeUtils();
+ includeManifests();
+ copyInputs(inputs);
+ }
+
+ private void copyInputs(List<? extends ActionInput> inputs) throws IOException {
+ for (ActionInput input : inputs) {
+ if (input.getExecPathString().contains("internal/_middlemen/")) {
+ continue;
+ }
+ // entire tools will be mounted in the sandbox, so don't copy parts of it
+ if (input.getExecPathString().startsWith("tools/")) {
+ continue;
+ }
+ Path target = sandboxPath.getRelative(input.getExecPathString());
+ Path source = execRoot.getRelative(input.getExecPathString());
+ FileSystemUtils.createDirectoryAndParents(target.getParentDirectory());
+ File targetFile = new File(target.getPathString());
+ // TODO(bazel-team): mount inputs inside sandbox instead of copying
+ Files.copy(new File(source.getPathString()), targetFile);
+ FilesystemUtils.chmod(targetFile, 0755);
+ }
+ }
+
+ private void includeManifests() throws IOException {
+ for (Entry<PathFragment, Artifact> manifest : this.manifests.entrySet()) {
+ String path = manifest.getValue().getPath().getPathString();
+ for (String line : Files.readLines(new File(path), Charset.defaultCharset())) {
+ String[] fields = line.split(" ");
+ String targetPath = sandboxPath.getPathString() + PathFragment.SEPARATOR_CHAR + fields[0];
+ String sourcePath = fields[1];
+ File source = new File(sourcePath);
+ File target = new File(targetPath);
+ Files.createParentDirs(target);
+ Files.copy(source, target);
+ }
+ }
+ }
+
+ private void setupBlazeUtils() throws IOException {
+ Path bin = this.sandboxPath.getChild("_bin");
+ if (!bin.isDirectory()) {
+ FilesystemUtils.mkdir(bin.getPathString(), 0755);
+ }
+ Files.copy(new File(this.embeddedBinaries.getChild("build-runfiles").getPathString()),
+ new File(bin.getChild("build-runfiles").getPathString()));
+ FilesystemUtils.chmod(bin.getChild("build-runfiles").getPathString(), 0755);
+ // TODO(bazel-team) filter tools out of input files instead
+ // some of the tools could be in inputs; we will mount entire tools anyway so it's just
+ // easier to remove them and remount inside sandbox
+ FilesystemUtils.rmTree(sandboxPath.getChild("tools").getPathString());
+ }
+
+
+ /**
+ * Runs given
+ *
+ * @param spawnArguments - arguments of spawn to run inside the sandbox
+ * @param env - environment to run sandbox in
+ * @param cwd - current working directory
+ * @param outErr - error output to capture sandbox's and command's stderr
+ * @throws CommandException
+ */
+ public void run(List<String> spawnArguments, ImmutableMap<String, String> env, File cwd,
+ FileOutErr outErr) throws CommandException {
+ List<String> args = new ArrayList<>();
+ args.add(execRoot.getRelative("_bin/namespace-sandbox").getPathString());
+
+ // Only for c++ compilation
+ if (includePrefix != null) {
+ for (PathFragment include : includeDirectories) {
+ args.add("-n");
+ args.add(include.getPathString());
+ }
+
+ args.add("-N");
+ args.add(includePrefix.getPathString());
+ }
+
+ if (debug) {
+ args.add("-D");
+ }
+ args.add("-t");
+ args.add(tools.getPathString());
+
+ args.add("-S");
+ args.add(sandboxPath.getPathString());
+ for (String mount : mounts) {
+ args.add("-m");
+ args.add(mount);
+ }
+
+ args.add("-C");
+ args.addAll(spawnArguments);
+ Command cmd = new Command(args.toArray(new String[] {}), env, cwd);
+
+ cmd.execute(
+ /* stdin */new byte[] {},
+ Command.NO_OBSERVER,
+ outErr.getOutputStream(),
+ outErr.getErrorStream(),
+ /* killSubprocessOnInterrupt */true);
+ }
+
+
+ public void cleanup() throws IOException {
+ FilesystemUtils.rmTree(sandboxPath.getPathString());
+ }
+
+
+ public void copyOutputs(Collection<? extends ActionInput> outputs, FileOutErr outErr)
+ throws IOException {
+ for (ActionInput output : outputs) {
+ Path source = this.sandboxPath.getRelative(output.getExecPathString());
+ Path target = this.execRoot.getRelative(output.getExecPathString());
+ FileSystemUtils.createDirectoryAndParents(target.getParentDirectory());
+ // TODO(bazel-team): eliminate cases when there are excessive outputs in spawns
+ // (java compilation expects "srclist" file in its outputs which is sometimes not produced)
+ if (source.isFile()) {
+ Files.move(new File(source.getPathString()), new File(target.getPathString()));
+ } else {
+ outErr.getErrorStream().write(("Output wasn't created by action: " + output + "\n")
+ .getBytes(StandardCharsets.UTF_8));
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/standalone/StandaloneContextConsumer.java b/src/main/java/com/google/devtools/build/lib/standalone/StandaloneContextConsumer.java
new file mode 100644
index 0000000..2011327
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/standalone/StandaloneContextConsumer.java
@@ -0,0 +1,57 @@
+// Copyright 2014 Google Inc. 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.standalone;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableMap.Builder;
+import com.google.devtools.build.lib.actions.ActionContextConsumer;
+import com.google.devtools.build.lib.actions.Executor.ActionContext;
+import com.google.devtools.build.lib.actions.SpawnActionContext;
+import com.google.devtools.build.lib.analysis.actions.FileWriteActionContext;
+import com.google.devtools.build.lib.rules.cpp.CppCompileActionContext;
+import com.google.devtools.build.lib.rules.cpp.IncludeScanningContext;
+import com.google.devtools.build.lib.rules.cpp.LinkStrategy;
+import com.google.devtools.build.lib.rules.test.TestStrategy;
+
+import java.util.Map;
+
+/**
+ * {@link ActionContextConsumer} that requests the action contexts necessary for standalone
+ * execution.
+ */
+public class StandaloneContextConsumer implements ActionContextConsumer {
+
+ @Override
+ public Map<String, String> getSpawnActionContexts() {
+ return ImmutableMap.of();
+ }
+
+ @Override
+ public Map<Class<? extends ActionContext>, String> getActionContexts() {
+ Builder<Class<? extends ActionContext>, String> actionContexts =
+ new ImmutableMap.Builder<Class<? extends ActionContext>, String>();
+
+ actionContexts.put(SpawnActionContext.class, "standalone");
+
+ // C++.
+ actionContexts.put(LinkStrategy.class, "");
+ actionContexts.put(IncludeScanningContext.class, "");
+ actionContexts.put(CppCompileActionContext.class, "");
+ actionContexts.put(TestStrategy.class, "");
+ actionContexts.put(FileWriteActionContext.class, "");
+
+ return actionContexts.build();
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/standalone/StandaloneContextProvider.java b/src/main/java/com/google/devtools/build/lib/standalone/StandaloneContextProvider.java
new file mode 100644
index 0000000..dbfac2e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/standalone/StandaloneContextProvider.java
@@ -0,0 +1,126 @@
+// Copyright 2014 Google Inc. 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.standalone;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableList.Builder;
+import com.google.devtools.build.lib.actions.ActionContextProvider;
+import com.google.devtools.build.lib.actions.ActionExecutionContext;
+import com.google.devtools.build.lib.actions.ActionGraph;
+import com.google.devtools.build.lib.actions.ActionInputFileCache;
+import com.google.devtools.build.lib.actions.ActionMetadata;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.ArtifactResolver;
+import com.google.devtools.build.lib.actions.ExecutionStrategy;
+import com.google.devtools.build.lib.actions.Executor.ActionContext;
+import com.google.devtools.build.lib.actions.ExecutorInitException;
+import com.google.devtools.build.lib.buildtool.BuildRequest;
+import com.google.devtools.build.lib.exec.ExecutionOptions;
+import com.google.devtools.build.lib.exec.FileWriteStrategy;
+import com.google.devtools.build.lib.rules.cpp.IncludeScanningContext;
+import com.google.devtools.build.lib.rules.cpp.LocalGccStrategy;
+import com.google.devtools.build.lib.rules.cpp.LocalLinkStrategy;
+import com.google.devtools.build.lib.rules.test.ExclusiveTestStrategy;
+import com.google.devtools.build.lib.rules.test.StandaloneTestStrategy;
+import com.google.devtools.build.lib.rules.test.TestActionContext;
+import com.google.devtools.build.lib.runtime.BlazeRuntime;
+import com.google.devtools.build.lib.util.OS;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
+
+import java.io.IOException;
+
+/**
+ * Provide a standalone, local execution context.
+ */
+public class StandaloneContextProvider implements ActionContextProvider {
+
+ /**
+ * a IncludeScanningContext that does nothing. Since local execution does not need to
+ * discover inclusion in advance, we do not need include scanning.
+ */
+ @ExecutionStrategy(contextType = IncludeScanningContext.class)
+ class DummyIncludeScanningContext implements IncludeScanningContext {
+ @Override
+ public void extractIncludes(ActionExecutionContext actionExecutionContext,
+ ActionMetadata resourceOwner, Artifact primaryInput, Artifact primaryOutput)
+ throws IOException, InterruptedException {
+ FileSystemUtils.writeContent(primaryOutput.getPath(), new byte[]{});
+ }
+
+ @Override
+ public ArtifactResolver getArtifactResolver() {
+ return runtime.getView().getArtifactFactory();
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private final ActionContext localSpawnStrategy;
+ private final ImmutableList<ActionContext> strategies;
+ private final BlazeRuntime runtime;
+
+ public StandaloneContextProvider(
+ BlazeRuntime runtime, BuildRequest buildRequest) {
+ boolean verboseFailures = buildRequest.getOptions(ExecutionOptions.class).verboseFailures;
+
+ localSpawnStrategy = new LocalSpawnStrategy(
+ runtime.getDirectories().getExecRoot(), verboseFailures);
+ this.runtime = runtime;
+
+ TestActionContext testStrategy = new StandaloneTestStrategy(buildRequest,
+ runtime.getStartupOptionsProvider(), runtime.getBinTools(), runtime.getRunfilesPrefix());
+ Builder<ActionContext> strategiesBuilder = ImmutableList.builder();
+ // order of strategies passed to builder is significant - when there are many strategies that
+ // could potentially be used and a spawnActionContext doesn't specify which one it wants, the
+ // last one from strategies list will be used
+
+ // put sandboxed strategy first, as we don't want it by default
+ if (OS.getCurrent() == OS.LINUX) {
+ LinuxSandboxedStrategy sandboxedLinuxStrategy =
+ new LinuxSandboxedStrategy(runtime.getDirectories(), verboseFailures);
+ strategiesBuilder.add(sandboxedLinuxStrategy);
+ }
+ strategiesBuilder.add(
+ localSpawnStrategy,
+ new DummyIncludeScanningContext(),
+ new LocalLinkStrategy(),
+ testStrategy,
+ new ExclusiveTestStrategy(testStrategy),
+ new LocalGccStrategy(buildRequest),
+ new FileWriteStrategy());
+
+
+ this.strategies = strategiesBuilder.build();
+ }
+
+ @Override
+ public Iterable<ActionContext> getActionContexts() {
+ return strategies;
+ }
+
+ @Override
+ public void executorCreated(Iterable<ActionContext> usedContexts) throws ExecutorInitException {
+ }
+
+ @Override
+ public void executionPhaseStarting(
+ ActionInputFileCache actionInputFileCache,
+ ActionGraph actionGraph,
+ Iterable<Artifact> topLevelArtifacts) throws ExecutorInitException {
+ }
+
+ @Override
+ public void executionPhaseEnding() {}
+}
+
+
diff --git a/src/main/java/com/google/devtools/build/lib/standalone/StandaloneModule.java b/src/main/java/com/google/devtools/build/lib/standalone/StandaloneModule.java
new file mode 100644
index 0000000..06bffd0
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/standalone/StandaloneModule.java
@@ -0,0 +1,59 @@
+// Copyright 2014 Google Inc. 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.standalone;
+
+import com.google.common.eventbus.Subscribe;
+import com.google.devtools.build.lib.actions.ActionContextConsumer;
+import com.google.devtools.build.lib.actions.ActionContextProvider;
+import com.google.devtools.build.lib.buildtool.BuildRequest;
+import com.google.devtools.build.lib.buildtool.buildevent.BuildStartingEvent;
+import com.google.devtools.build.lib.runtime.BlazeModule;
+import com.google.devtools.build.lib.runtime.BlazeRuntime;
+import com.google.devtools.build.lib.runtime.Command;
+
+/**
+ * StandaloneModule provides pluggable functionality for blaze.
+ */
+public class StandaloneModule extends BlazeModule {
+ private final ActionContextConsumer actionContextConsumer = new StandaloneContextConsumer();
+ private BuildRequest buildRequest;
+ private BlazeRuntime runtime;
+
+ /**
+ * Returns the action context provider the module contributes to Blaze, if any.
+ */
+ @Override
+ public ActionContextProvider getActionContextProvider() {
+ return new StandaloneContextProvider(runtime, buildRequest);
+ }
+
+ /**
+ * Returns the action context consumer the module contributes to Blaze, if any.
+ */
+ @Override
+ public ActionContextConsumer getActionContextConsumer() {
+ return actionContextConsumer;
+ }
+
+ @Override
+ public void beforeCommand(BlazeRuntime runtime, Command command) {
+ this.runtime = runtime;
+ runtime.getEventBus().register(this);
+ }
+
+ @Subscribe
+ public void buildStarting(BuildStartingEvent event) {
+ buildRequest = event.getRequest();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/ASTNode.java b/src/main/java/com/google/devtools/build/lib/syntax/ASTNode.java
new file mode 100644
index 0000000..81ca584
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/ASTNode.java
@@ -0,0 +1,65 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.devtools.build.lib.events.Location;
+
+import java.io.Serializable;
+
+/**
+ * Root class for nodes in the Abstract Syntax Tree of the Build language.
+ */
+public abstract class ASTNode implements Serializable {
+
+ private Location location;
+
+ protected ASTNode() {}
+
+ @VisibleForTesting // productionVisibility = Visibility.PACKAGE_PRIVATE
+ public void setLocation(Location location) {
+ this.location = location;
+ }
+
+ public Location getLocation() {
+ return location;
+ }
+
+ /**
+ * Print the syntax node in a form useful for debugging. The output is not
+ * precisely specified, and should not be used by pretty-printing routines.
+ */
+ @Override
+ public abstract String toString();
+
+ @Override
+ public int hashCode() {
+ throw new UnsupportedOperationException(); // avoid nondeterminism
+ }
+
+ @Override
+ public boolean equals(Object that) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Implements the double dispatch by calling into the node specific
+ * <code>visit</code> method of the {@link SyntaxTreeVisitor}
+ *
+ * @param visitor the {@link SyntaxTreeVisitor} instance to dispatch to.
+ */
+ public abstract void accept(SyntaxTreeVisitor visitor);
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/AbstractFunction.java b/src/main/java/com/google/devtools/build/lib/syntax/AbstractFunction.java
new file mode 100644
index 0000000..f444c23
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/AbstractFunction.java
@@ -0,0 +1,64 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Partial implementation of Function interface.
+ */
+public abstract class AbstractFunction implements Function {
+
+ private final String name;
+
+ protected AbstractFunction(String name) {
+ this.name = name;
+ }
+
+ /**
+ * Returns the name of this function.
+ */
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public Class<?> getObjectType() {
+ return null;
+ }
+
+ /**
+ * Abstract implementation of Function that accepts no parameters.
+ */
+ public abstract static class NoArgFunction extends AbstractFunction {
+
+ public NoArgFunction(String name) {
+ super(name);
+ }
+
+ @Override
+ public Object call(List<Object> args, Map<String, Object> kwargs, FuncallExpression ast,
+ Environment env) throws EvalException, InterruptedException {
+ if (args.size() != 1 || kwargs.size() != 0) {
+ throw new EvalException(ast.getLocation(), "Invalid number of arguments (expected 0)");
+ }
+ return call(args.get(0), ast, env);
+ }
+
+ public abstract Object call(Object self, FuncallExpression ast, Environment env)
+ throws EvalException, InterruptedException;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/Argument.java b/src/main/java/com/google/devtools/build/lib/syntax/Argument.java
new file mode 100644
index 0000000..0706dee
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/Argument.java
@@ -0,0 +1,122 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+/**
+ * Syntax node for a function argument. This can be a key/value pair such as
+ * appears as a keyword argument to a function call or just an expression that
+ * is used as a positional argument. It also can be used for function definitions
+ * to identify the name (and optionally the default value) of the argument.
+ */
+public final class Argument extends ASTNode {
+
+ private final Ident name;
+
+ private final Expression value;
+
+ private final boolean kwargs;
+
+ /**
+ * Create a new argument.
+ * At call site: name is optional, value is mandatory. kwargs is true for ** arguments.
+ * At definition site: name is mandatory, (default) value is optional.
+ */
+ public Argument(Ident name, Expression value, boolean kwargs) {
+ this.name = name;
+ this.value = value;
+ this.kwargs = kwargs;
+ }
+
+ public Argument(Ident name, Expression value) {
+ this.name = name;
+ this.value = value;
+ this.kwargs = false;
+ }
+
+ /**
+ * Creates an Argument with null as name. It can be used as positional arguments
+ * of function calls.
+ */
+ public Argument(Expression value) {
+ this(null, value);
+ }
+
+ /**
+ * Creates an Argument with null as value. It can be used as a mandatory keyword argument
+ * of a function definition.
+ */
+ public Argument(Ident name) {
+ this(name, null);
+ }
+
+ /**
+ * Returns the name of this keyword argument or null if this argument is
+ * positional.
+ */
+ public Ident getName() {
+ return name;
+ }
+
+ /**
+ * Returns the String value of the Ident of this argument. Shortcut for arg.getName().getName().
+ */
+ public String getArgName() {
+ return name.getName();
+ }
+
+ /**
+ * Returns the syntax of this argument expression.
+ */
+ public Expression getValue() {
+ return value;
+ }
+
+ /**
+ * Returns true if this argument is positional.
+ */
+ public boolean isPositional() {
+ return name == null && !kwargs;
+ }
+
+ /**
+ * Returns true if this argument is a keyword argument.
+ */
+ public boolean isNamed() {
+ return name != null;
+ }
+
+ /**
+ * Returns true if this argument is a **kwargs argument.
+ */
+ public boolean isKwargs() {
+ return kwargs;
+ }
+
+ /**
+ * Returns true if this argument has value.
+ */
+ public boolean hasValue() {
+ return value != null;
+ }
+
+ @Override
+ public String toString() {
+ return isNamed() ? name + "=" + value : String.valueOf(value);
+ }
+
+ @Override
+ public void accept(SyntaxTreeVisitor visitor) {
+ visitor.visit(this);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/AssignmentStatement.java b/src/main/java/com/google/devtools/build/lib/syntax/AssignmentStatement.java
new file mode 100644
index 0000000..619e841
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/AssignmentStatement.java
@@ -0,0 +1,108 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+import com.google.common.base.Preconditions;
+
+/**
+ * Syntax node for an assignment statement.
+ */
+public final class AssignmentStatement extends Statement {
+
+ private final Expression lvalue;
+
+ private final Expression expression;
+
+ /**
+ * Constructs an assignment: "lvalue := value".
+ */
+ AssignmentStatement(Expression lvalue, Expression expression) {
+ this.lvalue = lvalue;
+ this.expression = expression;
+ }
+
+ /**
+ * Returns the LHS of the assignment.
+ */
+ public Expression getLValue() {
+ return lvalue;
+ }
+
+ /**
+ * Returns the RHS of the assignment.
+ */
+ public Expression getExpression() {
+ return expression;
+ }
+
+ @Override
+ public String toString() {
+ return lvalue + " = " + expression + '\n';
+ }
+
+ @Override
+ void exec(Environment env) throws EvalException, InterruptedException {
+ if (!(lvalue instanceof Ident)) {
+ throw new EvalException(getLocation(),
+ "can only assign to variables, not to '" + lvalue + "'");
+ }
+
+ Ident ident = (Ident) lvalue;
+ Object result = expression.eval(env);
+ Preconditions.checkNotNull(result, "result of " + expression + " is null");
+
+ if (env.isSkylarkEnabled()) {
+ // The variable may have been referenced successfully if a global variable
+ // with the same name exists. In this case an Exception needs to be thrown.
+ SkylarkEnvironment skylarkEnv = (SkylarkEnvironment) env;
+ if (skylarkEnv.hasBeenReadGlobalVariable(ident.getName())) {
+ throw new EvalException(getLocation(), "Variable '" + ident.getName()
+ + "' is referenced before assignment."
+ + "The variable is defined in the global scope.");
+ }
+ Class<?> variableType = skylarkEnv.getVariableType(ident.getName());
+ Class<?> resultType = EvalUtils.getSkylarkType(result.getClass());
+ if (variableType != null && !variableType.equals(resultType)
+ && !resultType.equals(Environment.NoneType.class)
+ && !variableType.equals(Environment.NoneType.class)) {
+ throw new EvalException(getLocation(), String.format("Incompatible variable types, "
+ + "trying to assign %s (type of %s) to variable %s which is already %s",
+ EvalUtils.prettyPrintValue(result),
+ EvalUtils.getDatatypeName(result),
+ ident.getName(),
+ EvalUtils.getDataTypeNameFromClass(variableType)));
+ }
+ }
+ env.update(ident.getName(), result);
+ }
+
+ @Override
+ public void accept(SyntaxTreeVisitor visitor) {
+ visitor.visit(this);
+ }
+
+ @Override
+ void validate(ValidationEnvironment env) throws EvalException {
+ // TODO(bazel-team): Implement other validations.
+ if (lvalue instanceof Ident) {
+ Ident ident = (Ident) lvalue;
+ SkylarkType resultType = expression.validate(env);
+ env.update(ident.getName(), resultType, getLocation());
+ } else {
+ throw new EvalException(getLocation(),
+ "can only assign to variables, not to '" + lvalue + "'");
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/BinaryOperatorExpression.java b/src/main/java/com/google/devtools/build/lib/syntax/BinaryOperatorExpression.java
new file mode 100644
index 0000000..f53538f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/BinaryOperatorExpression.java
@@ -0,0 +1,412 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.devtools.build.lib.syntax.ClassObject.SkylarkClassObject;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.IllegalFormatException;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Syntax node for a binary operator expression.
+ */
+public final class BinaryOperatorExpression extends Expression {
+
+ private final Expression lhs;
+
+ private final Expression rhs;
+
+ private final Operator operator;
+
+ public BinaryOperatorExpression(Operator operator,
+ Expression lhs,
+ Expression rhs) {
+ this.lhs = lhs;
+ this.rhs = rhs;
+ this.operator = operator;
+ }
+
+ public Expression getLhs() {
+ return lhs;
+ }
+
+ public Expression getRhs() {
+ return rhs;
+ }
+
+ /**
+ * Returns the operator kind for this binary operation.
+ */
+ public Operator getOperator() {
+ return operator;
+ }
+
+ @Override
+ public String toString() {
+ return lhs + " " + operator + " " + rhs;
+ }
+
+ private int compare(Object lval, Object rval) throws EvalException {
+ if (!(lval instanceof Comparable)) {
+ throw new EvalException(getLocation(), lval + " is not comparable");
+ }
+ try {
+ return ((Comparable) lval).compareTo(rval);
+ } catch (ClassCastException e) {
+ throw new EvalException(getLocation(), "Cannot compare " + EvalUtils.getDatatypeName(lval)
+ + " with " + EvalUtils.getDatatypeName(rval));
+ }
+ }
+
+ @Override
+ Object eval(Environment env) throws EvalException, InterruptedException {
+ Object lval = lhs.eval(env);
+
+ // Short-circuit operators
+ if (operator == Operator.AND) {
+ if (EvalUtils.toBoolean(lval)) {
+ return rhs.eval(env);
+ } else {
+ return lval;
+ }
+ }
+
+ if (operator == Operator.OR) {
+ if (EvalUtils.toBoolean(lval)) {
+ return lval;
+ } else {
+ return rhs.eval(env);
+ }
+ }
+
+ Object rval = rhs.eval(env);
+
+ switch (operator) {
+ case PLUS: {
+ // int + int
+ if (lval instanceof Integer && rval instanceof Integer) {
+ return ((Integer) lval).intValue() + ((Integer) rval).intValue();
+ }
+
+ // string + string
+ if (lval instanceof String && rval instanceof String) {
+ return (String) lval + (String) rval;
+ }
+
+ // list + list, tuple + tuple (list + tuple, tuple + list => error)
+ if (lval instanceof List<?> && rval instanceof List<?>) {
+ List<?> llist = (List<?>) lval;
+ List<?> rlist = (List<?>) rval;
+ if (EvalUtils.isImmutable(llist) != EvalUtils.isImmutable(rlist)) {
+ throw new EvalException(getLocation(), "can only concatenate "
+ + EvalUtils.getDatatypeName(rlist) + " (not \""
+ + EvalUtils.getDatatypeName(llist) + "\") to "
+ + EvalUtils.getDatatypeName(rlist));
+ }
+ if (llist instanceof GlobList<?> || rlist instanceof GlobList<?>) {
+ return GlobList.concat(llist, rlist);
+ } else {
+ List<Object> result = Lists.newArrayListWithCapacity(llist.size() + rlist.size());
+ result.addAll(llist);
+ result.addAll(rlist);
+ return EvalUtils.makeSequence(result, EvalUtils.isImmutable(llist));
+ }
+ }
+
+ if (lval instanceof SkylarkList && rval instanceof SkylarkList) {
+ return SkylarkList.concat((SkylarkList) lval, (SkylarkList) rval, getLocation());
+ }
+
+ if (env.isSkylarkEnabled() && lval instanceof Map<?, ?> && rval instanceof Map<?, ?>) {
+ Map<?, ?> ldict = (Map<?, ?>) lval;
+ Map<?, ?> rdict = (Map<?, ?>) rval;
+ Map<Object, Object> result = Maps.newHashMapWithExpectedSize(ldict.size() + rdict.size());
+ result.putAll(ldict);
+ result.putAll(rdict);
+ return result;
+ }
+
+ if (env.isSkylarkEnabled()
+ && lval instanceof SkylarkClassObject && rval instanceof SkylarkClassObject) {
+ return SkylarkClassObject.concat(
+ (SkylarkClassObject) lval, (SkylarkClassObject) rval, getLocation());
+ }
+
+ if (env.isSkylarkEnabled() && lval instanceof SkylarkNestedSet) {
+ return new SkylarkNestedSet((SkylarkNestedSet) lval, rval, getLocation());
+ }
+ break;
+ }
+
+ case MINUS: {
+ if (lval instanceof Integer && rval instanceof Integer) {
+ return ((Integer) lval).intValue() - ((Integer) rval).intValue();
+ }
+ break;
+ }
+
+ case MULT: {
+ // int * int
+ if (lval instanceof Integer && rval instanceof Integer) {
+ return ((Integer) lval).intValue() * ((Integer) rval).intValue();
+ }
+
+ // string * int
+ if (lval instanceof String && rval instanceof Integer) {
+ return Strings.repeat((String) lval, ((Integer) rval).intValue());
+ }
+
+ // int * string
+ if (lval instanceof Integer && rval instanceof String) {
+ return Strings.repeat((String) rval, ((Integer) lval).intValue());
+ }
+ break;
+ }
+
+ case PERCENT: {
+ // int % int
+ if (lval instanceof Integer && rval instanceof Integer) {
+ return ((Integer) lval).intValue() % ((Integer) rval).intValue();
+ }
+
+ // string % tuple, string % dict, string % anything-else
+ if (lval instanceof String) {
+ try {
+ String pattern = (String) lval;
+ if (rval instanceof List<?>) {
+ List<?> rlist = (List<?>) rval;
+ if (EvalUtils.isTuple(rlist)) {
+ return EvalUtils.formatString(pattern, rlist);
+ }
+ /* string % list: fall thru */
+ }
+ if (rval instanceof SkylarkList) {
+ SkylarkList rlist = (SkylarkList) rval;
+ if (rlist.isTuple()) {
+ return EvalUtils.formatString(pattern, rlist.toList());
+ }
+ }
+
+ return EvalUtils.formatString(pattern,
+ Collections.singletonList(rval));
+ } catch (IllegalFormatException e) {
+ throw new EvalException(getLocation(), e.getMessage());
+ }
+ }
+ break;
+ }
+
+ case EQUALS_EQUALS: {
+ return lval.equals(rval);
+ }
+
+ case NOT_EQUALS: {
+ return !lval.equals(rval);
+ }
+
+ case LESS: {
+ return compare(lval, rval) < 0;
+ }
+
+ case LESS_EQUALS: {
+ return compare(lval, rval) <= 0;
+ }
+
+ case GREATER: {
+ return compare(lval, rval) > 0;
+ }
+
+ case GREATER_EQUALS: {
+ return compare(lval, rval) >= 0;
+ }
+
+ case IN: {
+ if (rval instanceof SkylarkList) {
+ for (Object obj : (SkylarkList) rval) {
+ if (obj.equals(lval)) {
+ return true;
+ }
+ }
+ return false;
+ } else if (rval instanceof Collection<?>) {
+ return ((Collection<?>) rval).contains(lval);
+ } else if (rval instanceof Map<?, ?>) {
+ return ((Map<?, ?>) rval).containsKey(lval);
+ } else if (rval instanceof String) {
+ if (lval instanceof String) {
+ return ((String) rval).contains((String) lval);
+ } else {
+ throw new EvalException(getLocation(),
+ "in operator only works on strings if the left operand is also a string");
+ }
+ } else {
+ throw new EvalException(getLocation(),
+ "in operator only works on lists, tuples, dictionaries and strings");
+ }
+ }
+
+ default: {
+ throw new AssertionError("Unsupported binary operator: " + operator);
+ }
+ } // endswitch
+
+ throw new EvalException(getLocation(),
+ "unsupported operand types for '" + operator + "': '"
+ + EvalUtils.getDatatypeName(lval) + "' and '"
+ + EvalUtils.getDatatypeName(rval) + "'");
+ }
+
+ @Override
+ public void accept(SyntaxTreeVisitor visitor) {
+ visitor.visit(this);
+ }
+
+ @Override
+ SkylarkType validate(ValidationEnvironment env) throws EvalException {
+ SkylarkType ltype = lhs.validate(env);
+ SkylarkType rtype = rhs.validate(env);
+ String lname = EvalUtils.getDataTypeNameFromClass(ltype.getType());
+ String rname = EvalUtils.getDataTypeNameFromClass(rtype.getType());
+
+ switch (operator) {
+ case AND: {
+ return ltype.infer(rtype, "and operator", rhs.getLocation(), lhs.getLocation());
+ }
+
+ case OR: {
+ return ltype.infer(rtype, "or operator", rhs.getLocation(), lhs.getLocation());
+ }
+
+ case PLUS: {
+ // int + int
+ if (ltype == SkylarkType.INT && rtype == SkylarkType.INT) {
+ return SkylarkType.INT;
+ }
+
+ // string + string
+ if (ltype == SkylarkType.STRING && rtype == SkylarkType.STRING) {
+ return SkylarkType.STRING;
+ }
+
+ // list + list
+ if (ltype.isList() && rtype.isList()) {
+ return ltype.infer(rtype, "list concatenation", rhs.getLocation(), lhs.getLocation());
+ }
+
+ // dict + dict
+ if (ltype.isDict() && rtype.isDict()) {
+ return ltype.infer(rtype, "dict concatenation", rhs.getLocation(), lhs.getLocation());
+ }
+
+ // struct + struct
+ if (ltype.isStruct() && rtype.isStruct()) {
+ return SkylarkType.of(ClassObject.class);
+ }
+
+ if (ltype.isNset()) {
+ if (rtype.isNset()) {
+ return ltype.infer(rtype, "nested set", rhs.getLocation(), lhs.getLocation());
+ } else if (rtype.isList()) {
+ return ltype.infer(SkylarkType.of(SkylarkNestedSet.class, rtype.getGenericType1()),
+ "nested set", rhs.getLocation(), lhs.getLocation());
+ }
+ if (rtype != SkylarkType.UNKNOWN) {
+ throw new EvalException(getLocation(), String.format("can only concatenate nested sets "
+ + "with other nested sets or list of items, not '" + rname + "'"));
+ }
+ }
+
+ break;
+ }
+
+ case MULT: {
+ // int * int
+ if (ltype == SkylarkType.INT && rtype == SkylarkType.INT) {
+ return SkylarkType.INT;
+ }
+
+ // string * int
+ if (ltype == SkylarkType.STRING && rtype == SkylarkType.INT) {
+ return SkylarkType.STRING;
+ }
+
+ // int * string
+ if (ltype == SkylarkType.INT && rtype == SkylarkType.STRING) {
+ return SkylarkType.STRING;
+ }
+ break;
+ }
+
+ case MINUS: {
+ if (ltype == SkylarkType.INT && rtype == SkylarkType.INT) {
+ return SkylarkType.INT;
+ }
+ break;
+ }
+
+ case PERCENT: {
+ // int % int
+ if (ltype == SkylarkType.INT && rtype == SkylarkType.INT) {
+ return SkylarkType.INT;
+ }
+
+ // string % tuple, string % dict, string % anything-else
+ if (ltype == SkylarkType.STRING) {
+ return SkylarkType.STRING;
+ }
+ break;
+ }
+
+ case EQUALS_EQUALS:
+ case NOT_EQUALS:
+ case LESS:
+ case LESS_EQUALS:
+ case GREATER:
+ case GREATER_EQUALS: {
+ if (ltype != SkylarkType.UNKNOWN && !(Comparable.class.isAssignableFrom(ltype.getType()))) {
+ throw new EvalException(getLocation(), lname + " is not comparable");
+ }
+ ltype.infer(rtype, "comparison", lhs.getLocation(), rhs.getLocation());
+ return SkylarkType.BOOL;
+ }
+
+ case IN: {
+ if (rtype.isList()
+ || rtype.isSet()
+ || rtype.isDict()
+ || rtype == SkylarkType.STRING) {
+ return SkylarkType.BOOL;
+ } else {
+ if (rtype != SkylarkType.UNKNOWN) {
+ throw new EvalException(getLocation(), String.format("operand 'in' only works on "
+ + "strings, dictionaries, lists, sets or tuples, not on a(n) %s",
+ EvalUtils.getDataTypeNameFromClass(rtype.getType())));
+ }
+ }
+ }
+ } // endswitch
+
+ if (ltype != SkylarkType.UNKNOWN && rtype != SkylarkType.UNKNOWN) {
+ throw new EvalException(getLocation(),
+ "unsupported operand types for '" + operator + "': '" + lname + "' and '" + rname + "'");
+ }
+ return SkylarkType.UNKNOWN;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/BuildFileAST.java b/src/main/java/com/google/devtools/build/lib/syntax/BuildFileAST.java
new file mode 100644
index 0000000..6c85ab1
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/BuildFileAST.java
@@ -0,0 +1,244 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.packages.CachingPackageLocator;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+/**
+ * Abstract syntax node for an entire BUILD file.
+ */
+public class BuildFileAST extends ASTNode {
+
+ private final ImmutableList<Statement> stmts;
+
+ private final ImmutableList<Comment> comments;
+
+ private final ImmutableSet<PathFragment> imports;
+
+ /**
+ * Whether any errors were encountered during scanning or parsing.
+ */
+ private final boolean containsErrors;
+
+ private final String contentHashCode;
+
+ private BuildFileAST(Lexer lexer, List<Statement> preludeStatements, Parser.ParseResult result) {
+ this(lexer, preludeStatements, result, null);
+ }
+
+ private BuildFileAST(Lexer lexer, List<Statement> preludeStatements,
+ Parser.ParseResult result, String contentHashCode) {
+ this.stmts = ImmutableList.<Statement>builder()
+ .addAll(preludeStatements)
+ .addAll(result.statements)
+ .build();
+ this.comments = ImmutableList.copyOf(result.comments);
+ this.containsErrors = result.containsErrors;
+ this.contentHashCode = contentHashCode;
+ this.imports = fetchImports(this.stmts);
+ if (result.statements.size() > 0) {
+ setLocation(lexer.createLocation(
+ result.statements.get(0).getLocation().getStartOffset(),
+ result.statements.get(result.statements.size() - 1).getLocation().getEndOffset()));
+ } else {
+ setLocation(Location.fromFile(lexer.getFilename()));
+ }
+ }
+
+ private ImmutableSet<PathFragment> fetchImports(List<Statement> stmts) {
+ Set<PathFragment> imports = new HashSet<>();
+ for (Statement stmt : stmts) {
+ if (stmt instanceof LoadStatement) {
+ LoadStatement imp = (LoadStatement) stmt;
+ imports.add(imp.getImportPath());
+ }
+ }
+ return ImmutableSet.copyOf(imports);
+ }
+
+ /**
+ * Returns true if any errors were encountered during scanning or parsing. If
+ * set, clients should not rely on the correctness of the AST for builds or
+ * BUILD-file editing.
+ */
+ public boolean containsErrors() {
+ return containsErrors;
+ }
+
+ /**
+ * Returns an (immutable, ordered) list of statements in this BUILD file.
+ */
+ public ImmutableList<Statement> getStatements() {
+ return stmts;
+ }
+
+ /**
+ * Returns an (immutable, ordered) list of comments in this BUILD file.
+ */
+ public ImmutableList<Comment> getComments() {
+ return comments;
+ }
+
+ /**
+ * Returns an (immutable) set of imports in this BUILD file.
+ */
+ public ImmutableCollection<PathFragment> getImports() {
+ return imports;
+ }
+
+ /**
+ * Executes this build file in a given Environment.
+ *
+ * <p>If, for any reason, execution of a statement cannot be completed, an
+ * {@link EvalException} is thrown by {@link Statement#exec(Environment)}.
+ * This exception is caught here and reported through reporter and execution
+ * continues on the next statement. In effect, there is a "try/except" block
+ * around every top level statement. Such exceptions are not ignored, though:
+ * they are visible via the return value. Rules declared in a package
+ * containing any error (including loading-phase semantical errors that
+ * cannot be checked here) must also be considered "in error".
+ *
+ * <p>Note that this method will not affect the value of {@link
+ * #containsErrors()}; that refers only to lexer/parser errors.
+ *
+ * @return true if no error occurred during execution.
+ */
+ public boolean exec(Environment env, EventHandler eventHandler) throws InterruptedException {
+ boolean ok = true;
+ for (Statement stmt : stmts) {
+ try {
+ stmt.exec(env);
+ } catch (EvalException e) {
+ ok = false;
+ // Do not report errors caused by a previous parsing error, as it has already been
+ // reported.
+ if (e.isDueToIncompleteAST()) {
+ continue;
+ }
+ // When the exception is raised from another file, report first the location in the
+ // BUILD file (as it is the most probable cause for the error).
+ Location exnLoc = e.getLocation();
+ Location nodeLoc = stmt.getLocation();
+ if (exnLoc == null || !nodeLoc.getPath().equals(exnLoc.getPath())) {
+ eventHandler.handle(Event.error(nodeLoc,
+ e.getMessage() + " (raised from " + exnLoc + ")"));
+ } else {
+ eventHandler.handle(Event.error(exnLoc, e.getMessage()));
+ }
+ }
+ }
+ return ok;
+ }
+
+ @Override
+ public String toString() {
+ return "BuildFileAST" + getStatements();
+ }
+
+ @Override
+ public void accept(SyntaxTreeVisitor visitor) {
+ visitor.visit(this);
+ }
+
+ /**
+ * Parse the specified build file, returning its AST. All errors during
+ * scanning or parsing will be reported to the reporter.
+ *
+ * @throws IOException if the file cannot not be read.
+ */
+ public static BuildFileAST parseBuildFile(Path buildFile, EventHandler eventHandler,
+ CachingPackageLocator locator, boolean parsePython)
+ throws IOException {
+ ParserInputSource inputSource = ParserInputSource.create(buildFile);
+ return parseBuildFile(inputSource, eventHandler, locator, parsePython);
+ }
+
+ /**
+ * Parse the specified build file, returning its AST. All errors during
+ * scanning or parsing will be reported to the reporter.
+ */
+ public static BuildFileAST parseBuildFile(ParserInputSource input,
+ List<Statement> preludeStatements,
+ EventHandler eventHandler,
+ CachingPackageLocator locator,
+ boolean parsePython) {
+ Lexer lexer = new Lexer(input, eventHandler, parsePython);
+ Parser.ParseResult result = Parser.parseFile(lexer, eventHandler, locator, parsePython);
+ return new BuildFileAST(lexer, preludeStatements, result);
+ }
+
+ public static BuildFileAST parseBuildFile(ParserInputSource input, EventHandler eventHandler,
+ CachingPackageLocator locator, boolean parsePython) {
+ Lexer lexer = new Lexer(input, eventHandler, parsePython);
+ Parser.ParseResult result = Parser.parseFile(lexer, eventHandler, locator, parsePython);
+ return new BuildFileAST(lexer, ImmutableList.<Statement>of(), result);
+ }
+
+ /**
+ * Parse the specified build file, returning its AST. All errors during
+ * scanning or parsing will be reported to the reporter.
+ */
+ public static BuildFileAST parseBuildFile(Lexer lexer, EventHandler eventHandler) {
+ Parser.ParseResult result = Parser.parseFile(lexer, eventHandler, null, false);
+ return new BuildFileAST(lexer, ImmutableList.<Statement>of(), result);
+ }
+
+ /**
+ * Parse the specified Skylark file, returning its AST. All errors during
+ * scanning or parsing will be reported to the reporter.
+ *
+ * @throws IOException if the file cannot not be read.
+ */
+ public static BuildFileAST parseSkylarkFile(Path file, EventHandler eventHandler,
+ CachingPackageLocator locator, ValidationEnvironment validationEnvironment)
+ throws IOException {
+ ParserInputSource input = ParserInputSource.create(file);
+ Lexer lexer = new Lexer(input, eventHandler, false);
+ Parser.ParseResult result =
+ Parser.parseFileForSkylark(lexer, eventHandler, locator, validationEnvironment);
+ return new BuildFileAST(lexer, ImmutableList.<Statement>of(), result, input.contentHashCode());
+ }
+
+ /**
+ * Parse the specified build file, without building the AST.
+ *
+ * @return true if the input file is syntactically valid
+ */
+ public static boolean checkSyntax(ParserInputSource input,
+ EventHandler eventHandler, boolean parsePython) {
+ return !parseBuildFile(input, eventHandler, null, parsePython).containsErrors();
+ }
+
+ /**
+ * Returns a hash code calculated from the string content of the source file of this AST.
+ */
+ @Nullable public String getContentHashCode() {
+ return contentHashCode;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/ClassObject.java b/src/main/java/com/google/devtools/build/lib/syntax/ClassObject.java
new file mode 100644
index 0000000..3b1cccf
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/ClassObject.java
@@ -0,0 +1,113 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Sets;
+import com.google.common.collect.Sets.SetView;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.events.Location;
+
+import java.util.Map;
+
+import javax.annotation.Nullable;
+
+/**
+ * An interface for objects behaving like Skylark structs.
+ */
+// TODO(bazel-team): type checks
+public interface ClassObject {
+
+ /**
+ * Returns the value associated with the name field in this struct,
+ * or null if the field does not exist.
+ */
+ @Nullable
+ Object getValue(String name);
+
+ /**
+ * Returns the fields of this struct.
+ */
+ ImmutableCollection<String> getKeys();
+
+ /**
+ * Returns a customized error message to print if the name is not a valid struct field
+ * of this struct, or returns null to use the default error message.
+ */
+ @Nullable String errorMessage(String name);
+
+ /**
+ * An implementation class of ClassObject for structs created in Skylark code.
+ */
+ @Immutable
+ @SkylarkModule(name = "struct",
+ doc = "A special language element to support structs (i.e. simple value objects). "
+ + "See the global <code>struct</code> method for more details.")
+ public class SkylarkClassObject implements ClassObject {
+
+ private final ImmutableMap<String, Object> values;
+ private final Location creationLoc;
+ private final String errorMessage;
+
+ /**
+ * Creates a built-in struct (i.e. without creation loc). The errorMessage has to have
+ * exactly one '%s' parameter to substitute the struct field name.
+ */
+ public SkylarkClassObject(Map<String, Object> values, String errorMessage) {
+ this.values = ImmutableMap.copyOf(values);
+ this.creationLoc = null;
+ this.errorMessage = errorMessage;
+ }
+
+ public SkylarkClassObject(Map<String, Object> values, Location creationLoc) {
+ this.values = ImmutableMap.copyOf(values);
+ this.creationLoc = Preconditions.checkNotNull(creationLoc);
+ this.errorMessage = null;
+ }
+
+ @Override
+ public Object getValue(String name) {
+ return values.get(name);
+ }
+
+ @Override
+ public ImmutableCollection<String> getKeys() {
+ return values.keySet();
+ }
+
+ public Location getCreationLoc() {
+ return Preconditions.checkNotNull(creationLoc,
+ "This struct was not created in a Skylark code");
+ }
+
+ static SkylarkClassObject concat(
+ SkylarkClassObject lval, SkylarkClassObject rval, Location loc) throws EvalException {
+ SetView<String> commonFields = Sets.intersection(lval.values.keySet(), rval.values.keySet());
+ if (!commonFields.isEmpty()) {
+ throw new EvalException(loc, "Cannot concat structs with common field(s): "
+ + Joiner.on(",").join(commonFields));
+ }
+ return new SkylarkClassObject(ImmutableMap.<String, Object>builder()
+ .putAll(lval.values).putAll(rval.values).build(), loc);
+ }
+
+ @Override
+ public String errorMessage(String name) {
+ return errorMessage != null ? String.format(errorMessage, name) : null;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/CommaSeparatedPackageNameListConverter.java b/src/main/java/com/google/devtools/build/lib/syntax/CommaSeparatedPackageNameListConverter.java
new file mode 100644
index 0000000..070e928
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/CommaSeparatedPackageNameListConverter.java
@@ -0,0 +1,54 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+import com.google.common.base.Splitter;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.cmdline.LabelValidator;
+import com.google.devtools.common.options.Converter;
+import com.google.devtools.common.options.OptionsParsingException;
+
+import java.util.List;
+
+/**
+ * A converter from strings containing comma-separated names of packages to lists of strings.
+ */
+public class CommaSeparatedPackageNameListConverter
+ implements Converter<List<String>> {
+
+ private static final Splitter SPACE_SPLITTER = Splitter.on(',');
+
+ @Override
+ public List<String> convert(String input) throws OptionsParsingException {
+ if (Strings.isNullOrEmpty(input)) {
+ return ImmutableList.of();
+ }
+ ImmutableList.Builder<String> list = ImmutableList.builder();
+ for (String s : SPACE_SPLITTER.split(input)) {
+ String errorMessage = LabelValidator.validatePackageName(s);
+ if (errorMessage != null) {
+ throw new OptionsParsingException(errorMessage);
+ }
+ list.add(s);
+ }
+ return list.build();
+ }
+
+ @Override
+ public String getTypeDescription() {
+ return "comma-separated list of package names";
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/Comment.java b/src/main/java/com/google/devtools/build/lib/syntax/Comment.java
new file mode 100644
index 0000000..29d9474
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/Comment.java
@@ -0,0 +1,40 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+/**
+ * Syntax node for comments.
+ */
+public final class Comment extends ASTNode {
+
+ protected final String value;
+
+ public Comment(String value) {
+ this.value = value;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ @Override
+ public void accept(SyntaxTreeVisitor visitor) {
+ visitor.visit(this);
+ }
+
+ @Override
+ public String toString() {
+ return value;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/DictComprehension.java b/src/main/java/com/google/devtools/build/lib/syntax/DictComprehension.java
new file mode 100644
index 0000000..a69605e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/DictComprehension.java
@@ -0,0 +1,102 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+import com.google.common.collect.ImmutableMap;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * Syntax node for dictionary comprehension expressions.
+ */
+public class DictComprehension extends Expression {
+
+ private final Expression keyExpression;
+ private final Expression valueExpression;
+ private final Ident loopVar;
+ private final Expression listExpression;
+
+ public DictComprehension(Expression keyExpression, Expression valueExpression, Ident loopVar,
+ Expression listExpression) {
+ this.keyExpression = keyExpression;
+ this.valueExpression = valueExpression;
+ this.loopVar = loopVar;
+ this.listExpression = listExpression;
+ }
+
+ Expression getKeyExpression() {
+ return keyExpression;
+ }
+
+ Expression getValueExpression() {
+ return valueExpression;
+ }
+
+ Ident getLoopVar() {
+ return loopVar;
+ }
+
+ Expression getListExpression() {
+ return listExpression;
+ }
+
+ @Override
+ Object eval(Environment env) throws EvalException, InterruptedException {
+ // We want to keep the iteration order
+ LinkedHashMap<Object, Object> map = new LinkedHashMap<>();
+ Iterable<?> elements = EvalUtils.toIterable(listExpression.eval(env), getLocation());
+ for (Object element : elements) {
+ env.update(loopVar.getName(), element);
+ Object key = keyExpression.eval(env);
+ map.put(key, valueExpression.eval(env));
+ }
+ return ImmutableMap.copyOf(map);
+ }
+
+ @Override
+ SkylarkType validate(ValidationEnvironment env) throws EvalException {
+ SkylarkType elementsType = listExpression.validate(env);
+ // TODO(bazel-team): GenericType1 should be a SkylarkType.
+ Class<?> listElementType = elementsType.getGenericType1();
+ SkylarkType listElementSkylarkType = listElementType.equals(Object.class)
+ ? SkylarkType.UNKNOWN : SkylarkType.of(listElementType);
+ env.update(loopVar.getName(), listElementSkylarkType, getLocation());
+ SkylarkType keyType = keyExpression.validate(env);
+ if (!keyType.isSimple()) {
+ // TODO(bazel-team): this is most probably dead code but it's better to have it here
+ // in case we enable e.g. list of lists or we validate function calls on Java objects
+ throw new EvalException(getLocation(), "Dict comprehension key must be of a simple type");
+ }
+ valueExpression.validate(env);
+ if (elementsType != SkylarkType.UNKNOWN && !elementsType.isList()) {
+ throw new EvalException(getLocation(), "Dict comprehension elements must be a list");
+ }
+ return SkylarkType.of(Map.class, keyType.getType());
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append('{').append(keyExpression).append(": ").append(valueExpression);
+ sb.append(" for ").append(loopVar).append(" in ").append(listExpression);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public void accept(SyntaxTreeVisitor visitor) {
+ visitor.accept(this);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/DictionaryLiteral.java b/src/main/java/com/google/devtools/build/lib/syntax/DictionaryLiteral.java
new file mode 100644
index 0000000..8f79739
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/DictionaryLiteral.java
@@ -0,0 +1,117 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Syntax node for dictionary literals.
+ */
+public class DictionaryLiteral extends Expression {
+
+ static final class DictionaryEntryLiteral extends ASTNode {
+
+ private final Expression key;
+ private final Expression value;
+
+ public DictionaryEntryLiteral(Expression key, Expression value) {
+ this.key = key;
+ this.value = value;
+ }
+
+ Expression getKey() {
+ return key;
+ }
+
+ Expression getValue() {
+ return value;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(key);
+ sb.append(": ");
+ sb.append(value);
+ return sb.toString();
+ }
+
+ @Override
+ public void accept(SyntaxTreeVisitor visitor) {
+ visitor.visit(this);
+ }
+ }
+
+ private final ImmutableList<DictionaryEntryLiteral> entries;
+
+ public DictionaryLiteral(List<DictionaryEntryLiteral> exprs) {
+ this.entries = ImmutableList.copyOf(exprs);
+ }
+
+ @Override
+ Object eval(Environment env) throws EvalException, InterruptedException {
+ // We need LinkedHashMap to maintain the order during iteration (e.g. for loops)
+ Map<Object, Object> map = new LinkedHashMap<>();
+ for (DictionaryEntryLiteral entry : entries) {
+ if (entry == null) {
+ throw new EvalException(getLocation(), "null expression in " + this);
+ }
+ map.put(entry.key.eval(env), entry.value.eval(env));
+
+ }
+ return map;
+ }
+
+ @Override
+ public String toString() {
+ StringBuffer sb = new StringBuffer();
+ sb.append("{");
+ String sep = "";
+ for (DictionaryEntryLiteral e : entries) {
+ sb.append(sep);
+ sb.append(e);
+ sep = ", ";
+ }
+ sb.append("}");
+ return sb.toString();
+ }
+
+ @Override
+ public void accept(SyntaxTreeVisitor visitor) {
+ visitor.visit(this);
+ }
+
+ public ImmutableList<DictionaryEntryLiteral> getEntries() {
+ return entries;
+ }
+
+ @Override
+ SkylarkType validate(ValidationEnvironment env) throws EvalException {
+ SkylarkType type = SkylarkType.UNKNOWN;
+ for (DictionaryEntryLiteral entry : entries) {
+ SkylarkType nextType = entry.key.validate(env);
+ entry.value.validate(env);
+ if (!nextType.isSimple()) {
+ throw new EvalException(getLocation(),
+ String.format("Dict cannot contain composite type '%s' as key", nextType));
+ }
+ type = type.infer(nextType, "dict literal", entry.getLocation(), getLocation());
+ }
+ return SkylarkType.of(Map.class, type.getType());
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/DotExpression.java b/src/main/java/com/google/devtools/build/lib/syntax/DotExpression.java
new file mode 100644
index 0000000..b0ae5a9
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/DotExpression.java
@@ -0,0 +1,110 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.syntax.FuncallExpression.MethodDescriptor;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+
+/**
+ * Syntax node for a dot expression.
+ * e.g. obj.field, but not obj.method()
+ */
+public final class DotExpression extends Expression {
+
+ private final Expression obj;
+
+ private final Ident field;
+
+ public DotExpression(Expression obj, Ident field) {
+ this.obj = obj;
+ this.field = field;
+ }
+
+ public Expression getObj() {
+ return obj;
+ }
+
+ public Ident getField() {
+ return field;
+ }
+
+ @Override
+ public String toString() {
+ return obj + "." + field;
+ }
+
+ @Override
+ Object eval(Environment env) throws EvalException, InterruptedException {
+ Object objValue = obj.eval(env);
+ String name = field.getName();
+ Object result = eval(objValue, name, getLocation());
+ if (result == null) {
+ if (objValue instanceof ClassObject) {
+ String customErrorMessage = ((ClassObject) objValue).errorMessage(name);
+ if (customErrorMessage != null) {
+ throw new EvalException(getLocation(), customErrorMessage);
+ }
+ }
+ throw new EvalException(getLocation(), "Object of type '"
+ + EvalUtils.getDatatypeName(objValue) + "' has no field '" + name + "'");
+ }
+ return result;
+ }
+
+ /**
+ * Returns the field of the given name of the struct objValue, or null if no such field exists.
+ */
+ public static Object eval(Object objValue, String name, Location loc) throws EvalException {
+ Object result = null;
+ if (objValue instanceof ClassObject) {
+ result = ((ClassObject) objValue).getValue(name);
+ result = SkylarkType.convertToSkylark(result, loc);
+ // If we access NestedSets using ClassObject.getValue() we won't know the generic type,
+ // so we have to disable it. This should not happen.
+ SkylarkType.checkTypeAllowedInSkylark(result, loc);
+ } else {
+ try {
+ List<MethodDescriptor> methods = FuncallExpression.getMethods(objValue.getClass(), name, 0);
+ if (methods != null && methods.size() > 0) {
+ MethodDescriptor method = Iterables.getOnlyElement(methods);
+ if (method.getAnnotation().structField()) {
+ result = FuncallExpression.callMethod(
+ method, name, objValue, new Object[] {}, loc);
+ }
+ }
+ } catch (ExecutionException | IllegalAccessException | InvocationTargetException e) {
+ throw new EvalException(loc, "Method invocation failed: " + e);
+ }
+ }
+
+ return result;
+ }
+
+ @Override
+ public void accept(SyntaxTreeVisitor visitor) {
+ visitor.visit(this);
+ }
+
+ @Override
+ SkylarkType validate(ValidationEnvironment env) throws EvalException {
+ obj.validate(env);
+ // TODO(bazel-team): check existance of field
+ return SkylarkType.UNKNOWN;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/Environment.java b/src/main/java/com/google/devtools/build/lib/syntax/Environment.java
new file mode 100644
index 0000000..a148a70
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/Environment.java
@@ -0,0 +1,345 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+/**
+ * The BUILD environment.
+ */
+public class Environment {
+
+ @SkylarkBuiltin(name = "True", returnType = Boolean.class, doc = "Literal for the boolean true.")
+ private static final Boolean TRUE = true;
+
+ @SkylarkBuiltin(name = "False", returnType = Boolean.class,
+ doc = "Literal for the boolean false.")
+ private static final Boolean FALSE = false;
+
+ @SkylarkBuiltin(name = "PACKAGE_NAME", returnType = String.class,
+ doc = "The name of the package the rule or build extension is called from. "
+ + "This variable is special, because its value comes from outside of the extension "
+ + "module (it comes from the BUILD file), so it can only be accessed in functions "
+ + "(transitively) called from BUILD files. For example:<br>"
+ + "<pre class=language-python>def extension():\n"
+ + " return PACKAGE_NAME</pre>"
+ + "In this case calling <code>extension()</code> works from the BUILD file (if the "
+ + "function is loaded), but not as a top level function call in the extension module.")
+ public static final String PKG_NAME = "PACKAGE_NAME";
+
+ /**
+ * There should be only one instance of this type to allow "== None" tests.
+ */
+ @Immutable
+ public static final class NoneType {
+ @Override
+ public String toString() { return "None"; }
+ private NoneType() {}
+ }
+
+ @SkylarkBuiltin(name = "None", returnType = NoneType.class, doc = "Literal for the None value.")
+ public static final NoneType NONE = new NoneType();
+
+ protected final Map<String, Object> env = new HashMap<>();
+
+ // Functions with namespaces. Works only in the global environment.
+ protected final Map<Class<?>, Map<String, Function>> functions = new HashMap<>();
+
+ /**
+ * The parent environment. For Skylark it's the global environment,
+ * used for global read only variable lookup.
+ */
+ protected final Environment parent;
+
+ /**
+ * Map from a Skylark extension to an environment, which contains all symbols defined in the
+ * extension.
+ */
+ protected Map<PathFragment, SkylarkEnvironment> importedExtensions;
+
+ /**
+ * A set of disable variables propagating through function calling. This is needed because
+ * UserDefinedFunctions lock the definition Environment which should be immutable.
+ */
+ protected Set<String> disabledVariables = new HashSet<>();
+
+ /**
+ * A set of disable namespaces propagating through function calling. See disabledVariables.
+ */
+ protected Set<Class<?>> disabledNameSpaces = new HashSet<>();
+
+ /**
+ * A set of variables propagating through function calling. It's only used to call
+ * native rules from Skylark build extensions.
+ */
+ protected Set<String> propagatingVariables = new HashSet<>();
+
+ /**
+ * An EventHandler for errors and warnings. This is not used in the BUILD language,
+ * however it might be used in Skylark code called from the BUILD language.
+ */
+ @Nullable protected EventHandler eventHandler;
+
+ /**
+ * Constructs an empty root non-Skylark environment.
+ * The root environment is also the global environment.
+ */
+ public Environment() {
+ this.parent = null;
+ this.importedExtensions = new HashMap<>();
+ setupGlobal();
+ }
+
+ /**
+ * Constructs an empty child environment.
+ */
+ public Environment(Environment parent) {
+ Preconditions.checkNotNull(parent);
+ this.parent = parent;
+ this.importedExtensions = new HashMap<>();
+ }
+
+ /**
+ * Constructs an empty child environment with an EventHandler.
+ */
+ public Environment(Environment parent, EventHandler eventHandler) {
+ this(parent);
+ this.eventHandler = Preconditions.checkNotNull(eventHandler);
+ }
+
+ // Sets up the global environment
+ private void setupGlobal() {
+ // In Python 2.x, True and False are global values and can be redefined by the user.
+ // In Python 3.x, they are keywords. We implement them as values, for the sake of
+ // simplicity. We define them as Boolean objects.
+ env.put("False", FALSE);
+ env.put("True", TRUE);
+ env.put("None", NONE);
+ }
+
+ public boolean isSkylarkEnabled() {
+ return false;
+ }
+
+ protected boolean hasVariable(String varname) {
+ return env.containsKey(varname);
+ }
+
+ /**
+ * @return the value from the environment whose name is "varname".
+ * @throws NoSuchVariableException if the variable is not defined in the Environment.
+ *
+ */
+ public Object lookup(String varname) throws NoSuchVariableException {
+ if (disabledVariables.contains(varname)) {
+ throw new NoSuchVariableException(varname);
+ }
+ Object value = env.get(varname);
+ if (value == null) {
+ if (parent != null) {
+ return parent.lookup(varname);
+ }
+ throw new NoSuchVariableException(varname);
+ }
+ return value;
+ }
+
+ /**
+ * Like <code>lookup(String)</code>, but instead of throwing an exception in
+ * the case where "varname" is not defined, "defaultValue" is returned instead.
+ *
+ */
+ public Object lookup(String varname, Object defaultValue) {
+ Object value = env.get(varname);
+ if (value == null) {
+ if (parent != null) {
+ return parent.lookup(varname, defaultValue);
+ }
+ return defaultValue;
+ }
+ return value;
+ }
+
+ /**
+ * Updates the value of variable "varname" in the environment, corresponding
+ * to an {@link AssignmentStatement}.
+ */
+ public void update(String varname, Object value) {
+ Preconditions.checkNotNull(value, "update(value == null)");
+ env.put(varname, value);
+ }
+
+ /**
+ * Same as {@link #update}, but also marks the variable propagating, meaning it will
+ * be present in the execution environment of a UserDefinedFunction called from this
+ * Environment. Using this method is discouraged.
+ */
+ public void updateAndPropagate(String varname, Object value) {
+ update(varname, value);
+ propagatingVariables.add(varname);
+ }
+
+ /**
+ * Remove the variable from the environment, returning
+ * any previous mapping (null if there was none).
+ */
+ public Object remove(String varname) {
+ return env.remove(varname);
+ }
+
+ /**
+ * Returns the (immutable) set of names of all variables defined in this
+ * environment. Exposed for testing; not very efficient!
+ */
+ @VisibleForTesting
+ public Set<String> getVariableNames() {
+ if (parent == null) {
+ return env.keySet();
+ } else {
+ Set<String> vars = new HashSet<>();
+ vars.addAll(env.keySet());
+ vars.addAll(parent.getVariableNames());
+ return vars;
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ throw new UnsupportedOperationException(); // avoid nondeterminism
+ }
+
+ @Override
+ public boolean equals(Object that) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder out = new StringBuilder();
+ out.append("Environment{");
+ List<String> keys = new ArrayList<>(env.keySet());
+ Collections.sort(keys);
+ for (String key: keys) {
+ out.append(key).append(" -> ").append(env.get(key)).append(", ");
+ }
+ out.append("}");
+ if (parent != null) {
+ out.append("=>");
+ out.append(parent.toString());
+ }
+ return out.toString();
+ }
+
+ /**
+ * An exception thrown when an attempt is made to lookup a non-existent
+ * variable in the environment.
+ */
+ public static class NoSuchVariableException extends Exception {
+ NoSuchVariableException(String variable) {
+ super("no such variable: " + variable);
+ }
+ }
+
+ /**
+ * An exception thrown when an attempt is made to import a symbol from a file
+ * that was not properly loaded.
+ */
+ public static class LoadFailedException extends Exception {
+ LoadFailedException(String file) {
+ super("file '" + file + "' was not correctly loaded. Make sure the 'load' statement appears "
+ + "in the global scope, in the BUILD file");
+ }
+ }
+
+ public void setImportedExtensions(Map<PathFragment, SkylarkEnvironment> importedExtensions) {
+ this.importedExtensions = importedExtensions;
+ }
+
+ public void importSymbol(PathFragment extension, String symbol)
+ throws NoSuchVariableException, LoadFailedException {
+ if (!importedExtensions.containsKey(extension)) {
+ throw new LoadFailedException(extension.toString());
+ }
+ Object value = importedExtensions.get(extension).lookup(symbol);
+ if (!isSkylarkEnabled()) {
+ value = SkylarkType.convertFromSkylark(value);
+ }
+ update(symbol, value);
+ }
+
+ /**
+ * Registers a function with namespace to this global environment.
+ */
+ public void registerFunction(Class<?> nameSpace, String name, Function function) {
+ Preconditions.checkArgument(parent == null);
+ if (!functions.containsKey(nameSpace)) {
+ functions.put(nameSpace, new HashMap<String, Function>());
+ }
+ functions.get(nameSpace).put(name, function);
+ }
+
+ private Map<String, Function> getNamespaceFunctions(Class<?> nameSpace) {
+ if (disabledNameSpaces.contains(nameSpace)
+ || (parent != null && parent.disabledNameSpaces.contains(nameSpace))) {
+ return null;
+ }
+ Environment topLevel = this;
+ while (topLevel.parent != null) {
+ topLevel = topLevel.parent;
+ }
+ return topLevel.functions.get(nameSpace);
+ }
+
+ /**
+ * Returns the function of the namespace of the given name or null of it does not exists.
+ */
+ public Function getFunction(Class<?> nameSpace, String name) {
+ Map<String, Function> nameSpaceFunctions = getNamespaceFunctions(nameSpace);
+ return nameSpaceFunctions != null ? nameSpaceFunctions.get(name) : null;
+ }
+
+ /**
+ * Returns the function names registered with the namespace.
+ */
+ public Set<String> getFunctionNames(Class<?> nameSpace) {
+ Map<String, Function> nameSpaceFunctions = getNamespaceFunctions(nameSpace);
+ return nameSpaceFunctions != null ? nameSpaceFunctions.keySet() : ImmutableSet.<String>of();
+ }
+
+ /**
+ * Return the current stack trace (list of function names).
+ */
+ public ImmutableList<String> getStackTrace() {
+ // Empty list, since this environment does not allow function definition
+ // (see SkylarkEnvironment)
+ return ImmutableList.of();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/EvalException.java b/src/main/java/com/google/devtools/build/lib/syntax/EvalException.java
new file mode 100644
index 0000000..27aba0f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/EvalException.java
@@ -0,0 +1,105 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.events.Location;
+
+/**
+ * Exceptions thrown during evaluation of BUILD ASTs or Skylark extensions.
+ *
+ * <p>This exception must always correspond to a repeatable, permanent error, i.e. evaluating the
+ * same package again must yield the same exception. Notably, do not use this for reporting I/O
+ * errors.
+ *
+ * <p>This requirement is in place so that we can cache packages where an error is reported by way
+ * of {@link EvalException}.
+ */
+public class EvalException extends Exception {
+
+ private final Location location;
+ private final String message;
+ private final boolean dueToIncompleteAST;
+
+ /**
+ * @param location the location where evaluation/execution failed.
+ * @param message the error message.
+ */
+ public EvalException(Location location, String message) {
+ this.location = location;
+ this.message = Preconditions.checkNotNull(message);
+ this.dueToIncompleteAST = false;
+ }
+
+ /**
+ * @param location the location where evaluation/execution failed.
+ * @param message the error message.
+ * @param dueToIncompleteAST if the error is caused by a previous error, such as parsing.
+ */
+ public EvalException(Location location, String message, boolean dueToIncompleteAST) {
+ this.location = location;
+ this.message = Preconditions.checkNotNull(message);
+ this.dueToIncompleteAST = dueToIncompleteAST;
+ }
+
+ private EvalException(Location location, Throwable cause) {
+ super(cause);
+ this.location = location;
+ // This is only used from Skylark, it's useful for debugging. Note that this only happens
+ // when the Precondition below kills the execution anyway.
+ if (cause.getMessage() == null) {
+ cause.printStackTrace();
+ }
+ this.message = Preconditions.checkNotNull(cause.getMessage());
+ this.dueToIncompleteAST = false;
+ }
+
+ /**
+ * Returns the error message.
+ */
+ @Override
+ public String getMessage() {
+ return message;
+ }
+
+ /**
+ * Returns the location of the evaluation error.
+ */
+ public Location getLocation() {
+ return location;
+ }
+
+ public boolean isDueToIncompleteAST() {
+ return dueToIncompleteAST;
+ }
+
+ /**
+ * A class to support a special case of EvalException when the cause of the error is an
+ * Exception during a direct Java call.
+ */
+ public static final class EvalExceptionWithJavaCause extends EvalException {
+
+ public EvalExceptionWithJavaCause(Location location, Throwable cause) {
+ super(location, cause);
+ }
+ }
+
+ /**
+ * Returns the error message with location info if exists.
+ */
+ public String print() {
+ return getLocation() == null ? getMessage() : getLocation().print() + ": " + getMessage();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/EvalUtils.java b/src/main/java/com/google/devtools/build/lib/syntax/EvalUtils.java
new file mode 100644
index 0000000..70d89bc
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/EvalUtils.java
@@ -0,0 +1,590 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.syntax.ClassObject.SkylarkClassObject;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Formattable;
+import java.util.Formatter;
+import java.util.IllegalFormatException;
+import java.util.List;
+import java.util.Map;
+import java.util.MissingFormatWidthException;
+import java.util.Set;
+
+/**
+ * Utilities used by the evaluator.
+ */
+public abstract class EvalUtils {
+
+ // TODO(bazel-team): Yet an other hack committed in the name of Skylark. One problem is that the
+ // syntax package cannot depend on actions so we have to have this until Actions are immutable.
+ // The other is that BuildConfigurations are technically not immutable but they cannot be modified
+ // from Skylark.
+ private static final ImmutableSet<Class<?>> quasiImmutableClasses;
+ static {
+ try {
+ ImmutableSet.Builder<Class<?>> builder = ImmutableSet.builder();
+ builder.add(Class.forName("com.google.devtools.build.lib.actions.Action"));
+ builder.add(Class.forName("com.google.devtools.build.lib.analysis.config.BuildConfiguration"));
+ builder.add(Class.forName("com.google.devtools.build.lib.actions.Root"));
+ quasiImmutableClasses = builder.build();
+ } catch (ClassNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private EvalUtils() {
+ }
+
+ /**
+ * @return true if the specified sequence is a tuple; false if it's a modifiable list.
+ */
+ public static boolean isTuple(List<?> l) {
+ return isTuple(l.getClass());
+ }
+
+ public static boolean isTuple(Class<?> c) {
+ Preconditions.checkState(List.class.isAssignableFrom(c));
+ if (ImmutableList.class.isAssignableFrom(c)) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * @return true if the specified value is immutable (suitable for use as a
+ * dictionary key) according to the rules of the Build language.
+ */
+ public static boolean isImmutable(Object o) {
+ if (o instanceof Map<?, ?> || o instanceof Function
+ || o instanceof FilesetEntry || o instanceof GlobList<?>) {
+ return false;
+ } else if (o instanceof List<?>) {
+ return isTuple((List<?>) o); // tuples are immutable, lists are not.
+ } else {
+ return true; // string/int
+ }
+ }
+
+ /**
+ * Returns true if the type is immutable in the skylark language.
+ */
+ public static boolean isSkylarkImmutable(Class<?> c) {
+ if (c.isAnnotationPresent(Immutable.class)) {
+ return true;
+ } else if (c.equals(String.class) || c.equals(Integer.class) || c.equals(Boolean.class)
+ || SkylarkList.class.isAssignableFrom(c) || ImmutableMap.class.isAssignableFrom(c)
+ || NestedSet.class.isAssignableFrom(c)) {
+ return true;
+ } else {
+ for (Class<?> classObject : quasiImmutableClasses) {
+ if (classObject.isAssignableFrom(c)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns a transitive superclass or interface implemented by c which is annotated
+ * with SkylarkModule. Returns null if no such class or interface exists.
+ */
+ @VisibleForTesting
+ static Class<?> getParentWithSkylarkModule(Class<?> c) {
+ if (c == null) {
+ return null;
+ }
+ if (c.isAnnotationPresent(SkylarkModule.class)) {
+ return c;
+ }
+ Class<?> parent = getParentWithSkylarkModule(c.getSuperclass());
+ if (parent != null) {
+ return parent;
+ }
+ for (Class<?> ifparent : c.getInterfaces()) {
+ ifparent = getParentWithSkylarkModule(ifparent);
+ if (ifparent != null) {
+ return ifparent;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the Skylark equivalent type of the parameter. Note that the Skylark
+ * language doesn't have inheritance.
+ */
+ public static Class<?> getSkylarkType(Class<?> c) {
+ if (ImmutableList.class.isAssignableFrom(c)) {
+ return ImmutableList.class;
+ } else if (List.class.isAssignableFrom(c)) {
+ return List.class;
+ } else if (SkylarkList.class.isAssignableFrom(c)) {
+ return SkylarkList.class;
+ } else if (Map.class.isAssignableFrom(c)) {
+ return Map.class;
+ } else if (NestedSet.class.isAssignableFrom(c)) {
+ // This could be removed probably
+ return NestedSet.class;
+ } else if (Set.class.isAssignableFrom(c)) {
+ return Set.class;
+ } else {
+ // Check if one of the superclasses or implemented interfaces has the SkylarkModule
+ // annotation. If yes return that class.
+ Class<?> parent = getParentWithSkylarkModule(c);
+ if (parent != null) {
+ return parent;
+ }
+ }
+ return c;
+ }
+
+ /**
+ * Returns a pretty name for the datatype of object 'o' in the Build language.
+ */
+ public static String getDatatypeName(Object o) {
+ Preconditions.checkNotNull(o);
+ if (o instanceof SkylarkList) {
+ return ((SkylarkList) o).isTuple() ? "tuple" : "list";
+ }
+ return getDataTypeNameFromClass(o.getClass());
+ }
+
+ /**
+ * Returns a pretty name for the datatype equivalent of class 'c' in the Build language.
+ */
+ public static String getDataTypeNameFromClass(Class<?> c) {
+ if (c.equals(Object.class)) {
+ return "unknown";
+ } else if (c.equals(String.class)) {
+ return "string";
+ } else if (c.equals(Integer.class)) {
+ return "int";
+ } else if (c.equals(Boolean.class)) {
+ return "bool";
+ } else if (c.equals(Void.TYPE) || c.equals(Environment.NoneType.class)) {
+ return "None";
+ } else if (List.class.isAssignableFrom(c)) {
+ return isTuple(c) ? "tuple" : "list";
+ } else if (GlobList.class.isAssignableFrom(c)) {
+ return "list";
+ } else if (Map.class.isAssignableFrom(c)) {
+ return "dict";
+ } else if (Function.class.isAssignableFrom(c)) {
+ return "function";
+ } else if (c.equals(FilesetEntry.class)) {
+ return "FilesetEntry";
+ } else if (NestedSet.class.isAssignableFrom(c) || SkylarkNestedSet.class.isAssignableFrom(c)) {
+ return "set";
+ } else if (SkylarkClassObject.class.isAssignableFrom(c)) {
+ return "struct";
+ } else if (SkylarkList.class.isAssignableFrom(c)) {
+ // TODO(bazel-team): this is not entirely correct, it can also be a tuple.
+ return "list";
+ } else if (c.isAnnotationPresent(SkylarkModule.class)) {
+ SkylarkModule module = c.getAnnotation(SkylarkModule.class);
+ return c.getAnnotation(SkylarkModule.class).name()
+ + (module.namespace() ? " (a language module)" : "");
+ } else {
+ if (c.getSimpleName().isEmpty()) {
+ return c.getName();
+ } else {
+ return c.getSimpleName();
+ }
+ }
+ }
+
+ /**
+ * Returns a sequence of the appropriate list/tuple datatype for 'seq', based on 'isTuple'.
+ */
+ public static List<?> makeSequence(List<?> seq, boolean isTuple) {
+ return isTuple ? ImmutableList.copyOf(seq) : seq;
+ }
+
+ /**
+ * Print build-language value 'o' in display format into the specified buffer.
+ */
+ public static void printValue(Object o, Appendable buffer) {
+ // Exception-swallowing wrapper due to annoying Appendable interface.
+ try {
+ printValueX(o, buffer);
+ } catch (IOException e) {
+ throw new AssertionError(e); // can't happen
+ }
+ }
+
+ private static void printValueX(Object o, Appendable buffer)
+ throws IOException {
+ if (o == null) {
+ throw new NullPointerException(); // None is not a build language value.
+ } else if (o instanceof String ||
+ o instanceof Integer ||
+ o instanceof Double) {
+ buffer.append(o.toString());
+
+ } else if (o == Environment.NONE) {
+ buffer.append("None");
+
+ } else if (o == Boolean.TRUE) {
+ buffer.append("True");
+
+ } else if (o == Boolean.FALSE) {
+ buffer.append("False");
+
+ } else if (o instanceof List<?>) {
+ List<?> seq = (List<?>) o;
+ boolean isTuple = isImmutable(seq);
+ String sep = "";
+ buffer.append(isTuple ? '(' : '[');
+ for (int ii = 0, len = seq.size(); ii < len; ++ii) {
+ buffer.append(sep);
+ prettyPrintValue(seq.get(ii), buffer);
+ sep = ", ";
+ }
+ buffer.append(isTuple ? ')' : ']');
+
+ } else if (o instanceof Map<?, ?>) {
+ Map<?, ?> dict = (Map<?, ?>) o;
+ buffer.append('{');
+ String sep = "";
+ for (Map.Entry<?, ?> entry : dict.entrySet()) {
+ buffer.append(sep);
+ prettyPrintValue(entry.getKey(), buffer);
+ buffer.append(": ");
+ prettyPrintValue(entry.getValue(), buffer);
+ sep = ", ";
+ }
+ buffer.append('}');
+
+ } else if (o instanceof Function) {
+ Function func = (Function) o;
+ buffer.append("<function " + func.getName() + ">");
+
+ } else if (o instanceof FilesetEntry) {
+ FilesetEntry entry = (FilesetEntry) o;
+ buffer.append("FilesetEntry(srcdir = ");
+ prettyPrintValue(entry.getSrcLabel().toString(), buffer);
+ buffer.append(", files = ");
+ prettyPrintValue(makeStringList(entry.getFiles()), buffer);
+ buffer.append(", excludes = ");
+ prettyPrintValue(makeList(entry.getExcludes()), buffer);
+ buffer.append(", destdir = ");
+ prettyPrintValue(entry.getDestDir().getPathString(), buffer);
+ buffer.append(", strip_prefix = ");
+ prettyPrintValue(entry.getStripPrefix(), buffer);
+ buffer.append(", symlinks = \"");
+ buffer.append(entry.getSymlinkBehavior().toString());
+ buffer.append("\")");
+ } else if (o instanceof PathFragment) {
+ buffer.append(((PathFragment) o).getPathString());
+ } else {
+ buffer.append(o.toString());
+ }
+ }
+
+ private static List<?> makeList(Collection<?> list) {
+ return list == null ? Lists.newArrayList() : Lists.newArrayList(list);
+ }
+
+ private static List<String> makeStringList(List<Label> labels) {
+ if (labels == null) { return Collections.emptyList(); }
+ List<String> strings = Lists.newArrayListWithCapacity(labels.size());
+ for (Label label : labels) {
+ strings.add(label.toString());
+ }
+ return strings;
+ }
+
+ /**
+ * Print build-language value 'o' in parseable format into the specified
+ * buffer. (Only differs from printValueX in treatment of strings at toplevel,
+ * i.e. not within a sequence or dict)
+ */
+ public static void prettyPrintValue(Object o, Appendable buffer) {
+ // Exception-swallowing wrapper due to annoying Appendable interface.
+ try {
+ prettyPrintValueX(o, buffer);
+ } catch (IOException e) {
+ throw new AssertionError(e); // can't happen
+ }
+ }
+
+ private static void prettyPrintValueX(Object o, Appendable buffer)
+ throws IOException {
+ if (o instanceof Label) {
+ o = o.toString(); // Pretty-print a label like a string
+ }
+ if (o instanceof String) {
+ String s = (String) o;
+ buffer.append('"');
+ for (int ii = 0, len = s.length(); ii < len; ++ii) {
+ char c = s.charAt(ii);
+ switch (c) {
+ case '\r':
+ buffer.append('\\').append('r');
+ break;
+ case '\n':
+ buffer.append('\\').append('n');
+ break;
+ case '\t':
+ buffer.append('\\').append('t');
+ break;
+ case '\"':
+ buffer.append('\\').append('"');
+ break;
+ default:
+ if (c < 32) {
+ buffer.append(String.format("\\x%02x", (int) c));
+ } else {
+ buffer.append(c); // no need to support UTF-8
+ }
+ } // endswitch
+ }
+ buffer.append('\"');
+ } else {
+ printValueX(o, buffer);
+ }
+ }
+
+ /**
+ * Pretty-print value 'o' to a string. Convenience overloading of
+ * prettyPrintValue(Object, Appendable).
+ */
+ public static String prettyPrintValue(Object o) {
+ StringBuffer buffer = new StringBuffer();
+ prettyPrintValue(o, buffer);
+ return buffer.toString();
+ }
+
+ /**
+ * Pretty-print values of 'o' separated by the separator.
+ */
+ public static String prettyPrintValues(String separator, Iterable<Object> o) {
+ return Joiner.on(separator).join(Iterables.transform(o,
+ new com.google.common.base.Function<Object, String>() {
+ @Override
+ public String apply(Object input) {
+ return prettyPrintValue(input);
+ }
+ }));
+ }
+
+ /**
+ * Print value 'o' to a string. Convenience overloading of printValue(Object, Appendable).
+ */
+ public static String printValue(Object o) {
+ StringBuffer buffer = new StringBuffer();
+ printValue(o, buffer);
+ return buffer.toString();
+ }
+
+ public static Object checkNotNull(Expression expr, Object obj) throws EvalException {
+ if (obj == null) {
+ throw new EvalException(expr.getLocation(),
+ "Unexpected null value, please send a bug report. "
+ + "This was generated by '" + expr + "'");
+ }
+ return obj;
+ }
+
+ /**
+ * Convert BUILD language objects to Formattable so JDK can render them correctly.
+ * Don't do this for numeric or string types because we want %d, %x, %s to work.
+ */
+ private static Object makeFormattable(final Object o) {
+ if (o instanceof Integer || o instanceof Double || o instanceof String) {
+ return o;
+ } else {
+ return new Formattable() {
+ @Override
+ public String toString() {
+ return "Formattable[" + o + "]";
+ }
+
+ @Override
+ public void formatTo(Formatter formatter, int flags, int width,
+ int precision) {
+ printValue(o, formatter.out());
+ }
+ };
+ }
+ }
+
+ private static final Object[] EMPTY = new Object[0];
+
+ /*
+ * N.B. MissingFormatWidthException is the only kind of IllegalFormatException
+ * whose constructor can take and display arbitrary error message, hence its use below.
+ */
+
+ /**
+ * Perform Python-style string formatting. Implemented by delegation to Java's
+ * own string formatting routine to avoid reinventing the wheel. In more
+ * obscure cases, semantics follow JDK (not Python) rules.
+ *
+ * @param pattern a format string.
+ * @param tuple a tuple containing positional arguments
+ */
+ public static String formatString(String pattern, List<?> tuple)
+ throws IllegalFormatException {
+ int count = countPlaceholders(pattern);
+ if (count < tuple.size()) {
+ throw new MissingFormatWidthException(
+ "not all arguments converted during string formatting");
+ }
+
+ List<Object> args = new ArrayList<>();
+
+ for (Object o : tuple) {
+ args.add(makeFormattable(o));
+ }
+
+ try {
+ return String.format(pattern, args.toArray(EMPTY));
+ } catch (IllegalFormatException e) {
+ throw new MissingFormatWidthException(
+ "invalid arguments for format string");
+ }
+ }
+
+ private static int countPlaceholders(String pattern) {
+ int length = pattern.length();
+ boolean afterPercent = false;
+ int i = 0;
+ int count = 0;
+ while (i < length) {
+ switch (pattern.charAt(i)) {
+ case 's':
+ case 'd':
+ if (afterPercent) {
+ count++;
+ afterPercent = false;
+ }
+ break;
+
+ case '%':
+ afterPercent = !afterPercent;
+ break;
+
+ default:
+ if (afterPercent) {
+ throw new MissingFormatWidthException("invalid arguments for format string");
+ }
+ afterPercent = false;
+ break;
+ }
+ i++;
+ }
+
+ return count;
+ }
+
+ /**
+ * @return the truth value of an object, according to Python rules.
+ * http://docs.python.org/2/library/stdtypes.html#truth-value-testing
+ */
+ public static boolean toBoolean(Object o) {
+ if (o == null || o == Environment.NONE) {
+ return false;
+ } else if (o instanceof Boolean) {
+ return (Boolean) o;
+ } else if (o instanceof String) {
+ return !((String) o).isEmpty();
+ } else if (o instanceof Integer) {
+ return (Integer) o != 0;
+ } else if (o instanceof Collection<?>) {
+ return !((Collection<?>) o).isEmpty();
+ } else if (o instanceof Map<?, ?>) {
+ return !((Map<?, ?>) o).isEmpty();
+ } else if (o instanceof NestedSet<?>) {
+ return !((NestedSet<?>) o).isEmpty();
+ } else if (o instanceof SkylarkNestedSet) {
+ return !((SkylarkNestedSet) o).isEmpty();
+ } else if (o instanceof Iterable<?>) {
+ return !(Iterables.isEmpty((Iterable<?>) o));
+ } else {
+ return true;
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ public static Collection<Object> toCollection(Object o, Location loc) throws EvalException {
+ if (o instanceof Collection) {
+ return (Collection<Object>) o;
+ } else if (o instanceof Map<?, ?>) {
+ // For dictionaries we iterate through the keys only
+ return ((Map<Object, Object>) o).keySet();
+ } else if (o instanceof SkylarkNestedSet) {
+ return ((SkylarkNestedSet) o).toCollection();
+ } else {
+ throw new EvalException(loc,
+ "type '" + EvalUtils.getDatatypeName(o) + "' is not a collection");
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ public static Iterable<Object> toIterable(Object o, Location loc) throws EvalException {
+ if (o instanceof String) {
+ // This is not as efficient as special casing String in for and dict and list comprehension
+ // statements. However this is a more unified way.
+ // The regex matches every character in the string until the end of the string,
+ // so "abc" will be split into ["a", "b", "c"].
+ return ImmutableList.<Object>copyOf(((String) o).split("(?!^)"));
+ } else if (o instanceof Iterable) {
+ return (Iterable<Object>) o;
+ } else if (o instanceof Map<?, ?>) {
+ // For dictionaries we iterate through the keys only
+ return ((Map<Object, Object>) o).keySet();
+ } else {
+ throw new EvalException(loc,
+ "type '" + EvalUtils.getDatatypeName(o) + "' is not an iterable");
+ }
+ }
+
+ /**
+ * Returns the size of the Skylark object or -1 in case the object doesn't have a size.
+ */
+ public static int size(Object arg) {
+ if (arg instanceof String) {
+ return ((String) arg).length();
+ } else if (arg instanceof Map) {
+ return ((Map<?, ?>) arg).size();
+ } else if (arg instanceof SkylarkList) {
+ return ((SkylarkList) arg).size();
+ } else if (arg instanceof Iterable) {
+ // Iterables.size() checks if arg is a Collection so it's efficient in that sense.
+ return Iterables.size((Iterable<?>) arg);
+ }
+ return -1;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/Expression.java b/src/main/java/com/google/devtools/build/lib/syntax/Expression.java
new file mode 100644
index 0000000..1659eb0
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/Expression.java
@@ -0,0 +1,51 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+/**
+ * Base class for all expression nodes in the AST.
+ */
+public abstract class Expression extends ASTNode {
+
+ /**
+ * Returns the result of evaluating this build-language expression in the
+ * specified environment. All BUILD language datatypes are mapped onto the
+ * corresponding Java types as follows:
+ *
+ * <pre>
+ * int -> Integer
+ * float -> Double (currently not generated by the grammar)
+ * str -> String
+ * [...] -> List<Object> (mutable)
+ * (...) -> List<Object> (immutable)
+ * {...} -> Map<Object, Object>
+ * func -> Function
+ * </pre>
+ *
+ * @return the result of evaluting the expression: a Java object corresponding
+ * to a datatype in the BUILD language.
+ * @throws EvalException if the expression could not be evaluated.
+ */
+ abstract Object eval(Environment env) throws EvalException, InterruptedException;
+
+ /**
+ * Returns the inferred type of the result of the Expression.
+ *
+ * <p>Checks the semantics of the Expression using the SkylarkEnvironment according to
+ * the rules of the Skylark language, throws EvalException in case of a semantical error.
+ *
+ * @see Statement
+ */
+ abstract SkylarkType validate(ValidationEnvironment env) throws EvalException;
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/ExpressionStatement.java b/src/main/java/com/google/devtools/build/lib/syntax/ExpressionStatement.java
new file mode 100644
index 0000000..f742d40
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/ExpressionStatement.java
@@ -0,0 +1,51 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+/**
+ * Syntax node for a function call statement. Used for build rules.
+ */
+public final class ExpressionStatement extends Statement {
+
+ private final Expression expr;
+
+ public ExpressionStatement(Expression expr) {
+ this.expr = expr;
+ }
+
+ public Expression getExpression() {
+ return expr;
+ }
+
+ @Override
+ public String toString() {
+ return expr.toString() + '\n';
+ }
+
+ @Override
+ void exec(Environment env) throws EvalException, InterruptedException {
+ expr.eval(env);
+ }
+
+ @Override
+ public void accept(SyntaxTreeVisitor visitor) {
+ visitor.visit(this);
+ }
+
+ @Override
+ void validate(ValidationEnvironment env) throws EvalException {
+ expr.validate(env);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/FilesetEntry.java b/src/main/java/com/google/devtools/build/lib/syntax/FilesetEntry.java
new file mode 100644
index 0000000..4586b64
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/FilesetEntry.java
@@ -0,0 +1,175 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.Collection;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+/**
+ * FilesetEntry is a value object used to represent a "FilesetEntry" inside a "Fileset" BUILD rule.
+ */
+public final class FilesetEntry {
+ /** SymlinkBehavior decides what to do when a source file of a FilesetEntry is a symlink. */
+ public enum SymlinkBehavior {
+ /** Just copies the symlink as-is. May result in dangling links. */
+ COPY,
+ /** Follow the link and make the destination point to the absolute path of the final target. */
+ DEREFERENCE;
+
+ public static SymlinkBehavior parse(String value) throws IllegalArgumentException {
+ return valueOf(value.toUpperCase());
+ }
+
+ @Override
+ public String toString() {
+ return super.toString().toLowerCase();
+ }
+ }
+
+ private final Label srcLabel;
+ @Nullable private final ImmutableList<Label> files;
+ @Nullable private final ImmutableSet<String> excludes;
+ private final PathFragment destDir;
+ private final SymlinkBehavior symlinkBehavior;
+ private final String stripPrefix;
+
+ /**
+ * Constructs a FilesetEntry with the given values.
+ *
+ * @param srcLabel the label of the source directory. Must be non-null.
+ * @param files The explicit files to include. May be null.
+ * @param excludes The files to exclude. Man be null. May only be non-null if files is null.
+ * @param destDir The target-relative output directory.
+ * @param symlinkBehavior how to treat symlinks on the input. See
+ * {@link FilesetEntry.SymlinkBehavior}.
+ * @param stripPrefix the prefix to strip from the package-relative path. If ".", keep only the
+ * basename.
+ */
+ public FilesetEntry(Label srcLabel,
+ @Nullable List<Label> files,
+ @Nullable List<String> excludes,
+ String destDir,
+ SymlinkBehavior symlinkBehavior,
+ String stripPrefix) {
+ this.srcLabel = checkNotNull(srcLabel);
+ this.destDir = new PathFragment((destDir == null) ? "" : destDir);
+ this.files = files == null ? null : ImmutableList.copyOf(files);
+ this.excludes = (excludes == null || excludes.isEmpty()) ? null : ImmutableSet.copyOf(excludes);
+ this.symlinkBehavior = symlinkBehavior;
+ this.stripPrefix = stripPrefix;
+ }
+
+ /**
+ * @return the source label.
+ */
+ public Label getSrcLabel() {
+ return srcLabel;
+ }
+
+ /**
+ * @return the destDir. Non null.
+ */
+ public PathFragment getDestDir() {
+ return destDir;
+ }
+
+ /**
+ * @return how symlinks should be handled.
+ */
+ public SymlinkBehavior getSymlinkBehavior() {
+ return symlinkBehavior;
+ }
+
+ /**
+ * @return an immutable list of excludes. Null if none specified.
+ */
+ @Nullable
+ public ImmutableSet<String> getExcludes() {
+ return excludes;
+ }
+
+ /**
+ * @return an immutable list of file labels. Null if none specified.
+ */
+ @Nullable
+ public ImmutableList<Label> getFiles() {
+ return files;
+ }
+
+ /**
+ * @return true if this Fileset should get files from the source directory.
+ */
+ public boolean isSourceFileset() {
+ return "BUILD".equals(srcLabel.getName());
+ }
+
+ /**
+ * @return all prerequisite labels in the FilesetEntry.
+ */
+ public Collection<Label> getLabels() {
+ Set<Label> labels = new LinkedHashSet<>();
+ if (files != null) {
+ labels.addAll(files);
+ } else {
+ labels.add(srcLabel);
+ }
+ return labels;
+ }
+
+ /**
+ * @return the prefix that should be stripped from package-relative path names.
+ */
+ public String getStripPrefix() {
+ return stripPrefix;
+ }
+
+ /**
+ * @return null if the entry is valid, and a human-readable error message otherwise.
+ */
+ @Nullable
+ public String validate() {
+ if (excludes != null && files != null) {
+ return "Cannot specify both 'files' and 'excludes' in a FilesetEntry";
+ } else if (files != null && !isSourceFileset()) {
+ return "Cannot specify files with Fileset label '" + srcLabel + "'";
+ } else if (destDir.isAbsolute()) {
+ return "Cannot specify absolute destdir '" + destDir + "'";
+ } else if (!stripPrefix.equals(".") && files == null) {
+ return "If the strip prefix is not '.', files must be specified";
+ } else if (new PathFragment(stripPrefix).containsUplevelReferences()) {
+ return "Strip prefix must not contain uplevel references";
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return String.format("FilesetEntry(srcdir=%s, destdir=%s, strip_prefix=%s, symlinks=%s, "
+ + "%d file(s) and %d excluded)", srcLabel, destDir, stripPrefix, symlinkBehavior,
+ files != null ? files.size() : 0,
+ excludes != null ? excludes.size() : 0);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/ForStatement.java b/src/main/java/com/google/devtools/build/lib/syntax/ForStatement.java
new file mode 100644
index 0000000..34a4eea
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/ForStatement.java
@@ -0,0 +1,97 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+
+import java.util.List;
+
+/**
+ * Syntax node for a for loop statement.
+ */
+public final class ForStatement extends Statement {
+
+ private final Ident variable;
+ private final Expression collection;
+ private final ImmutableList<Statement> block;
+
+ /**
+ * Constructs a for loop statement.
+ */
+ ForStatement(Ident variable, Expression collection, List<Statement> block) {
+ this.variable = Preconditions.checkNotNull(variable);
+ this.collection = Preconditions.checkNotNull(collection);
+ this.block = ImmutableList.copyOf(block);
+ }
+
+ public Ident getVariable() {
+ return variable;
+ }
+
+ public Expression getCollection() {
+ return collection;
+ }
+
+ public ImmutableList<Statement> block() {
+ return block;
+ }
+
+ @Override
+ public String toString() {
+ // TODO(bazel-team): if we want to print the complete statement, the function
+ // needs an extra argument to specify indentation level.
+ return "for " + variable + " in " + collection + ": ...\n";
+ }
+
+ @Override
+ void exec(Environment env) throws EvalException, InterruptedException {
+ Object o = collection.eval(env);
+ Iterable<?> col = EvalUtils.toIterable(o, getLocation());
+
+ int i = 0;
+ for (Object it : ImmutableList.copyOf(col)) {
+ env.update(variable.getName(), it);
+ for (Statement stmt : block) {
+ stmt.exec(env);
+ }
+ i++;
+ }
+ // TODO(bazel-team): This should not happen if every collection is immutable.
+ if (i != EvalUtils.size(col)) {
+ throw new EvalException(getLocation(),
+ String.format("Cannot modify '%s' during during iteration.", collection.toString()));
+ }
+ }
+
+ @Override
+ public void accept(SyntaxTreeVisitor visitor) {
+ visitor.visit(this);
+ }
+
+ @Override
+ void validate(ValidationEnvironment env) throws EvalException {
+ if (env.isTopLevel()) {
+ throw new EvalException(getLocation(),
+ "'For' is not allowed as a the top level statement");
+ }
+ // TODO(bazel-team): validate variable. Maybe make it temporarily readonly.
+ SkylarkType type = collection.validate(env);
+ env.checkIterable(type, getLocation());
+ env.update(variable.getName(), SkylarkType.UNKNOWN, getLocation());
+ for (Statement stmt : block) {
+ stmt.validate(env);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/FuncallExpression.java b/src/main/java/com/google/devtools/build/lib/syntax/FuncallExpression.java
new file mode 100644
index 0000000..e24d97f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/FuncallExpression.java
@@ -0,0 +1,550 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+import com.google.common.base.Preconditions;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.syntax.EvalException.EvalExceptionWithJavaCause;
+import com.google.devtools.build.lib.util.StringUtilities;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+
+/**
+ * Syntax node for a function call expression.
+ */
+public final class FuncallExpression extends Expression {
+
+ private static enum ArgConversion {
+ FROM_SKYLARK,
+ TO_SKYLARK,
+ NO_CONVERSION
+ }
+
+ /**
+ * A value class to store Methods with their corresponding SkylarkCallable annotations.
+ * This is needed because the annotation is sometimes in a superclass.
+ */
+ public static final class MethodDescriptor {
+ private final Method method;
+ private final SkylarkCallable annotation;
+
+ private MethodDescriptor(Method method, SkylarkCallable annotation) {
+ this.method = method;
+ this.annotation = annotation;
+ }
+
+ Method getMethod() {
+ return method;
+ }
+
+ /**
+ * Returns the SkylarkCallable annotation corresponding to this method.
+ */
+ public SkylarkCallable getAnnotation() {
+ return annotation;
+ }
+ }
+
+ private static final LoadingCache<Class<?>, Map<String, List<MethodDescriptor>>> methodCache =
+ CacheBuilder.newBuilder()
+ .initialCapacity(10)
+ .maximumSize(100)
+ .build(new CacheLoader<Class<?>, Map<String, List<MethodDescriptor>>>() {
+
+ @Override
+ public Map<String, List<MethodDescriptor>> load(Class<?> key) throws Exception {
+ Map<String, List<MethodDescriptor>> methodMap = new HashMap<>();
+ for (Method method : key.getMethods()) {
+ // Synthetic methods lead to false multiple matches
+ if (method.isSynthetic()) {
+ continue;
+ }
+ SkylarkCallable callable = getAnnotationFromParentClass(
+ method.getDeclaringClass(), method);
+ if (callable == null) {
+ continue;
+ }
+ String name = callable.name();
+ if (name.isEmpty()) {
+ name = StringUtilities.toPythonStyleFunctionName(method.getName());
+ }
+ String signature = name + "#" + method.getParameterTypes().length;
+ if (methodMap.containsKey(signature)) {
+ methodMap.get(signature).add(new MethodDescriptor(method, callable));
+ } else {
+ methodMap.put(signature, Lists.newArrayList(new MethodDescriptor(method, callable)));
+ }
+ }
+ return ImmutableMap.copyOf(methodMap);
+ }
+ });
+
+ /**
+ * Returns a map of methods and corresponding SkylarkCallable annotations
+ * of the methods of the classObj class reachable from Skylark.
+ */
+ public static ImmutableMap<Method, SkylarkCallable> collectSkylarkMethodsWithAnnotation(
+ Class<?> classObj) {
+ ImmutableMap.Builder<Method, SkylarkCallable> methodMap = ImmutableMap.builder();
+ for (Method method : classObj.getMethods()) {
+ // Synthetic methods lead to false multiple matches
+ if (!method.isSynthetic()) {
+ SkylarkCallable annotation = getAnnotationFromParentClass(classObj, method);
+ if (annotation != null) {
+ methodMap.put(method, annotation);
+ }
+ }
+ }
+ return methodMap.build();
+ }
+
+ private static SkylarkCallable getAnnotationFromParentClass(Class<?> classObj, Method method) {
+ boolean keepLooking = false;
+ try {
+ Method superMethod = classObj.getMethod(method.getName(), method.getParameterTypes());
+ if (classObj.isAnnotationPresent(SkylarkModule.class)
+ && superMethod.isAnnotationPresent(SkylarkCallable.class)) {
+ return superMethod.getAnnotation(SkylarkCallable.class);
+ } else {
+ keepLooking = true;
+ }
+ } catch (NoSuchMethodException e) {
+ // The class might not have the specified method, so an exceptions is OK.
+ keepLooking = true;
+ }
+ if (keepLooking) {
+ if (classObj.getSuperclass() != null) {
+ SkylarkCallable annotation = getAnnotationFromParentClass(classObj.getSuperclass(), method);
+ if (annotation != null) {
+ return annotation;
+ }
+ }
+ for (Class<?> interfaceObj : classObj.getInterfaces()) {
+ SkylarkCallable annotation = getAnnotationFromParentClass(interfaceObj, method);
+ if (annotation != null) {
+ return annotation;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * An exception class to handle exceptions in direct Java API calls.
+ */
+ public static final class FuncallException extends Exception {
+
+ public FuncallException(String msg) {
+ super(msg);
+ }
+ }
+
+ private final Expression obj;
+
+ private final Ident func;
+
+ private final List<Argument> args;
+
+ private final int numPositionalArgs;
+
+ /**
+ * Note: the grammar definition restricts the function value in a function
+ * call expression to be a global identifier; however, the representation of
+ * values in the interpreter is flexible enough to allow functions to be
+ * arbitrary expressions. In any case, the "func" expression is always
+ * evaluated, so functions and variables share a common namespace.
+ */
+ public FuncallExpression(Expression obj, Ident func,
+ List<Argument> args) {
+ for (Argument arg : args) {
+ Preconditions.checkArgument(arg.hasValue());
+ }
+ this.obj = obj;
+ this.func = func;
+ this.args = args;
+ this.numPositionalArgs = countPositionalArguments();
+ }
+
+ /**
+ * Note: the grammar definition restricts the function value in a function
+ * call expression to be a global identifier; however, the representation of
+ * values in the interpreter is flexible enough to allow functions to be
+ * arbitrary expressions. In any case, the "func" expression is always
+ * evaluated, so functions and variables share a common namespace.
+ */
+ public FuncallExpression(Ident func, List<Argument> args) {
+ this(null, func, args);
+ }
+
+ /**
+ * Returns the number of positional arguments.
+ */
+ private int countPositionalArguments() {
+ int num = 0;
+ for (Argument arg : args) {
+ if (arg.isPositional()) {
+ num++;
+ }
+ }
+ return num;
+ }
+
+ /**
+ * Returns the function expression.
+ */
+ public Ident getFunction() {
+ return func;
+ }
+
+ /**
+ * Returns the object the function called on.
+ * It's null if the function is not called on an object.
+ */
+ public Expression getObject() {
+ return obj;
+ }
+
+ /**
+ * Returns an (immutable, ordered) list of function arguments. The first n are
+ * positional and the remaining ones are keyword args, where n =
+ * getNumPositionalArguments().
+ */
+ public List<Argument> getArguments() {
+ return Collections.unmodifiableList(args);
+ }
+
+ /**
+ * Returns the number of arguments which are positional; the remainder are
+ * keyword arguments.
+ */
+ public int getNumPositionalArguments() {
+ return numPositionalArgs;
+ }
+
+ @Override
+ public String toString() {
+ if (func.getName().equals("$substring")) {
+ return obj + "[" + args.get(0) + ":" + args.get(1) + "]";
+ }
+ if (func.getName().equals("$index")) {
+ return obj + "[" + args.get(0) + "]";
+ }
+ if (obj != null) {
+ return obj + "." + func + "(" + args + ")";
+ }
+ return func + "(" + args + ")";
+ }
+
+ /**
+ * Returns the list of Skylark callable Methods of objClass with the given name
+ * and argument number.
+ */
+ public static List<MethodDescriptor> getMethods(Class<?> objClass, String methodName, int argNum)
+ throws ExecutionException {
+ return methodCache.get(objClass).get(methodName + "#" + argNum);
+ }
+
+ /**
+ * Returns the list of the Skylark name of all Skylark callable methods.
+ */
+ public static List<String> getMethodNames(Class<?> objClass)
+ throws ExecutionException {
+ List<String> names = new ArrayList<>();
+ for (List<MethodDescriptor> methods : methodCache.get(objClass).values()) {
+ for (MethodDescriptor method : methods) {
+ // TODO(bazel-team): store the Skylark name in the MethodDescriptor.
+ String name = method.annotation.name();
+ if (name.isEmpty()) {
+ name = StringUtilities.toPythonStyleFunctionName(method.method.getName());
+ }
+ names.add(name);
+ }
+ }
+ return names;
+ }
+
+ static Object callMethod(MethodDescriptor methodDescriptor, String methodName, Object obj,
+ Object[] args, Location loc) throws EvalException, IllegalAccessException,
+ IllegalArgumentException, InvocationTargetException {
+ Method method = methodDescriptor.getMethod();
+ if (obj == null && !Modifier.isStatic(method.getModifiers())) {
+ throw new EvalException(loc, "Method '" + methodName + "' is not static");
+ }
+ // This happens when the interface is public but the implementation classes
+ // have reduced visibility.
+ method.setAccessible(true);
+ Object result = method.invoke(obj, args);
+ if (method.getReturnType().equals(Void.TYPE)) {
+ return Environment.NONE;
+ }
+ if (result == null) {
+ if (methodDescriptor.getAnnotation().allowReturnNones()) {
+ return Environment.NONE;
+ } else {
+ throw new EvalException(loc,
+ "Method invocation returned None, please contact Skylark developers: " + methodName
+ + "(" + EvalUtils.prettyPrintValues(", ", ImmutableList.copyOf(args)) + ")");
+ }
+ }
+ result = SkylarkType.convertToSkylark(result, method);
+ if (result != null && !EvalUtils.isSkylarkImmutable(result.getClass())) {
+ throw new EvalException(loc, "Method '" + methodName
+ + "' returns a mutable object (type of " + EvalUtils.getDatatypeName(result) + ")");
+ }
+ return result;
+ }
+
+ // TODO(bazel-team): If there's exactly one usable method, this works. If there are multiple
+ // matching methods, it still can be a problem. Figure out how the Java compiler does it
+ // exactly and copy that behaviour.
+ // TODO(bazel-team): check if this and SkylarkBuiltInFunctions.createObject can be merged.
+ private Object invokeJavaMethod(
+ Object obj, Class<?> objClass, String methodName, List<Object> args) throws EvalException {
+ try {
+ MethodDescriptor matchingMethod = null;
+ List<MethodDescriptor> methods = getMethods(objClass, methodName, args.size());
+ if (methods != null) {
+ for (MethodDescriptor method : methods) {
+ Class<?>[] params = method.getMethod().getParameterTypes();
+ int i = 0;
+ boolean matching = true;
+ for (Class<?> param : params) {
+ if (!param.isAssignableFrom(args.get(i).getClass())) {
+ matching = false;
+ break;
+ }
+ i++;
+ }
+ if (matching) {
+ if (matchingMethod == null) {
+ matchingMethod = method;
+ } else {
+ throw new EvalException(func.getLocation(),
+ "Multiple matching methods for " + formatMethod(methodName, args)
+ + " in " + EvalUtils.getDataTypeNameFromClass(objClass));
+ }
+ }
+ }
+ }
+ if (matchingMethod != null && !matchingMethod.getAnnotation().structField()) {
+ return callMethod(matchingMethod, methodName, obj, args.toArray(), getLocation());
+ } else {
+ throw new EvalException(getLocation(), "No matching method found for "
+ + formatMethod(methodName, args) + " in "
+ + EvalUtils.getDataTypeNameFromClass(objClass));
+ }
+ } catch (IllegalAccessException e) {
+ // TODO(bazel-team): Print a nice error message. Maybe the method exists
+ // and an argument is missing or has the wrong type.
+ throw new EvalException(getLocation(), "Method invocation failed: " + e);
+ } catch (InvocationTargetException e) {
+ if (e.getCause() instanceof FuncallException) {
+ throw new EvalException(getLocation(), e.getCause().getMessage());
+ } else if (e.getCause() != null) {
+ throw new EvalExceptionWithJavaCause(getLocation(), e.getCause());
+ } else {
+ // This is unlikely to happen
+ throw new EvalException(getLocation(), "Method invocation failed: " + e);
+ }
+ } catch (ExecutionException e) {
+ throw new EvalException(getLocation(), "Method invocation failed: " + e);
+ }
+ }
+
+ private String formatMethod(String methodName, List<Object> args) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(methodName).append("(");
+ boolean first = true;
+ for (Object obj : args) {
+ if (!first) {
+ sb.append(", ");
+ }
+ sb.append(EvalUtils.getDatatypeName(obj));
+ first = false;
+ }
+ return sb.append(")").toString();
+ }
+
+ /**
+ * Add one argument to the keyword map, raising an exception when names conflict.
+ */
+ private void addKeywordArg(Map<String, Object> kwargs, String name, Object value)
+ throws EvalException {
+ if (kwargs.put(name, value) != null) {
+ throw new EvalException(getLocation(),
+ "duplicate keyword '" + name + "' in call to '" + func + "'");
+ }
+ }
+
+ /**
+ * Add multiple arguments to the keyword map (**kwargs).
+ */
+ private void addKeywordArgs(Map<String, Object> kwargs, Object items)
+ throws EvalException {
+ if (!(items instanceof Map<?, ?>)) {
+ throw new EvalException(getLocation(),
+ "Argument after ** must be a dictionary, not " + EvalUtils.getDatatypeName(items));
+ }
+ for (Map.Entry<?, ?> entry : ((Map<?, ?>) items).entrySet()) {
+ if (!(entry.getKey() instanceof String)) {
+ throw new EvalException(getLocation(),
+ "Keywords must be strings, not " + EvalUtils.getDatatypeName(entry.getKey()));
+ }
+ addKeywordArg(kwargs, (String) entry.getKey(), entry.getValue());
+ }
+ }
+
+ private void evalArguments(List<Object> posargs, Map<String, Object> kwargs,
+ Environment env, Function function)
+ throws EvalException, InterruptedException {
+ ArgConversion conversion = getArgConversion(function);
+ for (Argument arg : args) {
+ Object value = arg.getValue().eval(env);
+ if (conversion == ArgConversion.FROM_SKYLARK) {
+ value = SkylarkType.convertFromSkylark(value);
+ } else if (conversion == ArgConversion.TO_SKYLARK) {
+ // We try to auto convert the type if we can.
+ value = SkylarkType.convertToSkylark(value, getLocation());
+ // We call into Skylark so we need to be sure that the caller uses the appropriate types.
+ SkylarkType.checkTypeAllowedInSkylark(value, getLocation());
+ }
+ if (arg.isPositional()) {
+ posargs.add(value);
+ } else if (arg.isKwargs()) { // expand the kwargs
+ addKeywordArgs(kwargs, value);
+ } else {
+ addKeywordArg(kwargs, arg.getArgName(), value);
+ }
+ }
+ if (function instanceof UserDefinedFunction) {
+ // Adding the default values for a UserDefinedFunction if needed.
+ UserDefinedFunction func = (UserDefinedFunction) function;
+ if (args.size() < func.getArgs().size()) {
+ for (Map.Entry<String, Object> entry : func.getDefaultValues().entrySet()) {
+ String key = entry.getKey();
+ if (func.getArgIndex(key) >= numPositionalArgs && !kwargs.containsKey(key)) {
+ kwargs.put(key, entry.getValue());
+ }
+ }
+ }
+ }
+ }
+
+ static boolean isNamespace(Class<?> classObject) {
+ return classObject.isAnnotationPresent(SkylarkModule.class)
+ && classObject.getAnnotation(SkylarkModule.class).namespace();
+ }
+
+ @Override
+ Object eval(Environment env) throws EvalException, InterruptedException {
+ List<Object> posargs = new ArrayList<>();
+ Map<String, Object> kwargs = new LinkedHashMap<>();
+
+ if (obj != null) {
+ Object objValue = obj.eval(env);
+ // Strings, lists and dictionaries (maps) have functions that we want to use in MethodLibrary.
+ // For other classes, we can call the Java methods.
+ Function function =
+ env.getFunction(EvalUtils.getSkylarkType(objValue.getClass()), func.getName());
+ if (function != null) {
+ if (!isNamespace(objValue.getClass())) {
+ posargs.add(objValue);
+ }
+ evalArguments(posargs, kwargs, env, function);
+ return EvalUtils.checkNotNull(this, function.call(posargs, kwargs, this, env));
+ } else if (env.isSkylarkEnabled()) {
+
+ // When calling a Java method, the name is not in the Environment, so
+ // evaluating 'func' would fail. For arguments we don't need to consider the default
+ // arguments since the Java function doesn't have any.
+
+ evalArguments(posargs, kwargs, env, null);
+ if (!kwargs.isEmpty()) {
+ throw new EvalException(func.getLocation(),
+ "Keyword arguments are not allowed when calling a java method");
+ }
+ if (objValue instanceof Class<?>) {
+ // Static Java method call
+ return invokeJavaMethod(null, (Class<?>) objValue, func.getName(), posargs);
+ } else {
+ return invokeJavaMethod(objValue, objValue.getClass(), func.getName(), posargs);
+ }
+ } else {
+ throw new EvalException(getLocation(), String.format(
+ "function '%s' is not defined on '%s'", func.getName(),
+ EvalUtils.getDatatypeName(objValue)));
+ }
+ }
+
+ Object funcValue = func.eval(env);
+ if (!(funcValue instanceof Function)) {
+ throw new EvalException(getLocation(),
+ "'" + EvalUtils.getDatatypeName(funcValue)
+ + "' object is not callable");
+ }
+ Function function = (Function) funcValue;
+ evalArguments(posargs, kwargs, env, function);
+ return EvalUtils.checkNotNull(this, function.call(posargs, kwargs, this, env));
+ }
+
+ private ArgConversion getArgConversion(Function function) {
+ if (function == null) {
+ // It means we try to call a Java function.
+ return ArgConversion.FROM_SKYLARK;
+ }
+ // If we call a UserDefinedFunction we call into Skylark. If we call from Skylark
+ // the argument conversion is invariant, but if we call from the BUILD language
+ // we might need an auto conversion.
+ return function instanceof UserDefinedFunction
+ ? ArgConversion.TO_SKYLARK : ArgConversion.NO_CONVERSION;
+ }
+
+ @Override
+ public void accept(SyntaxTreeVisitor visitor) {
+ visitor.visit(this);
+ }
+
+ @Override
+ SkylarkType validate(ValidationEnvironment env) throws EvalException {
+ // TODO(bazel-team): implement semantical check.
+
+ if (obj != null) {
+ // TODO(bazel-team): validate function calls on objects too.
+ return env.getReturnType(obj.validate(env), func.getName(), getLocation());
+ } else {
+ // TODO(bazel-team): Imported functions are not validated properly.
+ if (!env.hasSymbolInEnvironment(func.getName())) {
+ throw new EvalException(getLocation(),
+ String.format("function '%s' does not exist", func.getName()));
+ }
+ return env.getReturnType(func.getName(), getLocation());
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/Function.java b/src/main/java/com/google/devtools/build/lib/syntax/Function.java
new file mode 100644
index 0000000..5636a95
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/Function.java
@@ -0,0 +1,49 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Function values in the BUILD language.
+ *
+ * <p>Each implementation of this interface defines a function in the BUILD language.
+ *
+ */
+public interface Function {
+
+ /**
+ * Implements the behavior of a call to a function with positional arguments
+ * "args" and keyword arguments "kwargs". The "ast" argument is provided to
+ * allow construction of EvalExceptions containing source information.
+ */
+ Object call(List<Object> args,
+ Map<String, Object> kwargs,
+ FuncallExpression ast,
+ Environment env)
+ throws EvalException, InterruptedException;
+
+ /**
+ * Returns the name of the function.
+ */
+ String getName();
+
+ // TODO(bazel-team): implement this for MethodLibrary functions as well.
+ /**
+ * Returns the type of the object on which this function is defined or null
+ * if this is a global function.
+ */
+ Class<?> getObjectType();
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/FunctionDefStatement.java b/src/main/java/com/google/devtools/build/lib/syntax/FunctionDefStatement.java
new file mode 100644
index 0000000..29ed579
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/FunctionDefStatement.java
@@ -0,0 +1,97 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.syntax.SkylarkType.SkylarkFunctionType;
+
+import java.util.Collection;
+
+/**
+ * Syntax node for a function definition.
+ */
+public class FunctionDefStatement extends Statement {
+
+ private final Ident ident;
+ private final ImmutableList<Argument> args;
+ private final ImmutableList<Statement> statements;
+
+ public FunctionDefStatement(Ident ident, Collection<Argument> args,
+ Collection<Statement> statements) {
+ for (Argument arg : args) {
+ Preconditions.checkArgument(arg.isNamed());
+ }
+ this.ident = ident;
+ this.args = ImmutableList.copyOf(args);
+ this.statements = ImmutableList.copyOf(statements);
+ }
+
+ @Override
+ void exec(Environment env) throws EvalException, InterruptedException {
+ ImmutableMap.Builder<String, Object> defaultValues = ImmutableMap.builder();
+ for (Argument arg : args) {
+ if (arg.hasValue()) {
+ defaultValues.put(arg.getArgName(), arg.getValue().eval(env));
+ }
+ }
+ env.update(ident.getName(), new UserDefinedFunction(
+ ident, args, defaultValues.build(), statements, (SkylarkEnvironment) env));
+ }
+
+ @Override
+ public String toString() {
+ return "def " + ident + "(" + args + "):\n";
+ }
+
+ public Ident getIdent() {
+ return ident;
+ }
+
+ public ImmutableList<Statement> getStatements() {
+ return statements;
+ }
+
+ public ImmutableList<Argument> getArgs() {
+ return args;
+ }
+
+ @Override
+ public void accept(SyntaxTreeVisitor visitor) {
+ visitor.visit(this);
+ }
+
+ @Override
+ void validate(ValidationEnvironment env) throws EvalException {
+ SkylarkFunctionType type = SkylarkFunctionType.of(ident.getName());
+ ValidationEnvironment localEnv = new ValidationEnvironment(env, type);
+ for (Argument i : args) {
+ SkylarkType argType = SkylarkType.UNKNOWN;
+ if (i.hasValue()) {
+ argType = i.getValue().validate(env);
+ if (argType.equals(SkylarkType.NONE)) {
+ argType = SkylarkType.UNKNOWN;
+ }
+ }
+ localEnv.update(i.getArgName(), argType, getLocation());
+ }
+ for (Statement stmts : statements) {
+ stmts.validate(localEnv);
+ }
+ env.updateFunction(ident.getName(), type, getLocation());
+ // Register a dummy return value with an incompatible type if there was no return statement.
+ type.setReturnType(SkylarkType.NONE, getLocation());
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/GlobCriteria.java b/src/main/java/com/google/devtools/build/lib/syntax/GlobCriteria.java
new file mode 100644
index 0000000..577bd4a
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/GlobCriteria.java
@@ -0,0 +1,214 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+import com.google.common.base.Functions;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableList.Builder;
+import com.google.common.collect.Iterables;
+
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+/**
+ * Either the arguments to a glob call (the include and exclude lists) or the
+ * contents of a fixed list that was appended to a list of glob results.
+ * (The latter need to be stored by {@link GlobList} in order to fully
+ * reproduce the inputs that created the output list.)
+ *
+ * <p>For example, the expression
+ * <code>glob(['*.java']) + ['x.properties']</code>
+ * will result in two GlobCriteria: one has include = ['*.java'], glob = true
+ * and the other, include = ['x.properties'], glob = false.
+ */
+public class GlobCriteria {
+
+ /**
+ * A list of names or patterns that are included by this glob. They should
+ * consist of characters that are valid in labels in the BUILD language.
+ */
+ private final ImmutableList<String> include;
+
+ /**
+ * A list of names or patterns that are excluded by this glob. They should
+ * consist of characters that are valid in labels in the BUILD language.
+ */
+ private final ImmutableList<String> exclude;
+
+ /** True if the includes list was passed to glob(), false if not. */
+ private final boolean glob;
+
+ /**
+ * Parses criteria from its {@link #toExpression} form.
+ * Package-private for use by tests and GlobList.
+ * @throws IllegalArgumentException if the expression cannot be parsed
+ */
+ public static GlobCriteria parse(String text) {
+ if (text.startsWith("glob([") && text.endsWith("])")) {
+ int excludeIndex = text.indexOf("], exclude=[");
+ if (excludeIndex == -1) {
+ String listText = text.substring(6, text.length() - 2);
+ return new GlobCriteria(parseList(listText), ImmutableList.<String>of(), true);
+ } else {
+ String listText = text.substring(6, excludeIndex);
+ String excludeText = text.substring(excludeIndex + 12, text.length() - 2);
+ return new GlobCriteria(parseList(listText), parseList(excludeText), true);
+ }
+ } else if (text.startsWith("[") && text.endsWith("]")) {
+ String listText = text.substring(1, text.length() - 1);
+ return new GlobCriteria(parseList(listText), ImmutableList.<String>of(), false);
+ } else {
+ throw new IllegalArgumentException(
+ "unrecognized format (not from toExpression?): " + text);
+ }
+ }
+
+ /**
+ * Constructs a copy of a given glob critera object, with additional exclude patterns added.
+ *
+ * @param base a glob criteria object to copy. Must be an actual glob
+ * @param excludes a list of pattern strings indicating new excludes to provide
+ * @return a new glob criteria object which contains the same parameters as {@code base}, with
+ * the additional patterns in {@code excludes} added.
+ * @throws IllegalArgumentException if {@code base} is not a glob
+ */
+ public static GlobCriteria createWithAdditionalExcludes(GlobCriteria base,
+ List<String> excludes) {
+ Preconditions.checkArgument(base.isGlob());
+ return fromGlobCall(base.include,
+ ImmutableList.copyOf(Iterables.concat(base.exclude, excludes)));
+ }
+
+ /**
+ * Constructs a copy of a fixed list, converted to Strings.
+ */
+ public static GlobCriteria fromList(Iterable<?> list) {
+ Iterable<String> strings = Iterables.transform(list, Functions.toStringFunction());
+ return new GlobCriteria(ImmutableList.copyOf(strings), ImmutableList.<String>of(), false);
+ }
+
+ /**
+ * Constructs a glob call with include and exclude list.
+ *
+ * @param include list of included patterns
+ * @param exclude list of excluded patterns
+ */
+ public static GlobCriteria fromGlobCall(
+ ImmutableList<String> include, ImmutableList<String> exclude) {
+ return new GlobCriteria(include, exclude, true);
+ }
+
+ /**
+ * Constructs a glob call with include and exclude list.
+ */
+ private GlobCriteria(ImmutableList<String> include, ImmutableList<String> exclude, boolean glob) {
+ this.include = include;
+ this.exclude = exclude;
+ this.glob = glob;
+ }
+
+ /**
+ * Returns the patterns that were included in this {@code glob()} call.
+ */
+ public ImmutableList<String> getIncludePatterns() {
+ return include;
+ }
+
+ /**
+ * Returns the patterns that were excluded in this {@code glob()} call.
+ */
+ public ImmutableList<String> getExcludePatterns() {
+ return exclude;
+ }
+
+ /**
+ * Returns true if the include list was passed to {@code glob()}, false
+ * if it was a fixed list. If this returns false, the exclude list will
+ * always be empty.
+ */
+ public boolean isGlob() {
+ return glob;
+ }
+
+ /**
+ * Returns a String that represents this glob as a BUILD expression.
+ * For example, <code>glob(['abc', 'def'], exclude=['uvw', 'xyz'])</code>
+ * or <code>['foo', 'bar', 'baz']</code>.
+ */
+ public String toExpression() {
+ StringBuilder sb = new StringBuilder();
+ if (glob) {
+ sb.append("glob(");
+ }
+ sb.append('[');
+ appendList(sb, include);
+ if (!exclude.isEmpty()) {
+ sb.append("], exclude=[");
+ appendList(sb, exclude);
+ }
+ sb.append(']');
+ if (glob) {
+ sb.append(')');
+ }
+ return sb.toString();
+ }
+
+ @Override
+ public String toString() {
+ return toExpression();
+ }
+
+ /**
+ * Takes a list of Strings, quotes them in single quotes, and appends them to
+ * a StringBuilder separated by a comma and space. This can be parsed back
+ * out by {@link #parseList}.
+ */
+ private static void appendList(StringBuilder sb, List<String> list) {
+ boolean first = true;
+ for (String content : list) {
+ if (!first) {
+ sb.append(", ");
+ }
+ sb.append('\'').append(content).append('\'');
+ first = false;
+ }
+ }
+
+ /**
+ * Takes a String in the format created by {@link #appendList} and returns
+ * the original Strings. A null String (which may be returned when Pattern
+ * does not find a match) or the String "" (which will be captured in "[]")
+ * will result in an empty list.
+ */
+ private static ImmutableList<String> parseList(@Nullable String text) {
+ if (text == null) {
+ return ImmutableList.of();
+ }
+ Iterable<String> split = Splitter.on(", ").split(text);
+ Builder<String> listBuilder = ImmutableList.builder();
+ for (String element : split) {
+ if (!element.isEmpty()) {
+ if ((element.length() < 2) || !element.startsWith("'") || !element.endsWith("'")) {
+ throw new IllegalArgumentException("expected a filename or pattern in quotes: " + text);
+ }
+ listBuilder.add(element.substring(1, element.length() - 1));
+ }
+ }
+ return listBuilder.build();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/GlobList.java b/src/main/java/com/google/devtools/build/lib/syntax/GlobList.java
new file mode 100644
index 0000000..82afd01
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/GlobList.java
@@ -0,0 +1,122 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Splitter;
+import com.google.common.collect.ForwardingList;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableList.Builder;
+import com.google.common.collect.Iterables;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Glob matches and information about glob patterns, which are useful to
+ * ide_build_info. Its implementation of the List interface is as an immutable
+ * list of the matching files. Glob criteria can be retrieved through
+ * {@link #getCriteria}.
+ *
+ * @param <E> the element this List contains (generally either String or Label)
+ */
+public class GlobList<E> extends ForwardingList<E> {
+
+ /** Include/exclude criteria. */
+ private final ImmutableList<GlobCriteria> criteria;
+
+ /** Matching files (usually either String or Label). */
+ private final ImmutableList<E> matches;
+
+ /**
+ * Constructs a list with {@code glob()} call results.
+ *
+ * @param includes the patterns that the glob includes
+ * @param excludes the patterns that the glob excludes
+ * @param matches the filenames that matched the includes/excludes criteria
+ */
+ public static <T> GlobList<T> captureResults(List<String> includes,
+ List<String> excludes, List<T> matches) {
+ GlobCriteria criteria = GlobCriteria.fromGlobCall(
+ ImmutableList.copyOf(includes), ImmutableList.copyOf(excludes));
+ return new GlobList<>(ImmutableList.of(criteria), matches);
+ }
+
+ /**
+ * Parses a GlobInfo from its {@link #toExpression} representation.
+ */
+ public static GlobList<String> parse(String text) {
+ List<GlobCriteria> criteria = new ArrayList<>();
+ Iterable<String> globs = Splitter.on(" + ").split(text);
+ for (String glob : globs) {
+ criteria.add(GlobCriteria.parse(glob));
+ }
+ return new GlobList<>(criteria, ImmutableList.<String>of());
+ }
+
+ /**
+ * Concatenates two lists into a new GlobList. If either of the lists is a
+ * GlobList, its GlobCriteria are preserved. Otherwise a simple GlobCriteria
+ * is created to represent the fixed list.
+ */
+ public static <T> GlobList<T> concat(
+ List<? extends T> list1, List<? extends T> list2) {
+ // we add the list to both includes and matches, preserving order
+ Builder<GlobCriteria> criteriaBuilder = ImmutableList.<GlobCriteria>builder();
+ if (list1 instanceof GlobList<?>) {
+ criteriaBuilder.addAll(((GlobList<?>) list1).criteria);
+ } else {
+ criteriaBuilder.add(GlobCriteria.fromList(list1));
+ }
+ if (list2 instanceof GlobList<?>) {
+ criteriaBuilder.addAll(((GlobList<?>) list2).criteria);
+ } else {
+ criteriaBuilder.add(GlobCriteria.fromList(list2));
+ }
+ List<T> matches = ImmutableList.copyOf(Iterables.concat(list1, list2));
+ return new GlobList<>(criteriaBuilder.build(), matches);
+ }
+
+ /**
+ * Constructs a list with given criteria and matches.
+ */
+ public GlobList(List<GlobCriteria> criteria, List<E> matches) {
+ Preconditions.checkNotNull(criteria);
+ Preconditions.checkNotNull(matches);
+ this.criteria = ImmutableList.copyOf(criteria);
+ this.matches = ImmutableList.copyOf(matches);
+ }
+
+ /**
+ * Returns the criteria used to create this list, from which the
+ * includes/excludes can be retrieved.
+ */
+ public ImmutableList<GlobCriteria> getCriteria() {
+ return criteria;
+ }
+
+ /**
+ * Returns a String that represents this glob list as a BUILD expression.
+ */
+ public String toExpression() {
+ return Joiner.on(" + ").join(criteria);
+ }
+
+ @Override
+ protected ImmutableList<E> delegate() {
+ return matches;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/Ident.java b/src/main/java/com/google/devtools/build/lib/syntax/Ident.java
new file mode 100644
index 0000000..86bd458
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/Ident.java
@@ -0,0 +1,66 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+/**
+ * Syntax node for an identifier.
+ */
+public final class Ident extends Expression {
+
+ private final String name;
+
+ public Ident(String name) {
+ this.name = name;
+ }
+
+ /**
+ * Returns the name of the Ident.
+ */
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+
+ @Override
+ Object eval(Environment env) throws EvalException {
+ try {
+ return env.lookup(name);
+ } catch (Environment.NoSuchVariableException e) {
+ if (name.equals("$error$")) {
+ throw new EvalException(getLocation(), "contains syntax error(s)", true);
+ } else {
+ throw new EvalException(getLocation(), "name '" + name + "' is not defined");
+ }
+ }
+ }
+
+ @Override
+ public void accept(SyntaxTreeVisitor visitor) {
+ visitor.visit(this);
+ }
+
+ @Override
+ SkylarkType validate(ValidationEnvironment env) throws EvalException {
+ if (env.hasSymbolInEnvironment(name)) {
+ return env.getVartype(name);
+ } else {
+ throw new EvalException(getLocation(), "name '" + name + "' is not defined");
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/IfStatement.java b/src/main/java/com/google/devtools/build/lib/syntax/IfStatement.java
new file mode 100644
index 0000000..3877a9c
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/IfStatement.java
@@ -0,0 +1,138 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+
+import java.util.List;
+
+// TODO(bazel-team): maybe we should get rid of the ConditionalStatements and
+// create a chain of if-else statements for elif-s.
+/**
+ * Syntax node for an if/else statement.
+ */
+public final class IfStatement extends Statement {
+
+ /**
+ * Syntax node for an [el]if statement.
+ */
+ static final class ConditionalStatements extends Statement {
+
+ private final Expression condition;
+ private final ImmutableList<Statement> stmts;
+
+ public ConditionalStatements(Expression condition, List<Statement> stmts) {
+ this.condition = Preconditions.checkNotNull(condition);
+ this.stmts = ImmutableList.copyOf(stmts);
+ }
+
+ @Override
+ void exec(Environment env) throws EvalException, InterruptedException {
+ for (Statement stmt : stmts) {
+ stmt.exec(env);
+ }
+ }
+
+ @Override
+ public String toString() {
+ // TODO(bazel-team): see TODO in the outer class
+ return "[el]if " + condition + ": ...\n";
+ }
+
+ @Override
+ public void accept(SyntaxTreeVisitor visitor) {
+ visitor.visit(this);
+ }
+
+ Expression getCondition() {
+ return condition;
+ }
+
+ ImmutableList<Statement> getStmts() {
+ return stmts;
+ }
+
+ @Override
+ void validate(ValidationEnvironment env) throws EvalException {
+ // EvalUtils.toBoolean() evaluates everything so we don't need type check here.
+ condition.validate(env);
+ validateStmts(env, stmts);
+ }
+ }
+
+ private final ImmutableList<ConditionalStatements> thenBlocks;
+ private final ImmutableList<Statement> elseBlock;
+
+ /**
+ * Constructs a if-elif-else statement. The else part is mandatory, but the list may be empty.
+ * ThenBlocks has to have at least one element.
+ */
+ IfStatement(List<ConditionalStatements> thenBlocks, List<Statement> elseBlock) {
+ Preconditions.checkArgument(thenBlocks.size() > 0);
+ this.thenBlocks = ImmutableList.copyOf(thenBlocks);
+ this.elseBlock = ImmutableList.copyOf(elseBlock);
+ }
+
+ public ImmutableList<ConditionalStatements> getThenBlocks() {
+ return thenBlocks;
+ }
+
+ public ImmutableList<Statement> getElseBlock() {
+ return elseBlock;
+ }
+
+ @Override
+ public String toString() {
+ // TODO(bazel-team): if we want to print the complete statement, the function
+ // needs an extra argument to specify indentation level.
+ return "if : ...\n";
+ }
+
+ @Override
+ void exec(Environment env) throws EvalException, InterruptedException {
+ for (ConditionalStatements stmt : thenBlocks) {
+ if (EvalUtils.toBoolean(stmt.getCondition().eval(env))) {
+ stmt.exec(env);
+ return;
+ }
+ }
+ for (Statement stmt : elseBlock) {
+ stmt.exec(env);
+ }
+ }
+
+ @Override
+ public void accept(SyntaxTreeVisitor visitor) {
+ visitor.visit(this);
+ }
+
+ @Override
+ void validate(ValidationEnvironment env) throws EvalException {
+ env.startTemporarilyDisableReadonlyCheckSession();
+ for (ConditionalStatements stmts : thenBlocks) {
+ stmts.validate(env);
+ }
+ validateStmts(env, elseBlock);
+ env.finishTemporarilyDisableReadonlyCheckSession();
+ }
+
+ private static void validateStmts(ValidationEnvironment env, List<Statement> stmts)
+ throws EvalException {
+ for (Statement stmt : stmts) {
+ stmt.validate(env);
+ }
+ env.finishTemporarilyDisableReadonlyCheckBranch();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/IntegerLiteral.java b/src/main/java/com/google/devtools/build/lib/syntax/IntegerLiteral.java
new file mode 100644
index 0000000..e6852e6b
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/IntegerLiteral.java
@@ -0,0 +1,34 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+/**
+ * Syntax node for an integer literal.
+ */
+public final class IntegerLiteral extends Literal<Integer> {
+
+ public IntegerLiteral(Integer value) {
+ super(value);
+ }
+
+ @Override
+ public void accept(SyntaxTreeVisitor visitor) {
+ visitor.visit(this);
+ }
+
+ @Override
+ SkylarkType validate(ValidationEnvironment env) throws EvalException {
+ return SkylarkType.INT;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/Label.java b/src/main/java/com/google/devtools/build/lib/syntax/Label.java
new file mode 100644
index 0000000..89db4de
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/Label.java
@@ -0,0 +1,412 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ComparisonChain;
+import com.google.devtools.build.lib.cmdline.LabelValidator;
+import com.google.devtools.build.lib.cmdline.LabelValidator.BadLabelException;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.packages.PackageIdentifier;
+import com.google.devtools.build.lib.util.StringCanonicalizer;
+import com.google.devtools.build.lib.util.StringUtilities;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.io.InvalidObjectException;
+import java.io.ObjectInputStream;
+import java.io.Serializable;
+
+/**
+ * A class to identify a BUILD target. All targets belong to exactly one package.
+ * The name of a target is called its label. A typical label looks like this:
+ * //dir1/dir2:target_name where 'dir1/dir2' identifies the package containing a BUILD file,
+ * and 'target_name' identifies the target within the package.
+ *
+ * <p>Parsing is robust against bad input, for example, from the command line.
+ */
+@SkylarkModule(name = "Label", doc = "A BUILD target identifier.")
+@Immutable @ThreadSafe
+public final class Label implements Comparable<Label>, Serializable {
+
+ /**
+ * Thrown by the parsing methods to indicate a bad label.
+ */
+ public static class SyntaxException extends Exception {
+ public SyntaxException(String message) {
+ super(message);
+ }
+ }
+
+ /**
+ * Factory for Labels from absolute string form, possibly including a repository name prefix. For
+ * example:
+ * <pre>
+ * //foo/bar
+ * {@literal @}foo//bar
+ * {@literal @}foo//bar:baz
+ * </pre>
+ */
+ public static Label parseRepositoryLabel(String absName) throws SyntaxException {
+ String repo = PackageIdentifier.DEFAULT_REPOSITORY;
+ int packageStartPos = absName.indexOf("//");
+ if (packageStartPos > 0) {
+ repo = absName.substring(0, packageStartPos);
+ absName = absName.substring(packageStartPos);
+ }
+ try {
+ LabelValidator.PackageAndTarget labelParts = LabelValidator.parseAbsoluteLabel(absName);
+ return new Label(new PackageIdentifier(repo, new PathFragment(labelParts.getPackageName())),
+ labelParts.getTargetName());
+ } catch (BadLabelException e) {
+ throw new SyntaxException(e.getMessage());
+ }
+ }
+
+ /**
+ * Factory for Labels from absolute string form. e.g.
+ * <pre>
+ * //foo/bar
+ * //foo/bar:quux
+ * </pre>
+ */
+ public static Label parseAbsolute(String absName) throws SyntaxException {
+ try {
+ LabelValidator.PackageAndTarget labelParts = LabelValidator.parseAbsoluteLabel(absName);
+ return create(labelParts.getPackageName(), labelParts.getTargetName());
+ } catch (BadLabelException e) {
+ throw new SyntaxException(e.getMessage());
+ }
+ }
+
+ /**
+ * Alternate factory method for Labels from absolute strings. This is a convenience method for
+ * cases when a Label needs to be initialized statically, so the declared exception is
+ * inconvenient.
+ *
+ * <p>Do not use this when the argument is not hard-wired.
+ */
+ public static Label parseAbsoluteUnchecked(String absName) {
+ try {
+ return parseAbsolute(absName);
+ } catch (SyntaxException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ /**
+ * Factory for Labels from separate components.
+ *
+ * @param packageName The name of the package. The package name does
+ * <b>not</b> include {@code //}. Must be valid according to
+ * {@link LabelValidator#validatePackageName}.
+ * @param targetName The name of the target within the package. Must be
+ * valid according to {@link LabelValidator#validateTargetName}.
+ * @throws SyntaxException if either of the arguments was invalid.
+ */
+ public static Label create(String packageName, String targetName) throws SyntaxException {
+ return new Label(packageName, targetName);
+ }
+
+ /**
+ * Similar factory to above, but takes a package identifier to allow external repository labels
+ * to be created.
+ */
+ public static Label create(PackageIdentifier packageId, String targetName)
+ throws SyntaxException {
+ return new Label(packageId, targetName);
+ }
+
+ /**
+ * Resolves a relative label using a workspace-relative path to the current working directory. The
+ * method handles these cases:
+ * <ul>
+ * <li>The label is absolute.
+ * <li>The label starts with a colon.
+ * <li>The label consists of a relative path, a colon, and a local part.
+ * <li>The label consists only of a local part.
+ * </ul>
+ *
+ * <p>Note that this method does not support any of the special syntactic constructs otherwise
+ * supported on the command line, like ":all", "/...", and so on.
+ *
+ * <p>It would be cleaner to use the TargetPatternEvaluator for this resolution, but that is not
+ * possible, because it is sometimes necessary to resolve a relative label before the package path
+ * is setup; in particular, before the tools/defaults package is created.
+ *
+ * @throws SyntaxException if the resulting label is not valid
+ */
+ public static Label parseCommandLineLabel(String label, PathFragment workspaceRelativePath)
+ throws SyntaxException {
+ Preconditions.checkArgument(!workspaceRelativePath.isAbsolute());
+ if (label.startsWith("//")) {
+ return parseAbsolute(label);
+ }
+ int index = label.indexOf(':');
+ if (index < 0) {
+ index = 0;
+ label = ":" + label;
+ }
+ PathFragment path = workspaceRelativePath.getRelative(label.substring(0, index));
+ // Use the String, String constructor, to make sure that the package name goes through the
+ // validity check.
+ return new Label(path.getPathString(), label.substring(index + 1));
+ }
+
+ /**
+ * Validates the given target name and returns a canonical String instance if it is valid.
+ * Otherwise it throws a SyntaxException.
+ */
+ private static String canonicalizeTargetName(String name) throws SyntaxException {
+ String error = LabelValidator.validateTargetName(name);
+ if (error != null) {
+ error = "invalid target name '" + StringUtilities.sanitizeControlChars(name) + "': " + error;
+ throw new SyntaxException(error);
+ }
+
+ // TODO(bazel-team): This should be an error, but we can't make it one for legacy reasons.
+ if (name.endsWith("/.")) {
+ name = name.substring(0, name.length() - 2);
+ }
+
+ return StringCanonicalizer.intern(name);
+ }
+
+ /**
+ * Validates the given package name and returns a canonical PathFragment instance if it is valid.
+ * Otherwise it throws a SyntaxException.
+ */
+ private static PathFragment validate(String packageName, String name) throws SyntaxException {
+ String error = LabelValidator.validatePackageName(packageName);
+ if (error != null) {
+ error = "invalid package name '" + packageName + "': " + error;
+ // This check is just for a more helpful error message
+ // i.e. valid target name, invalid package name, colon-free label form
+ // used => probably they meant "//foo:bar.c" not "//foo/bar.c".
+ if (packageName.endsWith("/" + name)) {
+ error += " (perhaps you meant \":" + name + "\"?)";
+ }
+ throw new SyntaxException(error);
+ }
+ return new PathFragment(packageName);
+ }
+
+ /** The name and repository of the package. */
+ private final PackageIdentifier packageIdentifier;
+
+ /** The name of the target within the package. Canonical. */
+ private final String name;
+
+ /**
+ * Constructor from a package name, target name. Both are checked for validity
+ * and a SyntaxException is thrown if either is invalid.
+ * TODO(bazel-team): move the validation to {@link PackageIdentifier}. Unfortunately, there are a
+ * bazillion tests that use invalid package names (taking advantage of the fact that calling
+ * Label(PathFragment, String) doesn't validate the package name).
+ */
+ private Label(String packageName, String name) throws SyntaxException {
+ this(validate(packageName, name), name);
+ }
+
+ /**
+ * Constructor from canonical valid package name and a target name. The target
+ * name is checked for validity and a SyntaxException is throw if it isn't.
+ */
+ private Label(PathFragment packageName, String name) throws SyntaxException {
+ this(PackageIdentifier.createInDefaultRepo(packageName), name);
+ }
+
+ private Label(PackageIdentifier packageIdentifier, String name)
+ throws SyntaxException {
+ Preconditions.checkNotNull(packageIdentifier);
+ Preconditions.checkNotNull(name);
+
+ try {
+ this.packageIdentifier = packageIdentifier;
+ this.name = canonicalizeTargetName(name);
+ } catch (SyntaxException e) {
+ // This check is just for a more helpful error message
+ // i.e. valid target name, invalid package name, colon-free label form
+ // used => probably they meant "//foo:bar.c" not "//foo/bar.c".
+ if (packageIdentifier.getPackageFragment().getPathString().endsWith("/" + name)) {
+ throw new SyntaxException(e.getMessage() + " (perhaps you meant \":" + name + "\"?)");
+ }
+ throw e;
+ }
+ }
+
+ private Object writeReplace() {
+ return new LabelSerializationProxy(toString());
+ }
+
+ private void readObject(ObjectInputStream stream) throws InvalidObjectException {
+ throw new InvalidObjectException("Serialization is allowed only by proxy");
+ }
+
+ public PackageIdentifier getPackageIdentifier() {
+ return packageIdentifier;
+ }
+
+ /**
+ * Returns the name of the package in which this rule was declared (e.g. {@code
+ * //file/base:fileutils_test} returns {@code file/base}).
+ */
+ @SkylarkCallable(name = "package", structField = true,
+ doc = "The package part of this label. "
+ + "For instance:<br>"
+ + "<pre class=language-python>label(\"//pkg/foo:abc\").package == \"pkg/foo\"</pre>")
+ public String getPackageName() {
+ return packageIdentifier.getPackageFragment().getPathString();
+ }
+
+ /**
+ * Returns the path fragment of the package in which this rule was declared (e.g. {@code
+ * //file/base:fileutils_test} returns {@code file/base}).
+ */
+ public PathFragment getPackageFragment() {
+ return packageIdentifier.getPackageFragment();
+ }
+
+ public static final com.google.common.base.Function<Label, PathFragment> PACKAGE_FRAGMENT =
+ new com.google.common.base.Function<Label, PathFragment>() {
+ @Override
+ public PathFragment apply(Label label) {
+ return label.getPackageFragment();
+ }
+ };
+
+ /**
+ * Returns the label as a path fragment, using the package and the label name.
+ */
+ public PathFragment toPathFragment() {
+ return packageIdentifier.getPackageFragment().getRelative(name);
+ }
+
+ /**
+ * Returns the name by which this rule was declared (e.g. {@code //foo/bar:baz}
+ * returns {@code baz}).
+ */
+ @SkylarkCallable(name = "name", structField = true,
+ doc = "The name of this label within the package. "
+ + "For instance:<br>"
+ + "<pre class=language-python>label(\"//pkg/foo:abc\").name == \"abc\"</pre>")
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Renders this label in canonical form.
+ *
+ * <p>invariant: {@code parseAbsolute(x.toString()).equals(x)}
+ */
+ @Override
+ public String toString() {
+ return packageIdentifier.getRepository() + "//" + packageIdentifier.getPackageFragment()
+ + ":" + name;
+ }
+
+ /**
+ * Renders this label in shorthand form.
+ *
+ * <p>Labels with canonical form {@code //foo/bar:bar} have the shorthand form {@code //foo/bar}.
+ * All other labels have identical shorthand and canonical forms.
+ */
+ public String toShorthandString() {
+ return packageIdentifier.getRepository() + (getPackageFragment().getBaseName().equals(name)
+ ? "//" + getPackageFragment()
+ : toString());
+ }
+
+ /**
+ * Returns a label in the same package as this label with the given target name.
+ *
+ * @throws SyntaxException if {@code targetName} is not a valid target name
+ */
+ public Label getLocalTargetLabel(String targetName) throws SyntaxException {
+ return new Label(packageIdentifier, targetName);
+ }
+
+ /**
+ * Resolves a relative or absolute label name. If given name is absolute, then this method calls
+ * {@link #parseAbsolute}. Otherwise, it calls {@link #getLocalTargetLabel}.
+ *
+ * <p>For example:
+ * {@code :quux} relative to {@code //foo/bar:baz} is {@code //foo/bar:quux};
+ * {@code //wiz:quux} relative to {@code //foo/bar:baz} is {@code //wiz:quux}.
+ *
+ * @param relName the relative label name; must be non-empty.
+ */
+ @SkylarkCallable(name = "relative", doc =
+ "Resolves a relative or absolute label name.<br>"
+ + "For example:<br><ul>"
+ + "<li><code>:quux</code> relative to <code>//foo/bar:baz</code> is "
+ + "<code>//foo/bar:quux</code></li>"
+ + "<li><code>//wiz:quux</code> relative to <code>//foo/bar:baz</code> is "
+ + "<code>//wiz:quux</code></li></ul>")
+ public Label getRelative(String relName) throws SyntaxException {
+ if (relName.length() == 0) {
+ throw new SyntaxException("empty package-relative label");
+ }
+ if (relName.startsWith("//")) {
+ return parseAbsolute(relName);
+ } else if (relName.equals(":")) {
+ throw new SyntaxException("':' is not a valid package-relative label");
+ } else if (relName.charAt(0) == ':') {
+ return getLocalTargetLabel(relName.substring(1));
+ } else {
+ return getLocalTargetLabel(relName);
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return name.hashCode() ^ packageIdentifier.hashCode();
+ }
+
+ /**
+ * Two labels are equal iff both their name and their package name are equal.
+ */
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof Label)) {
+ return false;
+ }
+ Label otherLabel = (Label) other;
+ return name.equals(otherLabel.name) // least likely one first
+ && packageIdentifier.equals(otherLabel.packageIdentifier);
+ }
+
+ /**
+ * Defines the order between labels.
+ *
+ * <p>Labels are ordered primarily by package name and secondarily by target name. Both components
+ * are ordered lexicographically. Thus {@code //a:b/c} comes before {@code //a/b:a}, i.e. the
+ * position of the colon is significant to the order.
+ */
+ @Override
+ public int compareTo(Label other) {
+ return ComparisonChain.start()
+ .compare(packageIdentifier, other.packageIdentifier)
+ .compare(name, other.name)
+ .result();
+ }
+
+ /**
+ * Returns a suitable string for the user-friendly representation of the Label. Works even if the
+ * argument is null.
+ */
+ public static String print(Label label) {
+ return label == null ? "(unknown)" : label.toString();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/LabelSerializationProxy.java b/src/main/java/com/google/devtools/build/lib/syntax/LabelSerializationProxy.java
new file mode 100644
index 0000000..5b4556a
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/LabelSerializationProxy.java
@@ -0,0 +1,49 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+import java.io.Externalizable;
+import java.io.IOException;
+import java.io.ObjectOutput;
+
+/**
+ * A serialization proxy for {@code Label}.
+ */
+public class LabelSerializationProxy implements Externalizable {
+
+ private String labelString;
+
+ public LabelSerializationProxy(String labelString) {
+ this.labelString = labelString;
+ }
+
+ // For deserialization machinery.
+ public LabelSerializationProxy() {
+ }
+
+ @Override
+ public void writeExternal(ObjectOutput out) throws IOException {
+ // Manual serialization gives us about a 30% reduction in size.
+ out.writeUTF(labelString);
+ }
+
+ @Override
+ public void readExternal(java.io.ObjectInput in) throws IOException {
+ this.labelString = in.readUTF();
+ }
+
+ private Object readResolve() {
+ return Label.parseAbsoluteUnchecked(labelString);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/Lexer.java b/src/main/java/com/google/devtools/build/lib/syntax/Lexer.java
new file mode 100644
index 0000000..fc12c66
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/Lexer.java
@@ -0,0 +1,803 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.util.Pair;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Stack;
+
+/**
+ * A tokenizer for the BUILD language.
+ * <p>
+ * See: <a href="https://docs.python.org/2/reference/lexical_analysis.html"/>
+ * for some details.
+ * <p>
+ * Since BUILD files are small, we just tokenize the entire file a-priori
+ * instead of interleaving scanning with parsing.
+ */
+public final class Lexer {
+
+ private final EventHandler eventHandler;
+
+ // Input buffer and position
+ private char[] buffer;
+ private int pos;
+
+ /**
+ * The part of the location information that is common to all LexerLocation
+ * instances created by this Lexer. Factored into a separate object so that
+ * many Locations instances can share the same information as compactly as
+ * possible, without closing over a Lexer instance.
+ */
+ private static class LocationInfo {
+ final LineNumberTable lineNumberTable;
+ final Path filename;
+ LocationInfo(Path filename, LineNumberTable lineNumberTable) {
+ this.filename = filename;
+ this.lineNumberTable = lineNumberTable;
+ }
+ }
+
+ private final LocationInfo locationInfo;
+
+ // The stack of enclosing indentation levels; always contains '0' at the
+ // bottom.
+ private final Stack<Integer> indentStack = new Stack<>();
+
+ private final List<Token> tokens = new ArrayList<>();
+
+ // The number of unclosed open-parens ("(", '{', '[') at the current point in
+ // the stream. Whitespace is handled differently when this is nonzero.
+ private int openParenStackDepth = 0;
+
+ private boolean containsErrors;
+
+ private boolean parsePython;
+
+ /**
+ * Constructs a lexer which tokenizes the contents of the specified
+ * InputBuffer. Any errors during lexing are reported on "handler".
+ */
+ public Lexer(ParserInputSource input, EventHandler eventHandler, boolean parsePython) {
+ this.buffer = input.getContent();
+ this.pos = 0;
+ this.parsePython = parsePython;
+ this.eventHandler = eventHandler;
+ this.locationInfo = new LocationInfo(input.getPath(),
+ LineNumberTable.create(buffer, input.getPath()));
+
+ indentStack.push(0);
+ tokenize();
+ }
+
+ public Lexer(ParserInputSource input, EventHandler eventHandler) {
+ this(input, eventHandler, false);
+ }
+
+ /**
+ * Returns the filename from which the lexer's input came. Returns a dummy
+ * value if the input came from a string.
+ */
+ public Path getFilename() {
+ return locationInfo.filename;
+ }
+
+ /**
+ * Returns true if there were errors during scanning of this input file or
+ * string. The Lexer may attempt to recover from errors, but clients should
+ * not rely on the results of scanning if this flag is set.
+ */
+ public boolean containsErrors() {
+ return containsErrors;
+ }
+
+ /**
+ * Returns the (mutable) list of tokens generated by the Lexer.
+ */
+ public List<Token> getTokens() {
+ return tokens;
+ }
+
+ private void popParen() {
+ if (openParenStackDepth == 0) {
+ error("indentation error");
+ } else {
+ openParenStackDepth--;
+ }
+ }
+
+ private void error(String message) {
+ error(message, pos - 1, pos - 1);
+ }
+
+ private void error(String message, int start, int end) {
+ this.containsErrors = true;
+ eventHandler.handle(Event.error(createLocation(start, end), message));
+ }
+
+ Location createLocation(int start, int end) {
+ return new LexerLocation(locationInfo, start, end);
+ }
+
+ // Don't use an inner class as we don't want to close over the Lexer, only
+ // the LocationInfo.
+ @Immutable
+ private static final class LexerLocation extends Location {
+
+ private final LineNumberTable lineNumberTable;
+
+ LexerLocation(LocationInfo locationInfo, int start, int end) {
+ super(start, end);
+ this.lineNumberTable = locationInfo.lineNumberTable;
+ }
+
+ @Override
+ public PathFragment getPath() {
+ return lineNumberTable.getPath(getStartOffset()).asFragment();
+ }
+
+ @Override
+ public LineAndColumn getStartLineAndColumn() {
+ return lineNumberTable.getLineAndColumn(getStartOffset());
+ }
+
+ @Override
+ public LineAndColumn getEndLineAndColumn() {
+ return lineNumberTable.getLineAndColumn(getEndOffset());
+ }
+ }
+
+ /** invariant: symbol positions are half-open intervals. */
+ private void addToken(Token s) {
+ tokens.add(s);
+ }
+
+ /**
+ * Parses an end-of-line sequence, handling statement indentation correctly.
+ *
+ * UNIX newlines are assumed (LF). Carriage returns are always ignored.
+ *
+ * ON ENTRY: 'pos' is the index of the char after '\n'.
+ * ON EXIT: 'pos' is the index of the next non-space char after '\n'.
+ */
+ private void newline() {
+ if (openParenStackDepth > 0) {
+ newlineInsideExpression(); // in an expression: ignore space
+ } else {
+ newlineOutsideExpression(); // generate NEWLINE/INDENT/OUTDENT tokens
+ }
+ }
+
+ private void newlineInsideExpression() {
+ while (pos < buffer.length) {
+ switch (buffer[pos]) {
+ case ' ': case '\t': case '\r':
+ pos++;
+ break;
+ default:
+ return;
+ }
+ }
+ }
+
+ private void newlineOutsideExpression() {
+ if (pos > 1) { // skip over newline at start of file
+ addToken(new Token(TokenKind.NEWLINE, pos - 1, pos));
+ }
+
+ // we're in a stmt: suck up space at beginning of next line
+ int indentLen = 0;
+ while (pos < buffer.length) {
+ char c = buffer[pos];
+ if (c == ' ') {
+ indentLen++;
+ pos++;
+ } else if (c == '\t') {
+ indentLen += 8 - indentLen % 8;
+ pos++;
+ } else if (c == '\n') { // entirely blank line: discard
+ indentLen = 0;
+ pos++;
+ } else if (c == '#') { // line containing only indented comment
+ int oldPos = pos;
+ while (pos < buffer.length && c != '\n') {
+ c = buffer[pos++];
+ }
+ addToken(new Token(TokenKind.COMMENT, oldPos, pos - 1, bufferSlice(oldPos, pos - 1)));
+ indentLen = 0;
+ } else { // printing character
+ break;
+ }
+ }
+
+ if (pos == buffer.length) {
+ indentLen = 0;
+ } // trailing space on last line
+
+ int peekedIndent = indentStack.peek();
+ if (peekedIndent < indentLen) { // push a level
+ indentStack.push(indentLen);
+ addToken(new Token(TokenKind.INDENT, pos - 1, pos));
+
+ } else if (peekedIndent > indentLen) { // pop one or more levels
+ while (peekedIndent > indentLen) {
+ indentStack.pop();
+ addToken(new Token(TokenKind.OUTDENT, pos - 1, pos));
+ peekedIndent = indentStack.peek();
+ }
+
+ if (peekedIndent < indentLen) {
+ error("indentation error");
+ }
+ }
+ }
+
+ /**
+ * Returns true if current position is in the middle of a triple quote
+ * delimiter (3 x quot), and advances 'pos' by two if so.
+ */
+ private boolean skipTripleQuote(char quot) {
+ if (pos + 1 < buffer.length && buffer[pos] == quot && buffer[pos + 1] == quot) {
+ pos += 2;
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Scans a string literal delimited by 'quot', containing escape sequences.
+ *
+ * ON ENTRY: 'pos' is 1 + the index of the first delimiter
+ * ON EXIT: 'pos' is 1 + the index of the last delimiter.
+ *
+ * @return the string-literal token.
+ */
+ private Token escapedStringLiteral(char quot) {
+ boolean inTriplequote = skipTripleQuote(quot);
+
+ int oldPos = pos - 1;
+ // more expensive second choice that expands escaped into a buffer
+ StringBuilder literal = new StringBuilder();
+ while (pos < buffer.length) {
+ char c = buffer[pos];
+ pos++;
+ switch (c) {
+ case '\n':
+ if (inTriplequote) {
+ literal.append(c);
+ break;
+ } else {
+ error("unterminated string literal at eol", oldPos, pos);
+ newline();
+ return new Token(TokenKind.STRING, oldPos, pos, literal.toString());
+ }
+ case '\\':
+ if (pos == buffer.length) {
+ error("unterminated string literal at eof", oldPos, pos);
+ return new Token(TokenKind.STRING, oldPos, pos, literal.toString());
+ }
+ c = buffer[pos];
+ pos++;
+ switch (c) {
+ case '\n':
+ // ignore end of line character
+ break;
+ case 'n':
+ literal.append('\n');
+ break;
+ case 'r':
+ literal.append('\r');
+ break;
+ case 't':
+ literal.append('\t');
+ break;
+ case '\\':
+ literal.append('\\');
+ break;
+ case '\'':
+ literal.append('\'');
+ break;
+ case '"':
+ literal.append('"');
+ break;
+ case '0': case '1': case '2': case '3':
+ case '4': case '5': case '6': case '7': { // octal escape
+ int octal = c - '0';
+ if (pos < buffer.length) {
+ c = buffer[pos];
+ if (c >= '0' && c <= '7') {
+ pos++;
+ octal = (octal << 3) | (c - '0');
+ if (pos < buffer.length) {
+ c = buffer[pos];
+ if (c >= '0' && c <= '7') {
+ pos++;
+ octal = (octal << 3) | (c - '0');
+ }
+ }
+ }
+ }
+ literal.append((char) (octal & 0xff));
+ break;
+ }
+ case 'a': case 'b': case 'f': case 'N': case 'u': case 'U': case 'v': case 'x':
+ // exists in Python but not implemented in Blaze => error
+ error("escape sequence not implemented: \\" + c, oldPos, pos);
+ break;
+ default:
+ // unknown char escape => "\literal"
+ literal.append('\\');
+ literal.append(c);
+ break;
+ }
+ break;
+ case '\'':
+ case '"':
+ if (c != quot
+ || (inTriplequote && !skipTripleQuote(quot))) {
+ // Non-matching quote, treat it like a regular char.
+ literal.append(c);
+ } else {
+ // Matching close-delimiter, all done.
+ return new Token(TokenKind.STRING, oldPos, pos, literal.toString());
+ }
+ break;
+ default:
+ literal.append(c);
+ break;
+ }
+ }
+ error("unterminated string literal at eof", oldPos, pos);
+ return new Token(TokenKind.STRING, oldPos, pos, literal.toString());
+ }
+
+ /**
+ * Scans a string literal delimited by 'quot'.
+ *
+ * <ul>
+ * <li> ON ENTRY: 'pos' is 1 + the index of the first delimiter
+ * <li> ON EXIT: 'pos' is 1 + the index of the last delimiter.
+ * </ul>
+ *
+ * @param isRaw if true, do not escape the string.
+ * @return the string-literal token.
+ */
+ private Token stringLiteral(char quot, boolean isRaw) {
+ int oldPos = pos - 1;
+
+ // Don't even attempt to parse triple-quotes here.
+ if (skipTripleQuote(quot)) {
+ pos -= 2;
+ return escapedStringLiteral(quot);
+ }
+
+ // first quick optimistic scan for a simple non-escaped string
+ while (pos < buffer.length) {
+ char c = buffer[pos++];
+ switch (c) {
+ case '\n':
+ error("unterminated string literal at eol", oldPos, pos);
+ Token t = new Token(TokenKind.STRING, oldPos, pos,
+ bufferSlice(oldPos + 1, pos - 1));
+ newline();
+ return t;
+ case '\\':
+ if (isRaw) {
+ // skip the next character
+ pos++;
+ break;
+ } else {
+ // oops, hit an escape, need to start over & build a new string buffer
+ pos = oldPos + 1;
+ return escapedStringLiteral(quot);
+ }
+ case '\'':
+ case '"':
+ if (c == quot) {
+ // close-quote, all done.
+ return new Token(TokenKind.STRING, oldPos, pos,
+ bufferSlice(oldPos + 1, pos - 1));
+ }
+ }
+ }
+
+ error("unterminated string literal at eof", oldPos, pos);
+ return new Token(TokenKind.STRING, oldPos, pos,
+ bufferSlice(oldPos + 1, pos));
+ }
+
+ private static final Map<String, TokenKind> keywordMap = new HashMap<>();
+
+ static {
+ keywordMap.put("and", TokenKind.AND);
+ keywordMap.put("as", TokenKind.AS);
+ keywordMap.put("class", TokenKind.CLASS); // reserved for future expansion
+ keywordMap.put("def", TokenKind.DEF);
+ keywordMap.put("elif", TokenKind.ELIF);
+ keywordMap.put("else", TokenKind.ELSE);
+ keywordMap.put("except", TokenKind.EXCEPT);
+ keywordMap.put("finally", TokenKind.FINALLY);
+ keywordMap.put("for", TokenKind.FOR);
+ keywordMap.put("from", TokenKind.FROM);
+ keywordMap.put("if", TokenKind.IF);
+ keywordMap.put("import", TokenKind.IMPORT);
+ keywordMap.put("in", TokenKind.IN);
+ keywordMap.put("not", TokenKind.NOT);
+ keywordMap.put("or", TokenKind.OR);
+ keywordMap.put("return", TokenKind.RETURN);
+ keywordMap.put("try", TokenKind.TRY);
+ }
+
+ private TokenKind getTokenKindForIdentfier(String id) {
+ TokenKind kind = keywordMap.get(id);
+ return kind == null ? TokenKind.IDENTIFIER : kind;
+ }
+
+ private String scanIdentifier() {
+ int oldPos = pos - 1;
+ while (pos < buffer.length) {
+ switch (buffer[pos]) {
+ case '_':
+ case 'a': case 'b': case 'c': case 'd': case 'e': case 'f':
+ case 'g': case 'h': case 'i': case 'j': case 'k': case 'l':
+ case 'm': case 'n': case 'o': case 'p': case 'q': case 'r':
+ case 's': case 't': case 'u': case 'v': case 'w': case 'x':
+ case 'y': case 'z':
+ case 'A': case 'B': case 'C': case 'D': case 'E': case 'F':
+ case 'G': case 'H': case 'I': case 'J': case 'K': case 'L':
+ case 'M': case 'N': case 'O': case 'P': case 'Q': case 'R':
+ case 'S': case 'T': case 'U': case 'V': case 'W': case 'X':
+ case 'Y': case 'Z':
+ case '0': case '1': case '2': case '3': case '4': case '5':
+ case '6': case '7': case '8': case '9':
+ pos++;
+ break;
+ default:
+ return bufferSlice(oldPos, pos);
+ }
+ }
+ return bufferSlice(oldPos, pos);
+ }
+
+ /**
+ * Scans an identifier or keyword.
+ *
+ * ON ENTRY: 'pos' is 1 + the index of the first char in the identifier.
+ * ON EXIT: 'pos' is 1 + the index of the last char in the identifier.
+ *
+ * @return the identifier or keyword token.
+ */
+ private Token identifierOrKeyword() {
+ int oldPos = pos - 1;
+ String id = scanIdentifier();
+ TokenKind kind = getTokenKindForIdentfier(id);
+ return new Token(kind, oldPos, pos,
+ (kind == TokenKind.IDENTIFIER) ? id : null);
+ }
+
+ private String scanInteger() {
+ int oldPos = pos - 1;
+ while (pos < buffer.length) {
+ char c = buffer[pos];
+ switch (c) {
+ case 'X': case 'x':
+ case 'a': case 'A':
+ case 'b': case 'B':
+ case 'c': case 'C':
+ case 'd': case 'D':
+ case 'e': case 'E':
+ case 'f': case 'F':
+ case '0': case '1':
+ case '2': case '3':
+ case '4': case '5':
+ case '6': case '7':
+ case '8': case '9':
+ pos++;
+ break;
+ default:
+ return bufferSlice(oldPos, pos);
+ }
+ }
+ // TODO(bazel-team): (2009) to do roundtripping when we evaluate the integer
+ // constants, we must save the actual text of the tokens, not just their
+ // integer value.
+
+ return bufferSlice(oldPos, pos);
+ }
+
+ /**
+ * Scans an integer literal.
+ *
+ * ON ENTRY: 'pos' is 1 + the index of the first char in the literal.
+ * ON EXIT: 'pos' is 1 + the index of the last char in the literal.
+ *
+ * @return the integer token.
+ */
+ private Token integer() {
+ int oldPos = pos - 1;
+ String literal = scanInteger();
+
+ final String substring;
+ final int radix;
+ if (literal.startsWith("0x") || literal.startsWith("0X")) {
+ radix = 16;
+ substring = literal.substring(2);
+ } else if (literal.startsWith("0") && literal.length() > 1) {
+ radix = 8;
+ substring = literal.substring(1);
+ } else {
+ radix = 10;
+ substring = literal;
+ }
+
+ int value = 0;
+ try {
+ value = Integer.parseInt(substring, radix);
+ } catch (NumberFormatException e) {
+ error("invalid base-" + radix + " integer constant: " + literal);
+ }
+
+ return new Token(TokenKind.INT, oldPos, pos, value);
+ }
+
+ /**
+ * Tokenizes a two-char operator.
+ * @return true if it tokenized an operator
+ */
+ private boolean tokenizeTwoChars() {
+ if (pos + 2 >= buffer.length) {
+ return false;
+ }
+ char c1 = buffer[pos];
+ char c2 = buffer[pos + 1];
+ if (c2 == '=') {
+ switch (c1) {
+ case '=': {
+ addToken(new Token(TokenKind.EQUALS_EQUALS, pos, pos + 2));
+ return true;
+ }
+ case '!': {
+ addToken(new Token(TokenKind.NOT_EQUALS, pos, pos + 2));
+ return true;
+ }
+ case '>': {
+ addToken(new Token(TokenKind.GREATER_EQUALS, pos, pos + 2));
+ return true;
+ }
+ case '<': {
+ addToken(new Token(TokenKind.LESS_EQUALS, pos, pos + 2));
+ return true;
+ }
+ case '+': {
+ addToken(new Token(TokenKind.PLUS_EQUALS, pos, pos + 2));
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Performs tokenization of the character buffer of file contents provided to
+ * the constructor.
+ */
+ private void tokenize() {
+ while (pos < buffer.length) {
+ if (tokenizeTwoChars()) {
+ pos += 2;
+ continue;
+ }
+ char c = buffer[pos];
+ pos++;
+ switch (c) {
+ case '{': {
+ addToken(new Token(TokenKind.LBRACE, pos - 1, pos));
+ openParenStackDepth++;
+ break;
+ }
+ case '}': {
+ addToken(new Token(TokenKind.RBRACE, pos - 1, pos));
+ popParen();
+ break;
+ }
+ case '(': {
+ addToken(new Token(TokenKind.LPAREN, pos - 1, pos));
+ openParenStackDepth++;
+ break;
+ }
+ case ')': {
+ addToken(new Token(TokenKind.RPAREN, pos - 1, pos));
+ popParen();
+ break;
+ }
+ case '[': {
+ addToken(new Token(TokenKind.LBRACKET, pos - 1, pos));
+ openParenStackDepth++;
+ break;
+ }
+ case ']': {
+ addToken(new Token(TokenKind.RBRACKET, pos - 1, pos));
+ popParen();
+ break;
+ }
+ case '>': {
+ addToken(new Token(TokenKind.GREATER, pos - 1, pos));
+ break;
+ }
+ case '<': {
+ addToken(new Token(TokenKind.LESS, pos - 1, pos));
+ break;
+ }
+ case ':': {
+ addToken(new Token(TokenKind.COLON, pos - 1, pos));
+ break;
+ }
+ case ',': {
+ addToken(new Token(TokenKind.COMMA, pos - 1, pos));
+ break;
+ }
+ case '+': {
+ addToken(new Token(TokenKind.PLUS, pos - 1, pos));
+ break;
+ }
+ case '-': {
+ addToken(new Token(TokenKind.MINUS, pos - 1, pos));
+ break;
+ }
+ case '=': {
+ addToken(new Token(TokenKind.EQUALS, pos - 1, pos));
+ break;
+ }
+ case '%': {
+ addToken(new Token(TokenKind.PERCENT, pos - 1, pos));
+ break;
+ }
+ case ';': {
+ addToken(new Token(TokenKind.SEMI, pos - 1, pos));
+ break;
+ }
+ case '.': {
+ addToken(new Token(TokenKind.DOT, pos - 1, pos));
+ break;
+ }
+ case '*': {
+ addToken(new Token(TokenKind.STAR, pos - 1, pos));
+ break;
+ }
+ case ' ':
+ case '\t':
+ case '\r': {
+ /* ignore */
+ break;
+ }
+ case '\\': {
+ // Backslash character is valid only at the end of a line (or in a string)
+ if (pos + 1 < buffer.length && buffer[pos] == '\n') {
+ pos++; // skip the end of line character
+ } else {
+ addToken(new Token(TokenKind.ILLEGAL, pos - 1, pos, Character.toString(c)));
+ }
+ break;
+ }
+ case '\n': {
+ newline();
+ break;
+ }
+ case '#': {
+ int oldPos = pos - 1;
+ while (pos < buffer.length) {
+ c = buffer[pos];
+ if (c == '\n') {
+ break;
+ } else {
+ pos++;
+ }
+ }
+ addToken(new Token(TokenKind.COMMENT, oldPos, pos, bufferSlice(oldPos, pos)));
+ break;
+ }
+ case '\'':
+ case '\"': {
+ addToken(stringLiteral(c, false));
+ break;
+ }
+ default: {
+ // detect raw strings, e.g. r"str"
+ if (c == 'r' && pos < buffer.length
+ && (buffer[pos] == '\'' || buffer[pos] == '\"')) {
+ c = buffer[pos];
+ pos++;
+ addToken(stringLiteral(c, true));
+ break;
+ }
+
+ if (Character.isDigit(c)) {
+ addToken(integer());
+ } else if (Character.isJavaIdentifierStart(c) && c != '$') {
+ addToken(identifierOrKeyword());
+ } else {
+ // Some characters in Python are not recognized in Blaze syntax (e.g. '!')
+ if (parsePython) {
+ addToken(new Token(TokenKind.ILLEGAL, pos - 1, pos, Character.toString(c)));
+ } else {
+ error("invalid character: '" + c + "'");
+ }
+ }
+ break;
+ } // default
+ } // switch
+ } // while
+
+ if (indentStack.size() > 1) { // top of stack is always zero
+ addToken(new Token(TokenKind.NEWLINE, pos - 1, pos));
+ while (indentStack.size() > 1) {
+ indentStack.pop();
+ addToken(new Token(TokenKind.OUTDENT, pos - 1, pos));
+ }
+ }
+
+ // Like Python, always end with a NEWLINE token, even if no '\n' in input:
+ if (tokens.size() == 0
+ || tokens.get(tokens.size() - 1).kind != TokenKind.NEWLINE) {
+ addToken(new Token(TokenKind.NEWLINE, pos - 1, pos));
+ }
+
+ addToken(new Token(TokenKind.EOF, pos, pos));
+ }
+
+ /**
+ * Returns the character in the input buffer at the given position.
+ *
+ * @param at the position to get the character at.
+ * @return the character at the given position.
+ */
+ public char charAt(int at) {
+ return buffer[at];
+ }
+
+ /**
+ * Returns the string at the current line, minus the new line.
+ *
+ * @param line the line from which to retrieve the String, 1-based
+ * @return the text of the line
+ */
+ public String stringAtLine(int line) {
+ Pair<Integer, Integer> offsets = locationInfo.lineNumberTable.getOffsetsForLine(line);
+ return bufferSlice(offsets.first, offsets.second);
+ }
+
+ /**
+ * Returns parts of the source buffer based on offsets
+ *
+ * @param start the beginning offset for the slice
+ * @param end the offset immediately following the slice
+ * @return the text at offset start with length end - start
+ */
+ private String bufferSlice(int start, int end) {
+ return new String(this.buffer, start, end - start);
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/LineNumberTable.java b/src/main/java/com/google/devtools/build/lib/syntax/LineNumberTable.java
new file mode 100644
index 0000000..4842a16
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/LineNumberTable.java
@@ -0,0 +1,235 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.events.Location.LineAndColumn;
+import com.google.devtools.build.lib.util.Pair;
+import com.google.devtools.build.lib.util.StringUtilities;
+import com.google.devtools.build.lib.vfs.Path;
+
+import java.io.Serializable;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A table to keep track of line numbers in source files. The client creates a LineNumberTable for
+ * their buffer using {@link #create}. The client can then ask for the line and column given a
+ * position using ({@link #getLineAndColumn(int)}).
+ */
+abstract class LineNumberTable implements Serializable {
+
+ /**
+ * Returns the (line, column) pair for the specified offset.
+ */
+ abstract LineAndColumn getLineAndColumn(int offset);
+
+ /**
+ * Returns the (start, end) offset pair for a specified line, not including
+ * newline chars.
+ */
+ abstract Pair<Integer, Integer> getOffsetsForLine(int line);
+
+ /**
+ * Returns the path corresponding to the given offset.
+ */
+ abstract Path getPath(int offset);
+
+ static LineNumberTable create(char[] buffer, Path path) {
+ // If #line appears within a BUILD file, we assume it has been preprocessed
+ // by gconfig2blaze. We ignore all actual newlines and compute the logical
+ // LNT based only on the presence of #line markers.
+ return StringUtilities.containsSubarray(buffer, "\n#line ".toCharArray())
+ ? new HashLine(buffer, path)
+ : new Regular(buffer, path);
+ }
+
+ /**
+ * Line number table implementation for regular source files. Records
+ * offsets of newlines.
+ */
+ @Immutable
+ private static class Regular extends LineNumberTable {
+
+ /**
+ * A mapping from line number (line >= 1) to character offset into the file.
+ */
+ private final int[] linestart;
+ private final Path path;
+ private final int bufferLength;
+
+ private Regular(char[] buffer, Path path) {
+ // Compute the size.
+ int size = 2;
+ for (int i = 0; i < buffer.length; i++) {
+ if (buffer[i] == '\n') {
+ size++;
+ }
+ }
+ linestart = new int[size];
+
+ int index = 0;
+ linestart[index++] = 0; // The 0th line does not exist - so we fill something in
+ // to make sure the start pos for the 1st line ends up at
+ // linestart[1]. Using 0 is useful for tables that are
+ // completely empty.
+ linestart[index++] = 0; // The first line ("line 1") starts at offset 0.
+
+ // Scan the buffer and record the offset of each line start. Doing this
+ // once upfront is faster than checking each char as it is pulled from
+ // the buffer.
+ for (int i = 0; i < buffer.length; i++) {
+ if (buffer[i] == '\n') {
+ linestart[index++] = i + 1;
+ }
+ }
+ this.bufferLength = buffer.length;
+ this.path = path;
+ }
+
+ private int getLineAt(int pos) {
+ if (pos < 0) {
+ throw new IllegalArgumentException("Illegal position: " + pos);
+ }
+ int lowBoundary = 1, highBoundary = linestart.length - 1;
+ while (true) {
+ if ((highBoundary - lowBoundary) <= 1) {
+ if (linestart[highBoundary] > pos) {
+ return lowBoundary;
+ } else {
+ return highBoundary;
+ }
+ }
+ int medium = lowBoundary + ((highBoundary - lowBoundary) >> 1);
+ if (linestart[medium] > pos) {
+ highBoundary = medium;
+ } else {
+ lowBoundary = medium;
+ }
+ }
+ }
+
+ @Override
+ LineAndColumn getLineAndColumn(int offset) {
+ int line = getLineAt(offset);
+ int column = offset - linestart[line] + 1;
+ return new LineAndColumn(line, column);
+ }
+
+ @Override
+ Path getPath(int offset) {
+ return path;
+ }
+
+ @Override
+ Pair<Integer, Integer> getOffsetsForLine(int line) {
+ if (line <= 0 || line >= linestart.length) {
+ throw new IllegalArgumentException("Illegal line: " + line);
+ }
+ return Pair.of(linestart[line], line < linestart.length - 1
+ ? linestart[line + 1]
+ : bufferLength);
+ }
+ }
+
+ /**
+ * Line number table implementation for source files that have been
+ * preprocessed. Ignores newlines and uses only #line directives.
+ */
+ // TODO(bazel-team): Use binary search instead of linear search.
+ @Immutable
+ private static class HashLine extends LineNumberTable {
+
+ /**
+ * Represents a "#line" directive
+ */
+ private static class SingleHashLine implements Serializable {
+ final private int offset;
+ final private int line;
+ final private Path path;
+
+ SingleHashLine(int offset, int line, Path path) {
+ this.offset = offset;
+ this.line = line;
+ this.path = path;
+ }
+ }
+
+ private static final Pattern pattern = Pattern.compile("\n#line ([0-9]+) \"([^\"\\n]+)\"");
+
+ private final List<SingleHashLine> table;
+ private final Path defaultPath;
+ private final int bufferLength;
+
+ private HashLine(char[] buffer, Path defaultPath) {
+ // Not especially efficient, but that's fine: we just exec'd Python.
+ String bufString = new String(buffer);
+ Matcher m = pattern.matcher(bufString);
+ ImmutableList.Builder<SingleHashLine> tableBuilder = ImmutableList.builder();
+ while (m.find()) {
+ tableBuilder.add(new SingleHashLine(
+ m.start(0) + 1, //offset (+1 to skip \n in pattern)
+ Integer.valueOf(m.group(1)), // line number
+ defaultPath.getRelative(m.group(2)))); // filename is an absolute path
+ }
+ this.table = tableBuilder.build();
+ this.bufferLength = buffer.length;
+ this.defaultPath = defaultPath;
+ }
+
+ @Override
+ LineAndColumn getLineAndColumn(int offset) {
+ int line = -1;
+ for (int ii = 0, len = table.size(); ii < len; ii++) {
+ SingleHashLine hash = table.get(ii);
+ if (hash.offset > offset) {
+ break;
+ }
+ line = hash.line;
+ }
+ return new LineAndColumn(line, 1);
+ }
+
+ @Override
+ Path getPath(int offset) {
+ Path path = this.defaultPath;
+ for (int ii = 0, len = table.size(); ii < len; ii++) {
+ SingleHashLine hash = table.get(ii);
+ if (hash.offset > offset) {
+ break;
+ }
+ path = hash.path;
+ }
+ return path;
+ }
+
+ /**
+ * Returns 0, 0 for an unknown line
+ */
+ @Override
+ Pair<Integer, Integer> getOffsetsForLine(int line) {
+ for (int ii = 0, len = table.size(); ii < len; ii++) {
+ if (table.get(ii).line == line) {
+ return Pair.of(table.get(ii).offset, ii < len - 1
+ ? table.get(ii + 1).offset
+ : bufferLength);
+ }
+ }
+ return Pair.of(0, 0);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/ListComprehension.java b/src/main/java/com/google/devtools/build/lib/syntax/ListComprehension.java
new file mode 100644
index 0000000..6a13ba8
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/ListComprehension.java
@@ -0,0 +1,133 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Syntax node for lists comprehension expressions.
+ */
+public final class ListComprehension extends Expression {
+
+ private final Expression elementExpression;
+ // This cannot be a map, because we need to both preserve order _and_ allow duplicate identifiers.
+ private final List<Map.Entry<Ident, Expression>> lists;
+
+ /**
+ * [elementExpr (for var in listExpr)+]
+ */
+ public ListComprehension(Expression elementExpression) {
+ this.elementExpression = elementExpression;
+ lists = new ArrayList<Map.Entry<Ident, Expression>>();
+ }
+
+ @Override
+ Object eval(Environment env) throws EvalException, InterruptedException {
+ if (lists.size() == 0) {
+ return convert(new ArrayList<>(), env);
+ }
+
+ List<Map.Entry<Ident, Iterable<?>>> listValues = Lists.newArrayListWithCapacity(lists.size());
+ int size = 1;
+ for (Map.Entry<Ident, Expression> list : lists) {
+ Object listValueObject = list.getValue().eval(env);
+ final Iterable<?> listValue = EvalUtils.toIterable(listValueObject, getLocation());
+ int listSize = EvalUtils.size(listValue);
+ if (listSize == 0) {
+ return convert(new ArrayList<>(), env);
+ }
+ size *= listSize;
+ listValues.add(Maps.<Ident, Iterable<?>>immutableEntry(list.getKey(), listValue));
+ }
+ List<Object> resultList = Lists.newArrayListWithCapacity(size);
+ evalLists(env, listValues, resultList);
+ return convert(resultList, env);
+ }
+
+ private Object convert(List<Object> list, Environment env) throws EvalException {
+ if (env.isSkylarkEnabled()) {
+ return SkylarkList.list(list, getLocation());
+ } else {
+ return list;
+ }
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append('[').append(elementExpression);
+ for (Map.Entry<Ident, Expression> list : lists) {
+ sb.append(" for ").append(list.getKey()).append(" in ").append(list.getValue());
+ }
+ sb.append(']');
+ return sb.toString();
+ }
+
+ public Expression getElementExpression() {
+ return elementExpression;
+ }
+
+ public void add(Ident ident, Expression listExpression) {
+ lists.add(Maps.immutableEntry(ident, listExpression));
+ }
+
+ public List<Map.Entry<Ident, Expression>> getLists() {
+ return lists;
+ }
+
+ @Override
+ public void accept(SyntaxTreeVisitor visitor) {
+ visitor.visit(this);
+ }
+
+ /**
+ * Evaluates element expression over all combinations of list element values.
+ *
+ * <p>Iterates over all elements in outermost list (list at index 0) and
+ * updates the value of the list variable in the environment on each
+ * iteration. If there are no other lists to iterate over added evaluation
+ * of the element expression to the result. Otherwise calls itself recursively
+ * with all the lists except the outermost.
+ */
+ private void evalLists(Environment env, List<Map.Entry<Ident, Iterable<?>>> listValues,
+ List<Object> result) throws EvalException, InterruptedException {
+ Map.Entry<Ident, Iterable<?>> listValue = listValues.get(0);
+ for (Object listElement : listValue.getValue()) {
+ env.update(listValue.getKey().getName(), listElement);
+ if (listValues.size() == 1) {
+ result.add(elementExpression.eval(env));
+ } else {
+ evalLists(env, listValues.subList(1, listValues.size()), result);
+ }
+ }
+ }
+
+ @Override
+ SkylarkType validate(ValidationEnvironment env) throws EvalException {
+ for (Map.Entry<Ident, Expression> list : lists) {
+ // TODO(bazel-team): Get the type of elements
+ SkylarkType type = list.getValue().validate(env);
+ env.checkIterable(type, getLocation());
+ env.update(list.getKey().getName(), SkylarkType.UNKNOWN, getLocation());
+ }
+ elementExpression.validate(env);
+ return SkylarkType.of(SkylarkList.class);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/ListLiteral.java b/src/main/java/com/google/devtools/build/lib/syntax/ListLiteral.java
new file mode 100644
index 0000000..9437135
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/ListLiteral.java
@@ -0,0 +1,128 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Syntax node for list and tuple literals.
+ *
+ * (Note that during evaluation, both list and tuple values are represented by
+ * java.util.List objects, the only difference between them being whether or not
+ * they are mutable.)
+ */
+public final class ListLiteral extends Expression {
+
+ /**
+ * Types of the ListLiteral.
+ */
+ public static enum Kind {LIST, TUPLE}
+
+ private final Kind kind;
+
+ private final List<Expression> exprs;
+
+ private ListLiteral(Kind kind, List<Expression> exprs) {
+ this.kind = kind;
+ this.exprs = exprs;
+ }
+
+ public static ListLiteral makeList(List<Expression> exprs) {
+ return new ListLiteral(Kind.LIST, exprs);
+ }
+
+ public static ListLiteral makeTuple(List<Expression> exprs) {
+ return new ListLiteral(Kind.TUPLE, exprs);
+ }
+
+ /**
+ * Returns the list of expressions for each element of the tuple.
+ */
+ public List<Expression> getElements() {
+ return exprs;
+ }
+
+ /**
+ * Returns true if this list is a tuple (a hash table, immutable list).
+ */
+ public boolean isTuple() {
+ return kind == Kind.TUPLE;
+ }
+
+ private static char startChar(Kind kind) {
+ switch(kind) {
+ case LIST: return '[';
+ case TUPLE: return '(';
+ }
+ return '[';
+ }
+
+ private static char endChar(Kind kind) {
+ switch(kind) {
+ case LIST: return ']';
+ case TUPLE: return ')';
+ }
+ return ']';
+ }
+
+ @Override
+ public String toString() {
+ StringBuffer sb = new StringBuffer();
+ sb.append(startChar(kind));
+ String sep = "";
+ for (Expression e : exprs) {
+ sb.append(sep);
+ sb.append(e);
+ sep = ", ";
+ }
+ sb.append(endChar(kind));
+ return sb.toString();
+ }
+
+ @Override
+ Object eval(Environment env) throws EvalException, InterruptedException {
+ List<Object> result = new ArrayList<>();
+ for (Expression expr : exprs) {
+ // Convert NPEs to EvalExceptions.
+ if (expr == null) {
+ throw new EvalException(getLocation(), "null expression in " + this);
+ }
+ result.add(expr.eval(env));
+ }
+ if (env.isSkylarkEnabled()) {
+ return isTuple()
+ ? SkylarkList.tuple(result) : SkylarkList.list(result, getLocation());
+ } else {
+ return EvalUtils.makeSequence(result, isTuple());
+ }
+ }
+
+ @Override
+ public void accept(SyntaxTreeVisitor visitor) {
+ visitor.visit(this);
+ }
+
+ @Override
+ SkylarkType validate(ValidationEnvironment env) throws EvalException {
+ SkylarkType type = SkylarkType.UNKNOWN;
+ if (!isTuple()) {
+ for (Expression expr : exprs) {
+ SkylarkType nextType = expr.validate(env);
+ type = type.infer(nextType, "list literal", expr.getLocation(), getLocation());
+ }
+ }
+ return SkylarkType.of(SkylarkList.class, type.getType());
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/Literal.java b/src/main/java/com/google/devtools/build/lib/syntax/Literal.java
new file mode 100644
index 0000000..9289081
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/Literal.java
@@ -0,0 +1,44 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+/**
+ * Generic base class for primitive literals.
+ */
+public abstract class Literal<T> extends Expression {
+
+ protected final T value;
+
+ protected Literal(T value) {
+ this.value = value;
+ }
+
+ /**
+ * Returns the value of this literal.
+ */
+ public T getValue() {
+ return value;
+ }
+
+ @Override
+ public String toString() {
+ return value.toString();
+ }
+
+ @Override
+ Object eval(Environment env) {
+ return value;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/LoadStatement.java b/src/main/java/com/google/devtools/build/lib/syntax/LoadStatement.java
new file mode 100644
index 0000000..6873995
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/LoadStatement.java
@@ -0,0 +1,78 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.List;
+
+/**
+ * Syntax node for an import statement.
+ */
+public final class LoadStatement extends Statement {
+
+ private final ImmutableList<Ident> symbols;
+ private final PathFragment importPath;
+
+ /**
+ * Constructs an import statement.
+ */
+ LoadStatement(String path, List<Ident> symbols) {
+ this.symbols = ImmutableList.copyOf(symbols);
+ this.importPath = new PathFragment(path + ".bzl");
+ }
+
+ public ImmutableList<Ident> getSymbols() {
+ return symbols;
+ }
+
+ public PathFragment getImportPath() {
+ return importPath;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("load(\"%s\", %s)", importPath, Joiner.on(", ").join(symbols));
+ }
+
+ @Override
+ void exec(Environment env) throws EvalException, InterruptedException {
+ for (Ident i : symbols) {
+ try {
+ if (i.getName().startsWith("_")) {
+ throw new EvalException(getLocation(), "symbol '" + i + "' is private and cannot "
+ + "be imported");
+ }
+ env.importSymbol(getImportPath(), i.getName());
+ } catch (Environment.NoSuchVariableException | Environment.LoadFailedException e) {
+ throw new EvalException(getLocation(), e.getMessage());
+ }
+ }
+ }
+
+ @Override
+ public void accept(SyntaxTreeVisitor visitor) {
+ visitor.visit(this);
+ }
+
+ @Override
+ void validate(ValidationEnvironment env) throws EvalException {
+ // TODO(bazel-team): implement semantical check.
+ for (Ident symbol : symbols) {
+ env.update(symbol.getName(), SkylarkType.UNKNOWN, getLocation());
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/MixedModeFunction.java b/src/main/java/com/google/devtools/build/lib/syntax/MixedModeFunction.java
new file mode 100644
index 0000000..0427157
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/MixedModeFunction.java
@@ -0,0 +1,187 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.packages.Type.ConversionException;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Abstract implementation of Function for functions that accept a mixture of
+ * positional and keyword parameters, as in Python.
+ */
+public abstract class MixedModeFunction extends AbstractFunction {
+
+ // Nomenclature:
+ // "Parameters" are formal parameters of a function definition.
+ // "Arguments" are actual parameters supplied at the call site.
+
+ // Number of regular named parameters (excluding *p and **p) in the
+ // equivalent Python function definition).
+ private final List<String> parameters;
+
+ // Number of leading "parameters" which are mandatory
+ private final int numMandatoryParameters;
+
+ // True if this function requires all arguments to be named
+ // TODO(bazel-team): replace this by a count of arguments before the * with optional arg,
+ // in the style Python 3 or PEP 3102.
+ private final boolean onlyNamedArguments;
+
+ // Location of the function definition, or null for builtin functions.
+ protected final Location location;
+
+ /**
+ * Constructs an instance of Function that supports Python-style mixed-mode
+ * parameter passing.
+ *
+ * @param parameters a list of named parameters
+ * @param numMandatoryParameters the number of leading parameters which are
+ * considered mandatory; the remaining ones may be omitted, in which
+ * case they will have the default value of null.
+ */
+ public MixedModeFunction(String name,
+ Iterable<String> parameters,
+ int numMandatoryParameters,
+ boolean onlyNamedArguments) {
+ this(name, parameters, numMandatoryParameters, onlyNamedArguments, null);
+ }
+
+ protected MixedModeFunction(String name,
+ Iterable<String> parameters,
+ int numMandatoryParameters,
+ boolean onlyNamedArguments,
+ Location location) {
+ super(name);
+ this.parameters = ImmutableList.copyOf(parameters);
+ this.numMandatoryParameters = numMandatoryParameters;
+ this.onlyNamedArguments = onlyNamedArguments;
+ this.location = location;
+ }
+
+ @Override
+ public Object call(List<Object> args,
+ Map<String, Object> kwargs,
+ FuncallExpression ast,
+ Environment env)
+ throws EvalException, InterruptedException {
+
+ // ast is null when called from Java (as there's no Skylark call site).
+ Location loc = ast == null ? location : ast.getLocation();
+ if (onlyNamedArguments && args.size() > 0) {
+ throw new EvalException(loc,
+ getSignature() + " does not accept positional arguments");
+ }
+
+ if (kwargs == null) {
+ kwargs = ImmutableMap.<String, Object>of();
+ }
+
+ int numParams = parameters.size();
+ int numArgs = args.size();
+ Object[] namedArguments = new Object[numParams];
+
+ // first, positional arguments:
+ if (numArgs > numParams) {
+ throw new EvalException(loc,
+ "too many positional arguments in call to " + getSignature());
+ }
+ for (int ii = 0; ii < numArgs; ++ii) {
+ namedArguments[ii] = args.get(ii);
+ }
+
+ // TODO(bazel-team): here, support *varargs splicing
+
+ // second, keyword arguments:
+ for (Map.Entry<String, Object> entry : kwargs.entrySet()) {
+ String keyword = entry.getKey();
+ int pos = parameters.indexOf(keyword);
+ if (pos == -1) {
+ throw new EvalException(loc,
+ "unexpected keyword '" + keyword
+ + "' in call to " + getSignature());
+ } else {
+ if (namedArguments[pos] != null) {
+ throw new EvalException(loc, getSignature()
+ + " got multiple values for keyword argument '" + keyword + "'");
+ }
+ namedArguments[pos] = kwargs.get(keyword);
+ }
+ }
+
+ // third, defaults:
+ for (int ii = 0; ii < numMandatoryParameters; ++ii) {
+ if (namedArguments[ii] == null) {
+ throw new EvalException(loc,
+ getSignature() + " received insufficient arguments");
+ }
+ }
+ // (defaults are always null so nothing extra to do here.)
+
+ try {
+ return call(namedArguments, ast, env);
+ } catch (ConversionException | IllegalArgumentException | IllegalStateException
+ | ClassCastException e) {
+ throw new EvalException(loc, e.getMessage());
+ }
+ }
+
+ /**
+ * Like Function.call, but generalised to support Python-style mixed-mode
+ * keyword and positional parameter passing.
+ *
+ * @param args an array of argument values corresponding to the list
+ * of named parameters passed to the constructor.
+ */
+ protected Object call(Object[] args, FuncallExpression ast)
+ throws EvalException, ConversionException, InterruptedException {
+ throw new UnsupportedOperationException("Method not overridden");
+ }
+
+ /**
+ * Override this method instead of the one above, if you need to access
+ * the environment.
+ */
+ protected Object call(Object[] args, FuncallExpression ast, Environment env)
+ throws EvalException, ConversionException, InterruptedException {
+ return call(args, ast);
+ }
+
+ /**
+ * Render this object in the form of an equivalent Python function signature.
+ */
+ public String getSignature() {
+ StringBuffer sb = new StringBuffer();
+ sb.append(getName()).append('(');
+ int ii = 0;
+ int len = parameters.size();
+ for (; ii < len; ++ii) {
+ String parameter = parameters.get(ii);
+ if (ii > 0) {
+ sb.append(", ");
+ }
+ sb.append(parameter);
+ if (ii >= numMandatoryParameters) {
+ sb.append(" = null");
+ }
+ }
+ sb.append(')');
+ return sb.toString();
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/NotExpression.java b/src/main/java/com/google/devtools/build/lib/syntax/NotExpression.java
new file mode 100644
index 0000000..5a13e79
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/NotExpression.java
@@ -0,0 +1,52 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+/**
+ * As syntax node for the not boolean operation.
+ */
+public class NotExpression extends Expression {
+
+ private final Expression expression;
+
+ public NotExpression(Expression expression) {
+ this.expression = expression;
+ }
+
+ Expression getExpression() {
+ return expression;
+ }
+
+ @Override
+ Object eval(Environment env) throws EvalException, InterruptedException {
+ return !EvalUtils.toBoolean(expression.eval(env));
+ }
+
+ @Override
+ public String toString() {
+ return "not " + expression;
+ }
+
+ @Override
+ public void accept(SyntaxTreeVisitor visitor) {
+ visitor.visit(this);
+ }
+
+ @Override
+ SkylarkType validate(ValidationEnvironment env) throws EvalException {
+ // Don't need type check here since EvalUtils.toBoolean() converts everything.
+ expression.validate(env);
+ return SkylarkType.BOOL;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/Operator.java b/src/main/java/com/google/devtools/build/lib/syntax/Operator.java
new file mode 100644
index 0000000..628570e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/Operator.java
@@ -0,0 +1,47 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+/**
+ * Infix operators supported by the build language.
+ */
+public enum Operator {
+
+ AND("and"),
+ EQUALS_EQUALS("=="),
+ GREATER(">"),
+ GREATER_EQUALS(">="),
+ IN("in"),
+ LESS("<"),
+ LESS_EQUALS("<="),
+ MINUS("-"),
+ MULT("*"),
+ NOT("not"),
+ NOT_EQUALS("!="),
+ OR("or"),
+ PERCENT("%"),
+ PLUS("+");
+
+ private final String name;
+
+ private Operator(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/Parser.java b/src/main/java/com/google/devtools/build/lib/syntax/Parser.java
new file mode 100644
index 0000000..66c3c67
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/Parser.java
@@ -0,0 +1,1274 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.packages.CachingPackageLocator;
+import com.google.devtools.build.lib.syntax.DictionaryLiteral.DictionaryEntryLiteral;
+import com.google.devtools.build.lib.syntax.IfStatement.ConditionalStatements;
+import com.google.devtools.build.lib.vfs.Path;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Recursive descent parser for LL(2) BUILD language.
+ * Loosely based on Python 2 grammar.
+ * See https://docs.python.org/2/reference/grammar.html
+ *
+ */
+class Parser {
+
+ /**
+ * Combines the parser result into a single value object.
+ */
+ public static final class ParseResult {
+ /** The statements (rules, basically) from the parsed file. */
+ public final List<Statement> statements;
+
+ /** The comments from the parsed file. */
+ public final List<Comment> comments;
+
+ /** Whether the file contained any errors. */
+ public final boolean containsErrors;
+
+ public ParseResult(List<Statement> statements, List<Comment> comments, boolean containsErrors) {
+ // No need to copy here; when the object is created, the parser instance is just about to go
+ // out of scope and be garbage collected.
+ this.statements = Preconditions.checkNotNull(statements);
+ this.comments = Preconditions.checkNotNull(comments);
+ this.containsErrors = containsErrors;
+ }
+ }
+
+ private static final EnumSet<TokenKind> STATEMENT_TERMINATOR_SET =
+ EnumSet.of(TokenKind.EOF, TokenKind.NEWLINE);
+
+ private static final EnumSet<TokenKind> LIST_TERMINATOR_SET =
+ EnumSet.of(TokenKind.EOF, TokenKind.RBRACKET, TokenKind.SEMI);
+
+ private static final EnumSet<TokenKind> DICT_TERMINATOR_SET =
+ EnumSet.of(TokenKind.EOF, TokenKind.RBRACE, TokenKind.SEMI);
+
+ private static final EnumSet<TokenKind> EXPR_TERMINATOR_SET = EnumSet.of(
+ TokenKind.EOF,
+ TokenKind.COMMA,
+ TokenKind.COLON,
+ TokenKind.FOR,
+ TokenKind.PLUS,
+ TokenKind.MINUS,
+ TokenKind.PERCENT,
+ TokenKind.RPAREN,
+ TokenKind.RBRACKET);
+
+ private Token token; // current lookahead token
+ private Token pushedToken = null; // used to implement LL(2)
+
+ private static final boolean DEBUGGING = false;
+
+ private final Lexer lexer;
+ private final EventHandler eventHandler;
+ private final List<Comment> comments;
+ private final boolean parsePython;
+ /** Whether advanced language constructs are allowed */
+ private boolean skylarkMode = false;
+
+ private static final Map<TokenKind, Operator> binaryOperators =
+ new ImmutableMap.Builder<TokenKind, Operator>()
+ .put(TokenKind.AND, Operator.AND)
+ .put(TokenKind.EQUALS_EQUALS, Operator.EQUALS_EQUALS)
+ .put(TokenKind.GREATER, Operator.GREATER)
+ .put(TokenKind.GREATER_EQUALS, Operator.GREATER_EQUALS)
+ .put(TokenKind.IN, Operator.IN)
+ .put(TokenKind.LESS, Operator.LESS)
+ .put(TokenKind.LESS_EQUALS, Operator.LESS_EQUALS)
+ .put(TokenKind.MINUS, Operator.MINUS)
+ .put(TokenKind.NOT_EQUALS, Operator.NOT_EQUALS)
+ .put(TokenKind.OR, Operator.OR)
+ .put(TokenKind.PERCENT, Operator.PERCENT)
+ .put(TokenKind.PLUS, Operator.PLUS)
+ .put(TokenKind.STAR, Operator.MULT)
+ .build();
+
+ private static final Map<TokenKind, Operator> augmentedAssignmentMethods =
+ new ImmutableMap.Builder<TokenKind, Operator>()
+ .put(TokenKind.PLUS_EQUALS, Operator.PLUS) // += // TODO(bazel-team): other similar operators
+ .build();
+
+ /** Highest precedence goes last.
+ * Based on: http://docs.python.org/2/reference/expressions.html#operator-precedence
+ **/
+ private static final List<EnumSet<Operator>> operatorPrecedence = ImmutableList.of(
+ EnumSet.of(Operator.OR),
+ EnumSet.of(Operator.AND),
+ EnumSet.of(Operator.NOT),
+ EnumSet.of(Operator.EQUALS_EQUALS, Operator.NOT_EQUALS, Operator.LESS, Operator.LESS_EQUALS,
+ Operator.GREATER, Operator.GREATER_EQUALS, Operator.IN),
+ EnumSet.of(Operator.MINUS, Operator.PLUS),
+ EnumSet.of(Operator.MULT, Operator.PERCENT));
+
+ private Iterator<Token> tokens = null;
+ private int errorsCount;
+ private boolean recoveryMode; // stop reporting errors until next statement
+
+ private CachingPackageLocator locator;
+
+ private List<Path> includedFiles;
+
+ private static final String PREPROCESSING_NEEDED =
+ "Add \"# PYTHON-PREPROCESSING-REQUIRED\" on the first line of the file";
+
+ private Parser(Lexer lexer, EventHandler eventHandler, CachingPackageLocator locator,
+ boolean parsePython) {
+ this.lexer = lexer;
+ this.eventHandler = eventHandler;
+ this.parsePython = parsePython;
+ this.tokens = lexer.getTokens().iterator();
+ this.comments = new ArrayList<Comment>();
+ this.locator = locator;
+ this.includedFiles = new ArrayList<Path>();
+ this.includedFiles.add(lexer.getFilename());
+ nextToken();
+ }
+
+ private Parser(Lexer lexer, EventHandler eventHandler, CachingPackageLocator locator) {
+ this(lexer, eventHandler, locator, false /* parsePython */);
+ }
+
+ public Parser setSkylarkMode(boolean skylarkMode) {
+ this.skylarkMode = skylarkMode;
+ return this;
+ }
+
+ /**
+ * Entry-point to parser that parses a build file with comments. All errors
+ * encountered during parsing are reported via "reporter".
+ */
+ public static ParseResult parseFile(
+ Lexer lexer, EventHandler eventHandler, CachingPackageLocator locator,
+ boolean parsePython) {
+ Parser parser = new Parser(lexer, eventHandler, locator, parsePython);
+ List<Statement> statements = parser.parseFileInput();
+ return new ParseResult(statements, parser.comments,
+ parser.errorsCount > 0 || lexer.containsErrors());
+ }
+
+ /**
+ * Entry-point to parser that parses a build file with comments. All errors
+ * encountered during parsing are reported via "reporter". Enable Skylark extensions
+ * that are not part of the core BUILD language.
+ */
+ public static ParseResult parseFileForSkylark(
+ Lexer lexer, EventHandler eventHandler, CachingPackageLocator locator,
+ ValidationEnvironment validationEnvironment) {
+ Parser parser = new Parser(lexer, eventHandler, locator).setSkylarkMode(true);
+ List<Statement> statements = parser.parseFileInput();
+ boolean hasSemanticalErrors = false;
+ try {
+ for (Statement statement : statements) {
+ statement.validate(validationEnvironment);
+ }
+ } catch (EvalException e) {
+ eventHandler.handle(Event.error(e.getLocation(), e.getMessage()));
+ hasSemanticalErrors = true;
+ }
+ return new ParseResult(statements, parser.comments,
+ parser.errorsCount > 0 || lexer.containsErrors() || hasSemanticalErrors);
+ }
+
+ /**
+ * Entry-point to parser that parses a statement. All errors encountered
+ * during parsing are reported via "reporter".
+ */
+ @VisibleForTesting
+ public static Statement parseStatement(
+ Lexer lexer, EventHandler eventHandler) {
+ return new Parser(lexer, eventHandler, null).parseSmallStatement();
+ }
+
+ /**
+ * Entry-point to parser that parses an expression. All errors encountered
+ * during parsing are reported via "reporter". The expression may be followed
+ * by newline tokens.
+ */
+ @VisibleForTesting
+ public static Expression parseExpression(Lexer lexer, EventHandler eventHandler) {
+ Parser parser = new Parser(lexer, eventHandler, null);
+ Expression result = parser.parseExpression();
+ while (parser.token.kind == TokenKind.NEWLINE) {
+ parser.nextToken();
+ }
+ parser.expect(TokenKind.EOF);
+ return result;
+ }
+
+ private void addIncludedFiles(List<Path> files) {
+ this.includedFiles.addAll(files);
+ }
+
+ private void reportError(Location location, String message) {
+ errorsCount++;
+ // Limit the number of reported errors to avoid spamming output.
+ if (errorsCount <= 5) {
+ eventHandler.handle(Event.error(location, message));
+ }
+ }
+
+ private void syntaxError(Token token) {
+ if (!recoveryMode) {
+ String msg = token.kind == TokenKind.INDENT
+ ? "indentation error"
+ : "syntax error at '" + token + "'";
+ reportError(lexer.createLocation(token.left, token.right), msg);
+ recoveryMode = true;
+ }
+ }
+
+ // Consumes the current token. If it is not of the specified (expected)
+ // kind, reports a syntax error.
+ private boolean expect(TokenKind kind) {
+ boolean expected = token.kind == kind;
+ if (!expected) {
+ syntaxError(token);
+ }
+ nextToken();
+ return expected;
+ }
+
+ /**
+ * Consume tokens past the first token that has a kind that is in the set of
+ * teminatingTokens.
+ * @param terminatingTokens
+ * @return the end offset of the terminating token.
+ */
+ private int syncPast(EnumSet<TokenKind> terminatingTokens) {
+ Preconditions.checkState(terminatingTokens.contains(TokenKind.EOF));
+ while (!terminatingTokens.contains(token.kind)) {
+ nextToken();
+ }
+ int end = token.right;
+ // read past the synchronization token
+ nextToken();
+ return end;
+ }
+
+ /**
+ * Consume tokens until we reach the first token that has a kind that is in
+ * the set of teminatingTokens.
+ * @param terminatingTokens
+ * @return the end offset of the terminating token.
+ */
+ private int syncTo(EnumSet<TokenKind> terminatingTokens) {
+ // EOF must be in the set to prevent an infinite loop
+ Preconditions.checkState(terminatingTokens.contains(TokenKind.EOF));
+ // read past the problematic token
+ int previous = token.right;
+ nextToken();
+ int current = previous;
+ while (!terminatingTokens.contains(token.kind)) {
+ nextToken();
+ previous = current;
+ current = token.right;
+ }
+ return previous;
+ }
+
+ private void nextToken() {
+ if (pushedToken != null) {
+ token = pushedToken;
+ pushedToken = null;
+ } else {
+ if (token == null || token.kind != TokenKind.EOF) {
+ token = tokens.next();
+ // transparently handle comment tokens
+ while (token.kind == TokenKind.COMMENT) {
+ makeComment(token);
+ token = tokens.next();
+ }
+ }
+ }
+ if (DEBUGGING) {
+ System.err.print(token);
+ }
+ }
+
+ private void pushToken(Token tokenToPush) {
+ if (pushedToken != null) {
+ throw new IllegalStateException("Exceeded LL(2) lookahead!");
+ }
+ pushedToken = token;
+ token = tokenToPush;
+ }
+
+ // create an error expression
+ private Ident makeErrorExpression(int start, int end) {
+ return setLocation(new Ident("$error$"), start, end);
+ }
+
+ // Convenience wrapper around ASTNode.setLocation that returns the node.
+ private <NODE extends ASTNode> NODE
+ setLocation(NODE node, int startOffset, int endOffset) {
+ node.setLocation(lexer.createLocation(startOffset, endOffset));
+ return node;
+ }
+
+ // Another convenience wrapper method around ASTNode.setLocation
+ private <NODE extends ASTNode> NODE setLocation(NODE node, Location location) {
+ node.setLocation(location);
+ return node;
+ }
+
+ // Convenience method that uses end offset from the last node.
+ private <NODE extends ASTNode> NODE setLocation(NODE node, int startOffset, ASTNode lastNode) {
+ return setLocation(node, startOffset, lastNode.getLocation().getEndOffset());
+ }
+
+ // create a funcall expression
+ private Expression makeFuncallExpression(Expression receiver, Ident function,
+ List<Argument> args,
+ int start, int end) {
+ if (function.getLocation() == null) {
+ function = setLocation(function, start, end);
+ }
+ boolean seenKeywordArg = false;
+ boolean seenKwargs = false;
+ for (Argument arg : args) {
+ if (arg.isPositional()) {
+ if (seenKeywordArg || seenKwargs) {
+ reportError(arg.getLocation(), "syntax error: non-keyword arg after keyword arg");
+ return makeErrorExpression(start, end);
+ }
+ } else if (arg.isKwargs()) {
+ if (seenKwargs) {
+ reportError(arg.getLocation(), "there can be only one **kwargs argument");
+ return makeErrorExpression(start, end);
+ }
+ seenKwargs = true;
+ } else {
+ seenKeywordArg = true;
+ }
+ }
+
+ return setLocation(new FuncallExpression(receiver, function, args), start, end);
+ }
+
+ // arg ::= IDENTIFIER '=' expr
+ // | expr
+ private Argument parseFunctionCallArgument() {
+ int start = token.left;
+ if (token.kind == TokenKind.IDENTIFIER) {
+ Token identToken = token;
+ String name = (String) token.value;
+ Ident ident = setLocation(new Ident(name), start, token.right);
+ nextToken();
+ if (token.kind == TokenKind.EQUALS) { // it's a named argument
+ nextToken();
+ Expression expr = parseExpression();
+ return setLocation(new Argument(ident, expr), start, expr);
+ } else { // oops, back up!
+ pushToken(identToken);
+ }
+ }
+ // parse **expr
+ if (token.kind == TokenKind.STAR) {
+ expect(TokenKind.STAR);
+ expect(TokenKind.STAR);
+ Expression expr = parseExpression();
+ return setLocation(new Argument(null, expr, true), start, expr);
+ }
+ // parse a positional argument
+ Expression expr = parseExpression();
+ return setLocation(new Argument(expr), start, expr);
+ }
+
+ // arg ::= IDENTIFIER '=' expr
+ // | IDENTIFIER
+ private Argument parseFunctionDefArgument(boolean onlyOptional) {
+ int start = token.left;
+ Ident ident = parseIdent();
+ if (token.kind == TokenKind.EQUALS) { // there's a default value
+ nextToken();
+ Expression expr = parseExpression();
+ return setLocation(new Argument(ident, expr), start, expr);
+ } else if (onlyOptional) {
+ reportError(ident.getLocation(),
+ "Optional arguments are only allowed at the end of the argument list.");
+ }
+ return setLocation(new Argument(ident), start, ident);
+ }
+
+ // funcall_suffix ::= '(' arg_list? ')'
+ private Expression parseFuncallSuffix(int start, Expression receiver,
+ Ident function) {
+ List<Argument> args = Collections.emptyList();
+ expect(TokenKind.LPAREN);
+ int end;
+ if (token.kind == TokenKind.RPAREN) {
+ end = token.right;
+ nextToken(); // RPAREN
+ } else {
+ args = parseFunctionCallArguments(); // (includes optional trailing comma)
+ end = token.right;
+ expect(TokenKind.RPAREN);
+ }
+ return makeFuncallExpression(receiver, function, args, start, end);
+ }
+
+ // selector_suffix ::= '.' IDENTIFIER
+ // |'.' IDENTIFIER funcall_suffix
+ private Expression parseSelectorSuffix(int start, Expression receiver) {
+ expect(TokenKind.DOT);
+ if (token.kind == TokenKind.IDENTIFIER) {
+ Ident ident = parseIdent();
+ if (token.kind == TokenKind.LPAREN) {
+ return parseFuncallSuffix(start, receiver, ident);
+ } else {
+ return setLocation(new DotExpression(receiver, ident), start, token.right);
+ }
+ } else {
+ syntaxError(token);
+ int end = syncTo(EXPR_TERMINATOR_SET);
+ return makeErrorExpression(start, end);
+ }
+ }
+
+ // arg_list ::= ( (arg ',')* arg ','? )?
+ private List<Argument> parseFunctionCallArguments() {
+ List<Argument> args = new ArrayList<>();
+ // terminating tokens for an arg list
+ while (token.kind != TokenKind.RPAREN) {
+ if (token.kind == TokenKind.EOF) {
+ syntaxError(token);
+ break;
+ }
+ args.add(parseFunctionCallArgument());
+ if (token.kind == TokenKind.COMMA) {
+ nextToken();
+ } else {
+ break;
+ }
+ }
+ return args;
+ }
+
+ // expr_list ::= ( (expr ',')* expr ','? )?
+ private List<Expression> parseExprList() {
+ List<Expression> list = new ArrayList<>();
+ // terminating tokens for an expression list
+ while (token.kind != TokenKind.RPAREN && token.kind != TokenKind.RBRACKET) {
+ list.add(parseExpression());
+ if (token.kind == TokenKind.COMMA) {
+ nextToken();
+ } else {
+ break;
+ }
+ }
+ return list;
+ }
+
+ // dict_entry_list ::= ( (dict_entry ',')* dict_entry ','? )?
+ private List<DictionaryEntryLiteral> parseDictEntryList() {
+ List<DictionaryEntryLiteral> list = new ArrayList<>();
+ // the terminating token for a dict entry list
+ while (token.kind != TokenKind.RBRACE) {
+ list.add(parseDictEntry());
+ if (token.kind == TokenKind.COMMA) {
+ nextToken();
+ } else {
+ break;
+ }
+ }
+ return list;
+ }
+
+ // dict_entry ::= expression ':' expression
+ private DictionaryEntryLiteral parseDictEntry() {
+ int start = token.left;
+ Expression key = parseExpression();
+ expect(TokenKind.COLON);
+ Expression value = parseExpression();
+ return setLocation(new DictionaryEntryLiteral(key, value), start, value);
+ }
+
+ private ExpressionStatement mocksubincludeExpression(
+ String labelName, String file, Location location) {
+ List<Argument> args = new ArrayList<>();
+ args.add(setLocation(new Argument(new StringLiteral(labelName, '"')), location));
+ args.add(setLocation(new Argument(new StringLiteral(file, '"')), location));
+ Ident mockIdent = setLocation(new Ident("mocksubinclude"), location);
+ Expression funCall = new FuncallExpression(null, mockIdent, args);
+ return setLocation(new ExpressionStatement(funCall), location);
+ }
+
+ // parse a file from an include call
+ private void include(String labelName, List<Statement> list, Location location) {
+ if (locator == null) {
+ return;
+ }
+
+ try {
+ Label label = Label.parseAbsolute(labelName);
+ String packageName = label.getPackageFragment().getPathString();
+ Path packagePath = locator.getBuildFileForPackage(packageName);
+ if (packagePath == null) {
+ reportError(location, "Package '" + packageName + "' not found");
+ list.add(mocksubincludeExpression(labelName, "", location));
+ return;
+ }
+ Path path = packagePath.getParentDirectory();
+ Path file = path.getRelative(label.getName());
+
+ if (this.includedFiles.contains(file)) {
+ reportError(location, "Recursive inclusion of file '" + path + "'");
+ return;
+ }
+ ParserInputSource inputSource = ParserInputSource.create(file);
+
+ // Insert call to the mocksubinclude function to get the dependencies right.
+ list.add(mocksubincludeExpression(labelName, file.toString(), location));
+
+ Lexer lexer = new Lexer(inputSource, eventHandler, parsePython);
+ Parser parser = new Parser(lexer, eventHandler, locator, parsePython);
+ parser.addIncludedFiles(this.includedFiles);
+ list.addAll(parser.parseFileInput());
+ } catch (Label.SyntaxException e) {
+ reportError(location, "Invalid label '" + labelName + "'");
+ } catch (IOException e) {
+ reportError(location, "Include of '" + labelName + "' failed: " + e.getMessage());
+ list.add(mocksubincludeExpression(labelName, "", location));
+ }
+ }
+
+ // primary ::= INTEGER
+ // | STRING
+ // | STRING '.' IDENTIFIER funcall_suffix
+ // | IDENTIFIER
+ // | IDENTIFIER funcall_suffix
+ // | IDENTIFIER '.' selector_suffix
+ // | list_expression
+ // | '(' ')' // a tuple with zero elements
+ // | '(' expr ')' // a parenthesized expression
+ // | '(' expr ',' expr_list ')' // a tuple with n elements
+ // | dict_expression
+ // | '-' primary_with_suffix
+ private Expression parsePrimary() {
+ int start = token.left;
+ switch (token.kind) {
+ case INT: {
+ IntegerLiteral literal = new IntegerLiteral((Integer) token.value);
+ setLocation(literal, start, token.right);
+ nextToken();
+ return literal;
+ }
+ case STRING: {
+ String value = (String) token.value;
+ int end = token.right;
+ char quoteChar = lexer.charAt(start);
+ nextToken();
+ if (token.kind == TokenKind.STRING) {
+ reportError(lexer.createLocation(end, token.left),
+ "Implicit string concatenation is forbidden, use the + operator");
+ }
+ StringLiteral literal = new StringLiteral(value, quoteChar);
+ setLocation(literal, start, end);
+ return literal;
+ }
+ case IDENTIFIER: {
+ Ident ident = parseIdent();
+ if (token.kind == TokenKind.LPAREN) { // it's a function application
+ return parseFuncallSuffix(start, null, ident);
+ } else {
+ return ident;
+ }
+ }
+ case LBRACKET: { // it's a list
+ return parseListExpression();
+ }
+ case LBRACE: { // it's a dictionary
+ return parseDictExpression();
+ }
+ case LPAREN: {
+ nextToken();
+ // check for the empty tuple literal
+ if (token.kind == TokenKind.RPAREN) {
+ ListLiteral literal =
+ ListLiteral.makeTuple(Collections.<Expression>emptyList());
+ setLocation(literal, start, token.right);
+ nextToken();
+ return literal;
+ }
+ // parse the first expression
+ Expression expression = parseExpression();
+ if (token.kind == TokenKind.COMMA) { // it's a tuple
+ nextToken();
+ // parse the rest of the expression tuple
+ List<Expression> tuple = parseExprList();
+ // add the first expression to the front of the tuple
+ tuple.add(0, expression);
+ expect(TokenKind.RPAREN);
+ return setLocation(
+ ListLiteral.makeTuple(tuple), start, token.right);
+ }
+ setLocation(expression, start, token.right);
+ if (token.kind == TokenKind.RPAREN) {
+ nextToken();
+ return expression;
+ }
+ syntaxError(token);
+ int end = syncTo(EXPR_TERMINATOR_SET);
+ return makeErrorExpression(start, end);
+ }
+ case MINUS: {
+ nextToken();
+
+ List<Argument> args = new ArrayList<>();
+ Expression expr = parsePrimaryWithSuffix();
+ args.add(setLocation(new Argument(expr), start, expr));
+ return makeFuncallExpression(null, new Ident("-"), args,
+ start, token.right);
+ }
+ default: {
+ syntaxError(token);
+ int end = syncTo(EXPR_TERMINATOR_SET);
+ return makeErrorExpression(start, end);
+ }
+ }
+ }
+
+ // primary_with_suffix ::= primary selector_suffix*
+ // | primary substring_suffix
+ private Expression parsePrimaryWithSuffix() {
+ int start = token.left;
+ Expression receiver = parsePrimary();
+ while (true) {
+ if (token.kind == TokenKind.DOT) {
+ receiver = parseSelectorSuffix(start, receiver);
+ } else if (token.kind == TokenKind.LBRACKET) {
+ receiver = parseSubstringSuffix(start, receiver);
+ } else {
+ break;
+ }
+ }
+ return receiver;
+ }
+
+ // substring_suffix ::= '[' expression? ':' expression? ']'
+ private Expression parseSubstringSuffix(int start, Expression receiver) {
+ List<Argument> args = new ArrayList<>();
+ Expression startExpr;
+ Expression endExpr;
+
+ expect(TokenKind.LBRACKET);
+ int loc1 = token.left;
+ if (token.kind == TokenKind.COLON) {
+ startExpr = setLocation(new IntegerLiteral(0), token.left, token.right);
+ } else {
+ startExpr = parseExpression();
+ }
+ args.add(setLocation(new Argument(startExpr), loc1, startExpr));
+ // This is a dictionary access
+ if (token.kind == TokenKind.RBRACKET) {
+ expect(TokenKind.RBRACKET);
+ return makeFuncallExpression(receiver, new Ident("$index"), args,
+ start, token.right);
+ }
+ // This is a substring
+ expect(TokenKind.COLON);
+ int loc2 = token.left;
+ if (token.kind == TokenKind.RBRACKET) {
+ endExpr = setLocation(new IntegerLiteral(Integer.MAX_VALUE), token.left, token.right);
+ } else {
+ endExpr = parseExpression();
+ }
+ expect(TokenKind.RBRACKET);
+
+ args.add(setLocation(new Argument(endExpr), loc2, endExpr));
+ return makeFuncallExpression(receiver, new Ident("$substring"), args,
+ start, token.right);
+ }
+
+ // loop_variables ::= '(' variables ')'
+ // | variables
+ // variables ::= ident (',' ident)*
+ private Ident parseForLoopVariables() {
+ int start = token.left;
+ boolean hasParen = false;
+ if (token.kind == TokenKind.LPAREN) {
+ hasParen = true;
+ nextToken();
+ }
+
+ // TODO(bazel-team): allow multiple variables in the core Blaze language too.
+ Ident firstIdent = parseIdent();
+ boolean multipleVariables = false;
+
+ while (token.kind == TokenKind.COMMA) {
+ multipleVariables = true;
+ nextToken();
+ parseIdent();
+ }
+
+ if (hasParen) {
+ expect(TokenKind.RPAREN);
+ }
+
+ int end = token.right;
+ if (multipleVariables && !parsePython) {
+ reportError(lexer.createLocation(start, end),
+ "For loops with multiple variables are not yet supported. "
+ + PREPROCESSING_NEEDED);
+ }
+ return multipleVariables ? makeErrorExpression(start, end) : firstIdent;
+ }
+
+ // list_expression ::= '[' ']'
+ // |'[' expr ']'
+ // |'[' expr ',' expr_list ']'
+ // |'[' expr ('FOR' loop_variables 'IN' expr)+ ']'
+ private Expression parseListExpression() {
+ int start = token.left;
+ expect(TokenKind.LBRACKET);
+ if (token.kind == TokenKind.RBRACKET) { // empty List
+ ListLiteral literal =
+ ListLiteral.makeList(Collections.<Expression>emptyList());
+ setLocation(literal, start, token.right);
+ nextToken();
+ return literal;
+ }
+ Expression expression = parseExpression();
+ Preconditions.checkNotNull(expression,
+ "null element in list in AST at %s:%s", token.left, token.right);
+ switch (token.kind) {
+ case RBRACKET: { // singleton List
+ ListLiteral literal =
+ ListLiteral.makeList(Collections.singletonList(expression));
+ setLocation(literal, start, token.right);
+ nextToken();
+ return literal;
+ }
+ case FOR: { // list comprehension
+ ListComprehension listComprehension =
+ new ListComprehension(expression);
+ do {
+ nextToken();
+ Ident ident = parseForLoopVariables();
+ if (token.kind == TokenKind.IN) {
+ nextToken();
+ Expression listExpression = parseExpression();
+ listComprehension.add(ident, listExpression);
+ } else {
+ break;
+ }
+ if (token.kind == TokenKind.RBRACKET) {
+ setLocation(listComprehension, start, token.right);
+ nextToken();
+ return listComprehension;
+ }
+ } while (token.kind == TokenKind.FOR);
+
+ syntaxError(token);
+ int end = syncPast(LIST_TERMINATOR_SET);
+ return makeErrorExpression(start, end);
+ }
+ case COMMA: {
+ nextToken();
+ List<Expression> list = parseExprList();
+ Preconditions.checkState(!list.contains(null),
+ "null element in list in AST at %s:%s", token.left, token.right);
+ list.add(0, expression);
+ if (token.kind == TokenKind.RBRACKET) {
+ ListLiteral literal = ListLiteral.makeList(list);
+ setLocation(literal, start, token.right);
+ nextToken();
+ return literal;
+ }
+ syntaxError(token);
+ int end = syncPast(LIST_TERMINATOR_SET);
+ return makeErrorExpression(start, end);
+ }
+ default: {
+ syntaxError(token);
+ int end = syncPast(LIST_TERMINATOR_SET);
+ return makeErrorExpression(start, end);
+ }
+ }
+ }
+
+ // dict_expression ::= '{' '}'
+ // |'{' dict_entry_list '}'
+ // |'{' dict_entry 'FOR' loop_variables 'IN' expr '}'
+ private Expression parseDictExpression() {
+ int start = token.left;
+ expect(TokenKind.LBRACE);
+ if (token.kind == TokenKind.RBRACE) { // empty List
+ DictionaryLiteral literal =
+ new DictionaryLiteral(ImmutableList.<DictionaryEntryLiteral>of());
+ setLocation(literal, start, token.right);
+ nextToken();
+ return literal;
+ }
+ DictionaryEntryLiteral entry = parseDictEntry();
+ if (token.kind == TokenKind.FOR) {
+ // Dict comprehension
+ nextToken();
+ Ident loopVar = parseForLoopVariables();
+ expect(TokenKind.IN);
+ Expression listExpression = parseExpression();
+ expect(TokenKind.RBRACE);
+ return setLocation(new DictComprehension(
+ entry.getKey(), entry.getValue(), loopVar, listExpression), start, token.right);
+ }
+ List<DictionaryEntryLiteral> entries = new ArrayList<>();
+ entries.add(entry);
+ if (token.kind == TokenKind.COMMA) {
+ expect(TokenKind.COMMA);
+ entries.addAll(parseDictEntryList());
+ }
+ if (token.kind == TokenKind.RBRACE) {
+ DictionaryLiteral literal = new DictionaryLiteral(entries);
+ setLocation(literal, start, token.right);
+ nextToken();
+ return literal;
+ }
+ syntaxError(token);
+ int end = syncPast(DICT_TERMINATOR_SET);
+ return makeErrorExpression(start, end);
+ }
+
+ private Ident parseIdent() {
+ if (token.kind != TokenKind.IDENTIFIER) {
+ syntaxError(token);
+ return makeErrorExpression(token.left, token.right);
+ }
+ Ident ident = new Ident(((String) token.value));
+ setLocation(ident, token.left, token.right);
+ nextToken();
+ return ident;
+ }
+
+ // binop_expression ::= binop_expression OP binop_expression
+ // | parsePrimaryWithSuffix
+ // This function takes care of precedence between operators (see operatorPrecedence for
+ // the order), and it assumes left-to-right associativity.
+ private Expression parseBinOpExpression(int prec) {
+ int start = token.left;
+ Expression expr = parseExpression(prec + 1);
+ // The loop is not strictly needed, but it prevents risks of stack overflow. Depth is
+ // limited to number of different precedence levels (operatorPrecedence.size()).
+ for (;;) {
+ if (!binaryOperators.containsKey(token.kind)) {
+ return expr;
+ }
+ Operator operator = binaryOperators.get(token.kind);
+ if (!operatorPrecedence.get(prec).contains(operator)) {
+ return expr;
+ }
+ nextToken();
+ Expression secondary = parseExpression(prec + 1);
+ expr = optimizeBinOpExpression(operator, expr, secondary);
+ setLocation(expr, start, secondary);
+ }
+ }
+
+ // Optimize binary expressions.
+ // string literal + string literal can be concatenated into one string literal
+ // so we don't have to do the expensive string concatenation at runtime.
+ private Expression optimizeBinOpExpression(
+ Operator operator, Expression expr, Expression secondary) {
+ if (operator == Operator.PLUS) {
+ if (expr instanceof StringLiteral && secondary instanceof StringLiteral) {
+ StringLiteral left = (StringLiteral) expr;
+ StringLiteral right = (StringLiteral) secondary;
+ if (left.getQuoteChar() == right.getQuoteChar()) {
+ return new StringLiteral(left.getValue() + right.getValue(), left.getQuoteChar());
+ }
+ }
+ }
+ return new BinaryOperatorExpression(operator, expr, secondary);
+ }
+
+ private Expression parseExpression() {
+ return parseExpression(0);
+ }
+
+ private Expression parseExpression(int prec) {
+ if (prec >= operatorPrecedence.size()) {
+ return parsePrimaryWithSuffix();
+ }
+ if (token.kind == TokenKind.NOT && operatorPrecedence.get(prec).contains(Operator.NOT)) {
+ return parseNotExpression(prec);
+ }
+ return parseBinOpExpression(prec);
+ }
+
+ // not_expr :== 'not' expr
+ private Expression parseNotExpression(int prec) {
+ int start = token.left;
+ expect(TokenKind.NOT);
+ Expression expression = parseExpression(prec + 1);
+ NotExpression notExpression = new NotExpression(expression);
+ return setLocation(notExpression, start, token.right);
+ }
+
+ // file_input ::= ('\n' | stmt)* EOF
+ private List<Statement> parseFileInput() {
+ List<Statement> list = new ArrayList<>();
+ while (token.kind != TokenKind.EOF) {
+ if (token.kind == TokenKind.NEWLINE) {
+ expect(TokenKind.NEWLINE);
+ } else {
+ parseTopLevelStatement(list);
+ }
+ }
+ return list;
+ }
+
+ // load(STRING (COMMA STRING)*)
+ private void parseLoad(List<Statement> list) {
+ int start = token.left;
+ if (token.kind != TokenKind.STRING) {
+ expect(TokenKind.STRING);
+ return;
+ }
+ String path = (String) token.value;
+ nextToken();
+ expect(TokenKind.COMMA);
+
+ List<Ident> symbols = new ArrayList<>();
+ if (token.kind == TokenKind.STRING) {
+ symbols.add(new Ident((String) token.value));
+ }
+ expect(TokenKind.STRING);
+ while (token.kind == TokenKind.COMMA) {
+ expect(TokenKind.COMMA);
+ if (token.kind == TokenKind.STRING) {
+ symbols.add(new Ident((String) token.value));
+ }
+ expect(TokenKind.STRING);
+ }
+ expect(TokenKind.RPAREN);
+ list.add(setLocation(new LoadStatement(path, symbols), start, token.left));
+ }
+
+ private void parseTopLevelStatement(List<Statement> list) {
+ // In Python grammar, there is no "top-level statement" and imports are
+ // considered as "small statements". We are a bit stricter than Python here.
+ int start = token.left;
+
+ // Check if there is an include
+ if (token.kind == TokenKind.IDENTIFIER) {
+ Token identToken = token;
+ Ident ident = parseIdent();
+
+ if (ident.getName().equals("include") && token.kind == TokenKind.LPAREN && !skylarkMode) {
+ expect(TokenKind.LPAREN);
+ if (token.kind == TokenKind.STRING) {
+ include((String) token.value, list, lexer.createLocation(start, token.right));
+ }
+ expect(TokenKind.STRING);
+ expect(TokenKind.RPAREN);
+ return;
+ } else if (ident.getName().equals("load") && token.kind == TokenKind.LPAREN) {
+ expect(TokenKind.LPAREN);
+ parseLoad(list);
+ return;
+ }
+ pushToken(identToken); // push the ident back to parse it as a statement
+ }
+ parseStatement(list, true);
+ }
+
+ // simple_stmt ::= small_stmt (';' small_stmt)* ';'? NEWLINE
+ private void parseSimpleStatement(List<Statement> list) {
+ list.add(parseSmallStatement());
+
+ while (token.kind == TokenKind.SEMI) {
+ nextToken();
+ if (token.kind == TokenKind.NEWLINE) {
+ break;
+ }
+ list.add(parseSmallStatement());
+ }
+ expect(TokenKind.NEWLINE);
+ // This is a safe place to recover: There is a new line at top-level
+ // and the parser is at the end of a statement.
+ recoveryMode = false;
+ }
+
+ // small_stmt ::= assign_stmt
+ // | expr
+ // | RETURN expr
+ // assign_stmt ::= expr ('=' | augassign) expr
+ // augassign ::= ('+=' )
+ // Note that these are in Python, but not implemented here (at least for now):
+ // '-=' | '*=' | '/=' | '%=' | '&=' | '|=' | '^=' |'<<=' | '>>=' | '**=' | '//='
+ // Semantic difference from Python:
+ // In Skylark, x += y is simple syntactic sugar for x = x + y.
+ // In Python, x += y is more or less equivalent to x = x + y, but if a method is defined
+ // on x.__iadd__(y), then it takes precedence, and in the case of lists it side-effects
+ // the original list (it doesn't do that on tuples); if no such method is defined it falls back
+ // to the x.__add__(y) method that backs x + y. In Skylark, we don't support this side-effect.
+ // Note also that there is a special casing to translate 'ident[key] = value'
+ // to 'ident = ident + {key: value}'. This is needed to support the pure version of Python-like
+ // dictionary assignment syntax.
+ private Statement parseSmallStatement() {
+ int start = token.left;
+ if (token.kind == TokenKind.RETURN) {
+ return parseReturnStatement();
+ }
+ Expression expression = parseExpression();
+ if (token.kind == TokenKind.EQUALS) {
+ nextToken();
+ Expression rvalue = parseExpression();
+ if (expression instanceof FuncallExpression) {
+ FuncallExpression func = (FuncallExpression) expression;
+ if (func.getFunction().getName().equals("$index") && func.getObject() instanceof Ident) {
+ // Special casing to translate 'ident[key] = value' to 'ident = ident + {key: value}'
+ // Note that the locations of these extra expressions are fake.
+ Preconditions.checkArgument(func.getArguments().size() == 1);
+ DictionaryLiteral dictRValue = setLocation(new DictionaryLiteral(ImmutableList.of(
+ setLocation(new DictionaryEntryLiteral(func.getArguments().get(0).getValue(), rvalue),
+ start, token.right))), start, token.right);
+ BinaryOperatorExpression binExp = setLocation(new BinaryOperatorExpression(
+ Operator.PLUS, func.getObject(), dictRValue), start, token.right);
+ return setLocation(new AssignmentStatement(func.getObject(), binExp), start, token.right);
+ }
+ }
+ return setLocation(new AssignmentStatement(expression, rvalue), start, rvalue);
+ } else if (augmentedAssignmentMethods.containsKey(token.kind)) {
+ Operator operator = augmentedAssignmentMethods.get(token.kind);
+ nextToken();
+ Expression operand = parseExpression();
+ int end = operand.getLocation().getEndOffset();
+ return setLocation(new AssignmentStatement(expression,
+ setLocation(new BinaryOperatorExpression(
+ operator, expression, operand), start, end)),
+ start, end);
+ } else {
+ return setLocation(new ExpressionStatement(expression), start, expression);
+ }
+ }
+
+ // if_stmt ::= IF expr ':' suite [ELIF expr ':' suite]* [ELSE ':' suite]?
+ private void parseIfStatement(List<Statement> list) {
+ int start = token.left;
+ List<ConditionalStatements> thenBlocks = new ArrayList<>();
+ thenBlocks.add(parseConditionalStatements(TokenKind.IF));
+ while (token.kind == TokenKind.ELIF) {
+ thenBlocks.add(parseConditionalStatements(TokenKind.ELIF));
+ }
+ List<Statement> elseBlock = new ArrayList<>();
+ if (token.kind == TokenKind.ELSE) {
+ expect(TokenKind.ELSE);
+ expect(TokenKind.COLON);
+ parseSuite(elseBlock);
+ }
+ Statement stmt = new IfStatement(thenBlocks, elseBlock);
+ list.add(setLocation(stmt, start, token.right));
+ }
+
+ // cond_stmts ::= [EL]IF expr ':' suite
+ private ConditionalStatements parseConditionalStatements(TokenKind tokenKind) {
+ int start = token.left;
+ expect(tokenKind);
+ Expression expr = parseExpression();
+ expect(TokenKind.COLON);
+ List<Statement> thenBlock = new ArrayList<>();
+ parseSuite(thenBlock);
+ ConditionalStatements stmt = new ConditionalStatements(expr, thenBlock);
+ return setLocation(stmt, start, token.right);
+ }
+
+ // for_stmt ::= FOR IDENTIFIER IN expr ':' suite
+ private void parseForStatement(List<Statement> list) {
+ int start = token.left;
+ expect(TokenKind.FOR);
+ Ident ident = parseIdent();
+ expect(TokenKind.IN);
+ Expression collection = parseExpression();
+ expect(TokenKind.COLON);
+ List<Statement> block = new ArrayList<>();
+ parseSuite(block);
+ Statement stmt = new ForStatement(ident, collection, block);
+ list.add(setLocation(stmt, start, token.right));
+ }
+
+ // def foo(bar1, bar2):
+ private void parseFunctionDefStatement(List<Statement> list) {
+ int start = token.left;
+ expect(TokenKind.DEF);
+ Ident ident = parseIdent();
+ expect(TokenKind.LPAREN);
+ // parsing the function arguments, at this point only identifiers
+ // TODO(bazel-team): support proper arguments with default values and kwargs
+ List<Argument> args = parseFunctionDefArguments();
+ expect(TokenKind.RPAREN);
+ expect(TokenKind.COLON);
+ List<Statement> block = new ArrayList<>();
+ parseSuite(block);
+ FunctionDefStatement stmt = new FunctionDefStatement(ident, args, block);
+ list.add(setLocation(stmt, start, token.right));
+ }
+
+ private List<Argument> parseFunctionDefArguments() {
+ List<Argument> args = new ArrayList<>();
+ Set<String> argNames = new HashSet<>();
+ boolean onlyOptional = false;
+ while (token.kind != TokenKind.RPAREN) {
+ Argument arg = parseFunctionDefArgument(onlyOptional);
+ if (arg.hasValue()) {
+ onlyOptional = true;
+ }
+ args.add(arg);
+ if (argNames.contains(arg.getArgName())) {
+ reportError(lexer.createLocation(token.left, token.right),
+ "duplicate argument name in function definition");
+ }
+ argNames.add(arg.getArgName());
+ if (token.kind == TokenKind.COMMA) {
+ nextToken();
+ } else {
+ break;
+ }
+ }
+ return args;
+ }
+
+ // suite ::= simple_stmt
+ // | NEWLINE INDENT stmt+ OUTDENT
+ private void parseSuite(List<Statement> list) {
+ if (token.kind == TokenKind.NEWLINE) {
+ expect(TokenKind.NEWLINE);
+ if (token.kind != TokenKind.INDENT) {
+ reportError(lexer.createLocation(token.left, token.right),
+ "expected an indented block");
+ return;
+ }
+ expect(TokenKind.INDENT);
+ while (token.kind != TokenKind.OUTDENT && token.kind != TokenKind.EOF) {
+ parseStatement(list, false);
+ }
+ expect(TokenKind.OUTDENT);
+ } else {
+ Statement stmt = parseSmallStatement();
+ list.add(stmt);
+ expect(TokenKind.NEWLINE);
+ }
+ }
+
+ // skipSuite does not check that the code is syntactically correct, it
+ // just skips based on indentation levels.
+ private void skipSuite() {
+ if (token.kind == TokenKind.NEWLINE) {
+ expect(TokenKind.NEWLINE);
+ if (token.kind != TokenKind.INDENT) {
+ reportError(lexer.createLocation(token.left, token.right),
+ "expected an indented block");
+ return;
+ }
+ expect(TokenKind.INDENT);
+
+ // Don't try to parse all the Python syntax, just skip the block
+ // until the corresponding outdent token.
+ int depth = 1;
+ while (depth > 0) {
+ // Because of the way the lexer works, this should never happen
+ Preconditions.checkState(token.kind != TokenKind.EOF);
+
+ if (token.kind == TokenKind.INDENT) {
+ depth++;
+ }
+ if (token.kind == TokenKind.OUTDENT) {
+ depth--;
+ }
+ nextToken();
+ }
+
+ } else {
+ // the block ends at the newline token
+ // e.g. if x == 3: print "three"
+ syncTo(STATEMENT_TERMINATOR_SET);
+ }
+ }
+
+ // stmt ::= simple_stmt
+ // | compound_stmt
+ private void parseStatement(List<Statement> list, boolean isTopLevel) {
+ if (token.kind == TokenKind.DEF && skylarkMode) {
+ if (!isTopLevel) {
+ reportError(lexer.createLocation(token.left, token.right),
+ "nested functions are not allowed. Move the function to top-level");
+ }
+ parseFunctionDefStatement(list);
+ } else if (token.kind == TokenKind.IF && skylarkMode) {
+ parseIfStatement(list);
+ } else if (token.kind == TokenKind.FOR && skylarkMode) {
+ if (isTopLevel) {
+ reportError(lexer.createLocation(token.left, token.right),
+ "for loops are not allowed on top-level. Put it into a function");
+ }
+ parseForStatement(list);
+ } else if (token.kind == TokenKind.IF
+ || token.kind == TokenKind.ELSE
+ || token.kind == TokenKind.FOR
+ || token.kind == TokenKind.CLASS
+ || token.kind == TokenKind.DEF
+ || token.kind == TokenKind.TRY) {
+ skipBlock();
+ } else {
+ parseSimpleStatement(list);
+ }
+ }
+
+ // return_stmt ::= RETURN expr
+ private ReturnStatement parseReturnStatement() {
+ int start = token.left;
+ expect(TokenKind.RETURN);
+ Expression expression = parseExpression();
+ return setLocation(new ReturnStatement(expression), start, expression);
+ }
+
+ // block ::= ('if' | 'for' | 'class') expr ':' suite
+ private void skipBlock() {
+ int start = token.left;
+ Token blockToken = token;
+ syncTo(EnumSet.of(TokenKind.COLON, TokenKind.EOF)); // skip over expression or name
+ if (!parsePython) {
+ reportError(lexer.createLocation(start, token.right), "syntax error at '"
+ + blockToken + "': This Python-style construct is not supported. "
+ + PREPROCESSING_NEEDED);
+ }
+ expect(TokenKind.COLON);
+ skipSuite();
+ }
+
+ // create a comment node
+ private void makeComment(Token token) {
+ comments.add(setLocation(new Comment((String) token.value), token.left, token.right));
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/ParserInputSource.java b/src/main/java/com/google/devtools/build/lib/syntax/ParserInputSource.java
new file mode 100644
index 0000000..488c762
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/ParserInputSource.java
@@ -0,0 +1,112 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+import com.google.common.hash.HashCode;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
+import com.google.devtools.build.lib.vfs.Path;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * An abstraction for reading input from a file or taking it as a pre-cooked
+ * char[] or String.
+ */
+public abstract class ParserInputSource {
+
+ protected ParserInputSource() {}
+
+ /**
+ * Returns the content of the input source.
+ */
+ public abstract char [] getContent();
+
+ /**
+ * Returns the path of the input source. Note: Once constructed, this object
+ * will never re-read the content from path.
+ */
+ public abstract Path getPath();
+
+ /**
+ * Create an input source instance by (eagerly) reading from the file at
+ * path. The file is assumed to be ISO-8859-1 encoded and smaller than
+ * 2 Gigs - these assumptions are reasonable for BUILD files, which is
+ * all we care about here.
+ */
+ public static ParserInputSource create(Path path) throws IOException {
+ char[] content = FileSystemUtils.readContentAsLatin1(path);
+ if (path.getFileSize() > content.length) {
+ // This assertion is to help diagnose problems arising from the
+ // filesystem; see bugs and #859334 and #920195.
+ throw new IOException("Unexpected short read from file '" + path
+ + "' (expected " + path.getFileSize() + ", got " + content.length + " bytes)");
+ }
+ return create(content, path);
+ }
+
+ /**
+ * Create an input source from the given content, and associate path with
+ * this source. Path will be used in error messages etc. but we will *never*
+ * attempt to read the content from path.
+ */
+ public static ParserInputSource create(String content, Path path) {
+ return create(content.toCharArray(), path);
+ }
+
+ /**
+ * Create an input source from the given content, and associate path with
+ * this source. Path will be used in error messages etc. but we will *never*
+ * attempt to read the content from path.
+ */
+ public static ParserInputSource create(final char[] content, final Path path) {
+ return new ParserInputSource() {
+
+ @Override
+ public char[] getContent() {
+ return content;
+ }
+
+ @Override
+ public Path getPath() {
+ return path;
+ }
+ };
+ }
+
+ /**
+ * Create an input source from the given input stream, and associate path
+ * with this source. 'path' will be used in error messages, etc, but will
+ * not (in general) be used to to read the content from path.
+ *
+ * (The exception is the case in which Python pre-processing is required; the
+ * path will be used to provide the input to the Python pre-processor.
+ * Arguably, we should just send the content as input to the subprocess
+ * instead of using the path, but it's not clear it's worth the effort.)
+ */
+ public static ParserInputSource create(InputStream in, Path path) throws IOException {
+ try {
+ return create(new String(FileSystemUtils.readContentAsLatin1(in)), path);
+ } finally {
+ in.close();
+ }
+ }
+
+ /**
+ * Returns a hash code calculated from the string content of this file.
+ */
+ public String contentHashCode() throws IOException {
+ return HashCode.fromBytes(getPath().getMD5Digest()).toString();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/ReturnStatement.java b/src/main/java/com/google/devtools/build/lib/syntax/ReturnStatement.java
new file mode 100644
index 0000000..07032c2
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/ReturnStatement.java
@@ -0,0 +1,75 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.syntax.SkylarkType.SkylarkFunctionType;
+
+/**
+ * A wrapper Statement class for return expressions.
+ */
+public class ReturnStatement extends Statement {
+
+ /**
+ * Exception sent by the return statement, to be caught by the function body.
+ */
+ public class ReturnException extends EvalException {
+ Object value;
+
+ public ReturnException(Location location, Object value) {
+ super(location, "Return statements must be inside a function");
+ this.value = value;
+ }
+
+ public Object getValue() {
+ return value;
+ }
+ }
+
+ private final Expression returnExpression;
+
+ public ReturnStatement(Expression returnExpression) {
+ this.returnExpression = returnExpression;
+ }
+
+ @Override
+ void exec(Environment env) throws EvalException, InterruptedException {
+ throw new ReturnException(returnExpression.getLocation(), returnExpression.eval(env));
+ }
+
+ Expression getReturnExpression() {
+ return returnExpression;
+ }
+
+ @Override
+ public String toString() {
+ return "return " + returnExpression;
+ }
+
+ @Override
+ public void accept(SyntaxTreeVisitor visitor) {
+ visitor.visit(this);
+ }
+
+ @Override
+ void validate(ValidationEnvironment env) throws EvalException {
+ // TODO(bazel-team): save the return type in the environment, to type-check functions.
+ SkylarkFunctionType fct = env.getCurrentFunction();
+ if (fct == null) {
+ throw new EvalException(getLocation(), "Return statements must be inside a function");
+ }
+ SkylarkType resultType = returnExpression.validate(env);
+ fct.setReturnType(resultType, getLocation());
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/SelectorValue.java b/src/main/java/com/google/devtools/build/lib/syntax/SelectorValue.java
new file mode 100644
index 0000000..4fb3bdb
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/SelectorValue.java
@@ -0,0 +1,45 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+import java.util.Map;
+
+/**
+ * The value passed to a select({...}) statement, e.g.:
+ *
+ * <pre>
+ * rule(
+ * name = 'myrule',
+ * deps = select({
+ * 'a': [':adep'],
+ * 'b': [':bdep'],
+ * })
+ * </pre>
+ */
+public final class SelectorValue {
+ Map<?, ?> dictionary;
+
+ public SelectorValue(Map<?, ?> dictionary) {
+ this.dictionary = dictionary;
+ }
+
+ public Map<?, ?> getDictionary() {
+ return dictionary;
+ }
+
+ @Override
+ public String toString() {
+ return "selector({...})";
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/SkylarkBuiltin.java b/src/main/java/com/google/devtools/build/lib/syntax/SkylarkBuiltin.java
new file mode 100644
index 0000000..a2f0d1b
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/SkylarkBuiltin.java
@@ -0,0 +1,61 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+
+/**
+ * An annotation to mark built-in keyword argument methods accessible from Skylark.
+ */
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface SkylarkBuiltin {
+
+ String name();
+
+ String doc();
+
+ Param[] mandatoryParams() default {};
+
+ Param[] optionalParams() default {};
+
+ boolean hidden() default false;
+
+ Class<?> objectType() default Object.class;
+
+ Class<?> returnType() default Object.class;
+
+ boolean onlyLoadingPhase() default false;
+
+ /**
+ * An annotation for parameters of Skylark built-in functions.
+ */
+ @Retention(RetentionPolicy.RUNTIME)
+ public @interface Param {
+
+ String name();
+
+ String doc();
+
+ Class<?> type() default Object.class;
+
+ Class<?> generic1() default Object.class;
+
+ boolean callbackEnabled() default false;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/SkylarkCallable.java b/src/main/java/com/google/devtools/build/lib/syntax/SkylarkCallable.java
new file mode 100644
index 0000000..ae6987f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/SkylarkCallable.java
@@ -0,0 +1,36 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * A marker interface for Java methods which can be called from Skylark.
+ */
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface SkylarkCallable {
+ String name() default "";
+
+ String doc();
+
+ boolean hidden() default false;
+
+ boolean structField() default false;
+
+ boolean allowReturnNones() default false;
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/SkylarkCallbackFunction.java b/src/main/java/com/google/devtools/build/lib/syntax/SkylarkCallbackFunction.java
new file mode 100644
index 0000000..2e94be8
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/SkylarkCallbackFunction.java
@@ -0,0 +1,44 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+import com.google.common.collect.ImmutableList;
+
+
+/**
+ * A helper class for calling Skylark functions from Java.
+ */
+public class SkylarkCallbackFunction {
+
+ private final UserDefinedFunction callback;
+ private final FuncallExpression ast;
+ private final SkylarkEnvironment funcallEnv;
+
+ public SkylarkCallbackFunction(UserDefinedFunction callback, FuncallExpression ast,
+ SkylarkEnvironment funcallEnv) {
+ this.callback = callback;
+ this.ast = ast;
+ this.funcallEnv = funcallEnv;
+ }
+
+ public Object call(ClassObject ctx, Object... arguments) throws EvalException {
+ try {
+ return callback.call(
+ ImmutableList.<Object>builder().add(ctx).add(arguments).build(), null, ast, funcallEnv);
+ } catch (InterruptedException | ClassCastException
+ | IllegalArgumentException e) {
+ throw new EvalException(ast.getLocation(), e.getMessage());
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/SkylarkEnvironment.java b/src/main/java/com/google/devtools/build/lib/syntax/SkylarkEnvironment.java
new file mode 100644
index 0000000..7e6f414
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/SkylarkEnvironment.java
@@ -0,0 +1,253 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.util.Fingerprint;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+/**
+ * The environment for Skylark.
+ */
+public class SkylarkEnvironment extends Environment {
+
+ /**
+ * This set contains the variable names of all the successful lookups from the global
+ * environment. This is necessary because if in a function definition something
+ * reads a global variable after which a local variable with the same name is assigned an
+ * Exception needs to be thrown.
+ */
+ private final Set<String> readGlobalVariables = new HashSet<>();
+
+ private ImmutableList<String> stackTrace;
+
+ @Nullable private String fileContentHashCode;
+
+ /**
+ * Creates a Skylark Environment for function calling, from the global Environment of the
+ * caller Environment (which must be a Skylark Environment).
+ */
+ public static SkylarkEnvironment createEnvironmentForFunctionCalling(
+ Environment callerEnv, SkylarkEnvironment definitionEnv,
+ UserDefinedFunction function) throws EvalException {
+ if (callerEnv.getStackTrace().contains(function.getName())) {
+ throw new EvalException(function.getLocation(), "Recursion was detected when calling '"
+ + function.getName() + "' from '" + Iterables.getLast(callerEnv.getStackTrace()) + "'");
+ }
+ ImmutableList<String> stackTrace = new ImmutableList.Builder<String>()
+ .addAll(callerEnv.getStackTrace())
+ .add(function.getName())
+ .build();
+ SkylarkEnvironment childEnv =
+ // Always use the caller Environment's EventHandler. We cannot assume that the
+ // definition Environment's EventHandler is still working properly.
+ new SkylarkEnvironment(definitionEnv, stackTrace, callerEnv.eventHandler);
+ try {
+ for (String varname : callerEnv.propagatingVariables) {
+ childEnv.updateAndPropagate(varname, callerEnv.lookup(varname));
+ }
+ } catch (NoSuchVariableException e) {
+ // This should never happen.
+ throw new IllegalStateException(e);
+ }
+ childEnv.disabledVariables = callerEnv.disabledVariables;
+ childEnv.disabledNameSpaces = callerEnv.disabledNameSpaces;
+ return childEnv;
+ }
+
+ private SkylarkEnvironment(SkylarkEnvironment definitionEnv, ImmutableList<String> stackTrace,
+ EventHandler eventHandler) {
+ super(definitionEnv.getGlobalEnvironment());
+ this.stackTrace = stackTrace;
+ this.eventHandler = Preconditions.checkNotNull(eventHandler,
+ "EventHandler cannot be null in an Environment which calls into Skylark");
+ }
+
+ /**
+ * Creates a global SkylarkEnvironment.
+ */
+ public SkylarkEnvironment(EventHandler eventHandler, String astFileContentHashCode) {
+ super();
+ stackTrace = ImmutableList.of();
+ this.eventHandler = eventHandler;
+ this.fileContentHashCode = astFileContentHashCode;
+ }
+
+ @VisibleForTesting
+ public SkylarkEnvironment(EventHandler eventHandler) {
+ this(eventHandler, null);
+ }
+
+ public SkylarkEnvironment(SkylarkEnvironment globalEnv) {
+ super(globalEnv);
+ stackTrace = ImmutableList.of();
+ this.eventHandler = globalEnv.eventHandler;
+ }
+
+ @Override
+ public ImmutableList<String> getStackTrace() {
+ return stackTrace;
+ }
+
+ /**
+ * Clones this Skylark global environment.
+ */
+ public SkylarkEnvironment cloneEnv(EventHandler eventHandler) {
+ Preconditions.checkArgument(isGlobalEnvironment());
+ SkylarkEnvironment newEnv = new SkylarkEnvironment(eventHandler, this.fileContentHashCode);
+ for (Entry<String, Object> entry : env.entrySet()) {
+ newEnv.env.put(entry.getKey(), entry.getValue());
+ }
+ for (Map.Entry<Class<?>, Map<String, Function>> functionMap : functions.entrySet()) {
+ newEnv.functions.put(functionMap.getKey(), functionMap.getValue());
+ }
+ return newEnv;
+ }
+
+ /**
+ * Returns the global environment. Only works for Skylark environments. For the global Skylark
+ * environment this method returns this Environment.
+ */
+ public SkylarkEnvironment getGlobalEnvironment() {
+ // If there's a parent that's the global environment, otherwise this is.
+ return parent != null ? (SkylarkEnvironment) parent : this;
+ }
+
+ /**
+ * Returns true if this is a Skylark global environment.
+ */
+ public boolean isGlobalEnvironment() {
+ return parent == null;
+ }
+
+ /**
+ * Returns true if varname has been read as a global variable.
+ */
+ public boolean hasBeenReadGlobalVariable(String varname) {
+ return readGlobalVariables.contains(varname);
+ }
+
+ @Override
+ public boolean isSkylarkEnabled() {
+ return true;
+ }
+
+ /**
+ * @return the value from the environment whose name is "varname".
+ * @throws NoSuchVariableException if the variable is not defined in the environment.
+ */
+ @Override
+ public Object lookup(String varname) throws NoSuchVariableException {
+ if (disabledVariables.contains(varname)) {
+ throw new NoSuchVariableException(varname);
+ }
+ Object value = env.get(varname);
+ if (value == null) {
+ if (parent != null && parent.hasVariable(varname)) {
+ readGlobalVariables.add(varname);
+ return parent.lookup(varname);
+ }
+ throw new NoSuchVariableException(varname);
+ }
+ return value;
+ }
+
+ /**
+ * Like <code>lookup(String)</code>, but instead of throwing an exception in
+ * the case where "varname" is not defined, "defaultValue" is returned instead.
+ */
+ @Override
+ public Object lookup(String varname, Object defaultValue) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Updates the value of variable "varname" in the environment, corresponding
+ * to an AssignmentStatement.
+ */
+ @Override
+ public void update(String varname, Object value) {
+ Preconditions.checkNotNull(value, "update(value == null)");
+ env.put(varname, value);
+ }
+
+ /**
+ * Returns the class of the variable or null if the variable does not exist. This function
+ * works only in the local Environment, it doesn't check the global Environment.
+ */
+ public Class<?> getVariableType(String varname) {
+ Object variable = env.get(varname);
+ return variable != null ? EvalUtils.getSkylarkType(variable.getClass()) : null;
+ }
+
+ /**
+ * Removes the functions and the modules (i.e. the symbol of the module from the top level
+ * Environment and the functions attached to it) from the Environment which should be present
+ * only during the loading phase.
+ */
+ public void disableOnlyLoadingPhaseObjects() {
+ List<String> objectsToRemove = new ArrayList<>();
+ List<Class<?>> modulesToRemove = new ArrayList<>();
+ for (Map.Entry<String, Object> entry : env.entrySet()) {
+ Object object = entry.getValue();
+ if (object instanceof SkylarkFunction) {
+ if (((SkylarkFunction) object).isOnlyLoadingPhase()) {
+ objectsToRemove.add(entry.getKey());
+ }
+ } else if (object.getClass().isAnnotationPresent(SkylarkModule.class)) {
+ if (object.getClass().getAnnotation(SkylarkModule.class).onlyLoadingPhase()) {
+ objectsToRemove.add(entry.getKey());
+ modulesToRemove.add(entry.getValue().getClass());
+ }
+ }
+ }
+ for (String symbol : objectsToRemove) {
+ disabledVariables.add(symbol);
+ }
+ for (Class<?> moduleClass : modulesToRemove) {
+ disabledNameSpaces.add(moduleClass);
+ }
+ }
+
+ public void handleEvent(Event event) {
+ eventHandler.handle(event);
+ }
+
+ /**
+ * Returns a hash code calculated from the hash code of this Environment and the
+ * transitive closure of other Environments it loads.
+ */
+ public String getTransitiveFileContentHashCode() {
+ Fingerprint fingerprint = new Fingerprint();
+ fingerprint.addString(Preconditions.checkNotNull(fileContentHashCode));
+ // Calculate a new hash from the hash of the loaded Environments.
+ for (SkylarkEnvironment env : importedExtensions.values()) {
+ fingerprint.addString(env.getTransitiveFileContentHashCode());
+ }
+ return fingerprint.hexDigestAndReset();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/SkylarkFunction.java b/src/main/java/com/google/devtools/build/lib/syntax/SkylarkFunction.java
new file mode 100644
index 0000000..bd2cc83
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/SkylarkFunction.java
@@ -0,0 +1,317 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.packages.Type.ConversionException;
+import com.google.devtools.build.lib.syntax.EvalException.EvalExceptionWithJavaCause;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.ExecutionException;
+
+/**
+ * A function class for Skylark built in functions. Supports mandatory and optional arguments.
+ * All usable arguments have to be specified. In case of ambiguous arguments (a parameter is
+ * specified as positional and keyword arguments in the function call) an exception is thrown.
+ */
+public abstract class SkylarkFunction extends AbstractFunction {
+
+ private ImmutableList<String> parameters;
+ private ImmutableMap<String, SkylarkBuiltin.Param> parameterTypes;
+ private int mandatoryParamNum;
+ private boolean configured = false;
+ private Class<?> objectType;
+ private boolean onlyLoadingPhase;
+
+ /**
+ * Creates a SkylarkFunction with the given name.
+ */
+ public SkylarkFunction(String name) {
+ super(name);
+ }
+
+ /**
+ * Configures the parameter of this Skylark function using the annotation.
+ */
+ @VisibleForTesting
+ public void configure(SkylarkBuiltin annotation) {
+ Preconditions.checkState(!configured);
+ Preconditions.checkArgument(getName().equals(annotation.name()),
+ getName() + " != " + annotation.name());
+ mandatoryParamNum = 0;
+ ImmutableList.Builder<String> paramListBuilder = ImmutableList.builder();
+ ImmutableMap.Builder<String, SkylarkBuiltin.Param> paramTypeBuilder = ImmutableMap.builder();
+ for (SkylarkBuiltin.Param param : annotation.mandatoryParams()) {
+ paramListBuilder.add(param.name());
+ paramTypeBuilder.put(param.name(), param);
+ mandatoryParamNum++;
+ }
+ for (SkylarkBuiltin.Param param : annotation.optionalParams()) {
+ paramListBuilder.add(param.name());
+ paramTypeBuilder.put(param.name(), param);
+ }
+ parameters = paramListBuilder.build();
+ parameterTypes = paramTypeBuilder.build();
+ this.objectType = annotation.objectType().equals(Object.class) ? null : annotation.objectType();
+ this.onlyLoadingPhase = annotation.onlyLoadingPhase();
+ configured = true;
+ }
+
+ /**
+ * Returns true if the SkylarkFunction is configured.
+ */
+ public boolean isConfigured() {
+ return configured;
+ }
+
+ @Override
+ public Class<?> getObjectType() {
+ return objectType;
+ }
+
+ public boolean isOnlyLoadingPhase() {
+ return onlyLoadingPhase;
+ }
+
+ @Override
+ public Object call(List<Object> args,
+ Map<String, Object> kwargs,
+ FuncallExpression ast,
+ Environment env)
+ throws EvalException, InterruptedException {
+
+ Preconditions.checkState(configured, "Function " + getName() + " was not configured");
+ try {
+ ImmutableMap.Builder<String, Object> arguments = new ImmutableMap.Builder<>();
+ if (objectType != null && !FuncallExpression.isNamespace(objectType)) {
+ arguments.put("self", args.remove(0));
+ }
+
+ int maxParamNum = parameters.size();
+ int paramNum = args.size() + kwargs.size();
+
+ if (paramNum < mandatoryParamNum) {
+ throw new EvalException(ast.getLocation(),
+ String.format("incorrect number of arguments (got %s, expected at least %s)",
+ paramNum, mandatoryParamNum));
+ } else if (paramNum > maxParamNum) {
+ throw new EvalException(ast.getLocation(),
+ String.format("incorrect number of arguments (got %s, expected at most %s)",
+ paramNum, maxParamNum));
+ }
+
+ for (int i = 0; i < mandatoryParamNum; i++) {
+ Preconditions.checkState(i < args.size() || kwargs.containsKey(parameters.get(i)),
+ String.format("missing mandatory parameter: %s", parameters.get(i)));
+ }
+
+ for (int i = 0; i < args.size(); i++) {
+ checkTypeAndAddArg(parameters.get(i), args.get(i), arguments, ast.getLocation());
+ }
+
+ for (Entry<String, Object> kwarg : kwargs.entrySet()) {
+ int idx = parameters.indexOf(kwarg.getKey());
+ if (idx < 0) {
+ throw new EvalException(ast.getLocation(),
+ String.format("unknown keyword argument: %s", kwarg.getKey()));
+ }
+ if (idx < args.size()) {
+ throw new EvalException(ast.getLocation(),
+ String.format("ambiguous argument: %s", kwarg.getKey()));
+ }
+ checkTypeAndAddArg(kwarg.getKey(), kwarg.getValue(), arguments, ast.getLocation());
+ }
+
+ return call(arguments.build(), ast, env);
+ } catch (ConversionException | IllegalArgumentException | IllegalStateException
+ | ClassCastException | ClassNotFoundException | ExecutionException e) {
+ if (e.getMessage() != null) {
+ throw new EvalException(ast.getLocation(), e.getMessage());
+ } else {
+ // TODO(bazel-team): ideally this shouldn't happen, however we need this for debugging
+ throw new EvalExceptionWithJavaCause(ast.getLocation(), e);
+ }
+ }
+ }
+
+ private void checkTypeAndAddArg(String paramName, Object value,
+ ImmutableMap.Builder<String, Object> arguments, Location loc) throws EvalException {
+ SkylarkBuiltin.Param param = parameterTypes.get(paramName);
+ if (param.callbackEnabled() && Function.class.isAssignableFrom(value.getClass())) {
+ // If we pass a function as an argument we trust the Function implementation with the type
+ // check. It's OK since the function needs to be called manually anyway.
+ arguments.put(paramName, value);
+ return;
+ }
+ if (!(param.type().isAssignableFrom(value.getClass()))) {
+ throw new EvalException(loc, String.format("expected %s for '%s' but got %s instead\n"
+ + "%s.%s: %s",
+ EvalUtils.getDataTypeNameFromClass(param.type()), paramName,
+ EvalUtils.getDatatypeName(value), getName(), paramName, param.doc()));
+ }
+ if (param.type().equals(SkylarkList.class)) {
+ checkGeneric(paramName, param, value, ((SkylarkList) value).getGenericType(), loc);
+ } else if (param.type().equals(SkylarkNestedSet.class)) {
+ checkGeneric(paramName, param, value, ((SkylarkNestedSet) value).getGenericType(), loc);
+ }
+ arguments.put(paramName, value);
+ }
+
+ private void checkGeneric(String paramName, SkylarkBuiltin.Param param, Object value,
+ Class<?> genericType, Location loc) throws EvalException {
+ if (!genericType.equals(Object.class) && !param.generic1().isAssignableFrom(genericType)) {
+ String mainType = EvalUtils.getDataTypeNameFromClass(param.type());
+ throw new EvalException(loc, String.format(
+ "expected %s of %ss for '%s' but got %s of %ss instead\n%s.%s: %s",
+ mainType, EvalUtils.getDataTypeNameFromClass(param.generic1()),
+ paramName,
+ EvalUtils.getDatatypeName(value), EvalUtils.getDataTypeNameFromClass(genericType),
+ getName(), paramName, param.doc()));
+ }
+ }
+
+ /**
+ * The actual function call. All positional and keyword arguments are put in the
+ * arguments map.
+ */
+ protected abstract Object call(
+ Map<String, Object> arguments, FuncallExpression ast, Environment env) throws EvalException,
+ ConversionException,
+ IllegalArgumentException,
+ IllegalStateException,
+ ClassCastException,
+ ClassNotFoundException,
+ ExecutionException;
+
+ /**
+ * An intermediate class to provide a simpler interface for Skylark functions.
+ */
+ public abstract static class SimpleSkylarkFunction extends SkylarkFunction {
+
+ public SimpleSkylarkFunction(String name) {
+ super(name);
+ }
+
+ @Override
+ protected final Object call(
+ Map<String, Object> arguments, FuncallExpression ast, Environment env) throws EvalException,
+ ConversionException,
+ IllegalArgumentException,
+ IllegalStateException,
+ ClassCastException,
+ ExecutionException {
+ return call(arguments, ast.getLocation());
+ }
+
+ /**
+ * The actual function call. All positional and keyword arguments are put in the
+ * arguments map.
+ */
+ protected abstract Object call(Map<String, Object> arguments, Location loc)
+ throws EvalException,
+ ConversionException,
+ IllegalArgumentException,
+ IllegalStateException,
+ ClassCastException,
+ ExecutionException;
+ }
+
+ public static <TYPE> Iterable<TYPE> castList(Object obj, final Class<TYPE> type) {
+ if (obj == null) {
+ return ImmutableList.of();
+ }
+ return ((SkylarkList) obj).to(type);
+ }
+
+ public static <TYPE> Iterable<TYPE> castList(
+ Object obj, final Class<TYPE> type, final String what) throws ConversionException {
+ if (obj == null) {
+ return ImmutableList.of();
+ }
+ return Iterables.transform(Type.LIST.convert(obj, what),
+ new com.google.common.base.Function<Object, TYPE>() {
+ @Override
+ public TYPE apply(Object input) {
+ try {
+ return type.cast(input);
+ } catch (ClassCastException e) {
+ throw new IllegalArgumentException(String.format(
+ "expected %s type for '%s' but got %s instead",
+ EvalUtils.getDataTypeNameFromClass(type), what,
+ EvalUtils.getDatatypeName(input)));
+ }
+ }
+ });
+ }
+
+ public static <KEY_TYPE, VALUE_TYPE> ImmutableMap<KEY_TYPE, VALUE_TYPE> toMap(
+ Iterable<Map.Entry<KEY_TYPE, VALUE_TYPE>> obj) {
+ ImmutableMap.Builder<KEY_TYPE, VALUE_TYPE> builder = ImmutableMap.builder();
+ for (Map.Entry<KEY_TYPE, VALUE_TYPE> entry : obj) {
+ builder.put(entry.getKey(), entry.getValue());
+ }
+ return builder.build();
+ }
+
+ public static <KEY_TYPE, VALUE_TYPE> Iterable<Map.Entry<KEY_TYPE, VALUE_TYPE>> castMap(Object obj,
+ final Class<KEY_TYPE> keyType, final Class<VALUE_TYPE> valueType, final String what) {
+ if (obj == null) {
+ return ImmutableList.of();
+ }
+ if (!(obj instanceof Map<?, ?>)) {
+ throw new IllegalArgumentException(String.format(
+ "expected a dictionary for %s but got %s instead",
+ what, EvalUtils.getDatatypeName(obj)));
+ }
+ return Iterables.transform(((Map<?, ?>) obj).entrySet(),
+ new com.google.common.base.Function<Map.Entry<?, ?>, Map.Entry<KEY_TYPE, VALUE_TYPE>>() {
+ // This is safe. We check the type of the key-value pairs for every entry in the Map.
+ // In Map.Entry the key always has the type of the first generic parameter, the
+ // value has the second.
+ @SuppressWarnings("unchecked")
+ @Override
+ public Map.Entry<KEY_TYPE, VALUE_TYPE> apply(Map.Entry<?, ?> input) {
+ if (keyType.isAssignableFrom(input.getKey().getClass())
+ && valueType.isAssignableFrom(input.getValue().getClass())) {
+ return (Map.Entry<KEY_TYPE, VALUE_TYPE>) input;
+ }
+ throw new IllegalArgumentException(String.format(
+ "expected <%s, %s> type for '%s' but got <%s, %s> instead",
+ keyType.getSimpleName(), valueType.getSimpleName(), what,
+ EvalUtils.getDatatypeName(input.getKey()),
+ EvalUtils.getDatatypeName(input.getValue())));
+ }
+ });
+ }
+
+ // TODO(bazel-team): this is only used in SkylarkRuleConfgiuredTargetBuilder, fix typing for
+ // structs then remove this.
+ public static <TYPE> TYPE cast(Object elem, Class<TYPE> type, String what, Location loc)
+ throws EvalException {
+ try {
+ return type.cast(elem);
+ } catch (ClassCastException e) {
+ throw new EvalException(loc, String.format("expected %s for '%s' but got %s instead",
+ type.getSimpleName(), what, EvalUtils.getDatatypeName(elem)));
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/SkylarkList.java b/src/main/java/com/google/devtools/build/lib/syntax/SkylarkList.java
new file mode 100644
index 0000000..ef9fe10
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/SkylarkList.java
@@ -0,0 +1,373 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.devtools.build.lib.events.Location;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * A class to handle lists and tuples in Skylark.
+ */
+@SkylarkModule(name = "list",
+ doc = "A language built-in type to support lists. Example of list literal:<br>"
+ + "<pre class=language-python>l = [1, 2, 3]</pre>"
+ + "Accessing elements is possible using indexing (starts from <code>0</code>):<br>"
+ + "<pre class=language-python>e = l[1] # e == 2</pre>"
+ + "Lists support the <code>+</code> operator to concatenate two lists. Example:<br>"
+ + "<pre class=language-python>l = [1, 2] + [3, 4] # l == [1, 2, 3, 4]\n"
+ + "l = [\"a\", \"b\"]\n"
+ + "l += [\"c\"] # l == [\"a\", \"b\", \"c\"]</pre>"
+ + "List elements have to be of the same type, <code>[1, 2, \"c\"]</code> results in an "
+ + "error. Lists - just like everything - are immutable, therefore <code>l[1] = \"a\""
+ + "</code> is not supported.")
+public abstract class SkylarkList implements Iterable<Object> {
+
+ private final boolean tuple;
+ private final Class<?> genericType;
+
+ private SkylarkList(boolean tuple, Class<?> genericType) {
+ this.tuple = tuple;
+ this.genericType = genericType;
+ }
+
+ /**
+ * The size of the list.
+ */
+ public abstract int size();
+
+ /**
+ * Returns true if the list is empty.
+ */
+ public abstract boolean isEmpty();
+
+ /**
+ * Returns the i-th element of the list.
+ */
+ public abstract Object get(int i);
+
+ /**
+ * Returns true if this list is a tuple.
+ */
+ public boolean isTuple() {
+ return tuple;
+ }
+
+ @VisibleForTesting
+ public Class<?> getGenericType() {
+ return genericType;
+ }
+
+ @Override
+ public String toString() {
+ return toList().toString();
+ }
+
+ // TODO(bazel-team): we should be very careful using this method. Check and remove
+ // auto conversions on the Java-Skylark interface if possible.
+ /**
+ * Converts this Skylark list to a Java list.
+ */
+ public abstract List<?> toList();
+
+ @SuppressWarnings("unchecked")
+ public <T> Iterable<T> to(Class<T> type) {
+ Preconditions.checkArgument(this == EMPTY_LIST || type.isAssignableFrom(genericType));
+ return (Iterable<T>) this;
+ }
+
+ private static final class EmptySkylarkList extends SkylarkList {
+ private EmptySkylarkList(boolean tuple) {
+ super(tuple, Object.class);
+ }
+
+ @Override
+ public Iterator<Object> iterator() {
+ return ImmutableList.of().iterator();
+ }
+
+ @Override
+ public int size() {
+ return 0;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return true;
+ }
+
+ @Override
+ public Object get(int i) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public List<?> toList() {
+ return isTuple() ? ImmutableList.of() : Lists.newArrayList();
+ }
+
+ @Override
+ public String toString() {
+ return "[]";
+ }
+ }
+
+ /**
+ * An empty Skylark list.
+ */
+ public static final SkylarkList EMPTY_LIST = new EmptySkylarkList(false);
+
+ private static final class SimpleSkylarkList extends SkylarkList {
+ private final ImmutableList<Object> list;
+
+ private SimpleSkylarkList(ImmutableList<Object> list, boolean tuple, Class<?> genericType) {
+ super(tuple, genericType);
+ this.list = Preconditions.checkNotNull(list);
+ }
+
+ @Override
+ public Iterator<Object> iterator() {
+ return list.iterator();
+ }
+
+ @Override
+ public int size() {
+ return list.size();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return list.isEmpty();
+ }
+
+ @Override
+ public Object get(int i) {
+ return list.get(i);
+ }
+
+ @Override
+ public List<?> toList() {
+ return isTuple() ? list : Lists.newArrayList(list);
+ }
+
+ @Override
+ public String toString() {
+ return list.toString();
+ }
+
+ @Override
+ public int hashCode() {
+ return list.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof SimpleSkylarkList)) {
+ return false;
+ }
+ SimpleSkylarkList other = (SimpleSkylarkList) obj;
+ return other.list.equals(this.list);
+ }
+ }
+
+ /**
+ * A Skylark list to support lazy iteration (i.e. we only call iterator on the object this
+ * list masks when it's absolutely necessary). This is useful if iteration is expensive
+ * (e.g. NestedSet-s). Size(), get() and isEmpty() are expensive operations but
+ * concatenation is quick.
+ */
+ private static final class LazySkylarkList extends SkylarkList {
+ private final Iterable<Object> iterable;
+ private ImmutableList<Object> list = null;
+
+ private LazySkylarkList(Iterable<Object> iterable, boolean tuple, Class<?> genericType) {
+ super(tuple, genericType);
+ this.iterable = Preconditions.checkNotNull(iterable);
+ }
+
+ @Override
+ public Iterator<Object> iterator() {
+ return iterable.iterator();
+ }
+
+ @Override
+ public int size() {
+ return Iterables.size(iterable);
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return Iterables.isEmpty(iterable);
+ }
+
+ @Override
+ public Object get(int i) {
+ return getList().get(i);
+ }
+
+ @Override
+ public List<?> toList() {
+ return getList();
+ }
+
+ private ImmutableList<Object> getList() {
+ if (list == null) {
+ list = ImmutableList.copyOf(iterable);
+ }
+ return list;
+ }
+ }
+
+ /**
+ * A Skylark list to support quick concatenation of lists. Concatenation is O(1),
+ * size(), isEmpty() is O(n), get() is O(h).
+ */
+ private static final class ConcatenatedSkylarkList extends SkylarkList {
+ private final SkylarkList left;
+ private final SkylarkList right;
+
+ private ConcatenatedSkylarkList(
+ SkylarkList left, SkylarkList right, boolean tuple, Class<?> genericType) {
+ super(tuple, genericType);
+ this.left = Preconditions.checkNotNull(left);
+ this.right = Preconditions.checkNotNull(right);
+ }
+
+ @Override
+ public Iterator<Object> iterator() {
+ return Iterables.concat(left, right).iterator();
+ }
+
+ @Override
+ public int size() {
+ // We shouldn't evaluate the size function until it's necessary, because it can be expensive
+ // for lazy lists (e.g. lists containing a NestedSet).
+ // TODO(bazel-team): make this class more clever to store the size and empty parameters
+ // for every non-LazySkylarkList member.
+ return left.size() + right.size();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return left.isEmpty() && right.isEmpty();
+ }
+
+ @Override
+ public Object get(int i) {
+ int leftSize = left.size();
+ if (i < leftSize) {
+ return left.get(i);
+ } else {
+ return right.get(i - leftSize);
+ }
+ }
+
+ @Override
+ public List<?> toList() {
+ return ImmutableList.<Object>builder().addAll(left).addAll(right).build();
+ }
+ }
+
+ /**
+ * Returns a Skylark list containing elements without a type check. Only use if all elements
+ * are of the same type.
+ */
+ public static SkylarkList list(Collection<?> elements, Class<?> genericType) {
+ if (elements.isEmpty()) {
+ return EMPTY_LIST;
+ }
+ return new SimpleSkylarkList(ImmutableList.copyOf(elements), false, genericType);
+ }
+
+ /**
+ * Returns a Skylark list containing elements without a type check and without creating
+ * an immutable copy. Therefore the iterable containing elements must be immutable
+ * (which is not checked here so callers must be extra careful). This way
+ * it's possibly to create a SkylarkList without requesting the original iterator. This
+ * can be useful for nested set - list conversions.
+ */
+ @SuppressWarnings("unchecked")
+ public static SkylarkList lazyList(Iterable<?> elements, Class<?> genericType) {
+ return new LazySkylarkList((Iterable<Object>) elements, false, genericType);
+ }
+
+ /**
+ * Returns a Skylark list containing elements. Performs type check and throws an exception
+ * in case the list contains elements of different type.
+ */
+ public static SkylarkList list(Collection<?> elements, Location loc) throws EvalException {
+ if (elements.isEmpty()) {
+ return EMPTY_LIST;
+ }
+ return new SimpleSkylarkList(
+ ImmutableList.copyOf(elements), false, getGenericType(elements, loc));
+ }
+
+ private static Class<?> getGenericType(Collection<?> elements, Location loc)
+ throws EvalException {
+ Class<?> genericType = elements.iterator().next().getClass();
+ for (Object element : elements) {
+ Class<?> type = element.getClass();
+ if (!EvalUtils.getSkylarkType(genericType).equals(EvalUtils.getSkylarkType(type))) {
+ throw new EvalException(loc, String.format(
+ "Incompatible types in list: found a %s but the first element is a %s",
+ EvalUtils.getDataTypeNameFromClass(type),
+ EvalUtils.getDataTypeNameFromClass(genericType)));
+ }
+ }
+ return genericType;
+ }
+
+ /**
+ * Returns a Skylark list created from Skylark lists left and right. Throws an exception
+ * if they are not of the same generic type.
+ */
+ public static SkylarkList concat(SkylarkList left, SkylarkList right, Location loc)
+ throws EvalException {
+ if (left.isTuple() != right.isTuple()) {
+ throw new EvalException(loc, "cannot concatenate lists and tuples");
+ }
+ if (left == EMPTY_LIST) {
+ return right;
+ }
+ if (right == EMPTY_LIST) {
+ return left;
+ }
+ if (!left.genericType.equals(right.genericType)) {
+ throw new EvalException(loc, String.format("cannot concatenate list of %s with list of %s",
+ EvalUtils.getDataTypeNameFromClass(left.genericType),
+ EvalUtils.getDataTypeNameFromClass(right.genericType)));
+ }
+ return new ConcatenatedSkylarkList(left, right, left.isTuple(), left.genericType);
+ }
+
+ /**
+ * Returns a Skylark tuple containing elements.
+ */
+ public static SkylarkList tuple(List<?> elements) {
+ // Tuple elements do not have to have the same type.
+ return new SimpleSkylarkList(ImmutableList.copyOf(elements), true, Object.class);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/SkylarkModule.java b/src/main/java/com/google/devtools/build/lib/syntax/SkylarkModule.java
new file mode 100644
index 0000000..96421b2
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/SkylarkModule.java
@@ -0,0 +1,38 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * An annotation to mark Skylark modules or Skylark accessible Java data types.
+ * A Skylark modules always corresponds to exactly one Java class.
+ */
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface SkylarkModule {
+
+ String name();
+
+ String doc();
+
+ boolean hidden() default false;
+
+ boolean namespace() default false;
+
+ boolean onlyLoadingPhase() default false;
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/SkylarkNestedSet.java b/src/main/java/com/google/devtools/build/lib/syntax/SkylarkNestedSet.java
new file mode 100644
index 0000000..17fc55f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/SkylarkNestedSet.java
@@ -0,0 +1,193 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.events.Location;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nullable;
+
+/**
+ * A generic type safe NestedSet wrapper for Skylark.
+ */
+@SkylarkModule(name = "set",
+ doc = "A language built-in type to supports (nested) sets. "
+ + "Sets can be created using the global <code>set</code> function, and they "
+ + "support the <code>+</code> operator to extends and nest sets. Examples:<br>"
+ + "<pre class=language-python>s = set([1, 2])\n"
+ + "s += [3] # s == {1, 2, 3}\n"
+ + "s += set([4, 5]) # s == {1, 2, 3, {4, 5}}</pre>"
+ + "Note that in these examples <code>{..}</code> is not a valid literal to create sets. "
+ + "Sets have a fixed generic type, so <code>set([1]) + [\"a\"]</code> or "
+ + "<code>set([1]) + set([\"a\"])</code> results in an error.")
+@Immutable
+public final class SkylarkNestedSet implements Iterable<Object> {
+
+ private final Class<?> genericType;
+ @Nullable private final List<Object> items;
+ @Nullable private final List<NestedSet<Object>> transitiveItems;
+ private final NestedSet<?> set;
+
+ public SkylarkNestedSet(Order order, Object item, Location loc) throws EvalException {
+ this(order, Object.class, item, loc, new ArrayList<Object>(),
+ new ArrayList<NestedSet<Object>>());
+ }
+
+ public SkylarkNestedSet(SkylarkNestedSet left, Object right, Location loc) throws EvalException {
+ this(left.set.getOrder(), left.genericType, right, loc,
+ new ArrayList<Object>(checkItems(left.items, loc)),
+ new ArrayList<NestedSet<Object>>(checkItems(left.transitiveItems, loc)));
+ }
+
+ private static <T> T checkItems(T items, Location loc) throws EvalException {
+ // SkylarkNestedSets created directly from ordinary NestedSets (those were created in a
+ // native rule) don't have directly accessible items and transitiveItems, so we cannot
+ // add more elements to them.
+ if (items == null) {
+ throw new EvalException(loc, "Cannot add more elements to this set. Sets created in "
+ + "native rules cannot be left side operands of the + operator.");
+ }
+ return items;
+ }
+
+ // This is safe because of the type checking
+ @SuppressWarnings("unchecked")
+ private SkylarkNestedSet(Order order, Class<?> genericType, Object item, Location loc,
+ List<Object> items, List<NestedSet<Object>> transitiveItems) throws EvalException {
+
+ // Adding the item
+ if (item instanceof SkylarkNestedSet) {
+ SkylarkNestedSet nestedSet = (SkylarkNestedSet) item;
+ if (!nestedSet.isEmpty()) {
+ genericType = checkType(genericType, nestedSet.genericType, loc);
+ transitiveItems.add((NestedSet<Object>) nestedSet.set);
+ }
+ } else if (item instanceof SkylarkList) {
+ // TODO(bazel-team): we should check ImmutableList here but it screws up genrule at line 43
+ for (Object object : (SkylarkList) item) {
+ genericType = checkType(genericType, object.getClass(), loc);
+ items.add(object);
+ }
+ } else {
+ throw new EvalException(loc,
+ String.format("cannot add '%s'-s to nested sets", EvalUtils.getDatatypeName(item)));
+ }
+ this.genericType = Preconditions.checkNotNull(genericType, "type cannot be null");
+
+ // Initializing the real nested set
+ NestedSetBuilder<Object> builder = new NestedSetBuilder<Object>(order);
+ builder.addAll(items);
+ try {
+ for (NestedSet<Object> nestedSet : transitiveItems) {
+ builder.addTransitive(nestedSet);
+ }
+ } catch (IllegalStateException e) {
+ throw new EvalException(loc, e.getMessage());
+ }
+ this.set = builder.build();
+ this.items = ImmutableList.copyOf(items);
+ this.transitiveItems = ImmutableList.copyOf(transitiveItems);
+ }
+
+ /**
+ * Returns a type safe SkylarkNestedSet. Use this instead of the constructor if possible.
+ */
+ public static <T> SkylarkNestedSet of(Class<T> genericType, NestedSet<T> set) {
+ return new SkylarkNestedSet(genericType, set);
+ }
+
+ /**
+ * A not type safe constructor for SkylarkNestedSet. It's discouraged to use it unless type
+ * generic safety is guaranteed from the caller side.
+ */
+ SkylarkNestedSet(Class<?> genericType, NestedSet<?> set) {
+ // This is here for the sake of FuncallExpression.
+ this.genericType = Preconditions.checkNotNull(genericType, "type cannot be null");
+ this.set = Preconditions.checkNotNull(set, "set cannot be null");
+ this.items = null;
+ this.transitiveItems = null;
+ }
+
+ private static Class<?> checkType(Class<?> builderType, Class<?> itemType, Location loc)
+ throws EvalException {
+ if (Map.class.isAssignableFrom(itemType) || SkylarkList.class.isAssignableFrom(itemType)
+ || ClassObject.class.isAssignableFrom(itemType)) {
+ throw new EvalException(loc, String.format("nested set item is composite (type of %s)",
+ EvalUtils.getDataTypeNameFromClass(itemType)));
+ }
+ if (!EvalUtils.isSkylarkImmutable(itemType)) {
+ throw new EvalException(loc, String.format("nested set item is not immutable (type of %s)",
+ EvalUtils.getDataTypeNameFromClass(itemType)));
+ }
+ if (builderType.equals(Object.class)) {
+ return itemType;
+ }
+ if (!EvalUtils.getSkylarkType(builderType).equals(EvalUtils.getSkylarkType(itemType))) {
+ throw new EvalException(loc, String.format(
+ "nested set item is type of %s but the nested set accepts only %s-s",
+ EvalUtils.getDataTypeNameFromClass(itemType),
+ EvalUtils.getDataTypeNameFromClass(builderType)));
+ }
+ return builderType;
+ }
+
+ /**
+ * Returns the NestedSet embedded in this SkylarkNestedSet if it is of the parameter type.
+ */
+ // The precondition ensures generic type safety
+ @SuppressWarnings("unchecked")
+ public <T> NestedSet<T> getSet(Class<T> type) {
+ // Empty sets don't need have to have a type since they don't have items
+ if (set.isEmpty()) {
+ return (NestedSet<T>) set;
+ }
+ Preconditions.checkArgument(type.isAssignableFrom(genericType),
+ String.format("Expected %s as a type but got %s",
+ EvalUtils.getDataTypeNameFromClass(type),
+ EvalUtils.getDataTypeNameFromClass(genericType)));
+ return (NestedSet<T>) set;
+ }
+
+ // For some reason this cast is unsafe in Java
+ @SuppressWarnings("unchecked")
+ @Override
+ public Iterator<Object> iterator() {
+ return (Iterator<Object>) set.iterator();
+ }
+
+ public Collection<Object> toCollection() {
+ return ImmutableList.copyOf(set.toCollection());
+ }
+
+ public boolean isEmpty() {
+ return set.isEmpty();
+ }
+
+ @VisibleForTesting
+ public Class<?> getGenericType() {
+ return genericType;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/SkylarkType.java b/src/main/java/com/google/devtools/build/lib/syntax/SkylarkType.java
new file mode 100644
index 0000000..04c345f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/SkylarkType.java
@@ -0,0 +1,307 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.events.Location;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.lang.reflect.WildcardType;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+/**
+ * A class representing types available in Skylark.
+ */
+public class SkylarkType {
+
+ private static final class Global {}
+
+ public static final SkylarkType UNKNOWN = new SkylarkType(Object.class);
+ public static final SkylarkType NONE = new SkylarkType(Environment.NoneType.class);
+ public static final SkylarkType GLOBAL = new SkylarkType(Global.class);
+
+ public static final SkylarkType STRING = new SkylarkType(String.class);
+ public static final SkylarkType INT = new SkylarkType(Integer.class);
+ public static final SkylarkType BOOL = new SkylarkType(Boolean.class);
+
+ private final Class<?> type;
+
+ // TODO(bazel-team): Change this to SkylarkType and check generics of generics etc.
+ // Object.class is used for UNKNOWN.
+ private Class<?> generic1;
+
+ public static SkylarkType of(Class<?> type, Class<?> generic1) {
+ return new SkylarkType(type, generic1);
+ }
+
+ public static SkylarkType of(Class<?> type) {
+ if (type.equals(Object.class)) {
+ return SkylarkType.UNKNOWN;
+ } else if (type.equals(String.class)) {
+ return SkylarkType.STRING;
+ } else if (type.equals(Integer.class)) {
+ return SkylarkType.INT;
+ } else if (type.equals(Boolean.class)) {
+ return SkylarkType.BOOL;
+ }
+ return new SkylarkType(type);
+ }
+
+ private SkylarkType(Class<?> type, Class<?> generic1) {
+ this.type = Preconditions.checkNotNull(type);
+ this.generic1 = Preconditions.checkNotNull(generic1);
+ }
+
+ private SkylarkType(Class<?> type) {
+ this.type = Preconditions.checkNotNull(type);
+ this.generic1 = Object.class;
+ }
+
+ public Class<?> getType() {
+ return type;
+ }
+
+ Class<?> getGenericType1() {
+ return generic1;
+ }
+
+ /**
+ * Returns the stronger type of this and o if they are compatible. Stronger means that
+ * the more information is available, e.g. STRING is stronger than UNKNOWN and
+ * LIST<STRING> is stronger than LIST<UNKNOWN>. Note than there's no type
+ * hierarchy in Skylark.
+ * <p>If they are not compatible an EvalException is thrown.
+ */
+ SkylarkType infer(SkylarkType o, String name, Location thisLoc, Location originalLoc)
+ throws EvalException {
+ if (this == o) {
+ return this;
+ }
+ if (this == UNKNOWN || this.equals(SkylarkType.NONE)) {
+ return o;
+ }
+ if (o == UNKNOWN || o.equals(SkylarkType.NONE)) {
+ return this;
+ }
+ if (!type.equals(o.type)) {
+ throw new EvalException(thisLoc, String.format("bad %s: %s is incompatible with %s at %s",
+ name,
+ EvalUtils.getDataTypeNameFromClass(o.getType()),
+ EvalUtils.getDataTypeNameFromClass(this.getType()),
+ originalLoc));
+ }
+ if (generic1.equals(Object.class)) {
+ return o;
+ }
+ if (o.generic1.equals(Object.class)) {
+ return this;
+ }
+ if (!generic1.equals(o.generic1)) {
+ throw new EvalException(thisLoc, String.format("bad %s: incompatible generic variable types "
+ + "%s with %s",
+ name,
+ EvalUtils.getDataTypeNameFromClass(o.generic1),
+ EvalUtils.getDataTypeNameFromClass(this.generic1)));
+ }
+ return this;
+ }
+
+ boolean isStruct() {
+ return type.equals(ClassObject.class);
+ }
+
+ boolean isList() {
+ return SkylarkList.class.isAssignableFrom(type);
+ }
+
+ boolean isDict() {
+ return Map.class.isAssignableFrom(type);
+ }
+
+ boolean isSet() {
+ return Set.class.isAssignableFrom(type);
+ }
+
+ boolean isNset() {
+ // TODO(bazel-team): NestedSets are going to be a bit strange with 2 type info (validation
+ // and execution time). That can be cleaned up once we have complete type inference.
+ return SkylarkNestedSet.class.isAssignableFrom(type);
+ }
+
+ boolean isSimple() {
+ return !isStruct() && !isDict() && !isList() && !isNset() && !isSet();
+ }
+
+ @Override
+ public String toString() {
+ return this == UNKNOWN ? "Unknown" : EvalUtils.getDataTypeNameFromClass(type);
+ }
+
+ // hashCode() and equals() only uses the type field
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof SkylarkType)) {
+ return false;
+ }
+ SkylarkType o = (SkylarkType) other;
+ return this.type.equals(o.type);
+ }
+
+ @Override
+ public int hashCode() {
+ return type.hashCode();
+ }
+
+ /**
+ * A class representing the type of a Skylark function.
+ */
+ public static final class SkylarkFunctionType extends SkylarkType {
+
+ private final String name;
+ @Nullable private SkylarkType returnType;
+ @Nullable private Location returnTypeLoc;
+
+ public static SkylarkFunctionType of(String name) {
+ return new SkylarkFunctionType(name, null);
+ }
+
+ public static SkylarkFunctionType of(String name, SkylarkType returnType) {
+ return new SkylarkFunctionType(name, returnType);
+ }
+
+ private SkylarkFunctionType(String name, SkylarkType returnType) {
+ super(Function.class);
+ this.name = name;
+ this.returnType = returnType;
+ }
+
+ public SkylarkType getReturnType() {
+ return returnType;
+ }
+
+ /**
+ * Sets the return type of the function type if it's compatible with the existing return type.
+ * Note that setting NONE only has an effect if the return type hasn't been set previously.
+ */
+ public void setReturnType(SkylarkType newReturnType, Location newLoc) throws EvalException {
+ if (returnType == null) {
+ returnType = newReturnType;
+ returnTypeLoc = newLoc;
+ } else if (newReturnType != SkylarkType.NONE) {
+ returnType =
+ returnType.infer(newReturnType, "return type of " + name, newLoc, returnTypeLoc);
+ if (returnType == newReturnType) {
+ returnTypeLoc = newLoc;
+ }
+ }
+ }
+ }
+
+ private static boolean isTypeAllowedInSkylark(Object object) {
+ if (object instanceof NestedSet<?>) {
+ return false;
+ } else if (object instanceof List<?>) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Throws EvalException if the type of the object is not allowed to be present in Skylark.
+ */
+ static void checkTypeAllowedInSkylark(Object object, Location loc) throws EvalException {
+ if (!isTypeAllowedInSkylark(object)) {
+ throw new EvalException(loc,
+ "Type is not allowed in Skylark: "
+ + object.getClass().getSimpleName());
+ }
+ }
+
+ private static Class<?> getGenericTypeFromMethod(Method method) {
+ // This is where we can infer generic type information, so SkylarkNestedSets can be
+ // created in a safe way. Eventually we should probably do something with Lists and Maps too.
+ ParameterizedType t = (ParameterizedType) method.getGenericReturnType();
+ Type type = t.getActualTypeArguments()[0];
+ if (type instanceof Class) {
+ return (Class<?>) type;
+ }
+ if (type instanceof WildcardType) {
+ WildcardType wildcard = (WildcardType) type;
+ Type upperBound = wildcard.getUpperBounds()[0];
+ if (upperBound instanceof Class) {
+ // i.e. List<? extends SuperClass>
+ return (Class<?>) upperBound;
+ }
+ }
+ // It means someone annotated a method with @SkylarkCallable with no specific generic type info.
+ // We shouldn't annotate methods which return List<?> or List<T>.
+ throw new IllegalStateException("Cannot infer type from method signature " + method);
+ }
+
+ /**
+ * Converts an object retrieved from a Java method to a Skylark-compatible type.
+ */
+ static Object convertToSkylark(Object object, Method method) {
+ if (object instanceof NestedSet<?>) {
+ return new SkylarkNestedSet(getGenericTypeFromMethod(method), (NestedSet<?>) object);
+ } else if (object instanceof List<?>) {
+ return SkylarkList.list((List<?>) object, getGenericTypeFromMethod(method));
+ }
+ return object;
+ }
+
+ /**
+ * Converts an object to a Skylark-compatible type if possible.
+ */
+ public static Object convertToSkylark(Object object, Location loc) throws EvalException {
+ if (object instanceof List<?>) {
+ return SkylarkList.list((List<?>) object, loc);
+ }
+ return object;
+ }
+
+ /**
+ * Converts object from a Skylark-compatible wrapper type to its original type.
+ */
+ public static Object convertFromSkylark(Object value) {
+ if (value instanceof SkylarkList) {
+ return ((SkylarkList) value).toList();
+ }
+ return value;
+ }
+
+ /**
+ * Creates a SkylarkType from the SkylarkBuiltin annotation.
+ */
+ public static SkylarkType getReturnType(SkylarkBuiltin annotation) {
+ if (annotation.returnType().equals(Object.class)) {
+ return SkylarkType.UNKNOWN;
+ }
+ if (Function.class.isAssignableFrom(annotation.returnType())) {
+ return SkylarkFunctionType.of(annotation.name());
+ }
+ return SkylarkType.of(annotation.returnType());
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/Statement.java b/src/main/java/com/google/devtools/build/lib/syntax/Statement.java
new file mode 100644
index 0000000..ca89b1a
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/Statement.java
@@ -0,0 +1,44 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+/**
+ * Base class for all statements nodes in the AST.
+ */
+public abstract class Statement extends ASTNode {
+
+ /**
+ * Executes the statement in the specified build environment, which may be
+ * modified.
+ *
+ * @throws EvalException if execution of the statement could not be completed.
+ */
+ abstract void exec(Environment env) throws EvalException, InterruptedException;
+
+ /**
+ * Checks the semantics of the Statement using the SkylarkEnvironment according to
+ * the rules of the Skylark language. The SkylarkEnvironment can be used e.g. to check
+ * variable type collision, read only variables, detecting recursion, existence of
+ * built-in variables, functions, etc.
+ *
+ * <p>The semantical check should be performed after the Skylark extension is loaded
+ * (i.e. is syntactically correct) and before is executed. The point of the semantical check
+ * is to make sure (as much as possible) that no error can occur during execution (Skylark
+ * programmers get a "compile time" error). It should also check execution branches (e.g. in
+ * if statements) that otherwise might never get executed.
+ *
+ * @throws EvalException if the Statement has a semantical error.
+ */
+ abstract void validate(ValidationEnvironment env) throws EvalException;
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/StringLiteral.java b/src/main/java/com/google/devtools/build/lib/syntax/StringLiteral.java
new file mode 100644
index 0000000..98d5045
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/StringLiteral.java
@@ -0,0 +1,56 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+/**
+ * Syntax node for a string literal.
+ */
+public final class StringLiteral extends Literal<String> {
+
+ private final char quoteChar;
+
+ public StringLiteral(String value, char quoteChar) {
+ super(value);
+ this.quoteChar = quoteChar;
+ }
+
+ @Override
+ public String toString() {
+ return new StringBuilder()
+ .append(quoteChar)
+ .append(value.replace(Character.toString(quoteChar), "\\" + quoteChar))
+ .append(quoteChar)
+ .toString();
+ }
+
+ @Override
+ public void accept(SyntaxTreeVisitor visitor) {
+ visitor.visit(this);
+ }
+
+ /**
+ * Gets the quote character that was used for this string. For example, if
+ * the string was 'hello, world!', then this method returns '\''.
+ *
+ * @return the character used to quote the string.
+ */
+ public char getQuoteChar() {
+ return quoteChar;
+ }
+
+ @Override
+ SkylarkType validate(ValidationEnvironment env) throws EvalException {
+ return SkylarkType.STRING;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/SyntaxTreeVisitor.java b/src/main/java/com/google/devtools/build/lib/syntax/SyntaxTreeVisitor.java
new file mode 100644
index 0000000..5a95026
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/SyntaxTreeVisitor.java
@@ -0,0 +1,145 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+import com.google.devtools.build.lib.syntax.DictionaryLiteral.DictionaryEntryLiteral;
+import com.google.devtools.build.lib.syntax.IfStatement.ConditionalStatements;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A visitor for visiting the nodes in the syntax tree left to right, top to
+ * bottom.
+ */
+public class SyntaxTreeVisitor {
+
+ public void visit(ASTNode node) {
+ // dispatch to the node specific method
+ node.accept(this);
+ }
+
+ public void visitAll(List<? extends ASTNode> nodes) {
+ for (ASTNode node : nodes) {
+ visit(node);
+ }
+ }
+
+ // node specific visit methods
+ public void visit(Argument node) {
+ if (node.isNamed()) {
+ visit(node.getName());
+ }
+ if (node.hasValue()) {
+ visit(node.getValue());
+ }
+ }
+
+ public void visit(BuildFileAST node) {
+ visitAll(node.getStatements());
+ visitAll(node.getComments());
+ }
+
+ public void visit(BinaryOperatorExpression node) {
+ visit(node.getLhs());
+ visit(node.getRhs());
+ }
+
+ public void visit(FuncallExpression node) {
+ visit(node.getFunction());
+ visitAll(node.getArguments());
+ }
+
+ public void visit(Ident node) {
+ }
+
+ public void visit(ListComprehension node) {
+ visit(node.getElementExpression());
+ for (Map.Entry<Ident, Expression> list : node.getLists()) {
+ visit(list.getKey());
+ visit(list.getValue());
+ }
+ }
+
+ public void accept(DictComprehension node) {
+ visit(node.getKeyExpression());
+ visit(node.getValueExpression());
+ visit(node.getLoopVar());
+ visit(node.getListExpression());
+ }
+
+ public void visit(ListLiteral node) {
+ visitAll(node.getElements());
+ }
+
+ public void visit(IntegerLiteral node) {
+ }
+
+ public void visit(StringLiteral node) {
+ }
+
+ public void visit(AssignmentStatement node) {
+ visit(node.getLValue());
+ visit(node.getExpression());
+ }
+
+ public void visit(ExpressionStatement node) {
+ visit(node.getExpression());
+ }
+
+ public void visit(IfStatement node) {
+ for (ConditionalStatements stmt : node.getThenBlocks()) {
+ visit(stmt);
+ }
+ for (Statement stmt : node.getElseBlock()) {
+ visit(stmt);
+ }
+ }
+
+ public void visit(ConditionalStatements node) {
+ visit(node.getCondition());
+ for (Statement stmt : node.getStmts()) {
+ visit(stmt);
+ }
+ }
+
+ public void visit(FunctionDefStatement node) {
+ visit(node.getIdent());
+ for (Argument arg : node.getArgs()) {
+ visit(arg);
+ }
+ for (Statement stmt : node.getStatements()) {
+ visit(stmt);
+ }
+ }
+
+ public void visit(DictionaryLiteral node) {
+ for (DictionaryEntryLiteral entry : node.getEntries()) {
+ visit(entry);
+ }
+ }
+
+ public void visit(DictionaryEntryLiteral node) {
+ visit(node.getKey());
+ visit(node.getValue());
+ }
+
+ public void visit(NotExpression node) {
+ visit(node.getExpression());
+ }
+
+ public void visit(Comment node) {
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/Token.java b/src/main/java/com/google/devtools/build/lib/syntax/Token.java
new file mode 100644
index 0000000..e3bcfec
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/Token.java
@@ -0,0 +1,50 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+/**
+ * A Token represents an actual lexeme; that is, a lexical unit, its location in
+ * the input text, its lexical kind (TokenKind) and any associated value.
+ */
+public class Token {
+
+ public final TokenKind kind;
+ public final int left;
+ public final int right;
+ public final Object value;
+
+ public Token(TokenKind kind, int left, int right) {
+ this(kind, left, right, null);
+ }
+
+ public Token(TokenKind kind, int left, int right, Object value) {
+ this.kind = kind;
+ this.left = left;
+ this.right = right;
+ this.value = value;
+ }
+
+ /**
+ * Constructs an easy-to-read string representation of token, suitable for use
+ * in user error messages.
+ */
+ @Override
+ public String toString() {
+ // TODO(bazel-team): do proper escaping of string literals
+ return kind == TokenKind.STRING ? ("\"" + value + "\"")
+ : value == null ? kind.getPrettyName()
+ : value.toString();
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/TokenKind.java b/src/main/java/com/google/devtools/build/lib/syntax/TokenKind.java
new file mode 100644
index 0000000..f6dad9f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/TokenKind.java
@@ -0,0 +1,83 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+/**
+ * A TokenKind is an enumeration of each different kind of lexical symbol.
+ */
+public enum TokenKind {
+
+ AND("and"),
+ AS("as"),
+ CLASS("class"),
+ COLON(":"),
+ COMMA(","),
+ COMMENT("comment"),
+ DEF("def"),
+ DOT("."),
+ ELIF("elif"),
+ ELSE("else"),
+ EOF("EOF"),
+ EQUALS("="),
+ EQUALS_EQUALS("=="),
+ EXCEPT("except"),
+ FINALLY("finally"),
+ FOR("for"),
+ FROM("from"),
+ GREATER(">"),
+ GREATER_EQUALS(">="),
+ IDENTIFIER("identifier"),
+ IF("if"),
+ ILLEGAL("illegal character"),
+ IMPORT("import"),
+ IN("in"),
+ INDENT("indent"),
+ INT("integer"),
+ LBRACE("{"),
+ LBRACKET("["),
+ LESS("<"),
+ LESS_EQUALS("<="),
+ LPAREN("("),
+ MINUS("-"),
+ NEWLINE("newline"),
+ NOT("not"),
+ NOT_EQUALS("!="),
+ OR("or"),
+ OUTDENT("outdent"),
+ PERCENT("%"),
+ PLUS("+"),
+ PLUS_EQUALS("+="),
+ RBRACE("}"),
+ RBRACKET("]"),
+ RETURN("return"),
+ RPAREN(")"),
+ SEMI(";"),
+ STAR("*"),
+ STRING("string"),
+ TRY("try");
+
+ private final String prettyName;
+
+ private TokenKind(String prettyName) {
+ this.prettyName = prettyName;
+ }
+
+ /**
+ * Returns the pretty name for this token, for use in error messages for the user.
+ */
+ public String getPrettyName() {
+ return prettyName;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/UserDefinedFunction.java b/src/main/java/com/google/devtools/build/lib/syntax/UserDefinedFunction.java
new file mode 100644
index 0000000..cd909a9
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/UserDefinedFunction.java
@@ -0,0 +1,115 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.devtools.build.lib.events.Location;
+
+/**
+ * The actual function registered in the environment. This function is defined in the
+ * parsed code using {@link FunctionDefStatement}.
+ */
+public class UserDefinedFunction extends MixedModeFunction {
+
+ private final ImmutableList<Argument> args;
+ private final ImmutableMap<String, Integer> argIndexes;
+ private final ImmutableMap<String, Object> defaultValues;
+ private final ImmutableList<Statement> statements;
+ private final SkylarkEnvironment definitionEnv;
+
+ private static ImmutableList<String> argumentToStringList(ImmutableList<Argument> args) {
+ Function<Argument, String> function = new Function<Argument, String>() {
+ @Override
+ public String apply(Argument id) {
+ return id.getArgName();
+ }
+ };
+ return ImmutableList.copyOf(Lists.transform(args, function));
+ }
+
+ private static int mandatoryArgNum(ImmutableList<Argument> args) {
+ int mandatoryArgNum = 0;
+ for (Argument arg : args) {
+ if (!arg.hasValue()) {
+ mandatoryArgNum++;
+ }
+ }
+ return mandatoryArgNum;
+ }
+
+ UserDefinedFunction(Ident function, ImmutableList<Argument> args,
+ ImmutableMap<String, Object> defaultValues,
+ ImmutableList<Statement> statements, SkylarkEnvironment definitionEnv) {
+ super(function.getName(), argumentToStringList(args), mandatoryArgNum(args), false,
+ function.getLocation());
+ this.args = args;
+ this.statements = statements;
+ this.definitionEnv = definitionEnv;
+ this.defaultValues = defaultValues;
+
+ ImmutableMap.Builder<String, Integer> argIndexes = new ImmutableMap.Builder<> ();
+ int i = 0;
+ for (Argument arg : args) {
+ if (!arg.isKwargs()) { // TODO(bazel-team): add varargs support?
+ argIndexes.put(arg.getArgName(), i++);
+ }
+ }
+ this.argIndexes = argIndexes.build();
+ }
+
+ public ImmutableList<Argument> getArgs() {
+ return args;
+ }
+
+ public Integer getArgIndex(String s) {
+ return argIndexes.get(s);
+ }
+
+ ImmutableMap<String, Object> getDefaultValues() {
+ return defaultValues;
+ }
+
+ ImmutableList<Statement> getStatements() {
+ return statements;
+ }
+
+ Location getLocation() {
+ return location;
+ }
+
+ @Override
+ public Object call(Object[] namedArguments, FuncallExpression ast, Environment env)
+ throws EvalException, InterruptedException {
+ SkylarkEnvironment functionEnv = SkylarkEnvironment.createEnvironmentForFunctionCalling(
+ env, definitionEnv, this);
+
+ // Registering the functions's arguments as variables in the local Environment
+ int i = 0;
+ for (Object arg : namedArguments) {
+ functionEnv.update(args.get(i++).getArgName(), arg);
+ }
+
+ try {
+ for (Statement stmt : statements) {
+ stmt.exec(functionEnv);
+ }
+ } catch (ReturnStatement.ReturnException e) {
+ return e.getValue();
+ }
+ return Environment.NONE;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/ValidationEnvironment.java b/src/main/java/com/google/devtools/build/lib/syntax/ValidationEnvironment.java
new file mode 100644
index 0000000..6afb96a
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/ValidationEnvironment.java
@@ -0,0 +1,244 @@
+// Copyright 2014 Google Inc. 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.syntax;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.collect.CollectionUtils;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.syntax.SkylarkType.SkylarkFunctionType;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.Stack;
+
+/**
+ * An Environment for the semantic checking of Skylark files.
+ *
+ * @see Statement#validate
+ * @see Expression#validate
+ */
+public class ValidationEnvironment {
+
+ private final ValidationEnvironment parent;
+
+ private Map<SkylarkType, Map<String, SkylarkType>> variableTypes = new HashMap<>();
+
+ private Map<String, Location> variableLocations = new HashMap<>();
+
+ private Set<String> readOnlyVariables = new HashSet<>();
+
+ // A stack of variable-sets which are read only but can be assigned in different
+ // branches of if-else statements.
+ private Stack<Set<String>> futureReadOnlyVariables = new Stack<>();
+
+ // The function we are currently validating.
+ private SkylarkFunctionType currentFunction;
+
+ // Whether this validation environment is not modified therefore clonable or not.
+ private boolean clonable;
+
+ public ValidationEnvironment(
+ ImmutableMap<SkylarkType, ImmutableMap<String, SkylarkType>> builtinVariableTypes) {
+ parent = null;
+ variableTypes = CollectionUtils.copyOf(builtinVariableTypes);
+ readOnlyVariables.addAll(builtinVariableTypes.get(SkylarkType.GLOBAL).keySet());
+ clonable = true;
+ }
+
+ private ValidationEnvironment(Map<SkylarkType, Map<String, SkylarkType>> builtinVariableTypes,
+ Set<String> readOnlyVariables) {
+ parent = null;
+ this.variableTypes = CollectionUtils.copyOf(builtinVariableTypes);
+ this.readOnlyVariables = new HashSet<>(readOnlyVariables);
+ clonable = false;
+ }
+
+ @Override
+ public ValidationEnvironment clone() {
+ Preconditions.checkState(clonable);
+ return new ValidationEnvironment(variableTypes, readOnlyVariables);
+ }
+
+ /**
+ * Creates a local ValidationEnvironment to validate user defined function bodies.
+ */
+ public ValidationEnvironment(ValidationEnvironment parent, SkylarkFunctionType currentFunction) {
+ this.parent = parent;
+ this.variableTypes.put(SkylarkType.GLOBAL, new HashMap<String, SkylarkType>());
+ this.currentFunction = currentFunction;
+ for (String var : parent.readOnlyVariables) {
+ if (!parent.variableLocations.containsKey(var)) {
+ // Mark built in global vars readonly. Variables defined in Skylark may be shadowed locally.
+ readOnlyVariables.add(var);
+ }
+ }
+ this.clonable = false;
+ }
+
+ /**
+ * Returns true if this ValidationEnvironment is top level i.e. has no parent.
+ */
+ public boolean isTopLevel() {
+ return parent == null;
+ }
+
+ /**
+ * Updates the variable type if the new type is "stronger" then the old one.
+ * The old and the new vartype has to be compatible, otherwise an EvalException is thrown.
+ * The new type is stronger if the old one doesn't exist or unknown.
+ */
+ public void update(String varname, SkylarkType newVartype, Location location)
+ throws EvalException {
+ checkReadonly(varname, location);
+ if (parent == null) { // top-level values are immutable
+ readOnlyVariables.add(varname);
+ if (!futureReadOnlyVariables.isEmpty()) {
+ // Currently validating an if-else statement
+ futureReadOnlyVariables.peek().add(varname);
+ }
+ }
+ SkylarkType oldVartype = variableTypes.get(SkylarkType.GLOBAL).get(varname);
+ if (oldVartype != null) {
+ newVartype = oldVartype.infer(newVartype, "variable '" + varname + "'",
+ location, variableLocations.get(varname));
+ }
+ variableTypes.get(SkylarkType.GLOBAL).put(varname, newVartype);
+ variableLocations.put(varname, location);
+ clonable = false;
+ }
+
+ private void checkReadonly(String varname, Location location) throws EvalException {
+ if (readOnlyVariables.contains(varname)) {
+ throw new EvalException(location, String.format("Variable %s is read only", varname));
+ }
+ }
+
+ public void checkIterable(SkylarkType type, Location loc) throws EvalException {
+ if (type == SkylarkType.UNKNOWN) {
+ // Until all the language is properly typed, we ignore Object types.
+ return;
+ }
+ if (!Iterable.class.isAssignableFrom(type.getType())
+ && !Map.class.isAssignableFrom(type.getType())
+ && !String.class.equals(type.getType())) {
+ throw new EvalException(loc,
+ "type '" + EvalUtils.getDataTypeNameFromClass(type.getType()) + "' is not iterable");
+ }
+ }
+
+ /**
+ * Returns true if the symbol exists in the validation environment.
+ */
+ public boolean hasSymbolInEnvironment(String varname) {
+ return variableTypes.get(SkylarkType.GLOBAL).containsKey(varname)
+ || topLevel().variableTypes.get(SkylarkType.GLOBAL).containsKey(varname);
+ }
+
+ /**
+ * Returns the type of the existing variable.
+ */
+ public SkylarkType getVartype(String varname) {
+ SkylarkType type = variableTypes.get(SkylarkType.GLOBAL).get(varname);
+ if (type == null && parent != null) {
+ type = parent.getVartype(varname);
+ }
+ return Preconditions.checkNotNull(type,
+ String.format("Variable %s is not found in the validation environment", varname));
+ }
+
+ public SkylarkFunctionType getCurrentFunction() {
+ return currentFunction;
+ }
+
+ /**
+ * Returns the return type of the function.
+ */
+ public SkylarkType getReturnType(String funcName, Location loc) throws EvalException {
+ return getReturnType(SkylarkType.GLOBAL, funcName, loc);
+ }
+
+ /**
+ * Returns the return type of the object function.
+ */
+ public SkylarkType getReturnType(SkylarkType objectType, String funcName, Location loc)
+ throws EvalException {
+ // All functions are registered in the top level ValidationEnvironment.
+ Map<String, SkylarkType> functions = topLevel().variableTypes.get(objectType);
+ // TODO(bazel-team): eventually not finding the return type should be a validation error,
+ // because it means the function doesn't exist. First we have to make sure that we register
+ // every possible function before.
+ if (functions != null) {
+ SkylarkType functionType = functions.get(funcName);
+ if (functionType != null && functionType != SkylarkType.UNKNOWN) {
+ if (!(functionType instanceof SkylarkFunctionType)) {
+ throw new EvalException(loc, (objectType == SkylarkType.GLOBAL ? "" : objectType + ".")
+ + funcName + " is not a function");
+ }
+ return ((SkylarkFunctionType) functionType).getReturnType();
+ }
+ }
+ return SkylarkType.UNKNOWN;
+ }
+
+ private ValidationEnvironment topLevel() {
+ return Preconditions.checkNotNull(parent == null ? this : parent);
+ }
+
+ /**
+ * Adds a user defined function to the validation environment is not exists.
+ */
+ public void updateFunction(String name, SkylarkFunctionType type, Location loc)
+ throws EvalException {
+ checkReadonly(name, loc);
+ if (variableTypes.get(SkylarkType.GLOBAL).containsKey(name)) {
+ throw new EvalException(loc, "function " + name + " already exists");
+ }
+ variableTypes.get(SkylarkType.GLOBAL).put(name, type);
+ clonable = false;
+ }
+
+ /**
+ * Starts a session with temporarily disabled readonly checking for variables between branches.
+ * This is useful to validate control flows like if-else when we know that certain parts of the
+ * code cannot both be executed.
+ */
+ public void startTemporarilyDisableReadonlyCheckSession() {
+ futureReadOnlyVariables.add(new HashSet<String>());
+ clonable = false;
+ }
+
+ /**
+ * Finishes the session with temporarily disabled readonly checking.
+ */
+ public void finishTemporarilyDisableReadonlyCheckSession() {
+ Set<String> variables = futureReadOnlyVariables.pop();
+ readOnlyVariables.addAll(variables);
+ if (!futureReadOnlyVariables.isEmpty()) {
+ futureReadOnlyVariables.peek().addAll(variables);
+ }
+ clonable = false;
+ }
+
+ /**
+ * Finishes a branch of temporarily disabled readonly checking.
+ */
+ public void finishTemporarilyDisableReadonlyCheckBranch() {
+ readOnlyVariables.removeAll(futureReadOnlyVariables.peek());
+ clonable = false;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/unix/Directories.java b/src/main/java/com/google/devtools/build/lib/unix/Directories.java
new file mode 100644
index 0000000..b6c3cda
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/unix/Directories.java
@@ -0,0 +1,87 @@
+// Copyright 2014 Google Inc. 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.unix;
+
+import com.google.devtools.build.lib.shell.AbnormalTerminationException;
+import com.google.devtools.build.lib.shell.Command;
+import com.google.devtools.build.lib.shell.CommandException;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+
+/**
+ * Provides utility methods for working with directories in a Unix environment.
+ */
+public final class Directories {
+
+ /**
+ * Deletes a file or directory and all contents recursively, like {@code rm -rf file}.
+ *
+ * <p>If the file argument is a symbolic link, the link will be deleted but not the target of the
+ * link. If the argument is a directory, symbolic links within the directory will not be followed.
+ * If the argument does not exist, throws a FileNotFoundException.
+ *
+ * @param file the file or directory to delete
+ * @throws FileNotFoundException if the file or directory does not exist
+ * @throws IOException if an I/O error occurs
+ */
+ public static void deleteRecursively(File file) throws IOException {
+ deleteRecursivelyImpl(file, true);
+ }
+
+ /**
+ * Deletes a file or directory and all contents recursively, like {@code rm -rf file}, if it
+ * exists.
+ *
+ * <p>If the file argument is a symbolic link, the link will be deleted but not the target of the
+ * link. If the argument is a directory, symbolic links within the directory will not be followed.
+ *
+ * @param file the file or directory to delete
+ * @return {@code true} if the file or directory was deleted by this method; {@code false} if the
+ * file or directory could not be deleted because it did not exist
+ * @throws IOException if an I/O error occurs
+ */
+ public static boolean deleteRecursivelyIfExists(File file) throws IOException {
+ return deleteRecursivelyImpl(file, false);
+ }
+
+ private static boolean deleteRecursivelyImpl(File file, boolean failIfFileDoesNotExist)
+ throws IOException {
+ if (!file.exists()) {
+ if (failIfFileDoesNotExist) {
+ throw new FileNotFoundException(file.getPath());
+ } else {
+ return false;
+ }
+ }
+ String filePath = file.getPath();
+ if (!filePath.isEmpty() && filePath.charAt(0) == '-') {
+ filePath = "./" + filePath;
+ }
+ try {
+ new Command(new String[] {"/bin/rm", "-rf", filePath}).execute();
+ } catch (AbnormalTerminationException e) {
+ String message =
+ e.getResult().getTerminationStatus() + ": " + new String(e.getResult().getStderr());
+ throw new IOException(message, e);
+ } catch (CommandException e) {
+ throw new IOException(e);
+ }
+ return true;
+ }
+
+ private Directories() {}
+}
diff --git a/src/main/java/com/google/devtools/build/lib/unix/ErrnoFileStatus.java b/src/main/java/com/google/devtools/build/lib/unix/ErrnoFileStatus.java
new file mode 100644
index 0000000..fa0c3a7
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/unix/ErrnoFileStatus.java
@@ -0,0 +1,94 @@
+// Copyright 2014 Google Inc. 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.unix;
+
+import com.google.devtools.build.lib.UnixJniLoader;
+
+
+/**
+ * A subsclass of FileStatus which contains an errno.
+ * If there is an error, all other data fields are undefined.
+ */
+public class ErrnoFileStatus extends FileStatus {
+
+ private final int errno;
+
+ // These constants are passed in from JNI via ErrnoConstants.
+ public static final int ENOENT;
+ public static final int EACCES;
+ public static final int ELOOP;
+ public static final int ENOTDIR;
+ public static final int ENAMETOOLONG;
+
+ static {
+ ErrnoConstants constants = ErrnoConstants.getErrnoConstants();
+ ENOENT = constants.ENOENT;
+ EACCES = constants.EACCES;
+ ELOOP = constants.ELOOP;
+ ENOTDIR = constants.ENOTDIR;
+ ENAMETOOLONG = constants.ENAMETOOLONG;
+ }
+
+ /**
+ * Constructs a ErrnoFileSatus instance. (Called only from JNI code.)
+ */
+ private ErrnoFileStatus(int st_mode, int st_atime, int st_atimensec, int st_mtime,
+ int st_mtimensec, int st_ctime, int st_ctimensec, long st_size,
+ int st_dev, long st_ino) {
+ super(st_mode, st_atime, st_atimensec, st_mtime, st_mtimensec, st_ctime, st_ctimensec, st_size,
+ st_dev, st_ino);
+ this.errno = 0;
+ }
+
+ /**
+ * Constructs a ErrnoFileSatus instance. (Called only from JNI code.)
+ */
+ private ErrnoFileStatus(int errno) {
+ super(0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
+ this.errno = errno;
+ }
+
+ public int getErrno() {
+ return errno;
+ }
+
+ public boolean hasError() {
+ // errno = 0 means the operation succeeded.
+ return errno != 0;
+ }
+
+ // Used to transfer the constants from native to java code.
+ private static class ErrnoConstants {
+
+ // These are set in JNI.
+ private int ENOENT;
+ private int EACCES;
+ private int ELOOP;
+ private int ENOTDIR;
+ private int ENAMETOOLONG;
+
+ public static ErrnoConstants getErrnoConstants() {
+ ErrnoConstants constants = new ErrnoConstants();
+ constants.initErrnoConstants();
+ return constants;
+ }
+
+ static {
+ UnixJniLoader.loadJni();
+ }
+
+ private native void initErrnoConstants();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/unix/FileAccessException.java b/src/main/java/com/google/devtools/build/lib/unix/FileAccessException.java
new file mode 100644
index 0000000..9ea4c6e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/unix/FileAccessException.java
@@ -0,0 +1,43 @@
+// Copyright 2014 Google Inc. 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.unix;
+
+import java.io.IOException;
+
+/**
+ * An IOException subclass that is thrown when a POSIX filesystem call returns
+ * an EACCES errno. The message is generally "Permission denied".
+ */
+public class FileAccessException extends IOException {
+ /**
+ * Constructs a <code>FileAccessException</code> with <code>null</code>
+ * as its error detail message.
+ */
+ public FileAccessException() {
+ super();
+ }
+
+ /**
+ * Constructs an <code>FileAccessException</code> with the specified detail
+ * message. The error message string <code>s</code> can later be
+ * retrieved by the <code>{@link java.lang.Throwable#getMessage}</code>
+ * method of class <code>java.lang.Throwable</code>.
+ *
+ * @param s the detail message.
+ */
+ public FileAccessException(String s) {
+ super(s);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/unix/FileStatus.java b/src/main/java/com/google/devtools/build/lib/unix/FileStatus.java
new file mode 100644
index 0000000..10f4d99
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/unix/FileStatus.java
@@ -0,0 +1,262 @@
+// Copyright 2014 Google Inc. 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.unix;
+
+/**
+ * <p>Equivalent to UNIX's "struct stat", a FileStatus instance contains
+ * various bits of metadata about a directory entry.
+ *
+ * <p>The Java SDK provides access to some but not all of the information
+ * available via the stat(2) and lstat(2) syscalls, but often requires that
+ * multiple calls be made to obtain it. By reifying stat buffers as Java
+ * objects and providing a wrapper around the stat/lstat calls, we give client
+ * applications access to the richer file metadata and enable a reduction in
+ * the number of system calls, which is critical for high-performance tools.
+ *
+ * <p>This class is optimized for memory usage. Operations that are not yet
+ * required for any client are intentionally unimplemented to save space.
+ * Currently, we only support these fields: st_mode, st_size, st_atime,
+ * st_atimensec, st_mtime, st_mtimensec, st_ctime, st_ctimensec, st_dev, st_ino.
+ * Methods that require other fields throw UnsupportedOperationException.
+ */
+public class FileStatus {
+
+ private final int st_mode;
+ private final int st_atime; // (unsigned)
+ private final int st_atimensec; // (unsigned)
+ private final int st_mtime; // (unsigned)
+ private final int st_mtimensec; // (unsigned)
+ private final int st_ctime; // (unsigned)
+ private final int st_ctimensec; // (unsigned)
+ private final long st_size;
+ private final int st_dev;
+ private final long st_ino;
+
+ /**
+ * Constructs a FileStatus instance. (Called only from JNI code.)
+ */
+ protected FileStatus(int st_mode, int st_atime, int st_atimensec, int st_mtime, int st_mtimensec,
+ int st_ctime, int st_ctimensec, long st_size, int st_dev, long st_ino) {
+ this.st_mode = st_mode;
+ this.st_atime = st_atime;
+ this.st_atimensec = st_atimensec;
+ this.st_mtime = st_mtime;
+ this.st_mtimensec = st_mtimensec;
+ this.st_ctime = st_ctime;
+ this.st_ctimensec = st_ctimensec;
+ this.st_size = st_size;
+ this.st_dev = st_dev;
+ this.st_ino = st_ino;
+ }
+
+ /**
+ * Returns the device number of this inode.
+ */
+ public int getDeviceNumber() {
+ return st_dev;
+ }
+
+ /**
+ * Returns the number of this inode. Inode numbers are (usually) unique for
+ * a given device.
+ */
+ public long getInodeNumber() {
+ return st_ino;
+ }
+
+ /**
+ * Returns true iff this file is a regular file.
+ */
+ public boolean isRegularFile() {
+ return (st_mode & S_IFMT) == S_IFREG;
+ }
+
+ /**
+ * Returns true iff this file is a directory.
+ */
+ public boolean isDirectory() {
+ return (st_mode & S_IFMT) == S_IFDIR;
+ }
+
+ /**
+ * Returns true iff this file is a symbolic link.
+ */
+ public boolean isSymbolicLink() {
+ return (st_mode & S_IFMT) == S_IFLNK;
+ }
+
+ /**
+ * Returns true iff this file is a character device.
+ */
+ public boolean isCharacterDevice() {
+ return (st_mode & S_IFMT) == S_IFCHR;
+ }
+
+ /**
+ * Returns true iff this file is a block device.
+ */
+ public boolean isBlockDevice() {
+ return (st_mode & S_IFMT) == S_IFBLK;
+ }
+
+ /**
+ * Returns true iff this file is a FIFO.
+ */
+ public boolean isFIFO() {
+ return (st_mode & S_IFMT) == S_IFIFO;
+ }
+
+ /**
+ * Returns true iff this file is a UNIX-domain socket.
+ */
+ public boolean isSocket() {
+ return (st_mode & S_IFMT) == S_IFSOCK;
+ }
+
+ /**
+ * Returns true iff this file has its "set UID" bit set.
+ */
+ public boolean isSetUserId() {
+ return (st_mode & S_ISUID) != 0;
+ }
+
+ /**
+ * Returns true iff this file has its "set GID" bit set.
+ */
+ public boolean isSetGroupId() {
+ return (st_mode & S_ISGID) != 0;
+ }
+
+ /**
+ * Returns true iff this file has its "sticky" bit set. See UNIX manuals for
+ * explanation.
+ */
+ public boolean isSticky() {
+ return (st_mode & S_ISVTX) != 0;
+ }
+
+ /**
+ * Returns the user/group/other permissions part of the mode bits (i.e.
+ * st_mode masked with 0777), interpreted according to longstanding UNIX
+ * tradition.
+ */
+ public int getPermissions() {
+ return st_mode & S_IRWXA;
+ }
+
+ /**
+ * Returns the total size, in bytes, of this file.
+ */
+ public long getSize() {
+ return st_size;
+ }
+
+ /**
+ * Returns the last access time of this file (seconds since UNIX epoch).
+ */
+ public long getLastAccessTime() {
+ return unsignedIntToLong(st_atime);
+ }
+
+ /**
+ * Returns the fractional part of the last access time of this file (nanoseconds).
+ */
+ public long getFractionalLastAccessTime() {
+ return unsignedIntToLong(st_atimensec);
+ }
+
+ /**
+ * Returns the last modified time of this file (seconds since UNIX epoch).
+ */
+ public long getLastModifiedTime() {
+ return unsignedIntToLong(st_mtime);
+ }
+
+ /**
+ * Returns the fractional part of the last modified time of this file (nanoseconds).
+ */
+ public long getFractionalLastModifiedTime() {
+ return unsignedIntToLong(st_mtimensec);
+ }
+
+ /**
+ * Returns the last change time of this file (seconds since UNIX epoch).
+ */
+ public long getLastChangeTime() {
+ return unsignedIntToLong(st_ctime);
+ }
+
+ /**
+ * Returns the fractional part of the last change time of this file (nanoseconds).
+ */
+ public long getFractionalLastChangeTime() {
+ return unsignedIntToLong(st_ctimensec);
+ }
+
+ ////////////////////////////////////////////////////////////////////////
+
+ @Override
+ public String toString() {
+ return String.format("FileStatus(mode=0%06o,size=%d,mtime=%d)",
+ st_mode, st_size, st_mtime);
+ }
+
+ @Override
+ public int hashCode() {
+ return st_mode;
+ }
+
+ ////////////////////////////////////////////////////////////////////////
+ // Platform-specific details. These fields are public so that they can
+ // be used from other packages. See POSIX and/or Linux manuals for details.
+ //
+ // These need to be kept in sync with the native code and system call
+ // interface. (The unit tests ensure that.) Of course, this decoding could
+ // be done in the JNI code to ensure maximum portability, but (a) we don't
+ // expect we'll need that any time soon, and (b) that would require eager
+ // rather than on-demand bitmunging of all attributes. In any case, it's not
+ // part of the interface so it can be easily changed later if necessary.
+
+ public static final int S_IFMT = 0170000; // mask: filetype bitfields
+ public static final int S_IFSOCK = 0140000; // socket
+ public static final int S_IFLNK = 0120000; // symbolic link
+ public static final int S_IFREG = 0100000; // regular file
+ public static final int S_IFBLK = 0060000; // block device
+ public static final int S_IFDIR = 0040000; // directory
+ public static final int S_IFCHR = 0020000; // character device
+ public static final int S_IFIFO = 0010000; // fifo
+ public static final int S_ISUID = 0004000; // set UID bit
+ public static final int S_ISGID = 0002000; // set GID bit (see below)
+ public static final int S_ISVTX = 0001000; // sticky bit (see below)
+ public static final int S_IRWXA = 00777; // mask: all permissions
+ public static final int S_IRWXU = 00700; // mask: file owner permissions
+ public static final int S_IRUSR = 00400; // owner has read permission
+ public static final int S_IWUSR = 00200; // owner has write permission
+ public static final int S_IXUSR = 00100; // owner has execute permission
+ public static final int S_IRWXG = 00070; // mask: group permissions
+ public static final int S_IRGRP = 00040; // group has read permission
+ public static final int S_IWGRP = 00020; // group has write permission
+ public static final int S_IXGRP = 00010; // group has execute permission
+ public static final int S_IRWXO = 00007; // mask: other permissions
+ public static final int S_IROTH = 00004; // others have read permission
+ public static final int S_IWOTH = 00002; // others have write permisson
+ public static final int S_IXOTH = 00001; // others have execute permission
+
+ public static final int S_IEXEC = 00111; // owner, group, world execute
+
+ static long unsignedIntToLong(int i) {
+ return (i & 0x7FFFFFFF) - (long) (i & 0x80000000);
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/unix/FilesystemUtils.java b/src/main/java/com/google/devtools/build/lib/unix/FilesystemUtils.java
new file mode 100644
index 0000000..462ed9c
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/unix/FilesystemUtils.java
@@ -0,0 +1,442 @@
+// Copyright 2014 Google Inc. 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.unix;
+
+import com.google.common.hash.HashCode;
+import com.google.devtools.build.lib.UnixJniLoader;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.logging.Level;
+import java.util.logging.LogManager;
+import java.util.logging.Logger;
+
+/**
+ * Utility methods for access to UNIX filesystem calls not exposed by the Java
+ * SDK. Exception messages are selected to be consistent with those generated
+ * by the java.io package where appropriate--see package javadoc for details.
+ */
+public final class FilesystemUtils {
+
+ private FilesystemUtils() {}
+
+ /**
+ * Returns true iff the file identified by 'path' is a symbolic link. Has
+ * similar semantics to POSIX stat(2) syscall, with all errors being mapped to
+ * a false return.
+ *
+ * @param path the file of interest
+ * @return true iff path exists, is accessible and is a symlink
+ */
+ public static boolean isSymbolicLink(File path) {
+ try {
+ return lstat(path.toString()).isSymbolicLink();
+ } catch (IOException e) {
+ return false;
+ }
+ }
+
+
+ /**
+ * Returns true iff the file identified by 'path' is a directory. Has
+ * similar semantics to POSIX stat(2) syscall, with all errors being mapped to
+ * a false return.
+ *
+ * @param path the file of interest
+ * @return true iff path exists, is accessible and is a symlink
+ */
+ public static boolean isDirectory(String path) {
+ try {
+ return lstat(path).isDirectory();
+ } catch (IOException e) {
+ return false;
+ }
+ }
+
+
+ /**
+ * Marks the file or directory {@code path} as executable. (Non-atomic)
+ *
+ * @see File#setReadOnly
+ *
+ * @param path the file of interest
+ * @throws FileAccessException if path can't be accessed
+ * @throws FileNotFoundException if path doesn't exist
+ * @throws IOException for other filesystem or path errors
+ */
+ public static void setExecutable(File path) throws IOException {
+ String p = path.toString();
+ chmod(p, stat(p).getPermissions() | FileStatus.S_IEXEC);
+ }
+
+ /**
+ * Marks the file or directory {@code path} as owner writable. (Non-atomic)
+ *
+ * @see File#setReadOnly
+ *
+ * @param path the file of interest
+ * @throws FileAccessException if path can't be accessed
+ * @throws FileNotFoundException if path doesn't exist
+ * @throws IOException for other filesystem or path errors
+ */
+ public static void setWritable(File path) throws IOException {
+ String p = path.toString();
+ chmod(p, stat(p).getPermissions() | FileStatus.S_IWUSR);
+ }
+
+ /**
+ * Changes permissions of a file.
+ *
+ * @param path the file whose mode to change.
+ * @param mode the mode bits within 07777, interpreted according to
+ * long-standing UNIX tradition.
+ * @throws IOException if the chmod() syscall failed.
+ */
+ public static void chmod(File path, int mode) throws IOException {
+ int mask = FileStatus.S_ISUID |
+ FileStatus.S_ISGID |
+ FileStatus.S_ISVTX |
+ FileStatus.S_IRWXA;
+ chmod(path.toString(), mode & mask);
+ }
+
+ /*
+ * Native-based implementation
+ */
+
+ static {
+ if (!java.nio.charset.Charset.defaultCharset().name().equals("ISO-8859-1")) {
+ // Defer the Logger call, so we don't deadlock if this is called from Logger
+ // initialization code.
+ new Thread() {
+ @Override
+ public void run() {
+ // wait (if necessary) until the logging system is initialized
+ synchronized (LogManager.getLogManager()) {}
+ Logger.getLogger("com.google.devtools.build.lib.unix.FilesystemUtils").log(Level.FINE,
+ "WARNING: Default character set is not latin1; java.io.File and " +
+ "com.google.devtools.build.lib.unix.FilesystemUtils will represent some filenames " +
+ "differently.");
+ }
+ }.start();
+ }
+ UnixJniLoader.loadJni();
+ }
+
+ /**
+ * Native wrapper around Linux readlink(2) call.
+ *
+ * @param path the file of interest
+ * @return the pathname to which the symbolic link 'path' links
+ * @throws IOException iff the readlink() call failed
+ */
+ public static native String readlink(String path) throws IOException;
+
+ /**
+ * Native wrapper around POSIX chmod(2) syscall: Changes the file access
+ * permissions of 'path' to 'mode'.
+ *
+ * @param path the file of interest
+ * @param mode the POSIX type and permission mode bits to set
+ * @throws IOException iff the chmod() call failed.
+ */
+ public static native void chmod(String path, int mode) throws IOException;
+
+ /**
+ * Native wrapper around POSIX symlink(2) syscall.
+ *
+ * @param oldpath the file to link to
+ * @param newpath the new path for the link
+ * @throws IOException iff the symlink() syscall failed.
+ */
+ public static native void symlink(String oldpath, String newpath)
+ throws IOException;
+
+ /**
+ * Native wrapper around POSIX stat(2) syscall.
+ *
+ * @param path the file to stat.
+ * @return a FileStatus instance containing the metadata.
+ * @throws IOException if the stat() syscall failed.
+ */
+ public static native FileStatus stat(String path) throws IOException;
+
+ /**
+ * Native wrapper around POSIX lstat(2) syscall.
+ *
+ * @param path the file to lstat.
+ * @return a FileStatus instance containing the metadata.
+ * @throws IOException if the lstat() syscall failed.
+ */
+ public static native FileStatus lstat(String path) throws IOException;
+
+ /**
+ * Native wrapper around POSIX stat(2) syscall.
+ *
+ * @param path the file to stat.
+ * @return an ErrnoFileStatus instance containing the metadata.
+ * If there was an error, the return value's hasError() method
+ * will return true, and all stat information is undefined.
+ */
+ public static native ErrnoFileStatus errnoStat(String path);
+
+ /**
+ * Native wrapper around POSIX lstat(2) syscall.
+ *
+ * @param path the file to lstat.
+ * @return an ErrnoFileStatus instance containing the metadata.
+ * If there was an error, the return value's hasError() method
+ * will return true, and all stat information is undefined.
+ */
+ public static native ErrnoFileStatus errnoLstat(String path);
+
+ /**
+ * Native wrapper around POSIX utime(2) syscall.
+ *
+ * Note: negative file times are interpreted as unsigned time_t.
+ *
+ * @param path the file whose times to change.
+ * @param now if true, ignore actime/modtime parameters and use current time.
+ * @param actime the file access time in seconds since the UNIX epoch.
+ * @param modtime the file modification time in seconds since the UNIX epoch.
+ * @throws IOException if the utime() syscall failed.
+ */
+ public static native void utime(String path, boolean now,
+ int actime, int modtime) throws IOException;
+
+ /**
+ * Native wrapper around POSIX mkdir(2) syscall.
+ *
+ * Caveat: errno==EEXIST is mapped to the return value "false", not
+ * IOException. It requires an additional stat() to determine if mkdir
+ * failed because the directory already exists.
+ *
+ * @param path the directory to create.
+ * @param mode the mode with which to create the directory.
+ * @return true if the directory was successfully created; false if the
+ * system call returned EEXIST because some kind of a file (not necessarily
+ * a directory) already exists.
+ * @throws IOException if the mkdir() syscall failed for any other reason.
+ */
+ public static native boolean mkdir(String path, int mode)
+ throws IOException;
+
+ /**
+ * Native wrapper around POSIX opendir(2)/readdir(3)/closedir(3) syscall.
+ *
+ * @param path the directory to read.
+ * @return the list of directory entries in the order they were returned by
+ * the system, excluding "." and "..".
+ * @throws IOException if the call to opendir failed for any reason.
+ */
+ public static String[] readdir(String path) throws IOException {
+ return readdir(path, ReadTypes.NONE).names;
+ }
+
+ /**
+ * An enum for specifying now the types of the individual entries returned by
+ * {@link #readdir(String, ReadTypes)} is to be returned.
+ */
+ public enum ReadTypes {
+ NONE('n'), // Do not read types
+ NOFOLLOW('d'), // Do not follow symlinks
+ FOLLOW('f'); // Follow symlinks; never returns "SYMLINK" and returns "UNKNOWN" when dangling
+
+ private final char code;
+
+ private ReadTypes(char code) {
+ this.code = code;
+ }
+
+ private char getCode() {
+ return code;
+ }
+ }
+
+ /**
+ * A compound return type for readdir(), analogous to struct dirent[] in C. A low memory profile
+ * is critical for this class, as instances are expected to be kept around for caching for
+ * potentially a long time.
+ */
+ public static final class Dirents {
+
+ /**
+ * The type of the directory entry.
+ */
+ public enum Type {
+ FILE,
+ DIRECTORY,
+ SYMLINK,
+ UNKNOWN;
+
+ private static Type forChar(char c) {
+ if (c == 'f') {
+ return Type.FILE;
+ } else if (c == 'd') {
+ return Type.DIRECTORY;
+ } else if (c == 's') {
+ return Type.SYMLINK;
+ } else {
+ return Type.UNKNOWN;
+ }
+ }
+ }
+
+ /** The names of the entries in a directory. */
+ private final String[] names;
+ /**
+ * An optional (nullable) array of entry types, corresponding positionally
+ * to the "names" field. The types are:
+ * 'd': a subdirectory
+ * 'f': a regular file
+ * 's': a symlink (only returned with {@code NOFOLLOW})
+ * '?': anything else
+ * Note that unlike libc, this implementation of readdir() follows
+ * symlinks when determining these types.
+ *
+ * <p>This is intentionally a byte array rather than a array of enums to save memory.
+ */
+ private final byte[] types;
+
+ /** called from JNI */
+ public Dirents(String[] names, byte[] types) {
+ this.names = names;
+ this.types = types;
+ }
+
+ public int size() {
+ return names.length;
+ }
+
+ public boolean hasTypes() {
+ return types != null;
+ }
+
+ public String getName(int i) {
+ return names[i];
+ }
+
+ public Type getType(int i) {
+ return Type.forChar((char) types[i]);
+ }
+ }
+
+ /**
+ * Native wrapper around POSIX opendir(2)/readdir(3)/closedir(3) syscall.
+ *
+ * @param path the directory to read.
+ * @param readTypes How the types of individual entries should be returned. If {@code NONE},
+ * the "types" field in the result will be null.
+ * @return a Dirents object, containing "names", the list of directory entries
+ * (excluding "." and "..") in the order they were returned by the system,
+ * and "types", an array of entry types (file, directory, etc) corresponding
+ * positionally to "names".
+ * @throws IOException if the call to opendir failed for any reason.
+ */
+ public static Dirents readdir(String path, ReadTypes readTypes) throws IOException {
+ // Passing enums to native code is possible, but onerous; we use a char instead.
+ return readdir(path, readTypes.getCode());
+ }
+
+ private static native Dirents readdir(String path, char typeCode)
+ throws IOException;
+
+ /**
+ * Native wrapper around POSIX rename(2) syscall.
+ *
+ * @param oldpath the source location.
+ * @param newpath the destination location.
+ * @throws IOException if the rename failed for any reason.
+ */
+ public static native void rename(String oldpath, String newpath)
+ throws IOException;
+
+ /**
+ * Native wrapper around POSIX remove(3) C library call.
+ *
+ * @param path the file or directory to remove.
+ * @return true iff the file was actually deleted by this call.
+ * @throws IOException if the remove failed, but the file was present prior to the call.
+ */
+ public static native boolean remove(String path) throws IOException;
+
+ /********************************************************************
+ * *
+ * Linux extended file attributes *
+ * *
+ ********************************************************************/
+
+ /**
+ * Native wrapper around Linux getxattr(2) syscall.
+ *
+ * @param path the file whose extended attribute is to be returned.
+ * @param name the name of the extended attribute key.
+ * @return the value of the extended attribute associated with 'path', if
+ * any, or null if no such attribute is defined (ENODATA).
+ * @throws IOException if the call failed for any other reason.
+ */
+ public static native byte[] getxattr(String path, String name)
+ throws IOException;
+
+ /**
+ * Native wrapper around Linux lgetxattr(2) syscall. (Like getxattr, but
+ * does not follow symbolic links.)
+ *
+ * @param path the file whose extended attribute is to be returned.
+ * @param name the name of the extended attribute key.
+ * @return the value of the extended attribute associated with 'path', if
+ * any, or null if no such attribute is defined (ENODATA).
+ * @throws IOException if the call failed for any other reason.
+ */
+ public static native byte[] lgetxattr(String path, String name)
+ throws IOException;
+
+ /**
+ * Returns the MD5 digest of the specified file, following symbolic links.
+ *
+ * @param path the file whose MD5 digest is required.
+ * @return the MD5 digest, as a 16-byte array.
+ * @throws IOException if the call failed for any reason.
+ */
+ static native byte[] md5sumAsBytes(String path) throws IOException;
+
+ /**
+ * Returns the MD5 digest of the specified file, following symbolic links.
+ *
+ * @param path the file whose MD5 digest is required.
+ * @return the MD5 digest, as a {@link HashCode}
+ * @throws IOException if the call failed for any reason.
+ */
+ public static HashCode md5sum(String path) throws IOException {
+ return HashCode.fromBytes(md5sumAsBytes(path));
+ }
+
+ /**
+ * Removes entire directory tree. Doesn't follow symlinks.
+ *
+ * @param path the file or directory to remove.
+ * @throws IOException if the remove failed.
+ */
+ public static void rmTree(String path) throws IOException {
+ if (isDirectory(path)) {
+ String[] contents = readdir(path);
+ for (String entry : contents) {
+ rmTree(path + "/" + entry);
+ }
+ }
+ remove(path.toString());
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/unix/LocalClientSocket.java b/src/main/java/com/google/devtools/build/lib/unix/LocalClientSocket.java
new file mode 100644
index 0000000..46980da
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/unix/LocalClientSocket.java
@@ -0,0 +1,117 @@
+// Copyright 2014 Google Inc. 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.unix;
+
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.SocketException;
+
+/**
+ * <p>An implementation of client Socket for local (AF_UNIX) sockets.
+ *
+ * <p>This class intentionally doesn't extend java.net.Socket although it
+ * has some similarity to it. The java.net class hierarchy is a terrible mess
+ * and is inextricably coupled to the Internet Protocol.
+ *
+ * <p>This code is not intended to be portable to non-UNIX platforms.
+ */
+public class LocalClientSocket extends LocalSocket {
+
+ /**
+ * Constructs an unconnected local client socket.
+ *
+ * @throws IOException if the socket could not be created.
+ */
+ public LocalClientSocket() throws IOException {
+ super();
+ }
+
+ /**
+ * Constructs a client socket and connects it to the specified address.
+ *
+ * @throws IOException if either of the socket/connect operations failed.
+ */
+ public LocalClientSocket(LocalSocketAddress address) throws IOException {
+ super();
+ connect(address);
+ }
+
+ /**
+ * Connect to the specified server. Blocks until the server accepts the
+ * connection.
+ *
+ * @throws IOException if the connection failed.
+ */
+ public synchronized void connect(LocalSocketAddress address)
+ throws IOException {
+ checkNotClosed();
+ if (state == State.CONNECTED) {
+ throw new SocketException("socket is already connected");
+ }
+ connect(fd, address.getName().toString()); // JNI
+ this.address = address;
+ this.state = State.CONNECTED;
+ }
+
+ /**
+ * Returns the input stream for reading from the server.
+ *
+ * @param closeSocket close the socket when this input stream is closed.
+ * @throws IOException if there was a problem.
+ */
+ public synchronized InputStream getInputStream(final boolean closeSocket) throws IOException {
+ checkConnected();
+ checkInputNotShutdown();
+ return new FileInputStream(fd) {
+ @Override
+ public void close() throws IOException {
+ if (closeSocket) {
+ LocalClientSocket.this.close();
+ }
+ }
+ };
+ }
+
+ /**
+ * Returns the input stream for reading from the server.
+ *
+ * @throws IOException if there was a problem.
+ */
+ public synchronized InputStream getInputStream() throws IOException {
+ return getInputStream(false);
+ }
+
+ /**
+ * Returns the output stream for writing to the server.
+ *
+ * @throws IOException if there was a problem.
+ */
+ public synchronized OutputStream getOutputStream() throws IOException {
+ checkConnected();
+ checkOutputNotShutdown();
+ return new FileOutputStream(fd) {
+ @Override public void close() {
+ // Don't close the file descriptor.
+ }
+ };
+ }
+
+ @Override
+ public String toString() {
+ return "LocalClientSocket(" + address + ")";
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/unix/LocalServerSocket.java b/src/main/java/com/google/devtools/build/lib/unix/LocalServerSocket.java
new file mode 100644
index 0000000..4eb1265
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/unix/LocalServerSocket.java
@@ -0,0 +1,173 @@
+// Copyright 2014 Google Inc. 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.unix;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.net.Socket;
+import java.net.SocketException;
+import java.net.SocketTimeoutException;
+
+/**
+ * <p>An implementation of ServerSocket for local (AF_UNIX) sockets.
+ *
+ * <p>This class intentionally doesn't extend java.net.ServerSocket although it
+ * has some similarity to it. The java.net class hierarchy is a terrible mess
+ * and is inextricably coupled to the Internet Protocol.
+ *
+ * <p>This code is not intended to be portable to non-UNIX platforms.
+ */
+public class LocalServerSocket extends LocalSocket {
+
+ // Socket timeout in milliseconds. No timeout by default.
+ private long soTimeoutMillis = 0;
+
+ /**
+ * Constructs an unbound local server socket.
+ */
+ public LocalServerSocket() throws IOException {
+ super();
+ }
+
+ /**
+ * Constructs a server socket, binds it to the specified address, and
+ * listens for incoming connections with the specified backlog.
+ *
+ * @throws IOException if any of the socket/bind/listen operations failed.
+ */
+ public LocalServerSocket(LocalSocketAddress address, int backlog)
+ throws IOException {
+ this();
+ bind(address);
+ listen(backlog);
+ }
+
+ /**
+ * Constructs a server socket, binds it to the specified address, and begin
+ * listening for incoming connections using the default backlog.
+ *
+ * @throws IOException if any of the socket/bind/listen operations failed.
+ */
+ public LocalServerSocket(LocalSocketAddress address) throws IOException {
+ this(address, 50);
+ }
+
+ /**
+ * Specifies the timeout in milliseconds for accept(). Setting it to
+ * zero means an indefinite timeout.
+ */
+ public void setSoTimeout(long timeoutMillis) {
+ soTimeoutMillis = timeoutMillis;
+ }
+
+ /**
+ * Returns the current timeout in milliseconds.
+ */
+ public long getSoTimeout() {
+ return soTimeoutMillis;
+ }
+
+ /**
+ * Binds the specified address to this socket. The socket must be unbound.
+ * This causes the filesystem entry to appear.
+ *
+ * @throws IOException if the bind failed.
+ */
+ public synchronized void bind(LocalSocketAddress address)
+ throws IOException {
+ if (address == null) {
+ throw new NullPointerException("address");
+ }
+ checkNotClosed();
+ if (state != State.NEW) {
+ throw new SocketException("socket is already bound to an address");
+ }
+ bind(fd, address.getName().toString()); // JNI
+ this.address = address;
+ this.state = State.BOUND;
+ }
+
+ /**
+ * Listen for incoming connections on a socket using the specfied backlog.
+ * The socket must be bound but not already listening.
+ *
+ * @throws IOException if the listen failed.
+ */
+ public synchronized void listen(int backlog) throws IOException {
+ if (backlog < 1) {
+ throw new IllegalArgumentException("backlog=" + backlog);
+ }
+ checkNotClosed();
+ if (address == null) {
+ throw new SocketException("socket has no address bound");
+ }
+ if (state == State.LISTENING) {
+ throw new SocketException("socket is already listening");
+ }
+ listen(fd, backlog); // JNI
+ this.state = State.LISTENING;
+ }
+
+ /**
+ * Blocks until a connection is made to this socket and accepts it, returning
+ * a new socket connected to the client.
+ *
+ * @return the new socket connected to the client.
+ * @throws IOException if an error occurs when waiting for a connection.
+ * @throws SocketTimeoutException if a timeout was previously set with
+ * setSoTimeout and the timeout has been reached.
+ * @throws InterruptedIOException if the thread is interrupted when the
+ * method is blocked.
+ */
+ public synchronized Socket accept()
+ throws IOException, SocketTimeoutException, InterruptedIOException {
+ if (state != State.LISTENING) {
+ throw new SocketException("socket is not in listening state");
+ }
+
+ // Throws a SocketTimeoutException if timeout.
+ if (soTimeoutMillis != 0) {
+ poll(fd, soTimeoutMillis); // JNI
+ }
+
+ FileDescriptor clientFd = new FileDescriptor();
+ accept(fd, clientFd); // JNI
+ final LocalSocketImpl impl = new LocalSocketImpl(clientFd);
+ return new Socket(impl) {
+ @Override
+ public boolean isConnected() {
+ return true;
+ }
+ @Override
+ public synchronized void close() throws IOException {
+ if (isClosed()) {
+ return;
+ } else {
+ super.close();
+ // Workaround for the fact that super.created==false because we
+ // created the impl ourselves. As a result, super.close() doesn't
+ // call impl.close(). *Sigh*, java.net is horrendous.
+ // (Perhaps we should dispense with Socket/SocketImpl altogether?)
+ impl.close();
+ }
+ }
+ };
+ }
+
+ @Override
+ public String toString() {
+ return "LocalServerSocket(" + address + ")";
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/unix/LocalSocket.java b/src/main/java/com/google/devtools/build/lib/unix/LocalSocket.java
new file mode 100644
index 0000000..c9d1c91
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/unix/LocalSocket.java
@@ -0,0 +1,217 @@
+// Copyright 2014 Google Inc. 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.unix;
+
+import com.google.devtools.build.lib.UnixJniLoader;
+
+import java.io.Closeable;
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.net.SocketException;
+import java.net.SocketTimeoutException;
+
+/**
+ * Abstract superclass for client and server local sockets.
+ */
+abstract class LocalSocket implements Closeable {
+
+ protected enum State {
+ NEW,
+ BOUND, // server only
+ LISTENING, // server only
+ CONNECTED, // client only
+ CLOSED,
+ }
+
+ protected LocalSocketAddress address = null;
+ protected FileDescriptor fd = new FileDescriptor();
+ protected State state;
+ protected boolean inputShutdown = false;
+ protected boolean outputShutdown = false;
+
+ /**
+ * Constructs an unconnected local socket.
+ */
+ protected LocalSocket() throws IOException {
+ socket(fd);
+ if (!fd.valid()) {
+ throw new IOException("Couldn't create socket!");
+ }
+ this.state = State.NEW;
+ }
+
+ /**
+ * Returns the address of the endpoint this socket is bound to.
+ *
+ * @return a <code>SocketAddress</code> representing the local endpoint of
+ * this socket.
+ */
+ public LocalSocketAddress getLocalSocketAddress() {
+ return address;
+ }
+
+ /**
+ * Closes this socket. This operation is idempotent.
+ *
+ * To be consistent with Java Socket, the shutdown states of the socket are
+ * not changed. This makes it easier to port applications between Socket and
+ * LocalSocket.
+ *
+ * @throws IOException if an I/O error occurred when closing the socket.
+ */
+ @Override
+ public synchronized void close() throws IOException {
+ if (state == State.CLOSED) {
+ return;
+ }
+ // Closes the file descriptor if it has not been closed by the
+ // input/output streams.
+ if (!fd.valid()) {
+ throw new IllegalStateException("LocalSocket.close(-1)");
+ }
+ close(fd);
+ if (fd.valid()) {
+ throw new IllegalStateException("LocalSocket.close() did not set fd to -1");
+ }
+ this.state = State.CLOSED;
+ }
+
+ /**
+ * Returns the closed state of the ServerSocket.
+ *
+ * @return true if the socket has been closed
+ */
+ public synchronized boolean isClosed() {
+ // If the file descriptor has been closed by the input/output
+ // streams, marks the socket as closed too.
+ return state == State.CLOSED;
+ }
+
+ /**
+ * Returns the connected state of the ClientSocket.
+ *
+ * @return true if the socket is currently connected.
+ */
+ public synchronized boolean isConnected() {
+ return state == State.CONNECTED;
+ }
+
+ protected synchronized void checkConnected() throws SocketException {
+ if (!isConnected()) {
+ throw new SocketException("Transport endpoint is not connected");
+ }
+ }
+
+ protected synchronized void checkNotClosed() throws SocketException {
+ if (isClosed()) {
+ throw new SocketException("socket is closed");
+ }
+ }
+
+ /**
+ * Returns the shutdown state of the input channel.
+ *
+ * @return true is the input channel of the socket is shutdown.
+ */
+ public synchronized boolean isInputShutdown() {
+ return inputShutdown;
+ }
+
+ /**
+ * Returns the shutdown state of the output channel.
+ *
+ * @return true is the input channel of the socket is shutdown.
+ */
+ public synchronized boolean isOutputShutdown() {
+ return outputShutdown;
+ }
+
+ protected synchronized void checkInputNotShutdown() throws SocketException {
+ if (isInputShutdown()) {
+ throw new SocketException("Socket input is shutdown");
+ }
+ }
+
+ protected synchronized void checkOutputNotShutdown() throws SocketException {
+ if (isOutputShutdown()) {
+ throw new SocketException("Socket output is shutdown");
+ }
+ }
+
+ static final int SHUT_RD = 0; // Mapped to BSD SHUT_RD in JNI.
+ static final int SHUT_WR = 1; // Mapped to BSD SHUT_WR in JNI.
+
+ public synchronized void shutdownInput() throws IOException {
+ checkNotClosed();
+ checkConnected();
+ checkInputNotShutdown();
+ inputShutdown = true;
+ shutdown(fd, SHUT_RD);
+ }
+
+ public synchronized void shutdownOutput() throws IOException {
+ checkNotClosed();
+ checkConnected();
+ checkOutputNotShutdown();
+ outputShutdown = true;
+ shutdown(fd, SHUT_WR);
+ }
+
+ ////////////////////////////////////////////////////////////////////////
+ // JNI:
+
+ static {
+ UnixJniLoader.loadJni();
+ }
+
+ // The native calls below are thin wrappers around linux system calls. The
+ // semantics remains the same except for poll(). See the comments for the
+ // method.
+ //
+ // Note: FileDescriptor is a box for a mutable integer that is visible only
+ // to native code.
+
+ // Generic operations:
+ protected static native void socket(FileDescriptor server)
+ throws IOException;
+ static native void close(FileDescriptor server)
+ throws IOException;
+ /**
+ * Shut down part of a full-duplex connection
+ * @param code Must be either SHUT_RD or SHUT_WR
+ */
+ static native void shutdown(FileDescriptor fd, int code)
+ throws IOException;
+
+ /**
+ * This method checks waits for the given file descriptor to become available for read.
+ * If timeoutMillis passed and there is no activity, a SocketTimeoutException will be thrown.
+ */
+ protected static native void poll(FileDescriptor read, long timeoutMillis)
+ throws IOException, SocketTimeoutException, InterruptedIOException;
+
+ // Server operations:
+ protected static native void bind(FileDescriptor server, String filename)
+ throws IOException;
+ protected static native void listen(FileDescriptor server, int backlog)
+ throws IOException;
+ protected static native void accept(FileDescriptor server,
+ FileDescriptor client)
+ throws IOException;
+
+ // Client operations:
+ protected static native void connect(FileDescriptor client, String filename)
+ throws IOException;
+}
diff --git a/src/main/java/com/google/devtools/build/lib/unix/LocalSocketAddress.java b/src/main/java/com/google/devtools/build/lib/unix/LocalSocketAddress.java
new file mode 100644
index 0000000..b92a04d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/unix/LocalSocketAddress.java
@@ -0,0 +1,56 @@
+// Copyright 2014 Google Inc. 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.unix;
+
+import java.io.File;
+import java.net.SocketAddress;
+
+/**
+ * An implementation of SocketAddress for naming local sockets, i.e. files in
+ * the UNIX file system.
+ */
+public class LocalSocketAddress extends SocketAddress {
+
+ private final File name;
+
+ /**
+ * Constructs a SocketAddress for the specified file.
+ */
+ public LocalSocketAddress(File name) {
+ this.name = name;
+ }
+
+ /**
+ * Returns the filename of this local socket address.
+ */
+ public File getName() {
+ return name;
+ }
+
+ @Override
+ public String toString() {
+ return name.toString();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ return other instanceof LocalSocketAddress &&
+ ((LocalSocketAddress) other).name.equals(this.name);
+ }
+
+ @Override
+ public int hashCode() {
+ return name.hashCode();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/unix/LocalSocketImpl.java b/src/main/java/com/google/devtools/build/lib/unix/LocalSocketImpl.java
new file mode 100644
index 0000000..aee473a
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/unix/LocalSocketImpl.java
@@ -0,0 +1,168 @@
+// Copyright 2014 Google Inc. 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.unix;
+
+import com.google.devtools.build.lib.UnixJniLoader;
+
+import java.io.Closeable;
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.SocketAddress;
+import java.net.SocketImpl;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * A simple implementation of SocketImpl for sockets that wrap a UNIX
+ * file-descriptor. This SocketImpl assumes that the socket is already
+ * created, bound, connected and supports no socket options or out-of-band
+ * features. This is used to implement server-side accepted client sockets
+ * (i.e. those returned by {@link LocalServerSocket#accept}).
+ */
+class LocalSocketImpl extends SocketImpl {
+ private static final Logger logger =
+ Logger.getLogger(LocalSocketImpl.class.getName());
+
+ static {
+ UnixJniLoader.loadJni();
+ init();
+ }
+
+ // The logic here is a little twisted, to support JDK7 and JDK8.
+
+ // 1) In JDK7, the FileDescriptor class keeps a reference count of
+ // instances using the fd, and closes it when it goes to 0. The
+ // reference count is only decremented by the finalizer for a
+ // given class. When the call to close() happens, the fd is
+ // closed regardless of the current state of the refcount.
+ //
+ // 2) In JDK8, every instance that uses the fd registers a Closeable
+ // with the FileDescriptor. Since the FileDescriptor has a
+ // reference to every user, only when all of the users and the
+ // FileDescriptor get GC'd does the finalizer run. An explicit
+ // call to close() calls FileDescriptor.closeAll(), which
+ // force-closes all of the users.
+
+ // So, in our case:
+
+ // 1) ref() increments the refcount in JDK7, and registers with the
+ // FD in JDK8.
+
+ // 2) unref() decrements the refcount in JDK7, and does nothing in
+ // JDK8.
+
+ // 3) The finalizer decrements the refcount in JDK7, and simply
+ // calls close() in JDK8 (where we don't have to worry about
+ // multiple live users of the FD). The close() method itself is
+ // idempotent.
+
+ // 4) close() calls fd.closeAll in JDK8, which, in turn, calls
+ // closer.close(). In JDK7, close() calls closer.close()
+ // explicitly.
+ private static native void init();
+ private static native void ref(FileDescriptor fd, Closeable closeable);
+ private static native boolean unref(FileDescriptor fd);
+ private static native boolean close0(FileDescriptor fd, Closeable closeable);
+
+ private final boolean isInitialized;
+ private final Closeable closer = new Closeable() {
+ AtomicBoolean isClosed = new AtomicBoolean(false);
+ @Override public void close() throws IOException {
+ if (isClosed.compareAndSet(false, true)) {
+ LocalSocket.close(fd);
+ }
+ }
+ };
+
+ // Note to callers: if you pass a FD into this constructor, this
+ // instance is now responsible for closing it (in the sense of
+ // LocalSocket.close()). If some other instance tries to close it,
+ // then terrible things will happen.
+ LocalSocketImpl(FileDescriptor fd) {
+ this.fd = fd; // (inherited field)
+ ref(fd, closer);
+ isInitialized = true;
+ }
+
+ @Override protected void finalize() {
+ try {
+ if (isInitialized) {
+ if (!unref(fd)) {
+ // JDK8 codepath
+ close0(fd, closer);
+ }
+ }
+ } catch (Exception e) {
+ logger.log(Level.WARNING, "Unable to access FileDescriptor class - " +
+ "may cause a file descriptor leak", e);
+ }
+ }
+ @Override protected InputStream getInputStream() {
+ return new FileInputStream(getFileDescriptor());
+ }
+ @Override protected OutputStream getOutputStream() {
+ return new FileOutputStream(getFileDescriptor());
+ }
+ @Override protected void close() throws IOException {
+ if (fd.valid()) {
+ if (!close0(fd, closer)) {
+ // JDK7 codepath
+ closer.close();
+ }
+ }
+ }
+
+ // Unused:
+ @Override
+ public void setOption(int optID, Object value) {
+ throw new UnsupportedOperationException("setOption");
+ }
+ @Override
+ public Object getOption(int optID) {
+ throw new UnsupportedOperationException("getOption");
+ }
+ @Override protected void create(boolean stream) {
+ throw new UnsupportedOperationException("create");
+ }
+ @Override protected void connect(String host, int port) {
+ throw new UnsupportedOperationException("connect");
+ }
+ @Override protected void connect(InetAddress address, int port) {
+ throw new UnsupportedOperationException("connect2");
+ }
+ @Override protected void connect(SocketAddress address, int timeout) {
+ throw new UnsupportedOperationException("connect3");
+ }
+ @Override protected void bind(InetAddress host, int port) {
+ throw new UnsupportedOperationException("bind");
+ }
+ @Override protected void listen(int backlog) {
+ throw new UnsupportedOperationException("listen");
+ }
+ @Override protected void accept(SocketImpl s) {
+ throw new UnsupportedOperationException("accept");
+ }
+ @Override protected int available() {
+ throw new UnsupportedOperationException("available");
+ }
+ @Override protected void sendUrgentData(int i) {
+ throw new UnsupportedOperationException("sendUrgentData");
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/unix/ProcessUtils.java b/src/main/java/com/google/devtools/build/lib/unix/ProcessUtils.java
new file mode 100644
index 0000000..5288e17
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/unix/ProcessUtils.java
@@ -0,0 +1,50 @@
+// Copyright 2014 Google Inc. 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.unix;
+
+import com.google.devtools.build.lib.UnixJniLoader;
+
+
+/**
+ * Various utilities related to UNIX processes.
+ */
+public final class ProcessUtils {
+
+ private ProcessUtils() {}
+
+ static {
+ UnixJniLoader.loadJni();
+ }
+
+ /**
+ * Native wrapper around POSIX getgid(2).
+ *
+ * @return the real group ID of the current process.
+ */
+ public static native int getgid();
+
+ /**
+ * Native wrapper around POSIX getpid(2) syscall.
+ *
+ * @return the process ID of this process.
+ */
+ public static native int getpid();
+
+ /**
+ * Native wrapper around POSIX getuid(2).
+ *
+ * @return the real user ID of the current process.
+ */
+ public static native int getuid();
+}
diff --git a/src/main/java/com/google/devtools/build/lib/util/AbruptExitException.java b/src/main/java/com/google/devtools/build/lib/util/AbruptExitException.java
new file mode 100644
index 0000000..ff62a7e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/AbruptExitException.java
@@ -0,0 +1,52 @@
+// Copyright 2014 Google Inc. 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;
+
+/**
+ * An exception thrown by various error conditions that are severe enough to halt the command (e.g.
+ * even a --keep_going build). These typically need to signal to the handling code what happened.
+ * Therefore, these exceptions contain a recommended ExitCode allowing the exception to "set" a
+ * returned numeric exit code.
+ *
+ * When an instance of this exception is thrown, Blaze will try to halt as soon as reasonably
+ * possible.
+ */
+public class AbruptExitException extends Exception {
+
+ private final ExitCode exitCode;
+
+ public AbruptExitException(String message, ExitCode exitCode) {
+ super(message);
+ this.exitCode = exitCode;
+ }
+
+ public AbruptExitException(String message, ExitCode exitCode, Throwable cause) {
+ super(message, cause);
+ this.exitCode = exitCode;
+ }
+
+ public AbruptExitException(ExitCode exitCode, Throwable cause) {
+ super(cause);
+ this.exitCode = exitCode;
+ }
+
+ public AbruptExitException(ExitCode exitCode) {
+ this.exitCode = exitCode;
+ }
+
+ public ExitCode getExitCode() {
+ return exitCode;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/util/AbstractIndexer.java b/src/main/java/com/google/devtools/build/lib/util/AbstractIndexer.java
new file mode 100644
index 0000000..4b61fe6
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/AbstractIndexer.java
@@ -0,0 +1,37 @@
+// Copyright 2014 Google Inc. 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 static java.nio.charset.StandardCharsets.UTF_8;
+
+/**
+ * Abstract class for string indexers.
+ */
+abstract class AbstractIndexer implements StringIndexer {
+
+ /**
+ * Conversion from String to byte[].
+ */
+ protected static byte[] string2bytes(String string) {
+ return string.getBytes(UTF_8);
+ }
+
+ /**
+ * Conversion from byte[] to String.
+ */
+ protected static String bytes2string(byte[] bytes) {
+ return new String(bytes, UTF_8);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/util/AnsiStrippingOutputStream.java b/src/main/java/com/google/devtools/build/lib/util/AnsiStrippingOutputStream.java
new file mode 100644
index 0000000..6c6b878
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/AnsiStrippingOutputStream.java
@@ -0,0 +1,176 @@
+// Copyright 2014 Google Inc. 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 java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * A pass-thru {@link OutputStream} that strips ANSI control codes.
+ */
+public class AnsiStrippingOutputStream extends OutputStream {
+ // The idea is straightforward: the regexp for ANSI control codes is
+ // \x1b\[[;0-9]*[a-zA-Z] . Implementing it as a stream is a little ugly,
+ // though.
+
+ private enum State {
+ NORMAL,
+ AFTER_ESCAPE,
+ PARAMETER,
+ }
+
+ private byte[] outputBuffer;
+ private int outputBufferPos;
+
+ private static final int ESCAPE_BUFFER_LENGTH = 128;
+ private byte[] escapeCodeBuffer;
+ private int escapeCodeBufferPos;
+ private OutputStream output;
+ private State state;
+
+ public AnsiStrippingOutputStream(OutputStream output) {
+ this.output = output;
+ escapeCodeBuffer = new byte[ESCAPE_BUFFER_LENGTH];
+ escapeCodeBufferPos = 0;
+ state = State.NORMAL;
+ }
+
+ @Override
+ public synchronized void write(int b) throws IOException {
+ // As per the contract of OutputStream.write(int)
+ byte[] array = { (byte) (b & 0xff) };
+ write(array, 0, 1);
+ }
+
+ @Override
+ public synchronized void write(byte b[], int off, int len) throws IOException {
+ int i = 0;
+ if (state == State.NORMAL) {
+
+ // Avoid outputBuffer allocation entirely if that's possible
+ while ((i < len) && (b[off + i] != 0x1b)) {
+ i++;
+ }
+ if (i == len) {
+ output.write(b, off, len);
+ return;
+ }
+ }
+
+ // In the worst case, the contents of the escape buffer and the contents
+ // of the input buffer are both copied to the output, so the length of the
+ // output buffer should be the sum of the length of both these buffers.
+ outputBuffer = new byte[len + ESCAPE_BUFFER_LENGTH];
+ System.arraycopy(b, off, outputBuffer, 0, i);
+ outputBufferPos = i;
+
+ for (; i < len; i++) {
+ processByte(b[off + i]);
+ }
+
+ try {
+ output.write(outputBuffer, 0, outputBufferPos);
+ } finally {
+ outputBuffer = null; // Make it possible to garbage collect the array
+ }
+ }
+
+ private void processByte(byte b) {
+ switch (state) {
+ case NORMAL:
+ if (escapeCodeBufferPos != 0) {
+ throw new IllegalStateException();
+ }
+ if (b == 0x1b) {
+ state = State.AFTER_ESCAPE;
+ addByteToEscapeBuffer(b);
+ } else {
+ dumpByte(b);
+ }
+ break;
+
+ case AFTER_ESCAPE:
+ if (b == '[') {
+ state = State.PARAMETER;
+ addByteToEscapeBuffer(b);
+ } else if (b == 0x1b) {
+ dumpEscapeBuffer();
+ state = State.AFTER_ESCAPE;
+ addByteToEscapeBuffer(b);
+ } else {
+ dumpEscapeBuffer();
+ dumpByte(b);
+ state = State.NORMAL;
+ }
+ break;
+
+ case PARAMETER:
+ if ((b >= '0' && b <= '9') || b == ';') {
+ // Parameter continues
+ addByteToEscapeBuffer(b);
+ } else if ((b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z')) {
+ // Found a control sequence, discard it and revert to normal state
+ discardEscapeBuffer();
+ state = State.NORMAL;
+ } else if (b == 0x1b) {
+ // Another escape sequence begins immediately after, and this is
+ // an illegal escape sequence
+ dumpEscapeBuffer();
+ state = State.AFTER_ESCAPE;
+ addByteToEscapeBuffer(b);
+ } else {
+ // Illegal control sequence, output it
+ dumpEscapeBuffer();
+ state = State.NORMAL;
+ }
+ break;
+ }
+ }
+
+ private void addByteToEscapeBuffer(byte b) {
+ escapeCodeBuffer[escapeCodeBufferPos++] = b;
+ if (escapeCodeBufferPos == ESCAPE_BUFFER_LENGTH) {
+ // Buffer full. Assume that no sane code emits an ANSI control code this
+ // long and revert to normal state.
+ dumpEscapeBuffer();
+ state = State.NORMAL;
+ }
+ }
+
+ private void discardEscapeBuffer() {
+ escapeCodeBufferPos = 0;
+ }
+
+ private void dumpByte(byte b) {
+ outputBuffer[outputBufferPos++] = b;
+ }
+
+ private void dumpEscapeBuffer() {
+ System.arraycopy(escapeCodeBuffer, 0,
+ outputBuffer, outputBufferPos, escapeCodeBufferPos);
+ outputBufferPos += escapeCodeBufferPos;
+ escapeCodeBufferPos = 0;
+ }
+
+ @Override
+ public void flush() throws IOException {
+ output.flush();
+ }
+
+ @Override
+ public void close() throws IOException {
+ output.close();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/util/BinaryPredicate.java b/src/main/java/com/google/devtools/build/lib/util/BinaryPredicate.java
new file mode 100644
index 0000000..c7709e2
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/BinaryPredicate.java
@@ -0,0 +1,38 @@
+// Copyright 2014 Google Inc. 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.base.Predicate;
+
+import javax.annotation.Nullable;
+
+/**
+ * A two-argument version of {@link Predicate} that determines a true or false value for pairs of
+ * inputs.
+ *
+ * <p>Just as a {@link Predicate} is useful for filtering iterables of values, a {@link
+ * BinaryPredicate} is useful for filtering iterables of paired values, like {@link
+ * java.util.Map.Entry} or {@link Pair}.
+ *
+ * <p>See {@link Predicate} for implementation notes and advice.
+ */
+public interface BinaryPredicate<X, Y> {
+
+ /**
+ * Applies this {@link BinaryPredicate} to the given objects.
+ *
+ * @return the value of this predicate when applied to inputs {@code x, y}
+ */
+ boolean apply(@Nullable X x, @Nullable Y y);
+}
diff --git a/src/main/java/com/google/devtools/build/lib/util/BlazeClock.java b/src/main/java/com/google/devtools/build/lib/util/BlazeClock.java
new file mode 100644
index 0000000..72806dd
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/BlazeClock.java
@@ -0,0 +1,51 @@
+// Copyright 2014 Google Inc. 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.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.util.Clock;
+import com.google.devtools.build.lib.util.JavaClock;
+
+/**
+ * Provides the clock implementation used by Blaze, which is {@link JavaClock}
+ * by default, but can be overridden at runtime. Note that if you set this
+ * clock, you also have to set the clock used by the Profiler.
+ */
+@ThreadSafe
+public abstract class BlazeClock {
+
+ private BlazeClock() {
+ }
+
+ private static volatile Clock instance = new JavaClock();
+
+ /**
+ * Returns singleton instance of the clock
+ */
+ public static Clock instance() {
+ return instance;
+ }
+
+ /**
+ * Overrides default clock instance.
+ */
+ public static synchronized void setClock(Clock clock) {
+ instance = clock;
+ }
+
+ public static long nanoTime() {
+ return instance().nanoTime();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/util/CanonicalStringIndexer.java b/src/main/java/com/google/devtools/build/lib/util/CanonicalStringIndexer.java
new file mode 100644
index 0000000..618e88b
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/CanonicalStringIndexer.java
@@ -0,0 +1,113 @@
+// Copyright 2014 Google Inc. 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.base.Preconditions;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.util.StringCanonicalizer;
+
+import java.util.Map;
+
+/**
+ * A string indexer backed by a map and reverse index lookup.
+ * Every unique string is stored in memory exactly once.
+ */
+@ThreadSafe
+public class CanonicalStringIndexer extends AbstractIndexer {
+
+ private static final int NOT_FOUND = -1;
+
+ // This is similar to (Synchronized) BiMap.
+ // These maps *must* be weakly threadsafe to ensure thread safety for string
+ // indexer as a whole. Specifically, mutating operations are serialized, but
+ // read-only operations may be executed concurrently with mutators.
+ private final Map<String, Integer> stringToInt;
+ private final Map<Integer, String> intToString;
+
+ /*
+ * Creates an indexer instance from two backing maps. These maps may be
+ * pre-initialized with data, but *must*:
+ * a. Support read-only operations done concurrently with mutations.
+ * Note that mutations will be serialized.
+ * b. Be reverse mappings of each other, if pre-initialized.
+ */
+ public CanonicalStringIndexer(Map<String, Integer> stringToInt,
+ Map<Integer, String> intToString) {
+ Preconditions.checkArgument(stringToInt.size() == intToString.size());
+ this.stringToInt = stringToInt;
+ this.intToString = intToString;
+ }
+
+
+ @Override
+ public synchronized void clear() {
+ stringToInt.clear();
+ intToString.clear();
+ }
+
+ @Override
+ public int size() {
+ return intToString.size();
+ }
+
+ @Override
+ public int getOrCreateIndex(String s) {
+ Integer i = stringToInt.get(s);
+ if (i == null) {
+ synchronized (this) {
+ // First, make sure another thread hasn't just added the entry:
+ i = stringToInt.get(s);
+ if (i != null) {
+ return i;
+ }
+
+ int ind = intToString.size();
+ s = StringCanonicalizer.intern(s);
+ stringToInt.put(s, ind);
+ intToString.put(ind, s);
+ return ind;
+ }
+ } else {
+ return i;
+ }
+ }
+
+ @Override
+ public int getIndex(String s) {
+ Integer i = stringToInt.get(s);
+ return (i == null) ? NOT_FOUND : i;
+ }
+
+ @Override
+ public synchronized boolean addString(String s) {
+ int originalSize = size();
+ getOrCreateIndex(s);
+ return (size() > originalSize);
+ }
+
+ @Override
+ public String getStringForIndex(int i) {
+ return intToString.get(i);
+ }
+
+ @Override
+ public synchronized String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("size = ").append(size()).append("\n");
+ for (Map.Entry<String, Integer> entry : stringToInt.entrySet()) {
+ builder.append(entry.getKey()).append(" <==> ").append(entry.getValue()).append("\n");
+ }
+ return builder.toString();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/util/Clock.java b/src/main/java/com/google/devtools/build/lib/util/Clock.java
new file mode 100644
index 0000000..878cb11
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/Clock.java
@@ -0,0 +1,33 @@
+// Copyright 2014 Google Inc. 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;
+
+/**
+ * This class provides an interface for a pluggable clock.
+ */
+public interface Clock {
+
+ /**
+ * Returns the current time in milliseconds. The milliseconds are counted from midnight
+ * Jan 1, 1970.
+ */
+ long currentTimeMillis();
+
+ /**
+ * Returns the current time in nanoseconds. The nanoseconds are measured relative to some
+ * unknown, but fixed event. Unfortunately, a sequence of calls to this method is *not*
+ * guaranteed to return non-decreasing values, so callers should be tolerant to this behavior.
+ */
+ long nanoTime();
+}
diff --git a/src/main/java/com/google/devtools/build/lib/util/CommandBuilder.java b/src/main/java/com/google/devtools/build/lib/util/CommandBuilder.java
new file mode 100644
index 0000000..372802d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/CommandBuilder.java
@@ -0,0 +1,176 @@
+// Copyright 2014 Google Inc. 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.CharMatcher;
+import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.devtools.build.lib.shell.Command;
+import com.google.devtools.build.lib.vfs.Path;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Implements OS aware {@link Command} builder. At this point only Linux, Mac
+ * and Windows XP are supported.
+ *
+ * <p>Builder will also apply heuristic to identify trivial cases where
+ * unix-like command lines could be automatically converted into the
+ * Windows-compatible form.
+ *
+ * <p>TODO(bazel-team): (2010) Some of the code here is very similar to the
+ * {@link com.google.devtools.build.lib.shell.Shell} class. This should be looked at.
+ */
+public final class CommandBuilder {
+
+ private static final List<String> SHELLS = ImmutableList.of("/bin/sh", "/bin/bash");
+
+ private static final Splitter ARGV_SPLITTER = Splitter.on(CharMatcher.anyOf(" \t"));
+
+ private final OS system;
+ private final List<String> argv = new ArrayList<>();
+ private final Map<String, String> env = new HashMap<>();
+ private File workingDir = null;
+ private boolean useShell = false;
+
+ public CommandBuilder() {
+ this(OS.getCurrent());
+ }
+
+ @VisibleForTesting
+ CommandBuilder(OS system) {
+ this.system = system;
+ }
+
+ public CommandBuilder addArg(String arg) {
+ Preconditions.checkNotNull(arg, "Argument must not be null");
+ argv.add(arg);
+ return this;
+ }
+
+ public CommandBuilder addArgs(Iterable<String> args) {
+ Preconditions.checkArgument(!Iterables.contains(args, null), "Arguments must not be null");
+ Iterables.addAll(argv, args);
+ return this;
+ }
+
+ public CommandBuilder addArgs(String... args) {
+ return addArgs(Arrays.asList(args));
+ }
+
+ public CommandBuilder addEnv(Map<String, String> env) {
+ Preconditions.checkNotNull(env);
+ this.env.putAll(env);
+ return this;
+ }
+
+ public CommandBuilder emptyEnv() {
+ env.clear();
+ return this;
+ }
+
+ public CommandBuilder setEnv(Map<String, String> env) {
+ emptyEnv();
+ addEnv(env);
+ return this;
+ }
+
+ public CommandBuilder setWorkingDir(Path path) {
+ Preconditions.checkNotNull(path);
+ workingDir = path.getPathFile();
+ return this;
+ }
+
+ public CommandBuilder useTempDir() {
+ workingDir = new File(System.getProperty("java.io.tmpdir"));
+ return this;
+ }
+
+ public CommandBuilder useShell(boolean useShell) {
+ this.useShell = useShell;
+ return this;
+ }
+
+ private boolean argvStartsWithSh() {
+ return argv.size() >= 2 && SHELLS.contains(argv.get(0)) && "-c".equals(argv.get(1));
+ }
+
+ private String[] transformArgvForLinux() {
+ // If command line already starts with "/bin/sh -c", ignore useShell attribute.
+ if (useShell && !argvStartsWithSh()) {
+ // c.g.io.base.shell.Shell.shellify() actually concatenates argv into the space-separated
+ // string here. Not sure why, but we will do the same.
+ return new String[] { "/bin/sh", "-c", Joiner.on(' ').join(argv) };
+ }
+ return argv.toArray(new String[argv.size()]);
+ }
+
+ private String[] transformArgvForWindows() {
+ List<String> modifiedArgv;
+ // Heuristic: replace "/bin/sh -c" with something more appropriate for Windows.
+ if (argvStartsWithSh()) {
+ useShell = true;
+ modifiedArgv = Lists.newArrayList(argv.subList(2, argv.size()));
+ } else {
+ modifiedArgv = Lists.newArrayList(argv);
+ }
+
+ if (!modifiedArgv.isEmpty()) {
+ // args can contain whitespace, so figure out the first word
+ String argv0 = modifiedArgv.get(0);
+ String command = ARGV_SPLITTER.split(argv0).iterator().next();
+
+ // Automatically enable CMD.EXE use if we are executing something else besides "*.exe" file.
+ if (!command.toLowerCase().endsWith(".exe")) {
+ useShell = true;
+ }
+ } else {
+ // This is degenerate "/bin/sh -c" case. We ensure that Windows behavior is identical
+ // to the Linux - call shell that will do nothing.
+ useShell = true;
+ }
+ if (useShell) {
+ // /S - strip first and last quotes and execute everything else as is.
+ // /E:ON - enable extended command set.
+ // /V:ON - enable delayed variable expansion
+ // /D - ignore AutoRun registry entries.
+ // /C - execute command. This must be the last option before the command itself.
+ return new String[] { "CMD.EXE", "/S", "/E:ON", "/V:ON", "/D", "/C",
+ "\"" + Joiner.on(' ').join(modifiedArgv) + "\"" };
+ } else {
+ return modifiedArgv.toArray(new String[argv.size()]);
+ }
+ }
+
+ public Command build() {
+ Preconditions.checkState(system != OS.UNKNOWN, "Unidentified operating system");
+ Preconditions.checkNotNull(workingDir, "Working directory must be set");
+ Preconditions.checkState(argv.size() > 0, "At least one argument is expected");
+
+ return new Command(
+ system == OS.WINDOWS ? transformArgvForWindows() : transformArgvForLinux(),
+ env, workingDir);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/util/CommandDescriptionForm.java b/src/main/java/com/google/devtools/build/lib/util/CommandDescriptionForm.java
new file mode 100644
index 0000000..8d37275
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/CommandDescriptionForm.java
@@ -0,0 +1,41 @@
+// Copyright 2014 Google Inc. 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;
+
+/**
+ * Forms in which a command can be described by {@link CommandFailureUtils#describeCommand}.
+ */
+public enum CommandDescriptionForm {
+ /**
+ * A form that is usually suitable for identifying the command but not for
+ * re-executing it. The working directory and environment are not shown, and
+ * the arguments are truncated to a maximum of a few hundred bytes.
+ */
+ ABBREVIATED,
+
+ /**
+ * A form that is complete and suitable for a user to copy and paste into a
+ * shell. On Linux, the command is placed in a subshell so it has no side
+ * effects on the user's shell. On Windows, this is not implemented, but the
+ * side effects in question are less severe (no "exec").
+ */
+ COMPLETE,
+
+ /**
+ * A form that is complete and does not isolate side effects. Suitable for
+ * launch scripts, i.e., "blaze run --script_path".
+ */
+ COMPLETE_UNISOLATED,
+}
diff --git a/src/main/java/com/google/devtools/build/lib/util/CommandFailureUtils.java b/src/main/java/com/google/devtools/build/lib/util/CommandFailureUtils.java
new file mode 100644
index 0000000..9178f985
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/CommandFailureUtils.java
@@ -0,0 +1,252 @@
+// Copyright 2014 Google Inc. 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.base.Preconditions;
+import com.google.common.collect.Ordering;
+
+import java.io.File;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.Map;
+
+import javax.annotation.Nullable;
+
+/**
+ * Utility methods for describing command failures.
+ * See also the CommandUtils class.
+ * Unlike that one, this class does not depend on Command;
+ * instead, it just manipulates command lines represented as
+ * Collection<String>.
+ */
+public class CommandFailureUtils {
+
+ // Interface that provides building blocks when describing command.
+ private interface DescribeCommandImpl {
+ void describeCommandBeginIsolate(StringBuilder message);
+ void describeCommandEndIsolate(StringBuilder message);
+ void describeCommandCwd(String cwd, StringBuilder message);
+ void describeCommandEnvPrefix(StringBuilder message);
+ void describeCommandEnvVar(StringBuilder message, Map.Entry<String, String> entry);
+ void describeCommandElement(StringBuilder message, String commandElement);
+ void describeCommandExec(StringBuilder message);
+ }
+
+ private static final class LinuxDescribeCommandImpl implements DescribeCommandImpl {
+
+ @Override
+ public void describeCommandBeginIsolate(StringBuilder message) {
+ message.append("(");
+ }
+
+ @Override
+ public void describeCommandEndIsolate(StringBuilder message) {
+ message.append(")");
+ }
+
+ @Override
+ public void describeCommandCwd(String cwd, StringBuilder message) {
+ message.append("cd ").append(ShellEscaper.escapeString(cwd)).append(" && \\\n ");
+ }
+
+ @Override
+ public void describeCommandEnvPrefix(StringBuilder message) {
+ message.append("env - \\\n ");
+ }
+
+ @Override
+ public void describeCommandEnvVar(StringBuilder message, Map.Entry<String, String> entry) {
+ message.append(ShellEscaper.escapeString(entry.getKey())).append('=')
+ .append(ShellEscaper.escapeString(entry.getValue())).append(" \\\n ");
+ }
+
+ @Override
+ public void describeCommandElement(StringBuilder message, String commandElement) {
+ message.append(ShellEscaper.escapeString(commandElement));
+ }
+
+ @Override
+ public void describeCommandExec(StringBuilder message) {
+ message.append("exec ");
+ }
+ }
+
+ // TODO(bazel-team): (2010) Add proper escaping. We can't use ShellUtils.shellEscape() as it is
+ // incompatible with CMD.EXE syntax, but something else might be needed.
+ private static final class WindowsDescribeCommandImpl implements DescribeCommandImpl {
+
+ @Override
+ public void describeCommandBeginIsolate(StringBuilder message) {
+ // TODO(bazel-team): Implement this.
+ }
+
+ @Override
+ public void describeCommandEndIsolate(StringBuilder message) {
+ // TODO(bazel-team): Implement this.
+ }
+
+ @Override
+ public void describeCommandCwd(String cwd, StringBuilder message) {
+ message.append("cd ").append(cwd).append("\n");
+ }
+
+ @Override
+ public void describeCommandEnvPrefix(StringBuilder message) { }
+
+ @Override
+ public void describeCommandEnvVar(StringBuilder message, Map.Entry<String, String> entry) {
+ message.append("SET ").append(entry.getKey()).append('=')
+ .append(entry.getValue()).append("\n ");
+ }
+
+ @Override
+ public void describeCommandElement(StringBuilder message, String commandElement) {
+ message.append(commandElement);
+ }
+
+ @Override
+ public void describeCommandExec(StringBuilder message) {
+ // TODO(bazel-team): Implement this if possible for greater efficiency.
+ }
+ }
+
+ private static final DescribeCommandImpl describeCommandImpl =
+ OS.getCurrent() == OS.WINDOWS ? new WindowsDescribeCommandImpl()
+ : new LinuxDescribeCommandImpl();
+
+ private CommandFailureUtils() {} // Prevent instantiation.
+
+ private static Comparator<Map.Entry<String, String>> mapEntryComparator =
+ new Comparator<Map.Entry<String, String>>() {
+ @Override
+ public int compare(Map.Entry<String, String> x, Map.Entry<String, String> y) {
+ // A map can never have two keys with the same value, so we only need to compare the keys.
+ return x.getKey().compareTo(y.getKey());
+ }
+ };
+
+ /**
+ * Construct a string that describes the command.
+ * Currently this returns a message of the form "foo bar baz",
+ * with shell meta-characters appropriately quoted and/or escaped,
+ * prefixed (if verbose is true) with an "env" command to set the environment.
+ *
+ * @param form Form of the command to generate; see the documentation of the
+ * {@link CommandDescriptionForm} values.
+ */
+ public static String describeCommand(CommandDescriptionForm form,
+ Collection<String> commandLineElements,
+ @Nullable Map<String, String> environment, @Nullable String cwd) {
+ Preconditions.checkNotNull(form);
+ final int APPROXIMATE_MAXIMUM_MESSAGE_LENGTH = 200;
+ StringBuilder message = new StringBuilder();
+ int size = commandLineElements.size();
+ int numberRemaining = size;
+ if (form == CommandDescriptionForm.COMPLETE) {
+ describeCommandImpl.describeCommandBeginIsolate(message);
+ }
+ if (form != CommandDescriptionForm.ABBREVIATED) {
+ if (cwd != null) {
+ describeCommandImpl.describeCommandCwd(cwd, message);
+ }
+ /*
+ * On Linux, insert an "exec" keyword to save a fork in "blaze run"
+ * generated scripts. If we use "env" as a wrapper, the "exec" needs to
+ * be applied to the entire "env" invocation.
+ *
+ * On Windows, this is a no-op.
+ */
+ describeCommandImpl.describeCommandExec(message);
+ /*
+ * Java does not provide any way to invoke a subprocess with the environment variables
+ * in a specified order. The order of environment variables in the 'environ' array
+ * (which is set by the 'envp' parameter to the execve() system call)
+ * is determined by the order of iteration on a HashMap constructed inside Java's
+ * ProcessBuilder class (in the ProcessEnvironment class), which is nondeterministic.
+ *
+ * Nevertheless, we *print* the environment variables here in sorted order, rather
+ * than in the potentially nondeterministic order that will be actually used.
+ * This is slightly dubious... in theory a process's behaviour could depend on the order
+ * of the environment variables passed to it. (For example, the order of environment
+ * variables in the environ array affects the output of '/usr/bin/env'.)
+ * However, in practice very few processes depend on the order of the environment
+ * variables, and using a deterministic sorted order here makes Blaze's output more
+ * deterministic and easier to read. So this seems the lesser of two evils... I think.
+ * Anyway, it's not like we have much choice... even if we wanted to, there's no way to
+ * print out the nondeterministic order that will actually be used, since there's
+ * no way to guarantee that the iteration over entrySet() here will return the same
+ * sequence as the iteration over entrySet() inside the ProcessBuilder class
+ * (in ProcessEnvironment.StringEnvironment.toEnvironmentBlock()).
+ */
+ if (environment != null) {
+ describeCommandImpl.describeCommandEnvPrefix(message);
+ for (Map.Entry<String, String> entry :
+ Ordering.from(mapEntryComparator).sortedCopy(environment.entrySet())) {
+ message.append(" ");
+ describeCommandImpl.describeCommandEnvVar(message, entry);
+ }
+ }
+ }
+ for (String commandElement : commandLineElements) {
+ if (form == CommandDescriptionForm.ABBREVIATED &&
+ message.length() + commandElement.length() > APPROXIMATE_MAXIMUM_MESSAGE_LENGTH) {
+ message.append(
+ " ... (remaining " + numberRemaining + " argument(s) skipped)");
+ break;
+ } else {
+ if (numberRemaining < size) {
+ message.append(' ');
+ }
+ describeCommandImpl.describeCommandElement(message, commandElement);
+ numberRemaining--;
+ }
+ }
+ if (form == CommandDescriptionForm.COMPLETE) {
+ describeCommandImpl.describeCommandEndIsolate(message);
+ }
+ return message.toString();
+ }
+
+ /**
+ * Construct an error message that describes a failed command invocation.
+ * Currently this returns a message of the form "error executing command foo
+ * bar baz".
+ */
+ public static String describeCommandError(boolean verbose,
+ Collection<String> commandLineElements,
+ Map<String, String> env, String cwd) {
+ CommandDescriptionForm form = verbose
+ ? CommandDescriptionForm.COMPLETE
+ : CommandDescriptionForm.ABBREVIATED;
+ return "error executing command " + (verbose ? "\n " : "")
+ + describeCommand(form, commandLineElements, env, cwd);
+ }
+
+ /**
+ * Construct an error message that describes a failed command invocation.
+ * Currently this returns a message of the form "foo failed: error executing
+ * command /dir/foo bar baz".
+ */
+ public static String describeCommandFailure(boolean verbose,
+ Collection<String> commandLineElements,
+ Map<String, String> env, String cwd) {
+ String commandName = commandLineElements.iterator().next();
+ // Extract the part of the command name after the last "/", if any.
+ String shortCommandName = new File(commandName).getName();
+ return shortCommandName + " failed: " +
+ describeCommandError(verbose, commandLineElements, env, cwd);
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/util/CommandUtils.java b/src/main/java/com/google/devtools/build/lib/util/CommandUtils.java
new file mode 100644
index 0000000..e6c0011
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/CommandUtils.java
@@ -0,0 +1,88 @@
+// Copyright 2014 Google Inc. 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.shell.AbnormalTerminationException;
+import com.google.devtools.build.lib.shell.Command;
+import com.google.devtools.build.lib.shell.CommandException;
+import com.google.devtools.build.lib.shell.CommandResult;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Map;
+
+/**
+ * Utility methods relating to the {@link Command} class.
+ */
+public class CommandUtils {
+
+ private CommandUtils() {} // Prevent instantiation.
+
+ private static Collection<String> commandLine(Command command) {
+ return Arrays.asList(command.getCommandLineElements());
+ }
+
+ private static Map<String, String> env(Command command) {
+ return command.getEnvironmentVariables();
+ }
+
+ private static String cwd(Command command) {
+ return command.getWorkingDirectory() == null ? null : command.getWorkingDirectory().getPath();
+ }
+
+ /**
+ * Construct an error message that describes a failed command invocation.
+ * Currently this returns a message of the form "error executing command foo
+ * bar baz".
+ */
+ public static String describeCommandError(boolean verbose, Command command) {
+ return CommandFailureUtils.describeCommandError(verbose, commandLine(command), env(command),
+ cwd(command));
+ }
+
+ /**
+ * Construct an error message that describes a failed command invocation.
+ * Currently this returns a message of the form "foo failed: error executing
+ * command /dir/foo bar baz".
+ */
+ public static String describeCommandFailure(boolean verbose, Command command) {
+ return CommandFailureUtils.describeCommandFailure(verbose, commandLine(command), env(command),
+ cwd(command));
+ }
+
+ /**
+ * Construct an error message that describes a failed command invocation.
+ * Currently this returns a message of the form "foo failed: error executing
+ * command /dir/foo bar baz: exception message", with the
+ * command's stdout and stderr output appended if available.
+ */
+ public static String describeCommandFailure(boolean verbose, CommandException exception) {
+ String message = describeCommandFailure(verbose, exception.getCommand()) + ": "
+ + exception.getMessage();
+ if (exception instanceof AbnormalTerminationException) {
+ CommandResult result = ((AbnormalTerminationException) exception).getResult();
+ try {
+ return message + "\n" +
+ new String(result.getStdout()) +
+ new String(result.getStderr());
+ } catch (IllegalStateException e) {
+ // This can happen if the command didn't save stdout/stderr,
+ // so ignore this exception and fall through to the ordinary case.
+ }
+ }
+ return message;
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/util/CompactStringIndexer.java b/src/main/java/com/google/devtools/build/lib/util/CompactStringIndexer.java
new file mode 100644
index 0000000..698758d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/CompactStringIndexer.java
@@ -0,0 +1,546 @@
+// Copyright 2014 Google Inc. 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 static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Lists;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadCompatible;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+
+import java.util.ArrayList;
+
+/**
+ * Provides memory-efficient bidirectional mapping String <-> unique integer.
+ * Uses byte-wise compressed prefix trie internally.
+ * <p>
+ * Class allows index retrieval for the given string, addition of the new
+ * index and string retrieval for the given index. It also allows efficient
+ * serialization of the internal data structures.
+ * <p>
+ * Internally class stores list of nodes with each node containing byte[]
+ * representation of compressed trie node:
+ * <pre>
+ * varint32 parentIndex; // index of the parent node
+ * varint32 keylen; // length of the node key
+ * byte[keylen] key; // node key data
+ * repeated jumpEntry { // Zero or more jump entries, referencing child nodes
+ * byte key // jump key (first byte of the child node key)
+ * varint32 nodeIndex // child index
+ * }
+ * <p>
+ * Note that jumpEntry key byte is actually duplicated in the child node
+ * instance. This is done to improve performance of the index->string
+ * lookup (so we can avoid jump table parsing during this lookup).
+ * <p>
+ * Root node of the trie must have parent id pointing to itself.
+ * <p>
+ * TODO(bazel-team): (2010) Consider more fine-tuned locking mechanism - e.g.
+ * distinguishing between read and write locks.
+ */
+@ThreadSafe
+public class CompactStringIndexer extends AbstractIndexer {
+
+ private static final int NOT_FOUND = -1;
+
+ private ArrayList<byte[]> nodes; // Compressed prefix trie nodes.
+ private int rootId; // Root node id.
+
+ /*
+ * Creates indexer instance.
+ */
+ public CompactStringIndexer (int expectedCapacity) {
+ Preconditions.checkArgument(expectedCapacity > 0);
+ nodes = Lists.newArrayListWithExpectedSize(expectedCapacity);
+ rootId = NOT_FOUND;
+ }
+
+ /**
+ * Allocates new node index. Must be called only from
+ * synchronized methods.
+ */
+ private int allocateIndex() {
+ nodes.add(null);
+ return nodes.size() - 1;
+ }
+
+ /**
+ * Replaces given node record with the new one. Must be called only from
+ * synchronized methods.
+ * <p>
+ * Subclasses can override this method to be notified when an update actually
+ * takes place.
+ */
+ @ThreadCompatible
+ protected void updateNode(int index, byte[] content) {
+ nodes.set(index, content);
+ }
+
+ /**
+ * Returns parent id for the given node content.
+ *
+ * @return parent node id
+ */
+ private int getParentId(byte[] content) {
+ int[] intHolder = new int[1];
+ VarInt.getVarInt(content, 0, intHolder);
+ return intHolder[0];
+ }
+
+ /**
+ * Creates new node using specified key suffix. Must be called from
+ * synchronized methods.
+ *
+ * @param parentNode parent node id
+ * @param key original key that is being added to the indexer
+ * @param offset node key offset in the original key.
+ *
+ * @return new node id corresponding to the given key
+ */
+ private int createNode(int parentNode, byte[] key, int offset) {
+ int index = allocateIndex();
+
+ int len = key.length - offset;
+ Preconditions.checkState(len >= 0);
+
+ // Content consists of parent id, key length and key. There are no jump records.
+ byte[] content = new byte[VarInt.varIntSize(parentNode) + VarInt.varIntSize(len) + len];
+ // Add parent id.
+ int contentOffset = VarInt.putVarInt(parentNode, content, 0);
+ // Add node key length.
+ contentOffset = VarInt.putVarInt(len, content, contentOffset);
+ // Add node key content.
+ System.arraycopy(key, offset, content, contentOffset, len);
+
+ updateNode(index, content);
+ return index;
+ }
+
+ /**
+ * Updates jump entry index in the given node.
+ *
+ * @param node node id to update
+ * @param oldIndex old jump entry index
+ * @param newIndex updated jump entry index
+ */
+ private void updateJumpEntry(int node, int oldIndex, int newIndex) {
+ byte[] content = nodes.get(node);
+ int[] intHolder = new int[1];
+ int offset = VarInt.getVarInt(content, 0, intHolder); // parent id
+ offset = VarInt.getVarInt(content, offset, intHolder); // key length
+ offset += intHolder[0]; // Offset now points to the first jump entry.
+ while (offset < content.length) {
+ int next = VarInt.getVarInt(content, offset + 1, intHolder); // jump index
+ if (intHolder[0] == oldIndex) {
+ // Substitute oldIndex value with newIndex.
+ byte[] newContent =
+ new byte[content.length + VarInt.varIntSize(newIndex) - VarInt.varIntSize(oldIndex)];
+ System.arraycopy(content, 0, newContent, 0, offset + 1);
+ offset = VarInt.putVarInt(newIndex, newContent, offset + 1);
+ System.arraycopy(content, next, newContent, offset, content.length - next);
+ updateNode(node, newContent);
+ return;
+ } else {
+ offset = next;
+ }
+ }
+ StringBuilder builder = new StringBuilder().append("Index ").append(oldIndex)
+ .append(" is not present in the node ").append(node).append(", ");
+ dumpNodeContent(builder, content);
+ throw new IllegalArgumentException(builder.toString());
+ }
+
+ /**
+ * Creates new branch node content at the predefined location, splitting
+ * prefix from the given node and optionally adding another child node
+ * jump entry.
+ *
+ * @param originalNode node that will be split
+ * @param newBranchNode new branch node id
+ * @param splitOffset offset at which to split original node key
+ * @param indexKey optional additional jump key
+ * @param childIndex optional additional jump index. Optional jump entry will
+ * be skipped if this index is set to NOT_FOUND.
+ */
+ private void createNewBranchNode(int originalNode, int newBranchNode, int splitOffset,
+ byte indexKey, int childIndex) {
+ byte[] content = nodes.get(originalNode);
+ int[] intHolder = new int[1];
+ int keyOffset = VarInt.getVarInt(content, 0, intHolder); // parent id
+
+ // If original node is a root node, new branch node will become new root. So set parent id
+ // appropriately (for root node it is set to the node's own id).
+ int parentIndex = (originalNode == intHolder[0] ? newBranchNode : intHolder[0]);
+
+ keyOffset = VarInt.getVarInt(content, keyOffset, intHolder); // key length
+ Preconditions.checkState(intHolder[0] >= splitOffset);
+ // Calculate new content size.
+ int newSize = VarInt.varIntSize(parentIndex)
+ + VarInt.varIntSize(splitOffset) + splitOffset
+ + 1 + VarInt.varIntSize(originalNode)
+ + (childIndex != NOT_FOUND ? 1 + VarInt.varIntSize(childIndex) : 0);
+ // New content consists of parent id, new key length, truncated key and two jump records.
+ byte[] newContent = new byte[newSize];
+ // Add parent id.
+ int contentOffset = VarInt.putVarInt(parentIndex, newContent, 0);
+ // Add adjusted key length.
+ contentOffset = VarInt.putVarInt(splitOffset, newContent, contentOffset);
+ // Add truncated key content and first jump key.
+ System.arraycopy(content, keyOffset, newContent, contentOffset, splitOffset + 1);
+ // Add index for the first jump key.
+ contentOffset = VarInt.putVarInt(originalNode, newContent, contentOffset + splitOffset + 1);
+ // If requested, add additional jump entry.
+ if (childIndex != NOT_FOUND) {
+ // Add second jump key.
+ newContent[contentOffset] = indexKey;
+ // Add index for the second jump key.
+ VarInt.putVarInt(childIndex, newContent, contentOffset + 1);
+ }
+ updateNode(newBranchNode, newContent);
+ }
+
+ /**
+ * Inject newly created branch node into the trie data structure. Method
+ * will update parent node jump entry to point to the new branch node (or
+ * will update root id if branch node becomes new root) and will truncate
+ * key prefix from the original node that was split (that prefix now
+ * resides in the branch node).
+ *
+ * @param originalNode node that will be split
+ * @param newBranchNode new branch node id
+ * @param commonPrefixLength how many bytes should be split into the new branch node.
+ */
+ private void injectNewBranchNode(int originalNode, int newBranchNode, int commonPrefixLength) {
+ byte[] content = nodes.get(originalNode);
+
+ int parentId = getParentId(content);
+ if (originalNode == parentId) {
+ rootId = newBranchNode; // update root index
+ } else {
+ updateJumpEntry(parentId, originalNode, newBranchNode);
+ }
+
+ // Truncate prefix from the original node and set its parent to the our new branch node.
+ int[] intHolder = new int[1];
+ int suffixOffset = VarInt.getVarInt(content, 0, intHolder); // parent id
+ suffixOffset = VarInt.getVarInt(content, suffixOffset, intHolder); // key length
+ int len = intHolder[0] - commonPrefixLength;
+ Preconditions.checkState(len >= 0);
+ suffixOffset += commonPrefixLength;
+ // New content consists of parent id, new key length and duplicated key suffix.
+ byte[] newContent = new byte[VarInt.varIntSize(newBranchNode) + VarInt.varIntSize(len) +
+ (content.length - suffixOffset)];
+ // Add parent id.
+ int contentOffset = VarInt.putVarInt(newBranchNode, newContent, 0);
+ // Add new key length.
+ contentOffset = VarInt.putVarInt(len, newContent, contentOffset);
+ // Add key and jump table.
+ System.arraycopy(content, suffixOffset, newContent, contentOffset,
+ content.length - suffixOffset);
+ updateNode(originalNode, newContent);
+ }
+
+ /**
+ * Adds new child node (that uses specified key suffix) to the given
+ * current node.
+ * Example:
+ * <pre>
+ * Had "ab". Adding "abcd".
+ *
+ * 1:"ab",'c'->2
+ * 1:"ab" -> \
+ * 2:"cd"
+ * </pre>
+ */
+ private int addChildNode(int parentNode, byte[] key, int keyOffset) {
+ int child = createNode(parentNode, key, keyOffset);
+
+ byte[] content = nodes.get(parentNode);
+ // Add jump table entry to the parent node.
+ int entryOffset = content.length;
+ // New content consists of original content and additional jump record.
+ byte[] newContent = new byte[entryOffset + 1 + VarInt.varIntSize(child)];
+ // Copy original content.
+ System.arraycopy(content, 0, newContent, 0, entryOffset);
+ // Add jump key.
+ newContent[entryOffset] = key[keyOffset];
+ // Add jump index.
+ VarInt.putVarInt(child, newContent, entryOffset + 1);
+
+ updateNode(parentNode, newContent);
+ return child;
+ }
+
+ /**
+ * Splits node into two at the specified offset.
+ * Example:
+ * <pre>
+ * Had "abcd". Adding "ab".
+ *
+ * 2:"ab",'c'->1
+ * 1:"abcd" -> \
+ * 1:"cd"
+ * </pre>
+ */
+ private int splitNodeSuffix(int nodeToSplit, int commonPrefixLength) {
+ int newBranchNode = allocateIndex();
+ // Create new node with truncated key.
+ createNewBranchNode(nodeToSplit, newBranchNode, commonPrefixLength, (byte) 0, NOT_FOUND);
+ injectNewBranchNode(nodeToSplit, newBranchNode, commonPrefixLength);
+
+ return newBranchNode;
+ }
+
+ /**
+ * Splits node into two at the specified offset and adds another leaf.
+ * Example:
+ * <pre>
+ * Had "abcd". Adding "abef".
+ *
+ * 3:"ab",'c'->1,'e'->2
+ * 1:"abcd" -> / \
+ * 1:"cd" 2:"ef"
+ * </pre>
+ */
+ private int addBranch(int nodeToSplit, byte[] key, int offset, int commonPrefixLength) {
+ int newBranchNode = allocateIndex();
+ int child = createNode(newBranchNode, key, offset + commonPrefixLength);
+ // Create new node with the truncated key and reference to the new child node.
+ createNewBranchNode(nodeToSplit, newBranchNode, commonPrefixLength,
+ key[offset + commonPrefixLength], child);
+ injectNewBranchNode(nodeToSplit, newBranchNode, commonPrefixLength);
+
+ return child;
+ }
+
+ private int findOrCreateIndexInternal(int node, byte[] key, int offset,
+ boolean createIfNotFound) {
+ byte[] content = nodes.get(node);
+ int[] intHolder = new int[1];
+ int contentOffset = VarInt.getVarInt(content, 0, intHolder); // parent id
+ contentOffset = VarInt.getVarInt(content, contentOffset, intHolder); // key length
+ int skyKeyLen = intHolder[0];
+ int remainingKeyLen = key.length - offset;
+ int minKeyLen = remainingKeyLen > skyKeyLen ? skyKeyLen : remainingKeyLen;
+
+ // Compare given key/offset content with the node key. Skip first key byte for recursive
+ // calls - this byte is equal to the byte in the jump entry and was already compared.
+ for (int i = (offset > 0 ? 1 : 0); i < minKeyLen; i++) { // compare key
+ if (key[offset + i] != content[contentOffset + i]) {
+ // Mismatch found somewhere in the middle of the node key. If requested, node
+ // should be split and another leaf added for the new key.
+ return createIfNotFound ? addBranch(node, key, offset, i) : NOT_FOUND;
+ }
+ }
+
+ if (remainingKeyLen > minKeyLen) {
+ // Node key matched portion of the key - find appropriate jump entry. If found - recursion.
+ // If not - mismatch (we will add new child node if requested).
+ contentOffset += skyKeyLen;
+ while (contentOffset < content.length) {
+ if (key[offset + skyKeyLen] == content[contentOffset]) { // compare index value
+ VarInt.getVarInt(content, contentOffset + 1, intHolder);
+ // Found matching jump entry - recursively compare the child.
+ return findOrCreateIndexInternal(intHolder[0], key, offset + skyKeyLen,
+ createIfNotFound);
+ } else {
+ // Jump entry key does not match. Skip rest of the entry data.
+ contentOffset = VarInt.getVarInt(content, contentOffset + 1, intHolder);
+ }
+ }
+ // There are no matching jump entries - report mismatch or create a new leaf if necessary.
+ return createIfNotFound ? addChildNode(node, key, offset + skyKeyLen) : NOT_FOUND;
+ } else if (skyKeyLen > minKeyLen) {
+ // Key suffix is a subset of the node key. Report mismatch or split the node if requested).
+ return createIfNotFound ? splitNodeSuffix(node, minKeyLen) : NOT_FOUND;
+ } else {
+ // Node key exactly matches key suffix - return associated index value.
+ return node;
+ }
+ }
+
+ private synchronized int findOrCreateIndex(byte[] key, boolean createIfNotFound) {
+ if (rootId == NOT_FOUND) {
+ // Root node does not seem to exist - create it if needed.
+ if (createIfNotFound) {
+ rootId = createNode(0, key, 0);
+ Preconditions.checkState(rootId == 0);
+ return 0;
+ } else {
+ return NOT_FOUND;
+ }
+ }
+ return findOrCreateIndexInternal(rootId, key, 0, createIfNotFound);
+ }
+
+ private byte[] reconstructKeyInternal(int node, int suffixSize) {
+ byte[] content = nodes.get(node);
+ Preconditions.checkNotNull(content);
+ int[] intHolder = new int[1];
+ int contentOffset = VarInt.getVarInt(content, 0, intHolder); // parent id
+ int parentNode = intHolder[0];
+ contentOffset = VarInt.getVarInt(content, contentOffset, intHolder); // key length
+ int len = intHolder[0];
+ byte[] key;
+ if (node != parentNode) {
+ // We haven't reached root node yet. Make a recursive call, adjusting suffix length.
+ key = reconstructKeyInternal(parentNode, suffixSize + len);
+ } else {
+ // We are in a root node. Finally allocate array for the key. It will be filled up
+ // on our way back from recursive call tree.
+ key = new byte[suffixSize + len];
+ }
+ // Fill appropriate portion of the full key with the node key content.
+ System.arraycopy(content, contentOffset, key, key.length - suffixSize - len, len);
+ return key;
+ }
+
+ private byte[] reconstructKey(int node) {
+ return reconstructKeyInternal(node, 0);
+ }
+
+ /* (non-Javadoc)
+ * @see com.google.devtools.build.lib.util.StringIndexer#clear()
+ */
+ @Override
+ public synchronized void clear() {
+ nodes.clear();
+ }
+
+ /* (non-Javadoc)
+ * @see com.google.devtools.build.lib.util.StringIndexer#size()
+ */
+ @Override
+ public synchronized int size() {
+ return nodes.size();
+ }
+
+ protected int getOrCreateIndexForBytes(byte[] bytes) {
+ return findOrCreateIndex(bytes, true);
+ }
+
+ protected synchronized boolean addBytes(byte[] bytes) {
+ int count = nodes.size();
+ int index = getOrCreateIndexForBytes(bytes);
+ return index >= count;
+ }
+
+ protected int getIndexForBytes(byte[] bytes) {
+ return findOrCreateIndex(bytes, false);
+ }
+
+ /* (non-Javadoc)
+ * @see com.google.devtools.build.lib.util.StringIndexer#getOrCreateIndex(java.lang.String)
+ */
+ @Override
+ public int getOrCreateIndex(String s) {
+ return getOrCreateIndexForBytes(string2bytes(s));
+ }
+
+ /* (non-Javadoc)
+ * @see com.google.devtools.build.lib.util.StringIndexer#getIndex(java.lang.String)
+ */
+ @Override
+ public int getIndex(String s) {
+ return getIndexForBytes(string2bytes(s));
+ }
+
+ /* (non-Javadoc)
+ * @see com.google.devtools.build.lib.util.StringIndexer#addString(java.lang.String)
+ */
+ @Override
+ public boolean addString(String s) {
+ return addBytes(string2bytes(s));
+ }
+
+ protected synchronized byte[] getBytesForIndex(int i) {
+ Preconditions.checkArgument(i >= 0);
+ if (i >= nodes.size()) {
+ return null;
+ }
+ return reconstructKey(i);
+ }
+
+ /* (non-Javadoc)
+ * @see com.google.devtools.build.lib.util.StringIndexer#getStringForIndex(int)
+ */
+ @Override
+ public String getStringForIndex(int i) {
+ byte[] bytes = getBytesForIndex(i);
+ return bytes != null ? bytes2string(bytes) : null;
+ }
+
+ private void dumpNodeContent(StringBuilder builder, byte[] content) {
+ int[] intHolder = new int[1];
+ int offset = VarInt.getVarInt(content, 0, intHolder);
+ builder.append("parent: ").append(intHolder[0]);
+ offset = VarInt.getVarInt(content, offset, intHolder);
+ int len = intHolder[0];
+ builder.append(", len: ").append(len).append(", key: \"")
+ .append(new String(content, offset, len, UTF_8)).append('"');
+ offset += len;
+ while (offset < content.length) {
+ builder.append(", '").append(new String(content, offset, 1, UTF_8)).append("': ");
+ offset = VarInt.getVarInt(content, offset + 1, intHolder);
+ builder.append(intHolder[0]);
+ }
+ builder.append(", size: ").append(content.length);
+ }
+
+ private int dumpContent(StringBuilder builder, int node, int indent, boolean[] seen) {
+ for(int i = 0; i < indent; i++) {
+ builder.append(" ");
+ }
+ builder.append(node).append(": ");
+ if (node >= nodes.size()) {
+ builder.append("OUT_OF_BOUNDS\n");
+ return 0;
+ } else if (seen[node]) {
+ builder.append("ALREADY_SEEN\n");
+ return 0;
+ }
+ seen[node] = true;
+ byte[] content = nodes.get(node);
+ if (content == null) {
+ builder.append("NULL\n");
+ return 0;
+ }
+ dumpNodeContent(builder, content);
+ builder.append("\n");
+ int contentSize = content.length;
+
+ int[] intHolder = new int[1];
+ int contentOffset = VarInt.getVarInt(content, 0, intHolder); // parent id
+ contentOffset = VarInt.getVarInt(content, contentOffset, intHolder); // key length
+ contentOffset += intHolder[0];
+ while (contentOffset < content.length) {
+ contentOffset = VarInt.getVarInt(content, contentOffset + 1, intHolder);
+ contentSize += dumpContent(builder, intHolder[0], indent + 1, seen);
+ }
+ return contentSize;
+ }
+
+ @Override
+ public synchronized String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("size = ").append(nodes.size()).append("\n");
+ if (nodes.size() > 0) {
+ int contentSize = dumpContent(builder, rootId, 0, new boolean[nodes.size()]);
+ builder.append("contentSize = ").append(contentSize).append("\n");
+ }
+ return builder.toString();
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/util/DependencySet.java b/src/main/java/com/google/devtools/build/lib/util/DependencySet.java
new file mode 100644
index 0000000..788037d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/DependencySet.java
@@ -0,0 +1,225 @@
+// Copyright 2014 Google Inc. 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.base.Preconditions;
+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 {
+ return process(FileSystemUtils.readContent(dotdFile));
+ }
+
+ /**
+ * 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) {
+ // 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();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/util/ExitCode.java b/src/main/java/com/google/devtools/build/lib/util/ExitCode.java
new file mode 100644
index 0000000..8307538
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/ExitCode.java
@@ -0,0 +1,181 @@
+// Copyright 2014 Google Inc. 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.base.Objects;
+
+import java.util.Collection;
+import java.util.HashMap;
+
+/**
+ * <p>Anything marked FAILURE is generally from a problem with the source code
+ * under consideration. In these cases, a re-run in an identical client should
+ * produce an identical return code all things being constant.
+ *
+ * <p>Anything marked as an ERROR is generally a problem unrelated to the
+ * source code itself. It is either something wrong with the user's command
+ * line or the user's machine or environment.
+ *
+ * <p>Note that these exit codes should be kept consistent with the codes
+ * returned by Blaze's launcher in //devtools/blaze/main:blaze.cc
+ */
+public class ExitCode {
+ // Tracks all exit codes defined here and elsewhere in Bazel.
+ private static final HashMap<Integer, ExitCode> exitCodeRegistry = new HashMap<>();
+
+ public static final ExitCode SUCCESS = ExitCode.create(0, "SUCCESS");
+ public static final ExitCode BUILD_FAILURE = ExitCode.create(1, "BUILD_FAILURE");
+ public static final ExitCode PARSING_FAILURE = ExitCode.createUnregistered(1, "PARSING_FAILURE");
+ public static final ExitCode COMMAND_LINE_ERROR = ExitCode.create(2, "COMMAND_LINE_ERROR");
+ public static final ExitCode TESTS_FAILED = ExitCode.create(3, "TESTS_FAILED");
+ public static final ExitCode PARTIAL_ANALYSIS_FAILURE =
+ ExitCode.createUnregistered(3, "PARTIAL_ANALYSIS_FAILURE");
+ public static final ExitCode NO_TESTS_FOUND = ExitCode.create(4, "NO_TESTS_FOUND");
+ public static final ExitCode RUN_FAILURE = ExitCode.create(6, "RUN_FAILURE");
+ public static final ExitCode ANALYSIS_FAILURE = ExitCode.create(7, "ANALYSIS_FAILURE");
+ public static final ExitCode INTERRUPTED = ExitCode.create(8, "INTERRUPTED");
+ public static final ExitCode OOM_ERROR = ExitCode.createInfrastructureFailure(33, "OOM_ERROR");
+ public static final ExitCode LOCAL_ENVIRONMENTAL_ERROR =
+ ExitCode.createInfrastructureFailure(36, "LOCAL_ENVIRONMENTAL_ERROR");
+ public static final ExitCode BLAZE_INTERNAL_ERROR =
+ ExitCode.createInfrastructureFailure(37, "BLAZE_INTERNAL_ERROR");
+ public static final ExitCode RESERVED = ExitCode.createInfrastructureFailure(40, "RESERVED");
+ /*
+ exit codes [50..60] and 253 are reserved for site specific wrappers to Bazel.
+ */
+
+ /**
+ * Creates and returns an ExitCode. Requires a unique exit code number.
+ *
+ * @param code the int value for this exit code
+ * @param name a human-readable description
+ */
+ public static ExitCode create(int code, String name) {
+ return new ExitCode(code, name, /*infrastructureFailure=*/false, /*register=*/true);
+ }
+
+ /**
+ * Creates and returns an ExitCode that represents an infrastructure failure.
+ *
+ * @param code the int value for this exit code
+ * @param name a human-readable description
+ */
+ public static ExitCode createInfrastructureFailure(int code, String name) {
+ return new ExitCode(code, name, /*infrastructureFailure=*/true, /*register=*/true);
+ }
+
+ /**
+ * Creates and returns an ExitCode that has the same numeric code as another ExitCode. This is to
+ * allow the duplicate error codes listed above to be registered, but is private to prevent other
+ * users from creating duplicate error codes in the future.
+ *
+ * @param code the int value for this exit code
+ * @param name a human-readable description
+ */
+ private static ExitCode createUnregistered(int code, String name) {
+ return new ExitCode(code, name, /*infrastructureFailure=*/false, /*register=*/false);
+ }
+
+ /**
+ * Add the given exit code to the registry.
+ *
+ * @param exitCode the exit code to register
+ * @throws IllegalStateException if the numeric exit code is already in the registry.
+ */
+ private static void register(ExitCode exitCode) {
+ synchronized (exitCodeRegistry) {
+ int codeNum = exitCode.getNumericExitCode();
+ if (exitCodeRegistry.containsKey(codeNum)) {
+ throw new IllegalStateException(
+ "Exit code " + codeNum + " (" + exitCode.name + ") already registered");
+ }
+ exitCodeRegistry.put(codeNum, exitCode);
+ }
+ }
+
+ /**
+ * Returns all registered ExitCodes.
+ */
+ public static Collection<ExitCode> values() {
+ synchronized (exitCodeRegistry) {
+ return exitCodeRegistry.values();
+ }
+ }
+
+ private final int numericExitCode;
+ private final String name;
+ private final boolean infrastructureFailure;
+
+ /**
+ * Whenever a new exit code is created, it is registered (to prevent exit codes with identical
+ * numeric codes from being created). However, there are some exit codes in this file that have
+ * duplicate numeric codes, so these are not registered.
+ */
+ private ExitCode(int exitCode, String name, boolean infrastructureFailure, boolean register) {
+ this.numericExitCode = exitCode;
+ this.name = name;
+ this.infrastructureFailure = infrastructureFailure;
+ if (register) {
+ ExitCode.register(this);
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(numericExitCode, name, infrastructureFailure);
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (object instanceof ExitCode) {
+ ExitCode that = (ExitCode) object;
+ return this.numericExitCode == that.numericExitCode
+ && this.name.equals(that.name)
+ && this.infrastructureFailure == that.infrastructureFailure;
+ }
+ return false;
+ }
+
+ /**
+ * Returns the human-readable name for this exit code. Not guaranteed to be stable, use the
+ * numeric exit code for that.
+ */
+ @Override
+ public String toString() {
+ return name;
+ }
+
+ /**
+ * Returns the error's int value.
+ */
+ public int getNumericExitCode() {
+ return numericExitCode;
+ }
+
+ /**
+ * Returns the human-readable name.
+ */
+ public String name() {
+ return name;
+ }
+
+ /**
+ * Returns true if the current exit code represents a failure of Blaze infrastructure,
+ * vs. a build failure.
+ */
+ public boolean isInfrastructureFailure() {
+ return infrastructureFailure;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/util/FileType.java b/src/main/java/com/google/devtools/build/lib/util/FileType.java
new file mode 100644
index 0000000..c91b17b
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/FileType.java
@@ -0,0 +1,278 @@
+// Copyright 2014 Google Inc. 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.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * A base class for FileType matchers.
+ */
+@Immutable
+public abstract class FileType implements Predicate<String> {
+ // A special file type
+ public static final FileType NO_EXTENSION = new FileType() {
+ @Override
+ public boolean apply(String filename) {
+ return filename.lastIndexOf('.') == -1;
+ }
+ };
+
+ public static FileType of(final String ext) {
+ return new FileType() {
+ @Override
+ public boolean apply(String filename) {
+ return filename.endsWith(ext);
+ }
+ @Override
+ public List<String> getExtensions() {
+ return ImmutableList.of(ext);
+ }
+ };
+ }
+
+ public static FileType of(final Iterable<String> extensions) {
+ return new FileType() {
+ @Override
+ public boolean apply(String filename) {
+ for (String ext : extensions) {
+ if (filename.endsWith(ext)) {
+ return true;
+ }
+ }
+ return false;
+ }
+ @Override
+ public List<String> getExtensions() {
+ return ImmutableList.copyOf(extensions);
+ }
+ };
+ }
+
+ public static FileType of(final String... extensions) {
+ return of(Arrays.asList(extensions));
+ }
+
+ @Override
+ public String toString() {
+ return getExtensions().toString();
+ }
+
+ /**
+ * Returns true if the the filename matches. The filename should be a basename (the filename
+ * component without a path) for performance reasons.
+ */
+ @Override
+ public abstract boolean apply(String filename);
+
+ /**
+ * Get a list of filename extensions this matcher handles. The first entry in the list (if
+ * available) is the primary extension that code can use to construct output file names.
+ * The list can be empty for some matchers.
+ *
+ * @return a list of filename extensions
+ */
+ public List<String> getExtensions() {
+ return ImmutableList.of();
+ }
+
+ /** Return true if a file name is matched by the FileType */
+ public boolean matches(String filename) {
+ int slashIndex = filename.lastIndexOf('/');
+ if (slashIndex != -1) {
+ filename = filename.substring(slashIndex + 1);
+ }
+ return apply(filename);
+ }
+
+ /** Return true if a file referred by path is matched by the FileType */
+ public boolean matches(Path path) {
+ return apply(path.getBaseName());
+ }
+
+ /** Return true if a file referred by fragment is matched by the FileType */
+ public boolean matches(PathFragment fragment) {
+ return apply(fragment.getBaseName());
+ }
+
+ // Check FileTypes
+
+ /**
+ * An interface for entities that have a filename.
+ */
+ public interface HasFilename {
+ /**
+ * Returns the filename of this entity.
+ */
+ String getFilename();
+ }
+
+ /**
+ * Checks whether an Iterable<? extends HasFileType> contains any of the specified file types.
+ *
+ * <p>At least one FileType must be specified.
+ */
+ public static <T extends HasFilename> boolean contains(final Iterable<T> items,
+ FileType... fileTypes) {
+ Preconditions.checkState(fileTypes.length > 0, "Must specify at least one file type");
+ final FileTypeSet fileTypeSet = FileTypeSet.of(fileTypes);
+ for (T item : items) {
+ if (fileTypeSet.matches(item.getFilename())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Checks whether a HasFileType is any of the specified file types.
+ *
+ * <p>At least one FileType must be specified.
+ */
+ public static <T extends HasFilename> boolean contains(T item, FileType... fileTypes) {
+ return FileTypeSet.of(fileTypes).matches(item.getFilename());
+ }
+
+
+ private static <T extends HasFilename> Predicate<T> typeMatchingPredicateFor(
+ final FileType matchingType) {
+ return new Predicate<T>() {
+ @Override
+ public boolean apply(T item) {
+ return matchingType.matches(item.getFilename());
+ }
+ };
+ }
+
+ private static <T extends HasFilename> Predicate<T> typeMatchingPredicateFor(
+ final FileTypeSet matchingTypes) {
+ return new Predicate<T>() {
+ @Override
+ public boolean apply(T item) {
+ return matchingTypes.matches(item.getFilename());
+ }
+ };
+ }
+
+ private static <T extends HasFilename> Predicate<T> typeMatchingPredicateFrom(
+ final Predicate<String> fileTypePredicate) {
+ return new Predicate<T>() {
+ @Override
+ public boolean apply(T item) {
+ return fileTypePredicate.apply(item.getFilename());
+ }
+ };
+ }
+
+ /**
+ * A filter for Iterable<? extends HasFileType> that returns only those whose FileType matches the
+ * specified Predicate.
+ */
+ public static <T extends HasFilename> Iterable<T> filter(final Iterable<T> items,
+ final Predicate<String> predicate) {
+ return Iterables.filter(items, typeMatchingPredicateFrom(predicate));
+ }
+
+ /**
+ * A filter for Iterable<? extends HasFileType> that returns only those of the specified file
+ * types.
+ */
+ public static <T extends HasFilename> Iterable<T> filter(final Iterable<T> items,
+ FileType... fileTypes) {
+ return filter(items, FileTypeSet.of(fileTypes));
+ }
+
+ /**
+ * A filter for Iterable<? extends HasFileType> that returns only those of the specified file
+ * types.
+ */
+ public static <T extends HasFilename> Iterable<T> filter(final Iterable<T> items,
+ FileTypeSet fileTypes) {
+ return Iterables.filter(items, typeMatchingPredicateFor(fileTypes));
+ }
+
+ /**
+ * A filter for Iterable<? extends HasFileType> that returns only those of the specified file
+ * type.
+ */
+ public static <T extends HasFilename> Iterable<T> filter(final Iterable<T> items,
+ FileType fileType) {
+ return Iterables.filter(items, typeMatchingPredicateFor(fileType));
+ }
+
+ /**
+ * A filter for Iterable<? extends HasFileType> that returns everything except the specified file
+ * type.
+ */
+ public static <T extends HasFilename> Iterable<T> except(final Iterable<T> items,
+ FileType fileType) {
+ return Iterables.filter(items, Predicates.not(typeMatchingPredicateFor(fileType)));
+ }
+
+
+ /**
+ * A filter for List<? extends HasFileType> that returns only those of the specified file types.
+ * The result is a mutable list, computed eagerly; see {@link #filter} for a lazy variant.
+ */
+ public static <T extends HasFilename> List<T> filterList(final Iterable<T> items,
+ FileType... fileTypes) {
+ if (fileTypes.length > 0) {
+ return filterList(items, FileTypeSet.of(fileTypes));
+ } else {
+ return new ArrayList<>();
+ }
+ }
+
+ /**
+ * A filter for List<? extends HasFileType> that returns only those of the specified file type.
+ * The result is a mutable list, computed eagerly.
+ */
+ public static <T extends HasFilename> List<T> filterList(final Iterable<T> items,
+ final FileType fileType) {
+ List<T> result = new ArrayList<>();
+ for (T item : items) {
+ if (fileType.matches(item.getFilename())) {
+ result.add(item);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * A filter for List<? extends HasFileType> that returns only those of the specified file types.
+ * The result is a mutable list, computed eagerly.
+ */
+ public static <T extends HasFilename> List<T> filterList(final Iterable<T> items,
+ final FileTypeSet fileTypeSet) {
+ List<T> result = new ArrayList<>();
+ for (T item : items) {
+ if (fileTypeSet.matches(item.getFilename())) {
+ result.add(item);
+ }
+ }
+ return result;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/util/FileTypeSet.java b/src/main/java/com/google/devtools/build/lib/util/FileTypeSet.java
new file mode 100644
index 0000000..694e877
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/FileTypeSet.java
@@ -0,0 +1,139 @@
+// Copyright 2014 Google Inc. 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.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * A set of FileTypes for grouped matching.
+ */
+@Immutable
+public class FileTypeSet implements Predicate<String> {
+ private final ImmutableSet<FileType> types;
+
+ /** A set that matches all files. */
+ public static final FileTypeSet ANY_FILE =
+ new FileTypeSet() {
+ @Override
+ public String toString() {
+ return "any files";
+ }
+ @Override
+ public boolean matches(String filename) {
+ return true;
+ }
+ @Override
+ public List<String> getExtensions() {
+ return ImmutableList.<String>of();
+ }
+ };
+
+ /** A predicate that matches no files. */
+ public static final FileTypeSet NO_FILE =
+ new FileTypeSet(ImmutableList.<FileType>of()) {
+ @Override
+ public String toString() {
+ return "no files";
+ }
+ @Override
+ public boolean matches(String filename) {
+ return false;
+ }
+ };
+
+ private FileTypeSet() {
+ this.types = null;
+ }
+
+ private FileTypeSet(FileType... fileTypes) {
+ this.types = ImmutableSet.copyOf(fileTypes);
+ }
+
+ private FileTypeSet(Iterable<FileType> fileTypes) {
+ this.types = ImmutableSet.copyOf(fileTypes);
+ }
+
+ /**
+ * Returns a set that matches only the provided {@code fileTypes}.
+ *
+ * <p>If {@code fileTypes} is empty, the returned predicate will match no files.
+ */
+ public static FileTypeSet of(FileType... fileTypes) {
+ if (fileTypes.length == 0) {
+ return FileTypeSet.NO_FILE;
+ } else {
+ return new FileTypeSet(fileTypes);
+ }
+ }
+
+ /**
+ * Returns a set that matches only the provided {@code fileTypes}.
+ *
+ * <p>If {@code fileTypes} is empty, the returned predicate will match no files.
+ */
+ public static FileTypeSet of(Iterable<FileType> fileTypes) {
+ if (Iterables.isEmpty(fileTypes)) {
+ return FileTypeSet.NO_FILE;
+ } else {
+ return new FileTypeSet(fileTypes);
+ }
+ }
+
+ /** Returns true if the filename can be matched by any FileType in this set. */
+ public boolean matches(String filename) {
+ int slashIndex = filename.lastIndexOf('/');
+ if (slashIndex != -1) {
+ filename = filename.substring(slashIndex + 1);
+ }
+ for (FileType type : types) {
+ if (type.apply(filename)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** Returns true if this predicate matches nothing. */
+ public boolean isNone() {
+ return this == FileTypeSet.NO_FILE;
+ }
+
+ @Override
+ public boolean apply(String filename) {
+ return matches(filename);
+ }
+
+ /** Returns the list of possible file extensions for this file type. Can be empty. */
+ public List<String> getExtensions() {
+ List<String> extensions = new ArrayList<>();
+ for (FileType type : types) {
+ extensions.addAll(type.getExtensions());
+ }
+ return extensions;
+ }
+
+ @Override
+ public String toString() {
+ return StringUtil.joinEnglishList(getExtensions());
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/util/Fingerprint.java b/src/main/java/com/google/devtools/build/lib/util/Fingerprint.java
new file mode 100644
index 0000000..e4c0876
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/Fingerprint.java
@@ -0,0 +1,319 @@
+// Copyright 2014 Google Inc. 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 static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * Simplified wrapper for MD5 message digests. See also
+ * com.google.math.crypto.MD5HMAC for a similar interface.
+ *
+ * @see java.security.MessageDigest
+ */
+public final class Fingerprint {
+
+ private final MessageDigest md;
+
+ /**
+ * Creates and initializes a new MD5 object; if this fails, Java must be
+ * installed incorrectly.
+ */
+ public Fingerprint() {
+ try {
+ md = MessageDigest.getInstance("md5");
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException("MD5 not available");
+ }
+ }
+
+ /**
+ * Completes the hash computation by doing final operations, e.g., padding.
+ *
+ * <p>This method has the side-effect of resetting the underlying digest computer.
+ *
+ * @return the MD5 digest as a 16-byte array
+ * @see java.security.MessageDigest#digest()
+ */
+ public byte[] digestAndReset() {
+ return md.digest();
+ }
+
+ /**
+ * Completes the hash computation and returns the digest as a string.
+ *
+ * <p>This method has the side-effect of resetting the underlying digest computer.
+ *
+ * @return the MD5 digest as a 32-character string of hexadecimal digits
+ * @see com.google.math.crypto.MD5HMAC#toString()
+ */
+ public String hexDigestAndReset() {
+ return hexDigest(digestAndReset());
+ }
+
+ /**
+ * Returns a string representation of an MD5 digest.
+ *
+ * @param digest the MD5 digest, perhaps from a previous call to digest
+ * @return the digest as a 32-character string of hexadecimal digits
+ */
+ public static String hexDigest(byte[] digest) {
+ StringBuilder b = new StringBuilder(32);
+ for (int i = 0; i < digest.length; i++) {
+ int n = digest[i];
+ b.append("0123456789abcdef".charAt((n >> 4) & 0xF));
+ b.append("0123456789abcdef".charAt(n & 0xF));
+ }
+ return b.toString();
+ }
+
+ /**
+ * Override of Object.toString to return a string for the MD5 digest without
+ * finalizing the digest computation. Calling hexDigest() instead will
+ * finalize the digest computation.
+ *
+ * @return the string returned by hexDigest()
+ */
+ @Override
+ public String toString() {
+ try {
+ // MD5 does support cloning, so this should not fail
+ return hexDigest(((MessageDigest) md.clone()).digest());
+ } catch (CloneNotSupportedException e) {
+ // MessageDigest does not support cloning,
+ // so just return the toString() on the MessageDigest.
+ return md.toString();
+ }
+ }
+
+ /**
+ * Updates the digest with 0 or more bytes.
+ *
+ * @param input the array of bytes with which to update the digest
+ * @see java.security.MessageDigest#update(byte[])
+ */
+ public Fingerprint addBytes(byte[] input) {
+ md.update(input);
+ return this;
+ }
+
+ /**
+ * Updates the digest with the specified number of bytes starting at offset.
+ *
+ * @param input the array of bytes with which to update the digest
+ * @param offset the offset into the array
+ * @param len the number of bytes to use
+ * @see java.security.MessageDigest#update(byte[], int, int)
+ */
+ public Fingerprint addBytes(byte[] input, int offset, int len) {
+ md.update(input, offset, len);
+ return this;
+ }
+
+ /**
+ * Updates the digest with a boolean value.
+ */
+ public Fingerprint addBoolean(boolean input) {
+ addBytes(new byte[] { (byte) (input ? 1 : 0) });
+ return this;
+ }
+
+ /**
+ * Updates the digest with the little-endian bytes of a given int value.
+ *
+ * @param input the integer with which to update the digest
+ */
+ public Fingerprint addInt(int input) {
+ md.update(new byte[] {
+ (byte) input,
+ (byte) (input >> 8),
+ (byte) (input >> 16),
+ (byte) (input >> 24),
+ });
+
+ return this;
+ }
+
+ /**
+ * Updates the digest with the little-endian bytes of a given long value.
+ *
+ * @param input the long with which to update the digest
+ */
+ public Fingerprint addLong(long input) {
+ md.update(new byte[]{
+ (byte) input,
+ (byte) (input >> 8),
+ (byte) (input >> 16),
+ (byte) (input >> 24),
+ (byte) (input >> 32),
+ (byte) (input >> 40),
+ (byte) (input >> 48),
+ (byte) (input >> 56),
+ });
+
+ return this;
+ }
+
+ /**
+ * Updates the digest with a UUID.
+ *
+ * @param uuid the UUID with which to update the digest. Must not be null.
+ */
+ public Fingerprint addUUID(UUID uuid) {
+ addLong(uuid.getLeastSignificantBits());
+ addLong(uuid.getMostSignificantBits());
+ return this;
+ }
+
+ /**
+ * Updates the digest with a String using its length plus its UTF8 encoded bytes.
+ *
+ * @param input the String with which to update the digest
+ * @see java.security.MessageDigest#update(byte[])
+ */
+ public Fingerprint addString(String input) {
+ byte[] bytes = input.getBytes(UTF_8);
+ addInt(bytes.length);
+ md.update(bytes);
+ return this;
+ }
+
+ /**
+ * Updates the digest with a String using its length and content.
+ *
+ * @param input the String with which to update the digest
+ * @see java.security.MessageDigest#update(byte[])
+ */
+ public Fingerprint addStringLatin1(String input) {
+ addInt(input.length());
+ byte[] bytes = new byte[input.length()];
+ for (int i = 0; i < input.length(); i++) {
+ bytes[i] = (byte) input.charAt(i);
+ }
+ md.update(bytes);
+ return this;
+ }
+
+ /**
+ * Updates the digest with a Path.
+ *
+ * @param input the Path with which to update the digest.
+ */
+ public Fingerprint addPath(Path input) {
+ addStringLatin1(input.getPathString());
+ return this;
+ }
+
+ /**
+ * Updates the digest with a Path.
+ *
+ * @param input the Path with which to update the digest.
+ */
+ public Fingerprint addPath(PathFragment input) {
+ addStringLatin1(input.getPathString());
+ return this;
+ }
+
+ /**
+ * Updates the digest with inputs by iterating over them and invoking
+ * {@code #addString(String)} on each element.
+ *
+ * @param inputs the inputs with which to update the digest
+ */
+ public Fingerprint addStrings(Iterable<String> inputs) {
+ addInt(Iterables.size(inputs));
+ for (String input : inputs) {
+ addString(input);
+ }
+
+ return this;
+ }
+
+ /**
+ * Updates the digest with inputs by iterating over them and invoking
+ * {@code #addString(String)} on each element.
+ *
+ * @param inputs the inputs with which to update the digest
+ */
+ public Fingerprint addStrings(String... inputs) {
+ addInt(inputs.length);
+ for (String input : inputs) {
+ addString(input);
+ }
+
+ return this;
+ }
+
+ /**
+ * Updates the digest with inputs which are pairs in a map, by iterating over
+ * the map entries and invoking {@code #addString(String)} on each key and
+ * value.
+ *
+ * @param inputs the inputs in a map with which to update the digest
+ */
+ public Fingerprint addStringMap(Map<String, String> inputs) {
+ addInt(inputs.size());
+ for (Map.Entry<String, String> entry : inputs.entrySet()) {
+ addString(entry.getKey());
+ addString(entry.getValue());
+ }
+
+ return this;
+ }
+
+ /**
+ * Updates the digest with a list of paths by iterating over them and
+ * invoking {@link #addPath(PathFragment)} on each element.
+ *
+ * @param inputs the paths with which to update the digest
+ */
+ public Fingerprint addPaths(Iterable<PathFragment> inputs) {
+ addInt(Iterables.size(inputs));
+ for (PathFragment path : inputs) {
+ addPath(path);
+ }
+
+ return this;
+ }
+
+ /**
+ * Reset the Fingerprint for additional use as though previous digesting had not been done.
+ */
+ public void reset() {
+ md.reset();
+ }
+
+ // -------- Convenience methods ----------------------------
+
+ /**
+ * Computes the hex digest from a String using UTF8 encoding and returning
+ * the hexDigest().
+ *
+ * @param input the String from which to compute the digest
+ */
+ public static String md5Digest(String input) {
+ Fingerprint f = new Fingerprint();
+ f.addBytes(input.getBytes(UTF_8));
+ return f.hexDigestAndReset();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/util/GroupedList.java b/src/main/java/com/google/devtools/build/lib/util/GroupedList.java
new file mode 100644
index 0000000..2bf956d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/GroupedList.java
@@ -0,0 +1,344 @@
+// Copyright 2014 Google Inc. 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.base.Objects;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.collect.CompactHashSet;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Encapsulates a list of lists. Is intended to be used in "batch" mode -- to set the value of a
+ * GroupedList, users should first construct a {@link GroupedListHelper}, add elements to it, and
+ * then {@link #append} the helper to a new GroupedList instance. The generic type T <i>must not</i>
+ * be a {@link List}.
+ *
+ * <p>Despite the "list" name, it is an error for the same element to appear multiple times in the
+ * list. Users are responsible for not trying to add the same element to a GroupedList twice.
+ */
+public class GroupedList<T> implements Iterable<Iterable<T>> {
+ // Total number of items in the list. At least elements.size(), but might be larger if there are
+ // any nested lists.
+ private int size = 0;
+ // Items in this GroupedList. Each element is either of type T or List<T>.
+ // Non-final only for #remove.
+ private List<Object> elements;
+
+ public GroupedList() {
+ // We optimize for small lists.
+ this.elements = new ArrayList<>(1);
+ }
+
+ // Only for use when uncompressing a GroupedList.
+ private GroupedList(int size, List<Object> elements) {
+ this.size = size;
+ this.elements = new ArrayList<>(elements);
+ }
+
+ /** Appends the list constructed in helper to this list. */
+ public void append(GroupedListHelper<T> helper) {
+ Preconditions.checkState(helper.currentGroup == null, "%s %s", this, helper);
+ // Do a check to make sure we don't have lists here. Note that if helper.elements is empty,
+ // Iterables.getFirst will return null, and null is not instanceof List.
+ Preconditions.checkState(!(Iterables.getFirst(helper.elements, null) instanceof List),
+ "Cannot make grouped list of lists: %s", helper);
+ elements.addAll(helper.groupedList);
+ size += helper.size();
+ }
+
+ /**
+ * Removes the elements in toRemove from this list. Takes time proportional to the size of the
+ * list, so should not be called often.
+ */
+ public void remove(Set<T> toRemove) {
+ elements = remove(elements, toRemove);
+ size -= toRemove.size();
+ }
+
+ /** Returns the number of elements in this list. */
+ public int size() {
+ return size;
+ }
+
+ /** Returns true if this list contains no elements. */
+ public boolean isEmpty() {
+ return elements.isEmpty();
+ }
+
+ private static final Object EMPTY_LIST = new Object();
+
+ public Object compress() {
+ switch (size()) {
+ case 0:
+ return EMPTY_LIST;
+ case 1:
+ return Iterables.getOnlyElement(elements);
+ default:
+ return elements.toArray();
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ public Set<T> toSet() {
+ ImmutableSet.Builder<T> builder = ImmutableSet.builder();
+ for (Object obj : elements) {
+ if (obj instanceof List) {
+ builder.addAll((List<T>) obj);
+ } else {
+ builder.add((T) obj);
+ }
+ }
+ return builder.build();
+ }
+
+ private static int sizeOf(Object obj) {
+ return obj instanceof List ? ((List<?>) obj).size() : 1;
+ }
+
+ public static <E> GroupedList<E> create(Object compressed) {
+ if (compressed == EMPTY_LIST) {
+ return new GroupedList<>();
+ }
+ if (compressed.getClass().isArray()) {
+ List<Object> elements = new ArrayList<>();
+ int size = 0;
+ for (Object item : (Object[]) compressed) {
+ size += sizeOf(item);
+ elements.add(item);
+ }
+ return new GroupedList<>(size, elements);
+ }
+ // Just a single element.
+ return new GroupedList<>(1, ImmutableList.<Object>of(compressed));
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == null) {
+ return false;
+ }
+ if (this.getClass() != other.getClass()) {
+ return false;
+ }
+ GroupedList<?> that = (GroupedList<?>) other;
+ return elements.equals(that.elements);
+ }
+
+ @Override
+ @SuppressWarnings("deprecation")
+ public String toString() {
+ return Objects.toStringHelper(this)
+ .add("elements", elements)
+ .add("size", size).toString();
+
+ }
+
+ /**
+ * Iterator that returns the next group in elements for each call to {@link #next}. A custom
+ * iterator is needed here because, to optimize memory, we store single-element lists as elements
+ * internally, and so they must be wrapped before they're returned.
+ */
+ private class GroupedIterator implements Iterator<Iterable<T>> {
+ private final Iterator<Object> iter = elements.iterator();
+
+ @Override
+ public boolean hasNext() {
+ return iter.hasNext();
+ }
+
+ @SuppressWarnings("unchecked") // Cast of Object to List<T> or T.
+ @Override
+ public Iterable<T> next() {
+ Object obj = iter.next();
+ if (obj instanceof List) {
+ return (List<T>) obj;
+ }
+ return ImmutableList.of((T) obj);
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ @Override
+ public Iterator<Iterable<T>> iterator() {
+ return new GroupedIterator();
+ }
+
+ /**
+ * Removes everything in toRemove from the list of lists, elements. Called both by GroupedList and
+ * GroupedListHelper.
+ */
+ private static <E> List<Object> remove(List<Object> elements, Set<E> toRemove) {
+ int removedCount = 0;
+ // elements.size is an upper bound of the needed size. Since normally removal happens just
+ // before the list is finished and compressed, optimizing this size isn't a concern.
+ List<Object> newElements = new ArrayList<>(elements.size());
+ for (Object obj : elements) {
+ if (obj instanceof List) {
+ ImmutableList.Builder<E> newGroup = new ImmutableList.Builder<>();
+ @SuppressWarnings("unchecked")
+ List<E> oldGroup = (List<E>) obj;
+ for (E elt : oldGroup) {
+ if (toRemove.contains(elt)) {
+ removedCount++;
+ } else {
+ newGroup.add(elt);
+ }
+ }
+ ImmutableList<E> group = newGroup.build();
+ addItem(group, newElements);
+ } else {
+ if (toRemove.contains(obj)) {
+ removedCount++;
+ } else {
+ newElements.add(obj);
+ }
+ }
+ }
+ Preconditions.checkState(removedCount == toRemove.size(),
+ "%s %s %s %s", removedCount, removedCount, elements, newElements);
+ return newElements;
+ }
+
+ private static void addItem(Collection<?> item, List<Object> elements) {
+ switch (item.size()) {
+ case 0:
+ return;
+ case 1:
+ elements.add(Iterables.getOnlyElement(item));
+ return;
+ default:
+ elements.add(ImmutableList.copyOf(item));
+ }
+ }
+
+ /**
+ * Builder-like object for GroupedLists. An already-existing grouped list is appended to by
+ * constructing a helper, mutating it, and then appending that helper to the grouped list.
+ */
+ public static class GroupedListHelper<E> implements Iterable<E> {
+ // Non-final only for removal.
+ private List<Object> groupedList;
+ private List<E> currentGroup = null;
+ private final Set<E> elements = CompactHashSet.create();
+
+ private GroupedListHelper(GroupedList<E> groupedList) {
+ this.groupedList = new ArrayList<>(groupedList.elements);
+ for (Iterable<E> group : groupedList) {
+ Iterables.addAll(elements, group);
+ }
+ }
+
+ public GroupedListHelper() {
+ // Optimize for short lists.
+ groupedList = new ArrayList<>(1);
+ }
+
+ /**
+ * Add an element to this list. If in a group, will be added to the current group. Otherwise,
+ * goes in a group of its own.
+ */
+ public void add(E elt) {
+ Preconditions.checkState(elements.add(elt), "%s %s", elt, this);
+ if (currentGroup == null) {
+ groupedList.add(elt);
+ } else {
+ currentGroup.add(elt);
+ }
+ }
+
+ /**
+ * Remove all elements of toRemove from this list. It is a fatal error if any elements of
+ * toRemove are not present. Takes time proportional to the size of the list, so should not be
+ * called often.
+ */
+ public void remove(Set<E> toRemove) {
+ groupedList = GroupedList.remove(groupedList, toRemove);
+ int oldSize = size();
+ elements.removeAll(toRemove);
+ Preconditions.checkState(oldSize == size() + toRemove.size(),
+ "%s %s %s", oldSize, toRemove, this);
+ }
+
+ /**
+ * Starts a group. All elements added until {@link #endGroup} will be in the same group. Each
+ * call of {@link #startGroup} must be paired with a following {@link #endGroup} call.
+ */
+ public void startGroup() {
+ Preconditions.checkState(currentGroup == null, this);
+ currentGroup = new ArrayList<>();
+ }
+
+ private void addList(Collection<E> group) {
+ addItem(group, groupedList);
+ }
+
+ /** Ends a group started with {@link #startGroup}. */
+ public void endGroup() {
+ Preconditions.checkNotNull(currentGroup);
+ addList(currentGroup);
+ currentGroup = null;
+ }
+
+ /** Returns true if elt is present in the list. */
+ public boolean contains(E elt) {
+ return elements.contains(elt);
+ }
+
+ private int size() {
+ return elements.size();
+ }
+
+ /** Returns true if list is empty. */
+ public boolean isEmpty() {
+ return elements.isEmpty();
+ }
+
+ @Override
+ public Iterator<E> iterator() {
+ return elements.iterator();
+ }
+
+ /** Create a GroupedListHelper from a collection of elements, all put in the same group.*/
+ public static <F> GroupedListHelper<F> create(Collection<F> elements) {
+ GroupedListHelper<F> helper = new GroupedListHelper<>();
+ helper.addList(elements);
+ helper.elements.addAll(elements);
+ Preconditions.checkState(helper.elements.size() == elements.size(),
+ "%s %s", helper, elements);
+ return helper;
+ }
+
+ @Override
+ @SuppressWarnings("deprecation")
+ public String toString() {
+ return Objects.toStringHelper(this)
+ .add("groupedList", groupedList)
+ .add("elements", elements)
+ .add("currentGroup", currentGroup).toString();
+
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/util/IncludeScanningUtil.java b/src/main/java/com/google/devtools/build/lib/util/IncludeScanningUtil.java
new file mode 100644
index 0000000..24f55e4
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/IncludeScanningUtil.java
@@ -0,0 +1,48 @@
+// Copyright 2014 Google Inc. 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.Constants;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+/**
+ * Static utilities for include scanning.
+ */
+public class IncludeScanningUtil {
+ private IncludeScanningUtil() {
+ }
+
+ private static final String INCLUDES_SUFFIX = ".includes";
+ public static final PathFragment GREPPED_INCLUDES =
+ new PathFragment(Constants.PRODUCT_NAME + "-out/_grepped_includes");
+
+ /**
+ * Returns the exec-root relative output path for grepped includes.
+ *
+ * @param srcExecPath the exec-root relative path of the source file.
+ */
+ public static PathFragment getExecRootRelativeOutputPath(PathFragment srcExecPath) {
+ return GREPPED_INCLUDES.getRelative(getRootRelativeOutputPath(srcExecPath));
+ }
+
+ /**
+ * Returns the root relative output path for grepped includes.
+ *
+ * @param srcExecPath the exec-root relative path of the source file.
+ */
+ public static PathFragment getRootRelativeOutputPath(PathFragment srcExecPath) {
+ return srcExecPath.replaceName(srcExecPath.getBaseName() + INCLUDES_SUFFIX);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/util/JavaClock.java b/src/main/java/com/google/devtools/build/lib/util/JavaClock.java
new file mode 100644
index 0000000..bdd1116
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/JavaClock.java
@@ -0,0 +1,36 @@
+// Copyright 2014 Google Inc. 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;
+
+/**
+ * Class provides a simple clock implementation used by the tool. By default it uses {@link System}
+ * class.
+ */
+public class JavaClock implements Clock {
+
+ public JavaClock() {
+ }
+
+ @Override
+ public long currentTimeMillis() {
+ return System.currentTimeMillis();
+ }
+
+ @Override
+ public long nanoTime() {
+ // Note that some JVM implementations of System#nanoTime don't yield a non-decreasing
+ // sequence of values.
+ return System.nanoTime();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/util/LazyString.java b/src/main/java/com/google/devtools/build/lib/util/LazyString.java
new file mode 100644
index 0000000..0e037b2
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/LazyString.java
@@ -0,0 +1,41 @@
+// Copyright 2014 Google Inc. 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;
+
+/**
+ * This class serves as a base implementation for a {@code CharSequence}
+ * that delay string construction (mostly till the execution phase).
+ *
+ * They are not full implementations, they lack {@code #charAt(int)} and
+ * {@code #subSequence(int, int)}.
+ */
+public abstract class LazyString implements CharSequence {
+
+ @Override
+ public char charAt(int index) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int length() {
+ return toString().length();
+ }
+
+ @Override
+ public CharSequence subSequence(int start, int end) {
+ throw new UnsupportedOperationException();
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/util/LoggingUtil.java b/src/main/java/com/google/devtools/build/lib/util/LoggingUtil.java
new file mode 100644
index 0000000..5170727
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/LoggingUtil.java
@@ -0,0 +1,87 @@
+// Copyright 2014 Google Inc. 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.base.Preconditions;
+import com.google.common.util.concurrent.Uninterruptibles;
+import com.google.devtools.build.lib.concurrent.ThreadSafety;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+import java.util.logging.Logger;
+
+/**
+ * Logging utilities for sending log messages to a remote service. Log messages
+ * will not be output anywhere else, including the terminal and blaze clients.
+ */
+@ThreadSafety.ThreadSafe
+public final class LoggingUtil {
+ // TODO(bazel-team): this class is a thin wrapper around Logger and could probably be discarded.
+ private static Future<Logger> remoteLogger;
+
+ /**
+ * Installs the remote logger.
+ *
+ * <p>This can only be called once, and the caller should not keep the
+ * reference to the logger.
+ *
+ * @param logger The logger future. Must have already started.
+ */
+ public static synchronized void installRemoteLogger(Future<Logger> logger) {
+ Preconditions.checkState(remoteLogger == null);
+ remoteLogger = logger;
+ }
+
+ /** Returns the installed logger, or null if none is installed. */
+ public static synchronized Logger getRemoteLogger() {
+ try {
+ return (remoteLogger == null) ? null : Uninterruptibles.getUninterruptibly(remoteLogger);
+ } catch (ExecutionException e) {
+ throw new RuntimeException("Unexpected error initializing remote logging", e);
+ }
+ }
+
+ /**
+ * @see #logToRemote(Level, String, Throwable, String...).
+ */
+ public static void logToRemote(Level level, String msg, Throwable trace) {
+ Logger logger = getRemoteLogger();
+ if (logger != null) {
+ logger.log(level, msg, trace);
+ }
+ }
+
+ /**
+ * Log a message to the remote backend. This is done out of thread, so this
+ * method is non-blocking.
+ *
+ * @param level The severity level. Non null.
+ * @param msg The log message. Non null.
+ * @param trace The stack trace. May be null.
+ * @param values Additional values to upload.
+ */
+ public static void logToRemote(Level level, String msg, Throwable trace,
+ String... values) {
+ Logger logger = getRemoteLogger();
+ if (logger != null) {
+ LogRecord logRecord = new LogRecord(level, msg);
+ logRecord.setThrown(trace);
+ logRecord.setParameters(values);
+ logger.log(logRecord);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/util/NetUtil.java b/src/main/java/com/google/devtools/build/lib/util/NetUtil.java
new file mode 100644
index 0000000..498da77
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/NetUtil.java
@@ -0,0 +1,38 @@
+// Copyright 2014 Google Inc. 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 java.net.InetAddress;
+import java.net.UnknownHostException;
+
+/**
+ * Various utility methods for network related stuff.
+ */
+public final class NetUtil {
+
+ private NetUtil() {
+ }
+
+ /**
+ * Returns the short hostname or <code>unknown</code> if the host name could
+ * not be determined.
+ */
+ public static String findShortHostName() {
+ try {
+ return InetAddress.getLocalHost().getHostName();
+ } catch (UnknownHostException e) {
+ return "unknown";
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/util/OS.java b/src/main/java/com/google/devtools/build/lib/util/OS.java
new file mode 100644
index 0000000..b19bd7e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/OS.java
@@ -0,0 +1,43 @@
+// Copyright 2014 Google Inc. 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;
+
+/**
+ * An operating system.
+ */
+public enum OS {
+ DARWIN,
+ LINUX,
+ WINDOWS,
+ UNKNOWN;
+
+ /**
+ * The current operating system.
+ */
+ public static OS getCurrent() {
+ return HOST_SYSTEM;
+ }
+ // We inject a the OS name through blaze.os, so we can have
+ // some coverage for Windows specific code on Linux.
+ private static String getOsName() {
+ String override = System.getProperty("blaze.os");
+ return override == null ? System.getProperty("os.name") : override;
+ }
+
+ private static final OS HOST_SYSTEM =
+ "Mac OS X".equals(getOsName()) ? OS.DARWIN : (
+ "Linux".equals(getOsName()) ? OS.LINUX : (
+ getOsName().contains("Windows") ? OS.WINDOWS : OS.UNKNOWN));
+}
+
diff --git a/src/main/java/com/google/devtools/build/lib/util/OptionsUtils.java b/src/main/java/com/google/devtools/build/lib/util/OptionsUtils.java
new file mode 100644
index 0000000..11bf94f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/OptionsUtils.java
@@ -0,0 +1,154 @@
+// Copyright 2014 Google Inc. 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.PathFragment;
+import com.google.devtools.common.options.Converter;
+import com.google.devtools.common.options.Converters;
+import com.google.devtools.common.options.OptionsParser.UnparsedOptionValueDescription;
+import com.google.devtools.common.options.OptionsParsingException;
+import com.google.devtools.common.options.OptionsProvider;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Blaze-specific option utilities.
+ */
+public final class OptionsUtils {
+
+ /**
+ * Returns a string representation of the non-hidden specified options; option values are
+ * shell-escaped.
+ */
+ public static String asShellEscapedString(Iterable<UnparsedOptionValueDescription> optionsList) {
+ StringBuffer result = new StringBuffer();
+ for (UnparsedOptionValueDescription option : optionsList) {
+ if (option.isHidden()) {
+ continue;
+ }
+ if (result.length() != 0) {
+ result.append(' ');
+ }
+ String value = option.getUnparsedValue();
+ if (option.isBooleanOption()) {
+ boolean isEnabled = false;
+ try {
+ isEnabled = new Converters.BooleanConverter().convert(value);
+ } catch (OptionsParsingException e) {
+ throw new RuntimeException("Unexpected parsing exception", e);
+ }
+ result.append(isEnabled ? "--" : "--no").append(option.getName());
+ } else {
+ result.append("--").append(option.getName());
+ if (value != null) { // Can be null for Void options.
+ result.append("=").append(ShellEscaper.escapeString(value));
+ }
+ }
+ }
+ return result.toString();
+ }
+
+ /**
+ * Returns a string representation of the non-hidden explicitly or implicitly
+ * specified options; option values are shell-escaped.
+ */
+ public static String asShellEscapedString(OptionsProvider options) {
+ return asShellEscapedString(options.asListOfUnparsedOptions());
+ }
+
+ /**
+ * Returns a string representation of the non-hidden explicitly or implicitly
+ * specified options, filtering out any sensitive options; option values are
+ * shell-escaped.
+ */
+ public static String asFilteredShellEscapedString(OptionsProvider options,
+ Iterable<UnparsedOptionValueDescription> optionsList) {
+ return asShellEscapedString(optionsList);
+ }
+
+ /**
+ * Returns a string representation of the non-hidden explicitly or implicitly
+ * specified options, filtering out any sensitive options; option values are
+ * shell-escaped.
+ */
+ public static String asFilteredShellEscapedString(OptionsProvider options) {
+ return asFilteredShellEscapedString(options, options.asListOfUnparsedOptions());
+ }
+
+ /**
+ * Converter from String to PathFragment.
+ */
+ public static class PathFragmentConverter
+ implements Converter<PathFragment> {
+
+ @Override
+ public PathFragment convert(String input) {
+ return new PathFragment(input);
+ }
+
+ @Override
+ public String getTypeDescription() {
+ return "a path";
+ }
+ }
+
+ /**
+ * Converter from String to PathFragment.
+ *
+ * <p>Complains if the path is not absolute.
+ */
+ public static class AbsolutePathFragmentConverter
+ implements Converter<PathFragment> {
+
+ @Override
+ public PathFragment convert(String input) throws OptionsParsingException {
+ PathFragment pathFragment = new PathFragment(input);
+ if (!pathFragment.isAbsolute()) {
+ throw new OptionsParsingException("Expected absolute path, found " + input);
+ }
+ return pathFragment;
+ }
+
+ @Override
+ public String getTypeDescription() {
+ return "an absolute path";
+ }
+ }
+
+ /**
+ * Converts from a colon-separated list of strings into a list of PathFragment instances.
+ */
+ public static class PathFragmentListConverter
+ implements Converter<List<PathFragment>> {
+
+ @Override
+ public List<PathFragment> convert(String input) {
+ List<PathFragment> list = new ArrayList<>();
+ for (String piece : input.split(":")) {
+ if (!piece.equals("")) {
+ list.add(new PathFragment(piece));
+ }
+ }
+ return Collections.unmodifiableList(list);
+ }
+
+ @Override
+ public String getTypeDescription() {
+ return "a colon-separated list of paths";
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/util/OsUtils.java b/src/main/java/com/google/devtools/build/lib/util/OsUtils.java
new file mode 100644
index 0000000..fa12f59
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/OsUtils.java
@@ -0,0 +1,74 @@
+// Copyright 2014 Google Inc. 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.PathFragment;
+
+/**
+ * Operating system-specific utilities.
+ */
+public final class OsUtils {
+
+ private static final String EXECUTABLE_EXTENSION = OS.getCurrent() == OS.WINDOWS ? ".exe" : "";
+
+ // Utility class.
+ private OsUtils() {
+ }
+
+ /**
+ * Returns the extension used for executables on the current platform (.exe
+ * for Windows, empty string for others).
+ */
+ public static String executableExtension() {
+ return EXECUTABLE_EXTENSION;
+ }
+
+ /**
+ * Loads JNI libraries, if necessary under the current platform.
+ */
+ public static void maybeForceJNI(PathFragment installBase) {
+ if (jniLibsAvailable()) {
+ forceJNI(installBase);
+ }
+ }
+
+ private static boolean jniLibsAvailable() {
+ // JNI libraries work fine on Windows, but at the moment we are not using any.
+ return OS.getCurrent() != OS.WINDOWS;
+ }
+
+ // Force JNI linking at a moment when we have 'installBase' handy, and print
+ // an informative error if it fails.
+ private static void forceJNI(PathFragment installBase) {
+ try {
+ ProcessUtils.getpid(); // force JNI initialization
+ } catch (UnsatisfiedLinkError t) {
+ System.err.println("JNI initialization failed: " + t.getMessage() + ". "
+ + "Possibly your installation has been corrupted; "
+ + "if this problem persists, try 'rm -fr " + installBase + "'.");
+ throw t;
+ }
+ }
+
+ /**
+ * Returns the PID of the current process, or -1 if not available.
+ */
+ public static int getpid() {
+ if (jniLibsAvailable()) {
+ return ProcessUtils.getpid();
+ }
+ return -1;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/util/Pair.java b/src/main/java/com/google/devtools/build/lib/util/Pair.java
new file mode 100644
index 0000000..a377c3c
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/Pair.java
@@ -0,0 +1,122 @@
+// Copyright 2014 Google Inc. 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.base.Function;
+
+import java.util.Comparator;
+import java.util.Objects;
+
+import javax.annotation.Nullable;
+
+/**
+ * An immutable, semantic-free ordered pair of nullable values. Avoid using it in public APIs.
+ */
+public final class Pair<A, B> {
+
+ /**
+ * Creates a new pair containing the given elements in order.
+ */
+ public static <A, B> Pair<A, B> of(@Nullable A first, @Nullable B second) {
+ return new Pair<A, B>(first, second);
+ }
+
+ /**
+ * The first element of the pair.
+ */
+ @Nullable
+ public final A first;
+
+ /**
+ * The second element of the pair.
+ */
+ @Nullable
+ public final B second;
+
+ /**
+ * Constructor. It is usually easier to call {@link #of}.
+ */
+ public Pair(@Nullable A first, @Nullable B second) {
+ this.first = first;
+ this.second = second;
+ }
+
+ @Nullable
+ public A getFirst() {
+ return first;
+ }
+
+ @Nullable
+ public B getSecond() {
+ return second;
+ }
+
+ @Override
+ public String toString() {
+ return "(" + first + ", " + second + ")";
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof Pair)) {
+ return false;
+ }
+ Pair<?, ?> p = (Pair<?, ?>) o;
+ return Objects.equals(first, p.first) && Objects.equals(second, p.second);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(first, second);
+ }
+
+ /**
+ * A function that maps to the first element in a pair.
+ */
+ public static <A, B> Function<Pair<A, B>, A> firstFunction() {
+ return new Function<Pair<A, B>, A>() {
+ @Override
+ public A apply(Pair<A, B> pair) {
+ return pair.first;
+ }
+ };
+ }
+
+ /**
+ * A function that maps to the second element in a pair.
+ */
+ public static <A, B> Function<Pair<A, B>, B> secondFunction() {
+ return new Function<Pair<A, B>, B>() {
+ @Override
+ public B apply(Pair<A, B> pair) {
+ return pair.second;
+ }
+ };
+ }
+
+ /**
+ * A comparator that compares pairs by comparing the first element.
+ */
+ public static <T extends Comparable<T>, B> Comparator<Pair<T, B>> compareByFirst() {
+ return new Comparator<Pair<T, B>>() {
+ @Override
+ public int compare(Pair<T, B> o1, Pair<T, B> o2) {
+ return o1.first.compareTo(o2.first);
+ }
+ };
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/util/PathFragmentFilter.java b/src/main/java/com/google/devtools/build/lib/util/PathFragmentFilter.java
new file mode 100644
index 0000000..34f6cd2
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/PathFragmentFilter.java
@@ -0,0 +1,111 @@
+// Copyright 2014 Google Inc. 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.base.Joiner;
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.common.options.Converter;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Handles options that specify list of included/excluded directories.
+ * Validates whether path is included in that filter.
+ *
+ * Excluded directories always take precedence over included ones (path depth
+ * and order are not important).
+ */
+public class PathFragmentFilter implements Serializable {
+ private final List<PathFragment> inclusions;
+ private final List<PathFragment> exclusions;
+
+ /**
+ * Converts from a colon-separated list of of paths with optional '-' prefix into the
+ * PathFragmentFilter:
+ * [-]path1[,[-]path2]...
+ *
+ * Order of paths is not important. Empty entries are ignored. '-' marks an excluded path.
+ */
+ public static class PathFragmentFilterConverter implements Converter<PathFragmentFilter> {
+
+ @Override
+ public PathFragmentFilter convert(String input) {
+ List<PathFragment> inclusionList = new ArrayList<>();
+ List<PathFragment> exclusionList = new ArrayList<>();
+
+ for (String piece : Splitter.on(',').split(input)) {
+ if (piece.length() > 1 && piece.startsWith("-")) {
+ exclusionList.add(new PathFragment(piece.substring(1)));
+ } else if (piece.length() > 0) {
+ inclusionList.add(new PathFragment(piece));
+ }
+ }
+
+ // TODO(bazel-team): (2010) Both lists could be optimized not to include unnecessary
+ // entries - e.g. entry 'a/b/c' is not needed if 'a/b' is also specified and 'a/b' on
+ // inclusion list is not needed if 'a' or 'a/b' is on exclusion list.
+ return new PathFragmentFilter(inclusionList, exclusionList);
+ }
+
+ @Override
+ public String getTypeDescription() {
+ return "a comma-separated list of paths with prefix '-' specifying excluded paths";
+ }
+
+ }
+
+ /**
+ * Creates new PathFragmentFilter using provided inclusion and exclusion path lists.
+ */
+ public PathFragmentFilter(List<PathFragment> inclusions, List<PathFragment> exclusions) {
+ this.inclusions = ImmutableList.copyOf(inclusions);
+ this.exclusions = ImmutableList.copyOf(exclusions);
+ }
+
+ /**
+ * @return true iff path is included (it is not on the exclusion list and
+ * it is either on the inclusion list or inclusion list is empty).
+ */
+ public boolean isIncluded(PathFragment path) {
+ for (PathFragment excludedPath : exclusions) {
+ if (path.startsWith(excludedPath)) {
+ return false;
+ }
+ }
+ for (PathFragment includedPath : inclusions) {
+ if (path.startsWith(includedPath)) {
+ return true;
+ }
+ }
+ return inclusions.isEmpty(); // If inclusion filter is not specified, path is included.
+ }
+
+ @Override
+ public String toString() {
+ List<String> list = Lists.newArrayListWithExpectedSize(inclusions.size() + exclusions.size());
+ for (PathFragment path : inclusions) {
+ list.add(path.getPathString());
+ }
+ for (PathFragment path : exclusions) {
+ list.add("-" + path.getPathString());
+ }
+ return Joiner.on(',').join(list);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/util/PersistentMap.java b/src/main/java/com/google/devtools/build/lib/util/PersistentMap.java
new file mode 100644
index 0000000..7fd4b6d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/PersistentMap.java
@@ -0,0 +1,486 @@
+// Copyright 2014 Google Inc. 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.collect.ForwardingMap;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
+import com.google.devtools.build.lib.vfs.Path;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * A map that is backed by persistent storage. It uses two files on disk for
+ * this: The first file contains all the entries and gets written when invoking
+ * the {@link #save()} method. The second file contains a journal of all entries
+ * that were added to or removed from the map since constructing the instance of
+ * the map or the last invocation of {@link #save()} and gets written after each
+ * update of the map although sub-classes are free to implement their own
+ * journal update strategy.
+ *
+ * <p><b>Ceci n'est pas un Map</b>. Strictly speaking, the {@link Map}
+ * interface doesn't permit the possibility of failure. This class uses
+ * persistence; persistence means I/O, and I/O means the possibility of
+ * failure. Therefore the semantics of this may deviate from the Map contract
+ * in failure cases. In particular, updates are not guaranteed to succeed.
+ * However, I/O failures are guaranteed to be reported upon the subsequent call
+ * to a method that throws {@code IOException} such as {@link #save}.
+ *
+ * <p>To populate the map entries using the previously persisted entries call
+ * {@link #load()} prior to invoking any other map operation.
+ * <p>
+ * Like {@link Hashtable} but unlike {@link HashMap}, this class does
+ * <em>not</em> allow <tt>null</tt> to be used as a key or a value.
+ * <p>
+ * IO failures during reading or writing the map entries to disk may result in
+ * {@link AssertionError} getting thrown from the failing method.
+ * <p>
+ * The implementation of the map is not synchronized. If access from multiple
+ * threads is required it must be synchronized using an external object.
+ * <p>
+ * The constructor allows passing in a version number that gets written to the
+ * files on disk and checked before reading from disk. Files with an
+ * incompatible version number will be ignored. This allows the client code to
+ * change the persistence format without polluting the file system name space.
+ */
+public abstract class PersistentMap<K, V> extends ForwardingMap<K, V> {
+
+ private static final int MAGIC = 0x20071105;
+
+ private static final int ENTRY_MAGIC = 0xfe;
+
+ private final int version;
+ private final Path mapFile;
+ private final Path journalFile;
+ private final Map<K, V> journal;
+ private DataOutputStream journalOut;
+
+ /**
+ * 'dirty' is true when the in-memory representation of the map is more recent
+ * than the on-disk representation.
+ */
+ private boolean dirty;
+
+ /**
+ * If non-null, contains the message from an {@code IOException} thrown by a
+ * previously failed write. This error is deferred until the next call to a
+ * method which is able to throw an exception.
+ */
+ private String deferredIOFailure = null;
+
+ /**
+ * 'loaded' is true when the in-memory representation is at least as recent as
+ * the on-disk representation.
+ */
+ private boolean loaded;
+
+ private final Map<K, V> delegate;
+
+ /**
+ * Creates a new PersistentMap instance using the specified backing map.
+ *
+ * @param version the version tag. Changing the version tag allows updating
+ * the on disk format. The map will never read from a file that was
+ * written using a different version tag.
+ * @param map the backing map to use for this PersistentMap.
+ * @param mapFile the file to save the map entries to.
+ * @param journalFile the journal file to write entries between invocations of
+ * {@link #save()}.
+ */
+ public PersistentMap(int version, Map<K, V> map, Path mapFile, Path journalFile) {
+ this.version = version;
+ journal = new LinkedHashMap<>();
+ this.mapFile = mapFile;
+ this.journalFile = journalFile;
+ delegate = map;
+ }
+
+ @Override protected Map<K, V> delegate() {
+ return delegate;
+ }
+
+ @Override
+ public V put(K key, V value) {
+ if (key == null) {
+ throw new NullPointerException();
+ }
+ if (value == null) {
+ throw new NullPointerException();
+ }
+ V previous = delegate().put(key, value);
+ journal.put(key, value);
+ markAsDirty();
+ return previous;
+ }
+
+ /**
+ * Marks the map as dirty and potentially writes updated entries to the
+ * journal.
+ */
+ private void markAsDirty() {
+ dirty = true;
+ if (updateJournal()) {
+ writeJournal();
+ }
+ }
+
+ /**
+ * Determines if the journal should be updated. The default implementation
+ * always returns 'true', but subclasses are free to override this to
+ * implement their own journal updating strategy. For example it is possible
+ * to implement an update at most every five seconds using the following code:
+ *
+ * <pre>
+ * private long nextUpdate;
+ * protected boolean updateJournal() {
+ * long time = System.currentTimeMillis();
+ * if (time > nextUpdate) {
+ * nextUpdate = time + 5 * 1000;
+ * return true;
+ * }
+ * return false;
+ * }
+ * </pre>
+ */
+ protected boolean updateJournal() {
+ return true;
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public V remove(Object object) {
+ V previous = delegate().remove(object);
+ if (previous != null) {
+ // we know that 'object' must be an instance of K, because the
+ // remove call succeeded, i.e. 'object' was mapped to 'previous'.
+ journal.put((K) object, null); // unchecked
+ markAsDirty();
+ }
+ return previous;
+ }
+
+ /**
+ * Updates the persistent journal by writing all entries to the
+ * {@link #journalOut} stream and clearing the in memory journal.
+ */
+ private void writeJournal() {
+ try {
+ if (journalOut == null) {
+ journalOut = createMapFile(journalFile);
+ }
+ writeEntries(journalOut, journal);
+ journalOut.flush();
+ journal.clear();
+ } catch (IOException e) {
+ this.deferredIOFailure = e.getMessage() + " during journal append";
+ }
+ }
+
+ protected void forceFlush() {
+ if (dirty) {
+ writeJournal();
+ }
+ }
+
+ /**
+ * Load the previous written map entries from disk.
+ *
+ * @param failFast if true, throw IOException rather than silently ignoring.
+ * @throws IOException
+ */
+ public void load(boolean failFast) throws IOException {
+ if (!loaded) {
+ loadEntries(mapFile, failFast);
+ if (journalFile.exists()) {
+ try {
+ loadEntries(journalFile, failFast);
+ } catch (IOException e) {
+ if (failFast) {
+ throw e;
+ }
+ //Else: ignore any errors reading the journal file as it may contain
+ //partial entries.
+ }
+ // Force the map to be dirty, so that we can save it to disk.
+ dirty = true;
+ save(/*fullSave=*/true);
+ } else {
+ dirty = false;
+ }
+ loaded = true;
+ }
+ }
+
+ /**
+ * Load the previous written map entries from disk.
+ *
+ * @throws IOException
+ */
+ public void load() throws IOException {
+ load(/*throwOnLoadFailure=*/false);
+ }
+
+ @Override
+ public void clear() {
+ super.clear();
+ markAsDirty();
+ try {
+ save();
+ } catch (IOException e) {
+ this.deferredIOFailure = e.getMessage() + " during map write";
+ }
+ }
+
+ /**
+ * Saves all the entries of this map to disk and deletes the journal file.
+ *
+ * @throws IOException if there was an I/O error during this call, or any previous call since the
+ * last save().
+ */
+ public long save() throws IOException {
+ return save(false);
+ }
+
+ /**
+ * Saves all the entries of this map to disk and deletes the journal file.
+ *
+ * @param fullSave if true, always write the full cache to disk, without the
+ * journal.
+ * @throws IOException if there was an I/O error during this call, or any
+ * previous call since the last save().
+ */
+ private long save(boolean fullSave) throws IOException {
+ /* Report a previously failing I/O operation. */
+ if (deferredIOFailure != null) {
+ try {
+ throw new IOException(deferredIOFailure);
+ } finally {
+ deferredIOFailure = null;
+ }
+ }
+ if (dirty) {
+ if (!fullSave && keepJournal()) {
+ forceFlush();
+ journalOut.close();
+ journalOut = null;
+ return journalSize() + cacheSize();
+ } else {
+ dirty = false;
+ Path mapTemp =
+ mapFile.getRelative(FileSystemUtils.replaceExtension(mapFile.asFragment(), ".tmp"));
+ try {
+ saveEntries(delegate(), mapTemp);
+ mapTemp.renameTo(mapFile);
+ } finally {
+ mapTemp.delete();
+ }
+ clearJournal();
+ journalFile.delete();
+ return cacheSize();
+ }
+ } else {
+ return cacheSize();
+ }
+ }
+
+ protected final long journalSize() throws IOException {
+ return journalFile.exists() ? journalFile.getFileSize() : 0;
+ }
+
+ protected final long cacheSize() throws IOException {
+ return mapFile.exists() ? mapFile.getFileSize() : 0;
+ }
+
+ /**
+ * If true, keep the journal during the save(). The journal is flushed, but
+ * the map file is not touched. This may be useful in cases where the journal
+ * is much smaller than the map.
+ */
+ protected boolean keepJournal() {
+ return false;
+ }
+
+ private void clearJournal() throws IOException {
+ journal.clear();
+ if (journalOut != null) {
+ journalOut.close();
+ journalOut = null;
+ }
+ }
+
+ private void loadEntries(Path mapFile, boolean failFast) throws IOException {
+ if (!mapFile.exists()) {
+ return;
+ }
+ DataInputStream in =
+ new DataInputStream(new BufferedInputStream(mapFile.getInputStream()));
+ try {
+ long fileSize = mapFile.getFileSize();
+ if (fileSize < (16)) {
+ if (failFast) {
+ throw new IOException(mapFile + " is too short: Only " + fileSize + " bytes");
+ } else {
+ return;
+ }
+ }
+ if (in.readLong() != MAGIC) { // not a PersistentMap
+ if (failFast) {
+ throw new IOException("Unexpected format");
+ }
+ return;
+ }
+ if (in.readLong() != version) { // PersistentMap version incompatible
+ if (failFast) {
+ throw new IOException("Unexpected format");
+ }
+ return;
+ }
+ readEntries(in, failFast);
+ } finally {
+ in.close();
+ }
+ }
+
+ /**
+ * Saves the entries in the specified map into the specified file.
+ *
+ * @param map the map to be written into the file.
+ * @param mapFile the file the map is written to.
+ * @throws IOException
+ */
+ private void saveEntries(Map<K, V> map, Path mapFile) throws IOException {
+ DataOutputStream out = createMapFile(mapFile);
+ writeEntries(out, map);
+ out.close();
+ }
+
+ /**
+ * Creates the specified file and returns the DataOuputStream suitable for writing entries.
+ *
+ * @param mapFile the file the map is written to.
+ * @return the DataOutputStream that was can be used for saving the map to the file.
+ * @throws IOException
+ */
+ private DataOutputStream createMapFile(Path mapFile) throws IOException {
+ FileSystemUtils.createDirectoryAndParents(mapFile.getParentDirectory());
+ DataOutputStream out =
+ new DataOutputStream(new BufferedOutputStream(mapFile.getOutputStream()));
+ out.writeLong(MAGIC);
+ out.writeLong(version);
+ return out;
+ }
+
+ /**
+ * Writes the Map entries to the specified DataOutputStream.
+ *
+ * @param out the DataOutputStream to write the Map entries to.
+ * @param map the Map containing the entries to be written to the
+ * DataOutputStream.
+ * @throws IOException
+ */
+ private void writeEntries(DataOutputStream out, Map<K, V> map)
+ throws IOException {
+ for (Map.Entry<K, V> entry : map.entrySet()) {
+ out.writeByte(ENTRY_MAGIC);
+ writeKey(entry.getKey(), out);
+ V value = entry.getValue();
+ boolean isEntry = (value != null);
+ out.writeBoolean(isEntry);
+ if (isEntry) {
+ writeValue(value, out);
+ }
+ }
+ }
+
+ /**
+ * Reads the Map entries from the specified DataInputStream.
+ *
+ * @param failFast if true, throw IOException if entries are in an unexpected
+ * format.
+ * @param in the DataInputStream to read the Map entries from.
+ * @throws IOException
+ */
+ private void readEntries(DataInputStream in, boolean failFast) throws IOException {
+ Map<K, V> map = delegate();
+ while (hasEntries(in, failFast)) {
+ K key = readKey(in);
+ boolean isEntry = in.readBoolean();
+ if (isEntry) {
+ V value = readValue(in);
+ map.put(key, value);
+ } else {
+ map.remove(key);
+ }
+ }
+ }
+
+ private boolean hasEntries(DataInputStream in, boolean failFast) throws IOException {
+ if (in.available() <= 0) {
+ return false;
+ } else if (!(in.readUnsignedByte() == ENTRY_MAGIC)) {
+ if (failFast) {
+ throw new IOException("Corrupted entry separator");
+ } else {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Writes a key of this map into the specified DataOutputStream.
+ *
+ * @param key the key to write to the DataOutputStream.
+ * @param out the DataOutputStream to write the entry to.
+ * @throws IOException
+ */
+ protected abstract void writeKey(K key, DataOutputStream out)
+ throws IOException;
+
+ /**
+ * Writes a value of this map into the specified DataOutputStream.
+ *
+ * @param value the value to write to the DataOutputStream.
+ * @param out the DataOutputStream to write the entry to.
+ * @throws IOException
+ */
+ protected abstract void writeValue(V value, DataOutputStream out)
+ throws IOException;
+
+ /**
+ * Reads an entry of this map from the specified DataInputStream.
+ *
+ * @param in the DataOutputStream to read the entry from.
+ * @return the entry that was read from the DataInputStream.
+ * @throws IOException
+ */
+ protected abstract K readKey(DataInputStream in) throws IOException;
+
+ /**
+ * Reads an entry of this map from the specified DataInputStream.
+ *
+ * @param in the DataOutputStream to read the entry from.
+ * @return the entry that was read from the DataInputStream.
+ * @throws IOException
+ */
+ protected abstract V readValue(DataInputStream in) throws IOException;
+}
diff --git a/src/main/java/com/google/devtools/build/lib/util/ProcMeminfoParser.java b/src/main/java/com/google/devtools/build/lib/util/ProcMeminfoParser.java
new file mode 100644
index 0000000..44c1112
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/ProcMeminfoParser.java
@@ -0,0 +1,121 @@
+// Copyright 2014 Google Inc. 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.CharMatcher;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.io.Files;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Parse and return information from /proc/meminfo.
+ */
+public class ProcMeminfoParser {
+
+ public static final String FILE = "/proc/meminfo";
+
+ private final Map<String, Long> memInfo;
+
+ /**
+ * Populates memory information by reading /proc/meminfo.
+ * @throws IOException if reading the file failed.
+ */
+ public ProcMeminfoParser() throws IOException {
+ this(FILE);
+ }
+
+ @VisibleForTesting
+ public ProcMeminfoParser(String fileName) throws IOException {
+ List<String> lines = Files.readLines(new File(fileName), Charset.defaultCharset());
+ ImmutableMap.Builder<String, Long> builder = ImmutableMap.builder();
+ for (String line : lines) {
+ int colon = line.indexOf(":");
+ String keyword = line.substring(0, colon);
+ String valString = line.substring(colon + 1);
+ try {
+ long val = Long.parseLong(CharMatcher.inRange('0', '9').retainFrom(valString));
+ builder.put(keyword, val);
+ } catch (NumberFormatException e) {
+ // Ignore: we'll fail later if somebody tries to capture this value.
+ }
+ }
+ memInfo = builder.build();
+ }
+
+ /**
+ * Gets a named field in KB.
+ */
+ public long getRamKb(String keyword) {
+ Long val = memInfo.get(keyword);
+ if (val == null) {
+ throw new IllegalArgumentException("Can't locate " + keyword + " in the /proc/meminfo");
+ }
+ return val;
+ }
+
+ /**
+ * Return the total physical memory.
+ */
+ public long getTotalKb() {
+ return getRamKb("MemTotal");
+ }
+
+ /**
+ * Return the inactive memory.
+ */
+ public long getInactiveKb() {
+ return getRamKb("Inactive");
+ }
+
+ /**
+ * Return the active memory.
+ */
+ public long getActiveKb() {
+ return getRamKb("Active");
+ }
+
+ /**
+ * Return the slab memory.
+ */
+ public long getSlabKb() {
+ return getRamKb("Slab");
+ }
+
+ /**
+ * Convert KB to MB.
+ */
+ public static double kbToMb(long kb) {
+ return kb / 1E3;
+ }
+
+ /**
+ * Calculates amount of free RAM from /proc/meminfo content by using
+ * MemTotal - (Active + 0.3*InActive + 0.8*Slab) formula.
+ * Assumption here is that we allow Blaze to use all memory except when
+ * used by active pages, 30% of the inactive pages (since they may become
+ * active at any time) and 80% of memory used by kernel slab heap (since we
+ * want to keep most of the slab heap in the memory but do not want it to
+ * consume all available free memory).
+ */
+ public long getFreeRamKb() {
+ return getTotalKb() - getActiveKb() - (long)(getInactiveKb() * 0.3) - (long)(getSlabKb() * 0.8);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/util/ProcessUtils.java b/src/main/java/com/google/devtools/build/lib/util/ProcessUtils.java
new file mode 100644
index 0000000..ec68736
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/ProcessUtils.java
@@ -0,0 +1,86 @@
+// Copyright 2014 Google Inc. 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.concurrent.ThreadSafety.ThreadSafe;
+
+/**
+ * OS Process related utilities.
+ *
+ * <p>Default implementation forwards all requests to
+ * {@link com.google.devtools.build.lib.unix.ProcessUtils}. The default implementation
+ * can be overridden by {@code #setImplementation(ProcessUtilsImpl)} method.
+ */
+@ThreadSafe
+public final class ProcessUtils {
+
+ /**
+ * Describes implementation to which all {@code ProcessUtils} requests are
+ * forwarded.
+ */
+ public interface ProcessUtilsImpl {
+ /** @see ProcessUtils#getgid() */
+ int getgid();
+
+ /** @see ProcessUtils#getpid() */
+ int getpid();
+
+ /** @see ProcessUtils#getuid() */
+ int getuid();
+ }
+
+ private volatile static ProcessUtilsImpl implementation = new ProcessUtilsImpl() {
+
+ @Override
+ public int getgid() {
+ return com.google.devtools.build.lib.unix.ProcessUtils.getgid();
+ }
+
+ @Override
+ public int getpid() {
+ return com.google.devtools.build.lib.unix.ProcessUtils.getpid();
+ }
+
+ @Override
+ public int getuid() {
+ return com.google.devtools.build.lib.unix.ProcessUtils.getuid();
+ }
+ };
+
+ private ProcessUtils() {
+ // prevent construction.
+ }
+
+ /**
+ * @return the real group ID of the current process.
+ */
+ public static int getgid() {
+ return implementation.getgid();
+ }
+
+ /**
+ * @return the process ID of this process.
+ */
+ public static int getpid() {
+ return implementation.getpid();
+ }
+
+ /**
+ * @return the real user ID of the current process.
+ */
+ public static int getuid() {
+ return implementation.getuid();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/util/RegexFilter.java b/src/main/java/com/google/devtools/build/lib/util/RegexFilter.java
new file mode 100644
index 0000000..d7c6834
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/RegexFilter.java
@@ -0,0 +1,167 @@
+// Copyright 2014 Google Inc. 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.base.Joiner;
+import com.google.devtools.common.options.Converter;
+import com.google.devtools.common.options.OptionsParsingException;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+/**
+ * Handles options that specify list of included/excluded regex expressions.
+ * Validates whether string is included in that filter.
+ *
+ * String is considered to be included into the filter if it does not match
+ * any of the excluded regex expressions and if it matches at least one
+ * included regex expression.
+ */
+public class RegexFilter implements Serializable {
+ private final Pattern inclusionPattern;
+ private final Pattern exclusionPattern;
+ private final int hashCode;
+
+ /**
+ * Converts from a colon-separated list of regex expressions with optional
+ * -/+ prefix into the RegexFilter. Colons prefixed with backslash are
+ * considered to be part of regex definition and not a delimiter between
+ * separate regex expressions.
+ *
+ * Order of expressions is not important. Empty entries are ignored.
+ * '-' marks an excluded expression.
+ */
+ public static class RegexFilterConverter
+ implements Converter<RegexFilter> {
+
+ @Override
+ public RegexFilter convert(String input) throws OptionsParsingException {
+ List<String> inclusionList = new ArrayList<>();
+ List<String> exclusionList = new ArrayList<>();
+
+ for (String piece : input.split("(?<!\\\\),")) { // Split on ',' but not on '\,'
+ piece = piece.replace("\\,", ",");
+ boolean isExcluded = piece.startsWith("-");
+ if (isExcluded || piece.startsWith("+")) {
+ piece = piece.substring(1);
+ }
+ if (piece.length() > 0) {
+ (isExcluded ? exclusionList : inclusionList).add(piece);
+ }
+ }
+
+ try {
+ return new RegexFilter(inclusionList, exclusionList);
+ } catch (PatternSyntaxException e) {
+ throw new OptionsParsingException("Failed to build valid regular expression: "
+ + e.getMessage());
+ }
+ }
+
+ @Override
+ public String getTypeDescription() {
+ return "a comma-separated list of regex expressions with prefix '-' specifying"
+ + " excluded paths";
+ }
+
+ }
+
+ /**
+ * Creates new RegexFilter using provided inclusion and exclusion path lists.
+ */
+ public RegexFilter(List<String> inclusions, List<String> exclusions) {
+ inclusionPattern = convertRegexListToPattern(inclusions);
+ exclusionPattern = convertRegexListToPattern(exclusions);
+ hashCode = Objects.hash(inclusions, exclusions);
+ }
+
+ /**
+ * Converts list of regex expressions into one compiled regex expression.
+ */
+ private static Pattern convertRegexListToPattern(List<String> regexList) {
+ if (regexList.size() == 0) {
+ return null;
+ }
+ // Wrap each individual regex in the independent group, combine them using '|' and
+ // wrap in the non-capturing group.
+ return Pattern.compile("(?:(?>" + Joiner.on(")|(?>").join(regexList) + "))");
+ }
+
+ /**
+ * @return true iff given string is included (it is does not match exclusion
+ * pattern (if any) and matches inclusionPatter (if any).
+ */
+ public boolean isIncluded(String value) {
+ if (exclusionPattern != null && exclusionPattern.matcher(value).find()) {
+ return false;
+ }
+ if (inclusionPattern == null) {
+ return true;
+ }
+ return inclusionPattern.matcher(value).find();
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ if (inclusionPattern != null) {
+ builder.append(inclusionPattern.pattern().replace(",", "\\,"));
+ if (exclusionPattern != null) {
+ builder.append(",");
+ }
+ }
+ if (exclusionPattern != null) {
+ builder.append("-");
+ builder.append(exclusionPattern.pattern().replace(",", "\\,"));
+ }
+ return builder.toString();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof RegexFilter)) {
+ return false;
+ }
+
+ RegexFilter otherFilter = (RegexFilter) other;
+ if (this.exclusionPattern == null ^ otherFilter.exclusionPattern == null) {
+ return false;
+ }
+ if (this.inclusionPattern == null ^ otherFilter.inclusionPattern == null) {
+ return false;
+ }
+ if (this.exclusionPattern != null && !this.exclusionPattern.pattern().equals(
+ otherFilter.exclusionPattern.pattern())) {
+ return false;
+ }
+ if (this.inclusionPattern != null && !this.inclusionPattern.pattern().equals(
+ otherFilter.inclusionPattern.pattern())) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ return hashCode;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/util/ResourceFileLoader.java b/src/main/java/com/google/devtools/build/lib/util/ResourceFileLoader.java
new file mode 100644
index 0000000..ce26c6c
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/ResourceFileLoader.java
@@ -0,0 +1,57 @@
+// Copyright 2014 Google Inc. 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 static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.io.ByteStreams;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * A little utility to load resources (property files) from jars or
+ * the classpath. Recommended for longer texts that do not fit nicely into
+ * a piece of Java code - e.g. a template for a lengthy email.
+ */
+public final class ResourceFileLoader {
+
+ private ResourceFileLoader() {}
+
+ /**
+ * Loads a text resource that is located in a directory on the Java classpath that
+ * corresponds to the package of <code>relativeToClass</code> using UTF8 encoding.
+ * E.g.
+ * <code>loadResource(Class.forName("com.google.foo.Foo", "bar.txt"))</code>
+ * will look for <code>com/google/foo/bar.txt</code> in the classpath.
+ */
+ public static String loadResource(Class<?> relativeToClass, String resourceName)
+ throws IOException {
+ ClassLoader loader = relativeToClass.getClassLoader();
+ // TODO(bazel-team): use relativeToClass.getPackage().getName().
+ String className = relativeToClass.getName();
+ String packageName = className.substring(0, className.lastIndexOf('.'));
+ String path = packageName.replace('.', '/');
+ String resource = path + '/' + resourceName;
+ InputStream stream = loader.getResourceAsStream(resource);
+ if (stream == null) {
+ throw new IOException(resourceName + " not found.");
+ }
+ try {
+ return new String(ByteStreams.toByteArray(stream), UTF_8);
+ } finally {
+ stream.close();
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/util/ResourceUsage.java b/src/main/java/com/google/devtools/build/lib/util/ResourceUsage.java
new file mode 100644
index 0000000..55807f2
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/ResourceUsage.java
@@ -0,0 +1,353 @@
+// Copyright 2014 Google Inc. 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 static java.nio.charset.StandardCharsets.US_ASCII;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Splitter;
+import com.google.common.collect.Iterables;
+import com.google.common.io.Files;
+
+import com.sun.management.OperatingSystemMXBean;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.management.ManagementFactory;
+import java.lang.management.MemoryMXBean;
+import java.util.Iterator;
+
+/**
+ * Provides methods to measure the current resource usage of the current
+ * process. Also provides some convenience methods to obtain several system
+ * characteristics, like number of processors , total memory, etc.
+ */
+public final class ResourceUsage {
+
+ /*
+ * Use com.sun.management.OperatingSystemMXBean instead of
+ * java.lang.management.OperatingSystemMXBean because the latter does not
+ * support getTotalPhysicalMemorySize() and getFreePhysicalMemorySize().
+ */
+ private static final OperatingSystemMXBean OS_BEAN =
+ (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean();
+
+ private static final MemoryMXBean MEM_BEAN = ManagementFactory.getMemoryMXBean();
+ private static final Splitter WHITESPACE_SPLITTER = Splitter.on(CharMatcher.WHITESPACE);
+
+ /**
+ * Calculates an estimate of the current total CPU usage and the CPU usage of
+ * the process in percent measured from the two given measurements. The
+ * returned CPU usages rea average values for the time between the two
+ * measurements. The returned array contains the total CPU usage at index 0
+ * and the CPU usage of the measured process at index 1.
+ */
+ public static float[] calculateCurrentCpuUsage(Measurement oldMeasurement,
+ Measurement newMeasurement) {
+ if (oldMeasurement == null) {
+ return new float[2];
+ }
+ long idleJiffies =
+ newMeasurement.getTotalCpuIdleTimeInJiffies()
+ - oldMeasurement.getTotalCpuIdleTimeInJiffies();
+ long oldProcessJiffies =
+ oldMeasurement.getCpuUtilizationInJiffies()[0]
+ + oldMeasurement.getCpuUtilizationInJiffies()[1];
+ long newProcessJiffies =
+ newMeasurement.getCpuUtilizationInJiffies()[0]
+ + newMeasurement.getCpuUtilizationInJiffies()[1];
+ long processJiffies = newProcessJiffies - oldProcessJiffies;
+ long elapsedTimeJiffies =
+ newMeasurement.getTimeInJiffies() - oldMeasurement.getTimeInJiffies();
+ int processors = getAvailableProcessors();
+ // TODO(bazel-team): Sometimes smaller then zero. Not sure why.
+ double totalUsage = Math.max(0, 1.0D - (double) idleJiffies / elapsedTimeJiffies / processors);
+ double usage = Math.max(0, (double) processJiffies / elapsedTimeJiffies / processors);
+ return new float[] {(float) totalUsage * 100, (float) usage * 100};
+ }
+
+ private ResourceUsage() {
+ }
+
+ /**
+ * Returns the number of processors available to the Java virtual machine.
+ */
+ public static int getAvailableProcessors() {
+ return OS_BEAN.getAvailableProcessors();
+ }
+
+ /**
+ * Returns the total physical memory in bytes.
+ */
+ public static long getTotalPhysicalMemorySize() {
+ return OS_BEAN.getTotalPhysicalMemorySize();
+ }
+
+ /**
+ * Returns the operating system architecture.
+ */
+ public static String getOsArchitecture() {
+ return OS_BEAN.getArch();
+ }
+
+ /**
+ * Returns the operating system name.
+ */
+ public static String getOsName() {
+ return OS_BEAN.getName();
+ }
+
+ /**
+ * Returns the operating system version.
+ */
+ public static String getOsVersion() {
+ return OS_BEAN.getVersion();
+ }
+
+ /**
+ * Returns the initial size of heap memory in bytes.
+ *
+ * @see MemoryMXBean#getHeapMemoryUsage()
+ */
+ public static long getHeapMemoryInit() {
+ return MEM_BEAN.getHeapMemoryUsage().getInit();
+ }
+
+ /**
+ * Returns the initial size of non heap memory in bytes.
+ *
+ * @see MemoryMXBean#getNonHeapMemoryUsage()
+ */
+ public static long getNonHeapMemoryInit() {
+ return MEM_BEAN.getNonHeapMemoryUsage().getInit();
+ }
+
+ /**
+ * Returns the maximum size of heap memory in bytes.
+ *
+ * @see MemoryMXBean#getHeapMemoryUsage()
+ */
+ public static long getHeapMemoryMax() {
+ return MEM_BEAN.getHeapMemoryUsage().getMax();
+ }
+
+ /**
+ * Returns the maximum size of non heap memory in bytes.
+ *
+ * @see MemoryMXBean#getNonHeapMemoryUsage()
+ */
+ public static long getNonHeapMemoryMax() {
+ return MEM_BEAN.getNonHeapMemoryUsage().getMax();
+ }
+
+ /**
+ * Returns a measurement of the current resource usage of the current process.
+ */
+ public static Measurement measureCurrentResourceUsage() {
+ return measureCurrentResourceUsage("self");
+ }
+
+ /**
+ * Returns a measurement of the current resource usage of the process with the
+ * given process id.
+ *
+ * @param processId the process id or <code>self</code> for the current
+ * process.
+ */
+ public static Measurement measureCurrentResourceUsage(String processId) {
+ return new Measurement(MEM_BEAN.getHeapMemoryUsage().getUsed(), MEM_BEAN.getHeapMemoryUsage()
+ .getCommitted(), MEM_BEAN.getNonHeapMemoryUsage().getUsed(), MEM_BEAN
+ .getNonHeapMemoryUsage().getCommitted(), (float) OS_BEAN.getSystemLoadAverage(), OS_BEAN
+ .getFreePhysicalMemorySize(), getCurrentTotalIdleTimeInJiffies(),
+ getCurrentCpuUtilizationInJiffies(processId));
+ }
+
+ /**
+ * Returns the current total idle time of the processors since system boot.
+ * Reads /proc/stat to obtain this information.
+ */
+ private static long getCurrentTotalIdleTimeInJiffies() {
+ try {
+ File file = new File("/proc/stat");
+ String content = Files.toString(file, US_ASCII);
+ String value = Iterables.get(WHITESPACE_SPLITTER.split(content), 5);
+ return Long.parseLong(value);
+ } catch (NumberFormatException | IOException e) {
+ return 0L;
+ }
+ }
+
+ /**
+ * Returns the current cpu utilization of the current process with the given
+ * id in jiffies. The returned array contains the following information: The
+ * 1st entry is the number of jiffies that the process has executed in user
+ * mode, and the 2nd entry is the number of jiffies that the process has
+ * executed in kernel mode. Reads /proc/self/stat to obtain this information.
+ *
+ * @param processId the process id or <code>self</code> for the current
+ * process.
+ */
+ private static long[] getCurrentCpuUtilizationInJiffies(String processId) {
+ try {
+ File file = new File("/proc/" + processId + "/stat");
+ if (file.isDirectory()) {
+ return new long[2];
+ }
+ Iterator<String> stat = WHITESPACE_SPLITTER.split(
+ Files.toString(file, US_ASCII)).iterator();
+ for (int i = 0; i < 13; ++i) {
+ stat.next();
+ }
+ long token13 = Long.parseLong(stat.next());
+ long token14 = Long.parseLong(stat.next());
+ return new long[] { token13, token14 };
+ } catch (NumberFormatException e) {
+ return new long[2];
+ } catch (IOException e) {
+ return new long[2];
+ }
+ }
+
+ /**
+ * A snapshot of the resource usage of the current process at a point in time.
+ */
+ public static class Measurement {
+
+ private final long timeInNanos;
+ private final long heapMemoryUsed;
+ private final long heapMemoryCommitted;
+ private final long nonHeapMemoryUsed;
+ private final long nonHeapMemoryCommitted;
+ private final float loadAverageLastMinute;
+ private final long freePhysicalMemory;
+ private final long totalCpuIdleTimeInJiffies;
+ private final long[] cpuUtilizationInJiffies;
+
+ public Measurement(long heapMemoryUsed, long heapMemoryCommitted, long nonHeapMemoryUsed,
+ long nonHeapMemoryCommitted, float loadAverageLastMinute, long freePhysicalMemory,
+ long totalCpuIdleTimeInJiffies, long[] cpuUtilizationInJiffies) {
+ super();
+ timeInNanos = System.nanoTime();
+ this.heapMemoryUsed = heapMemoryUsed;
+ this.heapMemoryCommitted = heapMemoryCommitted;
+ this.nonHeapMemoryUsed = nonHeapMemoryUsed;
+ this.nonHeapMemoryCommitted = nonHeapMemoryCommitted;
+ this.loadAverageLastMinute = loadAverageLastMinute;
+ this.freePhysicalMemory = freePhysicalMemory;
+ this.totalCpuIdleTimeInJiffies = totalCpuIdleTimeInJiffies;
+ this.cpuUtilizationInJiffies = cpuUtilizationInJiffies;
+ }
+
+ /**
+ * Returns the time of the measurement in jiffies.
+ */
+ public long getTimeInJiffies() {
+ return timeInNanos / 10000000;
+ }
+
+ /**
+ * Returns the time of the measurement in ms.
+ */
+ public long getTimeInMs() {
+ return timeInNanos / 1000000;
+ }
+
+ /**
+ * Returns the amount of used heap memory in bytes at the time of
+ * measurement.
+ *
+ * @see MemoryMXBean#getHeapMemoryUsage()
+ */
+ public long getHeapMemoryUsed() {
+ return heapMemoryUsed;
+ }
+
+ /**
+ * Returns the amount of used non heap memory in bytes at the time of
+ * measurement.
+ *
+ * @see MemoryMXBean#getNonHeapMemoryUsage()
+ */
+ public long getHeapMemoryCommitted() {
+ return heapMemoryCommitted;
+ }
+
+ /**
+ * Returns the amount of memory in bytes that is committed for the Java
+ * virtual machine to use for the heap at the time of measurement.
+ *
+ * @see MemoryMXBean#getHeapMemoryUsage()
+ */
+ public long getNonHeapMemoryUsed() {
+ return nonHeapMemoryUsed;
+ }
+
+ /**
+ * Returns the amount of memory in bytes that is committed for the Java
+ * virtual machine to use for non heap memory at the time of measurement.
+ *
+ * @see MemoryMXBean#getNonHeapMemoryUsage()
+ */
+ public long getNonHeapMemoryCommitted() {
+ return nonHeapMemoryCommitted;
+ }
+
+ /**
+ * Returns the system load average for the last minute at the time of
+ * measurement.
+ *
+ * @see OperatingSystemMXBean#getSystemLoadAverage()
+ */
+ public float getLoadAverageLastMinute() {
+ return loadAverageLastMinute;
+ }
+
+ /**
+ * Returns the free physical memmory in bytes at the time of measurement.
+ */
+ public long getFreePhysicalMemory() {
+ return freePhysicalMemory;
+ }
+
+ /**
+ * Returns the current total cpu idle since system boot in jiffies.
+ */
+ public long getTotalCpuIdleTimeInJiffies() {
+ return totalCpuIdleTimeInJiffies;
+ }
+
+ /**
+ * Returns the current cpu utilization of the current process in jiffies.
+ * The returned array contains the following information: The 1st entry is
+ * the number of jiffies that the process has executed in user mode, and the
+ * 2nd entry is the number of jiffies that the process has executed in
+ * kernel mode. Reads /proc/self/stat to obtain this information.
+ */
+ public long[] getCpuUtilizationInJiffies() {
+ return cpuUtilizationInJiffies;
+ }
+
+ /**
+ * Returns the current cpu utilization of the current process in ms. The
+ * returned array contains the following information: The 1st entry is the
+ * number of ms that the process has executed in user mode, and the 2nd
+ * entry is the number of ms that the process has executed in kernel mode.
+ * Reads /proc/self/stat to obtain this information.
+ */
+ public long[] getCpuUtilizationInMs() {
+ return new long[] {cpuUtilizationInJiffies[0] * 10, cpuUtilizationInJiffies[1] * 10};
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/util/ShellEscaper.java b/src/main/java/com/google/devtools/build/lib/util/ShellEscaper.java
new file mode 100644
index 0000000..fd23443
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/ShellEscaper.java
@@ -0,0 +1,202 @@
+// Copyright 2014 Google Inc. 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.base.CharMatcher;
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
+import com.google.common.collect.Iterables;
+import com.google.common.escape.CharEscaperBuilder;
+import com.google.common.escape.Escaper;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+
+import java.io.IOException;
+
+/**
+ * Utility class to escape strings for use with shell commands.
+ *
+ * <p>Escaped strings may safely be inserted into shell commands. Escaping is
+ * only done if necessary. Strings containing only shell-neutral characters
+ * will not be escaped.
+ *
+ * <p>This is a replacement for {@code ShellUtils.shellEscape(String)} and
+ * {@code ShellUtils.prettyPrintArgv(java.util.List)} (see
+ * {@link com.google.devtools.build.lib.shell.ShellUtils}). Its advantage is the use
+ * of standard building blocks from the {@code com.google.common.base}
+ * package, such as {@link Joiner} and {@link CharMatcher}, making this class
+ * more efficient and reliable than {@code ShellUtils}.
+ *
+ * <p>The behavior is slightly different though: this implementation will
+ * defensively escape non-ASCII letters and digits, whereas
+ * {@code shellEscape} does not.
+ */
+@Immutable
+public final class ShellEscaper extends Escaper {
+ // Note: extending Escaper may seem desirable, but is in fact harmful.
+ // The class would then need to implement escape(Appendable), returning an Appendable
+ // that escapes everything it receives. In case of shell escaping, we most often join
+ // string parts on spaces, using a Joiner. Spaces are escaped characters. Using the
+ // Appendable returned by escape(Appendable) would escape these spaces too, which
+ // is unwanted.
+
+ public static final ShellEscaper INSTANCE = new ShellEscaper();
+
+ private static final Function<String, String> AS_FUNCTION = INSTANCE.asFunction();
+
+ private static final Joiner SPACE_JOINER = Joiner.on(' ');
+ private static final Escaper STRONGQUOTE_ESCAPER =
+ new CharEscaperBuilder().addEscape('\'', "'\\''").toEscaper();
+ private static final CharMatcher SAFECHAR_MATCHER =
+ CharMatcher.anyOf("@%-_+:,./")
+ .or(CharMatcher.inRange('0', '9')) // We can't use CharMatcher.JAVA_LETTER_OR_DIGIT,
+ .or(CharMatcher.inRange('a', 'z')) // that would also accept non-ASCII digits and
+ .or(CharMatcher.inRange('A', 'Z')); // letters.
+
+ /**
+ * Escapes a string by adding strong (single) quotes around it if necessary.
+ *
+ * <p>A string is not escaped iff it only contains safe characters.
+ * The following characters are safe:
+ * <ul>
+ * <li>ASCII letters and digits: [a-zA-Z0-9]
+ * <li>shell-neutral characters: at symbol (@), percent symbol (%),
+ * dash/minus sign (-), underscore (_), plus sign (+), colon (:),
+ * comma(,), period (.) and slash (/).
+ * </ul>
+ *
+ * <p>A string is escaped iff it contains at least one non-safe character.
+ * Escaped strings are created by replacing every occurrence of single
+ * quotes with the string '\'' and enclosing the result in a pair of
+ * single quotes.
+ *
+ * <p>Examples:
+ * <ul>
+ * <li>"{@code foo}" becomes "{@code foo}" (remains the same)
+ * <li>"{@code +bar}" becomes "{@code +bar}" (remains the same)
+ * <li>"" becomes "{@code''}" (empty string becomes a pair of strong quotes)
+ * <li>"{@code $BAZ}" becomes "{@code '$BAZ'}"
+ * <li>"{@code quote'd}" becomes "{@code 'quote'\''d'}"
+ * </ul>
+ */
+ @Override
+ public String escape(String unescaped) {
+ final String s = unescaped.toString();
+ if (s.isEmpty()) {
+ // Empty string is a special case: needs to be quoted to ensure that it
+ // gets treated as a separate argument.
+ return "''";
+ } else {
+ return SAFECHAR_MATCHER.matchesAllOf(s)
+ ? s
+ : "'" + STRONGQUOTE_ESCAPER.escape(s) + "'";
+ }
+ }
+
+ public static String escapeString(String unescaped) {
+ return INSTANCE.escape(unescaped);
+ }
+
+ /**
+ * Transforms the input {@code Iterable} of unescaped strings to an
+ * {@code Iterable} of escaped ones. The escaping is done lazily.
+ */
+ public static Iterable<String> escapeAll(Iterable<? extends String> unescaped) {
+ return Iterables.transform(unescaped, AS_FUNCTION);
+ }
+
+ /**
+ * Escapes all strings in {@code argv} individually and joins them on
+ * single spaces into {@code out}. The result is appended directly into
+ * {@code out}, without adding a separator.
+ *
+ * <p>This method works as if by invoking
+ * {@link #escapeJoinAll(Appendable, Iterable, Joiner)} with
+ * {@code Joiner.on(' ')}.
+ *
+ * @param out what the result will be appended to
+ * @param argv the strings to escape and join
+ * @return the same reference as {@code out}, now containing the the
+ * joined, escaped fragments
+ * @throws IOException if an I/O error occurs while appending
+ */
+ public static Appendable escapeJoinAll(Appendable out, Iterable<? extends String> argv)
+ throws IOException {
+ return SPACE_JOINER.appendTo(out, escapeAll(argv));
+ }
+
+ /**
+ * Escapes all strings in {@code argv} individually and joins them into
+ * {@code out} using the specified {@link Joiner}. The result is appended
+ * directly into {@code out}, without adding a separator.
+ *
+ * <p>The resulting strings are the same as if escaped one by one using
+ * {@link #escapeString(String)}.
+ *
+ * <p>Example: if the joiner is {@code Joiner.on('|')}, then the input
+ * {@code ["abc", "de'f"]} will be escaped as "{@code abc|'de'\''f'}".
+ * If {@code out} initially contains "{@code 123}", then the returned
+ * {@code Appendable} will contain "{@code 123abc|'de'\''f'}".
+ *
+ * @param out what the result will be appended to
+ * @param argv the strings to escape and join
+ * @param joiner the {@link Joiner} to use to join the escaped strings
+ * @return the same reference as {@code out}, now containing the the
+ * joined, escaped fragments
+ * @throws IOException if an I/O error occurs while appending
+ */
+ public static Appendable escapeJoinAll(Appendable out, Iterable<? extends String> argv,
+ Joiner joiner) throws IOException {
+ return joiner.appendTo(out, escapeAll(argv));
+ }
+
+ /**
+ * Escapes all strings in {@code argv} individually and joins them on
+ * single spaces, then returns the resulting string.
+ *
+ * <p>This method works as if by invoking
+ * {@link #escapeJoinAll(Iterable, Joiner)} with {@code Joiner.on(' ')}.
+ *
+ * <p>Example: {@code ["abc", "de'f"]} will be escaped and joined as
+ * "abc 'de'\''f'".
+ *
+ * @param argv the strings to escape and join
+ * @return the string of escaped and joined input elements
+ */
+ public static String escapeJoinAll(Iterable<? extends String> argv) {
+ return SPACE_JOINER.join(escapeAll(argv));
+ }
+
+ /**
+ * Escapes all strings in {@code argv} individually and joins them using
+ * the specified {@link Joiner}, then returns the resulting string.
+ *
+ * <p>The resulting strings are the same as if escaped one by one using
+ * {@link #escapeString(String)}.
+ *
+ * <p>Example: if the joiner is {@code Joiner.on('|')}, then the input
+ * {@code ["abc", "de'f"]} will be escaped and joined as "abc|'de'\''f'".
+ *
+ * @param argv the strings to escape and join
+ * @param joiner the {@link Joiner} to use to join the escaped strings
+ * @return the string of escaped and joined input elements
+ */
+ public static String escapeJoinAll(Iterable<? extends String> argv, Joiner joiner) {
+ return joiner.join(escapeAll(argv));
+ }
+
+ private ShellEscaper() {
+ // Utility class - do not instantiate.
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/util/StringCanonicalizer.java b/src/main/java/com/google/devtools/build/lib/util/StringCanonicalizer.java
new file mode 100644
index 0000000..7bdbe7e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/StringCanonicalizer.java
@@ -0,0 +1,36 @@
+// Copyright 2014 Google Inc. 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.collect.Interner;
+import com.google.common.collect.Interners;
+
+/**
+ * Static singleton holder for the string interning pool. Doesn't use {@link String#intern}
+ * because that consumes permgen space.
+ */
+public final class StringCanonicalizer {
+
+ private static final Interner<String> interner = Interners.newWeakInterner();
+
+ private StringCanonicalizer() {
+ }
+
+ /**
+ * Interns a String.
+ */
+ public final static String intern(String arg) {
+ return interner.intern(arg);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/util/StringIndexer.java b/src/main/java/com/google/devtools/build/lib/util/StringIndexer.java
new file mode 100644
index 0000000..cf345d2
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/StringIndexer.java
@@ -0,0 +1,61 @@
+// Copyright 2014 Google Inc. 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;
+
+/**
+ * An object that provides bidirectional String <-> unique integer mapping.
+ */
+public interface StringIndexer {
+
+ /**
+ * Removes all mappings.
+ */
+ public void clear();
+
+ /**
+ * @return some measure of the size of the index.
+ */
+ public int size();
+
+ /**
+ * Creates new mapping for the given string if necessary and returns
+ * string index. Also, as a side effect, zero or more additional mappings
+ * may be created for various prefixes of the given string.
+ *
+ * @return a unique index.
+ */
+ public int getOrCreateIndex(String s);
+
+ /**
+ * @return a unique index for the given string or -1 if string
+ * was not added.
+ */
+ public int getIndex(String s);
+
+ /**
+ * Creates mapping for the given string if necessary.
+ * Also, as a side effect, zero or more additional mappings may be
+ * created for various prefixes of the given string.
+ *
+ * @return true if new mapping was created, false if mapping already existed.
+ */
+ public boolean addString(String s);
+
+ /**
+ * @return string associated with the given index or null if
+ * mapping does not exist.
+ */
+ public String getStringForIndex(int i);
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/util/StringTrie.java b/src/main/java/com/google/devtools/build/lib/util/StringTrie.java
new file mode 100644
index 0000000..4744c7e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/StringTrie.java
@@ -0,0 +1,90 @@
+// Copyright 2014 Google Inc. 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.base.Preconditions;
+
+
+/**
+ * A trie that operates on path segments of an input string instead of individual characters.
+ *
+ * <p>Only accepts strings that contain only low-ASCII characters (0-127)
+ *
+ * @param <T> the type of the values
+ */
+public class StringTrie<T> {
+ private static final int RANGE = 128;
+
+ @SuppressWarnings("unchecked")
+ private static class Node<T> {
+ private Node() {
+ children = new Node[RANGE];
+ }
+
+ private T value;
+ private Node<T> children[];
+ }
+
+ private final Node<T> root;
+
+ public StringTrie() {
+ root = new Node<T>();
+ }
+
+ /**
+ * Puts a value in the trie.
+ */
+ public void put(CharSequence key, T value) {
+ Node<T> current = root;
+
+ for (int i = 0; i < key.length(); i++) {
+ char ch = key.charAt(i);
+ Preconditions.checkState(ch < RANGE);
+ Node<T> next = current.children[ch];
+ if (next == null) {
+ next = new Node<T>();
+ current.children[ch] = next;
+ }
+
+ current = next;
+ }
+
+ current.value = value;
+ }
+
+ /**
+ * Gets a value from the trie. If there is an entry with the same key, that will be returned,
+ * otherwise, the value corresponding to the key that matches the longest prefix of the input.
+ */
+ public T get(String key) {
+ Node<T> current = root;
+ T lastValue = current.value;
+
+ for (int i = 0; i < key.length(); i++) {
+ char ch = key.charAt(i);
+ Preconditions.checkState(ch < RANGE);
+
+ current = current.children[ch];
+ if (current == null) {
+ break;
+ }
+
+ if (current.value != null) {
+ lastValue = current.value;
+ }
+ }
+
+ return lastValue;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/util/StringUtil.java b/src/main/java/com/google/devtools/build/lib/util/StringUtil.java
new file mode 100644
index 0000000..40f7ec1
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/StringUtil.java
@@ -0,0 +1,175 @@
+// Copyright 2014 Google Inc. 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.base.Function;
+import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Splitter;
+import com.google.common.collect.Iterables;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Various utility methods operating on strings.
+ */
+public class StringUtil {
+ /**
+ * Creates a comma-separated list of words as in English.
+ *
+ * <p>Example: ["a", "b", "c"] -> "a, b or c".
+ */
+ public static String joinEnglishList(Iterable<?> choices) {
+ return joinEnglishList(choices, "or", "");
+ }
+
+ /**
+ * Creates a comma-separated list of words as in English with the given last-separator.
+ *
+ * <p>Example with lastSeparator="then": ["a", "b", "c"] -> "a, b then c".
+ */
+ public static String joinEnglishList(Iterable<?> choices, String lastSeparator) {
+ return joinEnglishList(choices, lastSeparator, "");
+ }
+
+ /**
+ * Creates a comma-separated list of words as in English with the given last-separator and quotes.
+ *
+ * <p>Example with lastSeparator="then", quote="'": ["a", "b", "c"] -> "'a', 'b' then 'c'".
+ */
+ public static String joinEnglishList(Iterable<?> choices, String lastSeparator, String quote) {
+ StringBuilder buf = new StringBuilder();
+ for (Iterator<?> ii = choices.iterator(); ii.hasNext(); ) {
+ Object choice = ii.next();
+ if (buf.length() > 0) {
+ buf.append(ii.hasNext() ? "," : " " + lastSeparator);
+ buf.append(" ");
+ }
+ buf.append(quote).append(choice).append(quote);
+ }
+ return buf.length() == 0 ? "nothing" : buf.toString();
+ }
+
+ /**
+ * Split a single space-separated string into a List of values.
+ *
+ * <p>Individual values are canonicalized such that within and
+ * across calls to this method, equal values point to the same
+ * object.
+ *
+ * <p>If the input is null, return an empty list.
+ *
+ * @param in space-separated list of values, eg "value1 value2".
+ */
+ public static List<String> splitAndInternString(String in) {
+ List<String> result = new ArrayList<>();
+ if (in == null) {
+ return result;
+ }
+ for (String val : Splitter.on(" ").omitEmptyStrings().split(in)) {
+ // Note that splitter returns a substring(), effectively
+ // retaining the entire "in" String. Make an explicit copy here
+ // to avoid that memory pitfall. Further, because there may be
+ // many concurrent submissions that touch the same files,
+ // attempt to use a single reference for equal strings via the
+ // deduplicator.
+ result.add(StringCanonicalizer.intern(new String(val)));
+ }
+ return result;
+ }
+
+ /**
+ * Lists items up to a given limit, then prints how many were omitted.
+ */
+ public static StringBuilder listItemsWithLimit(StringBuilder appendTo, int limit,
+ Collection<?> items) {
+ Preconditions.checkState(limit > 0);
+ Joiner.on(", ").appendTo(appendTo, Iterables.limit(items, limit));
+ if (items.size() > limit) {
+ appendTo.append(" ...(omitting ")
+ .append(items.size() - limit)
+ .append(" more item(s))");
+ }
+ return appendTo;
+ }
+
+ /**
+ * Returns the ordinal representation of the number.
+ */
+ public static String ordinal(int number) {
+ switch (number) {
+ case 1:
+ return "1st";
+ case 2:
+ return "2nd";
+ case 3:
+ return "3rd";
+ default:
+ return number + "th";
+ }
+ }
+
+ /**
+ * Appends a prefix and a suffix to each of the Strings.
+ */
+ public static Iterable<String> append(Iterable<String> values, final String prefix,
+ final String suffix) {
+ return Iterables.transform(values, new Function<String, String>() {
+ @Override
+ public String apply(String input) {
+ return prefix + input + suffix;
+ }
+ });
+ }
+
+ /**
+ * Indents the specified string by the given number of characters.
+ *
+ * <p>The beginning of the string before the first newline is not indented.
+ */
+ public static String indent(String input, int depth) {
+ StringBuilder prefix = new StringBuilder();
+ prefix.append("\n");
+ for (int i = 0; i < depth; i++) {
+ prefix.append(" ");
+ }
+
+ return input.replace("\n", prefix);
+ }
+
+ /**
+ * Strips a suffix from a string. If the string does not end with the suffix, returns null.
+ */
+ public static String stripSuffix(String input, String suffix) {
+ return input.endsWith(suffix)
+ ? input.substring(0, input.length() - suffix.length())
+ : null;
+ }
+
+ /**
+ * Capitalizes the first character of a string.
+ */
+ public static String capitalize(String input) {
+ if (input.isEmpty()) {
+ return input;
+ }
+
+ char first = input.charAt(0);
+ char capitalized = Character.toUpperCase(first);
+ return first == capitalized ? input : capitalized + input.substring(1);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/util/StringUtilities.java b/src/main/java/com/google/devtools/build/lib/util/StringUtilities.java
new file mode 100644
index 0000000..9ac1d35
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/StringUtilities.java
@@ -0,0 +1,207 @@
+// Copyright 2014 Google Inc. 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.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.common.escape.CharEscaperBuilder;
+import com.google.common.escape.Escaper;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Various utility methods operating on strings.
+ */
+public class StringUtilities {
+
+ private static final Joiner NEWLINE_JOINER = Joiner.on('\n');
+
+ private static final Escaper KEY_ESCAPER = new CharEscaperBuilder()
+ .addEscape('!', "!!")
+ .addEscape('<', "!<")
+ .addEscape('>', "!>")
+ .toEscaper();
+
+ private static final Escaper CONTROL_CHAR_ESCAPER = new CharEscaperBuilder()
+ .addEscape('\r', "\\r")
+ .addEscapes(new char[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, /*13=\r*/
+ 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 127}, "<?>")
+ .toEscaper();
+
+ /**
+ * Java doesn't have multiline string literals, so having to join a bunch
+ * of lines is a very common problem. So, here's a static method that we
+ * can static import in such situations.
+ */
+ public static String joinLines(String... lines) {
+ return NEWLINE_JOINER.join(lines);
+ }
+
+ /**
+ * A corollary to {@link #joinLines(String[])} for collections.
+ */
+ public static String joinLines(Collection<String> lines) {
+ return NEWLINE_JOINER.join(lines);
+ }
+
+ /**
+ * combineKeys(x1, ..., xn):
+ * Computes a string that encodes the sequence
+ * x1, ..., xn. Distinct sequences map to distinct strings.
+ *
+ * The encoding is intended to be vaguely human-readable.
+ */
+ public static String combineKeys(Iterable<String> parts) {
+ final StringBuilder buf = new StringBuilder(128);
+ for (String part : parts) {
+ // We enclose each part in angle brackets to separate them. Some
+ // trickiness is required to ensure that the result is unique (distinct
+ // sequences map to distinct strings): we escape any angle bracket
+ // characters in the parts by preceding them with an escape character
+ // (we use "!") and we also need to escape any escape characters.
+ buf.append('<');
+ buf.append(KEY_ESCAPER.escape(part));
+ buf.append('>');
+ }
+ return buf.toString();
+ }
+
+ /**
+ * combineKeys(x1, ..., xn):
+ * Computes a string that encodes the sequence
+ * x1, ..., xn. Distinct sequences map to distinct strings.
+ *
+ * The encoding is intended to be vaguely human-readable.
+ */
+ public static String combineKeys(String... parts) {
+ return combineKeys(ImmutableList.copyOf(parts));
+ }
+
+ /**
+ * Replaces all occurrences of 'literal' in 'input' with 'replacement'.
+ * Like {@link String#replaceAll(String, String)} but for literal Strings
+ * instead of regular expression patterns.
+ *
+ * @param input the input String
+ * @param literal the literal String to replace in 'input'.
+ * @param replacement the replacement String to replace 'literal' in 'input'.
+ * @return the 'input' String with all occurrences of 'literal' replaced with
+ * 'replacement'.
+ */
+ public static String replaceAllLiteral(String input, String literal,
+ String replacement) {
+ int literalLength = literal.length();
+ if (literalLength == 0) {
+ return input;
+ }
+ StringBuilder result = new StringBuilder(
+ input.length() + replacement.length());
+ int start = 0;
+ int index = 0;
+
+ while ((index = input.indexOf(literal, start)) >= 0) {
+ result.append(input.substring(start, index));
+ result.append(replacement);
+ start = index + literalLength;
+ }
+ result.append(input.substring(start));
+ return result.toString();
+ }
+
+ /**
+ * Creates a simple key-value table of the form
+ *
+ * <pre>
+ * key: some value
+ * another key: some other value
+ * yet another key: and so on ...
+ * </pre>
+ *
+ * The return value will not include a final {@code "\n"}.
+ */
+ public static String layoutTable(Map<String, String> data) {
+ List<String> tableLines = new ArrayList<>();
+ for (Map.Entry<String, String> entry : data.entrySet()) {
+ tableLines.add(entry.getKey() + ": " + entry.getValue());
+ }
+ return NEWLINE_JOINER.join(tableLines);
+ }
+
+ /**
+ * Returns an easy-to-read string approximation of a number of bytes,
+ * e.g. "21MB". Note, these are IEEE units, i.e. decimal not binary powers.
+ */
+ public static String prettyPrintBytes(long bytes) {
+ if (bytes < 1E4) { // up to 10KB
+ return bytes + "B";
+ } else if (bytes < 1E7) { // up to 10MB
+ return ((int) (bytes / 1E3)) + "KB";
+ } else if (bytes < 1E11) { // up to 100GB
+ return ((int) (bytes / 1E6)) + "MB";
+ } else {
+ return ((int) (bytes / 1E9)) + "GB";
+ }
+ }
+
+ /**
+ * Returns true if 'source' contains 'target' as a sub-array.
+ */
+ public static boolean containsSubarray(char[] source, char[] target) {
+ if (target.length > source.length) {
+ return false;
+ }
+ for (int i = 0; i < source.length - target.length + 1; i++) {
+ boolean matches = true;
+ for (int j = 0; j < target.length; j++) {
+ if (source[i + j] != target[j]) {
+ matches = false;
+ break;
+ }
+ }
+ if (matches) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Replace control characters with visible strings.
+ * @return the sanitized string.
+ */
+ public static String sanitizeControlChars(String message) {
+ return CONTROL_CHAR_ESCAPER.escape(message);
+ }
+
+ /**
+ * Converts a Java style function name to a Python style function name the following way:
+ * every upper case character gets replaced with an underscore and its lower case counterpart.
+ * <p>E.g. fooBar --> foo_bar
+ */
+ public static String toPythonStyleFunctionName(String javaStyleFunctionName) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < javaStyleFunctionName.length(); i++) {
+ char c = javaStyleFunctionName.charAt(i);
+ if (Character.isUpperCase(c)) {
+ sb.append('_').append(Character.toLowerCase(c));
+ } else {
+ sb.append(c);
+ }
+ }
+ return sb.toString();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/util/ThreadUtils.java b/src/main/java/com/google/devtools/build/lib/util/ThreadUtils.java
new file mode 100644
index 0000000..7b8ebed
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/ThreadUtils.java
@@ -0,0 +1,50 @@
+// Copyright 2014 Google Inc. 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 java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Utility methods relating to threads and stack traces.
+ */
+public class ThreadUtils {
+ private static final Logger LOG = Logger.getLogger(ThreadUtils.class.getName());
+
+ private ThreadUtils() {
+ }
+
+ /** Write a thread dump to the blaze.INFO log if interrupt took too long. */
+ public static void warnAboutSlowInterrupt() {
+ LOG.warning("Interrupt took too long. Dumping thread state.");
+ for (Map.Entry <Thread, StackTraceElement[]> e : Thread.getAllStackTraces().entrySet()) {
+ Thread t = e.getKey();
+ LOG.warning("\"" + t.getName() + "\"" + " "
+ + " Thread id=" + t.getId() + " " + t.getState());
+ for (StackTraceElement line : e.getValue()) {
+ LOG.warning("\t" + line);
+ }
+ LOG.warning("");
+ }
+ LoggingUtil.logToRemote(Level.WARNING, "Slow interrupt", new SlowInterruptException());
+ }
+
+ private static final class SlowInterruptException extends RuntimeException {
+ public SlowInterruptException() {
+ super("Slow interruption...");
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/util/TimeUtilities.java b/src/main/java/com/google/devtools/build/lib/util/TimeUtilities.java
new file mode 100644
index 0000000..689744a
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/TimeUtilities.java
@@ -0,0 +1,41 @@
+// Copyright 2014 Google Inc. 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;
+
+/**
+ * Various utility methods operating on time values.
+ */
+public class TimeUtilities {
+
+ private TimeUtilities() {
+ }
+
+ /**
+ * Converts time to the user-friendly string representation.
+ *
+ * @param timeInNs The length of time in nanoseconds.
+ */
+ public static String prettyTime(long timeInNs) {
+ double ms = timeInNs / 1000000.0;
+ if (ms < 10.0) {
+ return String.format("%.2f ms", ms);
+ } else if (ms < 100.0) {
+ return String.format("%.1f ms", ms);
+ } else if (ms < 1000.0) {
+ return String.format("%.0f ms", ms);
+ }
+ return String.format("%.3f s", ms / 1000.0);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/util/UserUtils.java b/src/main/java/com/google/devtools/build/lib/util/UserUtils.java
new file mode 100644
index 0000000..93e2a66
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/UserUtils.java
@@ -0,0 +1,58 @@
+// Copyright 2014 Google Inc. 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.base.Strings;
+
+import java.util.Map;
+
+/**
+ * User information utility methods.
+ */
+public final class UserUtils {
+
+ private static final String ORIGINATING_USER_KEY = "BLAZE_ORIGINATING_USER";
+
+ private UserUtils() {
+ // prohibit instantiation
+ }
+
+ private static class Holder {
+ static final String userName = System.getProperty("user.name");
+ }
+
+ /**
+ * Returns the user name as provided by system property 'user.name'.
+ */
+ public static String getUserName() {
+ return Holder.userName;
+ }
+
+ /**
+ * Returns the originating user for this build from the command-line or the environment.
+ */
+ public static String getOriginatingUser(String originatingUser,
+ Map<String, String> clientEnv) {
+ if (!Strings.isNullOrEmpty(originatingUser)) {
+ return originatingUser;
+ }
+
+ if (!Strings.isNullOrEmpty(clientEnv.get(ORIGINATING_USER_KEY))) {
+ return clientEnv.get(ORIGINATING_USER_KEY);
+ }
+
+ return UserUtils.getUserName();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/util/VarInt.java b/src/main/java/com/google/devtools/build/lib/util/VarInt.java
new file mode 100644
index 0000000..fd5daab
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/VarInt.java
@@ -0,0 +1,286 @@
+// Copyright 2014 Google Inc. 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 java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+
+/**
+ * Common methods to encode and decode varints and varlongs into ByteBuffers and
+ * arrays.
+ */
+public class VarInt {
+
+ /**
+ * Maximum encoded size of 32-bit positive integers (in bytes)
+ */
+ public static final int MAX_VARINT_SIZE = 5;
+
+ /**
+ * maximum encoded size of 64-bit longs, and negative 32-bit ints (in bytes)
+ */
+ public static final int MAX_VARLONG_SIZE = 10;
+
+ private VarInt() { }
+
+ /** Returns the encoding size in bytes of its input value.
+ * @param i the integer to be measured
+ * @return the encoding size in bytes of its input value
+ */
+ public static int varIntSize(int i) {
+ int result = 0;
+ do {
+ result++;
+ i >>>= 7;
+ } while (i != 0);
+ return result;
+ }
+
+ /**
+ * Reads a varint from src, places its values into the first element of
+ * dst and returns the offset in to src of the first byte after the varint.
+ *
+ * @param src source buffer to retrieve from
+ * @param offset offset within src
+ * @param dst the resulting int value
+ * @return the updated offset after reading the varint
+ */
+ public static int getVarInt(byte[] src, int offset, int[] dst) {
+ int result = 0;
+ int shift = 0;
+ int b;
+ do {
+ if (shift >= 32) {
+ // Out of range
+ throw new IndexOutOfBoundsException("varint too long");
+ }
+ // Get 7 bits from next byte
+ b = src[offset++];
+ result |= (b & 0x7F) << shift;
+ shift += 7;
+ } while ((b & 0x80) != 0);
+ dst[0] = result;
+ return offset;
+ }
+
+ /**
+ * Encodes an integer in a variable-length encoding, 7 bits per byte, into a
+ * destination byte[], following the protocol buffer convention.
+ *
+ * @param v the int value to write to sink
+ * @param sink the sink buffer to write to
+ * @param offset the offset within sink to begin writing
+ * @return the updated offset after writing the varint
+ */
+ public static int putVarInt(int v, byte[] sink, int offset) {
+ do {
+ // Encode next 7 bits + terminator bit
+ int bits = v & 0x7F;
+ v >>>= 7;
+ byte b = (byte) (bits + ((v != 0) ? 0x80 : 0));
+ sink[offset++] = b;
+ } while (v != 0);
+ return offset;
+ }
+
+ /**
+ * Reads a varint from the current position of the given ByteBuffer and
+ * returns the decoded value as 32 bit integer.
+ *
+ * <p>The position of the buffer is advanced to the first byte after the
+ * decoded varint.
+ *
+ * @param src the ByteBuffer to get the var int from
+ * @return The integer value of the decoded varint
+ */
+ public static int getVarInt(ByteBuffer src) {
+ int tmp;
+ if ((tmp = src.get()) >= 0) {
+ return tmp;
+ }
+ int result = tmp & 0x7f;
+ if ((tmp = src.get()) >= 0) {
+ result |= tmp << 7;
+ } else {
+ result |= (tmp & 0x7f) << 7;
+ if ((tmp = src.get()) >= 0) {
+ result |= tmp << 14;
+ } else {
+ result |= (tmp & 0x7f) << 14;
+ if ((tmp = src.get()) >= 0) {
+ result |= tmp << 21;
+ } else {
+ result |= (tmp & 0x7f) << 21;
+ result |= (tmp = src.get()) << 28;
+ while (tmp < 0) {
+ // We get into this loop only in the case of overflow.
+ // By doing this, we can call getVarInt() instead of
+ // getVarLong() when we only need an int.
+ tmp = src.get();
+ }
+ }
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Encodes an integer in a variable-length encoding, 7 bits per byte, to a
+ * ByteBuffer sink.
+ * @param v the value to encode
+ * @param sink the ByteBuffer to add the encoded value
+ */
+ public static void putVarInt(int v, ByteBuffer sink) {
+ while (true) {
+ int bits = v & 0x7f;
+ v >>>= 7;
+ if (v == 0) {
+ sink.put((byte) bits);
+ return;
+ }
+ sink.put((byte) (bits | 0x80));
+ }
+ }
+
+ /**
+ * Reads a varint from the given InputStream and returns the decoded value
+ * as an int.
+ *
+ * @param inputStream the InputStream to read from
+ */
+ public static int getVarInt(InputStream inputStream) throws IOException {
+ int result = 0;
+ int shift = 0;
+ int b;
+ do {
+ if (shift >= 32) {
+ // Out of range
+ throw new IndexOutOfBoundsException("varint too long");
+ }
+ // Get 7 bits from next byte
+ b = inputStream.read();
+ result |= (b & 0x7F) << shift;
+ shift += 7;
+ } while ((b & 0x80) != 0);
+ return result;
+ }
+
+ /**
+ * Encodes an integer in a variable-length encoding, 7 bits per byte, and
+ * writes it to the given OutputStream.
+ *
+ * @param v the value to encode
+ * @param outputStream the OutputStream to write to
+ */
+ public static void putVarInt(int v, OutputStream outputStream) throws IOException {
+ byte[] bytes = new byte[varIntSize(v)];
+ putVarInt(v, bytes, 0);
+ outputStream.write(bytes);
+ }
+
+ /**
+ * Returns the encoding size in bytes of its input value.
+ *
+ * @param v the long to be measured
+ * @return the encoding size in bytes of a given long value.
+ */
+ public static int varLongSize(long v) {
+ int result = 0;
+ do {
+ result++;
+ v >>>= 7;
+ } while (v != 0);
+ return result;
+ }
+
+ /**
+ * Reads an up to 64 bit long varint from the current position of the
+ * given ByteBuffer and returns the decoded value as long.
+ *
+ * <p>The position of the buffer is advanced to the first byte after the
+ * decoded varint.
+ *
+ * @param src the ByteBuffer to get the var int from
+ * @return The integer value of the decoded long varint
+ */
+ public static long getVarLong(ByteBuffer src) {
+ long tmp;
+ if ((tmp = src.get()) >= 0) {
+ return tmp;
+ }
+ long result = tmp & 0x7f;
+ if ((tmp = src.get()) >= 0) {
+ result |= tmp << 7;
+ } else {
+ result |= (tmp & 0x7f) << 7;
+ if ((tmp = src.get()) >= 0) {
+ result |= tmp << 14;
+ } else {
+ result |= (tmp & 0x7f) << 14;
+ if ((tmp = src.get()) >= 0) {
+ result |= tmp << 21;
+ } else {
+ result |= (tmp & 0x7f) << 21;
+ if ((tmp = src.get()) >= 0) {
+ result |= tmp << 28;
+ } else {
+ result |= (tmp & 0x7f) << 28;
+ if ((tmp = src.get()) >= 0) {
+ result |= tmp << 35;
+ } else {
+ result |= (tmp & 0x7f) << 35;
+ if ((tmp = src.get()) >= 0) {
+ result |= tmp << 42;
+ } else {
+ result |= (tmp & 0x7f) << 42;
+ if ((tmp = src.get()) >= 0) {
+ result |= tmp << 49;
+ } else {
+ result |= (tmp & 0x7f) << 49;
+ if ((tmp = src.get()) >= 0) {
+ result |= tmp << 56;
+ } else {
+ result |= (tmp & 0x7f) << 56;
+ result |= ((long) src.get()) << 63;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Encodes a long integer in a variable-length encoding, 7 bits per byte, to a
+ * ByteBuffer sink.
+ * @param v the value to encode
+ * @param sink the ByteBuffer to add the encoded value
+ */
+ public static void putVarLong(long v, ByteBuffer sink) {
+ while (true) {
+ int bits = ((int) v) & 0x7f;
+ v >>>= 7;
+ if (v == 0) {
+ sink.put((byte) bits);
+ return;
+ }
+ sink.put((byte) (bits | 0x80));
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/util/io/AnsiTerminal.java b/src/main/java/com/google/devtools/build/lib/util/io/AnsiTerminal.java
new file mode 100644
index 0000000..93bc12a
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/io/AnsiTerminal.java
@@ -0,0 +1,198 @@
+// Copyright 2014 Google Inc. 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.io;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * A class which encapsulates the fancy curses-type stuff that you can do using
+ * standard ANSI terminal control sequences.
+ */
+public class AnsiTerminal {
+ private static final byte[] ESC = {27, (byte) '['};
+ private static final byte BEL = 7;
+ private static final byte UP = (byte) 'A';
+ private static final byte ERASE_LINE = (byte) 'K';
+ private static final byte SET_GRAPHICS = (byte) 'm';
+ private static final byte TEXT_BOLD = (byte) '1';
+ private static final byte[] SET_TERM_TITLE = {27, (byte) ']', (byte) '0', (byte) ';'};
+
+ public static final String FG_BLACK = "30";
+ public static final String FG_RED = "31";
+ public static final String FG_GREEN = "32";
+ public static final String FG_YELLOW = "33";
+ public static final String FG_BLUE = "34";
+ public static final String FG_MAGENTA = "35";
+ public static final String FG_CYAN = "36";
+ public static final String FG_GRAY = "37";
+ public static final String BG_BLACK = "40";
+ public static final String BG_RED = "41";
+ public static final String BG_GREEN = "42";
+ public static final String BG_YELLOW = "43";
+ public static final String BG_BLUE = "44";
+ public static final String BG_MAGENTA = "45";
+ public static final String BG_CYAN = "46";
+ public static final String BG_GRAY = "47";
+
+ public static byte[] CR = { 13 };
+
+ private final OutputStream out;
+
+ /**
+ * Creates an AnsiTerminal object wrapping an output stream which is going to
+ * be displayed in an ANSI compatible terminal or shell window.
+ *
+ * @param out the output stream
+ */
+ public AnsiTerminal(OutputStream out) {
+ this.out = out;
+ }
+
+ /**
+ * Moves the cursor upwards by a specified number of lines. This will not
+ * cause any scrolling if it tries to move above the top of the terminal
+ * window.
+ */
+ public void cursorUp(int numLines) throws IOException {
+ writeBytes(ESC, ("" + numLines).getBytes(), new byte[] { UP });
+ }
+
+ /**
+ * Clear the current terminal line from the cursor position to the end.
+ */
+ public void clearLine() throws IOException {
+ writeEscapeSequence(ERASE_LINE);
+ }
+
+ /**
+ * Makes any text output to the terminal appear in bold.
+ */
+ public void textBold() throws IOException {
+ writeEscapeSequence(TEXT_BOLD, SET_GRAPHICS);
+ }
+
+ /**
+ * Set the color of the foreground or background of the terminal.
+ *
+ * @param color one of the foreground or background color
+ * constants
+ */
+ public void setTextColor(String color) throws IOException {
+ writeBytes(ESC, color.getBytes(), new byte[] { SET_GRAPHICS });
+ }
+
+ /**
+ * Resets the terminal colors and fonts to defaults.
+ */
+ public void resetTerminal() throws IOException {
+ writeEscapeSequence((byte)'0', (byte)'m');
+ }
+
+ /**
+ * Makes text print on the terminal in red.
+ */
+ public void textRed() throws IOException {
+ setTextColor(FG_RED);
+ }
+
+ /**
+ * Makes text print on the terminal in blue.
+ */
+ public void textBlue() throws IOException {
+ setTextColor(FG_BLUE);
+ }
+
+ /**
+ * Makes text print on the terminal in red.
+ */
+ public void textGreen() throws IOException {
+ setTextColor(FG_GREEN);
+ }
+
+ /**
+ * Makes text print on the terminal in red.
+ */
+ public void textMagenta() throws IOException {
+ setTextColor(FG_MAGENTA);
+ }
+
+ /**
+ * Set the terminal title.
+ */
+ public void setTitle(String title) throws IOException {
+ writeBytes(SET_TERM_TITLE, title.getBytes(), new byte[] { BEL });
+ }
+
+ /**
+ * Writes a string to the terminal using the current font, color and cursor
+ * position settings.
+ *
+ * @param text the text to write
+ */
+ public void writeString(String text) throws IOException {
+ out.write(text.getBytes());
+ }
+
+ /**
+ * Writes a byte sequence to the terminal using the current font, color and
+ * cursor position settings.
+ *
+ * @param bytes the bytes to write
+ */
+ public void writeBytes(byte[] bytes) throws IOException {
+ out.write(bytes);
+ }
+
+ /**
+ * Utility method which makes it easier to generate the control sequences for
+ * the terminal.
+ *
+ * @param bytes bytes which should be prefixed with the terminal escape
+ * sequence to produce a valid control sequence
+ */
+ private void writeEscapeSequence(byte... bytes) throws IOException {
+ writeBytes(ESC, bytes);
+ }
+
+ /**
+ * Utility method for generating control sequences. Takes a collection of byte
+ * arrays, which contain the components of a control sequence, concatenates
+ * them, and prints them to the terminal.
+ *
+ * @param stuff the byte arrays that make up the sequence to be sent to the
+ * terminal
+ */
+ private void writeBytes(byte[]... stuff) throws IOException {
+ for (byte[] bytes : stuff) {
+ out.write(bytes);
+ }
+ }
+
+ /**
+ * Sends a carriage return to the terminal.
+ */
+ public void cr() throws IOException {
+ writeBytes(CR);
+ }
+
+ /**
+ * Flushes the underlying stream.
+ * This class does not do any buffering of its own, but the underlying
+ * OutputStream may.
+ */
+ public void flush() throws IOException {
+ out.flush();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/util/io/AnsiTerminalPrinter.java b/src/main/java/com/google/devtools/build/lib/util/io/AnsiTerminalPrinter.java
new file mode 100644
index 0000000..726c5dd
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/io/AnsiTerminalPrinter.java
@@ -0,0 +1,156 @@
+// Copyright 2014 Google Inc. 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.io;
+
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadCompatible;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.util.EnumSet;
+import java.util.logging.Logger;
+import java.util.regex.Pattern;
+
+/**
+ * Allows to print "colored" strings by parsing predefined string keywords,
+ * which, depending on the useColor value are either replaced with ANSI terminal
+ * coloring sequences (as defined by the {@link AnsiTerminal} class) or stripped.
+ *
+ * Supported keywords are defined by the enum {@link AnsiTerminalPrinter.Mode}.
+ * Following keywords are supported:
+ * INFO - switches color to green.
+ * ERROR - switches color to bold red.
+ * WARNING - switches color to magenta.
+ * NORMAL - resets terminal to the default state.
+ *
+ * Each keyword is starts with prefix "{#" followed by the enum constant name
+ * and suffix "#}". Keywords should not be inserted manually - provided enum
+ * constants should be used instead.
+ */
+@ThreadCompatible
+public class AnsiTerminalPrinter {
+
+ private static final String MODE_PREFIX = "{#";
+ private static final String MODE_SUFFIX = "#}";
+
+ // Mode pattern must match MODE_PREFIX and do lookahead for the rest of the
+ // mode string.
+ private static final String MODE_PATTERN = "\\{\\#(?=[A-Z]+\\#\\})";
+
+ /**
+ * List of supported coloring modes for the {@link AnsiTerminalPrinter}.
+ */
+ public static enum Mode {
+ INFO, // green
+ ERROR, // bold red
+ WARNING, // magenta
+ DEFAULT; // default color
+
+ @Override
+ public String toString() {
+ return MODE_PREFIX + name() + MODE_SUFFIX;
+ }
+ }
+
+ private static final Logger LOG = Logger.getLogger(AnsiTerminalPrinter.class.getName());
+ private static final EnumSet<Mode> MODES = EnumSet.allOf(Mode.class);
+ private static final Pattern PATTERN = Pattern.compile(MODE_PATTERN);
+
+ private final OutputStream stream;
+ private final PrintWriter writer;
+ private final AnsiTerminal terminal;
+ private boolean useColor;
+ private Mode lastMode = Mode.DEFAULT;
+
+ /**
+ * Creates new instance using provided OutputStream and sets coloring logic
+ * for that instance.
+ */
+ public AnsiTerminalPrinter(OutputStream out, boolean useColor) {
+ this.useColor = useColor;
+ terminal = new AnsiTerminal(out);
+ writer = new PrintWriter(out, true);
+ stream = out;
+ }
+
+ /**
+ * Writes the specified string to the output stream while injecting coloring
+ * sequences when appropriate mode keyword is found and flushes.
+ *
+ * List of supported mode keywords is defined by the enum {@link Mode}.
+ *
+ * See class documentation for details.
+ */
+ public void print(String str) {
+ for (String part : PATTERN.split(str)) {
+ int index = part.indexOf(MODE_SUFFIX);
+ // Mode name will contain at least one character, so suffix index
+ // must be at least 1. If it isn't then there is no match.
+ if (index > 1) {
+ for (Mode mode : MODES) {
+ if (index == mode.name().length() && part.startsWith(mode.name())) {
+ setupTerminal(mode);
+ part = part.substring(index + MODE_SUFFIX.length());
+ break;
+ }
+ }
+ }
+ writer.print(part);
+ writer.flush();
+ }
+ }
+
+ public void printLn(String str) {
+ print(str + "\n");
+ }
+
+ /**
+ * Returns the underlying OutputStream.
+ */
+ public OutputStream getOutputStream() {
+ return stream;
+ }
+
+ /**
+ * Injects coloring escape sequences if output should be colored and mode
+ * has been changed.
+ */
+ private void setupTerminal(Mode mode) {
+ if (!useColor) {
+ return;
+ }
+ try {
+ if (lastMode != mode) {
+ terminal.resetTerminal();
+ lastMode = mode;
+ if (mode == Mode.DEFAULT) {
+ return; // Terminal is already reset - nothing else to do.
+ } else if (mode == Mode.INFO) {
+ terminal.textGreen();
+ } else if (mode == Mode.ERROR) {
+ terminal.textRed();
+ terminal.textBold();
+ } else if (mode == Mode.WARNING) {
+ terminal.textMagenta();
+ }
+ }
+ } catch (IOException e) {
+ // AnsiTerminal state is now considered to be inconsistent - coloring
+ // should be disabled to prevent future use of AnsiTerminal instance.
+ LOG.warning("Disabling coloring due to " + e.getMessage());
+ useColor = false;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/util/io/DelegatingOutErr.java b/src/main/java/com/google/devtools/build/lib/util/io/DelegatingOutErr.java
new file mode 100644
index 0000000..83ccf2f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/io/DelegatingOutErr.java
@@ -0,0 +1,113 @@
+// Copyright 2014 Google Inc. 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.io;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * An {@link OutErr} specialization that supports subscribing / removing
+ * sinks, using {@link #addSink(OutErr)} and {@link #removeSink(OutErr)}.
+ * A sink is a destination to which the {@link DelegatingOutErr} will write.
+ *
+ * Also, we can hook up {@link System#out} / {@link System#err} as sources.
+ */
+public final class DelegatingOutErr extends OutErr {
+
+ /**
+ * Create a new instance to which no sinks have subscribed (basically just
+ * like a {@code /dev/null}.
+ */
+ public DelegatingOutErr() {
+ super(new DelegatingOutputStream(), new DelegatingOutputStream());
+ }
+
+
+ private final DelegatingOutputStream outSink() {
+ return (DelegatingOutputStream) getOutputStream();
+ }
+
+ private final DelegatingOutputStream errSink() {
+ return (DelegatingOutputStream) getErrorStream();
+ }
+
+ /**
+ * Add a sink, that is, after calling this method, {@code outErrSink} will
+ * receive all output / errors written to {@code this} object.
+ */
+ public void addSink(OutErr outErrSink) {
+ outSink().addSink(outErrSink.getOutputStream());
+ errSink().addSink(outErrSink.getErrorStream());
+ }
+
+ /**
+ * Remove the sink, that is, after calling this method, {@code outErrSink}
+ * will no longer receive output / errors written to {@code this} object.
+ */
+ public void removeSink(OutErr outErrSink) {
+ outSink().removeSink(outErrSink.getOutputStream());
+ errSink().removeSink(outErrSink.getErrorStream());
+ }
+
+ private static class DelegatingOutputStream extends OutputStream {
+
+ private final List<OutputStream> sinks = new ArrayList<>();
+
+ public void addSink(OutputStream sink) {
+ sinks.add(sink);
+ }
+
+ public void removeSink(OutputStream sink) {
+ sinks.remove(sink);
+ }
+
+ @Override
+ public void write(int b) throws IOException {
+ for (OutputStream sink : sinks) {
+ sink.write(b);
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ for (OutputStream sink : sinks) {
+ sink.close();
+ }
+ }
+
+ @Override
+ public void flush() throws IOException {
+ for (OutputStream sink : sinks) {
+ sink.flush();
+ }
+ }
+
+ @Override
+ public void write(byte[] b, int off, int len) throws IOException {
+ for (OutputStream sink : sinks) {
+ sink.write(b, off, len);
+ }
+ }
+
+ @Override
+ public void write(byte[] b) throws IOException {
+ for (OutputStream sink : sinks) {
+ sink.write(b);
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/util/io/FileOutErr.java b/src/main/java/com/google/devtools/build/lib/util/io/FileOutErr.java
new file mode 100644
index 0000000..4f9aecf
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/io/FileOutErr.java
@@ -0,0 +1,404 @@
+// Copyright 2014 Google Inc. 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.io;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.io.ByteStreams;
+import com.google.devtools.build.lib.concurrent.ThreadSafety;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
+import com.google.devtools.build.lib.vfs.Path;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PrintStream;
+
+/**
+ * An implementation of {@link OutErr} that captures all out/err output into
+ * a file for stdout and a file for stderr. The files are only created if any
+ * output is made.
+ * The OutErr assumes that the directory that will contain the output file
+ * must exist.
+ *
+ * You should not use this object from multiple different threads.
+ */
+@ThreadSafety.ThreadCompatible
+public class FileOutErr extends OutErr {
+
+ /**
+ * Create a new FileOutErr that will write its input,
+ * if any, to the files specified by stdout/stderr.
+ *
+ * No other process may write to the files,
+ *
+ * @param stdout The file for the stdout of this outErr
+ * @param stderr The file for the stderr of this outErr
+ */
+ public FileOutErr(Path stdout, Path stderr) {
+ super(new FileRecordingOutputStream(stdout), new FileRecordingOutputStream(stderr));
+ }
+
+ /**
+ * Creates a new FileOutErr that writes its input
+ * to the file specified by output. Both stdout/stderr will
+ * be copied into the single file.
+ *
+ * @param output The file for the both stdout and stderr of this outErr.
+ */
+ public FileOutErr(Path output) {
+ // We don't need to create a synchronized funnel here, like in the OutErr -- The
+ // respective functions in the FileRecordingOutputStream take care of locking.
+ this(new FileRecordingOutputStream(output));
+ }
+
+ /**
+ * Creates a new FileOutErr that discards its input. Useful
+ * for testing purposes.
+ */
+ @VisibleForTesting
+ public FileOutErr() {
+ this(new NullFileRecordingOutputStream());
+ }
+
+ private FileOutErr(OutputStream stream) {
+ // We need this function to duplicate the single new object into both arguments
+ // of the super-constructor.
+ super(stream, stream);
+ }
+
+ /**
+ * Returns true if any output was recorded.
+ */
+ public boolean hasRecordedOutput() {
+ return getFileOutputStream().hasRecordedOutput() || getFileErrorStream().hasRecordedOutput();
+ }
+
+ /**
+ * Returns true if output was recorded on stdout.
+ */
+ public boolean hasRecordedStdout() {
+ return getFileOutputStream().hasRecordedOutput();
+ }
+
+ /**
+ * Returns true if output was recorded on stderr.
+ */
+ public boolean hasRecordedStderr() {
+ return getFileErrorStream().hasRecordedOutput();
+ }
+
+ /**
+ * Returns the file this OutErr uses to buffer stdout
+ *
+ * The user must ensure that no other process is writing to the
+ * files at time of creation.
+ *
+ * @return the path object with the contents of stdout
+ */
+ public Path getOutputFile() {
+ return getFileOutputStream().getFile();
+ }
+
+ /**
+ * Returns the file this OutErr uses to buffer stderr.
+ *
+ * @return the path object with the contents of stderr
+ */
+ public Path getErrorFile() {
+ return getFileErrorStream().getFile();
+ }
+
+ /**
+ * Interprets the captured out content as an {@code ISO-8859-1} encoded
+ * string.
+ */
+ public String outAsLatin1() {
+ return getFileOutputStream().getRecordedOutput();
+ }
+
+ /**
+ * Interprets the captured err content as an {@code ISO-8859-1} encoded
+ * string.
+ */
+ public String errAsLatin1() {
+ return getFileErrorStream().getRecordedOutput();
+ }
+
+ /**
+ * Writes the captured out content to the given output stream,
+ * avoiding keeping the entire contents in memory.
+ */
+ public void dumpOutAsLatin1(OutputStream out) {
+ getFileOutputStream().dumpOut(out);
+ }
+
+ /**
+ * Writes the captured out content to the given output stream,
+ * avoiding keeping the entire contents in memory.
+ */
+ public void dumpErrAsLatin1(OutputStream out) {
+ getFileErrorStream().dumpOut(out);
+ }
+
+ private AbstractFileRecordingOutputStream getFileOutputStream() {
+ return (AbstractFileRecordingOutputStream) getOutputStream();
+ }
+
+ private AbstractFileRecordingOutputStream getFileErrorStream() {
+ return (AbstractFileRecordingOutputStream) getErrorStream();
+ }
+
+ /**
+ * An abstract supertype for the two other inner classes in this type
+ * to implement streams that can write to a file.
+ */
+ private abstract static class AbstractFileRecordingOutputStream extends OutputStream {
+
+ /**
+ * Returns true if this FileRecordingOutputStream has encountered an error.
+ *
+ * @return true there was an error, false otherwise.
+ */
+ abstract boolean hadError();
+
+ /**
+ * Returns the file this FileRecordingOutputStream is writing to.
+ */
+ abstract Path getFile();
+
+ /**
+ * Returns true if the FileOutErr has stored output.
+ */
+ abstract boolean hasRecordedOutput();
+
+ /**
+ * Returns the output this AbstractFileOutErr has recorded.
+ */
+ abstract String getRecordedOutput();
+
+ /**
+ * Writes the output to the given output stream,
+ * avoiding keeping the entire contents in memory.
+ */
+ abstract void dumpOut(OutputStream out);
+ }
+
+ /**
+ * An output stream that pretends to capture all its output into a file,
+ * but instead discards it.
+ */
+ private static class NullFileRecordingOutputStream extends AbstractFileRecordingOutputStream {
+
+ NullFileRecordingOutputStream() {
+ }
+
+ @Override
+ boolean hadError() {
+ return false;
+ }
+
+ @Override
+ Path getFile() {
+ return null;
+ }
+
+ @Override
+ boolean hasRecordedOutput() {
+ return false;
+ }
+
+ @Override
+ String getRecordedOutput() {
+ return "";
+ }
+
+ @Override
+ void dumpOut(OutputStream out) {
+ return;
+ }
+
+
+ @Override
+ public void write(byte[] b, int off, int len) {
+ }
+
+ @Override
+ public void write(int b) {
+ }
+
+ @Override
+ public void write(byte[] b) {
+ }
+ }
+
+
+ /**
+ * An output stream that captures all output into a file.
+ * The file is created only if output is received.
+ *
+ * The user must take care that nobody else is writing to the
+ * file that is backing the output stream.
+ *
+ * The write() methods of type are synchronized to ensure
+ * that writes from different threads are not mixed up.
+ *
+ * The outputStream is here only for the benefit of the pumping
+ * IO we're currently using for execution - Once that is gone,
+ * we can remove this output stream and fold its code into the
+ * FileOutErr.
+ */
+ @ThreadSafety.ThreadCompatible
+ private static class FileRecordingOutputStream extends AbstractFileRecordingOutputStream {
+
+ private final Path outputFile;
+ OutputStream outputStream;
+ String error;
+
+ FileRecordingOutputStream(Path outputFile) {
+ this.outputFile = outputFile;
+ }
+
+ @Override
+ boolean hadError() {
+ return error != null;
+ }
+
+ @Override
+ Path getFile() {
+ return outputFile;
+ }
+
+ private OutputStream getOutputStream() throws IOException {
+ // you should hold the lock before you invoke this method
+ if (outputStream == null) {
+ outputStream = outputFile.getOutputStream();
+ }
+ return outputStream;
+ }
+
+ private boolean hasOutputStream() {
+ return outputStream != null;
+ }
+
+ /**
+ * Called whenever the FileRecordingOutputStream finds an error.
+ */
+ private void recordError(IOException exception) {
+ String newErrorText = exception.getMessage();
+ error = (error == null) ? newErrorText : error + "\n" + newErrorText;
+ }
+
+ @Override
+ boolean hasRecordedOutput() {
+ if (hadError()) {
+ return true;
+ }
+ if (!outputFile.exists()) {
+ return false;
+ }
+ try {
+ return outputFile.getFileSize() > 0;
+ } catch (IOException ex) {
+ recordError(ex);
+ return true;
+ }
+ }
+
+ @Override
+ String getRecordedOutput() {
+ StringBuilder result = new StringBuilder();
+ try {
+ if (getFile().exists()) {
+ result.append(FileSystemUtils.readContentAsLatin1(getFile()));
+ }
+ } catch (IOException ex) {
+ recordError(ex);
+ }
+
+ if (hadError()) {
+ result.append(error);
+ }
+ return result.toString();
+ }
+
+ @Override
+ void dumpOut(OutputStream out) {
+ InputStream in = null;
+ try {
+ if (getFile().exists()) {
+ in = new FileInputStream(getFile().getPathString());
+ ByteStreams.copy(in, out);
+ }
+ } catch (IOException ex) {
+ recordError(ex);
+ } finally {
+ if (in != null) {
+ try {
+ in.close();
+ } catch (IOException e) {
+ // Ignore.
+ }
+ }
+ }
+
+ if (hadError()) {
+ PrintStream ps = new PrintStream(out);
+ ps.print(error);
+ ps.flush();
+ }
+ }
+
+ @Override
+ public synchronized void write(byte[] b, int off, int len) {
+ if (len > 0) {
+ try {
+ getOutputStream().write(b, off, len);
+ } catch (IOException ex) {
+ recordError(ex);
+ }
+ }
+ }
+
+ @Override
+ public synchronized void write(int b) {
+ try {
+ getOutputStream().write(b);
+ } catch (IOException ex) {
+ recordError(ex);
+ }
+ }
+
+ @Override
+ public synchronized void write(byte[] b) throws IOException {
+ if (b.length > 0) {
+ getOutputStream().write(b);
+ }
+ }
+
+ @Override
+ public synchronized void flush() throws IOException {
+ if (hasOutputStream()) {
+ getOutputStream().flush();
+ }
+ }
+
+ @Override
+ public synchronized void close() throws IOException {
+ if (hasOutputStream()) {
+ getOutputStream().close();
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/util/io/FileWatcher.java b/src/main/java/com/google/devtools/build/lib/util/io/FileWatcher.java
new file mode 100644
index 0000000..9355cc3
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/io/FileWatcher.java
@@ -0,0 +1,111 @@
+// Copyright 2014 Google Inc. 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.io;
+
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.vfs.Path;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * The FileWatcher dumps the contents of a files into an OutErr.
+ * It then stays active and dumps any content to the OutErr that is
+ * added to the file, until it is told to stop and all output has
+ * been dumped.
+ *
+ * This is useful to emulate streaming test output.
+ */
+@ThreadSafe
+public class FileWatcher extends Thread {
+
+ // How often we check for updates in the file we watch. (in ms)
+ private static final int WATCH_INTERVAL = 100;
+
+ private final Path outputFile;
+ private final OutErr output;
+ private volatile boolean finishPumping;
+ private long toSkip = 0;
+
+ /**
+ * Creates a FileWatcher that will dump any output that is appended to
+ * outputFile onto output. If skipExisting is true, the watcher will not dump
+ * any output that is in outputFile when we construct the watcher. If
+ * skipExisting is false, already existing output will be dumped, too.
+ *
+ * @param outputFile the File to watch
+ * @param output the outErr to dump the files contents to
+ * @param skipExisting whether to dump already existing output or not.
+ */
+ public FileWatcher(Path outputFile, OutErr output, boolean skipExisting) throws IOException {
+ super("Streaming Test Output Pump");
+ this.outputFile = outputFile;
+ this.output = output;
+ finishPumping = false;
+
+ if (outputFile.exists() && skipExisting) {
+ toSkip = outputFile.getFileSize();
+ }
+ }
+
+ /**
+ * Tells the FileWatcher to stop pumping output and finish.
+ * The FileWatcher will only finish until there is no data left to display.
+ * This means that it is rarely a good idea to unconditionally wait for the
+ * FileWatcher thread to terminate -- Instead, it is better to have a timeout.
+ */
+ @ThreadSafe
+ public void stopPumping() {
+ finishPumping = true;
+ }
+
+ @Override
+ public void run() {
+
+ try {
+
+ // Wait until the file exists, or we have to abort.
+ while (!outputFile.exists() && !finishPumping) {
+ Thread.sleep(WATCH_INTERVAL);
+ }
+
+ // Check that we did not have abort before the file was created.
+ if (outputFile.exists()) {
+ try (InputStream inputStream = outputFile.getInputStream();
+ OutputStream outputStream = output.getOutputStream();) {
+ byte[] buffer = new byte[1024];
+ while (!finishPumping || (inputStream.available() != 0)) {
+ if (inputStream.available() != 0) {
+ if (toSkip > 0) {
+ toSkip -= inputStream.skip(toSkip);
+ } else {
+ int read = inputStream.read(buffer);
+ if (read > 0) {
+ outputStream.write(buffer, 0, read);
+ }
+ }
+ } else {
+ Thread.sleep(WATCH_INTERVAL);
+ }
+ }
+ }
+ }
+ } catch (IOException ex) {
+ output.printOutLn("Failure reading or writing: " + ex.getMessage());
+ } catch (InterruptedException ex) {
+ // Don't do anything.
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/util/io/LineFlushingOutputStream.java b/src/main/java/com/google/devtools/build/lib/util/io/LineFlushingOutputStream.java
new file mode 100644
index 0000000..a5a10cf
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/io/LineFlushingOutputStream.java
@@ -0,0 +1,92 @@
+// Copyright 2014 Google Inc. 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.io;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * This stream maintains a buffer, which it flushes upon encountering bytes
+ * that might be new line characters. This stream implements {@link #close()}
+ * as {@link #flush()}.
+ */
+abstract class LineFlushingOutputStream extends OutputStream {
+
+ static final int BUFFER_LENGTH = 8192;
+ protected static byte NEWLINE = '\n';
+
+ /**
+ * The buffer containing the characters that have not been flushed yet.
+ */
+ protected final byte[] buffer = new byte[BUFFER_LENGTH];
+
+ /**
+ * The length of the buffer that's actually used.
+ */
+ protected int len = 0;
+
+ @Override
+ public synchronized void write(byte[] b, int off, int inlen)
+ throws IOException {
+ if (len == BUFFER_LENGTH) {
+ flush();
+ }
+ int charsInLine = 0;
+ while(inlen > charsInLine) {
+ boolean sawNewline = (b[off + charsInLine] == NEWLINE);
+ charsInLine++;
+ if (sawNewline || len + charsInLine == BUFFER_LENGTH) {
+ System.arraycopy(b, off, buffer, len, charsInLine);
+ len += charsInLine;
+ off += charsInLine;
+ inlen -= charsInLine;
+ flush();
+ charsInLine = 0;
+ }
+ }
+ System.arraycopy(b, off, buffer, len, charsInLine);
+ len += charsInLine;
+ }
+
+ @Override
+ public void write(int byteAsInt) throws IOException {
+ byte b = (byte) byteAsInt; // make sure we work with bytes in comparisons
+ write(new byte[] {b}, 0, 1);
+ }
+
+ /**
+ * Close is implemented as {@link #flush()}. Client code must close the
+ * underlying output stream itself in case that's desired.
+ */
+ @Override
+ public synchronized void close() throws IOException {
+ flush();
+ }
+
+ @Override
+ public final synchronized void flush() throws IOException {
+ flushingHook(); // The point of using a hook is to make it synchronized.
+ }
+
+ /**
+ * The implementing class must define this method, which must at least flush
+ * the bytes in {@code buffer[0] - buffer[len - 1]}, and reset {@code len=0}.
+ *
+ * Don't forget to synchronized the implementation of this method on whatever
+ * underlying object it writes to!
+ */
+ protected abstract void flushingHook() throws IOException;
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/util/io/LinePrefixingOutputStream.java b/src/main/java/com/google/devtools/build/lib/util/io/LinePrefixingOutputStream.java
new file mode 100644
index 0000000..23d6cd7
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/io/LinePrefixingOutputStream.java
@@ -0,0 +1,73 @@
+// Copyright 2014 Google Inc. 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.io;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * A stream that writes to another one, emittig a prefix before every line
+ * it emits. This stream will also add a newline for every flush; so it's not
+ * useful for anything other than simple text data (e.g. log files). Here's
+ * an example which demonstrates how an explicit flush or a flush caused by
+ * a full buffer causes a newline to be added to the output.
+ *
+ * <code>
+ * foo bar
+ * baz ba[flush]ng
+ * boo
+ * </code>
+ *
+ * This results in this output being emitted:
+ *
+ * <code>
+ * my prefix: foo bar
+ * my prefix: ba
+ * my prefix: ng
+ * my prefix: boo
+ * </code>
+ */
+public final class LinePrefixingOutputStream extends LineFlushingOutputStream {
+
+ private byte[] linePrefix;
+ private final OutputStream sink;
+
+ public LinePrefixingOutputStream(String linePrefix, OutputStream sink) {
+ this.linePrefix = linePrefix.getBytes(UTF_8);
+ this.sink = sink;
+ }
+
+ @Override
+ protected void flushingHook() throws IOException {
+ synchronized (sink) {
+ if (len == 0) {
+ sink.flush();
+ return;
+ }
+ byte lastByte = buffer[len - 1];
+ boolean lineIsIncomplete = lastByte != NEWLINE;
+ sink.write(linePrefix);
+ sink.write(buffer, 0, len);
+ if (lineIsIncomplete) {
+ sink.write(NEWLINE);
+ }
+ sink.flush();
+ len = 0;
+ }
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/util/io/OutErr.java b/src/main/java/com/google/devtools/build/lib/util/io/OutErr.java
new file mode 100644
index 0000000..c4700ea
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/io/OutErr.java
@@ -0,0 +1,132 @@
+// Copyright 2014 Google Inc. 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.io;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.io.PrintWriter;
+
+/**
+ * A pair of output streams to be used for redirecting the output and error
+ * streams of a subprocess.
+ */
+public class OutErr {
+
+ private final OutputStream out;
+ private final OutputStream err;
+
+ public static final OutErr SYSTEM_OUT_ERR = create(System.out, System.err);
+
+ /**
+ * Creates a new OutErr instance from the specified output and error streams.
+ */
+ public static OutErr create(OutputStream out, OutputStream err) {
+ return new OutErr(out, err);
+ }
+
+ protected OutErr(OutputStream out, OutputStream err) {
+ this.out = out;
+ this.err = err;
+ }
+
+ /**
+ * This method redirects {@link System#out} / {@link System#err} into
+ * {@code this} object. After calling this method, writing to
+ * {@link System#out} or {@link System#err} will result in
+ * {@code "System.out: " + message} or {@code "System.err: " + message}
+ * being written to the OutputStreams of {@code this} instance.
+ *
+ * Note: This method affects global variables.
+ */
+ public void addSystemOutErrAsSource() {
+ System.setOut(new PrintStream(new LinePrefixingOutputStream("System.out: ", getOutputStream()),
+ /*autoflush=*/false));
+ System.setErr(new PrintStream(new LinePrefixingOutputStream("System.err: ", getErrorStream()),
+ /*autoflush=*/false));
+ }
+
+ /**
+ * Creates a new OutErr instance from the specified stream.
+ * Writes to either the output or err of the new OutErr are written
+ * to outputStream, synchronized.
+ */
+ public static OutErr createSynchronizedFunnel(final OutputStream outputStream) {
+ OutputStream syncOut = new OutputStream() {
+
+ @Override
+ public synchronized void write(int b) throws IOException {
+ outputStream.write(b);
+ }
+
+ @Override
+ public synchronized void write(byte b[]) throws IOException {
+ outputStream.write(b);
+ }
+
+ @Override
+ public synchronized void write(byte b[], int off, int len) throws IOException {
+ outputStream.write(b, off, len);
+ }
+
+ @Override
+ public synchronized void flush() throws IOException {
+ outputStream.flush();
+ }
+
+ @Override
+ public synchronized void close() throws IOException {
+ outputStream.close();
+ }
+ };
+
+ return create(syncOut, syncOut);
+ }
+
+ public OutputStream getOutputStream() {
+ return out;
+ }
+
+ public OutputStream getErrorStream() {
+ return err;
+ }
+
+ /**
+ * Writes the specified string to the output stream, and flushes.
+ */
+ public void printOut(String s) {
+ PrintWriter writer = new PrintWriter(out, true);
+ writer.print(s);
+ writer.flush();
+ }
+
+ public void printOutLn(String s) {
+ printOut(s + "\n");
+ }
+
+ /**
+ * Writes the specified string to the error stream, and flushes.
+ */
+ public void printErr(String s) {
+ PrintWriter writer = new PrintWriter(err, true);
+ writer.print(s);
+ writer.flush();
+ }
+
+ public void printErrLn(String s) {
+ printErr(s + "\n");
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/util/io/RecordingOutErr.java b/src/main/java/com/google/devtools/build/lib/util/io/RecordingOutErr.java
new file mode 100644
index 0000000..d276afc
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/io/RecordingOutErr.java
@@ -0,0 +1,91 @@
+// Copyright 2014 Google Inc. 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.io;
+
+import java.io.ByteArrayOutputStream;
+import java.io.UnsupportedEncodingException;
+
+/**
+ * An implementation of {@link OutErr} that captures all out / err output and
+ * makes it available as ISO-8859-1 strings. Useful for implementing test
+ * cases that assert particular output.
+ */
+public class RecordingOutErr extends OutErr {
+
+ public RecordingOutErr() {
+ super(new ByteArrayOutputStream(), new ByteArrayOutputStream());
+ }
+
+ public RecordingOutErr(ByteArrayOutputStream out, ByteArrayOutputStream err) {
+ super(out, err);
+ }
+
+ /**
+ * Reset the captured content; that is, reset the out / err buffers.
+ */
+ public void reset() {
+ getOutputStream().reset();
+ getErrorStream().reset();
+ }
+
+ /**
+ * Interprets the captured out content as an {@code ISO-8859-1} encoded
+ * string.
+ */
+ public String outAsLatin1() {
+ try {
+ return getOutputStream().toString("ISO-8859-1");
+ } catch (UnsupportedEncodingException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ /**
+ * Interprets the captured err content as an {@code ISO-8859-1} encoded
+ * string.
+ */
+ public String errAsLatin1() {
+ try {
+ return getErrorStream().toString("ISO-8859-1");
+ } catch (UnsupportedEncodingException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ /**
+ * Returns true if any output was recorded.
+ */
+ public boolean hasRecordedOutput() {
+ return getOutputStream().size() > 0 || getErrorStream().size() > 0;
+ }
+
+ @Override
+ public String toString() {
+ String out = outAsLatin1();
+ String err = errAsLatin1();
+ return "" + ((out.length() > 0) ? ("stdout: " + out + "\n") : "")
+ + ((err.length() > 0) ? ("stderr: " + err) : "");
+ }
+
+ @Override
+ public ByteArrayOutputStream getOutputStream() {
+ return (ByteArrayOutputStream) super.getOutputStream();
+ }
+
+ @Override
+ public ByteArrayOutputStream getErrorStream() {
+ return (ByteArrayOutputStream) super.getErrorStream();
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/util/io/StreamDemultiplexer.java b/src/main/java/com/google/devtools/build/lib/util/io/StreamDemultiplexer.java
new file mode 100644
index 0000000..ffe0c19
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/io/StreamDemultiplexer.java
@@ -0,0 +1,186 @@
+// Copyright 2014 Google Inc. 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.io;
+
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadCompatible;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * The dual of {@link StreamMultiplexer}: This is an output stream into which
+ * you can dump the multiplexed stream, and it delegates the de-multiplexed
+ * content back into separate channels (instances of {@link OutputStream}).
+ *
+ * The format of the tagged output stream is as follows:
+ *
+ * <pre>
+ * combined :: = [ control_line payload ... ]+
+ * control_line :: = '@' marker '@'? '\n'
+ * payload :: = r'^[^\n]*\n'
+ * </pre>
+ *
+ * For more details, please see {@link StreamMultiplexer}.
+ */
+@ThreadCompatible
+public final class StreamDemultiplexer extends OutputStream {
+
+ @Override
+ public void close() throws IOException {
+ flush();
+ }
+
+ @Override
+ public void flush() throws IOException {
+ if (selectedStream != null) {
+ selectedStream.flush();
+ }
+ }
+
+ private static final byte AT = '@';
+ private static final byte NEWLINE = '\n';
+
+ /**
+ * The output streams, conveniently in an array indexed by the marker byte.
+ * Some of these will be null, most likely.
+ */
+ private final OutputStream[] outputStreams =
+ new OutputStream[Byte.MAX_VALUE + 1];
+
+ /**
+ * Each state in this FSM corresponds to a position in the grammar, which is
+ * simple enough that we can just move through it from beginning to end as we
+ * parse things.
+ */
+ private enum State {
+ EXPECT_CONTROL_STARTING_AT,
+ EXPECT_MARKER_BYTE,
+ EXPECT_AT_OR_NEWLINE,
+ EXPECT_PAYLOAD_OR_NEWLINE
+ }
+
+ private State state = State.EXPECT_CONTROL_STARTING_AT;
+ private boolean addNewlineToPayload;
+ private OutputStream selectedStream;
+
+ /**
+ * Construct a new demultiplexer. The {@code smallestMarkerByte} indicates
+ * the marker byte we would expect for {@code outputStreams[0]} to be used.
+ * So, if this first stream is your stdout and you're using the
+ * {@link StreamMultiplexer}, then you will need to set this to
+ * {@code '1'}. Because {@link StreamDemultiplexer} extends
+ * {@link OutputStream}, this constructor effectively creates an
+ * {@link OutputStream} instance which demultiplexes the tagged data client
+ * code writes to it into {@code outputStreams}.
+ */
+ public StreamDemultiplexer(byte smallestMarkerByte,
+ OutputStream... outputStreams) {
+ for (int i = 0; i < outputStreams.length; i++) {
+ this.outputStreams[smallestMarkerByte + i] = outputStreams[i];
+ }
+ }
+
+ @Override
+ public void write(int b) throws IOException {
+ // This dispatch traverses the finite state machine / grammar.
+ switch (state) {
+ case EXPECT_CONTROL_STARTING_AT:
+ parseControlStartingAt((byte) b);
+ resetFields();
+ break;
+ case EXPECT_MARKER_BYTE:
+ parseMarkerByte((byte) b);
+ break;
+ case EXPECT_AT_OR_NEWLINE:
+ parseAtOrNewline((byte) b);
+ break;
+ case EXPECT_PAYLOAD_OR_NEWLINE:
+ parsePayloadOrNewline((byte) b);
+ break;
+ }
+ }
+
+ /**
+ * Handles {@link State#EXPECT_PAYLOAD_OR_NEWLINE}, which is the payload
+ * we are actually transporting over the wire. At this point we can rely
+ * on a stream having been preselected into {@link #selectedStream}, and
+ * also we will add a newline if {@link #addNewlineToPayload} is set.
+ * Flushes at the end of every payload segment.
+ */
+ private void parsePayloadOrNewline(byte b) throws IOException {
+ if (b == NEWLINE) {
+ if (addNewlineToPayload) {
+ selectedStream.write(NEWLINE);
+ }
+ selectedStream.flush();
+ state = State.EXPECT_CONTROL_STARTING_AT;
+ } else {
+ selectedStream.write(b);
+ selectedStream.flush(); // slow?
+ }
+ }
+
+ /**
+ * Handles {@link State#EXPECT_AT_OR_NEWLINE}, which is either the
+ * suppress newline indicator (at) at the end of a control line, or the end
+ * of a control line.
+ */
+ private void parseAtOrNewline(byte b) throws IOException {
+ if (b == NEWLINE) {
+ state = State.EXPECT_PAYLOAD_OR_NEWLINE;
+ } else if (b == AT) {
+ addNewlineToPayload = false;
+ } else {
+ throw new IOException("Expected @ or \\n. (" + b + ")");
+ }
+ }
+
+ /**
+ * Reset the fields that are affected by our state.
+ */
+ private void resetFields() {
+ selectedStream = null;
+ addNewlineToPayload = true;
+ }
+
+ /**
+ * Handles {@link State#EXPECT_MARKER_BYTE}. The byte determines which stream
+ * we will be using, and will set {@link #selectedStream}.
+ */
+ private void parseMarkerByte(byte markerByte) throws IOException {
+ if (markerByte < 0 || markerByte > Byte.MAX_VALUE) {
+ String msg = "Illegal marker byte (" + markerByte + ")";
+ throw new IllegalArgumentException(msg);
+ }
+ if (markerByte > outputStreams.length
+ || outputStreams[markerByte] == null) {
+ throw new IOException("stream " + markerByte + " not registered.");
+ }
+ selectedStream = outputStreams[markerByte];
+ state = State.EXPECT_AT_OR_NEWLINE;
+ }
+
+ /**
+ * Handles {@link State#EXPECT_CONTROL_STARTING_AT}, the very first '@' with
+ * which each message starts.
+ */
+ private void parseControlStartingAt(byte b) throws IOException {
+ if (b != AT) {
+ throw new IOException("Expected control starting @. (" + b + ", "
+ + (char) b + ")");
+ }
+ state = State.EXPECT_MARKER_BYTE;
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/util/io/StreamMultiplexer.java b/src/main/java/com/google/devtools/build/lib/util/io/StreamMultiplexer.java
new file mode 100644
index 0000000..c214aa5
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/io/StreamMultiplexer.java
@@ -0,0 +1,132 @@
+// Copyright 2014 Google Inc. 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.io;
+
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * Instances of this class are multiplexers, which redirect multiple
+ * output streams into a single output stream with tagging so it can be
+ * de-multiplexed into multiple streams as needed. This allows us to
+ * use one connection for multiple streams, but more importantly it avoids
+ * multiple threads or select etc. on the receiving side: A client on the other
+ * end of a networking connection can simply read the tagged lines and then act
+ * on them within a sigle thread.
+ *
+ * The format of the tagged output stream is as follows:
+ *
+ * <pre>
+ * combined :: = [ control_line payload ... ]+
+ * control_line :: = '@' marker '@'? '\n'
+ * payload :: = r'^[^\n]*\n'
+ * </pre>
+ *
+ * So basically:
+ * <ul>
+ * <li>Control lines alternate with payload lines</li>
+ * <li>Both types of lines end with a newline, and never have a newline in
+ * them.</li>
+ * <li>The marker indicates which stream we mean.
+ * For now, '1'=stdout, '2'=stderr.</li>
+ * <li>The optional second '@' indicates that the following line is
+ * incomplete.</li>
+ * </ul>
+ *
+ * This format is optimized for easy interpretation by a Python client, but it's
+ * also a compromise in that it's still easy to interpret by a human (let's say
+ * you have to read the traffic over a wire for some reason).
+ */
+@ThreadSafe
+public final class StreamMultiplexer {
+
+ public static final byte STDOUT_MARKER = '1';
+ public static final byte STDERR_MARKER = '2';
+ public static final byte CONTROL_MARKER = '3';
+
+ private static final byte AT = '@';
+
+ private final Object mutex = new Object();
+ private final OutputStream multiplexed;
+
+ public StreamMultiplexer(OutputStream multiplexed) {
+ this.multiplexed = multiplexed;
+ }
+
+ private class MarkingStream extends LineFlushingOutputStream {
+
+ private final byte markerByte;
+
+ MarkingStream(byte markerByte) {
+ this.markerByte = markerByte;
+ }
+
+ @Override
+ protected void flushingHook() throws IOException {
+ synchronized (mutex) {
+ if (len == 0) {
+ multiplexed.flush();
+ return;
+ }
+ byte lastByte = buffer[len - 1];
+ boolean lineIsIncomplete = lastByte != NEWLINE;
+
+ multiplexed.write(AT);
+ multiplexed.write(markerByte);
+ if (lineIsIncomplete) {
+ multiplexed.write(AT);
+ }
+ multiplexed.write(NEWLINE);
+ multiplexed.write(buffer, 0, len);
+ if (lineIsIncomplete) {
+ multiplexed.write(NEWLINE);
+ }
+ multiplexed.flush();
+ }
+ len = 0;
+ }
+
+ }
+
+ /**
+ * Create a stream that will tag its contributions into the multiplexed stream
+ * with the marker '1', which means 'stdout'. Each newline byte leads
+ * to a forced automatic flush. Also, this stream never closes the underlying
+ * stream it delegates to - calling its {@code close()} method is equivalent
+ * to calling {@code flush}.
+ */
+ public OutputStream createStdout() {
+ return new MarkingStream(STDOUT_MARKER);
+ }
+
+ /**
+ * Like {@link #createStdout()}, except it tags with the marker '2' to
+ * indicate 'stderr'.
+ */
+ public OutputStream createStderr() {
+ return new MarkingStream(STDERR_MARKER);
+ }
+
+ /**
+ * Like {@link #createStdout()}, except it tags with the marker '3' to
+ * indicate control flow..
+ */
+ public OutputStream createControl() {
+ return new MarkingStream(CONTROL_MARKER);
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/util/io/TimestampGranularityMonitor.java b/src/main/java/com/google/devtools/build/lib/util/io/TimestampGranularityMonitor.java
new file mode 100644
index 0000000..64575ae
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/io/TimestampGranularityMonitor.java
@@ -0,0 +1,194 @@
+// Copyright 2014 Google Inc. 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.io;
+
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadCompatible;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.profiler.Profiler;
+import com.google.devtools.build.lib.profiler.ProfilerTask;
+import com.google.devtools.build.lib.util.Clock;
+
+/**
+ * A utility class for dealing with filesystem timestamp granularity issues.
+ *
+ * <p>
+ * Consider a sequence of commands such as
+ * <pre>
+ * echo ... > foo/bar
+ * blaze query ...
+ * echo ... > foo/bar
+ * blaze query ...
+ * </pre>
+ *
+ * If these commands all run very fast, it is possible that the timestamp
+ * on foo/bar is not changed by the second command, even though some time has
+ * passed, because the times are the same when rounded to the file system
+ * timestamp granularity (often 1 second).
+ * For performance, we assume that files
+ * timestamps haven't changed can safely be cached without reexamining their contents.
+ * But this assumption would be violated in the above scenario.
+ *
+ * <p>
+ * To address this, we record the current time at the start of executing
+ * a Blaze command, and whenever we check the timestamp of a source file
+ * or BUILD file, we check if the timestamp of that source file matches
+ * the current time. If so, we set a flag. At the end of the command,
+ * if the flag was set, then we wait until the clock has advanced, so
+ * that any file modifications performed after the command exits will
+ * result in a different file timestamp.
+ *
+ * <p>
+ * This class implicitly assumes that each filesystem's clock
+ * is the same as either System.currentTimeMillis() or
+ * System.currentTimeMillis() rounded down to the nearest second.
+ * That is not strictly correct; there might be clock skew between
+ * the cpu clock and the file system clocks (e.g. for NFS file systems),
+ * and some file systems might have different granularity (e.g. the old
+ * DOS FAT filesystem has TWO-second granularity timestamps).
+ * Clock skew can be addressed using NTP.
+ * Other granularities could be addressed by small changes to this class,
+ * if it turns out to be needed.
+ *
+ * <p>
+ * Another alternative design that we considered was to write to a file and
+ * read its timestamp. But doing that is a little tricky because we don't have
+ * a FileSystem or Path handy. Also, if we were going to do this, the stamp
+ * file that is used should be in the same file system as the input files.
+ * But the input file system(s) might not be writeable, and even if it is,
+ * it's hard for Blaze to find a writable file on the same filesystem as the
+ * input files.
+ */
+@ThreadCompatible
+public class TimestampGranularityMonitor {
+
+ /**
+ * The time of the start of the current Blaze command,
+ * in milliseconds.
+ */
+ private long commandStartTimeMillis;
+
+ /**
+ * The time of the start of the current Blaze command,
+ * in milliseconds, rounded to one second granularity.
+ */
+ private long commandStartTimeMillisRounded;
+
+ /**
+ * True iff we detected a source file or BUILD file whose (unrounded)
+ * timestamp matched the time at the start of the current Blaze command
+ * rounded to the nearest second.
+ */
+ private volatile boolean waitASecond;
+
+ /**
+ * True iff we detected a source file or BUILD file whose timestamp
+ * exactly matched the time at the start of the current Blaze command
+ * (measuring both in integral numbers of milliseconds).
+ */
+ private volatile boolean waitAMillisecond;
+
+ private final Clock clock;
+
+ public TimestampGranularityMonitor(Clock clock) {
+ this.clock = clock;
+ }
+
+ /**
+ * Record the time at which the Blaze command started.
+ * This is needed for use by waitForTimestampGranularity().
+ */
+ public void setCommandStartTime() {
+ this.commandStartTimeMillis = clock.currentTimeMillis();
+ this.commandStartTimeMillisRounded = roundDown(this.commandStartTimeMillis);
+ this.waitASecond = false;
+ this.waitAMillisecond = false;
+ }
+
+ /**
+ * Record that the output of this Blaze command depended on the contents
+ * of a build file or source file with the specified time stamp.
+ */
+ @ThreadSafe
+ public void notifyDependenceOnFileTime(long mtime) {
+ if (mtime == this.commandStartTimeMillis) {
+ this.waitAMillisecond = true;
+ }
+ if (mtime == this.commandStartTimeMillisRounded) {
+ this.waitASecond = true;
+ }
+ }
+
+ /**
+ * If needed, wait until the next "tick" of the filesystem timestamp clock.
+ * This is done to ensure that files created after the current Blaze command
+ * finishes will have timestamps different than files created before the
+ * current Blaze command started. Otherwise a sequence of commands
+ * such as
+ * <pre>
+ * echo ... > foo/BUILD
+ * blaze query ...
+ * echo ... > foo/BUILD
+ * blaze query ...
+ * </pre>
+ * could return wrong results, due to the contents of package foo
+ * being cached even though foo/BUILD changed.
+ */
+ public void waitForTimestampGranularity(OutErr outErr) {
+ if (this.waitASecond || this.waitAMillisecond) {
+ long startedWaiting = Profiler.nanoTimeMaybe();
+ boolean interrupted = false;
+
+ if (waitASecond) {
+ // 50ms slack after the whole-second boundary
+ while (clock.currentTimeMillis() < commandStartTimeMillisRounded + 1050) {
+ try {
+ Thread.sleep(50 /* milliseconds */);
+ } catch (InterruptedException e) {
+ if (!interrupted) {
+ outErr.printErrLn("INFO: Hang on a second...");
+ interrupted = true;
+ }
+ }
+ }
+ } else {
+ while (clock.currentTimeMillis() == commandStartTimeMillis) {
+ try {
+ Thread.sleep(1 /* milliseconds */);
+ } catch (InterruptedException e) {
+ if (!interrupted) {
+ outErr.printErrLn("INFO: Hang on a millisecond...");
+ interrupted = true;
+ }
+ }
+ }
+ }
+ if (interrupted) {
+ Thread.currentThread().interrupt();
+ }
+
+ Profiler.instance().logSimpleTask(startedWaiting, ProfilerTask.WAIT,
+ "Timestamp granularity");
+ }
+ }
+
+ /**
+ * Rounds the specified time, in milliseconds, down to the nearest second,
+ * and returns the result in milliseconds.
+ */
+ private static long roundDown(long millis) {
+ return millis / 1000 * 1000;
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/AbstractFileSystem.java b/src/main/java/com/google/devtools/build/lib/vfs/AbstractFileSystem.java
new file mode 100644
index 0000000..dd4375c
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/vfs/AbstractFileSystem.java
@@ -0,0 +1,136 @@
+// Copyright 2014 Google Inc. 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.vfs;
+
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.profiler.Profiler;
+import com.google.devtools.build.lib.profiler.ProfilerTask;
+import com.google.devtools.build.lib.unix.FileAccessException;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * This class implements the FileSystem interface using direct calls to the
+ * UNIX filesystem.
+ */
+@ThreadSafe
+abstract class AbstractFileSystem extends FileSystem {
+
+ protected static final String ERR_PERMISSION_DENIED = " (Permission denied)";
+ protected static final Profiler profiler = Profiler.instance();
+
+ @Override
+ protected InputStream getInputStream(Path path) throws FileNotFoundException {
+ // This loop is a workaround for an apparent bug in FileInputStrean.open, which delegates
+ // ultimately to JVM_Open in the Hotspot JVM. This call is not EINTR-safe, so we must do the
+ // retry here.
+ for (;;) {
+ try {
+ return createFileInputStream(path);
+ } catch (FileNotFoundException e) {
+ if (e.getMessage().endsWith("(Interrupted system call)")) {
+ continue;
+ } else {
+ throw e;
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns either normal or profiled FileInputStream.
+ */
+ private InputStream createFileInputStream(Path path) throws FileNotFoundException {
+ final String name = path.toString();
+ if (profiler.isActive() && (profiler.isProfiling(ProfilerTask.VFS_READ) ||
+ profiler.isProfiling(ProfilerTask.VFS_OPEN))) {
+ long startTime = Profiler.nanoTimeMaybe();
+ try {
+ // Replace default FileInputStream instance with the custom one that does profiling.
+ return new FileInputStream(name) {
+ @Override public int read(byte b[]) throws IOException {
+ return read(b, 0, b.length);
+ }
+ @Override public int read(byte b[], int off, int len) throws IOException {
+ long startTime = Profiler.nanoTimeMaybe();
+ try {
+ return super.read(b, off, len);
+ } finally {
+ profiler.logSimpleTask(startTime, ProfilerTask.VFS_READ, name);
+ }
+ }
+ };
+ } finally {
+ profiler.logSimpleTask(startTime, ProfilerTask.VFS_OPEN, name);
+ }
+ } else {
+ // Use normal FileInputStream instance if profiler is not enabled.
+ return new FileInputStream(path.toString());
+ }
+ }
+
+ /**
+ * Returns either normal or profiled FileOutputStream. Should be used by subclasses
+ * to create default OutputStream instance.
+ */
+ protected OutputStream createFileOutputStream(Path path, boolean append)
+ throws FileNotFoundException {
+ final String name = path.toString();
+ if (profiler.isActive() && (profiler.isProfiling(ProfilerTask.VFS_WRITE) ||
+ profiler.isProfiling(ProfilerTask.VFS_OPEN))) {
+ long startTime = Profiler.nanoTimeMaybe();
+ try {
+ return new FileOutputStream(name, append) {
+ @Override public void write(byte b[]) throws IOException {
+ write(b, 0, b.length);
+ }
+ @Override public void write(byte b[], int off, int len) throws IOException {
+ long startTime = Profiler.nanoTimeMaybe();
+ try {
+ super.write(b, off, len);
+ } finally {
+ profiler.logSimpleTask(startTime, ProfilerTask.VFS_WRITE, name);
+ }
+ }
+ };
+ } finally {
+ profiler.logSimpleTask(startTime, ProfilerTask.VFS_OPEN, name);
+ }
+ } else {
+ return new FileOutputStream(name, append);
+ }
+ }
+
+ @Override
+ protected OutputStream getOutputStream(Path path, boolean append) throws IOException {
+ synchronized (path) {
+ try {
+ return createFileOutputStream(path, append);
+ } catch (FileNotFoundException e) {
+ // Why does it throw a *FileNotFoundException* if it can't write?
+ // That does not make any sense! And its in a completely different
+ // format than in other situations, no less!
+ if (e.getMessage().equals(path + ERR_PERMISSION_DENIED)) {
+ throw new FileAccessException(e.getMessage());
+ }
+ throw e;
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/BatchStat.java b/src/main/java/com/google/devtools/build/lib/vfs/BatchStat.java
new file mode 100644
index 0000000..5144f31
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/vfs/BatchStat.java
@@ -0,0 +1,38 @@
+// Copyright 2014 Google Inc. 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.vfs;
+
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * An interface for doing a batch of stat() calls.
+ */
+public interface BatchStat {
+
+ /**
+ *
+ * @param includeDigest whether to include a file digest in the return values.
+ * @param includeLinks whether to include a symlink stat in the return values.
+ * @param paths The input paths to stat(), relative to the exec root.
+ * @return an array list of FileStatusWithDigest in the same order as the input. May
+ * contain null values.
+ * @throws IOException on unexpected failure.
+ * @throws InterruptedException on interrupt.
+ */
+ public List<FileStatusWithDigest> batchStat(boolean includeDigest,
+ boolean includeLinks,
+ Iterable<PathFragment> paths)
+ throws IOException, InterruptedException;
+}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/Canonicalizer.java b/src/main/java/com/google/devtools/build/lib/vfs/Canonicalizer.java
new file mode 100644
index 0000000..294a066
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/vfs/Canonicalizer.java
@@ -0,0 +1,36 @@
+// Copyright 2014 Google Inc. 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.vfs;
+
+import com.google.common.collect.Interner;
+import com.google.common.collect.Interners;
+
+/**
+ * Static singleton holder for certain interning pools.
+ */
+public final class Canonicalizer<E> {
+
+ private static final Interner<PathFragment> FRAGMENT_INTERNER =
+ Interners.newWeakInterner();
+
+ /**
+ * Creates an instance of Canonicalizer tracking path fragments.
+ */
+ public static Interner<PathFragment> fragments() {
+ return FRAGMENT_INTERNER;
+ }
+
+ private Canonicalizer() {
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/Dirent.java b/src/main/java/com/google/devtools/build/lib/vfs/Dirent.java
new file mode 100644
index 0000000..a2ee203
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/vfs/Dirent.java
@@ -0,0 +1,72 @@
+// Copyright 2014 Google Inc. 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.vfs;
+
+import com.google.common.base.Preconditions;
+
+import java.io.Serializable;
+import java.util.Objects;
+
+/**
+ * Directory entry representation returned by {@link Path#readdir}.
+ */
+public class Dirent implements Serializable {
+
+ /** Type of the directory entry */
+ public enum Type {
+ FILE,
+ DIRECTORY,
+ SYMLINK,
+ UNKNOWN;
+ }
+
+ private final String name;
+ private final Type type;
+
+ /** Creates a new dirent with the given name and type, both of which must be non-null. */
+ public Dirent(String name, Type type) {
+ this.name = Preconditions.checkNotNull(name);
+ this.type = Preconditions.checkNotNull(type);
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public Type getType() {
+ return type;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name, type);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof Dirent)) {
+ return false;
+ }
+ if (this == other) {
+ return true;
+ }
+ Dirent otherDirent = (Dirent) other;
+ return name.equals(otherDirent.name) && type.equals(otherDirent.type);
+ }
+
+ @Override
+ public String toString() {
+ return name + "[" + type.toString().toLowerCase() + "]";
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/FileStatus.java b/src/main/java/com/google/devtools/build/lib/vfs/FileStatus.java
new file mode 100644
index 0000000..c57b223
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/vfs/FileStatus.java
@@ -0,0 +1,82 @@
+// Copyright 2014 Google Inc. 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.vfs;
+
+import java.io.IOException;
+
+/**
+ * File status: mode, mtime, size, etc.
+ *
+ * <p>The result of calling any {@code FileStatus} instance method is not
+ * guaranteed to result in I/O to the file system at the moment of the call.
+ * The I/O providing the result (and hence the throwing of an I/O exception,
+ * where applicable) may occur at any moment between the call to {@link
+ * FileSystem#stat} and the call of the {@code FileStatus} instance method.
+ *
+ * <p>Callers therefore cannot assume that all the values are populated
+ * atomically, or that the results of any two {@code FileStatus} methods
+ * correspond to state of the file system at a single moment in time. Nor may
+ * they assume that repeated successful calls to any method of the same
+ * instance will return the same value.
+ *
+ * <p>(This permits conforming implementations to use an atomic {@code stat(2)}
+ * call on file systems where it is available, and individual accessor methods
+ * on those where it is not. Caching is possible but not required.)
+ */
+public interface FileStatus {
+
+ /**
+ * Returns true iff this file is a regular or special file (e.g. socket,
+ * fifo or device).
+ */
+ boolean isFile();
+
+ /**
+ * Returns true iff this file is a directory.
+ */
+ boolean isDirectory();
+
+ /**
+ * Returns true iff this file is a symbolic link.
+ */
+ boolean isSymbolicLink();
+
+ /**
+ * Returns the total size, in bytes, of this file.
+ */
+ long getSize() throws IOException;
+
+ /**
+ * Returns the last modified time of this file's data (milliseconds since
+ * UNIX epoch).
+ */
+ long getLastModifiedTime() throws IOException;
+
+ /**
+ * Returns the last change time of this file, where change means any change
+ * to the file, including metadata changes (milliseconds since UNIX epoch).
+ *
+ * Note: UNIX uses seconds!
+ */
+ long getLastChangeTime() throws IOException;
+
+ /**
+ * Returns the unique file node id. Usually it is computed using both device
+ * and inode numbers.
+ *
+ * <p>Think of this value as a reference to the underlying inode. "mv"ing file a to file b
+ * ought to cause the node ID of b to change, but appending / modifying b should not.
+ */
+ public long getNodeId() throws IOException;
+}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/FileStatusWithDigest.java b/src/main/java/com/google/devtools/build/lib/vfs/FileStatusWithDigest.java
new file mode 100644
index 0000000..3dd62a1
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/vfs/FileStatusWithDigest.java
@@ -0,0 +1,29 @@
+// Copyright 2014 Google Inc. 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.vfs;
+
+import java.io.IOException;
+
+import javax.annotation.Nullable;
+
+/**
+ * A FileStatus that also optionally returns a Digest.
+ */
+public interface FileStatusWithDigest extends FileStatus {
+ /**
+ * @return the digest of the file - optional.
+ */
+ @Nullable
+ byte[] getDigest() throws IOException;
+}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/FileStatusWithDigestAdapter.java b/src/main/java/com/google/devtools/build/lib/vfs/FileStatusWithDigestAdapter.java
new file mode 100644
index 0000000..3f608ce
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/vfs/FileStatusWithDigestAdapter.java
@@ -0,0 +1,76 @@
+// Copyright 2014 Google Inc. 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.vfs;
+
+import com.google.common.base.Preconditions;
+
+import java.io.IOException;
+
+import javax.annotation.Nullable;
+
+/**
+ * An adapter from FileStatus to FileStatusWithDigest.
+ */
+public class FileStatusWithDigestAdapter implements FileStatusWithDigest {
+ private final FileStatus stat;
+
+ public static FileStatusWithDigest adapt(FileStatus stat) {
+ return stat == null ? null : new FileStatusWithDigestAdapter(stat);
+ }
+
+ private FileStatusWithDigestAdapter(FileStatus stat) {
+ this.stat = Preconditions.checkNotNull(stat);
+ }
+
+ @Nullable
+ @Override
+ public byte[] getDigest() {
+ return null;
+ }
+
+ @Override
+ public boolean isFile() {
+ return stat.isFile();
+ }
+
+ @Override
+ public boolean isDirectory() {
+ return stat.isDirectory();
+ }
+
+ @Override
+ public boolean isSymbolicLink() {
+ return stat.isSymbolicLink();
+ }
+
+ @Override
+ public long getSize() throws IOException {
+ return stat.getSize();
+ }
+
+ @Override
+ public long getLastModifiedTime() throws IOException {
+ return stat.getLastModifiedTime();
+ }
+
+ @Override
+ public long getLastChangeTime() throws IOException {
+ return stat.getLastChangeTime();
+ }
+
+ @Override
+ public long getNodeId() throws IOException {
+ return stat.getNodeId();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/FileSystem.java b/src/main/java/com/google/devtools/build/lib/vfs/FileSystem.java
new file mode 100644
index 0000000..9d416098
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/vfs/FileSystem.java
@@ -0,0 +1,632 @@
+// Copyright 2014 Google Inc. 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.vfs;
+
+import static java.nio.charset.StandardCharsets.ISO_8859_1;
+
+import com.google.common.collect.Lists;
+import com.google.common.hash.Hashing;
+import com.google.common.io.ByteSource;
+import com.google.common.io.CharStreams;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.vfs.Dirent.Type;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * This interface models a file system using UNIX the naming scheme.
+ */
+@ThreadSafe
+public abstract class FileSystem {
+
+ /**
+ * An exception thrown when attempting to resolve an ordinary file as a symlink.
+ */
+ protected static final class NotASymlinkException extends IOException {
+ public NotASymlinkException(Path path) {
+ super(path.toString());
+ }
+ }
+
+ protected final Path rootPath;
+
+ protected FileSystem() {
+ this.rootPath = createRootPath();
+ }
+
+ /**
+ * Creates the root of all paths used by this filesystem. This is a hook
+ * allowing subclasses to define their own root path class. All other paths
+ * are created via the root path's {@link Path#createChildPath(String)} method.
+ * <p>
+ * Beware: this is called during the FileSystem constructor which may occur
+ * before subclasses are completely initialized.
+ */
+ protected Path createRootPath() {
+ return new Path(this);
+ }
+
+ /**
+ * Returns an absolute path instance, given an absolute path name, without
+ * double slashes, .., or . segments. While this method will normalize the
+ * path representation by creating a structured/parsed representation, it will
+ * not cause any IO. (e.g., it will not resolve symbolic links if it's a Unix
+ * file system.
+ */
+ public Path getPath(String pathName) {
+ return getPath(new PathFragment(pathName));
+ }
+
+ /**
+ * Returns an absolute path instance, given an absolute path name, without
+ * double slashes, .., or . segments. While this method will normalize the
+ * path representation by creating a structured/parsed representation, it will
+ * not cause any IO. (e.g., it will not resolve symbolic links if it's a Unix
+ * file system.
+ */
+ public Path getPath(PathFragment pathName) {
+ if (!pathName.isAbsolute()) {
+ throw new IllegalArgumentException(pathName.getPathString() + " (not an absolute path)");
+ }
+ return rootPath.getRelative(pathName);
+ }
+
+ /**
+ * Returns a path representing the root directory of the current file system.
+ */
+ public final Path getRootDirectory() {
+ return rootPath;
+ }
+
+ /**
+ * Returns whether or not the FileSystem supports modifications of files and
+ * file entries.
+ *
+ * <p>Returns true if FileSystem supports the following:
+ * <ul>
+ * <li>{@link #setWritable(Path, boolean)}</li>
+ * <li>{@link #setExecutable(Path, boolean)}</li>
+ * </ul>
+ *
+ * The above calls will result in an {@link UnsupportedOperationException} on
+ * a FileSystem where this method returns {@code false}.
+ */
+ public abstract boolean supportsModifications();
+
+ /**
+ * Returns whether or not the FileSystem supports symbolic links.
+ *
+ * <p>Returns true if FileSystem supports the following:
+ * <ul>
+ * <li>{@link #createSymbolicLink(Path, PathFragment)}</li>
+ * <li>{@link #getFileSize(Path, boolean)} where {@code followSymlinks=false}</li>
+ * <li>{@link #getLastModifiedTime(Path, boolean)} where {@code followSymlinks=false}</li>
+ * <li>{@link #readSymbolicLink(Path)} where the link points to a non-existent file</li>
+ * </ul>
+ *
+ * The above calls will result in an {@link UnsupportedOperationException} on
+ * a FileSystem where this method returns {@code false}.
+ */
+ public abstract boolean supportsSymbolicLinks();
+
+ /**
+ * Returns the type of the file system path belongs to.
+ *
+ * <p>The string returned is obtained directly from the operating system, so
+ * it's a best guess in absence of a guaranteed api.
+ *
+ * <p>This implementation uses <code>/proc/mounts</code> to determine the
+ * file system type.
+ */
+ public String getFileSystemType(Path path) {
+ String fileSystem = "unknown";
+ int bestMountPointSegmentCount = -1;
+ try {
+ Path canonicalPath = path.resolveSymbolicLinks();
+ Path mountTable = path.getRelative("/proc/mounts");
+ for (String line : CharStreams.readLines(new InputStreamReader(mountTable.getInputStream(),
+ ISO_8859_1))) {
+ String[] words = line.split("\\s+");
+ if (words.length >= 3) {
+ if (!words[1].startsWith("/")) {
+ continue;
+ }
+ Path mountPoint = path.getFileSystem().getPath(words[1]);
+ int segmentCount = mountPoint.asFragment().segmentCount();
+ if (canonicalPath.startsWith(mountPoint) && segmentCount > bestMountPointSegmentCount) {
+ bestMountPointSegmentCount = segmentCount;
+ fileSystem = words[2];
+ }
+ }
+ }
+ } catch (IOException e) {
+ // pass
+ }
+ return fileSystem;
+ }
+
+
+ /**
+ * Creates a directory with the name of the current path. See
+ * {@link Path#createDirectory} for specification.
+ */
+ protected abstract boolean createDirectory(Path path) throws IOException;
+
+ /**
+ * Returns the size in bytes of the file denoted by {@code path}. See
+ * {@link Path#getFileSize(Symlinks)} for specification.
+ *
+ * <p>Note: for <@link FileSystem>s where {@link #supportsSymbolicLinks()}
+ * returns false, this method will throw an
+ * {@link UnsupportedOperationException} if {@code followSymLinks=false}.
+ */
+ protected abstract long getFileSize(Path path, boolean followSymlinks) throws IOException;
+
+ /**
+ * Deletes the file denoted by {@code path}. See {@link Path#delete} for
+ * specification.
+ */
+ protected abstract boolean delete(Path path) throws IOException;
+
+ /**
+ * Returns the last modification time of the file denoted by {@code path}.
+ * See {@link Path#getLastModifiedTime(Symlinks)} for specification.
+ *
+ * Note: for {@link FileSystem}s where {@link #supportsSymbolicLinks()} returns
+ * false, this method will throw an {@link UnsupportedOperationException} if
+ * {@code followSymLinks=false}.
+ */
+ protected abstract long getLastModifiedTime(Path path,
+ boolean followSymlinks)
+ throws IOException;
+
+ /**
+ * Sets the last modification time of the file denoted by {@code path}. See
+ * {@link Path#setLastModifiedTime} for specification.
+ */
+ protected abstract void setLastModifiedTime(Path path, long newTime) throws IOException;
+
+ /**
+ * Returns value of the given extended attribute name or null if attribute
+ * does not exist or file system does not support extended attributes.
+ * <p>Default implementation assumes that file system does not support
+ * extended attributes and always returns null. Specific file system
+ * implementations should override this method if they do provide support
+ * for extended attributes.
+ *
+ * @param path the file whose extended attribute is to be returned.
+ * @param name the name of the extended attribute key.
+ * @return the value of the extended attribute associated with 'path', if
+ * any, or null if no such attribute is defined (ENODATA) or file
+ * system does not support extended attributes at all.
+ * @throws IOException if the call failed for any other reason.
+ */
+ protected byte[] getxattr(Path path, String name, boolean followSymlinks) throws IOException {
+ return null;
+ }
+
+ /**
+ * Returns the type of digest that may be returned by {@link #getFastDigest}, or {@code null}
+ * if the filesystem doesn't support them.
+ */
+ protected String getFastDigestFunctionType(Path path) {
+ return null;
+ }
+
+ /**
+ * Gets a fast digest for the given path, or {@code null} if there isn't one available or the
+ * filesystem doesn't support them. This digest should be suitable for detecting changes to the
+ * file.
+ */
+ protected byte[] getFastDigest(Path path) throws IOException {
+ return null;
+ }
+
+ /**
+ * Returns the MD5 digest of the file denoted by {@code path}. See
+ * {@link Path#getMD5Digest} for specification.
+ */
+ protected byte[] getMD5Digest(final Path path) throws IOException {
+ // Naive I/O implementation. Subclasses may (and do) optimize.
+ // This code is only used by the InMemory or Zip or other weird FSs.
+ return new ByteSource() {
+ @Override
+ public InputStream openStream() throws IOException {
+ return getInputStream(path);
+ }
+ }.hash(Hashing.md5()).asBytes();
+ }
+
+ /**
+ * Returns true if "path" denotes an existing symbolic link. See
+ * {@link Path#isSymbolicLink} for specification.
+ */
+ protected abstract boolean isSymbolicLink(Path path);
+
+ /**
+ * Appends a single regular path segment 'child' to 'dir', recursively
+ * resolving symbolic links in 'child'. 'dir' must be canonical. 'maxLinks' is
+ * the maximum number of symbolic links that may be traversed before it gives
+ * up (the Linux kernel uses 32).
+ *
+ * <p>(This method does not need to be synchronized; but the result may be
+ * stale in the case of concurrent modification.)
+ *
+ * @throws IOException if 'dir' is not an existing directory; or if
+ * stat(child) fails for any reason, or if 'child' is a symlink and
+ * readlink(child) fails for any reason (e.g. ENOENT, EACCES), or if
+ * the chain of symbolic links exceeds 'maxLinks'.
+ */
+ private Path appendSegment(Path dir, String child, int maxLinks) throws IOException {
+ Path naive = dir.getChild(child);
+
+ PathFragment linkTarget = resolveOneLink(naive);
+ if (linkTarget == null) {
+ return naive; // regular file or directory
+ }
+
+ if (maxLinks-- == 0) {
+ throw new IOException(naive + " (Too many levels of symbolic links)");
+ }
+ if (linkTarget.isAbsolute()) { dir = rootPath; }
+ for (String name : linkTarget.segments()) {
+ if (name.equals(".") || name.equals("")) {
+ // no-op
+ } else if (name.equals("..")) {
+ Path parent = dir.getParentDirectory();
+ // root's parent is root, when canonicalizing, so this is a no-op.
+ if (parent != null) { dir = parent; }
+ } else {
+ dir = appendSegment(dir, name, maxLinks);
+ }
+ }
+ return dir;
+ }
+
+ /**
+ * Helper method of {@link #resolveSymbolicLinks(Path)}. This method
+ * encapsulates the I/O component of a full canonicalization operation.
+ * Subclasses can (and do) provide more efficient implementations.
+ *
+ * <p>(This method does not need to be synchronized; but the result may be
+ * stale in the case of concurrent modification.)
+ *
+ * @param path a path, of which all but the last segment is guaranteed to be
+ * canonical
+ * @return {@link #readSymbolicLink} iff path is a symlink or null iff
+ * path exists but is not a symlink
+ * @throws IOException if the file did not exist, or a parent directory could
+ * not be searched
+ */
+ protected PathFragment resolveOneLink(Path path) throws IOException {
+ try {
+ return readSymbolicLink(path);
+ } catch (NotASymlinkException e) {
+ // Not a symbolic link. Check it exists.
+
+ // (A simple call to lstat would replace all of this.)
+ if (!exists(path, false)) {
+ throw new FileNotFoundException(path + " (No such file or directory)");
+ }
+
+ // TODO(bazel-team): (2009) ideally, throw ENOTDIR if dir is not a dir, but that
+ // would require twice as many stats, or a much more convoluted
+ // implementation (like glibc's canonicalize.c).
+
+ return null; // exists.
+ }
+ }
+
+ /**
+ * Returns the canonical path for the given path. See
+ * {@link Path#resolveSymbolicLinks} for specification.
+ */
+ protected final Path resolveSymbolicLinks(Path path)
+ throws IOException {
+ Path parentNode = path.getParentDirectory();
+ return parentNode == null
+ ? path // (root)
+ : appendSegment(resolveSymbolicLinks(parentNode), path.getBaseName(), 32);
+ }
+
+ /**
+ * Returns the status of a file. See {@link Path#stat(Symlinks)} for
+ * specification.
+ *
+ * <p>The default implementation of this method is a "lazy" one, based on
+ * other accessor methods such as {@link #isFile}, etc. Subclasses may provide
+ * more efficient specializations. However, we still try to follow Unix-like
+ * semantics of failing fast in case of non-existent files (or in case of
+ * permission issues).
+ */
+ protected FileStatus stat(final Path path, final boolean followSymlinks) throws IOException {
+ FileStatus status = new FileStatus() {
+ volatile Boolean isFile;
+ volatile Boolean isDirectory;
+ volatile Boolean isSymbolicLink;
+ volatile long size = -1;
+ volatile long mtime = -1;
+
+ @Override
+ public boolean isFile() {
+ if (isFile == null) { isFile = FileSystem.this.isFile(path, followSymlinks); }
+ return isFile;
+ }
+
+ @Override
+ public boolean isDirectory() {
+ if (isDirectory == null) {
+ isDirectory = FileSystem.this.isDirectory(path, followSymlinks);
+ }
+ return isDirectory;
+ }
+
+ @Override
+ public boolean isSymbolicLink() {
+ if (isSymbolicLink == null) { isSymbolicLink = FileSystem.this.isSymbolicLink(path); }
+ return isSymbolicLink;
+ }
+
+ @Override
+ public long getSize() throws IOException {
+ if (size == -1) { size = getFileSize(path, followSymlinks); }
+ return size;
+ }
+
+ @Override
+ public long getLastModifiedTime() throws IOException {
+ if (mtime == -1) { mtime = FileSystem.this.getLastModifiedTime(path, followSymlinks); }
+ return mtime;
+ }
+
+ @Override
+ public long getLastChangeTime() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public long getNodeId() {
+ throw new UnsupportedOperationException();
+ }
+ };
+
+ // Fail fast in case if some operations will actually fail, since stat() call sometimes used
+ // to verify file existence as well. We will use getLastModifiedTime() method for that purpose.
+ status.getLastModifiedTime();
+
+ return status;
+ }
+
+ /**
+ * Like stat(), but returns null on failures instead of throwing.
+ */
+ protected FileStatus statNullable(Path path, boolean followSymlinks) {
+ try {
+ return stat(path, followSymlinks);
+ } catch (IOException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Like {@link #stat}, but returns null if the file is not found (corresponding to
+ * {@code ENOENT} or {@code ENOTDIR} in Unix's stat(2) function) instead of throwing. Note that
+ * this implementation does <i>not</i> successfully catch {@code ENOTDIR} exceptions. If the
+ * instantiated filesystem can catch such errors, it should override this method to do so.
+ */
+ protected FileStatus statIfFound(Path path, boolean followSymlinks) throws IOException {
+ try {
+ return stat(path, followSymlinks);
+ } catch (FileNotFoundException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Returns true iff {@code path} denotes an existing directory. See
+ * {@link Path#isDirectory(Symlinks)} for specification.
+ */
+ protected abstract boolean isDirectory(Path path, boolean followSymlinks);
+
+ /**
+ * Returns true iff {@code path} denotes an existing regular or special file.
+ * See {@link Path#isFile(Symlinks)} for specification.
+ */
+ protected abstract boolean isFile(Path path, boolean followSymlinks);
+
+ /**
+ * Creates a symbolic link. See {@link Path#createSymbolicLink(Path)} for
+ * specification.
+ *
+ * <p>Note: for {@link FileSystem}s where {@link #supportsSymbolicLinks()}
+ * returns false, this method will throw an
+ * {@link UnsupportedOperationException}
+ */
+ protected abstract void createSymbolicLink(Path linkPath, PathFragment targetFragment)
+ throws IOException;
+
+ /**
+ * Returns the target of a symbolic link. See {@link Path#readSymbolicLink}
+ * for specification.
+ *
+ * <p>Note: for {@link FileSystem}s where {@link #supportsSymbolicLinks()}
+ * returns false, this method will throw an
+ * {@link UnsupportedOperationException} if the link points to a non-existent
+ * file.
+ *
+ * @throws NotASymlinkException if the current path is not a symbolic link
+ * @throws IOException if the contents of the link could not be read for any reason.
+ */
+ protected abstract PathFragment readSymbolicLink(Path path) throws IOException;
+
+ /**
+ * Returns true iff {@code path} denotes an existing file of any kind. See
+ * {@link Path#exists(Symlinks)} for specification.
+ */
+ protected abstract boolean exists(Path path, boolean followSymlinks);
+
+ /**
+ * Returns a collection containing the names of all entities within the
+ * directory denoted by the {@code path}.
+ *
+ * @throws IOException if there was an error reading the directory entries
+ */
+ protected abstract Collection<Path> getDirectoryEntries(Path path) throws IOException;
+
+ /**
+ * Returns a Dirents structure, listing the names of all entries within the
+ * directory {@code path}, plus their types (file, directory, other).
+ *
+ * @param followSymlinks whether to follow symlinks when determining the file types of
+ * individual directory entries. No matter the value of this parameter, symlinks are
+ * followed when resolving the directory whose entries are to be read.
+ * @throws IOException if there was an error reading the directory entries
+ */
+ protected Collection<Dirent> readdir(Path path, boolean followSymlinks) throws IOException {
+ Collection<Path> children = getDirectoryEntries(path);
+ List<Dirent> dirents = Lists.newArrayListWithCapacity(children.size());
+ for (Path child : children) {
+ FileStatus stat = statNullable(child, followSymlinks);
+ Dirent.Type type;
+ if (stat == null) {
+ type = Type.UNKNOWN;
+ } else if (stat.isFile()) {
+ type = Type.FILE;
+ } else if (stat.isDirectory()) {
+ type = Type.DIRECTORY;
+ } else if (stat.isSymbolicLink()) {
+ type = Type.SYMLINK;
+ } else {
+ type = Type.UNKNOWN;
+ }
+ dirents.add(new Dirent(child.getBaseName(), type));
+ }
+ return dirents;
+ }
+
+ /**
+ * Returns true iff the file represented by {@code path} is readable.
+ *
+ * @throws IOException if there was an error reading the file's metadata
+ */
+ protected abstract boolean isReadable(Path path) throws IOException;
+
+ /**
+ * Sets the file to readable (if the argument is true) or non-readable (if the
+ * argument is false)
+ *
+ * <p>Note: for {@link FileSystem}s where {@link #supportsModifications()}
+ * returns false or which do not support unreadable files, this method will
+ * throw an {@link UnsupportedOperationException}.
+ *
+ * @throws IOException if there was an error reading or writing the file's metadata
+ */
+ protected abstract void setReadable(Path path, boolean readable)
+ throws IOException;
+
+ /**
+ * Returns true iff the file represented by {@code path} is writable.
+ *
+ * @throws IOException if there was an error reading the file's metadata
+ */
+ protected abstract boolean isWritable(Path path) throws IOException;
+
+ /**
+ * Sets the file to writable (if the argument is true) or non-writable (if the
+ * argument is false)
+ *
+ * <p>Note: for {@link FileSystem}s where {@link #supportsModifications()}
+ * returns false, this method will throw an
+ * {@link UnsupportedOperationException}.
+ *
+ * @throws IOException if there was an error reading or writing the file's metadata
+ */
+ protected abstract void setWritable(Path path, boolean writable)
+ throws IOException;
+
+ /**
+ * Returns true iff the file represented by the path is executable.
+ *
+ * @throws IOException if there was an error reading the file's metadata
+ */
+ protected abstract boolean isExecutable(Path path) throws IOException;
+
+ /**
+ * Sets the file to executable, if the argument is true. It is currently not
+ * supported to unset the executable status of a file, so {code
+ * executable=false} yields an {@link UnsupportedOperationException}.
+ *
+ * <p>Note: for {@link FileSystem}s where {@link #supportsModifications()}
+ * returns false, this method will throw an
+ * {@link UnsupportedOperationException}.
+ *
+ * @throws IOException if there was an error reading or writing the file's metadata
+ */
+ protected abstract void setExecutable(Path path, boolean executable) throws IOException;
+
+ /**
+ * Sets the file permissions. If permission changes on this {@link FileSystem}
+ * are slow (e.g. one syscall per change), this method should aim to be faster
+ * than setting each permission individually. If this {@link FileSystem} does
+ * not support group or others permissions, those bits will be ignored.
+ *
+ * <p>Note: for {@link FileSystem}s where {@link #supportsModifications()}
+ * returns false, this method will throw an
+ * {@link UnsupportedOperationException}.
+ *
+ * @throws IOException if there was an error reading or writing the file's metadata
+ */
+ protected void chmod(Path path, int mode) throws IOException {
+ setReadable(path, (mode & 0400) != 0);
+ setWritable(path, (mode & 0200) != 0);
+ setExecutable(path, (mode & 0100) != 0);
+ }
+
+ /**
+ * Creates an InputStream accessing the file denoted by the path.
+ *
+ * @throws IOException if there was an error opening the file for reading
+ */
+ protected abstract InputStream getInputStream(Path path) throws IOException;
+
+ /**
+ * Creates an OutputStream accessing the file denoted by path.
+ *
+ * @throws IOException if there was an error opening the file for writing
+ */
+ protected final OutputStream getOutputStream(Path path) throws IOException {
+ return getOutputStream(path, false);
+ }
+
+ /**
+ * Creates an OutputStream accessing the file denoted by path.
+ *
+ * @param append whether to open the output stream in append mode
+ * @throws IOException if there was an error opening the file for writing
+ */
+ protected abstract OutputStream getOutputStream(Path path, boolean append) throws IOException;
+
+ /**
+ * Renames the file denoted by "sourceNode" to the location "targetNode".
+ * See {@link Path#renameTo} for specification.
+ */
+ protected abstract void renameTo(Path sourcePath, Path targetPath) throws IOException;
+}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/FileSystemUtils.java b/src/main/java/com/google/devtools/build/lib/vfs/FileSystemUtils.java
new file mode 100644
index 0000000..bc55032
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/vfs/FileSystemUtils.java
@@ -0,0 +1,988 @@
+// Copyright 2014 Google Inc. 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.vfs;
+
+import static java.nio.charset.StandardCharsets.ISO_8859_1;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.common.io.ByteSink;
+import com.google.common.io.ByteSource;
+import com.google.common.io.ByteStreams;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ConditionallyThreadSafe;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Helper functions that implement often-used complex operations on file
+ * systems.
+ */
+@ConditionallyThreadSafe // ThreadSafe except for deleteTree.
+public class FileSystemUtils {
+
+ static final Logger LOG = Logger.getLogger(FileSystemUtils.class.getName());
+ static final boolean LOG_FINER = LOG.isLoggable(Level.FINER);
+
+ private FileSystemUtils() {}
+
+ /****************************************************************************
+ * Path and PathFragment functions.
+ */
+
+ /**
+ * Throws exceptions if {@code baseName} is not a valid base name. A valid
+ * base name:
+ * <ul>
+ * <li>Is not null
+ * <li>Is not an empty string
+ * <li>Is not "." or ".."
+ * <li>Does not contain a slash
+ * </ul>
+ */
+ @ThreadSafe
+ public static void checkBaseName(String baseName) {
+ if (baseName.length() == 0) {
+ throw new IllegalArgumentException("Child must not be empty string ('')");
+ }
+ if (baseName.equals(".") || baseName.equals("..")) {
+ throw new IllegalArgumentException("baseName must not be '" + baseName + "'");
+ }
+ if (baseName.indexOf('/') != -1) {
+ throw new IllegalArgumentException("baseName must not contain a slash: '" + baseName + "'");
+ }
+ }
+
+ /**
+ * Returns the common ancestor between two paths, or null if none (including
+ * if they are on different filesystems).
+ */
+ public static Path commonAncestor(Path a, Path b) {
+ while (a != null && !b.startsWith(a)) {
+ a = a.getParentDirectory(); // returns null at root
+ }
+ return a;
+ }
+
+ /**
+ * Returns the longest common ancestor of the two path fragments, or either "/" or "" (depending
+ * on whether {@code a} is absolute or relative) if there is none.
+ */
+ public static PathFragment commonAncestor(PathFragment a, PathFragment b) {
+ while (a != null && !b.startsWith(a)) {
+ a = a.getParentDirectory();
+ }
+
+ return a;
+ }
+ /**
+ * Returns a path fragment from a given from-dir to a given to-path. May be
+ * either a short relative path "foo/bar", an up'n'over relative path
+ * "../../foo/bar" or an absolute path.
+ */
+ public static PathFragment relativePath(Path fromDir, Path to) {
+ if (to.getFileSystem() != fromDir.getFileSystem()) {
+ throw new IllegalArgumentException("fromDir and to must be on the same FileSystem");
+ }
+
+ return relativePath(fromDir.asFragment(), to.asFragment());
+ }
+
+ /**
+ * Returns a path fragment from a given from-dir to a given to-path.
+ */
+ public static PathFragment relativePath(PathFragment fromDir, PathFragment to) {
+ if (to.equals(fromDir)) {
+ return new PathFragment("."); // same dir, just return '.'
+ }
+ if (to.startsWith(fromDir)) {
+ return to.relativeTo(fromDir); // easy case--it's a descendant
+ }
+ PathFragment ancestor = commonAncestor(fromDir, to);
+ if (ancestor == null) {
+ return to; // no common ancestor, use 'to'
+ }
+ int levels = fromDir.relativeTo(ancestor).segmentCount();
+ StringBuilder dotdots = new StringBuilder();
+ for (int i = 0; i < levels; i++) {
+ dotdots.append("../");
+ }
+ return new PathFragment(dotdots.toString()).getRelative(to.relativeTo(ancestor));
+ }
+
+ /**
+ * Returns the longest prefix from a given set of 'prefixes' that are
+ * contained in 'path'. I.e the closest ancestor directory containing path.
+ * Returns null if none found.
+ */
+ public static PathFragment longestPathPrefix(PathFragment path, Set<PathFragment> prefixes) {
+ for (int i = path.segmentCount(); i >= 1; i--) {
+ PathFragment prefix = path.subFragment(0, i);
+ if (prefixes.contains(prefix)) {
+ return prefix;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Removes the shortest suffix beginning with '.' from the basename of the
+ * filename string. If the basename contains no '.', the filename is returned
+ * unchanged.
+ *
+ * e.g. "foo/bar.x" -> "foo/bar"
+ */
+ @ThreadSafe
+ public static String removeExtension(String filename) {
+ int lastDotIndex = filename.lastIndexOf('.');
+ if (lastDotIndex == -1) { return filename; }
+ int lastSlashIndex = filename.lastIndexOf('/');
+ if (lastSlashIndex > lastDotIndex) {
+ return filename;
+ }
+ return filename.substring(0, lastDotIndex);
+ }
+
+ /**
+ * Removes the shortest suffix beginning with '.' from the basename of the
+ * PathFragment. If the basename contains no '.', the filename is returned
+ * unchanged.
+ *
+ * <p>e.g. "foo/bar.x" -> "foo/bar"
+ */
+ @ThreadSafe
+ public static PathFragment removeExtension(PathFragment path) {
+ return path.replaceName(removeExtension(path.getBaseName()));
+ }
+
+ /**
+ * Removes the shortest suffix beginning with '.' from the basename of the
+ * Path. If the basename contains no '.', the filename is returned
+ * unchanged.
+ *
+ * <p>e.g. "foo/bar.x" -> "foo/bar"
+ */
+ @ThreadSafe
+ public static Path removeExtension(Path path) {
+ return path.getFileSystem().getPath(removeExtension(path.asFragment()));
+ }
+
+ /**
+ * Returns a new {@code PathFragment} formed by replacing the extension of the
+ * last path segment of {@code path} with {@code newExtension}. Null is
+ * returned iff {@code path} has zero segments.
+ */
+ public static PathFragment replaceExtension(PathFragment path, String newExtension) {
+ return path.replaceName(removeExtension(path.getBaseName()) + newExtension);
+ }
+
+ /**
+ * Returns a new {@code PathFragment} formed by replacing the extension of the
+ * last path segment of {@code path} with {@code newExtension}. Null is
+ * returned iff {@code path} has zero segments or it doesn't end with {@code oldExtension}.
+ */
+ public static PathFragment replaceExtension(PathFragment path, String newExtension,
+ String oldExtension) {
+ String base = path.getBaseName();
+ if (!base.endsWith(oldExtension)) {
+ return null;
+ }
+ String newBase = base.substring(0, base.length() - oldExtension.length()) + newExtension;
+ return path.replaceName(newBase);
+ }
+
+ /**
+ * Returns a new {@code Path} formed by replacing the extension of the
+ * last path segment of {@code path} with {@code newExtension}. Null is
+ * returned iff {@code path} has zero segments.
+ */
+ public static Path replaceExtension(Path path, String newExtension) {
+ PathFragment fragment = replaceExtension(path.asFragment(), newExtension);
+ return fragment == null ? null : path.getFileSystem().getPath(fragment);
+ }
+
+ /**
+ * Returns a new {@code PathFragment} formed by adding the extension to the last path segment of
+ * {@code path}. Null is returned if {@code path} has zero segments.
+ */
+ public static PathFragment appendExtension(PathFragment path, String newExtension) {
+ return path.replaceName(path.getBaseName() + newExtension);
+ }
+
+ /**
+ * Returns a new {@code PathFragment} formed by replacing the first, or all if
+ * {@code replaceAll} is true, {@code oldSegment} of {@code path} with {@code
+ * newSegment}.
+ */
+ public static PathFragment replaceSegments(PathFragment path,
+ String oldSegment, String newSegment, boolean replaceAll) {
+ int count = path.segmentCount();
+ for (int i = 0; i < count; i++) {
+ if (path.getSegment(i).equals(oldSegment)) {
+ path = new PathFragment(path.subFragment(0, i),
+ new PathFragment(newSegment),
+ path.subFragment(i+1, count));
+ if (!replaceAll) {
+ return path;
+ }
+ }
+ }
+ return path;
+ }
+
+ /**
+ * Returns a new {@code PathFragment} formed by appending the given string to the last path
+ * segment of {@code path} without removing the extension. Returns null if {@code path}
+ * has zero segments.
+ */
+ public static PathFragment appendWithoutExtension(PathFragment path, String toAppend) {
+ return path.replaceName(appendWithoutExtension(path.getBaseName(), toAppend));
+ }
+
+ /**
+ * Given a string that represents a file with an extension separated by a '.' and a string
+ * to append, return a string in which {@code toAppend} has been appended to {@code name}
+ * before the last '.' character. If {@code name} does not include a '.', appends {@code
+ * toAppend} at the end.
+ *
+ * <p>For example,
+ * ("libfoo.jar", "-src") ==> "libfoo-src.jar"
+ * ("libfoo", "-src") ==> "libfoo-src"
+ */
+ private static String appendWithoutExtension(String name, String toAppend) {
+ int dotIndex = name.lastIndexOf(".");
+ if (dotIndex > 0) {
+ String baseName = name.substring(0, dotIndex);
+ String extension = name.substring(dotIndex);
+ return baseName + toAppend + extension;
+ } else {
+ return name + toAppend;
+ }
+ }
+
+ /****************************************************************************
+ * FileSystem property functions.
+ */
+
+ /**
+ * Return the current working directory as expressed by the System property
+ * 'user.dir'.
+ */
+ public static Path getWorkingDirectory(FileSystem fs) {
+ return fs.getPath(getWorkingDirectory());
+ }
+
+ /**
+ * Returns the current working directory as expressed by the System property
+ * 'user.dir'. This version does not require a {@link FileSystem}.
+ */
+ public static PathFragment getWorkingDirectory() {
+ return new PathFragment(System.getProperty("user.dir", "/"));
+ }
+
+ /****************************************************************************
+ * Path FileSystem mutating operations.
+ */
+
+ /**
+ * "Touches" the file or directory specified by the path, following symbolic
+ * links. If it does not exist, it is created as an empty file; otherwise, the
+ * time of last access is updated to the current time.
+ *
+ * @throws IOException if there was an error while touching the file
+ */
+ @ThreadSafe
+ public static void touchFile(Path path) throws IOException {
+ if (path.exists()) {
+ // -1L means "use the current time", and is ultimately implemented by
+ // utime(path, null), thereby using the kernel's clock, not the JVM's.
+ // (A previous implementation based on the JVM clock was found to be
+ // skewy.)
+ path.setLastModifiedTime(-1L);
+ } else {
+ createEmptyFile(path);
+ }
+ }
+
+ /**
+ * Creates an empty regular file with the name of the current path, following
+ * symbolic links.
+ *
+ * @throws IOException if the file could not be created for any reason
+ * (including that there was already a file at that location)
+ */
+ public static void createEmptyFile(Path path) throws IOException {
+ path.getOutputStream().close();
+ }
+
+ /**
+ * Creates or updates a symbolic link from 'link' to 'target'. Replaces
+ * existing symbolic links with target, and skips the link creation if it is
+ * already present. Will also create any missing ancestor directories of the
+ * link. This method is non-atomic
+ *
+ * <p>Note: this method will throw an IOException if there is an unequal
+ * non-symlink at link.
+ *
+ * @throws IOException if the creation of the symbolic link was unsuccessful
+ * for any reason.
+ */
+ @ThreadSafe // but not atomic
+ public static void ensureSymbolicLink(Path link, Path target) throws IOException {
+ ensureSymbolicLink(link, target.asFragment());
+ }
+
+ /**
+ * Creates or updates a symbolic link from 'link' to 'target'. Replaces
+ * existing symbolic links with target, and skips the link creation if it is
+ * already present. Will also create any missing ancestor directories of the
+ * link. This method is non-atomic
+ *
+ * <p>Note: this method will throw an IOException if there is an unequal
+ * non-symlink at link.
+ *
+ * @throws IOException if the creation of the symbolic link was unsuccessful
+ * for any reason.
+ */
+ @ThreadSafe // but not atomic
+ public static void ensureSymbolicLink(Path link, String target) throws IOException {
+ ensureSymbolicLink(link, new PathFragment(target));
+ }
+
+ /**
+ * Creates or updates a symbolic link from 'link' to 'target'. Replaces
+ * existing symbolic links with target, and skips the link creation if it is
+ * already present. Will also create any missing ancestor directories of the
+ * link. This method is non-atomic
+ *
+ * <p>Note: this method will throw an IOException if there is an unequal
+ * non-symlink at link.
+ *
+ * @throws IOException if the creation of the symbolic link was unsuccessful
+ * for any reason.
+ */
+ @ThreadSafe // but not atomic
+ public static void ensureSymbolicLink(Path link, PathFragment target) throws IOException {
+ // TODO(bazel-team): (2009) consider adding the logic for recovering from the case when
+ // we have already created a parent directory symlink earlier.
+ try {
+ if (link.readSymbolicLink().equals(target)) {
+ return; // Do nothing if the link is already there.
+ }
+ } catch (IOException e) { // link missing or broken
+ /* fallthru and do the work below */
+ }
+ if (link.isSymbolicLink()) {
+ link.delete(); // Remove the symlink since it is pointing somewhere else.
+ } else {
+ createDirectoryAndParents(link.getParentDirectory());
+ }
+ try {
+ link.createSymbolicLink(target);
+ } catch (IOException e) {
+ // Only pass on exceptions caused by a true link creation failure.
+ if (!link.isSymbolicLink() ||
+ !link.resolveSymbolicLinks().equals(link.getRelative(target))) {
+ throw e;
+ }
+ }
+ }
+
+ private static ByteSource asByteSource(final Path path) {
+ return new ByteSource() {
+ @Override public InputStream openStream() throws IOException {
+ return path.getInputStream();
+ }
+ };
+ }
+
+ private static ByteSink asByteSink(final Path path, final boolean append) {
+ return new ByteSink() {
+ @Override public OutputStream openStream() throws IOException {
+ return path.getOutputStream(append);
+ }
+ };
+ }
+
+ private static ByteSink asByteSink(final Path path) {
+ return asByteSink(path, false);
+ }
+
+ /**
+ * Copies the file from location "from" to location "to", while overwriting a
+ * potentially existing "to". File's last modified time, executable and
+ * writable bits are also preserved.
+ *
+ * <p>If no error occurs, the method returns normally. If a parent directory does
+ * not exist, a FileNotFoundException is thrown. An IOException is thrown when
+ * other erroneous situations occur. (e.g. read errors)
+ */
+ @ThreadSafe // but not atomic
+ public static void copyFile(Path from, Path to) throws IOException {
+ try {
+ to.delete();
+ } catch (IOException e) {
+ throw new IOException("error copying file: "
+ + "couldn't delete destination: " + e.getMessage());
+ }
+ asByteSource(from).copyTo(asByteSink(to));
+ to.setLastModifiedTime(from.getLastModifiedTime()); // Preserve mtime.
+ if (!from.isWritable()) {
+ to.setWritable(false); // Make file read-only if original was read-only.
+ }
+ to.setExecutable(from.isExecutable()); // Copy executable bit.
+ }
+
+ /**
+ * Copies a tool binary from one path to another, returning the target path.
+ * The directory of the target path must already exist. The target copy's time
+ * is set to match, as well as its read-only and executable flags. The
+ * operation is skipped if the target file has the same time and size as the
+ * source.
+ */
+ public static Path copyTool(Path source, Path target) throws IOException {
+ FileStatus sourceStat = null;
+ FileStatus targetStat = target.statNullable();
+ if (targetStat != null) {
+ // stat the source file only if we'll need the stat.
+ sourceStat = source.stat(Symlinks.FOLLOW);
+ }
+ if (targetStat == null ||
+ targetStat.getLastModifiedTime() != sourceStat.getLastModifiedTime() ||
+ targetStat.getSize() != sourceStat.getSize()) {
+ copyFile(source, target);
+ target.setWritable(source.isWritable());
+ target.setExecutable(source.isExecutable());
+ target.setLastModifiedTime(source.getLastModifiedTime());
+ }
+ return target;
+ }
+
+ /****************************************************************************
+ * Directory tree operations.
+ */
+
+ /**
+ * Returns a new collection containing all of the paths below a given root
+ * path, for which the given predicate is true. Symbolic links are not
+ * followed, and may appear in the result.
+ *
+ * @throws IOException If the root does not denote a directory
+ */
+ @ThreadSafe
+ public static Collection<Path> traverseTree(Path root, Predicate<? super Path> predicate)
+ throws IOException {
+ List<Path> paths = new ArrayList<>();
+ traverseTree(paths, root, predicate);
+ return paths;
+ }
+
+ /**
+ * Populates an existing Path List, adding all of the paths below a given root
+ * path for which the given predicate is true. Symbolic links are not
+ * followed, and may appear in the result.
+ *
+ * @throws IOException If the root does not denote a directory
+ */
+ @ThreadSafe
+ public static void traverseTree(Collection<Path> paths, Path root,
+ Predicate<? super Path> predicate) throws IOException {
+ for (Path p : root.getDirectoryEntries()) {
+ if (predicate.apply(p)) {
+ paths.add(p);
+ }
+ if (p.isDirectory(Symlinks.NOFOLLOW)) {
+ traverseTree(paths, p, predicate);
+ }
+ }
+ }
+
+ /**
+ * Deletes 'p', and everything recursively beneath it if it's a directory.
+ * Does not follow any symbolic links.
+ *
+ * @throws IOException if any file could not be removed.
+ */
+ @ThreadSafe
+ public static void deleteTree(Path p) throws IOException {
+ deleteTreesBelow(p);
+ p.delete();
+ }
+
+ /**
+ * Deletes all dir trees recursively beneath 'dir' if it's a directory,
+ * nothing otherwise. Does not follow any symbolic links.
+ *
+ * @throws IOException if any file could not be removed.
+ */
+ @ThreadSafe
+ public static void deleteTreesBelow(Path dir) throws IOException {
+ if (dir.isDirectory(Symlinks.NOFOLLOW)) { // real directories (not symlinks)
+ dir.setReadable(true);
+ dir.setWritable(true);
+ dir.setExecutable(true);
+ for (Path child : dir.getDirectoryEntries()) {
+ deleteTree(child);
+ }
+ }
+ }
+
+ /**
+ * Delete all dir trees under a given 'dir' that don't start with one of a set
+ * of given 'prefixes'. Does not follow any symbolic links.
+ */
+ @ThreadSafe
+ public static void deleteTreesBelowNotPrefixed(Path dir, String[] prefixes) throws IOException {
+ dirloop:
+ for (Path p : dir.getDirectoryEntries()) {
+ String name = p.getBaseName();
+ for (int i = 0; i < prefixes.length; i++) {
+ if (name.startsWith(prefixes[i])) {
+ continue dirloop;
+ }
+ }
+ deleteTree(p);
+ }
+ }
+
+ /**
+ * Copies all dir trees under a given 'from' dir to location 'to', while overwriting
+ * all files in the potentially existing 'to'. Does not follow any symbolic links,
+ * but copies them instead.
+ *
+ * <p>The source and the destination must be non-overlapping, otherwise an
+ * IllegalArgumentException will be thrown. This method cannot be used to copy
+ * a dir tree to a sub tree of itself.
+ *
+ * <p>If no error occurs, the method returns normally. If the given 'from' does
+ * not exist, a FileNotFoundException is thrown. An IOException is thrown when
+ * other erroneous situations occur. (e.g. read errors)
+ */
+ @ThreadSafe
+ public static void copyTreesBelow(Path from , Path to) throws IOException {
+ if (to.startsWith(from)) {
+ throw new IllegalArgumentException(to + " is a subdirectory of " + from);
+ }
+
+ Collection<Path> entries = from.getDirectoryEntries();
+ for (Path entry : entries) {
+ if (entry.isDirectory(Symlinks.NOFOLLOW)) {
+ Path subDir = to.getChild(entry.getBaseName());
+ subDir.createDirectory();
+ copyTreesBelow(entry, subDir);
+ } else if (entry.isSymbolicLink()) {
+ Path newLink = to.getChild(entry.getBaseName());
+ newLink.createSymbolicLink(entry.readSymbolicLink());
+ } else {
+ Path newEntry = to.getChild(entry.getBaseName());
+ copyFile(entry, newEntry);
+ }
+ }
+ }
+
+ /**
+ * Attempts to create a directory with the name of the given path, creating
+ * ancestors as necessary.
+ *
+ * <p>Postcondition: completes normally iff {@code dir} denotes an existing
+ * directory (not necessarily canonical); completes abruptly otherwise.
+ *
+ * @return true if the directory was successfully created anew, false if it
+ * already existed (including the case where {@code dir} denotes a symlink
+ * to an existing directory)
+ * @throws IOException if the directory could not be created
+ */
+ @ThreadSafe
+ public static boolean createDirectoryAndParents(Path dir) throws IOException {
+ // Optimised for minimal number of I/O calls.
+
+ // Don't attempt to create the root directory.
+ if (dir.getParentDirectory() == null) { return false; }
+
+ FileSystem filesystem = dir.getFileSystem();
+ if (filesystem instanceof UnionFileSystem) {
+ // If using UnionFS, make sure that we do not traverse filesystem boundaries when creating
+ // parent directories by rehoming the path on the most specific filesystem.
+ FileSystem delegate = ((UnionFileSystem) filesystem).getDelegate(dir);
+ dir = delegate.getPath(dir.asFragment());
+ }
+
+ try {
+ return dir.createDirectory();
+ } catch (IOException e) {
+ if (e.getMessage().endsWith(" (No such file or directory)")) { // ENOENT
+ createDirectoryAndParents(dir.getParentDirectory());
+ return dir.createDirectory();
+ } else if (e.getMessage().endsWith(" (File exists)") && dir.isDirectory()) { // EEXIST
+ return false;
+ } else {
+ throw e; // some other error (e.g. ENOTDIR, EACCES, etc.)
+ }
+ }
+ }
+
+ /**
+ * Attempts to remove a relative chain of directories under a given base.
+ * Returns {@code true} if the removal was successful, and returns {@code
+ * false} if the removal fails because a directory was not empty. An
+ * {@link IOException} is thrown for any other errors.
+ */
+ @ThreadSafe
+ public static boolean removeDirectoryAndParents(Path base, PathFragment toRemove) {
+ if (toRemove.isAbsolute()) {
+ return false;
+ }
+ try {
+ for (; toRemove.segmentCount() > 0; toRemove = toRemove.getParentDirectory()) {
+ Path p = base.getRelative(toRemove);
+ if (p.exists()) {
+ p.delete();
+ }
+ }
+ } catch (IOException e) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Takes a map of directory fragments to root paths, and creates a symlink
+ * forest under an existing linkRoot to the corresponding source dirs or
+ * files. Symlink are made at the highest dir possible, linking files directly
+ * only when needed with nested packages.
+ */
+ public static void plantLinkForest(ImmutableMap<PathFragment, Path> packageRootMap, Path linkRoot)
+ throws IOException {
+ // Create a sorted map of all dirs (packages and their ancestors) to sets of their roots.
+ // Packages come from exactly one root, but their shared ancestors may come from more.
+ // The map is maintained sorted lexicographically, so parents are before their children.
+ Map<PathFragment, Set<Path>> dirRootsMap = Maps.newTreeMap();
+ for (Map.Entry<PathFragment, Path> entry : packageRootMap.entrySet()) {
+ PathFragment pkgDir = entry.getKey();
+ Path pkgRoot = entry.getValue();
+ for (int i = 1; i <= pkgDir.segmentCount(); i++) {
+ PathFragment dir = pkgDir.subFragment(0, i);
+ Set<Path> roots = dirRootsMap.get(dir);
+ if (roots == null) {
+ roots = Sets.newHashSet();
+ dirRootsMap.put(dir, roots);
+ }
+ roots.add(pkgRoot);
+ }
+ }
+ // Now add in roots for all non-pkg dirs that are in between two packages, and missed above.
+ for (Map.Entry<PathFragment, Set<Path>> entry : dirRootsMap.entrySet()) {
+ PathFragment dir = entry.getKey();
+ if (!packageRootMap.containsKey(dir)) {
+ PathFragment pkgDir = longestPathPrefix(dir, packageRootMap.keySet());
+ if (pkgDir != null) {
+ entry.getValue().add(packageRootMap.get(pkgDir));
+ }
+ }
+ }
+ // Create output dirs for all dirs that have more than one root and need to be split.
+ for (Map.Entry<PathFragment, Set<Path>> entry : dirRootsMap.entrySet()) {
+ PathFragment dir = entry.getKey();
+ if (entry.getValue().size() > 1) {
+ if (LOG_FINER) {
+ LOG.finer("mkdir " + linkRoot.getRelative(dir));
+ }
+ createDirectoryAndParents(linkRoot.getRelative(dir));
+ }
+ }
+ // Make dir links for single rooted dirs.
+ for (Map.Entry<PathFragment, Set<Path>> entry : dirRootsMap.entrySet()) {
+ PathFragment dir = entry.getKey();
+ Set<Path> roots = entry.getValue();
+ // Simple case of one root for this dir.
+ if (roots.size() == 1) {
+ if (dir.segmentCount() > 1 && dirRootsMap.get(dir.getParentDirectory()).size() == 1) {
+ continue; // skip--an ancestor will link this one in from above
+ }
+ // This is the top-most dir that can be linked to a single root. Make it so.
+ Path root = roots.iterator().next(); // lone root in set
+ if (LOG_FINER) {
+ LOG.finer("ln -s " + root.getRelative(dir) + " " + linkRoot.getRelative(dir));
+ }
+ linkRoot.getRelative(dir).createSymbolicLink(root.getRelative(dir));
+ }
+ }
+ // Make links for dirs within packages, skip parent-only dirs.
+ for (Map.Entry<PathFragment, Set<Path>> entry : dirRootsMap.entrySet()) {
+ PathFragment dir = entry.getKey();
+ if (entry.getValue().size() > 1) {
+ // If this dir is at or below a package dir, link in its contents.
+ PathFragment pkgDir = longestPathPrefix(dir, packageRootMap.keySet());
+ if (pkgDir != null) {
+ Path root = packageRootMap.get(pkgDir);
+ try {
+ Path absdir = root.getRelative(dir);
+ if (absdir.isDirectory()) {
+ if (LOG_FINER) {
+ LOG.finer("ln -s " + absdir + "/* " + linkRoot.getRelative(dir) + "/");
+ }
+ for (Path target : absdir.getDirectoryEntries()) {
+ PathFragment p = target.relativeTo(root);
+ if (!dirRootsMap.containsKey(p)) {
+ //LOG.finest("ln -s " + target + " " + linkRoot.getRelative(p));
+ linkRoot.getRelative(p).createSymbolicLink(target);
+ }
+ }
+ } else {
+ LOG.fine("Symlink planting skipping dir '" + absdir + "'");
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ // Otherwise its just an otherwise empty common parent dir.
+ }
+ }
+ }
+ }
+
+ /****************************************************************************
+ * Whole-file I/O utilities for characters and bytes. These convenience
+ * methods are not efficient and should not be used for large amounts of data!
+ */
+
+ private static char[] convertFromLatin1(byte[] content) {
+ char[] latin1 = new char[content.length];
+ for (int i = 0; i < latin1.length; i++) { // yeah, latin1 is this easy! :-)
+ latin1[i] = (char) (0xff & content[i]);
+ }
+ return latin1;
+ }
+
+ /**
+ * Writes lines to file using ISO-8859-1 encoding (isolatin1).
+ */
+ @ThreadSafe // but not atomic
+ public static void writeIsoLatin1(Path file, String... lines) throws IOException {
+ writeLinesAs(file, ISO_8859_1, lines);
+ }
+
+ /**
+ * Append lines to file using ISO-8859-1 encoding (isolatin1).
+ */
+ @ThreadSafe // but not atomic
+ public static void appendIsoLatin1(Path file, String... lines) throws IOException {
+ appendLinesAs(file, ISO_8859_1, lines);
+ }
+
+ /**
+ * Writes the specified String as ISO-8859-1 (latin1) encoded bytes to the
+ * file. Follows symbolic links.
+ *
+ * @throws IOException if there was an error
+ */
+ public static void writeContentAsLatin1(Path outputFile, String content) throws IOException {
+ writeContent(outputFile, ISO_8859_1, content);
+ }
+
+ /**
+ * Writes the specified String using the specified encoding to the file.
+ * Follows symbolic links.
+ *
+ * @throws IOException if there was an error
+ */
+ public static void writeContent(Path outputFile, Charset charset, String content)
+ throws IOException {
+ asByteSink(outputFile).asCharSink(charset).write(content);
+ }
+
+ /**
+ * Writes lines to file using the given encoding, ending every line with a
+ * line break '\n' character.
+ */
+ @ThreadSafe // but not atomic
+ public static void writeLinesAs(Path file, Charset charset, String... lines)
+ throws IOException {
+ writeLinesAs(file, charset, Arrays.asList(lines));
+ }
+
+ /**
+ * Appends lines to file using the given encoding, ending every line with a
+ * line break '\n' character.
+ */
+ @ThreadSafe // but not atomic
+ public static void appendLinesAs(Path file, Charset charset, String... lines)
+ throws IOException {
+ appendLinesAs(file, charset, Arrays.asList(lines));
+ }
+
+ /**
+ * Writes lines to file using the given encoding, ending every line with a
+ * line break '\n' character.
+ */
+ @ThreadSafe // but not atomic
+ public static void writeLinesAs(Path file, Charset charset, Iterable<String> lines)
+ throws IOException {
+ createDirectoryAndParents(file.getParentDirectory());
+ asByteSink(file).asCharSink(charset).writeLines(lines);
+ }
+
+ /**
+ * Appends lines to file using the given encoding, ending every line with a
+ * line break '\n' character.
+ */
+ @ThreadSafe // but not atomic
+ public static void appendLinesAs(Path file, Charset charset, Iterable<String> lines)
+ throws IOException {
+ createDirectoryAndParents(file.getParentDirectory());
+ asByteSink(file, true).asCharSink(charset).writeLines(lines);
+ }
+
+ /**
+ * Writes the specified byte array to the output file. Follows symbolic links.
+ *
+ * @throws IOException if there was an error
+ */
+ public static void writeContent(Path outputFile, byte[] content) throws IOException {
+ asByteSink(outputFile).write(content);
+ }
+
+ /**
+ * Returns the entirety of the specified input stream and returns it as a char
+ * array, decoding characters using ISO-8859-1 (Latin1).
+ *
+ * @throws IOException if there was an error
+ */
+ public static char[] readContentAsLatin1(InputStream in) throws IOException {
+ return convertFromLatin1(ByteStreams.toByteArray(in));
+ }
+
+ /**
+ * Returns the entirety of the specified file and returns it as a char array,
+ * decoding characters using ISO-8859-1 (Latin1).
+ *
+ * @throws IOException if there was an error
+ */
+ public static char[] readContentAsLatin1(Path inputFile) throws IOException {
+ return convertFromLatin1(readContent(inputFile));
+ }
+
+ /**
+ * Returns an iterable that allows iterating over ISO-8859-1 (Latin1) text
+ * file contents line by line. If the file ends in a line break, the iterator
+ * will return an empty string as the last element.
+ *
+ * @throws IOException if there was an error
+ */
+ public static Iterable<String> iterateLinesAsLatin1(Path inputFile) throws IOException {
+ return asByteSource(inputFile).asCharSource(ISO_8859_1).readLines();
+ }
+
+ /**
+ * Returns the entirety of the specified file and returns it as a byte array.
+ *
+ * @throws IOException if there was an error
+ */
+ public static byte[] readContent(Path inputFile) throws IOException {
+ return asByteSource(inputFile).read();
+ }
+
+ /**
+ * Reads at most {@code limit} bytes from {@code inputFile} and returns it as a byte array.
+ *
+ * @throws IOException if there was an error.
+ */
+ public static byte[] readContentWithLimit(Path inputFile, int limit) throws IOException {
+ Preconditions.checkArgument(limit >= 0, "limit needs to be >=0, but it is %s", limit);
+ ByteSource byteSource = asByteSource(inputFile);
+ byte[] buffer = new byte[limit];
+ try (InputStream inputStream = byteSource.openBufferedStream()) {
+ int read = ByteStreams.read(inputStream, buffer, 0, limit);
+ return Arrays.copyOf(buffer, read);
+ }
+ }
+
+ /**
+ * Dumps diagnostic information about the specified filesystem to {@code out}.
+ * This is the implementation of the filesystem part of the 'blaze dump'
+ * command. It lives here, rather than in DumpCommand, because it requires
+ * privileged access to members of this package.
+ *
+ * <p>Its results are unspecified and MUST NOT be interpreted programmatically.
+ */
+ public static void dump(FileSystem fs, final PrintStream out) {
+ if (!(fs instanceof UnixFileSystem)) {
+ out.println(" Not a UnixFileSystem.");
+ return;
+ }
+
+ // Unfortunately there's no "letrec" for anonymous functions so we have to
+ // (a) name the function, (b) put it in a box and (c) use List not array
+ // because of the generic type. *sigh*.
+ final List<Predicate<Path>> dumpFunction = new ArrayList<>();
+ dumpFunction.add(new Predicate<Path>() {
+ @Override
+ public boolean apply(Path child) {
+ Path path = child;
+ out.println(" " + path + " (" + path.toDebugString() + ")");
+ path.applyToChildren(dumpFunction.get(0));
+ return false;
+ }
+ });
+
+ fs.getRootDirectory().applyToChildren(dumpFunction.get(0));
+ }
+
+ /**
+ * Returns the type of the file system path belongs to.
+ */
+ public static String getFileSystem(Path path) {
+ return path.getFileSystem().getFileSystemType(path);
+ }
+
+ /**
+ * Returns whether the given path starts with any of the paths in the given
+ * list of prefixes.
+ */
+ public static boolean startsWithAny(Path path, Iterable<Path> prefixes) {
+ for (Path prefix : prefixes) {
+ if (path.startsWith(prefix)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns whether the given path starts with any of the paths in the given
+ * list of prefixes.
+ */
+ public static boolean startsWithAny(PathFragment path, Iterable<PathFragment> prefixes) {
+ for (PathFragment prefix : prefixes) {
+ if (path.startsWith(prefix)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/FileSystems.java b/src/main/java/com/google/devtools/build/lib/vfs/FileSystems.java
new file mode 100644
index 0000000..1e7aaae
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/vfs/FileSystems.java
@@ -0,0 +1,59 @@
+// Copyright 2014 Google Inc. 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.vfs;
+
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+
+/**
+ * This static file system singleton manages access to a single default
+ * {@link FileSystem} instance created within the methods of this class.
+ */
+@ThreadSafe
+@Deprecated // Instantiate and inject FileSystem instances directly, or use
+ // com.google.devtools.build.lib.vfs.util.FileSystems in tests.
+public final class FileSystems {
+
+ private FileSystems() {}
+
+ private static FileSystem defaultFileSystem;
+
+ /**
+ * Initializes the default {@link FileSystem} instance as a platform native
+ * (Unix) file system, creating one iff needed, and returns the instance.
+ *
+ * <p>This method is idempotent as long as the initialization is of the same
+ * type (Native/JavaIo/Union).
+ */
+ public static synchronized FileSystem initDefaultAsNative() {
+ if (!(defaultFileSystem instanceof UnixFileSystem)) {
+ defaultFileSystem = new UnixFileSystem();
+ }
+ return defaultFileSystem;
+ }
+
+ /**
+ * Initializes the default {@link FileSystem} instance as a java.io.File
+ * file system, creating one iff needed, and returns the instance.
+ *
+ * <p>This method is idempotent as long as the initialization is of the same
+ * type (Native/JavaIo/Union).
+ */
+ public static synchronized FileSystem initDefaultAsJavaIo() {
+ if (!(defaultFileSystem instanceof JavaIoFileSystem)) {
+ defaultFileSystem = new JavaIoFileSystem();
+ }
+ return defaultFileSystem;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/IORuntimeException.java b/src/main/java/com/google/devtools/build/lib/vfs/IORuntimeException.java
new file mode 100644
index 0000000..2769fe9
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/vfs/IORuntimeException.java
@@ -0,0 +1,78 @@
+// Copyright 2014 Google Inc. 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.
+// All Rights Reserved.
+
+package com.google.devtools.build.lib.vfs;
+
+import java.io.IOException;
+
+/**
+ * Signals that an I/O exception of some sort has occurred. Contrary to
+ * <code>java.io.IOException</code>, this class is a subclass of
+ * <code>RuntimeException</code>, which allows you to signal an I/O problem
+ * without polluting the callers. For details on why checked exceptions is bad,
+ * try searching for "java checked exception mistake" on Google.
+ */
+public class IORuntimeException extends RuntimeException {
+ /**
+ * Constructs a new IORuntimeException with null as its detail message.
+ */
+ public IORuntimeException() {
+ super();
+ }
+
+ /**
+ * Constructs a new IORuntimeException with the specified detail message.
+ */
+ public IORuntimeException(String message) {
+ super(message);
+ }
+
+ /**
+ * Constructs a new IORuntimeException with the specified detail message and
+ * cause.
+ *
+ * @param message the detail message, which is saved for later retrieval by
+ * the <code>Throwable.getMessage()</code> method.
+ * @param cause the cause (which is saved for later retrieval by the
+ * <code>Throwable.getCause()</code> method). (A null value is
+ * permitted, and indicates that the cause is nonexistent or unknown.)
+ */
+ public IORuntimeException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ /**
+ * Constructs a new IORuntimeException as a wrapper on a root cause
+ */
+ public IORuntimeException(Throwable cause) {
+ super(cause);
+ }
+
+ /**
+ * @return the actual IOException that caused this exception, or null if it
+ * was not caused by an IOException. Call <code>getCause()</code>
+ * instead if it was caused by other types of exceptions.
+ */
+ public IOException getCauseIOException() {
+ Throwable cause = getCause();
+ if (cause instanceof IOException) {
+ return (IOException) cause;
+ } else {
+ return null;
+ }
+ }
+
+ private static final long serialVersionUID = 1L;
+}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/JavaIoFileSystem.java b/src/main/java/com/google/devtools/build/lib/vfs/JavaIoFileSystem.java
new file mode 100644
index 0000000..08e67f7
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/vfs/JavaIoFileSystem.java
@@ -0,0 +1,486 @@
+// Copyright 2014 Google Inc. 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.vfs;
+
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.profiler.Profiler;
+import com.google.devtools.build.lib.profiler.ProfilerTask;
+import com.google.devtools.build.lib.unix.FileAccessException;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayList;
+import java.util.Collection;
+
+/**
+ * A FileSystem that does not use any JNI and hence, does not require a shared library be present at
+ * execution.
+ *
+ * <p>Note: Blaze profiler tasks are defined on the system call level - thus we do not distinguish
+ * (from profiling perspective) between different methods on this class that end up doing stat()
+ * system call - they all are associated with the VFS_STAT task.
+ */
+@ThreadSafe
+public class JavaIoFileSystem extends AbstractFileSystem {
+ private static final LinkOption[] NO_LINK_OPTION = new LinkOption[0];
+ // This isn't generally safe; we rely on the file system APIs not modifying the array.
+ private static final LinkOption[] NOFOLLOW_LINKS_OPTION =
+ new LinkOption[] { LinkOption.NOFOLLOW_LINKS };
+
+ protected static final String ERR_IS_DIRECTORY = " (Is a directory)";
+ protected static final String ERR_DIRECTORY_NOT_EMPTY = " (Directory not empty)";
+ protected static final String ERR_FILE_EXISTS = " (File exists)";
+ protected static final String ERR_NO_SUCH_FILE_OR_DIR = " (No such file or directory)";
+ protected static final String ERR_NOT_A_DIRECTORY = " (Not a directory)";
+
+ protected File getIoFile(Path path) {
+ return new File(path.toString());
+ }
+
+ private LinkOption[] linkOpts(boolean followSymlinks) {
+ return followSymlinks ? NO_LINK_OPTION : NOFOLLOW_LINKS_OPTION;
+ }
+
+ @Override
+ protected Collection<Path> getDirectoryEntries(Path path) throws IOException {
+ File file = getIoFile(path);
+ String[] entries = null;
+ long startTime = Profiler.nanoTimeMaybe();
+ try {
+ entries = file.list();
+ if (entries == null) {
+ if (file.exists()) {
+ throw new IOException(path + ERR_NOT_A_DIRECTORY);
+ } else {
+ throw new FileNotFoundException(path + ERR_NO_SUCH_FILE_OR_DIR);
+ }
+ }
+ } finally {
+ profiler.logSimpleTask(startTime, ProfilerTask.VFS_DIR, file.getPath());
+ }
+ Collection<Path> result = new ArrayList<>(entries.length);
+ for (String entry : entries) {
+ if (!entry.equals(".") && !entry.equals("..")) {
+ result.add(path.getChild(entry));
+ }
+ }
+ return result;
+ }
+
+ @Override
+ protected boolean exists(Path path, boolean followSymlinks) {
+ File file = getIoFile(path);
+ long startTime = Profiler.nanoTimeMaybe();
+ try {
+ return Files.exists(file.toPath(), linkOpts(followSymlinks));
+ } finally {
+ profiler.logSimpleTask(startTime, ProfilerTask.VFS_STAT, path.toString());
+ }
+ }
+
+ @Override
+ protected boolean isDirectory(Path path, boolean followSymlinks) {
+ File file = getIoFile(path);
+ long startTime = Profiler.nanoTimeMaybe();
+ try {
+ if (!followSymlinks && fileIsSymbolicLink(file)) {
+ return false;
+ }
+ return file.isDirectory();
+ } finally {
+ profiler.logSimpleTask(startTime, ProfilerTask.VFS_STAT, path.toString());
+ }
+ }
+
+ @Override
+ protected boolean isFile(Path path, boolean followSymlinks) {
+ File file = getIoFile(path);
+ long startTime = Profiler.nanoTimeMaybe();
+ try {
+ if (!followSymlinks && fileIsSymbolicLink(file)) {
+ return false;
+ }
+ return file.isFile();
+ } finally {
+ profiler.logSimpleTask(startTime, ProfilerTask.VFS_STAT, path.toString());
+ }
+ }
+
+ @Override
+ protected boolean isReadable(Path path) throws IOException {
+ File file = getIoFile(path);
+ long startTime = Profiler.nanoTimeMaybe();
+ try {
+ if (!file.exists()) {
+ throw new FileNotFoundException(path + ERR_NO_SUCH_FILE_OR_DIR);
+ }
+ return file.canRead();
+ } finally {
+ profiler.logSimpleTask(startTime, ProfilerTask.VFS_STAT, file.getPath());
+ }
+ }
+
+ @Override
+ protected boolean isWritable(Path path) throws IOException {
+ File file = getIoFile(path);
+ long startTime = Profiler.nanoTimeMaybe();
+ try {
+ if (!file.exists()) {
+ if (linkExists(file)) {
+ throw new IOException(path + ERR_PERMISSION_DENIED);
+ } else {
+ throw new FileNotFoundException(path + ERR_NO_SUCH_FILE_OR_DIR);
+ }
+ }
+ return file.canWrite();
+ } finally {
+ profiler.logSimpleTask(startTime, ProfilerTask.VFS_STAT, file.getPath());
+ }
+ }
+
+ @Override
+ protected boolean isExecutable(Path path) throws IOException {
+ File file = getIoFile(path);
+ long startTime = Profiler.nanoTimeMaybe();
+ try {
+ if (!file.exists()) {
+ throw new FileNotFoundException(path + ERR_NO_SUCH_FILE_OR_DIR);
+ }
+ return file.canExecute();
+ } finally {
+ profiler.logSimpleTask(startTime, ProfilerTask.VFS_STAT, file.getPath());
+ }
+ }
+
+ @Override
+ protected void setReadable(Path path, boolean readable) throws IOException {
+ File file = getIoFile(path);
+ if (!file.exists()) {
+ throw new FileNotFoundException(path + ERR_NO_SUCH_FILE_OR_DIR);
+ }
+ file.setReadable(readable);
+ }
+
+ @Override
+ protected void setWritable(Path path, boolean writable) throws IOException {
+ File file = getIoFile(path);
+ if (!file.exists()) {
+ throw new FileNotFoundException(path + ERR_NO_SUCH_FILE_OR_DIR);
+ }
+ file.setWritable(writable);
+ }
+
+ @Override
+ protected void setExecutable(Path path, boolean executable) throws IOException {
+ File file = getIoFile(path);
+ if (!file.exists()) {
+ throw new FileNotFoundException(path + ERR_NO_SUCH_FILE_OR_DIR);
+ }
+ file.setExecutable(executable);
+ }
+
+ @Override
+ public boolean supportsModifications() {
+ return true;
+ }
+
+ @Override
+ public boolean supportsSymbolicLinks() {
+ return true;
+ }
+
+ @Override
+ protected boolean createDirectory(Path path) throws IOException {
+
+ // We always synchronize on the current path before doing it on the parent path and file system
+ // path structure ensures that this locking order will never be reversed.
+ // When refactoring, check that subclasses still work as expected and there can be no
+ // deadlocks.
+ synchronized (path) {
+ File file = getIoFile(path);
+ if (file.mkdir()) {
+ return true;
+ }
+
+ // We will be checking the state of the parent path as well. Synchronize on it before
+ // attempting anything.
+ Path parentDirectory = path.getParentDirectory();
+ synchronized (parentDirectory) {
+ if (fileIsSymbolicLink(file)) {
+ throw new IOException(path + ERR_FILE_EXISTS);
+ }
+ if (file.isDirectory()) {
+ return false; // directory already existed
+ } else if (file.exists()) {
+ throw new IOException(path + ERR_FILE_EXISTS);
+ } else if (!file.getParentFile().exists()) {
+ throw new FileNotFoundException(path.getParentDirectory() + ERR_NO_SUCH_FILE_OR_DIR);
+ }
+ // Parent directory apparently exists - try to create our directory again - protecting
+ // against the case where parent directory would be created right before us obtaining
+ // synchronization lock.
+ if (file.mkdir()) {
+ return true; // Everything is fine finally.
+ } else if (!file.getParentFile().canWrite()) {
+ throw new FileAccessException(path + ERR_PERMISSION_DENIED);
+ } else {
+ // Parent exists, is writable, yet we can't create our directory.
+ throw new FileNotFoundException(path.getParentDirectory() + ERR_NOT_A_DIRECTORY);
+ }
+ }
+ }
+ }
+
+ private boolean linkExists(File file) {
+ String shortName = file.getName();
+ File parentFile = file.getParentFile();
+ if (parentFile == null) {
+ return false;
+ }
+ String[] filenames = parentFile.list();
+ if (filenames == null) {
+ return false;
+ }
+ for (String name : filenames) {
+ if (name.equals(shortName)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ protected void createSymbolicLink(Path linkPath, PathFragment targetFragment)
+ throws IOException {
+ File file = getIoFile(linkPath);
+ try {
+ Files.createSymbolicLink(file.toPath(), new File(targetFragment.getPathString()).toPath());
+ } catch (java.nio.file.FileAlreadyExistsException e) {
+ throw new IOException(linkPath + ERR_FILE_EXISTS);
+ } catch (java.nio.file.AccessDeniedException e) {
+ throw new IOException(linkPath + ERR_PERMISSION_DENIED);
+ } catch (java.nio.file.NoSuchFileException e) {
+ throw new FileNotFoundException(linkPath + ERR_NO_SUCH_FILE_OR_DIR);
+ }
+ }
+
+ @Override
+ protected PathFragment readSymbolicLink(Path path) throws IOException {
+ File file = getIoFile(path);
+ long startTime = Profiler.nanoTimeMaybe();
+ try {
+ String link = Files.readSymbolicLink(file.toPath()).toString();
+ return new PathFragment(link);
+ } catch (java.nio.file.NotLinkException e) {
+ throw new NotASymlinkException(path);
+ } catch (java.nio.file.NoSuchFileException e) {
+ throw new FileNotFoundException(path + ERR_NO_SUCH_FILE_OR_DIR);
+ } finally {
+ profiler.logSimpleTask(startTime, ProfilerTask.VFS_LINK, file.getPath());
+ }
+ }
+
+ @Override
+ protected void renameTo(Path sourcePath, Path targetPath) throws IOException {
+ synchronized (sourcePath) {
+ File sourceFile = getIoFile(sourcePath);
+ File targetFile = getIoFile(targetPath);
+ if (!sourceFile.renameTo(targetFile)) {
+ if (!sourceFile.exists()) {
+ throw new FileNotFoundException(sourcePath + ERR_NO_SUCH_FILE_OR_DIR);
+ }
+ if (targetFile.exists()) {
+ if (targetFile.isDirectory() && targetFile.list().length > 0) {
+ throw new IOException(targetPath + ERR_DIRECTORY_NOT_EMPTY);
+ } else if (sourceFile.isDirectory() && targetFile.isFile()) {
+ throw new IOException(sourcePath + " -> " + targetPath + ERR_NOT_A_DIRECTORY);
+ } else if (sourceFile.isFile() && targetFile.isDirectory()) {
+ throw new IOException(sourcePath + " -> " + targetPath + ERR_IS_DIRECTORY);
+ } else {
+ throw new IOException(sourcePath + " -> " + targetPath + ERR_PERMISSION_DENIED);
+ }
+ } else {
+ throw new FileAccessException(sourcePath + " -> " + targetPath + ERR_PERMISSION_DENIED);
+ }
+ }
+ }
+ }
+
+ @Override
+ protected long getFileSize(Path path, boolean followSymlinks) throws IOException {
+ long startTime = Profiler.nanoTimeMaybe();
+ try {
+ return stat(path, followSymlinks).getSize();
+ } finally {
+ profiler.logSimpleTask(startTime, ProfilerTask.VFS_STAT, path);
+ }
+ }
+
+ @Override
+ protected boolean delete(Path path) throws IOException {
+ File file = getIoFile(path);
+ long startTime = Profiler.nanoTimeMaybe();
+ synchronized (path) {
+ try {
+ if (file.delete()) {
+ return true;
+ }
+ if (file.exists()) {
+ if (file.isDirectory() && file.list().length > 0) {
+ throw new IOException(path + ERR_DIRECTORY_NOT_EMPTY);
+ } else {
+ throw new IOException(path + ERR_PERMISSION_DENIED);
+ }
+ }
+ return false;
+ } finally {
+ profiler.logSimpleTask(startTime, ProfilerTask.VFS_DELETE, file.getPath());
+ }
+ }
+ }
+
+ @Override
+ protected long getLastModifiedTime(Path path, boolean followSymlinks) throws IOException {
+ File file = getIoFile(path);
+ long startTime = Profiler.nanoTimeMaybe();
+ try {
+ return stat(path, followSymlinks).getLastModifiedTime();
+ } finally {
+ profiler.logSimpleTask(startTime, ProfilerTask.VFS_STAT, file.getPath());
+ }
+ }
+
+ @Override
+ protected boolean isSymbolicLink(Path path) {
+ File file = getIoFile(path);
+ long startTime = Profiler.nanoTimeMaybe();
+ try {
+ return fileIsSymbolicLink(file);
+ } finally {
+ profiler.logSimpleTask(startTime, ProfilerTask.VFS_STAT, file.getPath());
+ }
+ }
+
+ private boolean fileIsSymbolicLink(File file) {
+ return Files.isSymbolicLink(file.toPath());
+ }
+
+ @Override
+ protected void setLastModifiedTime(Path path, long newTime) throws IOException {
+ File file = getIoFile(path);
+ if (!file.setLastModified(newTime)) {
+ if (!file.exists()) {
+ throw new FileNotFoundException(path + ERR_NO_SUCH_FILE_OR_DIR);
+ } else if (!file.getParentFile().canWrite()) {
+ throw new FileAccessException(path.getParentDirectory() + ERR_PERMISSION_DENIED);
+ } else {
+ throw new FileAccessException(path + ERR_PERMISSION_DENIED);
+ }
+ }
+ }
+
+ @Override
+ protected byte[] getMD5Digest(Path path) throws IOException {
+ String name = path.toString();
+ long startTime = Profiler.nanoTimeMaybe();
+ try {
+ return super.getMD5Digest(path);
+ } finally {
+ profiler.logSimpleTask(startTime, ProfilerTask.VFS_MD5, name);
+ }
+ }
+
+ /**
+ * Returns the status of a file. See {@link Path#stat(Symlinks)} for
+ * specification.
+ *
+ * <p>The default implementation of this method is a "lazy" one, based on
+ * other accessor methods such as {@link #isFile}, etc. Subclasses may provide
+ * more efficient specializations. However, we still try to follow Unix-like
+ * semantics of failing fast in case of non-existent files (or in case of
+ * permission issues).
+ */
+ @Override
+ protected FileStatus stat(final Path path, final boolean followSymlinks) throws IOException {
+ File file = getIoFile(path);
+ final BasicFileAttributes attributes;
+ try {
+ attributes = Files.readAttributes(
+ file.toPath(), BasicFileAttributes.class, linkOpts(followSymlinks));
+ } catch (java.nio.file.FileSystemException e) {
+ throw new FileNotFoundException(path + ERR_NO_SUCH_FILE_OR_DIR);
+ }
+ FileStatus status = new FileStatus() {
+ @Override
+ public boolean isFile() {
+ return attributes.isRegularFile();
+ }
+
+ @Override
+ public boolean isDirectory() {
+ return attributes.isDirectory();
+ }
+
+ @Override
+ public boolean isSymbolicLink() {
+ return attributes.isSymbolicLink();
+ }
+
+ @Override
+ public long getSize() throws IOException {
+ return attributes.size();
+ }
+
+ @Override
+ public long getLastModifiedTime() throws IOException {
+ return attributes.lastModifiedTime().toMillis();
+ }
+
+ @Override
+ public long getLastChangeTime() {
+ // This is the best we can do with Java NIO...
+ return attributes.lastModifiedTime().toMillis();
+ }
+
+ @Override
+ public long getNodeId() {
+ // TODO(bazel-team): Consider making use of attributes.fileKey().
+ return -1;
+ }
+ };
+
+ return status;
+ }
+
+ @Override
+ protected FileStatus statIfFound(Path path, boolean followSymlinks) {
+ try {
+ return stat(path, followSymlinks);
+ } catch (FileNotFoundException e) {
+ // JavaIoFileSystem#stat (incorrectly) only throws FileNotFoundException (because it calls
+ // #getLastModifiedTime, which can only throw a FileNotFoundException), so we always hit this
+ // codepath. Thus, this method will incorrectly not throw an exception for some filesystem
+ // errors.
+ return null;
+ } catch (IOException e) {
+ // If this codepath is ever hit, then this method should be rewritten to properly distinguish
+ // between not-found exceptions and others.
+ throw new IllegalStateException(e);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/ModifiedFileSet.java b/src/main/java/com/google/devtools/build/lib/vfs/ModifiedFileSet.java
new file mode 100644
index 0000000..3d6a638
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/vfs/ModifiedFileSet.java
@@ -0,0 +1,126 @@
+// Copyright 2014 Google Inc. 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.vfs;
+
+import com.google.common.collect.ImmutableSet;
+
+import java.util.Objects;
+
+import javax.annotation.Nullable;
+
+/**
+ * An immutable set of modified source files. The scope of these files is context-dependent; in some
+ * uses this may mean information about all files in the client, while in other uses this may mean
+ * information about some specific subset of files. {@link #EVERYTHING_MODIFIED} can be used to
+ * indicate that all files of interest have been modified.
+ */
+public final class ModifiedFileSet {
+
+ public static final ModifiedFileSet EVERYTHING_MODIFIED = new ModifiedFileSet(null);
+ public static final ModifiedFileSet NOTHING_MODIFIED = new ModifiedFileSet(
+ ImmutableSet.<PathFragment>of());
+
+ @Nullable private final ImmutableSet<PathFragment> modified;
+
+ /**
+ * Whether all files of interest should be treated as potentially modified.
+ */
+ public boolean treatEverythingAsModified() {
+ return modified == null;
+ }
+
+ /**
+ * The set of files of interest that were modified.
+ *
+ * @throws IllegalStateException if {@link #treatEverythingAsModified} returns true.
+ */
+ public ImmutableSet<PathFragment> modifiedSourceFiles() {
+ if (treatEverythingAsModified()) {
+ throw new IllegalStateException();
+ }
+ return modified;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof ModifiedFileSet)) {
+ return false;
+ }
+ ModifiedFileSet other = (ModifiedFileSet) o;
+ return Objects.equals(modified, other.modified);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(modified);
+ }
+
+ @Override
+ public String toString() {
+ if (this == EVERYTHING_MODIFIED) {
+ return "EVERYTHING_MODIFIED";
+ } else if (this == NOTHING_MODIFIED) {
+ return "NOTHING_MODIFIED";
+ } else {
+ return modified.toString();
+ }
+ }
+
+ private ModifiedFileSet(ImmutableSet<PathFragment> modified) {
+ this.modified = modified;
+ }
+
+ /**
+ * The builder for {@link ModifiedFileSet}.
+ */
+ public static class Builder {
+ private final ImmutableSet.Builder<PathFragment> setBuilder =
+ ImmutableSet.<PathFragment>builder();
+
+ public ModifiedFileSet build() {
+ ImmutableSet<PathFragment> modified = setBuilder.build();
+ return modified.isEmpty() ? NOTHING_MODIFIED : new ModifiedFileSet(modified);
+ }
+
+ public Builder modify(PathFragment pathFragment) {
+ setBuilder.add(pathFragment);
+ return this;
+ }
+
+ public Builder modifyAll(Iterable<PathFragment> pathFragments) {
+ setBuilder.addAll(pathFragments);
+ return this;
+ }
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static ModifiedFileSet union(ModifiedFileSet mfs1, ModifiedFileSet mfs2) {
+ if (mfs1.treatEverythingAsModified() || mfs2.treatEverythingAsModified()) {
+ return ModifiedFileSet.EVERYTHING_MODIFIED;
+ }
+ if (mfs1.equals(ModifiedFileSet.NOTHING_MODIFIED)) {
+ return mfs2;
+ }
+ if (mfs2.equals(ModifiedFileSet.NOTHING_MODIFIED)) {
+ return mfs1;
+ }
+ return ModifiedFileSet.builder()
+ .modifyAll(mfs1.modifiedSourceFiles())
+ .modifyAll(mfs2.modifiedSourceFiles())
+ .build();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/Path.java b/src/main/java/com/google/devtools/build/lib/vfs/Path.java
new file mode 100644
index 0000000..de222fe
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/vfs/Path.java
@@ -0,0 +1,1099 @@
+// Copyright 2014 Google Inc. 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.vfs;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.util.OS;
+import com.google.devtools.build.lib.util.StringCanonicalizer;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.OutputStream;
+import java.io.Serializable;
+import java.lang.ref.Reference;
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.IdentityHashMap;
+import java.util.Objects;
+
+/**
+ * <p>Instances of this class represent pathnames, forming a tree
+ * structure to implement sharing of common prefixes (parent directory names).
+ * A node in these trees is something like foo, bar, .., ., or /. If the
+ * instance is not a root path, it will have a parent path. A path can also
+ * have children, which are indexed by name in a map.
+ *
+ * <p>There is some limited support for Windows-style paths. Most importantly, drive identifiers
+ * in front of a path (c:/abc) are supported. However, Windows-style backslash separators
+ * (C:\\foo\\bar) and drive-relative paths ("C:foo") are explicitly not supported, same with
+ * advanced features like \\\\network\\paths and \\\\?\\unc\\paths.
+ *
+ * <p>{@link FileSystem} implementations maintain pointers into this graph.
+ */
+@ThreadSafe
+public class Path implements Comparable<Path>, Serializable {
+
+ private static FileSystem fileSystemForSerialization;
+
+ /**
+ * We need to specify used FileSystem. In this case we can save memory during the serialization.
+ */
+ public static void setFileSystemForSerialization(FileSystem fileSystem) {
+ fileSystemForSerialization = fileSystem;
+ }
+
+ /**
+ * Returns FileSystem that we are using.
+ */
+ public static FileSystem getFileSystemForSerialization() {
+ return fileSystemForSerialization;
+ }
+
+ // These are basically final, but can't be marked as such in order to support serialization.
+ private FileSystem fileSystem;
+ private String name;
+ private Path parent;
+ private int depth;
+ private int hashCode;
+
+ private static final ReferenceQueue<Path> REFERENCE_QUEUE = new ReferenceQueue<>();
+
+ private static class PathWeakReferenceForCleanup extends WeakReference<Path> {
+ final Path parent;
+ final String baseName;
+
+ PathWeakReferenceForCleanup(Path referent, ReferenceQueue<Path> referenceQueue) {
+ super(referent, referenceQueue);
+ parent = referent.getParentDirectory();
+ baseName = referent.getBaseName();
+ }
+ }
+
+ private static final Thread PATH_CHILD_CACHE_CLEANUP_THREAD = new Thread("Path cache cleanup") {
+ @Override
+ public void run() {
+ while (true) {
+ try {
+ PathWeakReferenceForCleanup ref = (PathWeakReferenceForCleanup) REFERENCE_QUEUE.remove();
+ Path parent = ref.parent;
+ synchronized (parent) {
+ // It's possible that since this reference was enqueued for deletion, the Path was
+ // recreated with a new entry in the map. We definitely shouldn't delete that entry, so
+ // check to make sure they're the same.
+ Reference<Path> currentRef = ref.parent.children.get(ref.baseName);
+ if (currentRef == ref) {
+ ref.parent.children.remove(ref.baseName);
+ }
+ }
+ } catch (InterruptedException e) {
+ // Ignored.
+ }
+ }
+ }
+ };
+
+ static {
+ PATH_CHILD_CACHE_CLEANUP_THREAD.setDaemon(true);
+ PATH_CHILD_CACHE_CLEANUP_THREAD.start();
+ }
+
+ /**
+ * A mapping from a child file name to the {@link Path} representing it.
+ *
+ * <p>File names must be a single path segment. The strings must be
+ * canonical. We use IdentityHashMap instead of HashMap for reasons of space
+ * efficiency: instances are smaller by a single word. Also, since all path
+ * segments are interned, the universe of Paths holds a minimal number of
+ * references to strings. (It's doubtful that there's any time gain from use
+ * of an IdentityHashMap, since the time saved by avoiding string equality
+ * tests during hash lookups is probably equal to the time spent eagerly
+ * interning strings, unless the collision rate is high.)
+ *
+ * <p>The Paths are stored as weak references to ensure that a live
+ * Path for a directory does not hold a strong reference to all of its
+ * descendants, which would prevent collection of paths we never intend to
+ * use again. Stale references in the map must be treated as absent.
+ *
+ * <p>A Path may be recycled once there is no Path that refers to it or
+ * to one of its descendants. This means that any data stored in the
+ * Path instance by one of its subclasses must be recoverable: it's ok to
+ * store data in Paths as an optimization, but there must be another
+ * source for that data in case the Path is recycled.
+ *
+ * <p>We intentionally avoid using the existing library classes for reasons of
+ * space efficiency: while ConcurrentHashMap would reduce our locking
+ * overhead, and ReferenceMap would simplify the code a little, both of those
+ * classes have much higher per-instance overheads than IdentityHashMap.
+ *
+ * <p>The Path object must be synchronized while children is being
+ * accessed.
+ */
+ private IdentityHashMap<String, Reference<Path>> children;
+
+ /**
+ * Create a path instance. Should only be called by {@link #createChildPath}.
+ *
+ * @param name the name of this path; it must be canonicalized with {@link
+ * StringCanonicalizer#intern}
+ * @param parent this path's parent
+ */
+ protected Path(FileSystem fileSystem, String name, Path parent) {
+ this.fileSystem = fileSystem;
+ this.name = name;
+ this.parent = parent;
+ this.depth = parent == null ? 0 : parent.depth + 1;
+ this.hashCode = Objects.hash(parent, name);
+ }
+
+ /**
+ * Create the root path. Should only be called by
+ * {@link FileSystem#createRootPath()}.
+ */
+ protected Path(FileSystem fileSystem) {
+ this(fileSystem, StringCanonicalizer.intern("/"), null);
+ }
+
+ private void writeObject(ObjectOutputStream out) throws IOException {
+ Preconditions.checkState(fileSystem == fileSystemForSerialization, fileSystem);
+ out.writeUTF(getPathString());
+ }
+
+ private void readObject(ObjectInputStream in) throws IOException {
+ fileSystem = fileSystemForSerialization;
+ String p = in.readUTF();
+ PathFragment pf = new PathFragment(p);
+ PathFragment parentDir = pf.getParentDirectory();
+ if (parentDir == null) {
+ this.name = "/";
+ this.parent = null;
+ this.depth = 0;
+ } else {
+ this.name = pf.getBaseName();
+ this.parent = fileSystem.getPath(parentDir);
+ this.depth = this.parent.depth + 1;
+ }
+ this.hashCode = Objects.hash(parent, name);
+ }
+
+ /**
+ * Returns the filesystem instance to which this path belongs.
+ */
+ public FileSystem getFileSystem() {
+ return fileSystem;
+ }
+
+ public boolean isRootDirectory() {
+ return parent == null;
+ }
+
+ protected Path createChildPath(String childName) {
+ return new Path(fileSystem, childName, this);
+ }
+
+ /**
+ * Returns the child path named name, or creates such a path (and caches it)
+ * if it doesn't already exist.
+ */
+ private Path getCachedChildPath(String childName) {
+ // Don't hold the lock for the interning operation. It increases lock contention.
+ childName = StringCanonicalizer.intern(childName);
+ synchronized(this) {
+ if (children == null) {
+ // 66% of Paths have size == 1, 80% <= 2
+ children = new IdentityHashMap<String, Reference<Path>>(1);
+ }
+ Reference<Path> childRef = children.get(childName);
+ Path child;
+ if (childRef == null || (child = childRef.get()) == null) {
+ child = createChildPath(childName);
+ children.put(childName, new PathWeakReferenceForCleanup(child, REFERENCE_QUEUE));
+ }
+ return child;
+ }
+ }
+
+ /**
+ * Applies the specified function to each {@link Path} that is an existing direct
+ * descendant of this one. The Predicate is evaluated only for its
+ * side-effects.
+ *
+ * <p>This function exists to hide the "children" field, whose complex
+ * synchronization and identity requirements are too unsafe to be exposed to
+ * subclasses. For example, the "children" field must be synchronized for
+ * the duration of any iteration over it; it may be null; and references
+ * within it may be stale, and must be ignored.
+ */
+ protected synchronized void applyToChildren(Predicate<Path> function) {
+ if (children != null) {
+ for (Reference<Path> childRef : children.values()) {
+ Path child = childRef.get();
+ if (child != null) {
+ function.apply(child);
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns whether this path is recursively "under" {@code prefix} - that is,
+ * whether {@code path} is a prefix of this path.
+ *
+ * <p>This method returns {@code true} when called with this path itself. This
+ * method acts independently of the existence of files or folders.
+ *
+ * @param prefix a path which may or may not be a prefix of this path
+ */
+ public boolean startsWith(Path prefix) {
+ Path n = this;
+ for (int i = 0, len = depth - prefix.depth; i < len; i++) {
+ n = n.getParentDirectory();
+ }
+ return prefix.equals(n);
+ }
+
+ /**
+ * Computes a string representation of this path, and writes it to the
+ * given string builder. Only called locally with a new instance.
+ */
+ private void buildPathString(StringBuilder result) {
+ if (isRootDirectory()) {
+ result.append('/');
+ } else {
+ if (parent.isWindowsVolumeName()) {
+ result.append(parent.name);
+ } else {
+ parent.buildPathString(result);
+ }
+ if (!parent.isRootDirectory()) {
+ result.append('/');
+ }
+ result.append(name);
+ }
+ }
+
+ /**
+ * Returns true if the current path represents a Windows volume name (such as "c:" or "d:").
+ *
+ * <p>Paths such as '\\\\vol\\foo' are not supported.
+ */
+ private boolean isWindowsVolumeName() {
+ return OS.getCurrent() == OS.WINDOWS
+ && parent != null && parent.isRootDirectory() && name.length() == 2
+ && PathFragment.getWindowsDriveLetter(name) != '\0';
+ }
+
+ /**
+ * Returns the path as a string.
+ */
+ public String getPathString() {
+ // Profile driven optimization:
+ // Preallocate a size determined by the depth, so that
+ // we do not have to expand the capacity of the StringBuilder
+ StringBuilder builder = new StringBuilder(depth * 20);
+ buildPathString(builder);
+ return builder.toString();
+ }
+
+ /**
+ * Returns the path as a string.
+ */
+ @Override
+ public String toString() {
+ return getPathString();
+ }
+
+ @Override
+ public int hashCode() {
+ return hashCode;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof Path)) {
+ return false;
+ }
+ Path otherPath = (Path) other;
+ return fileSystem.equals(otherPath.fileSystem) && name.equals(otherPath.name)
+ && Objects.equals(parent, otherPath.parent);
+ }
+
+ /**
+ * Returns a string of debugging information associated with this path.
+ * The results are unspecified and MUST NOT be interpreted programmatically.
+ */
+ protected String toDebugString() {
+ return "";
+ }
+
+ /**
+ * Returns a path representing the parent directory of this path,
+ * or null iff this Path represents the root of the filesystem.
+ *
+ * <p>Note: This method normalises ".." and "." path segments by string
+ * processing, not by directory lookups.
+ */
+ public Path getParentDirectory() {
+ return parent;
+ }
+
+ /**
+ * Returns true iff this path denotes an existing file of any kind. Follows
+ * symbolic links.
+ */
+ public boolean exists() {
+ return fileSystem.exists(this, true);
+ }
+
+ /**
+ * Returns true iff this path denotes an existing file of any kind.
+ *
+ * @param followSymlinks if {@link Symlinks#FOLLOW}, and this path denotes a
+ * symbolic link, the link is dereferenced until a file other than a
+ * symbolic link is found
+ */
+ public boolean exists(Symlinks followSymlinks) {
+ return fileSystem.exists(this, followSymlinks.toBoolean());
+ }
+
+ /**
+ * Returns a new, immutable collection containing the names of all entities
+ * within the directory denoted by the current path. Follows symbolic links.
+ *
+ * @throws FileNotFoundException If the directory is not found
+ * @throws IOException If the path does not denote a directory
+ */
+ public Collection<Path> getDirectoryEntries() throws IOException, FileNotFoundException {
+ return fileSystem.getDirectoryEntries(this);
+ }
+
+ /**
+ * Returns a collection of the names and types of all entries within the directory
+ * denoted by the current path. Follows symbolic links if {@code followSymlinks} is true.
+ * Note that the order of the returned entries is not guaranteed.
+ *
+ * @param followSymlinks whether to follow symlinks or not
+ *
+ * @throws FileNotFoundException If the directory is not found
+ * @throws IOException If the path does not denote a directory
+ */
+ public Collection<Dirent> readdir(Symlinks followSymlinks) throws IOException {
+ return fileSystem.readdir(this, followSymlinks.toBoolean());
+ }
+
+ /**
+ * Returns a new, immutable collection containing the names of all entities
+ * within the directory denoted by the current path, for which the given
+ * predicate is true.
+ *
+ * @throws FileNotFoundException If the directory is not found
+ * @throws IOException If the path does not denote a directory
+ */
+ public Collection<Path> getDirectoryEntries(Predicate<? super Path> predicate)
+ throws IOException, FileNotFoundException {
+ return ImmutableList.<Path>copyOf(Iterables.filter(getDirectoryEntries(), predicate));
+ }
+
+ /**
+ * Returns the status of a file, following symbolic links.
+ *
+ * @throws IOException if there was an error obtaining the file status. Note,
+ * some implementations may defer the I/O, and hence the throwing of
+ * the exception, until the accessor methods of {@code FileStatus} are
+ * called.
+ */
+ public FileStatus stat() throws IOException {
+ return fileSystem.stat(this, true);
+ }
+
+ /**
+ * Like stat(), but returns null on file-nonexistence instead of throwing.
+ */
+ public FileStatus statNullable() {
+ return statNullable(Symlinks.FOLLOW);
+ }
+
+ /**
+ * Like stat(), but returns null on file-nonexistence instead of throwing.
+ */
+ public FileStatus statNullable(Symlinks symlinks) {
+ return fileSystem.statNullable(this, symlinks.toBoolean());
+ }
+
+ /**
+ * Returns the status of a file, optionally following symbolic links.
+ *
+ * @param followSymlinks if {@link Symlinks#FOLLOW}, and this path denotes a
+ * symbolic link, the link is dereferenced until a file other than a
+ * symbolic link is found
+ * @throws IOException if there was an error obtaining the file status. Note,
+ * some implementations may defer the I/O, and hence the throwing of
+ * the exception, until the accessor methods of {@code FileStatus} are
+ * called
+ */
+ public FileStatus stat(Symlinks followSymlinks) throws IOException {
+ return fileSystem.stat(this, followSymlinks.toBoolean());
+ }
+
+ /**
+ * Like {@link #stat}, but may return null if the file is not found (corresponding to
+ * {@code ENOENT} and {@code ENOTDIR} in Unix's stat(2) function) instead of throwing. Follows
+ * symbolic links.
+ */
+ public FileStatus statIfFound() throws IOException {
+ return fileSystem.statIfFound(this, true);
+ }
+
+ /**
+ * Like {@link #stat}, but may return null if the file is not found (corresponding to
+ * {@code ENOENT} and {@code ENOTDIR} in Unix's stat(2) function) instead of throwing.
+ *
+ * @param followSymlinks if {@link Symlinks#FOLLOW}, and this path denotes a
+ * symbolic link, the link is dereferenced until a file other than a
+ * symbolic link is found
+ */
+ public FileStatus statIfFound(Symlinks followSymlinks) throws IOException {
+ return fileSystem.statIfFound(this, followSymlinks.toBoolean());
+ }
+
+
+ /**
+ * Returns true iff this path denotes an existing directory. Follows symbolic
+ * links.
+ */
+ public boolean isDirectory() {
+ return fileSystem.isDirectory(this, true);
+ }
+
+ /**
+ * Returns true iff this path denotes an existing directory.
+ *
+ * @param followSymlinks if {@link Symlinks#FOLLOW}, and this path denotes a
+ * symbolic link, the link is dereferenced until a file other than a
+ * symbolic link is found
+ */
+ public boolean isDirectory(Symlinks followSymlinks) {
+ return fileSystem.isDirectory(this, followSymlinks.toBoolean());
+ }
+
+ /**
+ * Returns true iff this path denotes an existing regular or special file.
+ * Follows symbolic links.
+ *
+ * <p>For our purposes, "file" includes special files (socket, fifo, block or
+ * char devices) too; it excludes symbolic links and directories.
+ */
+ public boolean isFile() {
+ return fileSystem.isFile(this, true);
+ }
+
+ /**
+ * Returns true iff this path denotes an existing regular or special file.
+ *
+ * <p>For our purposes, a "file" includes special files (socket, fifo, block
+ * or char devices) too; it excludes symbolic links and directories.
+ *
+ * @param followSymlinks if {@link Symlinks#FOLLOW}, and this path denotes a
+ * symbolic link, the link is dereferenced until a file other than a
+ * symbolic link is found.
+ */
+ public boolean isFile(Symlinks followSymlinks) {
+ return fileSystem.isFile(this, followSymlinks.toBoolean());
+ }
+
+ /**
+ * Returns true iff this path denotes an existing symbolic link. Does not
+ * follow symbolic links.
+ */
+ public boolean isSymbolicLink() {
+ return fileSystem.isSymbolicLink(this);
+ }
+
+ /**
+ * Returns the last segment of this path, or "/" for the root directory.
+ */
+ public String getBaseName() {
+ return name;
+ }
+
+ /**
+ * Interprets the name of a path segment relative to the current path and
+ * returns the result.
+ *
+ * <p>This is a purely syntactic operation, i.e. it does no I/O, it does not
+ * validate the existence of any path, nor resolve symbolic links. If 'prefix'
+ * is not canonical, then a 'name' of '..' will be interpreted incorrectly.
+ *
+ * @precondition segment contains no slashes.
+ */
+ private Path getCanonicalPath(String segment) {
+ if (segment.equals(".") || segment.equals("")) {
+ return this; // that's a noop
+ } else if (segment.equals("..")) {
+ // root's parent is root, when canonicalising:
+ return parent == null || isWindowsVolumeName() ? this : parent;
+ } else {
+ return getCachedChildPath(segment);
+ }
+ }
+
+ /**
+ * Returns the path formed by appending the single non-special segment
+ * "baseName" to this path.
+ *
+ * <p>You should almost always use {@link #getRelative} instead, which has
+ * the same performance characteristics if the given name is a valid base
+ * name, and which also works for '.', '..', and strings containing '/'.
+ *
+ * @throws IllegalArgumentException if {@code baseName} is not a valid base
+ * name according to {@link FileSystemUtils#checkBaseName}
+ */
+ public Path getChild(String baseName) {
+ FileSystemUtils.checkBaseName(baseName);
+ return getCachedChildPath(baseName);
+ }
+
+ /**
+ * Returns the path formed by appending the relative or absolute path fragment
+ * {@code suffix} to this path.
+ *
+ * <p>If suffix is absolute, the current path will be ignored; otherwise, they
+ * will be combined. Up-level references ("..") cause the preceding path
+ * segment to be elided; this interpretation is only correct if the base path
+ * is canonical.
+ */
+ public Path getRelative(PathFragment suffix) {
+ Path result = suffix.isAbsolute() ? fileSystem.getRootDirectory() : this;
+ if (!suffix.windowsVolume().isEmpty()) {
+ result = result.getCanonicalPath(suffix.windowsVolume());
+ }
+ for (String segment : suffix.segments()) {
+ result = result.getCanonicalPath(segment);
+ }
+ return result;
+ }
+
+ /**
+ * Returns the path formed by appending the relative or absolute string
+ * {@code path} to this path.
+ *
+ * <p>If the given path string is absolute, the current path will be ignored;
+ * otherwise, they will be combined. Up-level references ("..") cause the
+ * preceding path segment to be elided.
+ *
+ * <p>This is a purely syntactic operation, i.e. it does no I/O, it does not
+ * validate the existence of any path, nor resolve symbolic links.
+ */
+ public Path getRelative(String path) {
+ // Fast path for valid base names.
+ if ((path.length() == 0) || (path.equals("."))) {
+ return this;
+ } else if (path.equals("..")) {
+ return parent == null ? this : parent;
+ } else if ((path.indexOf('/') != -1)) {
+ return getRelative(new PathFragment(path));
+ } else {
+ return getCachedChildPath(path);
+ }
+ }
+
+ /**
+ * Returns an absolute PathFragment representing this path.
+ */
+ public PathFragment asFragment() {
+ String[] resultSegments = new String[depth];
+ Path currentPath = this;
+ for (int pos = depth - 1; pos >= 0; pos--) {
+ resultSegments[pos] = currentPath.getBaseName();
+ currentPath = currentPath.getParentDirectory();
+ }
+
+ char driveLetter = '\0';
+ if (resultSegments.length > 0) {
+ driveLetter = PathFragment.getWindowsDriveLetter(resultSegments[0]);
+ if (driveLetter != '\0') {
+ // Strip off the first segment that contains the volume name.
+ resultSegments = Arrays.copyOfRange(resultSegments, 1, resultSegments.length);
+ }
+ }
+
+ return new PathFragment(driveLetter, true, resultSegments);
+ }
+
+
+ /**
+ * Returns a relative path fragment to this path, relative to {@code
+ * ancestorDirectory}. {@code ancestorDirectory} must be on the same
+ * filesystem as this path. (Currently, both this path and "ancestorDirectory"
+ * must be absolute, though this restriction could be loosened.)
+ * <p>
+ * <code>x.relativeTo(z) == y</code> implies
+ * <code>z.getRelative(y.getPathString()) == x</code>.
+ * <p>
+ * For example, <code>"/foo/bar/wiz".relativeTo("/foo")</code> returns
+ * <code>"bar/wiz"</code>.
+ *
+ * @throws IllegalArgumentException if this path is not beneath {@code
+ * ancestorDirectory} or if they are not part of the same filesystem
+ */
+ public PathFragment relativeTo(Path ancestorPath) {
+ checkSameFilesystem(ancestorPath);
+
+ // Fast path: when otherPath is the ancestor of this path
+ int resultSegmentCount = depth - ancestorPath.depth;
+ if (resultSegmentCount >= 0) {
+ String[] resultSegments = new String[resultSegmentCount];
+ Path currentPath = this;
+ for (int pos = resultSegmentCount - 1; pos >= 0; pos--) {
+ resultSegments[pos] = currentPath.getBaseName();
+ currentPath = currentPath.getParentDirectory();
+ }
+ if (ancestorPath.equals(currentPath)) {
+ return new PathFragment('\0', false, resultSegments);
+ }
+ }
+
+ throw new IllegalArgumentException("Path " + this + " is not beneath " + ancestorPath);
+ }
+
+ /**
+ * Checks that "this" and "that" are paths on the same filesystem.
+ */
+ protected void checkSameFilesystem(Path that) {
+ if (this.fileSystem != that.fileSystem) {
+ throw new IllegalArgumentException("Files are on different filesystems: "
+ + this + ", " + that);
+ }
+ }
+
+ /**
+ * Returns an output stream to the file denoted by the current path, creating
+ * it and truncating it if necessary. The stream is opened for writing.
+ *
+ * @throws FileNotFoundException If the file cannot be found or created.
+ * @throws IOException If a different error occurs.
+ */
+ public OutputStream getOutputStream() throws IOException, FileNotFoundException {
+ return getOutputStream(false);
+ }
+
+ /**
+ * Returns an output stream to the file denoted by the current path, creating
+ * it and truncating it if necessary. The stream is opened for writing.
+ *
+ * @param append whether to open the file in append mode.
+ * @throws FileNotFoundException If the file cannot be found or created.
+ * @throws IOException If a different error occurs.
+ */
+ public OutputStream getOutputStream(boolean append) throws IOException, FileNotFoundException {
+ return fileSystem.getOutputStream(this, append);
+ }
+
+ /**
+ * Creates a directory with the name of the current path, not following
+ * symbolic links. Returns normally iff the directory exists after the call:
+ * true if the directory was created by this call, false if the directory was
+ * already in existence. Throws an exception if the directory could not be
+ * created for any reason.
+ *
+ * @throws IOException if the directory creation failed for any reason
+ */
+ public boolean createDirectory() throws IOException {
+ return fileSystem.createDirectory(this);
+ }
+
+ /**
+ * Creates a symbolic link with the name of the current path, following
+ * symbolic links. The referent of the created symlink is is the absolute path
+ * "target"; it is not possible to create relative symbolic links via this
+ * method.
+ *
+ * @throws IOException if the creation of the symbolic link was unsuccessful
+ * for any reason
+ */
+ public void createSymbolicLink(Path target) throws IOException {
+ checkSameFilesystem(target);
+ fileSystem.createSymbolicLink(this, target.asFragment());
+ }
+
+ /**
+ * Creates a symbolic link with the name of the current path, following
+ * symbolic links. The referent of the created symlink is is the path fragment
+ * "target", which may be absolute or relative.
+ *
+ * @throws IOException if the creation of the symbolic link was unsuccessful
+ * for any reason
+ */
+ public void createSymbolicLink(PathFragment target) throws IOException {
+ fileSystem.createSymbolicLink(this, target);
+ }
+
+ /**
+ * Returns the target of the current path, which must be a symbolic link. The
+ * link contents are returned exactly, and may contain an absolute or relative
+ * path. Analogous to readlink(2).
+ *
+ * @return the content (i.e. target) of the symbolic link
+ * @throws IOException if the current path is not a symbolic link, or the
+ * contents of the link could not be read for any reason
+ */
+ public PathFragment readSymbolicLink() throws IOException {
+ return fileSystem.readSymbolicLink(this);
+ }
+
+ /**
+ * Returns the canonical path for this path, by repeatedly replacing symbolic
+ * links with their referents. Analogous to realpath(3).
+ *
+ * @return the canonical path for this path
+ * @throws IOException if any symbolic link could not be resolved, or other
+ * error occurred (for example, the path does not exist)
+ */
+ public Path resolveSymbolicLinks() throws IOException {
+ return fileSystem.resolveSymbolicLinks(this);
+ }
+
+ /**
+ * Renames the file denoted by the current path to the location "target", not
+ * following symbolic links.
+ *
+ * <p>Files cannot be atomically renamed across devices; copying is required.
+ * Use {@link FileSystemUtils#copyFile} followed by {@link Path#delete}.
+ *
+ * @throws IOException if the rename failed for any reason
+ */
+ public void renameTo(Path target) throws IOException {
+ checkSameFilesystem(target);
+ fileSystem.renameTo(this, target);
+ }
+
+ /**
+ * Returns the size in bytes of the file denoted by the current path,
+ * following symbolic links.
+ *
+ * <p>The size of directory or special file is undefined.
+ *
+ * @throws FileNotFoundException if the file denoted by the current path does
+ * not exist
+ * @throws IOException if the file's metadata could not be read, or some other
+ * error occurred
+ */
+ public long getFileSize() throws IOException, FileNotFoundException {
+ return fileSystem.getFileSize(this, true);
+ }
+
+ /**
+ * Returns the size in bytes of the file denoted by the current path.
+ *
+ * <p>The size of directory or special file is undefined. The size of a symbolic
+ * link is the length of the name of its referent.
+ *
+ * @param followSymlinks if {@link Symlinks#FOLLOW}, and this path denotes a
+ * symbolic link, the link is deferenced until a file other than a
+ * symbol link is found
+ * @throws FileNotFoundException if the file denoted by the current path does
+ * not exist
+ * @throws IOException if the file's metadata could not be read, or some other
+ * error occurred
+ */
+ public long getFileSize(Symlinks followSymlinks) throws IOException, FileNotFoundException {
+ return fileSystem.getFileSize(this, followSymlinks.toBoolean());
+ }
+
+ /**
+ * Deletes the file denoted by this path, not following symbolic links.
+ * Returns normally iff the file doesn't exist after the call: true if this
+ * call deleted the file, false if the file already didn't exist. Throws an
+ * exception if the file could not be deleted for any reason.
+ *
+ * @return true iff the file was actually deleted by this call
+ * @throws IOException if the deletion failed but the file was present prior
+ * to the call
+ */
+ public boolean delete() throws IOException {
+ return fileSystem.delete(this);
+ }
+
+ /**
+ * Returns the last modification time of the file, in milliseconds since the
+ * UNIX epoch, of the file denoted by the current path, following symbolic
+ * links.
+ *
+ * <p>Caveat: many filesystems store file times in seconds, so do not rely on
+ * the millisecond precision.
+ *
+ * @throws IOException if the operation failed for any reason
+ */
+ public long getLastModifiedTime() throws IOException {
+ return fileSystem.getLastModifiedTime(this, true);
+ }
+
+ /**
+ * Returns the last modification time of the file, in milliseconds since the
+ * UNIX epoch, of the file denoted by the current path.
+ *
+ * <p>Caveat: many filesystems store file times in seconds, so do not rely on
+ * the millisecond precision.
+ *
+ * @param followSymlinks if {@link Symlinks#FOLLOW}, and this path denotes a
+ * symbolic link, the link is dereferenced until a file other than a
+ * symbolic link is found
+ * @throws IOException if the modification time for the file could not be
+ * obtained for any reason
+ */
+ public long getLastModifiedTime(Symlinks followSymlinks) throws IOException {
+ return fileSystem.getLastModifiedTime(this, followSymlinks.toBoolean());
+ }
+
+ /**
+ * Sets the modification time of the file denoted by the current path. Follows
+ * symbolic links. If newTime is -1, the current time according to the kernel
+ * is used; this may differ from the JVM's clock.
+ *
+ * <p>Caveat: many filesystems store file times in seconds, so do not rely on
+ * the millisecond precision.
+ *
+ * @param newTime time, in milliseconds since the UNIX epoch, or -1L, meaning
+ * use the kernel's current time
+ * @throws IOException if the modification time for the file could not be set
+ * for any reason
+ */
+ public void setLastModifiedTime(long newTime) throws IOException {
+ fileSystem.setLastModifiedTime(this, newTime);
+ }
+
+ /**
+ * Returns value of the given extended attribute name or null if attribute does not exist or
+ * file system does not support extended attributes. Follows symlinks.
+ */
+ public byte[] getxattr(String name) throws IOException {
+ return fileSystem.getxattr(this, name, true);
+ }
+
+ /**
+ * Returns the type of digest that may be returned by {@link #getFastDigest}, or {@code null}
+ * if the filesystem doesn't support them.
+ */
+ public String getFastDigestFunctionType() {
+ return fileSystem.getFastDigestFunctionType(this);
+ }
+
+ /**
+ * Gets a fast digest for the given path, or {@code null} if there isn't one available. The
+ * digest should be suitable for detecting changes to the file.
+ */
+ public byte[] getFastDigest() throws IOException {
+ return fileSystem.getFastDigest(this);
+ }
+
+ /**
+ * Returns the MD5 digest of the file denoted by the current path, following
+ * symbolic links.
+ *
+ * <p>This method runs in O(n) time where n is the length of the file, but
+ * certain implementations may be much faster than the worst case.
+ *
+ * @return a new 16-byte array containing the file's MD5 digest
+ * @throws IOException if the MD5 digest could not be computed for any reason
+ */
+ public byte[] getMD5Digest() throws IOException {
+ return fileSystem.getMD5Digest(this);
+ }
+
+ /**
+ * Opens the file denoted by this path, following symbolic links, for reading,
+ * and returns an input stream to it.
+ *
+ * @throws IOException if the file was not found or could not be opened for
+ * reading
+ */
+ public InputStream getInputStream() throws IOException {
+ return fileSystem.getInputStream(this);
+ }
+
+ /**
+ * Returns a java.io.File representation of this path.
+ *
+ * <p>Caveat: the result may be useless if this path's getFileSystem() is not
+ * the UNIX filesystem.
+ */
+ public File getPathFile() {
+ return new File(getPathString());
+ }
+
+ /**
+ * Returns true if the file denoted by the current path, following symbolic
+ * links, is writable for the current user.
+ *
+ * @throws FileNotFoundException if the file does not exist, a dangling
+ * symbolic link was encountered, or the file's metadata could not be
+ * read
+ */
+ public boolean isWritable() throws IOException, FileNotFoundException {
+ return fileSystem.isWritable(this);
+ }
+
+ /**
+ * Sets the read permissions of the file denoted by the current path,
+ * following symbolic links. Permissions apply to the current user.
+ *
+ * @param readable if true, the file is set to readable; otherwise the file is
+ * made non-readable
+ * @throws FileNotFoundException if the file does not exist
+ * @throws IOException If the action cannot be taken (ie. permissions)
+ */
+ public void setReadable(boolean readable) throws IOException, FileNotFoundException {
+ fileSystem.setReadable(this, readable);
+ }
+
+ /**
+ * Sets the write permissions of the file denoted by the current path,
+ * following symbolic links. Permissions apply to the current user.
+ *
+ * <p>TODO(bazel-team): (2009) what about owner/group/others?
+ *
+ * @param writable if true, the file is set to writable; otherwise the file is
+ * made non-writable
+ * @throws FileNotFoundException if the file does not exist
+ * @throws IOException If the action cannot be taken (ie. permissions)
+ */
+ public void setWritable(boolean writable) throws IOException, FileNotFoundException {
+ fileSystem.setWritable(this, writable);
+ }
+
+ /**
+ * Returns true iff the file specified by the current path, following symbolic
+ * links, is executable by the current user.
+ *
+ * @throws FileNotFoundException if the file does not exist or a dangling
+ * symbolic link was encountered
+ * @throws IOException if some other I/O error occurred
+ */
+ public boolean isExecutable() throws IOException, FileNotFoundException {
+ return fileSystem.isExecutable(this);
+ }
+
+ /**
+ * Returns true iff the file specified by the current path, following symbolic
+ * links, is readable by the current user.
+ *
+ * @throws FileNotFoundException if the file does not exist or a dangling
+ * symbolic link was encountered
+ * @throws IOException if some other I/O error occurred
+ */
+ public boolean isReadable() throws IOException, FileNotFoundException {
+ return fileSystem.isReadable(this);
+ }
+
+ /**
+ * Sets the execute permission on the file specified by the current path,
+ * following symbolic links. Permissions apply to the current user.
+ *
+ * @throws FileNotFoundException if the file does not exist or a dangling
+ * symbolic link was encountered
+ * @throws IOException if the metadata change failed, for example because of
+ * permissions
+ */
+ public void setExecutable(boolean executable) throws IOException, FileNotFoundException {
+ fileSystem.setExecutable(this, executable);
+ }
+
+ /**
+ * Sets the permissions on the file specified by the current path, following
+ * symbolic links. If permission changes on this path's {@link FileSystem} are
+ * slow (e.g. one syscall per change), this method should aim to be faster
+ * than setting each permission individually. If this path's
+ * {@link FileSystem} does not support group and others permissions, those
+ * bits will be ignored.
+ *
+ * @throws FileNotFoundException if the file does not exist or a dangling
+ * symbolic link was encountered
+ * @throws IOException if the metadata change failed, for example because of
+ * permissions
+ */
+ public void chmod(int mode) throws IOException {
+ fileSystem.chmod(this, mode);
+ }
+
+ /**
+ * Compare Paths of the same file system using their PathFragments.
+ *
+ * <p>Paths from different filesystems will be compared using the identity
+ * hash code of their respective filesystems.
+ */
+ @Override
+ public int compareTo(Path o) {
+ // Fast-path.
+ if (equals(o)) {
+ return 0;
+ }
+
+ // If they are on different file systems, the file system decides the ordering.
+ FileSystem otherFs = o.getFileSystem();
+ if (!fileSystem.equals(otherFs)) {
+ int thisFileSystemHash = System.identityHashCode(fileSystem);
+ int otherFileSystemHash = System.identityHashCode(otherFs);
+ if (thisFileSystemHash < otherFileSystemHash) {
+ return -1;
+ } else if (thisFileSystemHash > otherFileSystemHash) {
+ return 1;
+ } else {
+ // TODO(bazel-team): Add a name to every file system to be used here.
+ return 0;
+ }
+ }
+
+ // Equal file system, but different paths, because of the canonicalization.
+ // We expect to often compare Paths that are very similar, for example for files in the same
+ // directory. This can be done efficiently by going up segment by segment until we get the
+ // identical path (canonicalization again), and then just compare the immediate child segments.
+ // Overall this is much faster than creating PathFragment instances, and comparing those, which
+ // requires us to always go up to the top-level directory and copy all segments into a new
+ // string array.
+ // This was previously showing up as a hotspot in a profile of globbing a large directory.
+ Path a = this, b = o;
+ int maxDepth = Math.min(a.depth, b.depth);
+ while (a.depth > maxDepth) {
+ a = a.getParentDirectory();
+ }
+ while (b.depth > maxDepth) {
+ b = b.getParentDirectory();
+ }
+ // One is the child of the other.
+ if (a.equals(b)) {
+ // If a is the same as this, this.depth must be less than o.depth.
+ return equals(a) ? -1 : 1;
+ }
+ Path previousa, previousb;
+ do {
+ previousa = a;
+ previousb = b;
+ a = a.getParentDirectory();
+ b = b.getParentDirectory();
+ } while (a != b); // This has to happen eventually.
+ return previousa.name.compareTo(previousb.name);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/PathFragment.java b/src/main/java/com/google/devtools/build/lib/vfs/PathFragment.java
new file mode 100644
index 0000000..1ee65a8
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/vfs/PathFragment.java
@@ -0,0 +1,655 @@
+// Copyright 2014 Google Inc. 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.vfs;
+
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.util.OS;
+import com.google.devtools.build.lib.util.StringCanonicalizer;
+
+import java.io.File;
+import java.io.InvalidObjectException;
+import java.io.ObjectInputStream;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Set;
+
+/**
+ * This class represents an immutable UNIX filesystem path, which may be absolute or relative. The
+ * path is maintained as a simple ordered list of path segment strings.
+ *
+ * <p>This class is independent from other VFS classes, especially anything requiring native code.
+ * It is safe to use in places that need simple segmented string path functionality.
+ *
+ * <p>There is some limited support for Windows-style paths. Most importantly, drive identifiers
+ * in front of a path (c:/abc) are supported and such paths are correctly recognized as absolute.
+ * However, Windows-style backslash separators (C:\\foo\\bar) are explicitly not supported, same
+ * with advanced features like \\\\network\\paths and \\\\?\\unc\\paths.
+ */
+@Immutable @ThreadSafe
+public final class PathFragment implements Comparable<PathFragment>, Serializable {
+
+ public static final int INVALID_SEGMENT = -1;
+
+ public static final char SEPARATOR_CHAR = '/';
+
+ public static final char EXTRA_SEPARATOR_CHAR =
+ (OS.getCurrent() == OS.WINDOWS) ? '\\' : '/';
+
+ public static final String ROOT_DIR = "/";
+
+ /** An empty path fragment. */
+ public static final PathFragment EMPTY_FRAGMENT = new PathFragment("");
+
+ public static final Function<String, PathFragment> TO_PATH_FRAGMENT =
+ new Function<String, PathFragment>() {
+ @Override
+ public PathFragment apply(String str) {
+ return new PathFragment(str);
+ }
+ };
+
+ public static final Predicate<PathFragment> IS_ABSOLUTE =
+ new Predicate<PathFragment>() {
+ @Override
+ public boolean apply(PathFragment input) {
+ return input.isAbsolute();
+ }
+ };
+
+ private static final Function<PathFragment, String> TO_SAFE_PATH_STRING =
+ new Function<PathFragment, String>() {
+ @Override
+ public String apply(PathFragment path) {
+ return path.getSafePathString();
+ }
+ };
+
+ // We have 3 word-sized fields (segments, hashCode and path), and 2
+ // byte-sized ones, which fits in 16 bytes. Object sizes are rounded
+ // to 16 bytes. Medium sized builds can easily hold millions of
+ // live PathFragments, so do not add further fields on a whim.
+
+ // The individual path components.
+ private final String[] segments;
+
+ // True both for UNIX-style absolute paths ("/foo") and Windows-style ("C:/foo").
+ private final boolean isAbsolute;
+
+ // Upper case windows drive letter, or '\0' if none. While a volumeName string is more
+ // general, we create a lot of these objects, so space is at a premium.
+ private final char driveLetter;
+
+ // hashCode and path are lazily initialized but semantically immutable.
+ private int hashCode;
+ private String path;
+
+ /**
+ * Construct a PathFragment from a string, which is an absolute or relative UNIX or Windows path.
+ */
+ public PathFragment(String path) {
+ this.driveLetter = getWindowsDriveLetter(path);
+ if (driveLetter != '\0') {
+ path = path.substring(2);
+ // TODO(bazel-team): Decide what to do about non-absolute paths with a volume name, e.g. C:x.
+ }
+ this.isAbsolute = path.length() > 0 && isSeparator(path.charAt(0));
+ this.segments = segment(path, isAbsolute ? 1 : 0);
+ }
+
+ private static boolean isSeparator(char c) {
+ return c == SEPARATOR_CHAR || c == EXTRA_SEPARATOR_CHAR;
+ }
+
+ /**
+ * Construct a PathFragment from a java.io.File, which is an absolute or
+ * relative UNIX path. Does not support Windows-style Files.
+ */
+ public PathFragment(File path) {
+ this(path.getPath());
+ }
+
+ /**
+ * Constructs a PathFragment, taking ownership of segments. Package-private,
+ * because it does not perform a defensive clone of the segments array. Used
+ * here in PathFragment, and by Path.asFragment() and Path.relativeTo().
+ */
+ PathFragment(char driveLetter, boolean isAbsolute, String[] segments) {
+ this.driveLetter = driveLetter;
+ this.isAbsolute = isAbsolute;
+ this.segments = segments;
+ }
+
+ /**
+ * Construct a PathFragment from a sequence of other PathFragments. The new
+ * fragment will be absolute iff the first fragment was absolute.
+ */
+ public PathFragment(PathFragment first, PathFragment second, PathFragment... more) {
+ // TODO(bazel-team): The handling of absolute path fragments in this constructor is unexpected.
+ this.segments = new String[sumLengths(first, second, more)];
+ int offset = 0;
+ offset += addSegments(offset, first);
+ offset += addSegments(offset, second);
+ for (PathFragment fragment : more) {
+ offset += addSegments(offset, fragment);
+ }
+ this.isAbsolute = first.isAbsolute;
+ this.driveLetter = first.driveLetter;
+ }
+
+ private int addSegments(int offset, PathFragment fragment) {
+ int count = fragment.segmentCount();
+ System.arraycopy(fragment.segments, 0, this.segments, offset, count);
+ return count;
+ }
+
+ private static int sumLengths(PathFragment first, PathFragment second, PathFragment[] more) {
+ int total = first.segmentCount() + second.segmentCount();
+ for (PathFragment fragment : more) {
+ total += fragment.segmentCount();
+ }
+ return total;
+ }
+
+ /**
+ * Segments the string passed in as argument and returns an array of strings.
+ * The split is performed along occurrences of (sequences of) the slash
+ * character.
+ *
+ * @param toSegment the string to segment
+ * @param offset how many characters from the start of the string to ignore.
+ */
+ private static String[] segment(String toSegment, int offset) {
+ char[] chars = toSegment.toCharArray();
+ int length = chars.length;
+
+ // Handle "/" and "" quickly.
+ if (length == offset) {
+ return new String[0];
+ }
+
+ // We make two passes through the array of characters: count & alloc,
+ // because simply using ArrayList was a bottleneck showing up during profiling.
+ int seg = 0;
+ int start = offset;
+ for (int i = offset; i < length; i++) {
+ if (isSeparator(chars[i])) {
+ if (i > start) { // to skip repeated separators
+ seg++;
+ }
+ start = i + 1;
+ }
+ }
+ if (start < length) {
+ seg++;
+ }
+ String[] result = new String[seg];
+ seg = 0;
+ start = offset;
+ for (int i = offset; i < length; i++) {
+ if (isSeparator(chars[i])) {
+ if (i > start) { // to skip repeated separators
+ // Make a copy of the String here to allow the interning to save memory. String.substring
+ // does not make a copy, but refers to the original char array, preventing garbage
+ // collection of the parts that are unnecessary.
+ result[seg] = StringCanonicalizer.intern(new String(chars, start, i - start));
+ seg++;
+ }
+ start = i + 1;
+ }
+ }
+ if (start < length) {
+ result[seg] = StringCanonicalizer.intern(new String(chars, start, length - start));
+ seg++;
+ }
+ return result;
+ }
+
+ private Object writeReplace() {
+ return new PathFragmentSerializationProxy(toString());
+ }
+
+ private void readObject(ObjectInputStream stream) throws InvalidObjectException {
+ throw new InvalidObjectException("Serialization is allowed only by proxy");
+ }
+
+ /**
+ * Returns the path string using '/' as the name-separator character. Returns "" if the path
+ * is both relative and empty.
+ */
+ public String getPathString() {
+ // Double-checked locking works, even without volatile, because path is a String, according to:
+ // http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
+ if (path == null) {
+ synchronized (this) {
+ if (path == null) {
+ path = StringCanonicalizer.intern(joinSegments(SEPARATOR_CHAR));
+ }
+ }
+ }
+ return path;
+ }
+
+ /**
+ * Returns "." if the path fragment is both relative and empty, or {@link
+ * #getPathString} otherwise.
+ */
+ // TODO(bazel-team): Change getPathString to do this - this behavior makes more sense.
+ public String getSafePathString() {
+ return (!isAbsolute && (segmentCount() == 0)) ? "." : getPathString();
+ }
+
+ /**
+ * Returns a sequence consisting of the {@link #getSafePathString()} return of each item in
+ * {@code fragments}.
+ */
+ public static Iterable<String> safePathStrings(Iterable<PathFragment> fragments) {
+ return Iterables.transform(fragments, TO_SAFE_PATH_STRING);
+ }
+
+ private String joinSegments(char separatorChar) {
+ if (segments.length == 0 && isAbsolute) {
+ return windowsVolume() + ROOT_DIR;
+ }
+
+ // Profile driven optimization:
+ // Preallocate a size determined by the number of segments, so that
+ // we do not have to expand the capacity of the StringBuilder.
+ // Heuristically, this estimate is right for about 99% of the time.
+ int estimateSize =
+ ((driveLetter != '\0') ? 2 : 0)
+ + ((segments.length == 0) ? 0 : (segments.length + 1) * 20);
+ StringBuilder result = new StringBuilder(estimateSize);
+ result.append(windowsVolume());
+ boolean initialSegment = true;
+ for (String segment : segments) {
+ if (!initialSegment || isAbsolute) {
+ result.append(separatorChar);
+ }
+ initialSegment = false;
+ result.append(segment);
+ }
+ return result.toString();
+ }
+
+ /**
+ * Return true iff none of the segments are either "." or "..".
+ */
+ public boolean isNormalized() {
+ for (String segment : segments) {
+ if (segment.equals(".") || segment.equals("..")) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Normalizes the path fragment: removes "." and ".." segments if possible
+ * (if there are too many ".." segments, the resulting PathFragment will still
+ * start with "..").
+ */
+ public PathFragment normalize() {
+ String[] scratchSegments = new String[segments.length];
+ int segmentCount = 0;
+
+ for (String segment : segments) {
+ if (segment.equals(".")) {
+ // Just discard it
+ } else if (segment.equals("..")) {
+ if (segmentCount > 0 && !scratchSegments[segmentCount - 1].equals("..")) {
+ // Remove the last segment, if there is one and it is not "..". This
+ // means that the resulting PathFragment can still contain ".."
+ // segments at the beginning.
+ segmentCount--;
+ } else {
+ scratchSegments[segmentCount++] = segment;
+ }
+ } else {
+ scratchSegments[segmentCount++] = segment;
+ }
+ }
+
+ if (segmentCount == segments.length) {
+ // Optimization, no new PathFragment needs to be created.
+ return this;
+ }
+
+ return new PathFragment(driveLetter, isAbsolute,
+ subarray(scratchSegments, 0, segmentCount));
+ }
+
+ /**
+ * Returns the path formed by appending the relative or absolute path fragment
+ * {@code suffix} to this path.
+ *
+ * <p>If suffix is absolute, the current path will be ignored; otherwise, they
+ * will be concatenated. This is a purely syntactic operation, with no path
+ * normalization or I/O performed.
+ */
+ public PathFragment getRelative(PathFragment otherFragment) {
+ return otherFragment.isAbsolute()
+ ? otherFragment
+ : new PathFragment(this, otherFragment);
+ }
+
+ /**
+ * Returns the path formed by appending the relative or absolute string
+ * {@code path} to this path.
+ *
+ * <p>If the given path string is absolute, the current path will be ignored;
+ * otherwise, they will be concatenated. This is a purely syntactic operation,
+ * with no path normalization or I/O performed.
+ */
+ public PathFragment getRelative(String path) {
+ return getRelative(new PathFragment(path));
+ }
+
+ /**
+ * Returns the path formed by appending the single non-special segment
+ * "baseName" to this path.
+ *
+ * <p>You should almost always use {@link #getRelative} instead, which has
+ * the same performance characteristics if the given name is a valid base
+ * name, and which also works for '.', '..', and strings containing '/'.
+ *
+ * @throws IllegalArgumentException if {@code baseName} is not a valid base
+ * name according to {@link FileSystemUtils#checkBaseName}
+ */
+ public PathFragment getChild(String baseName) {
+ FileSystemUtils.checkBaseName(baseName);
+ baseName = StringCanonicalizer.intern(baseName);
+ String[] newSegments = new String[segments.length + 1];
+ System.arraycopy(segments, 0, newSegments, 0, segments.length);
+ newSegments[newSegments.length - 1] = baseName;
+ return new PathFragment(driveLetter, isAbsolute, newSegments);
+ }
+
+ /**
+ * Returns the last segment of this path, or "" for the empty fragment.
+ */
+ public String getBaseName() {
+ return (segments.length == 0) ? "" : segments[segments.length - 1];
+ }
+
+ /**
+ * Returns a relative path fragment to this path, relative to
+ * {@code ancestorDirectory}.
+ * <p>
+ * <code>x.relativeTo(z) == y</code> implies
+ * <code>z.getRelative(y) == x</code>.
+ * <p>
+ * For example, <code>"foo/bar/wiz".relativeTo("foo")</code>
+ * returns <code>"bar/wiz"</code>.
+ */
+ public PathFragment relativeTo(PathFragment ancestorDirectory) {
+ String[] ancestorSegments = ancestorDirectory.segments();
+ int ancestorLength = ancestorSegments.length;
+
+ if (isAbsolute != ancestorDirectory.isAbsolute()
+ || segments.length < ancestorLength) {
+ throw new IllegalArgumentException("PathFragment " + this
+ + " is not beneath " + ancestorDirectory);
+ }
+
+ for (int index = 0; index < ancestorLength; index++) {
+ if (!segments[index].equals(ancestorSegments[index])) {
+ throw new IllegalArgumentException("PathFragment " + this
+ + " is not beneath " + ancestorDirectory);
+ }
+ }
+
+ int length = segments.length - ancestorLength;
+ String[] resultSegments = subarray(segments, ancestorLength, length);
+ return new PathFragment('\0', false, resultSegments);
+ }
+
+ /**
+ * Returns a relative path fragment to this path, relative to {@code path}.
+ */
+ public PathFragment relativeTo(String path) {
+ return relativeTo(new PathFragment(path));
+ }
+
+ /**
+ * Returns a new PathFragment formed by appending {@code newName} to the
+ * parent directory. Null is returned iff this method is called on a
+ * PathFragment with zero segments. If {@code newName} designates an absolute path,
+ * the value of {@code this} will be ignored and a PathFragment corresponding to
+ * {@code newName} will be returned. This behavior is consistent with the behavior of
+ * {@link #getRelative(String)}.
+ */
+ public PathFragment replaceName(String newName) {
+ return segments.length == 0 ? null : getParentDirectory().getRelative(newName);
+ }
+
+ /**
+ * Returns a path representing the parent directory of this path,
+ * or null iff this Path represents the root of the filesystem.
+ *
+ * <p>Note: This method DOES NOT normalize ".." and "." path segments.
+ */
+ public PathFragment getParentDirectory() {
+ return segments.length == 0 ? null : subFragment(0, segments.length - 1);
+ }
+
+ /**
+ * Returns true iff {@code prefix}, considered as a list of path segments, is
+ * a prefix of {@code this}, and that they are both relative or both
+ * absolute.
+ *
+ * This is a reflexive, transitive, anti-symmetric relation (i.e. a partial
+ * order)
+ */
+ public boolean startsWith(PathFragment prefix) {
+ if (this.isAbsolute != prefix.isAbsolute ||
+ this.segments.length < prefix.segments.length ||
+ this.driveLetter != prefix.driveLetter) {
+ return false;
+ }
+ for (int i = 0, len = prefix.segments.length; i < len; i++) {
+ if (!this.segments[i].equals(prefix.segments[i])) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Returns true iff {@code suffix}, considered as a list of path segments, is
+ * relative and a suffix of {@code this}, or both are absolute and equal.
+ *
+ * This is a reflexive, transitive, anti-symmetric relation (i.e. a partial
+ * order)
+ */
+ public boolean endsWith(PathFragment suffix) {
+ if ((suffix.isAbsolute && !suffix.equals(this)) ||
+ this.segments.length < suffix.segments.length) {
+ return false;
+ }
+ int offset = this.segments.length - suffix.segments.length;
+ for (int i = 0; i < suffix.segments.length; i++) {
+ if (!this.segments[offset + i].equals(suffix.segments[i])) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static String[] subarray(String[] array, int start, int length) {
+ String[] subarray = new String[length];
+ System.arraycopy(array, start, subarray, 0, length);
+ return subarray;
+ }
+
+ /**
+ * Returns a new path fragment that is a sub fragment of this one.
+ * The sub fragment begins at the specified <code>beginIndex</code> segment
+ * and ends at the segment at index <code>endIndex - 1</code>. Thus the number
+ * of segments in the new PathFragment is <code>endIndex - beginIndex</code>.
+ *
+ * @param beginIndex the beginning index, inclusive.
+ * @param endIndex the ending index, exclusive.
+ * @return the specified sub fragment, never null.
+ * @exception IndexOutOfBoundsException if the
+ * <code>beginIndex</code> is negative, or
+ * <code>endIndex</code> is larger than the length of
+ * this <code>String</code> object, or
+ * <code>beginIndex</code> is larger than
+ * <code>endIndex</code>.
+ */
+ public PathFragment subFragment(int beginIndex, int endIndex) {
+ int count = segments.length;
+ if ((beginIndex < 0) || (beginIndex > endIndex) || (endIndex > count)) {
+ throw new IndexOutOfBoundsException(String.format("path: %s, beginIndex: %d endIndex: %d",
+ toString(), beginIndex, endIndex));
+ }
+ boolean isAbsolute = (beginIndex == 0) && this.isAbsolute;
+ return ((beginIndex == 0) && (endIndex == count)) ? this :
+ new PathFragment(driveLetter, isAbsolute,
+ subarray(segments, beginIndex, endIndex - beginIndex));
+ }
+
+ /**
+ * Returns true iff the path represented by this object is absolute.
+ */
+ public boolean isAbsolute() {
+ return isAbsolute;
+ }
+
+ /**
+ * Returns the segments of this path fragment. This array should not be
+ * modified.
+ */
+ String[] segments() {
+ return segments;
+ }
+
+ public String windowsVolume() {
+ if (OS.getCurrent() != OS.WINDOWS) {
+ return "";
+ }
+ return (driveLetter != '\0') ? driveLetter + ":" : "";
+ }
+
+ /**
+ * Returns the number of segments in this path.
+ */
+ public int segmentCount() {
+ return segments.length;
+ }
+
+ /**
+ * Returns the specified segment of this path; index must be positive and
+ * less than numSegments().
+ */
+ public String getSegment(int index) {
+ return segments[index];
+ }
+
+ /**
+ * Returns the index of the first segment which equals one of the input values
+ * or {@link PathFragment#INVALID_SEGMENT} if none of the segments match.
+ */
+ public int getFirstSegment(Set<String> values) {
+ for (int i = 0; i < segments.length; i++) {
+ if (values.contains(segments[i])) {
+ return i;
+ }
+ }
+ return INVALID_SEGMENT;
+ }
+
+ /**
+ * Returns true iff this path contains uplevel references "..".
+ */
+ public boolean containsUplevelReferences() {
+ for (String segment : segments) {
+ if (segment.equals("..")) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Given a path, returns the Windows drive letter ('X'), or an null character if no volume
+ * name was specified.
+ */
+ static char getWindowsDriveLetter(String path) {
+ if (OS.getCurrent() == OS.WINDOWS
+ && path.length() >= 2 && path.charAt(1) == ':' && Character.isLetter(path.charAt(0))) {
+ return Character.toUpperCase(path.charAt(0));
+ }
+ return '\0';
+ }
+
+ @Override
+ public int hashCode() {
+ int h = hashCode;
+ if (h == 0) {
+ h = isAbsolute ? 1 : 0;
+ for (String segment : segments) {
+ h = h * 31 + segment.hashCode();
+ }
+ hashCode = h;
+ }
+ return h;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof PathFragment)) {
+ return false;
+ }
+ PathFragment otherPath = (PathFragment) other;
+ return isAbsolute == otherPath.isAbsolute &&
+ Arrays.equals(otherPath.segments, segments);
+ }
+
+ /**
+ * Compares two PathFragments using the lexicographical order.
+ */
+ @Override
+ public int compareTo(PathFragment p2) {
+ if (isAbsolute != p2.isAbsolute) {
+ return isAbsolute ? -1 : 1;
+ }
+ PathFragment p1 = this;
+ String[] segments1 = p1.segments;
+ String[] segments2 = p2.segments;
+ int len1 = segments1.length;
+ int len2 = segments2.length;
+ int n = Math.min(len1, len2);
+ for (int i = 0; i < n; i++) {
+ String segment1 = segments1[i];
+ String segment2 = segments2[i];
+ if (!segment1.equals(segment2)) {
+ return segment1.compareTo(segment2);
+ }
+ }
+ return len1 - len2;
+ }
+
+ @Override
+ public String toString() {
+ return getPathString();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/PathFragmentSerializationProxy.java b/src/main/java/com/google/devtools/build/lib/vfs/PathFragmentSerializationProxy.java
new file mode 100644
index 0000000..6e1b04d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/vfs/PathFragmentSerializationProxy.java
@@ -0,0 +1,50 @@
+// Copyright 2014 Google Inc. 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.vfs;
+
+import java.io.Externalizable;
+import java.io.IOException;
+import java.io.ObjectOutput;
+
+
+/**
+ * A helper proxy for serializing immutable {@link PathFragment} objects.
+ */
+public final class PathFragmentSerializationProxy implements Externalizable {
+ private String pathFragmentString;
+
+ public PathFragmentSerializationProxy(String pathFragmentString) {
+ this.pathFragmentString = pathFragmentString;
+ }
+
+ // For deserialization machinery.
+ public PathFragmentSerializationProxy() {
+ }
+
+ @Override
+ public void writeExternal(ObjectOutput out) throws IOException {
+ // Manual serialization gives us about a 30% reduction in size.
+ out.writeUTF(pathFragmentString);
+ }
+
+ @Override
+ public void readExternal(java.io.ObjectInput in) throws IOException {
+ this.pathFragmentString = in.readUTF();
+ }
+
+ private Object readResolve() {
+ return new PathFragment(pathFragmentString);
+ }
+}
+
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/ReadonlyFileSystem.java b/src/main/java/com/google/devtools/build/lib/vfs/ReadonlyFileSystem.java
new file mode 100644
index 0000000..fc668db
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/vfs/ReadonlyFileSystem.java
@@ -0,0 +1,103 @@
+// Copyright 2014 Google Inc. 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.vfs;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * An abstract partial implementation of FileSystem for read-only
+ * implementations.
+ *
+ * <p>Any ReadonlyFileSystem does not support the following:
+ * <ul>
+ * <li>{@link #createDirectory(Path)}</li>
+ * <li>{@link #createSymbolicLink(Path, PathFragment)}</li>
+ * <li>{@link #delete(Path)}</li>
+ * <li>{@link #getOutputStream(Path)}</li>
+ * <li>{@link #renameTo(Path, Path)}</li>
+ * <li>{@link #setExecutable(Path, boolean)}</li>
+ * <li>{@link #setLastModifiedTime(Path, long)}</li>
+ * <li>{@link #setWritable(Path, boolean)}</li>
+ * </ul>
+ * The above calls will always result in an {@link IOException}.
+ */
+public abstract class ReadonlyFileSystem extends FileSystem {
+
+ protected ReadonlyFileSystem() {
+ }
+
+ protected IOException modificationException() {
+ String longname = this.getClass().getName();
+ String shortname = longname.substring(longname.lastIndexOf(".") + 1);
+ return new IOException(
+ shortname + " does not support mutating operations");
+ }
+
+ @Override
+ protected OutputStream getOutputStream(Path path, boolean append) throws IOException {
+ throw modificationException();
+ }
+
+ @Override
+ protected void setReadable(Path path, boolean readable) throws IOException {
+ throw modificationException();
+ }
+
+ @Override
+ protected void setWritable(Path path, boolean writable) throws IOException {
+ throw modificationException();
+ }
+
+ @Override
+ protected void setExecutable(Path path, boolean executable) {
+ throw new UnsupportedOperationException("setExecutable");
+ }
+
+ @Override
+ public boolean supportsModifications() {
+ return false;
+ }
+
+ @Override
+ public boolean supportsSymbolicLinks() {
+ return false;
+ }
+
+ @Override
+ protected boolean createDirectory(Path path) throws IOException {
+ throw modificationException();
+ }
+
+ @Override
+ protected void createSymbolicLink(Path linkPath, PathFragment targetFragment) throws IOException {
+ throw modificationException();
+ }
+
+ @Override
+ protected void renameTo(Path sourcePath, Path targetPath) throws IOException {
+ throw modificationException();
+ }
+
+ @Override
+ protected boolean delete(Path path) throws IOException {
+ throw modificationException();
+ }
+
+ @Override
+ protected void setLastModifiedTime(Path path, long newTime) throws IOException {
+ throw modificationException();
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/RootedPath.java b/src/main/java/com/google/devtools/build/lib/vfs/RootedPath.java
new file mode 100644
index 0000000..c753aa6
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/vfs/RootedPath.java
@@ -0,0 +1,116 @@
+// Copyright 2014 Google Inc. 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.vfs;
+
+import com.google.common.base.Preconditions;
+
+import java.io.Serializable;
+import java.util.Objects;
+
+/**
+ * A {@link PathFragment} relative to a root, which is an absolute {@link Path}. Typically the root
+ * will be a package path entry.
+ *
+ * Two {@link RootedPath}s are considered equal iff they have equal roots and equal relative paths.
+ *
+ * TODO(bazel-team): refactor Artifact to use this instead of Root.
+ * TODO(bazel-team): use an opaque root representation so as to not expose the absolute path to
+ * clients via #asPath or #getRoot.
+ */
+public class RootedPath implements Serializable {
+
+ private final Path root;
+ private final PathFragment relativePath;
+ private final Path path;
+
+ /**
+ * Constructs a {@link RootedPath} from an absolute root path and a non-absolute relative path.
+ */
+ private RootedPath(Path root, PathFragment relativePath) {
+ Preconditions.checkState(!relativePath.isAbsolute(), "relativePath: %s root: %s", relativePath,
+ root);
+ this.root = root;
+ this.relativePath = relativePath.normalize();
+ this.path = root.getRelative(this.relativePath);
+ }
+
+ /**
+ * Returns a rooted path representing {@code relativePath} relative to {@code root}.
+ */
+ public static RootedPath toRootedPath(Path root, PathFragment relativePath) {
+ return new RootedPath(root, relativePath);
+ }
+
+ /**
+ * Returns a rooted path representing {@code path} under the root {@code root}.
+ */
+ public static RootedPath toRootedPath(Path root, Path path) {
+ Preconditions.checkState(path.startsWith(root), "path: %s root: %s", path, root);
+ return new RootedPath(root, path.relativeTo(root));
+ }
+
+ /**
+ * Returns a rooted path representing {@code path} under one of the package roots, or under the
+ * filesystem root if it's not under any package root.
+ */
+ public static RootedPath toRootedPathMaybeUnderRoot(Path path, Iterable<Path> packagePathRoots) {
+ for (Path root : packagePathRoots) {
+ if (path.startsWith(root)) {
+ return toRootedPath(root, path);
+ }
+ }
+ return toRootedPath(path.getFileSystem().getRootDirectory(), path);
+ }
+
+ public Path asPath() {
+ // Ideally, this helper method would not be needed. But Skyframe's FileFunction and
+ // DirectoryListingFunction need to do filesystem operations on the absolute path and
+ // Path#getRelative(relPath) is O(relPath.segmentCount()). Therefore we precompute the absolute
+ // path represented by this relative path.
+ return path;
+ }
+
+ public Path getRoot() {
+ return root;
+ }
+
+ /**
+ * Returns the (normalized) path relative to {@code #getRoot}.
+ */
+ public PathFragment getRelativePath() {
+ return relativePath;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof RootedPath)) {
+ return false;
+ }
+ RootedPath other = (RootedPath) obj;
+ return Objects.equals(root, other.root) && Objects.equals(relativePath, other.relativePath);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(root, relativePath);
+ }
+
+ @Override
+ public String toString() {
+ return "[" + root.toString() + "]/[" + relativePath.toString() + "]";
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/ScopeEscapableFileSystem.java b/src/main/java/com/google/devtools/build/lib/vfs/ScopeEscapableFileSystem.java
new file mode 100644
index 0000000..429de18
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/vfs/ScopeEscapableFileSystem.java
@@ -0,0 +1,143 @@
+// Copyright 2014 Google Inc. 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.vfs;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadHostile;
+
+import java.io.IOException;
+
+/**
+ * A file system that's capable of identifying paths residing outside its scope
+ * and using a delegator (such as {@link UnionFileSystem}) to re-route them
+ * to appropriate alternative file systems.
+ *
+ * <p>This is most useful for symlinks, which may ostensibly fall beneath some
+ * file system but resolve to paths outside that file system.
+ *
+ * <p>Note that we don't protect against cross-filesystem circular references.
+ * Therefore, care should be taken not to mix two scopable file systems that
+ * can reference each other. This theoretical safety cost is balanced by
+ * decreased code complexity requirements in implementations.
+ */
+public abstract class ScopeEscapableFileSystem extends FileSystem {
+
+ private FileSystem delegator;
+ protected final PathFragment scopeRoot;
+ private boolean enableScopeChecking = true; // Used for testing.
+
+ /**
+ * Instantiates a new ScopeEscapableFileSystem.
+ *
+ * @param scopeRoot the root path for the file system's scope. Any path
+ * that isn't beneath this one is considered out of scope according
+ * to {@link #inScope}. If null, scope checking is disabled. Note
+ * this is not the same thing as {@link FileSystem#rootPath}, which
+ * generally resolves to "/".
+ */
+ protected ScopeEscapableFileSystem(PathFragment scopeRoot) {
+ this.scopeRoot = scopeRoot;
+ }
+
+ @VisibleForTesting
+ void enableScopeChecking(boolean enable) {
+ this.enableScopeChecking = enable;
+ }
+
+ /**
+ * Sets the delegator used to resolve paths that fall outside this file
+ * system's scope.
+ *
+ * <p>This method is not thread safe. It's intended to be called during
+ * instance initialization, not during active usage. The only reason this
+ * isn't set as immutable state within the constructor is that the delegator
+ * may need a reference to this instance for its own constructor.
+ */
+ @ThreadHostile
+ public void setDelegator(FileSystem delegator) {
+ this.delegator = delegator;
+ }
+
+ /**
+ * Uses the delegator to convert a path fragment to a path that's bound
+ * to the file system that manages that path.
+ */
+ protected Path getDelegatedPath(PathFragment path) {
+ Preconditions.checkState(delegator != null);
+ return delegator.getPath(path);
+ }
+
+ /**
+ * Proxy for {@link FileSystem#resolveOneLink} that sends the input path
+ * through the delegator.
+ */
+ protected PathFragment resolveOneLinkWithDelegator(final PathFragment path) throws IOException {
+ Preconditions.checkState(delegator != null);
+ return delegator.resolveOneLink(getDelegatedPath(path));
+ }
+
+ /**
+ * Proxy for {@link FileSystem#stat} that sends the input path through
+ * the delegator.
+ */
+ protected FileStatus statWithDelegator(final PathFragment path, final boolean followSymlinks)
+ throws IOException {
+ Preconditions.checkState(delegator != null);
+ return delegator.stat(getDelegatedPath(path), followSymlinks);
+ }
+
+ /**
+ * Returns true if the given path is within this file system's scope, false
+ * otherwise.
+ *
+ * @param parentDepth the number of segments in the path's parent directory
+ * (only meaningful for paths that begin with ".."). The parent directory
+ * itself is assumed to be in scope.
+ * @param normalizedPath input path, expected to be normalized such that all
+ * ".." and "." segments are removed (with the exception of a possible
+ * prefix sequence of contiguous ".." segments)
+ */
+ protected boolean inScope(int parentDepth, PathFragment normalizedPath) {
+ if (scopeRoot == null || !enableScopeChecking) {
+ return true;
+ } else if (normalizedPath.isAbsolute()) {
+ return normalizedPath.startsWith(scopeRoot);
+ } else {
+ // Efficiency note: we're not accounting for "/scope/root/../root" paths here, i.e. paths
+ // that appear to go out of scope but ultimately stay within scope. This may result in
+ // unnecessary re-delegation back into the same FS. we're choosing to forgo that
+ // optimization under the assumption that such scenarios are rare and unimportant to
+ // overall performance. We can always enhance this if needed.
+ return parentDepth - leadingParentReferences(normalizedPath) >= scopeRoot.segmentCount();
+ }
+ }
+
+ /**
+ * Given a path that's normalized (no ".." or "." segments), except for a possible
+ * prefix sequence of contiguous ".." segments, returns the size of that prefix
+ * sequence.
+ *
+ * <p>Example allowed inputs: "/absolute/path", "relative/path", "../../relative/path".
+ * Example disallowed inputs: "/absolute/path/../path2", "relative/../path", "../relative/../p".
+ */
+ protected int leadingParentReferences(PathFragment normalizedPath) {
+ int leadingParentReferences = 0;
+ for (int i = 0; i < normalizedPath.segmentCount() &&
+ normalizedPath.getSegment(i).equals(".."); i++) {
+ leadingParentReferences++;
+ }
+ return leadingParentReferences;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/Symlinks.java b/src/main/java/com/google/devtools/build/lib/vfs/Symlinks.java
new file mode 100644
index 0000000..ceb353a
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/vfs/Symlinks.java
@@ -0,0 +1,30 @@
+// Copyright 2014 Google Inc. 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.vfs;
+
+/**
+ * An enumeration for selecting between {@code stat}- and {@code lstat}-like
+ * behavior in various {@link Path} operations.
+ */
+public enum Symlinks {
+
+ /** Follow symbolic links; stat(2)-like behaviour. */
+ FOLLOW,
+
+ /** Do not follow symbolic links; lstat(2)-like behaviour. */
+ NOFOLLOW;
+
+ boolean toBoolean() { return this == FOLLOW; }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/UnionFileSystem.java b/src/main/java/com/google/devtools/build/lib/vfs/UnionFileSystem.java
new file mode 100644
index 0000000..b349b53
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/vfs/UnionFileSystem.java
@@ -0,0 +1,419 @@
+// Copyright 2014 Google Inc. 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.vfs;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Lists;
+import com.google.devtools.build.lib.concurrent.ThreadSafety;
+import com.google.devtools.build.lib.util.StringTrie;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Collection;
+import java.util.Map;
+
+import javax.annotation.Nullable;
+
+/**
+ * Presents a unified view of multiple virtual {@link FileSystem} instances, to which requests are
+ * delegated based on a {@link PathFragment} prefix mapping.
+ * If multiple prefixes apply to a given path, the *longest* (i.e. most specific) match is used.
+ * The order in which the delegates are specified does not influence the mapping.
+ *
+ * <p>Paths are preserved absolutely, contrary to how "mount" works, e.g.:
+ * /foo/bar maps to /foo/bar on the delegate, even if it is mounted at /foo.
+ *
+ * <p>For example:
+ * "/in" maps to InFileSystem, "/" maps to OtherFileSystem.
+ * Reading from "/in/base/BUILD" through the UnionFileSystem will delegate the read operation to
+ * InFileSystem, which will read "/in/base/BUILD" relative to its root.
+ * ("mount" behavior would remap it to "/base/BUILD" on the delegate).
+ *
+ * <p>Intra-filesystem symbolic links are resolved to their ultimate targets.
+ * Cross-filesystem links are not currently supported.
+ */
+@ThreadSafety.ThreadSafe
+public class UnionFileSystem extends FileSystem {
+
+ // Prefix trie index, allowing children to easily inherit prefix mappings
+ // of their parents.
+ // This does not currently handle unicode filenames.
+ private StringTrie<FileSystem> pathDelegate;
+
+ // True iff the filesystem can be modified. If false, mutating operations
+ // will throw UnsupportedOperationExceptions.
+ private final boolean readOnly;
+
+ /**
+ * Creates a new modifiable UnionFileSystem with prefix mappings
+ * specified by a map.
+ *
+ * @param prefixMapping map of path prefixes to {@link FileSystem}s
+ */
+ public UnionFileSystem(Map<PathFragment, FileSystem> prefixMapping,
+ FileSystem rootFileSystem) {
+ this(prefixMapping, rootFileSystem, /* readOnly */ false);
+ }
+
+ /**
+ * Creates a new modifiable or read-only UnionFileSystem with prefix mappings
+ * specified by a map.
+ *
+ * @param prefixMapping map of path prefixes to delegate {@link FileSystem}s
+ * @param rootFileSystem root for default requests; i.e. mapping of "/"
+ * @param readOnly if true, mutating operations will throw
+ */
+ public UnionFileSystem(Map<PathFragment, FileSystem> prefixMapping,
+ FileSystem rootFileSystem, boolean readOnly) {
+ super();
+ Preconditions.checkNotNull(prefixMapping);
+ Preconditions.checkNotNull(rootFileSystem);
+ Preconditions.checkArgument(rootFileSystem != this, "Circular root filesystem.");
+ Preconditions.checkArgument(
+ !prefixMapping.containsKey(PathFragment.EMPTY_FRAGMENT),
+ "Attempted to specify an explicit root prefix mapping; " +
+ "please use the rootFileSystem argument instead.");
+
+ this.readOnly = readOnly;
+ this.pathDelegate = new StringTrie<FileSystem>();
+
+ for (Map.Entry<PathFragment, FileSystem> prefix : prefixMapping.entrySet()) {
+ FileSystem delegate = prefix.getValue();
+ PathFragment prefixPath = prefix.getKey();
+
+ // Extra slash prevents within-directory mappings, which Path can't handle.
+ String path = prefixPath.getPathString();
+ pathDelegate.put(path, delegate);
+ }
+ pathDelegate.put(PathFragment.EMPTY_FRAGMENT.getPathString(), rootFileSystem);
+ }
+
+ /**
+ * Retrieves the filesystem delegate of a path mapping.
+ * Does not follow symlinks (but you can call on a path preprocessed with
+ * {@link #resolveSymbolicLinks} to support this use case).
+ *
+ * @param path the {@link Path} to map to a filesystem
+ * @throws IllegalArgumentException if no delegate exists for the path
+ */
+ protected FileSystem getDelegate(Path path) {
+ Preconditions.checkNotNull(path);
+
+ String pathString = path.getPathString();
+ FileSystem immediateDelegate = pathDelegate.get(pathString);
+
+ // Should never actually happen if the root delegate is present.
+ Preconditions.checkArgument(immediateDelegate != null, "No delegate filesystem exists for %s",
+ pathString);
+ return immediateDelegate;
+ }
+
+ // Associates the path with the root of the given delegate filesystem.
+ // Necessary to avoid null pointer problems inside of the delegates.
+ protected Path adjustPath(Path path, FileSystem delegate) {
+ return delegate.getPath(path.asFragment());
+ }
+
+ /**
+ * Follow a symbolic link once using the appropriate delegate filesystem, also
+ * resolving parent directory symlinks.
+ *
+ * @param path {@link Path} to the symbolic link
+ */
+ @Override
+ protected PathFragment readSymbolicLink(Path path) throws IOException {
+ Preconditions.checkNotNull(path);
+ FileSystem delegate = getDelegate(path);
+ return delegate.readSymbolicLink(adjustPath(path, delegate));
+ }
+
+ @Override
+ protected PathFragment resolveOneLink(Path path) throws IOException {
+ Preconditions.checkNotNull(path);
+ FileSystem delegate = getDelegate(path);
+ return delegate.resolveOneLink(adjustPath(path, delegate));
+ }
+
+ private void checkModifiable() {
+ if (!supportsModifications()) {
+ throw new UnsupportedOperationException(
+ "Modifications to this " + getClass().getSimpleName() + " are disabled.");
+ }
+ }
+
+ @Override
+ public boolean supportsModifications() {
+ return !readOnly;
+ }
+
+ @Override
+ public boolean supportsSymbolicLinks() {
+ return true;
+ }
+
+ @Override
+ public String getFileSystemType(Path path) {
+ FileSystem delegate = getDelegate(path);
+ return delegate.getFileSystemType(path);
+ }
+
+ @Override
+ protected byte[] getMD5Digest(Path path) throws IOException {
+ FileSystem delegate = getDelegate(path);
+ return delegate.getMD5Digest(adjustPath(path, delegate));
+ }
+
+ @Override
+ protected boolean createDirectory(Path path) throws IOException {
+ checkModifiable();
+ // When creating the exact directory that is mapped,
+ // create it on both the parent's delegate and the path's delegate.
+ // This is necessary both for the parent to see the directory and for the
+ // delegate to use it.
+ // This is present to address this problematic case:
+ // / -> RootFs
+ // /foo -> FooFs
+ // mkdir /foo
+ // ls / ("foo" would be missing if not created on the parent)
+ // ls /foo (would fail if foo weren't also present on the child)
+ FileSystem delegate = getDelegate(path);
+ Path parent = path.getParentDirectory();
+ if (parent != null) {
+ FileSystem parentDelegate = getDelegate(parent);
+ if (parentDelegate != delegate) {
+ // There's a possibility it already exists on the parent, so don't die
+ // if the directory can't be created there.
+ parentDelegate.createDirectory(adjustPath(path, parentDelegate));
+ }
+ }
+ return delegate.createDirectory(adjustPath(path, delegate));
+ }
+
+ @Override
+ protected long getFileSize(Path path, boolean followSymlinks) throws IOException {
+ FileSystem delegate = getDelegate(path);
+ return delegate.getFileSize(adjustPath(path, delegate), followSymlinks);
+ }
+
+ @Override
+ protected boolean delete(Path path) throws IOException {
+ checkModifiable();
+ FileSystem delegate = getDelegate(path);
+ return delegate.delete(adjustPath(path, delegate));
+ }
+
+ @Override
+ protected long getLastModifiedTime(Path path, boolean followSymlinks) throws IOException {
+ FileSystem delegate = getDelegate(path);
+ return delegate.getLastModifiedTime(adjustPath(path, delegate), followSymlinks);
+ }
+
+ @Override
+ protected void setLastModifiedTime(Path path, long newTime) throws IOException {
+ checkModifiable();
+ FileSystem delegate = getDelegate(path);
+ delegate.setLastModifiedTime(adjustPath(path, delegate), newTime);
+ }
+
+ @Override
+ protected boolean isSymbolicLink(Path path) {
+ FileSystem delegate = getDelegate(path);
+ path = adjustPath(path, delegate);
+ return delegate.isSymbolicLink(path);
+ }
+
+ @Override
+ protected boolean isDirectory(Path path, boolean followSymlinks) {
+ FileSystem delegate = getDelegate(path);
+ return delegate.isDirectory(adjustPath(path, delegate), followSymlinks);
+ }
+
+ @Override
+ protected boolean isFile(Path path, boolean followSymlinks) {
+ FileSystem delegate = getDelegate(path);
+ return delegate.isFile(adjustPath(path, delegate), followSymlinks);
+ }
+
+ @Override
+ protected void createSymbolicLink(Path linkPath, PathFragment targetFragment) throws IOException {
+ checkModifiable();
+ if (!supportsSymbolicLinks()) {
+ throw new UnsupportedOperationException(
+ "Attempted to create a symlink, but symlink support is disabled.");
+ }
+
+ FileSystem delegate = getDelegate(linkPath);
+ delegate.createSymbolicLink(adjustPath(linkPath, delegate), targetFragment);
+ }
+
+ @Override
+ protected boolean exists(Path path, boolean followSymlinks) {
+ FileSystem delegate = getDelegate(path);
+ return delegate.exists(adjustPath(path, delegate), followSymlinks);
+ }
+
+ @Override
+ protected FileStatus stat(final Path path, final boolean followSymlinks) throws IOException {
+ FileSystem delegate = getDelegate(path);
+ return delegate.stat(adjustPath(path, delegate), followSymlinks);
+ }
+
+ // Needs to be overridden for the delegation logic, because the
+ // UnixFileSystem implements statNullable and stat as separate codepaths.
+ // More generally, we wish to delegate all filesystem operations.
+ @Override
+ protected FileStatus statNullable(Path path, boolean followSymlinks) {
+ FileSystem delegate = getDelegate(path);
+ return delegate.statNullable(adjustPath(path, delegate), followSymlinks);
+ }
+
+ @Override
+ @Nullable
+ protected FileStatus statIfFound(Path path, boolean followSymlinks) throws IOException {
+ FileSystem delegate = getDelegate(path);
+ return delegate.statIfFound(adjustPath(path, delegate), followSymlinks);
+ }
+
+ /**
+ * Retrieves the directory entries for the specified path under the assumption
+ * that {@code resolvedPath} is the resolved path of {@code path} in one of the
+ * underlying file systems.
+ *
+ * @param path the {@link Path} whose children are to be retrieved
+ */
+ @Override
+ protected Collection<Path> getDirectoryEntries(Path path) throws IOException {
+ FileSystem delegate = getDelegate(path);
+ Path resolvedPath = adjustPath(path, delegate);
+ Collection<Path> entries = resolvedPath.getDirectoryEntries();
+ Collection<Path> result = Lists.newArrayListWithCapacity(entries.size());
+ for (Path entry : entries) {
+ result.add(path.getChild(entry.getBaseName()));
+ }
+ return result;
+ }
+
+ // No need for the more complex logic of getDirectoryEntries; it calls it implicitly.
+ @Override
+ protected Collection<Dirent> readdir(Path path, boolean followSymlinks) throws IOException {
+ FileSystem delegate = getDelegate(path);
+ return delegate.readdir(adjustPath(path, delegate), followSymlinks);
+ }
+
+ @Override
+ protected boolean isReadable(Path path) throws IOException {
+ FileSystem delegate = getDelegate(path);
+ return delegate.isReadable(adjustPath(path, delegate));
+ }
+
+ @Override
+ protected void setReadable(Path path, boolean readable) throws IOException {
+ checkModifiable();
+ FileSystem delegate = getDelegate(path);
+ delegate.setReadable(adjustPath(path, delegate), readable);
+ }
+
+ @Override
+ protected boolean isWritable(Path path) throws IOException {
+ if (!supportsModifications()) {
+ return false;
+ }
+ FileSystem delegate = getDelegate(path);
+ return delegate.isWritable(adjustPath(path, delegate));
+ }
+
+ @Override
+ protected void setWritable(Path path, boolean writable) throws IOException {
+ checkModifiable();
+ FileSystem delegate = getDelegate(path);
+ delegate.setWritable(adjustPath(path, delegate), writable);
+ }
+
+ @Override
+ protected boolean isExecutable(Path path) throws IOException {
+ FileSystem delegate = getDelegate(path);
+ return delegate.isExecutable(adjustPath(path, delegate));
+ }
+
+ @Override
+ protected void setExecutable(Path path, boolean executable) throws IOException {
+ checkModifiable();
+ FileSystem delegate = getDelegate(path);
+ delegate.setExecutable(adjustPath(path, delegate), executable);
+ }
+
+ @Override
+ protected String getFastDigestFunctionType(Path path) {
+ FileSystem delegate = getDelegate(path);
+ return delegate.getFastDigestFunctionType(adjustPath(path, delegate));
+ }
+
+ @Override
+ protected byte[] getFastDigest(Path path) throws IOException {
+ FileSystem delegate = getDelegate(path);
+ return delegate.getFastDigest(adjustPath(path, delegate));
+ }
+
+ @Override
+ protected byte[] getxattr(Path path, String name, boolean followSymlinks) throws IOException {
+ FileSystem delegate = getDelegate(path);
+ return delegate.getxattr(adjustPath(path, delegate), name, followSymlinks);
+ }
+
+ @Override
+ protected InputStream getInputStream(Path path) throws IOException {
+ FileSystem delegate = getDelegate(path);
+ return delegate.getInputStream(adjustPath(path, delegate));
+ }
+
+ @Override
+ protected OutputStream getOutputStream(Path path, boolean append) throws IOException {
+ checkModifiable();
+ FileSystem delegate = getDelegate(path);
+ return delegate.getOutputStream(adjustPath(path, delegate), append);
+ }
+
+ @Override
+ protected void renameTo(Path sourcePath, Path targetPath) throws IOException {
+ checkModifiable();
+ FileSystem sourceDelegate = getDelegate(sourcePath);
+ if (!sourceDelegate.supportsModifications()) {
+ throw new UnsupportedOperationException(
+ "The filesystem for the source path "
+ + sourcePath.getPathString() + " does not support modifications.");
+ }
+ sourcePath = adjustPath(sourcePath, sourceDelegate);
+
+ FileSystem targetDelegate = getDelegate(targetPath);
+ if (!targetDelegate.supportsModifications()) {
+ throw new UnsupportedOperationException(
+ "The filesystem for the target path "
+ + targetPath.getPathString() + " does not support modifications.");
+ }
+ targetPath = adjustPath(targetPath, targetDelegate);
+
+ if (sourceDelegate == targetDelegate) {
+ // Easy, same filesystem.
+ sourceDelegate.renameTo(sourcePath, targetPath);
+ return;
+ } else {
+ // Copy across filesystems, then delete.
+ // copyFile throws on failure, so delete will never be reached if it fails.
+ FileSystemUtils.copyFile(sourcePath, targetPath);
+ sourceDelegate.delete(sourcePath);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/UnixFileSystem.java b/src/main/java/com/google/devtools/build/lib/vfs/UnixFileSystem.java
new file mode 100644
index 0000000..c7dd3a8
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/vfs/UnixFileSystem.java
@@ -0,0 +1,414 @@
+// Copyright 2014 Google Inc. 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.vfs;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Lists;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.profiler.Profiler;
+import com.google.devtools.build.lib.profiler.ProfilerTask;
+import com.google.devtools.build.lib.unix.ErrnoFileStatus;
+import com.google.devtools.build.lib.unix.FilesystemUtils;
+import com.google.devtools.build.lib.unix.FilesystemUtils.Dirents;
+import com.google.devtools.build.lib.unix.FilesystemUtils.ReadTypes;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * This class implements the FileSystem interface using direct calls to the
+ * UNIX filesystem.
+ */
+// Not final only for testing.
+@ThreadSafe
+public class UnixFileSystem extends AbstractFileSystem {
+
+ public static final UnixFileSystem INSTANCE = new UnixFileSystem();
+ /**
+ * Eager implementation of FileStatus for file systems that have an atomic
+ * stat(2) syscall. A proxy for {@link com.google.devtools.build.lib.unix.FileStatus}.
+ * Note that isFile and getLastModifiedTime have slightly different meanings
+ * between UNIX and VFS.
+ */
+ @VisibleForTesting
+ protected static class UnixFileStatus implements FileStatus {
+
+ private final com.google.devtools.build.lib.unix.FileStatus status;
+
+ UnixFileStatus(com.google.devtools.build.lib.unix.FileStatus status) {
+ this.status = status;
+ }
+
+ @Override
+ public boolean isFile() { return !isDirectory() && !isSymbolicLink(); }
+
+ @Override
+ public boolean isDirectory() { return status.isDirectory(); }
+
+ @Override
+ public boolean isSymbolicLink() { return status.isSymbolicLink(); }
+
+ @Override
+ public long getSize() { return status.getSize(); }
+
+ @Override
+ public long getLastModifiedTime() {
+ return (status.getLastModifiedTime() * 1000)
+ + (status.getFractionalLastModifiedTime() / 1000000);
+ }
+
+ @Override
+ public long getLastChangeTime() {
+ return (status.getLastChangeTime() * 1000)
+ + (status.getFractionalLastChangeTime() / 1000000);
+ }
+
+ @Override
+ public long getNodeId() {
+ // Note that we may want to include more information in this id number going forward,
+ // especially the device number.
+ return status.getInodeNumber();
+ }
+
+ int getPermissions() { return status.getPermissions(); }
+
+ @Override
+ public String toString() { return status.toString(); }
+ }
+
+ @Override
+ protected Collection<Path> getDirectoryEntries(Path path) throws IOException {
+ String name = path.getPathString();
+ String[] entries;
+ long startTime = Profiler.nanoTimeMaybe();
+ try {
+ entries = FilesystemUtils.readdir(name);
+ } finally {
+ profiler.logSimpleTask(startTime, ProfilerTask.VFS_DIR, name);
+ }
+ Collection<Path> result = new ArrayList<>(entries.length);
+ for (String entry : entries) {
+ result.add(path.getChild(entry));
+ }
+ return result;
+ }
+
+ @Override
+ protected PathFragment resolveOneLink(Path path) throws IOException {
+ // Beware, this seemingly simple code belies the complex specification of
+ // FileSystem.resolveOneLink().
+ return stat(path, false).isSymbolicLink()
+ ? readSymbolicLink(path)
+ : null;
+ }
+
+ /**
+ * Converts from {@link com.google.devtools.build.lib.unix.FilesystemUtils.Dirents.Type} to
+ * {@link com.google.devtools.build.lib.vfs.Dirent.Type}.
+ */
+ private static Dirent.Type convertToDirentType(Dirents.Type type) {
+ switch (type) {
+ case FILE:
+ return Dirent.Type.FILE;
+ case DIRECTORY:
+ return Dirent.Type.DIRECTORY;
+ case SYMLINK:
+ return Dirent.Type.SYMLINK;
+ case UNKNOWN:
+ return Dirent.Type.UNKNOWN;
+ default:
+ throw new IllegalArgumentException("Unknown type " + type);
+ }
+ }
+
+ @Override
+ protected Collection<Dirent> readdir(Path path, boolean followSymlinks) throws IOException {
+ String name = path.getPathString();
+ long startTime = Profiler.nanoTimeMaybe();
+ try {
+ Dirents unixDirents = FilesystemUtils.readdir(name,
+ followSymlinks ? ReadTypes.FOLLOW : ReadTypes.NOFOLLOW);
+ Preconditions.checkState(unixDirents.hasTypes());
+ List<Dirent> dirents = Lists.newArrayListWithCapacity(unixDirents.size());
+ for (int i = 0; i < unixDirents.size(); i++) {
+ dirents.add(new Dirent(unixDirents.getName(i),
+ convertToDirentType(unixDirents.getType(i))));
+ }
+ return dirents;
+ } finally {
+ profiler.logSimpleTask(startTime, ProfilerTask.VFS_DIR, name);
+ }
+ }
+
+ @Override
+ protected UnixFileStatus stat(Path path, boolean followSymlinks) throws IOException {
+ String name = path.getPathString();
+ long startTime = Profiler.nanoTimeMaybe();
+ try {
+ return new UnixFileStatus(followSymlinks
+ ? FilesystemUtils.stat(name)
+ : FilesystemUtils.lstat(name));
+ } finally {
+ profiler.logSimpleTask(startTime, ProfilerTask.VFS_STAT, name);
+ }
+ }
+
+ // Like stat(), but returns null instead of throwing.
+ // This is a performance optimization in the case where clients
+ // catch and don't re-throw.
+ @Override
+ protected UnixFileStatus statNullable(Path path, boolean followSymlinks) {
+ String name = path.getPathString();
+ long startTime = Profiler.nanoTimeMaybe();
+ try {
+ ErrnoFileStatus stat = followSymlinks
+ ? FilesystemUtils.errnoStat(name)
+ : FilesystemUtils.errnoLstat(name);
+ return stat.hasError() ? null : new UnixFileStatus(stat);
+ } finally {
+ profiler.logSimpleTask(startTime, ProfilerTask.VFS_STAT, name);
+ }
+ }
+
+ @Override
+ protected boolean exists(Path path, boolean followSymlinks) {
+ return statNullable(path, followSymlinks) != null;
+ }
+
+ /**
+ * Return true iff the {@code stat} of {@code path} resulted in an {@code ENOENT}
+ * or {@code ENOTDIR} error.
+ */
+ @Override
+ protected FileStatus statIfFound(Path path, boolean followSymlinks) throws IOException {
+ String name = path.getPathString();
+ long startTime = Profiler.nanoTimeMaybe();
+ try {
+ ErrnoFileStatus stat = followSymlinks
+ ? FilesystemUtils.errnoStat(name)
+ : FilesystemUtils.errnoLstat(name);
+ if (!stat.hasError()) {
+ return new UnixFileStatus(stat);
+ }
+ int errno = stat.getErrno();
+ if (errno == ErrnoFileStatus.ENOENT || errno == ErrnoFileStatus.ENOTDIR) {
+ return null;
+ }
+ // This should not return -- we are calling stat here just to throw the proper exception.
+ // However, since there may be transient IO errors, we cannot guarantee that an exception will
+ // be thrown.
+ // TODO(bazel-team): Extract the exception-construction code and make it visible separately in
+ // FilesystemUtils to avoid having to do a duplicate stat call.
+ return stat(path, followSymlinks);
+ } finally {
+ profiler.logSimpleTask(startTime, ProfilerTask.VFS_STAT, name);
+ }
+ }
+
+ @Override
+ protected boolean isDirectory(Path path, boolean followSymlinks) {
+ UnixFileStatus stat = statNullable(path, followSymlinks);
+ return stat != null && stat.isDirectory();
+ }
+
+ @Override
+ protected boolean isFile(Path path, boolean followSymlinks) {
+ // Note, FileStatus.isFile means *regular* file whereas Path.isFile may
+ // mean special file too, so we don't return FileStatus.isFile here.
+ UnixFileStatus status = statNullable(path, followSymlinks);
+ return status != null && !(status.isSymbolicLink() || status.isDirectory());
+ }
+
+ @Override
+ protected boolean isReadable(Path path) throws IOException {
+ return (stat(path, true).getPermissions() & 0400) != 0;
+ }
+
+ @Override
+ protected boolean isWritable(Path path) throws IOException {
+ return (stat(path, true).getPermissions() & 0200) != 0;
+ }
+
+ @Override
+ protected boolean isExecutable(Path path) throws IOException {
+ return (stat(path, true).getPermissions() & 0100) != 0;
+ }
+
+ /**
+ * Adds or remove the bits specified in "permissionBits" to the permission
+ * mask of the file specified by {@code path}. If the argument {@code add} is
+ * true, the specified permissions are added, otherwise they are removed.
+ *
+ * @throws IOException if there was an error writing the file's metadata
+ */
+ private void modifyPermissionBits(Path path, int permissionBits, boolean add)
+ throws IOException {
+ synchronized (path) {
+ int oldMode = stat(path, true).getPermissions();
+ int newMode = add ? (oldMode | permissionBits) : (oldMode & ~permissionBits);
+ FilesystemUtils.chmod(path.toString(), newMode);
+ }
+ }
+
+ @Override
+ protected void setReadable(Path path, boolean readable) throws IOException {
+ modifyPermissionBits(path, 0400, readable);
+ }
+
+ @Override
+ protected void setWritable(Path path, boolean writable) throws IOException {
+ modifyPermissionBits(path, 0200, writable);
+ }
+
+ @Override
+ protected void setExecutable(Path path, boolean executable) throws IOException {
+ modifyPermissionBits(path, 0111, executable);
+ }
+
+ @Override
+ protected void chmod(Path path, int mode) throws IOException {
+ synchronized (path) {
+ FilesystemUtils.chmod(path.toString(), mode);
+ }
+ }
+
+ @Override
+ public boolean supportsModifications() {
+ return true;
+ }
+
+ @Override
+ public boolean supportsSymbolicLinks() {
+ return true;
+ }
+
+ @Override
+ protected boolean createDirectory(Path path) throws IOException {
+ synchronized (path) {
+ // Note: UNIX mkdir(2), FilesystemUtils.mkdir() and createDirectory all
+ // have different ways of representing failure!
+ if (FilesystemUtils.mkdir(path.toString(), 0777)) {
+ return true; // successfully created
+ }
+
+ // false => EEXIST: something is already in the way (file/dir/symlink)
+ if (isDirectory(path, false)) {
+ return false; // directory already existed
+ } else {
+ throw new IOException(path + " (File exists)");
+ }
+ }
+ }
+
+ @Override
+ protected void createSymbolicLink(Path linkPath, PathFragment targetFragment)
+ throws IOException {
+ synchronized (linkPath) {
+ FilesystemUtils.symlink(targetFragment.toString(), linkPath.toString());
+ }
+ }
+
+ @Override
+ protected PathFragment readSymbolicLink(Path path) throws IOException {
+ String name = path.toString();
+ long startTime = Profiler.nanoTimeMaybe();
+ try {
+ return new PathFragment(FilesystemUtils.readlink(name));
+ } catch (IOException e) {
+ // EINVAL => not a symbolic link. Anything else is a real error.
+ throw e.getMessage().endsWith("(Invalid argument)") ? new NotASymlinkException(path) : e;
+ } finally {
+ profiler.logSimpleTask(startTime, ProfilerTask.VFS_LINK, name);
+ }
+ }
+
+ @Override
+ protected void renameTo(Path sourcePath, Path targetPath) throws IOException {
+ synchronized (sourcePath) {
+ FilesystemUtils.rename(sourcePath.toString(), targetPath.toString());
+ }
+ }
+
+ @Override
+ protected long getFileSize(Path path, boolean followSymlinks) throws IOException {
+ return stat(path, followSymlinks).getSize();
+ }
+
+ @Override
+ protected boolean delete(Path path) throws IOException {
+ String name = path.toString();
+ long startTime = Profiler.nanoTimeMaybe();
+ synchronized (path) {
+ try {
+ return FilesystemUtils.remove(name);
+ } finally {
+ profiler.logSimpleTask(startTime, ProfilerTask.VFS_DELETE, name);
+ }
+ }
+ }
+
+ @Override
+ protected long getLastModifiedTime(Path path, boolean followSymlinks) throws IOException {
+ return stat(path, followSymlinks).getLastModifiedTime();
+ }
+
+ @Override
+ protected boolean isSymbolicLink(Path path) {
+ UnixFileStatus stat = statNullable(path, false);
+ return stat != null && stat.isSymbolicLink();
+ }
+
+ @Override
+ protected void setLastModifiedTime(Path path, long newTime) throws IOException {
+ synchronized (path) {
+ if (newTime == -1L) { // "now"
+ FilesystemUtils.utime(path.toString(), true, 0, 0);
+ } else {
+ // newTime > MAX_INT => -ve unixTime
+ int unixTime = (int) (newTime / 1000);
+ FilesystemUtils.utime(path.toString(), false, unixTime, unixTime);
+ }
+ }
+ }
+
+ @Override
+ protected byte[] getxattr(Path path, String name, boolean followSymlinks) throws IOException {
+ String pathName = path.toString();
+ long startTime = Profiler.nanoTimeMaybe();
+ try {
+ return followSymlinks
+ ? FilesystemUtils.getxattr(pathName, name) : FilesystemUtils.lgetxattr(pathName, name);
+ } catch (UnsupportedOperationException e) {
+ // getxattr() syscall is not supported by the underlying filesystem (it returned ENOTSUP).
+ // Per method contract, treat this as ENODATA.
+ return null;
+ } finally {
+ profiler.logSimpleTask(startTime, ProfilerTask.VFS_XATTR, pathName);
+ }
+ }
+
+ @Override
+ protected byte[] getMD5Digest(Path path) throws IOException {
+ String name = path.toString();
+ long startTime = Profiler.nanoTimeMaybe();
+ try {
+ return FilesystemUtils.md5sum(name).asBytes();
+ } finally {
+ profiler.logSimpleTask(startTime, ProfilerTask.VFS_MD5, name);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/UnixGlob.java b/src/main/java/com/google/devtools/build/lib/vfs/UnixGlob.java
new file mode 100644
index 0000000..d512abc
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/vfs/UnixGlob.java
@@ -0,0 +1,785 @@
+// Copyright 2014 Google Inc. 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.vfs;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+import com.google.common.base.Splitter;
+import com.google.common.base.Throwables;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.common.util.concurrent.AbstractFuture;
+import com.google.common.util.concurrent.Futures;
+import com.google.devtools.build.lib.concurrent.AbstractQueueVisitor;
+import com.google.devtools.build.lib.profiler.Profiler;
+import com.google.devtools.build.lib.profiler.ProfilerTask;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.regex.Pattern;
+
+/**
+ * Implementation of a subset of UNIX-style file globbing, expanding "*" and "?" as wildcards, but
+ * not [a-z] ranges.
+ *
+ * <p><code>**</code> gets special treatment in include patterns. If it is used as a complete path
+ * segment it matches the filenames in subdirectories recursively.
+ */
+public final class UnixGlob {
+ private UnixGlob() {}
+
+ private static List<Path> globInternal(Path base, Collection<String> patterns,
+ Collection<String> excludePatterns,
+ boolean excludeDirectories,
+ Predicate<Path> dirPred,
+ boolean checkForInterruption,
+ FilesystemCalls syscalls,
+ ThreadPoolExecutor threadPool)
+ throws IOException, InterruptedException {
+ GlobVisitor visitor = (threadPool == null)
+ ? new GlobVisitor(checkForInterruption)
+ : new GlobVisitor(threadPool, checkForInterruption);
+ return visitor.glob(base, patterns, excludePatterns, excludeDirectories, dirPred, syscalls);
+ }
+
+ private static Future<List<Path>> globAsyncInternal(Path base, Collection<String> patterns,
+ Collection<String> excludePatterns,
+ boolean excludeDirectories,
+ Predicate<Path> dirPred,
+ FilesystemCalls syscalls,
+ boolean checkForInterruption,
+ ThreadPoolExecutor threadPool) {
+ GlobVisitor visitor = (threadPool == null)
+ ? new GlobVisitor(checkForInterruption)
+ : new GlobVisitor(threadPool, checkForInterruption);
+ return visitor.globAsync(base, patterns, excludePatterns, excludeDirectories, dirPred,
+ syscalls);
+ }
+
+ /**
+ * Checks that each pattern is valid, splits it into segments and checks
+ * that each segment contains only valid wildcards.
+ *
+ * @return list of segment arrays
+ */
+ private static List<String[]> checkAndSplitPatterns(Collection<String> patterns) {
+ List<String[]> list = Lists.newArrayListWithCapacity(patterns.size());
+ for (String pattern : patterns) {
+ String error = checkPatternForError(pattern);
+ if (error != null) {
+ throw new IllegalArgumentException(error + " (in glob pattern '" + pattern + "')");
+ }
+ Iterable<String> segments = Splitter.on('/').split(pattern);
+ list.add(Iterables.toArray(segments, String.class));
+ }
+ return list;
+ }
+
+ /**
+ * @return whether or not {@code pattern} contains illegal characters
+ */
+ public static String checkPatternForError(String pattern) {
+ if (pattern.isEmpty()) {
+ return "pattern cannot be empty";
+ }
+ if (pattern.charAt(0) == '/') {
+ return "pattern cannot be absolute";
+ }
+ for (int i = 0; i < pattern.length(); i++) {
+ char c = pattern.charAt(i);
+ switch (c) {
+ case '(': case ')':
+ case '{': case '}':
+ case '[': case ']':
+ return "illegal character '" + c + "'";
+ }
+ }
+ Iterable<String> segments = Splitter.on('/').split(pattern);
+ for (String segment : segments) {
+ if (segment.isEmpty()) {
+ return "empty segment not permitted";
+ }
+ if (segment.equals(".") || segment.equals("..")) {
+ return "segment '" + segment + "' not permitted";
+ }
+ if (segment.contains("**") && !segment.equals("**")) {
+ return "recursive wildcard must be its own segment";
+ }
+ }
+ return null;
+ }
+
+ private static boolean excludedOnMatch(Path path, List<String[]> excludePatterns,
+ int idx, Cache<String, Pattern> cache,
+ Predicate<Path> dirPred) {
+ for (String[] excludePattern : excludePatterns) {
+ String text = path.getBaseName();
+ if (idx == excludePattern.length
+ && matches(excludePattern[idx - 1], text, cache)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns the exclude patterns in {@code excludePatterns} which could
+ * apply to the children of {@code base}
+ *
+ * @param idx index into {@code excludePatterns} for the part of the pattern
+ * which might match {@code base}
+ */
+ private static List<String[]> getRelevantExcludes(
+ final Path base, List<String[]> excludePatterns, final int idx,
+ final Cache<String, Pattern> cache) {
+ if (excludePatterns.isEmpty()) {
+ return excludePatterns;
+ }
+ List<String[]> list = new ArrayList<>();
+ for (String[] patterns : excludePatterns) {
+ if (excludePatternMatches(patterns, idx, base, cache)) {
+ list.add(patterns);
+ }
+ }
+ return list;
+ }
+
+ /**
+ * @param patterns a list of patterns
+ * @param idx index into {@code patterns}
+ */
+ private static boolean excludePatternMatches(String[] patterns, int idx,
+ Path base,
+ Cache<String, Pattern> cache) {
+ if (idx == 0) {
+ return true;
+ }
+ String text = base.getBaseName();
+ return patterns.length > idx && matches(patterns[idx - 1], text, cache);
+ }
+
+ /**
+ * Calls {@link #matches(String, String, Cache) matches(pattern, str, null)}
+ */
+ public static boolean matches(String pattern, String str) {
+ return matches(pattern, str, null);
+ }
+
+ /**
+ * Returns whether {@code str} matches the glob pattern {@code pattern}. This
+ * method may use the {@code patternCache} to speed up the matching process.
+ *
+ * @param pattern a glob pattern
+ * @param str the string to match
+ * @param patternCache a cache from patterns to compiled Pattern objects, or
+ * {@code null} to skip caching
+ */
+ public static boolean matches(String pattern, String str,
+ Cache<String, Pattern> patternCache) {
+ if (pattern.length() == 0 || str.length() == 0) {
+ return false;
+ }
+
+ // Common case: **
+ if (pattern.equals("**")) {
+ return true;
+ }
+
+ // Common case: *
+ if (pattern.equals("*")) {
+ return true;
+ }
+
+ // If a filename starts with '.', this char must be matched explicitly.
+ if (str.charAt(0) == '.' && pattern.charAt(0) != '.') {
+ return false;
+ }
+
+ // Common case: *.xyz
+ if (pattern.charAt(0) == '*' && pattern.lastIndexOf('*') == 0) {
+ return str.endsWith(pattern.substring(1));
+ }
+ // Common case: xyz*
+ int lastIndex = pattern.length() - 1;
+ // The first clause of this if statement is unnecessary, but is an
+ // optimization--charAt runs faster than indexOf.
+ if (pattern.charAt(lastIndex) == '*' && pattern.indexOf('*') == lastIndex) {
+ return str.startsWith(pattern.substring(0, lastIndex));
+ }
+
+ Pattern regex = patternCache == null ? null : patternCache.getIfPresent(pattern);
+ if (regex == null) {
+ regex = makePatternFromWildcard(pattern);
+ if (patternCache != null) {
+ patternCache.put(pattern, regex);
+ }
+ }
+ return regex.matcher(str).matches();
+ }
+
+ /**
+ * Returns a regular expression implementing a matcher for "pattern", in which
+ * "*" and "?" are wildcards.
+ *
+ * <p>e.g. "foo*bar?.java" -> "foo.*bar.\\.java"
+ */
+ private static Pattern makePatternFromWildcard(String pattern) {
+ StringBuilder regexp = new StringBuilder();
+ for(int i = 0, len = pattern.length(); i < len; i++) {
+ char c = pattern.charAt(i);
+ switch(c) {
+ case '*':
+ regexp.append(".*");
+ break;
+ case '?':
+ regexp.append('.');
+ break;
+ //escape the regexp special characters that are allowed in wildcards
+ case '^': case '$': case '|': case '+':
+ case '{': case '}': case '[': case ']':
+ case '\\': case '.':
+ regexp.append('\\');
+ regexp.append(c);
+ break;
+ default:
+ regexp.append(c);
+ break;
+ }
+ }
+ return Pattern.compile(regexp.toString());
+ }
+
+ /**
+ * Filesystem calls required for glob().
+ */
+ public static interface FilesystemCalls {
+ /**
+ * Get directory entries and their types.
+ */
+ Collection<Dirent> readdir(Path path, Symlinks symlinks) throws IOException;
+
+ /**
+ * Return the stat() for the given path, or null.
+ */
+ FileStatus statNullable(Path path, Symlinks symlinks);
+ }
+
+ public static FilesystemCalls DEFAULT_SYSCALLS = new FilesystemCalls() {
+ @Override
+ public Collection<Dirent> readdir(Path path, Symlinks symlinks) throws IOException {
+ return path.readdir(symlinks);
+ }
+
+ @Override
+ public FileStatus statNullable(Path path, Symlinks symlinks) {
+ return path.statNullable(symlinks);
+ }
+ };
+
+ public static final AtomicReference<FilesystemCalls> DEFAULT_SYSCALLS_REF =
+ new AtomicReference<FilesystemCalls>(DEFAULT_SYSCALLS);
+
+ public static Builder forPath(Path path) {
+ return new Builder(path);
+ }
+
+ /**
+ * Builder class for UnixGlob.
+ *
+ *
+ */
+ public static class Builder {
+ private Path base;
+ private List<String> patterns;
+ private List<String> excludes;
+ private boolean excludeDirectories;
+ private Predicate<Path> pathFilter;
+ private ThreadPoolExecutor threadPool;
+ private AtomicReference<? extends FilesystemCalls> syscalls =
+ new AtomicReference<>(DEFAULT_SYSCALLS);
+
+ /**
+ * Creates a glob builder with the given base path.
+ */
+ public Builder(Path base) {
+ this.base = base;
+ this.patterns = Lists.newArrayList();
+ this.excludes = Lists.newArrayList();
+ this.excludeDirectories = false;
+ this.pathFilter = Predicates.alwaysTrue();
+ }
+
+ /**
+ * Adds a pattern to include to the glob builder.
+ *
+ * <p>For a description of the syntax of the patterns, see {@link UnixGlob}.
+ */
+ public Builder addPattern(String pattern) {
+ this.patterns.add(pattern);
+ return this;
+ }
+
+ /**
+ * Adds a pattern to include to the glob builder.
+ *
+ * <p>For a description of the syntax of the patterns, see {@link UnixGlob}.
+ */
+ public Builder addPatterns(String... patterns) {
+ for (String pattern : patterns) {
+ this.patterns.add(pattern);
+ }
+ return this;
+ }
+
+ /**
+ * Adds a pattern to include to the glob builder.
+ *
+ * <p>For a description of the syntax of the patterns, see {@link UnixGlob}.
+ */
+ public Builder addPatterns(Collection<String> patterns) {
+ this.patterns.addAll(patterns);
+ return this;
+ }
+
+ /**
+ * Sets the FilesystemCalls interface to use on this glob().
+ */
+ public Builder setFilesystemCalls(AtomicReference<? extends FilesystemCalls> syscalls) {
+ this.syscalls = (syscalls == null)
+ ? new AtomicReference<FilesystemCalls>(DEFAULT_SYSCALLS)
+ : syscalls;
+ return this;
+ }
+
+ /**
+ * Adds patterns to exclude from the results to the glob builder.
+ *
+ * <p>For a description of the syntax of the patterns, see {@link UnixGlob}.
+ */
+ public Builder addExcludes(String... excludes) {
+ this.excludes.addAll(Arrays.asList(excludes));
+ return this;
+ }
+
+ /**
+ * Adds patterns to exclude from the results to the glob builder.
+ *
+ * <p>For a description of the syntax of the patterns, see {@link UnixGlob}.
+ */
+ public Builder addExcludes(Collection<String> excludes) {
+ this.excludes.addAll(excludes);
+ return this;
+ }
+
+ /**
+ * If set to true, directories are not returned in the glob result.
+ */
+ public Builder setExcludeDirectories(boolean excludeDirectories) {
+ this.excludeDirectories = excludeDirectories;
+ return this;
+ }
+
+
+ /**
+ * Sets the threadpool to use for parallel glob evaluation.
+ * If unset, evaluation is done in-thread.
+ */
+ public Builder setThreadPool(ThreadPoolExecutor pool) {
+ this.threadPool = pool;
+ return this;
+ }
+
+
+ /**
+ * If set, the given predicate is called for every directory
+ * encountered. If it returns false, the corresponding item is not
+ * returned in the output and directories are not traversed either.
+ */
+ public Builder setDirectoryFilter(Predicate<Path> pathFilter) {
+ this.pathFilter = pathFilter;
+ return this;
+ }
+
+ /**
+ * Executes the glob.
+ */
+ public List<Path> glob() throws IOException {
+ try {
+ return globInternal(base, patterns, excludes, excludeDirectories, pathFilter, false,
+ syscalls.get(), threadPool);
+ } catch (InterruptedException e) {
+ // cannot happen, since we told globInternal not to throw
+ throw new IllegalStateException(e);
+ }
+ }
+
+ /**
+ * Executes the glob.
+ *
+ * @throws InterruptedException if the thread is interrupted.
+ */
+ public List<Path> globInterruptible() throws IOException, InterruptedException {
+ return globInternal(base, patterns, excludes, excludeDirectories, pathFilter, true,
+ syscalls.get(), threadPool);
+ }
+
+ /**
+ * Executes the glob asynchronously.
+ *
+ * @param checkForInterrupt if the returned future may throw
+ * InterruptedException.
+ */
+ public Future<List<Path>> globAsync(boolean checkForInterrupt) {
+ return globAsyncInternal(base, patterns, excludes, excludeDirectories, pathFilter,
+ syscalls.get(), checkForInterrupt, threadPool);
+ }
+ }
+
+ /**
+ * Adapts the result of the glob visitation as a Future.
+ */
+ private static class GlobFuture extends AbstractFuture<List<Path>> {
+ private final GlobVisitor visitor;
+ private final boolean checkForInterrupt;
+ private final Object completionLock = new Object();
+
+ public GlobFuture(GlobVisitor visitor, boolean checkForInterrupt) {
+ this.visitor = visitor;
+ this.checkForInterrupt = checkForInterrupt;
+ }
+
+ private List<Path> getSafe() throws InterruptedException, ExecutionException {
+ boolean interrupted = false;
+ try {
+ while (true) {
+ try {
+ return super.get();
+ } catch (InterruptedException e) {
+ if (checkForInterrupt) {
+ throw e;
+ }
+ interrupted = true;
+ } catch (ExecutionException e) {
+ // The checkForInterrupt logic is already handled in
+ // GlobVisitor#waitForCompletion().
+ Throwables.propagateIfInstanceOf(e.getCause(), InterruptedException.class);
+ throw e;
+ }
+ }
+ } finally {
+ if (!checkForInterrupt && interrupted) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+
+ @Override
+ public List<Path> get() throws InterruptedException, ExecutionException {
+ synchronized (completionLock) {
+ if (isDone()) {
+ return getSafe();
+ }
+
+ try {
+ visitor.waitForCompletion();
+ super.set(Lists.newArrayList(visitor.results));
+ } catch (Throwable t) {
+ super.setException(t);
+ }
+ List<Path> result = getSafe();
+ return result;
+ }
+ }
+
+ @Override
+ public boolean cancel(boolean mayInterruptIfRunning) {
+ synchronized (completionLock) {
+ if (isDone()) {
+ return false;
+ }
+
+ visitor.interrupt();
+ return true;
+ }
+ }
+ }
+
+ /**
+ * GlobVisitor executes a glob using parallelism, which is useful when
+ * the glob() requires many readdir() calls on high latency filesystems.
+ */
+ private static final class GlobVisitor extends AbstractQueueVisitor {
+ // These collections are used across workers and must therefore be
+ // thread-safe.
+
+ private final static String THREAD_NAME = "GlobVisitor";
+
+ private final Collection<Path> results =
+ Collections.synchronizedSet(Sets.<Path>newTreeSet());
+ private final Cache<String, Pattern> cache = CacheBuilder.newBuilder().build(
+ new CacheLoader<String, Pattern>() {
+ @Override
+ public Pattern load(String wildcard) {
+ return makePatternFromWildcard(wildcard);
+ }
+ });
+
+ private final GlobFuture result;
+ private final boolean failFastOnInterrupt;
+
+ public GlobVisitor(ThreadPoolExecutor executor, boolean failFastOnInterrupt) {
+ super(executor, /*shutdownOnCompletion=*/false, /*failFastOnException=*/true,
+ /*failFastOnInterrupt=*/failFastOnInterrupt);
+ this.result = new GlobFuture(this, failFastOnInterrupt);
+ this.failFastOnInterrupt = failFastOnInterrupt;
+ }
+
+ public GlobVisitor(boolean failFastOnInterrupt) {
+ super(/*concurrent=*/false, 0, 0, 0, null, /*failFastOnException=*/true,
+ /*failFastOnInterrupt=*/failFastOnInterrupt, THREAD_NAME);
+ this.result = new GlobFuture(this, failFastOnInterrupt);
+ this.failFastOnInterrupt = failFastOnInterrupt;
+ }
+
+ /**
+ * Performs wildcard globbing: returns the sorted list of filenames that match any of
+ * {@code patterns} relative to {@code base}, but which do not match {@code excludePatterns}.
+ * Directories are traversed if and only if they match {@code dirPred}. The predicate is also
+ * called for the root of the traversal.
+ *
+ * <p>Patterns may include "*" and "?", but not "[a-z]".
+ *
+ * <p><code>**</code> gets special treatment in include patterns. If it is
+ * used as a complete path segment it matches the filenames in
+ * subdirectories recursively.
+ *
+ * @throws IllegalArgumentException if any glob or exclude pattern
+ * {@linkplain #checkPatternForError(String) contains errors} or if
+ * any exclude pattern segment contains <code>**</code> or if any
+ * include pattern segment contains <code>**</code> but not equal to
+ * it.
+ */
+ public List<Path> glob(Path base, Collection<String> patterns,
+ Collection<String> excludePatterns, boolean excludeDirectories,
+ Predicate<Path> dirPred, FilesystemCalls syscalls)
+ throws IOException, InterruptedException {
+ try {
+ return globAsync(base, patterns, excludePatterns, excludeDirectories,
+ dirPred, syscalls).get();
+ } catch (ExecutionException e) {
+ Throwable cause = e.getCause();
+ Throwables.propagateIfPossible(cause, IOException.class);
+ throw new RuntimeException(e);
+ }
+ }
+
+ public Future<List<Path>> globAsync(Path base, Collection<String> patterns,
+ Collection<String> excludePatterns, boolean excludeDirectories,
+ Predicate<Path> dirPred, FilesystemCalls syscalls) {
+
+ FileStatus baseStat = syscalls.statNullable(base, Symlinks.FOLLOW);
+ if (baseStat == null || patterns.isEmpty()) {
+ return Futures.immediateFuture(Collections.<Path>emptyList());
+ }
+
+ List<String[]> splitPatterns = checkAndSplitPatterns(patterns);
+ List<String[]> splitExcludes = checkAndSplitPatterns(excludePatterns);
+
+ // We do a dumb loop, even though it will likely duplicate work
+ // (e.g., readdir calls). In order to optimize, we would need
+ // to keep track of which patterns shared sub-patterns and which did not
+ // (for example consider the glob [*/*.java, sub/*.java, */*.txt]).
+ for (String[] splitPattern : splitPatterns) {
+ queueGlob(base, baseStat.isDirectory(), splitPattern, 0, excludeDirectories,
+ splitExcludes, 0, results, cache, dirPred, syscalls);
+ }
+
+ return result;
+ }
+
+ protected void waitForCompletion() throws IOException, InterruptedException {
+ try {
+ super.work(failFastOnInterrupt);
+ } catch (InterruptedException e) {
+ if (failFastOnInterrupt) {
+ throw e;
+ } else {
+ Thread.currentThread().interrupt();
+ }
+ } catch (IORuntimeException e) {
+ if (Thread.interrupted()) {
+ // As per the contract of AbstractQueueVisitor#work, if an unchecked exception is thrown
+ // and the build is interrupted, the thrown exception is what will be rethrown. Since the
+ // user presumably wanted to interrupt the build, we ignore the thrown IORuntimeException
+ // (which doesn't indicate a programming bug) and throw an InterruptedException.
+ if (failFastOnInterrupt) {
+ throw new InterruptedException();
+ }
+ Thread.currentThread().interrupt();
+ }
+ throw e.getCauseIOException();
+ }
+ }
+
+ private void queueGlob(final Path base, final boolean baseIsDir,
+ final String[] patternParts, final int idx,
+ final boolean excludeDirectories,
+ final List<String[]> excludePatterns,
+ final int excludeIdx,
+ final Collection<Path> results, final Cache<String, Pattern> cache,
+ final Predicate<Path> dirPred, final FilesystemCalls syscalls) {
+ enqueue(new Runnable() {
+ @Override
+ public void run() {
+ Profiler.instance().startTask(ProfilerTask.VFS_GLOB, this);
+ try {
+ reallyGlob(base, baseIsDir, patternParts, idx, excludeDirectories,
+ excludePatterns, excludeIdx, results, cache, dirPred, syscalls);
+ } catch (IOException e) {
+ throw new IORuntimeException(e);
+ } catch (InterruptedException e) {
+ // When we get to this point, the main thread already knows that the
+ // globbing has been interrupted, so we do not need to report the
+ // error condition.
+ } finally {
+ Profiler.instance().completeTask(ProfilerTask.VFS_GLOB);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "%s glob(include=[%s], exclude=[%s], exclude_directories=%s)",
+ base.getPathString(),
+ "\"" + Joiner.on("\", \"").join(patternParts) + "\"",
+ "\"" + Joiner.on("\", \"").join(excludePatterns) + "\"",
+ excludeDirectories);
+ }
+ });
+
+ }
+
+ /**
+ * Expressed in Haskell:
+ * <pre>
+ * reallyGlob base [] = { base }
+ * reallyGlob base [x:xs] = union { reallyGlob(f, xs) | f results "base/x" }
+ * </pre>
+ */
+ private void reallyGlob(Path base, boolean baseIsDir, String[] patternParts, int idx,
+ boolean excludeDirectories,
+ List<String[]> excludePatterns,
+ int excludeIdx,
+ Collection<Path> results, Cache<String, Pattern> cache,
+ Predicate<Path> dirPred,
+ FilesystemCalls syscalls) throws IOException, InterruptedException {
+ if (failFastOnInterrupt && Thread.interrupted()) {
+ throw new InterruptedException();
+ }
+
+ if (baseIsDir && !dirPred.apply(base)) {
+ return;
+ }
+
+ if (idx == patternParts.length) { // Base case.
+ if (!(excludeDirectories && baseIsDir) &&
+ !excludedOnMatch(base, excludePatterns, excludeIdx, cache, dirPred)) {
+ results.add(base);
+ }
+
+ return;
+ }
+
+ if (!baseIsDir) {
+ // Nothing to find here.
+ return;
+ }
+
+ List<String[]> relevantExcludes
+ = getRelevantExcludes(base, excludePatterns, excludeIdx, cache);
+ final String pattern = patternParts[idx];
+
+ // ** is special: it can match nothing at all.
+ // For example, x/** matches x, **/y matches y, and x/**/y matches x/y.
+ if ("**".equals(pattern)) {
+ queueGlob(base, baseIsDir, patternParts, idx + 1, excludeDirectories,
+ excludePatterns, excludeIdx, results, cache, dirPred, syscalls);
+ }
+
+ if (!pattern.contains("*") && !pattern.contains("?")) {
+ // We do not need to do a readdir in this case, just a stat.
+ Path child = base.getChild(pattern);
+ FileStatus status = syscalls.statNullable(child, Symlinks.FOLLOW);
+ if (status == null || (!status.isDirectory() && !status.isFile())) {
+ // The file is a dangling symlink, fifo, does not exist, etc.
+ return;
+ }
+
+ boolean childIsDir = status.isDirectory();
+
+ queueGlob(child, childIsDir, patternParts, idx + 1, excludeDirectories,
+ relevantExcludes, excludeIdx + 1, results, cache, dirPred, syscalls);
+ return;
+ }
+
+ Collection<Dirent> dents = syscalls.readdir(base, Symlinks.FOLLOW);
+
+ for (Dirent dent : dents) {
+ Dirent.Type type = dent.getType();
+ if (type == Dirent.Type.UNKNOWN) {
+ // The file is a dangling symlink, fifo, etc.
+ continue;
+ }
+ boolean childIsDir = (type == Dirent.Type.DIRECTORY);
+ String text = dent.getName();
+ Path child = base.getChild(text);
+
+ if ("**".equals(pattern)) {
+ // Recurse without shifting the pattern.
+ if (childIsDir) {
+ queueGlob(child, childIsDir, patternParts, idx, excludeDirectories,
+ relevantExcludes, excludeIdx + 1, results, cache, dirPred, syscalls);
+ }
+ }
+ if (matches(pattern, text, cache)) {
+ // Recurse and consume one segment of the pattern.
+ if (childIsDir) {
+ queueGlob(child, childIsDir, patternParts, idx + 1, excludeDirectories,
+ relevantExcludes, excludeIdx + 1, results, cache, dirPred, syscalls);
+ } else {
+ // Instead of using an async call, just repeat the base case above.
+ if (idx + 1 == patternParts.length &&
+ !excludedOnMatch(child, relevantExcludes, excludeIdx + 1, cache, dirPred)) {
+ results.add(child);
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/ZipFileSystem.java b/src/main/java/com/google/devtools/build/lib/vfs/ZipFileSystem.java
new file mode 100644
index 0000000..558263d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/vfs/ZipFileSystem.java
@@ -0,0 +1,253 @@
+// Copyright 2014 Google Inc. 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.vfs;
+
+import com.google.common.base.Predicate;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+/**
+ * A FileSystem that provides a read-only filesystem view on a zip file.
+ * Inherits the constraints imposed by ReadonlyFileSystem.
+ */
+@ThreadSafe
+public class ZipFileSystem extends ReadonlyFileSystem {
+
+ private final ZipFile zipFile;
+
+ /**
+ * The sole purpose of this field is to hold a strong reference to all leaf
+ * {@link Path}s which have a non-null "entry" field, preventing them from
+ * being garbage-collected. (The leaf paths hold string references to their
+ * parents, so we don't need to include them here.)
+ *
+ * <p>This is necessary because {@link Path}s may be recycled when they
+ * become unreachable, but the ZipFileSystem uses them to hold the {@link
+ * ZipEntry} for that path, if any. Without this additional strong
+ * reference, ZipEntries would seem to "disappear" during garbage collection.
+ */
+ @SuppressWarnings("unused")
+ private final Object paths;
+
+ /**
+ * Constructs a ZipFileSystem from a zip file identified with a given path.
+ */
+ public ZipFileSystem(Path zipPath) throws IOException {
+ // Throw some more specific exceptions than ZipFile does.
+ // We do this using File instead of Path, in case zipPath points to an
+ // InMemoryFileSystem. This case is not really supported but
+ // can occur in tests.
+ File file = zipPath.getPathFile();
+ if (!file.exists()) {
+ throw new FileNotFoundException(String.format("File '%s' does not exist", zipPath));
+ }
+ if (!file.isFile()) {
+ throw new IOException(String.format("'%s' is not a file", zipPath));
+ }
+ if (!file.canRead()) {
+ throw new IOException(String.format("File '%s' is not readable", zipPath));
+ }
+
+ this.zipFile = new ZipFile(file);
+ this.paths = populatePathTree();
+ }
+
+ // ZipPath extends Path with a set-once ZipEntry field.
+ // TODO(bazel-team): (2009) Delete class ZipPath, and perform the
+ // Path-to-ZipEntry lookup in {@link #zipEntry} and {@link
+ // #getDirectoryEntries}. Then this field becomes redundant.
+ @ThreadSafe
+ private static class ZipPath extends Path {
+ /**
+ * Non-null iff this file/directory exists. Set by setZipEntry for files
+ * explicitly mentioned in the zipfile's table of contents, or implicitly
+ * an ancestor of them.
+ */
+ ZipEntry entry = null;
+
+ // Root path.
+ ZipPath(ZipFileSystem fileSystem) {
+ super(fileSystem);
+ }
+
+ // Non-root paths.
+ ZipPath(ZipFileSystem fileSystem, String name, ZipPath parent) {
+ super(fileSystem, name, parent);
+ }
+
+ void setZipEntry(ZipEntry entry) {
+ if (this.entry != null) {
+ throw new IllegalStateException("setZipEntry(" + entry
+ + ") called twice!");
+ }
+ this.entry = entry;
+
+ // Ensure all parents of this path have a directory ZipEntry:
+ for (ZipPath path = (ZipPath) getParentDirectory();
+ path != null && path.entry == null;
+ path = (ZipPath) path.getParentDirectory()) {
+ // Note, the ZipEntry for the root path is called "//", but that's ok.
+ path.setZipEntry(new ZipEntry(path + "/")); // trailing "/" => isDir
+ }
+ }
+
+ @Override
+ protected ZipPath createChildPath(String childName) {
+ return new ZipPath((ZipFileSystem) getFileSystem(), childName, this);
+ }
+ }
+
+ /**
+ * Scans the Zip file and associates a ZipEntry with each filename
+ * (ZipPath) that is mentioned in the table of contents. Returns a
+ * collection of all corresponding Paths.
+ */
+ private Collection<Path> populatePathTree() {
+ Collection<Path> paths = new ArrayList<>();
+ for (ZipEntry entry : Collections.list(zipFile.entries())) {
+ PathFragment frag = new PathFragment(entry.getName());
+ Path path = rootPath.getRelative(frag);
+ paths.add(path);
+ ((ZipPath) path).setZipEntry(entry);
+ }
+ return paths;
+ }
+
+ @Override
+ public String getFileSystemType(Path path) {
+ return "zipfs";
+ }
+
+ @Override
+ protected Path createRootPath() {
+ return new ZipPath(this);
+ }
+
+ /** Returns the ZipEntry associated with a given path name, if any. */
+ private static ZipEntry zipEntry(Path path) {
+ return ((ZipPath) path).entry;
+ }
+
+ /** Like zipEntry, but throws FileNotFoundException unless path exists. */
+ private static ZipEntry zipEntryNonNull(Path path)
+ throws FileNotFoundException {
+ ZipEntry zipEntry = zipEntry(path);
+ if (zipEntry == null) {
+ throw new FileNotFoundException(path + " (No such file or directory)");
+ }
+ return zipEntry;
+ }
+
+ @Override
+ protected InputStream getInputStream(Path path) throws IOException {
+ return zipFile.getInputStream(zipEntryNonNull(path));
+ }
+
+ @Override
+ protected Collection<Path> getDirectoryEntries(Path path)
+ throws IOException {
+ zipEntryNonNull(path);
+ final Collection<Path> result = new ArrayList<>();
+ ((ZipPath) path).applyToChildren(new Predicate<Path>() {
+ @Override
+ public boolean apply(Path child) {
+ if (zipEntry(child) != null) {
+ result.add(child);
+ }
+ return true;
+ }
+ });
+ return result;
+ }
+
+ @Override
+ protected boolean exists(Path path, boolean followSymlinks) {
+ return zipEntry(path) != null;
+ }
+
+ @Override
+ protected boolean isDirectory(Path path, boolean followSymlinks) {
+ ZipEntry entry = zipEntry(path);
+ return entry != null && entry.isDirectory();
+ }
+
+ @Override
+ protected boolean isFile(Path path, boolean followSymlinks) {
+ ZipEntry entry = zipEntry(path);
+ return entry != null && !entry.isDirectory();
+ }
+
+ @Override
+ protected boolean isReadable(Path path) throws IOException {
+ zipEntryNonNull(path);
+ return true;
+ }
+
+ @Override
+ protected boolean isWritable(Path path) throws IOException {
+ zipEntryNonNull(path);
+ return false;
+ }
+
+ @Override
+ protected boolean isExecutable(Path path) throws IOException {
+ zipEntryNonNull(path);
+ return false;
+ }
+
+ @Override
+ protected PathFragment readSymbolicLink(Path path) throws IOException {
+ zipEntryNonNull(path);
+ throw new NotASymlinkException(path);
+ }
+
+ @Override
+ protected long getFileSize(Path path, boolean followSymlinks)
+ throws IOException {
+ return zipEntryNonNull(path).getSize();
+ }
+
+ @Override
+ protected long getLastModifiedTime(Path path, boolean followSymlinks)
+ throws FileNotFoundException {
+ return zipEntryNonNull(path).getTime();
+ }
+
+ @Override
+ protected boolean isSymbolicLink(Path path) {
+ return false;
+ }
+
+ @Override
+ protected FileStatus statIfFound(Path path, boolean followSymlinks) {
+ try {
+ return stat(path, followSymlinks);
+ } catch (FileNotFoundException e) {
+ return null;
+ } catch (IOException e) {
+ // getLastModifiedTime can only throw FileNotFoundException, which is what stat uses.
+ throw new IllegalStateException (e);
+ }
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/FileInfo.java b/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/FileInfo.java
new file mode 100644
index 0000000..fff562f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/FileInfo.java
@@ -0,0 +1,49 @@
+// Copyright 2014 Google Inc. 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.vfs.inmemoryfs;
+
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.util.Clock;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * This interface represents a mutable file stored in an InMemoryFileSystem.
+ */
+@ThreadSafe
+public abstract class FileInfo extends InMemoryContentInfo {
+ protected FileInfo(Clock clock) {
+ super(clock);
+ }
+
+ @Override
+ public boolean isDirectory() {
+ return false;
+ }
+
+ @Override
+ public boolean isSymbolicLink() {
+ return false;
+ }
+
+ @Override
+ public boolean isFile() {
+ return true;
+ }
+
+ protected abstract byte[] readContent() throws IOException;
+
+ protected abstract OutputStream getOutputStream(boolean append) throws IOException;
+}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryContentInfo.java b/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryContentInfo.java
new file mode 100644
index 0000000..0e7de71
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryContentInfo.java
@@ -0,0 +1,212 @@
+// Copyright 2014 Google Inc. 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.vfs.inmemoryfs;
+
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.util.Clock;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.io.IOException;
+
+/**
+ * This interface defines the function directly supported by the "files" stored
+ * in a InMemoryFileSystem. This corresponds to a file or inode in UNIX: it
+ * doesn't have a path (it could have many paths due to hard links, or none if
+ * it's unlinked, i.e. garbage).
+ *
+ * <p>This class is thread-safe: instances may be accessed and modified from
+ * concurrent threads. Subclasses must preserve this property.
+ */
+@ThreadSafe
+public abstract class InMemoryContentInfo implements ScopeEscapableStatus {
+
+ private final Clock clock;
+
+ /**
+ * Stores the time when the file was last modified. This is atomically updated
+ * whenever the file changes, so all accesses must be synchronized.
+ */
+ private long lastModifiedTime;
+
+ /**
+ * Stores the time when the file information was changed. This is atomically updated
+ * whenever the file changes, so all accesses must be synchronized.
+ */
+ private long lastChangeTime;
+
+ /**
+ * Modifications to the isWritable field do not update the lastModifiedTime,
+ * so we don't need to synchronize; using volatile is enough.
+ */
+ private volatile boolean isWritable = true;
+ private volatile boolean isExecutable = false;
+ private volatile boolean isReadable = true;
+
+ protected InMemoryContentInfo(Clock clock) {
+ this(clock, true);
+ }
+
+ protected InMemoryContentInfo(Clock clock, boolean isMutable) {
+ this.clock = clock;
+ // When we create the file, it is modified.
+ if (isMutable) {
+ markModificationTime();
+ }
+ }
+
+ /**
+ * Returns true if the current object is a directory.
+ */
+ @Override
+ public abstract boolean isDirectory();
+
+ /**
+ * Returns true if the current object is a symbolic link.
+ */
+ @Override
+ public abstract boolean isSymbolicLink();
+
+ /**
+ * Returns true if the current object is a regular file.
+ */
+ @Override
+ public abstract boolean isFile();
+
+ /**
+ * Returns the size of the entity denoted by the current object. For files,
+ * this is the length in bytes, for directories the number of children. The
+ * size of links is unspecified.
+ */
+ @Override
+ public abstract long getSize() throws IOException;
+
+ /**
+ * Returns the time when the entity denoted by the current object was last
+ * modified.
+ */
+ @Override
+ public synchronized long getLastModifiedTime() {
+ return lastModifiedTime;
+ }
+
+ /**
+ * Returns the time when the entity denoted by the current object was last
+ * changed.
+ */
+ @Override
+ public synchronized long getLastChangeTime() {
+ return lastChangeTime;
+ }
+
+ /**
+ * Returns the file node id for the given instance, emulated by the
+ * identity hash code.
+ */
+ @Override
+ public long getNodeId() {
+ return System.identityHashCode(this);
+ }
+
+ /**
+ * Sets the time that denotes when the entity denoted by this object was last
+ * modified.
+ */
+ synchronized void setLastModifiedTime(long newTime) {
+ lastModifiedTime = newTime;
+ markChangeTime();
+ }
+
+ /**
+ * Sets the last modification and change times to the current time.
+ */
+ protected synchronized void markModificationTime() {
+ Preconditions.checkState(clock != null);
+ lastModifiedTime = clock.currentTimeMillis();
+ lastChangeTime = lastModifiedTime;
+ }
+
+ /**
+ * Sets the last change time to the current time.
+ */
+ protected synchronized void markChangeTime() {
+ Preconditions.checkState(clock != null);
+ lastChangeTime = clock.currentTimeMillis();
+ }
+
+ /**
+ * Sets whether the current file is readable.
+ */
+ boolean isReadable() {
+ return isReadable;
+ }
+
+ /**
+ * Returns whether the current file is readable.
+ */
+ void setReadable(boolean readable) {
+ isReadable = readable;
+ }
+
+
+ /**
+ * Sets whether the current file is writable.
+ */
+ void setWritable(boolean writable) {
+ isWritable = writable;
+ markChangeTime();
+ }
+
+ /**
+ * Returns whether the current file is writable.
+ */
+ boolean isWritable() {
+ return isWritable;
+ }
+
+ /**
+ * Sets whether the current file is executable.
+ */
+ void setExecutable(boolean executable) {
+ isExecutable = executable;
+ markChangeTime();
+ }
+
+ /**
+ * Returns whether the current file is executable.
+ */
+ boolean isExecutable() {
+ return isExecutable;
+ }
+
+ @Override
+ public boolean outOfScope() {
+ return false;
+ }
+
+ @Override
+ public PathFragment getEscapingPath() {
+ return null;
+ }
+
+ /**
+ * Called just before this inode is moved.
+ *
+ * @param targetPath where the inode is relocated.
+ * @throws IOException
+ */
+ protected void movedTo(Path targetPath) throws IOException {
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryDirectoryInfo.java b/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryDirectoryInfo.java
new file mode 100644
index 0000000..400490b
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryDirectoryInfo.java
@@ -0,0 +1,108 @@
+// Copyright 2014 Google Inc. 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.vfs.inmemoryfs;
+
+import com.google.common.collect.MapMaker;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.util.Clock;
+
+import java.util.Set;
+import java.util.concurrent.ConcurrentMap;
+
+/**
+ * This class represents a directory stored in an {@link InMemoryFileSystem}.
+ */
+@ThreadSafe
+class InMemoryDirectoryInfo extends InMemoryContentInfo {
+
+ private final ConcurrentMap<String, InMemoryContentInfo> directoryContent =
+ new MapMaker().makeMap();
+
+ InMemoryDirectoryInfo(Clock clock) {
+ this(clock, true);
+ }
+
+ protected InMemoryDirectoryInfo(Clock clock, boolean isMutable) {
+ super(clock, isMutable);
+ if (isMutable) {
+ setExecutable(true);
+ }
+ }
+
+ /**
+ * Adds a new child to this directory under the name "name". Callers must
+ * ensure that no entry of that name exists already.
+ */
+ synchronized void addChild(String name, InMemoryContentInfo inode) {
+ if (name == null) { throw new NullPointerException(); }
+ if (inode == null) { throw new NullPointerException(); }
+ if (directoryContent.put(name, inode) != null) {
+ throw new IllegalArgumentException("File already exists: " + name);
+ }
+ markModificationTime();
+ }
+
+ /**
+ * Does a directory lookup, and returns the "inode" for the specified name.
+ * Returns null if the child is not found.
+ */
+ synchronized InMemoryContentInfo getChild(String name) {
+ return directoryContent.get(name);
+ }
+
+ /**
+ * Removes a previously existing child from the directory specified by this
+ * object.
+ */
+ synchronized void removeChild(String name) {
+ if (directoryContent.remove(name) == null) {
+ throw new IllegalArgumentException(name + " is not a member of this directory");
+ }
+ markModificationTime();
+ }
+
+ /**
+ * This function returns the content of a directory. For now, it returns a set
+ * to reflect the semantics of the value returned (ie. unordered, no
+ * duplicates). If thats too slow, it should be changed later.
+ */
+ Set<String> getAllChildren() {
+ return directoryContent.keySet();
+ }
+
+ @Override
+ public boolean isDirectory() {
+ return true;
+ }
+
+ @Override
+ public boolean isSymbolicLink() {
+ return false;
+ }
+
+ @Override
+ public boolean isFile() {
+ return false;
+ }
+
+ /**
+ * In the InMemory hierarchy, the getSize on a directory always returns the
+ * number of children in the directory.
+ */
+ @Override
+ public long getSize() {
+ return directoryContent.size();
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryFileInfo.java b/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryFileInfo.java
new file mode 100644
index 0000000..f88285d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryFileInfo.java
@@ -0,0 +1,97 @@
+// Copyright 2014 Google Inc. 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.vfs.inmemoryfs;
+
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.util.Clock;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * InMemoryFileInfo manages file contents by storing them entirely in memory.
+ */
+@ThreadSafe
+public class InMemoryFileInfo extends FileInfo {
+
+ /**
+ * Updates to the content must atomically update the lastModifiedTime. So all
+ * accesses to this field must be synchronized.
+ */
+ protected byte[] content;
+
+ protected InMemoryFileInfo(Clock clock) {
+ super(clock);
+ content = new byte[0]; // New files start out empty.
+ }
+
+ @Override
+ public synchronized long getSize() {
+ return content.length;
+ }
+
+ @Override
+ public synchronized byte[] readContent() {
+ return content.clone();
+ }
+
+ private synchronized void setContent(byte[] newContent) {
+ content = newContent;
+ markModificationTime();
+ }
+
+ @Override
+ protected synchronized OutputStream getOutputStream(boolean append)
+ throws IOException {
+ OutputStream out = new ByteArrayOutputStream() {
+ private boolean closed = false;
+
+ @Override
+ public void write(byte[] data) throws IOException {
+ Preconditions.checkState(!closed);
+ super.write(data);
+ }
+
+ @Override
+ public void write(int dataByte) {
+ Preconditions.checkState(!closed);
+ super.write(dataByte);
+ }
+
+ @Override
+ public void write(byte[] data, int offset, int length) {
+ Preconditions.checkState(!closed);
+ super.write(data, offset, length);
+ }
+
+ @Override
+ public void close() {
+ flush();
+ closed = true;
+ }
+
+ @Override
+ public void flush() {
+ setContent(toByteArray().clone());
+ }
+ };
+
+ if (append) {
+ out.write(readContent());
+ }
+ return out;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryFileSystem.java b/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryFileSystem.java
new file mode 100644
index 0000000..8a3b823
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryFileSystem.java
@@ -0,0 +1,920 @@
+// Copyright 2014 Google Inc. 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.vfs.inmemoryfs;
+
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.unix.FileAccessException;
+import com.google.devtools.build.lib.util.Clock;
+import com.google.devtools.build.lib.util.JavaClock;
+import com.google.devtools.build.lib.vfs.FileStatus;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.lib.vfs.ScopeEscapableFileSystem;
+import com.google.devtools.build.lib.vfs.Symlinks;
+
+import java.io.ByteArrayInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.Stack;
+
+import javax.annotation.Nullable;
+
+/**
+ * This class provides a complete in-memory file system.
+ *
+ * <p>Naming convention: we use "path" for all {@link Path} variables, since these
+ * represent *names* and we use "node" or "inode" for InMemoryContentInfo
+ * variables, since these correspond to inodes in the UNIX file system.
+ *
+ * <p>The code is structured to be as similar to the implementation of UNIX "namei"
+ * as is reasonably possibly. This provides a firm reference point for many
+ * concepts and makes compatibility easier to achieve.
+ *
+ * <p>As a scope-escapable file system, this class supports re-delegation of symbolic links
+ * that escape its root. This is done through the use of {@link OutOfScopeFileStatus}
+ * and {@link OutOfScopeDirectoryStatus} objects, which may be returned by
+ * getDirectory, pathWalk, and scopeLimitedStat. Any code that calls one of these
+ * methods (either directly or indirectly) is obligated to check the possibility
+ * that its info represents an out-of-scope path. Lack of such a check will result
+ * in unchecked runtime exceptions upon any request for status data (as well as
+ * possible logical errors).
+ */
+@ThreadSafe
+public class InMemoryFileSystem extends ScopeEscapableFileSystem {
+
+ private final Clock clock;
+
+ // The root inode (a directory).
+ private final InMemoryDirectoryInfo rootInode;
+
+ // Maximum number of traversals before ELOOP is thrown.
+ private static final int MAX_TRAVERSALS = 256;
+
+ /**
+ * Creates a new InMemoryFileSystem with scope checking disabled (all paths are considered to be
+ * within scope) and a default clock.
+ */
+ public InMemoryFileSystem() {
+ this(new JavaClock());
+ }
+
+ /**
+ * Creates a new InMemoryFileSystem with scope checking disabled (all
+ * paths are considered to be within scope).
+ */
+ public InMemoryFileSystem(Clock clock) {
+ this(clock, null);
+ }
+
+ /**
+ * Creates a new InMemoryFileSystem with scope checking bound to
+ * scopeRoot, i.e. any path that's not below scopeRoot is considered
+ * to be out of scope.
+ */
+ protected InMemoryFileSystem(Clock clock, PathFragment scopeRoot) {
+ super(scopeRoot);
+ this.clock = clock;
+ this.rootInode = new InMemoryDirectoryInfo(clock);
+ rootInode.addChild(".", rootInode);
+ rootInode.addChild("..", rootInode);
+ }
+
+ /**
+ * The errors that {@link InMemoryFileSystem} might issue for different sorts of IO failures.
+ */
+ public enum Error {
+ ENOENT("No such file or directory"),
+ EACCES("Permission denied"),
+ ENOTDIR("Not a directory"),
+ EEXIST("File exists"),
+ EBUSY("Device or resource busy"),
+ ENOTEMPTY("Directory not empty"),
+ EISDIR("Is a directory"),
+ ELOOP("Too many levels of symbolic links");
+
+ private final String message;
+
+ private Error(String message) {
+ this.message = message;
+ }
+
+ @Override
+ public String toString() {
+ return message;
+ }
+
+ /** Implemented by exceptions that contain the extra info of which Error caused them. */
+ private static interface WithError {
+ Error getError();
+ }
+
+ /**
+ * The exceptions below extend their parent classes in order to additionally store the error
+ * that caused them. However, they must impersonate their parents to any outside callers,
+ * including in their toString() method, which prints the class name followed by the exception
+ * method. This method returns the same value as the toString() method of a {@link Throwable}'s
+ * parent would, so that the child class can have the same toString() value.
+ */
+ private static String parentThrowableToString(Throwable obj) {
+ String s = obj.getClass().getSuperclass().getName();
+ String message = obj.getLocalizedMessage();
+ return (message != null) ? (s + ": " + message) : s;
+ }
+
+ private static class IOExceptionWithError extends IOException implements WithError {
+ private final Error errorCode;
+
+ private IOExceptionWithError(String message, Error errorCode) {
+ super(message);
+ this.errorCode = errorCode;
+ }
+
+ @Override
+ public Error getError() {
+ return errorCode;
+ }
+
+ @Override
+ public String toString() {
+ return parentThrowableToString(this);
+ }
+ }
+
+
+ private static class FileNotFoundExceptionWithError
+ extends FileNotFoundException implements WithError {
+ private final Error errorCode;
+
+ private FileNotFoundExceptionWithError(String message, Error errorCode) {
+ super(message);
+ this.errorCode = errorCode;
+ }
+
+ @Override
+ public Error getError() {
+ return errorCode;
+ }
+
+ @Override
+ public String toString() {
+ return parentThrowableToString(this);
+ }
+ }
+
+
+ private static class FileAccessExceptionWithError
+ extends FileAccessException implements WithError {
+ private final Error errorCode;
+
+ private FileAccessExceptionWithError(String message, Error errorCode) {
+ super(message);
+ this.errorCode = errorCode;
+ }
+
+ @Override
+ public Error getError() {
+ return errorCode;
+ }
+
+ @Override
+ public String toString() {
+ return parentThrowableToString(this);
+ }
+ }
+
+ /**
+ * Returns a new IOException for the error. The exception message
+ * contains 'path', and is consistent with the messages returned by
+ * c.g.common.unix.FilesystemUtils.
+ */
+ public IOException exception(Path path) throws IOException {
+ String m = path + " (" + message + ")";
+ if (this == EACCES) {
+ throw new FileAccessExceptionWithError(m, this);
+ } else if (this == ENOENT) {
+ throw new FileNotFoundExceptionWithError(m, this);
+ } else {
+ throw new IOExceptionWithError(m, this);
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * <p>If <code>/proc/mounts</code> does not exist return {@code "inmemoryfs"}.
+ */
+ @Override
+ public String getFileSystemType(Path path) {
+ return path.getRelative("/proc/mounts").exists() ? super.getFileSystemType(path) : "inmemoryfs";
+ }
+
+ /****************************************************************************
+ * "Kernel" primitives: basic directory lookup primitives, in topological
+ * order.
+ */
+
+ /**
+ * Unlinks the entry 'child' from its existing parent directory 'dir'. Dual to
+ * insert. This succeeds even if 'child' names a non-empty directory; we need
+ * that for renameTo. 'child' must be a member of its parent directory,
+ * however. Fails if the directory was read-only.
+ */
+ private void unlink(InMemoryDirectoryInfo dir, String child, Path errorPath)
+ throws IOException {
+ if (!dir.isWritable()) { throw Error.EACCES.exception(errorPath); }
+ dir.removeChild(child);
+ }
+
+ /**
+ * Inserts inode 'childInode' into the existing directory 'dir' under the
+ * specified 'name'. Dual to unlink. Fails if the directory was read-only.
+ */
+ private void insert(InMemoryDirectoryInfo dir, String child,
+ InMemoryContentInfo childInode, Path errorPath)
+ throws IOException {
+ if (!dir.isWritable()) { throw Error.EACCES.exception(errorPath); }
+ dir.addChild(child, childInode);
+ }
+
+ /**
+ * Given an existing directory 'dir', looks up 'name' within it and returns
+ * its inode. Assumes the file exists, unless 'create', in which case it will
+ * try to create it. May fail with ENOTDIR, EACCES, ENOENT. Error messages
+ * will be reported against file 'path'.
+ */
+ private InMemoryContentInfo directoryLookup(InMemoryContentInfo dir,
+ String name,
+ boolean create,
+ Path path) throws IOException {
+ if (!dir.isDirectory()) { throw Error.ENOTDIR.exception(path); }
+ InMemoryDirectoryInfo imdi = (InMemoryDirectoryInfo) dir;
+ if (!imdi.isExecutable()) { throw Error.EACCES.exception(path); }
+ InMemoryContentInfo child = imdi.getChild(name);
+ if (child == null) {
+ if (!create) {
+ throw Error.ENOENT.exception(path);
+ } else {
+ child = makeFileInfo(clock, path.asFragment());
+ insert(imdi, name, child, path);
+ }
+ }
+ return child;
+ }
+
+ /**
+ * Low-level path-to-inode lookup routine. Analogous to path_walk() in many
+ * UNIX kernels. Given 'path', walks the directory tree from the root,
+ * resolving all symbolic links, and returns the designated inode.
+ *
+ * <p>If 'create' is false, the inode must exist; otherwise, it will be created
+ * and added to its parent directory, which must exist.
+ *
+ * <p>Iff the given path escapes this file system's scope, the returned value
+ * is an {@link OutOfScopeFileStatus} instance. Any code that calls this method
+ * needs to check for that possibility (via {@link ScopeEscapableStatus#outOfScope}).
+ *
+ * <p>May fail with ENOTDIR, ENOENT, EACCES, ELOOP.
+ */
+ private synchronized InMemoryContentInfo pathWalk(Path path, boolean create)
+ throws IOException {
+ // Implementation note: This is where we check for out-of-scope symlinks and
+ // trigger re-delegation to another file system accordingly. This code handles
+ // both absolute and relative symlinks. Some assumptions we make: First, only
+ // symlink targets as read from getNormalizedLinkContent() can escape our scope.
+ // This is because Path objects are all canonicalized (see {@link Path#getRelative},
+ // etc.) and symlink target segments that get added to the stack are in-scope by
+ // definition. Second, symlink targets with relative segments must have the form
+ // [".."]*[standard segment]+, i.e. only the ".." non-standard segment is allowed
+ // and it may only appear as part of a contiguous prefix sequence.
+
+ Stack<String> stack = new Stack<>();
+ PathFragment rootPathFragment = rootPath.asFragment();
+ for (Path p = path; !p.asFragment().equals(rootPathFragment); p = p.getParentDirectory()) {
+ stack.push(p.getBaseName());
+ }
+
+ InMemoryContentInfo inode = rootInode;
+ int parentDepth = -1;
+ int traversals = 0;
+
+ while (!stack.isEmpty()) {
+ traversals++;
+
+ String name = stack.pop();
+ parentDepth += name.equals("..") ? -1 : 1;
+
+ // ENOENT on last segment with 'create' => create a new file.
+ InMemoryContentInfo child = directoryLookup(inode, name, create && stack.isEmpty(), path);
+ if (child.isSymbolicLink()) {
+ PathFragment linkTarget = ((InMemoryLinkInfo) child).getNormalizedLinkContent();
+ if (!inScope(parentDepth, linkTarget)) {
+ return outOfScopeStatus(linkTarget, parentDepth, stack);
+ }
+ if (linkTarget.isAbsolute()) {
+ inode = rootInode;
+ parentDepth = -1;
+ }
+ if (traversals > MAX_TRAVERSALS) {
+ throw Error.ELOOP.exception(path);
+ }
+ for (int ii = linkTarget.segmentCount() - 1; ii >= 0; --ii) {
+ stack.push(linkTarget.getSegment(ii)); // Note this may include ".." segments.
+ }
+ } else {
+ inode = child;
+ }
+ }
+ return inode;
+ }
+
+ /**
+ * Helper routine for pathWalk: given a symlink target known to escape this file system's
+ * scope (and that has the form [".."]*[standard segment]+), the number of segments
+ * in the directory containing the symlink, and the remaining path segments following
+ * the symlink in the original input to pathWalk, returns an OutofScopeFileStatus
+ * initialized with an appropriate out-of-scope reformulation of pathWalk's original
+ * input.
+ */
+ private OutOfScopeFileStatus outOfScopeStatus(PathFragment linkTarget, int parentDepth,
+ Stack<String> descendantSegments) {
+
+ PathFragment escapingPath;
+ if (linkTarget.isAbsolute()) {
+ escapingPath = linkTarget;
+ } else {
+ // Relative out-of-scope paths must look like "../../../a/b/c". Find the target's
+ // parent path depth by subtracting one from parentDepth for each ".." reference.
+ // Then use that to retrieve a prefix of the scope root, which is the target's
+ // canonicalized parent path.
+ int leadingParentRefs = leadingParentReferences(linkTarget);
+ int baseDepth = parentDepth - leadingParentRefs;
+ Preconditions.checkState(baseDepth < scopeRoot.segmentCount());
+ escapingPath = baseDepth > 0
+ ? scopeRoot.subFragment(0, baseDepth)
+ : scopeRoot.subFragment(0, 0);
+ // Now add in everything that comes after the ".." sequence.
+ for (int i = leadingParentRefs; i < linkTarget.segmentCount(); i++) {
+ escapingPath = escapingPath.getRelative(linkTarget.getSegment(i));
+ }
+ }
+
+ // We've now converted the symlink to its target in canonicalized absolute path
+ // form. Since the symlink wasn't necessarily the final segment in the original
+ // input sent to pathWalk, now add in every segment that came after.
+ while (!descendantSegments.empty()) {
+ escapingPath = escapingPath.getRelative(descendantSegments.pop());
+ }
+
+ return new OutOfScopeFileStatus(escapingPath);
+ }
+
+ /**
+ * Given 'path', returns the existing directory inode it designates,
+ * following symbolic links.
+ *
+ * <p>May fail with ENOTDIR, or any exception from pathWalk.
+ *
+ * <p>Iff the given path escapes this file system's scope, this method skips
+ * ENOTDIR checking and returns an OutOfScopeDirectoryStatus instance. Any
+ * code that calls this method needs to check for that possibility
+ * (via {@link ScopeEscapableStatus#outOfScope}).
+ */
+ private InMemoryDirectoryInfo getDirectory(Path path) throws IOException {
+ InMemoryContentInfo dirInfo = pathWalk(path, false);
+ if (dirInfo.outOfScope()) {
+ return new OutOfScopeDirectoryStatus(dirInfo.getEscapingPath());
+ } else if (!dirInfo.isDirectory()) {
+ throw Error.ENOTDIR.exception(path);
+ } else {
+ return (InMemoryDirectoryInfo) dirInfo;
+ }
+ }
+
+ /**
+ * Helper method for stat, scopeLimitedStat: lock the internal state and return the
+ * path's (no symlink-followed) stat if the path's parent directory is within scope,
+ * else return an "out of scope" reference to the path's parent directory (which will
+ * presumably be re-delegated to another FS).
+ */
+ private synchronized InMemoryContentInfo getNoFollowStatOrOutOfScopeParent(Path path)
+ throws IOException {
+ InMemoryDirectoryInfo dirInfo = getDirectory(path.getParentDirectory());
+ return dirInfo.outOfScope()
+ ? dirInfo
+ : directoryLookup(dirInfo, path.getBaseName(), /*create=*/false, path);
+ }
+
+ /**
+ * Given 'path', returns the existing inode it designates, optionally
+ * following symbolic links. Analogous to UNIX stat(2)/lstat(2), except that
+ * it returns a mutable inode we can modify directly.
+ */
+ @Override
+ public FileStatus stat(Path path, boolean followSymlinks) throws IOException {
+ if (followSymlinks) {
+ InMemoryContentInfo status = scopeLimitedStat(path, true);
+ return status.outOfScope()
+ ? statWithDelegator(status.getEscapingPath(), true)
+ : status;
+ } else {
+ if (path.equals(rootPath)) {
+ return rootInode;
+ } else {
+ InMemoryContentInfo status = getNoFollowStatOrOutOfScopeParent(path);
+ // If out of scope, status references the path's parent directory. Else it references the
+ // path itself.
+ return status.outOfScope()
+ ? getDelegatedPath(status.getEscapingPath().getRelative(
+ path.getBaseName())).stat(Symlinks.NOFOLLOW)
+ : status;
+ }
+ }
+ }
+
+ @Override
+ @Nullable
+ public FileStatus statIfFound(Path path, boolean followSymlinks) throws IOException {
+ try {
+ return stat(path, followSymlinks);
+ } catch (IOException e) {
+ if (e instanceof Error.WithError) {
+ Error errorCode = ((Error.WithError) e).getError();
+ if (errorCode == Error.ENOENT || errorCode == Error.ENOTDIR) {
+ return null;
+ }
+ }
+ throw e;
+ }
+ }
+
+ /**
+ * Version of stat that returns an inode if the input path stays entirely within
+ * this file system's scope, otherwise an {@link OutOfScopeFileStatus}.
+ *
+ * <p>Any code that calls this method needs to check for either possibility via
+ * {@link ScopeEscapableStatus#outOfScope}.
+ */
+ protected InMemoryContentInfo scopeLimitedStat(Path path, boolean followSymlinks)
+ throws IOException {
+ if (followSymlinks) {
+ return pathWalk(path, false);
+ } else {
+ if (path.equals(rootPath)) {
+ return rootInode;
+ } else {
+ InMemoryContentInfo status = getNoFollowStatOrOutOfScopeParent(path);
+ // If out of scope, status references the path's parent directory. Else it references the
+ // path itself.
+ return status.outOfScope()
+ ? new OutOfScopeFileStatus(status.getEscapingPath().getRelative(path.getBaseName()))
+ : status;
+ }
+ }
+ }
+
+ /****************************************************************************
+ * FileSystem methods
+ */
+
+ /**
+ * This is a helper routing for {@link #resolveSymbolicLinks(Path)}, i.e.
+ * the "user-mode" routing for canonicalising paths. It is analogous to the
+ * code in glibc's realpath(3).
+ *
+ * <p>Just like realpath, resolveSymbolicLinks requires a quadratic number of
+ * directory lookups: n path segments are statted, and each stat requires a
+ * linear amount of work in the "kernel" routine.
+ */
+ @Override
+ protected PathFragment resolveOneLink(Path path) throws IOException {
+ // Beware, this seemingly simple code belies the complex specification of
+ // FileSystem.resolveOneLink().
+ InMemoryContentInfo status = scopeLimitedStat(path, false);
+ if (status.outOfScope()) {
+ return resolveOneLinkWithDelegator(status.getEscapingPath());
+ } else {
+ return status.isSymbolicLink()
+ ? ((InMemoryLinkInfo) status).getLinkContent()
+ : null;
+ }
+ }
+
+ @Override
+ protected boolean isDirectory(Path path, boolean followSymlinks) {
+ try {
+ return stat(path, followSymlinks).isDirectory();
+ } catch (IOException e) {
+ return false;
+ }
+ }
+
+ @Override
+ protected boolean isFile(Path path, boolean followSymlinks) {
+ try {
+ return stat(path, followSymlinks).isFile();
+ } catch (IOException e) {
+ return false;
+ }
+ }
+
+ @Override
+ protected boolean isSymbolicLink(Path path) {
+ try {
+ return stat(path, false).isSymbolicLink();
+ } catch (IOException e) {
+ return false;
+ }
+ }
+
+ @Override
+ protected boolean exists(Path path, boolean followSymlinks) {
+ try {
+ stat(path, followSymlinks);
+ return true;
+ } catch (IOException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Like {@link #exists}, but checks for existence within this filesystem's scope.
+ */
+ protected boolean scopeLimitedExists(Path path, boolean followSymlinks) {
+ try {
+ // Path#asFragment() always returns an absolute path, so inScope() is called with
+ // parentDepth = 0.
+ return inScope(0, path.asFragment()) && !scopeLimitedStat(path, followSymlinks).outOfScope();
+ } catch (IOException e) {
+ return false;
+ }
+ }
+
+ @Override
+ protected boolean isReadable(Path path) throws IOException {
+ InMemoryContentInfo status = scopeLimitedStat(path, true);
+ return status.outOfScope()
+ ? getDelegatedPath(status.getEscapingPath()).isReadable()
+ : status.isReadable();
+ }
+
+ @Override
+ protected void setReadable(Path path, boolean readable) throws IOException {
+ InMemoryContentInfo status;
+ synchronized (this) {
+ status = scopeLimitedStat(path, true);
+ if (!status.outOfScope()) {
+ status.setReadable(readable);
+ return;
+ }
+ }
+ // If we get here, we're out of scope.
+ getDelegatedPath(status.getEscapingPath()).setReadable(readable);
+ }
+
+ @Override
+ protected boolean isWritable(Path path) throws IOException {
+ InMemoryContentInfo status = scopeLimitedStat(path, true);
+ return status.outOfScope()
+ ? getDelegatedPath(status.getEscapingPath()).isWritable()
+ : status.isWritable();
+ }
+
+ @Override
+ protected void setWritable(Path path, boolean writable) throws IOException {
+ InMemoryContentInfo status;
+ synchronized (this) {
+ status = scopeLimitedStat(path, true);
+ if (!status.outOfScope()) {
+ status.setWritable(writable);
+ return;
+ }
+ }
+ // If we get here, we're out of scope.
+ getDelegatedPath(status.getEscapingPath()).setWritable(writable);
+ }
+
+ @Override
+ protected boolean isExecutable(Path path) throws IOException {
+ InMemoryContentInfo status = scopeLimitedStat(path, true);
+ return status.outOfScope()
+ ? getDelegatedPath(status.getEscapingPath()).isExecutable()
+ : status.isExecutable();
+ }
+
+ @Override
+ protected void setExecutable(Path path, boolean executable)
+ throws IOException {
+ InMemoryContentInfo status;
+ synchronized (this) {
+ status = scopeLimitedStat(path, true);
+ if (!status.outOfScope()) {
+ status.setExecutable(executable);
+ return;
+ }
+ }
+ // If we get here, we're out of scope.
+ getDelegatedPath(status.getEscapingPath()).setExecutable(executable);
+ }
+
+ @Override
+ public boolean supportsModifications() {
+ return true;
+ }
+
+ @Override
+ public boolean supportsSymbolicLinks() {
+ return true;
+ }
+
+ /**
+ * Constructs a new inode. Provided so that subclasses of InMemoryFileSystem
+ * can inject subclasses of FileInfo properly.
+ */
+ protected FileInfo makeFileInfo(Clock clock, PathFragment frag) {
+ return new InMemoryFileInfo(clock);
+ }
+
+ /**
+ * Returns a new path constructed by appending the child's base name to the
+ * escaped parent path. For example, assume our file system root is /foo
+ * and /foo/link1 -> /bar. This method can be used on child = /foo/link1/link2/name
+ * and parent = /bar/link2 to return /bar/link2/name, which is a semi-resolved
+ * path bound to a different file system.
+ */
+ private Path getDelegatedPath(PathFragment escapedParent, Path child) {
+ return getDelegatedPath(escapedParent.getRelative(child.getBaseName()));
+ }
+
+ @Override
+ protected boolean createDirectory(Path path) throws IOException {
+ if (path.equals(rootPath)) { throw Error.EACCES.exception(path); }
+
+ InMemoryDirectoryInfo parent;
+ synchronized (this) {
+ parent = getDirectory(path.getParentDirectory());
+ if (!parent.outOfScope()) {
+ InMemoryContentInfo child = parent.getChild(path.getBaseName());
+ if (child != null) { // already exists
+ if (child.isDirectory()) {
+ return false;
+ } else {
+ throw Error.EEXIST.exception(path);
+ }
+ }
+
+ InMemoryDirectoryInfo newDir = new InMemoryDirectoryInfo(clock);
+ newDir.addChild(".", newDir);
+ newDir.addChild("..", parent);
+ insert(parent, path.getBaseName(), newDir, path);
+
+ return true;
+ }
+ }
+
+ // If we get here, we're out of scope.
+ return getDelegatedPath(parent.getEscapingPath(), path).createDirectory();
+ }
+
+ @Override
+ protected void createSymbolicLink(Path path, PathFragment targetFragment)
+ throws IOException {
+ if (path.equals(rootPath)) { throw Error.EACCES.exception(path); }
+
+ InMemoryDirectoryInfo parent;
+ synchronized (this) {
+ parent = getDirectory(path.getParentDirectory());
+ if (!parent.outOfScope()) {
+ if (parent.getChild(path.getBaseName()) != null) { throw Error.EEXIST.exception(path); }
+ insert(parent, path.getBaseName(), new InMemoryLinkInfo(clock, targetFragment), path);
+ return;
+ }
+ }
+
+ // If we get here, we're out of scope.
+ getDelegatedPath(parent.getEscapingPath(), path).createSymbolicLink(targetFragment);
+ }
+
+ @Override
+ protected PathFragment readSymbolicLink(Path path) throws IOException {
+ InMemoryContentInfo status = scopeLimitedStat(path, false);
+ if (status.outOfScope()) {
+ return getDelegatedPath(status.getEscapingPath()).readSymbolicLink();
+ } else if (status.isSymbolicLink()) {
+ Preconditions.checkState(status instanceof InMemoryLinkInfo);
+ return ((InMemoryLinkInfo) status).getLinkContent();
+ } else {
+ throw new NotASymlinkException(path);
+ }
+ }
+
+ @Override
+ protected long getFileSize(Path path, boolean followSymlinks)
+ throws IOException {
+ return stat(path, followSymlinks).getSize();
+ }
+
+ @Override
+ protected Collection<Path> getDirectoryEntries(Path path) throws IOException {
+ InMemoryDirectoryInfo dirInfo;
+ synchronized (this) {
+ dirInfo = getDirectory(path);
+ if (!dirInfo.outOfScope()) {
+ FileStatus status = stat(path, false);
+ Preconditions.checkState(status instanceof InMemoryContentInfo);
+ if (!((InMemoryContentInfo) status).isReadable()) {
+ throw new IOException("Directory is not readable");
+ }
+
+ Set<String> allChildren = dirInfo.getAllChildren();
+ List<Path> result = new ArrayList<>(allChildren.size());
+ for (String child : allChildren) {
+ if (!(child.equals(".") || child.equals(".."))) {
+ result.add(path.getChild(child));
+ }
+ }
+ return result;
+ }
+ }
+
+ // If we get here, we're out of scope.
+ return getDelegatedPath(dirInfo.getEscapingPath()).getDirectoryEntries();
+ }
+
+ @Override
+ protected boolean delete(Path path) throws IOException {
+ if (path.equals(rootPath)) { throw Error.EBUSY.exception(path); }
+ if (!exists(path, false)) { return false; }
+
+ InMemoryDirectoryInfo parent;
+ synchronized (this) {
+ parent = getDirectory(path.getParentDirectory());
+ if (!parent.outOfScope()) {
+ InMemoryContentInfo child = parent.getChild(path.getBaseName());
+ if (child.isDirectory() && child.getSize() > 2) { throw Error.ENOTEMPTY.exception(path); }
+ unlink(parent, path.getBaseName(), path);
+ return true;
+ }
+ }
+
+ // If we get here, we're out of scope.
+ return getDelegatedPath(parent.getEscapingPath(), path).delete();
+ }
+
+ @Override
+ protected long getLastModifiedTime(Path path, boolean followSymlinks)
+ throws IOException {
+ return stat(path, followSymlinks).getLastModifiedTime();
+ }
+
+ @Override
+ protected void setLastModifiedTime(Path path, long newTime) throws IOException {
+ InMemoryContentInfo status;
+ synchronized (this) {
+ status = scopeLimitedStat(path, true);
+ if (!status.outOfScope()) {
+ status.setLastModifiedTime(newTime == -1L
+ ? clock.currentTimeMillis()
+ : newTime);
+ return;
+ }
+ }
+
+ // If we get here, we're out of scope.
+ getDelegatedPath(status.getEscapingPath()).setLastModifiedTime(newTime);
+ }
+
+ @Override
+ protected InputStream getInputStream(Path path) throws IOException {
+ InMemoryContentInfo status;
+ synchronized (this) {
+ status = scopeLimitedStat(path, true);
+ if (!status.outOfScope()) {
+ if (status.isDirectory()) { throw Error.EISDIR.exception(path); }
+ if (!path.isReadable()) { throw Error.EACCES.exception(path); }
+ Preconditions.checkState(status instanceof FileInfo);
+ return new ByteArrayInputStream(((FileInfo) status).readContent());
+ }
+ }
+
+ // If we get here, we're out of scope.
+ return getDelegatedPath(status.getEscapingPath()).getInputStream();
+ }
+
+ /**
+ * Creates a new file at the given path and returns its inode. If the path
+ * escapes this file system's scope, trivially returns an "out of scope" status.
+ * Calling code should check for both possibilities via
+ * {@link ScopeEscapableStatus#outOfScope}.
+ */
+ protected InMemoryContentInfo getOrCreateWritableInode(Path path)
+ throws IOException {
+ // open(WR_ONLY) of a dangling link writes through the link. That means
+ // that the usual path lookup operations have to behave differently when
+ // resolving a path with the intent to create it: instead of failing with
+ // ENOENT they have to return an open file. This is exactly how UNIX
+ // kernels do it, which is what we're trying to emulate.
+ InMemoryContentInfo child = pathWalk(path, /*create=*/true);
+ Preconditions.checkNotNull(child);
+ if (child.outOfScope()) {
+ return child;
+ } else if (child.isDirectory()) {
+ throw Error.EISDIR.exception(path);
+ } else { // existing or newly-created file
+ if (!child.isWritable()) { throw Error.EACCES.exception(path); }
+ return child;
+ }
+ }
+
+ @Override
+ protected OutputStream getOutputStream(Path path, boolean append)
+ throws IOException {
+ InMemoryContentInfo status;
+ synchronized (this) {
+ status = getOrCreateWritableInode(path);
+ if (!status.outOfScope()) {
+ return ((FileInfo) getOrCreateWritableInode(path)).getOutputStream(append);
+ }
+ }
+ // If we get here, we're out of scope.
+ return getDelegatedPath(status.getEscapingPath()).getOutputStream(append);
+ }
+
+ @Override
+ protected void renameTo(Path sourcePath, Path targetPath)
+ throws IOException {
+ if (sourcePath.equals(rootPath)) { throw Error.EACCES.exception(sourcePath); }
+ if (targetPath.equals(rootPath)) { throw Error.EACCES.exception(targetPath); }
+
+ InMemoryDirectoryInfo sourceParent;
+ InMemoryDirectoryInfo targetParent;
+
+ synchronized (this) {
+ sourceParent = getDirectory(sourcePath.getParentDirectory());
+ targetParent = getDirectory(targetPath.getParentDirectory());
+
+ // Handle the rename if both paths are within our scope.
+ if (!sourceParent.outOfScope() && !targetParent.outOfScope()) {
+ InMemoryContentInfo sourceInode = sourceParent.getChild(sourcePath.getBaseName());
+ if (sourceInode == null) { throw Error.ENOENT.exception(sourcePath); }
+ InMemoryContentInfo targetInode = targetParent.getChild(targetPath.getBaseName());
+
+ unlink(sourceParent, sourcePath.getBaseName(), sourcePath);
+ try {
+ // TODO(bazel-team): (2009) test with symbolic links.
+
+ // Precondition checks:
+ if (targetInode != null) { // already exists
+ if (targetInode.isDirectory()) {
+ if (!sourceInode.isDirectory()) {
+ throw new IOException(sourcePath + " -> " + targetPath + " (" + Error.EISDIR + ")");
+ }
+ if (targetInode.getSize() > 2) {
+ throw Error.ENOTEMPTY.exception(targetPath);
+ }
+ } else if (sourceInode.isDirectory()) {
+ throw new IOException(sourcePath + " -> " + targetPath + " (" + Error.ENOTDIR + ")");
+ }
+ unlink(targetParent, targetPath.getBaseName(), targetPath);
+ }
+ sourceInode.movedTo(targetPath);
+ insert(targetParent, targetPath.getBaseName(), sourceInode, targetPath);
+ return;
+
+ } catch (IOException e) {
+ sourceInode.movedTo(sourcePath);
+ insert(sourceParent, sourcePath.getBaseName(), sourceInode, sourcePath); // restore source
+ throw e;
+ }
+ }
+ }
+
+ // If we get here, either one or both paths is out of scope.
+ if (sourceParent.outOfScope() && targetParent.outOfScope()) {
+ Path delegatedSource = getDelegatedPath(sourceParent.getEscapingPath(), sourcePath);
+ Path delegatedTarget = getDelegatedPath(targetParent.getEscapingPath(), targetPath);
+ delegatedSource.renameTo(delegatedTarget);
+ } else {
+ // We don't support cross-file system renaming.
+ throw Error.EACCES.exception(targetPath);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryLinkInfo.java b/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryLinkInfo.java
new file mode 100644
index 0000000..f8837ee
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryLinkInfo.java
@@ -0,0 +1,76 @@
+// Copyright 2014 Google Inc. 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.vfs.inmemoryfs;
+
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.util.Clock;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+/**
+ * This interface represents a symbolic link to an absolute or relative path,
+ * stored in an InMemoryFileSystem.
+ */
+@ThreadSafe @Immutable
+class InMemoryLinkInfo extends InMemoryContentInfo {
+
+ private final PathFragment linkContent;
+ private final PathFragment normalizedLinkContent;
+
+ InMemoryLinkInfo(Clock clock, PathFragment linkContent) {
+ super(clock);
+ this.linkContent = linkContent;
+ this.normalizedLinkContent = linkContent.normalize();
+ }
+
+ @Override
+ public boolean isDirectory() {
+ return false;
+ }
+
+ @Override
+ public boolean isSymbolicLink() {
+ return true;
+ }
+
+ @Override
+ public boolean isFile() {
+ return false;
+ }
+
+ @Override
+ public long getSize() {
+ return linkContent.toString().length();
+ }
+
+ /**
+ * Returns the content of the symbolic link.
+ */
+ PathFragment getLinkContent() {
+ return linkContent;
+ }
+
+ /**
+ * Returns the content of the symbolic link, with ".." and "." removed
+ * (except for the possibility of necessary ".." segments at the beginning).
+ */
+ PathFragment getNormalizedLinkContent() {
+ return normalizedLinkContent;
+ }
+
+ @Override
+ public String toString() {
+ return super.toString() + " -> " + linkContent;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/OutOfScopeDirectoryStatus.java b/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/OutOfScopeDirectoryStatus.java
new file mode 100644
index 0000000..b757acd
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/OutOfScopeDirectoryStatus.java
@@ -0,0 +1,70 @@
+// Copyright 2014 Google Inc. 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.vfs.inmemoryfs;
+
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.util.Set;
+
+/**
+ * A directory status that signifies a path has left this file system's
+ * scope. All methods beside {@link #outOfScope} and {@link #getEscapingPath}
+ * are disabled.
+ */
+final class OutOfScopeDirectoryStatus extends InMemoryDirectoryInfo {
+ /**
+ * Contains the requested path resolved up to the point where it
+ * first escapes the scope. See
+ * {@link ScopeEscapableStatus#getEscapingPath} for an example.
+ */
+ private final PathFragment escapingPath;
+
+ public OutOfScopeDirectoryStatus(PathFragment escapingPath) {
+ super(null, false);
+ this.escapingPath = escapingPath;
+ }
+
+ @Override
+ public boolean outOfScope() {
+ return true;
+ }
+
+ @Override
+ public PathFragment getEscapingPath() {
+ return escapingPath;
+ }
+
+ private static UnsupportedOperationException failure() {
+ return new UnsupportedOperationException();
+ }
+
+ @Override public boolean isDirectory() { throw failure(); }
+ @Override public boolean isSymbolicLink() { throw failure(); }
+ @Override public boolean isFile() { throw failure(); }
+ @Override public long getSize() { throw failure(); }
+ @Override protected void markModificationTime() { throw failure(); }
+ @Override public synchronized long getLastModifiedTime() { throw failure(); }
+ @Override void setLastModifiedTime(long newTime) { throw failure(); }
+ @Override public synchronized long getLastChangeTime() { throw failure(); }
+ @Override boolean isReadable() { throw failure(); }
+ @Override void setReadable(boolean readable) { throw failure(); }
+ @Override void setWritable(boolean writable) { throw failure(); }
+ @Override void setExecutable(boolean executable) { throw failure(); }
+ @Override boolean isWritable() { throw failure(); }
+ @Override boolean isExecutable() { throw failure(); }
+ @Override void addChild(String name, InMemoryContentInfo inode) { throw failure(); }
+ @Override InMemoryContentInfo getChild(String name) { throw failure(); }
+ @Override void removeChild(String name) { throw failure(); }
+ @Override Set<String> getAllChildren() { throw failure(); }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/OutOfScopeFileStatus.java b/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/OutOfScopeFileStatus.java
new file mode 100644
index 0000000..177ac11
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/OutOfScopeFileStatus.java
@@ -0,0 +1,65 @@
+// Copyright 2014 Google Inc. 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.vfs.inmemoryfs;
+
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+/**
+ * A file status that signifies a path has left this file system's
+ * scope. All methods beside {@link #outOfScope} and {@link #getEscapingPath}
+ * are disabled.
+ */
+final class OutOfScopeFileStatus extends InMemoryContentInfo {
+
+ /**
+ * Contains the requested path resolved up to the point where it
+ * first escapes the scope. See
+ * {@link ScopeEscapableStatus#getEscapingPath} for an example.
+ */
+ private final PathFragment escapingPath;
+
+ public OutOfScopeFileStatus(PathFragment escapingPath) {
+ super(null, false);
+ this.escapingPath = escapingPath;
+ }
+
+ @Override
+ public boolean outOfScope() {
+ return true;
+ }
+
+ @Override
+ public PathFragment getEscapingPath() {
+ return escapingPath;
+ }
+
+ private static UnsupportedOperationException failure() {
+ return new UnsupportedOperationException();
+ }
+
+ @Override public boolean isDirectory() { throw failure(); }
+ @Override public boolean isSymbolicLink() { throw failure(); }
+ @Override public boolean isFile() { throw failure(); }
+ @Override public long getSize() { throw failure(); }
+ @Override protected void markModificationTime() { throw failure(); }
+ @Override public synchronized long getLastModifiedTime() { throw failure(); }
+ @Override void setLastModifiedTime(long newTime) { throw failure(); }
+ @Override public synchronized long getLastChangeTime() { throw failure(); }
+ @Override boolean isReadable() { throw failure(); }
+ @Override void setReadable(boolean readable) { throw failure(); }
+ @Override void setWritable(boolean writable) { throw failure(); }
+ @Override boolean isWritable() { throw failure(); }
+ @Override void setExecutable(boolean executable) { throw failure(); }
+ @Override boolean isExecutable() { throw failure(); }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/ScopeEscapableStatus.java b/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/ScopeEscapableStatus.java
new file mode 100644
index 0000000..4afec78
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/ScopeEscapableStatus.java
@@ -0,0 +1,46 @@
+// Copyright 2014 Google Inc. 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.vfs.inmemoryfs;
+
+import com.google.devtools.build.lib.vfs.FileStatus;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.lib.vfs.ScopeEscapableFileSystem;
+
+/**
+ * Interface definition for a file status that may signify that the
+ * referenced path falls outside the scope of the file system (see
+ * {@link ScopeEscapableFileSystem}) and can provide the "escaped"
+ * version of that path suitable for re-delegation to another file
+ * system.
+ */
+interface ScopeEscapableStatus extends FileStatus {
+
+ /**
+ * Returns true if this status corresponds to a path that leaves
+ * the file system's scope, false otherwise.
+ */
+ boolean outOfScope();
+
+ /**
+ * If this status represents a path that leaves the file system's scope,
+ * returns the requested path resolved up to the point where it first
+ * escapes the file system. For example: if the file system is mapped to
+ * /foo, the requested path is /foo/link1/link2/link3, and link1 -> /bar,
+ * this returns /bar/link2/link3.
+ *
+ * <p>If this status doesn't represent a scope-escaping path, returns
+ * null.
+ */
+ PathFragment getEscapingPath();
+}
diff --git a/src/main/java/com/google/devtools/build/lib/webstatusserver/IndexPageHandler.java b/src/main/java/com/google/devtools/build/lib/webstatusserver/IndexPageHandler.java
new file mode 100644
index 0000000..c9eb3ed
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/webstatusserver/IndexPageHandler.java
@@ -0,0 +1,82 @@
+// Copyright 2014 Google Inc. 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.webstatusserver;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gson.Gson;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+import com.sun.net.httpserver.HttpServer;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+
+/**
+ * Handlers for displaying the index page of server.
+ *
+ */
+public class IndexPageHandler {
+ private List<TestStatusHandler> testHandlers = new ArrayList<>();
+ private IndexPageJsonData dataHandler;
+ private StaticResourceHandler frontendHandler;
+
+ public IndexPageHandler(HttpServer server, List<TestStatusHandler> testHandlers) {
+ this.testHandlers = testHandlers;
+ this.dataHandler = new IndexPageJsonData(this);
+ this.frontendHandler =
+ StaticResourceHandler.createFromRelativePath("static/index.html", "text/html");
+ server.createContext("/", frontendHandler);
+ server.createContext("/tests/list", dataHandler);
+ }
+
+ /**
+ * Puts data from the build log into json suitable for frontend.
+ *
+ */
+ private class IndexPageJsonData implements HttpHandler {
+ private IndexPageHandler pageHandler;
+ private Gson gson = new Gson();
+ public IndexPageJsonData(IndexPageHandler indexPageHandler) {
+ this.pageHandler = indexPageHandler;
+ }
+
+ @Override
+ public void handle(HttpExchange exchange) throws IOException {
+ exchange.getResponseHeaders().put("Content-Type", ImmutableList.of("application/json"));
+ JsonArray response = new JsonArray();
+ for (TestStatusHandler handler : this.pageHandler.testHandlers) {
+ WebStatusBuildLog buildLog = handler.getBuildLog();
+ JsonObject test = new JsonObject();
+ test.add("targets", gson.toJsonTree(buildLog.getTargetList()));
+ test.addProperty("startTime", buildLog.getStartTime());
+ test.addProperty("finished", buildLog.finished());
+ test.addProperty("uuid", buildLog.getCommandId().toString());
+ response.add(test);
+ }
+ String serializedResponse = response.toString();
+ exchange.sendResponseHeaders(200, serializedResponse.length());
+ OutputStream os = exchange.getResponseBody();
+ os.write(serializedResponse.getBytes());
+ os.close();
+ }
+ }
+}
+
diff --git a/src/main/java/com/google/devtools/build/lib/webstatusserver/StaticResourceHandler.java b/src/main/java/com/google/devtools/build/lib/webstatusserver/StaticResourceHandler.java
new file mode 100644
index 0000000..cd9eb5f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/webstatusserver/StaticResourceHandler.java
@@ -0,0 +1,80 @@
+// Copyright 2014 Google Inc. 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.webstatusserver;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.io.CharStreams;
+import com.google.devtools.build.lib.util.ResourceFileLoader;
+
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.URL;
+import java.util.List;
+
+/**
+ * Handler for static resources (JS, html, css...)
+ */
+public class StaticResourceHandler implements HttpHandler {
+ private String response;
+ private List<String> contentType;
+ private int httpCode;
+
+ public static StaticResourceHandler createFromAbsolutePath(String path, String contentType) {
+ return new StaticResourceHandler(path, contentType, true);
+ }
+
+ public static StaticResourceHandler createFromRelativePath(String path, String contentType) {
+ return new StaticResourceHandler(path, contentType, false);
+ }
+
+ private StaticResourceHandler(String path, String contentType, boolean absolutePath) {
+ try {
+ if (absolutePath) {
+ InputStream resourceStream = loadFromAbsolutePath(WebStatusServerModule.class, path);
+ response = CharStreams.toString(new InputStreamReader(resourceStream));
+
+ } else {
+ response = ResourceFileLoader.loadResource(WebStatusServerModule.class, path);
+ }
+ httpCode = 200;
+ } catch (IOException e) {
+ throw new IllegalArgumentException("resource " + path + " not found");
+ }
+ this.contentType = ImmutableList.of(contentType);
+ }
+
+ @Override
+ public void handle(HttpExchange exchange) throws IOException {
+ exchange.getResponseHeaders().put("Content-Type", contentType);
+ exchange.sendResponseHeaders(httpCode, response.length());
+ OutputStream os = exchange.getResponseBody();
+ os.write(response.getBytes());
+ os.close();
+ }
+
+ public static InputStream loadFromAbsolutePath(Class<?> loadingClass, String path)
+ throws IOException {
+ URL resourceUrl = loadingClass.getClassLoader().getResource(path);
+ if (resourceUrl == null) {
+ throw new IllegalArgumentException("resource " + path + " not found");
+ }
+ return resourceUrl.openStream();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/webstatusserver/TestStatusHandler.java b/src/main/java/com/google/devtools/build/lib/webstatusserver/TestStatusHandler.java
new file mode 100644
index 0000000..41cb06d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/webstatusserver/TestStatusHandler.java
@@ -0,0 +1,148 @@
+// Copyright 2014 Google Inc. 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.webstatusserver;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableList.Builder;
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.reflect.TypeToken;
+
+import com.sun.net.httpserver.HttpContext;
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+import com.sun.net.httpserver.HttpServer;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.lang.reflect.Type;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * Collection of handlers for displaying the test data.
+ */
+class TestStatusHandler {
+ private StaticResourceHandler frontendHandler;
+ private WebStatusBuildLog buildLog;
+ private HttpHandler detailsHandler;
+ private HttpServer server;
+ private ImmutableList<HttpContext> contexts;
+ private CommandJsonData commandHandler;
+ private Gson gson = new Gson();
+
+ public TestStatusHandler(HttpServer server, WebStatusBuildLog buildLog) {
+ Builder<HttpContext> builder = ImmutableList.builder();
+ this.buildLog = buildLog;
+ this.server = server;
+ detailsHandler = new TestStatusResultJsonData(this);
+ commandHandler = new CommandJsonData(this);
+ frontendHandler = StaticResourceHandler.createFromRelativePath("static/test.html", "text/html");
+ builder.add(
+ server.createContext("/tests/" + buildLog.getCommandId() + "/details", detailsHandler));
+ builder.add(
+ server.createContext("/tests/" + buildLog.getCommandId() + "/info", commandHandler));
+ builder.add(server.createContext("/tests/" + buildLog.getCommandId(), frontendHandler));
+ contexts = builder.build();
+ }
+
+ public WebStatusBuildLog getBuildLog() {
+ return buildLog;
+ }
+
+
+ /**
+ * Serves JSON objects containing command info, which will be rendered by frontend.
+ */
+ private class CommandJsonData implements HttpHandler {
+ private TestStatusHandler testStatusHandler;
+
+ public CommandJsonData(TestStatusHandler testStatusHandler) {
+ this.testStatusHandler = testStatusHandler;
+ }
+
+ @Override
+ public void handle(HttpExchange exchange) throws IOException {
+ exchange.getResponseHeaders().put("Content-Type", ImmutableList.of("application/json"));
+ Type commandInfoType = new TypeToken<Map<String, JsonElement>>() {}.getType();
+ JsonObject response = gson.toJsonTree(testStatusHandler.buildLog.getCommandInfo(),
+ commandInfoType).getAsJsonObject();
+ response.addProperty("startTime", testStatusHandler.buildLog.getStartTime());
+ response.addProperty("finished", testStatusHandler.buildLog.finished());
+
+ String serializedResponse = response.toString();
+ exchange.sendResponseHeaders(200, serializedResponse.length());
+ OutputStream os = exchange.getResponseBody();
+ os.write(serializedResponse.getBytes());
+ os.close();
+ }
+ }
+
+ /**
+ * Serves JSON objects containing test cases, which will be rendered by frontend.
+ */
+ private class TestStatusResultJsonData implements HttpHandler {
+ private TestStatusHandler testStatusHandler;
+
+ public TestStatusResultJsonData(TestStatusHandler testStatusHandler) {
+ this.testStatusHandler = testStatusHandler;
+ }
+
+ @Override
+ public void handle(HttpExchange exchange) throws IOException {
+ Map<String, JsonObject> testInfo = testStatusHandler.buildLog.getTestCases();
+ exchange.getResponseHeaders().put("Content-Type", ImmutableList.of("application/json"));
+ JsonObject response = new JsonObject();
+ for (Entry<String, JsonObject> testCase : testInfo.entrySet()) {
+ response.add(testCase.getKey(), testCase.getValue());
+ }
+
+ String serializedResponse = response.toString();
+ exchange.sendResponseHeaders(200, serializedResponse.length());
+ OutputStream os = exchange.getResponseBody();
+ os.write(serializedResponse.getBytes());
+ os.close();
+ }
+ }
+
+ /**
+ * Adds another URI for existing test data. If specified URI is already used by some other
+ * handler, the previous handler will be removed.
+ */
+ public void overrideURI(String uri) {
+ String detailsPath = uri + "/details";
+ String commandPath = uri + "/info";
+ try {
+ this.server.removeContext(detailsPath);
+ this.server.removeContext(commandPath);
+ } catch (IllegalArgumentException e) {
+ // There was nothing to remove, so proceed with creation (unfortunately the server api doesn't
+ // have "hasContext" method)
+ }
+ this.server.createContext(detailsPath, this.detailsHandler);
+ this.server.createContext(commandPath, this.commandHandler);
+ }
+
+ /**
+ * Deregisters all the handlers associated with the test.
+ */
+ public void deregister() {
+ for (HttpContext c : this.contexts) {
+ this.server.removeContext(c);
+ }
+ }
+}
+
diff --git a/src/main/java/com/google/devtools/build/lib/webstatusserver/WebStatusBuildLog.java b/src/main/java/com/google/devtools/build/lib/webstatusserver/WebStatusBuildLog.java
new file mode 100644
index 0000000..86eed88
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/webstatusserver/WebStatusBuildLog.java
@@ -0,0 +1,200 @@
+// Copyright 2014 Google Inc. 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.webstatusserver;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.view.test.TestStatus.BlazeTestStatus;
+import com.google.devtools.build.lib.view.test.TestStatus.TestCase;
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.logging.Logger;
+
+/**
+ * Stores information about one build command. The data is stored in JSON so that it can be
+ * can be easily fed to frontend.
+ *
+ * <p> The information is grouped into following structures:
+ * <ul>
+ * <li> {@link #commandInfo} contain information about the build known when it starts but before
+ * anything is actually compiled/run
+ * <li> {@link #testCases} contain detailed information about each test case ran, for now they're
+ *
+ * </ul>
+ */
+public class WebStatusBuildLog {
+ private Gson gson = new Gson();
+ private boolean complete = false;
+ private static final Logger LOG =
+ Logger.getLogger(WebStatusEventCollector.class.getCanonicalName());
+ private Map<String, JsonElement> commandInfo = new HashMap<String, JsonElement>();
+ private Map<String, JsonObject> testCases = new HashMap<String, JsonObject>();
+ private long startTime;
+ private ImmutableList<String> targetList;
+ private UUID commandId;
+
+ public WebStatusBuildLog(UUID commandId) {
+ this.commandId = commandId;
+ }
+
+ public WebStatusBuildLog addInfo(String key, Object value) {
+ commandInfo.put(key, gson.toJsonTree(value));
+ return this;
+ }
+
+ public void addStartTime(long startTime) {
+ this.startTime = startTime;
+ }
+
+ public void addTargetList(List<String> targets) {
+ this.targetList = ImmutableList.copyOf(targets);
+ }
+
+ public void finish() {
+ commandInfo = ImmutableMap.copyOf(commandInfo);
+ complete = true;
+ }
+
+ public Map<String, JsonElement> getCommandInfo() {
+ return commandInfo;
+ }
+
+ public ImmutableMap<String, JsonObject> getTestCases() {
+ // TODO(bazel-team): not really immutable, since one can do addProperty on
+ // values (unfortunately gson doesn't support immutable JsonObjects)
+ return ImmutableMap.copyOf(testCases);
+ }
+
+ public boolean finished() {
+ return complete;
+ }
+
+ public List<String> getTargetList() {
+ return targetList;
+ }
+
+ public long getStartTime() {
+ return startTime;
+ }
+
+ public void addTestTarget(Label label) {
+ String targetName = label.toShorthandString();
+ if (!testCases.containsKey(targetName)) {
+ JsonObject summary = createTestCaseEmptyJsonNode(targetName);
+ summary.addProperty("finished", false);
+ summary.addProperty("status", "started");
+ testCases.put(targetName, summary);
+ } else {
+ // TODO(bazel-team): figure out if there are any situations it can happen
+ }
+ }
+
+ public void addTestSummary(Label label, BlazeTestStatus status, List<Long> testTimes,
+ boolean isCached) {
+ JsonObject testCase = testCases.get(label.toShorthandString());
+ testCase.addProperty("status", status.toString());
+ testCase.add("times", gson.toJsonTree(testTimes));
+ testCase.addProperty("cached", isCached);
+ testCase.addProperty("finished", true);
+ }
+
+ public void addTargetBuilt(Label label, boolean success) {
+ if (testCases.containsKey(label.toShorthandString())) {
+ if (success) {
+ testCases.get(label.toShorthandString()).addProperty("status", "built");
+ } else {
+ testCases.get(label.toShorthandString()).addProperty("status", "build failure");
+ }
+ } else {
+ LOG.info("Unhandled target: " + label);
+ }
+ }
+
+ @VisibleForTesting
+ static JsonObject createTestCaseEmptyJsonNode(String fullName) {
+ JsonObject currentNode = new JsonObject();
+ currentNode.addProperty("fullName", fullName);
+ currentNode.addProperty("name", "");
+ currentNode.addProperty("className", "");
+ currentNode.add("results", new JsonObject());
+ currentNode.add("times", new JsonObject());
+ currentNode.add("children", new JsonObject());
+ currentNode.add("failures", new JsonObject());
+ currentNode.add("errors", new JsonObject());
+ return currentNode;
+ }
+
+ private static JsonObject createTestCaseEmptyJsonNode(String fullName, TestCase testCase) {
+ JsonObject currentNode = createTestCaseEmptyJsonNode(fullName);
+ currentNode.addProperty("name", testCase.getName());
+ currentNode.addProperty("className", testCase.getClassName());
+ return currentNode;
+ }
+
+ private JsonObject mergeTestCases(JsonObject currentNode, String fullName, TestCase testCase,
+ int shardNumber) {
+ if (currentNode == null) {
+ currentNode = createTestCaseEmptyJsonNode(fullName, testCase);
+ }
+
+ if (testCase.getRun()) {
+ JsonObject results = (JsonObject) currentNode.get("results");
+ JsonObject times = (JsonObject) currentNode.get("times");
+
+ if (testCase.hasResult()) {
+ results.addProperty(Integer.toString(shardNumber), testCase.getResult());
+ }
+
+ if (testCase.hasStatus()) {
+ results.addProperty(Integer.toString(shardNumber), testCase.getStatus().toString());
+ }
+
+ if (testCase.hasRunDurationMillis()) {
+ times.addProperty(Integer.toString(shardNumber), testCase.getRunDurationMillis());
+ }
+ }
+ JsonObject children = (JsonObject) currentNode.get("children");
+
+ for (TestCase child : testCase.getChildList()) {
+ String fullChildName = child.getClassName() + "." + child.getName();
+ JsonObject childNode = mergeTestCases((JsonObject) children.get(fullChildName), fullChildName,
+ child, shardNumber);
+ if (!children.has(fullChildName)) {
+ children.add(fullChildName, childNode);
+ }
+ }
+ return currentNode;
+ }
+
+ public void addTestResult(Label label, TestCase testCase, int shardNumber) {
+ String testResultFullName = label.toShorthandString();
+ if (!testCases.containsKey(testResultFullName)) {
+ testCases.put(testResultFullName, createTestCaseEmptyJsonNode(testResultFullName, testCase));
+ }
+ mergeTestCases(testCases.get(testResultFullName), testResultFullName, testCase, shardNumber);
+ }
+
+ public UUID getCommandId() {
+ return commandId;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/webstatusserver/WebStatusEventCollector.java b/src/main/java/com/google/devtools/build/lib/webstatusserver/WebStatusEventCollector.java
new file mode 100644
index 0000000..40b0908
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/webstatusserver/WebStatusEventCollector.java
@@ -0,0 +1,135 @@
+// Copyright 2014 Google Inc. 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.webstatusserver;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableList.Builder;
+import com.google.common.eventbus.EventBus;
+import com.google.common.eventbus.Subscribe;
+import com.google.devtools.build.lib.analysis.BlazeVersionInfo;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.TargetCompleteEvent;
+import com.google.devtools.build.lib.buildtool.BuildRequest;
+import com.google.devtools.build.lib.buildtool.buildevent.BuildStartingEvent;
+import com.google.devtools.build.lib.buildtool.buildevent.TestFilteringCompleteEvent;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.Reporter;
+import com.google.devtools.build.lib.rules.test.TestResult;
+import com.google.devtools.build.lib.runtime.CommandCompleteEvent;
+import com.google.devtools.build.lib.runtime.CommandStartEvent;
+import com.google.devtools.build.lib.runtime.TestSummary;
+import com.google.devtools.build.lib.syntax.Label;
+
+import java.util.logging.Logger;
+
+/**
+ * This class monitors the build progress, collects events and preprocesses them for use by
+ * frontend.
+ *
+ */
+public class WebStatusEventCollector {
+ private static final Logger LOG =
+ Logger.getLogger(WebStatusEventCollector.class.getCanonicalName());
+ private final EventBus eventBus;
+ private final Reporter reporter;
+ private final int port;
+ private WebStatusBuildLog currentBuild;
+ private WebStatusServerModule serverModule;
+
+ public WebStatusEventCollector(EventBus eventBus, Reporter reporter,
+ WebStatusServerModule webStatusServerModule) {
+ this.eventBus = eventBus;
+ this.eventBus.register(this);
+ this.reporter = reporter;
+ this.port = webStatusServerModule.getPort();
+ this.serverModule = webStatusServerModule;
+ LOG.info("Created new status collector");
+ }
+
+ @Subscribe
+ public void buildStarted(BuildStartingEvent startingEvent) {
+ BuildRequest request = startingEvent.getRequest();
+ BlazeVersionInfo versionInfo = BlazeVersionInfo.instance();
+ currentBuild.addStartTime(request.getStartTime());
+ currentBuild.addTargetList(request.getTargets());
+ currentBuild
+ .addInfo("version", versionInfo)
+ .addInfo("commandName", request.getCommandName())
+ .addInfo("outputFs", startingEvent.getOutputFileSystem())
+ .addInfo("symlinkPrefix", request.getSymlinkPrefix())
+ .addInfo("optionsDescription", request.getOptionsDescription())
+ .addInfo("targets", request.getTargets())
+ .addInfo("viewOptions", request.getViewOptions());
+ }
+
+ @Subscribe
+ @SuppressWarnings("unused")
+ public void commandComplete(CommandCompleteEvent completeEvent) {
+ currentBuild.addInfo("endTime", completeEvent.getEventTimeInEpochTime());
+ currentBuild.finish();
+ }
+
+ @Subscribe
+ @SuppressWarnings("unused")
+ public void commandStarted(CommandStartEvent event) {
+ this.currentBuild = new WebStatusBuildLog(event.getCommandId());
+ this.serverModule.commandStarted();
+ String webStatusServerUrl = "http://localhost:" + port;
+ this.reporter.handle(Event.info("Status page: " + webStatusServerUrl + "/tests/"
+ + this.currentBuild.getCommandId() + " (alternative link: " + webStatusServerUrl
+ + WebStatusServerModule.LAST_TEST_URI + " )"));
+ }
+
+ @Subscribe
+ public void doneTestFiltering(TestFilteringCompleteEvent event) {
+ if (event.getTestTargets() != null) {
+ Builder<Label> builder = ImmutableList.builder();
+ for (ConfiguredTarget target : event.getTestTargets()) {
+ builder.add(target.getLabel());
+ }
+ doneTestFiltering(builder.build());
+ }
+ }
+
+ @VisibleForTesting
+ public void doneTestFiltering(Iterable<Label> testLabels) {
+ for (Label label : testLabels) {
+ currentBuild.addTestTarget(label);
+ }
+ }
+
+ @Subscribe
+ public void testTargetComplete(TestSummary summary) {
+ currentBuild.addTestSummary(summary.getTarget().getLabel(), summary.getStatus(),
+ summary.getTestTimes(), summary.isCached());
+ }
+
+ @Subscribe
+ public void testTargetResult(TestResult result) {
+ currentBuild.addTestResult(result.getTestAction().getOwner().getLabel(),
+ result.getData().getTestCase(), result.getShardNum());
+ }
+
+ @Subscribe
+ public void targetComplete(TargetCompleteEvent event) {
+ // TODO(bazel-team): would getting more details about failure be useful?
+ currentBuild.addTargetBuilt(event.getTarget().getTarget().getLabel(), !event.failed());
+ }
+
+ public WebStatusBuildLog getBuildLog() {
+ return this.currentBuild;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/webstatusserver/WebStatusServerModule.java b/src/main/java/com/google/devtools/build/lib/webstatusserver/WebStatusServerModule.java
new file mode 100644
index 0000000..13d4c8b
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/webstatusserver/WebStatusServerModule.java
@@ -0,0 +1,159 @@
+// Copyright 2014 Google Inc. 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.webstatusserver;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.analysis.BlazeDirectories;
+import com.google.devtools.build.lib.analysis.BlazeVersionInfo;
+import com.google.devtools.build.lib.runtime.BlazeModule;
+import com.google.devtools.build.lib.runtime.BlazeRuntime;
+import com.google.devtools.build.lib.runtime.BlazeServerStartupOptions;
+import com.google.devtools.build.lib.runtime.Command;
+import com.google.devtools.build.lib.util.AbruptExitException;
+import com.google.devtools.build.lib.util.Clock;
+import com.google.devtools.common.options.OptionsBase;
+import com.google.devtools.common.options.OptionsProvider;
+
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+import com.sun.net.httpserver.HttpServer;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.util.LinkedList;
+import java.util.UUID;
+import java.util.logging.Logger;
+
+/**
+ * Web server for monitoring blaze commands status.
+ */
+public class WebStatusServerModule extends BlazeModule {
+ static final String LAST_TEST_URI = "/tests/last";
+ // 100 is an arbitrary limit; it seems like a reasonable size for history and it's okay to change
+ // it
+ private static final int MAX_TESTS_STORED = 100;
+
+ private HttpServer server;
+ private boolean running = false;
+ private BlazeServerStartupOptions serverOptions;
+ private static final Logger LOG =
+ Logger.getLogger(WebStatusServerModule.class.getCanonicalName());
+ private int port;
+ private LinkedList<TestStatusHandler> testsRan = new LinkedList<>();
+ @SuppressWarnings("unused")
+ private WebStatusEventCollector collector;
+ @SuppressWarnings("unused")
+ private IndexPageHandler indexHandler;
+
+ @Override
+ public Iterable<Class<? extends OptionsBase>> getStartupOptions() {
+ return ImmutableList.<Class<? extends OptionsBase>>of(BlazeServerStartupOptions.class);
+ }
+
+ @Override
+ public void blazeStartup(OptionsProvider startupOptions, BlazeVersionInfo versionInfo,
+ UUID instanceId, BlazeDirectories directories, Clock clock) throws AbruptExitException {
+ serverOptions = startupOptions.getOptions(BlazeServerStartupOptions.class);
+ if (serverOptions.useWebStatusServer <= 0) {
+ LOG.info("web status server disabled");
+ return;
+ }
+ port = serverOptions.useWebStatusServer;
+ try {
+ server = HttpServer.create(new InetSocketAddress(port), 0);
+ serveStaticContent();
+ TextHandler lastCommandHandler = new TextHandler("No commands ran yet.");
+ server.createContext("/last", lastCommandHandler);
+ server.setExecutor(null);
+ server.start();
+ indexHandler = new IndexPageHandler(server, this.testsRan);
+ running = true;
+ LOG.info("Running web status server on port " + port);
+ } catch (IOException e) {
+ // TODO(bazel-team): Display information about why it failed
+ running = false;
+ LOG.warning("Unable to run web status server on port " + port);
+ }
+ }
+
+ @Override
+ public void beforeCommand(BlazeRuntime blazeRuntime, Command command) throws AbruptExitException {
+ if (!running) {
+ return;
+ }
+ collector =
+ new WebStatusEventCollector(blazeRuntime.getEventBus(), blazeRuntime.getReporter(), this);
+ }
+
+ public void commandStarted() {
+ WebStatusBuildLog currentBuild = collector.getBuildLog();
+
+ if (testsRan.size() == MAX_TESTS_STORED) {
+ TestStatusHandler oldestTest = testsRan.removeLast();
+ oldestTest.deregister();
+ }
+
+ TestStatusHandler lastTest = new TestStatusHandler(server, currentBuild);
+ testsRan.add(lastTest);
+
+ lastTest.overrideURI(LAST_TEST_URI);
+ }
+
+ private void serveStaticContent() {
+ StaticResourceHandler testjs =
+ StaticResourceHandler.createFromRelativePath("static/test.js", "application/javascript");
+ StaticResourceHandler indexjs =
+ StaticResourceHandler.createFromRelativePath("static/index.js", "application/javascript");
+ StaticResourceHandler style =
+ StaticResourceHandler.createFromRelativePath("static/style.css", "text/css");
+ StaticResourceHandler d3 = StaticResourceHandler.createFromAbsolutePath(
+ "third_party/javascript/d3/d3-js.js", "application/javascript");
+ StaticResourceHandler jquery = StaticResourceHandler.createFromAbsolutePath(
+ "third_party/javascript/jquery/v2_0_3/jquery_uncompressed.jslib",
+ "application/javascript");
+ StaticResourceHandler testFrontend =
+ StaticResourceHandler.createFromRelativePath("static/test.html", "text/html");
+
+ server.createContext("/css/style.css", style);
+ server.createContext("/js/test.js", testjs);
+ server.createContext("/js/index.js", indexjs);
+ server.createContext("/js/lib/d3.js", d3);
+ server.createContext("/js/lib/jquery.js", jquery);
+ server.createContext(LAST_TEST_URI, testFrontend);
+ }
+
+ private static class TextHandler implements HttpHandler {
+ private String response;
+
+ private TextHandler(String response) {
+ this.response = response;
+ }
+
+ @Override
+ public void handle(HttpExchange exchange) throws IOException {
+ exchange.getResponseHeaders().put("Content-Type", ImmutableList.of("text/plain"));
+ exchange.sendResponseHeaders(200, response.length());
+ OutputStream os = exchange.getResponseBody();
+ os.write(response.getBytes());
+ os.close();
+ }
+ }
+
+ public int getPort() {
+ return port;
+ }
+}
+
diff --git a/src/main/java/com/google/devtools/build/lib/webstatusserver/static/index.html b/src/main/java/com/google/devtools/build/lib/webstatusserver/static/index.html
new file mode 100644
index 0000000..f57bc30
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/webstatusserver/static/index.html
@@ -0,0 +1,14 @@
+<html>
+<head>
+ <title> Bazel web server </title>
+ <link rel="stylesheet" type="text/css" href="/css/style.css"></link>
+ <script src="/js/lib/d3.js" type="application/javascript"></script>
+ <script src="/js/lib/jquery.js" type="application/javascript"></script>
+ <script src="/js/index.js" type="application/javascript"></script>
+</head>
+<body onload="showData()">
+ <h1> Bazel web server status page </h1>
+ <div id="testsList">
+ </div>
+</body>
+</html>
diff --git a/src/main/java/com/google/devtools/build/lib/webstatusserver/static/index.js b/src/main/java/com/google/devtools/build/lib/webstatusserver/static/index.js
new file mode 100644
index 0000000..4ef9671
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/webstatusserver/static/index.js
@@ -0,0 +1,76 @@
+var icons = {
+ running: '\u25B6',
+ finished: '\u2611'
+};
+
+function showData() {
+ renderTestList(getTestsData());
+}
+
+function getTestsData() {
+ // TODO(bazel-team): change it to async callback retrieving data in background
+ // (for simplicity this is synchronous now)
+ return $.ajax({
+ type: 'GET',
+ url: document.URL + 'tests/list',
+ async: false
+ }).responseJSON;
+}
+
+function renderTestList(tests) {
+ var rows = d3.select('#testsList')
+ .selectAll()
+ .data(tests)
+ .enter().append('div')
+ .classed('info-cell', true);
+
+ // status
+ rows.append('div').classed('info-detail', true).text(function(j) {
+ return j.finished ? icons.finished : icons.running;
+ });
+
+ // target(s) name(s)
+ rows.append('div').classed('info-detail', true).text(function(j) {
+ if (j.targets.length == 1) {
+ return j.targets[0];
+ }
+ if (j.targets.length == 0) {
+ return 'Unknown target.';
+ }
+ return j.targets;
+ });
+
+ // start time
+ rows.append('div').classed('info-detail', true).text(function(j) {
+ // Pad value with 2 zeroes
+ function pad(value) {
+ return value < 10 ? '0' + value : value;
+ }
+
+ var
+ date = new Date(j.startTime),
+ today = new Date(Date.now()),
+ h = pad(date.getHours()),
+ m = pad(date.getMinutes()),
+ dd = pad(date.getDay()),
+ mm = pad(date.getMonth()),
+ yy = date.getYear(),
+ day;
+
+ // don't show date if ran today
+ if (dd != today.getDay() && mm != today.getMonth() &&
+ yy != today.getYear()) {
+ day = ' on ' + yy + '-' + mm + '-' + dd;
+ } else {
+ day = '';
+ }
+ return h + ':' + m;
+ });
+
+ // link
+ rows.append('div').classed('info-detail', true).classed('button', true)
+ .append('a').attr('href', function(datum, index) {
+ return '/tests/' + datum.uuid;
+ })
+ .text('link');
+}
diff --git a/src/main/java/com/google/devtools/build/lib/webstatusserver/static/test.html b/src/main/java/com/google/devtools/build/lib/webstatusserver/static/test.html
new file mode 100644
index 0000000..04a6fb7
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/webstatusserver/static/test.html
@@ -0,0 +1,28 @@
+<html>
+<head><title>Tests Result Page</title>
+ <link rel="stylesheet" type="text/css" href="/css/style.css"></link>
+ <script src="/js/lib/d3.js" type="application/javascript"></script>
+ <script src="/js/lib/jquery.js" type="application/javascript"></script>
+ <script src="/js/test.js" type="application/javascript"></script>
+</head>
+<body>
+<h1> Bazel web status server </h1>
+<div id="testInfo">
+ No test info to display.
+</div>
+<br>
+<div id="testFilters">
+ <div class="info-cell">
+ <input placeholder="Filter by name" type=text id="search"></input>
+ <!-- TODO(bazel-team) this is very simplistic view of tests,
+ we probably need more filters -->
+ <input type=checkbox checked=true id="boxPassed">passed</input>
+ <input type=checkbox checked=true id="boxFailed">failed</input>
+ <button id="clearFilters"> clear filters </button>
+ </div>
+</div>
+<div id="testDetails">
+ No test details to display.
+</div>
+</body>
+</html>
diff --git a/src/main/java/com/google/devtools/build/lib/webstatusserver/static/test.js b/src/main/java/com/google/devtools/build/lib/webstatusserver/static/test.js
new file mode 100644
index 0000000..406dcab
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/webstatusserver/static/test.js
@@ -0,0 +1,384 @@
+var icons = {
+ running: '?',
+ passed: '\u2705',
+ errors: '\u274c'
+};
+
+
+function showData() {
+ renderDetails(getDetailsData(), false);
+ renderInfo(getCommandInfo());
+}
+
+function getCommandInfo() {
+ var url = document.URL;
+ if (url[url.length - 1] != '/') {
+ url += '/';
+ }
+ return $.ajax({
+ type: 'GET',
+ url: url + 'info',
+ async: false
+ }).responseJSON;
+}
+
+function getDetailsData() {
+ // TODO(bazel-team): auto refresh, async callback
+ var url = document.URL;
+ if (url[url.length - 1] != '/') {
+ url += '/';
+ }
+ return $.ajax({
+ type: 'GET',
+ url: url + 'details',
+ async: false
+ }).responseJSON;
+}
+
+
+function showDate(d) {
+ function pad(x) {
+ return x < 10 ? '0' + x : '' + x;
+ }
+ var today = new Date();
+ var result = pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' +
+ pad(d.getSeconds());
+ if (d.getDate() === today.getDate() && d.getMonth() === today.getMonth() &&
+ d.getYear() === today.getYear()) {
+ result += ' today';
+ } else {
+ result += pad(d.getDate()) + ' ' + pad(d.getMonth()) + ' ' + d.getYear();
+ }
+ return result;
+}
+
+function renderInfo(info) {
+ $('#testInfo').empty();
+ var data = [
+ ['Targets: ', info['targets']],
+ ['Started at: ', showDate(new Date(info['startTime']))],
+ ];
+ if (info['finished']) {
+ data.push(['Finished at: ', showDate(new Date(info['endTime']))]);
+ } else {
+ data.push(['Still running']);
+ }
+ var selection = d3.select('#testInfo').selectAll()
+ .data(data)
+ .enter().append('div')
+ .classed('info-cell', true);
+ selection
+ .append('div')
+ .classed('info-detail', true)
+ .text(function(d) { return d[0]; });
+ selection
+ .append('div')
+ .classed('info-detail', true)
+ .text(function(d) { return d[1]; });
+}
+
+// predicate is either a predicate function or null - in the latter case
+// everything is shown
+function renderDetails(tests, predicate) {
+ $('#testDetails').empty();
+ if (tests.length == 0) {
+ $('#testDetails').text('No test details to display.');
+ return;
+ }
+ // flatten object to array and set visibility
+ tests = $.map(tests, function(element) {
+ if (predicate) {
+ setVisibility(predicate, element);
+ }
+ return element;
+ });
+ var rows = d3.select('#testDetails').selectAll()
+ .data(tests)
+ .enter().append('div')
+ .classed('test-case', true);
+
+ function addTestDetail(selection, toplevel) {
+ function fullName() {
+ selection.append('div').classed('test-detail', true).text(function(j) {
+ return j.fullName;
+ });
+ }
+ function propagateStatus(j) {
+ var result = '';
+ var failures = [];
+ var errors = [];
+ $.each(j.results, function(key, value) {
+ if (value == 'FAILED') {
+ failures.push(key);
+ }
+ if (value == 'ERROR') {
+ errors.push(key);
+ }
+ });
+ if (failures.length > 0) {
+ var s = failures.length > 1 ? 's' : '';
+ result += 'Failed on ' + failures.length + ' shard' + s + ': ' +
+ failures.join();
+ }
+ if (errors.length > 0) {
+ var s = failures.length > 1 ? 's' : '';
+ result += 'Errors on ' + errors.length + ' shard' + s + ': ' +
+ errors.join();
+ }
+ if (result == '') {
+ return j.status;
+ }
+ return result;
+ }
+ function testCaseStatus() {
+ selection.append('div')
+ .classed('test-detail', true)
+ .text(propagateStatus);
+ }
+ function testTargetStatus() {
+ selection.append('div')
+ .classed('test-detail', true)
+ .text(function(target) {
+ var childStatus = propagateStatus(target);
+ if (target.finished = false) {
+ return target.status + ' ' + stillRunning;
+ } else {
+ if (childStatus == 'PASSED') {
+ return target.status;
+ } else {
+ return target.status + ' ' + childStatus;
+ }
+ }
+ });
+ }
+ function testTargetStatusIcon() {
+ selection.append('div')
+ .classed('test-detail', true)
+ .attr('color', function(target) {
+ var childStatus = propagateStatus(target);
+ if (target.finished == false) {
+ return 'running';
+ } else {
+ if (childStatus == 'PASSED') {
+ return 'passed';
+ } else {
+ return 'errors';
+ }
+ }})
+ .text(function(target) {
+ var childStatus = propagateStatus(target);
+ if (target.finished == false) {
+ return icons.running;
+ } else {
+ if (childStatus == 'PASSED') {
+ return icons.passed;
+ } else {
+ return icons.errors;
+ }
+ }
+ });
+ }
+ function testCaseTime() {
+ selection.append('div').classed('test-detail', true).text(function(j) {
+ var times = $.map(j.times, function(element, key) { return element });
+ if (times.length < 1) {
+ return '?';
+ } else {
+ return Math.max.apply(Math, times) / 1000 + ' s';
+ }
+ });
+ }
+
+ function visibilityFilter() {
+ selection.attr('show', function(datum) {
+ return ('show' in datum) ? datum['show'] : true;
+ });
+ }
+
+ // Toplevel nodes represent test targets, so they look a bit different
+ if (toplevel) {
+ testTargetStatusIcon();
+ fullName();
+ } else {
+ testTargetStatusIcon();
+ fullName();
+ testCaseStatus();
+ testCaseTime();
+ }
+ visibilityFilter();
+ }
+
+ function addNestedDetails(table, toplevel) {
+ table.sort(function(data1, data2) {
+ if (data1.fullName < data2.fullName) {
+ return -1;
+ }
+ if (data1.fullName > data2.fullName) {
+ return 1;
+ }
+ return 0;
+ });
+
+ addTestDetail(table, toplevel);
+
+ // Add children nodes + show/hide button
+ var nonLeafNodes = table.filter(function(data, index) {
+ return !($.isEmptyObject(data.children));
+ });
+ var nextLevelNodes = nonLeafNodes.selectAll().data(function(d) {
+ return $.map(d.children, function(element, key) { return element });
+ });
+
+ if (nextLevelNodes.enter().empty()) {
+ return;
+ }
+
+ nonLeafNodes
+ .append('div')
+ .classed('test-detail', true)
+ .classed('button', true)
+ .text(function(j) {
+ return 'Show details';
+ })
+ .attr('toggle', 'off')
+ .on('click', function(datum) {
+ if ($(this).attr('toggle') == 'on') {
+ $(this).siblings('.test-case').not('[show=false]').hide();
+ $(this).attr('toggle', 'off');
+ $(this).text('Show details');
+ } else {
+ $(this).siblings('.test-case').not('[show=false]').show();
+ $(this).attr('toggle', 'on');
+ $(this).text('Hide details');
+ }
+ });
+ nextLevelNodes.enter().append('div').classed('test-case', true);
+ addNestedDetails(nextLevelNodes, false);
+ }
+
+ addNestedDetails(rows, true);
+ $('.button').siblings('.test-case').hide();
+ if (predicate) {
+ toggleVisibility();
+ }
+}
+
+function toggleVisibility() {
+ $('#testDetails > [show=false]').hide();
+ $('#testDetails > [show=true]').show();
+ $('[toggle=on]').siblings('[show=false]').hide();
+ $('[toggle=on]').siblings('[show=true]').show();
+}
+
+function setVisibility(predicate, object) {
+ var show = predicate(object);
+ var childrenPredicate = predicate;
+ // It rarely makes sense to show a non-leaf node and hide its children, so
+ // we just show all children
+ if (show) {
+ childrenPredicate = function() { return true; };
+ }
+ if ('children' in object) {
+ for (var child in object.children) {
+ setVisibility(childrenPredicate, object.children[child]);
+ show = object.children[child]['show'] || show;
+ }
+ }
+ object['show'] = show;
+}
+
+// given a list of predicates, return a function
+function intersectFilters(filterList) {
+ var filters = filterList.filter(function(x) { return x });
+ return function(x) {
+ for (var i = 0; i < filters.length; i++) {
+ if (!filters[i](x)) {
+ return false;
+ }
+ }
+ return true;
+ }
+}
+
+function textFilterActive() {
+ return $('#search').val();
+}
+
+function getTestFilters() {
+ var statusFilter = null;
+ var textFilter = null;
+ var filters = [];
+ var passed = $('#boxPassed').prop('checked');
+ var failed = $('#boxFailed').prop('checked');
+ // add checkbox filters only when necessary (ie. something is unchecked - when
+ // everything is checked this means user wants to see everything).
+ if (!(passed && failed)) {
+ var checkBoxFilters = [];
+ if (passed) {
+ checkBoxFilters.push(function(object) {
+ return object.status == 'PASSED';
+ });
+ }
+ if (failed) {
+ checkBoxFilters.push(function(object) {
+ return 'status' in object && object.status != 'PASSED';
+ });
+ }
+ filters.push(function(object) {
+ return checkBoxFilters.some(function(f) { return f(object); });
+ });
+ }
+ if (textFilterActive()) {
+ filters.push(function(object) {
+ // TODO(bazel-team): would case insentive search make more sense?
+ return ('fullName' in object &&
+ object.fullName.indexOf($('#search').val()) != -1);
+ });
+ }
+ return filters;
+}
+
+function redraw() {
+ renderDetails(getDetailsData(), intersectFilters(getTestFilters()));
+}
+
+function updateVisibleCases() {
+ var predicate = intersectFilters(getTestFilters());
+ var parentCases = d3.selectAll('#testDetails > div').data();
+ parentCases.forEach(function(element, index) {
+ setVisibility(predicate, element);
+ });
+ d3.selectAll('.test-detail').attr('show', function(datum) {
+ return ('show' in datum) ? datum['show'] : true;
+ });
+ d3.selectAll('.test-case').attr('show', function(datum) {
+ return ('show' in datum) ? datum['show'] : true;
+ });
+ toggleVisibility();
+ if (textFilterActive()) {
+ // expand nodes to save some clicking - if user searched for something that
+ // is leaf of the tree, she definitely wants to see it
+ $('#testDetails > [show=true]').find('[toggle=off]').click();
+ }
+}
+
+function enableControls() {
+ var redrawTimeout = null;
+ $('#boxPassed').click(updateVisibleCases);
+ $('#boxFailed').click(updateVisibleCases);
+ $('#search').keyup(function() {
+ clearTimeout(redrawTimeout);
+ redrawTimeout = setTimeout(updateVisibleCases, 500);
+ });
+ $('#clearFilters').click(function() {
+ $('#boxPassed').prop('checked', true);
+ $('#boxFailed').prop('checked', true);
+ $('#search').val('');
+ updateVisibleCases();
+ });
+}
+
+$(function() {
+ showData();
+ enableControls();
+});
diff --git a/src/main/java/com/google/devtools/build/skyframe/BuildDriver.java b/src/main/java/com/google/devtools/build/skyframe/BuildDriver.java
new file mode 100644
index 0000000..938735b
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skyframe/BuildDriver.java
@@ -0,0 +1,32 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.devtools.build.lib.events.EventHandler;
+
+/**
+ * A BuildDriver wraps a MemoizingEvaluator, passing along the proper Version.
+ */
+public interface BuildDriver {
+ /**
+ * See {@link MemoizingEvaluator#evaluate}, which has the same semantics except for the
+ * inclusion of a {@link Version} value.
+ */
+ <T extends SkyValue> EvaluationResult<T> evaluate(
+ Iterable<SkyKey> roots, boolean keepGoing, int numThreads, EventHandler reporter)
+ throws InterruptedException;
+
+ MemoizingEvaluator getGraphForTesting();
+}
diff --git a/src/main/java/com/google/devtools/build/skyframe/BuildingState.java b/src/main/java/com/google/devtools/build/skyframe/BuildingState.java
new file mode 100644
index 0000000..21deec1
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skyframe/BuildingState.java
@@ -0,0 +1,437 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadCompatible;
+import com.google.devtools.build.lib.util.GroupedList;
+import com.google.devtools.build.lib.util.GroupedList.GroupedListHelper;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Data the NodeEntry uses to maintain its state before it is done building. It allows the
+ * {@link NodeEntry} to keep the current state of the entry across invalidation and successive
+ * evaluations. A done node does not contain any of this data. However, if a node is marked dirty,
+ * its entry acquires a new {@code BuildingState} object, which persists until it is done again.
+ *
+ * <p>This class should be considered a private inner class of {@link NodeEntry} -- no other
+ * classes should instantiate a {@code BuildingState} object or call any of its methods directly.
+ * It is in a separate file solely to keep the {@link NodeEntry} class readable. In particular, the
+ * caller must synchronize access to this class.
+ */
+@ThreadCompatible
+final class BuildingState {
+ enum DirtyState {
+ /**
+ * The node's dependencies need to be checked to see if it needs to be rebuilt. The
+ * dependencies must be obtained through calls to {@link #getNextDirtyDirectDeps} and checked.
+ */
+ CHECK_DEPENDENCIES,
+ /**
+ * All of the node's dependencies are unchanged, and the value itself was not marked changed,
+ * so its current value is still valid -- it need not be rebuilt.
+ */
+ VERIFIED_CLEAN,
+ /**
+ * A rebuilding is required or in progress, because either the node itself changed or one of
+ * its dependencies did.
+ */
+ REBUILDING
+ }
+
+ /**
+ * During its life, a node can go through states as follows:
+ * <ol>
+ * <li>Non-existent
+ * <li>Just created ({@code evaluating} is false)
+ * <li>Evaluating ({@code evaluating} is true)
+ * <li>Done (meaning this buildingState object is null)
+ * <li>Just created (when it is dirtied during evaluation)
+ * <li>Reset (just before it is re-evaluated)
+ * <li>Evaluating
+ * <li>Done
+ * </ol>
+ *
+ * <p>The "just created" state is there to allow the {@link EvaluableGraph#createIfAbsent} and
+ * {@link NodeEntry#addReverseDepAndCheckIfDone} methods to be separate. All callers have to
+ * call both methods in that order if they want to create a node. The second method calls
+ * {@link #startEvaluating}, which transitions the current node to the "evaluating" state and
+ * returns true only the first time it was called. A caller that gets "true" back from that call
+ * must start the evaluation of this node, while any subsequent callers must not.
+ *
+ * <p>An entry is set to "evaluating" as soon as it is scheduled for evaluation. Thus, even a
+ * node that is never actually built (for instance, a dirty node that is verified as clean) is
+ * in the "evaluating" state until it is done.
+ */
+ private boolean evaluating = false;
+
+ /**
+ * The state of a dirty node. A node is marked dirty in the BuildingState constructor, and goes
+ * into either the state {@link DirtyState#CHECK_DEPENDENCIES} or {@link DirtyState#REBUILDING},
+ * depending on whether the caller specified that the node was itself changed or not. A non-null
+ * {@code dirtyState} indicates that the node {@link #isDirty} in some way.
+ */
+ private DirtyState dirtyState = null;
+
+ /**
+ * The number of dependencies that are known to be done in a {@link NodeEntry}. There is a
+ * potential check-then-act race here, so we need to make sure that when this is increased, we
+ * always check if the new value is equal to the number of required dependencies, and if so, we
+ * must re-schedule the node for evaluation.
+ *
+ * <p>There are two potential pitfalls here: 1) If multiple dependencies signal this node in
+ * close succession, this node should be scheduled exactly once. 2) If a thread is still working
+ * on this node, it should not be scheduled.
+ *
+ * <p>The first problem is solved by the {@link #signalDep} method, which also returns if the
+ * node needs to be re-scheduled, and ensures that only one thread gets a true return value.
+ *
+ * <p>The second problem is solved by first adding the newly discovered deps to a node's
+ * {@link #directDeps}, and then looping through the direct deps and registering this node as a
+ * reverse dependency. This ensures that the signaledDeps counter can only reach
+ * {@link #directDeps}.size() on the very last iteration of the loop, i.e., the thread is not
+ * working on the node anymore. Note that this requires that there is no code after the loop in
+ * {@code ParallelEvaluator.Evaluate#run}.
+ */
+ private int signaledDeps = 0;
+
+ /**
+ * Direct dependencies discovered during the build. They will be written to the immutable field
+ * {@code ValueEntry#directDeps} and the dependency group data to {@code ValueEntry#groupData}
+ * once the node is finished building. {@link SkyFunction}s can request deps in groups, and these
+ * groupings are preserved in this field.
+ */
+ private final GroupedList<SkyKey> directDeps = new GroupedList<>();
+
+ /**
+ * The set of reverse dependencies that are registered before the node has finished building.
+ * Upon building, these reverse deps will be signaled and then stored in the permanent
+ * {@code ValueEntry#reverseDeps}.
+ */
+ // TODO(bazel-team): Remove this field. With eager invalidation, all direct deps on this dirty
+ // node will be removed by the time evaluation starts, so reverse deps to signal can just be
+ // reverse deps in the main ValueEntry object.
+ private Object reverseDepsToSignal = ImmutableList.of();
+ private List<SkyKey> reverseDepsToRemove = null;
+ private boolean reverseDepIsSingleObject = false;
+
+ private static final ReverseDepsUtil<BuildingState> REVERSE_DEPS_UTIL =
+ new ReverseDepsUtil<BuildingState>() {
+ @Override
+ void setReverseDepsObject(BuildingState container, Object object) {
+ container.reverseDepsToSignal = object;
+ }
+
+ @Override
+ void setSingleReverseDep(BuildingState container, boolean singleObject) {
+ container.reverseDepIsSingleObject = singleObject;
+ }
+
+ @Override
+ void setReverseDepsToRemove(BuildingState container, List<SkyKey> object) {
+ container.reverseDepsToRemove = object;
+ }
+
+ @Override
+ Object getReverseDepsObject(BuildingState container) {
+ return container.reverseDepsToSignal;
+ }
+
+ @Override
+ boolean isSingleReverseDep(BuildingState container) {
+ return container.reverseDepIsSingleObject;
+ }
+
+ @Override
+ List<SkyKey> getReverseDepsToRemove(BuildingState container) {
+ return container.reverseDepsToRemove;
+ }
+ };
+
+ // Below are fields that are used for dirty nodes.
+
+ /**
+ * The dependencies requested (with group markers) last time the node was built (and below, the
+ * value last time the node was built). They will be compared to dependencies requested on this
+ * build to check whether this node has changed in {@link NodeEntry#setValue}. If they are null,
+ * it means that this node is being built for the first time. See {@link #directDeps} for more on
+ * dependency group storage.
+ */
+ private final GroupedList<SkyKey> lastBuildDirectDeps;
+ private final SkyValue lastBuildValue;
+
+ /**
+ * Which child should be re-evaluated next in the process of determining if this entry needs to
+ * be re-evaluated. Used by {@link #getNextDirtyDirectDeps} and {@link #signalDep(boolean)}.
+ */
+ private Iterator<Iterable<SkyKey>> dirtyDirectDepIterator = null;
+
+ BuildingState() {
+ lastBuildDirectDeps = null;
+ lastBuildValue = null;
+ }
+
+ private BuildingState(boolean isChanged, GroupedList<SkyKey> lastBuildDirectDeps,
+ SkyValue lastBuildValue) {
+ this.lastBuildDirectDeps = lastBuildDirectDeps;
+ this.lastBuildValue = Preconditions.checkNotNull(lastBuildValue);
+ Preconditions.checkState(isChanged || !this.lastBuildDirectDeps.isEmpty(),
+ "is being marked dirty, not changed, but has no children that could have dirtied it", this);
+ dirtyState = isChanged ? DirtyState.REBUILDING : DirtyState.CHECK_DEPENDENCIES;
+ if (dirtyState == DirtyState.CHECK_DEPENDENCIES) {
+ // We need to iterate through the deps to see if they have changed. Initialize the iterator.
+ dirtyDirectDepIterator = lastBuildDirectDeps.iterator();
+ }
+ }
+
+ static BuildingState newDirtyState(boolean isChanged,
+ GroupedList<SkyKey> lastBuildDirectDeps, SkyValue lastBuildValue) {
+ return new BuildingState(isChanged, lastBuildDirectDeps, lastBuildValue);
+ }
+
+ void markChanged() {
+ Preconditions.checkState(isDirty(), this);
+ Preconditions.checkState(!isChanged(), this);
+ Preconditions.checkState(!evaluating, this);
+ dirtyState = DirtyState.REBUILDING;
+ }
+
+ void forceChanged() {
+ Preconditions.checkState(isDirty(), this);
+ Preconditions.checkState(!isChanged(), this);
+ Preconditions.checkState(evaluating, this);
+ Preconditions.checkState(isReady(), this);
+ dirtyState = DirtyState.REBUILDING;
+ }
+
+ /**
+ * Returns whether all known children of this node have signaled that they are done.
+ */
+ boolean isReady() {
+ int directDepsSize = directDeps.size();
+ Preconditions.checkState(signaledDeps <= directDepsSize, "%s %s", directDepsSize, this);
+ return signaledDeps == directDepsSize;
+ }
+
+ /**
+ * Returns true if the entry is marked dirty, meaning that at least one of its transitive
+ * dependencies is marked changed.
+ *
+ * @see NodeEntry#isDirty()
+ */
+ boolean isDirty() {
+ return dirtyState != null;
+ }
+
+ /**
+ * Returns true if the entry is known to require re-evaluation.
+ *
+ * @see NodeEntry#isChanged()
+ */
+ boolean isChanged() {
+ return dirtyState == DirtyState.REBUILDING;
+ }
+
+ private boolean rebuilding() {
+ return dirtyState == DirtyState.REBUILDING;
+ }
+
+ /**
+ * Helper method to assert that node has finished building, as far as we can tell. We would
+ * actually like to check that the node has been evaluated, but that is not available in
+ * this context.
+ */
+ private void checkNotProcessing() {
+ Preconditions.checkState(evaluating, "not started building %s", this);
+ Preconditions.checkState(!isDirty() || dirtyState == DirtyState.VERIFIED_CLEAN
+ || rebuilding(), "not done building %s", this);
+ Preconditions.checkState(isReady(), "not done building %s", this);
+ }
+
+ /**
+ * Puts the node in the "evaluating" state if it is not already in it. Returns whether or not the
+ * node was already evaluating. Should only be called by
+ * {@link NodeEntry#addReverseDepAndCheckIfDone}.
+ */
+ boolean startEvaluating() {
+ boolean result = !evaluating;
+ evaluating = true;
+ return result;
+ }
+
+ /**
+ * Increments the number of children known to be finished. Returns true if the number of children
+ * finished is equal to the number of known children.
+ *
+ * <p>If the node is dirty and checking its deps for changes, this also updates {@link
+ * #dirtyState} as needed -- {@link DirtyState#REBUILDING} if the child has changed,
+ * and {@link DirtyState#VERIFIED_CLEAN} if the child has not changed and this was the last
+ * child to be checked (as determined by {@link #dirtyDirectDepIterator} == null, isReady(), and
+ * a flag set in {@link #getNextDirtyDirectDeps}).
+ *
+ * @see NodeEntry#signalDep(Version)
+ */
+ boolean signalDep(boolean childChanged) {
+ signaledDeps++;
+ if (isDirty() && !rebuilding()) {
+ // Synchronization isn't needed here because the only caller is ValueEntry, which does it
+ // through the synchronized method signalDep(long).
+ if (childChanged) {
+ dirtyState = DirtyState.REBUILDING;
+ } else if (dirtyState == DirtyState.CHECK_DEPENDENCIES && isReady()
+ && dirtyDirectDepIterator == null) {
+ // No other dep already marked this as REBUILDING, no deps outstanding, and this was
+ // the last block of deps to be checked.
+ dirtyState = DirtyState.VERIFIED_CLEAN;
+ }
+ }
+ return isReady();
+ }
+
+ /**
+ * Returns true if {@code newValue}.equals the value from the last time this node was built, and
+ * the deps requested during this evaluation are exactly those requested the last time this node
+ * was built, in the same order. Should only be used by {@link NodeEntry#setValue}.
+ */
+ boolean unchangedFromLastBuild(SkyValue newValue) {
+ checkNotProcessing();
+ return lastBuildValue.equals(newValue) && lastBuildDirectDeps.equals(directDeps);
+ }
+
+ boolean noDepsLastBuild() {
+ return lastBuildDirectDeps.isEmpty();
+ }
+
+ SkyValue getLastBuildValue() {
+ return Preconditions.checkNotNull(lastBuildValue, this);
+ }
+
+ /**
+ * Gets the current state of checking this dirty entry to see if it must be re-evaluated. Must be
+ * called each time evaluation of a dirty entry starts to find the proper action to perform next,
+ * as enumerated by {@link DirtyState}.
+ *
+ * @see NodeEntry#getDirtyState()
+ */
+ DirtyState getDirtyState() {
+ // Entry may not be ready if being built just for its errors.
+ Preconditions.checkState(isDirty(), "must be dirty to get dirty state %s", this);
+ Preconditions.checkState(evaluating, "must be evaluating to get dirty state %s", this);
+ return dirtyState;
+ }
+
+ /**
+ * Gets the next children to be re-evaluated to see if this dirty node needs to be re-evaluated.
+ *
+ * <p>If this is the last group of children to be checked, then sets {@link
+ * #dirtyDirectDepIterator} to null so that the final call to {@link #signalDep(boolean)} will
+ * know to mark this entry as {@link DirtyState#VERIFIED_CLEAN} if no deps have changed.
+ *
+ * See {@link NodeEntry#getNextDirtyDirectDeps}.
+ */
+ Collection<SkyKey> getNextDirtyDirectDeps() {
+ Preconditions.checkState(isDirty(), this);
+ Preconditions.checkState(dirtyState == DirtyState.CHECK_DEPENDENCIES, this);
+ Preconditions.checkState(evaluating, this);
+ List<SkyKey> nextDeps = ImmutableList.copyOf(dirtyDirectDepIterator.next());
+ if (!dirtyDirectDepIterator.hasNext()) {
+ // Done checking deps. If this last group is clean, the state will become VERIFIED_CLEAN.
+ dirtyDirectDepIterator = null;
+ }
+ return nextDeps;
+ }
+
+ void addDirectDeps(GroupedListHelper<SkyKey> depsThisRun) {
+ directDeps.append(depsThisRun);
+ }
+
+ /**
+ * Returns the direct deps found so far on this build. Should only be called before the node has
+ * finished building.
+ *
+ * @see NodeEntry#getTemporaryDirectDeps()
+ */
+ Set<SkyKey> getDirectDepsForBuild() {
+ return directDeps.toSet();
+ }
+
+ /**
+ * Returns the direct deps (in groups) found on this build. Should only be called when the node
+ * is done.
+ *
+ * @see NodeEntry#setStateFinishedAndReturnReverseDeps
+ */
+ GroupedList<SkyKey> getFinishedDirectDeps() {
+ return directDeps;
+ }
+
+ /**
+ * Returns reverse deps to signal that have been registered this build.
+ *
+ * @see NodeEntry#getReverseDeps()
+ */
+ ImmutableSet<SkyKey> getReverseDepsToSignal() {
+ return REVERSE_DEPS_UTIL.getReverseDeps(this);
+ }
+
+ /**
+ * Adds a reverse dependency that should be notified when this entry is done.
+ *
+ * @see NodeEntry#addReverseDepAndCheckIfDone(SkyKey)
+ */
+ void addReverseDepToSignal(SkyKey newReverseDep) {
+ REVERSE_DEPS_UTIL.consolidateReverseDepsRemovals(this);
+ REVERSE_DEPS_UTIL.addReverseDeps(this, Collections.singleton(newReverseDep));
+ }
+
+ /**
+ * @see NodeEntry#removeReverseDep(SkyKey)
+ */
+ void removeReverseDepToSignal(SkyKey reverseDep) {
+ REVERSE_DEPS_UTIL.removeReverseDep(this, reverseDep);
+ }
+
+ /**
+ * Removes a set of deps from the set of known direct deps. This is complicated by the need
+ * to maintain the group data. If we remove a dep that ended a group, then its predecessor's
+ * group data must be changed to indicate that it now ends the group.
+ *
+ * @see NodeEntry#removeUnfinishedDeps
+ */
+ void removeDirectDeps(Set<SkyKey> unfinishedDeps) {
+ directDeps.remove(unfinishedDeps);
+ }
+
+ @Override
+ @SuppressWarnings("deprecation")
+ public String toString() {
+ return Objects.toStringHelper(this) // MoreObjects is not in Guava
+ .add("evaluating", evaluating)
+ .add("dirtyState", dirtyState)
+ .add("signaledDeps", signaledDeps)
+ .add("directDeps", directDeps)
+ .add("reverseDepsToSignal", REVERSE_DEPS_UTIL.toString(this))
+ .add("lastBuildDirectDeps", lastBuildDirectDeps)
+ .add("lastBuildValue", lastBuildValue)
+ .add("dirtyDirectDepIterator", dirtyDirectDepIterator).toString();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/skyframe/CycleDeduper.java b/src/main/java/com/google/devtools/build/skyframe/CycleDeduper.java
new file mode 100644
index 0000000..f52333c
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skyframe/CycleDeduper.java
@@ -0,0 +1,90 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+
+/**
+ * Dedupes C candidate cycles of size O(L) in O(CL) time and memory in the common case and
+ * O(C^2 * L) time and O(CL) memory in the extreme case.
+ *
+ * Two cycles are considered duplicates if they are exactly the same except for the entry point.
+ * For example, 'a' -> 'b' -> 'c' -> 'a' is the considered the same as 'b' -> 'c' -> 'a' -> 'b'.
+ */
+class CycleDeduper<T> {
+
+ private HashMultimap<ImmutableSet<T>, ImmutableList<T>> knownCyclesByMembers =
+ HashMultimap.create();
+
+ /**
+ * Marks a non-empty list representing a cycle of unique values as being seen and returns true
+ * iff the cycle hasn't been seen before, accounting for logical equivalence of cycles.
+ *
+ * For example, the cycle 'a' -> 'b' -> 'c' -> 'a' is represented by the list ['a', 'b', 'c']
+ * and is logically equivalent to the cycle represented by the list ['b', 'c', 'a'].
+ */
+ public boolean seen(ImmutableList<T> cycle) {
+ ImmutableSet<T> cycleMembers = ImmutableSet.copyOf(cycle);
+ Preconditions.checkState(!cycle.isEmpty());
+ Preconditions.checkState(cycle.size() == cycleMembers.size(),
+ "cycle doesn't have unique members: " + cycle);
+
+ if (knownCyclesByMembers.containsEntry(cycleMembers, cycle)) {
+ return false;
+ }
+
+ // Of the C cycles, suppose there are D cycles that have the same members (but are in an
+ // incompatible order). This code path takes O(D * L) time. The common case is that D is
+ // very small.
+ boolean found = false;
+ for (ImmutableList<T> candidateCycle : knownCyclesByMembers.get(cycleMembers)) {
+ int startPos = candidateCycle.indexOf(cycle.get(0));
+ // The use of a multimap keyed by cycle members guarantees that the first element of 'cycle'
+ // is present in 'candidateCycle'.
+ Preconditions.checkState(startPos >= 0);
+ if (equalsWithSingleLoopFrom(cycle, candidateCycle, startPos)) {
+ found = true;
+ break;
+ }
+ }
+ // We add the cycle even if it's a duplicate so that future exact copies of this can be
+ // processed in O(L) time. We are already using O(CL) memory, and this optimization doesn't
+ // change that.
+ knownCyclesByMembers.put(cycleMembers, cycle);
+ return !found;
+ }
+
+ /**
+ * Returns true iff
+ * listA[0], listA[1], ..., listA[listA.size()]
+ * is the same as
+ * listB[start], listB[start+1], ..., listB[listB.size()-1], listB[0], ..., listB[start-1]
+ */
+ private boolean equalsWithSingleLoopFrom(ImmutableList<T> listA, ImmutableList<T> listB,
+ int start) {
+ if (listA.size() != listB.size()) {
+ return false;
+ }
+ int length = listA.size();
+ for (int i = 0; i < length; i++) {
+ if (!listA.get(i).equals(listB.get((i + start) % length))) {
+ return false;
+ }
+ }
+ return true;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/skyframe/CycleInfo.java b/src/main/java/com/google/devtools/build/skyframe/CycleInfo.java
new file mode 100644
index 0000000..a44d2fa
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skyframe/CycleInfo.java
@@ -0,0 +1,144 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicates;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+
+import java.io.Serializable;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Data for a single cycle in the graph, together with the path to the cycle. For any value, the
+ * head of path to the cycle should be the value itself, or, if the value is actually in the cycle,
+ * the cycle should start with the value.
+ */
+public class CycleInfo implements Serializable {
+ private final ImmutableList<SkyKey> cycle;
+ private final ImmutableList<SkyKey> pathToCycle;
+
+ @VisibleForTesting
+ public CycleInfo(Iterable<SkyKey> cycle) {
+ this(ImmutableList.<SkyKey>of(), cycle);
+ }
+
+ CycleInfo(Iterable<SkyKey> pathToCycle, Iterable<SkyKey> cycle) {
+ this.pathToCycle = ImmutableList.copyOf(pathToCycle);
+ this.cycle = ImmutableList.copyOf(cycle);
+ }
+
+ // If a cycle is already known, but we are processing a value in the middle of the cycle, we need
+ // to shift the cycle so that the value is at the head.
+ private CycleInfo(Iterable<SkyKey> cycle, int cycleStart) {
+ Preconditions.checkState(cycleStart >= 0, cycleStart);
+ ImmutableList.Builder<SkyKey> cycleTail = ImmutableList.builder();
+ ImmutableList.Builder<SkyKey> cycleHead = ImmutableList.builder();
+ int index = 0;
+ for (SkyKey key : cycle) {
+ if (index >= cycleStart) {
+ cycleHead.add(key);
+ } else {
+ cycleTail.add(key);
+ }
+ index++;
+ }
+ Preconditions.checkState(cycleStart < index, "%s >= %s ??", cycleStart, index);
+ this.cycle = cycleHead.addAll(cycleTail.build()).build();
+ this.pathToCycle = ImmutableList.of();
+ }
+
+ public ImmutableList<SkyKey> getCycle() {
+ return cycle;
+ }
+
+ public ImmutableList<SkyKey> getPathToCycle() {
+ return pathToCycle;
+ }
+
+ // Given a cycle and a value, if the value is part of the cycle, shift the cycle. Otherwise,
+ // prepend the value to the head of pathToCycle.
+ private static CycleInfo normalizeCycle(final SkyKey value, CycleInfo cycle) {
+ int index = cycle.cycle.indexOf(value);
+ if (index > -1) {
+ if (!cycle.pathToCycle.isEmpty()) {
+ // The head value we are considering is already part of a cycle, but we have reached it by a
+ // roundabout way. Since we should have reached it directly as well, filter this roundabout
+ // way out. Example (c has a dependence on top):
+ // top
+ // / ^
+ // a |
+ // / \ /
+ // b-> c
+ // In the traversal, we start at top, visit a, then c, then top. This yields the
+ // cycle {top,a,c}. Then we visit b, getting (b, {top,a,c}). Then we construct the full
+ // error for a. The error should just be the cycle {top,a,c}, but we have an extra copy of
+ // it via the path through b.
+ return null;
+ }
+ return new CycleInfo(cycle.cycle, index);
+ }
+ return new CycleInfo(Iterables.concat(ImmutableList.of(value), cycle.pathToCycle),
+ cycle.cycle);
+ }
+
+ /**
+ * Normalize multiple cycles. This includes removing multiple paths to the same cycle, so that
+ * a value does not depend on the same cycle multiple ways through the same child value. Note that
+ * a value can still depend on the same cycle multiple ways, it's just that each way must be
+ * through a different child value (a path with a different first element).
+ */
+ static Iterable<CycleInfo> prepareCycles(final SkyKey value, Iterable<CycleInfo> cycles) {
+ final Set<ImmutableList<SkyKey>> alreadyDoneCycles = new HashSet<>();
+ return Iterables.filter(Iterables.transform(cycles,
+ new Function<CycleInfo, CycleInfo>() {
+ @Override
+ public CycleInfo apply(CycleInfo input) {
+ CycleInfo normalized = normalizeCycle(value, input);
+ if (normalized != null && alreadyDoneCycles.add(normalized.cycle)) {
+ return normalized;
+ }
+ return null;
+ }
+ }), Predicates.notNull());
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(cycle, pathToCycle);
+ }
+
+ @Override
+ public boolean equals(Object that) {
+ if (this == that) {
+ return true;
+ }
+ if (!(that instanceof CycleInfo)) {
+ return false;
+ }
+
+ CycleInfo thatCycle = (CycleInfo) that;
+ return thatCycle.cycle.equals(this.cycle) && thatCycle.pathToCycle.equals(this.pathToCycle);
+ }
+
+ @Override
+ public String toString() {
+ return Iterables.toString(pathToCycle) + " -> " + Iterables.toString(cycle);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/skyframe/CyclesReporter.java b/src/main/java/com/google/devtools/build/skyframe/CyclesReporter.java
new file mode 100644
index 0000000..a9b0d8e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skyframe/CyclesReporter.java
@@ -0,0 +1,102 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.events.EventHandler;
+
+/**
+ * An utility for custom reporting of errors from cycles in the the Skyframe graph. This class is
+ * stateful in order to differentiate between new cycles and cycles that have already been
+ * reported (do not reuse the instances or cache the results as it could end up printing
+ * inconsistent information or leak memory). It treats two cycles as the same if they contain the
+ * same {@link SkyKey}s in the same order, but perhaps with different starting points. See
+ * {@link CycleDeduper} for more information.
+ */
+public class CyclesReporter {
+
+ /**
+ * Interface for reporting custom information about a single cycle.
+ */
+ public interface SingleCycleReporter {
+
+ /**
+ * Reports the given cycle and returns {@code true}, or return {@code false} if this
+ * {@link SingleCycleReporter} doesn't know how to report the cycle.
+ *
+ * @param topLevelKey the top level key that transitively depended on the cycle
+ * @param cycleInfo the cycle
+ * @param alreadyReported whether the cycle has already been reported to the
+ * {@link CyclesReporter}.
+ * @param eventHandler the eventHandler to which to report the error
+ */
+ boolean maybeReportCycle(SkyKey topLevelKey, CycleInfo cycleInfo, boolean alreadyReported,
+ EventHandler eventHandler);
+ }
+
+ private final ImmutableList<SingleCycleReporter> cycleReporters;
+ private final CycleDeduper<SkyKey> cycleDeduper = new CycleDeduper<>();
+
+ /**
+ * Constructs a {@link CyclesReporter} that delegates to the given {@link SingleCycleReporter}s,
+ * in the given order, to report custom information about cycles.
+ */
+ public CyclesReporter(SingleCycleReporter... cycleReporters) {
+ this.cycleReporters = ImmutableList.copyOf(cycleReporters);
+ }
+
+ /**
+ * Reports the given cycles, differentiating between cycles that have already been reported.
+ *
+ * @param cycles The {@code Iterable} of cycles.
+ * @param topLevelKey This key represents the top level value key that returned cycle errors.
+ * @param eventHandler the eventHandler to which to report the error
+ */
+ public void reportCycles(Iterable<CycleInfo> cycles, SkyKey topLevelKey,
+ EventHandler eventHandler) {
+ Preconditions.checkNotNull(eventHandler);
+ for (CycleInfo cycleInfo : cycles) {
+ boolean alreadyReported = false;
+ if (!cycleDeduper.seen(cycleInfo.getCycle())) {
+ alreadyReported = true;
+ }
+ boolean successfullyReported = false;
+ for (SingleCycleReporter cycleReporter : cycleReporters) {
+ if (cycleReporter.maybeReportCycle(topLevelKey, cycleInfo, alreadyReported, eventHandler)) {
+ successfullyReported = true;
+ break;
+ }
+ }
+ Preconditions.checkState(successfullyReported,
+ printArbitraryCycle(topLevelKey, cycleInfo, alreadyReported));
+ }
+ }
+
+ private String printArbitraryCycle(SkyKey topLevelKey, CycleInfo cycleInfo,
+ boolean alreadyReported) {
+ StringBuilder cycleMessage = new StringBuilder()
+ .append("topLevelKey: " + topLevelKey + "\n")
+ .append("alreadyReported: " + alreadyReported + "\n")
+ .append("path to cycle:\n");
+ for (SkyKey skyKey : cycleInfo.getPathToCycle()) {
+ cycleMessage.append(skyKey + "\n");
+ }
+ cycleMessage.append("cycle:\n");
+ for (SkyKey skyKey : cycleInfo.getCycle()) {
+ cycleMessage.append(skyKey + "\n");
+ }
+ return cycleMessage.toString();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/skyframe/Differencer.java b/src/main/java/com/google/devtools/build/skyframe/Differencer.java
new file mode 100644
index 0000000..f6433ac
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skyframe/Differencer.java
@@ -0,0 +1,45 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import java.util.Map;
+
+/**
+ * Calculate set of changed values in a graph.
+ */
+public interface Differencer {
+
+ /**
+ * Represents a set of changed values.
+ */
+ interface Diff {
+ /**
+ * Returns the value keys whose values have changed, but for which we don't have the new values.
+ */
+ Iterable<SkyKey> changedKeysWithoutNewValues();
+
+ /**
+ * Returns the value keys whose values have changed, along with their new values.
+ *
+ * <p> The values in here cannot have any dependencies. This is required in order to prevent
+ * conflation of injected values and derived values.
+ */
+ Map<SkyKey, ? extends SkyValue> changedKeysWithNewValues();
+ }
+
+ /**
+ * Returns the value keys that have changed between the two Versions.
+ */
+ Diff getDiff(Version fromVersion, Version toVersion) throws InterruptedException;
+}
diff --git a/src/main/java/com/google/devtools/build/skyframe/DirtiableGraph.java b/src/main/java/com/google/devtools/build/skyframe/DirtiableGraph.java
new file mode 100644
index 0000000..0781222
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skyframe/DirtiableGraph.java
@@ -0,0 +1,28 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+/**
+ * Interface for classes that need to remove values from graph. Currently just used by {@link
+ * EagerInvalidator}.
+ *
+ * <p>This class is not intended for direct use, and is only exposed as public for use in
+ * evaluation implementations outside of this package.
+ */
+public interface DirtiableGraph extends QueryableGraph {
+ /**
+ * Remove the value with given name from the graph.
+ */
+ void remove(SkyKey key);
+}
diff --git a/src/main/java/com/google/devtools/build/skyframe/DirtyKeyTracker.java b/src/main/java/com/google/devtools/build/skyframe/DirtyKeyTracker.java
new file mode 100644
index 0000000..b0b5074
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skyframe/DirtyKeyTracker.java
@@ -0,0 +1,43 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+
+import java.util.Set;
+
+/**
+ * Interface for implementations that need to keep track of dirty SkyKeys.
+ */
+public interface DirtyKeyTracker {
+
+ /**
+ * Marks the {@code skyKey} as dirty.
+ */
+ @ThreadSafe
+ void dirty(SkyKey skyKey);
+
+ /**
+ * Marks the {@code skyKey} as not dirty.
+ */
+ @ThreadSafe
+ void notDirty(SkyKey skyKey);
+
+ /**
+ * Returns the set of keys k for which there was a call to dirty(k) but not a subsequent call
+ * to notDirty(k).
+ */
+ @ThreadSafe
+ Set<SkyKey> getDirtyKeys();
+}
diff --git a/src/main/java/com/google/devtools/build/skyframe/DirtyKeyTrackerImpl.java b/src/main/java/com/google/devtools/build/skyframe/DirtyKeyTrackerImpl.java
new file mode 100644
index 0000000..e3e070cb
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skyframe/DirtyKeyTrackerImpl.java
@@ -0,0 +1,40 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+
+import java.util.Set;
+
+/** Encapsulates a thread-safe set of SkyKeys. */
+public class DirtyKeyTrackerImpl implements DirtyKeyTracker {
+
+ private final Set<SkyKey> dirtyKeys = Sets.newConcurrentHashSet();
+
+ @Override
+ public void dirty(SkyKey skyKey) {
+ dirtyKeys.add(skyKey);
+ }
+
+ @Override
+ public void notDirty(SkyKey skyKey) {
+ dirtyKeys.remove(skyKey);
+ }
+
+ @Override
+ public Set<SkyKey> getDirtyKeys() {
+ return ImmutableSet.copyOf(dirtyKeys);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/skyframe/EagerInvalidator.java b/src/main/java/com/google/devtools/build/skyframe/EagerInvalidator.java
new file mode 100644
index 0000000..fc2a2c7
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skyframe/EagerInvalidator.java
@@ -0,0 +1,85 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.devtools.build.skyframe.InvalidatingNodeVisitor.DeletingNodeVisitor;
+import com.google.devtools.build.skyframe.InvalidatingNodeVisitor.DirtyingNodeVisitor;
+import com.google.devtools.build.skyframe.InvalidatingNodeVisitor.InvalidationState;
+
+/**
+ * Utility class for performing eager invalidation on Skyframe graphs.
+ *
+ * <p>This is intended only for use in alternative {@code MemoizingEvaluator} implementations.
+ */
+public final class EagerInvalidator {
+
+ private EagerInvalidator() {}
+
+ /**
+ * Deletes given values. The {@code traverseGraph} parameter controls whether this method deletes
+ * (transitive) dependents of these nodes and relevant graph edges, or just the nodes themselves.
+ * Deleting just the nodes is inconsistent unless the graph will not be used for incremental
+ * builds in the future, but unfortunately there is a case where we delete nodes intra-build. As
+ * long as the full upward transitive closure of the nodes is specified for deletion, the graph
+ * remains consistent.
+ */
+ public static void delete(DirtiableGraph graph, Iterable<SkyKey> diff,
+ EvaluationProgressReceiver invalidationReceiver, InvalidationState state,
+ boolean traverseGraph, DirtyKeyTracker dirtyKeyTracker) throws InterruptedException {
+ InvalidatingNodeVisitor visitor =
+ createVisitor(/*delete=*/true, graph, diff, invalidationReceiver, state, traverseGraph,
+ dirtyKeyTracker);
+ if (visitor != null) {
+ visitor.run();
+ }
+ }
+
+ /**
+ * Creates an invalidation visitor that is ready to run. Caller should call #run() on the visitor.
+ * Allows test classes to keep a reference to the visitor, and await exceptions/interrupts.
+ */
+ @VisibleForTesting
+ static InvalidatingNodeVisitor createVisitor(boolean delete, DirtiableGraph graph,
+ Iterable<SkyKey> diff, EvaluationProgressReceiver invalidationReceiver,
+ InvalidationState state, boolean traverseGraph, DirtyKeyTracker dirtyKeyTracker) {
+ state.update(diff);
+ if (state.isEmpty()) {
+ return null;
+ }
+ return delete
+ ? new DeletingNodeVisitor(graph, invalidationReceiver, state, traverseGraph,
+ dirtyKeyTracker)
+ : new DirtyingNodeVisitor(graph, invalidationReceiver, state, dirtyKeyTracker);
+ }
+
+ /**
+ * Invalidates given values and their upward transitive closure in the graph.
+ */
+ public static void invalidate(DirtiableGraph graph, Iterable<SkyKey> diff,
+ EvaluationProgressReceiver invalidationReceiver, InvalidationState state,
+ DirtyKeyTracker dirtyKeyTracker)
+ throws InterruptedException {
+ // If we are invalidating, we must be in an incremental build by definition, so we must
+ // maintain a consistent graph state by traversing the graph and invalidating transitive
+ // dependencies. If edges aren't present, it would be impossible to check the dependencies of
+ // a dirty node in any case.
+ InvalidatingNodeVisitor visitor =
+ createVisitor(/*delete=*/false, graph, diff, invalidationReceiver, state,
+ /*traverseGraph=*/true, dirtyKeyTracker);
+ if (visitor != null) {
+ visitor.run();
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/skyframe/EdgelessNodeEntry.java b/src/main/java/com/google/devtools/build/skyframe/EdgelessNodeEntry.java
new file mode 100644
index 0000000..98fb61e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skyframe/EdgelessNodeEntry.java
@@ -0,0 +1,32 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+/**
+ * NodeEntry that does not store edges (directDeps and reverseDeps) when the node is done. Used to
+ * save memory when it is known that the graph will not be reused.
+ *
+ * <p>Graph edges must be stored for incremental builds, but if this program will terminate after a
+ * single run, edges can be thrown away in order to save memory. The edges will be stored in the
+ * {@link BuildingState} as usual while the node is being built, but will not be stored once the
+ * node is done and written to the graph. Any attempt to access the edges once the node is done will
+ * fail the build fast.
+ */
+class EdgelessNodeEntry extends NodeEntry {
+ @Override
+ protected boolean keepEdges() {
+ return false;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/skyframe/ErrorInfo.java b/src/main/java/com/google/devtools/build/skyframe/ErrorInfo.java
new file mode 100644
index 0000000..6873d19
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skyframe/ErrorInfo.java
@@ -0,0 +1,157 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.skyframe.SkyFunctionException.ReifiedSkyFunctionException;
+
+import java.io.Serializable;
+import java.util.Collection;
+
+import javax.annotation.Nullable;
+
+/**
+ * Information about why a {@link SkyValue} failed to evaluate successfully.
+ *
+ * <p>This is intended only for use in alternative {@code MemoizingEvaluator} implementations.
+ */
+public class ErrorInfo implements Serializable {
+ /**
+ * The set of descendants of this value that failed to build
+ */
+ private final NestedSet<SkyKey> rootCauses;
+
+ /**
+ * An exception thrown upon a value's failure to build. The exception is used for reporting, and
+ * thus may ultimately be rethrown by the caller. As well, during a --nokeep_going evaluation, if
+ * an error value is encountered from an earlier --keep_going build, the exception to be thrown is
+ * taken from here.
+ */
+ @Nullable private final Exception exception;
+ private final SkyKey rootCauseOfException;
+
+ private final Iterable<CycleInfo> cycles;
+
+ private final boolean isTransient;
+ private final boolean isCatastrophic;
+
+ public ErrorInfo(ReifiedSkyFunctionException builderException) {
+ this.rootCauseOfException = builderException.getRootCauseSkyKey();
+ this.rootCauses = NestedSetBuilder.create(Order.STABLE_ORDER, rootCauseOfException);
+ this.exception = Preconditions.checkNotNull(builderException.getCause(), builderException);
+ this.cycles = ImmutableList.of();
+ this.isTransient = builderException.isTransient();
+ this.isCatastrophic = builderException.isCatastrophic();
+ }
+
+ ErrorInfo(CycleInfo cycleInfo) {
+ this.rootCauses = NestedSetBuilder.emptySet(Order.STABLE_ORDER);
+ this.exception = null;
+ this.rootCauseOfException = null;
+ this.cycles = ImmutableList.of(cycleInfo);
+ this.isTransient = false;
+ this.isCatastrophic = false;
+ }
+
+ public ErrorInfo(SkyKey currentValue, Collection<ErrorInfo> childErrors) {
+ Preconditions.checkNotNull(currentValue);
+ Preconditions.checkState(!childErrors.isEmpty(),
+ "Error value %s with no exception must depend on another error value", currentValue);
+ NestedSetBuilder<SkyKey> builder = NestedSetBuilder.stableOrder();
+ ImmutableList.Builder<CycleInfo> cycleBuilder = ImmutableList.builder();
+ Exception firstException = null;
+ SkyKey firstChildKey = null;
+ boolean isTransient = false;
+ boolean isCatastrophic = false;
+ // Arbitrarily pick the first error.
+ for (ErrorInfo child : childErrors) {
+ if (firstException == null) {
+ firstException = child.getException();
+ firstChildKey = child.getRootCauseOfException();
+ }
+ builder.addTransitive(child.rootCauses);
+ cycleBuilder.addAll(CycleInfo.prepareCycles(currentValue, child.cycles));
+ isTransient |= child.isTransient();
+ isCatastrophic |= child.isCatastrophic();
+ }
+ this.rootCauses = builder.build();
+ this.exception = firstException;
+ this.rootCauseOfException = firstChildKey;
+ this.cycles = cycleBuilder.build();
+ this.isTransient = isTransient;
+ this.isCatastrophic = isCatastrophic;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("<ErrorInfo exception=%s rootCauses=%s cycles=%s>",
+ exception, rootCauses, cycles);
+ }
+
+ /**
+ * The root causes of a value that failed to build are its descendant values that failed to build.
+ * If a value's descendants all built successfully, but it failed to, its root cause will be
+ * itself. If a value depends on a cycle, but has no other errors, this method will return
+ * the empty set.
+ */
+ public Iterable<SkyKey> getRootCauses() {
+ return rootCauses;
+ }
+
+ /**
+ * The exception thrown when building a value. May be null if value's only error is depending
+ * on a cycle.
+ */
+ @Nullable public Exception getException() {
+ return exception;
+ }
+
+ public SkyKey getRootCauseOfException() {
+ return rootCauseOfException;
+ }
+
+ /**
+ * Any cycles found when building this value.
+ *
+ * <p>If there are a large number of cycles, only a limited number are returned here.
+ *
+ * <p>If this value has a child through which there are multiple paths to the same cycle, only one
+ * path is returned here. However, if there are multiple paths to the same cycle, each of which
+ * goes through a different child, each of them is returned here.
+ */
+ public Iterable<CycleInfo> getCycleInfo() {
+ return cycles;
+ }
+
+ /**
+ * Returns true iff the error is transient, i.e. if retrying the same computation could lead to a
+ * different result.
+ */
+ public boolean isTransient() {
+ return isTransient;
+ }
+
+
+ /**
+ * Returns true iff the error is catastrophic, i.e. it should halt even for a keepGoing update()
+ * call.
+ */
+ public boolean isCatastrophic() {
+ return isCatastrophic;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/skyframe/ErrorTransienceValue.java b/src/main/java/com/google/devtools/build/skyframe/ErrorTransienceValue.java
new file mode 100644
index 0000000..c0c445d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skyframe/ErrorTransienceValue.java
@@ -0,0 +1,29 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+/**
+ * A value that represents "error transience", i.e. anything which may have caused an unexpected
+ * failure.
+ */
+public final class ErrorTransienceValue implements SkyValue {
+ public static final SkyFunctionName FUNCTION_NAME =
+ new SkyFunctionName("ERROR_TRANSIENCE", false);
+
+ ErrorTransienceValue() {}
+
+ public static SkyKey key() {
+ return new SkyKey(FUNCTION_NAME, "ERROR_TRANSIENCE");
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/skyframe/EvaluableGraph.java b/src/main/java/com/google/devtools/build/skyframe/EvaluableGraph.java
new file mode 100644
index 0000000..3d9a934
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skyframe/EvaluableGraph.java
@@ -0,0 +1,26 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+/**
+ * Interface between a single version of the graph and the evaluator. Supports mutation of that
+ * single version of the graph.
+ */
+interface EvaluableGraph extends QueryableGraph {
+ /**
+ * Creates a new node with the specified key if it does not exist yet. Returns the node entry
+ * (either the existing one or the one just created), never {@code null}.
+ */
+ NodeEntry createIfAbsent(SkyKey key);
+}
diff --git a/src/main/java/com/google/devtools/build/skyframe/EvaluationProgressReceiver.java b/src/main/java/com/google/devtools/build/skyframe/EvaluationProgressReceiver.java
new file mode 100644
index 0000000..7928878
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skyframe/EvaluationProgressReceiver.java
@@ -0,0 +1,77 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.devtools.build.lib.concurrent.ThreadSafety;
+
+/**
+ * Receiver to inform callers which values have been invalidated. Values may be invalidated and then
+ * re-validated if they have been found not to be changed.
+ */
+public interface EvaluationProgressReceiver {
+ /**
+ * New state of the value entry after evaluation.
+ */
+ enum EvaluationState {
+ /** The value was successfully re-evaluated. */
+ BUILT,
+ /** The value is clean or re-validated. */
+ CLEAN,
+ }
+
+ /**
+ * New state of the value entry after invalidation.
+ */
+ enum InvalidationState {
+ /** The value is dirty, although it might get re-validated again. */
+ DIRTY,
+ /** The value is dirty and got deleted, cannot get re-validated again. */
+ DELETED,
+ }
+
+ /**
+ * Notifies that {@code value} has been invalidated.
+ *
+ * <p>{@code state} indicates the new state of the value.
+ *
+ * <p>This method is not called on invalidation of values which do not have a value (usually
+ * because they are in error).
+ *
+ * <p>May be called concurrently from multiple threads, possibly with the same {@code value}
+ * object.
+ */
+ @ThreadSafety.ThreadSafe
+ void invalidated(SkyValue value, InvalidationState state);
+
+ /**
+ * Notifies that {@code skyKey} is about to get queued for evaluation.
+ *
+ * <p>Note that we don't guarantee that it actually got enqueued or will, only that if
+ * everything "goes well" (e.g. no interrupts happen) it will.
+ *
+ * <p>This guarantee is intentionally vague to encourage writing robust implementations.
+ */
+ @ThreadSafety.ThreadSafe
+ void enqueueing(SkyKey skyKey);
+
+ /**
+ * Notifies that {@code value} has been evaluated.
+ *
+ * <p>{@code state} indicates the new state of the value.
+ *
+ * <p>This method is not called if the value builder threw an error when building this value.
+ */
+ @ThreadSafety.ThreadSafe
+ void evaluated(SkyKey skyKey, SkyValue value, EvaluationState state);
+}
diff --git a/src/main/java/com/google/devtools/build/skyframe/EvaluationResult.java b/src/main/java/com/google/devtools/build/skyframe/EvaluationResult.java
new file mode 100644
index 0000000..e518dca
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skyframe/EvaluationResult.java
@@ -0,0 +1,163 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * The result of a Skyframe {@link Evaluator#eval} call. Will contain all the
+ * successfully evaluated values, retrievable through {@link #get}. As well, the {@link ErrorInfo}
+ * for the first value that failed to evaluate (in the non-keep-going case), or any remaining values
+ * that failed to evaluate (in the keep-going case) will be retrievable.
+ *
+ * @param <T> The type of the values that the caller has requested.
+ */
+public class EvaluationResult<T extends SkyValue> {
+
+ private final boolean hasError;
+
+ private final Map<SkyKey, T> resultMap;
+ private final Map<SkyKey, ErrorInfo> errorMap;
+
+ /**
+ * Constructor for the "completed" case. Used only by {@link Builder}.
+ */
+ private EvaluationResult(Map<SkyKey, T> result, Map<SkyKey, ErrorInfo> errorMap,
+ boolean hasError) {
+ Preconditions.checkState(errorMap.isEmpty() || hasError,
+ "result=%s, errorMap=%s", result, errorMap);
+ this.resultMap = Preconditions.checkNotNull(result);
+ this.errorMap = Preconditions.checkNotNull(errorMap);
+ this.hasError = hasError;
+ }
+
+ /**
+ * Get a successfully evaluated value.
+ */
+ public T get(SkyKey key) {
+ Preconditions.checkNotNull(resultMap, key);
+ return resultMap.get(key);
+ }
+
+ /**
+ * @return Whether or not the eval successfully evaluated all requested values. Note that this
+ * may return true even if all values returned are available in get(). This happens if a top-level
+ * value depends transitively on some value that recovered from a {@link SkyFunctionException}.
+ */
+ public boolean hasError() {
+ return hasError;
+ }
+
+ /**
+ * @return All successfully evaluated {@link SkyValue}s.
+ */
+ public Collection<T> values() {
+ return Collections.unmodifiableCollection(resultMap.values());
+ }
+
+ /**
+ * Returns {@link Map} of {@link SkyKey}s to {@link ErrorInfo}. Note that currently some
+ * of the returned SkyKeys may not be the ones requested by the user. Moreover, the SkyKey
+ * is not necessarily the cause of the error -- it is just the value that was being evaluated
+ * when the error was discovered. For the cause of the error, use
+ * {@link ErrorInfo#getRootCauses()} on each ErrorInfo.
+ */
+ public Map<SkyKey, ErrorInfo> errorMap() {
+ return ImmutableMap.copyOf(errorMap);
+ }
+
+ /**
+ * @param key {@link SkyKey} to get {@link ErrorInfo} for.
+ */
+ public ErrorInfo getError(SkyKey key) {
+ return Preconditions.checkNotNull(errorMap, key).get(key);
+ }
+
+ /**
+ * @return Names of all values that were successfully evaluated.
+ */
+ public <S> Collection<? extends S> keyNames() {
+ return this.<S>getNames(resultMap.keySet());
+ }
+
+ @SuppressWarnings("unchecked")
+ private <S> Collection<? extends S> getNames(Collection<SkyKey> keys) {
+ Collection<S> names = Lists.newArrayListWithCapacity(keys.size());
+ for (SkyKey key : keys) {
+ names.add((S) key.argument());
+ }
+ return names;
+ }
+
+ /**
+ * Returns some error info. Convenience method equivalent to
+ * Iterables.getFirst({@link #errorMap()}, null).getValue().
+ */
+ public ErrorInfo getError() {
+ return Iterables.getFirst(errorMap.entrySet(), null).getValue();
+ }
+
+ @Override
+ @SuppressWarnings("deprecation")
+ public String toString() {
+ return Objects.toStringHelper(this) // MoreObjects is not in Guava
+ .add("hasError", hasError)
+ .add("errorMap", errorMap)
+ .add("resultMap", resultMap)
+ .toString();
+ }
+
+ public static <T extends SkyValue> Builder<T> builder() {
+ return new Builder<>();
+ }
+
+ /**
+ * Builder for {@link EvaluationResult}.
+ *
+ * <p>This is intended only for use in alternative {@code MemoizingEvaluator} implementations.
+ */
+ public static class Builder<T extends SkyValue> {
+ private final Map<SkyKey, T> result = new HashMap<>();
+ private final Map<SkyKey, ErrorInfo> errors = new HashMap<>();
+ private boolean hasError = false;
+
+ @SuppressWarnings("unchecked")
+ public Builder<T> addResult(SkyKey key, SkyValue value) {
+ result.put(key, Preconditions.checkNotNull((T) value, key));
+ return this;
+ }
+
+ public Builder<T> addError(SkyKey key, ErrorInfo error) {
+ errors.put(key, Preconditions.checkNotNull(error, key));
+ return this;
+ }
+
+ public EvaluationResult<T> build() {
+ return new EvaluationResult<>(result, errors, hasError);
+ }
+
+ public void setHasError(boolean hasError) {
+ this.hasError = hasError;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/skyframe/Evaluator.java b/src/main/java/com/google/devtools/build/skyframe/Evaluator.java
new file mode 100644
index 0000000..342eff1
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skyframe/Evaluator.java
@@ -0,0 +1,43 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.devtools.build.lib.events.EventHandler;
+
+/**
+ * An interface for the evaluator for a particular graph version.
+ */
+public interface Evaluator {
+ /**
+ * Factory to create Evaluator instances.
+ */
+ interface Factory {
+ /**
+ * @param graph the graph to operate on
+ * @param graphVersion the version at which to write entries in the graph.
+ * @param reporter where to write warning/error/progress messages.
+ * @param keepGoing whether {@link #eval} should continue if building a {link Value} fails.
+ * Otherwise, we throw an exception on failure.
+ */
+ Evaluator create(ProcessableGraph graph, long graphVersion, EventHandler reporter,
+ boolean keepGoing);
+ }
+
+ /**
+ * Evaluates a set of values. Returns an {@link EvaluationResult}. All elements of skyKeys must
+ * be keys for Values of subtype T.
+ */
+ <T extends SkyValue> EvaluationResult<T> eval(Iterable<SkyKey> skyKeys)
+ throws InterruptedException;
+}
diff --git a/src/main/java/com/google/devtools/build/skyframe/ImmutableDiff.java b/src/main/java/com/google/devtools/build/skyframe/ImmutableDiff.java
new file mode 100644
index 0000000..46ab29e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skyframe/ImmutableDiff.java
@@ -0,0 +1,43 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+import java.util.Map;
+
+/**
+ * Immutable implementation of {@link Differencer.Diff}.
+ */
+public class ImmutableDiff implements Differencer.Diff {
+
+ private final ImmutableList<SkyKey> valuesToInvalidate;
+ private final ImmutableMap<SkyKey, SkyValue> valuesToInject;
+
+ public ImmutableDiff(Iterable<SkyKey> valuesToInvalidate, Map<SkyKey, SkyValue> valuesToInject) {
+ this.valuesToInvalidate = ImmutableList.copyOf(valuesToInvalidate);
+ this.valuesToInject = ImmutableMap.copyOf(valuesToInject);
+ }
+
+ @Override
+ public Iterable<SkyKey> changedKeysWithoutNewValues() {
+ return valuesToInvalidate;
+ }
+
+ @Override
+ public Map<SkyKey, SkyValue> changedKeysWithNewValues() {
+ return valuesToInject;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/skyframe/InMemoryGraph.java b/src/main/java/com/google/devtools/build/skyframe/InMemoryGraph.java
new file mode 100644
index 0000000..44956da
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skyframe/InMemoryGraph.java
@@ -0,0 +1,126 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+import com.google.common.collect.MapMaker;
+import com.google.common.collect.Maps;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.concurrent.ConcurrentMap;
+
+import javax.annotation.Nullable;
+
+/**
+ * An in-memory graph implementation. All operations are thread-safe with ConcurrentMap semantics.
+ * Also see {@link NodeEntry}.
+ *
+ * <p>This class is public only for use in alternative graph implementations.
+ */
+public class InMemoryGraph implements ProcessableGraph {
+
+ protected final ConcurrentMap<SkyKey, NodeEntry> nodeMap =
+ new MapMaker().initialCapacity(1024).concurrencyLevel(200).makeMap();
+ private final boolean keepEdges;
+
+ InMemoryGraph() {
+ this(/*keepEdges=*/true);
+ }
+
+ public InMemoryGraph(boolean keepEdges) {
+ this.keepEdges = keepEdges;
+ }
+
+ @Override
+ public void remove(SkyKey skyKey) {
+ nodeMap.remove(skyKey);
+ }
+
+ @Override
+ public NodeEntry get(SkyKey skyKey) {
+ return nodeMap.get(skyKey);
+ }
+
+ @Override
+ public NodeEntry createIfAbsent(SkyKey key) {
+ NodeEntry newval = keepEdges ? new NodeEntry() : new EdgelessNodeEntry();
+ NodeEntry oldval = nodeMap.putIfAbsent(key, newval);
+ return oldval == null ? newval : oldval;
+ }
+
+ /** Only done nodes exist to the outside world. */
+ private static final Predicate<NodeEntry> NODE_DONE_PREDICATE =
+ new Predicate<NodeEntry>() {
+ @Override
+ public boolean apply(NodeEntry entry) {
+ return entry != null && entry.isDone();
+ }
+ };
+
+ /**
+ * Returns a value, if it exists. If not, returns null.
+ */
+ @Nullable public SkyValue getValue(SkyKey key) {
+ NodeEntry entry = get(key);
+ return NODE_DONE_PREDICATE.apply(entry) ? entry.getValue() : null;
+ }
+
+ /**
+ * Returns a read-only live view of the nodes in the graph. All node are included. Dirty values
+ * include their Node value. Values in error have a null value.
+ */
+ Map<SkyKey, SkyValue> getValues() {
+ return Collections.unmodifiableMap(Maps.transformValues(
+ nodeMap,
+ new Function<NodeEntry, SkyValue>() {
+ @Override
+ public SkyValue apply(NodeEntry entry) {
+ return entry.toValue();
+ }
+ }));
+ }
+
+ /**
+ * Returns a read-only live view of the done values in the graph. Dirty, changed, and error values
+ * are not present in the returned map
+ */
+ Map<SkyKey, SkyValue> getDoneValues() {
+ return Collections.unmodifiableMap(Maps.filterValues(Maps.transformValues(
+ nodeMap,
+ new Function<NodeEntry, SkyValue>() {
+ @Override
+ public SkyValue apply(NodeEntry entry) {
+ return entry.isDone() ? entry.getValue() : null;
+ }
+ }), Predicates.notNull()));
+ }
+
+ // Only for use by MemoizingEvaluator#delete
+ Map<SkyKey, NodeEntry> getAllValues() {
+ return Collections.unmodifiableMap(nodeMap);
+ }
+
+ @VisibleForTesting
+ protected ConcurrentMap<SkyKey, NodeEntry> getNodeMap() {
+ return nodeMap;
+ }
+
+ boolean keepsEdges() {
+ return keepEdges;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/skyframe/InMemoryMemoizingEvaluator.java b/src/main/java/com/google/devtools/build/skyframe/InMemoryMemoizingEvaluator.java
new file mode 100644
index 0000000..827cc7b
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skyframe/InMemoryMemoizingEvaluator.java
@@ -0,0 +1,317 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.skyframe.Differencer.Diff;
+import com.google.devtools.build.skyframe.InvalidatingNodeVisitor.DeletingInvalidationState;
+import com.google.devtools.build.skyframe.InvalidatingNodeVisitor.DirtyingInvalidationState;
+import com.google.devtools.build.skyframe.InvalidatingNodeVisitor.InvalidationState;
+import com.google.devtools.build.skyframe.NodeEntry.DependencyState;
+
+import java.io.PrintStream;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import javax.annotation.Nullable;
+
+/**
+ * An inmemory implementation that uses the eager invalidation strategy. This class is, by itself,
+ * not thread-safe. Neither is it thread-safe to use this class in parallel with any of the
+ * returned graphs. However, it is allowed to access the graph from multiple threads as long as
+ * that does not happen in parallel with an {@link #evaluate} call.
+ *
+ * <p>This memoizing evaluator requires a sequential versioning scheme. Evaluations
+ * must pass in a monotonically increasing {@link IntVersion}.
+ */
+public final class InMemoryMemoizingEvaluator implements MemoizingEvaluator {
+
+ private final ImmutableMap<? extends SkyFunctionName, ? extends SkyFunction> skyFunctions;
+ @Nullable private final EvaluationProgressReceiver progressReceiver;
+ // Not final only for testing.
+ private InMemoryGraph graph;
+ private IntVersion lastGraphVersion = null;
+
+ // State related to invalidation and deletion.
+ private Set<SkyKey> valuesToDelete = new LinkedHashSet<>();
+ private Set<SkyKey> valuesToDirty = new LinkedHashSet<>();
+ private Map<SkyKey, SkyValue> valuesToInject = new HashMap<>();
+ private final DirtyKeyTracker dirtyKeyTracker = new DirtyKeyTrackerImpl();
+ private final InvalidationState deleterState = new DeletingInvalidationState();
+ private final Differencer differencer;
+
+ // Keep edges in graph. Can be false to save memory, in which case incremental builds are
+ // not possible.
+ private final boolean keepEdges;
+
+ // Values that the caller explicitly specified are assumed to be changed -- they will be
+ // re-evaluated even if none of their children are changed.
+ private final InvalidationState invalidatorState = new DirtyingInvalidationState();
+
+ private final EmittedEventState emittedEventState;
+
+ private final AtomicBoolean evaluating = new AtomicBoolean(false);
+
+ public InMemoryMemoizingEvaluator(
+ Map<? extends SkyFunctionName, ? extends SkyFunction> skyFunctions, Differencer differencer) {
+ this(skyFunctions, differencer, null);
+ }
+
+ public InMemoryMemoizingEvaluator(
+ Map<? extends SkyFunctionName, ? extends SkyFunction> skyFunctions, Differencer differencer,
+ @Nullable EvaluationProgressReceiver invalidationReceiver) {
+ this(skyFunctions, differencer, invalidationReceiver, new EmittedEventState(), true);
+ }
+
+ public InMemoryMemoizingEvaluator(
+ Map<? extends SkyFunctionName, ? extends SkyFunction> skyFunctions, Differencer differencer,
+ @Nullable EvaluationProgressReceiver invalidationReceiver,
+ EmittedEventState emittedEventState, boolean keepEdges) {
+ this.skyFunctions = ImmutableMap.copyOf(skyFunctions);
+ this.differencer = Preconditions.checkNotNull(differencer);
+ this.progressReceiver = invalidationReceiver;
+ this.graph = new InMemoryGraph(keepEdges);
+ this.emittedEventState = emittedEventState;
+ this.keepEdges = keepEdges;
+ }
+
+ private void invalidate(Iterable<SkyKey> diff) {
+ Iterables.addAll(valuesToDirty, diff);
+ }
+
+ @Override
+ public void delete(final Predicate<SkyKey> deletePredicate) {
+ valuesToDelete.addAll(
+ Maps.filterEntries(graph.getAllValues(), new Predicate<Entry<SkyKey, NodeEntry>>() {
+ @Override
+ public boolean apply(Entry<SkyKey, NodeEntry> input) {
+ return input.getValue().isDirty() || deletePredicate.apply(input.getKey());
+ }
+ }).keySet());
+ }
+
+ @Override
+ public void deleteDirty(long versionAgeLimit) {
+ Preconditions.checkArgument(versionAgeLimit >= 0);
+ final Version threshold = new IntVersion(lastGraphVersion.getVal() - versionAgeLimit);
+ valuesToDelete.addAll(
+ Sets.filter(dirtyKeyTracker.getDirtyKeys(), new Predicate<SkyKey>() {
+ @Override
+ public boolean apply(SkyKey skyKey) {
+ NodeEntry entry = graph.get(skyKey);
+ Preconditions.checkNotNull(entry, skyKey);
+ Preconditions.checkState(entry.isDirty(), skyKey);
+ return entry.getVersion().atMost(threshold);
+ }
+ }));
+ }
+
+ @Override
+ public <T extends SkyValue> EvaluationResult<T> evaluate(Iterable<SkyKey> roots, Version version,
+ boolean keepGoing, int numThreads, EventHandler eventHandler)
+ throws InterruptedException {
+ // NOTE: Performance critical code. See bug "Null build performance parity".
+ IntVersion intVersion = (IntVersion) version;
+ Preconditions.checkState((lastGraphVersion == null && intVersion.getVal() == 0)
+ || version.equals(lastGraphVersion.next()),
+ "InMemoryGraph supports only monotonically increasing Integer versions: %s %s",
+ lastGraphVersion, version);
+ setAndCheckEvaluateState(true, roots);
+ try {
+ // The RecordingDifferencer implementation is not quite working as it should be at this point.
+ // It clears the internal data structures after getDiff is called and will not return
+ // diffs for historical versions. This makes the following code sensitive to interrupts.
+ // Ideally we would simply not update lastGraphVersion if an interrupt occurs.
+ Diff diff = differencer.getDiff(lastGraphVersion, version);
+ valuesToInject.putAll(diff.changedKeysWithNewValues());
+ invalidate(diff.changedKeysWithoutNewValues());
+ pruneInjectedValues(valuesToInject);
+ invalidate(valuesToInject.keySet());
+
+ performInvalidation();
+ injectValues(intVersion);
+
+ ParallelEvaluator evaluator = new ParallelEvaluator(graph, intVersion,
+ skyFunctions, eventHandler, emittedEventState, keepGoing, numThreads, progressReceiver,
+ dirtyKeyTracker);
+ return evaluator.eval(roots);
+ } finally {
+ lastGraphVersion = intVersion;
+ setAndCheckEvaluateState(false, roots);
+ }
+ }
+
+ /**
+ * Removes entries in {@code valuesToInject} whose values are equal to the present values in the
+ * graph.
+ */
+ private void pruneInjectedValues(Map<SkyKey, SkyValue> valuesToInject) {
+ for (Iterator<Entry<SkyKey, SkyValue>> it = valuesToInject.entrySet().iterator();
+ it.hasNext();) {
+ Entry<SkyKey, SkyValue> entry = it.next();
+ SkyKey key = entry.getKey();
+ SkyValue newValue = entry.getValue();
+ NodeEntry prevEntry = graph.get(key);
+ if (prevEntry != null && prevEntry.isDone()) {
+ Iterable<SkyKey> directDeps = prevEntry.getDirectDeps();
+ Preconditions.checkState(Iterables.isEmpty(directDeps),
+ "existing entry for %s has deps: %s", key, directDeps);
+ if (newValue.equals(prevEntry.getValue())
+ && !valuesToDirty.contains(key) && !valuesToDelete.contains(key)) {
+ it.remove();
+ }
+ }
+ }
+ }
+
+ /**
+ * Injects values in {@code valuesToInject} into the graph.
+ */
+ private void injectValues(IntVersion version) {
+ if (valuesToInject.isEmpty()) {
+ return;
+ }
+ for (Entry<SkyKey, SkyValue> entry : valuesToInject.entrySet()) {
+ SkyKey key = entry.getKey();
+ SkyValue value = entry.getValue();
+ Preconditions.checkState(value != null, key);
+ NodeEntry prevEntry = graph.createIfAbsent(key);
+ if (prevEntry.isDirty()) {
+ // There was an existing entry for this key in the graph.
+ // Get the node in the state where it is able to accept a value.
+ Preconditions.checkState(prevEntry.getTemporaryDirectDeps().isEmpty(), key);
+
+ DependencyState newState = prevEntry.addReverseDepAndCheckIfDone(null);
+ Preconditions.checkState(newState == DependencyState.NEEDS_SCHEDULING, key);
+
+ // Check that the previous node has no dependencies. Overwriting a value with deps with an
+ // injected value (which is by definition deps-free) needs a little additional bookkeeping
+ // (removing reverse deps from the dependencies), but more importantly it's something that
+ // we want to avoid, because it indicates confusion of input values and derived values.
+ Preconditions.checkState(prevEntry.noDepsLastBuild(),
+ "existing entry for %s has deps: %s", key, prevEntry);
+ }
+ prevEntry.setValue(value, version);
+ // The evaluate method previously invalidated all keys in valuesToInject that survived the
+ // pruneInjectedValues call. Now that this key's injected value is set, it is no longer dirty.
+ dirtyKeyTracker.notDirty(key);
+ }
+ // Start with a new map to avoid bloat since clear() does not downsize the map.
+ valuesToInject = new HashMap<>();
+ }
+
+ private void performInvalidation() throws InterruptedException {
+ EagerInvalidator.delete(graph, valuesToDelete, progressReceiver, deleterState, keepEdges,
+ dirtyKeyTracker);
+ // Note that clearing the valuesToDelete would not do an internal resizing. Therefore, if any
+ // build has a large set of dirty values, subsequent operations (even clearing) will be slower.
+ // Instead, just start afresh with a new LinkedHashSet.
+ valuesToDelete = new LinkedHashSet<>();
+
+ EagerInvalidator.invalidate(graph, valuesToDirty, progressReceiver, invalidatorState,
+ dirtyKeyTracker);
+ // Ditto.
+ valuesToDirty = new LinkedHashSet<>();
+ }
+
+ private void setAndCheckEvaluateState(boolean newValue, Object requestInfo) {
+ Preconditions.checkState(evaluating.getAndSet(newValue) != newValue,
+ "Re-entrant evaluation for request: %s", requestInfo);
+ }
+
+ @Override
+ public Map<SkyKey, SkyValue> getValues() {
+ return graph.getValues();
+ }
+
+ @Override
+ public Map<SkyKey, SkyValue> getDoneValues() {
+ return graph.getDoneValues();
+ }
+
+ @Override
+ @Nullable public SkyValue getExistingValueForTesting(SkyKey key) {
+ return graph.getValue(key);
+ }
+
+ @Override
+ @Nullable public ErrorInfo getExistingErrorForTesting(SkyKey key) {
+ NodeEntry entry = graph.get(key);
+ return (entry == null || !entry.isDone()) ? null : entry.getErrorInfo();
+ }
+
+ public void setGraphForTesting(InMemoryGraph graph) {
+ this.graph = graph;
+ }
+
+ @Override
+ public void dump(boolean summarize, PrintStream out) {
+ if (summarize) {
+ long nodes = 0;
+ long edges = 0;
+ for (NodeEntry entry : graph.getAllValues().values()) {
+ nodes++;
+ if (entry.isDone()) {
+ edges += Iterables.size(entry.getDirectDeps());
+ }
+ }
+ out.println("Node count: " + nodes);
+ out.println("Edge count: " + edges);
+ } else {
+ Function<SkyKey, String> keyFormatter =
+ new Function<SkyKey, String>() {
+ @Override
+ public String apply(SkyKey key) {
+ return String.format("%s:%s",
+ key.functionName(), key.argument().toString().replace('\n', '_'));
+ }
+ };
+
+ for (Entry<SkyKey, NodeEntry> mapPair : graph.getAllValues().entrySet()) {
+ SkyKey key = mapPair.getKey();
+ NodeEntry entry = mapPair.getValue();
+ if (entry.isDone()) {
+ out.print(keyFormatter.apply(key));
+ out.print("|");
+ out.println(Joiner.on('|').join(
+ Iterables.transform(entry.getDirectDeps(), keyFormatter)));
+ }
+ }
+ }
+ }
+
+ public static final EvaluatorSupplier SUPPLIER = new EvaluatorSupplier() {
+ @Override
+ public MemoizingEvaluator create(
+ Map<? extends SkyFunctionName, ? extends SkyFunction> skyFunctions, Differencer differencer,
+ @Nullable EvaluationProgressReceiver invalidationReceiver,
+ EmittedEventState emittedEventState, boolean keepEdges) {
+ return new InMemoryMemoizingEvaluator(skyFunctions, differencer, invalidationReceiver,
+ emittedEventState, keepEdges);
+ }
+ };
+}
diff --git a/src/main/java/com/google/devtools/build/skyframe/Injectable.java b/src/main/java/com/google/devtools/build/skyframe/Injectable.java
new file mode 100644
index 0000000..5325df3
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skyframe/Injectable.java
@@ -0,0 +1,23 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import java.util.Map;
+
+/**
+ * An object that accepts Skyframe key / value mapping.
+ */
+public interface Injectable {
+ void inject(Map<SkyKey, ? extends SkyValue> values);
+}
diff --git a/src/main/java/com/google/devtools/build/skyframe/IntVersion.java b/src/main/java/com/google/devtools/build/skyframe/IntVersion.java
new file mode 100644
index 0000000..3d2a31d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skyframe/IntVersion.java
@@ -0,0 +1,61 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+/**
+ * Versioning scheme based on integers.
+ */
+public final class IntVersion implements Version {
+
+ private final long val;
+
+ public IntVersion(long val) {
+ this.val = val;
+ }
+
+ public long getVal() {
+ return val;
+ }
+
+ public IntVersion next() {
+ return new IntVersion(val + 1);
+ }
+
+ @Override
+ public boolean atMost(Version other) {
+ if (!(other instanceof IntVersion)) {
+ return false;
+ }
+ return val <= ((IntVersion) other).val;
+ }
+
+ @Override
+ public int hashCode() {
+ return Long.valueOf(val).hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof IntVersion) {
+ IntVersion other = (IntVersion) obj;
+ return other.val == val;
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return "IntVersion: " + val;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/skyframe/InvalidatingNodeVisitor.java b/src/main/java/com/google/devtools/build/skyframe/InvalidatingNodeVisitor.java
new file mode 100644
index 0000000..7abf6c6
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skyframe/InvalidatingNodeVisitor.java
@@ -0,0 +1,350 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
+import com.google.devtools.build.lib.concurrent.AbstractQueueVisitor;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.util.Pair;
+
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import javax.annotation.Nullable;
+
+/**
+ * A visitor that is useful for invalidating transitive dependencies of Skyframe nodes.
+ *
+ * <p>Interruptibility: It is safe to interrupt the invalidation process at any time. Consider a
+ * graph and a set of modified nodes. Then the reverse transitive closure of the modified nodes is
+ * the set of dirty nodes. We provide interruptibility by making sure that the following invariant
+ * holds at any time:
+ *
+ * <p>If a node is dirty, but not removed (or marked as dirty) yet, then either it or any of its
+ * transitive dependencies must be in the {@link #pendingVisitations} set. Furthermore, reverse dep
+ * pointers must always point to existing nodes.
+ *
+ * <p>Thread-safety: This class should only be instantiated and called on a single thread, but
+ * internally it spawns many worker threads to process the graph. The thread-safety of the workers
+ * on the graph can be delicate, and is documented below. Moreover, no other modifications to the
+ * graph can take place while invalidation occurs.
+ *
+ * <p>This is intended only for use in alternative {@code MemoizingEvaluator} implementations.
+ */
+public abstract class InvalidatingNodeVisitor extends AbstractQueueVisitor {
+
+ // Default thread count is equal to the number of cores to exploit
+ // that level of hardware parallelism, since invalidation should be CPU-bound.
+ // We may consider increasing this in the future.
+ private static final int DEFAULT_THREAD_COUNT = Runtime.getRuntime().availableProcessors();
+
+ private static final boolean MUST_EXIST = true;
+
+ protected final DirtiableGraph graph;
+ @Nullable protected final EvaluationProgressReceiver invalidationReceiver;
+ protected final DirtyKeyTracker dirtyKeyTracker;
+ // Aliased to InvalidationState.pendingVisitations.
+ protected final Set<Pair<SkyKey, InvalidationType>> pendingVisitations;
+
+ protected InvalidatingNodeVisitor(
+ DirtiableGraph graph, @Nullable EvaluationProgressReceiver invalidationReceiver,
+ InvalidationState state, DirtyKeyTracker dirtyKeyTracker) {
+ super(/*concurrent*/true,
+ /*corePoolSize*/DEFAULT_THREAD_COUNT,
+ /*maxPoolSize*/DEFAULT_THREAD_COUNT,
+ 1, TimeUnit.SECONDS,
+ /*failFastOnException*/true,
+ /*failFastOnInterrupt*/true,
+ "skyframe-invalidator");
+ this.graph = Preconditions.checkNotNull(graph);
+ this.invalidationReceiver = invalidationReceiver;
+ this.dirtyKeyTracker = Preconditions.checkNotNull(dirtyKeyTracker);
+ this.pendingVisitations = state.pendingValues;
+ }
+
+ /**
+ * Initiates visitation and waits for completion.
+ */
+ void run() throws InterruptedException {
+ // Make a copy to avoid concurrent modification confusing us as to which nodes were passed by
+ // the caller, and which are added by other threads during the run. Since no tasks have been
+ // started yet (the queueDirtying calls start them), this is thread-safe.
+ for (Pair<SkyKey, InvalidationType> visitData : ImmutableList.copyOf(pendingVisitations)) {
+ // The caller may have specified non-existent SkyKeys, or there may be stale SkyKeys in
+ // pendingVisitations that have already been deleted. In both these cases, the nodes will not
+ // exist in the graph, so we must be tolerant of that case.
+ visit(visitData.first, visitData.second, !MUST_EXIST);
+ }
+ work(/*failFastOnInterrupt=*/true);
+ Preconditions.checkState(pendingVisitations.isEmpty(),
+ "All dirty nodes should have been processed: %s", pendingVisitations);
+ }
+
+ protected void informInvalidationReceiver(SkyValue value,
+ EvaluationProgressReceiver.InvalidationState state) {
+ if (invalidationReceiver != null && value != null) {
+ invalidationReceiver.invalidated(value, state);
+ }
+ }
+
+ /**
+ * Enqueues a node for invalidation.
+ */
+ @ThreadSafe
+ abstract void visit(SkyKey key, InvalidationType second, boolean mustExist);
+
+ @VisibleForTesting
+ enum InvalidationType {
+ /**
+ * The node is dirty and must be recomputed.
+ */
+ CHANGED,
+ /**
+ * The node is dirty, but may be marked clean later during change pruning.
+ */
+ DIRTIED,
+ /**
+ * The node is deleted.
+ */
+ DELETED;
+ }
+
+ /**
+ * Invalidation state object that keeps track of which nodes need to be invalidated, but have not
+ * been dirtied/deleted yet. This supports interrupts - by only deleting a node from this set
+ * when all its parents have been invalidated, we ensure that no information is lost when an
+ * interrupt comes in.
+ */
+ static class InvalidationState {
+ private final Set<Pair<SkyKey, InvalidationType>> pendingValues = Sets.newConcurrentHashSet();
+ private final InvalidationType defaultUpdateType;
+
+ private InvalidationState(InvalidationType defaultUpdateType) {
+ this.defaultUpdateType = Preconditions.checkNotNull(defaultUpdateType);
+ }
+
+ void update(Iterable<SkyKey> diff) {
+ Iterables.addAll(pendingValues, Iterables.transform(diff,
+ new Function<SkyKey, Pair<SkyKey, InvalidationType>>() {
+ @Override
+ public Pair<SkyKey, InvalidationType> apply(SkyKey skyKey) {
+ return Pair.of(skyKey, defaultUpdateType);
+ }
+ }));
+ }
+
+ @VisibleForTesting
+ boolean isEmpty() {
+ return pendingValues.isEmpty();
+ }
+
+ @VisibleForTesting
+ Set<Pair<SkyKey, InvalidationType>> getInvalidationsForTesting() {
+ return ImmutableSet.copyOf(pendingValues);
+ }
+ }
+
+ public static class DirtyingInvalidationState extends InvalidationState {
+ public DirtyingInvalidationState() {
+ super(InvalidationType.CHANGED);
+ }
+ }
+
+ static class DeletingInvalidationState extends InvalidationState {
+ public DeletingInvalidationState() {
+ super(InvalidationType.DELETED);
+ }
+ }
+
+ /**
+ * A node-deleting implementation.
+ */
+ static class DeletingNodeVisitor extends InvalidatingNodeVisitor {
+
+ private final Set<SkyKey> visitedValues = Sets.newConcurrentHashSet();
+ private final boolean traverseGraph;
+
+ protected DeletingNodeVisitor(DirtiableGraph graph,
+ EvaluationProgressReceiver invalidationReceiver, InvalidationState state,
+ boolean traverseGraph, DirtyKeyTracker dirtyKeyTracker) {
+ super(graph, invalidationReceiver, state, dirtyKeyTracker);
+ this.traverseGraph = traverseGraph;
+ }
+
+ @Override
+ public void visit(final SkyKey key, InvalidationType invalidationType, boolean mustExist) {
+ Preconditions.checkState(invalidationType == InvalidationType.DELETED, key);
+ if (!visitedValues.add(key)) {
+ return;
+ }
+ final Pair<SkyKey, InvalidationType> invalidationPair = Pair.of(key, invalidationType);
+ pendingVisitations.add(invalidationPair);
+ enqueue(new Runnable() {
+ @Override
+ public void run() {
+ NodeEntry entry = graph.get(key);
+ if (entry == null) {
+ pendingVisitations.remove(invalidationPair);
+ return;
+ }
+
+ if (traverseGraph) {
+ // Propagate deletion upwards.
+ for (SkyKey reverseDep : entry.getReverseDeps()) {
+ visit(reverseDep, InvalidationType.DELETED, !MUST_EXIST);
+ }
+ }
+
+ if (entry.isDone()) {
+ // Only process this node's value and children if it is done, since dirty nodes have
+ // no awareness of either.
+
+ // Unregister this node from direct deps, since reverse dep edges cannot point to
+ // non-existent nodes.
+ if (traverseGraph) {
+ for (SkyKey directDep : entry.getDirectDeps()) {
+ NodeEntry dep = graph.get(directDep);
+ if (dep != null) {
+ dep.removeReverseDep(key);
+ }
+ }
+ }
+ // Allow custom Value-specific logic to update dirtiness status.
+ informInvalidationReceiver(entry.getValue(),
+ EvaluationProgressReceiver.InvalidationState.DELETED);
+ }
+ if (traverseGraph) {
+ // Force reverseDeps consolidation (validates that attempts to remove reverse deps were
+ // really successful.
+ entry.getReverseDeps();
+ }
+ // Actually remove the node.
+ graph.remove(key);
+ dirtyKeyTracker.notDirty(key);
+
+ // Remove the node from the set as the last operation.
+ pendingVisitations.remove(invalidationPair);
+ }
+ });
+ }
+ }
+
+ /**
+ * A node-dirtying implementation.
+ */
+ static class DirtyingNodeVisitor extends InvalidatingNodeVisitor {
+
+ private final Set<Pair<SkyKey, InvalidationType>> visited = Sets.newConcurrentHashSet();
+
+ protected DirtyingNodeVisitor(DirtiableGraph graph,
+ EvaluationProgressReceiver invalidationReceiver, InvalidationState state,
+ DirtyKeyTracker dirtyKeyTracker) {
+ super(graph, invalidationReceiver, state, dirtyKeyTracker);
+ }
+
+ /**
+ * Queues a task to dirty the node named by {@code key}. May be called from multiple threads.
+ * It is possible that the same node is enqueued many times. However, we require that a node
+ * is only actually marked dirty/changed once, with two exceptions:
+ *
+ * (1) If a node is marked dirty, it can subsequently be marked changed. This can occur if, for
+ * instance, FileValue workspace/foo/foo.cc is marked dirty because FileValue workspace/foo is
+ * marked changed (and every FileValue depends on its parent). Then FileValue
+ * workspace/foo/foo.cc is itself changed (this can even happen on the same build).
+ *
+ * (2) If a node is going to be marked both dirty and changed, as, for example, in the previous
+ * case if both workspace/foo/foo.cc and workspace/foo have been changed in the same build, the
+ * thread marking workspace/foo/foo.cc dirty may race with the one marking it changed, and so
+ * try to mark it dirty after it has already been marked changed. In that case, the
+ * {@link NodeEntry} ignores the second marking.
+ *
+ * The invariant that we do not process a (SkyKey, InvalidationType) pair twice is enforced by
+ * the {@link #visited} set.
+ *
+ * The "invariant" is also enforced across builds by checking to see if the entry is already
+ * marked changed, or if it is already marked dirty and we are just going to mark it dirty
+ * again.
+ *
+ * If either of the above tests shows that we have already started a task to mark this entry
+ * dirty/changed, or that it is already marked dirty/changed, we do not continue this task.
+ */
+ @Override
+ @ThreadSafe
+ public void visit(final SkyKey key, final InvalidationType invalidationType,
+ final boolean mustExist) {
+ Preconditions.checkState(invalidationType != InvalidationType.DELETED, key);
+ final boolean isChanged = (invalidationType == InvalidationType.CHANGED);
+ final Pair<SkyKey, InvalidationType> invalidationPair = Pair.of(key, invalidationType);
+ if (!visited.add(invalidationPair)) {
+ return;
+ }
+ pendingVisitations.add(invalidationPair);
+ enqueue(new Runnable() {
+ @Override
+ public void run() {
+ NodeEntry entry = graph.get(key);
+
+ if (entry == null) {
+ Preconditions.checkState(!mustExist,
+ "%s does not exist in the graph but was enqueued for dirtying by another node",
+ key);
+ pendingVisitations.remove(invalidationPair);
+ return;
+ }
+
+ if (entry.isChanged() || (!isChanged && entry.isDirty())) {
+ // If this node is already marked changed, or we are only marking this node dirty, and
+ // it already is, move along.
+ pendingVisitations.remove(invalidationPair);
+ return;
+ }
+
+ // This entry remains in the graph in this dirty state until it is re-evaluated.
+ Pair<? extends Iterable<SkyKey>, ? extends SkyValue> depsAndValue =
+ entry.markDirty(isChanged);
+ // It is not safe to interrupt the logic from this point until the end of the method.
+ // Any exception thrown should be unrecoverable.
+ if (depsAndValue == null) {
+ // Another thread has already dirtied this node. Don't do anything in this thread.
+ pendingVisitations.remove(invalidationPair);
+ return;
+ }
+ // Propagate dirtiness upwards and mark this node dirty/changed. Reverse deps should only
+ // be marked dirty (because only a dependency of theirs has changed).
+ for (SkyKey reverseDep : entry.getReverseDeps()) {
+ visit(reverseDep, InvalidationType.DIRTIED, MUST_EXIST);
+ }
+
+ // Remove this node as a reverse dep from its children, since we have reset it and it no
+ // longer lists its children as direct deps.
+ for (SkyKey dep : depsAndValue.first) {
+ graph.get(dep).removeReverseDep(key);
+ }
+
+ SkyValue value = ValueWithMetadata.justValue(depsAndValue.second);
+ informInvalidationReceiver(value, EvaluationProgressReceiver.InvalidationState.DIRTY);
+ dirtyKeyTracker.dirty(key);
+ // Remove the node from the set as the last operation.
+ pendingVisitations.remove(invalidationPair);
+ }
+ });
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/skyframe/MemoizingEvaluator.java b/src/main/java/com/google/devtools/build/skyframe/MemoizingEvaluator.java
new file mode 100644
index 0000000..2c7f14e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skyframe/MemoizingEvaluator.java
@@ -0,0 +1,143 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Predicate;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetVisitor;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadHostile;
+import com.google.devtools.build.lib.events.EventHandler;
+
+import java.io.PrintStream;
+import java.util.Map;
+
+import javax.annotation.Nullable;
+
+/**
+ * A graph, defined by a set of functions that can construct values from value keys.
+ *
+ * <p>The value constructor functions ({@link SkyFunction}s) can declare dependencies on
+ * prerequisite {@link SkyValue}s. The {@link MemoizingEvaluator} implementation makes sure that
+ * those are created beforehand.
+ *
+ * <p>The graph caches previously computed value values. Arbitrary values can be invalidated between
+ * calls to {@link #evaluate}; they will be recreated the next time they are requested.
+ */
+public interface MemoizingEvaluator {
+
+ /**
+ * Computes the transitive closure of a given set of values at the given {@link Version}. See
+ * {@link EagerInvalidator#invalidate}.
+ *
+ * <p>The returned EvaluationResult is guaranteed to contain a result for at least one root if
+ * keepGoing is false. It will contain a result for every root if keepGoing is true, <i>unless</i>
+ * the evaluation failed with a "catastrophic" error. In that case, some or all results may be
+ * missing.
+ */
+ <T extends SkyValue> EvaluationResult<T> evaluate(
+ Iterable<SkyKey> roots,
+ Version version,
+ boolean keepGoing,
+ int numThreads,
+ EventHandler reporter)
+ throws InterruptedException;
+
+ /**
+ * Ensures that after the next completed {@link #evaluate} call the current values of any value
+ * matching this predicate (and all values that transitively depend on them) will be removed from
+ * the value cache. All values that were already marked dirty in the graph will also be deleted,
+ * regardless of whether or not they match the predicate.
+ *
+ * <p>If a later call to {@link #evaluate} requests some of the deleted values, those values will
+ * be recomputed and the new values stored in the cache again.
+ *
+ * <p>To delete all dirty values, you can specify a predicate that's always false.
+ */
+ void delete(Predicate<SkyKey> pred);
+
+ /**
+ * Marks dirty values for deletion if they have been dirty for at least as many graph versions
+ * as the specified limit.
+ *
+ * <p>This ensures that after the next completed {@link #evaluate} call, all such values, along
+ * with all values that transitively depend on them, will be removed from the value cache. Values
+ * that were marked dirty after the threshold version will not be affected by this call.
+ *
+ * <p>If a later call to {@link #evaluate} requests some of the deleted values, those values will
+ * be recomputed and the new values stored in the cache again.
+ *
+ * <p>To delete all dirty values, you can specify 0 for the limit.
+ */
+ void deleteDirty(long versionAgeLimit);
+
+ /**
+ * Returns the values in the graph.
+ *
+ * <p>The returned map may be a live view of the graph.
+ */
+ Map<SkyKey, SkyValue> getValues();
+
+
+ /**
+ * Returns the done (without error) values in the graph.
+ *
+ * <p>The returned map may be a live view of the graph.
+ */
+ Map<SkyKey, SkyValue> getDoneValues();
+
+ /**
+ * Returns a value if and only if an earlier call to {@link #evaluate} created it; null otherwise.
+ *
+ * <p>This method should only be used by tests that need to verify the presence of a value in the
+ * graph after an {@link #evaluate} call.
+ */
+ @VisibleForTesting
+ @Nullable
+ SkyValue getExistingValueForTesting(SkyKey key);
+
+ /**
+ * Returns an error if and only if an earlier call to {@link #evaluate} created it; null
+ * otherwise.
+ *
+ * <p>This method should only be used by tests that need to verify the presence of an error in the
+ * graph after an {@link #evaluate} call.
+ */
+ @VisibleForTesting
+ @Nullable
+ ErrorInfo getExistingErrorForTesting(SkyKey key);
+
+ /**
+ * Write the graph to the output stream. Not necessarily thread-safe. Use only for debugging
+ * purposes.
+ */
+ @ThreadHostile
+ void dump(boolean summarize, PrintStream out);
+
+ /**
+ * A supplier for creating instances of a particular evaluator implementation.
+ */
+ public static interface EvaluatorSupplier {
+ MemoizingEvaluator create(
+ Map<? extends SkyFunctionName, ? extends SkyFunction> skyFunctions, Differencer differencer,
+ @Nullable EvaluationProgressReceiver invalidationReceiver,
+ EmittedEventState emittedEventState, boolean keepEdges);
+ }
+
+ /**
+ * Keeps track of already-emitted events. Users of the graph should instantiate an
+ * {@code EmittedEventState} first and pass it to the graph during creation. This allows them to
+ * determine whether or not to replay events.
+ */
+ public static class EmittedEventState extends NestedSetVisitor.VisitedState<TaggedEvents> {}
+}
diff --git a/src/main/java/com/google/devtools/build/skyframe/MinimalVersion.java b/src/main/java/com/google/devtools/build/skyframe/MinimalVersion.java
new file mode 100644
index 0000000..6f75c15
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skyframe/MinimalVersion.java
@@ -0,0 +1,31 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+/**
+ * A Version "less than" all other versions, other than itself.
+ *
+ * <p>Only use in custom evaluator implementations.
+ */
+public class MinimalVersion implements Version {
+ public static final MinimalVersion INSTANCE = new MinimalVersion();
+
+ private MinimalVersion() {
+ }
+
+ @Override
+ public boolean atMost(Version other) {
+ return true;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/skyframe/NodeEntry.java b/src/main/java/com/google/devtools/build/skyframe/NodeEntry.java
new file mode 100644
index 0000000..243189d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skyframe/NodeEntry.java
@@ -0,0 +1,581 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Objects;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.util.GroupedList;
+import com.google.devtools.build.lib.util.GroupedList.GroupedListHelper;
+import com.google.devtools.build.lib.util.Pair;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+/**
+ * A node in the graph. All operations on this class are thread-safe. Care was taken to provide
+ * certain compound operations to avoid certain check-then-act races. That means this class is
+ * somewhat closely tied to the exact Evaluator implementation.
+ *
+ * <p>Consider the example with two threads working on two nodes, where one depends on the other,
+ * say b depends on a. If a completes first, it's done. If it completes second, it needs to signal
+ * b, and potentially re-schedule it. If b completes first, it must exit, because it will be
+ * signaled (and re-scheduled) by a. If it completes second, it must signal (and re-schedule)
+ * itself. However, if the Evaluator supported re-entrancy for a node, then this wouldn't have to
+ * be so strict, because duplicate scheduling would be less problematic.
+ *
+ * <p>The transient state of a {@code NodeEntry} is kept in a {@link BuildingState} object. Many of
+ * the methods of {@code NodeEntry} are just wrappers around the corresponding
+ * {@link BuildingState} methods.
+ *
+ * <p>This class is non-final only for testing purposes.
+ * <p>This class is public only for the benefit of alternative graph implementations outside of the
+ * package.
+ */
+public class NodeEntry {
+ /**
+ * Return code for {@link #addReverseDepAndCheckIfDone(SkyKey)}.
+ */
+ enum DependencyState {
+ /** The node is done. */
+ DONE,
+
+ /**
+ * The node was just created and needs to be scheduled for its first evaluation pass. The
+ * evaluator is responsible for signaling the reverse dependency node.
+ */
+ NEEDS_SCHEDULING,
+
+ /**
+ * The node was already created, but isn't done yet. The evaluator is responsible for
+ * signaling the reverse dependency node.
+ */
+ ADDED_DEP;
+ }
+
+ /** Actual data stored in this entry when it is done. */
+ private SkyValue value = null;
+
+ /**
+ * The last version of the graph at which this node entry was changed. In {@link #setValue} it
+ * may be determined that the data being written to the graph at a given version is the same as
+ * the already-stored data. In that case, the version will remain the same. The version can be
+ * thought of as the latest timestamp at which this entry was changed.
+ */
+ private Version version = MinimalVersion.INSTANCE;
+
+ /**
+ * This object represents a {@link GroupedList}<SkyKey> in a memory-efficient way. It stores the
+ * direct dependencies of this node, in groups if the {@code SkyFunction} requested them that way.
+ */
+ private Object directDeps = null;
+
+ /**
+ * This list stores the reverse dependencies of this node that have been declared so far.
+ *
+ * <p>In case of a single object we store the object unwrapped, without the list, for
+ * memory-efficiency.
+ */
+ @VisibleForTesting
+ protected Object reverseDeps = ImmutableList.of();
+
+ /**
+ * We take advantage of memory alignment to avoid doing a nasty {@code instanceof} for knowing
+ * if {@code reverseDeps} is a single object or a list.
+ */
+ protected boolean reverseDepIsSingleObject = false;
+
+ /**
+ * During the invalidation we keep the reverse deps to be removed in this list instead of directly
+ * removing them from {@code reverseDeps}. That is because removals from reverseDeps are O(N).
+ * Originally reverseDeps was a HashSet, but because of memory consumption we switched to a list.
+ *
+ * <p>This requires that any usage of reverseDeps (contains, add, the list of reverse deps) call
+ * {@code consolidateReverseDepsRemovals} first. While this operation is not free, it can be done
+ * more effectively than trying to remove each dirty reverse dependency individually (O(N) each
+ * time).
+ */
+ private List<SkyKey> reverseDepsToRemove = null;
+
+ private static final ReverseDepsUtil<NodeEntry> REVERSE_DEPS_UTIL =
+ new ReverseDepsUtil<NodeEntry>() {
+ @Override
+ void setReverseDepsObject(NodeEntry container, Object object) {
+ container.reverseDeps = object;
+ }
+
+ @Override
+ void setSingleReverseDep(NodeEntry container, boolean singleObject) {
+ container.reverseDepIsSingleObject = singleObject;
+ }
+
+ @Override
+ void setReverseDepsToRemove(NodeEntry container, List<SkyKey> object) {
+ container.reverseDepsToRemove = object;
+ }
+
+ @Override
+ Object getReverseDepsObject(NodeEntry container) {
+ return container.reverseDeps;
+ }
+
+ @Override
+ boolean isSingleReverseDep(NodeEntry container) {
+ return container.reverseDepIsSingleObject;
+ }
+
+ @Override
+ List<SkyKey> getReverseDepsToRemove(NodeEntry container) {
+ return container.reverseDepsToRemove;
+ }
+ };
+
+ /**
+ * The transient state of this entry, after it has been created but before it is done. It allows
+ * us to keep the current state of the entry across invalidation and successive evaluations.
+ */
+ @VisibleForTesting
+ protected BuildingState buildingState = new BuildingState();
+
+ /**
+ * Construct a NodeEntry. Use ONLY in Skyframe evaluation and graph implementations.
+ */
+ public NodeEntry() {
+ }
+
+ protected boolean keepEdges() {
+ return true;
+ }
+
+ /** Returns whether the entry has been built and is finished evaluating. */
+ synchronized boolean isDone() {
+ return buildingState == null;
+ }
+
+ /**
+ * Returns the value stored in this entry. This method may only be called after the evaluation of
+ * this node is complete, i.e., after {@link #setValue} has been called.
+ */
+ synchronized SkyValue getValue() {
+ Preconditions.checkState(isDone(), "no value until done. ValueEntry: %s", this);
+ return ValueWithMetadata.justValue(value);
+ }
+
+ /**
+ * Returns the {@link SkyValue} for this entry and the metadata associated with it (Like events
+ * and errors). This method may only be called after the evaluation of this node is complete,
+ * i.e., after {@link #setValue} has been called.
+ */
+ synchronized ValueWithMetadata getValueWithMetadata() {
+ Preconditions.checkState(isDone(), "no value until done: %s", this);
+ return ValueWithMetadata.wrapWithMetadata(value);
+ }
+
+ /**
+ * Returns the value, even if dirty or changed. Returns null otherwise.
+ */
+ public synchronized SkyValue toValue() {
+ if (isDone()) {
+ return getErrorInfo() == null ? getValue() : null;
+ } else if (isChanged() || isDirty()) {
+ return (buildingState.getLastBuildValue() == null)
+ ? null
+ : ValueWithMetadata.justValue(buildingState.getLastBuildValue());
+ }
+ throw new AssertionError("Value in bad state: " + this);
+ }
+
+ /**
+ * Returns an immutable iterable of the direct deps of this node. This method may only be called
+ * after the evaluation of this node is complete, i.e., after {@link #setValue} has been called.
+ *
+ * <p>This method is not very efficient, but is only be called in limited circumstances --
+ * when the node is about to be deleted, or when the node is expected to have no direct deps (in
+ * which case the overhead is not so bad). It should not be called repeatedly for the same node,
+ * since each call takes time proportional to the number of direct deps of the node.
+ */
+ synchronized Iterable<SkyKey> getDirectDeps() {
+ assertKeepEdges();
+ Preconditions.checkState(isDone(), "no deps until done. ValueEntry: %s", this);
+ return GroupedList.<SkyKey>create(directDeps).toSet();
+ }
+
+ /**
+ * Returns the error, if any, associated to this node. This method may only be called after
+ * the evaluation of this node is complete, i.e., after {@link #setValue} has been called.
+ */
+ @Nullable
+ synchronized ErrorInfo getErrorInfo() {
+ Preconditions.checkState(isDone(), "no errors until done. ValueEntry: %s", this);
+ return ValueWithMetadata.getMaybeErrorInfo(value);
+ }
+
+ private synchronized Set<SkyKey> setStateFinishedAndReturnReverseDeps() {
+ // Get reverse deps that need to be signaled.
+ ImmutableSet<SkyKey> reverseDepsToSignal = buildingState.getReverseDepsToSignal();
+ REVERSE_DEPS_UTIL.consolidateReverseDepsRemovals(this);
+ REVERSE_DEPS_UTIL.addReverseDeps(this, reverseDepsToSignal);
+ this.directDeps = buildingState.getFinishedDirectDeps().compress();
+
+ // Set state of entry to done.
+ buildingState = null;
+
+ if (!keepEdges()) {
+ this.directDeps = null;
+ this.reverseDeps = null;
+ }
+ return reverseDepsToSignal;
+ }
+
+ /**
+ * Returns the set of reverse deps that have been declared so far this build. Only for use in
+ * debugging and when bubbling errors up in the --nokeep_going case, where we need to know what
+ * parents this entry has.
+ */
+ synchronized Set<SkyKey> getInProgressReverseDeps() {
+ Preconditions.checkState(!isDone(), this);
+ return buildingState.getReverseDepsToSignal();
+ }
+
+ /**
+ * Transitions the node from the EVALUATING to the DONE state and simultaneously sets it to the
+ * given value and error state. It then returns the set of reverse dependencies that need to be
+ * signaled.
+ *
+ * <p>This is an atomic operation to avoid a race where two threads work on two nodes, where one
+ * node depends on another (b depends on a). When a finishes, it signals <b>exactly</b> the set
+ * of reverse dependencies that are registered at the time of the {@code setValue} call. If b
+ * comes in before a, it is signaled (and re-scheduled) by a, otherwise it needs to do that
+ * itself.
+ *
+ * <p>{@code version} indicates the graph version at which this node is being written. If the
+ * entry determines that the new value is equal to the previous value, the entry will keep its
+ * current version. Callers can query that version to see if the node considers its value to have
+ * changed.
+ */
+ public synchronized Set<SkyKey> setValue(SkyValue value, Version version) {
+ Preconditions.checkState(isReady(), "%s %s", this, value);
+ // This check may need to be removed when we move to a non-linear versioning sequence.
+ Preconditions.checkState(this.version.atMost(version),
+ "%s %s %s", this, version, value);
+
+ if (isDirty() && buildingState.unchangedFromLastBuild(value)) {
+ // If the value is the same as before, just use the old value. Note that we don't use the new
+ // value, because preserving == equality is even better than .equals() equality.
+ this.value = buildingState.getLastBuildValue();
+ } else {
+ // If this is a new value, or it has changed since the last build, set the version to the
+ // current graph version.
+ this.version = version;
+ this.value = value;
+ }
+
+ return setStateFinishedAndReturnReverseDeps();
+ }
+
+ /**
+ * Queries if the node is done and adds the given key as a reverse dependency. The return code
+ * indicates whether a) the node is done, b) the reverse dependency is the first one, so the
+ * node needs to be scheduled, or c) the reverse dependency was added, and the node does not
+ * need to be scheduled.
+ *
+ * <p>This method <b>must</b> be called before any processing of the entry. This encourages
+ * callers to check that the entry is ready to be processed.
+ *
+ * <p>Adding the dependency and checking if the node needs to be scheduled is an atomic operation
+ * to avoid a race where two threads work on two nodes, where one depends on the other (b depends
+ * on a). In that case, we need to ensure that b is re-scheduled exactly once when a is done.
+ * However, a may complete first, in which case b has to re-schedule itself. Also see {@link
+ * #setValue}.
+ *
+ * <p>If the parameter is {@code null}, then no reverse dependency is added, but we still check
+ * if the node needs to be scheduled.
+ */
+ synchronized DependencyState addReverseDepAndCheckIfDone(SkyKey reverseDep) {
+ if (reverseDep != null) {
+ if (keepEdges()) {
+ REVERSE_DEPS_UTIL.consolidateReverseDepsRemovals(this);
+ REVERSE_DEPS_UTIL.maybeCheckReverseDepNotPresent(this, reverseDep);
+ }
+ if (isDone()) {
+ if (keepEdges()) {
+ REVERSE_DEPS_UTIL.addReverseDeps(this, ImmutableList.of(reverseDep));
+ }
+ } else {
+ // Parent should never register itself twice in the same build.
+ buildingState.addReverseDepToSignal(reverseDep);
+ }
+ }
+ if (isDone()) {
+ return DependencyState.DONE;
+ }
+ return buildingState.startEvaluating() ? DependencyState.NEEDS_SCHEDULING
+ : DependencyState.ADDED_DEP;
+ }
+
+ /**
+ * Removes a reverse dependency.
+ */
+ synchronized void removeReverseDep(SkyKey reverseDep) {
+ if (!keepEdges()) {
+ return;
+ }
+ REVERSE_DEPS_UTIL.removeReverseDep(this, reverseDep);
+ if (!isDone()) {
+ // This is currently unnecessary -- the only time we remove a reverse dep that was added this
+ // build is during the clean following a build failure. In that case, this node that is not
+ // done will be deleted soon, so clearing the reverse dep is not required.
+ buildingState.removeReverseDepToSignal(reverseDep);
+ }
+ }
+
+ /**
+ * Returns a copy of the set of reverse dependencies. Note that this introduces a potential
+ * check-then-act race; {@link #removeReverseDep} may fail for a key that is returned here.
+ */
+ synchronized Iterable<SkyKey> getReverseDeps() {
+ assertKeepEdges();
+ Preconditions.checkState(isDone() || buildingState.getReverseDepsToSignal().isEmpty(),
+ "Reverse deps should only be queried before the build has begun "
+ + "or after the node is done %s", this);
+ return REVERSE_DEPS_UTIL.getReverseDeps(this);
+ }
+
+ /**
+ * Tell this node that one of its dependencies is now done. Callers must check the return value,
+ * and if true, they must re-schedule this node for evaluation. Equivalent to
+ * {@code #signalDep(Long.MAX_VALUE)}. Since this entry's version is less than
+ * {@link Long#MAX_VALUE}, informing this entry that a child of it has version
+ * {@link Long#MAX_VALUE} will force it to re-evaluate.
+ */
+ synchronized boolean signalDep() {
+ return signalDep(/*childVersion=*/new IntVersion(Long.MAX_VALUE));
+ }
+
+ /**
+ * Tell this entry that one of its dependencies is now done. Callers must check the return value,
+ * and if true, they must re-schedule this node for evaluation.
+ *
+ * @param childVersion If this entry {@link #isDirty()} and {@code childVersion} is not at most
+ * {@link #getVersion()}, then this entry records that one of its children has changed since it
+ * was last evaluated (namely, it was last evaluated at version {@link #getVersion()} and the
+ * child was last evaluated at {@code childVersion}. Thus, the next call to
+ * {@link #getDirtyState()} will return {@link BuildingState.DirtyState#REBUILDING}.
+ */
+ synchronized boolean signalDep(Version childVersion) {
+ Preconditions.checkState(!isDone(), "Value must not be done in signalDep %s", this);
+ return buildingState.signalDep(/*childChanged=*/!childVersion.atMost(getVersion()));
+ }
+
+ /**
+ * Returns true if the entry is marked dirty, meaning that at least one of its transitive
+ * dependencies is marked changed.
+ */
+ public synchronized boolean isDirty() {
+ return !isDone() && buildingState.isDirty();
+ }
+
+ /**
+ * Returns true if the entry is marked changed, meaning that it must be re-evaluated even if its
+ * dependencies' values have not changed.
+ */
+ synchronized boolean isChanged() {
+ return !isDone() && buildingState.isChanged();
+ }
+
+ /** Checks that a caller is not trying to access not-stored graph edges. */
+ private void assertKeepEdges() {
+ Preconditions.checkState(keepEdges(), "Graph edges not stored. %s", this);
+ }
+
+ /**
+ * Marks this node dirty, or changed if {@code isChanged} is true. The node is put in the
+ * just-created state. It will be re-evaluated if necessary during the evaluation phase,
+ * but if it has not changed, it will not force a re-evaluation of its parents.
+ *
+ * @return The direct deps and value of this entry, or null if the entry has already been marked
+ * dirty. In the latter case, the caller should abort its handling of this node, since another
+ * thread is already dealing with it.
+ */
+ @Nullable
+ synchronized Pair<? extends Iterable<SkyKey>, ? extends SkyValue> markDirty(boolean isChanged) {
+ assertKeepEdges();
+ if (isDone()) {
+ GroupedList<SkyKey> lastDirectDeps = GroupedList.create(directDeps);
+ buildingState = BuildingState.newDirtyState(isChanged, lastDirectDeps, value);
+ Pair<? extends Iterable<SkyKey>, ? extends SkyValue> result =
+ Pair.of(lastDirectDeps.toSet(), value);
+ value = null;
+ directDeps = null;
+ return result;
+ }
+ // The caller may be simultaneously trying to mark this node dirty and changed, and the dirty
+ // thread may have lost the race, but it is the caller's responsibility not to try to mark
+ // this node changed twice. The end result of racing markers must be a changed node, since one
+ // of the markers is trying to mark the node changed.
+ Preconditions.checkState(isChanged != isChanged(),
+ "Cannot mark node dirty twice or changed twice: %s", this);
+ Preconditions.checkState(value == null, "Value should have been reset already %s", this);
+ Preconditions.checkState(directDeps == null, "direct deps not already reset %s", this);
+ if (isChanged) {
+ // If the changed marker lost the race, we just need to mark changed in this method -- all
+ // other work was done by the dirty marker.
+ buildingState.markChanged();
+ }
+ return null;
+ }
+
+ /**
+ * Marks this entry as up-to-date at this version.
+ *
+ * @return {@link Set} of reverse dependencies to signal that this node is done.
+ */
+ synchronized Set<SkyKey> markClean() {
+ this.value = buildingState.getLastBuildValue();
+ // This checks both the value and the direct deps, but since we're passing in the same value,
+ // the value check should be trivial.
+ Preconditions.checkState(buildingState.unchangedFromLastBuild(this.value),
+ "Direct deps must be the same as those found last build for node to be marked clean: %s",
+ this);
+ Preconditions.checkState(isDirty(), this);
+ Preconditions.checkState(!buildingState.isChanged(), "shouldn't be changed: %s", this);
+ return setStateFinishedAndReturnReverseDeps();
+ }
+
+ /**
+ * Forces this node to be reevaluated, even if none of its dependencies are known to have
+ * changed.
+ *
+ * <p>Used when an external caller has reason to believe that re-evaluating may yield a new
+ * result. This method should not be used if one of the normal deps of this node has changed,
+ * the usual change-pruning process should work in that case.
+ */
+ synchronized void forceRebuild() {
+ buildingState.forceChanged();
+ }
+
+ /**
+ * Gets the current version of this entry.
+ */
+ synchronized Version getVersion() {
+ return version;
+ }
+
+ /**
+ * Gets the current state of checking this dirty entry to see if it must be re-evaluated. Must be
+ * called each time evaluation of a dirty entry starts to find the proper action to perform next,
+ * as enumerated by {@link BuildingState.DirtyState}.
+ *
+ * @see BuildingState#getDirtyState()
+ */
+ synchronized BuildingState.DirtyState getDirtyState() {
+ return buildingState.getDirtyState();
+ }
+
+ /**
+ * Should only be called if the entry is dirty. During the examination to see if the entry must be
+ * re-evaluated, this method returns the next group of children to be checked. Callers should
+ * have already called {@link #getDirtyState} and received a return value of
+ * {@link BuildingState.DirtyState#CHECK_DEPENDENCIES} before calling this method -- any other
+ * return value from {@link #getDirtyState} means that this method must not be called, since
+ * whether or not the node needs to be rebuilt is already known.
+ *
+ * <p>Deps are returned in groups. The deps in each group were requested in parallel by the
+ * {@code SkyFunction} last build, meaning independently of the values of any other deps in this
+ * group (although possibly depending on deps in earlier groups). Thus the caller may check all
+ * the deps in this group in parallel, since the deps in all previous groups are verified
+ * unchanged. See {@link SkyFunction.Environment#getValues} for more on dependency groups.
+ *
+ * <p>The caller should register these as deps of this entry using {@link #addTemporaryDirectDeps}
+ * before checking them.
+ *
+ * @see BuildingState#getNextDirtyDirectDeps()
+ */
+ synchronized Collection<SkyKey> getNextDirtyDirectDeps() {
+ return buildingState.getNextDirtyDirectDeps();
+ }
+
+ /**
+ * Returns the set of direct dependencies. This may only be called while the node is being
+ * evaluated, that is, before {@link #setValue} and after {@link #markDirty}.
+ */
+ synchronized Set<SkyKey> getTemporaryDirectDeps() {
+ Preconditions.checkState(!isDone(), "temporary shouldn't be done: %s", this);
+ return buildingState.getDirectDepsForBuild();
+ }
+
+ synchronized boolean noDepsLastBuild() {
+ return buildingState.noDepsLastBuild();
+ }
+
+ /**
+ * Remove dep from direct deps. This should only be called if this entry is about to be
+ * committed as a cycle node, but some of its children were not checked for cycles, either
+ * because the cycle was discovered before some children were checked; some children didn't have a
+ * chance to finish before the evaluator aborted; or too many cycles were found when it came time
+ * to check the children.
+ */
+ synchronized void removeUnfinishedDeps(Set<SkyKey> unfinishedDeps) {
+ buildingState.removeDirectDeps(unfinishedDeps);
+ }
+
+ synchronized void addTemporaryDirectDeps(GroupedListHelper<SkyKey> helper) {
+ Preconditions.checkState(!isDone(), "add temp shouldn't be done: %s %s", helper, this);
+ buildingState.addDirectDeps(helper);
+ }
+
+ /**
+ * Returns true if the node is ready to be evaluated, i.e., it has been signaled exactly as many
+ * times as it has temporary dependencies. This may only be called while the node is being
+ * evaluated, that is, before {@link #setValue} and after {@link #markDirty}.
+ */
+ synchronized boolean isReady() {
+ Preconditions.checkState(!isDone(), "can't be ready if done: %s", this);
+ return buildingState.isReady();
+ }
+
+ @Override
+ @SuppressWarnings("deprecation")
+ public String toString() {
+ return Objects.toStringHelper(this) // MoreObjects is not in Guava
+ .add("value", value)
+ .add("version", version)
+ .add("directDeps", directDeps == null ? null : GroupedList.create(directDeps))
+ .add("reverseDeps", REVERSE_DEPS_UTIL.toString(this))
+ .add("buildingState", buildingState).toString();
+ }
+
+ /**
+ * Do not use except in custom evaluator implementations! Added only temporarily.
+ *
+ * <p>Clones a NodeEntry iff it is a done node. Otherwise it fails.
+ */
+ @Deprecated
+ public synchronized NodeEntry cloneNodeEntry() {
+ // As this is temporary, for now lets limit to done nodes
+ Preconditions.checkState(isDone(), "Only done nodes can be copied");
+ NodeEntry nodeEntry = new NodeEntry();
+ nodeEntry.value = value;
+ nodeEntry.version = this.version;
+ REVERSE_DEPS_UTIL.addReverseDeps(nodeEntry, REVERSE_DEPS_UTIL.getReverseDeps(this));
+ nodeEntry.directDeps = directDeps;
+ nodeEntry.buildingState = null;
+ return nodeEntry;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/skyframe/NullDirtyKeyTrackerImpl.java b/src/main/java/com/google/devtools/build/skyframe/NullDirtyKeyTrackerImpl.java
new file mode 100644
index 0000000..937f1cb
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skyframe/NullDirtyKeyTrackerImpl.java
@@ -0,0 +1,37 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.collect.ImmutableSet;
+
+import java.util.Set;
+
+/**
+ * Tracks nothing. Should be used by evaluators that don't do dirty node garbage collection.
+ */
+public class NullDirtyKeyTrackerImpl implements DirtyKeyTracker {
+
+ @Override
+ public void dirty(SkyKey skyKey) {
+ }
+
+ @Override
+ public void notDirty(SkyKey skyKey) {
+ }
+
+ @Override
+ public Set<SkyKey> getDirtyKeys() {
+ return ImmutableSet.of();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/skyframe/ParallelEvaluator.java b/src/main/java/com/google/devtools/build/skyframe/ParallelEvaluator.java
new file mode 100644
index 0000000..39f11d7
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skyframe/ParallelEvaluator.java
@@ -0,0 +1,1786 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Function;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Interner;
+import com.google.common.collect.Interners;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetVisitor;
+import com.google.devtools.build.lib.concurrent.AbstractQueueVisitor;
+import com.google.devtools.build.lib.concurrent.ExecutorShutdownUtil;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadCompatible;
+import com.google.devtools.build.lib.concurrent.ThrowableRecordingRunnableWrapper;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.events.StoredEventHandler;
+import com.google.devtools.build.lib.profiler.Profiler;
+import com.google.devtools.build.lib.profiler.ProfilerTask;
+import com.google.devtools.build.lib.util.GroupedList.GroupedListHelper;
+import com.google.devtools.build.skyframe.BuildingState.DirtyState;
+import com.google.devtools.build.skyframe.EvaluationProgressReceiver.EvaluationState;
+import com.google.devtools.build.skyframe.NodeEntry.DependencyState;
+import com.google.devtools.build.skyframe.Scheduler.SchedulerException;
+import com.google.devtools.build.skyframe.SkyFunctionException.ReifiedSkyFunctionException;
+import com.google.devtools.build.skyframe.ValueOrExceptionUtils.BottomException;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import javax.annotation.Nullable;
+
+/**
+ * Evaluates a set of given functions ({@code SkyFunction}s) with arguments ({@code SkyKey}s).
+ * Cycles are not allowed and are detected during the traversal.
+ *
+ * <p>This class implements multi-threaded evaluation. This is a fairly complex process that has
+ * strong consistency requirements between the {@link ProcessableGraph}, the nodes in the graph of
+ * type {@link NodeEntry}, the work queue, and the set of in-flight nodes.
+ *
+ * <p>The basic invariants are:
+ *
+ * <p>A node can be in one of three states: ready, waiting, and done. A node is ready if and only
+ * if all of its dependencies have been signaled. A node is done if it has a value. It is waiting
+ * if not all of its dependencies have been signaled.
+ *
+ * <p>A node must be in the work queue if and only if it is ready. It is an error for a node to be
+ * in the work queue twice at the same time.
+ *
+ * <p>A node is considered in-flight if it has been created, and is not done yet. In case of an
+ * interrupt, the work queue is discarded, and the in-flight set is used to remove partially
+ * computed values.
+ *
+ * <p>Each evaluation of the graph takes place at a "version," which is currently given by a
+ * non-negative {@code long}. The version can also be thought of as an "mtime." Each node in the
+ * graph has a version, which is the last version at which its value changed. This version data is
+ * used to avoid unnecessary re-evaluation of values. If a node is re-evaluated and found to have
+ * the same data as before, its version (mtime) remains the same. If all of a node's children's
+ * have the same version as before, its re-evaluation can be skipped.
+ *
+ * <p>This class is not intended for direct use, and is only exposed as public for use in
+ * evaluation implementations outside of this package.
+ */
+public final class ParallelEvaluator implements Evaluator {
+ private final ProcessableGraph graph;
+ private final Version graphVersion;
+
+ private final Predicate<SkyKey> nodeEntryIsDone = new Predicate<SkyKey>() {
+ @Override
+ public boolean apply(SkyKey skyKey) {
+ return isDoneForBuild(graph.get(skyKey));
+ }
+ };
+
+ private final ImmutableMap<? extends SkyFunctionName, ? extends SkyFunction> skyFunctions;
+
+ private final EventHandler reporter;
+ private final NestedSetVisitor<TaggedEvents> replayingNestedSetEventVisitor;
+ private final boolean keepGoing;
+ private final int threadCount;
+ @Nullable private final EvaluationProgressReceiver progressReceiver;
+ private final DirtyKeyTracker dirtyKeyTracker;
+ private final AtomicBoolean errorEncountered = new AtomicBoolean(false);
+
+ private static final Interner<SkyKey> KEY_CANONICALIZER = Interners.newWeakInterner();
+
+ public ParallelEvaluator(ProcessableGraph graph, Version graphVersion,
+ ImmutableMap<? extends SkyFunctionName, ? extends SkyFunction> skyFunctions,
+ final EventHandler reporter,
+ MemoizingEvaluator.EmittedEventState emittedEventState,
+ boolean keepGoing, int threadCount,
+ @Nullable EvaluationProgressReceiver progressReceiver,
+ DirtyKeyTracker dirtyKeyTracker) {
+ this.graph = graph;
+ this.skyFunctions = skyFunctions;
+ this.graphVersion = graphVersion;
+ this.reporter = Preconditions.checkNotNull(reporter);
+ this.keepGoing = keepGoing;
+ this.threadCount = threadCount;
+ this.progressReceiver = progressReceiver;
+ this.dirtyKeyTracker = Preconditions.checkNotNull(dirtyKeyTracker);
+ this.replayingNestedSetEventVisitor =
+ new NestedSetVisitor<>(new NestedSetEventReceiver(reporter), emittedEventState);
+ }
+
+ /**
+ * Receives the events from the NestedSet and delegates to the reporter.
+ */
+ private static class NestedSetEventReceiver implements NestedSetVisitor.Receiver<TaggedEvents> {
+
+ private final EventHandler reporter;
+
+ public NestedSetEventReceiver(EventHandler reporter) {
+ this.reporter = reporter;
+ }
+ @Override
+ public void accept(TaggedEvents events) {
+ String tag = events.getTag();
+ for (Event e : events.getEvents()) {
+ reporter.handle(e.withTag(tag));
+ }
+ }
+ }
+
+ /**
+ * A suitable {@link SkyFunction.Environment} implementation.
+ */
+ class SkyFunctionEnvironment implements SkyFunction.Environment {
+ private boolean building = true;
+ private boolean valuesMissing = false;
+ private SkyKey depErrorKey = null;
+ private final SkyKey skyKey;
+ private SkyValue value = null;
+ private ErrorInfo errorInfo = null;
+ private final Map<SkyKey, ValueWithMetadata> bubbleErrorInfo;
+ /** The set of values previously declared as dependencies. */
+ private final Set<SkyKey> directDeps;
+
+ /**
+ * The grouped list of values requested during this build as dependencies. On a subsequent
+ * build, if this value is dirty, all deps in the same dependency group can be checked in
+ * parallel for changes. In other words, if dep1 and dep2 are in the same group, then dep1 will
+ * be checked in parallel with dep2. See {@link #getValues} for more.
+ */
+ private final GroupedListHelper<SkyKey> newlyRequestedDeps = new GroupedListHelper<>();
+
+ /**
+ * The value visitor managing the thread pool. Used to enqueue parents when this value is
+ * finished, and, during testing, to block until an exception is thrown if a value builder
+ * requests that.
+ */
+ private final ValueVisitor visitor;
+
+ /** The set of errors encountered while fetching children. */
+ private final Collection<ErrorInfo> childErrorInfos = new LinkedHashSet<>();
+ private final StoredEventHandler eventHandler = new StoredEventHandler() {
+ @Override
+ public void handle(Event e) {
+ checkActive();
+ switch (e.getKind()) {
+ case INFO:
+ throw new UnsupportedOperationException("Values should not display INFO messages: " +
+ skyKey + " printed " + e.getLocation() + ": " + e.getMessage());
+ case PROGRESS:
+ reporter.handle(e);
+ break;
+ default:
+ super.handle(e);
+ }
+ }
+ };
+
+ private SkyFunctionEnvironment(SkyKey skyKey, Set<SkyKey> directDeps, ValueVisitor visitor) {
+ this(skyKey, directDeps, null, visitor);
+ }
+
+ private SkyFunctionEnvironment(SkyKey skyKey, Set<SkyKey> directDeps,
+ @Nullable Map<SkyKey, ValueWithMetadata> bubbleErrorInfo, ValueVisitor visitor) {
+ this.skyKey = skyKey;
+ this.directDeps = Collections.unmodifiableSet(directDeps);
+ this.bubbleErrorInfo = bubbleErrorInfo;
+ this.childErrorInfos.addAll(childErrorInfos);
+ this.visitor = visitor;
+ }
+
+ private void checkActive() {
+ Preconditions.checkState(building, skyKey);
+ }
+
+ private NestedSet<TaggedEvents> buildEvents(boolean missingChildren) {
+ // Aggregate the nested set of events from the direct deps, also adding the events from
+ // building this value.
+ NestedSetBuilder<TaggedEvents> eventBuilder = NestedSetBuilder.stableOrder();
+ ImmutableList<Event> events = eventHandler.getEvents();
+ if (!events.isEmpty()) {
+ eventBuilder.add(new TaggedEvents(getTagFromKey(), events));
+ }
+ for (SkyKey dep : graph.get(skyKey).getTemporaryDirectDeps()) {
+ ValueWithMetadata value = getValueMaybeFromError(dep, bubbleErrorInfo);
+ if (value != null) {
+ eventBuilder.addTransitive(value.getTransitiveEvents());
+ } else {
+ Preconditions.checkState(missingChildren, "", dep, skyKey);
+ }
+ }
+ return eventBuilder.build();
+ }
+
+ /**
+ * If this node has an error, that is, if errorInfo is non-null, do nothing. Otherwise, set
+ * errorInfo to the union of the child errors that were recorded earlier by getValueOrException,
+ * if there are any.
+ */
+ private void finalizeErrorInfo() {
+ if (errorInfo == null && !childErrorInfos.isEmpty()) {
+ errorInfo = new ErrorInfo(skyKey, childErrorInfos);
+ }
+ }
+
+ private void setValue(SkyValue newValue) {
+ Preconditions.checkState(errorInfo == null && bubbleErrorInfo == null,
+ "%s %s %s %s", skyKey, newValue, errorInfo, bubbleErrorInfo);
+ Preconditions.checkState(value == null, "%s %s %s", skyKey, value, newValue);
+ value = newValue;
+ }
+
+ /**
+ * Set this node to be in error. The node's value must not have already been set. However, all
+ * dependencies of this node <i>must</i> already have been registered, since this method may
+ * register a dependence on the error transience node, which should always be the last dep.
+ */
+ private void setError(ErrorInfo errorInfo) {
+ Preconditions.checkState(value == null, "%s %s %s", skyKey, value, errorInfo);
+ Preconditions.checkState(this.errorInfo == null,
+ "%s %s %s", skyKey, this.errorInfo, errorInfo);
+
+ if (errorInfo.isTransient()) {
+ DependencyState triState =
+ graph.get(ErrorTransienceValue.key()).addReverseDepAndCheckIfDone(skyKey);
+ Preconditions.checkState(triState == DependencyState.DONE,
+ "%s %s %s", skyKey, triState, errorInfo);
+
+ final NodeEntry state = graph.get(skyKey);
+ state.addTemporaryDirectDeps(
+ GroupedListHelper.create(ImmutableList.of(ErrorTransienceValue.key())));
+ state.signalDep();
+ }
+
+ this.errorInfo = Preconditions.checkNotNull(errorInfo, skyKey);
+ }
+
+ /** Get a child of the value being evaluated, for use by the value builder. */
+ private ValueOrUntypedException getValueOrUntypedException(SkyKey depKey) {
+ checkActive();
+ depKey = KEY_CANONICALIZER.intern(depKey); // Canonicalize SkyKeys to save memory.
+ ValueWithMetadata value = getValueMaybeFromError(depKey, bubbleErrorInfo);
+ if (value == null) {
+ // If this entry is not yet done then (optionally) record the missing dependency and return
+ // null.
+ valuesMissing = true;
+ if (bubbleErrorInfo != null) {
+ // Values being built just for their errors don't get to request new children.
+ return ValueOrExceptionUtils.ofNull();
+ }
+ Preconditions.checkState(!directDeps.contains(depKey), "%s %s %s", skyKey, depKey, value);
+ addDep(depKey);
+ valuesMissing = true;
+ return ValueOrExceptionUtils.ofNull();
+ }
+
+ if (!directDeps.contains(depKey)) {
+ // If this child is done, we will return it, but also record that it was newly requested so
+ // that the dependency can be properly registered in the graph.
+ addDep(depKey);
+ }
+
+ replayingNestedSetEventVisitor.visit(value.getTransitiveEvents());
+ ErrorInfo errorInfo = value.getErrorInfo();
+
+ if (errorInfo != null) {
+ childErrorInfos.add(errorInfo);
+ }
+
+ if (value.getValue() != null && (keepGoing || errorInfo == null)) {
+ // The caller is given the value of the value if there was no error computing the value, or
+ // if this is a keepGoing build (in which case each value should get child values even if
+ // there are also errors).
+ return ValueOrExceptionUtils.ofValueUntyped(value.getValue());
+ }
+
+ // There was an error building the value, which we will either report by throwing an exception
+ // or insulate the caller from by returning null.
+ Preconditions.checkNotNull(errorInfo, "%s %s %s", skyKey, depKey, value);
+
+ if (!keepGoing && errorInfo.getException() != null && bubbleErrorInfo == null) {
+ // Child errors should not be propagated in noKeepGoing mode (except during error bubbling).
+ // Instead we should fail fast.
+
+ // We arbitrarily record the first child error.
+ if (depErrorKey == null) {
+ depErrorKey = depKey;
+ }
+ valuesMissing = true;
+ return ValueOrExceptionUtils.ofNull();
+ }
+
+ if (bubbleErrorInfo != null) {
+ // Set interrupted status, so that builder doesn't try anything fancy after this.
+ Thread.currentThread().interrupt();
+ }
+ if (errorInfo.getException() != null) {
+ // Give builder a chance to handle this exception.
+ Exception e = errorInfo.getException();
+ return ValueOrExceptionUtils.ofExn(e);
+ }
+ // In a cycle.
+ Preconditions.checkState(!Iterables.isEmpty(errorInfo.getCycleInfo()), "%s %s %s %s", skyKey,
+ depKey, errorInfo, value);
+ valuesMissing = true;
+ return ValueOrExceptionUtils.ofNull();
+ }
+
+ private <E extends Exception> ValueOrException<E> getValueOrException(SkyKey depKey,
+ Class<E> exceptionClass) {
+ return ValueOrExceptionUtils.downcovert(getValueOrException(depKey, exceptionClass,
+ BottomException.class), exceptionClass);
+ }
+
+ private <E1 extends Exception, E2 extends Exception> ValueOrException2<E1, E2>
+ getValueOrException(SkyKey depKey, Class<E1> exceptionClass1, Class<E2> exceptionClass2) {
+ return ValueOrExceptionUtils.downconvert(getValueOrException(depKey, exceptionClass1,
+ exceptionClass2, BottomException.class), exceptionClass1, exceptionClass2);
+ }
+
+ private <E1 extends Exception, E2 extends Exception, E3 extends Exception>
+ ValueOrException3<E1, E2, E3> getValueOrException(SkyKey depKey, Class<E1> exceptionClass1,
+ Class<E2> exceptionClass2, Class<E3> exceptionClass3) {
+ return ValueOrExceptionUtils.downconvert(getValueOrException(depKey, exceptionClass1,
+ exceptionClass2, exceptionClass3, BottomException.class), exceptionClass1,
+ exceptionClass2, exceptionClass3);
+ }
+
+ private <E1 extends Exception, E2 extends Exception, E3 extends Exception,
+ E4 extends Exception> ValueOrException4<E1, E2, E3, E4> getValueOrException(SkyKey depKey,
+ Class<E1> exceptionClass1, Class<E2> exceptionClass2, Class<E3> exceptionClass3,
+ Class<E4> exceptionClass4) {
+ SkyFunctionException.validateExceptionType(exceptionClass1);
+ SkyFunctionException.validateExceptionType(exceptionClass2);
+ SkyFunctionException.validateExceptionType(exceptionClass3);
+ SkyFunctionException.validateExceptionType(exceptionClass4);
+ ValueOrUntypedException voe = getValueOrUntypedException(depKey);
+ SkyValue value = voe.getValue();
+ if (value != null) {
+ return ValueOrExceptionUtils.ofValue(value);
+ }
+ Exception e = voe.getException();
+ if (e != null) {
+ if (exceptionClass1.isInstance(e)) {
+ return ValueOrExceptionUtils.ofExn1(exceptionClass1.cast(e));
+ }
+ if (exceptionClass2.isInstance(e)) {
+ return ValueOrExceptionUtils.ofExn2(exceptionClass2.cast(e));
+ }
+ if (exceptionClass3.isInstance(e)) {
+ return ValueOrExceptionUtils.ofExn3(exceptionClass3.cast(e));
+ }
+ if (exceptionClass4.isInstance(e)) {
+ return ValueOrExceptionUtils.ofExn4(exceptionClass4.cast(e));
+ }
+ }
+ valuesMissing = true;
+ return ValueOrExceptionUtils.ofNullValue();
+ }
+
+ @Override
+ @Nullable
+ public SkyValue getValue(SkyKey depKey) {
+ try {
+ return getValueOrThrow(depKey, BottomException.class);
+ } catch (BottomException e) {
+ throw new IllegalStateException("shouldn't reach here");
+ }
+ }
+
+ @Override
+ @Nullable
+ public <E extends Exception> SkyValue getValueOrThrow(SkyKey depKey, Class<E> exceptionClass)
+ throws E {
+ return getValueOrException(depKey, exceptionClass).get();
+ }
+
+ @Override
+ @Nullable
+ public <E1 extends Exception, E2 extends Exception> SkyValue getValueOrThrow(SkyKey depKey,
+ Class<E1> exceptionClass1, Class<E2> exceptionClass2) throws E1, E2 {
+ return getValueOrException(depKey, exceptionClass1, exceptionClass2).get();
+ }
+
+ @Override
+ @Nullable
+ public <E1 extends Exception, E2 extends Exception,
+ E3 extends Exception> SkyValue getValueOrThrow(SkyKey depKey, Class<E1> exceptionClass1,
+ Class<E2> exceptionClass2, Class<E3> exceptionClass3) throws E1, E2, E3 {
+ return getValueOrException(depKey, exceptionClass1, exceptionClass2, exceptionClass3).get();
+ }
+
+ @Override
+ public <E1 extends Exception, E2 extends Exception, E3 extends Exception,
+ E4 extends Exception> SkyValue getValueOrThrow(SkyKey depKey, Class<E1> exceptionClass1,
+ Class<E2> exceptionClass2, Class<E3> exceptionClass3, Class<E4> exceptionClass4) throws E1,
+ E2, E3, E4 {
+ return getValueOrException(depKey, exceptionClass1, exceptionClass2, exceptionClass3,
+ exceptionClass4).get();
+ }
+
+ @Override
+ public Map<SkyKey, SkyValue> getValues(Iterable<SkyKey> depKeys) {
+ return Maps.transformValues(getValuesOrThrow(depKeys, BottomException.class),
+ GET_VALUE_FROM_VOE);
+ }
+
+ @Override
+ public <E extends Exception> Map<SkyKey, ValueOrException<E>> getValuesOrThrow(
+ Iterable<SkyKey> depKeys, Class<E> exceptionClass) {
+ return Maps.transformValues(getValuesOrThrow(depKeys, exceptionClass, BottomException.class),
+ makeSafeDowncastToVOEFunction(exceptionClass));
+ }
+
+ @Override
+ public <E1 extends Exception,
+ E2 extends Exception> Map<SkyKey, ValueOrException2<E1, E2>> getValuesOrThrow(
+ Iterable<SkyKey> depKeys, Class<E1> exceptionClass1, Class<E2> exceptionClass2) {
+ return Maps.transformValues(getValuesOrThrow(depKeys, exceptionClass1, exceptionClass2,
+ BottomException.class), makeSafeDowncastToVOE2Function(exceptionClass1,
+ exceptionClass2));
+ }
+
+ @Override
+ public <E1 extends Exception, E2 extends Exception, E3 extends Exception> Map<SkyKey,
+ ValueOrException3<E1, E2, E3>> getValuesOrThrow(Iterable<SkyKey> depKeys,
+ Class<E1> exceptionClass1, Class<E2> exceptionClass2, Class<E3> exceptionClass3) {
+ return Maps.transformValues(getValuesOrThrow(depKeys, exceptionClass1, exceptionClass2,
+ exceptionClass3, BottomException.class), makeSafeDowncastToVOE3Function(exceptionClass1,
+ exceptionClass2, exceptionClass3));
+ }
+
+ @Override
+ public <E1 extends Exception, E2 extends Exception, E3 extends Exception,
+ E4 extends Exception> Map<SkyKey, ValueOrException4<E1, E2, E3, E4>> getValuesOrThrow(
+ Iterable<SkyKey> depKeys, Class<E1> exceptionClass1, Class<E2> exceptionClass2,
+ Class<E3> exceptionClass3, Class<E4> exceptionClass4) {
+ Map<SkyKey, ValueOrException4<E1, E2, E3, E4>> result = new HashMap<>();
+ newlyRequestedDeps.startGroup();
+ for (SkyKey depKey : depKeys) {
+ if (result.containsKey(depKey)) {
+ continue;
+ }
+ result.put(depKey, getValueOrException(depKey, exceptionClass1, exceptionClass2,
+ exceptionClass3, exceptionClass4));
+ }
+ newlyRequestedDeps.endGroup();
+ return Collections.unmodifiableMap(result);
+ }
+
+ private void addDep(SkyKey key) {
+ if (!newlyRequestedDeps.contains(key)) {
+ // dep may have been requested already this evaluation. If not, add it.
+ newlyRequestedDeps.add(key);
+ }
+ }
+
+ @Override
+ public boolean valuesMissing() {
+ return valuesMissing;
+ }
+
+ /**
+ * If {@code !keepGoing} and there is at least one dep in error, returns a dep in error.
+ * Otherwise returns {@code null}.
+ */
+ @Nullable
+ private SkyKey getDepErrorKey() {
+ return depErrorKey;
+ }
+
+ @Override
+ public EventHandler getListener() {
+ checkActive();
+ return eventHandler;
+ }
+
+ private void doneBuilding() {
+ building = false;
+ }
+
+ /**
+ * Apply the change to the graph (mostly) atomically and signal all nodes that are waiting for
+ * this node to complete. Adding nodes and signaling is not atomic, but may need to be changed
+ * for interruptibility.
+ *
+ * <p>Parents are only enqueued if {@code enqueueParents} holds. Parents should be enqueued
+ * unless (1) this node is being built after the main evaluation has aborted, or (2) this node
+ * is being built with --nokeep_going, and so we are about to shut down the main evaluation
+ * anyway.
+ *
+ * <p>The node entry is informed if the node's value and error are definitive via the flag
+ * {@code completeValue}.
+ */
+ void commit(boolean enqueueParents) {
+ NodeEntry primaryEntry = Preconditions.checkNotNull(graph.get(skyKey), skyKey);
+ // Construct the definitive error info, if there is one.
+ finalizeErrorInfo();
+
+ // We have the following implications:
+ // errorInfo == null => value != null => enqueueParents.
+ // All these implications are strict:
+ // (1) errorInfo != null && value != null happens for values with recoverable errors.
+ // (2) value == null && enqueueParents happens for values that are found to have errors
+ // during a --keep_going build.
+
+ NestedSet<TaggedEvents> events = buildEvents(/*missingChildren=*/false);
+ if (value == null) {
+ Preconditions.checkNotNull(errorInfo, "%s %s", skyKey, primaryEntry);
+ // We could consider using max(childVersions) here instead of graphVersion. When full
+ // versioning is implemented, this would allow evaluation at a version between
+ // max(childVersions) and graphVersion to re-use this result.
+ Set<SkyKey> reverseDeps = primaryEntry.setValue(
+ ValueWithMetadata.error(errorInfo, events), graphVersion);
+ signalValuesAndEnqueueIfReady(enqueueParents ? visitor : null, reverseDeps, graphVersion);
+ } else {
+ // We must be enqueueing parents if we have a value.
+ Preconditions.checkState(enqueueParents, "%s %s", skyKey, primaryEntry);
+ Set<SkyKey> reverseDeps;
+ Version valueVersion;
+ // If this entry is dirty, setValue may not actually change it, if it determines that
+ // the data being written now is the same as the data already present in the entry.
+ // We could consider using max(childVersions) here instead of graphVersion. When full
+ // versioning is implemented, this would allow evaluation at a version between
+ // max(childVersions) and graphVersion to re-use this result.
+ reverseDeps = primaryEntry.setValue(
+ ValueWithMetadata.normal(value, errorInfo, events), graphVersion);
+ // Note that if this update didn't actually change the value entry, this version may not
+ // be the graph version.
+ valueVersion = primaryEntry.getVersion();
+ Preconditions.checkState(valueVersion.atMost(graphVersion),
+ "%s should be at most %s in the version partial ordering",
+ valueVersion, graphVersion);
+ if (progressReceiver != null) {
+ // Tell the receiver that this value was built. If valueVersion.equals(graphVersion), it
+ // was evaluated this run, and so was changed. Otherwise, it is less than graphVersion,
+ // by the Preconditions check above, and was not actually changed this run -- when it was
+ // written above, its version stayed below this update's version, so its value remains the
+ // same as before.
+ progressReceiver.evaluated(skyKey, value,
+ valueVersion.equals(graphVersion) ? EvaluationState.BUILT : EvaluationState.CLEAN);
+ }
+ signalValuesAndEnqueueIfReady(visitor, reverseDeps, valueVersion);
+ }
+
+ visitor.notifyDone(skyKey);
+ replayingNestedSetEventVisitor.visit(events);
+ }
+
+ @Nullable
+ private String getTagFromKey() {
+ return skyFunctions.get(skyKey.functionName()).extractTag(skyKey);
+ }
+
+ /**
+ * Gets the latch that is counted down when an exception is thrown in {@code
+ * AbstractQueueVisitor}. For use in tests to check if an exception actually was thrown. Calling
+ * {@code AbstractQueueVisitor#awaitExceptionForTestingOnly} can throw a spurious {@link
+ * InterruptedException} because {@link CountDownLatch#await} checks the interrupted bit before
+ * returning, even if the latch is already at 0. See bug "testTwoErrors is flaky".
+ */
+ CountDownLatch getExceptionLatchForTesting() {
+ return visitor.getExceptionLatchForTestingOnly();
+ }
+
+ @Override
+ public boolean inErrorBubblingForTesting() {
+ return bubbleErrorInfo != null;
+ }
+ }
+
+ private static final Function<ValueOrException<BottomException>, SkyValue> GET_VALUE_FROM_VOE =
+ new Function<ValueOrException<BottomException>, SkyValue>() {
+ @Override
+ public SkyValue apply(ValueOrException<BottomException> voe) {
+ return ValueOrExceptionUtils.downcovert(voe);
+ }
+ };
+
+ private static <E extends Exception>
+ Function<ValueOrException2<E, BottomException>, ValueOrException<E>>
+ makeSafeDowncastToVOEFunction(final Class<E> exceptionClass) {
+ return new Function<ValueOrException2<E, BottomException>, ValueOrException<E>>() {
+ @Override
+ public ValueOrException<E> apply(ValueOrException2<E, BottomException> voe) {
+ return ValueOrExceptionUtils.downcovert(voe, exceptionClass);
+ }
+ };
+ }
+
+ private static <E1 extends Exception, E2 extends Exception>
+ Function<ValueOrException3<E1, E2, BottomException>, ValueOrException2<E1, E2>>
+ makeSafeDowncastToVOE2Function(final Class<E1> exceptionClass1,
+ final Class<E2> exceptionClass2) {
+ return new Function<ValueOrException3<E1, E2, BottomException>,
+ ValueOrException2<E1, E2>>() {
+ @Override
+ public ValueOrException2<E1, E2> apply(ValueOrException3<E1, E2, BottomException> voe) {
+ return ValueOrExceptionUtils.downconvert(voe, exceptionClass1, exceptionClass2);
+ }
+ };
+ }
+
+ private static <E1 extends Exception, E2 extends Exception, E3 extends Exception>
+ Function<ValueOrException4<E1, E2, E3, BottomException>, ValueOrException3<E1, E2, E3>>
+ makeSafeDowncastToVOE3Function(final Class<E1> exceptionClass1,
+ final Class<E2> exceptionClass2, final Class<E3> exceptionClass3) {
+ return new Function<ValueOrException4<E1, E2, E3, BottomException>,
+ ValueOrException3<E1, E2, E3>>() {
+ @Override
+ public ValueOrException3<E1, E2, E3> apply(ValueOrException4<E1, E2, E3,
+ BottomException> voe) {
+ return ValueOrExceptionUtils.downconvert(voe, exceptionClass1, exceptionClass2,
+ exceptionClass3);
+ }
+ };
+ }
+
+ private class ValueVisitor extends AbstractQueueVisitor {
+ private AtomicBoolean preventNewEvaluations = new AtomicBoolean(false);
+ private final Set<SkyKey> inflightNodes = Sets.newConcurrentHashSet();
+
+ private ValueVisitor(int threadCount) {
+ super(/*concurrent*/true,
+ threadCount,
+ threadCount,
+ 1, TimeUnit.SECONDS,
+ /*failFastOnException*/true,
+ /*failFastOnInterrupt*/true,
+ "skyframe-evaluator");
+ }
+
+ @Override
+ protected boolean isCriticalError(Throwable e) {
+ return e instanceof RuntimeException;
+ }
+
+ protected void waitForCompletion() throws InterruptedException {
+ work(/*failFastOnInterrupt=*/true);
+ }
+
+ public void enqueueEvaluation(final SkyKey key) {
+ // We unconditionally add the key to the set of in-flight nodes because even if evaluation is
+ // never scheduled we still want to remove the previously created NodeEntry from the graph.
+ // Otherwise we would leave the graph in a weird state (wasteful garbage in the best case and
+ // inconsistent in the worst case).
+ boolean newlyEnqueued = inflightNodes.add(key);
+ // All nodes enqueued for evaluation will be either verified clean, re-evaluated, or cleaned
+ // up after being in-flight when an error happens in nokeep_going mode or in the event of an
+ // interrupt. In any of these cases, they won't be dirty anymore.
+ if (newlyEnqueued) {
+ dirtyKeyTracker.notDirty(key);
+ }
+ if (preventNewEvaluations.get()) {
+ return;
+ }
+ if (newlyEnqueued && progressReceiver != null) {
+ progressReceiver.enqueueing(key);
+ }
+ enqueue(new Evaluate(this, key));
+ }
+
+ public void preventNewEvaluations() {
+ preventNewEvaluations.set(true);
+ }
+
+ public void notifyDone(SkyKey key) {
+ inflightNodes.remove(key);
+ }
+
+ private boolean isInflight(SkyKey key) {
+ return inflightNodes.contains(key);
+ }
+ }
+
+ /**
+ * An action that evaluates a value.
+ */
+ private class Evaluate implements Runnable {
+ private final ValueVisitor visitor;
+ /** The name of the value to be evaluated. */
+ private final SkyKey skyKey;
+
+ private Evaluate(ValueVisitor visitor, SkyKey skyKey) {
+ this.visitor = visitor;
+ this.skyKey = skyKey;
+ }
+
+ private void enqueueChild(SkyKey skyKey, NodeEntry entry, SkyKey child) {
+ Preconditions.checkState(!entry.isDone(), "%s %s", skyKey, entry);
+ Preconditions.checkState(!ErrorTransienceValue.key().equals(child),
+ "%s cannot request ErrorTransienceValue as a dep: %s", skyKey, entry);
+
+ NodeEntry depEntry = graph.createIfAbsent(child);
+ switch (depEntry.addReverseDepAndCheckIfDone(skyKey)) {
+ case DONE :
+ if (entry.signalDep(depEntry.getVersion())) {
+ // This can only happen if there are no more children to be added.
+ visitor.enqueueEvaluation(skyKey);
+ }
+ break;
+ case ADDED_DEP :
+ break;
+ case NEEDS_SCHEDULING :
+ visitor.enqueueEvaluation(child);
+ break;
+ }
+ }
+
+ /**
+ * Returns true if this depGroup consists of the error transience value and the error transience
+ * value is newer than the entry, meaning that the entry must be re-evaluated.
+ */
+ private boolean invalidatedByErrorTransience(Collection<SkyKey> depGroup, NodeEntry entry) {
+ return depGroup.size() == 1
+ && depGroup.contains(ErrorTransienceValue.key())
+ && !graph.get(ErrorTransienceValue.key()).getVersion().atMost(entry.getVersion());
+ }
+
+ @Override
+ public void run() {
+ NodeEntry state = graph.get(skyKey);
+ Preconditions.checkNotNull(state, "%s %s", skyKey, state);
+ Preconditions.checkState(state.isReady(), "%s %s", skyKey, state);
+
+ if (state.isDirty()) {
+ switch (state.getDirtyState()) {
+ case CHECK_DEPENDENCIES:
+ // Evaluating a dirty node for the first time, and checking its children to see if any
+ // of them have changed. Note that there must be dirty children for this to happen.
+
+ // Check the children group by group -- we don't want to evaluate a value that is no
+ // longer needed because an earlier dependency changed. For example, //foo:foo depends
+ // on target //bar:bar and is built. Then foo/BUILD is modified to remove the dependence
+ // on bar, and bar/BUILD is deleted. Reloading //bar:bar would incorrectly throw an
+ // exception. To avoid this, we must reload foo/BUILD first, at which point we will
+ // discover that it has changed, and re-evaluate target //foo:foo from scratch.
+ // On the other hand, when an action requests all of its inputs, we can safely check all
+ // of them in parallel on a subsequent build. So we allow checking an entire group in
+ // parallel here, if the node builder requested a group last build.
+ Collection<SkyKey> directDepsToCheck = state.getNextDirtyDirectDeps();
+
+ if (invalidatedByErrorTransience(directDepsToCheck, state)) {
+ // If this dep is the ErrorTransienceValue and the ErrorTransienceValue has been
+ // updated then we need to force a rebuild. We would like to just signal the entry as
+ // usual, but we can't, because then the ErrorTransienceValue would remain as a dep,
+ // which would be incorrect if, for instance, the value re-evaluated to a non-error.
+ state.forceRebuild();
+ break; // Fall through to re-evaluation.
+ } else {
+ // If this isn't the error transience value, it is safe to add these deps back to the
+ // node -- even if one of them has changed, the contract of pruning is that the node
+ // will request these deps again when it rebuilds. We must add these deps before
+ // enqueuing them, so that the node knows that it depends on them.
+ state.addTemporaryDirectDeps(GroupedListHelper.create(directDepsToCheck));
+ }
+
+ for (SkyKey directDep : directDepsToCheck) {
+ enqueueChild(skyKey, state, directDep);
+ }
+ return;
+ case VERIFIED_CLEAN:
+ // No child has a changed value. This node can be marked done and its parents signaled
+ // without any re-evaluation.
+ visitor.notifyDone(skyKey);
+ Set<SkyKey> reverseDeps = state.markClean();
+ SkyValue value = state.getValue();
+ if (progressReceiver != null && value != null) {
+ // Tell the receiver that the value was not actually changed this run.
+ progressReceiver.evaluated(skyKey, value, EvaluationState.CLEAN);
+ }
+ signalValuesAndEnqueueIfReady(visitor, reverseDeps, state.getVersion());
+ return;
+ case REBUILDING:
+ // Nothing to be done if we are already rebuilding.
+ }
+ }
+
+ // TODO(bazel-team): Once deps are requested in a deterministic order within a group, or the
+ // framework is resilient to rearranging group order, change this so that
+ // SkyFunctionEnvironment "follows along" as the node builder runs, iterating through the
+ // direct deps that were requested on a previous run. This would allow us to avoid the
+ // conversion of the direct deps into a set.
+ Set<SkyKey> directDeps = state.getTemporaryDirectDeps();
+ Preconditions.checkState(!directDeps.contains(ErrorTransienceValue.key()),
+ "%s cannot have a dep on ErrorTransienceValue during building: %s", skyKey, state);
+ // Get the corresponding SkyFunction and call it on this value.
+ SkyFunctionEnvironment env = new SkyFunctionEnvironment(skyKey, directDeps, visitor);
+ SkyFunctionName functionName = skyKey.functionName();
+ SkyFunction factory = skyFunctions.get(functionName);
+ Preconditions.checkState(factory != null, "%s %s", functionName, state);
+
+ SkyValue value = null;
+ Profiler.instance().startTask(ProfilerTask.SKYFUNCTION, skyKey);
+ try {
+ // TODO(bazel-team): count how many of these calls returns null vs. non-null
+ value = factory.compute(skyKey, env);
+ } catch (final SkyFunctionException builderException) {
+ ReifiedSkyFunctionException reifiedBuilderException =
+ new ReifiedSkyFunctionException(builderException, skyKey);
+ // Propagated transitive errors are treated the same as missing deps.
+ if (reifiedBuilderException.getRootCauseSkyKey().equals(skyKey)) {
+ boolean shouldFailFast = !keepGoing || builderException.isCatastrophic();
+ if (shouldFailFast) {
+ // After we commit this error to the graph but before the eval call completes with the
+ // error there is a race-like opportunity for the error to be used, either by an
+ // in-flight computation or by a future computation.
+ if (errorEncountered.compareAndSet(false, true)) {
+ // This is the first error encountered.
+ visitor.preventNewEvaluations();
+ } else {
+ // This is not the first error encountered, so we ignore it so that we can terminate
+ // with the first error.
+ return;
+ }
+ }
+
+ registerNewlyDiscoveredDepsForDoneEntry(skyKey, state, env);
+ ErrorInfo errorInfo = new ErrorInfo(reifiedBuilderException);
+ env.setError(errorInfo);
+ env.commit(/*enqueueParents=*/keepGoing);
+ if (!shouldFailFast) {
+ return;
+ }
+ throw SchedulerException.ofError(errorInfo, skyKey);
+ }
+ } catch (InterruptedException ie) {
+ // InterruptedException cannot be thrown by Runnable.run, so we must wrap it.
+ // Interrupts can be caught by both the Evaluator and the AbstractQueueVisitor.
+ // The former will unwrap the IE and propagate it as is; the latter will throw a new IE.
+ throw SchedulerException.ofInterruption(ie, skyKey);
+ } catch (RuntimeException re) {
+ // Programmer error (most likely NPE or a failed precondition in a SkyFunction). Output
+ // some context together with the exception.
+ String msg = prepareCrashMessage(skyKey, state.getInProgressReverseDeps());
+ throw new RuntimeException(msg, re);
+ } finally {
+ env.doneBuilding();
+ Profiler.instance().completeTask(ProfilerTask.SKYFUNCTION);
+ }
+
+ GroupedListHelper<SkyKey> newDirectDeps = env.newlyRequestedDeps;
+
+ if (value != null) {
+ Preconditions.checkState(!env.valuesMissing,
+ "%s -> %s, ValueEntry: %s", skyKey, newDirectDeps, state);
+ env.setValue(value);
+ registerNewlyDiscoveredDepsForDoneEntry(skyKey, state, env);
+ env.commit(/*enqueueParents=*/true);
+ return;
+ }
+
+ if (!newDirectDeps.isEmpty() && env.getDepErrorKey() != null) {
+ Preconditions.checkState(!keepGoing);
+ // We encountered a child error in noKeepGoing mode, so we want to fail fast. But we first
+ // need to add the edge between the current node and the child error it requested so that
+ // error bubbling can occur. Note that this edge will subsequently be removed during graph
+ // cleaning (since the current node will never be committed to the graph).
+ SkyKey childErrorKey = env.getDepErrorKey();
+ NodeEntry childErrorEntry = Preconditions.checkNotNull(graph.get(childErrorKey),
+ "skyKey: %s, state: %s childErrorKey: %s", skyKey, state, childErrorKey);
+ if (!state.getTemporaryDirectDeps().contains(childErrorKey)) {
+ // This means the cached error was freshly requested (e.g. the parent has never been
+ // built before).
+ Preconditions.checkState(newDirectDeps.contains(childErrorKey), "%s %s %s", state,
+ childErrorKey, newDirectDeps);
+ state.addTemporaryDirectDeps(GroupedListHelper.create(ImmutableList.of(childErrorKey)));
+ DependencyState childErrorState = childErrorEntry.addReverseDepAndCheckIfDone(skyKey);
+ Preconditions.checkState(childErrorState == DependencyState.DONE,
+ "skyKey: %s, state: %s childErrorKey: %s", skyKey, state, childErrorKey,
+ childErrorEntry);
+ } else {
+ // This means the cached error was previously requested, and was then subsequently (after
+ // a restart) requested along with another sibling dep. This can happen on an incremental
+ // eval call when the parent is dirty and the child error is in a separate dependency
+ // group from the sibling dep.
+ Preconditions.checkState(!newDirectDeps.contains(childErrorKey), "%s %s %s", state,
+ childErrorKey, newDirectDeps);
+ Preconditions.checkState(childErrorEntry.isDone(),
+ "skyKey: %s, state: %s childErrorKey: %s", skyKey, state, childErrorKey,
+ childErrorEntry);
+ }
+ ErrorInfo childErrorInfo = Preconditions.checkNotNull(childErrorEntry.getErrorInfo());
+ throw SchedulerException.ofError(childErrorInfo, childErrorKey);
+ }
+
+ // TODO(bazel-team): This code is not safe to interrupt, because we would lose the state in
+ // newDirectDeps.
+
+ // TODO(bazel-team): An ill-behaved SkyFunction can throw us into an infinite loop where we
+ // add more dependencies on every run. [skyframe-core]
+
+ // Add all new keys to the set of known deps.
+ state.addTemporaryDirectDeps(newDirectDeps);
+
+ // If there were no newly requested dependencies, at least one of them was in error or there
+ // is a bug in the SkyFunction implementation. The environment has collected its errors, so we
+ // just order it to be built.
+ if (newDirectDeps.isEmpty()) {
+ // TODO(bazel-team): This means a bug in the SkyFunction. What to do?
+ Preconditions.checkState(!env.childErrorInfos.isEmpty(), "%s %s", skyKey, state);
+ env.commit(/*enqueueParents=*/keepGoing);
+ if (!keepGoing) {
+ throw SchedulerException.ofError(state.getErrorInfo(), skyKey);
+ }
+ return;
+ }
+
+ for (SkyKey newDirectDep : newDirectDeps) {
+ enqueueChild(skyKey, state, newDirectDep);
+ }
+ // It is critical that there is no code below this point.
+ }
+
+ private String prepareCrashMessage(SkyKey skyKey, Iterable<SkyKey> reverseDeps) {
+ StringBuilder reverseDepDump = new StringBuilder();
+ for (SkyKey key : reverseDeps) {
+ if (reverseDepDump.length() > MAX_REVERSEDEP_DUMP_LENGTH) {
+ reverseDepDump.append(", ...");
+ break;
+ }
+ if (reverseDepDump.length() > 0) {
+ reverseDepDump.append(", ");
+ }
+ reverseDepDump.append("'");
+ reverseDepDump.append(key.toString());
+ reverseDepDump.append("'");
+ }
+
+ return String.format(
+ "Unrecoverable error while evaluating node '%s' (requested by nodes %s)",
+ skyKey, reverseDepDump);
+ }
+
+ private static final int MAX_REVERSEDEP_DUMP_LENGTH = 1000;
+ }
+
+ /**
+ * Signals all parents that this node is finished. If visitor is not null, also enqueues any
+ * parents that are ready. If visitor is null, indicating that we are building this node after
+ * the main build aborted, then skip any parents that are already done (that can happen with
+ * cycles).
+ */
+ private void signalValuesAndEnqueueIfReady(@Nullable ValueVisitor visitor, Iterable<SkyKey> keys,
+ Version version) {
+ if (visitor != null) {
+ for (SkyKey key : keys) {
+ if (graph.get(key).signalDep(version)) {
+ visitor.enqueueEvaluation(key);
+ }
+ }
+ } else {
+ for (SkyKey key : keys) {
+ NodeEntry entry = Preconditions.checkNotNull(graph.get(key), key);
+ if (!entry.isDone()) {
+ // In cycles, we can have parents that are already done.
+ entry.signalDep(version);
+ }
+ }
+ }
+ }
+
+ /**
+ * If child is not done, removes key from child's reverse deps. Returns whether child should be
+ * removed from key's entry's direct deps.
+ */
+ private boolean removeIncompleteChild(SkyKey key, SkyKey child) {
+ NodeEntry childEntry = graph.get(child);
+ if (!isDoneForBuild(childEntry)) {
+ childEntry.removeReverseDep(key);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Add any additional deps that were registered during the run of a builder that finished by
+ * creating a node or throwing an error. Builders may throw errors even if all their deps were
+ * not provided -- we trust that a SkyFunction may be know it should throw an error even if not
+ * all of its requested deps are done. However, that means we're assuming the SkyFunction would
+ * throw that same error if all of its requested deps were done. Unfortunately, there is no way to
+ * enforce that condition.
+ */
+ private void registerNewlyDiscoveredDepsForDoneEntry(SkyKey skyKey, NodeEntry entry,
+ SkyFunctionEnvironment env) {
+ Set<SkyKey> unfinishedDeps = new HashSet<>();
+ Iterables.addAll(unfinishedDeps,
+ Iterables.filter(env.newlyRequestedDeps, Predicates.not(nodeEntryIsDone)));
+ env.newlyRequestedDeps.remove(unfinishedDeps);
+ entry.addTemporaryDirectDeps(env.newlyRequestedDeps);
+ for (SkyKey newDep : env.newlyRequestedDeps) {
+ NodeEntry depEntry = graph.get(newDep);
+ DependencyState triState = depEntry.addReverseDepAndCheckIfDone(skyKey);
+ Preconditions.checkState(DependencyState.DONE == triState,
+ "new dep %s was not already done for %s. ValueEntry: %s. DepValueEntry: %s",
+ newDep, skyKey, entry, depEntry);
+ entry.signalDep();
+ }
+ Preconditions.checkState(entry.isReady(), "%s %s %s", skyKey, entry, env.newlyRequestedDeps);
+ }
+
+ private void informProgressReceiverThatValueIsDone(SkyKey key) {
+ if (progressReceiver != null) {
+ NodeEntry entry = graph.get(key);
+ Preconditions.checkState(entry.isDone(), entry);
+ SkyValue value = entry.getValue();
+ if (value != null) {
+ Version valueVersion = entry.getVersion();
+ Preconditions.checkState(valueVersion.atMost(graphVersion),
+ "%s should be at most %s in the version partial ordering", valueVersion, graphVersion);
+ // Nodes with errors will have no value. Don't inform the receiver in that case.
+ // For most nodes we do not inform the progress receiver if they were already done
+ // when we retrieve them, but top-level nodes are presumably of more interest.
+ // If valueVersion is not equal to graphVersion, it must be less than it (by the
+ // Preconditions check above), and so the node is clean.
+ progressReceiver.evaluated(key, value, valueVersion.equals(graphVersion)
+ ? EvaluationState.BUILT
+ : EvaluationState.CLEAN);
+ }
+ }
+ }
+
+ @Override
+ @ThreadCompatible
+ public <T extends SkyValue> EvaluationResult<T> eval(Iterable<SkyKey> skyKeys)
+ throws InterruptedException {
+ ImmutableSet<SkyKey> skyKeySet = ImmutableSet.copyOf(skyKeys);
+
+ // Optimization: if all required node values are already present in the cache, return them
+ // directly without launching the heavy machinery, spawning threads, etc.
+ // Inform progressReceiver that these nodes are done to be consistent with the main code path.
+ if (Iterables.all(skyKeySet, nodeEntryIsDone)) {
+ for (SkyKey skyKey : skyKeySet) {
+ informProgressReceiverThatValueIsDone(skyKey);
+ }
+ // Note that the 'catastrophe' parameter doesn't really matter here (it's only used for
+ // sanity checking).
+ return constructResult(null, skyKeySet, null, /*catastrophe=*/false);
+ }
+
+ if (!keepGoing) {
+ Set<SkyKey> cachedErrorKeys = new HashSet<>();
+ for (SkyKey skyKey : skyKeySet) {
+ NodeEntry entry = graph.get(skyKey);
+ if (entry == null) {
+ continue;
+ }
+ if (entry.isDone() && entry.getErrorInfo() != null) {
+ informProgressReceiverThatValueIsDone(skyKey);
+ cachedErrorKeys.add(skyKey);
+ }
+ }
+
+ // Errors, even cached ones, should halt evaluations not in keepGoing mode.
+ if (!cachedErrorKeys.isEmpty()) {
+ // Note that the 'catastrophe' parameter doesn't really matter here (it's only used for
+ // sanity checking).
+ return constructResult(null, cachedErrorKeys, null, /*catastrophe=*/false);
+ }
+ }
+
+ // We delay this check until we know that some kind of evaluation is necessary, since !keepGoing
+ // and !keepsEdges are incompatible only in the case of a failed evaluation -- there is no
+ // need to be overly harsh to callers who are just trying to retrieve a cached result.
+ Preconditions.checkState(keepGoing || !(graph instanceof InMemoryGraph)
+ || ((InMemoryGraph) graph).keepsEdges(),
+ "nokeep_going evaluations are not allowed if graph edges are not kept: %s", skyKeys);
+
+ Profiler.instance().startTask(ProfilerTask.SKYFRAME_EVAL, skyKeySet);
+ try {
+ return eval(skyKeySet, new ValueVisitor(threadCount));
+ } finally {
+ Profiler.instance().completeTask(ProfilerTask.SKYFRAME_EVAL);
+ }
+ }
+
+ @ThreadCompatible
+ private <T extends SkyValue> EvaluationResult<T> eval(ImmutableSet<SkyKey> skyKeys,
+ ValueVisitor visitor) throws InterruptedException {
+ // We unconditionally add the ErrorTransienceValue here, to ensure that it will be created, and
+ // in the graph, by the time that it is needed. Creating it on demand in a parallel context sets
+ // up a race condition, because there is no way to atomically create a node and set its value.
+ NodeEntry errorTransienceEntry = graph.createIfAbsent(ErrorTransienceValue.key());
+ DependencyState triState = errorTransienceEntry.addReverseDepAndCheckIfDone(null);
+ Preconditions.checkState(triState != DependencyState.ADDED_DEP,
+ "%s %s", errorTransienceEntry, triState);
+ if (triState != DependencyState.DONE) {
+ errorTransienceEntry.setValue(new ErrorTransienceValue(), graphVersion);
+ // The error transience entry is always invalidated by the RecordingDifferencer.
+ // Now that the entry's value is set, it is no longer dirty.
+ dirtyKeyTracker.notDirty(ErrorTransienceValue.key());
+
+ Preconditions.checkState(
+ errorTransienceEntry.addReverseDepAndCheckIfDone(null) != DependencyState.ADDED_DEP,
+ errorTransienceEntry);
+ }
+ for (SkyKey skyKey : skyKeys) {
+ NodeEntry entry = graph.createIfAbsent(skyKey);
+ // This must be equivalent to the code in enqueueChild above, in order to be thread-safe.
+ switch (entry.addReverseDepAndCheckIfDone(null)) {
+ case NEEDS_SCHEDULING:
+ visitor.enqueueEvaluation(skyKey);
+ break;
+ case DONE:
+ informProgressReceiverThatValueIsDone(skyKey);
+ break;
+ case ADDED_DEP:
+ break;
+ default:
+ throw new IllegalStateException(entry + " for " + skyKey + " in unknown state");
+ }
+ }
+ try {
+ return waitForCompletionAndConstructResult(visitor, skyKeys);
+ } finally {
+ // TODO(bazel-team): In nokeep_going mode or in case of an interrupt, we need to remove
+ // partial values from the graph. Find a better way to handle those cases.
+ clean(visitor.inflightNodes);
+ }
+ }
+
+ private void clean(Set<SkyKey> inflightNodes) throws InterruptedException {
+ boolean alreadyInterrupted = Thread.interrupted();
+ // This parallel computation is fully cpu-bound, so we use a thread for each processor.
+ ExecutorService executor = Executors.newFixedThreadPool(
+ Runtime.getRuntime().availableProcessors(),
+ new ThreadFactoryBuilder().setNameFormat("ParallelEvaluator#clean %d").build());
+ ThrowableRecordingRunnableWrapper wrapper =
+ new ThrowableRecordingRunnableWrapper("ParallelEvaluator#clean");
+ for (final SkyKey key : inflightNodes) {
+ final NodeEntry entry = graph.get(key);
+ if (entry.isDone()) {
+ // Entry may be done in case of a RuntimeException or other programming bug. Do nothing,
+ // since (a) we're about to crash anyway, and (b) getTemporaryDirectDeps cannot be called
+ // on a done node, so the call below would crash, which would mask the actual exception
+ // that caused this state.
+ continue;
+ }
+ executor.execute(wrapper.wrap(new Runnable() {
+ @Override
+ public void run() {
+ cleanInflightNode(key, entry);
+ }
+ }));
+ }
+ // We uninterruptibly wait for all nodes to be cleaned because we want to make sure the graph
+ // is left in a good state.
+ //
+ // TODO(bazel-team): Come up with a better design for graph cleaning such that we can respond
+ // to interrupts in constant time.
+ boolean newlyInterrupted = ExecutorShutdownUtil.uninterruptibleShutdown(executor);
+ Throwables.propagateIfPossible(wrapper.getFirstThrownError());
+ if (newlyInterrupted || alreadyInterrupted) {
+ throw new InterruptedException();
+ }
+ }
+
+ private void cleanInflightNode(SkyKey key, NodeEntry entry) {
+ Set<SkyKey> temporaryDeps = entry.getTemporaryDirectDeps();
+ graph.remove(key);
+ for (SkyKey dep : temporaryDeps) {
+ NodeEntry nodeEntry = graph.get(dep);
+ // The direct dep might have already been cleaned from the graph.
+ if (nodeEntry != null) {
+ // Only bother removing the reverse dep on done nodes since other in-flight nodes will be
+ // cleaned too.
+ if (nodeEntry.isDone()) {
+ nodeEntry.removeReverseDep(key);
+ }
+ }
+ }
+ }
+
+ private <T extends SkyValue> EvaluationResult<T> waitForCompletionAndConstructResult(
+ ValueVisitor visitor, Iterable<SkyKey> skyKeys) throws InterruptedException {
+ Map<SkyKey, ValueWithMetadata> bubbleErrorInfo = null;
+ boolean catastrophe = false;
+ try {
+ visitor.waitForCompletion();
+ } catch (final SchedulerException e) {
+ Throwables.propagateIfPossible(e.getCause(), InterruptedException.class);
+ if (Thread.interrupted()) {
+ // As per the contract of AbstractQueueVisitor#work, if an unchecked exception is thrown and
+ // the build is interrupted, the thrown exception is what will be rethrown. Since the user
+ // presumably wanted to interrupt the build, we ignore the thrown SchedulerException (which
+ // doesn't indicate a programming bug) and throw an InterruptedException.
+ throw new InterruptedException();
+ }
+
+ SkyKey errorKey = Preconditions.checkNotNull(e.getFailedValue(), e);
+ // ErrorInfo could only be null if SchedulerException wrapped an InterruptedException, but
+ // that should have been propagated.
+ ErrorInfo errorInfo = Preconditions.checkNotNull(e.getErrorInfo(), errorKey);
+ catastrophe = errorInfo.isCatastrophic();
+ if (!catastrophe || !keepGoing) {
+ bubbleErrorInfo = bubbleErrorUp(errorInfo, errorKey, skyKeys, visitor);
+ } else {
+ // Bubbling the error up requires that graph edges are present for done nodes. This is not
+ // always the case in a keepGoing evaluation, since it is assumed that done nodes do not
+ // need to be traversed. In this case, we hope the caller is tolerant of a possibly empty
+ // result, and return prematurely.
+ bubbleErrorInfo = ImmutableMap.of(errorKey, graph.get(errorKey).getValueWithMetadata());
+ }
+ }
+
+ // Successful evaluation, either because keepGoing or because we actually did succeed.
+ // TODO(bazel-team): Maybe report root causes during the build for lower latency.
+ return constructResult(visitor, skyKeys, bubbleErrorInfo, catastrophe);
+ }
+
+ /**
+ * Walk up graph to find a top-level node (without parents) that wanted this failure. Store
+ * the failed nodes along the way in a map, with ErrorInfos that are appropriate for that layer.
+ * Example:
+ * foo bar
+ * \ /
+ * unrequested baz
+ * \ |
+ * failed-node
+ * User requests foo, bar. When failed-node fails, we look at its parents. unrequested is not
+ * in-flight, so we replace failed-node by baz and repeat. We look at baz's parents. foo is
+ * in-flight, so we replace baz by foo. Since foo is a top-level node and doesn't have parents,
+ * we then break, since we know a top-level node, foo, that depended on the failed node.
+ *
+ * There's the potential for a weird "track jump" here in the case:
+ * foo
+ * / \
+ * fail1 fail2
+ * If fail1 and fail2 fail simultaneously, fail2 may start propagating up in the loop below.
+ * However, foo requests fail1 first, and then throws an exception based on that. This is not
+ * incorrect, but may be unexpected.
+ *
+ * <p>Returns a map of errors that have been constructed during the bubbling up, so that the
+ * appropriate error can be returned to the caller, even though that error was not written to the
+ * graph. If a cycle is detected during the bubbling, this method aborts and returns null so that
+ * the normal cycle detection can handle the cycle.
+ *
+ * <p>Note that we are not propagating error to the first top-level node but to the highest one,
+ * because during this process we can add useful information about error from other nodes.
+ */
+ private Map<SkyKey, ValueWithMetadata> bubbleErrorUp(final ErrorInfo leafFailure,
+ SkyKey errorKey, Iterable<SkyKey> skyKeys, ValueVisitor visitor) {
+ Set<SkyKey> rootValues = ImmutableSet.copyOf(skyKeys);
+ ErrorInfo error = leafFailure;
+ Map<SkyKey, ValueWithMetadata> bubbleErrorInfo = new HashMap<>();
+ boolean externalInterrupt = false;
+ while (true) {
+ NodeEntry errorEntry = graph.get(errorKey);
+ Iterable<SkyKey> reverseDeps = errorEntry.isDone()
+ ? errorEntry.getReverseDeps()
+ : errorEntry.getInProgressReverseDeps();
+ // We should break from loop only when node doesn't have any parents.
+ if (Iterables.isEmpty(reverseDeps)) {
+ Preconditions.checkState(rootValues.contains(errorKey),
+ "Current key %s has to be a top-level key: %s", errorKey, rootValues);
+ break;
+ }
+ SkyKey parent = null;
+ NodeEntry parentEntry = null;
+ for (SkyKey bubbleParent : reverseDeps) {
+ if (bubbleErrorInfo.containsKey(bubbleParent)) {
+ // We are in a cycle. Don't try to bubble anything up -- cycle detection will kick in.
+ return null;
+ }
+ NodeEntry bubbleParentEntry = Preconditions.checkNotNull(graph.get(bubbleParent),
+ "parent %s of %s not in graph", bubbleParent, errorKey);
+ // Might be the parent that requested the error.
+ if (bubbleParentEntry.isDone()) {
+ // This parent is cached from a previous evaluate call. We shouldn't bubble up to it
+ // since any error message produced won't be meaningful to this evaluate call.
+ // The child error must also be cached from a previous build.
+ Preconditions.checkState(errorEntry.isDone(), "%s %s", errorEntry, bubbleParentEntry);
+ Version parentVersion = bubbleParentEntry.getVersion();
+ Version childVersion = errorEntry.getVersion();
+ Preconditions.checkState(childVersion.atMost(graphVersion)
+ && !childVersion.equals(graphVersion),
+ "child entry is not older than the current graph version, but had a done parent. "
+ + "child: %s childEntry: %s, childVersion: %s"
+ + "bubbleParent: %s bubbleParentEntry: %s, parentVersion: %s, graphVersion: %s",
+ errorKey, errorEntry, childVersion,
+ bubbleParent, bubbleParentEntry, parentVersion, graphVersion);
+ Preconditions.checkState(parentVersion.atMost(graphVersion)
+ && !parentVersion.equals(graphVersion),
+ "parent entry is not older than the current graph version. "
+ + "child: %s childEntry: %s, childVersion: %s"
+ + "bubbleParent: %s bubbleParentEntry: %s, parentVersion: %s, graphVersion: %s",
+ errorKey, errorEntry, childVersion,
+ bubbleParent, bubbleParentEntry, parentVersion, graphVersion);
+ continue;
+ }
+ // Arbitrarily pick the first in-flight parent.
+ Preconditions.checkState(visitor.isInflight(bubbleParent),
+ "errorKey: %s, errorEntry: %s, bubbleParent: %s, bubbleParentEntry: %s", errorKey,
+ errorEntry, bubbleParent, bubbleParentEntry);
+ parent = bubbleParent;
+ parentEntry = bubbleParentEntry;
+ break;
+ }
+ Preconditions.checkNotNull(parent, "", errorKey, bubbleErrorInfo);
+ errorKey = parent;
+ SkyFunction factory = skyFunctions.get(parent.functionName());
+ if (parentEntry.isDirty()) {
+ switch (parentEntry.getDirtyState()) {
+ case CHECK_DEPENDENCIES:
+ // If this value's child was bubbled up to, it did not signal this value, and so we must
+ // manually make it ready to build.
+ parentEntry.signalDep();
+ // Fall through to REBUILDING, since state is now REBUILDING.
+ case REBUILDING:
+ // Nothing to be done.
+ break;
+ default:
+ throw new AssertionError(parent + " not in valid dirty state: " + parentEntry);
+ }
+ }
+ SkyFunctionEnvironment env =
+ new SkyFunctionEnvironment(parent, parentEntry.getTemporaryDirectDeps(),
+ bubbleErrorInfo, visitor);
+ externalInterrupt = externalInterrupt || Thread.currentThread().isInterrupted();
+ try {
+ // This build is only to check if the parent node can give us a better error. We don't
+ // care about a return value.
+ factory.compute(parent, env);
+ } catch (SkyFunctionException builderException) {
+ ReifiedSkyFunctionException reifiedBuilderException =
+ new ReifiedSkyFunctionException(builderException, parent);
+ if (reifiedBuilderException.getRootCauseSkyKey().equals(parent)) {
+ error = new ErrorInfo(reifiedBuilderException);
+ bubbleErrorInfo.put(errorKey,
+ ValueWithMetadata.error(new ErrorInfo(errorKey, ImmutableSet.of(error)),
+ env.buildEvents(/*missingChildren=*/true)));
+ continue;
+ }
+ } catch (InterruptedException interruptedException) {
+ // Do nothing.
+ // This throw happens if the builder requested the failed node, and then checked the
+ // interrupted state later -- getValueOrThrow sets the interrupted bit after the failed
+ // value is requested, to prevent the builder from doing too much work.
+ } finally {
+ // Clear interrupted status. We're not listening to interrupts here.
+ Thread.interrupted();
+ }
+ // Builder didn't throw an exception, so just propagate this one up.
+ bubbleErrorInfo.put(errorKey,
+ ValueWithMetadata.error(new ErrorInfo(errorKey, ImmutableSet.of(error)),
+ env.buildEvents(/*missingChildren=*/true)));
+ }
+
+ // Reset the interrupt bit if there was an interrupt from outside this evaluator interrupt.
+ // Note that there are internal interrupts set in the node builder environment if an error
+ // bubbling node calls getValueOrThrow() on a node in error.
+ if (externalInterrupt) {
+ Thread.currentThread().interrupt();
+ }
+ return bubbleErrorInfo;
+ }
+
+ /**
+ * Constructs an {@link EvaluationResult} from the {@link #graph}. Looks for cycles if there
+ * are unfinished nodes but no error was already found through bubbling up
+ * (as indicated by {@code bubbleErrorInfo} being null).
+ *
+ * <p>{@code visitor} may be null, but only in the case where all graph entries corresponding to
+ * {@code skyKeys} are known to be in the DONE state ({@code entry.isDone()} returns true).
+ */
+ private <T extends SkyValue> EvaluationResult<T> constructResult(
+ @Nullable ValueVisitor visitor, Iterable<SkyKey> skyKeys,
+ Map<SkyKey, ValueWithMetadata> bubbleErrorInfo, boolean catastrophe) {
+ Preconditions.checkState(!keepGoing || catastrophe || bubbleErrorInfo == null,
+ "", skyKeys, bubbleErrorInfo);
+ EvaluationResult.Builder<T> result = EvaluationResult.builder();
+ List<SkyKey> cycleRoots = new ArrayList<>();
+ boolean hasError = false;
+ for (SkyKey skyKey : skyKeys) {
+ ValueWithMetadata valueWithMetadata = getValueMaybeFromError(skyKey, bubbleErrorInfo);
+ // Cycle checking: if there is a cycle, evaluation cannot progress, therefore,
+ // the final values will not be in DONE state when the work runs out.
+ if (valueWithMetadata == null) {
+ // Don't look for cycles if the build failed for a known reason.
+ if (bubbleErrorInfo == null) {
+ cycleRoots.add(skyKey);
+ }
+ hasError = true;
+ continue;
+ }
+ SkyValue value = valueWithMetadata.getValue();
+ // TODO(bazel-team): Verify that message replay is fast and works in failure
+ // modes [skyframe-core]
+ // Note that replaying events here is only necessary on null builds, because otherwise we
+ // would have already printed the transitive messages after building these values.
+ replayingNestedSetEventVisitor.visit(valueWithMetadata.getTransitiveEvents());
+ ErrorInfo errorInfo = valueWithMetadata.getErrorInfo();
+ Preconditions.checkState(value != null || errorInfo != null, skyKey);
+ hasError = hasError || (errorInfo != null);
+ if (!keepGoing && errorInfo != null) {
+ // value will be null here unless the value was already built on a prior keepGoing build.
+ result.addError(skyKey, errorInfo);
+ continue;
+ }
+ if (value == null) {
+ // Note that we must be in the keepGoing case. Only make this value an error if it doesn't
+ // have a value. The error shouldn't matter to the caller since the value succeeded after a
+ // fashion.
+ result.addError(skyKey, errorInfo);
+ } else {
+ result.addResult(skyKey, value);
+ }
+ }
+ if (!cycleRoots.isEmpty()) {
+ Preconditions.checkState(visitor != null, skyKeys);
+ checkForCycles(cycleRoots, result, visitor, keepGoing);
+ }
+ Preconditions.checkState(bubbleErrorInfo == null || hasError,
+ "If an error bubbled up, some top-level node must be in error", bubbleErrorInfo, skyKeys);
+ result.setHasError(hasError);
+ return result.build();
+ }
+
+ private <T extends SkyValue> void checkForCycles(
+ Iterable<SkyKey> badRoots, EvaluationResult.Builder<T> result, final ValueVisitor visitor,
+ boolean keepGoing) {
+ for (SkyKey root : badRoots) {
+ ErrorInfo errorInfo = checkForCycles(root, visitor, keepGoing);
+ if (errorInfo == null) {
+ // This node just wasn't finished when evaluation aborted -- there were no cycles below it.
+ Preconditions.checkState(!keepGoing, "", root, badRoots);
+ continue;
+ }
+ Preconditions.checkState(!Iterables.isEmpty(errorInfo.getCycleInfo()),
+ "%s was not evaluated, but was not part of a cycle", root);
+ result.addError(root, errorInfo);
+ if (!keepGoing) {
+ return;
+ }
+ }
+ }
+
+ /**
+ * Marker value that we push onto a stack before we push a node's children on. When the marker
+ * value is popped, we know that all the children are finished. We would use null instead, but
+ * ArrayDeque does not permit null elements.
+ */
+ private static final SkyKey CHILDREN_FINISHED =
+ new SkyKey(new SkyFunctionName("MARKER", false), "MARKER");
+
+ /** The max number of cycles we will report to the user for a given root, to avoid OOMing. */
+ private static final int MAX_CYCLES = 20;
+
+ /**
+ * The algorithm for this cycle detector is as follows. We visit the graph depth-first, keeping
+ * track of the path we are currently on. We skip any DONE nodes (they are transitively
+ * error-free). If we come to a node already on the path, we immediately construct a cycle. If
+ * we are in the noKeepGoing case, we return ErrorInfo with that cycle to the caller. Otherwise,
+ * we continue. Once all of a node's children are done, we construct an error value for it, based
+ * on those children. Finally, when the original root's node is constructed, we return its
+ * ErrorInfo.
+ */
+ private ErrorInfo checkForCycles(SkyKey root, ValueVisitor visitor, boolean keepGoing) {
+ // The number of cycles found. Do not keep on searching for more cycles after this many were
+ // found.
+ int cyclesFound = 0;
+ // The path through the graph currently being visited.
+ List<SkyKey> graphPath = new ArrayList<>();
+ // Set of nodes on the path, to avoid expensive searches through the path for cycles.
+ Set<SkyKey> pathSet = new HashSet<>();
+
+ // Maintain a stack explicitly instead of recursion to avoid stack overflows
+ // on extreme graphs (with long dependency chains).
+ Deque<SkyKey> toVisit = new ArrayDeque<>();
+
+ toVisit.push(root);
+
+ // The procedure for this check is as follows: we visit a node, push it onto the graph stack,
+ // push a marker value onto the toVisit stack, and then push all of its children onto the
+ // toVisit stack. Thus, when the marker node comes to the top of the toVisit stack, we have
+ // visited the downward transitive closure of the value. At that point, all of its children must
+ // be finished, and so we can build the definitive error info for the node, popping it off the
+ // graph stack.
+ while (!toVisit.isEmpty()) {
+ SkyKey key = toVisit.pop();
+ NodeEntry entry = graph.get(key);
+
+ if (key == CHILDREN_FINISHED) {
+ // A marker node means we are done with all children of a node. Since all nodes have
+ // errors, we must have found errors in the children when that happens.
+ key = graphPath.remove(graphPath.size() - 1);
+ entry = graph.get(key);
+ pathSet.remove(key);
+ // Skip this node if it was first/last node of a cycle, and so has already been processed.
+ if (entry.isDone()) {
+ continue;
+ }
+ if (!keepGoing) {
+ // in the --nokeep_going mode, we would have already returned if we'd found a cycle below
+ // this node. The fact that we haven't means that there were no cycles below this node
+ // -- it just hadn't finished evaluating. So skip it.
+ continue;
+ }
+ if (cyclesFound < MAX_CYCLES) {
+ // Value must be ready, because all of its children have finished, so we can build its
+ // error.
+ Preconditions.checkState(entry.isReady(), "%s not ready. ValueEntry: %s", key, entry);
+ } else if (!entry.isReady()) {
+ removeIncompleteChildrenForCycle(key, entry, entry.getTemporaryDirectDeps());
+ }
+ Set<SkyKey> directDeps = entry.getTemporaryDirectDeps();
+ // Find out which children have errors. Similar logic to that in Evaluate#run().
+ List<ErrorInfo> errorDeps = getChildrenErrorsForCycle(directDeps);
+ Preconditions.checkState(!errorDeps.isEmpty(),
+ "Value %s was not successfully evaluated, but had no child errors. ValueEntry: %s", key,
+ entry);
+ SkyFunctionEnvironment env = new SkyFunctionEnvironment(key, directDeps, visitor);
+ env.setError(new ErrorInfo(key, errorDeps));
+ env.commit(/*enqueueParents=*/false);
+ }
+
+ // Nothing to be done for this node if it already has an entry.
+ if (entry.isDone()) {
+ continue;
+ }
+ if (cyclesFound == MAX_CYCLES) {
+ // Do not keep on searching for cycles indefinitely, to avoid excessive runtime/OOMs.
+ continue;
+ }
+
+ if (pathSet.contains(key)) {
+ int cycleStart = graphPath.indexOf(key);
+ // Found a cycle!
+ cyclesFound++;
+ Iterable<SkyKey> cycle = graphPath.subList(cycleStart, graphPath.size());
+ // Put this node into a consistent state for building if it is dirty.
+ if (entry.isDirty() && entry.getDirtyState() == DirtyState.CHECK_DEPENDENCIES) {
+ // In the check deps state, entry has exactly one child not done yet. Note that this node
+ // must be part of the path to the cycle we have found (since done nodes cannot be in
+ // cycles, and this is the only missing one). Thus, it will not be removed below in
+ // removeDescendantsOfCycleValue, so it is safe here to signal that it is done.
+ entry.signalDep();
+ }
+ if (keepGoing) {
+ // Any children of this node that we haven't already visited are not worth visiting,
+ // since this node is about to be done. Thus, the only child worth visiting is the one in
+ // this cycle, the cycleChild (which may == key if this cycle is a self-edge).
+ SkyKey cycleChild = selectCycleChild(key, graphPath, cycleStart);
+ removeDescendantsOfCycleValue(key, entry, cycleChild, toVisit,
+ graphPath.size() - cycleStart);
+ ValueWithMetadata dummyValue = ValueWithMetadata.wrapWithMetadata(new SkyValue() {});
+
+
+ SkyFunctionEnvironment env =
+ new SkyFunctionEnvironment(key, entry.getTemporaryDirectDeps(),
+ ImmutableMap.of(cycleChild, dummyValue), visitor);
+
+ // Construct error info for this node. Get errors from children, which are all done
+ // except possibly for the cycleChild.
+ List<ErrorInfo> allErrors =
+ getChildrenErrors(entry.getTemporaryDirectDeps(), /*unfinishedChild=*/cycleChild);
+ CycleInfo cycleInfo = new CycleInfo(cycle);
+ // Add in this cycle.
+ allErrors.add(new ErrorInfo(cycleInfo));
+ env.setError(new ErrorInfo(key, allErrors));
+ env.commit(/*enqueueParents=*/false);
+ continue;
+ } else {
+ // We need to return right away in the noKeepGoing case, so construct the cycle (with the
+ // path) and return.
+ Preconditions.checkState(graphPath.get(0).equals(root),
+ "%s not reached from %s. ValueEntry: %s", key, root, entry);
+ return new ErrorInfo(new CycleInfo(graphPath.subList(0, cycleStart), cycle));
+ }
+ }
+
+ // This node is not yet known to be in a cycle. So process its children.
+ Iterable<? extends SkyKey> children = graph.get(key).getTemporaryDirectDeps();
+ if (Iterables.isEmpty(children)) {
+ continue;
+ }
+
+ // This marker flag will tell us when all this node's children have been processed.
+ toVisit.push(CHILDREN_FINISHED);
+ // This node is now part of the path through the graph.
+ graphPath.add(key);
+ pathSet.add(key);
+ for (SkyKey nextValue : children) {
+ toVisit.push(nextValue);
+ }
+ }
+ return keepGoing ? getAndCheckDone(root).getErrorInfo() : null;
+ }
+
+ /**
+ * Returns the child of this node that is in the cycle that was just found. If the cycle is a
+ * self-edge, returns the node itself.
+ */
+ private static SkyKey selectCycleChild(SkyKey key, List<SkyKey> graphPath, int cycleStart) {
+ return cycleStart + 1 == graphPath.size() ? key : graphPath.get(cycleStart + 1);
+ }
+
+ /**
+ * Get all the errors of child nodes. There must be at least one cycle amongst them.
+ *
+ * @param children child nodes to query for errors.
+ * @return List of ErrorInfos from all children that had errors.
+ */
+ private List<ErrorInfo> getChildrenErrorsForCycle(Iterable<SkyKey> children) {
+ List<ErrorInfo> allErrors = new ArrayList<>();
+ boolean foundCycle = false;
+ for (SkyKey child : children) {
+ ErrorInfo errorInfo = getAndCheckDone(child).getErrorInfo();
+ if (errorInfo != null) {
+ foundCycle |= !Iterables.isEmpty(errorInfo.getCycleInfo());
+ allErrors.add(errorInfo);
+ }
+ }
+ Preconditions.checkState(foundCycle, "", children, allErrors);
+ return allErrors;
+ }
+
+ /**
+ * Get all the errors of child nodes.
+ *
+ * @param children child nodes to query for errors.
+ * @param unfinishedChild child which is allowed to not be done.
+ * @return List of ErrorInfos from all children that had errors.
+ */
+ private List<ErrorInfo> getChildrenErrors(Iterable<SkyKey> children, SkyKey unfinishedChild) {
+ List<ErrorInfo> allErrors = new ArrayList<>();
+ for (SkyKey child : children) {
+ ErrorInfo errorInfo = getErrorMaybe(child, /*allowUnfinished=*/child.equals(unfinishedChild));
+ if (errorInfo != null) {
+ allErrors.add(errorInfo);
+ }
+ }
+ return allErrors;
+ }
+
+ @Nullable
+ private ErrorInfo getErrorMaybe(SkyKey key, boolean allowUnfinished) {
+ if (!allowUnfinished) {
+ return getAndCheckDone(key).getErrorInfo();
+ }
+ NodeEntry entry = Preconditions.checkNotNull(graph.get(key), key);
+ return entry.isDone() ? entry.getErrorInfo() : null;
+ }
+
+ /**
+ * Removes direct children of key from toVisit and from the entry itself, and makes the entry
+ * ready if necessary. We must do this because it would not make sense to try to build the
+ * children after building the entry. It would violate the invariant that a parent can only be
+ * built after its children are built; See bug "Precondition error while evaluating a Skyframe
+ * graph with a cycle".
+ *
+ * @param key SkyKey of node in a cycle.
+ * @param entry NodeEntry of node in a cycle.
+ * @param cycleChild direct child of key in the cycle, or key itself if the cycle is a self-edge.
+ * @param toVisit list of remaining nodes to visit by the cycle-checker.
+ * @param cycleLength the length of the cycle found.
+ */
+ private void removeDescendantsOfCycleValue(SkyKey key, NodeEntry entry,
+ @Nullable SkyKey cycleChild, Iterable<SkyKey> toVisit, int cycleLength) {
+ Set<SkyKey> unvisitedDeps = new HashSet<>(entry.getTemporaryDirectDeps());
+ unvisitedDeps.remove(cycleChild);
+ // Remove any children from this node that are not part of the cycle we just found. They are
+ // irrelevant to the node as it stands, and if they are deleted from the graph because they are
+ // not built by the end of cycle-checking, we would have dangling references.
+ removeIncompleteChildrenForCycle(key, entry, unvisitedDeps);
+ if (!entry.isReady()) {
+ // The entry has at most one undone dep now, its cycleChild. Signal to make entry ready. Note
+ // that the entry can conceivably be ready if its cycleChild already found a different cycle
+ // and was built.
+ entry.signalDep();
+ }
+ Preconditions.checkState(entry.isReady(), "%s %s %s", key, cycleChild, entry);
+ Iterator<SkyKey> it = toVisit.iterator();
+ while (it.hasNext()) {
+ SkyKey descendant = it.next();
+ if (descendant == CHILDREN_FINISHED) {
+ // Marker value, delineating the end of a group of children that were enqueued.
+ cycleLength--;
+ if (cycleLength == 0) {
+ // We have seen #cycleLength-1 marker values, and have arrived at the one for this value,
+ // so we are done.
+ return;
+ }
+ continue; // Don't remove marker values.
+ }
+ if (cycleLength == 1) {
+ // Remove the direct children remaining to visit of the cycle node.
+ Preconditions.checkState(unvisitedDeps.contains(descendant),
+ "%s %s %s %s %s", key, descendant, cycleChild, unvisitedDeps, entry);
+ it.remove();
+ }
+ }
+ throw new IllegalStateException("There were not " + cycleLength + " groups of children in "
+ + toVisit + " when trying to remove children of " + key + " other than " + cycleChild);
+ }
+
+ private void removeIncompleteChildrenForCycle(SkyKey key, NodeEntry entry,
+ Iterable<SkyKey> children) {
+ Set<SkyKey> unfinishedDeps = new HashSet<>();
+ for (SkyKey child : children) {
+ if (removeIncompleteChild(key, child)) {
+ unfinishedDeps.add(child);
+ }
+ }
+ entry.removeUnfinishedDeps(unfinishedDeps);
+ }
+
+ private NodeEntry getAndCheckDone(SkyKey key) {
+ NodeEntry entry = graph.get(key);
+ Preconditions.checkNotNull(entry, key);
+ Preconditions.checkState(entry.isDone(), "%s %s", key, entry);
+ return entry;
+ }
+
+ private ValueWithMetadata getValueMaybeFromError(SkyKey key,
+ @Nullable Map<SkyKey, ValueWithMetadata> bubbleErrorInfo) {
+ SkyValue value = bubbleErrorInfo == null ? null : bubbleErrorInfo.get(key);
+ NodeEntry entry = graph.get(key);
+ if (value != null) {
+ Preconditions.checkNotNull(entry,
+ "Value cannot have error before evaluation started", key, value);
+ return ValueWithMetadata.wrapWithMetadata(value);
+ }
+ return isDoneForBuild(entry) ? entry.getValueWithMetadata() : null;
+ }
+
+ /**
+ * Return true if the entry does not need to be re-evaluated this build. The entry will need to
+ * be re-evaluated if it is not done, but also if it was not completely evaluated last build and
+ * this build is keepGoing.
+ */
+ private boolean isDoneForBuild(@Nullable NodeEntry entry) {
+ return entry != null && entry.isDone();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/skyframe/ProcessableGraph.java b/src/main/java/com/google/devtools/build/skyframe/ProcessableGraph.java
new file mode 100644
index 0000000..8bf8a38
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skyframe/ProcessableGraph.java
@@ -0,0 +1,24 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+/**
+ * A graph that is both Dirtiable (values can be deleted) and Evaluable (values can be added). All
+ * methods in this interface (as inherited from super-interfaces) should be thread-safe.
+ *
+ * <p>This class is not intended for direct use, and is only exposed as public for use in
+ * evaluation implementations outside of this package.
+ */
+public interface ProcessableGraph extends DirtiableGraph, EvaluableGraph {
+}
diff --git a/src/main/java/com/google/devtools/build/skyframe/QueryableGraph.java b/src/main/java/com/google/devtools/build/skyframe/QueryableGraph.java
new file mode 100644
index 0000000..e1cfc0a
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skyframe/QueryableGraph.java
@@ -0,0 +1,24 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+/**
+ * A graph that exposes its entries and structure, for use by classes that must traverse it.
+ */
+public interface QueryableGraph {
+ /**
+ * Returns the node with the given name, or {@code null} if the node does not exist.
+ */
+ NodeEntry get(SkyKey key);
+}
diff --git a/src/main/java/com/google/devtools/build/skyframe/RecordingDifferencer.java b/src/main/java/com/google/devtools/build/skyframe/RecordingDifferencer.java
new file mode 100644
index 0000000..3ebbf33
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skyframe/RecordingDifferencer.java
@@ -0,0 +1,76 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.concurrent.ThreadSafety;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A simple Differencer which just records the invalidated values it's been given.
+ */
+@ThreadSafety.ThreadCompatible
+public class RecordingDifferencer implements Differencer, Injectable {
+
+ private List<SkyKey> valuesToInvalidate;
+ private Map<SkyKey, SkyValue> valuesToInject;
+
+ public RecordingDifferencer() {
+ clear();
+ }
+
+ private void clear() {
+ valuesToInvalidate = new ArrayList<>();
+ valuesToInject = new HashMap<>();
+ }
+
+ @Override
+ public Diff getDiff(Version fromVersion, Version toVersion) {
+ Diff diff = new ImmutableDiff(valuesToInvalidate, valuesToInject);
+ clear();
+ return diff;
+ }
+
+ /**
+ * Store the given values for invalidation.
+ */
+ public void invalidate(Iterable<SkyKey> values) {
+ Iterables.addAll(valuesToInvalidate, values);
+ }
+
+ /**
+ * Invalidates the cached values of any values in error transiently.
+ *
+ * <p>If a future call to {@link MemoizingEvaluator#evaluate} requests a value that transitively
+ * depends on any value that was in an error state (or is one of these), they will be re-computed.
+ */
+ public void invalidateTransientErrors() {
+ // All transient error values have a dependency on the single global ERROR_TRANSIENCE value,
+ // so we only have to invalidate that one value to catch everything.
+ invalidate(ImmutableList.of(ErrorTransienceValue.key()));
+ }
+
+ /**
+ * Store the given values for injection.
+ */
+ @Override
+ public void inject(Map<SkyKey, ? extends SkyValue> values) {
+ valuesToInject.putAll(values);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/skyframe/ReverseDepsUtil.java b/src/main/java/com/google/devtools/build/skyframe/ReverseDepsUtil.java
new file mode 100644
index 0000000..13d8c4b
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skyframe/ReverseDepsUtil.java
@@ -0,0 +1,211 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A utility class that allows us to keep the reverse dependencies as an array list instead of a
+ * set. This is more memory-efficient. At the same time it allows us to group the removals and
+ * uniqueness checks so that it also performs well.
+ *
+ * <p>The reason of this class it to share non-trivial code between BuildingState and NodeEntry. We
+ * could simply make those two classes extend this class instead, but we would be less
+ * memory-efficient since object memory alignment does not cross classes ( you would have two memory
+ * alignments, one for the base class and one for the extended one).
+ */
+abstract class ReverseDepsUtil<T> {
+
+ static final int MAYBE_CHECK_THRESHOLD = 10;
+
+ abstract void setReverseDepsObject(T container, Object object);
+
+ abstract void setSingleReverseDep(T container, boolean singleObject);
+
+ abstract void setReverseDepsToRemove(T container, List<SkyKey> object);
+
+ abstract Object getReverseDepsObject(T container);
+
+ abstract boolean isSingleReverseDep(T container);
+
+ abstract List<SkyKey> getReverseDepsToRemove(T container);
+
+ /**
+ * We check that the reverse dependency is not already present. We only do that if reverseDeps is
+ * small, so that it does not impact performance.
+ */
+ void maybeCheckReverseDepNotPresent(T container, SkyKey reverseDep) {
+ if (isSingleReverseDep(container)) {
+ Preconditions.checkState(!getReverseDepsObject(container).equals(reverseDep),
+ "Reverse dep %s already present", reverseDep);
+ return;
+ }
+ @SuppressWarnings("unchecked")
+ List<SkyKey> asList = (List<SkyKey>) getReverseDepsObject(container);
+ if (asList.size() < MAYBE_CHECK_THRESHOLD) {
+ Preconditions.checkState(!asList.contains(reverseDep), "Reverse dep %s already present"
+ + " in %s", reverseDep, asList);
+ }
+ }
+
+ /**
+ * We use a memory-efficient trick to keep reverseDeps memory usage low. Edges in Bazel are
+ * dominant over the number of nodes.
+ *
+ * <p>Most of the nodes have zero or one reverse dep. That is why we use immutable versions of the
+ * lists for those cases. In case of the size being > 1 we switch to an ArrayList. That is because
+ * we also have a decent number of nodes for which the reverseDeps are huge (for example almost
+ * everything depends on BuildInfo node).
+ *
+ * <p>We also optimize for the case where we have only one dependency. In that case we keep the
+ * object directly instead of a wrapper list.
+ */
+ @SuppressWarnings("unchecked")
+ void addReverseDeps(T container, Collection<SkyKey> newReverseDeps) {
+ if (newReverseDeps.isEmpty()) {
+ return;
+ }
+ Object reverseDeps = getReverseDepsObject(container);
+ int reverseDepsSize = isSingleReverseDep(container) ? 1 : ((List<SkyKey>) reverseDeps).size();
+ int newSize = reverseDepsSize + newReverseDeps.size();
+ if (newSize == 1) {
+ overwriteReverseDepsWithObject(container, Iterables.getOnlyElement(newReverseDeps));
+ } else if (reverseDepsSize == 0) {
+ overwriteReverseDepsList(container, Lists.newArrayList(newReverseDeps));
+ } else if (reverseDepsSize == 1) {
+ List<SkyKey> newList = Lists.newArrayListWithExpectedSize(newSize);
+ newList.add((SkyKey) reverseDeps);
+ newList.addAll(newReverseDeps);
+ overwriteReverseDepsList(container, newList);
+ } else {
+ ((List<SkyKey>) reverseDeps).addAll(newReverseDeps);
+ }
+ }
+
+ /**
+ * See {@code addReverseDeps} method.
+ */
+ void removeReverseDep(T container, SkyKey reverseDep) {
+ if (isSingleReverseDep(container)) {
+ // This removal is cheap so let's do it and not keep it in reverseDepsToRemove.
+ // !equals should only happen in case of catastrophe.
+ if (getReverseDepsObject(container).equals(reverseDep)) {
+ overwriteReverseDepsList(container, ImmutableList.<SkyKey>of());
+ }
+ return;
+ }
+ @SuppressWarnings("unchecked")
+ List<SkyKey> reverseDepsAsList = (List<SkyKey>) getReverseDepsObject(container);
+ if (reverseDepsAsList.isEmpty()) {
+ return;
+ }
+ List<SkyKey> reverseDepsToRemove = getReverseDepsToRemove(container);
+ if (reverseDepsToRemove == null) {
+ reverseDepsToRemove = Lists.newArrayListWithExpectedSize(1);
+ setReverseDepsToRemove(container, reverseDepsToRemove);
+ }
+ reverseDepsToRemove.add(reverseDep);
+ }
+
+ ImmutableSet<SkyKey> getReverseDeps(T container) {
+ consolidateReverseDepsRemovals(container);
+
+ // TODO(bazel-team): Unfortunately, we need to make a copy here right now to be on the safe side
+ // wrt. thread-safety. The parents of a node get modified when any of the parents is deleted,
+ // and we can't handle that right now.
+ if (isSingleReverseDep(container)) {
+ return ImmutableSet.of((SkyKey) getReverseDepsObject(container));
+ } else {
+ @SuppressWarnings("unchecked")
+ List<SkyKey> reverseDeps = (List<SkyKey>) getReverseDepsObject(container);
+ ImmutableSet<SkyKey> set = ImmutableSet.copyOf(reverseDeps);
+ Preconditions.checkState(set.size() == reverseDeps.size(),
+ "Duplicate reverse deps present in %s: %s. %s", this, reverseDeps, container);
+ return set;
+ }
+ }
+
+ void consolidateReverseDepsRemovals(T container) {
+ List<SkyKey> reverseDepsToRemove = getReverseDepsToRemove(container);
+ Object reverseDeps = getReverseDepsObject(container);
+ if (reverseDepsToRemove == null) {
+ return;
+ }
+ Preconditions.checkState(!isSingleReverseDep(container),
+ "We do not use reverseDepsToRemove for single lists: %s", container);
+ // Should not happen, as we only create reverseDepsToRemove in case we have at least one
+ // reverse dep to remove.
+ Preconditions.checkState((!((List<?>) reverseDeps).isEmpty()),
+ "Could not remove %s elements from %s.\nReverse deps to remove: %s. %s",
+ reverseDepsToRemove.size(), reverseDeps, reverseDepsToRemove, container);
+
+ Set<SkyKey> toRemove = Sets.newHashSet(reverseDepsToRemove);
+ int expectedRemovals = toRemove.size();
+ Preconditions.checkState(expectedRemovals == reverseDepsToRemove.size(),
+ "A reverse dependency tried to remove itself twice: %s. %s", reverseDepsToRemove,
+ container);
+
+ @SuppressWarnings("unchecked")
+ List<SkyKey> reverseDepsAsList = (List<SkyKey>) reverseDeps;
+ List<SkyKey> newReverseDeps = Lists
+ .newArrayListWithExpectedSize(Math.max(0, reverseDepsAsList.size() - expectedRemovals));
+
+ for (SkyKey reverseDep : reverseDepsAsList) {
+ if (!toRemove.contains(reverseDep)) {
+ newReverseDeps.add(reverseDep);
+ }
+ }
+ Preconditions.checkState(newReverseDeps.size() == reverseDepsAsList.size() - expectedRemovals,
+ "Could not remove some elements from %s.\nReverse deps to remove: %s. %s", reverseDeps,
+ toRemove, container);
+
+ if (newReverseDeps.isEmpty()) {
+ overwriteReverseDepsList(container, ImmutableList.<SkyKey>of());
+ } else if (newReverseDeps.size() == 1) {
+ overwriteReverseDepsWithObject(container, newReverseDeps.get(0));
+ } else {
+ overwriteReverseDepsList(container, newReverseDeps);
+ }
+ setReverseDepsToRemove(container, null);
+ }
+
+ @SuppressWarnings("deprecation")
+ String toString(T container) {
+ return Objects.toStringHelper("ReverseDeps") // MoreObjects is not in Guava
+ .add("reverseDeps", getReverseDepsObject(container))
+ .add("singleReverseDep", isSingleReverseDep(container))
+ .add("reverseDepsToRemove", getReverseDepsToRemove(container))
+ .toString();
+ }
+
+ private void overwriteReverseDepsWithObject(T container, SkyKey newObject) {
+ setReverseDepsObject(container, newObject);
+ setSingleReverseDep(container, true);
+ }
+
+ private void overwriteReverseDepsList(T container, List<SkyKey> list) {
+ setReverseDepsObject(container, list);
+ setSingleReverseDep(container, false);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/skyframe/Scheduler.java b/src/main/java/com/google/devtools/build/skyframe/Scheduler.java
new file mode 100644
index 0000000..f05860f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skyframe/Scheduler.java
@@ -0,0 +1,78 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Preconditions;
+
+import javax.annotation.Nullable;
+
+/**
+ * A work queue -- takes {@link Runnable}s and runs them when requested.
+ */
+interface Scheduler {
+ /**
+ * Schedules a new action to be eventually done.
+ */
+ void schedule(Runnable action);
+
+ /**
+ * Runs the actions that have been scheduled. These actions can in turn schedule new actions,
+ * which will be run as well.
+ *
+ * @throw SchedulerException wrapping a scheduled action's exception.
+ */
+ void run() throws SchedulerException;
+
+ /**
+ * Wrapper exception that {@link Runnable}s can throw, to be caught and handled
+ * by callers of {@link #run}.
+ */
+ static class SchedulerException extends RuntimeException {
+ private final SkyKey failedValue;
+ private final ErrorInfo errorInfo;
+
+ private SchedulerException(@Nullable Throwable cause, @Nullable ErrorInfo errorInfo,
+ SkyKey failedValue) {
+ super(errorInfo != null ? errorInfo.getException() : cause);
+ this.errorInfo = errorInfo;
+ this.failedValue = Preconditions.checkNotNull(failedValue, errorInfo);
+ }
+
+ /**
+ * Returns a SchedulerException wrapping an expected error, e.g. an error describing an expected
+ * build failure when trying to evaluate the given value, that should cause Skyframe to produce
+ * useful error information to the user.
+ */
+ static SchedulerException ofError(ErrorInfo errorInfo, SkyKey failedValue) {
+ Preconditions.checkNotNull(errorInfo);
+ return new SchedulerException(errorInfo.getException(), errorInfo, failedValue);
+ }
+
+ /**
+ * Returns a SchedulerException wrapping an InterruptedException, e.g. if the user interrupts
+ * the build, that should cause Skyframe to exit as soon as possible.
+ */
+ static SchedulerException ofInterruption(InterruptedException cause, SkyKey failedValue) {
+ return new SchedulerException(cause, null, failedValue);
+ }
+
+ SkyKey getFailedValue() {
+ return failedValue;
+ }
+
+ @Nullable ErrorInfo getErrorInfo() {
+ return errorInfo;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/skyframe/SequentialBuildDriver.java b/src/main/java/com/google/devtools/build/skyframe/SequentialBuildDriver.java
new file mode 100644
index 0000000..9b7f036
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skyframe/SequentialBuildDriver.java
@@ -0,0 +1,46 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.events.EventHandler;
+
+/**
+ * A driver for auto-updating graphs which operate over monotonically increasing integer versions.
+ */
+public class SequentialBuildDriver implements BuildDriver {
+ private final MemoizingEvaluator memoizingEvaluator;
+ private IntVersion curVersion;
+
+ public SequentialBuildDriver(MemoizingEvaluator evaluator) {
+ this.memoizingEvaluator = Preconditions.checkNotNull(evaluator);
+ this.curVersion = new IntVersion(0);
+ }
+
+ @Override
+ public <T extends SkyValue> EvaluationResult<T> evaluate(
+ Iterable<SkyKey> roots, boolean keepGoing, int numThreads, EventHandler reporter)
+ throws InterruptedException {
+ try {
+ return memoizingEvaluator.evaluate(roots, curVersion, keepGoing, numThreads, reporter);
+ } finally {
+ curVersion = curVersion.next();
+ }
+ }
+
+ @Override
+ public MemoizingEvaluator getGraphForTesting() {
+ return memoizingEvaluator;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/skyframe/SkyFunction.java b/src/main/java/com/google/devtools/build/skyframe/SkyFunction.java
new file mode 100644
index 0000000..324c03d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skyframe/SkyFunction.java
@@ -0,0 +1,187 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.devtools.build.lib.events.EventHandler;
+
+import java.util.Map;
+
+import javax.annotation.Nullable;
+
+/**
+ * Machinery to evaluate a single value.
+ *
+ * <p>The builder is supposed to access only direct dependencies of the value. However, the direct
+ * dependencies need not be known in advance. The builder can request arbitrary values using
+ * {@link Environment#getValue}. If the values are not ready, the call will return null; in that
+ * case the builder can either try to proceed (and potentially indicate more dependencies by
+ * additional {@code getValue} calls), or just return null, in which case the missing dependencies
+ * will be computed and the builder will be started again.
+ */
+public interface SkyFunction {
+
+ /**
+ * When a value is requested, this method is called with the name of the value and a value
+ * building environment.
+ *
+ * <p>This method should return a constructed value, or null if any dependencies were missing
+ * ({@link Environment#valuesMissing} was true before returning). In that case the missing
+ * dependencies will be computed and the value builder restarted.
+ *
+ * <p>Implementations must be threadsafe and reentrant.
+ *
+ * @throws SkyFunctionException on failure
+ * @throws InterruptedException when the user interrupts the build
+ */
+ @Nullable SkyValue compute(SkyKey skyKey, Environment env) throws SkyFunctionException,
+ InterruptedException;
+
+ /**
+ * Extracts a tag (target label) from a SkyKey if it has one. Otherwise return null.
+ *
+ * <p>The tag is used for filtering out non-error event messages that do not match --output_filter
+ * flag. If a SkyFunction returns null in this method it means that all the info/warning messages
+ * associated with this value will be shown, no matter what --output_filter says.
+ */
+ @Nullable
+ String extractTag(SkyKey skyKey);
+
+ /**
+ * The services provided to the value builder by the graph implementation.
+ */
+ interface Environment {
+ /**
+ * Returns a direct dependency. If the specified value is not in the set of already evaluated
+ * direct dependencies, returns null. Also returns null if the specified value has already been
+ * evaluated and found to be in error.
+ *
+ * <p>On a subsequent build, if any of this value's dependencies have changed they will be
+ * re-evaluated in the same order as originally requested by the {@code SkyFunction} using
+ * this {@code getValue} call (see {@link #getValues} for when preserving the order is not
+ * important).
+ */
+ @Nullable
+ SkyValue getValue(SkyKey valueName);
+
+ /**
+ * Returns a direct dependency. If the specified value is not in the set of already evaluated
+ * direct dependencies, returns null. If the specified value has already been evaluated and
+ * found to be in error, throws the exception coming from the error. Value builders may
+ * use this method to continue evaluation even if one of their children is in error by catching
+ * the thrown exception and proceeding. The caller must specify the exception that might be
+ * thrown using the {@code exceptionClass} argument. If the child's exception is not an instance
+ * of {@code exceptionClass}, returns null without throwing.
+ *
+ * <p>The exception class given cannot be a supertype or a subtype of {@link RuntimeException},
+ * or a subtype of {@link InterruptedException}. See
+ * {@link SkyFunctionException#validateExceptionType} for details.
+ */
+ @Nullable
+ <E extends Exception> SkyValue getValueOrThrow(SkyKey depKey, Class<E> exceptionClass) throws E;
+ @Nullable
+ <E1 extends Exception, E2 extends Exception> SkyValue getValueOrThrow(SkyKey depKey,
+ Class<E1> exceptionClass1, Class<E2> exceptionClass2) throws E1, E2;
+ @Nullable
+ <E1 extends Exception, E2 extends Exception, E3 extends Exception> SkyValue getValueOrThrow(
+ SkyKey depKey, Class<E1> exceptionClass1, Class<E2> exceptionClass2,
+ Class<E3> exceptionClass3) throws E1, E2, E3;
+ @Nullable
+ <E1 extends Exception, E2 extends Exception, E3 extends Exception, E4 extends Exception>
+ SkyValue getValueOrThrow(SkyKey depKey, Class<E1> exceptionClass1,
+ Class<E2> exceptionClass2, Class<E3> exceptionClass3, Class<E4> exceptionClass4)
+ throws E1, E2, E3, E4;
+
+ /**
+ * Returns true iff any of the past {@link #getValue}(s) or {@link #getValueOrThrow} method
+ * calls for this instance returned null (because the value was not yet present and done in the
+ * graph).
+ *
+ * <p>If this returns true, the {@link SkyFunction} must return {@code null}.
+ */
+ boolean valuesMissing();
+
+ /**
+ * Requests {@code depKeys} "in parallel", independent of each others' values. These keys may be
+ * thought of as a "dependency group" -- they are requested together by this value.
+ *
+ * <p>In general, if the result of one getValue call can affect the argument of a later getValue
+ * call, the two calls cannot be merged into a single getValues call, since the result of the
+ * first call might change on a later build. Inversely, if the result of one getValue call
+ * cannot affect the parameters of the next getValue call, the two keys can form a dependency
+ * group and the two getValue calls merged into one getValues call.
+ *
+ * <p>This means that on subsequent builds, when checking to see if a value requires rebuilding,
+ * all the values in this group may be simultaneously checked. A SkyFunction should request a
+ * dependency group if checking the deps serially on a subsequent build would take too long, and
+ * if the builder would request all deps anyway as long as no earlier deps had changed.
+ * SkyFunction.Environment implementations may also choose to request these deps in
+ * parallel on the first build, potentially speeding up the build.
+ *
+ * <p>While re-evaluating every value in the group may take longer than re-evaluating just the
+ * first one and finding that it has changed, no extra work is done: the contract of the
+ * dependency group means that the builder, when called to rebuild this value, will request all
+ * values in the group again anyway, so they would have to have been built in any case.
+ *
+ * <p>Example of when to use getValues: A ListProcessor value is built with key inputListRef.
+ * The builder first calls getValue(InputList.key(inputListRef)), and retrieves inputList. It
+ * then iterates through inputList, calling getValue on each input. Finally, it processes the
+ * whole list and returns. Say inputList is (a, b, c). Since the builder will unconditionally
+ * call getValue(a), getValue(b), and getValue(c), the builder can instead just call
+ * getValues({a, b, c}). If the value is later dirtied the evaluator will build a, b, and c in
+ * parallel (assuming the inputList value was unchanged), and re-evaluate the ListProcessor
+ * value only if at least one of them was changed. On the other hand, if the InputList changes
+ * to be (a, b, d), then the evaluator will see that the first dep has changed, and call the
+ * builder to rebuild from scratch, without considering the dep group of {a, b, c}.
+ *
+ * <p>Example of when not to use getValues: A BestMatch value is built with key
+ * <potentialMatchesRef, matchCriterion>. The builder first calls
+ * getValue(PotentialMatches.key(potentialMatchesRef) and retrieves potentialMatches. It then
+ * iterates through potentialMatches, calling getValue on each potential match until it finds
+ * one that satisfies matchCriterion. In this case, if potentialMatches is (a, b, c), it would
+ * be <i>incorrect</i> to call getValues({a, b, c}), because it is not known yet whether
+ * requesting b or c will be necessary -- if a matches, then we will never call b or c.
+ */
+ Map<SkyKey, SkyValue> getValues(Iterable<SkyKey> depKeys);
+
+ /**
+ * The same as {@link #getValues} but the returned objects may throw when attempting to retrieve
+ * their value. Note that even if the requested values can throw different kinds of exceptions,
+ * only exceptions of type {@code E} will be preserved in the returned objects. All others will
+ * be null.
+ */
+ <E extends Exception> Map<SkyKey, ValueOrException<E>> getValuesOrThrow(
+ Iterable<SkyKey> depKeys, Class<E> exceptionClass);
+ <E1 extends Exception, E2 extends Exception> Map<SkyKey, ValueOrException2<E1, E2>>
+ getValuesOrThrow(Iterable<SkyKey> depKeys, Class<E1> exceptionClass1,
+ Class<E2> exceptionClass2);
+ <E1 extends Exception, E2 extends Exception, E3 extends Exception>
+ Map<SkyKey, ValueOrException3<E1, E2, E3>> getValuesOrThrow(Iterable<SkyKey> depKeys,
+ Class<E1> exceptionClass1, Class<E2> exceptionClass2, Class<E3> exceptionClass3);
+ <E1 extends Exception, E2 extends Exception, E3 extends Exception, E4 extends Exception>
+ Map<SkyKey, ValueOrException4<E1, E2, E3, E4>> getValuesOrThrow(Iterable<SkyKey> depKeys,
+ Class<E1> exceptionClass1, Class<E2> exceptionClass2, Class<E3> exceptionClass3,
+ Class<E4> exceptionClass4);
+
+ /**
+ * Returns the {@link EventHandler} that a SkyFunction should use to print any errors,
+ * warnings, or progress messages while building.
+ */
+ EventHandler getListener();
+
+ /** Returns whether we are currently in error bubbling. */
+ @VisibleForTesting
+ boolean inErrorBubblingForTesting();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/skyframe/SkyFunctionException.java b/src/main/java/com/google/devtools/build/skyframe/SkyFunctionException.java
new file mode 100644
index 0000000..71b4710
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skyframe/SkyFunctionException.java
@@ -0,0 +1,133 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+
+import com.google.common.base.Preconditions;
+
+import javax.annotation.Nullable;
+
+/**
+ * Base class of exceptions thrown by {@link SkyFunction#compute} on failure.
+ *
+ * SkyFunctions should declare a subclass {@code C} of {@link SkyFunctionException} whose
+ * constructors forward fine-grained exception types (e.g. {@link IOException}) to
+ * {@link SkyFunctionException}'s constructor, and they should also declare
+ * {@link SkyFunction#compute} to throw {@code C}. This way the type system checks that no
+ * unexpected exceptions are thrown by the {@link SkyFunction}.
+ *
+ * <p>We took this approach over using a generic exception class since Java disallows it because of
+ * type erasure
+ * (see http://docs.oracle.com/javase/tutorial/java/generics/restrictions.html#cannotCatch).
+ *
+ * <p> Note that there are restrictions on what Exception types are allowed to be wrapped in this
+ * manner. See {@link SkyFunctionException#validateExceptionType}.
+ *
+ * <p>Failures are explicitly either transient or persistent. The transience of the failure from
+ * {@link SkyFunction#compute} should be influenced only by the computations done, and not by the
+ * transience of the failures from computations requested via
+ * {@link SkyFunction.Environment#getValueOrThrow}.
+ */
+public abstract class SkyFunctionException extends Exception {
+
+ /** The transience of the error. */
+ public enum Transience {
+ // An error that may or may not occur again if the computation were re-run. If a computation
+ // results in a transient error and is needed on a subsequent MemoizingEvaluator#evaluate call,
+ // it will be re-executed.
+ TRANSIENT,
+
+ // An error that is completely deterministic and persistent in terms of the computation's
+ // inputs. Persistent errors may be cached.
+ PERSISTENT;
+ }
+
+ private final Transience transience;
+ @Nullable
+ private final SkyKey rootCause;
+
+ public SkyFunctionException(Exception cause, Transience transience) {
+ this(cause, transience, null);
+ }
+
+ /** Used to rethrow a child error that the parent cannot handle. */
+ public SkyFunctionException(Exception cause, SkyKey childKey) {
+ this(cause, Transience.PERSISTENT, childKey);
+ }
+
+ private SkyFunctionException(Exception cause, Transience transience, SkyKey rootCause) {
+ super(Preconditions.checkNotNull(cause));
+ SkyFunctionException.validateExceptionType(cause.getClass());
+ this.transience = transience;
+ this.rootCause = rootCause;
+ }
+
+ @Nullable
+ final SkyKey getRootCauseSkyKey() {
+ return rootCause;
+ }
+
+ final boolean isTransient() {
+ return transience == Transience.TRANSIENT;
+ }
+
+ /**
+ * Catastrophic failures halt the build even when in keepGoing mode.
+ */
+ public boolean isCatastrophic() {
+ return false;
+ }
+
+ @Override
+ public Exception getCause() {
+ return (Exception) super.getCause();
+ }
+
+ static <E extends Throwable> void validateExceptionType(Class<E> exceptionClass) {
+ if (exceptionClass.equals(ValueOrExceptionUtils.BottomException.class)) {
+ return;
+ }
+
+ if (exceptionClass.isAssignableFrom(RuntimeException.class)) {
+ throw new IllegalStateException(exceptionClass.getSimpleName() + " is a supertype of "
+ + "RuntimeException. Don't do this since then you would potentially swallow all "
+ + "RuntimeExceptions, even those from Skyframe");
+ }
+ if (RuntimeException.class.isAssignableFrom(exceptionClass)) {
+ throw new IllegalStateException(exceptionClass.getSimpleName() + " is a subtype of "
+ + "RuntimeException. You should rewrite your code to use checked exceptions.");
+ }
+ if (InterruptedException.class.isAssignableFrom(exceptionClass)) {
+ throw new IllegalStateException(exceptionClass.getSimpleName() + " is a subtype of "
+ + "InterruptedException. Don't do this; Skyframe handles interrupts separately from the "
+ + "general SkyFunctionException mechanism.");
+ }
+ }
+
+ /** A {@link SkyFunctionException} with a definite root cause. */
+ static class ReifiedSkyFunctionException extends SkyFunctionException {
+ private final boolean isCatastrophic;
+
+ ReifiedSkyFunctionException(SkyFunctionException e, SkyKey key) {
+ super(e.getCause(), e.transience, Preconditions.checkNotNull(e.getRootCauseSkyKey() == null
+ ? key : e.getRootCauseSkyKey()));
+ this.isCatastrophic = e.isCatastrophic();
+ }
+
+ @Override
+ public boolean isCatastrophic() {
+ return isCatastrophic;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/skyframe/SkyFunctionName.java b/src/main/java/com/google/devtools/build/skyframe/SkyFunctionName.java
new file mode 100644
index 0000000..389d4d8
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skyframe/SkyFunctionName.java
@@ -0,0 +1,90 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Predicate;
+
+import java.io.Serializable;
+import java.util.Set;
+
+/**
+ * An identifier for a {@code SkyFunction}.
+ */
+public final class SkyFunctionName implements Serializable {
+ public static SkyFunctionName computed(String name) {
+ return new SkyFunctionName(name, true);
+ }
+
+ private final String name;
+ private final boolean isComputed;
+
+ public SkyFunctionName(String name, boolean isComputed) {
+ this.name = name;
+ this.isComputed = isComputed;
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof SkyFunctionName)) {
+ return false;
+ }
+ SkyFunctionName other = (SkyFunctionName) obj;
+ return name.equals(other.name);
+ }
+
+ @Override
+ public int hashCode() {
+ return name.hashCode();
+ }
+
+ /**
+ * Returns whether the values of this type are computed. The computation of a computed value must
+ * be deterministic and may only access requested dependencies.
+ */
+ public boolean isComputed() {
+ return isComputed;
+ }
+
+ /**
+ * A predicate that returns true for {@link SkyKey}s that have the given {@link SkyFunctionName}.
+ */
+ public static Predicate<SkyKey> functionIs(final SkyFunctionName functionName) {
+ return new Predicate<SkyKey>() {
+ @Override
+ public boolean apply(SkyKey skyKey) {
+ return functionName.equals(skyKey.functionName());
+ }
+ };
+ }
+
+ /**
+ * A predicate that returns true for {@link SkyKey}s that have the given {@link SkyFunctionName}.
+ */
+ public static Predicate<SkyKey> functionIsIn(final Set<SkyFunctionName> functionNames) {
+ return new Predicate<SkyKey>() {
+ @Override
+ public boolean apply(SkyKey skyKey) {
+ return functionNames.contains(skyKey.functionName());
+ }
+ };
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/skyframe/SkyKey.java b/src/main/java/com/google/devtools/build/skyframe/SkyKey.java
new file mode 100644
index 0000000..cc1dd1f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skyframe/SkyKey.java
@@ -0,0 +1,86 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.base.Function;
+import com.google.common.base.Preconditions;
+
+import java.io.Serializable;
+
+/**
+ * A {@link SkyKey} is effectively a pair (type, name) that identifies a Skyframe value.
+ */
+public final class SkyKey implements Serializable {
+ private final SkyFunctionName functionName;
+
+ /**
+ * The name of the value.
+ *
+ * <p>This is deliberately an untyped Object so that we can use arbitrary value types (e.g.,
+ * Labels, PathFragments, BuildConfigurations, etc.) as value names without incurring
+ * serialization costs in the in-memory implementation of the graph.
+ */
+ private final Object argument;
+
+ /**
+ * Cache the hash code for this object. It might be expensive to compute.
+ */
+ private final int hashCode;
+
+ public SkyKey(SkyFunctionName functionName, Object valueName) {
+ this.functionName = Preconditions.checkNotNull(functionName);
+ this.argument = Preconditions.checkNotNull(valueName);
+ this.hashCode = 31 * functionName.hashCode() + argument.hashCode();
+ }
+
+ public SkyFunctionName functionName() {
+ return functionName;
+ }
+
+ public Object argument() {
+ return argument;
+ }
+
+ @Override
+ public String toString() {
+ return functionName + ":" + argument;
+ }
+
+ @Override
+ public int hashCode() {
+ return hashCode;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ SkyKey other = (SkyKey) obj;
+ return argument.equals(other.argument) && functionName.equals(other.functionName);
+ }
+
+ public static final Function<SkyKey, Object> NODE_NAME = new Function<SkyKey, Object>() {
+ @Override
+ public Object apply(SkyKey input) {
+ return input.argument();
+ }
+ };
+}
diff --git a/src/main/java/com/google/devtools/build/skyframe/SkyValue.java b/src/main/java/com/google/devtools/build/skyframe/SkyValue.java
new file mode 100644
index 0000000..7cfaa78
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skyframe/SkyValue.java
@@ -0,0 +1,22 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import java.io.Serializable;
+
+/**
+ * A return value of a {@code SkyFunction}.
+ */
+public interface SkyValue extends Serializable {
+}
diff --git a/src/main/java/com/google/devtools/build/skyframe/TaggedEvents.java b/src/main/java/com/google/devtools/build/skyframe/TaggedEvents.java
new file mode 100644
index 0000000..056175e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skyframe/TaggedEvents.java
@@ -0,0 +1,62 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.events.Event;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * A wrapper of {@link Event} that contains a tag of the label where the event was generated. This
+ * class allows us to tell where the events are coming from when we group all the tags in a
+ * NestedSet.
+ *
+ * <p>The only usage of this code for now is to be able to use --output_filter in Skyframe
+ *
+ * <p>This is intended only for use in alternative {@code MemoizingEvaluator} implementations.
+ */
+@Immutable
+public final class TaggedEvents {
+
+ @Nullable
+ private final String tag;
+ private final ImmutableCollection<Event> events;
+
+ TaggedEvents(@Nullable String tag, ImmutableCollection<Event> events) {
+
+ this.tag = tag;
+ this.events = events;
+ }
+
+ @Nullable
+ String getTag() {
+ return tag;
+ }
+
+ ImmutableCollection<Event> getEvents() {
+ return events;
+ }
+
+ /**
+ * Returns <i>some</i> moderately sane representation of the events. Should never be used in
+ * user-visible places, only for debugging and testing.
+ */
+ @Override
+ public String toString() {
+ return tag == null ? "<unknown>" : tag + ": " + Iterables.toString(events);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/skyframe/ValueOrException.java b/src/main/java/com/google/devtools/build/skyframe/ValueOrException.java
new file mode 100644
index 0000000..d682095
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skyframe/ValueOrException.java
@@ -0,0 +1,24 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import javax.annotation.Nullable;
+
+/** Wrapper for a value or the typed exception thrown when trying to compute it. */
+public abstract class ValueOrException<E extends Exception> extends ValueOrUntypedException {
+
+ /** Gets the stored value. Throws an exception if one was thrown when computing this value. */
+ @Nullable
+ public abstract SkyValue get() throws E;
+}
diff --git a/src/main/java/com/google/devtools/build/skyframe/ValueOrException2.java b/src/main/java/com/google/devtools/build/skyframe/ValueOrException2.java
new file mode 100644
index 0000000..deedbb1
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skyframe/ValueOrException2.java
@@ -0,0 +1,25 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import javax.annotation.Nullable;
+
+/** Wrapper for a value or the typed exception thrown when trying to compute it. */
+public abstract class ValueOrException2<E1 extends Exception, E2 extends Exception>
+ extends ValueOrUntypedException {
+
+ /** Gets the stored value. Throws an exception if one was thrown when computing this value. */
+ @Nullable
+ public abstract SkyValue get() throws E1, E2;
+}
diff --git a/src/main/java/com/google/devtools/build/skyframe/ValueOrException3.java b/src/main/java/com/google/devtools/build/skyframe/ValueOrException3.java
new file mode 100644
index 0000000..e737c55
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skyframe/ValueOrException3.java
@@ -0,0 +1,25 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import javax.annotation.Nullable;
+
+/** Wrapper for a value or the typed exception thrown when trying to compute it. */
+public abstract class ValueOrException3<E1 extends Exception, E2 extends Exception,
+ E3 extends Exception> extends ValueOrUntypedException {
+
+ /** Gets the stored value. Throws an exception if one was thrown when computing this value. */
+ @Nullable
+ public abstract SkyValue get() throws E1, E2, E3;
+}
diff --git a/src/main/java/com/google/devtools/build/skyframe/ValueOrException4.java b/src/main/java/com/google/devtools/build/skyframe/ValueOrException4.java
new file mode 100644
index 0000000..176f405
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skyframe/ValueOrException4.java
@@ -0,0 +1,25 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import javax.annotation.Nullable;
+
+/** Wrapper for a value or the typed exception thrown when trying to compute it. */
+public abstract class ValueOrException4<E1 extends Exception, E2 extends Exception,
+ E3 extends Exception, E4 extends Exception> extends ValueOrUntypedException {
+
+ /** Gets the stored value. Throws an exception if one was thrown when computing this value. */
+ @Nullable
+ public abstract SkyValue get() throws E1, E2, E3, E4;
+}
diff --git a/src/main/java/com/google/devtools/build/skyframe/ValueOrExceptionUtils.java b/src/main/java/com/google/devtools/build/skyframe/ValueOrExceptionUtils.java
new file mode 100644
index 0000000..e66f4fa
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skyframe/ValueOrExceptionUtils.java
@@ -0,0 +1,520 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import javax.annotation.Nullable;
+
+/** Utilities for producing and consuming ValueOrException(2|3|4)? instances. */
+class ValueOrExceptionUtils {
+
+ /** The bottom exception type. */
+ class BottomException extends Exception {
+ }
+
+ @Nullable
+ public static SkyValue downcovert(ValueOrException<BottomException> voe) {
+ return voe.getValue();
+ }
+
+ public static <E1 extends Exception> ValueOrException<E1> downcovert(
+ ValueOrException2<E1, BottomException> voe, Class<E1> exceptionClass1) {
+ Exception e = voe.getException();
+ if (e == null) {
+ return new ValueOrExceptionValueImpl<>(voe.getValue());
+ }
+ // Here and below, we use type-safe casts for performance reasons. Another approach would be
+ // cascading try-catch-rethrow blocks, but that has a higher performance penalty.
+ if (exceptionClass1.isInstance(e)) {
+ return new ValueOrExceptionExnImpl<>(exceptionClass1.cast(e));
+ }
+ throw new IllegalStateException("shouldn't reach here " + e.getClass() + " " + exceptionClass1,
+ e);
+ }
+
+ public static <E1 extends Exception, E2 extends Exception> ValueOrException2<E1, E2> downconvert(
+ ValueOrException3<E1, E2, BottomException> voe, Class<E1> exceptionClass1,
+ Class<E2> exceptionClass2) {
+ Exception e = voe.getException();
+ if (e == null) {
+ return new ValueOrException2ValueImpl<>(voe.getValue());
+ }
+ if (exceptionClass1.isInstance(e)) {
+ return new ValueOrException2Exn1Impl<>(exceptionClass1.cast(e));
+ }
+ if (exceptionClass2.isInstance(e)) {
+ return new ValueOrException2Exn2Impl<>(exceptionClass2.cast(e));
+ }
+ throw new IllegalStateException("shouldn't reach here " + e.getClass() + " " + exceptionClass1
+ + " " + exceptionClass2, e);
+ }
+
+ public static <E1 extends Exception, E2 extends Exception, E3 extends Exception>
+ ValueOrException3<E1, E2, E3> downconvert(ValueOrException4<E1, E2, E3, BottomException> voe,
+ Class<E1> exceptionClass1, Class<E2> exceptionClass2, Class<E3> exceptionClass3) {
+ Exception e = voe.getException();
+ if (e == null) {
+ return new ValueOrException3ValueImpl<>(voe.getValue());
+ }
+ if (exceptionClass1.isInstance(e)) {
+ return new ValueOrException3Exn1Impl<>(exceptionClass1.cast(e));
+ }
+ if (exceptionClass2.isInstance(e)) {
+ return new ValueOrException3Exn2Impl<>(exceptionClass2.cast(e));
+ }
+ if (exceptionClass3.isInstance(e)) {
+ return new ValueOrException3Exn3Impl<>(exceptionClass3.cast(e));
+ }
+ throw new IllegalStateException("shouldn't reach here " + e.getClass() + " " + exceptionClass1
+ + " " + exceptionClass2 + " " + exceptionClass3, e);
+ }
+
+ public static <E extends Exception> ValueOrException<E> ofNull() {
+ return ValueOrExceptionValueImpl.ofNull();
+ }
+
+ public static ValueOrUntypedException ofValueUntyped(SkyValue value) {
+ return new ValueOrUntypedExceptionImpl(value);
+ }
+
+ public static <E extends Exception> ValueOrException<E> ofExn(E e) {
+ return new ValueOrExceptionExnImpl<>(e);
+ }
+
+ public static <E1 extends Exception, E2 extends Exception, E3 extends Exception,
+ E4 extends Exception> ValueOrException4<E1, E2, E3, E4> ofNullValue() {
+ return ValueOrException4ValueImpl.ofNullValue();
+ }
+
+ public static <E1 extends Exception, E2 extends Exception, E3 extends Exception,
+ E4 extends Exception> ValueOrException4<E1, E2, E3, E4> ofValue(SkyValue value) {
+ return new ValueOrException4ValueImpl<>(value);
+ }
+
+ public static <E1 extends Exception, E2 extends Exception, E3 extends Exception,
+ E4 extends Exception> ValueOrException4<E1, E2, E3, E4> ofExn1(E1 e) {
+ return new ValueOrException4Exn1Impl<>(e);
+ }
+
+ public static <E1 extends Exception, E2 extends Exception, E3 extends Exception,
+ E4 extends Exception> ValueOrException4<E1, E2, E3, E4> ofExn2(E2 e) {
+ return new ValueOrException4Exn2Impl<>(e);
+ }
+
+ public static <E1 extends Exception, E2 extends Exception, E3 extends Exception,
+ E4 extends Exception> ValueOrException4<E1, E2, E3, E4> ofExn3(E3 e) {
+ return new ValueOrException4Exn3Impl<>(e);
+ }
+
+ public static <E1 extends Exception, E2 extends Exception, E3 extends Exception,
+ E4 extends Exception> ValueOrException4<E1, E2, E3, E4> ofExn4(E4 e) {
+ return new ValueOrException4Exn4Impl<>(e);
+ }
+
+ private static class ValueOrUntypedExceptionImpl extends ValueOrUntypedException {
+ @Nullable
+ private final SkyValue value;
+
+ ValueOrUntypedExceptionImpl(@Nullable SkyValue value) {
+ this.value = value;
+ }
+
+ @Override
+ @Nullable
+ public SkyValue getValue() {
+ return value;
+ }
+
+ @Override
+ public Exception getException() {
+ return null;
+ }
+ }
+
+ private static class ValueOrExceptionValueImpl<E extends Exception> extends ValueOrException<E> {
+ private static final ValueOrExceptionValueImpl<Exception> NULL =
+ new ValueOrExceptionValueImpl<Exception>((SkyValue) null);
+
+ @Nullable
+ private final SkyValue value;
+
+ private ValueOrExceptionValueImpl(@Nullable SkyValue value) {
+ this.value = value;
+ }
+
+ @Override
+ @Nullable
+ public SkyValue get() {
+ return value;
+ }
+
+ @Override
+ @Nullable
+ public SkyValue getValue() {
+ return value;
+ }
+
+ @Override
+ @Nullable
+ public Exception getException() {
+ return null;
+ }
+
+ @SuppressWarnings("unchecked")
+ public static <E extends Exception> ValueOrExceptionValueImpl<E> ofNull() {
+ return (ValueOrExceptionValueImpl<E>) NULL;
+ }
+ }
+
+ private static class ValueOrExceptionExnImpl<E extends Exception> extends ValueOrException<E> {
+ private final E e;
+
+ private ValueOrExceptionExnImpl(E e) {
+ this.e = e;
+ }
+
+ @Override
+ public SkyValue get() throws E {
+ throw e;
+ }
+
+ @Override
+ @Nullable
+ public SkyValue getValue() {
+ return null;
+ }
+
+ @Override
+ public Exception getException() {
+ return e;
+ }
+ }
+
+ private static class ValueOrException2ValueImpl<E1 extends Exception, E2 extends Exception>
+ extends ValueOrException2<E1, E2> {
+ @Nullable
+ private final SkyValue value;
+
+ ValueOrException2ValueImpl(@Nullable SkyValue value) {
+ this.value = value;
+ }
+
+ @Override
+ @Nullable
+ public SkyValue get() throws E1, E2 {
+ return value;
+ }
+
+ @Override
+ @Nullable
+ public Exception getException() {
+ return null;
+ }
+
+ @Override
+ @Nullable
+ public SkyValue getValue() {
+ return value;
+ }
+ }
+
+ private static class ValueOrException2Exn1Impl<E1 extends Exception, E2 extends Exception>
+ extends ValueOrException2<E1, E2> {
+ private final E1 e;
+
+ private ValueOrException2Exn1Impl(E1 e) {
+ this.e = e;
+ }
+
+ @Override
+ public SkyValue get() throws E1 {
+ throw e;
+ }
+
+ @Override
+ public Exception getException() {
+ return e;
+ }
+
+ @Override
+ @Nullable
+ public SkyValue getValue() {
+ return null;
+ }
+ }
+
+ private static class ValueOrException2Exn2Impl<E1 extends Exception, E2 extends Exception>
+ extends ValueOrException2<E1, E2> {
+ private final E2 e;
+
+ private ValueOrException2Exn2Impl(E2 e) {
+ this.e = e;
+ }
+
+ @Override
+ public SkyValue get() throws E2 {
+ throw e;
+ }
+
+ @Override
+ public Exception getException() {
+ return e;
+ }
+
+ @Override
+ @Nullable
+ public SkyValue getValue() {
+ return null;
+ }
+ }
+
+ private static class ValueOrException3ValueImpl<E1 extends Exception, E2 extends Exception,
+ E3 extends Exception> extends ValueOrException3<E1, E2, E3> {
+ @Nullable
+ private final SkyValue value;
+
+ ValueOrException3ValueImpl(@Nullable SkyValue value) {
+ this.value = value;
+ }
+
+ @Override
+ @Nullable
+ public SkyValue get() throws E1, E2 {
+ return value;
+ }
+
+ @Override
+ @Nullable
+ public Exception getException() {
+ return null;
+ }
+
+ @Override
+ @Nullable
+ public SkyValue getValue() {
+ return value;
+ }
+ }
+
+ private static class ValueOrException3Exn1Impl<E1 extends Exception, E2 extends Exception,
+ E3 extends Exception> extends ValueOrException3<E1, E2, E3> {
+ private final E1 e;
+
+ private ValueOrException3Exn1Impl(E1 e) {
+ this.e = e;
+ }
+
+ @Override
+ public SkyValue get() throws E1 {
+ throw e;
+ }
+
+ @Override
+ public Exception getException() {
+ return e;
+ }
+
+ @Override
+ @Nullable
+ public SkyValue getValue() {
+ return null;
+ }
+ }
+
+ private static class ValueOrException3Exn2Impl<E1 extends Exception, E2 extends Exception,
+ E3 extends Exception> extends ValueOrException3<E1, E2, E3> {
+ private final E2 e;
+
+ private ValueOrException3Exn2Impl(E2 e) {
+ this.e = e;
+ }
+
+ @Override
+ public SkyValue get() throws E2 {
+ throw e;
+ }
+
+ @Override
+ public Exception getException() {
+ return e;
+ }
+
+ @Override
+ @Nullable
+ public SkyValue getValue() {
+ return null;
+ }
+ }
+
+ private static class ValueOrException3Exn3Impl<E1 extends Exception, E2 extends Exception,
+ E3 extends Exception> extends ValueOrException3<E1, E2, E3> {
+ private final E3 e;
+
+ private ValueOrException3Exn3Impl(E3 e) {
+ this.e = e;
+ }
+
+ @Override
+ public SkyValue get() throws E3 {
+ throw e;
+ }
+
+ @Override
+ public Exception getException() {
+ return e;
+ }
+
+ @Override
+ @Nullable
+ public SkyValue getValue() {
+ return null;
+ }
+ }
+
+ private static class ValueOrException4ValueImpl<E1 extends Exception, E2 extends Exception,
+ E3 extends Exception, E4 extends Exception> extends ValueOrException4<E1, E2, E3, E4> {
+ private static final ValueOrException4ValueImpl<Exception, Exception, Exception,
+ Exception> NULL = new ValueOrException4ValueImpl<>((SkyValue) null);
+
+ @Nullable
+ private final SkyValue value;
+
+ ValueOrException4ValueImpl(@Nullable SkyValue value) {
+ this.value = value;
+ }
+
+ @Override
+ @Nullable
+ public SkyValue get() throws E1, E2 {
+ return value;
+ }
+
+ @Override
+ @Nullable
+ public Exception getException() {
+ return null;
+ }
+
+ @Override
+ @Nullable
+ public SkyValue getValue() {
+ return value;
+ }
+
+ @SuppressWarnings("unchecked")
+ private static <E1 extends Exception, E2 extends Exception, E3 extends Exception,
+ E4 extends Exception>ValueOrException4ValueImpl<E1, E2, E3, E4> ofNullValue() {
+ return (ValueOrException4ValueImpl<E1, E2, E3, E4>) NULL;
+ }
+ }
+
+ private static class ValueOrException4Exn1Impl<E1 extends Exception, E2 extends Exception,
+ E3 extends Exception, E4 extends Exception> extends ValueOrException4<E1, E2, E3, E4> {
+ private final E1 e;
+
+ private ValueOrException4Exn1Impl(E1 e) {
+ this.e = e;
+ }
+
+ @Override
+ public SkyValue get() throws E1 {
+ throw e;
+ }
+
+ @Override
+ public Exception getException() {
+ return e;
+ }
+
+ @Override
+ @Nullable
+ public SkyValue getValue() {
+ return null;
+ }
+ }
+
+ private static class ValueOrException4Exn2Impl<E1 extends Exception, E2 extends Exception,
+ E3 extends Exception, E4 extends Exception> extends ValueOrException4<E1, E2, E3, E4> {
+ private final E2 e;
+
+ private ValueOrException4Exn2Impl(E2 e) {
+ this.e = e;
+ }
+
+ @Override
+ public SkyValue get() throws E2 {
+ throw e;
+ }
+
+ @Override
+ public Exception getException() {
+ return e;
+ }
+
+ @Override
+ @Nullable
+ public SkyValue getValue() {
+ return null;
+ }
+ }
+
+ private static class ValueOrException4Exn3Impl<E1 extends Exception, E2 extends Exception,
+ E3 extends Exception, E4 extends Exception> extends ValueOrException4<E1, E2, E3, E4> {
+ private final E3 e;
+
+ private ValueOrException4Exn3Impl(E3 e) {
+ this.e = e;
+ }
+
+ @Override
+ public SkyValue get() throws E3 {
+ throw e;
+ }
+
+ @Override
+ public Exception getException() {
+ return e;
+ }
+
+ @Override
+ @Nullable
+ public SkyValue getValue() {
+ return null;
+ }
+ }
+
+ private static class ValueOrException4Exn4Impl<E1 extends Exception, E2 extends Exception,
+ E3 extends Exception, E4 extends Exception> extends ValueOrException4<E1, E2, E3, E4> {
+ private final E4 e;
+
+ private ValueOrException4Exn4Impl(E4 e) {
+ this.e = e;
+ }
+
+ @Override
+ public SkyValue get() throws E4 {
+ throw e;
+ }
+
+ @Override
+ public Exception getException() {
+ return e;
+ }
+
+ @Override
+ @Nullable
+ public SkyValue getValue() {
+ return null;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/skyframe/ValueOrUntypedException.java b/src/main/java/com/google/devtools/build/skyframe/ValueOrUntypedException.java
new file mode 100644
index 0000000..c7ea7d4
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skyframe/ValueOrUntypedException.java
@@ -0,0 +1,34 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+import javax.annotation.Nullable;
+
+/**
+ * Wrapper for a value or the untyped exception thrown when trying to compute it.
+ *
+ * <p>This is an implementation detail of {@link ParallelEvaluator} and
+ * {@link ValueOrExceptionUtils}. It's an abstract class (as opposed to an interface) to avoid
+ * exposing the methods outside the package.
+ */
+abstract class ValueOrUntypedException {
+
+ /** Returns the stored value, if there was one. */
+ @Nullable
+ abstract SkyValue getValue();
+
+ /** Returns the stored exception, if there was one. */
+ @Nullable
+ abstract Exception getException();
+}
diff --git a/src/main/java/com/google/devtools/build/skyframe/ValueWithMetadata.java b/src/main/java/com/google/devtools/build/skyframe/ValueWithMetadata.java
new file mode 100644
index 0000000..956e404
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skyframe/ValueWithMetadata.java
@@ -0,0 +1,209 @@
+// Copyright 2014 Google Inc. 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
+package com.google.devtools.build.skyframe;
+
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+
+import java.util.Objects;
+
+import javax.annotation.Nullable;
+
+/**
+ * Encapsulation of data stored by {@link NodeEntry} when the value has finished building.
+ *
+ * <p>This is intended only for use in alternative {@code MemoizingEvaluator} implementations.
+ */
+public abstract class ValueWithMetadata implements SkyValue {
+ protected final SkyValue value;
+
+ private static final NestedSet<TaggedEvents> NO_EVENTS =
+ NestedSetBuilder.<TaggedEvents>emptySet(Order.STABLE_ORDER);
+
+ public ValueWithMetadata(SkyValue value) {
+ this.value = value;
+ }
+
+ /** Builds a value entry value that has an error (and no value value).
+ *
+ * <p>This is intended only for use in alternative {@code MemoizingEvaluator} implementations.
+ */
+ public static ValueWithMetadata error(ErrorInfo errorInfo,
+ NestedSet<TaggedEvents> transitiveEvents) {
+ return new ErrorInfoValue(errorInfo, null, transitiveEvents);
+ }
+
+ /**
+ * Builds a value entry value that has a value value, and possibly an error (constructed from its
+ * children's errors).
+ *
+ * <p>This is intended only for use in alternative {@code MemoizingEvaluator} implementations.
+ */
+ static SkyValue normal(@Nullable SkyValue value, @Nullable ErrorInfo errorInfo,
+ NestedSet<TaggedEvents> transitiveEvents) {
+ Preconditions.checkState(value != null || errorInfo != null,
+ "Value and error cannot both be null");
+ if (errorInfo == null) {
+ return transitiveEvents.isEmpty()
+ ? value
+ : new ValueWithEvents(value, transitiveEvents);
+ }
+ return new ErrorInfoValue(errorInfo, value, transitiveEvents);
+ }
+
+
+ @Nullable SkyValue getValue() {
+ return value;
+ }
+
+ @Nullable
+ abstract ErrorInfo getErrorInfo();
+
+ abstract NestedSet<TaggedEvents> getTransitiveEvents();
+
+ static final class ValueWithEvents extends ValueWithMetadata {
+
+ private final NestedSet<TaggedEvents> transitiveEvents;
+
+ ValueWithEvents(SkyValue value, NestedSet<TaggedEvents> transitiveEvents) {
+ super(Preconditions.checkNotNull(value));
+ this.transitiveEvents = Preconditions.checkNotNull(transitiveEvents);
+ }
+
+ @Nullable
+ @Override
+ ErrorInfo getErrorInfo() { return null; }
+
+ @Override
+ NestedSet<TaggedEvents> getTransitiveEvents() { return transitiveEvents; }
+
+ /**
+ * We override equals so that if the same value is written to a {@link NodeEntry} twice, it can
+ * verify that the two values are equal, and avoid incrementing its version.
+ */
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ ValueWithEvents that = (ValueWithEvents) o;
+
+ // Shallow equals is a middle ground between using default equals, which might miss
+ // nested sets with the same elements, and deep equality checking, which would be expensive.
+ // All three choices are sound, since shallow equals and default equals are more
+ // conservative than deep equals. Using shallow equals means that we may unnecessarily
+ // consider some values unequal that are actually equal, but this is still a net win over
+ // deep equals.
+ return value.equals(that.value) && transitiveEvents.shallowEquals(that.transitiveEvents);
+ }
+
+ @Override
+ public int hashCode() {
+ return 31 * value.hashCode() + transitiveEvents.hashCode();
+ }
+
+ @Override
+ public String toString() { return value.toString(); }
+ }
+
+ static final class ErrorInfoValue extends ValueWithMetadata {
+
+ private final ErrorInfo errorInfo;
+ private final NestedSet<TaggedEvents> transitiveEvents;
+
+ ErrorInfoValue(ErrorInfo errorInfo, @Nullable SkyValue value,
+ NestedSet<TaggedEvents> transitiveEvents) {
+ super(value);
+ this.errorInfo = Preconditions.checkNotNull(errorInfo);
+ this.transitiveEvents = Preconditions.checkNotNull(transitiveEvents);
+ }
+
+ @Nullable
+ @Override
+ ErrorInfo getErrorInfo() { return errorInfo; }
+
+ @Override
+ NestedSet<TaggedEvents> getTransitiveEvents() { return transitiveEvents; }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ ErrorInfoValue that = (ErrorInfoValue) o;
+
+ // Shallow equals is a middle ground between using default equals, which might miss
+ // nested sets with the same elements, and deep equality checking, which would be expensive.
+ // All three choices are sound, since shallow equals and default equals are more
+ // conservative than deep equals. Using shallow equals means that we may unnecessarily
+ // consider some values unequal that are actually equal, but this is still a net win over
+ // deep equals.
+ return Objects.equals(this.value, that.value)
+ && Objects.equals(this.errorInfo, that.errorInfo)
+ && transitiveEvents.shallowEquals(that.transitiveEvents);
+ }
+
+ @Override
+ public int hashCode() {
+ return 31 * Objects.hash(value, errorInfo) + transitiveEvents.shallowHashCode();
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder result = new StringBuilder();
+ if (value != null) {
+ result.append("Value: ").append(value);
+ }
+ if (errorInfo != null) {
+ if (result.length() > 0) {
+ result.append("; ");
+ }
+ result.append("Error: ").append(errorInfo);
+ }
+ return result.toString();
+ }
+ }
+
+ static SkyValue justValue(SkyValue value) {
+ if (value instanceof ValueWithMetadata) {
+ return ((ValueWithMetadata) value).getValue();
+ }
+ return value;
+ }
+
+ static ValueWithMetadata wrapWithMetadata(SkyValue value) {
+ if (value instanceof ValueWithMetadata) {
+ return (ValueWithMetadata) value;
+ }
+ return new ValueWithEvents(value, NO_EVENTS);
+ }
+
+ @Nullable
+ public static ErrorInfo getMaybeErrorInfo(SkyValue value) {
+ if (value.getClass() == ErrorInfoValue.class) {
+ return ((ValueWithMetadata) value).getErrorInfo();
+ }
+ return null;
+
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/google/devtools/build/skyframe/Version.java b/src/main/java/com/google/devtools/build/skyframe/Version.java
new file mode 100644
index 0000000..90a6020
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skyframe/Version.java
@@ -0,0 +1,32 @@
+// Copyright 2014 Google Inc. 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.skyframe;
+
+/**
+ * A Version defines a value in a version tree used in persistent data structures.
+ * See http://en.wikipedia.org/wiki/Persistent_data_structure.
+ */
+public interface Version {
+ /**
+ * Defines a partial order relation on versions. Returns true if this object is at most
+ * {@code other} in that partial order. If x.equals(y), then x.atMost(y).
+ *
+ * <p>If x.atMost(y) returns false, then there are two possibilities: y < x in the partial order,
+ * so y.atMost(x) returns true and !x.equals(y), or x and y are incomparable in this partial
+ * order. This may be because x and y are instances of different Version implementations (although
+ * it is legal for different Version implementations to be comparable as well).
+ * See http://en.wikipedia.org/wiki/Partially_ordered_set.
+ */
+ boolean atMost(Version other);
+}