@@ -5,6 +5,7 @@ package parser
5
5
import (
6
6
"bufio"
7
7
"bytes"
8
+ "encoding/base64"
8
9
"fmt"
9
10
"io"
10
11
"os"
@@ -21,8 +22,10 @@ import (
21
22
"github.com/buildbuddy-io/buildbuddy/cli/workspace"
22
23
"github.com/buildbuddy-io/buildbuddy/server/remote_cache/digest"
23
24
"github.com/buildbuddy-io/buildbuddy/server/util/disk"
25
+ "github.com/buildbuddy-io/buildbuddy/server/util/proto"
24
26
"github.com/google/shlex"
25
27
28
+ bfpb "github.com/buildbuddy-io/buildbuddy/proto/bazel_flags"
26
29
repb "github.com/buildbuddy-io/buildbuddy/proto/remote_execution"
27
30
)
28
31
82
85
flagShortNamePattern = regexp .MustCompile (`^[a-z]$` )
83
86
)
84
87
88
+ // Before Bazel 7, the flag protos did not contain the `RequiresValue` field,
89
+ // so there is no way to identify expansion options, which must be parsed
90
+ // differently. Since there are only nineteen such options (and bazel 6 is
91
+ // currently only receiving maintenance support and thus unlikely to add new
92
+ // expansion options), we can just enumerate them here so that we can correctly
93
+ // identify them in the absence of that field.
94
+ var preBazel7ExpansionOptions = map [string ]struct {}{
95
+ "noincompatible_genquery_use_graphless_query" : struct {}{},
96
+ "incompatible_genquery_use_graphless_query" : struct {}{},
97
+ "persistent_android_resource_processor" : struct {}{},
98
+ "persistent_multiplex_android_resource_processor" : struct {}{},
99
+ "persistent_android_dex_desugar" : struct {}{},
100
+ "persistent_multiplex_android_dex_desugar" : struct {}{},
101
+ "start_app" : struct {}{},
102
+ "debug_app" : struct {}{},
103
+ "java_debug" : struct {}{},
104
+ "remote_download_minimal" : struct {}{},
105
+ "remote_download_toplevel" : struct {}{},
106
+ "long" : struct {}{},
107
+ "short" : struct {}{},
108
+ "expunge_async" : struct {}{},
109
+ "experimental_spawn_scheduler" : struct {}{},
110
+ "experimental_persistent_javac" : struct {}{},
111
+ "null" : struct {}{},
112
+ "order_results" : struct {}{},
113
+ "noorder_results" : struct {}{},
114
+ }
115
+
85
116
// OptionSet contains a set of Option schemas, indexed for ease of parsing.
86
117
type OptionSet struct {
87
118
All []* Option
@@ -138,10 +169,19 @@ func (s *OptionSet) Next(args []string, start int) (option *Option, value string
138
169
longName = longName [:eqIndex ]
139
170
option = s .ByName [longName ]
140
171
optValue = & v
141
- // Unlike command options, startup options don't allow specifying
142
- // booleans as --name=0, --name=false etc.
143
- if s .IsStartupOptions && option != nil && option .BoolLike {
144
- return nil , "" , - 1 , fmt .Errorf ("in option %q: option %q does not take a value" , startToken , option .Name )
172
+ if option != nil && ! option .RequiresValue {
173
+ // Unlike command options, startup options don't allow specifying
174
+ // values for options that do not require values.
175
+ if s .IsStartupOptions {
176
+ return nil , "" , - 1 , fmt .Errorf ("in option %q: option %q does not take a value" , startToken , option .Name )
177
+ }
178
+ // Boolean options may specify values, but expansion options ignore
179
+ // values and output a warning. Since we canonicalize the options and
180
+ // remove the value ourselves, we should output the warning instead.
181
+ if ! option .HasNegative {
182
+ log .Warnf ("option '--%s' is an expansion option. It does not accept values, and does not change its expansion based on the value provided. Value '%s' will be ignored." , option .Name , * optValue )
183
+ optValue = nil
184
+ }
145
185
}
146
186
} else {
147
187
option = s .ByName [longName ]
@@ -150,7 +190,7 @@ func (s *OptionSet) Next(args []string, start int) (option *Option, value string
150
190
if option == nil && strings .HasPrefix (longName , "no" ) {
151
191
longName := strings .TrimPrefix (longName , "no" )
152
192
option = s .ByName [longName ]
153
- if option != nil && ! option .BoolLike {
193
+ if option != nil && ! option .HasNegative {
154
194
return nil , "" , - 1 , fmt .Errorf ("illegal use of 'no' prefix on non-boolean option: %s" , startToken )
155
195
}
156
196
v := "0"
@@ -171,8 +211,11 @@ func (s *OptionSet) Next(args []string, start int) (option *Option, value string
171
211
}
172
212
next = start + 1
173
213
if optValue == nil {
174
- if option .BoolLike {
175
- v := "1"
214
+ if ! option .RequiresValue {
215
+ v := ""
216
+ if option .HasNegative {
217
+ v = "1"
218
+ }
176
219
optValue = & v
177
220
} else {
178
221
if start + 1 >= len (args ) {
@@ -184,7 +227,7 @@ func (s *OptionSet) Next(args []string, start int) (option *Option, value string
184
227
}
185
228
}
186
229
// Canonicalize boolean values.
187
- if option .BoolLike {
230
+ if option .HasNegative {
188
231
if * optValue == "false" || * optValue == "no" {
189
232
* optValue = "0"
190
233
} else if * optValue == "true" || * optValue == "yes" {
@@ -197,18 +240,26 @@ func (s *OptionSet) Next(args []string, start int) (option *Option, value string
197
240
// formatoption returns a canonical representation of an option name=value
198
241
// assignment as a single token.
199
242
func formatOption (option * Option , value string ) string {
200
- if option .BoolLike {
201
- // We use "--name" or "--noname" as the canonical representation for
202
- // bools, since these are the only formats allowed for startup options.
203
- // Subcommands like "build" and "run" do allow other formats like
204
- // "--name=true" or "--name=0", but we choose to stick with the lowest
205
- // common demoninator between subcommands and startup options here,
206
- // mainly to avoid confusion.
207
- if value == "1" {
208
- return "--" + option .Name
209
- }
243
+ if option .RequiresValue {
244
+ return "--" + option .Name + "=" + value
245
+ }
246
+ if ! option .HasNegative {
247
+ return "--" + option .Name
248
+ }
249
+ // We use "--name" or "--noname" as the canonical representation for
250
+ // bools, since these are the only formats allowed for startup options.
251
+ // Subcommands like "build" and "run" do allow other formats like
252
+ // "--name=true" or "--name=0", but we choose to stick with the lowest
253
+ // common demoninator between subcommands and startup options here,
254
+ // mainly to avoid confusion.
255
+ if value == "1" || value == "true" || value == "yes" || value == "" {
256
+ return "--" + option .Name
257
+ }
258
+ if value == "0" || value == "false" || value == "no" {
210
259
return "--no" + option .Name
211
260
}
261
+ // Account for flags that have negative forms, but also accept non-boolean
262
+ // arguments, like `--subcommands=pretty_print`
212
263
return "--" + option .Name + "=" + value
213
264
}
214
265
@@ -229,17 +280,16 @@ type Option struct {
229
280
// Each occurrence of the flag value is accumulated in a list.
230
281
Multi bool
231
282
232
- // BoolLike specifies whether the flag uses Bazel's "boolean value syntax"
233
- // [1]. Options that are bool-like allow a "no" prefix to be used in order
234
- // to set the value to false.
235
- //
236
- // BoolLike flags are also parsed differently. Their name and value, if any,
237
- // must appear as a single token, which means the "=" syntax has to be used
238
- // when assigning a value. For example, "bazel build --subcommands false" is
239
- // actually equivalent to "bazel build --subcommands=true //false:false".
240
- //
241
- // [1]: https://github.com/bazelbuild/bazel/blob/824ecba998a573198c1fe07c8bf87ead680aae92/src/main/java/com/google/devtools/common/options/OptionDefinition.java#L255-L264
242
- BoolLike bool
283
+ // HasNegative specifies whether the flag allows a "no" prefix" to be used in
284
+ // order to set the value to false.
285
+ HasNegative bool
286
+
287
+ // Flags that do not require a value must be parsed differently. Their name
288
+ // and value, if any,must appear as a single token, which means the "=" syntax
289
+ // has to be used when assigning a value. For example, "bazel build
290
+ // --subcommands false" is actually equivalent to "bazel build
291
+ // --subcommands=true //false:false".
292
+ RequiresValue bool
243
293
}
244
294
245
295
// BazelHelpFunc returns the output of "bazel help <topic>". This output is
@@ -279,10 +329,11 @@ func parseHelpLine(line, topic string) *Option {
279
329
}
280
330
281
331
return & Option {
282
- Name : name ,
283
- ShortName : shortName ,
284
- Multi : multi ,
285
- BoolLike : no != "" || description == "" ,
332
+ Name : name ,
333
+ ShortName : shortName ,
334
+ Multi : multi ,
335
+ HasNegative : no != "" ,
336
+ RequiresValue : no == "" && description != "" ,
286
337
}
287
338
}
288
339
@@ -350,15 +401,111 @@ func (s *CommandLineSchema) CommandSupportsOpt(opt string) bool {
350
401
return false
351
402
}
352
403
404
+ // DecodeHelpFlagsAsProto takes the output of `bazel help flags-as-proto` and
405
+ // returns the FlagCollection proto message it encodes.
406
+ func DecodeHelpFlagsAsProto (protoHelp string ) (* bfpb.FlagCollection , error ) {
407
+ b , err := base64 .StdEncoding .DecodeString (protoHelp )
408
+ if err != nil {
409
+ return nil , err
410
+ }
411
+ flagCollection := & bfpb.FlagCollection {}
412
+ if err := proto .Unmarshal (b , flagCollection ); err != nil {
413
+ return nil , err
414
+ }
415
+ return flagCollection , nil
416
+ }
417
+
418
+ // GetOptionSetsFromProto takes a FlagCollection proto message, converts it into
419
+ // Options, places each option into OptionSets based on the commands it
420
+ // specifies (creating new OptionSets if necessary), and then returns a map
421
+ // such that those OptionSets are keyed by the associated command (or "startup"
422
+ // in the case of startup options).
423
+ func GetOptionSetsfromProto (flagCollection * bfpb.FlagCollection ) (map [string ]* OptionSet , error ) {
424
+ sets := make (map [string ]* OptionSet )
425
+ for _ , info := range flagCollection .FlagInfos {
426
+ if info .GetName () == "bazelrc" {
427
+ // `bazel help flags-as-proto` incorrectly reports `bazelrc` as not
428
+ // allowing multiple values.
429
+ // See https://github.com/bazelbuild/bazel/issues/24730 for more info.
430
+ v := true
431
+ info .AllowsMultiple = & v
432
+ }
433
+ if info .GetName () == "experimental_convenience_symlinks" || info .GetName () == "subcommands" {
434
+ // `bazel help flags-as-proto` incorrectly reports
435
+ // `experimental_convenience_symlinks` and `subcommands` as not
436
+ // having negative forms.
437
+ // See https://github.com/bazelbuild/bazel/issues/24882 for more info.
438
+ v := true
439
+ info .HasNegativeFlag = & v
440
+ }
441
+ if info .RequiresValue == nil {
442
+ // If flags-as-proto does not support RequiresValue, mark flags with
443
+ // negative forms and known expansion flags as not requiring values, and
444
+ // mark all other flags as requiring values.
445
+ if info .GetHasNegativeFlag () {
446
+ v := false
447
+ info .RequiresValue = & v
448
+ } else if _ , ok := preBazel7ExpansionOptions [info .GetName ()]; ok {
449
+ v := false
450
+ info .RequiresValue = & v
451
+ } else {
452
+ v := true
453
+ info .RequiresValue = & v
454
+ }
455
+ }
456
+ o := & Option {
457
+ Name : info .GetName (),
458
+ ShortName : info .GetAbbreviation (),
459
+ Multi : info .GetAllowsMultiple (),
460
+ HasNegative : info .GetHasNegativeFlag (),
461
+ RequiresValue : info .GetRequiresValue (),
462
+ }
463
+ for _ , cmd := range info .GetCommands () {
464
+ var set * OptionSet
465
+ var ok bool
466
+ if set , ok = sets [cmd ]; ! ok {
467
+ set = & OptionSet {
468
+ All : []* Option {},
469
+ ByName : make (map [string ]* Option ),
470
+ ByShortName : make (map [string ]* Option ),
471
+ }
472
+ sets [cmd ] = set
473
+ }
474
+ set .All = append (set .All , o )
475
+ set .ByName [o .Name ] = o
476
+ if o .ShortName != "" {
477
+ set .ByShortName [o .ShortName ] = o
478
+ }
479
+ }
480
+ }
481
+ return sets , nil
482
+ }
483
+
353
484
// GetCommandLineSchema returns the effective CommandLineSchemas for the given
354
485
// command line.
355
486
func getCommandLineSchema (args []string , bazelHelp BazelHelpFunc , onlyStartupOptions bool ) (* CommandLineSchema , error ) {
356
- startupHelp , err := bazelHelp ("startup_options" )
357
- if err != nil {
358
- return nil , err
487
+ var optionSets map [string ]* OptionSet
488
+ // try flags-as-proto first; fall back to parsing help if bazel version does not support it.
489
+ if protoHelp , err := bazelHelp ("flags-as-proto" ); err == nil {
490
+ flagCollection , err := DecodeHelpFlagsAsProto (protoHelp )
491
+ if err != nil {
492
+ return nil , err
493
+ }
494
+ sets , err := GetOptionSetsfromProto (flagCollection )
495
+ if err != nil {
496
+ return nil , err
497
+ }
498
+ optionSets = sets
359
499
}
360
- schema := & CommandLineSchema {
361
- StartupOptions : parseBazelHelp (startupHelp , "startup_options" ),
500
+ schema := & CommandLineSchema {}
501
+ if startupOptions , ok := optionSets ["startup" ]; ok {
502
+ schema .StartupOptions = startupOptions
503
+ } else {
504
+ startupHelp , err := bazelHelp ("startup_options" )
505
+ if err != nil {
506
+ return nil , err
507
+ }
508
+ schema .StartupOptions = parseBazelHelp (startupHelp , "startup_options" )
362
509
}
363
510
bazelCommands , err := BazelCommands ()
364
511
if err != nil {
@@ -394,11 +541,15 @@ func getCommandLineSchema(args []string, bazelHelp BazelHelpFunc, onlyStartupOpt
394
541
if schema .Command == "" {
395
542
return schema , nil
396
543
}
397
- commandHelp , err := bazelHelp (schema .Command )
398
- if err != nil {
399
- return nil , err
544
+ if commandOptions , ok := optionSets [schema .Command ]; ok {
545
+ schema .CommandOptions = commandOptions
546
+ } else {
547
+ commandHelp , err := bazelHelp (schema .Command )
548
+ if err != nil {
549
+ return nil , err
550
+ }
551
+ schema .CommandOptions = parseBazelHelp (commandHelp , schema .Command )
400
552
}
401
- schema .CommandOptions = parseBazelHelp (commandHelp , schema .Command )
402
553
return schema , nil
403
554
}
404
555
0 commit comments