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 17import ( 18 "android/soong/android" 19 "android/soong/bazel" 20 "fmt" 21 "reflect" 22 "strings" 23 24 "github.com/google/blueprint" 25 "github.com/google/blueprint/proptools" 26) 27 28type BazelAttributes struct { 29 Attrs map[string]string 30} 31 32type BazelTarget struct { 33 name string 34 content string 35 ruleClass string 36 bzlLoadLocation string 37} 38 39// IsLoadedFromStarlark determines if the BazelTarget's rule class is loaded from a .bzl file, 40// as opposed to a native rule built into Bazel. 41func (t BazelTarget) IsLoadedFromStarlark() bool { 42 return t.bzlLoadLocation != "" 43} 44 45// BazelTargets is a typedef for a slice of BazelTarget objects. 46type BazelTargets []BazelTarget 47 48// String returns the string representation of BazelTargets, without load 49// statements (use LoadStatements for that), since the targets are usually not 50// adjacent to the load statements at the top of the BUILD file. 51func (targets BazelTargets) String() string { 52 var res string 53 for i, target := range targets { 54 res += target.content 55 if i != len(targets)-1 { 56 res += "\n\n" 57 } 58 } 59 return res 60} 61 62// LoadStatements return the string representation of the sorted and deduplicated 63// Starlark rule load statements needed by a group of BazelTargets. 64func (targets BazelTargets) LoadStatements() string { 65 bzlToLoadedSymbols := map[string][]string{} 66 for _, target := range targets { 67 if target.IsLoadedFromStarlark() { 68 bzlToLoadedSymbols[target.bzlLoadLocation] = 69 append(bzlToLoadedSymbols[target.bzlLoadLocation], target.ruleClass) 70 } 71 } 72 73 var loadStatements []string 74 for bzl, ruleClasses := range bzlToLoadedSymbols { 75 loadStatement := "load(\"" 76 loadStatement += bzl 77 loadStatement += "\", " 78 ruleClasses = android.SortedUniqueStrings(ruleClasses) 79 for i, ruleClass := range ruleClasses { 80 loadStatement += "\"" + ruleClass + "\"" 81 if i != len(ruleClasses)-1 { 82 loadStatement += ", " 83 } 84 } 85 loadStatement += ")" 86 loadStatements = append(loadStatements, loadStatement) 87 } 88 return strings.Join(android.SortedUniqueStrings(loadStatements), "\n") 89} 90 91type bpToBuildContext interface { 92 ModuleName(module blueprint.Module) string 93 ModuleDir(module blueprint.Module) string 94 ModuleSubDir(module blueprint.Module) string 95 ModuleType(module blueprint.Module) string 96 97 VisitAllModules(visit func(blueprint.Module)) 98 VisitDirectDeps(module blueprint.Module, visit func(blueprint.Module)) 99} 100 101type CodegenContext struct { 102 config android.Config 103 context android.Context 104 mode CodegenMode 105 additionalDeps []string 106} 107 108func (c *CodegenContext) Mode() CodegenMode { 109 return c.mode 110} 111 112// CodegenMode is an enum to differentiate code-generation modes. 113type CodegenMode int 114 115const ( 116 // Bp2Build: generate BUILD files with targets buildable by Bazel directly. 117 // 118 // This mode is used for the Soong->Bazel build definition conversion. 119 Bp2Build CodegenMode = iota 120 121 // QueryView: generate BUILD files with targets representing fully mutated 122 // Soong modules, representing the fully configured Soong module graph with 123 // variants and dependency endges. 124 // 125 // This mode is used for discovering and introspecting the existing Soong 126 // module graph. 127 QueryView 128) 129 130func (mode CodegenMode) String() string { 131 switch mode { 132 case Bp2Build: 133 return "Bp2Build" 134 case QueryView: 135 return "QueryView" 136 default: 137 return fmt.Sprintf("%d", mode) 138 } 139} 140 141// AddNinjaFileDeps adds dependencies on the specified files to be added to the ninja manifest. The 142// primary builder will be rerun whenever the specified files are modified. Allows us to fulfill the 143// PathContext interface in order to add dependencies on hand-crafted BUILD files. Note: must also 144// call AdditionalNinjaDeps and add them manually to the ninja file. 145func (ctx *CodegenContext) AddNinjaFileDeps(deps ...string) { 146 ctx.additionalDeps = append(ctx.additionalDeps, deps...) 147} 148 149// AdditionalNinjaDeps returns additional ninja deps added by CodegenContext 150func (ctx *CodegenContext) AdditionalNinjaDeps() []string { 151 return ctx.additionalDeps 152} 153 154func (ctx *CodegenContext) Config() android.Config { return ctx.config } 155func (ctx *CodegenContext) Context() android.Context { return ctx.context } 156 157// NewCodegenContext creates a wrapper context that conforms to PathContext for 158// writing BUILD files in the output directory. 159func NewCodegenContext(config android.Config, context android.Context, mode CodegenMode) *CodegenContext { 160 return &CodegenContext{ 161 context: context, 162 config: config, 163 mode: mode, 164 } 165} 166 167// props is an unsorted map. This function ensures that 168// the generated attributes are sorted to ensure determinism. 169func propsToAttributes(props map[string]string) string { 170 var attributes string 171 for _, propName := range android.SortedStringKeys(props) { 172 if shouldGenerateAttribute(propName) { 173 attributes += fmt.Sprintf(" %s = %s,\n", propName, props[propName]) 174 } 175 } 176 return attributes 177} 178 179func GenerateBazelTargets(ctx *CodegenContext, generateFilegroups bool) (map[string]BazelTargets, CodegenMetrics) { 180 buildFileToTargets := make(map[string]BazelTargets) 181 buildFileToAppend := make(map[string]bool) 182 183 // Simple metrics tracking for bp2build 184 metrics := CodegenMetrics{ 185 RuleClassCount: make(map[string]int), 186 } 187 188 dirs := make(map[string]bool) 189 190 bpCtx := ctx.Context() 191 bpCtx.VisitAllModules(func(m blueprint.Module) { 192 dir := bpCtx.ModuleDir(m) 193 dirs[dir] = true 194 195 var t BazelTarget 196 197 switch ctx.Mode() { 198 case Bp2Build: 199 if b, ok := m.(android.Bazelable); ok && b.HasHandcraftedLabel() { 200 metrics.handCraftedTargetCount += 1 201 metrics.TotalModuleCount += 1 202 pathToBuildFile := getBazelPackagePath(b) 203 // We are using the entire contents of handcrafted build file, so if multiple targets within 204 // a package have handcrafted targets, we only want to include the contents one time. 205 if _, exists := buildFileToAppend[pathToBuildFile]; exists { 206 return 207 } 208 var err error 209 t, err = getHandcraftedBuildContent(ctx, b, pathToBuildFile) 210 if err != nil { 211 panic(fmt.Errorf("Error converting %s: %s", bpCtx.ModuleName(m), err)) 212 } 213 // TODO(b/181575318): currently we append the whole BUILD file, let's change that to do 214 // something more targeted based on the rule type and target 215 buildFileToAppend[pathToBuildFile] = true 216 } else if btm, ok := m.(android.BazelTargetModule); ok { 217 t = generateBazelTarget(bpCtx, m, btm) 218 metrics.RuleClassCount[t.ruleClass] += 1 219 } else { 220 metrics.TotalModuleCount += 1 221 return 222 } 223 case QueryView: 224 // Blocklist certain module types from being generated. 225 if canonicalizeModuleType(bpCtx.ModuleType(m)) == "package" { 226 // package module name contain slashes, and thus cannot 227 // be mapped cleanly to a bazel label. 228 return 229 } 230 t = generateSoongModuleTarget(bpCtx, m) 231 default: 232 panic(fmt.Errorf("Unknown code-generation mode: %s", ctx.Mode())) 233 } 234 235 buildFileToTargets[dir] = append(buildFileToTargets[dir], t) 236 }) 237 if generateFilegroups { 238 // Add a filegroup target that exposes all sources in the subtree of this package 239 // NOTE: This also means we generate a BUILD file for every Android.bp file (as long as it has at least one module) 240 for dir, _ := range dirs { 241 buildFileToTargets[dir] = append(buildFileToTargets[dir], BazelTarget{ 242 name: "bp2build_all_srcs", 243 content: `filegroup(name = "bp2build_all_srcs", srcs = glob(["**/*"]))`, 244 ruleClass: "filegroup", 245 }) 246 } 247 } 248 249 return buildFileToTargets, metrics 250} 251 252func getBazelPackagePath(b android.Bazelable) string { 253 label := b.HandcraftedLabel() 254 pathToBuildFile := strings.TrimPrefix(label, "//") 255 pathToBuildFile = strings.Split(pathToBuildFile, ":")[0] 256 return pathToBuildFile 257} 258 259func getHandcraftedBuildContent(ctx *CodegenContext, b android.Bazelable, pathToBuildFile string) (BazelTarget, error) { 260 p := android.ExistentPathForSource(ctx, pathToBuildFile, HandcraftedBuildFileName) 261 if !p.Valid() { 262 return BazelTarget{}, fmt.Errorf("Could not find file %q for handcrafted target.", pathToBuildFile) 263 } 264 c, err := b.GetBazelBuildFileContents(ctx.Config(), pathToBuildFile, HandcraftedBuildFileName) 265 if err != nil { 266 return BazelTarget{}, err 267 } 268 // TODO(b/181575318): once this is more targeted, we need to include name, rule class, etc 269 return BazelTarget{ 270 content: c, 271 }, nil 272} 273 274func generateBazelTarget(ctx bpToBuildContext, m blueprint.Module, btm android.BazelTargetModule) BazelTarget { 275 ruleClass := btm.RuleClass() 276 bzlLoadLocation := btm.BzlLoadLocation() 277 278 // extract the bazel attributes from the module. 279 props := getBuildProperties(ctx, m) 280 281 delete(props.Attrs, "bp2build_available") 282 283 // Return the Bazel target with rule class and attributes, ready to be 284 // code-generated. 285 attributes := propsToAttributes(props.Attrs) 286 targetName := targetNameForBp2Build(ctx, m) 287 return BazelTarget{ 288 name: targetName, 289 ruleClass: ruleClass, 290 bzlLoadLocation: bzlLoadLocation, 291 content: fmt.Sprintf( 292 bazelTarget, 293 ruleClass, 294 targetName, 295 attributes, 296 ), 297 } 298} 299 300// Convert a module and its deps and props into a Bazel macro/rule 301// representation in the BUILD file. 302func generateSoongModuleTarget(ctx bpToBuildContext, m blueprint.Module) BazelTarget { 303 props := getBuildProperties(ctx, m) 304 305 // TODO(b/163018919): DirectDeps can have duplicate (module, variant) 306 // items, if the modules are added using different DependencyTag. Figure 307 // out the implications of that. 308 depLabels := map[string]bool{} 309 if aModule, ok := m.(android.Module); ok { 310 ctx.VisitDirectDeps(aModule, func(depModule blueprint.Module) { 311 depLabels[qualifiedTargetLabel(ctx, depModule)] = true 312 }) 313 } 314 attributes := propsToAttributes(props.Attrs) 315 316 depLabelList := "[\n" 317 for depLabel, _ := range depLabels { 318 depLabelList += fmt.Sprintf(" %q,\n", depLabel) 319 } 320 depLabelList += " ]" 321 322 targetName := targetNameWithVariant(ctx, m) 323 return BazelTarget{ 324 name: targetName, 325 content: fmt.Sprintf( 326 soongModuleTarget, 327 targetName, 328 ctx.ModuleName(m), 329 canonicalizeModuleType(ctx.ModuleType(m)), 330 ctx.ModuleSubDir(m), 331 depLabelList, 332 attributes), 333 } 334} 335 336func getBuildProperties(ctx bpToBuildContext, m blueprint.Module) BazelAttributes { 337 var allProps map[string]string 338 // TODO: this omits properties for blueprint modules (blueprint_go_binary, 339 // bootstrap_go_binary, bootstrap_go_package), which will have to be handled separately. 340 if aModule, ok := m.(android.Module); ok { 341 allProps = ExtractModuleProperties(aModule) 342 } 343 344 return BazelAttributes{ 345 Attrs: allProps, 346 } 347} 348 349// Generically extract module properties and types into a map, keyed by the module property name. 350func ExtractModuleProperties(aModule android.Module) map[string]string { 351 ret := map[string]string{} 352 353 // Iterate over this android.Module's property structs. 354 for _, properties := range aModule.GetProperties() { 355 propertiesValue := reflect.ValueOf(properties) 356 // Check that propertiesValue is a pointer to the Properties struct, like 357 // *cc.BaseLinkerProperties or *java.CompilerProperties. 358 // 359 // propertiesValue can also be type-asserted to the structs to 360 // manipulate internal props, if needed. 361 if isStructPtr(propertiesValue.Type()) { 362 structValue := propertiesValue.Elem() 363 for k, v := range extractStructProperties(structValue, 0) { 364 ret[k] = v 365 } 366 } else { 367 panic(fmt.Errorf( 368 "properties must be a pointer to a struct, got %T", 369 propertiesValue.Interface())) 370 } 371 } 372 373 return ret 374} 375 376func isStructPtr(t reflect.Type) bool { 377 return t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Struct 378} 379 380// prettyPrint a property value into the equivalent Starlark representation 381// recursively. 382func prettyPrint(propertyValue reflect.Value, indent int) (string, error) { 383 if isZero(propertyValue) { 384 // A property value being set or unset actually matters -- Soong does set default 385 // values for unset properties, like system_shared_libs = ["libc", "libm", "libdl"] at 386 // https://cs.android.com/android/platform/superproject/+/master:build/soong/cc/linker.go;l=281-287;drc=f70926eef0b9b57faf04c17a1062ce50d209e480 387 // 388 // In Bazel-parlance, we would use "attr.<type>(default = <default 389 // value>)" to set the default value of unset attributes. In the cases 390 // where the bp2build converter didn't set the default value within the 391 // mutator when creating the BazelTargetModule, this would be a zero 392 // value. For those cases, we return an empty string so we don't 393 // unnecessarily generate empty values. 394 return "", nil 395 } 396 397 var ret string 398 switch propertyValue.Kind() { 399 case reflect.String: 400 ret = fmt.Sprintf("\"%v\"", escapeString(propertyValue.String())) 401 case reflect.Bool: 402 ret = strings.Title(fmt.Sprintf("%v", propertyValue.Interface())) 403 case reflect.Int, reflect.Uint, reflect.Int64: 404 ret = fmt.Sprintf("%v", propertyValue.Interface()) 405 case reflect.Ptr: 406 return prettyPrint(propertyValue.Elem(), indent) 407 case reflect.Slice: 408 if propertyValue.Len() == 0 { 409 return "", nil 410 } 411 412 if propertyValue.Len() == 1 { 413 // Single-line list for list with only 1 element 414 ret += "[" 415 indexedValue, err := prettyPrint(propertyValue.Index(0), indent) 416 if err != nil { 417 return "", err 418 } 419 ret += indexedValue 420 ret += "]" 421 } else { 422 // otherwise, use a multiline list. 423 ret += "[\n" 424 for i := 0; i < propertyValue.Len(); i++ { 425 indexedValue, err := prettyPrint(propertyValue.Index(i), indent+1) 426 if err != nil { 427 return "", err 428 } 429 430 if indexedValue != "" { 431 ret += makeIndent(indent + 1) 432 ret += indexedValue 433 ret += ",\n" 434 } 435 } 436 ret += makeIndent(indent) 437 ret += "]" 438 } 439 440 case reflect.Struct: 441 // Special cases where the bp2build sends additional information to the codegenerator 442 // by wrapping the attributes in a custom struct type. 443 if attr, ok := propertyValue.Interface().(bazel.Attribute); ok { 444 return prettyPrintAttribute(attr, indent) 445 } else if label, ok := propertyValue.Interface().(bazel.Label); ok { 446 return fmt.Sprintf("%q", label.Label), nil 447 } 448 449 ret = "{\n" 450 // Sort and print the struct props by the key. 451 structProps := extractStructProperties(propertyValue, indent) 452 for _, k := range android.SortedStringKeys(structProps) { 453 ret += makeIndent(indent + 1) 454 ret += fmt.Sprintf("%q: %s,\n", k, structProps[k]) 455 } 456 ret += makeIndent(indent) 457 ret += "}" 458 case reflect.Interface: 459 // TODO(b/164227191): implement pretty print for interfaces. 460 // Interfaces are used for for arch, multilib and target properties. 461 return "", nil 462 default: 463 return "", fmt.Errorf( 464 "unexpected kind for property struct field: %s", propertyValue.Kind()) 465 } 466 return ret, nil 467} 468 469// Converts a reflected property struct value into a map of property names and property values, 470// which each property value correctly pretty-printed and indented at the right nest level, 471// since property structs can be nested. In Starlark, nested structs are represented as nested 472// dicts: https://docs.bazel.build/skylark/lib/dict.html 473func extractStructProperties(structValue reflect.Value, indent int) map[string]string { 474 if structValue.Kind() != reflect.Struct { 475 panic(fmt.Errorf("Expected a reflect.Struct type, but got %s", structValue.Kind())) 476 } 477 478 ret := map[string]string{} 479 structType := structValue.Type() 480 for i := 0; i < structValue.NumField(); i++ { 481 field := structType.Field(i) 482 if shouldSkipStructField(field) { 483 continue 484 } 485 486 fieldValue := structValue.Field(i) 487 if isZero(fieldValue) { 488 // Ignore zero-valued fields 489 continue 490 } 491 492 propertyName := proptools.PropertyNameForField(field.Name) 493 prettyPrintedValue, err := prettyPrint(fieldValue, indent+1) 494 if err != nil { 495 panic( 496 fmt.Errorf( 497 "Error while parsing property: %q. %s", 498 propertyName, 499 err)) 500 } 501 if prettyPrintedValue != "" { 502 ret[propertyName] = prettyPrintedValue 503 } 504 } 505 506 return ret 507} 508 509func isZero(value reflect.Value) bool { 510 switch value.Kind() { 511 case reflect.Func, reflect.Map, reflect.Slice: 512 return value.IsNil() 513 case reflect.Array: 514 valueIsZero := true 515 for i := 0; i < value.Len(); i++ { 516 valueIsZero = valueIsZero && isZero(value.Index(i)) 517 } 518 return valueIsZero 519 case reflect.Struct: 520 valueIsZero := true 521 for i := 0; i < value.NumField(); i++ { 522 valueIsZero = valueIsZero && isZero(value.Field(i)) 523 } 524 return valueIsZero 525 case reflect.Ptr: 526 if !value.IsNil() { 527 return isZero(reflect.Indirect(value)) 528 } else { 529 return true 530 } 531 default: 532 zeroValue := reflect.Zero(value.Type()) 533 result := value.Interface() == zeroValue.Interface() 534 return result 535 } 536} 537 538func escapeString(s string) string { 539 s = strings.ReplaceAll(s, "\\", "\\\\") 540 541 // b/184026959: Reverse the application of some common control sequences. 542 // These must be generated literally in the BUILD file. 543 s = strings.ReplaceAll(s, "\t", "\\t") 544 s = strings.ReplaceAll(s, "\n", "\\n") 545 s = strings.ReplaceAll(s, "\r", "\\r") 546 547 return strings.ReplaceAll(s, "\"", "\\\"") 548} 549 550func makeIndent(indent int) string { 551 if indent < 0 { 552 panic(fmt.Errorf("indent column cannot be less than 0, but got %d", indent)) 553 } 554 return strings.Repeat(" ", indent) 555} 556 557func targetNameForBp2Build(c bpToBuildContext, logicModule blueprint.Module) string { 558 return strings.Replace(c.ModuleName(logicModule), bazel.BazelTargetModuleNamePrefix, "", 1) 559} 560 561func targetNameWithVariant(c bpToBuildContext, logicModule blueprint.Module) string { 562 name := "" 563 if c.ModuleSubDir(logicModule) != "" { 564 // TODO(b/162720883): Figure out a way to drop the "--" variant suffixes. 565 name = c.ModuleName(logicModule) + "--" + c.ModuleSubDir(logicModule) 566 } else { 567 name = c.ModuleName(logicModule) 568 } 569 570 return strings.Replace(name, "//", "", 1) 571} 572 573func qualifiedTargetLabel(c bpToBuildContext, logicModule blueprint.Module) string { 574 return fmt.Sprintf("//%s:%s", c.ModuleDir(logicModule), targetNameWithVariant(c, logicModule)) 575} 576