platform_build_soong/android/bazel_paths.go
Sam Delmerico 24c5603815 refactor bp2build module allowlists into struct
To make testing easier, refactor existing module-global variables into a
struct that can be mocked.

Test: build/bazel/bp2build.go
Change-Id: I9d177677644ea743641a745b1839a3a8b29f902a
2022-04-14 13:31:05 +00:00

479 lines
20 KiB
Go

// Copyright 2015 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package android
import (
"fmt"
"path/filepath"
"strings"
"android/soong/bazel"
"github.com/google/blueprint"
"github.com/google/blueprint/pathtools"
)
// bazel_paths contains methods to:
// * resolve Soong path and module references into bazel.LabelList
// * resolve Bazel path references into Soong-compatible paths
//
// There is often a similar method for Bazel as there is for Soong path handling and should be used
// in similar circumstances
//
// Bazel Soong
//
// BazelLabelForModuleSrc PathForModuleSrc
// BazelLabelForModuleSrcExcludes PathForModuleSrcExcludes
// BazelLabelForModuleDeps n/a
// tbd PathForSource
// tbd ExistentPathsForSources
// PathForBazelOut PathForModuleOut
//
// Use cases:
// * Module contains a property (often tagged `android:"path"`) that expects paths *relative to the
// module directory*:
// * BazelLabelForModuleSrcExcludes, if the module also contains an excludes_<propname> property
// * BazelLabelForModuleSrc, otherwise
// * Converting references to other modules to Bazel Labels:
// BazelLabelForModuleDeps
// * Converting a path obtained from bazel_handler cquery results:
// PathForBazelOut
//
// NOTE: all Soong globs are expanded within Soong rather than being converted to a Bazel glob
// syntax. This occurs because Soong does not have a concept of crossing package boundaries,
// so the glob as computed by Soong may contain paths that cross package-boundaries. These
// would be unknowingly omitted if the glob were handled by Bazel. By expanding globs within
// Soong, we support identification and detection (within Bazel) use of paths that cross
// package boundaries.
//
// Path resolution:
// * filepath/globs: resolves as itself or is converted to an absolute Bazel label (e.g.
// //path/to/dir:<filepath>) if path exists in a separate package or subpackage.
// * references to other modules (using the ":name{.tag}" syntax). These resolve as a Bazel label
// for a target. If the Bazel target is in the local module directory, it will be returned
// relative to the current package (e.g. ":<target>"). Otherwise, it will be returned as an
// absolute Bazel label (e.g. "//path/to/dir:<target>"). If the reference to another module
// cannot be resolved,the function will panic. This is often due to the dependency not being added
// via an AddDependency* method.
// A minimal context interface to check if a module should be converted by bp2build,
// with functions containing information to match against allowlists and denylists.
// If a module is deemed to be convertible by bp2build, then it should rely on a
// BazelConversionPathContext for more functions for dep/path features.
type BazelConversionContext interface {
Config() Config
Module() Module
OtherModuleType(m blueprint.Module) string
OtherModuleName(m blueprint.Module) string
OtherModuleDir(m blueprint.Module) string
ModuleErrorf(format string, args ...interface{})
}
// A subset of the ModuleContext methods which are sufficient to resolve references to paths/deps in
// order to form a Bazel-compatible label for conversion.
type BazelConversionPathContext interface {
EarlyModulePathContext
BazelConversionContext
ModuleErrorf(fmt string, args ...interface{})
PropertyErrorf(property, fmt string, args ...interface{})
GetDirectDep(name string) (blueprint.Module, blueprint.DependencyTag)
ModuleFromName(name string) (blueprint.Module, bool)
AddUnconvertedBp2buildDep(string)
AddMissingBp2buildDep(dep string)
}
// BazelLabelForModuleDeps expects a list of reference to other modules, ("<module>"
// or ":<module>") and returns a Bazel-compatible label which corresponds to dependencies on the
// module within the given ctx.
func BazelLabelForModuleDeps(ctx BazelConversionPathContext, modules []string) bazel.LabelList {
return BazelLabelForModuleDepsWithFn(ctx, modules, BazelModuleLabel)
}
// BazelLabelForModuleWholeDepsExcludes expects two lists: modules (containing modules to include in
// the list), and excludes (modules to exclude from the list). Both of these should contain
// references to other modules, ("<module>" or ":<module>"). It returns a Bazel-compatible label
// list which corresponds to dependencies on the module within the given ctx, and the excluded
// dependencies. Prebuilt dependencies will be appended with _alwayslink so they can be handled as
// whole static libraries.
func BazelLabelForModuleDepsExcludes(ctx BazelConversionPathContext, modules, excludes []string) bazel.LabelList {
return BazelLabelForModuleDepsExcludesWithFn(ctx, modules, excludes, BazelModuleLabel)
}
// BazelLabelForModuleDepsWithFn expects a list of reference to other modules, ("<module>"
// or ":<module>") and applies moduleToLabelFn to determine and return a Bazel-compatible label
// which corresponds to dependencies on the module within the given ctx.
func BazelLabelForModuleDepsWithFn(ctx BazelConversionPathContext, modules []string,
moduleToLabelFn func(BazelConversionPathContext, blueprint.Module) string) bazel.LabelList {
var labels bazel.LabelList
// In some cases, a nil string list is different than an explicitly empty list.
if len(modules) == 0 && modules != nil {
labels.Includes = []bazel.Label{}
return labels
}
for _, module := range modules {
bpText := module
if m := SrcIsModule(module); m == "" {
module = ":" + module
}
if m, t := SrcIsModuleWithTag(module); m != "" {
l := getOtherModuleLabel(ctx, m, t, moduleToLabelFn)
if l != nil {
l.OriginalModuleName = bpText
labels.Includes = append(labels.Includes, *l)
}
} else {
ctx.ModuleErrorf("%q, is not a module reference", module)
}
}
return labels
}
// BazelLabelForModuleDepsExcludesWithFn expects two lists: modules (containing modules to include in the
// list), and excludes (modules to exclude from the list). Both of these should contain references
// to other modules, ("<module>" or ":<module>"). It applies moduleToLabelFn to determine and return a
// Bazel-compatible label list which corresponds to dependencies on the module within the given ctx, and
// the excluded dependencies.
func BazelLabelForModuleDepsExcludesWithFn(ctx BazelConversionPathContext, modules, excludes []string,
moduleToLabelFn func(BazelConversionPathContext, blueprint.Module) string) bazel.LabelList {
moduleLabels := BazelLabelForModuleDepsWithFn(ctx, RemoveListFromList(modules, excludes), moduleToLabelFn)
if len(excludes) == 0 {
return moduleLabels
}
excludeLabels := BazelLabelForModuleDepsWithFn(ctx, excludes, moduleToLabelFn)
return bazel.LabelList{
Includes: moduleLabels.Includes,
Excludes: excludeLabels.Includes,
}
}
func BazelLabelForModuleSrcSingle(ctx BazelConversionPathContext, path string) bazel.Label {
if srcs := BazelLabelForModuleSrcExcludes(ctx, []string{path}, []string(nil)).Includes; len(srcs) > 0 {
return srcs[0]
}
return bazel.Label{}
}
func BazelLabelForModuleDepSingle(ctx BazelConversionPathContext, path string) bazel.Label {
if srcs := BazelLabelForModuleDepsExcludes(ctx, []string{path}, []string(nil)).Includes; len(srcs) > 0 {
return srcs[0]
}
return bazel.Label{}
}
// BazelLabelForModuleSrc expects a list of path (relative to local module directory) and module
// references (":<module>") and returns a bazel.LabelList{} containing the resolved references in
// paths, relative to the local module, or Bazel-labels (absolute if in a different package or
// relative if within the same package).
// Properties must have been annotated with struct tag `android:"path"` so that dependencies modules
// will have already been handled by the path_deps mutator.
func BazelLabelForModuleSrc(ctx BazelConversionPathContext, paths []string) bazel.LabelList {
return BazelLabelForModuleSrcExcludes(ctx, paths, []string(nil))
}
// BazelLabelForModuleSrc expects lists of path and excludes (relative to local module directory)
// and module references (":<module>") and returns a bazel.LabelList{} containing the resolved
// references in paths, minus those in excludes, relative to the local module, or Bazel-labels
// (absolute if in a different package or relative if within the same package).
// Properties must have been annotated with struct tag `android:"path"` so that dependencies modules
// will have already been handled by the path_deps mutator.
func BazelLabelForModuleSrcExcludes(ctx BazelConversionPathContext, paths, excludes []string) bazel.LabelList {
excludeLabels := expandSrcsForBazel(ctx, excludes, []string(nil))
excluded := make([]string, 0, len(excludeLabels.Includes))
for _, e := range excludeLabels.Includes {
excluded = append(excluded, e.Label)
}
labels := expandSrcsForBazel(ctx, paths, excluded)
labels.Excludes = excludeLabels.Includes
labels = transformSubpackagePaths(ctx, labels)
return labels
}
// Returns true if a prefix + components[:i] + /Android.bp exists
// TODO(b/185358476) Could check for BUILD file instead of checking for Android.bp file, or ensure BUILD is always generated?
func directoryHasBlueprint(fs pathtools.FileSystem, prefix string, components []string, componentIndex int) bool {
blueprintPath := prefix
if blueprintPath != "" {
blueprintPath = blueprintPath + "/"
}
blueprintPath = blueprintPath + strings.Join(components[:componentIndex+1], "/")
blueprintPath = blueprintPath + "/Android.bp"
if exists, _, _ := fs.Exists(blueprintPath); exists {
return true
} else {
return false
}
}
// Transform a path (if necessary) to acknowledge package boundaries
//
// e.g. something like
// async_safe/include/async_safe/CHECK.h
// might become
// //bionic/libc/async_safe:include/async_safe/CHECK.h
// if the "async_safe" directory is actually a package and not just a directory.
//
// In particular, paths that extend into packages are transformed into absolute labels beginning with //.
func transformSubpackagePath(ctx BazelConversionPathContext, path bazel.Label) bazel.Label {
var newPath bazel.Label
// Don't transform OriginalModuleName
newPath.OriginalModuleName = path.OriginalModuleName
if strings.HasPrefix(path.Label, "//") {
// Assume absolute labels are already correct (e.g. //path/to/some/package:foo.h)
newPath.Label = path.Label
return newPath
}
newLabel := ""
pathComponents := strings.Split(path.Label, "/")
foundBlueprint := false
// Check the deepest subdirectory first and work upwards
for i := len(pathComponents) - 1; i >= 0; i-- {
pathComponent := pathComponents[i]
var sep string
if !foundBlueprint && directoryHasBlueprint(ctx.Config().fs, ctx.ModuleDir(), pathComponents, i) {
sep = ":"
foundBlueprint = true
} else {
sep = "/"
}
if newLabel == "" {
newLabel = pathComponent
} else {
newLabel = pathComponent + sep + newLabel
}
}
if foundBlueprint {
// Ensure paths end up looking like //bionic/... instead of //./bionic/...
moduleDir := ctx.ModuleDir()
if strings.HasPrefix(moduleDir, ".") {
moduleDir = moduleDir[1:]
}
// Make the path into an absolute label (e.g. //bionic/libc/foo:bar.h instead of just foo:bar.h)
if moduleDir == "" {
newLabel = "//" + newLabel
} else {
newLabel = "//" + moduleDir + "/" + newLabel
}
}
newPath.Label = newLabel
return newPath
}
// Transform paths to acknowledge package boundaries
// See transformSubpackagePath() for more information
func transformSubpackagePaths(ctx BazelConversionPathContext, paths bazel.LabelList) bazel.LabelList {
var newPaths bazel.LabelList
for _, include := range paths.Includes {
newPaths.Includes = append(newPaths.Includes, transformSubpackagePath(ctx, include))
}
for _, exclude := range paths.Excludes {
newPaths.Excludes = append(newPaths.Excludes, transformSubpackagePath(ctx, exclude))
}
return newPaths
}
// Converts root-relative Paths to a list of bazel.Label relative to the module in ctx.
func RootToModuleRelativePaths(ctx BazelConversionPathContext, paths Paths) []bazel.Label {
var newPaths []bazel.Label
for _, path := range PathsWithModuleSrcSubDir(ctx, paths, "") {
s := path.Rel()
newPaths = append(newPaths, bazel.Label{Label: s})
}
return newPaths
}
// expandSrcsForBazel returns bazel.LabelList with paths rooted from the module's local source
// directory and Bazel target labels, excluding those included in the excludes argument (which
// should already be expanded to resolve references to Soong-modules). Valid elements of paths
// include:
// * filepath, relative to local module directory, resolves as a filepath relative to the local
// source directory
// * glob, relative to the local module directory, resolves as filepath(s), relative to the local
// module directory. Because Soong does not have a concept of crossing package boundaries, the
// glob as computed by Soong may contain paths that cross package-boundaries that would be
// unknowingly omitted if the glob were handled by Bazel. To allow identification and detect
// (within Bazel) use of paths that cross package boundaries, we expand globs within Soong rather
// than converting Soong glob syntax to Bazel glob syntax. **Invalid for excludes.**
// * other modules using the ":name{.tag}" syntax. These modules must implement SourceFileProducer
// or OutputFileProducer. These resolve as a Bazel label for a target. If the Bazel target is in
// the local module directory, it will be returned relative to the current package (e.g.
// ":<target>"). Otherwise, it will be returned as an absolute Bazel label (e.g.
// "//path/to/dir:<target>"). If the reference to another module cannot be resolved,the function
// will panic.
// Properties passed as the paths or excludes argument must have been annotated with struct tag
// `android:"path"` so that dependencies on other modules will have already been handled by the
// path_deps mutator.
func expandSrcsForBazel(ctx BazelConversionPathContext, paths, expandedExcludes []string) bazel.LabelList {
if paths == nil {
return bazel.LabelList{}
}
labels := bazel.LabelList{
Includes: []bazel.Label{},
}
// expandedExcludes contain module-dir relative paths, but root-relative paths
// are needed for GlobFiles later.
var rootRelativeExpandedExcludes []string
for _, e := range expandedExcludes {
rootRelativeExpandedExcludes = append(rootRelativeExpandedExcludes, filepath.Join(ctx.ModuleDir(), e))
}
for _, p := range paths {
if m, tag := SrcIsModuleWithTag(p); m != "" {
l := getOtherModuleLabel(ctx, m, tag, BazelModuleLabel)
if l != nil && !InList(l.Label, expandedExcludes) {
l.OriginalModuleName = fmt.Sprintf(":%s", m)
labels.Includes = append(labels.Includes, *l)
}
} else {
var expandedPaths []bazel.Label
if pathtools.IsGlob(p) {
// e.g. turn "math/*.c" in
// external/arm-optimized-routines to external/arm-optimized-routines/math/*.c
rootRelativeGlobPath := pathForModuleSrc(ctx, p).String()
expandedPaths = RootToModuleRelativePaths(ctx, GlobFiles(ctx, rootRelativeGlobPath, rootRelativeExpandedExcludes))
} else {
if !InList(p, expandedExcludes) {
expandedPaths = append(expandedPaths, bazel.Label{Label: p})
}
}
labels.Includes = append(labels.Includes, expandedPaths...)
}
}
return labels
}
// getOtherModuleLabel returns a bazel.Label for the given dependency/tag combination for the
// module. The label will be relative to the current directory if appropriate. The dependency must
// already be resolved by either deps mutator or path deps mutator.
func getOtherModuleLabel(ctx BazelConversionPathContext, dep, tag string,
labelFromModule func(BazelConversionPathContext, blueprint.Module) string) *bazel.Label {
m, _ := ctx.ModuleFromName(dep)
// The module was not found in an Android.bp file, this is often due to:
// * a limited manifest
// * a required module not being converted from Android.mk
if m == nil {
ctx.AddMissingBp2buildDep(dep)
return &bazel.Label{
Label: ":" + dep + "__BP2BUILD__MISSING__DEP",
}
}
if !convertedToBazel(ctx, m) {
ctx.AddUnconvertedBp2buildDep(dep)
}
label := BazelModuleLabel(ctx, ctx.Module())
otherLabel := labelFromModule(ctx, m)
// TODO(b/165114590): Convert tag (":name{.tag}") to corresponding Bazel implicit output targets.
if samePackage(label, otherLabel) {
otherLabel = bazelShortLabel(otherLabel)
}
return &bazel.Label{
Label: otherLabel,
}
}
func BazelModuleLabel(ctx BazelConversionPathContext, module blueprint.Module) string {
// TODO(b/165114590): Convert tag (":name{.tag}") to corresponding Bazel implicit output targets.
if !convertedToBazel(ctx, module) {
return bp2buildModuleLabel(ctx, module)
}
b, _ := module.(Bazelable)
return b.GetBazelLabel(ctx, module)
}
func bazelShortLabel(label string) string {
i := strings.Index(label, ":")
if i == -1 {
panic(fmt.Errorf("Could not find the ':' character in '%s', expected a fully qualified label.", label))
}
return label[i:]
}
func bazelPackage(label string) string {
i := strings.Index(label, ":")
if i == -1 {
panic(fmt.Errorf("Could not find the ':' character in '%s', expected a fully qualified label.", label))
}
return label[0:i]
}
func samePackage(label1, label2 string) bool {
return bazelPackage(label1) == bazelPackage(label2)
}
func bp2buildModuleLabel(ctx BazelConversionContext, module blueprint.Module) string {
moduleName := ctx.OtherModuleName(module)
moduleDir := ctx.OtherModuleDir(module)
return fmt.Sprintf("//%s:%s", moduleDir, moduleName)
}
// BazelOutPath is a Bazel output path compatible to be used for mixed builds within Soong/Ninja.
type BazelOutPath struct {
OutputPath
}
// ensure BazelOutPath implements Path
var _ Path = BazelOutPath{}
// ensure BazelOutPath implements genPathProvider
var _ genPathProvider = BazelOutPath{}
// ensure BazelOutPath implements objPathProvider
var _ objPathProvider = BazelOutPath{}
func (p BazelOutPath) genPathWithExt(ctx ModuleOutPathContext, subdir, ext string) ModuleGenPath {
return PathForModuleGen(ctx, subdir, pathtools.ReplaceExtension(p.path, ext))
}
func (p BazelOutPath) objPathWithExt(ctx ModuleOutPathContext, subdir, ext string) ModuleObjPath {
return PathForModuleObj(ctx, subdir, pathtools.ReplaceExtension(p.path, ext))
}
// PathForBazelOut returns a Path representing the paths... under an output directory dedicated to
// bazel-owned outputs.
func PathForBazelOut(ctx PathContext, paths ...string) BazelOutPath {
execRootPathComponents := append([]string{"execroot", "__main__"}, paths...)
execRootPath := filepath.Join(execRootPathComponents...)
validatedExecRootPath, err := validatePath(execRootPath)
if err != nil {
reportPathError(ctx, err)
}
outputPath := OutputPath{basePath{"", ""},
ctx.Config().soongOutDir,
ctx.Config().BazelContext.OutputBase()}
return BazelOutPath{
OutputPath: outputPath.withRel(validatedExecRootPath),
}
}
// PathsForBazelOut returns a list of paths representing the paths under an output directory
// dedicated to Bazel-owned outputs.
func PathsForBazelOut(ctx PathContext, paths []string) Paths {
outs := make(Paths, 0, len(paths))
for _, p := range paths {
outs = append(outs, PathForBazelOut(ctx, p))
}
return outs
}