| Sam Delmerico | 7f88956 | 2022-03-25 14:55:40 +0000 | [diff] [blame] | 1 | // Copyright 2021 Google Inc. All rights reserved. | 
|  | 2 | // | 
|  | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | 4 | // you may not use this file except in compliance with the License. | 
|  | 5 | // You may obtain a copy of the License at | 
|  | 6 | // | 
|  | 7 | //     http://www.apache.org/licenses/LICENSE-2.0 | 
|  | 8 | // | 
|  | 9 | // Unless required by applicable law or agreed to in writing, software | 
|  | 10 | // distributed under the License is distributed on an "AS IS" BASIS, | 
|  | 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
|  | 12 | // See the License for the specific language governing permissions and | 
|  | 13 | // limitations under the License. | 
|  | 14 |  | 
|  | 15 | package android | 
|  | 16 |  | 
|  | 17 | import ( | 
|  | 18 | "fmt" | 
|  | 19 | "reflect" | 
|  | 20 | "regexp" | 
|  | 21 | "sort" | 
|  | 22 | "strings" | 
|  | 23 |  | 
|  | 24 | "android/soong/bazel" | 
|  | 25 | "android/soong/starlark_fmt" | 
|  | 26 |  | 
|  | 27 | "github.com/google/blueprint" | 
|  | 28 | ) | 
|  | 29 |  | 
|  | 30 | // BazelVarExporter is a collection of configuration variables that can be exported for use in Bazel rules | 
|  | 31 | type BazelVarExporter interface { | 
|  | 32 | // asBazel expands strings of configuration variables into their concrete values | 
|  | 33 | asBazel(Config, ExportedStringVariables, ExportedStringListVariables, ExportedConfigDependingVariables) []bazelConstant | 
|  | 34 | } | 
|  | 35 |  | 
|  | 36 | // ExportedVariables is a collection of interdependent configuration variables | 
|  | 37 | type ExportedVariables struct { | 
|  | 38 | // Maps containing toolchain variables that are independent of the | 
|  | 39 | // environment variables of the build. | 
|  | 40 | exportedStringVars         ExportedStringVariables | 
|  | 41 | exportedStringListVars     ExportedStringListVariables | 
|  | 42 | exportedStringListDictVars ExportedStringListDictVariables | 
|  | 43 |  | 
|  | 44 | exportedVariableReferenceDictVars ExportedVariableReferenceDictVariables | 
|  | 45 |  | 
|  | 46 | /// Maps containing variables that are dependent on the build config. | 
|  | 47 | exportedConfigDependingVars ExportedConfigDependingVariables | 
|  | 48 |  | 
|  | 49 | pctx PackageContext | 
|  | 50 | } | 
|  | 51 |  | 
|  | 52 | // NewExportedVariables creats an empty ExportedVariables struct with non-nil maps | 
|  | 53 | func NewExportedVariables(pctx PackageContext) ExportedVariables { | 
|  | 54 | return ExportedVariables{ | 
|  | 55 | exportedStringVars:                ExportedStringVariables{}, | 
|  | 56 | exportedStringListVars:            ExportedStringListVariables{}, | 
|  | 57 | exportedStringListDictVars:        ExportedStringListDictVariables{}, | 
|  | 58 | exportedVariableReferenceDictVars: ExportedVariableReferenceDictVariables{}, | 
|  | 59 | exportedConfigDependingVars:       ExportedConfigDependingVariables{}, | 
|  | 60 | pctx:                              pctx, | 
|  | 61 | } | 
|  | 62 | } | 
|  | 63 |  | 
|  | 64 | func (ev ExportedVariables) asBazel(config Config, | 
|  | 65 | stringVars ExportedStringVariables, stringListVars ExportedStringListVariables, cfgDepVars ExportedConfigDependingVariables) []bazelConstant { | 
|  | 66 | ret := []bazelConstant{} | 
|  | 67 | ret = append(ret, ev.exportedStringVars.asBazel(config, stringVars, stringListVars, cfgDepVars)...) | 
|  | 68 | ret = append(ret, ev.exportedStringListVars.asBazel(config, stringVars, stringListVars, cfgDepVars)...) | 
|  | 69 | ret = append(ret, ev.exportedStringListDictVars.asBazel(config, stringVars, stringListVars, cfgDepVars)...) | 
|  | 70 | // Note: ExportedVariableReferenceDictVars collections can only contain references to other variables and must be printed last | 
|  | 71 | ret = append(ret, ev.exportedVariableReferenceDictVars.asBazel(config, stringVars, stringListVars, cfgDepVars)...) | 
| Sam Delmerico | b9b6c67 | 2022-10-21 11:52:18 -0400 | [diff] [blame] | 72 | ret = append(ret, ev.exportedConfigDependingVars.asBazel(config, stringVars, stringListVars, cfgDepVars)...) | 
| Sam Delmerico | 7f88956 | 2022-03-25 14:55:40 +0000 | [diff] [blame] | 73 | return ret | 
|  | 74 | } | 
|  | 75 |  | 
|  | 76 | // ExportStringStaticVariable declares a static string variable and exports it to | 
|  | 77 | // Bazel's toolchain. | 
|  | 78 | func (ev ExportedVariables) ExportStringStaticVariable(name string, value string) { | 
|  | 79 | ev.pctx.StaticVariable(name, value) | 
|  | 80 | ev.exportedStringVars.set(name, value) | 
|  | 81 | } | 
|  | 82 |  | 
|  | 83 | // ExportStringListStaticVariable declares a static variable and exports it to | 
|  | 84 | // Bazel's toolchain. | 
|  | 85 | func (ev ExportedVariables) ExportStringListStaticVariable(name string, value []string) { | 
|  | 86 | ev.pctx.StaticVariable(name, strings.Join(value, " ")) | 
|  | 87 | ev.exportedStringListVars.set(name, value) | 
|  | 88 | } | 
|  | 89 |  | 
|  | 90 | // ExportVariableConfigMethod declares a variable whose value is evaluated at | 
|  | 91 | // runtime via a function with access to the Config and exports it to Bazel's | 
|  | 92 | // toolchain. | 
|  | 93 | func (ev ExportedVariables) ExportVariableConfigMethod(name string, method interface{}) blueprint.Variable { | 
|  | 94 | ev.exportedConfigDependingVars.set(name, method) | 
|  | 95 | return ev.pctx.VariableConfigMethod(name, method) | 
|  | 96 | } | 
|  | 97 |  | 
|  | 98 | // ExportSourcePathVariable declares a static "source path" variable and exports | 
|  | 99 | // it to Bazel's toolchain. | 
|  | 100 | func (ev ExportedVariables) ExportSourcePathVariable(name string, value string) { | 
|  | 101 | ev.pctx.SourcePathVariable(name, value) | 
|  | 102 | ev.exportedStringVars.set(name, value) | 
|  | 103 | } | 
|  | 104 |  | 
| Sam Delmerico | 932c01c | 2022-03-25 16:33:26 +0000 | [diff] [blame] | 105 | // ExportVariableFuncVariable declares a variable whose value is evaluated at | 
|  | 106 | // runtime via a function and exports it to Bazel's toolchain. | 
|  | 107 | func (ev ExportedVariables) ExportVariableFuncVariable(name string, f func() string) { | 
|  | 108 | ev.exportedConfigDependingVars.set(name, func(config Config) string { | 
|  | 109 | return f() | 
|  | 110 | }) | 
|  | 111 | ev.pctx.VariableFunc(name, func(PackageVarContext) string { | 
|  | 112 | return f() | 
|  | 113 | }) | 
|  | 114 | } | 
|  | 115 |  | 
| Sam Delmerico | 7f88956 | 2022-03-25 14:55:40 +0000 | [diff] [blame] | 116 | // ExportString only exports a variable to Bazel, but does not declare it in Soong | 
|  | 117 | func (ev ExportedVariables) ExportString(name string, value string) { | 
|  | 118 | ev.exportedStringVars.set(name, value) | 
|  | 119 | } | 
|  | 120 |  | 
|  | 121 | // ExportStringList only exports a variable to Bazel, but does not declare it in Soong | 
|  | 122 | func (ev ExportedVariables) ExportStringList(name string, value []string) { | 
|  | 123 | ev.exportedStringListVars.set(name, value) | 
|  | 124 | } | 
|  | 125 |  | 
|  | 126 | // ExportStringListDict only exports a variable to Bazel, but does not declare it in Soong | 
|  | 127 | func (ev ExportedVariables) ExportStringListDict(name string, value map[string][]string) { | 
|  | 128 | ev.exportedStringListDictVars.set(name, value) | 
|  | 129 | } | 
|  | 130 |  | 
|  | 131 | // ExportVariableReferenceDict only exports a variable to Bazel, but does not declare it in Soong | 
|  | 132 | func (ev ExportedVariables) ExportVariableReferenceDict(name string, value map[string]string) { | 
|  | 133 | ev.exportedVariableReferenceDictVars.set(name, value) | 
|  | 134 | } | 
|  | 135 |  | 
|  | 136 | // ExportedConfigDependingVariables is a mapping of variable names to functions | 
|  | 137 | // of type func(config Config) string which return the runtime-evaluated string | 
|  | 138 | // value of a particular variable | 
|  | 139 | type ExportedConfigDependingVariables map[string]interface{} | 
|  | 140 |  | 
|  | 141 | func (m ExportedConfigDependingVariables) set(k string, v interface{}) { | 
|  | 142 | m[k] = v | 
|  | 143 | } | 
|  | 144 |  | 
| Sam Delmerico | b9b6c67 | 2022-10-21 11:52:18 -0400 | [diff] [blame] | 145 | func (m ExportedConfigDependingVariables) asBazel(config Config, | 
|  | 146 | stringVars ExportedStringVariables, stringListVars ExportedStringListVariables, cfgDepVars ExportedConfigDependingVariables) []bazelConstant { | 
|  | 147 | ret := make([]bazelConstant, 0, len(m)) | 
|  | 148 | for variable, unevaluatedVar := range m { | 
|  | 149 | evalFunc := reflect.ValueOf(unevaluatedVar) | 
|  | 150 | validateVariableMethod(variable, evalFunc) | 
|  | 151 | evaluatedResult := evalFunc.Call([]reflect.Value{reflect.ValueOf(config)}) | 
|  | 152 | evaluatedValue := evaluatedResult[0].Interface().(string) | 
|  | 153 | expandedVars, err := expandVar(config, evaluatedValue, stringVars, stringListVars, cfgDepVars) | 
|  | 154 | if err != nil { | 
|  | 155 | panic(fmt.Errorf("error expanding config variable %s: %s", variable, err)) | 
|  | 156 | } | 
|  | 157 | if len(expandedVars) > 1 { | 
|  | 158 | ret = append(ret, bazelConstant{ | 
|  | 159 | variableName:       variable, | 
|  | 160 | internalDefinition: starlark_fmt.PrintStringList(expandedVars, 0), | 
|  | 161 | }) | 
|  | 162 | } else { | 
|  | 163 | ret = append(ret, bazelConstant{ | 
|  | 164 | variableName:       variable, | 
|  | 165 | internalDefinition: fmt.Sprintf(`"%s"`, validateCharacters(expandedVars[0])), | 
|  | 166 | }) | 
|  | 167 | } | 
|  | 168 | } | 
|  | 169 | return ret | 
|  | 170 | } | 
|  | 171 |  | 
| Sam Delmerico | 7f88956 | 2022-03-25 14:55:40 +0000 | [diff] [blame] | 172 | // Ensure that string s has no invalid characters to be generated into the bzl file. | 
|  | 173 | func validateCharacters(s string) string { | 
|  | 174 | for _, c := range []string{`\n`, `"`, `\`} { | 
|  | 175 | if strings.Contains(s, c) { | 
|  | 176 | panic(fmt.Errorf("%s contains illegal character %s", s, c)) | 
|  | 177 | } | 
|  | 178 | } | 
|  | 179 | return s | 
|  | 180 | } | 
|  | 181 |  | 
|  | 182 | type bazelConstant struct { | 
|  | 183 | variableName       string | 
|  | 184 | internalDefinition string | 
|  | 185 | sortLast           bool | 
|  | 186 | } | 
|  | 187 |  | 
|  | 188 | // ExportedStringVariables is a mapping of variable names to string values | 
|  | 189 | type ExportedStringVariables map[string]string | 
|  | 190 |  | 
|  | 191 | func (m ExportedStringVariables) set(k string, v string) { | 
|  | 192 | m[k] = v | 
|  | 193 | } | 
|  | 194 |  | 
|  | 195 | func (m ExportedStringVariables) asBazel(config Config, | 
|  | 196 | stringVars ExportedStringVariables, stringListVars ExportedStringListVariables, cfgDepVars ExportedConfigDependingVariables) []bazelConstant { | 
|  | 197 | ret := make([]bazelConstant, 0, len(m)) | 
|  | 198 | for k, variableValue := range m { | 
|  | 199 | expandedVar, err := expandVar(config, variableValue, stringVars, stringListVars, cfgDepVars) | 
|  | 200 | if err != nil { | 
|  | 201 | panic(fmt.Errorf("error expanding config variable %s: %s", k, err)) | 
|  | 202 | } | 
|  | 203 | if len(expandedVar) > 1 { | 
| Liz Kammer | ba5f32a | 2023-10-16 16:27:31 -0400 | [diff] [blame] | 204 | panic(fmt.Errorf("%q expands to more than one string value: %q", variableValue, expandedVar)) | 
| Sam Delmerico | 7f88956 | 2022-03-25 14:55:40 +0000 | [diff] [blame] | 205 | } | 
|  | 206 | ret = append(ret, bazelConstant{ | 
|  | 207 | variableName:       k, | 
|  | 208 | internalDefinition: fmt.Sprintf(`"%s"`, validateCharacters(expandedVar[0])), | 
|  | 209 | }) | 
|  | 210 | } | 
|  | 211 | return ret | 
|  | 212 | } | 
|  | 213 |  | 
|  | 214 | // ExportedStringListVariables is a mapping of variable names to a list of strings | 
|  | 215 | type ExportedStringListVariables map[string][]string | 
|  | 216 |  | 
|  | 217 | func (m ExportedStringListVariables) set(k string, v []string) { | 
|  | 218 | m[k] = v | 
|  | 219 | } | 
|  | 220 |  | 
|  | 221 | func (m ExportedStringListVariables) asBazel(config Config, | 
|  | 222 | stringScope ExportedStringVariables, stringListScope ExportedStringListVariables, | 
|  | 223 | exportedVars ExportedConfigDependingVariables) []bazelConstant { | 
|  | 224 | ret := make([]bazelConstant, 0, len(m)) | 
|  | 225 | // For each exported variable, recursively expand elements in the variableValue | 
|  | 226 | // list to ensure that interpolated variables are expanded according to their values | 
|  | 227 | // in the variable scope. | 
|  | 228 | for k, variableValue := range m { | 
|  | 229 | var expandedVars []string | 
|  | 230 | for _, v := range variableValue { | 
|  | 231 | expandedVar, err := expandVar(config, v, stringScope, stringListScope, exportedVars) | 
|  | 232 | if err != nil { | 
|  | 233 | panic(fmt.Errorf("Error expanding config variable %s=%s: %s", k, v, err)) | 
|  | 234 | } | 
|  | 235 | expandedVars = append(expandedVars, expandedVar...) | 
|  | 236 | } | 
|  | 237 | // Assign the list as a bzl-private variable; this variable will be exported | 
|  | 238 | // out through a constants struct later. | 
|  | 239 | ret = append(ret, bazelConstant{ | 
|  | 240 | variableName:       k, | 
|  | 241 | internalDefinition: starlark_fmt.PrintStringList(expandedVars, 0), | 
|  | 242 | }) | 
|  | 243 | } | 
|  | 244 | return ret | 
|  | 245 | } | 
|  | 246 |  | 
|  | 247 | // ExportedStringListDictVariables is a mapping from variable names to a | 
|  | 248 | // dictionary which maps keys to lists of strings | 
|  | 249 | type ExportedStringListDictVariables map[string]map[string][]string | 
|  | 250 |  | 
|  | 251 | func (m ExportedStringListDictVariables) set(k string, v map[string][]string) { | 
|  | 252 | m[k] = v | 
|  | 253 | } | 
|  | 254 |  | 
|  | 255 | // Since dictionaries are not supported in Ninja, we do not expand variables for dictionaries | 
|  | 256 | func (m ExportedStringListDictVariables) asBazel(_ Config, _ ExportedStringVariables, | 
|  | 257 | _ ExportedStringListVariables, _ ExportedConfigDependingVariables) []bazelConstant { | 
|  | 258 | ret := make([]bazelConstant, 0, len(m)) | 
|  | 259 | for k, dict := range m { | 
|  | 260 | ret = append(ret, bazelConstant{ | 
|  | 261 | variableName:       k, | 
|  | 262 | internalDefinition: starlark_fmt.PrintStringListDict(dict, 0), | 
|  | 263 | }) | 
|  | 264 | } | 
|  | 265 | return ret | 
|  | 266 | } | 
|  | 267 |  | 
|  | 268 | // ExportedVariableReferenceDictVariables is a mapping from variable names to a | 
|  | 269 | // dictionary which references previously defined variables. This is used to | 
|  | 270 | // create a Starlark output such as: | 
| Colin Cross | d079e0b | 2022-08-16 10:27:33 -0700 | [diff] [blame] | 271 | // | 
|  | 272 | //	string_var1 = "string1 | 
|  | 273 | //	var_ref_dict_var1 = { | 
|  | 274 | //		"key1": string_var1 | 
|  | 275 | //	} | 
|  | 276 | // | 
| Sam Delmerico | 7f88956 | 2022-03-25 14:55:40 +0000 | [diff] [blame] | 277 | // This type of variable collection must be expanded last so that it recognizes | 
|  | 278 | // previously defined variables. | 
|  | 279 | type ExportedVariableReferenceDictVariables map[string]map[string]string | 
|  | 280 |  | 
|  | 281 | func (m ExportedVariableReferenceDictVariables) set(k string, v map[string]string) { | 
|  | 282 | m[k] = v | 
|  | 283 | } | 
|  | 284 |  | 
|  | 285 | func (m ExportedVariableReferenceDictVariables) asBazel(_ Config, _ ExportedStringVariables, | 
|  | 286 | _ ExportedStringListVariables, _ ExportedConfigDependingVariables) []bazelConstant { | 
|  | 287 | ret := make([]bazelConstant, 0, len(m)) | 
|  | 288 | for n, dict := range m { | 
|  | 289 | for k, v := range dict { | 
|  | 290 | matches, err := variableReference(v) | 
|  | 291 | if err != nil { | 
|  | 292 | panic(err) | 
|  | 293 | } else if !matches.matches { | 
|  | 294 | panic(fmt.Errorf("Expected a variable reference, got %q", v)) | 
|  | 295 | } else if len(matches.fullVariableReference) != len(v) { | 
|  | 296 | panic(fmt.Errorf("Expected only a variable reference, got %q", v)) | 
|  | 297 | } | 
|  | 298 | dict[k] = "_" + matches.variable | 
|  | 299 | } | 
|  | 300 | ret = append(ret, bazelConstant{ | 
|  | 301 | variableName:       n, | 
|  | 302 | internalDefinition: starlark_fmt.PrintDict(dict, 0), | 
|  | 303 | sortLast:           true, | 
|  | 304 | }) | 
|  | 305 | } | 
|  | 306 | return ret | 
|  | 307 | } | 
|  | 308 |  | 
|  | 309 | // BazelToolchainVars expands an ExportedVariables collection and returns a string | 
|  | 310 | // of formatted Starlark variable definitions | 
|  | 311 | func BazelToolchainVars(config Config, exportedVars ExportedVariables) string { | 
|  | 312 | results := exportedVars.asBazel( | 
|  | 313 | config, | 
|  | 314 | exportedVars.exportedStringVars, | 
|  | 315 | exportedVars.exportedStringListVars, | 
|  | 316 | exportedVars.exportedConfigDependingVars, | 
|  | 317 | ) | 
|  | 318 |  | 
|  | 319 | sort.Slice(results, func(i, j int) bool { | 
|  | 320 | if results[i].sortLast != results[j].sortLast { | 
|  | 321 | return !results[i].sortLast | 
|  | 322 | } | 
|  | 323 | return results[i].variableName < results[j].variableName | 
|  | 324 | }) | 
|  | 325 |  | 
|  | 326 | definitions := make([]string, 0, len(results)) | 
|  | 327 | constants := make([]string, 0, len(results)) | 
|  | 328 | for _, b := range results { | 
|  | 329 | definitions = append(definitions, | 
|  | 330 | fmt.Sprintf("_%s = %s", b.variableName, b.internalDefinition)) | 
|  | 331 | constants = append(constants, | 
|  | 332 | fmt.Sprintf("%[1]s%[2]s = _%[2]s,", starlark_fmt.Indention(1), b.variableName)) | 
|  | 333 | } | 
|  | 334 |  | 
|  | 335 | // Build the exported constants struct. | 
|  | 336 | ret := bazel.GeneratedBazelFileWarning | 
|  | 337 | ret += "\n\n" | 
|  | 338 | ret += strings.Join(definitions, "\n\n") | 
|  | 339 | ret += "\n\n" | 
|  | 340 | ret += "constants = struct(\n" | 
|  | 341 | ret += strings.Join(constants, "\n") | 
|  | 342 | ret += "\n)" | 
|  | 343 |  | 
|  | 344 | return ret | 
|  | 345 | } | 
|  | 346 |  | 
|  | 347 | type match struct { | 
|  | 348 | matches               bool | 
|  | 349 | fullVariableReference string | 
|  | 350 | variable              string | 
|  | 351 | } | 
|  | 352 |  | 
|  | 353 | func variableReference(input string) (match, error) { | 
|  | 354 | // e.g. "${ExternalCflags}" | 
|  | 355 | r := regexp.MustCompile(`\${(?:config\.)?([a-zA-Z0-9_]+)}`) | 
|  | 356 |  | 
|  | 357 | matches := r.FindStringSubmatch(input) | 
|  | 358 | if len(matches) == 0 { | 
|  | 359 | return match{}, nil | 
|  | 360 | } | 
|  | 361 | if len(matches) != 2 { | 
|  | 362 | return match{}, fmt.Errorf("Expected to only match 1 subexpression in %s, got %d", input, len(matches)-1) | 
|  | 363 | } | 
|  | 364 | return match{ | 
|  | 365 | matches:               true, | 
|  | 366 | fullVariableReference: matches[0], | 
|  | 367 | // Index 1 of FindStringSubmatch contains the subexpression match | 
|  | 368 | // (variable name) of the capture group. | 
|  | 369 | variable: matches[1], | 
|  | 370 | }, nil | 
|  | 371 | } | 
|  | 372 |  | 
|  | 373 | // expandVar recursively expand interpolated variables in the exportedVars scope. | 
|  | 374 | // | 
|  | 375 | // We're using a string slice to track the seen variables to avoid | 
|  | 376 | // stackoverflow errors with infinite recursion. it's simpler to use a | 
|  | 377 | // string slice than to handle a pass-by-referenced map, which would make it | 
|  | 378 | // quite complex to track depth-first interpolations. It's also unlikely the | 
|  | 379 | // interpolation stacks are deep (n > 1). | 
|  | 380 | func expandVar(config Config, toExpand string, stringScope ExportedStringVariables, | 
|  | 381 | stringListScope ExportedStringListVariables, exportedVars ExportedConfigDependingVariables) ([]string, error) { | 
|  | 382 |  | 
|  | 383 | // Internal recursive function. | 
|  | 384 | var expandVarInternal func(string, map[string]bool) (string, error) | 
|  | 385 | expandVarInternal = func(toExpand string, seenVars map[string]bool) (string, error) { | 
|  | 386 | var ret string | 
|  | 387 | remainingString := toExpand | 
|  | 388 | for len(remainingString) > 0 { | 
|  | 389 | matches, err := variableReference(remainingString) | 
|  | 390 | if err != nil { | 
|  | 391 | panic(err) | 
|  | 392 | } | 
|  | 393 | if !matches.matches { | 
|  | 394 | return ret + remainingString, nil | 
|  | 395 | } | 
|  | 396 | matchIndex := strings.Index(remainingString, matches.fullVariableReference) | 
|  | 397 | ret += remainingString[:matchIndex] | 
|  | 398 | remainingString = remainingString[matchIndex+len(matches.fullVariableReference):] | 
|  | 399 |  | 
|  | 400 | variable := matches.variable | 
|  | 401 | // toExpand contains a variable. | 
|  | 402 | if _, ok := seenVars[variable]; ok { | 
|  | 403 | return ret, fmt.Errorf( | 
|  | 404 | "Unbounded recursive interpolation of variable: %s", variable) | 
|  | 405 | } | 
|  | 406 | // A map is passed-by-reference. Create a new map for | 
|  | 407 | // this scope to prevent variables seen in one depth-first expansion | 
|  | 408 | // to be also treated as "seen" in other depth-first traversals. | 
|  | 409 | newSeenVars := map[string]bool{} | 
|  | 410 | for k := range seenVars { | 
|  | 411 | newSeenVars[k] = true | 
|  | 412 | } | 
|  | 413 | newSeenVars[variable] = true | 
|  | 414 | if unexpandedVars, ok := stringListScope[variable]; ok { | 
|  | 415 | expandedVars := []string{} | 
|  | 416 | for _, unexpandedVar := range unexpandedVars { | 
|  | 417 | expandedVar, err := expandVarInternal(unexpandedVar, newSeenVars) | 
|  | 418 | if err != nil { | 
|  | 419 | return ret, err | 
|  | 420 | } | 
|  | 421 | expandedVars = append(expandedVars, expandedVar) | 
|  | 422 | } | 
|  | 423 | ret += strings.Join(expandedVars, " ") | 
|  | 424 | } else if unexpandedVar, ok := stringScope[variable]; ok { | 
|  | 425 | expandedVar, err := expandVarInternal(unexpandedVar, newSeenVars) | 
|  | 426 | if err != nil { | 
|  | 427 | return ret, err | 
|  | 428 | } | 
|  | 429 | ret += expandedVar | 
|  | 430 | } else if unevaluatedVar, ok := exportedVars[variable]; ok { | 
|  | 431 | evalFunc := reflect.ValueOf(unevaluatedVar) | 
|  | 432 | validateVariableMethod(variable, evalFunc) | 
|  | 433 | evaluatedResult := evalFunc.Call([]reflect.Value{reflect.ValueOf(config)}) | 
|  | 434 | evaluatedValue := evaluatedResult[0].Interface().(string) | 
|  | 435 | expandedVar, err := expandVarInternal(evaluatedValue, newSeenVars) | 
|  | 436 | if err != nil { | 
|  | 437 | return ret, err | 
|  | 438 | } | 
|  | 439 | ret += expandedVar | 
|  | 440 | } else { | 
|  | 441 | return "", fmt.Errorf("Unbound config variable %s", variable) | 
|  | 442 | } | 
|  | 443 | } | 
|  | 444 | return ret, nil | 
|  | 445 | } | 
|  | 446 | var ret []string | 
| Sam Delmerico | 932c01c | 2022-03-25 16:33:26 +0000 | [diff] [blame] | 447 | stringFields := splitStringKeepingQuotedSubstring(toExpand, ' ') | 
|  | 448 | for _, v := range stringFields { | 
| Sam Delmerico | 7f88956 | 2022-03-25 14:55:40 +0000 | [diff] [blame] | 449 | val, err := expandVarInternal(v, map[string]bool{}) | 
|  | 450 | if err != nil { | 
|  | 451 | return ret, err | 
|  | 452 | } | 
|  | 453 | ret = append(ret, val) | 
|  | 454 | } | 
|  | 455 |  | 
|  | 456 | return ret, nil | 
|  | 457 | } | 
|  | 458 |  | 
| Sam Delmerico | 932c01c | 2022-03-25 16:33:26 +0000 | [diff] [blame] | 459 | // splitStringKeepingQuotedSubstring splits a string on a provided separator, | 
|  | 460 | // but it will not split substrings inside unescaped double quotes. If the double | 
|  | 461 | // quotes are escaped, then the returned string will only include the quote, and | 
|  | 462 | // not the escape. | 
|  | 463 | func splitStringKeepingQuotedSubstring(s string, delimiter byte) []string { | 
|  | 464 | var ret []string | 
|  | 465 | quote := byte('"') | 
|  | 466 |  | 
|  | 467 | var substring []byte | 
|  | 468 | quoted := false | 
|  | 469 | escaped := false | 
|  | 470 |  | 
|  | 471 | for i := range s { | 
|  | 472 | if !quoted && s[i] == delimiter { | 
|  | 473 | ret = append(ret, string(substring)) | 
|  | 474 | substring = []byte{} | 
|  | 475 | continue | 
|  | 476 | } | 
|  | 477 |  | 
|  | 478 | characterIsEscape := i < len(s)-1 && s[i] == '\\' && s[i+1] == quote | 
|  | 479 | if characterIsEscape { | 
|  | 480 | escaped = true | 
|  | 481 | continue | 
|  | 482 | } | 
|  | 483 |  | 
|  | 484 | if s[i] == quote { | 
|  | 485 | if !escaped { | 
|  | 486 | quoted = !quoted | 
|  | 487 | } | 
|  | 488 | escaped = false | 
|  | 489 | } | 
|  | 490 |  | 
|  | 491 | substring = append(substring, s[i]) | 
|  | 492 | } | 
|  | 493 |  | 
|  | 494 | ret = append(ret, string(substring)) | 
|  | 495 |  | 
|  | 496 | return ret | 
|  | 497 | } | 
|  | 498 |  | 
| Sam Delmerico | 7f88956 | 2022-03-25 14:55:40 +0000 | [diff] [blame] | 499 | func validateVariableMethod(name string, methodValue reflect.Value) { | 
|  | 500 | methodType := methodValue.Type() | 
|  | 501 | if methodType.Kind() != reflect.Func { | 
|  | 502 | panic(fmt.Errorf("method given for variable %s is not a function", | 
|  | 503 | name)) | 
|  | 504 | } | 
|  | 505 | if n := methodType.NumIn(); n != 1 { | 
|  | 506 | panic(fmt.Errorf("method for variable %s has %d inputs (should be 1)", | 
|  | 507 | name, n)) | 
|  | 508 | } | 
|  | 509 | if n := methodType.NumOut(); n != 1 { | 
|  | 510 | panic(fmt.Errorf("method for variable %s has %d outputs (should be 1)", | 
|  | 511 | name, n)) | 
|  | 512 | } | 
|  | 513 | if kind := methodType.Out(0).Kind(); kind != reflect.String { | 
|  | 514 | panic(fmt.Errorf("method for variable %s does not return a string", | 
|  | 515 | name)) | 
|  | 516 | } | 
|  | 517 | } |