blob: 5eff0561633116ba794d824224bf10188a622773 [file] [log] [blame]
// Copyright 2017 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;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.io.BaseEncoding;
import com.google.devtools.build.lib.buildeventstream.BuildEventContext;
import com.google.devtools.build.lib.buildeventstream.BuildEventId;
import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos;
import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos.BuildEvent;
import com.google.devtools.build.lib.buildeventstream.BuildEventWithOrderConstraint;
import com.google.devtools.build.lib.buildeventstream.GenericBuildEvent;
import com.google.devtools.build.lib.runtime.proto.CommandLineOuterClass.ChunkList;
import com.google.devtools.build.lib.runtime.proto.CommandLineOuterClass.CommandLine;
import com.google.devtools.build.lib.runtime.proto.CommandLineOuterClass.CommandLineSection;
import com.google.devtools.build.lib.runtime.proto.CommandLineOuterClass.Option;
import com.google.devtools.build.lib.runtime.proto.CommandLineOuterClass.OptionList;
import com.google.devtools.build.lib.util.Pair;
import com.google.devtools.common.options.OptionDefinition;
import com.google.devtools.common.options.OptionEffectTag;
import com.google.devtools.common.options.OptionMetadataTag;
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.OptionsParsingResult;
import com.google.devtools.common.options.ParsedOptionDescription;
import com.google.devtools.common.options.proto.OptionFilters;
import com.google.protobuf.InvalidProtocolBufferException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
/** A build event reporting the command line by which Bazel was invoked. */
public abstract class CommandLineEvent implements BuildEventWithOrderConstraint {
@Override
public Collection<BuildEventId> getChildrenEvents() {
return ImmutableList.of();
}
@Override
public Collection<BuildEventId> postedAfter() {
return ImmutableList.of(BuildEventId.buildStartedId());
}
/** A CommandLineEvent that stores functions and values common to both Bazel command lines. */
public abstract static class BazelCommandLineEvent extends CommandLineEvent {
protected final String productName;
protected final OptionsParsingResult activeStartupOptions;
protected final String commandName;
protected final OptionsParsingResult commandOptions;
BazelCommandLineEvent(
String productName,
OptionsParsingResult activeStartupOptions,
String commandName,
OptionsParsingResult commandOptions) {
this.productName = productName;
this.activeStartupOptions = activeStartupOptions;
this.commandName = commandName;
this.commandOptions = commandOptions;
}
CommandLineSection getExecutableSection() {
return CommandLineSection.newBuilder()
.setSectionLabel("executable")
.setChunkList(ChunkList.newBuilder().addChunk(productName))
.build();
}
CommandLineSection getCommandSection() {
return CommandLineSection.newBuilder()
.setSectionLabel("command")
.setChunkList(ChunkList.newBuilder().addChunk(commandName))
.build();
}
/**
* Convert an array of tags to the equivalent proto-generated enum values.
*
* <p>The proto type is duplicate in order to not burden the OptionsParser with the proto
* dependency. A test guarantees that the two enum types are kept in sync with matching indices.
*/
static List<OptionFilters.OptionEffectTag> getProtoEffectTags(OptionEffectTag[] tagArray) {
ArrayList<OptionFilters.OptionEffectTag> effectTags = new ArrayList<>(tagArray.length);
for (OptionEffectTag tag : tagArray) {
effectTags.add(OptionFilters.OptionEffectTag.forNumber(tag.getValue()));
}
return effectTags;
}
/**
* Convert an array of tags to the equivalent proto-generated enum values.
*
* <p>The proto type is duplicate in order to not burden the OptionsParser with the proto
* dependency. A test guarantees that the two enum types are kept in sync with matching indices.
*/
static List<OptionFilters.OptionMetadataTag> getProtoMetadataTags(
OptionMetadataTag[] tagArray) {
ArrayList<OptionFilters.OptionMetadataTag> metadataTags = new ArrayList<>(tagArray.length);
for (OptionMetadataTag tag : tagArray) {
metadataTags.add(OptionFilters.OptionMetadataTag.forNumber(tag.getValue()));
}
return metadataTags;
}
List<Option> getOptionListFromParsedOptionDescriptions(
List<ParsedOptionDescription> parsedOptionDescriptions) {
List<Option> options = new ArrayList<>();
for (ParsedOptionDescription parsedOption : parsedOptionDescriptions) {
options.add(
createOption(
parsedOption.getOptionDefinition(),
parsedOption.getCommandLineForm(),
parsedOption.getUnconvertedValue()));
}
return options;
}
private Option createOption(
OptionDefinition optionDefinition, String combinedForm, @Nullable String value) {
Option.Builder option = Option.newBuilder();
option.setCombinedForm(combinedForm);
option.setOptionName(optionDefinition.getOptionName());
if (value != null) {
option.setOptionValue(value);
}
option.addAllEffectTags(getProtoEffectTags(optionDefinition.getOptionEffectTags()));
option.addAllMetadataTags(getProtoMetadataTags(optionDefinition.getOptionMetadataTags()));
return option.build();
}
/**
* Returns the startup option section of the command line for the startup options as the server
* received them at its startup. Since not all client options get passed to the server as
* startup options, this might not represent the actual list of startup options as the user
* provided them.
*/
CommandLineSection getActiveStartupOptions() {
return CommandLineSection.newBuilder()
.setSectionLabel("startup options")
.setOptionList(
OptionList.newBuilder()
.addAllOption(
getOptionListFromParsedOptionDescriptions(
activeStartupOptions.asCompleteListOfParsedOptions())))
.build();
}
/**
* Returns the final part of the command line, containing whatever was left after obtaining the
* command and its options.
*/
CommandLineSection getResidual() {
// Potential further split: how the residual, if any is accepted, gets interpreted depends on
// the command. For example, for build commands, we might want to consider separating out
// project files, as in runtime.commands.ProjectFileSupport. To properly report this, we would
// need to let the command customize how the residual is listed. This catch-all could serve
// as a default in this case.
return CommandLineSection.newBuilder()
.setSectionLabel("residual")
.setChunkList(ChunkList.newBuilder().addAllChunk(commandOptions.getResidue()))
.build();
}
}
/** This reports a reassembled version of the command line as Bazel received it. */
public static class OriginalCommandLineEvent extends BazelCommandLineEvent {
public static final String LABEL = "original";
private final Optional<List<Pair<String, String>>> originalStartupOptions;
public OriginalCommandLineEvent(
BlazeRuntime runtime,
String commandName,
OptionsParsingResult commandOptions,
Optional<List<Pair<String, String>>> originalStartupOptions) {
this(
runtime.getProductName(),
runtime.getStartupOptionsProvider(),
commandName,
commandOptions,
originalStartupOptions);
}
@VisibleForTesting
OriginalCommandLineEvent(
String productName,
OptionsParsingResult activeStartupOptions,
String commandName,
OptionsParsingResult commandOptions,
Optional<List<Pair<String, String>>> originalStartupOptions) {
super(productName, activeStartupOptions, commandName, commandOptions);
this.originalStartupOptions = originalStartupOptions;
}
@Override
public BuildEventId getEventId() {
return BuildEventId.structuredCommandlineId(LABEL);
}
/**
* Returns the literal command line options as received. These are not the final parsed values,
* but are passed as is from the client, so we do not have the full OptionDefinition
* information. In this form, only set the "combinedForm" field.
*/
private CommandLineSection getStartupOptionSection() {
if (originalStartupOptions.isPresent()) {
List<Option> options = new ArrayList<>();
for (Pair<String, String> sourceToOptionPair : originalStartupOptions.get()) {
// Only add the options that were added by the command line.
// TODO(b/19881919) decide the format that option source information should take and then
// add all options, tagged with the source, instead of filtering out the rc options.
if (sourceToOptionPair.first != null && sourceToOptionPair.first.isEmpty()) {
options.add(
Option.newBuilder().setCombinedForm(sourceToOptionPair.getSecond()).build());
}
}
return CommandLineSection.newBuilder()
.setSectionLabel("startup options")
.setOptionList(OptionList.newBuilder().addAllOption(options))
.build();
} else {
// If we were not provided with the startup options, fallback to reporting the active ones
// stored by the Bazel Runtime.
return getActiveStartupOptions();
}
}
private CommandLineSection getExplicitCommandOptions() {
List<ParsedOptionDescription> explicitOptions =
commandOptions
.asListOfExplicitOptions()
.stream()
.filter(
parsedOptionDescription ->
parsedOptionDescription.getPriority().getPriorityCategory()
== OptionPriority.PriorityCategory.COMMAND_LINE)
.collect(Collectors.toList());
return CommandLineSection.newBuilder()
.setSectionLabel("command options")
.setOptionList(
OptionList.newBuilder()
.addAllOption(getOptionListFromParsedOptionDescriptions(explicitOptions)))
.build();
}
@Override
public BuildEventStreamProtos.BuildEvent asStreamProto(BuildEventContext converters) {
return GenericBuildEvent.protoChaining(this)
.setStructuredCommandLine(
CommandLine.newBuilder()
.setCommandLineLabel(LABEL)
.addSections(getExecutableSection())
.addSections(getStartupOptionSection())
.addSections(getCommandSection())
.addSections(getExplicitCommandOptions())
.addSections(getResidual())
.build())
.build();
}
}
/** This reports the canonical form of the command line. */
public static class CanonicalCommandLineEvent extends BazelCommandLineEvent {
public static final String LABEL = "canonical";
public CanonicalCommandLineEvent(
BlazeRuntime runtime, String commandName, OptionsParsingResult commandOptions) {
this(
runtime.getProductName(),
runtime.getStartupOptionsProvider(),
commandName,
commandOptions);
}
@VisibleForTesting
CanonicalCommandLineEvent(
String productName,
OptionsParsingResult activeStartupOptions,
String commandName,
OptionsParsingResult commandOptions) {
super(productName, activeStartupOptions, commandName, commandOptions);
}
@Override
public BuildEventId getEventId() {
return BuildEventId.structuredCommandlineId(LABEL);
}
/**
* Returns the effective startup options.
*
* <p>Since in this command line the command options include invocation policy's and rcs'
* contents expanded fully, the list of startup options should prevent reapplication of these
* contents.
*
* <p>The options parser does not understand the effect of these flags, since the relationship
* between these startup options and the command options is not held within the options parser,
* so instead, we add a small hack. Remove any explicit mentions of these flags, and explicitly
* add the options that prevent Blaze from looking for the default rc files.
*/
private CommandLineSection getCanonicalStartupOptions() {
List<Option> unfilteredOptions = getActiveStartupOptions().getOptionList().getOptionList();
// Create the fake ones to prevent reapplication of the original rc file contents.
OptionsParser fakeOptions = OptionsParser.newOptionsParser(BlazeServerStartupOptions.class);
try {
fakeOptions.parse("--ignore_all_rc_files");
} catch (OptionsParsingException e) {
// Unless someone changes the definition of these flags, this is impossible.
throw new IllegalStateException(e);
}
// Remove any instances of the applied, and add the new blocking ones.
return CommandLineSection.newBuilder()
.setSectionLabel("startup options")
.setOptionList(
OptionList.newBuilder()
.addAllOption(
unfilteredOptions
.stream()
.filter(
option -> {
String optionName = option.getOptionName();
return !optionName.equals("ignore_all_rc_files")
&& !optionName.equals("blazerc")
&& !optionName.equals("master_blazerc")
&& !optionName.equals("bazelrc")
&& !optionName.equals("master_bazelrc")
&& !optionName.equals("invocation_policy");
})
.collect(Collectors.toList()))
.addAllOption(
getOptionListFromParsedOptionDescriptions(
fakeOptions.asCompleteListOfParsedOptions())))
.build();
}
/** Returns the canonical command options, overridden and default values are not listed. */
private CommandLineSection getCanonicalCommandOptions() {
return CommandLineSection.newBuilder()
.setSectionLabel("command options")
.setOptionList(
OptionList.newBuilder()
.addAllOption(
getOptionListFromParsedOptionDescriptions(
commandOptions.asListOfCanonicalOptions())))
.build();
}
@Override
public BuildEventStreamProtos.BuildEvent asStreamProto(BuildEventContext converters) {
return GenericBuildEvent.protoChaining(this)
.setStructuredCommandLine(
CommandLine.newBuilder()
.setCommandLineLabel(LABEL)
.addSections(getExecutableSection())
.addSections(getCanonicalStartupOptions())
.addSections(getCommandSection())
.addSections(getCanonicalCommandOptions())
.addSections(getResidual())
.build())
.build();
}
}
/**
* A command line that Bazel accepts via flag (yes, we see the irony there).
*
* <p>Permits Bazel to report command lines from the tool that invoked it, if such a tool exists.
*/
public static final class ToolCommandLineEvent extends CommandLineEvent {
public static final String LABEL = "tool";
private final CommandLine commandLine;
ToolCommandLineEvent(CommandLine commandLine) {
this.commandLine = commandLine;
}
@Override
public BuildEvent asStreamProto(BuildEventContext converters) {
return GenericBuildEvent.protoChaining(this).setStructuredCommandLine(commandLine).build();
}
/**
* The label of this command line event is always "tool," so that the BuildStartingEvent
* correctly tracks its children. The provided command line may have its own label that will be
* more descriptive.
*/
@Override
public BuildEventId getEventId() {
return BuildEventId.structuredCommandlineId(LABEL);
}
/**
* The converter for the option value. We accept the command line both in base64 encoded proto
* form and as unstructured strings.
*/
public static class Converter
implements com.google.devtools.common.options.Converter<ToolCommandLineEvent> {
@Override
public ToolCommandLineEvent convert(String input) throws OptionsParsingException {
if (input.isEmpty()) {
return new ToolCommandLineEvent(CommandLine.getDefaultInstance());
}
CommandLine commandLine;
try {
// Try decoding the input as a base64 encoded binary proto.
commandLine = CommandLine.parseFrom(BaseEncoding.base64().decode(input));
} catch (IllegalArgumentException e) {
// If the value was not recognized as a base64-encoded proto, store the flag value as a
// single string chunk.
commandLine =
CommandLine.newBuilder()
.setCommandLineLabel(LABEL)
.addSections(
CommandLineSection.newBuilder()
.setChunkList(ChunkList.newBuilder().addChunk(input)))
.build();
} catch (InvalidProtocolBufferException e) {
throw new OptionsParsingException(
String.format("Malformed value of --experimental_tool_command_line: %s", input), e);
}
return new ToolCommandLineEvent(commandLine);
}
@Override
public String getTypeDescription() {
return "A command line, either as a simple string, or as a base64-encoded binary form of a"
+ " CommandLine proto";
}
}
}
}