Skip to content

Commit 676c8be

Browse files
tempozbduffany
andauthored
CLI: Refactor config parsing to use the new parsing constructs (#9389)
This change is a little odd, since many of the refactored functions in `parser.go` now belong in `parsed.go`, but I elected to make the changes first and then move them in a later PR so as to make viewing the diffs easier when reviewing. Primarily, this change: - Directly uses the options-retrieval abilities of parsed.Args to consume the rc file options - Parses the rc files into `parsed.Config` structs in a map keyed by config name and into a default config struct for unnamed configs (for example, `common` or `build` as opposed to `common:foo` or `build:bar`) - Uses these config structs to expand config options Small behavior changes include that `common:` will no longer be equivalent to `common`, which mirrors bazel's bahavior, and that the `always` phase is now supported. Once this is approved, a follow-on PR will move the functions indicated with the relevant `TODO`s to their appropriate locations with minimal changes as are necessary to move them as described. --------- Co-authored-by: Brandon Duffany <brandon@buildbuddy.io>
1 parent 79ef4a9 commit 676c8be

File tree

5 files changed

+499
-426
lines changed

5 files changed

+499
-426
lines changed

cli/parser/bazelrc/bazelrc.go

Lines changed: 91 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package bazelrc
33
import (
44
"bufio"
55
"fmt"
6+
"maps"
67
"os"
78
"path/filepath"
89
"runtime"
@@ -16,6 +17,9 @@ import (
1617
const (
1718
EnablePlatformSpecificConfigFlag = "enable_platform_specific_config"
1819
workspacePrefix = `%workspace%/`
20+
21+
CommonPhase = "common"
22+
AlwaysPhase = "always"
1923
)
2024

2125
var (
@@ -58,6 +62,18 @@ var (
5862
"coverage": "test",
5963
}
6064

65+
unconditionalCommandPhases = map[string]struct{}{
66+
CommonPhase: {},
67+
AlwaysPhase: {},
68+
}
69+
70+
allPhases = func() map[string]struct{} {
71+
v := maps.Clone(bazelCommands)
72+
maps.Insert(v, maps.All(unconditionalCommandPhases))
73+
v["startup"] = struct{}{}
74+
return v
75+
}()
76+
6177
StartupFlagNoRc = map[string]struct{}{
6278
"ignore_all_rc_files": {},
6379
"home_rc": {},
@@ -69,45 +85,14 @@ var (
6985

7086
// RcRule is a rule parsed from a bazelrc file.
7187
type RcRule struct {
72-
Phase string
73-
Config string
88+
Phase string
89+
// Make Config a pointer to a string so we can distinguish default configs
90+
// from configs with blank names.
91+
Config *string
7492
// Tokens contains the raw (non-canonicalized) tokens in the rule.
7593
Tokens []string
7694
}
7795

78-
type Rules struct {
79-
All []*RcRule
80-
ByPhaseAndConfig map[string]map[string][]*RcRule
81-
}
82-
83-
func StructureRules(rules []*RcRule) *Rules {
84-
r := &Rules{
85-
All: rules,
86-
ByPhaseAndConfig: map[string]map[string][]*RcRule{},
87-
}
88-
for _, rule := range rules {
89-
byConfig := r.ByPhaseAndConfig[rule.Phase]
90-
if byConfig == nil {
91-
byConfig = map[string][]*RcRule{}
92-
r.ByPhaseAndConfig[rule.Phase] = byConfig
93-
}
94-
byConfig[rule.Config] = append(byConfig[rule.Config], rule)
95-
}
96-
return r
97-
}
98-
99-
func (r *Rules) ForPhaseAndConfig(phase, config string) []*RcRule {
100-
byConfig := r.ByPhaseAndConfig[phase]
101-
if byConfig == nil {
102-
return nil
103-
}
104-
return byConfig[config]
105-
}
106-
107-
func (r *RcRule) String() string {
108-
return fmt.Sprintf("phase=%q,config=%q,tokens=%v", r.Phase, r.Config, r.Tokens)
109-
}
110-
11196
// getPhases returns the command's inheritance hierarchy in increasing order of
11297
// precedence.
11398
//
@@ -128,35 +113,42 @@ func GetPhases(command string) (out []string) {
128113
return
129114
}
130115

131-
func appendRcRulesFromImport(workspaceDir, path string, opts []*RcRule, optional bool, importStack []string) ([]*RcRule, error) {
116+
func appendRcRulesFromImport(workspaceDir, path string, namedConfigs map[string]map[string][]string, defaultConfig map[string][]string, optional bool, importStack []string) error {
132117
if strings.HasPrefix(path, workspacePrefix) {
133118
path = filepath.Join(workspaceDir, path[len(workspacePrefix):])
134119
}
135-
136-
file, err := os.Open(path)
120+
realPath, err := Realpath(path)
137121
if err != nil {
138122
if optional {
139-
return opts, nil
123+
return nil
140124
}
141-
return nil, err
125+
return fmt.Errorf("could not determine real path of bazelrc file: %s", err)
142126
}
143-
defer file.Close()
144-
return appendRcRulesFromFile(workspaceDir, file, opts, importStack)
127+
128+
return AppendRcRulesFromFile(workspaceDir, realPath, namedConfigs, defaultConfig, importStack, optional)
145129
}
146130

147-
func appendRcRulesFromFile(workspaceDir string, f *os.File, rules []*RcRule, importStack []string) ([]*RcRule, error) {
148-
rpath, err := realpath(f.Name())
149-
if err != nil {
150-
return nil, fmt.Errorf("could not determine real path of bazelrc file: %s", err)
131+
// AppendRCRulesFromFile reads and lexes the provided rc file and appends the
132+
// args to the provided configs based on the detected phase and name.
133+
//
134+
// configs is a map keyed by config name where the values are maps keyed by
135+
// phase name where the values are lists containing all the rules for that
136+
// config in the order they are encountered.
137+
func AppendRcRulesFromFile(workspaceDir string, realPath string, namedConfigs map[string]map[string][]string, defaultConfig map[string][]string, importStack []string, optional bool) error {
138+
if slices.Contains(importStack, realPath) {
139+
return fmt.Errorf("circular import detected: %s -> %s", strings.Join(importStack, " -> "), realPath)
151140
}
152-
for _, path := range importStack {
153-
if path == rpath {
154-
return nil, fmt.Errorf("circular import detected: %s -> %s", strings.Join(importStack, " -> "), rpath)
141+
importStack = append(importStack, realPath)
142+
file, err := os.Open(realPath)
143+
if err != nil {
144+
if optional {
145+
return nil
155146
}
147+
return err
156148
}
157-
importStack = append(importStack, rpath)
149+
defer file.Close()
158150

159-
scanner := bufio.NewScanner(f)
151+
scanner := bufio.NewScanner(file)
160152
for scanner.Scan() {
161153
line := scanner.Text()
162154
// Handle line continuations (lines can end with "\" to effectively
@@ -175,9 +167,8 @@ func appendRcRulesFromFile(workspaceDir string, f *os.File, rules []*RcRule, imp
175167
if tokens[0] == "import" || tokens[0] == "try-import" {
176168
isOptional := tokens[0] == "try-import"
177169
path := strings.TrimSpace(strings.TrimPrefix(line, tokens[0]))
178-
rules, err = appendRcRulesFromImport(workspaceDir, path, rules, isOptional, importStack)
179-
if err != nil {
180-
return nil, err
170+
if err := appendRcRulesFromImport(workspaceDir, path, namedConfigs, defaultConfig, isOptional, importStack); err != nil {
171+
return err
181172
}
182173
continue
183174
}
@@ -190,18 +181,29 @@ func appendRcRulesFromFile(workspaceDir string, f *os.File, rules []*RcRule, imp
190181
if rule == nil {
191182
continue
192183
}
193-
// Bazel doesn't support configs for startup options and ignores them if
194-
// they appear in a bazelrc: https://bazel.build/run/bazelrc#config
195-
if rule.Phase == "startup" && rule.Config != "" {
184+
if rule.Config == nil {
185+
defaultConfig[rule.Phase] = append(defaultConfig[rule.Phase], rule.Tokens...)
186+
continue
187+
}
188+
// Bazel doesn't support named configs for startup options and ignores them
189+
// if they appear in a bazelrc: https://bazel.build/run/bazelrc#config
190+
if rule.Phase == "startup" {
196191
continue
197192
}
198-
rules = append(rules, rule)
193+
config, ok := namedConfigs[*rule.Config]
194+
if !ok {
195+
config = make(map[string][]string)
196+
namedConfigs[*rule.Config] = config
197+
}
198+
config[rule.Phase] = append(config[rule.Phase], rule.Tokens...)
199199
}
200-
log.Debugf("Adding rc rules from %q: %v", rpath, rules)
201-
return rules, scanner.Err()
200+
log.Debugf("Added rc rules from %q; new configs: %#v", realPath, namedConfigs)
201+
return scanner.Err()
202202
}
203203

204-
func realpath(path string) (string, error) {
204+
// Realpath evaluates any symlinks in the given path and then returns the
205+
// absolute path.
206+
func Realpath(path string) (string, error) {
205207
directPath, err := filepath.EvalSymlinks(path)
206208
if err != nil {
207209
return "", err
@@ -229,55 +231,24 @@ func parseRcRule(line string) (*RcRule, error) {
229231
// bazel ignores .bazelrc lines consisting of a single shlex token
230232
return nil, nil
231233
}
232-
if strings.HasPrefix(tokens[0], "-") {
233-
return &RcRule{
234-
Phase: "common",
235-
Tokens: tokens,
236-
}, nil
237-
}
238-
if !strings.Contains(tokens[0], ":") {
239-
return &RcRule{
240-
Phase: tokens[0],
241-
Tokens: tokens[1:],
242-
}, nil
243-
}
244-
phaseConfig := strings.Split(tokens[0], ":")
245-
if len(phaseConfig) != 2 {
246-
return nil, fmt.Errorf("invalid bazelrc syntax: %s", phaseConfig)
234+
phase := tokens[0]
235+
var configName *string
236+
if colonIndex := strings.Index(tokens[0], ":"); colonIndex != -1 {
237+
phase = tokens[0][:colonIndex]
238+
v := tokens[0][colonIndex+1:]
239+
configName = &v
247240
}
241+
248242
return &RcRule{
249-
Phase: phaseConfig[0],
250-
Config: phaseConfig[1],
243+
Phase: phase,
244+
Config: configName,
251245
Tokens: tokens[1:],
252246
}, nil
253247
}
254248

255-
func ParseRCFiles(workspaceDir string, filePaths ...string) ([]*RcRule, error) {
256-
opts := make([]*RcRule, 0)
257-
seen := map[string]bool{}
258-
for _, filePath := range filePaths {
259-
r, err := realpath(filePath)
260-
if err != nil {
261-
continue
262-
}
263-
if seen[r] {
264-
continue
265-
}
266-
seen[r] = true
267-
268-
file, err := os.Open(filePath)
269-
if err != nil {
270-
continue
271-
}
272-
defer file.Close()
273-
opts, err = appendRcRulesFromFile(workspaceDir, file, opts, nil /*=importStack*/)
274-
if err != nil {
275-
return nil, err
276-
}
277-
}
278-
return opts, nil
279-
}
280-
249+
// GetBazelOS returns the os string that `enable_platform_specific_config` will
250+
// expect based on the detected runtime.GOOS.
251+
//
281252
// Mirroring the behavior here:
282253
// https://github.com/bazelbuild/bazel/blob/master/src/main/java/com/google/devtools/build/lib/runtime/ConfigExpander.java#L41
283254
func GetBazelOS() string {
@@ -297,12 +268,28 @@ func GetBazelOS() string {
297268
}
298269
}
299270

271+
// IsBazelCommand returns whether the given string is recognized as a bazel
272+
// command.
300273
func IsBazelCommand(command string) bool {
301274
_, ok := bazelCommands[command]
302275
return ok
303276
}
304277

278+
// Parent returns the parent command of the given command, if one exists.
305279
func Parent(command string) (string, bool) {
306280
parent, ok := parentCommand[command]
307281
return parent, ok
308282
}
283+
284+
// IsUnconditionalCommandPhase returns whether or not this is a phase that should always
285+
// be evaluated, regardless of the command.
286+
func IsUnconditionalCommandPhase(phase string) bool {
287+
_, ok := unconditionalCommandPhases[phase]
288+
return ok
289+
}
290+
291+
// IsPhase returns whether or not this is a valid phase for a bazel rc line.
292+
func IsPhase(phase string) bool {
293+
_, ok := allPhases[phase]
294+
return ok
295+
}

cli/parser/parsed/parsed.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -538,15 +538,14 @@ func (a *OrderedArgs) appendOption(option options.Option, startupOptionInsertInd
538538
return startupOptionInsertIndex, commandOptionInsertIndex, fmt.Errorf("Failed to append Option: option '%s' is not a startup option and the command '%s' does not support it.", option.Name(), command)
539539
}
540540

541-
// Offset exists as a temporary hack while we continue to move away from
542-
// passing arguments around as strings. It returns the offset in the
543-
// string-based arg slice where the argument at this index is located.
544-
func (a *OrderedArgs) Offset(index int) int {
545-
offset := 0
546-
for _, arg := range a.Args[:index] {
547-
offset += len(arg.Format())
541+
type Config struct {
542+
ByPhase map[string][]arguments.Argument
543+
}
544+
545+
func NewConfig() *Config {
546+
return &Config{
547+
ByPhase: make(map[string][]arguments.Argument),
548548
}
549-
return offset
550549
}
551550

552551
func Partition(args []arguments.Argument) *PartitionedArgs {
@@ -670,6 +669,8 @@ func (a *PartitionedArgs) RemoveOptions(optionNames ...string) []*IndexedOption
670669
func (a *PartitionedArgs) Append(args ...arguments.Argument) error {
671670
for _, arg := range args {
672671
switch arg := arg.(type) {
672+
case *arguments.DoubleDash:
673+
// skip
673674
case *arguments.PositionalArgument:
674675
switch {
675676
case a.Command == nil:

0 commit comments

Comments
 (0)