1// Copyright 2015 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 android
16
17import (
18	"android/soong/bazel"
19	"fmt"
20	"path/filepath"
21	"strings"
22
23	"github.com/google/blueprint"
24	"github.com/google/blueprint/pathtools"
25)
26
27// bazel_paths contains methods to:
28//   * resolve Soong path and module references into bazel.LabelList
29//   * resolve Bazel path references into Soong-compatible paths
30//
31// There is often a similar method for Bazel as there is for Soong path handling and should be used
32// in similar circumstances
33//
34// Bazel                                Soong
35//
36// BazelLabelForModuleSrc               PathForModuleSrc
37// BazelLabelForModuleSrcExcludes       PathForModuleSrcExcludes
38// BazelLabelForModuleDeps              n/a
39// tbd                                  PathForSource
40// tbd                                  ExistentPathsForSources
41// PathForBazelOut                      PathForModuleOut
42//
43// Use cases:
44//  * Module contains a property (often tagged `android:"path"`) that expects paths *relative to the
45//    module directory*:
46//     * BazelLabelForModuleSrcExcludes, if the module also contains an excludes_<propname> property
47//     * BazelLabelForModuleSrc, otherwise
48//  * Converting references to other modules to Bazel Labels:
49//     BazelLabelForModuleDeps
50//  * Converting a path obtained from bazel_handler cquery results:
51//     PathForBazelOut
52//
53// NOTE: all Soong globs are expanded within Soong rather than being converted to a Bazel glob
54//       syntax. This occurs because Soong does not have a concept of crossing package boundaries,
55//       so the glob as computed by Soong may contain paths that cross package-boundaries. These
56//       would be unknowingly omitted if the glob were handled by Bazel. By expanding globs within
57//       Soong, we support identification and detection (within Bazel) use of paths that cross
58//       package boundaries.
59//
60// Path resolution:
61// * filepath/globs: resolves as itself or is converted to an absolute Bazel label (e.g.
62//   //path/to/dir:<filepath>) if path exists in a separate package or subpackage.
63// * references to other modules (using the ":name{.tag}" syntax). These resolve as a Bazel label
64//   for a target. If the Bazel target is in the local module directory, it will be returned
65//   relative to the current package (e.g.  ":<target>"). Otherwise, it will be returned as an
66//   absolute Bazel label (e.g.  "//path/to/dir:<target>"). If the reference to another module
67//   cannot be resolved,the function will panic. This is often due to the dependency not being added
68//   via an AddDependency* method.
69
70// A subset of the ModuleContext methods which are sufficient to resolve references to paths/deps in
71// order to form a Bazel-compatible label for conversion.
72type BazelConversionPathContext interface {
73	EarlyModulePathContext
74
75	GetDirectDep(name string) (blueprint.Module, blueprint.DependencyTag)
76	Module() Module
77	ModuleType() string
78	OtherModuleName(m blueprint.Module) string
79	OtherModuleDir(m blueprint.Module) string
80}
81
82// BazelLabelForModuleDeps expects a list of reference to other modules, ("<module>"
83// or ":<module>") and returns a Bazel-compatible label which corresponds to dependencies on the
84// module within the given ctx.
85func BazelLabelForModuleDeps(ctx BazelConversionPathContext, modules []string) bazel.LabelList {
86	var labels bazel.LabelList
87	for _, module := range modules {
88		bpText := module
89		if m := SrcIsModule(module); m == "" {
90			module = ":" + module
91		}
92		if m, t := SrcIsModuleWithTag(module); m != "" {
93			l := getOtherModuleLabel(ctx, m, t)
94			l.OriginalModuleName = bpText
95			labels.Includes = append(labels.Includes, l)
96		} else {
97			ctx.ModuleErrorf("%q, is not a module reference", module)
98		}
99	}
100	return labels
101}
102
103func BazelLabelForModuleSrcSingle(ctx BazelConversionPathContext, path string) bazel.Label {
104	return BazelLabelForModuleSrcExcludes(ctx, []string{path}, []string(nil)).Includes[0]
105}
106
107// BazelLabelForModuleSrc expects a list of path (relative to local module directory) and module
108// references (":<module>") and returns a bazel.LabelList{} containing the resolved references in
109// paths, relative to the local module, or Bazel-labels (absolute if in a different package or
110// relative if within the same package).
111// Properties must have been annotated with struct tag `android:"path"` so that dependencies modules
112// will have already been handled by the path_deps mutator.
113func BazelLabelForModuleSrc(ctx BazelConversionPathContext, paths []string) bazel.LabelList {
114	return BazelLabelForModuleSrcExcludes(ctx, paths, []string(nil))
115}
116
117// BazelLabelForModuleSrc expects lists of path and excludes (relative to local module directory)
118// and module references (":<module>") and returns a bazel.LabelList{} containing the resolved
119// references in paths, minus those in excludes, relative to the local module, or Bazel-labels
120// (absolute if in a different package or relative if within the same package).
121// Properties must have been annotated with struct tag `android:"path"` so that dependencies modules
122// will have already been handled by the path_deps mutator.
123func BazelLabelForModuleSrcExcludes(ctx BazelConversionPathContext, paths, excludes []string) bazel.LabelList {
124	excludeLabels := expandSrcsForBazel(ctx, excludes, []string(nil))
125	excluded := make([]string, 0, len(excludeLabels.Includes))
126	for _, e := range excludeLabels.Includes {
127		excluded = append(excluded, e.Label)
128	}
129	labels := expandSrcsForBazel(ctx, paths, excluded)
130	labels.Excludes = excludeLabels.Includes
131	labels = transformSubpackagePaths(ctx, labels)
132	return labels
133}
134
135// Returns true if a prefix + components[:i] + /Android.bp exists
136// TODO(b/185358476) Could check for BUILD file instead of checking for Android.bp file, or ensure BUILD is always generated?
137func directoryHasBlueprint(fs pathtools.FileSystem, prefix string, components []string, componentIndex int) bool {
138	blueprintPath := prefix
139	if blueprintPath != "" {
140		blueprintPath = blueprintPath + "/"
141	}
142	blueprintPath = blueprintPath + strings.Join(components[:componentIndex+1], "/")
143	blueprintPath = blueprintPath + "/Android.bp"
144	if exists, _, _ := fs.Exists(blueprintPath); exists {
145		return true
146	} else {
147		return false
148	}
149}
150
151// Transform a path (if necessary) to acknowledge package boundaries
152//
153// e.g. something like
154//   async_safe/include/async_safe/CHECK.h
155// might become
156//   //bionic/libc/async_safe:include/async_safe/CHECK.h
157// if the "async_safe" directory is actually a package and not just a directory.
158//
159// In particular, paths that extend into packages are transformed into absolute labels beginning with //.
160func transformSubpackagePath(ctx BazelConversionPathContext, path bazel.Label) bazel.Label {
161	var newPath bazel.Label
162
163	// Don't transform OriginalModuleName
164	newPath.OriginalModuleName = path.OriginalModuleName
165
166	if strings.HasPrefix(path.Label, "//") {
167		// Assume absolute labels are already correct (e.g. //path/to/some/package:foo.h)
168		newPath.Label = path.Label
169		return newPath
170	}
171
172	newLabel := ""
173	pathComponents := strings.Split(path.Label, "/")
174	foundBlueprint := false
175	// Check the deepest subdirectory first and work upwards
176	for i := len(pathComponents) - 1; i >= 0; i-- {
177		pathComponent := pathComponents[i]
178		var sep string
179		if !foundBlueprint && directoryHasBlueprint(ctx.Config().fs, ctx.ModuleDir(), pathComponents, i) {
180			sep = ":"
181			foundBlueprint = true
182		} else {
183			sep = "/"
184		}
185		if newLabel == "" {
186			newLabel = pathComponent
187		} else {
188			newLabel = pathComponent + sep + newLabel
189		}
190	}
191	if foundBlueprint {
192		// Ensure paths end up looking like //bionic/... instead of //./bionic/...
193		moduleDir := ctx.ModuleDir()
194		if strings.HasPrefix(moduleDir, ".") {
195			moduleDir = moduleDir[1:]
196		}
197		// Make the path into an absolute label (e.g. //bionic/libc/foo:bar.h instead of just foo:bar.h)
198		if moduleDir == "" {
199			newLabel = "//" + newLabel
200		} else {
201			newLabel = "//" + moduleDir + "/" + newLabel
202		}
203	}
204	newPath.Label = newLabel
205
206	return newPath
207}
208
209// Transform paths to acknowledge package boundaries
210// See transformSubpackagePath() for more information
211func transformSubpackagePaths(ctx BazelConversionPathContext, paths bazel.LabelList) bazel.LabelList {
212	var newPaths bazel.LabelList
213	for _, include := range paths.Includes {
214		newPaths.Includes = append(newPaths.Includes, transformSubpackagePath(ctx, include))
215	}
216	for _, exclude := range paths.Excludes {
217		newPaths.Excludes = append(newPaths.Excludes, transformSubpackagePath(ctx, exclude))
218	}
219	return newPaths
220}
221
222// expandSrcsForBazel returns bazel.LabelList with paths rooted from the module's local source
223// directory and Bazel target labels, excluding those included in the excludes argument (which
224// should already be expanded to resolve references to Soong-modules). Valid elements of paths
225// include:
226// * filepath, relative to local module directory, resolves as a filepath relative to the local
227//   source directory
228// * glob, relative to the local module directory, resolves as filepath(s), relative to the local
229//    module directory. Because Soong does not have a concept of crossing package boundaries, the
230//    glob as computed by Soong may contain paths that cross package-boundaries that would be
231//    unknowingly omitted if the glob were handled by Bazel. To allow identification and detect
232//    (within Bazel) use of paths that cross package boundaries, we expand globs within Soong rather
233//    than converting Soong glob syntax to Bazel glob syntax. **Invalid for excludes.**
234// * other modules using the ":name{.tag}" syntax. These modules must implement SourceFileProducer
235//    or OutputFileProducer. These resolve as a Bazel label for a target. If the Bazel target is in
236//    the local module directory, it will be returned relative to the current package (e.g.
237//    ":<target>"). Otherwise, it will be returned as an absolute Bazel label (e.g.
238//    "//path/to/dir:<target>"). If the reference to another module cannot be resolved,the function
239//    will panic.
240// Properties passed as the paths or excludes argument must have been annotated with struct tag
241// `android:"path"` so that dependencies on other modules will have already been handled by the
242// path_deps mutator.
243func expandSrcsForBazel(ctx BazelConversionPathContext, paths, expandedExcludes []string) bazel.LabelList {
244	if paths == nil {
245		return bazel.LabelList{}
246	}
247	labels := bazel.LabelList{
248		Includes: []bazel.Label{},
249	}
250
251	// expandedExcludes contain module-dir relative paths, but root-relative paths
252	// are needed for GlobFiles later.
253	var rootRelativeExpandedExcludes []string
254	for _, e := range expandedExcludes {
255		rootRelativeExpandedExcludes = append(rootRelativeExpandedExcludes, filepath.Join(ctx.ModuleDir(), e))
256	}
257
258	for _, p := range paths {
259		if m, tag := SrcIsModuleWithTag(p); m != "" {
260			l := getOtherModuleLabel(ctx, m, tag)
261			if !InList(l.Label, expandedExcludes) {
262				l.OriginalModuleName = fmt.Sprintf(":%s", m)
263				labels.Includes = append(labels.Includes, l)
264			}
265		} else {
266			var expandedPaths []bazel.Label
267			if pathtools.IsGlob(p) {
268				// e.g. turn "math/*.c" in
269				// external/arm-optimized-routines to external/arm-optimized-routines/math/*.c
270				rootRelativeGlobPath := pathForModuleSrc(ctx, p).String()
271				globbedPaths := GlobFiles(ctx, rootRelativeGlobPath, rootRelativeExpandedExcludes)
272				globbedPaths = PathsWithModuleSrcSubDir(ctx, globbedPaths, "")
273				for _, path := range globbedPaths {
274					s := path.Rel()
275					expandedPaths = append(expandedPaths, bazel.Label{Label: s})
276				}
277			} else {
278				if !InList(p, expandedExcludes) {
279					expandedPaths = append(expandedPaths, bazel.Label{Label: p})
280				}
281			}
282			labels.Includes = append(labels.Includes, expandedPaths...)
283		}
284	}
285	return labels
286}
287
288// getOtherModuleLabel returns a bazel.Label for the given dependency/tag combination for the
289// module. The label will be relative to the current directory if appropriate. The dependency must
290// already be resolved by either deps mutator or path deps mutator.
291func getOtherModuleLabel(ctx BazelConversionPathContext, dep, tag string) bazel.Label {
292	m, _ := ctx.GetDirectDep(dep)
293	if m == nil {
294		panic(fmt.Errorf(`Cannot get direct dep %q of %q.
295		This is likely because it was not added via AddDependency().
296		This may be due a mutator skipped during bp2build.`, dep, ctx.Module().Name()))
297	}
298	otherLabel := bazelModuleLabel(ctx, m, tag)
299	label := bazelModuleLabel(ctx, ctx.Module(), "")
300	if samePackage(label, otherLabel) {
301		otherLabel = bazelShortLabel(otherLabel)
302	}
303
304	return bazel.Label{
305		Label: otherLabel,
306	}
307}
308
309func bazelModuleLabel(ctx BazelConversionPathContext, module blueprint.Module, tag string) string {
310	// TODO(b/165114590): Convert tag (":name{.tag}") to corresponding Bazel implicit output targets.
311	b, ok := module.(Bazelable)
312	// TODO(b/181155349): perhaps return an error here if the module can't be/isn't being converted
313	if !ok || !b.ConvertedToBazel(ctx) {
314		return bp2buildModuleLabel(ctx, module)
315	}
316	return b.GetBazelLabel(ctx, module)
317}
318
319func bazelShortLabel(label string) string {
320	i := strings.Index(label, ":")
321	return label[i:]
322}
323
324func bazelPackage(label string) string {
325	i := strings.Index(label, ":")
326	return label[0:i]
327}
328
329func samePackage(label1, label2 string) bool {
330	return bazelPackage(label1) == bazelPackage(label2)
331}
332
333func bp2buildModuleLabel(ctx BazelConversionPathContext, module blueprint.Module) string {
334	moduleName := ctx.OtherModuleName(module)
335	moduleDir := ctx.OtherModuleDir(module)
336	return fmt.Sprintf("//%s:%s", moduleDir, moduleName)
337}
338
339// BazelOutPath is a Bazel output path compatible to be used for mixed builds within Soong/Ninja.
340type BazelOutPath struct {
341	OutputPath
342}
343
344var _ Path = BazelOutPath{}
345var _ objPathProvider = BazelOutPath{}
346
347func (p BazelOutPath) objPathWithExt(ctx ModuleOutPathContext, subdir, ext string) ModuleObjPath {
348	return PathForModuleObj(ctx, subdir, pathtools.ReplaceExtension(p.path, ext))
349}
350
351// PathForBazelOut returns a Path representing the paths... under an output directory dedicated to
352// bazel-owned outputs.
353func PathForBazelOut(ctx PathContext, paths ...string) BazelOutPath {
354	execRootPathComponents := append([]string{"execroot", "__main__"}, paths...)
355	execRootPath := filepath.Join(execRootPathComponents...)
356	validatedExecRootPath, err := validatePath(execRootPath)
357	if err != nil {
358		reportPathError(ctx, err)
359	}
360
361	outputPath := OutputPath{basePath{"", ""},
362		ctx.Config().buildDir,
363		ctx.Config().BazelContext.OutputBase()}
364
365	return BazelOutPath{
366		OutputPath: outputPath.withRel(validatedExecRootPath),
367	}
368}
369
370// PathsForBazelOut returns a list of paths representing the paths under an output directory
371// dedicated to Bazel-owned outputs.
372func PathsForBazelOut(ctx PathContext, paths []string) Paths {
373	outs := make(Paths, 0, len(paths))
374	for _, p := range paths {
375		outs = append(outs, PathForBazelOut(ctx, p))
376	}
377	return outs
378}
379