blob: c3081940dd0a2aeac4b4e4e9bb04ffcdb23710fb [file] [log] [blame]
// Copyright 2023 The Tint Authors.
//
// 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 build implements a extensible build file list and module dependency
// generator for Tint.
// See: docs/tint/gen.md
package build
import (
"bytes"
"context"
"encoding/json"
"flag"
"fmt"
"log"
"os"
"os/exec"
"path"
"path/filepath"
"regexp"
"strings"
"dawn.googlesource.com/dawn/tools/src/cmd/gen/common"
"dawn.googlesource.com/dawn/tools/src/container"
"dawn.googlesource.com/dawn/tools/src/fileutils"
"dawn.googlesource.com/dawn/tools/src/glob"
"dawn.googlesource.com/dawn/tools/src/match"
"dawn.googlesource.com/dawn/tools/src/template"
"dawn.googlesource.com/dawn/tools/src/transform"
"github.com/mzohreva/gographviz/graphviz"
"github.com/tidwall/jsonc"
)
const srcTint = "src/tint"
func init() {
common.Register(&Cmd{})
}
type Cmd struct {
flags struct {
dot bool
}
}
func (Cmd) Name() string {
return "build"
}
func (Cmd) Desc() string {
return `build generates BUILD.* files in each of Tint's source directories`
}
func (c *Cmd) RegisterFlags(ctx context.Context, cfg *common.Config) ([]string, error) {
flag.BoolVar(&c.flags.dot, "dot", false, "emit GraphViz DOT files for each target kind")
return nil, nil
}
func (c Cmd) Run(ctx context.Context, cfg *common.Config) error {
p := NewProject(CanonicalizePath(path.Join(fileutils.DawnRoot(), srcTint)), cfg)
for _, stage := range []struct {
desc string
fn func(p *Project) error
}{
{"loading 'externals.json'", loadExternals},
{"populating source files", populateSourceFiles},
{"scanning source files", scanSourceFiles},
{"loading directory configs", applyDirectoryConfigs},
{"building dependencies", buildDependencies},
{"checking for cycles", checkForCycles},
{"emitting build files", emitBuildFiles},
} {
if cfg.Flags.Verbose {
log.Printf("%v...\n", stage.desc)
}
if err := stage.fn(p); err != nil {
return err
}
}
if c.flags.dot {
for _, kind := range AllTargetKinds {
if err := emitDotFile(p, kind); err != nil {
return err
}
}
}
if cfg.Flags.Verbose {
log.Println("done")
}
return nil
}
// loadExternals loads the 'externals.json' file in this directory.
func loadExternals(p *Project) error {
content, err := os.ReadFile(path.Join(fileutils.ThisDir(), "externals.json"))
if err != nil {
return err
}
externals := container.NewMap[string, struct {
IncludePattern string `json:"include_pattern"`
Condition string
}]()
if err := json.Unmarshal(jsonc.ToJSON(content), &externals); err != nil {
return fmt.Errorf("failed to parse 'externals.json': %w", err)
}
for _, name := range externals.Keys() {
external := externals[name]
match, err := match.New(external.IncludePattern)
if err != nil {
return fmt.Errorf("'externals.json' matcher error: %w", err)
}
name := ExternalDependencyName(name)
p.externals.Add(name, ExternalDependency{
Name: name,
Condition: external.Condition,
includePatternMatch: match,
})
}
return nil
}
// Globs all the source files, and creates populates the Project with Directory, Target and File.
// File name patterns are used to bin each file into a target for the directory.
func populateSourceFiles(p *Project) error {
paths, err := glob.Scan(p.Root, glob.MustParseConfig(`{
"paths": [
{
"include": [
"*/**.cc",
"*/**.h",
"*/**.inl"
]
},
{
"exclude": [
"fuzzers/**"
]
}]
}`))
if err != nil {
return err
}
for _, filepath := range paths {
filepath = CanonicalizePath(filepath)
dir, name := path.Split(filepath)
if kind := targetKindFromFilename(name); kind != targetInvalid {
directory := p.AddDirectory(dir)
p.AddTarget(directory, kind).AddSourceFile(p.AddFile(filepath))
}
}
return nil
}
// scanSourceFiles scans all the source files for:
// * #includes to build a dependencies between targets
// * 'GEN_BUILD:' directives
func scanSourceFiles(p *Project) error {
// ParsedFile describes all the includes and conditions found in a source file
type ParsedFile struct {
conditions []string
includes []Include
}
// parseFile parses the source file at 'path' represented by 'file'
// As this is run concurrently, it must not modify any shared state (including file)
parseFile := func(path string, file *File) (string, *ParsedFile, error) {
body, err := os.ReadFile(file.AbsPath())
if err != nil {
return path, nil, err
}
out := &ParsedFile{}
for i, line := range strings.Split(string(body), "\n") {
if match := reCondition.FindStringSubmatch(line); len(match) > 0 {
out.conditions = append(out.conditions, match[1])
}
if !reIgnoreInclude.MatchString(line) {
if match := reInclude.FindStringSubmatch(line); len(match) > 0 {
out.includes = append(out.includes, Include{match[1], i + 1})
}
}
}
return path, out, nil
}
// Create a new map by calling parseFile() on each entry of p.Files
// This is performed over multiple concurrent goroutines.
parsedFiles, err := transform.GoMap(p.Files, parseFile)
if err != nil {
return err
}
// For each file, of each target, of each directory...
for _, dir := range p.Directories {
for _, target := range dir.Targets() {
for _, file := range target.SourceFiles() {
// Retrieve the parsed file information
parsed := parsedFiles[file.Path()]
// Apply any conditions
for _, condition := range parsed.conditions {
if file.Condition == "" {
file.Condition = condition
} else {
file.Condition += " && " + condition
}
}
file.Includes = append(file.Includes, parsed.includes...)
}
}
}
return nil
}
// applyDirectoryConfigs loads a 'BUILD.cfg' file in each source directory (if found), and
// applies the config to the Directory and/or Targets.
func applyDirectoryConfigs(p *Project) error {
// Config for a single target of a directory
type TargetConfig struct {
AdditionalDependencies []string
}
// Config for a directory
type DirectoryConfig struct {
// Condition for all targets in the directory
Condition string
// Configuration for the 'lib' target
Lib TargetConfig
// Configuration for the 'test' target
Test TargetConfig
// Configuration for the 'bench' target
Bench TargetConfig
// Configuration for the 'cmd' target
Cmd TargetConfig
}
// For each directory in the project...
for _, dir := range p.Directories.Values() {
path := path.Join(dir.AbsPath(), "BUILD.cfg")
content, err := os.ReadFile(path)
if err != nil {
continue
}
// Parse the config
cfg := DirectoryConfig{}
if err := json.Unmarshal(jsonc.ToJSON(content), &cfg); err != nil {
return fmt.Errorf("error while parsing '%v': %w", path, err)
}
// Apply any directory-level condition
for _, target := range dir.Targets() {
target.Condition = cfg.Condition
}
// For each target config...
for _, tc := range []struct {
cfg *TargetConfig
kind TargetKind
}{
{&cfg.Lib, targetLib},
{&cfg.Test, targetTest},
{&cfg.Bench, targetBench},
{&cfg.Cmd, targetCmd},
} {
// Add any additional dependencies
for _, depPattern := range tc.cfg.AdditionalDependencies {
match, err := match.New(depPattern)
if err != nil {
return fmt.Errorf("%v: failed to parse %v.AdditionalDependencies '%v': %w", path, tc.kind, depPattern, err)
}
additionalDeps := []*Target{}
for _, target := range p.Targets.Keys() {
if match(string(target)) {
additionalDeps = append(additionalDeps, p.Targets[target])
}
}
if len(additionalDeps) == 0 {
return fmt.Errorf("%v: %v.AdditionalDependencies '%v' did not match any targets", path, tc.kind, depPattern)
}
target := p.AddTarget(dir, tc.kind)
for _, dep := range additionalDeps {
if dep != target {
target.Dependencies.AddInternal(dep)
}
}
}
}
}
return nil
}
// buildDependencies walks all the #includes in all files, building the dependency information for
// all targets and files in the project. Errors if any cyclic includes are found.
func buildDependencies(p *Project) error {
type state int
const (
unvisited state = iota
visiting
checked
)
cache := container.NewMap[string, state]()
type FileInclude struct {
file string
inc Include
}
var walk func(file *File, route []FileInclude) error
walk = func(file *File, route []FileInclude) error {
// Adds the dependency to the file and target's list of internal dependencies
addInternalDependency := func(dep *Target) {
file.TransitiveDependencies.AddInternal(dep)
if file.Target != dep {
file.Target.Dependencies.AddInternal(dep)
}
}
// Adds the dependency to the file and target's list of external dependencies
addExternalDependency := func(dep ExternalDependency) {
file.TransitiveDependencies.AddExternal(dep)
file.Target.Dependencies.AddExternal(dep)
}
filePath := file.Path()
switch cache[filePath] {
case unvisited:
cache[filePath] = visiting
for _, include := range file.Includes {
if strings.HasPrefix(include.Path, srcTint) {
// #include "src/tint/..."
path := include.Path[len(srcTint)+1:] // Strip 'src/tint/'
includeFile := p.File(path)
if includeFile == nil {
return fmt.Errorf(`%v:%v includes non-existent file '%v'`, file.Path(), include.Line, path)
}
if file.Target.Kind == targetLib && includeFile.Target.Kind != targetLib {
return fmt.Errorf(`%v:%v lib target must not include %v target`, file.Path(), include.Line, includeFile.Target.Kind)
}
addInternalDependency(includeFile.Target)
// Gather the dependencies for the included file
if err := walk(includeFile, append(route, FileInclude{file: file.Path(), inc: include})); err != nil {
return err
}
for _, dependency := range includeFile.TransitiveDependencies.Internal() {
addInternalDependency(dependency)
}
for _, dependency := range includeFile.TransitiveDependencies.External() {
addExternalDependency(dependency)
}
} else {
// Check for external includes
for _, external := range p.externals.Values() {
if external.includePatternMatch(include.Path) {
addExternalDependency(external)
}
}
}
}
cache[filePath] = checked
case visiting:
err := strings.Builder{}
fmt.Fprintln(&err, "cyclic include found:")
for _, include := range route {
fmt.Fprintf(&err, " %v:%v includes '%v'\n", include.file, include.inc.Line, include.inc.Path)
}
return fmt.Errorf(err.String())
}
return nil
}
for _, file := range p.Files.Values() {
if err := walk(file, []FileInclude{}); err != nil {
return err
}
}
return nil
}
// checkForCycles ensures that the graph of target dependencies are acyclic (a DAG)
func checkForCycles(p *Project) error {
type state int
const (
unvisited state = iota
visiting
checked
)
cache := container.NewMap[TargetName, state]()
var walk func(t *Target, path []TargetName) error
walk = func(t *Target, path []TargetName) error {
switch cache[t.Name] {
case unvisited:
cache[t.Name] = visiting
for _, dep := range t.Dependencies.Internal() {
if err := walk(dep, append(path, dep.Name)); err != nil {
return err
}
}
cache[t.Name] = checked
case visiting:
err := strings.Builder{}
fmt.Fprintln(&err, "cyclic target dependency found:")
for _, t := range path {
fmt.Fprintln(&err, " ", string(t))
}
fmt.Fprintln(&err, " ", string(t.Name))
return fmt.Errorf(err.String())
}
return nil
}
for _, target := range p.Targets.Values() {
if err := walk(target, []TargetName{target.Name}); err != nil {
return err
}
}
return nil
}
// emitBuildFiles emits a 'BUILD.*' file in each source directory for each
// 'BUILD.*.tmpl' found in this directory.
func emitBuildFiles(p *Project) error {
// Glob all the template files
templatePaths, err := glob.Glob(path.Join(fileutils.ThisDir(), "*.tmpl"))
if err != nil {
return err
}
if len(templatePaths) == 0 {
return fmt.Errorf("no template files found")
}
// Load the templates
templates := container.NewMap[string, *template.Template]()
for _, path := range templatePaths {
tmpl, err := template.FromFile(path)
if err != nil {
return err
}
templates[path] = tmpl
}
// process executes all the templates for the directory dir
// This is run concurrently, so must not modify shared state
process := func(dir *Directory) (common.StaleFiles, error) {
stale := common.StaleFiles{}
// For each template...
for _, tmplPath := range templatePaths {
_, tmplName := filepath.Split(tmplPath)
outputName := strings.TrimSuffix(tmplName, ".tmpl")
outputPath := path.Join(dir.AbsPath(), outputName)
// Attempt to read the existing output file
existing, err := os.ReadFile(outputPath)
if err != nil {
existing = nil
}
// If the file is annotated with a GEN_BUILD:DO_NOT_GENERATE directive, leave it alone
if reDoNotGenerate.Match(existing) {
continue
}
// Buffer for output
w := &bytes.Buffer{}
// Write the header
relTmplPath, err := filepath.Rel(fileutils.DawnRoot(), tmplPath)
if err != nil {
return nil, err
}
w.WriteString(common.Header(string(existing), CanonicalizePath(relTmplPath), "#"))
// Write the template output
err = templates[tmplPath].Run(w, dir, map[string]any{})
if err != nil {
return nil, err
}
// Format the output if it's a GN file.
if path.Ext(outputName) == ".gn" {
unformatted := w.String()
gn := exec.Command("gn", "format", "--stdin")
gn.Stdin = bytes.NewReader([]byte(unformatted))
w.Reset()
gn.Stdout = w
gn.Stderr = w
if err := gn.Run(); err != nil {
return nil, fmt.Errorf("%v\ngn format failed: %w\n%v", unformatted, err, w.String())
}
}
if string(existing) != w.String() {
stale = append(stale, outputPath)
}
if !p.cfg.Flags.CheckStale {
if err := os.WriteFile(outputPath, w.Bytes(), 0666); err != nil {
return nil, err
}
}
}
return stale, nil
}
// Concurrently run process() on all the directories.
staleLists, err := transform.GoSlice(p.Directories.Values(), process)
if err != nil {
return err
}
if p.cfg.Flags.Verbose || p.cfg.Flags.CheckStale {
// Collect all stale files into a flat list
stale := transform.Flatten(staleLists)
if p.cfg.Flags.CheckStale && len(stale) > 0 {
return stale
}
if p.cfg.Flags.Verbose {
log.Printf("generated %v files\n", len(stale))
}
}
return nil
}
// targetKindFromFilename returns the target kind my pattern matching the filename
func targetKindFromFilename(filename string) TargetKind {
switch {
case strings.HasSuffix(filename, "_test.cc"), strings.HasSuffix(filename, "_test.h"):
return targetTest
case strings.HasSuffix(filename, "_bench.cc"), strings.HasSuffix(filename, "_bench.h"):
return targetBench
case filename == "main.cc":
return targetCmd
case strings.HasSuffix(filename, ".cc"), strings.HasSuffix(filename, ".h"):
return targetLib
}
return targetInvalid
}
// emitDotFile writes a GraphViz DOT file visualizing the target dependency graph
func emitDotFile(p *Project, kind TargetKind) error {
g := graphviz.Graph{}
nodes := container.NewMap[TargetName, int]()
targets := []*Target{}
for _, target := range p.Targets.Values() {
if target.Kind == kind {
targets = append(targets, target)
}
}
for _, target := range targets {
nodes.Add(target.Name, g.AddNode(string(target.Name)))
}
for _, target := range targets {
for _, dep := range target.Dependencies.Internal() {
g.AddEdge(nodes[target.Name], nodes[dep.Name], "")
}
}
g.MakeDirected()
g.DefaultNodeAttribute(graphviz.Shape, graphviz.ShapeBox)
g.DefaultNodeAttribute(graphviz.FontName, "Courier")
g.DefaultNodeAttribute(graphviz.FontSize, "14")
g.DefaultNodeAttribute(graphviz.Style, graphviz.StyleFilled+","+graphviz.StyleRounded)
g.DefaultNodeAttribute(graphviz.FillColor, "yellow")
g.DefaultEdgeAttribute(graphviz.FontName, "Courier")
g.DefaultEdgeAttribute(graphviz.FontSize, "12")
file, err := os.Create(path.Join(p.Root, fmt.Sprintf("%v.dot", kind)))
if err != nil {
return err
}
defer file.Close()
g.GenerateDOT(file)
return nil
}
var (
// Regular expressions used by this file
reInclude = regexp.MustCompile(`\s*#\s*include\s*\"([^"]+)\"`)
reIgnoreInclude = regexp.MustCompile(`//\s*GEN_BUILD:IGNORE`)
reCondition = regexp.MustCompile(`//\s*GEN_BUILD:CONDITION\((.*)\)\s*$`)
reDoNotGenerate = regexp.MustCompile(`#\s*GEN_BUILD:DO_NOT_GENERATE`)
)