blob: e9c6066c94c5e42e8823abd29911d2cf164388cc [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package main
import (
"context"
"path/filepath"
"regexp"
"strings"
"go.chromium.org/luci/common/data/stringset"
"go.chromium.org/luci/common/logging"
kpb "infra/cmd/package_index/kythe/proto"
)
const mojomScript = "/mojom_bindings_generator.py"
// mojomImportRe is used for finding required_input for a Mojom target,
// by finding the imports in its source.
// This could possibly have false positives (e.g. if there's an import in a
// C-style multiline comment), but it shouldn't have any false negatives, which
// is more important here.
var mojomImportRe = regexp.MustCompile(`(?m)^\s*import\s*"([^"]*)"`)
// mojomTarget contains all information needed to process a mojom target.
type mojomTarget struct {
imports []string
args []string
rootDir string
outDir string
corpus string
buildConfig string
targetName string
target gnTargetInfo
hashMap *FileHashMap
ctx context.Context
}
// newMojomTarget initializes a new mojomTarget struct.
func newMojomTarget(ctx context.Context, gnTargetDict map[string]gnTargetInfo, targetName string,
hashMap *FileHashMap, rootDir, outDir, corpus, buildConfig string) (*mojomTarget, error) {
m := &mojomTarget{
ctx: ctx,
targetName: targetName,
rootDir: rootDir,
outDir: outDir,
corpus: corpus,
buildConfig: buildConfig,
hashMap: hashMap,
}
m.target = gnTargetDict[m.targetName]
m.args = m.mergeFeatureArgs(gnTargetDict)
imp, err := m.findMojomImports()
if err != nil {
return nil, err
}
m.imports = imp
return m, nil
}
// mergeFeatureArgs returns a combined list of args from the Mojom target targetName,
// stored in gnTargetsMap, and args from a parser target based on targetName.
//
// The Mojom toolchain works in two phases, first parsing the file with one tool
// which dumps the AST, then feeding the AST into the bindings generator. The
// Kythe indexer, however, works in one phase, and hence needs some arguments
// from each of these tools. In particular, definitions gated on disabled
// features are removed from the AST directly by the parser tool.
func (m *mojomTarget) mergeFeatureArgs(gnTargetDict map[string]gnTargetInfo) []string {
args := gnTargetDict[m.targetName].Args
if len(args) > 0 && strings.HasSuffix(args[0], mojomScript) {
args = args[1:]
}
parserTarget := m.targetName[:len(m.targetName)-len("__generator")] + "__parser"
parserTargetDict := gnTargetDict[parserTarget]
parserTargetArgs := parserTargetDict.Args
for i := 0; i < len(parserTargetArgs)-1; i++ {
if parserTargetArgs[i] == "--enable_feature" {
args = append(args, parserTargetArgs[i:i+2]...)
}
}
return args
}
// findMojomImports finds the direct imports of a Mojom target.
//
// We do this by using a quick and dirty regex to extract files that are
// actually imported, rather than using the gn dependency structure. A Mojom
// file is allowed to import any file it transitively depends on, which usually
// includes way more files than it actually includes.
func (m *mojomTarget) findMojomImports() ([]string, error) {
var importPaths []string
args := m.target.Args
for i := 0; i < len(args)-1; i++ {
if args[i] == "-I" {
imp, err := filepath.Abs(filepath.Join(m.rootDir, m.outDir, args[i+1]))
if err != nil {
return nil, err
}
importPaths = append(importPaths, imp)
}
}
imports := stringset.New(0)
for _, src := range m.target.Sources {
gn, err := convertGnPath(m.ctx, src, m.outDir)
if err != nil {
return nil, err
}
p := filepath.Join(m.rootDir, m.outDir, gn)
imports.AddAll(findImports(m.ctx, mojomImportRe, p, importPaths))
}
return imports.ToSlice(), nil
}
// getUnit returns a compilation unit for a mojom target.
func (m *mojomTarget) getUnit() (*kpb.CompilationUnit, error) {
unitProto := &kpb.CompilationUnit{}
var sourceFiles []string
for _, src := range m.target.Sources {
gn, err := convertGnPath(m.ctx, src, m.outDir)
if err != nil {
return nil, err
}
sourceFiles = append(sourceFiles, convertPathToForwardSlashes(gn))
}
unitProto.SourceFile = sourceFiles
// gn produces an unsubstituted {{response_file_name}} for filelist. We
// can't work with this, so we remove it and add the source files as a
// positional argument instead.
for _, arg := range m.args {
if !strings.HasPrefix(arg, "--filelist=") {
unitProto.Argument = append(unitProto.Argument, arg)
}
}
unitProto.Argument = append(unitProto.Argument, sourceFiles...)
unitProto.VName = &kpb.VName{Corpus: m.corpus, Language: "mojom"}
if m.buildConfig != "" {
injectUnitBuildDetails(m.ctx, unitProto, m.buildConfig)
}
// Files in a module might import other files in the same module. Don't
// include the file twice if so.
var importedFiles []string
srcSet := stringset.NewFromSlice(sourceFiles...)
for _, imp := range m.imports {
p, err := filepath.Rel(filepath.Join(m.rootDir, m.outDir), convertPathToForwardSlashes(imp))
if err != nil {
return nil, err
}
if !srcSet.Has(p) {
importedFiles = append(importedFiles, p)
}
}
for _, requiredFile := range append(sourceFiles, importedFiles...) {
p, err := filepath.Abs(filepath.Join(m.rootDir, m.outDir, requiredFile))
if err != nil {
return nil, err
}
// We don't want to fail completely if the file doesn't exist.
h, ok := m.hashMap.Filehash(p)
if !ok {
logging.Warningf(m.ctx, "Missing from filehashes %s\n", p)
continue
}
requiredInput := &kpb.CompilationUnit_FileInput{
VName: &kpb.VName{
Corpus: m.corpus,
Path: convertPathToForwardSlashes(normalizePath(m.outDir, requiredFile)),
},
Info: &kpb.FileInfo{
Digest: h,
Path: convertPathToForwardSlashes(requiredFile),
},
}
unitProto.RequiredInput = append(unitProto.GetRequiredInput(), requiredInput)
}
return unitProto, nil
}
// getFiles retrieves a list of all files that are required for compilation of a target.
// Returns a list of all included files, in their absolute paths.
func (m *mojomTarget) getFiles() ([]string, error) {
var dataFiles []string
for _, src := range m.target.Sources {
gn, err := convertGnPath(m.ctx, src, m.outDir)
if err != nil {
return nil, err
}
dataFiles = append(dataFiles, filepath.Join(m.rootDir, m.outDir, gn))
}
return dataFiles, nil
}
// mojomTargetProcessor takes in a target and either returns an error if the target
// isn't a proto target or returns a processedTarget struct.
//
// If files is true, process the target files. Otherwise, process the target unit.
func mojomTargetProcessor(ctx context.Context, rootPath, outDir, corpus, buildConfig string,
hashMaps *FileHashMap, t *gnTarget) (GnTargetInterface, error) {
if !isMojomTarget(t) {
return nil, errNotSupported
}
return newMojomTarget(ctx, gnTargetsMap, t.targetName, hashMaps, rootPath, outDir, corpus, buildConfig)
}
// IsMojomTarget checks if a GN target is a Mojom target.
//
// Note that there are multiple GN targets for each Mojom build rule, due to
// how the mojom.gni rules are defined. We pick a single canonical one out of
// this set, namely the __generator target, which generates the standard C++
// bindings.
func isMojomTarget(t *gnTarget) bool {
if !strings.HasSuffix(t.targetName, "__generator") {
return false
}
script := t.targetInfo.Script
// Determine if wrapper script is used. If it is, extract actual script
// which is located at the very first argument.
if strings.HasSuffix(script, "/python2_action.py") && len(t.targetInfo.Args) > 0 {
script = t.targetInfo.Args[0]
}
if !strings.HasSuffix(script, mojomScript) {
return false
}
args := t.targetInfo.Args
argsSet := stringset.NewFromSlice(args...)
if !argsSet.Has("generate") || argsSet.Has("--variant") ||
argsSet.Has("--generate_non_variant_code") || argsSet.Has("--generate_message_ids") {
return false
}
// For now we don't support xrefs for languages other than C++, so
// the mojom analyzer only bothers with the C++ output.
argsCpp := false
for i := 0; i < len(args)-1; i++ {
if args[i] == "-g" && args[i+1] == "c++" {
argsCpp = true
break
}
}
if !argsCpp {
return false
}
// TODO(crbug.com/1057746): Fix cross reference support for auto-generated files.
for _, src := range t.targetInfo.Sources {
if strings.HasPrefix(src, "//out") {
return false
}
}
return true
}