blob: 7a04a13d7b1a7311db99ef4b1c3c94770a40dc46 [file]
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package main
import (
"cmp"
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"os"
"path/filepath"
"slices"
"strings"
"sync"
"text/tabwriter"
"time"
"unique"
"go.chromium.org/luci/common/data/stringset"
"go.chromium.org/luci/common/logging"
"go.chromium.org/luci/common/logging/gologger"
kpb "go.chromium.org/infra/cmd/package_index/kythe/proto"
)
// Size of buffered channels.
var chanSize = 1000
// Number of goroutines to use in parallel processing.
var numRoutines = 32
var (
projectFlag = flag.String("project", "chromium", "Project this kzip should be generated for. This should be set to 'chromium', 'chrome', or 'chromiumos'. Defaults to 'chromium'.")
outputFlag = flag.String("path_to_archive_output", "", "Path to index pack archive to be generated.")
compDBFlag = flag.String("path_to_compdb", "", "Path to the compilation database.")
gnFlag = flag.String("path_to_gn_targets", "", "Path to the gn targets json file.")
corpusFlag = flag.String("corpus", "", "Kythe corpus to use for the vname.")
existingKzipsFlag = flag.String("path_to_java_kzips", "", "Path to already generated java kzips which will be included in the final index pack.")
buildFlag = flag.String("build_config", "", "Build config to use in the unit file.")
clangTargetArchFlag = flag.String("clang_target_arch", "", "Target architecture to provide via -target in any clang commands.")
checkoutFlag = flag.String("checkout_dir", "", "Root of the repository.")
outDirFlag = flag.String("out_dir", "src/out/Debug", "Output directory from which compilation is run.")
filepathsFlag = flag.Bool("keep_filepaths_files", false, "Keep the .filepaths files used for index pack generation.")
verboseFlag = flag.Bool("verbose", false, "Print the details of every file being written to the index pack.")
useSisoFlag = flag.Bool("use_siso", false, "Call siso query deps to get clang files instead of reading from *.filepaths")
allowedProjects = stringset.NewFromSlice(
"chromium",
"chromiumos",
"chrome",
"chromeos")
)
// This should be same as
// https://chromium.googlesource.com/build/+/siso/v1.4.22/siso/subcmd/query/deps.go#77
// but we want to avoid depending on build repo from here.
type depInfo struct {
Target string `json:"target"`
Deps []string `json:"deps"`
}
// Only built when use_siso = true.
// Key: source file, value: list of dependencies.
type targetDepsMap = map[unique.Handle[string]][]unique.Handle[string]
// validateFlags checks that the required flags are present.
func validateFlags(ctx context.Context) {
flagErr := false
if !allowedProjects.Has(*projectFlag) {
logging.Errorf(ctx, "project not supported. [%s] are the only supported projects.", strings.Join(allowedProjects.ToSortedSlice(), ", "))
flagErr = true
}
if *outputFlag == "" {
logging.Errorf(ctx, "path_to_archive_output flag required.")
flagErr = true
}
if *compDBFlag == "" {
logging.Errorf(ctx, "path_to_compdb flag required.")
flagErr = true
}
if *gnFlag == "" {
logging.Errorf(ctx, "path_to_gn_targets flag required.")
flagErr = true
}
if *corpusFlag == "" {
logging.Errorf(ctx, "corpus flag required.")
flagErr = true
}
if *checkoutFlag == "" {
logging.Errorf(ctx, "checkout_dir flag required.")
flagErr = true
}
if flagErr {
panic("Flags are missing or not supported")
}
}
// Return if a filename is a source file with certain file extensions.
func isSourceFile(filename string) bool {
ext := filepath.Ext(filename)
switch ext {
case ".c", ".cc", ".cxx", ".cpp", ".m", ".mm", ".S":
return true
default:
return false
}
}
func loadSisoJsonDeps(depsReader io.Reader) (targetDepsMap, error) {
m := make(targetDepsMap)
var target depInfo
decoder := json.NewDecoder(depsReader)
for {
// Clean up the target variable to reuse.
target.Target = ""
target.Deps = target.Deps[:0]
err := decoder.Decode(&target)
if errors.Is(err, io.EOF) {
break
}
if err != nil {
return m, fmt.Errorf("fail to decode record with error: %v", err)
}
// Only process obj/ targets.
if len(target.Deps) == 0 || !strings.HasPrefix(target.Target, "obj/") {
continue
}
var sourceFileHandle unique.Handle[string]
targetHandles := make([]unique.Handle[string], 0, len(target.Deps))
for _, dep := range target.Deps {
h := unique.Make(dep)
// Find the first source file.
// It is usually the first/second element of the dependency list.
if sourceFileHandle == (unique.Handle[string]{}) && isSourceFile(dep) {
sourceFileHandle = h
}
targetHandles = append(targetHandles, h)
}
if sourceFileHandle == (unique.Handle[string]{}) {
// If no source file, just ignore the current group.
continue
}
value, ok := m[sourceFileHandle]
if !ok {
m[sourceFileHandle] = targetHandles
continue
}
// Merge the dependencies if source file is previously encountered.
set := make(map[unique.Handle[string]]struct{})
for _, val := range value {
set[val] = struct{}{}
}
for _, val := range targetHandles {
set[val] = struct{}{}
}
result := make([]unique.Handle[string], 0, len(set))
for key := range set {
result = append(result, key)
}
slices.SortFunc(result, func(a, b unique.Handle[string]) int {
return cmp.Compare(a.Value(), b.Value())
})
m[sourceFileHandle] = result
}
return m, nil
}
func main() {
ctx := gologger.StdConfig.Use(context.Background())
flag.Parse()
validateFlags(ctx)
// Remove the old zip archive (if it exists). This avoids the new index
// pack being added to the old zip archive.
if _, err := os.Stat(*outputFlag); err == nil { // Old kzip exists.
err = os.Remove(*outputFlag)
if err != nil {
panic(err)
}
}
// Setup.
if *verboseFlag {
ctx = logging.SetLevel(ctx, logging.Debug)
}
logging.Infof(ctx, "%s: Index generation...", time.Now().Format("15:04:05"))
rootPath, err := filepath.Abs(filepath.Join(*checkoutFlag, ".."))
if err != nil {
panic(err)
}
ip := newIndexPack(ctx, *outputFlag, rootPath, *outDirFlag, *compDBFlag,
*gnFlag, *existingKzipsFlag, *corpusFlag, *buildFlag, *clangTargetArchFlag)
// Process existing kzips.
existingKzipChannel := make(chan string, chanSize)
go func() {
if ip.existingJavaKzipsPath != "" {
err := ip.mergeExistingKzips(existingKzipChannel)
if err != nil {
panic(err)
}
} else {
close(existingKzipChannel)
}
}()
var kzipEntryWg sync.WaitGroup
kzipEntryChannel := make(chan kzipEntry, 100) // Channel size is reduced for chromiumos builder.
kzipSet := NewConcurrentSet(0)
kzipEntryWg.Add(1)
go func() {
err := ip.processExistingKzips(ctx, existingKzipChannel, kzipEntryChannel, kzipSet)
if err != nil {
panic(err)
}
kzipEntryWg.Done()
}()
// Process targets.
unitProtoChannel := make(chan *kpb.CompilationUnit, chanSize)
dataFileChannel := make(chan string, chanSize)
// Process clang targets.
clangTargets := NewClangTargets(ip.compDBPath)
clangTargets.DataWg.Add(numRoutines)
clangTargets.KzipDataWg.Add(numRoutines)
clangTargets.UnitWg.Add(numRoutines)
// Load siso output into memory when enabled.
var targetDepsMap targetDepsMap
if *useSisoFlag {
targetDepsMap, err = loadSisoJsonDeps(os.Stdin)
if err != nil {
panic(err)
}
}
// Process clang targets.
for range numRoutines {
go func() {
err := clangTargets.ProcessClangTargets(ip.ctx, ip.rootPath, ip.outDir, ip.corpus,
ip.buildConfig, ip.clangTargetArch, ip.hashMaps, dataFileChannel, unitProtoChannel, *useSisoFlag, targetDepsMap)
if err != nil {
panic(err)
}
}()
}
// Process GN targets.
gnTargets := NewGnTargets(ip.gnTargetsPath)
gnTargets.DataWg.Add(numRoutines)
gnTargets.KzipDataWg.Add(numRoutines)
gnTargets.UnitWg.Add(numRoutines)
for range numRoutines {
go func() {
err := gnTargets.ProcessGnTargets(ip.ctx, ip.rootPath, ip.outDir, ip.corpus, ip.buildConfig, ip.hashMaps,
dataFileChannel, unitProtoChannel)
if err != nil {
panic(err)
}
}()
}
// Convert data files to kzipEntries.
kzipEntryWg.Add(numRoutines)
for range numRoutines {
go func() {
if err := ip.dataFileToKzipEntry(ctx, dataFileChannel, kzipEntryChannel); err != nil {
panic(err)
}
kzipEntryWg.Done()
// Signal targets to start unit proto processing.
gnTargets.KzipDataWg.Done()
clangTargets.KzipDataWg.Done()
}()
}
// Convert unit protos to kzipEntries.
kzipEntryWg.Add(numRoutines)
for range numRoutines {
go func() {
ip.unitFileToKzipEntry(ctx, unitProtoChannel, kzipEntryChannel)
kzipEntryWg.Done()
}()
}
// Close dataFileChannel and unitProtoChannel after all targets have been processed and sent.
go func() {
gnTargets.DataWg.Wait()
clangTargets.DataWg.Wait()
close(dataFileChannel)
}()
go func() {
gnTargets.UnitWg.Wait()
clangTargets.UnitWg.Wait()
close(unitProtoChannel)
}()
// Close kzipEntryChannel after all kzip entries have been sent.
go func() {
kzipEntryWg.Wait()
close(kzipEntryChannel)
}()
// Write all data file and unit proto entries to kzip.
err = ip.writeToKzip(kzipEntryChannel)
// Print stats regardless of success.
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, '-', tabwriter.AlignRight|tabwriter.Debug)
fmt.Fprintln(tw, "Language\tCompilationUnits\tRequiredInputs\tSourceFiles")
numUnits := 0
numRequiredInputs := 0
numSourceFiles := 0
for l, st := range ip.stats {
nri := len(st.requiredInputs)
nsf := len(st.sourceFiles)
fmt.Fprintf(tw, "%s\t%d\t%d\t%d\n", l, st.numCompilationUnits, nri, nsf)
numUnits += st.numCompilationUnits
numRequiredInputs += nri
numSourceFiles += nsf
}
fmt.Fprintf(tw, "Total\t%d\t%d\t%d\n", numUnits, numRequiredInputs, numSourceFiles)
if tw.Flush() != nil {
logging.Warningf(ctx, "failed to flush stats to stdout")
}
if err != nil {
panic(err)
}
// Clean up.
if !*filepathsFlag {
// Remove all *.filepaths files.
removeFilepathsFiles(ip.ctx, filepath.Join(rootPath, "src"))
}
logging.Infof(ctx, "%s: Done.", time.Now().Format("15:04:05"))
os.Exit(0)
}