com.google.devtools.build.lib.runtime.BlazeOptionHandler.java Source code

Java tutorial

Introduction

Here is the source code for com.google.devtools.build.lib.runtime.BlazeOptionHandler.java

Source

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

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.base.Predicates;
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.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.EventHandler;
import com.google.devtools.build.lib.events.ExtendedEventHandler;
import com.google.devtools.build.lib.runtime.commands.ProjectFileSupport;
import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.InvocationPolicy;
import com.google.devtools.build.lib.util.ExitCode;
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.common.options.InvocationPolicyEnforcer;
import com.google.devtools.common.options.OptionDefinition;
import com.google.devtools.common.options.OptionPriority.PriorityCategory;
import com.google.devtools.common.options.OptionsParser;
import com.google.devtools.common.options.OptionsParsingException;
import com.google.devtools.common.options.OptionsProvider;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
import java.util.logging.Level;
import javax.annotation.Nullable;

/**
 * Handles parsing the blaze command arguments.
 *
 * <p>This class manages rc options, default options, and invocation policy.
 */
public class BlazeOptionHandler {
    // Keep in sync with options added in OptionProcessor::AddRcfileArgsAndOptions()
    private static final ImmutableSet<String> INTERNAL_COMMAND_OPTIONS = ImmutableSet.of("rc_source",
            "default_override", "isatty", "terminal_columns", "ignore_client_env", "client_env", "client_cwd");

    private final BlazeRuntime runtime;
    private final OptionsParser optionsParser;
    private final BlazeWorkspace workspace;
    private final BlazeCommand command;
    private final Command commandAnnotation;
    private final String commandName;
    private final InvocationPolicy invocationPolicy;
    private final List<String> rcfileNotes = new ArrayList<>();

    public BlazeOptionHandler(BlazeRuntime runtime, BlazeWorkspace workspace, BlazeCommand command,
            Command commandAnnotation, String commandName, OptionsParser optionsParser,
            InvocationPolicy invocationPolicy) {
        this.runtime = runtime;
        this.workspace = workspace;
        this.command = command;
        this.commandAnnotation = commandAnnotation;
        this.commandName = commandName;
        this.optionsParser = optionsParser;
        this.invocationPolicy = invocationPolicy;
    }

    // Return options as OptionsProvider so the options can't be easily modified after we've
    // applied the invocation policy.
    OptionsProvider getOptionsResult() {
        return optionsParser;
    }

    public List<String> getRcfileNotes() {
        return rcfileNotes;
    }

    /**
     * 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(EventHandler eventHandler) {
        if (!commandAnnotation.mustRunInWorkspace()) {
            return ExitCode.SUCCESS;
        }

        if (!workspace.getDirectories().inWorkspace()) {
            eventHandler.handle(
                    Event.error("The '" + commandName + "' command is only supported from within a workspace."));
            return ExitCode.COMMAND_LINE_ERROR;
        }

        Path workspacePath = workspace.getWorkspace();
        // TODO(kchodorow): Remove this once spaces are supported.
        if (workspacePath.getPathString().contains(" ")) {
            eventHandler.handle(Event.error(runtime.getProductName()
                    + " does not currently work properly from paths " + "containing spaces (" + workspace + ")."));
            return ExitCode.LOCAL_ENVIRONMENTAL_ERROR;
        }

        Path doNotBuild = workspacePath.getParentDirectory().getRelative(BlazeWorkspace.DO_NOT_BUILD_FILE_NAME);

        if (doNotBuild.exists()) {
            if (!commandAnnotation.canRunInOutputDirectory()) {
                eventHandler.handle(Event.error(getNotInRealWorkspaceError(doNotBuild)));
                return ExitCode.COMMAND_LINE_ERROR;
            } else {
                eventHandler.handle(
                        Event.warn(runtime.getProductName() + " is run from output directory. This is unsound."));
            }
        }
        return ExitCode.SUCCESS;
    }

    private void parseArgsAndConfigs(List<String> args, ExtendedEventHandler eventHandler)
            throws OptionsParsingException {
        Path workspaceDirectory = workspace.getWorkspace();
        // TODO(ulfjack): The working directory is passed by the client as part of CommonCommandOptions,
        // and we can't know it until after we've parsed the options, so use the workspace for now.
        Path workingDirectory = workspace.getWorkspace();

        Function<OptionDefinition, String> commandOptionSourceFunction = option -> {
            if (INTERNAL_COMMAND_OPTIONS.contains(option.getOptionName())) {
                return "options generated by " + runtime.getProductName() + " launcher";
            } else {
                return "command line options";
            }
        };

        // Explicit command-line options:
        List<String> cmdLineAfterCommand = args.subList(1, args.size());
        optionsParser.parseWithSourceFunction(PriorityCategory.COMMAND_LINE, commandOptionSourceFunction,
                cmdLineAfterCommand);

        // Command-specific options from .blazerc passed in via --default_override
        // and --rc_source. A no-op if none are provided.
        ClientOptions rcFileOptions = optionsParser.getOptions(ClientOptions.class);
        List<Pair<String, ListMultimap<String, String>>> optionsMap = getOptionsMap(eventHandler,
                rcFileOptions.rcSource, rcFileOptions.optionsOverrides, runtime.getCommandMap().keySet());

        parseOptionsForCommand(rcfileNotes, commandAnnotation, optionsParser, optionsMap, null, null);
        if (commandAnnotation.builds()) {
            // splits project files from targets in the traditional sense
            ProjectFileSupport.handleProjectFiles(eventHandler, runtime.getProjectFileProvider(),
                    workspaceDirectory, workingDirectory, optionsParser, commandAnnotation.name());
        }

        // Fix-point iteration until all configs are loaded.
        List<String> configsLoaded = ImmutableList.of();
        Set<String> unknownConfigs = new LinkedHashSet<>();
        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,
                    unknownConfigs);
            configsLoaded = commonOptions.configs;
            commonOptions = optionsParser.getOptions(CommonCommandOptions.class);
        }
        if (!unknownConfigs.isEmpty()) {
            if (commonOptions.allowUndefinedConfigs) {
                eventHandler.handle(Event.warn(
                        "Config values are not defined in any .rc file: " + Joiner.on(", ").join(unknownConfigs)));
            } else {
                throw new OptionsParsingException(
                        "Config values are not defined in any .rc file: " + Joiner.on(", ").join(unknownConfigs));
            }
        }
    }

    /**
     * Parses the options, taking care not to generate any output to outErr, return, or throw an
     * exception.
     *
     * @return ExitCode.SUCCESS if everything went well, or some other value if not
     */
    ExitCode parseOptions(List<String> args, ExtendedEventHandler eventHandler) {
        // The initialization code here was carefully written to parse the options early before we call
        // into the BlazeModule APIs, which means we must not generate any output to outErr, return, or
        // throw an exception. All the events happening here are instead stored in a temporary event
        // handler, and later replayed.
        ExitCode earlyExitCode = checkCwdInWorkspace(eventHandler);
        if (!earlyExitCode.equals(ExitCode.SUCCESS)) {
            return earlyExitCode;
        }

        try {
            parseArgsAndConfigs(args, eventHandler);
            // Allow the command to edit the options.
            command.editOptions(optionsParser);
            // Migration of --watchfs to a command option.
            // TODO(ulfjack): Get rid of the startup option and drop this code.
            if (runtime.getStartupOptionsProvider().getOptions(BlazeServerStartupOptions.class).watchFS) {
                try {
                    optionsParser.parse("--watchfs");
                } catch (OptionsParsingException e) {
                    // This should never happen.
                    throw new IllegalStateException(e);
                }
            }
            // Merge the invocation policy that is user-supplied, from the command line, and any
            // invocation policy that was added by a module. The module one goes 'first,' so the user
            // one has priority.
            InvocationPolicy combinedPolicy = InvocationPolicy.newBuilder()
                    .mergeFrom(runtime.getModuleInvocationPolicy()).mergeFrom(invocationPolicy).build();
            InvocationPolicyEnforcer optionsPolicyEnforcer = new InvocationPolicyEnforcer(combinedPolicy,
                    Level.INFO);
            // Enforce the invocation policy. It is intentional that this is the last step in preparing
            // the options. The invocation policy is used in security-critical contexts, and may be used
            // as a last resort to override flags. That means that the policy can override flags set in
            // BlazeCommand.editOptions, so the code needs to be safe regardless of the actual flag
            // values. At the time of this writing, editOptions was only used as a convenience feature or
            // to improve the user experience, but not required for safety or correctness.
            optionsPolicyEnforcer.enforce(optionsParser, commandName);
            // Print warnings for odd options usage
            for (String warning : optionsParser.getWarnings()) {
                eventHandler.handle(Event.warn(warning));
            }
        } catch (OptionsParsingException e) {
            eventHandler.handle(Event.error(e.getMessage()));
            return ExitCode.COMMAND_LINE_ERROR;
        }
        return ExitCode.SUCCESS;
    }

    /**
     * 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
     * @param unknownConfigs optional; a collection that the method will populate with the config
     *     values in {@code configs} that none of the .rc files had entries for
     * @throws OptionsParsingException
     */
    protected static void parseOptionsForCommand(List<String> rcfileNotes, Command commandAnnotation,
            OptionsParser optionsParser, List<Pair<String, ListMultimap<String, String>>> optionsMap,
            @Nullable Collection<String> configs, @Nullable Collection<String> unknownConfigs)
            throws OptionsParsingException {
        Set<String> knownConfigs = new HashSet<>();
        for (String commandToParse : getCommandNamesToParse(commandAnnotation)) {
            for (Pair<String, ListMultimap<String, String>> entry : optionsMap) {
                String rcFile = entry.first;
                List<String> allOptions = new ArrayList<>();
                if (configs == null) {
                    Collection<String> values = entry.second.get(commandToParse);
                    if (!values.isEmpty()) {
                        allOptions.addAll(entry.second.get(commandToParse));
                        String inherited = commandToParse.equals(commandAnnotation.name()) ? "" : "Inherited ";
                        String source = rcFile.equals("client") ? "Options provided by the client"
                                : String.format("Reading rc options for '%s' from %s", commandAnnotation.name(),
                                        rcFile);
                        rcfileNotes.add(String.format("%s:\n  %s'%s' options: %s", source, inherited,
                                commandToParse, Joiner.on(' ').join(values)));
                    }
                } else {
                    for (String config : configs) {
                        String configDef = commandToParse + ":" + config;
                        Collection<String> values = entry.second.get(configDef);
                        if (!values.isEmpty()) {
                            allOptions.addAll(values);
                            knownConfigs.add(config);
                            rcfileNotes.add(String.format("Found applicable config definition %s in file %s: %s",
                                    configDef, rcFile, String.join(" ", values)));
                        }
                    }
                }
                processOptionList(optionsParser, rcFile, allOptions);
            }
        }
        if (unknownConfigs != null && configs != null && configs.size() > knownConfigs.size()) {
            configs.stream().filter(Predicates.not(Predicates.in(knownConfigs)))
                    .forEachOrdered(unknownConfigs::add);
        }
    }

    // Processes the option list for an .rc file - command pair.
    private static void processOptionList(OptionsParser optionsParser, String rcfile, List<String> rcfileOptions)
            throws OptionsParsingException {
        if (!rcfileOptions.isEmpty()) {
            optionsParser.parse(PriorityCategory.RC_FILE, rcfile, rcfileOptions);
        }
    }

    private static List<String> getCommandNamesToParse(Command commandAnnotation) {
        List<String> result = new ArrayList<>();
        result.add("common");
        getCommandNamesToParseHelper(commandAnnotation, 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 String getNotInRealWorkspaceError(Path doNotBuildFile) {
        String message = String.format("%1$s should not be called from a %1$s output directory. ",
                runtime.getProductName());
        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;
    }

    /**
     * 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(EventHandler eventHandler,
            List<String> rcFiles, List<ClientOptions.OptionOverride> overrides, Set<String> validCommands) {
        List<Pair<String, ListMultimap<String, String>>> result = new ArrayList<>();

        String lastRcFile = null;
        ListMultimap<String, String> lastMap = null;
        for (ClientOptions.OptionOverride override : overrides) {
            if (override.blazeRc < 0 || override.blazeRc >= rcFiles.size()) {
                eventHandler.handle(
                        Event.warn("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")) {
                eventHandler.handle(Event.warn("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;
    }
}