platform_build_soong/android/proto.go
Spandan Das ab29f57ce6 Fix target_compatible_with non-determinism for proto.include_dirs
A single proto providing directory can be used by multiple soong modules. Some of these
can be
1. Host specific
2. Device specific
3. Both host and device

Since the generated proto_library can have 1:many mapping, it should
have an empty target_compatible_with. Compatiblity will be enforced at
the top-level {cc|java|python}_proto_library.

(This is a followup to aosp/2727054 which did not handle this correctly)

Test: Added a unit test

Change-Id: I09b3def70e3d043fd8ba0d1eb4ffff1910f097d1
2023-09-01 21:30:29 +00:00

431 lines
16 KiB
Go

// Copyright 2017 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 (
"path/filepath"
"strings"
"sync"
"android/soong/bazel"
"github.com/google/blueprint"
"github.com/google/blueprint/proptools"
)
const (
canonicalPathFromRootDefault = true
)
// TODO(ccross): protos are often used to communicate between multiple modules. If the only
// way to convert a proto to source is to reference it as a source file, and external modules cannot
// reference source files in other modules, then every module that owns a proto file will need to
// export a library for every type of external user (lite vs. full, c vs. c++ vs. java). It would
// be better to support a proto module type that exported a proto file along with some include dirs,
// and then external modules could depend on the proto module but use their own settings to
// generate the source.
type ProtoFlags struct {
Flags []string
CanonicalPathFromRoot bool
Dir ModuleGenPath
SubDir ModuleGenPath
OutTypeFlag string
OutParams []string
Deps Paths
}
type protoDependencyTag struct {
blueprint.BaseDependencyTag
name string
}
var ProtoPluginDepTag = protoDependencyTag{name: "plugin"}
func ProtoDeps(ctx BottomUpMutatorContext, p *ProtoProperties) {
if String(p.Proto.Plugin) != "" && String(p.Proto.Type) != "" {
ctx.ModuleErrorf("only one of proto.type and proto.plugin can be specified.")
}
if plugin := String(p.Proto.Plugin); plugin != "" {
ctx.AddFarVariationDependencies(ctx.Config().BuildOSTarget.Variations(),
ProtoPluginDepTag, "protoc-gen-"+plugin)
}
}
func GetProtoFlags(ctx ModuleContext, p *ProtoProperties) ProtoFlags {
var flags []string
var deps Paths
if len(p.Proto.Local_include_dirs) > 0 {
localProtoIncludeDirs := PathsForModuleSrc(ctx, p.Proto.Local_include_dirs)
flags = append(flags, JoinWithPrefix(localProtoIncludeDirs.Strings(), "-I"))
}
if len(p.Proto.Include_dirs) > 0 {
rootProtoIncludeDirs := PathsForSource(ctx, p.Proto.Include_dirs)
flags = append(flags, JoinWithPrefix(rootProtoIncludeDirs.Strings(), "-I"))
}
ctx.VisitDirectDepsWithTag(ProtoPluginDepTag, func(dep Module) {
if hostTool, ok := dep.(HostToolProvider); !ok || !hostTool.HostToolPath().Valid() {
ctx.PropertyErrorf("proto.plugin", "module %q is not a host tool provider",
ctx.OtherModuleName(dep))
} else {
plugin := String(p.Proto.Plugin)
deps = append(deps, hostTool.HostToolPath().Path())
flags = append(flags, "--plugin=protoc-gen-"+plugin+"="+hostTool.HostToolPath().String())
}
})
var protoOutFlag string
if plugin := String(p.Proto.Plugin); plugin != "" {
protoOutFlag = "--" + plugin + "_out"
}
return ProtoFlags{
Flags: flags,
Deps: deps,
OutTypeFlag: protoOutFlag,
CanonicalPathFromRoot: proptools.BoolDefault(p.Proto.Canonical_path_from_root, canonicalPathFromRootDefault),
Dir: PathForModuleGen(ctx, "proto"),
SubDir: PathForModuleGen(ctx, "proto", ctx.ModuleDir()),
}
}
type ProtoProperties struct {
Proto struct {
// Proto generator type. C++: full or lite. Java: micro, nano, stream, or lite.
Type *string `android:"arch_variant"`
// Proto plugin to use as the generator. Must be a cc_binary_host module.
Plugin *string `android:"arch_variant"`
// list of directories that will be added to the protoc include paths.
Include_dirs []string
// list of directories relative to the bp file that will
// be added to the protoc include paths.
Local_include_dirs []string
// whether to identify the proto files from the root of the
// source tree (the original method in Android, useful for
// android-specific protos), or relative from where they were
// specified (useful for external/third party protos).
//
// This defaults to true today, but is expected to default to
// false in the future.
Canonical_path_from_root *bool
} `android:"arch_variant"`
}
func ProtoRule(rule *RuleBuilder, protoFile Path, flags ProtoFlags, deps Paths,
outDir WritablePath, depFile WritablePath, outputs WritablePaths) {
var protoBase string
if flags.CanonicalPathFromRoot {
protoBase = "."
} else {
rel := protoFile.Rel()
protoBase = strings.TrimSuffix(protoFile.String(), rel)
}
rule.Command().
BuiltTool("aprotoc").
FlagWithArg(flags.OutTypeFlag+"=", strings.Join(flags.OutParams, ",")+":"+outDir.String()).
FlagWithDepFile("--dependency_out=", depFile).
FlagWithArg("-I ", protoBase).
Flags(flags.Flags).
Input(protoFile).
Implicits(deps).
ImplicitOutputs(outputs)
rule.Command().
BuiltTool("dep_fixer").Flag(depFile.String())
}
// Bp2buildProtoInfo contains information necessary to pass on to language specific conversion.
type Bp2buildProtoInfo struct {
Type *string
Proto_libs bazel.LabelList
Transitive_proto_libs bazel.LabelList
}
type ProtoAttrs struct {
Srcs bazel.LabelListAttribute
Import_prefix *string
Strip_import_prefix *string
Deps bazel.LabelListAttribute
}
// For each package in the include_dirs property a proto_library target should
// be added to the BUILD file in that package and a mapping should be added here
var includeDirsToProtoDeps = map[string]string{
"external/protobuf/src": "//external/protobuf:libprotobuf-proto",
}
// Partitions srcs by the pkg it is in
// srcs has been created using `TransformSubpackagePaths`
// This function uses existence of Android.bp/BUILD files to create a label that is compatible with the package structure of bp2build workspace
func partitionSrcsByPackage(currentDir string, srcs bazel.LabelList) map[string]bazel.LabelList {
getPackageFromLabel := func(label string) string {
// Remove any preceding //
label = strings.TrimPrefix(label, "//")
split := strings.Split(label, ":")
if len(split) == 1 {
// e.g. foo.proto
return currentDir
} else if split[0] == "" {
// e.g. :foo.proto
return currentDir
} else {
return split[0]
}
}
pkgToSrcs := map[string]bazel.LabelList{}
for _, src := range srcs.Includes {
pkg := getPackageFromLabel(src.Label)
list := pkgToSrcs[pkg]
list.Add(&src)
pkgToSrcs[pkg] = list
}
return pkgToSrcs
}
// Bp2buildProtoProperties converts proto properties, creating a proto_library and returning the
// information necessary for language-specific handling.
func Bp2buildProtoProperties(ctx Bp2buildMutatorContext, m *ModuleBase, srcs bazel.LabelListAttribute) (Bp2buildProtoInfo, bool) {
var info Bp2buildProtoInfo
if srcs.IsEmpty() {
return info, false
}
var protoLibraries bazel.LabelList
var transitiveProtoLibraries bazel.LabelList
var directProtoSrcs bazel.LabelList
// For filegroups that should be converted to proto_library just collect the
// labels of converted proto_library targets.
for _, protoSrc := range srcs.Value.Includes {
src := protoSrc.OriginalModuleName
if fg, ok := ToFileGroupAsLibrary(ctx, src); ok &&
fg.ShouldConvertToProtoLibrary(ctx) {
protoLibraries.Add(&bazel.Label{
Label: fg.GetProtoLibraryLabel(ctx),
})
} else {
directProtoSrcs.Add(&protoSrc)
}
}
name := m.Name() + "_proto"
depsFromFilegroup := protoLibraries
var canonicalPathFromRoot bool
if len(directProtoSrcs.Includes) > 0 {
pkgToSrcs := partitionSrcsByPackage(ctx.ModuleDir(), directProtoSrcs)
protoIncludeDirs := []string{}
for _, pkg := range SortedStringKeys(pkgToSrcs) {
srcs := pkgToSrcs[pkg]
attrs := ProtoAttrs{
Srcs: bazel.MakeLabelListAttribute(srcs),
}
attrs.Deps.Append(bazel.MakeLabelListAttribute(depsFromFilegroup))
for axis, configToProps := range m.GetArchVariantProperties(ctx, &ProtoProperties{}) {
for _, rawProps := range configToProps {
var props *ProtoProperties
var ok bool
if props, ok = rawProps.(*ProtoProperties); !ok {
ctx.ModuleErrorf("Could not cast ProtoProperties to expected type")
}
if axis == bazel.NoConfigAxis {
info.Type = props.Proto.Type
canonicalPathFromRoot = proptools.BoolDefault(props.Proto.Canonical_path_from_root, canonicalPathFromRootDefault)
if !canonicalPathFromRoot {
// an empty string indicates to strips the package path
path := ""
attrs.Strip_import_prefix = &path
}
for _, dir := range props.Proto.Include_dirs {
if dep, ok := includeDirsToProtoDeps[dir]; ok {
attrs.Deps.Add(bazel.MakeLabelAttribute(dep))
} else {
protoIncludeDirs = append(protoIncludeDirs, dir)
}
}
// proto.local_include_dirs are similar to proto.include_dirs, except that it is relative to the module directory
for _, dir := range props.Proto.Local_include_dirs {
relativeToTop := pathForModuleSrc(ctx, dir).String()
protoIncludeDirs = append(protoIncludeDirs, relativeToTop)
}
} else if props.Proto.Type != info.Type && props.Proto.Type != nil {
ctx.ModuleErrorf("Cannot handle arch-variant types for protos at this time.")
}
}
}
if p, ok := m.module.(PkgPathInterface); ok && p.PkgPath(ctx) != nil {
// python_library with pkg_path
// proto_library for this module should have the pkg_path as the import_prefix
attrs.Import_prefix = p.PkgPath(ctx)
attrs.Strip_import_prefix = proptools.StringPtr("")
}
tags := ApexAvailableTagsWithoutTestApexes(ctx.(TopDownMutatorContext), ctx.Module())
moduleDir := ctx.ModuleDir()
if !canonicalPathFromRoot {
// Since we are creating the proto_library in a subpackage, set the import_prefix relative to the current package
if rel, err := filepath.Rel(moduleDir, pkg); err != nil {
ctx.ModuleErrorf("Could not get relative path for %v %v", pkg, err)
} else if rel != "." {
attrs.Import_prefix = &rel
}
}
// TODO - b/246997908: Handle potential orphaned proto_library targets
// To create proto_library targets in the same package, we split the .proto files
// This means that if a proto_library in a subpackage imports another proto_library from the parent package
// (or a different subpackage), it will not find it.
// The CcProtoGen action itself runs fine because we construct the correct ProtoInfo,
// but the FileDescriptorSet of each proto_library might not be compile-able
//
// Add manual tag if either
// 1. .proto files are in more than one package
// 2. proto.include_dirs is not empty
if len(SortedStringKeys(pkgToSrcs)) > 1 || len(protoIncludeDirs) > 0 {
tags.Append(bazel.MakeStringListAttribute([]string{"manual"}))
}
ctx.CreateBazelTargetModule(
bazel.BazelTargetModuleProperties{Rule_class: "proto_library"},
CommonAttributes{Name: name, Dir: proptools.StringPtr(pkg), Tags: tags},
&attrs,
)
l := ""
if pkg == moduleDir { // same package that the original module lives in
l = ":" + name
} else {
l = "//" + pkg + ":" + name
}
protoLibraries.Add(&bazel.Label{
Label: l,
})
}
// Partitioning by packages can create dupes of protoIncludeDirs, so dedupe it first.
protoLibrariesInIncludeDir := createProtoLibraryTargetsForIncludeDirs(ctx, SortedUniqueStrings(protoIncludeDirs))
transitiveProtoLibraries.Append(protoLibrariesInIncludeDir)
}
info.Proto_libs = protoLibraries
info.Transitive_proto_libs = transitiveProtoLibraries
return info, true
}
// PkgPathInterface is used as a type assertion in bp2build to get pkg_path property of python_library_host
type PkgPathInterface interface {
PkgPath(ctx BazelConversionContext) *string
}
var (
protoIncludeDirGeneratedSuffix = ".include_dir_bp2build_generated_proto"
protoIncludeDirsBp2buildKey = NewOnceKey("protoIncludeDirsBp2build")
)
func getProtoIncludeDirsBp2build(config Config) *sync.Map {
return config.Once(protoIncludeDirsBp2buildKey, func() interface{} {
return &sync.Map{}
}).(*sync.Map)
}
// key for dynamically creating proto_library per proto.include_dirs
type protoIncludeDirKey struct {
dir string
subpackgeInDir string
}
// createProtoLibraryTargetsForIncludeDirs creates additional proto_library targets for .proto files in includeDirs
// Since Bazel imposes a constratint that the proto_library must be in the same package as the .proto file, this function
// might create the targets in a subdirectory of `includeDir`
// Returns the labels of the proto_library targets
func createProtoLibraryTargetsForIncludeDirs(ctx Bp2buildMutatorContext, includeDirs []string) bazel.LabelList {
var ret bazel.LabelList
for _, dir := range includeDirs {
if exists, _, _ := ctx.Config().fs.Exists(filepath.Join(dir, "Android.bp")); !exists {
ctx.ModuleErrorf("TODO: Add support for proto.include_dir: %v. This directory does not contain an Android.bp file", dir)
}
dirMap := getProtoIncludeDirsBp2build(ctx.Config())
// Find all proto file targets in this dir
protoLabelsInDir := BazelLabelForSrcPatternExcludes(ctx, dir, "**/*.proto", []string{})
// Partition the labels by package and subpackage(s)
protoLabelelsPartitionedByPkg := partitionSrcsByPackage(dir, protoLabelsInDir)
for _, pkg := range SortedStringKeys(protoLabelelsPartitionedByPkg) {
label := strings.ReplaceAll(dir, "/", ".") + protoIncludeDirGeneratedSuffix
ret.Add(&bazel.Label{
Label: "//" + pkg + ":" + label,
})
key := protoIncludeDirKey{dir: dir, subpackgeInDir: pkg}
if _, exists := dirMap.LoadOrStore(key, true); exists {
// A proto_library has already been created for this package relative to this include dir
continue
}
srcs := protoLabelelsPartitionedByPkg[pkg]
rel, err := filepath.Rel(dir, pkg)
if err != nil {
ctx.ModuleErrorf("Could not create a proto_library in pkg %v due to %v\n", pkg, err)
}
// Create proto_library
attrs := ProtoAttrs{
Srcs: bazel.MakeLabelListAttribute(srcs),
Strip_import_prefix: proptools.StringPtr(""),
}
if rel != "." {
attrs.Import_prefix = proptools.StringPtr(rel)
}
// If a specific directory is listed in proto.include_dirs of two separate modules (one host-specific and another device-specific),
// we do not want to create the proto_library with target_compatible_with of the first visited of these two modules
// As a workarounds, delete `target_compatible_with`
alwaysEnabled := bazel.BoolAttribute{}
alwaysEnabled.Value = proptools.BoolPtr(true)
// Add android and linux explicitly so that fillcommonbp2buildmoduleattrs can override these configs
// When we extend b support for other os'es (darwin/windows), we should add those configs here as well
alwaysEnabled.SetSelectValue(bazel.OsConfigurationAxis, bazel.OsAndroid, proptools.BoolPtr(true))
alwaysEnabled.SetSelectValue(bazel.OsConfigurationAxis, bazel.OsLinux, proptools.BoolPtr(true))
ctx.CreateBazelTargetModuleWithRestrictions(
bazel.BazelTargetModuleProperties{Rule_class: "proto_library"},
CommonAttributes{
Name: label,
Dir: proptools.StringPtr(pkg),
// This proto_library is used to construct a ProtoInfo
// But it might not be buildable on its own
Tags: bazel.MakeStringListAttribute([]string{"manual"}),
},
&attrs,
alwaysEnabled,
)
}
}
return ret
}