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