[tint] Add build support for protobufs

Change-Id: I764ecca72d93f35e4cb7478df5887a0632738226
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/161001
Reviewed-by: Antonio Maiorano <amaiorano@google.com>
Commit-Queue: Ben Clayton <bclayton@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
diff --git a/src/tint/CMakeLists.txt b/src/tint/CMakeLists.txt
index 3a47bd1..052ab20 100644
--- a/src/tint/CMakeLists.txt
+++ b/src/tint/CMakeLists.txt
@@ -177,6 +177,26 @@
   target_include_directories(${TARGET} PRIVATE "${TINT_SPIRV_TOOLS_DIR}/include")
 endfunction()
 
+function(tint_lib_compile_options TARGET)
+  if (TINT_ENABLE_INSTALL)
+    install(TARGETS ${TARGET}
+      LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
+      ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
+    )
+  endif()
+  tint_default_compile_options(${TARGET})
+endfunction()
+
+function(tint_proto_compile_options TARGET)
+  if (TINT_ENABLE_INSTALL)
+    install(TARGETS ${TARGET}
+      LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
+      ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
+    )
+  endif()
+  tint_core_compile_options(${TARGET})
+endfunction()
+
 function(tint_test_compile_options TARGET)
   tint_default_compile_options(${TARGET})
   set_target_properties(${TARGET} PROPERTIES FOLDER "Tests")
@@ -324,7 +344,7 @@
     if(TINT_BUILD_CMD_TOOLS)
       set(IS_ENABLED TRUE PARENT_SCOPE)
     endif()
-  elseif(${KIND} STREQUAL lib)
+  elseif((${KIND} STREQUAL lib) OR (${KIND} STREQUAL proto))
     set(IS_ENABLED TRUE PARENT_SCOPE)
   elseif((${KIND} STREQUAL test) OR (${KIND} STREQUAL test_cmd))
     if(TINT_BUILD_TESTS)
@@ -363,13 +383,12 @@
 
   if(${KIND} STREQUAL lib)
     add_library(${TARGET} STATIC EXCLUDE_FROM_ALL)
-    if (TINT_ENABLE_INSTALL)
-      install(TARGETS ${TARGET}
-        LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
-        ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
-      )
-    endif()
-    tint_default_compile_options(${TARGET})
+    tint_lib_compile_options(${TARGET})
+  elseif(${KIND} STREQUAL proto)
+    add_library(${TARGET} STATIC EXCLUDE_FROM_ALL)
+    list(APPEND TINT_PROTO_TARGETS ${TARGET})
+    set(TINT_PROTO_TARGETS ${TINT_PROTO_TARGETS} PARENT_SCOPE)
+    tint_proto_compile_options(${TARGET})
   elseif(${KIND} STREQUAL cmd)
     add_executable(${TARGET})
     tint_default_compile_options(${TARGET})
@@ -611,6 +630,19 @@
 
 
 ################################################################################
+# Generate protobuf sources
+################################################################################
+foreach(PROTO_TARGET ${TINT_PROTO_TARGETS})
+  generate_protos(
+    TARGET ${PROTO_TARGET}
+    PROTOC_OUT_DIR "${DAWN_BUILD_GEN_DIR}/src/tint/")
+  target_include_directories(${PROTO_TARGET} PRIVATE "${DAWN_BUILD_GEN_DIR}/src/tint/")
+  target_include_directories(${PROTO_TARGET} PUBLIC "${DAWN_BUILD_GEN_DIR}")
+  target_link_libraries(${PROTO_TARGET} libprotobuf)
+endforeach()
+
+
+################################################################################
 # Bespoke target settings
 ################################################################################
 if (MSVC)
diff --git a/src/tint/tint.gni b/src/tint/tint.gni
index 137def3..d40c570 100644
--- a/src/tint/tint.gni
+++ b/src/tint/tint.gni
@@ -27,8 +27,13 @@
 
 import("//build_overrides/build.gni")
 
+import("//third_party/protobuf/proto_library.gni")
+
 import("../../scripts/tint_overrides_with_defaults.gni")
 
+###############################################################################
+# Tint library target
+###############################################################################
 template("libtint_source_set") {
   source_set(target_name) {
     forward_variables_from(invoker, "*", [ "configs" ])
@@ -57,6 +62,18 @@
 }
 
 ###############################################################################
+# Tint protobuf library target
+###############################################################################
+template("tint_proto_library") {
+  proto_library(target_name) {
+    forward_variables_from(invoker, "*", [ "configs" ])
+    generate_cc = true
+    generate_python = false
+    use_protobuf_full = true
+  }
+}
+
+###############################################################################
 # Executables - only built when tint_build_cmds is enabled
 ###############################################################################
 template("tint_executable") {
diff --git a/tools/src/cmd/gen/build/BUILD.cmake.tmpl b/tools/src/cmd/gen/build/BUILD.cmake.tmpl
index 66250f4..9e8412d 100644
--- a/tools/src/cmd/gen/build/BUILD.cmake.tmpl
+++ b/tools/src/cmd/gen/build/BUILD.cmake.tmpl
@@ -1,5 +1,6 @@
 {{- Eval "Includes"         $}}
 {{- Eval "TargetIfNotEmpty" ($.Project.Target $ "lib")}}
+{{- Eval "TargetIfNotEmpty" ($.Project.Target $ "proto")}}
 {{- Eval "TargetIfNotEmpty" ($.Project.Target $ "cmd")}}
 {{- Eval "TargetIfNotEmpty" ($.Project.Target $ "test_cmd")}}
 {{- Eval "TargetIfNotEmpty" ($.Project.Target $ "bench_cmd")}}
diff --git a/tools/src/cmd/gen/build/BUILD.gn.tmpl b/tools/src/cmd/gen/build/BUILD.gn.tmpl
index 128309b..6e156f2 100644
--- a/tools/src/cmd/gen/build/BUILD.gn.tmpl
+++ b/tools/src/cmd/gen/build/BUILD.gn.tmpl
@@ -13,7 +13,9 @@
 }
 {{- end}}
 
+
 {{- Eval "TargetIfNotEmpty" ($.Project.Target $ "lib")}}
+{{- Eval "TargetIfNotEmpty" ($.Project.Target $ "proto")}}
 {{- Eval "TargetIfNotEmpty" ($.Project.Target $ "cmd")}}
 {{- Eval "TargetIfNotEmpty" ($.Project.Target $ "test")}}
 {{- Eval "TargetIfNotEmpty" ($.Project.Target $ "test_cmd")}}
@@ -55,6 +57,8 @@
 
 {{  if      $.Kind.IsLib -}}
 libtint_source_set("{{$.Directory.Name}}") {
+{{- else if $.Kind.IsProto -}}
+tint_proto_library("proto") {
 {{- else if $.Kind.IsCmd -}}
 tint_executable("{{$.Directory.Name}}") {
 {{- else if $.Kind.IsTest -}}
diff --git a/tools/src/cmd/gen/build/build.go b/tools/src/cmd/gen/build/build.go
index 3f09d29..db2bbc0 100644
--- a/tools/src/cmd/gen/build/build.go
+++ b/tools/src/cmd/gen/build/build.go
@@ -183,7 +183,8 @@
 					"*/**.cc",
 					"*/**.h",
 					"*/**.inl",
-					"*/**.mm"
+					"*/**.mm",
+					"*/**.proto"
 				]
 			},
 			{
@@ -201,7 +202,14 @@
 		dir, name := path.Split(filepath)
 		if kind := targetKindFromFilename(name); kind != targetInvalid {
 			directory := p.AddDirectory(dir)
-			p.AddTarget(directory, kind).AddSourceFile(p.AddFile(filepath))
+			target := p.AddTarget(directory, kind)
+			target.AddSourceFile(p.AddFile(filepath))
+
+			if kind == targetProto {
+				noExt, _ := fileutils.SplitExt(filepath)
+				target.AddGeneratedFile(p.AddGeneratedFile(noExt + ".pb.h"))
+				target.AddGeneratedFile(p.AddGeneratedFile(noExt + ".pb.cc"))
+			}
 		}
 	}
 
@@ -222,12 +230,17 @@
 	// 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) {
-		conditions := []Condition{}
+		if file.IsGenerated {
+			return "", nil, nil
+		}
 
 		body, err := os.ReadFile(file.AbsPath())
 		if err != nil {
 			return path, nil, err
 		}
+
+		conditions := []Condition{}
+
 		out := &ParsedFile{}
 		for i, line := range strings.Split(string(body), "\n") {
 			wrapErr := func(err error) error {
@@ -366,6 +379,7 @@
 			kind TargetKind
 		}{
 			{cfg.Lib, targetLib},
+			{cfg.Proto, targetProto},
 			{cfg.Test, targetTest},
 			{cfg.TestCmd, targetTestCmd},
 			{cfg.Bench, targetBench},
diff --git a/tools/src/cmd/gen/build/directory_config.go b/tools/src/cmd/gen/build/directory_config.go
index 0f5ac94..3cd4225 100644
--- a/tools/src/cmd/gen/build/directory_config.go
+++ b/tools/src/cmd/gen/build/directory_config.go
@@ -27,7 +27,7 @@
 
 package build
 
-// Config for a single target of a directory
+// TargetConfig holds configuration options for a single target of a directory
 type TargetConfig struct {
 	// Override for the output name of this target
 	OutputName string
@@ -42,12 +42,14 @@
 	}
 }
 
-// Config for a directory
+// DirectoryConfig holds configuration options 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 'proto' target
+	Proto *TargetConfig
 	// Configuration for the 'test' target
 	Test *TargetConfig
 	// Configuration for the 'test_cmd' target
diff --git a/tools/src/cmd/gen/build/file.go b/tools/src/cmd/gen/build/file.go
index b0ad5de..9d74bb4 100644
--- a/tools/src/cmd/gen/build/file.go
+++ b/tools/src/cmd/gen/build/file.go
@@ -38,6 +38,8 @@
 
 // File holds information about a source file
 type File struct {
+	// The file is generated from a target
+	IsGenerated bool
 	// The directory that holds this source file
 	Directory *Directory
 	// The target that this source file belongs to
diff --git a/tools/src/cmd/gen/build/project.go b/tools/src/cmd/gen/build/project.go
index 25857f6..96d2bc7 100644
--- a/tools/src/cmd/gen/build/project.go
+++ b/tools/src/cmd/gen/build/project.go
@@ -71,15 +71,36 @@
 }
 
 // AddFile gets or creates a File with the given project-relative path
-func (p *Project) AddFile(file string) *File {
-	return p.Files.GetOrCreate(file, func() *File {
-		dir, name := path.Split(file)
+func (p *Project) AddFile(filepath string) *File {
+	file := p.Files.GetOrCreate(filepath, func() *File {
+		dir, name := path.Split(filepath)
 		return &File{
 			Directory:              p.Directory(dir),
 			Name:                   name,
 			TransitiveDependencies: NewDependencies(p),
 		}
 	})
+	if file.IsGenerated {
+		panic("AddFile() called with path that already exists for generated file")
+	}
+	return file
+}
+
+// AddGeneratedFile gets or creates a generated File with the given project-relative path
+func (p *Project) AddGeneratedFile(filepath string) *File {
+	file := p.Files.GetOrCreate(filepath, func() *File {
+		dir, name := path.Split(filepath)
+		return &File{
+			IsGenerated:            true,
+			Directory:              p.Directory(dir),
+			Name:                   name,
+			TransitiveDependencies: NewDependencies(p),
+		}
+	})
+	if !file.IsGenerated {
+		panic("AddGeneratedFile() called with path that already exists for non-generated file")
+	}
+	return file
 }
 
 // File returns the File with the given project-relative path
@@ -92,11 +113,12 @@
 	name := p.TargetName(dir, kind)
 	return p.Targets.GetOrCreate(name, func() *Target {
 		t := &Target{
-			Name:          name,
-			Directory:     dir,
-			Kind:          kind,
-			SourceFileSet: container.NewSet[string](),
-			Dependencies:  NewDependencies(p),
+			Name:             name,
+			Directory:        dir,
+			Kind:             kind,
+			SourceFileSet:    container.NewSet[string](),
+			GeneratedFileSet: container.NewSet[string](),
+			Dependencies:     NewDependencies(p),
 		}
 		dir.TargetNames.Add(name)
 		p.Targets.Add(name, t)
diff --git a/tools/src/cmd/gen/build/target.go b/tools/src/cmd/gen/build/target.go
index 116d8fe..a207b19 100644
--- a/tools/src/cmd/gen/build/target.go
+++ b/tools/src/cmd/gen/build/target.go
@@ -34,7 +34,7 @@
 	"dawn.googlesource.com/dawn/tools/src/transform"
 )
 
-// Directory holds information about a build target
+// Target holds information about a build target
 type Target struct {
 	// The target's name
 	Name TargetName
@@ -44,6 +44,8 @@
 	Directory *Directory
 	// All project-relative paths of source files that are part of this target
 	SourceFileSet container.Set[string]
+	// All project-relative paths of files generated by this target
+	GeneratedFileSet container.Set[string]
 	// Dependencies of this target
 	Dependencies *Dependencies
 	// An optional custom output name for the target
@@ -52,12 +54,24 @@
 	Condition Condition
 }
 
-// AddSourceFile adds the File to the target's source set
+// AddSourceFile adds the File to the target's source file set
 func (t *Target) AddSourceFile(f *File) {
+	if f.IsGenerated {
+		panic("attempting to add a generated file to SourceFileSet")
+	}
 	t.SourceFileSet.Add(f.Path())
 	f.Target = t
 }
 
+// AddGeneratedFile adds the File to the target's generated file set
+func (t *Target) AddGeneratedFile(f *File) {
+	if !f.IsGenerated {
+		panic("attempting to add a non-generated file to GeneratedFileSet")
+	}
+	t.GeneratedFileSet.Add(f.Path())
+	f.Target = t
+}
+
 // SourceFiles returns the sorted list of the target's source files
 func (t *Target) SourceFiles() []*File {
 	out := make([]*File, len(t.SourceFileSet))
@@ -67,12 +81,12 @@
 	return out
 }
 
-// SourceFiles returns the sorted list of the target's source files that have no build condition
+// UnconditionalSourceFiles returns the sorted list of the target's source files that have no build condition
 func (t *Target) UnconditionalSourceFiles() []*File {
 	return transform.Filter(t.SourceFiles(), func(t *File) bool { return t.Condition == nil })
 }
 
-// A collection of source files and dependencies sharing the same condition
+// TargetConditional is a collection of source files and dependencies sharing the same condition
 type TargetConditional struct {
 	Condition            Condition
 	SourceFiles          []*File
@@ -80,7 +94,7 @@
 	ExternalDependencies []ExternalDependency
 }
 
-// A collection of source files and dependencies sharing the same condition
+// TargetConditionals is a collection of source files and dependencies sharing the same condition
 type TargetConditionals []*TargetConditional
 
 // HasSourceFiles returns true if any of the conditionals in l have source files
diff --git a/tools/src/cmd/gen/build/target_kind.go b/tools/src/cmd/gen/build/target_kind.go
index 4556660..9a5977c 100644
--- a/tools/src/cmd/gen/build/target_kind.go
+++ b/tools/src/cmd/gen/build/target_kind.go
@@ -29,6 +29,8 @@
 
 import (
 	"strings"
+
+	"dawn.googlesource.com/dawn/tools/src/fileutils"
 )
 
 // TargetKind is an enumerator of target kinds
@@ -37,6 +39,8 @@
 const (
 	// A library target, used for production code.
 	targetLib TargetKind = "lib"
+	// A library target generated from a proto file, used for production code.
+	targetProto TargetKind = "proto"
 	// A library target, used for test binaries.
 	targetTest TargetKind = "test"
 	// A library target, used for benchmark binaries.
@@ -58,13 +62,16 @@
 // IsLib returns true if the TargetKind is 'lib'
 func (k TargetKind) IsLib() bool { return k == targetLib }
 
+// IsProto returns true if the TargetKind is 'proto'
+func (k TargetKind) IsProto() bool { return k == targetProto }
+
 // IsTest returns true if the TargetKind is 'test'
 func (k TargetKind) IsTest() bool { return k == targetTest }
 
 // IsBench returns true if the TargetKind is 'bench'
 func (k TargetKind) IsBench() bool { return k == targetBench }
 
-// IsBench returns true if the TargetKind is 'fuzz'
+// IsFuzz returns true if the TargetKind is 'fuzz'
 func (k TargetKind) IsFuzz() bool { return k == targetFuzz }
 
 // IsCmd returns true if the TargetKind is 'cmd'
@@ -85,9 +92,10 @@
 // IsBenchOrBenchCmd returns true if the TargetKind is 'bench' or 'bench_cmd'
 func (k TargetKind) IsBenchOrBenchCmd() bool { return k.IsBench() || k.IsBenchCmd() }
 
-// All the target kinds
+// AllTargetKinds is a list of all the target kinds
 var AllTargetKinds = []TargetKind{
 	targetLib,
+	targetProto,
 	targetTest,
 	targetBench,
 	targetFuzz,
@@ -99,10 +107,10 @@
 
 // targetKindFromFilename returns the target kind my pattern matching the filename
 func targetKindFromFilename(filename string) TargetKind {
-	noExt, ext := filename, ""
-	if i := strings.LastIndex(filename, "."); i >= 0 {
-		noExt = filename[:i]
-		ext = filename[i+1:]
+	noExt, ext := fileutils.SplitExt(filename)
+
+	if ext == "proto" {
+		return targetProto
 	}
 
 	if ext != "cc" && ext != "mm" && ext != "h" {
@@ -134,13 +142,13 @@
 func isValidDependency(from, to TargetKind) bool {
 	switch from {
 	case targetLib, targetCmd:
-		return to == targetLib
+		return to == targetLib || to == targetProto
 	case targetTest, targetTestCmd:
-		return to == targetLib || to == targetTest
+		return to == targetLib || to == targetProto || to == targetTest
 	case targetBench, targetBenchCmd:
-		return to == targetLib || to == targetBench
+		return to == targetLib || to == targetProto || to == targetBench
 	case targetFuzz, targetFuzzCmd:
-		return to == targetLib || to == targetFuzz
+		return to == targetLib || to == targetProto || to == targetFuzz
 	default:
 		return false
 	}
diff --git a/tools/src/fileutils/ext.go b/tools/src/fileutils/ext.go
new file mode 100644
index 0000000..e655039
--- /dev/null
+++ b/tools/src/fileutils/ext.go
@@ -0,0 +1,41 @@
+// Copyright 2023 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+package fileutils
+
+import "strings"
+
+// SplitExt splits the file name at the last '.', returning the no-extension and
+// extension parts.
+func SplitExt(filename string) (noExt, ext string) {
+	noExt, ext = filename, ""
+	if i := strings.LastIndex(filename, "."); i >= 0 {
+		noExt = filename[:i]
+		ext = filename[i+1:]
+	}
+	return
+}