blob: acd43a0aae3e0b3c92bd149d9b1290ee13e79955 [file] [log] [blame]
// Copyright 2014 The Bazel Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.devtools.build.lib.runtime.commands;
import com.google.common.base.CaseFormat;
import com.google.common.base.Joiner;
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.ImmutableSortedMap;
import com.google.common.collect.Iterables;
import com.google.common.escape.Escaper;
import com.google.common.html.HtmlEscapers;
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.analysis.NoBuildEvent;
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.BlazeCommandResult;
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.runtime.CommandEnvironment;
import com.google.devtools.build.lib.runtime.commands.proto.BazelFlagsProto;
import com.google.devtools.build.lib.util.ExitCode;
import com.google.devtools.build.lib.util.StringUtil;
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.OptionDefinition;
import com.google.devtools.common.options.OptionDocumentationCategory;
import com.google.devtools.common.options.OptionEffectTag;
import com.google.devtools.common.options.OptionFilterDescriptions;
import com.google.devtools.common.options.OptionMetadataTag;
import com.google.devtools.common.options.OptionsBase;
import com.google.devtools.common.options.OptionsParser;
import com.google.devtools.common.options.OptionsParser.HelpVerbosity;
import com.google.devtools.common.options.OptionsProvider;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Predicate;
/** 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.",
completion = "command|{startup_options,target-syntax,info-keys}",
help = "resource:help.txt"
)
public final class HelpCommand implements BlazeCommand {
private static final Joiner SPACE_JOINER = Joiner.on(" ");
/**
* Only to be used to escape the internal hard-coded help texts when outputting HTML from help,
* which don't pose a security risk.
*/
private static final Escaper HTML_ESCAPER = HtmlEscapers.htmlEscaper();
public static class Options extends OptionsBase {
@Option(
name = "help_verbosity",
defaultValue = "medium",
converter = Converters.HelpVerbosityConverter.class,
documentationCategory = OptionDocumentationCategory.LOGGING,
effectTags = {OptionEffectTag.AFFECTS_OUTPUTS, OptionEffectTag.TERMINAL_OUTPUT},
help = "Select the verbosity of the help command."
)
public OptionsParser.HelpVerbosity helpVerbosity;
@Option(
name = "long",
abbrev = 'l',
defaultValue = "null",
expansion = {"--help_verbosity=long"},
documentationCategory = OptionDocumentationCategory.LOGGING,
effectTags = {OptionEffectTag.AFFECTS_OUTPUTS, OptionEffectTag.TERMINAL_OUTPUT},
help = "Show full description of each option, instead of just its name."
)
public Void showLongFormOptions;
@Option(
name = "short",
defaultValue = "null",
expansion = {"--help_verbosity=short"},
documentationCategory = OptionDocumentationCategory.LOGGING,
effectTags = {OptionEffectTag.AFFECTS_OUTPUTS, OptionEffectTag.TERMINAL_OUTPUT},
help = "Show only the names of the options, not their types or meanings."
)
public Void showShortFormOptions;
}
@Override
public void editOptions(OptionsParser optionsParser) {}
@Override
public BlazeCommandResult exec(CommandEnvironment env, OptionsProvider options) {
env.getEventBus().post(new NoBuildEvent());
BlazeRuntime runtime = env.getRuntime();
OutErr outErr = env.getReporter().getOutErr();
Options helpOptions = options.getOptions(Options.class);
if (options.getResidue().isEmpty()) {
emitBlazeVersionInfo(outErr, runtime.getProductName());
emitGenericHelp(outErr, runtime);
return BlazeCommandResult.exitCode(ExitCode.SUCCESS);
}
if (options.getResidue().size() != 1) {
env.getReporter().handle(Event.error("You must specify exactly one command"));
return BlazeCommandResult.exitCode(ExitCode.COMMAND_LINE_ERROR);
}
String helpSubject = options.getResidue().get(0);
String productName = runtime.getProductName();
// Go through the custom subjects before going through Bazel commands.
switch (helpSubject) {
case "startup_options":
emitBlazeVersionInfo(outErr, runtime.getProductName());
emitStartupOptions(outErr, helpOptions.helpVerbosity, runtime);
return BlazeCommandResult.exitCode(ExitCode.SUCCESS);
case "target-syntax":
emitBlazeVersionInfo(outErr, runtime.getProductName());
emitTargetSyntaxHelp(outErr, productName);
return BlazeCommandResult.exitCode(ExitCode.SUCCESS);
case "info-keys":
emitInfoKeysHelp(env, outErr);
return BlazeCommandResult.exitCode(ExitCode.SUCCESS);
case "completion":
emitCompletionHelp(runtime, outErr);
return BlazeCommandResult.exitCode(ExitCode.SUCCESS);
case "flags-as-proto":
emitFlagsAsProtoHelp(runtime, outErr);
return BlazeCommandResult.exitCode(ExitCode.SUCCESS);
case "everything-as-html":
new HtmlEmitter(runtime).emit(outErr);
return BlazeCommandResult.exitCode(ExitCode.SUCCESS);
default: // fall out
}
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, runtime.getProductName(), provider));
return BlazeCommandResult.exitCode(ExitCode.SUCCESS);
} else {
env.getReporter().handle(Event.error(
null, "'" + helpSubject + "' is neither a command nor a build rule"));
return BlazeCommandResult.exitCode(ExitCode.COMMAND_LINE_ERROR);
}
}
emitBlazeVersionInfo(outErr, productName);
outErr.printOut(
BlazeCommandUtils.getUsage(
command.getClass(),
helpOptions.helpVerbosity,
runtime.getBlazeModules(),
runtime.getRuleClassProvider(),
productName));
return BlazeCommandResult.exitCode(ExitCode.SUCCESS);
}
private void emitBlazeVersionInfo(OutErr outErr, String productName) {
String releaseInfo = BlazeVersionInfo.instance().getReleaseName();
String line = String.format("[%s %s]", productName, releaseInfo);
outErr.printOut(String.format("%80s\n", line));
}
private void emitStartupOptions(
OutErr outErr, HelpVerbosity helpVerbosity, BlazeRuntime runtime) {
outErr.printOut(
BlazeCommandUtils.expandHelpTopic(
"startup_options",
"resource:startup_options.txt",
getClass(),
BlazeCommandUtils.getStartupOptions(runtime.getBlazeModules()),
helpVerbosity,
runtime.getProductName()));
}
private void emitCompletionHelp(BlazeRuntime runtime, OutErr outErr) {
Map<String, BlazeCommand> commandsByName = getSortedCommands(runtime);
outErr.printOutLn("BAZEL_COMMAND_LIST=\"" + SPACE_JOINER.join(commandsByName.keySet()) + "\"");
outErr.printOutLn("BAZEL_INFO_KEYS=\"");
for (String name : InfoCommand.getHardwiredInfoItemNames(runtime.getProductName())) {
outErr.printOutLn(name);
}
outErr.printOutLn("\"");
Consumer<OptionsParser> startupOptionVisitor =
parser -> {
outErr.printOutLn("BAZEL_STARTUP_OPTIONS=\"");
outErr.printOut(parser.getOptionsCompletion());
outErr.printOutLn("\"");
};
CommandOptionVisitor commandOptionVisitor =
(commandName, commandAnnotation, parser) -> {
String varName = CaseFormat.LOWER_HYPHEN.to(CaseFormat.UPPER_UNDERSCORE, commandName);
if (!Strings.isNullOrEmpty(commandAnnotation.completion())) {
outErr.printOutLn(
"BAZEL_COMMAND_"
+ varName
+ "_ARGUMENT=\""
+ commandAnnotation.completion()
+ "\"");
}
outErr.printOutLn("BAZEL_COMMAND_" + varName + "_FLAGS=\"");
outErr.printOut(parser.getOptionsCompletion());
outErr.printOutLn("\"");
};
visitAllOptions(runtime, startupOptionVisitor, commandOptionVisitor);
}
private void emitFlagsAsProtoHelp(BlazeRuntime runtime, OutErr outErr) {
Map<String, BazelFlagsProto.FlagInfo.Builder> flags = new HashMap<>();
Predicate<OptionDefinition> allOptions = option -> true;
BiConsumer<String, OptionDefinition> visitor =
(commandName, option) -> {
if (ImmutableSet.copyOf(option.getOptionMetadataTags())
.contains(OptionMetadataTag.INTERNAL)) {
return;
}
BazelFlagsProto.FlagInfo.Builder info =
flags.computeIfAbsent(option.getOptionName(), key -> createFlagInfo(option));
info.addCommands(commandName);
};
Consumer<OptionsParser> startupOptionVisitor =
parser -> {
parser.visitOptions(allOptions, option -> visitor.accept("startup", option));
};
CommandOptionVisitor commandOptionVisitor =
(commandName, commandAnnotation, parser) -> {
parser.visitOptions(allOptions, option -> visitor.accept(commandName, option));
};
visitAllOptions(runtime, startupOptionVisitor, commandOptionVisitor);
BazelFlagsProto.FlagCollection.Builder collectionBuilder =
BazelFlagsProto.FlagCollection.newBuilder();
for (BazelFlagsProto.FlagInfo.Builder info : flags.values()) {
collectionBuilder.addFlagInfos(info);
}
outErr.printOut(Base64.getEncoder().encodeToString(collectionBuilder.build().toByteArray()));
}
private BazelFlagsProto.FlagInfo.Builder createFlagInfo(OptionDefinition option) {
BazelFlagsProto.FlagInfo.Builder flagBuilder = BazelFlagsProto.FlagInfo.newBuilder();
flagBuilder.setName(option.getOptionName());
flagBuilder.setHasNegativeFlag(option.hasNegativeOption());
flagBuilder.setDocumentation(option.getHelpText());
return flagBuilder;
}
private void visitAllOptions(
BlazeRuntime runtime,
Consumer<OptionsParser> startupOptionVisitor,
CommandOptionVisitor commandOptionVisitor) {
// First startup_options
Iterable<BlazeModule> blazeModules = runtime.getBlazeModules();
ConfiguredRuleClassProvider ruleClassProvider = runtime.getRuleClassProvider();
Map<String, BlazeCommand> commandsByName = getSortedCommands(runtime);
Iterable<Class<? extends OptionsBase>> options =
BlazeCommandUtils.getStartupOptions(blazeModules);
startupOptionVisitor.accept(OptionsParser.newOptionsParser(options));
for (Map.Entry<String, BlazeCommand> e : commandsByName.entrySet()) {
BlazeCommand command = e.getValue();
Command annotation = command.getClass().getAnnotation(Command.class);
options = BlazeCommandUtils.getOptions(command.getClass(), blazeModules, ruleClassProvider);
commandOptionVisitor.visit(e.getKey(), annotation, OptionsParser.newOptionsParser(options));
}
}
private static Map<String, BlazeCommand> getSortedCommands(BlazeRuntime runtime) {
return ImmutableSortedMap.copyOf(runtime.getCommandMap());
}
private void emitTargetSyntaxHelp(OutErr outErr, String productName) {
outErr.printOut(
BlazeCommandUtils.expandHelpTopic(
"target-syntax",
"resource:target-syntax.txt",
getClass(),
ImmutableList.<Class<? extends OptionsBase>>of(),
OptionsParser.HelpVerbosity.MEDIUM,
productName));
}
private void emitInfoKeysHelp(CommandEnvironment env, OutErr outErr) {
for (InfoItem item : InfoCommand.getInfoItemMap(env,
OptionsParser.newOptionsParser(
ImmutableList.<Class<? extends OptionsBase>>of())).values()) {
outErr.printOut(String.format("%-23s %s\n", item.getName(), item.getDescription()));
}
}
private void emitGenericHelp(OutErr outErr, BlazeRuntime runtime) {
outErr.printOut(String.format("Usage: %s <command> <options> ...\n\n",
runtime.getProductName()));
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().
replace("%{product}", runtime.getProductName());
outErr.printOut(String.format(" %-19s %s\n", name, shortDescription));
}
outErr.printOut("\n");
outErr.printOut("Getting more help:\n");
outErr.printOut(String.format(" %s help <command>\n", runtime.getProductName()));
outErr.printOut(" Prints help and options for <command>.\n");
outErr.printOut(String.format(" %s help startup_options\n", runtime.getProductName()));
outErr.printOut(String.format(" Options for the JVM hosting %s.\n",
runtime.getProductName()));
outErr.printOut(String.format(" %s help target-syntax\n", runtime.getProductName()));
outErr.printOut(" Explains the syntax for specifying targets.\n");
outErr.printOut(String.format(" %s help info-keys\n", runtime.getProductName()));
outErr.printOut(" Displays a list of keys used by the info command.\n");
}
private static final class HtmlEmitter {
private final BlazeRuntime runtime;
private HtmlEmitter(BlazeRuntime runtime) {
this.runtime = runtime;
}
private void emit(OutErr outErr) {
Map<String, BlazeCommand> commandsByName = getSortedCommands(runtime);
StringBuilder result = new StringBuilder();
result.append("<h2>Commands</h2>\n");
result.append("<table>\n");
for (Map.Entry<String, BlazeCommand> e : commandsByName.entrySet()) {
BlazeCommand command = e.getValue();
Command annotation = command.getClass().getAnnotation(Command.class);
if (annotation.hidden()) {
continue;
}
String shortDescription = annotation.shortDescription().
replace("%{product}", runtime.getProductName());
result.append("<tr>\n");
result.append(
String.format(
" <td><a href=\"#%s\"><code>%s</code></a></td>\n", e.getKey(), e.getKey()));
result.append(" <td>").append(HTML_ESCAPER.escape(shortDescription)).append("</td>\n");
result.append("</tr>\n");
}
result.append("</table>\n");
result.append("\n");
result.append("<h2>Startup Options</h2>\n");
appendOptionsHtml(result, BlazeCommandUtils.getStartupOptions(runtime.getBlazeModules()));
result.append("\n");
result.append("<h2><a name=\"common_options\">Options Common to all Commands</a></h2>\n");
appendOptionsHtml(result, BlazeCommandUtils.getCommonOptions(runtime.getBlazeModules()));
result.append("\n");
for (Map.Entry<String, BlazeCommand> e : commandsByName.entrySet()) {
result.append(
String.format(
"<h2><a name=\"%s\">%s Options</a></h2>\n", e.getKey(), capitalize(e.getKey())));
BlazeCommand command = e.getValue();
Command annotation = command.getClass().getAnnotation(Command.class);
if (annotation.hidden()) {
continue;
}
List<String> inheritedCmdNames = new ArrayList<>();
for (Class<? extends BlazeCommand> base : annotation.inherits()) {
String name = base.getAnnotation(Command.class).name();
inheritedCmdNames.add(String.format("<a href=\"#%s\">%s</a>", name, name));
}
if (!inheritedCmdNames.isEmpty()) {
result.append("<p>Inherits all options from ");
result.append(StringUtil.joinEnglishList(inheritedCmdNames, "and"));
result.append(".</p>\n\n");
}
Set<Class<? extends OptionsBase>> options = new HashSet<>();
Collections.addAll(options, annotation.options());
for (BlazeModule blazeModule : runtime.getBlazeModules()) {
Iterables.addAll(options, blazeModule.getCommandOptions(annotation));
}
appendOptionsHtml(result, options);
result.append("\n");
// For now, we print all the configuration options in a list after all the non-configuration
// options. Note that usesConfigurationOptions is only true for the build command right now.
if (annotation.usesConfigurationOptions()) {
options.clear();
Collections.addAll(options, annotation.options());
if (annotation.usesConfigurationOptions()) {
options.addAll(runtime.getRuleClassProvider().getConfigurationOptions());
}
appendOptionsHtml(result, options);
result.append("\n");
}
}
// Describe the tags once, any mentions above should link to these descriptions.
String productName = runtime.getProductName();
ImmutableMap<OptionEffectTag, String> effectTagDescriptions =
OptionFilterDescriptions.getOptionEffectTagDescription(productName);
result.append("<h3>Option Effect Tags</h3>\n");
result.append("<table>\n");
for (OptionEffectTag tag : OptionEffectTag.values()) {
String tagDescription = effectTagDescriptions.get(tag);
result.append("<tr>\n");
result.append(
String.format(
"<td id=\"effect_tag_%s\"><code>%s</code></td>\n",
tag, tag.name().toLowerCase()));
result.append(String.format("<td>%s</td>\n", HTML_ESCAPER.escape(tagDescription)));
result.append("</tr>\n");
}
result.append("</table>\n");
ImmutableMap<OptionMetadataTag, String> metadataTagDescriptions =
OptionFilterDescriptions.getOptionMetadataTagDescription(productName);
result.append("<h3>Option Metadata Tags</h3>\n");
result.append("<table>\n");
for (OptionMetadataTag tag : OptionMetadataTag.values()) {
// skip the tags that are reserved for undocumented flags.
if (!tag.equals(OptionMetadataTag.HIDDEN) && !tag.equals(OptionMetadataTag.INTERNAL)) {
String tagDescription = metadataTagDescriptions.get(tag);
result.append("<tr>\n");
result.append(
String.format(
"<td id=\"metadata_tag_%s\"><code>%s</code></td>\n",
tag, tag.name().toLowerCase()));
result.append(String.format("<td>%s</td>\n", HTML_ESCAPER.escape(tagDescription)));
result.append("</tr>\n");
}
}
result.append("</table>\n");
outErr.printOut(result.toString());
}
private void appendOptionsHtml(
StringBuilder result, Iterable<Class<? extends OptionsBase>> optionsClasses) {
OptionsParser parser = OptionsParser.newOptionsParser(optionsClasses);
String productName = runtime.getProductName();
result.append(
parser
.describeOptionsHtml(HTML_ESCAPER, productName)
.replace("%{product}", productName));
}
private static String capitalize(String s) {
return s.substring(0, 1).toUpperCase(Locale.US) + s.substring(1);
}
}
/** A visitor for Blaze commands and their respective command line options. */
@FunctionalInterface
interface CommandOptionVisitor {
/**
* Visits a Blaze command by providing access to its name, its meta-data and its command line
* options (via an {@link OptionsParser} instance).
*
* @param commandName name of the command, e.g. "help".
* @param commandAnnotation {@link Command} that contains addition information about the
* command.
* @param parser an {@link OptionsParser} instance that provides access to all options supported
* by the command.
*/
void visit(String commandName, Command commandAnnotation, OptionsParser parser);
}
}