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