Merge "Add find_input_delta" into main
diff --git a/cmd/find_input_delta/find_input_delta/Android.bp b/cmd/find_input_delta/find_input_delta/Android.bp
new file mode 100644
index 0000000..6b2dbc7
--- /dev/null
+++ b/cmd/find_input_delta/find_input_delta/Android.bp
@@ -0,0 +1,18 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+bootstrap_go_package {
+    name: "soong-cmd-find_input_delta-find_input_delta",
+    pkgPath: "android/soong/cmd/find_input_delta/find_input_delta",
+    deps: [
+        "golang-protobuf-encoding-prototext",
+        "golang-protobuf-reflect-protoreflect",
+        "golang-protobuf-runtime-protoimpl",
+        "soong-cmd-find_input_delta-proto",
+        "soong-cmd-find_input_delta-lib",
+    ],
+    srcs: [
+        "main.go",
+    ],
+}
diff --git a/cmd/find_input_delta/find_input_delta/main.go b/cmd/find_input_delta/find_input_delta/main.go
new file mode 100644
index 0000000..6b657ea
--- /dev/null
+++ b/cmd/find_input_delta/find_input_delta/main.go
@@ -0,0 +1,88 @@
+// Copyright 2024 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 main
+
+import (
+	"flag"
+	"os"
+	"strings"
+
+	fid_lib "android/soong/cmd/find_input_delta/find_input_delta_lib"
+)
+
+func main() {
+	var top string
+	var prior_state_file string
+	var new_state_file string
+	var target string
+	var inputs_file string
+	var template string
+	var inputs []string
+	var inspect bool
+	var err error
+
+	flag.StringVar(&top, "top", ".", "path to top of workspace")
+	flag.StringVar(&prior_state_file, "prior_state", "", "prior internal state file")
+	flag.StringVar(&new_state_file, "new_state", "", "new internal state file")
+	flag.StringVar(&target, "target", "", "name of ninja output file for build action")
+	flag.StringVar(&inputs_file, "inputs_file", "", "file containing list of input files")
+	flag.StringVar(&template, "template", fid_lib.DefaultTemplate, "output template for FileList")
+	flag.BoolVar(&inspect, "inspect", false, "whether to inspect file contents")
+
+	flag.Parse()
+
+	if target == "" {
+		panic("must specify --target")
+	}
+	if prior_state_file == "" {
+		prior_state_file = target + ".pc_state"
+	}
+	if new_state_file == "" {
+		new_state_file = prior_state_file + ".new"
+	}
+
+	if err = os.Chdir(top); err != nil {
+		panic(err)
+	}
+
+	inputs = flag.Args()
+	if inputs_file != "" {
+		data, err := os.ReadFile(inputs_file)
+		if err != nil {
+			panic(err)
+		}
+		inputs = append(inputs, strings.Split(string(data), "\n")...)
+	}
+
+	// Read the prior state
+	prior_state, err := fid_lib.LoadState(prior_state_file, fid_lib.OsFs)
+	if err != nil {
+		panic(err)
+	}
+	// Create the new state
+	new_state, err := fid_lib.CreateState(inputs, inspect, fid_lib.OsFs)
+	if err != nil {
+		panic(err)
+	}
+	if err = fid_lib.WriteState(new_state, new_state_file); err != nil {
+		panic(err)
+	}
+
+	file_list := *fid_lib.CompareInternalState(prior_state, new_state, target)
+
+	if err = file_list.Format(os.Stdout, template); err != nil {
+		panic(err)
+	}
+}
diff --git a/cmd/find_input_delta/find_input_delta_lib/Android.bp b/cmd/find_input_delta/find_input_delta_lib/Android.bp
new file mode 100644
index 0000000..795b140
--- /dev/null
+++ b/cmd/find_input_delta/find_input_delta_lib/Android.bp
@@ -0,0 +1,34 @@
+// Copyright 2024 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+bootstrap_go_package {
+    name: "soong-cmd-find_input_delta-lib",
+    pkgPath: "android/soong/cmd/find_input_delta/find_input_delta_lib",
+    deps: [
+        "golang-protobuf-encoding-prototext",
+        "golang-protobuf-reflect-protoreflect",
+        "golang-protobuf-runtime-protoimpl",
+        "soong-cmd-find_input_delta-proto",
+        "blueprint-pathtools",
+    ],
+    srcs: [
+        "fs.go",
+        "file_list.go",
+        "internal_state.go",
+    ],
+}
diff --git a/cmd/find_input_delta/find_input_delta_lib/file_list.go b/cmd/find_input_delta/find_input_delta_lib/file_list.go
new file mode 100644
index 0000000..23337ad
--- /dev/null
+++ b/cmd/find_input_delta/find_input_delta_lib/file_list.go
@@ -0,0 +1,78 @@
+// Copyright 2024 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 find_input_delta_lib
+
+import (
+	"io"
+	"text/template"
+
+	fid_exp "android/soong/cmd/find_input_delta/find_input_delta_proto"
+	"google.golang.org/protobuf/proto"
+)
+
+var DefaultTemplate = `
+	{{- define "contents"}}
+		{{- range .Deletions}}-{{.}} {{end}}
+		{{- range .Additions}}+{{.}} {{end}}
+		{{- range .Changes}}+{{- .Name}} {{end}}
+		{{- range .Changes}}
+		  {{- if or .Additions .Deletions .Changes}}--file {{.Name}} {{template "contents" .}}--endfile {{end}}
+		{{- end}}
+	{{- end}}
+	{{- template "contents" .}}`
+
+type FileList struct {
+	// The name of the parent for the list of file differences.
+	// For the outermost FileList, this is the name of the ninja target.
+	// Under `Changes`, it is the name of the changed file.
+	Name string
+
+	// The added files
+	Additions []string
+
+	// The deleted files
+	Deletions []string
+
+	// The modified files
+	Changes []FileList
+}
+
+func (fl FileList) Marshal() (*fid_exp.FileList, error) {
+	ret := &fid_exp.FileList{
+		Name: proto.String(fl.Name),
+	}
+	if len(fl.Additions) > 0 {
+		ret.Additions = fl.Additions
+	}
+	for _, ch := range fl.Changes {
+		change, err := ch.Marshal()
+		if err != nil {
+			return nil, err
+		}
+		ret.Changes = append(ret.Changes, change)
+	}
+	if len(fl.Deletions) > 0 {
+		ret.Deletions = fl.Deletions
+	}
+	return ret, nil
+}
+
+func (fl FileList) Format(wr io.Writer, format string) error {
+	tmpl, err := template.New("filelist").Parse(format)
+	if err != nil {
+		return err
+	}
+	return tmpl.Execute(wr, fl)
+}
diff --git a/cmd/find_input_delta/find_input_delta_lib/file_list_test.go b/cmd/find_input_delta/find_input_delta_lib/file_list_test.go
new file mode 100644
index 0000000..2459f1e
--- /dev/null
+++ b/cmd/find_input_delta/find_input_delta_lib/file_list_test.go
@@ -0,0 +1,131 @@
+// Copyright 2024 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 find_input_delta_lib
+
+import (
+	"bytes"
+	"slices"
+	"testing"
+
+	// For Assert*.
+	"android/soong/android"
+)
+
+func (fl *FileList) Equal(other *FileList) bool {
+	if fl.Name != other.Name {
+		return false
+	}
+	if !slices.Equal(fl.Additions, other.Additions) {
+		return false
+	}
+	if !slices.Equal(fl.Deletions, other.Deletions) {
+		return false
+	}
+	if len(fl.Changes) != len(other.Changes) {
+		return false
+	}
+	for idx, ch := range fl.Changes {
+		if !ch.Equal(&other.Changes[idx]) {
+			return false
+		}
+	}
+	return true
+}
+
+func TestFormat(t *testing.T) {
+	testCases := []struct {
+		Name     string
+		Template string
+		Input    FileList
+		Expected string
+		Err      error
+	}{
+		{
+			Name:     "no contents",
+			Template: DefaultTemplate,
+			Input: FileList{
+				Name:      "target",
+				Additions: []string{"add1", "add2"},
+				Deletions: []string{"del1", "del2"},
+				Changes: []FileList{
+					FileList{Name: "mod1"},
+					FileList{Name: "mod2"},
+				},
+			},
+			Expected: "-del1 -del2 +add1 +add2 +mod1 +mod2 ",
+			Err:      nil,
+		},
+		{
+			Name:     "adds",
+			Template: DefaultTemplate,
+			Input: FileList{
+				Name:      "target",
+				Additions: []string{"add1", "add2"},
+			},
+			Expected: "+add1 +add2 ",
+			Err:      nil,
+		},
+		{
+			Name:     "deletes",
+			Template: DefaultTemplate,
+			Input: FileList{
+				Name:      "target",
+				Deletions: []string{"del1", "del2"},
+			},
+			Expected: "-del1 -del2 ",
+			Err:      nil,
+		},
+		{
+			Name:     "changes",
+			Template: DefaultTemplate,
+			Input: FileList{
+				Name: "target",
+				Changes: []FileList{
+					FileList{Name: "mod1"},
+					FileList{Name: "mod2"},
+				},
+			},
+			Expected: "+mod1 +mod2 ",
+			Err:      nil,
+		},
+		{
+			Name:     "with contents",
+			Template: DefaultTemplate,
+			Input: FileList{
+				Name:      "target",
+				Additions: []string{"add1", "add2"},
+				Deletions: []string{"del1", "del2"},
+				Changes: []FileList{
+					FileList{
+						Name: "mod1",
+					},
+					FileList{
+						Name:      "mod2",
+						Additions: []string{"a1"},
+						Deletions: []string{"d1"},
+					},
+				},
+			},
+			Expected: "-del1 -del2 +add1 +add2 +mod1 +mod2 --file mod2 -d1 +a1 --endfile ",
+			Err:      nil,
+		},
+	}
+	for _, tc := range testCases {
+		buf := bytes.NewBuffer([]byte{})
+		err := tc.Input.Format(buf, tc.Template)
+		android.AssertSame(t, tc.Name, tc.Err, err)
+		android.AssertSame(t, tc.Name, tc.Expected, buf.String())
+	}
+}
diff --git a/cmd/find_input_delta/find_input_delta_lib/fs.go b/cmd/find_input_delta/find_input_delta_lib/fs.go
new file mode 100644
index 0000000..4a83ed7
--- /dev/null
+++ b/cmd/find_input_delta/find_input_delta_lib/fs.go
@@ -0,0 +1,46 @@
+// Copyright 2024 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 find_input_delta_lib
+
+import (
+	"io"
+	"io/fs"
+	"os"
+)
+
+// OsFs provides a minimal implementation so that we can use testing/fstest for
+// unit tests.
+var OsFs fileSystem = osFS{}
+
+type fileSystem interface {
+	Open(path string) (fs.File, error)
+	Stat(path string) (os.FileInfo, error)
+	ReadFile(path string) ([]byte, error)
+}
+
+type file interface {
+	io.Closer
+	io.Reader
+	io.ReaderAt
+	io.Seeker
+	Stat() (os.FileInfo, error)
+}
+
+// osFS implements fileSystem using the local disk.
+type osFS struct{}
+
+func (osFS) Open(path string) (fs.File, error)     { return os.Open(path) }
+func (osFS) Stat(path string) (os.FileInfo, error) { return os.Stat(path) }
+func (osFS) ReadFile(path string) ([]byte, error)  { return os.ReadFile(path) }
diff --git a/cmd/find_input_delta/find_input_delta_lib/internal_state.go b/cmd/find_input_delta/find_input_delta_lib/internal_state.go
new file mode 100644
index 0000000..b2ff8c7
--- /dev/null
+++ b/cmd/find_input_delta/find_input_delta_lib/internal_state.go
@@ -0,0 +1,122 @@
+// Copyright 2024 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 find_input_delta_lib
+
+import (
+	"errors"
+	"fmt"
+	"io/fs"
+	"slices"
+
+	fid_proto "android/soong/cmd/find_input_delta/find_input_delta_proto_internal"
+	"github.com/google/blueprint/pathtools"
+	"google.golang.org/protobuf/proto"
+)
+
+// Load the internal state from a file.
+// If the file does not exist, an empty state is returned.
+func LoadState(filename string, fsys fs.ReadFileFS) (*fid_proto.PartialCompileInputs, error) {
+	var message = &fid_proto.PartialCompileInputs{}
+	data, err := fsys.ReadFile(filename)
+	if err != nil && !errors.Is(err, fs.ErrNotExist) {
+		return message, err
+	}
+	proto.Unmarshal(data, message)
+	return message, nil
+}
+
+type StatReadFileFS interface {
+	fs.StatFS
+	fs.ReadFileFS
+}
+
+// Create the internal state by examining the inputs.
+func CreateState(inputs []string, inspect_contents bool, fsys StatReadFileFS) (*fid_proto.PartialCompileInputs, error) {
+	ret := &fid_proto.PartialCompileInputs{}
+	slices.Sort(inputs)
+	for _, input := range inputs {
+		stat, err := fs.Stat(fsys, input)
+		if err != nil {
+			return ret, err
+		}
+		pci := &fid_proto.PartialCompileInput{
+			Name:      proto.String(input),
+			MtimeNsec: proto.Int64(stat.ModTime().UnixNano()),
+			// If we ever have an easy hash, assign it here.
+		}
+		if inspect_contents {
+			contents, err := InspectFileContents(input)
+			if err != nil {
+				return ret, err
+			}
+			if contents != nil {
+				pci.Contents = contents
+			}
+		}
+		ret.InputFiles = append(ret.InputFiles, pci)
+	}
+	return ret, nil
+}
+
+// Inspect the file and extract the state of the elements in the archive.
+// If this is not an archive of some sort, nil is returned.
+func InspectFileContents(name string) ([]*fid_proto.PartialCompileInput, error) {
+	// TODO: Actually inspect the contents.
+	fmt.Printf("inspecting contents for %s\n", name)
+	return nil, nil
+}
+
+func WriteState(s *fid_proto.PartialCompileInputs, path string) error {
+	data, err := proto.Marshal(s)
+	if err != nil {
+		return err
+	}
+	return pathtools.WriteFileIfChanged(path, data, 0644)
+}
+
+func CompareInternalState(prior, other *fid_proto.PartialCompileInputs, target string) *FileList {
+	return CompareInputFiles(prior.GetInputFiles(), other.GetInputFiles(), target)
+}
+
+func CompareInputFiles(prior, other []*fid_proto.PartialCompileInput, name string) *FileList {
+	fl := &FileList{
+		Name: name,
+	}
+	PriorMap := make(map[string]*fid_proto.PartialCompileInput, len(prior))
+	// We know that the lists are properly sorted, so we can simply compare them.
+	for _, v := range prior {
+		PriorMap[v.GetName()] = v
+	}
+	otherMap := make(map[string]*fid_proto.PartialCompileInput, len(other))
+	for _, v := range other {
+		name = v.GetName()
+		otherMap[name] = v
+		if _, ok := PriorMap[name]; !ok {
+			// Added file
+			fl.Additions = append(fl.Additions, name)
+		} else if !proto.Equal(PriorMap[name], v) {
+			// Changed file
+			fl.Changes = append(fl.Changes, *CompareInputFiles(PriorMap[name].GetContents(), v.GetContents(), name))
+		}
+	}
+	for _, v := range prior {
+		name := v.GetName()
+		if _, ok := otherMap[name]; !ok {
+			// Deleted file
+			fl.Deletions = append(fl.Deletions, name)
+		}
+	}
+	return fl
+}
diff --git a/cmd/find_input_delta/find_input_delta_lib/internal_state_test.go b/cmd/find_input_delta/find_input_delta_lib/internal_state_test.go
new file mode 100644
index 0000000..20b8efa
--- /dev/null
+++ b/cmd/find_input_delta/find_input_delta_lib/internal_state_test.go
@@ -0,0 +1,232 @@
+// Copyright 2024 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 find_input_delta_lib
+
+import (
+	"errors"
+	"io/fs"
+	"testing"
+	"testing/fstest"
+	"time"
+
+	// For Assert*.
+	"android/soong/android"
+
+	fid_proto "android/soong/cmd/find_input_delta/find_input_delta_proto_internal"
+	"google.golang.org/protobuf/proto"
+)
+
+// Various state files
+
+func marshalProto(t *testing.T, message proto.Message) []byte {
+	data, err := proto.Marshal(message)
+	if err != nil {
+		t.Errorf("%v", err)
+	}
+	return data
+}
+
+func protoFile(name string, mtime_nsec int64, hash string, contents []*fid_proto.PartialCompileInput) (pci *fid_proto.PartialCompileInput) {
+	pci = &fid_proto.PartialCompileInput{
+		Name: proto.String(name),
+	}
+	if mtime_nsec != 0 {
+		pci.MtimeNsec = proto.Int64(mtime_nsec)
+	}
+	if len(hash) > 0 {
+		pci.Hash = proto.String(hash)
+	}
+	if contents != nil {
+		pci.Contents = contents
+	}
+	return
+}
+
+func TestLoadState(t *testing.T) {
+	testCases := []struct {
+		Name     string
+		Filename string
+		Mapfs    fs.ReadFileFS
+		Expected *fid_proto.PartialCompileInputs
+		Err      error
+	}{
+		{
+			Name:     "missing file",
+			Filename: "missing",
+			Mapfs:    fstest.MapFS{},
+			Expected: &fid_proto.PartialCompileInputs{},
+			Err:      nil,
+		},
+		{
+			Name:     "bad file",
+			Filename: ".",
+			Mapfs:    OsFs,
+			Expected: &fid_proto.PartialCompileInputs{},
+			Err:      errors.New("read failed"),
+		},
+		{
+			Name:     "file with mtime",
+			Filename: "state.old",
+			Mapfs: fstest.MapFS{
+				"state.old": &fstest.MapFile{
+					Data: marshalProto(t, &fid_proto.PartialCompileInputs{
+						InputFiles: []*fid_proto.PartialCompileInput{
+							protoFile("input1", 100, "", nil),
+						},
+					}),
+				},
+			},
+			Expected: &fid_proto.PartialCompileInputs{
+				InputFiles: []*fid_proto.PartialCompileInput{
+					protoFile("input1", 100, "", nil),
+				},
+			},
+			Err: nil,
+		},
+		{
+			Name:     "file with mtime and hash",
+			Filename: "state.old",
+			Mapfs: fstest.MapFS{
+				"state.old": &fstest.MapFile{
+					Data: marshalProto(t, &fid_proto.PartialCompileInputs{
+						InputFiles: []*fid_proto.PartialCompileInput{
+							protoFile("input1", 100, "crc:crc_value", nil),
+						},
+					}),
+				},
+			},
+			Expected: &fid_proto.PartialCompileInputs{
+				InputFiles: []*fid_proto.PartialCompileInput{
+					protoFile("input1", 100, "crc:crc_value", nil),
+				},
+			},
+			Err: nil,
+		},
+	}
+	for _, tc := range testCases {
+		actual, err := LoadState(tc.Filename, tc.Mapfs)
+		if tc.Err == nil {
+			android.AssertSame(t, tc.Name, tc.Err, err)
+		} else if err == nil {
+			t.Errorf("%s: expected error, did not get one", tc.Name)
+		}
+		if !proto.Equal(tc.Expected, actual) {
+			t.Errorf("%s: expected %v, actual %v", tc.Name, tc.Expected, actual)
+		}
+	}
+}
+
+func TestCreateState(t *testing.T) {
+	testCases := []struct {
+		Name     string
+		Inputs   []string
+		Inspect  bool
+		Mapfs    StatReadFileFS
+		Expected *fid_proto.PartialCompileInputs
+		Err      error
+	}{
+		{
+			Name:     "no inputs",
+			Inputs:   []string{},
+			Mapfs:    fstest.MapFS{},
+			Expected: &fid_proto.PartialCompileInputs{},
+			Err:      nil,
+		},
+		{
+			Name:   "files found",
+			Inputs: []string{"baz", "foo", "bar"},
+			Mapfs: fstest.MapFS{
+				"foo": &fstest.MapFile{ModTime: time.Unix(0, 100).UTC()},
+				"baz": &fstest.MapFile{ModTime: time.Unix(0, 300).UTC()},
+				"bar": &fstest.MapFile{ModTime: time.Unix(0, 200).UTC()},
+			},
+			Expected: &fid_proto.PartialCompileInputs{
+				InputFiles: []*fid_proto.PartialCompileInput{
+					// Files are always sorted.
+					protoFile("bar", 200, "", nil),
+					protoFile("baz", 300, "", nil),
+					protoFile("foo", 100, "", nil),
+				},
+			},
+			Err: nil,
+		},
+	}
+	for _, tc := range testCases {
+		actual, err := CreateState(tc.Inputs, tc.Inspect, tc.Mapfs)
+		if tc.Err == nil {
+			android.AssertSame(t, tc.Name, tc.Err, err)
+		} else if err == nil {
+			t.Errorf("%s: expected error, did not get one", tc.Name)
+		}
+		if !proto.Equal(tc.Expected, actual) {
+			t.Errorf("%s: expected %v, actual %v", tc.Name, tc.Expected, actual)
+		}
+	}
+}
+
+func TestCompareInternalState(t *testing.T) {
+	testCases := []struct {
+		Name     string
+		Target   string
+		Prior    *fid_proto.PartialCompileInputs
+		New      *fid_proto.PartialCompileInputs
+		Expected *FileList
+	}{
+		{
+			Name:   "prior is empty",
+			Target: "foo",
+			Prior:  &fid_proto.PartialCompileInputs{},
+			New: &fid_proto.PartialCompileInputs{
+				InputFiles: []*fid_proto.PartialCompileInput{
+					protoFile("file1", 100, "", nil),
+				},
+			},
+			Expected: &FileList{
+				Name:      "foo",
+				Additions: []string{"file1"},
+			},
+		},
+		{
+			Name:   "one of each",
+			Target: "foo",
+			Prior: &fid_proto.PartialCompileInputs{
+				InputFiles: []*fid_proto.PartialCompileInput{
+					protoFile("file0", 100, "", nil),
+					protoFile("file1", 100, "", nil),
+					protoFile("file2", 200, "", nil),
+				},
+			},
+			New: &fid_proto.PartialCompileInputs{
+				InputFiles: []*fid_proto.PartialCompileInput{
+					protoFile("file0", 100, "", nil),
+					protoFile("file1", 200, "", nil),
+					protoFile("file3", 300, "", nil),
+				},
+			},
+			Expected: &FileList{
+				Name:      "foo",
+				Additions: []string{"file3"},
+				Changes:   []FileList{FileList{Name: "file1"}},
+				Deletions: []string{"file2"},
+			},
+		},
+	}
+	for _, tc := range testCases {
+		actual := CompareInternalState(tc.Prior, tc.New, tc.Target)
+		if !tc.Expected.Equal(actual) {
+			t.Errorf("%s: expected %q, actual %q", tc.Name, tc.Expected, actual)
+		}
+	}
+}
diff --git a/cmd/find_input_delta/find_input_delta_proto/Android.bp b/cmd/find_input_delta/find_input_delta_proto/Android.bp
new file mode 100644
index 0000000..18eba6b
--- /dev/null
+++ b/cmd/find_input_delta/find_input_delta_proto/Android.bp
@@ -0,0 +1,29 @@
+// Copyright 2024 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+bootstrap_go_package {
+    name: "soong-cmd-find_input_delta-proto",
+    pkgPath: "android/soong/cmd/find_input_delta/find_input_delta_proto",
+    deps: [
+        "golang-protobuf-reflect-protoreflect",
+        "golang-protobuf-runtime-protoimpl",
+    ],
+    srcs: [
+        "target_delta_files.pb.go",
+    ],
+}
diff --git a/cmd/find_input_delta/find_input_delta_proto/file_list.pb.go b/cmd/find_input_delta/find_input_delta_proto/file_list.pb.go
new file mode 100644
index 0000000..648ef22
--- /dev/null
+++ b/cmd/find_input_delta/find_input_delta_proto/file_list.pb.go
@@ -0,0 +1,198 @@
+//
+// Copyright (C) 2024 The Android Open-Source Project
+//
+// 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.
+
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// 	protoc-gen-go v1.33.0
+// 	protoc        v3.21.12
+// source: file_list.proto
+
+package proto
+
+import (
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	reflect "reflect"
+	sync "sync"
+)
+
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type FileList struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// The name of the file.
+	// In the outermost message, this is the name of the Ninja target.
+	// When used in `changes`, this is the name of the changed file.
+	Name *string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"`
+	// The added files.
+	Additions []string `protobuf:"bytes,2,rep,name=additions" json:"additions,omitempty"`
+	// The deleted files.
+	Deletions []string `protobuf:"bytes,3,rep,name=deletions" json:"deletions,omitempty"`
+	// The changed files.
+	Changes []*FileList `protobuf:"bytes,4,rep,name=changes" json:"changes,omitempty"`
+}
+
+func (x *FileList) Reset() {
+	*x = FileList{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_file_list_proto_msgTypes[0]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *FileList) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*FileList) ProtoMessage() {}
+
+func (x *FileList) ProtoReflect() protoreflect.Message {
+	mi := &file_file_list_proto_msgTypes[0]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use FileList.ProtoReflect.Descriptor instead.
+func (*FileList) Descriptor() ([]byte, []int) {
+	return file_file_list_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *FileList) GetName() string {
+	if x != nil && x.Name != nil {
+		return *x.Name
+	}
+	return ""
+}
+
+func (x *FileList) GetAdditions() []string {
+	if x != nil {
+		return x.Additions
+	}
+	return nil
+}
+
+func (x *FileList) GetDeletions() []string {
+	if x != nil {
+		return x.Deletions
+	}
+	return nil
+}
+
+func (x *FileList) GetChanges() []*FileList {
+	if x != nil {
+		return x.Changes
+	}
+	return nil
+}
+
+var File_file_list_proto protoreflect.FileDescriptor
+
+var file_file_list_proto_rawDesc = []byte{
+	0x0a, 0x0f, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74,
+	0x6f, 0x12, 0x1e, 0x61, 0x6e, 0x64, 0x72, 0x6f, 0x69, 0x64, 0x2e, 0x66, 0x69, 0x6e, 0x64, 0x5f,
+	0x69, 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x64, 0x65, 0x6c, 0x74, 0x61, 0x5f, 0x70, 0x72, 0x6f, 0x74,
+	0x6f, 0x22, 0x9e, 0x01, 0x0a, 0x08, 0x46, 0x69, 0x6c, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x12,
+	0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61,
+	0x6d, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x64, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18,
+	0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x61, 0x64, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73,
+	0x12, 0x1c, 0x0a, 0x09, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x03, 0x20,
+	0x03, 0x28, 0x09, 0x52, 0x09, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x42,
+	0x0a, 0x07, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32,
+	0x28, 0x2e, 0x61, 0x6e, 0x64, 0x72, 0x6f, 0x69, 0x64, 0x2e, 0x66, 0x69, 0x6e, 0x64, 0x5f, 0x69,
+	0x6e, 0x70, 0x75, 0x74, 0x5f, 0x64, 0x65, 0x6c, 0x74, 0x61, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f,
+	0x2e, 0x46, 0x69, 0x6c, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x07, 0x63, 0x68, 0x61, 0x6e, 0x67,
+	0x65, 0x73, 0x42, 0x26, 0x5a, 0x24, 0x61, 0x6e, 0x64, 0x72, 0x6f, 0x69, 0x64, 0x2f, 0x73, 0x6f,
+	0x6f, 0x6e, 0x67, 0x2f, 0x66, 0x69, 0x6e, 0x64, 0x5f, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x64,
+	0x65, 0x6c, 0x74, 0x61, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f,
+}
+
+var (
+	file_file_list_proto_rawDescOnce sync.Once
+	file_file_list_proto_rawDescData = file_file_list_proto_rawDesc
+)
+
+func file_file_list_proto_rawDescGZIP() []byte {
+	file_file_list_proto_rawDescOnce.Do(func() {
+		file_file_list_proto_rawDescData = protoimpl.X.CompressGZIP(file_file_list_proto_rawDescData)
+	})
+	return file_file_list_proto_rawDescData
+}
+
+var file_file_list_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
+var file_file_list_proto_goTypes = []interface{}{
+	(*FileList)(nil), // 0: android.find_input_delta_proto.FileList
+}
+var file_file_list_proto_depIdxs = []int32{
+	0, // 0: android.find_input_delta_proto.FileList.changes:type_name -> android.find_input_delta_proto.FileList
+	1, // [1:1] is the sub-list for method output_type
+	1, // [1:1] is the sub-list for method input_type
+	1, // [1:1] is the sub-list for extension type_name
+	1, // [1:1] is the sub-list for extension extendee
+	0, // [0:1] is the sub-list for field type_name
+}
+
+func init() { file_file_list_proto_init() }
+func file_file_list_proto_init() {
+	if File_file_list_proto != nil {
+		return
+	}
+	if !protoimpl.UnsafeEnabled {
+		file_file_list_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*FileList); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+	}
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+			RawDescriptor: file_file_list_proto_rawDesc,
+			NumEnums:      0,
+			NumMessages:   1,
+			NumExtensions: 0,
+			NumServices:   0,
+		},
+		GoTypes:           file_file_list_proto_goTypes,
+		DependencyIndexes: file_file_list_proto_depIdxs,
+		MessageInfos:      file_file_list_proto_msgTypes,
+	}.Build()
+	File_file_list_proto = out.File
+	file_file_list_proto_rawDesc = nil
+	file_file_list_proto_goTypes = nil
+	file_file_list_proto_depIdxs = nil
+}
diff --git a/cmd/find_input_delta/find_input_delta_proto/file_list.proto b/cmd/find_input_delta/find_input_delta_proto/file_list.proto
new file mode 100644
index 0000000..d7faca9
--- /dev/null
+++ b/cmd/find_input_delta/find_input_delta_proto/file_list.proto
@@ -0,0 +1,34 @@
+//
+// Copyright (C) 2024 The Android Open-Source Project
+//
+// 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.
+
+syntax = "proto2";
+package android.find_input_delta_proto;
+option go_package = "android/soong/find_input_delta/proto";
+
+message FileList {
+  // The name of the file.
+  // In the outermost message, this is the name of the Ninja target.
+  // When used in `changes`, this is the name of the changed file.
+  optional string name = 1;
+
+  // The added files.
+  repeated string additions = 2;
+
+  // The deleted files.
+  repeated string deletions = 3;
+
+  // The changed files.
+  repeated FileList changes = 4;
+}
diff --git a/cmd/find_input_delta/find_input_delta_proto/regen.sh b/cmd/find_input_delta/find_input_delta_proto/regen.sh
new file mode 100644
index 0000000..d773659
--- /dev/null
+++ b/cmd/find_input_delta/find_input_delta_proto/regen.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+aprotoc --go_out=paths=source_relative:. file_list.proto
diff --git a/cmd/find_input_delta/find_input_delta_proto_internal/Android.bp b/cmd/find_input_delta/find_input_delta_proto_internal/Android.bp
new file mode 100644
index 0000000..00ba9ff
--- /dev/null
+++ b/cmd/find_input_delta/find_input_delta_proto_internal/Android.bp
@@ -0,0 +1,29 @@
+// Copyright 2024 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+bootstrap_go_package {
+    name: "soong-cmd-find_input_delta-proto_internal",
+    pkgPath: "android/soong/cmd/find_input_delta/find_input_delta_proto_internal",
+    deps: [
+        "golang-protobuf-reflect-protoreflect",
+        "golang-protobuf-runtime-protoimpl",
+    ],
+    srcs: [
+        "internal_state.pb.go",
+    ],
+}
diff --git a/cmd/find_input_delta/find_input_delta_proto_internal/internal_state.pb.go b/cmd/find_input_delta/find_input_delta_proto_internal/internal_state.pb.go
new file mode 100644
index 0000000..2229a32
--- /dev/null
+++ b/cmd/find_input_delta/find_input_delta_proto_internal/internal_state.pb.go
@@ -0,0 +1,268 @@
+//
+// Copyright (C) 2024 The Android Open-Source Project
+//
+// 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.
+
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// 	protoc-gen-go v1.33.0
+// 	protoc        v3.21.12
+// source: internal_state.proto
+
+package proto
+
+import (
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	reflect "reflect"
+	sync "sync"
+)
+
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+// The state of all inputs.
+type PartialCompileInputs struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// The status of each file.
+	InputFiles []*PartialCompileInput `protobuf:"bytes,1,rep,name=input_files,json=inputFiles" json:"input_files,omitempty"`
+}
+
+func (x *PartialCompileInputs) Reset() {
+	*x = PartialCompileInputs{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_internal_state_proto_msgTypes[0]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *PartialCompileInputs) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*PartialCompileInputs) ProtoMessage() {}
+
+func (x *PartialCompileInputs) ProtoReflect() protoreflect.Message {
+	mi := &file_internal_state_proto_msgTypes[0]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use PartialCompileInputs.ProtoReflect.Descriptor instead.
+func (*PartialCompileInputs) Descriptor() ([]byte, []int) {
+	return file_internal_state_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *PartialCompileInputs) GetInputFiles() []*PartialCompileInput {
+	if x != nil {
+		return x.InputFiles
+	}
+	return nil
+}
+
+// The state of one input.
+type PartialCompileInput struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// The name of the file.
+	Name *string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"`
+	// The timestamp of the file in (Unix) nanoseconds.
+	MtimeNsec *int64 `protobuf:"varint,2,opt,name=mtime_nsec,json=mtimeNsec" json:"mtime_nsec,omitempty"`
+	// The hash of the file, in the form ‘{HASHNAME}:{VALUE}’
+	Hash *string `protobuf:"bytes,3,opt,name=hash" json:"hash,omitempty"`
+	// Contents of the file, if the file was inspected (such as jar files, etc).
+	Contents []*PartialCompileInput `protobuf:"bytes,4,rep,name=contents" json:"contents,omitempty"`
+}
+
+func (x *PartialCompileInput) Reset() {
+	*x = PartialCompileInput{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_internal_state_proto_msgTypes[1]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *PartialCompileInput) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*PartialCompileInput) ProtoMessage() {}
+
+func (x *PartialCompileInput) ProtoReflect() protoreflect.Message {
+	mi := &file_internal_state_proto_msgTypes[1]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use PartialCompileInput.ProtoReflect.Descriptor instead.
+func (*PartialCompileInput) Descriptor() ([]byte, []int) {
+	return file_internal_state_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *PartialCompileInput) GetName() string {
+	if x != nil && x.Name != nil {
+		return *x.Name
+	}
+	return ""
+}
+
+func (x *PartialCompileInput) GetMtimeNsec() int64 {
+	if x != nil && x.MtimeNsec != nil {
+		return *x.MtimeNsec
+	}
+	return 0
+}
+
+func (x *PartialCompileInput) GetHash() string {
+	if x != nil && x.Hash != nil {
+		return *x.Hash
+	}
+	return ""
+}
+
+func (x *PartialCompileInput) GetContents() []*PartialCompileInput {
+	if x != nil {
+		return x.Contents
+	}
+	return nil
+}
+
+var File_internal_state_proto protoreflect.FileDescriptor
+
+var file_internal_state_proto_rawDesc = []byte{
+	0x0a, 0x14, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65,
+	0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x1e, 0x61, 0x6e, 0x64, 0x72, 0x6f, 0x69, 0x64, 0x2e,
+	0x66, 0x69, 0x6e, 0x64, 0x5f, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x64, 0x65, 0x6c, 0x74, 0x61,
+	0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x6c, 0x0a, 0x14, 0x50, 0x61, 0x72, 0x74, 0x69, 0x61,
+	0x6c, 0x43, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x73, 0x12, 0x54,
+	0x0a, 0x0b, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20,
+	0x03, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x61, 0x6e, 0x64, 0x72, 0x6f, 0x69, 0x64, 0x2e, 0x66, 0x69,
+	0x6e, 0x64, 0x5f, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x64, 0x65, 0x6c, 0x74, 0x61, 0x5f, 0x70,
+	0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x50, 0x61, 0x72, 0x74, 0x69, 0x61, 0x6c, 0x43, 0x6f, 0x6d, 0x70,
+	0x69, 0x6c, 0x65, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x52, 0x0a, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x46,
+	0x69, 0x6c, 0x65, 0x73, 0x22, 0xad, 0x01, 0x0a, 0x13, 0x50, 0x61, 0x72, 0x74, 0x69, 0x61, 0x6c,
+	0x43, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x12, 0x12, 0x0a, 0x04,
+	0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65,
+	0x12, 0x1d, 0x0a, 0x0a, 0x6d, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x6e, 0x73, 0x65, 0x63, 0x18, 0x02,
+	0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x6d, 0x74, 0x69, 0x6d, 0x65, 0x4e, 0x73, 0x65, 0x63, 0x12,
+	0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68,
+	0x61, 0x73, 0x68, 0x12, 0x4f, 0x0a, 0x08, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x73, 0x18,
+	0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x61, 0x6e, 0x64, 0x72, 0x6f, 0x69, 0x64, 0x2e,
+	0x66, 0x69, 0x6e, 0x64, 0x5f, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x64, 0x65, 0x6c, 0x74, 0x61,
+	0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x50, 0x61, 0x72, 0x74, 0x69, 0x61, 0x6c, 0x43, 0x6f,
+	0x6d, 0x70, 0x69, 0x6c, 0x65, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x52, 0x08, 0x63, 0x6f, 0x6e, 0x74,
+	0x65, 0x6e, 0x74, 0x73, 0x42, 0x26, 0x5a, 0x24, 0x61, 0x6e, 0x64, 0x72, 0x6f, 0x69, 0x64, 0x2f,
+	0x73, 0x6f, 0x6f, 0x6e, 0x67, 0x2f, 0x66, 0x69, 0x6e, 0x64, 0x5f, 0x69, 0x6e, 0x70, 0x75, 0x74,
+	0x5f, 0x64, 0x65, 0x6c, 0x74, 0x61, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f,
+}
+
+var (
+	file_internal_state_proto_rawDescOnce sync.Once
+	file_internal_state_proto_rawDescData = file_internal_state_proto_rawDesc
+)
+
+func file_internal_state_proto_rawDescGZIP() []byte {
+	file_internal_state_proto_rawDescOnce.Do(func() {
+		file_internal_state_proto_rawDescData = protoimpl.X.CompressGZIP(file_internal_state_proto_rawDescData)
+	})
+	return file_internal_state_proto_rawDescData
+}
+
+var file_internal_state_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
+var file_internal_state_proto_goTypes = []interface{}{
+	(*PartialCompileInputs)(nil), // 0: android.find_input_delta_proto.PartialCompileInputs
+	(*PartialCompileInput)(nil),  // 1: android.find_input_delta_proto.PartialCompileInput
+}
+var file_internal_state_proto_depIdxs = []int32{
+	1, // 0: android.find_input_delta_proto.PartialCompileInputs.input_files:type_name -> android.find_input_delta_proto.PartialCompileInput
+	1, // 1: android.find_input_delta_proto.PartialCompileInput.contents:type_name -> android.find_input_delta_proto.PartialCompileInput
+	2, // [2:2] is the sub-list for method output_type
+	2, // [2:2] is the sub-list for method input_type
+	2, // [2:2] is the sub-list for extension type_name
+	2, // [2:2] is the sub-list for extension extendee
+	0, // [0:2] is the sub-list for field type_name
+}
+
+func init() { file_internal_state_proto_init() }
+func file_internal_state_proto_init() {
+	if File_internal_state_proto != nil {
+		return
+	}
+	if !protoimpl.UnsafeEnabled {
+		file_internal_state_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*PartialCompileInputs); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_internal_state_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*PartialCompileInput); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+	}
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+			RawDescriptor: file_internal_state_proto_rawDesc,
+			NumEnums:      0,
+			NumMessages:   2,
+			NumExtensions: 0,
+			NumServices:   0,
+		},
+		GoTypes:           file_internal_state_proto_goTypes,
+		DependencyIndexes: file_internal_state_proto_depIdxs,
+		MessageInfos:      file_internal_state_proto_msgTypes,
+	}.Build()
+	File_internal_state_proto = out.File
+	file_internal_state_proto_rawDesc = nil
+	file_internal_state_proto_goTypes = nil
+	file_internal_state_proto_depIdxs = nil
+}
diff --git a/cmd/find_input_delta/find_input_delta_proto_internal/internal_state.proto b/cmd/find_input_delta/find_input_delta_proto_internal/internal_state.proto
new file mode 100644
index 0000000..113fc64
--- /dev/null
+++ b/cmd/find_input_delta/find_input_delta_proto_internal/internal_state.proto
@@ -0,0 +1,39 @@
+//
+// Copyright (C) 2024 The Android Open-Source Project
+//
+// 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.
+
+syntax = "proto2";
+package android.find_input_delta_proto;
+option go_package = "android/soong/find_input_delta/proto";
+
+// The state of all inputs.
+message PartialCompileInputs {
+  // The status of each file.
+  repeated PartialCompileInput input_files = 1;
+}
+
+// The state of one input.
+message PartialCompileInput {
+  // The name of the file.
+  optional string name = 1;
+
+  // The timestamp of the file in (Unix) nanoseconds.
+  optional int64 mtime_nsec = 2;
+
+  // The hash of the file, in the form ‘{HASHNAME}:{VALUE}’
+  optional string hash = 3;
+
+  // Contents of the file, if the file was inspected (such as jar files, etc).
+  repeated PartialCompileInput contents = 4;
+}
diff --git a/cmd/find_input_delta/find_input_delta_proto_internal/regen.sh b/cmd/find_input_delta/find_input_delta_proto_internal/regen.sh
new file mode 100644
index 0000000..cbaf7d0
--- /dev/null
+++ b/cmd/find_input_delta/find_input_delta_proto_internal/regen.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+aprotoc --go_out=paths=source_relative:.  internal_state.proto