aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/main/java/com/google/devtools/build/lib/runtime/BlazeOptionHandler.java
blob: 25afbf2cd7890469dce7be61ad3eee2bba563d50 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
// 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;
  }
}