| // 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(); | |
| } | |
| Option createStarlarkOption(String starlarkFlag, @Nullable Object value) { | |
| String combinedForm = String.format("--%s=%s", starlarkFlag, value); | |
| Option.Builder option = Option.newBuilder(); | |
| option.setCombinedForm(combinedForm); | |
| option.setOptionName(starlarkFlag); | |
| if (value != null) { | |
| option.setOptionValue(String.valueOf(value)); | |
| } | |
| 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()); | |
| List<Option> starlarkOptions = | |
| commandOptions.getStarlarkOptions().entrySet().stream() | |
| .map(e -> createStarlarkOption(e.getKey(), e.getValue())) | |
| .collect(Collectors.toList()); | |
| return CommandLineSection.newBuilder() | |
| .setSectionLabel("command options") | |
| .setOptionList( | |
| OptionList.newBuilder() | |
| .addAllOption(getOptionListFromParsedOptionDescriptions(explicitOptions)) | |
| .addAllOption(starlarkOptions)) | |
| .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.builder().optionsClasses(BlazeServerStartupOptions.class).build(); | |
| 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() { | |
| List<Option> starlarkOptions = | |
| commandOptions.getStarlarkOptions().entrySet().stream() | |
| .map(e -> createStarlarkOption(e.getKey(), e.getValue())) | |
| .collect(Collectors.toList()); | |
| return CommandLineSection.newBuilder() | |
| .setSectionLabel("command options") | |
| .setOptionList( | |
| OptionList.newBuilder() | |
| .addAllOption( | |
| getOptionListFromParsedOptionDescriptions( | |
| commandOptions.asListOfCanonicalOptions())) | |
| .addAllOption(starlarkOptions)) | |
| .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"; | |
| } | |
| } | |
| } | |
| } |