1// Copyright 2020 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 15package bp2build 16 17/* 18For shareable/common functionality for conversion from soong-module to build files 19for queryview/bp2build 20*/ 21 22import ( 23 "fmt" 24 "reflect" 25 "sort" 26 "strings" 27 28 "android/soong/android" 29 "android/soong/bazel" 30 "android/soong/starlark_fmt" 31 "github.com/google/blueprint" 32 "github.com/google/blueprint/proptools" 33) 34 35type BazelAttributes struct { 36 Attrs map[string]string 37} 38 39type BazelLoadSymbol struct { 40 // The name of the symbol in the file being loaded 41 symbol string 42 // The name the symbol wil have in this file. Can be left blank to use the same name as symbol. 43 alias string 44} 45 46type BazelLoad struct { 47 file string 48 symbols []BazelLoadSymbol 49} 50 51type BazelTarget struct { 52 name string 53 packageName string 54 content string 55 ruleClass string 56 loads []BazelLoad 57} 58 59// Label is the fully qualified Bazel label constructed from the BazelTarget's 60// package name and target name. 61func (t BazelTarget) Label() string { 62 if t.packageName == "." { 63 return "//:" + t.name 64 } else { 65 return "//" + t.packageName + ":" + t.name 66 } 67} 68 69// PackageName returns the package of the Bazel target. 70// Defaults to root of tree. 71func (t BazelTarget) PackageName() string { 72 if t.packageName == "" { 73 return "." 74 } 75 return t.packageName 76} 77 78// BazelTargets is a typedef for a slice of BazelTarget objects. 79type BazelTargets []BazelTarget 80 81func (targets BazelTargets) packageRule() *BazelTarget { 82 for _, target := range targets { 83 if target.ruleClass == "package" { 84 return &target 85 } 86 } 87 return nil 88} 89 90// sort a list of BazelTargets in-place, by name, and by generated/handcrafted types. 91func (targets BazelTargets) sort() { 92 sort.Slice(targets, func(i, j int) bool { 93 return targets[i].name < targets[j].name 94 }) 95} 96 97// String returns the string representation of BazelTargets, without load 98// statements (use LoadStatements for that), since the targets are usually not 99// adjacent to the load statements at the top of the BUILD file. 100func (targets BazelTargets) String() string { 101 var res strings.Builder 102 for i, target := range targets { 103 if target.ruleClass != "package" { 104 res.WriteString(target.content) 105 } 106 if i != len(targets)-1 { 107 res.WriteString("\n\n") 108 } 109 } 110 return res.String() 111} 112 113// LoadStatements return the string representation of the sorted and deduplicated 114// Starlark rule load statements needed by a group of BazelTargets. 115func (targets BazelTargets) LoadStatements() string { 116 // First, merge all the load statements from all the targets onto one list 117 bzlToLoadedSymbols := map[string][]BazelLoadSymbol{} 118 for _, target := range targets { 119 for _, load := range target.loads { 120 outer: 121 for _, symbol := range load.symbols { 122 alias := symbol.alias 123 if alias == "" { 124 alias = symbol.symbol 125 } 126 for _, otherSymbol := range bzlToLoadedSymbols[load.file] { 127 otherAlias := otherSymbol.alias 128 if otherAlias == "" { 129 otherAlias = otherSymbol.symbol 130 } 131 if symbol.symbol == otherSymbol.symbol && alias == otherAlias { 132 continue outer 133 } else if alias == otherAlias { 134 panic(fmt.Sprintf("Conflicting destination (%s) for loads of %s and %s", alias, symbol.symbol, otherSymbol.symbol)) 135 } 136 } 137 bzlToLoadedSymbols[load.file] = append(bzlToLoadedSymbols[load.file], symbol) 138 } 139 } 140 } 141 142 var loadStatements strings.Builder 143 for i, bzl := range android.SortedKeys(bzlToLoadedSymbols) { 144 symbols := bzlToLoadedSymbols[bzl] 145 loadStatements.WriteString("load(\"") 146 loadStatements.WriteString(bzl) 147 loadStatements.WriteString("\", ") 148 sort.Slice(symbols, func(i, j int) bool { 149 if symbols[i].symbol < symbols[j].symbol { 150 return true 151 } 152 return symbols[i].alias < symbols[j].alias 153 }) 154 for j, symbol := range symbols { 155 if symbol.alias != "" && symbol.alias != symbol.symbol { 156 loadStatements.WriteString(symbol.alias) 157 loadStatements.WriteString(" = ") 158 } 159 loadStatements.WriteString("\"") 160 loadStatements.WriteString(symbol.symbol) 161 loadStatements.WriteString("\"") 162 if j != len(symbols)-1 { 163 loadStatements.WriteString(", ") 164 } 165 } 166 loadStatements.WriteString(")") 167 if i != len(bzlToLoadedSymbols)-1 { 168 loadStatements.WriteString("\n") 169 } 170 } 171 return loadStatements.String() 172} 173 174type bpToBuildContext interface { 175 ModuleName(module blueprint.Module) string 176 ModuleDir(module blueprint.Module) string 177 ModuleSubDir(module blueprint.Module) string 178 ModuleType(module blueprint.Module) string 179 180 VisitAllModules(visit func(blueprint.Module)) 181 VisitDirectDeps(module blueprint.Module, visit func(blueprint.Module)) 182} 183 184type CodegenContext struct { 185 config android.Config 186 context *android.Context 187 mode CodegenMode 188 additionalDeps []string 189 unconvertedDepMode unconvertedDepsMode 190 topDir string 191} 192 193func (ctx *CodegenContext) Mode() CodegenMode { 194 return ctx.mode 195} 196 197// CodegenMode is an enum to differentiate code-generation modes. 198type CodegenMode int 199 200const ( 201 // QueryView - generate BUILD files with targets representing fully mutated 202 // Soong modules, representing the fully configured Soong module graph with 203 // variants and dependency edges. 204 // 205 // This mode is used for discovering and introspecting the existing Soong 206 // module graph. 207 QueryView CodegenMode = iota 208) 209 210type unconvertedDepsMode int 211 212const ( 213 // Include a warning in conversion metrics about converted modules with unconverted direct deps 214 warnUnconvertedDeps unconvertedDepsMode = iota 215 // Error and fail conversion if encountering a module with unconverted direct deps 216 // Enabled by setting environment variable `BP2BUILD_ERROR_UNCONVERTED` 217 errorModulesUnconvertedDeps 218) 219 220func (mode CodegenMode) String() string { 221 switch mode { 222 case QueryView: 223 return "QueryView" 224 default: 225 return fmt.Sprintf("%d", mode) 226 } 227} 228 229// AddNinjaFileDeps adds dependencies on the specified files to be added to the ninja manifest. The 230// primary builder will be rerun whenever the specified files are modified. Allows us to fulfill the 231// PathContext interface in order to add dependencies on hand-crafted BUILD files. Note: must also 232// call AdditionalNinjaDeps and add them manually to the ninja file. 233func (ctx *CodegenContext) AddNinjaFileDeps(deps ...string) { 234 ctx.additionalDeps = append(ctx.additionalDeps, deps...) 235} 236 237// AdditionalNinjaDeps returns additional ninja deps added by CodegenContext 238func (ctx *CodegenContext) AdditionalNinjaDeps() []string { 239 return ctx.additionalDeps 240} 241 242func (ctx *CodegenContext) Config() android.Config { return ctx.config } 243func (ctx *CodegenContext) Context() *android.Context { return ctx.context } 244 245// NewCodegenContext creates a wrapper context that conforms to PathContext for 246// writing BUILD files in the output directory. 247func NewCodegenContext(config android.Config, context *android.Context, mode CodegenMode, topDir string) *CodegenContext { 248 var unconvertedDeps unconvertedDepsMode 249 return &CodegenContext{ 250 context: context, 251 config: config, 252 mode: mode, 253 unconvertedDepMode: unconvertedDeps, 254 topDir: topDir, 255 } 256} 257 258// props is an unsorted map. This function ensures that 259// the generated attributes are sorted to ensure determinism. 260func propsToAttributes(props map[string]string) string { 261 var attributes string 262 for _, propName := range android.SortedKeys(props) { 263 attributes += fmt.Sprintf(" %s = %s,\n", propName, props[propName]) 264 } 265 return attributes 266} 267 268type conversionResults struct { 269 buildFileToTargets map[string]BazelTargets 270 moduleNameToPartition map[string]string 271} 272 273func (r conversionResults) BuildDirToTargets() map[string]BazelTargets { 274 return r.buildFileToTargets 275} 276 277func GenerateBazelTargets(ctx *CodegenContext, generateFilegroups bool) (conversionResults, []error) { 278 ctx.Context().BeginEvent("GenerateBazelTargets") 279 defer ctx.Context().EndEvent("GenerateBazelTargets") 280 buildFileToTargets := make(map[string]BazelTargets) 281 282 dirs := make(map[string]bool) 283 moduleNameToPartition := make(map[string]string) 284 285 var errs []error 286 287 bpCtx := ctx.Context() 288 bpCtx.VisitAllModules(func(m blueprint.Module) { 289 dir := bpCtx.ModuleDir(m) 290 dirs[dir] = true 291 292 var targets []BazelTarget 293 294 switch ctx.Mode() { 295 case QueryView: 296 // Blocklist certain module types from being generated. 297 if canonicalizeModuleType(bpCtx.ModuleType(m)) == "package" { 298 // package module name contain slashes, and thus cannot 299 // be mapped cleanly to a bazel label. 300 return 301 } 302 t, err := generateSoongModuleTarget(bpCtx, m) 303 if err != nil { 304 errs = append(errs, err) 305 } 306 targets = append(targets, t) 307 default: 308 errs = append(errs, fmt.Errorf("Unknown code-generation mode: %s", ctx.Mode())) 309 return 310 } 311 312 for _, target := range targets { 313 targetDir := target.PackageName() 314 buildFileToTargets[targetDir] = append(buildFileToTargets[targetDir], target) 315 } 316 }) 317 318 if len(errs) > 0 { 319 return conversionResults{}, errs 320 } 321 322 if generateFilegroups { 323 // Add a filegroup target that exposes all sources in the subtree of this package 324 // NOTE: This also means we generate a BUILD file for every Android.bp file (as long as it has at least one module) 325 // 326 // This works because: https://bazel.build/reference/be/functions#exports_files 327 // "As a legacy behaviour, also files mentioned as input to a rule are exported with the 328 // default visibility until the flag --incompatible_no_implicit_file_export is flipped. However, this behavior 329 // should not be relied upon and actively migrated away from." 330 // 331 // TODO(b/198619163): We should change this to export_files(glob(["**/*"])) instead, but doing that causes these errors: 332 // "Error in exports_files: generated label '//external/avb:avbtool' conflicts with existing py_binary rule" 333 // So we need to solve all the "target ... is both a rule and a file" warnings first. 334 for dir := range dirs { 335 buildFileToTargets[dir] = append(buildFileToTargets[dir], BazelTarget{ 336 name: "bp2build_all_srcs", 337 content: `filegroup(name = "bp2build_all_srcs", srcs = glob(["**/*"]), tags = ["manual"])`, 338 ruleClass: "filegroup", 339 }) 340 } 341 } 342 343 return conversionResults{ 344 buildFileToTargets: buildFileToTargets, 345 moduleNameToPartition: moduleNameToPartition, 346 }, errs 347} 348 349// Convert a module and its deps and props into a Bazel macro/rule 350// representation in the BUILD file. 351func generateSoongModuleTarget(ctx bpToBuildContext, m blueprint.Module) (BazelTarget, error) { 352 props, err := getBuildProperties(ctx, m) 353 354 // TODO(b/163018919): DirectDeps can have duplicate (module, variant) 355 // items, if the modules are added using different DependencyTag. Figure 356 // out the implications of that. 357 depLabels := map[string]bool{} 358 if aModule, ok := m.(android.Module); ok { 359 ctx.VisitDirectDeps(aModule, func(depModule blueprint.Module) { 360 depLabels[qualifiedTargetLabel(ctx, depModule)] = true 361 }) 362 } 363 364 for p := range ignoredPropNames { 365 delete(props.Attrs, p) 366 } 367 attributes := propsToAttributes(props.Attrs) 368 369 depLabelList := "[\n" 370 for depLabel := range depLabels { 371 depLabelList += fmt.Sprintf(" %q,\n", depLabel) 372 } 373 depLabelList += " ]" 374 375 targetName := targetNameWithVariant(ctx, m) 376 return BazelTarget{ 377 name: targetName, 378 packageName: ctx.ModuleDir(m), 379 content: fmt.Sprintf( 380 soongModuleTargetTemplate, 381 targetName, 382 ctx.ModuleName(m), 383 canonicalizeModuleType(ctx.ModuleType(m)), 384 ctx.ModuleSubDir(m), 385 depLabelList, 386 attributes), 387 }, err 388} 389 390func getBuildProperties(ctx bpToBuildContext, m blueprint.Module) (BazelAttributes, error) { 391 // TODO: this omits properties for blueprint modules (blueprint_go_binary, 392 // bootstrap_go_binary, bootstrap_go_package), which will have to be handled separately. 393 if aModule, ok := m.(android.Module); ok { 394 return extractModuleProperties(aModule.GetProperties(), false) 395 } 396 397 return BazelAttributes{}, nil 398} 399 400// Generically extract module properties and types into a map, keyed by the module property name. 401func extractModuleProperties(props []interface{}, checkForDuplicateProperties bool) (BazelAttributes, error) { 402 ret := map[string]string{} 403 404 // Iterate over this android.Module's property structs. 405 for _, properties := range props { 406 propertiesValue := reflect.ValueOf(properties) 407 // Check that propertiesValue is a pointer to the Properties struct, like 408 // *cc.BaseLinkerProperties or *java.CompilerProperties. 409 // 410 // propertiesValue can also be type-asserted to the structs to 411 // manipulate internal props, if needed. 412 if isStructPtr(propertiesValue.Type()) { 413 structValue := propertiesValue.Elem() 414 ok, err := extractStructProperties(structValue, 0) 415 if err != nil { 416 return BazelAttributes{}, err 417 } 418 for k, v := range ok { 419 if existing, exists := ret[k]; checkForDuplicateProperties && exists { 420 return BazelAttributes{}, fmt.Errorf( 421 "%s (%v) is present in properties whereas it should be consolidated into a commonAttributes", 422 k, existing) 423 } 424 ret[k] = v 425 } 426 } else { 427 return BazelAttributes{}, 428 fmt.Errorf( 429 "properties must be a pointer to a struct, got %T", 430 propertiesValue.Interface()) 431 } 432 } 433 434 return BazelAttributes{ 435 Attrs: ret, 436 }, nil 437} 438 439func isStructPtr(t reflect.Type) bool { 440 return t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Struct 441} 442 443// prettyPrint a property value into the equivalent Starlark representation 444// recursively. 445func prettyPrint(propertyValue reflect.Value, indent int, emitZeroValues bool) (string, error) { 446 if !emitZeroValues && isZero(propertyValue) { 447 // A property value being set or unset actually matters -- Soong does set default 448 // values for unset properties, like system_shared_libs = ["libc", "libm", "libdl"] at 449 // https://cs.android.com/android/platform/superproject/+/main:build/soong/cc/linker.go;l=281-287;drc=f70926eef0b9b57faf04c17a1062ce50d209e480 450 // 451 // In Bazel-parlance, we would use "attr.<type>(default = <default 452 // value>)" to set the default value of unset attributes. In the cases 453 // where the bp2build converter didn't set the default value within the 454 // mutator when creating the BazelTargetModule, this would be a zero 455 // value. For those cases, we return an empty string so we don't 456 // unnecessarily generate empty values. 457 return "", nil 458 } 459 460 switch propertyValue.Kind() { 461 case reflect.String: 462 return fmt.Sprintf("\"%v\"", escapeString(propertyValue.String())), nil 463 case reflect.Bool: 464 return starlark_fmt.PrintBool(propertyValue.Bool()), nil 465 case reflect.Int, reflect.Uint, reflect.Int64: 466 return fmt.Sprintf("%v", propertyValue.Interface()), nil 467 case reflect.Ptr: 468 return prettyPrint(propertyValue.Elem(), indent, emitZeroValues) 469 case reflect.Slice: 470 elements := make([]string, 0, propertyValue.Len()) 471 for i := 0; i < propertyValue.Len(); i++ { 472 val, err := prettyPrint(propertyValue.Index(i), indent, emitZeroValues) 473 if err != nil { 474 return "", err 475 } 476 if val != "" { 477 elements = append(elements, val) 478 } 479 } 480 return starlark_fmt.PrintList(elements, indent, func(s string) string { 481 return "%s" 482 }), nil 483 484 case reflect.Struct: 485 // Special cases where the bp2build sends additional information to the codegenerator 486 // by wrapping the attributes in a custom struct type. 487 if attr, ok := propertyValue.Interface().(bazel.Attribute); ok { 488 return prettyPrintAttribute(attr, indent) 489 } else if label, ok := propertyValue.Interface().(bazel.Label); ok { 490 return fmt.Sprintf("%q", label.Label), nil 491 } 492 493 // Sort and print the struct props by the key. 494 structProps, err := extractStructProperties(propertyValue, indent) 495 496 if err != nil { 497 return "", err 498 } 499 500 if len(structProps) == 0 { 501 return "", nil 502 } 503 return starlark_fmt.PrintDict(structProps, indent), nil 504 case reflect.Interface: 505 // TODO(b/164227191): implement pretty print for interfaces. 506 // Interfaces are used for for arch, multilib and target properties. 507 return "", nil 508 case reflect.Map: 509 if v, ok := propertyValue.Interface().(bazel.StringMapAttribute); ok { 510 return starlark_fmt.PrintStringStringDict(v, indent), nil 511 } 512 return "", fmt.Errorf("bp2build expects map of type map[string]string for field: %s", propertyValue) 513 default: 514 return "", fmt.Errorf( 515 "unexpected kind for property struct field: %s", propertyValue.Kind()) 516 } 517} 518 519// Converts a reflected property struct value into a map of property names and property values, 520// which each property value correctly pretty-printed and indented at the right nest level, 521// since property structs can be nested. In Starlark, nested structs are represented as nested 522// dicts: https://docs.bazel.build/skylark/lib/dict.html 523func extractStructProperties(structValue reflect.Value, indent int) (map[string]string, error) { 524 if structValue.Kind() != reflect.Struct { 525 return map[string]string{}, fmt.Errorf("Expected a reflect.Struct type, but got %s", structValue.Kind()) 526 } 527 528 var err error 529 530 ret := map[string]string{} 531 structType := structValue.Type() 532 for i := 0; i < structValue.NumField(); i++ { 533 field := structType.Field(i) 534 if shouldSkipStructField(field) { 535 continue 536 } 537 538 fieldValue := structValue.Field(i) 539 if isZero(fieldValue) { 540 // Ignore zero-valued fields 541 continue 542 } 543 544 // if the struct is embedded (anonymous), flatten the properties into the containing struct 545 if field.Anonymous { 546 if field.Type.Kind() == reflect.Ptr { 547 fieldValue = fieldValue.Elem() 548 } 549 if fieldValue.Type().Kind() == reflect.Struct { 550 propsToMerge, err := extractStructProperties(fieldValue, indent) 551 if err != nil { 552 return map[string]string{}, err 553 } 554 for prop, value := range propsToMerge { 555 ret[prop] = value 556 } 557 continue 558 } 559 } 560 561 propertyName := proptools.PropertyNameForField(field.Name) 562 var prettyPrintedValue string 563 prettyPrintedValue, err = prettyPrint(fieldValue, indent+1, false) 564 if err != nil { 565 return map[string]string{}, fmt.Errorf( 566 "Error while parsing property: %q. %s", 567 propertyName, 568 err) 569 } 570 if prettyPrintedValue != "" { 571 ret[propertyName] = prettyPrintedValue 572 } 573 } 574 575 return ret, nil 576} 577 578func isZero(value reflect.Value) bool { 579 switch value.Kind() { 580 case reflect.Func, reflect.Map, reflect.Slice: 581 return value.IsNil() 582 case reflect.Array: 583 valueIsZero := true 584 for i := 0; i < value.Len(); i++ { 585 valueIsZero = valueIsZero && isZero(value.Index(i)) 586 } 587 return valueIsZero 588 case reflect.Struct: 589 valueIsZero := true 590 for i := 0; i < value.NumField(); i++ { 591 valueIsZero = valueIsZero && isZero(value.Field(i)) 592 } 593 return valueIsZero 594 case reflect.Ptr: 595 if !value.IsNil() { 596 return isZero(reflect.Indirect(value)) 597 } else { 598 return true 599 } 600 // Always print bool/strings, if you want a bool/string attribute to be able to take the default value, use a 601 // pointer instead 602 case reflect.Bool, reflect.String: 603 return false 604 default: 605 if !value.IsValid() { 606 return true 607 } 608 zeroValue := reflect.Zero(value.Type()) 609 result := value.Interface() == zeroValue.Interface() 610 return result 611 } 612} 613 614func escapeString(s string) string { 615 s = strings.ReplaceAll(s, "\\", "\\\\") 616 617 // b/184026959: Reverse the application of some common control sequences. 618 // These must be generated literally in the BUILD file. 619 s = strings.ReplaceAll(s, "\t", "\\t") 620 s = strings.ReplaceAll(s, "\n", "\\n") 621 s = strings.ReplaceAll(s, "\r", "\\r") 622 623 return strings.ReplaceAll(s, "\"", "\\\"") 624} 625 626func targetNameWithVariant(c bpToBuildContext, logicModule blueprint.Module) string { 627 name := "" 628 if c.ModuleSubDir(logicModule) != "" { 629 // TODO(b/162720883): Figure out a way to drop the "--" variant suffixes. 630 name = c.ModuleName(logicModule) + "--" + c.ModuleSubDir(logicModule) 631 } else { 632 name = c.ModuleName(logicModule) 633 } 634 635 return strings.Replace(name, "//", "", 1) 636} 637 638func qualifiedTargetLabel(c bpToBuildContext, logicModule blueprint.Module) string { 639 return fmt.Sprintf("//%s:%s", c.ModuleDir(logicModule), targetNameWithVariant(c, logicModule)) 640} 641