go: support //go:embed directives with embedsrcs attribute (#2806)

go_library, go_binary, and go_test now support the //go:embed
directive in Go 1.16. When building a package containing a //go:embed
directive, they'll match patterns against embedable files to generate
an embedcfg file to pass to the compiler.

Embeddable files must be listed in the new embedsrcs attribute.

The GoSource provider now has an embedsrcs field to support this.

Fixes #2775
diff --git a/go/core.rst b/go/core.rst
index e732c8a..755e855 100644
--- a/go/core.rst
+++ b/go/core.rst
@@ -113,6 +113,14 @@
 | files are also permitted. Files may be filtered at build time                                    |
 | using Go `build constraints`_.                                                                   |
 +----------------------------+-----------------------------+---------------------------------------+
+| :param:`embedsrcs`         | :type:`label_list`          | :value:`[]`                           |
++----------------------------+-----------------------------+---------------------------------------+
+| The list of files that may be embedded into the compiled package using                           |
+| ``//go:embed`` directives. All files must be in the same logical directory                       |
+| or a subdirectory as source files. All source files containing ``//go:embed``                    |
+| directives must be in the same logical directory. It's okay to mix static and                    |
+| generated source files and static and generated embeddable files.                                |
++----------------------------+-----------------------------+---------------------------------------+
 | :param:`x_defs`            | :type:`string_dict`         | :value:`{}`                           |
 +----------------------------+-----------------------------+---------------------------------------+
 | Map of defines to add to the go link command.                                                    |
@@ -309,6 +317,14 @@
 | files are also permitted. Files may be filtered at build time                                    |
 | using Go `build constraints`_.                                                                   |
 +----------------------------+-----------------------------+---------------------------------------+
+| :param:`embedsrcs`         | :type:`label_list`          | :value:`[]`                           |
++----------------------------+-----------------------------+---------------------------------------+
+| The list of files that may be embedded into the compiled package using                           |
+| ``//go:embed`` directives. All files must be in the same logical directory                       |
+| or a subdirectory as source files. All source files containing ``//go:embed``                    |
+| directives must be in the same logical directory. It's okay to mix static and                    |
+| generated source files and static and generated embeddable files.                                |
++----------------------------+-----------------------------+---------------------------------------+
 | :param:`deps`              | :type:`label_list`          | :value:`[]`                           |
 +----------------------------+-----------------------------+---------------------------------------+
 | List of Go libraries this package imports directly.                                              |
@@ -547,6 +563,14 @@
 | and the embedding library may not also have ``cgo = True``. See Embedding_                       |
 | for more information.                                                                            |
 +----------------------------+-----------------------------+---------------------------------------+
+| :param:`embedsrcs`         | :type:`label_list`          | :value:`[]`                           |
++----------------------------+-----------------------------+---------------------------------------+
+| The list of files that may be embedded into the compiled package using                           |
+| ``//go:embed`` directives. All files must be in the same logical directory                       |
+| or a subdirectory as source files. All source files containing ``//go:embed``                    |
+| directives must be in the same logical directory. It's okay to mix static and                    |
+| generated source files and static and generated embeddable files.                                |
++----------------------------+-----------------------------+---------------------------------------+
 | :param:`data`              | :type:`label_list`          | :value:`[]`                           |
 +----------------------------+-----------------------------+---------------------------------------+
 | List of files needed by this rule at run-time. This may include data files                       |
diff --git a/go/private/actions/archive.bzl b/go/private/actions/archive.bzl
index bbb0876..4f1fbd3 100644
--- a/go/private/actions/archive.bzl
+++ b/go/private/actions/archive.bzl
@@ -96,6 +96,7 @@
             go,
             sources = split.go + split.c + split.asm + split.cxx + split.objc + split.headers,
             cover = source.cover,
+            embedsrcs = source.embedsrcs,
             importpath = importpath,
             importmap = importmap,
             archives = direct,
@@ -119,6 +120,7 @@
             go,
             sources = split.go + split.c + split.asm + split.cxx + split.objc + split.headers,
             cover = source.cover,
+            embedsrcs = source.embedsrcs,
             importpath = importpath,
             importmap = importmap,
             archives = direct,
@@ -149,6 +151,7 @@
         orig_srcs = as_tuple(source.orig_srcs),
         _orig_src_map = tuple([source.orig_src_map.get(src, src) for src in source.srcs]),
         _cover = as_tuple(source.cover),
+        _embedsrcs = as_tuple(source.embedsrcs),
         _x_defs = tuple(source.x_defs.items()),
         _gc_goopts = as_tuple(source.gc_goopts),
         _cgo = source.cgo,
diff --git a/go/private/actions/compilepkg.bzl b/go/private/actions/compilepkg.bzl
index 5e22df8..6398616 100644
--- a/go/private/actions/compilepkg.bzl
+++ b/go/private/actions/compilepkg.bzl
@@ -34,6 +34,7 @@
         go,
         sources = None,
         cover = None,
+        embedsrcs = [],
         importpath = "",
         importmap = "",
         archives = [],
@@ -56,7 +57,7 @@
     if out_lib == None:
         fail("out_lib is a required parameter")
 
-    inputs = (sources + [go.package_list] +
+    inputs = (sources + embedsrcs + [go.package_list] +
               [archive.data.export_file for archive in archives] +
               go.sdk.tools + go.sdk.headers + go.stdlib.libs)
     outputs = [out_lib, out_export]
@@ -64,6 +65,7 @@
 
     args = go.builder_args(go, "compilepkg")
     args.add_all(sources, before_each = "-src")
+    args.add_all(embedsrcs, before_each = "-embedsrc", expand_directories = False)
     if cover and go.coverdata:
         inputs.append(go.coverdata.data.export_file)
         args.add("-arc", _archive(go.coverdata))
diff --git a/go/private/context.bzl b/go/private/context.bzl
index 8a2c59c..00f6d4c 100644
--- a/go/private/context.bzl
+++ b/go/private/context.bzl
@@ -170,6 +170,7 @@
     source["srcs"] = s.srcs + source["srcs"]
     source["orig_srcs"] = s.orig_srcs + source["orig_srcs"]
     source["orig_src_map"].update(s.orig_src_map)
+    source["embedsrcs"] = source["embedsrcs"] + s.embedsrcs
     source["cover"] = source["cover"] + s.cover
     source["deps"] = source["deps"] + s.deps
     source["x_defs"].update(s.x_defs)
@@ -214,6 +215,7 @@
     attr_srcs = [f for t in getattr(attr, "srcs", []) for f in as_iterable(t.files)]
     generated_srcs = getattr(library, "srcs", [])
     srcs = attr_srcs + generated_srcs
+    embedsrcs = [f for t in getattr(attr, "embedsrcs", []) for f in as_iterable(t.files)]
     source = {
         "library": library,
         "mode": go.mode,
@@ -221,6 +223,7 @@
         "orig_srcs": srcs,
         "orig_src_map": {},
         "cover": [],
+        "embedsrcs": embedsrcs,
         "x_defs": {},
         "deps": getattr(attr, "deps", []),
         "gc_goopts": getattr(attr, "gc_goopts", []),
diff --git a/go/private/rules/binary.bzl b/go/private/rules/binary.bzl
index 9f17366..855c363 100644
--- a/go/private/rules/binary.bzl
+++ b/go/private/rules/binary.bzl
@@ -88,6 +88,7 @@
         "embed": attr.label_list(
             providers = [GoLibrary],
         ),
+        "embedsrcs": attr.label_list(allow_files = True),
         "importpath": attr.string(),
         "gc_goopts": attr.string_list(),
         "gc_linkopts": attr.string_list(),
diff --git a/go/private/rules/library.bzl b/go/private/rules/library.bzl
index 9dd8625..704da18 100644
--- a/go/private/rules/library.bzl
+++ b/go/private/rules/library.bzl
@@ -60,6 +60,7 @@
         "importmap": attr.string(),
         "importpath_aliases": attr.string_list(),  # experimental, undocumented
         "embed": attr.label_list(providers = [GoLibrary]),
+        "embedsrcs": attr.label_list(allow_files = True),
         "gc_goopts": attr.string_list(),
         "x_defs": attr.string_dict(),
         "cgo": attr.bool(),
diff --git a/go/private/rules/test.bzl b/go/private/rules/test.bzl
index d400aa1..7e90d3f 100644
--- a/go/private/rules/test.bzl
+++ b/go/private/rules/test.bzl
@@ -18,6 +18,7 @@
 )
 load(
     "//go/private:common.bzl",
+    "as_list",
     "asm_exts",
     "cgo_exts",
     "go_exts",
@@ -182,6 +183,7 @@
         "srcs": attr.label_list(allow_files = go_exts + asm_exts + cgo_exts),
         "deps": attr.label_list(providers = [GoLibrary]),
         "embed": attr.label_list(providers = [GoLibrary]),
+        "embedsrcs": attr.label_list(allow_files = True),
         "importpath": attr.string(),
         "gc_goopts": attr.string_list(),
         "gc_linkopts": attr.string_list(),
@@ -391,21 +393,22 @@
         source = GoSource(
             library = library,
             mode = go.mode,
-            srcs = arc_data.srcs,
-            orig_srcs = arc_data.orig_srcs,
+            srcs = as_list(arc_data.srcs),
+            orig_srcs = as_list(arc_data.orig_srcs),
             orig_src_map = dict(zip(arc_data.srcs, arc_data._orig_src_map)),
             cover = arc_data._cover,
+            embedsrcs = as_list(arc_data._embedsrcs),
             x_defs = dict(arc_data._x_defs),
             deps = deps,
-            gc_goopts = arc_data._gc_goopts,
+            gc_goopts = as_list(arc_data._gc_goopts),
             runfiles = go._ctx.runfiles(files = arc_data.data_files),
             cgo = arc_data._cgo,
-            cdeps = arc_data._cdeps,
-            cppopts = arc_data._cppopts,
-            copts = arc_data._copts,
-            cxxopts = arc_data._cxxopts,
-            clinkopts = arc_data._clinkopts,
-            cgo_exports = arc_data._cgo_exports,
+            cdeps = as_list(arc_data._cdeps),
+            cppopts = as_list(arc_data._cppopts),
+            copts = as_list(arc_data._copts),
+            cxxopts = as_list(arc_data._cxxopts),
+            clinkopts = as_list(arc_data._clinkopts),
+            cgo_exports = as_list(arc_data._cgo_exports),
         )
 
         # If this archive needs to be recompiled, use go.archive.
diff --git a/go/providers.rst b/go/providers.rst
index 992debd..7dfe9c1 100644
--- a/go/providers.rst
+++ b/go/providers.rst
@@ -139,6 +139,13 @@
 | Maps generated files in :param:`srcs` back to :param:`orig_srcs`. Not all                        |
 | generated files may appear in here.                                                              |
 +--------------------------------+-----------------------------------------------------------------+
+| :param:`embedsrcs`             | :type:`list of File`                                            |
++--------------------------------+-----------------------------------------------------------------+
+| Files that may be embedded into the compiled package using ``//go:embed``                        |
+| directives. All files must be in the same logical directory or a subdirectory                    |
+| as source files. However, it's okay to mix static and generated source files                     |
+| and static and generated embeddable files.                                                       |
++--------------------------------+-----------------------------------------------------------------+
 | :param:`cover`                 | :type:`list of File`                                            |
 +--------------------------------+-----------------------------------------------------------------+
 | List of source files to instrument for code coverage.                                            |
diff --git a/go/tools/builders/BUILD.bazel b/go/tools/builders/BUILD.bazel
index a3b406a..019a22d 100644
--- a/go/tools/builders/BUILD.bazel
+++ b/go/tools/builders/BUILD.bazel
@@ -7,6 +7,7 @@
     srcs = [
         "filter.go",
         "filter_test.go",
+        "read.go",
     ],
 )
 
@@ -20,6 +21,7 @@
         "compile.go",
         "compilepkg.go",
         "cover.go",
+        "embedcfg.go",
         "env.go",
         "filter.go",
         "filter_buildid.go",
@@ -30,6 +32,7 @@
         "importcfg.go",
         "link.go",
         "pack.go",
+        "read.go",
         "replicate.go",
         "stdlib.go",
     ] + select({
diff --git a/go/tools/builders/asm.go b/go/tools/builders/asm.go
index c3fb845..18f9f84 100644
--- a/go/tools/builders/asm.go
+++ b/go/tools/builders/asm.go
@@ -51,7 +51,7 @@
 	source := flags.Args()[0]
 
 	// Filter the input file.
-	metadata, err := readFileInfo(build.Default, source, false)
+	metadata, err := readFileInfo(build.Default, source)
 	if err != nil {
 		return err
 	}
diff --git a/go/tools/builders/compilepkg.go b/go/tools/builders/compilepkg.go
index a543366..c8896d5 100644
--- a/go/tools/builders/compilepkg.go
+++ b/go/tools/builders/compilepkg.go
@@ -40,7 +40,7 @@
 
 	fs := flag.NewFlagSet("GoCompilePkg", flag.ExitOnError)
 	goenv := envFlags(fs)
-	var unfilteredSrcs, coverSrcs multiFlag
+	var unfilteredSrcs, coverSrcs, embedSrcs multiFlag
 	var deps archiveMultiFlag
 	var importPath, packagePath, nogoPath, packageListPath, coverMode string
 	var outPath, outFactsPath, cgoExportHPath string
@@ -48,6 +48,7 @@
 	var gcFlags, asmFlags, cppFlags, cFlags, cxxFlags, objcFlags, objcxxFlags, ldFlags quoteMultiFlag
 	fs.Var(&unfilteredSrcs, "src", ".go, .c, .cc, .m, .mm, .s, or .S file to be filtered and compiled")
 	fs.Var(&coverSrcs, "cover", ".go file that should be instrumented for coverage (must also be a -src)")
+	fs.Var(&embedSrcs, "embedsrc", "file that may be compiled into the package with a //go:embed directive")
 	fs.Var(&deps, "arc", "Import path, package path, and file name of a direct dependency, separated by '='")
 	fs.StringVar(&importPath, "importpath", "", "The import path of the package being compiled. Not passed to the compiler, but may be displayed in debug data.")
 	fs.StringVar(&packagePath, "p", "", "The package path (importmap) of the package being compiled")
@@ -81,6 +82,9 @@
 	for i := range unfilteredSrcs {
 		unfilteredSrcs[i] = abs(unfilteredSrcs[i])
 	}
+	for i := range embedSrcs {
+		embedSrcs[i] = abs(embedSrcs[i])
+	}
 	for i := range coverSrcs {
 		coverSrcs[i] = abs(coverSrcs[i])
 	}
@@ -124,6 +128,7 @@
 		deps,
 		coverMode,
 		coverSrcs,
+		embedSrcs,
 		cgoEnabled,
 		cc,
 		gcFlags,
@@ -149,6 +154,7 @@
 	deps []archive,
 	coverMode string,
 	coverSrcs []string,
+	embedSrcs []string,
 	cgoEnabled bool,
 	cc string,
 	gcFlags []string,
@@ -318,6 +324,37 @@
 	}
 	defer os.Remove(importcfgPath)
 
+	// Build an embedcfg file mapping embed patterns to filenames.
+	// Embed patterns are relative to any one of a list of root directories
+	// that may contain embeddable files. Source files containing embed patterns
+	// must be in one of these root directories so the pattern appears to be
+	// relative to the source file. Usually, there are two roots: the source
+	// directory, and the output directory (so that generated files are
+	// embeddable). There may be additional roots if sources are in multiple
+	// directories (like if there are are generated source files).
+	var srcDirs []string
+	srcDirs = append(srcDirs, filepath.Dir(outPath))
+	for _, src := range srcs.goSrcs {
+		srcDirs = append(srcDirs, filepath.Dir(src.filename))
+	}
+	sort.Strings(srcDirs) // group duplicates to uniq them below.
+	embedRootDirs := srcDirs[:1]
+	for _, dir := range srcDirs {
+		prev := embedRootDirs[len(embedRootDirs)-1]
+		if dir == prev || strings.HasPrefix(dir, prev+string(filepath.Separator)) {
+			// Skip duplicates.
+			continue
+		}
+		embedRootDirs = append(embedRootDirs, dir)
+	}
+	embedcfgPath, err := buildEmbedcfgFile(srcs.goSrcs, embedSrcs, embedRootDirs, workDir)
+	if err != nil {
+		return err
+	}
+	if embedcfgPath != "" {
+		defer os.Remove(embedcfgPath)
+	}
+
 	// Run nogo concurrently.
 	var nogoChan chan error
 	outFactsPath := filepath.Join(workDir, nogoFact)
@@ -349,7 +386,7 @@
 	}
 
 	// Compile the filtered .go files.
-	if err := compileGo(goenv, goSrcs, packagePath, importcfgPath, asmHdrPath, symabisPath, gcFlags, outPath); err != nil {
+	if err := compileGo(goenv, goSrcs, packagePath, importcfgPath, embedcfgPath, asmHdrPath, symabisPath, gcFlags, outPath); err != nil {
 		return err
 	}
 
@@ -419,9 +456,12 @@
 	return appendFiles(goenv, outXPath, []string{pkgDefPath})
 }
 
-func compileGo(goenv *env, srcs []string, packagePath, importcfgPath, asmHdrPath, symabisPath string, gcFlags []string, outPath string) error {
+func compileGo(goenv *env, srcs []string, packagePath, importcfgPath, embedcfgPath, asmHdrPath, symabisPath string, gcFlags []string, outPath string) error {
 	args := goenv.goTool("compile")
 	args = append(args, "-p", packagePath, "-importcfg", importcfgPath, "-pack")
+	if embedcfgPath != "" {
+		args = append(args, "-embedcfg", embedcfgPath)
+	}
 	if asmHdrPath != "" {
 		args = append(args, "-asmhdr", asmHdrPath)
 	}
diff --git a/go/tools/builders/embedcfg.go b/go/tools/builders/embedcfg.go
new file mode 100644
index 0000000..00eff4a
--- /dev/null
+++ b/go/tools/builders/embedcfg.go
@@ -0,0 +1,384 @@
+// Copyright 2021 The Bazel Authors. 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 (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path"
+	"path/filepath"
+	"runtime"
+	"sort"
+	"strings"
+)
+
+// buildEmbedcfgFile writes an embedcfg file to be read by the compiler.
+// An embedcfg file can be used in Go 1.16 or higher if the "embed" package
+// is imported and there are one or more //go:embed comments in .go files.
+// The embedcfg file maps //go:embed patterns to actual file names.
+//
+// The embedcfg file will be created in workDir, and its name is returned.
+// The caller is responsible for deleting it. If no embedcfg file is needed,
+// "" is returned with no error.
+//
+// All source files listed in goSrcs with //go:embed comments must be in one
+// of the directories in embedRootDirs (not in a subdirectory). Embed patterns
+// are evaluated relative to the source directory. Embed sources (embedSrcs)
+// outside those directories are ignored, since they can't be matched by any
+// valid pattern.
+func buildEmbedcfgFile(goSrcs []fileInfo, embedSrcs, embedRootDirs []string, workDir string) (string, error) {
+	// Check whether this package uses embedding and whether the toolchain
+	// supports it (Go 1.16+). With Go 1.15 and lower, we'll try to compile
+	// without an embedcfg file, and the compiler will complain the "embed"
+	// package is missing.
+	var major, minor int
+	if n, err := fmt.Sscanf(runtime.Version(), "go%d.%d", &major, &minor); n != 2 || err != nil {
+		// Can't parse go version. Maybe it's a development version; fall through.
+	} else if major < 1 || (major == 1 && minor < 16) {
+		return "", nil
+	}
+	importEmbed := false
+	haveEmbed := false
+	for _, src := range goSrcs {
+		if len(src.embeds) > 0 {
+			haveEmbed = true
+			rootDir := findInRootDirs(src.filename, embedRootDirs)
+			if rootDir == "" || strings.Contains(src.filename[len(rootDir)+1:], string(filepath.Separator)) {
+				// Report an error if a source files appears in a subdirectory of
+				// another source directory. In this situation, the same file could be
+				// referenced with different paths.
+				return "", fmt.Errorf("%s: source files with //go:embed should be in same directory. Allowed directories are:\n\t%s",
+					src.filename,
+					strings.Join(embedRootDirs, "\n\t"))
+			}
+		}
+		for _, imp := range src.imports {
+			if imp.path == "embed" {
+				importEmbed = true
+			}
+		}
+	}
+	if !importEmbed || !haveEmbed {
+		return "", nil
+	}
+
+	// Build a tree of embeddable files. This includes paths listed with
+	// -embedsrc. If one of those paths is a directory, the tree includes
+	// its files and subdirectories. Paths in the tree are relative to the
+	// path in embedRootDirs that contains them.
+	root, err := buildEmbedTree(embedSrcs, embedRootDirs)
+	if err != nil {
+		return "", err
+	}
+
+	// Resolve patterns to sets of files.
+	var embedcfg struct {
+		Patterns map[string][]string
+		Files    map[string]string
+	}
+	embedcfg.Patterns = make(map[string][]string)
+	embedcfg.Files = make(map[string]string)
+	for _, src := range goSrcs {
+		for _, embed := range src.embeds {
+			matchedPaths, matchedFiles, err := resolveEmbed(embed, root)
+			if err != nil {
+				return "", err
+			}
+			embedcfg.Patterns[embed.pattern] = matchedPaths
+			for i, rel := range matchedPaths {
+				embedcfg.Files[rel] = matchedFiles[i]
+			}
+		}
+	}
+
+	// Write the configuration to a JSON file.
+	embedcfgData, err := json.MarshalIndent(&embedcfg, "", "\t")
+	if err != nil {
+		return "", err
+	}
+	embedcfgName := filepath.Join(workDir, "embedcfg")
+	if err := ioutil.WriteFile(embedcfgName, embedcfgData, 0666); err != nil {
+		return "", err
+	}
+	return embedcfgName, nil
+}
+
+// findInRootDirs returns a string from rootDirs which is a parent of the
+// file path p. If there is no such string, findInRootDirs returns "".
+func findInRootDirs(p string, rootDirs []string) string {
+	dir := filepath.Dir(p)
+	for _, rootDir := range rootDirs {
+		if rootDir == dir ||
+			(strings.HasPrefix(dir, rootDir) && len(dir) > len(rootDir)+1 && dir[len(rootDir)] == filepath.Separator) {
+			return rootDir
+		}
+	}
+	return ""
+}
+
+// embedNode represents an embeddable file or directory in a tree.
+type embedNode struct {
+	name       string                // base name
+	path       string                // absolute file path
+	children   map[string]*embedNode // non-nil for directory
+	childNames []string              // sorted
+}
+
+// add inserts file nodes into the tree rooted at f for the slash-separated
+// path src, relative to the absolute file path rootDir. If src points to a
+// directory, add recursively inserts nodes for its contents. If a node already
+// exists (for example, if a source file and a generated file have the same
+// name), add leaves the existing node in place.
+func (n *embedNode) add(rootDir, src string) error {
+	// Create nodes for parents of src.
+	parent := n
+	parts := strings.Split(src, "/")
+	for _, p := range parts[:len(parts)-1] {
+		if parent.children[p] == nil {
+			parent.children[p] = &embedNode{
+				name:     p,
+				children: make(map[string]*embedNode),
+			}
+		}
+		parent = parent.children[p]
+	}
+
+	// Create a node for src. If src is a directory, recursively create nodes for
+	// its contents. Go embedding ignores symbolic links, but Bazel may use links
+	// for generated files and directories, so we follow them here.
+	var visit func(*embedNode, string, os.FileInfo) error
+	visit = func(parent *embedNode, path string, fi os.FileInfo) error {
+		base := filepath.Base(path)
+		if parent.children[base] == nil {
+			parent.children[base] = &embedNode{name: base, path: path}
+		}
+		if !fi.IsDir() {
+			return nil
+		}
+		node := parent.children[base]
+		node.children = make(map[string]*embedNode)
+		f, err := os.Open(path)
+		if err != nil {
+			return err
+		}
+		names, err := f.Readdirnames(0)
+		f.Close()
+		if err != nil {
+			return err
+		}
+		for _, name := range names {
+			cPath := filepath.Join(path, name)
+			cfi, err := os.Stat(cPath)
+			if err != nil {
+				return err
+			}
+			if err := visit(node, cPath, cfi); err != nil {
+				return err
+			}
+		}
+		return nil
+	}
+
+	path := filepath.Join(rootDir, src)
+	fi, err := os.Stat(path)
+	if err != nil {
+		return err
+	}
+	return visit(parent, path, fi)
+}
+
+func (n *embedNode) isDir() bool {
+	return n.children != nil
+}
+
+// get returns a tree node, given a slash-separated path relative to the
+// receiver. get returns nil if no node exists with that path.
+func (n *embedNode) get(path string) *embedNode {
+	if path == "." || path == "" {
+		return n
+	}
+	for _, part := range strings.Split(path, "/") {
+		n = n.children[part]
+		if n == nil {
+			return nil
+		}
+	}
+	return n
+}
+
+var errSkip = errors.New("skip")
+
+// walk calls fn on each node in the tree rooted at n in depth-first pre-order.
+func (n *embedNode) walk(fn func(rel string, n *embedNode) error) error {
+	var visit func(string, *embedNode) error
+	visit = func(rel string, node *embedNode) error {
+		err := fn(rel, node)
+		if err == errSkip {
+			return nil
+		} else if err != nil {
+			return err
+		}
+		for _, name := range node.childNames {
+			if err := visit(path.Join(rel, name), node.children[name]); err != nil && err != errSkip {
+				return err
+			}
+		}
+		return nil
+	}
+	err := visit("", n)
+	if err == errSkip {
+		return nil
+	}
+	return err
+}
+
+// buildEmbedTree constructs a logical directory tree of embeddable files.
+// The tree may contain a mix of static and generated files from multiple
+// root directories. Directory artifacts are recursively expanded.
+func buildEmbedTree(embedSrcs, embedRootDirs []string) (root *embedNode, err error) {
+	defer func() {
+		if err != nil {
+			err = fmt.Errorf("building tree of embeddable files in directories %s: %v", strings.Join(embedRootDirs, string(filepath.ListSeparator)), err)
+		}
+	}()
+
+	// Add each path to the tree.
+	root = &embedNode{name: "", children: make(map[string]*embedNode)}
+	for _, src := range embedSrcs {
+		rootDir := findInRootDirs(src, embedRootDirs)
+		if rootDir == "" {
+			// Embedded path cannot be matched by any valid pattern. Ignore.
+			continue
+		}
+		rel := filepath.ToSlash(src[len(rootDir)+1:])
+		if err := root.add(rootDir, rel); err != nil {
+			return nil, err
+		}
+	}
+
+	// Sort children in each directory node.
+	var visit func(*embedNode)
+	visit = func(node *embedNode) {
+		node.childNames = make([]string, 0, len(node.children))
+		for name, child := range node.children {
+			node.childNames = append(node.childNames, name)
+			visit(child)
+		}
+		sort.Strings(node.childNames)
+	}
+	visit(root)
+
+	return root, nil
+}
+
+// resolveEmbed matches a //go:embed pattern in a source file to a set of
+// embeddable files in the given tree.
+func resolveEmbed(embed fileEmbed, root *embedNode) (matchedPaths, matchedFiles []string, err error) {
+	defer func() {
+		if err != nil {
+			err = fmt.Errorf("%v: could not embed %s: %v", embed.pos, embed.pattern, err)
+		}
+	}()
+
+	// Check that the pattern has valid syntax.
+	if _, err := path.Match(embed.pattern, ""); err != nil || !validEmbedPattern(embed.pattern) {
+		return nil, nil, fmt.Errorf("invalid pattern syntax")
+	}
+
+	// Search for matching files.
+	err = root.walk(func(matchRel string, matchNode *embedNode) error {
+		if ok, _ := path.Match(embed.pattern, matchRel); !ok {
+			// Non-matching file or directory.
+			return nil
+		}
+		if !matchNode.isDir() {
+			// Matching file. Add to list.
+			matchedPaths = append(matchedPaths, matchRel)
+			matchedFiles = append(matchedFiles, matchNode.path)
+			return nil
+		}
+
+		// Matching directory. Recursively add all files in subdirectories.
+		// Don't add hidden files or directories (starting with "." or "_").
+		// See golang/go#42328.
+		matchTreeErr := matchNode.walk(func(childRel string, childNode *embedNode) error {
+			if childRel != "" {
+				if base := path.Base(childRel); strings.HasPrefix(base, ".") || strings.HasPrefix(base, "_") {
+					return errSkip
+				}
+			}
+			if !childNode.isDir() {
+				matchedPaths = append(matchedPaths, path.Join(matchRel, childRel))
+				matchedFiles = append(matchedFiles, childNode.path)
+			}
+			return nil
+		})
+		if matchTreeErr != nil {
+			return matchTreeErr
+		}
+		return errSkip
+	})
+	if err != nil && err != errSkip {
+		return nil, nil, err
+	}
+	if len(matchedPaths) == 0 {
+		return nil, nil, fmt.Errorf("no matching files found")
+	}
+	return matchedPaths, matchedFiles, nil
+}
+
+func validEmbedPattern(pattern string) bool {
+	return pattern != "." && fsValidPath(pattern)
+}
+
+// validPath reports whether the given path name
+// is valid for use in a call to Open.
+// Path names passed to open are unrooted, slash-separated
+// sequences of path elements, like “x/y/z”.
+// Path names must not contain a “.” or “..” or empty element,
+// except for the special case that the root directory is named “.”.
+//
+// Paths are slash-separated on all systems, even Windows.
+// Backslashes must not appear in path names.
+//
+// Copied from io/fs.ValidPath in Go 1.16beta1.
+func fsValidPath(name string) bool {
+	if name == "." {
+		// special case
+		return true
+	}
+
+	// Iterate over elements in name, checking each.
+	for {
+		i := 0
+		for i < len(name) && name[i] != '/' {
+			if name[i] == '\\' {
+				return false
+			}
+			i++
+		}
+		elem := name[:i]
+		if elem == "" || elem == "." || elem == ".." {
+			return false
+		}
+		if i == len(name) {
+			return true // reached clean ending
+		}
+		name = name[i+1:]
+	}
+}
diff --git a/go/tools/builders/filter.go b/go/tools/builders/filter.go
index f129e80..fbb0f2a 100644
--- a/go/tools/builders/filter.go
+++ b/go/tools/builders/filter.go
@@ -18,21 +18,24 @@
 	"fmt"
 	"go/ast"
 	"go/build"
-	"go/parser"
 	"go/token"
-	"log"
+	"os"
 	"path/filepath"
-	"strconv"
 	"strings"
 )
 
 type fileInfo struct {
 	filename string
 	ext      ext
+	header   []byte
+	fset     *token.FileSet
+	parsed   *ast.File
+	parseErr error
 	matched  bool
 	isCgo    bool
 	pkg      string
-	imports  []string
+	imports  []fileImport
+	embeds   []fileEmbed
 }
 
 type ext int
@@ -47,6 +50,17 @@
 	hExt
 )
 
+type fileImport struct {
+	path string
+	pos  token.Pos
+	doc  *ast.CommentGroup
+}
+
+type fileEmbed struct {
+	pattern string
+	pos     token.Position
+}
+
 type archiveSrcs struct {
 	goSrcs, cSrcs, cxxSrcs, objcSrcs, objcxxSrcs, sSrcs, hSrcs []fileInfo
 }
@@ -56,7 +70,7 @@
 func filterAndSplitFiles(fileNames []string) (archiveSrcs, error) {
 	var res archiveSrcs
 	for _, s := range fileNames {
-		src, err := readFileInfo(build.Default, s, true)
+		src, err := readFileInfo(build.Default, s)
 		if err != nil {
 			return archiveSrcs{}, err
 		}
@@ -87,7 +101,7 @@
 
 // readFileInfo applies build constraints to an input file and returns whether
 // it should be compiled.
-func readFileInfo(bctx build.Context, input string, needPackage bool) (fileInfo, error) {
+func readFileInfo(bctx build.Context, input string) (fileInfo, error) {
 	fi := fileInfo{filename: input}
 	if ext := filepath.Ext(input); ext == ".C" {
 		fi.ext = cxxExt
@@ -125,53 +139,30 @@
 		}
 		fi.matched = match
 	}
-	// if we don't need the package, and we are cgo, no need to parse the file
-	if !needPackage && bctx.CgoEnabled {
-		return fi, nil
-	}
-	// if it's not a go file, there is no package or cgo
-	if !strings.HasSuffix(input, ".go") {
+	// If it's not a go file, there's nothing more to read.
+	if fi.ext != goExt {
 		return fi, nil
 	}
 
-	// read the file header
-	fset := token.NewFileSet()
-	parsed, err := parser.ParseFile(fset, input, nil, parser.ImportsOnly)
+	// Scan the file for imports and embeds.
+	f, err := os.Open(input)
 	if err != nil {
-		return fi, err
+		return fileInfo{}, err
 	}
-	fi.pkg = parsed.Name.String()
+	defer f.Close()
+	fi.fset = token.NewFileSet()
+	if err := readGoInfo(f, &fi); err != nil {
+		return fileInfo{}, err
+	}
 
-	for _, decl := range parsed.Decls {
-		d, ok := decl.(*ast.GenDecl)
-		if !ok {
-			continue
-		}
-		for _, dspec := range d.Specs {
-			spec, ok := dspec.(*ast.ImportSpec)
-			if !ok {
-				continue
-			}
-			imp, err := strconv.Unquote(spec.Path.Value)
-			if err != nil {
-				log.Panicf("%s: invalid string `%s`", input, spec.Path.Value)
-			}
-			if imp == "C" {
-				fi.isCgo = true
-				break
-			}
+	// Exclude cgo files if cgo is not enabled.
+	for _, imp := range fi.imports {
+		if imp.path == "C" {
+			fi.isCgo = true
+			break
 		}
 	}
-	// matched if cgo is enabled or the file is not cgo
 	fi.matched = fi.matched && (bctx.CgoEnabled || !fi.isCgo)
 
-	for _, i := range parsed.Imports {
-		path, err := strconv.Unquote(i.Path.Value)
-		if err != nil {
-			return fi, err
-		}
-		fi.imports = append(fi.imports, path)
-	}
-
 	return fi, nil
 }
diff --git a/go/tools/builders/importcfg.go b/go/tools/builders/importcfg.go
index 2cfaab1..72dedad 100644
--- a/go/tools/builders/importcfg.go
+++ b/go/tools/builders/importcfg.go
@@ -76,7 +76,8 @@
 	imports := make(map[string]*archive)
 	var derr depsError
 	for _, f := range files {
-		for _, path := range f.imports {
+		for _, imp := range f.imports {
+			path := imp.path
 			if _, ok := imports[path]; ok || path == "C" || isRelative(path) {
 				// TODO(#1645): Support local (relative) import paths. We don't emit
 				// errors for them here, but they will probably break something else.
diff --git a/go/tools/builders/read.go b/go/tools/builders/read.go
new file mode 100644
index 0000000..b03c02b
--- /dev/null
+++ b/go/tools/builders/read.go
@@ -0,0 +1,551 @@
+// Copyright 2012 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This file was adapted from Go src/go/build/read.go at commit 8634a234df2a
+// on 2021-01-26. It's used to extract metadata from .go files without requiring
+// them to be in the same directory.
+
+package main
+
+import (
+	"bufio"
+	"errors"
+	"fmt"
+	"go/ast"
+	"go/parser"
+	"go/token"
+	"io"
+	"strconv"
+	"strings"
+	"unicode"
+	"unicode/utf8"
+)
+
+type importReader struct {
+	b    *bufio.Reader
+	buf  []byte
+	peek byte
+	err  error
+	eof  bool
+	nerr int
+	pos  token.Position
+}
+
+func newImportReader(name string, r io.Reader) *importReader {
+	return &importReader{
+		b: bufio.NewReader(r),
+		pos: token.Position{
+			Filename: name,
+			Line:     1,
+			Column:   1,
+		},
+	}
+}
+
+func isIdent(c byte) bool {
+	return 'A' <= c && c <= 'Z' || 'a' <= c && c <= 'z' || '0' <= c && c <= '9' || c == '_' || c >= utf8.RuneSelf
+}
+
+var (
+	errSyntax = errors.New("syntax error")
+	errNUL    = errors.New("unexpected NUL in input")
+)
+
+// syntaxError records a syntax error, but only if an I/O error has not already been recorded.
+func (r *importReader) syntaxError() {
+	if r.err == nil {
+		r.err = errSyntax
+	}
+}
+
+// readByte reads the next byte from the input, saves it in buf, and returns it.
+// If an error occurs, readByte records the error in r.err and returns 0.
+func (r *importReader) readByte() byte {
+	c, err := r.b.ReadByte()
+	if err == nil {
+		r.buf = append(r.buf, c)
+		if c == 0 {
+			err = errNUL
+		}
+	}
+	if err != nil {
+		if err == io.EOF {
+			r.eof = true
+		} else if r.err == nil {
+			r.err = err
+		}
+		c = 0
+	}
+	return c
+}
+
+// readByteNoBuf is like readByte but doesn't buffer the byte.
+// It exhausts r.buf before reading from r.b.
+func (r *importReader) readByteNoBuf() byte {
+	var c byte
+	var err error
+	if len(r.buf) > 0 {
+		c = r.buf[0]
+		r.buf = r.buf[1:]
+	} else {
+		c, err = r.b.ReadByte()
+		if err == nil && c == 0 {
+			err = errNUL
+		}
+	}
+
+	if err != nil {
+		if err == io.EOF {
+			r.eof = true
+		} else if r.err == nil {
+			r.err = err
+		}
+		return 0
+	}
+	r.pos.Offset++
+	if c == '\n' {
+		r.pos.Line++
+		r.pos.Column = 1
+	} else {
+		r.pos.Column++
+	}
+	return c
+}
+
+// peekByte returns the next byte from the input reader but does not advance beyond it.
+// If skipSpace is set, peekByte skips leading spaces and comments.
+func (r *importReader) peekByte(skipSpace bool) byte {
+	if r.err != nil {
+		if r.nerr++; r.nerr > 10000 {
+			panic("go/build: import reader looping")
+		}
+		return 0
+	}
+
+	// Use r.peek as first input byte.
+	// Don't just return r.peek here: it might have been left by peekByte(false)
+	// and this might be peekByte(true).
+	c := r.peek
+	if c == 0 {
+		c = r.readByte()
+	}
+	for r.err == nil && !r.eof {
+		if skipSpace {
+			// For the purposes of this reader, semicolons are never necessary to
+			// understand the input and are treated as spaces.
+			switch c {
+			case ' ', '\f', '\t', '\r', '\n', ';':
+				c = r.readByte()
+				continue
+
+			case '/':
+				c = r.readByte()
+				if c == '/' {
+					for c != '\n' && r.err == nil && !r.eof {
+						c = r.readByte()
+					}
+				} else if c == '*' {
+					var c1 byte
+					for (c != '*' || c1 != '/') && r.err == nil {
+						if r.eof {
+							r.syntaxError()
+						}
+						c, c1 = c1, r.readByte()
+					}
+				} else {
+					r.syntaxError()
+				}
+				c = r.readByte()
+				continue
+			}
+		}
+		break
+	}
+	r.peek = c
+	return r.peek
+}
+
+// nextByte is like peekByte but advances beyond the returned byte.
+func (r *importReader) nextByte(skipSpace bool) byte {
+	c := r.peekByte(skipSpace)
+	r.peek = 0
+	return c
+}
+
+var goEmbed = []byte("go:embed")
+
+// findEmbed advances the input reader to the next //go:embed comment.
+// It reports whether it found a comment.
+// (Otherwise it found an error or EOF.)
+func (r *importReader) findEmbed(first bool) bool {
+	// The import block scan stopped after a non-space character,
+	// so the reader is not at the start of a line on the first call.
+	// After that, each //go:embed extraction leaves the reader
+	// at the end of a line.
+	startLine := !first
+	var c byte
+	for r.err == nil && !r.eof {
+		c = r.readByteNoBuf()
+	Reswitch:
+		switch c {
+		default:
+			startLine = false
+
+		case '\n':
+			startLine = true
+
+		case ' ', '\t':
+			// leave startLine alone
+
+		case '"':
+			startLine = false
+			for r.err == nil {
+				if r.eof {
+					r.syntaxError()
+				}
+				c = r.readByteNoBuf()
+				if c == '\\' {
+					r.readByteNoBuf()
+					if r.err != nil {
+						r.syntaxError()
+						return false
+					}
+					continue
+				}
+				if c == '"' {
+					c = r.readByteNoBuf()
+					goto Reswitch
+				}
+			}
+			goto Reswitch
+
+		case '`':
+			startLine = false
+			for r.err == nil {
+				if r.eof {
+					r.syntaxError()
+				}
+				c = r.readByteNoBuf()
+				if c == '`' {
+					c = r.readByteNoBuf()
+					goto Reswitch
+				}
+			}
+
+		case '/':
+			c = r.readByteNoBuf()
+			switch c {
+			default:
+				startLine = false
+				goto Reswitch
+
+			case '*':
+				var c1 byte
+				for (c != '*' || c1 != '/') && r.err == nil {
+					if r.eof {
+						r.syntaxError()
+					}
+					c, c1 = c1, r.readByteNoBuf()
+				}
+				startLine = false
+
+			case '/':
+				if startLine {
+					// Try to read this as a //go:embed comment.
+					for i := range goEmbed {
+						c = r.readByteNoBuf()
+						if c != goEmbed[i] {
+							goto SkipSlashSlash
+						}
+					}
+					c = r.readByteNoBuf()
+					if c == ' ' || c == '\t' {
+						// Found one!
+						return true
+					}
+				}
+			SkipSlashSlash:
+				for c != '\n' && r.err == nil && !r.eof {
+					c = r.readByteNoBuf()
+				}
+				startLine = true
+			}
+		}
+	}
+	return false
+}
+
+// readKeyword reads the given keyword from the input.
+// If the keyword is not present, readKeyword records a syntax error.
+func (r *importReader) readKeyword(kw string) {
+	r.peekByte(true)
+	for i := 0; i < len(kw); i++ {
+		if r.nextByte(false) != kw[i] {
+			r.syntaxError()
+			return
+		}
+	}
+	if isIdent(r.peekByte(false)) {
+		r.syntaxError()
+	}
+}
+
+// readIdent reads an identifier from the input.
+// If an identifier is not present, readIdent records a syntax error.
+func (r *importReader) readIdent() {
+	c := r.peekByte(true)
+	if !isIdent(c) {
+		r.syntaxError()
+		return
+	}
+	for isIdent(r.peekByte(false)) {
+		r.peek = 0
+	}
+}
+
+// readString reads a quoted string literal from the input.
+// If an identifier is not present, readString records a syntax error.
+func (r *importReader) readString() {
+	switch r.nextByte(true) {
+	case '`':
+		for r.err == nil {
+			if r.nextByte(false) == '`' {
+				break
+			}
+			if r.eof {
+				r.syntaxError()
+			}
+		}
+	case '"':
+		for r.err == nil {
+			c := r.nextByte(false)
+			if c == '"' {
+				break
+			}
+			if r.eof || c == '\n' {
+				r.syntaxError()
+			}
+			if c == '\\' {
+				r.nextByte(false)
+			}
+		}
+	default:
+		r.syntaxError()
+	}
+}
+
+// readImport reads an import clause - optional identifier followed by quoted string -
+// from the input.
+func (r *importReader) readImport() {
+	c := r.peekByte(true)
+	if c == '.' {
+		r.peek = 0
+	} else if isIdent(c) {
+		r.readIdent()
+	}
+	r.readString()
+}
+
+// readComments is like io.ReadAll, except that it only reads the leading
+// block of comments in the file.
+func readComments(f io.Reader) ([]byte, error) {
+	r := newImportReader("", f)
+	r.peekByte(true)
+	if r.err == nil && !r.eof {
+		// Didn't reach EOF, so must have found a non-space byte. Remove it.
+		r.buf = r.buf[:len(r.buf)-1]
+	}
+	return r.buf, r.err
+}
+
+// readGoInfo expects a Go file as input and reads the file up to and including the import section.
+// It records what it learned in *info.
+// If info.fset is non-nil, readGoInfo parses the file and sets info.parsed, info.parseErr,
+// info.imports, info.embeds, and info.embedErr.
+//
+// It only returns an error if there are problems reading the file,
+// not for syntax errors in the file itself.
+func readGoInfo(f io.Reader, info *fileInfo) error {
+	r := newImportReader(info.filename, f)
+
+	r.readKeyword("package")
+	r.readIdent()
+	for r.peekByte(true) == 'i' {
+		r.readKeyword("import")
+		if r.peekByte(true) == '(' {
+			r.nextByte(false)
+			for r.peekByte(true) != ')' && r.err == nil {
+				r.readImport()
+			}
+			r.nextByte(false)
+		} else {
+			r.readImport()
+		}
+	}
+
+	info.header = r.buf
+
+	// If we stopped successfully before EOF, we read a byte that told us we were done.
+	// Return all but that last byte, which would cause a syntax error if we let it through.
+	if r.err == nil && !r.eof {
+		info.header = r.buf[:len(r.buf)-1]
+	}
+
+	// If we stopped for a syntax error, consume the whole file so that
+	// we are sure we don't change the errors that go/parser returns.
+	if r.err == errSyntax {
+		r.err = nil
+		for r.err == nil && !r.eof {
+			r.readByte()
+		}
+		info.header = r.buf
+	}
+	if r.err != nil {
+		return r.err
+	}
+
+	if info.fset == nil {
+		return nil
+	}
+
+	// Parse file header & record imports.
+	info.parsed, info.parseErr = parser.ParseFile(info.fset, info.filename, info.header, parser.ImportsOnly|parser.ParseComments)
+	if info.parseErr != nil {
+		return nil
+	}
+	info.pkg = info.parsed.Name.Name
+
+	hasEmbed := false
+	for _, decl := range info.parsed.Decls {
+		d, ok := decl.(*ast.GenDecl)
+		if !ok {
+			continue
+		}
+		for _, dspec := range d.Specs {
+			spec, ok := dspec.(*ast.ImportSpec)
+			if !ok {
+				continue
+			}
+			quoted := spec.Path.Value
+			path, err := strconv.Unquote(quoted)
+			if err != nil {
+				return fmt.Errorf("parser returned invalid quoted string: <%s>", quoted)
+			}
+			if path == "embed" {
+				hasEmbed = true
+			}
+
+			doc := spec.Doc
+			if doc == nil && len(d.Specs) == 1 {
+				doc = d.Doc
+			}
+			info.imports = append(info.imports, fileImport{path, spec.Pos(), doc})
+		}
+	}
+
+	// If the file imports "embed",
+	// we have to look for //go:embed comments
+	// in the remainder of the file.
+	// The compiler will enforce the mapping of comments to
+	// declared variables. We just need to know the patterns.
+	// If there were //go:embed comments earlier in the file
+	// (near the package statement or imports), the compiler
+	// will reject them. They can be (and have already been) ignored.
+	if hasEmbed {
+		var line []byte
+		for first := true; r.findEmbed(first); first = false {
+			line = line[:0]
+			pos := r.pos
+			for {
+				c := r.readByteNoBuf()
+				if c == '\n' || r.err != nil || r.eof {
+					break
+				}
+				line = append(line, c)
+			}
+			// Add args if line is well-formed.
+			// Ignore badly-formed lines - the compiler will report them when it finds them,
+			// and we can pretend they are not there to help go list succeed with what it knows.
+			embs, err := parseGoEmbed(string(line), pos)
+			if err == nil {
+				info.embeds = append(info.embeds, embs...)
+			}
+		}
+	}
+
+	return nil
+}
+
+// parseGoEmbed parses the text following "//go:embed" to extract the glob patterns.
+// It accepts unquoted space-separated patterns as well as double-quoted and back-quoted Go strings.
+// This is based on a similar function in cmd/compile/internal/gc/noder.go;
+// this version calculates position information as well.
+func parseGoEmbed(args string, pos token.Position) ([]fileEmbed, error) {
+	trimBytes := func(n int) {
+		pos.Offset += n
+		pos.Column += utf8.RuneCountInString(args[:n])
+		args = args[n:]
+	}
+	trimSpace := func() {
+		trim := strings.TrimLeftFunc(args, unicode.IsSpace)
+		trimBytes(len(args) - len(trim))
+	}
+
+	var list []fileEmbed
+	for trimSpace(); args != ""; trimSpace() {
+		var path string
+		pathPos := pos
+	Switch:
+		switch args[0] {
+		default:
+			i := len(args)
+			for j, c := range args {
+				if unicode.IsSpace(c) {
+					i = j
+					break
+				}
+			}
+			path = args[:i]
+			trimBytes(i)
+
+		case '`':
+			i := strings.Index(args[1:], "`")
+			if i < 0 {
+				return nil, fmt.Errorf("invalid quoted string in //go:embed: %s", args)
+			}
+			path = args[1 : 1+i]
+			trimBytes(1 + i + 1)
+
+		case '"':
+			i := 1
+			for ; i < len(args); i++ {
+				if args[i] == '\\' {
+					i++
+					continue
+				}
+				if args[i] == '"' {
+					q, err := strconv.Unquote(args[:i+1])
+					if err != nil {
+						return nil, fmt.Errorf("invalid quoted string in //go:embed: %s", args[:i+1])
+					}
+					path = q
+					trimBytes(i + 1)
+					break Switch
+				}
+			}
+			if i >= len(args) {
+				return nil, fmt.Errorf("invalid quoted string in //go:embed: %s", args)
+			}
+		}
+
+		if args != "" {
+			r, _ := utf8.DecodeRuneInString(args)
+			if !unicode.IsSpace(r) {
+				return nil, fmt.Errorf("invalid quoted string in //go:embed: %s", args)
+			}
+		}
+		list = append(list, fileEmbed{path, pathPos})
+	}
+	return list, nil
+}
diff --git a/tests/core/go_library/BUILD.bazel b/tests/core/go_library/BUILD.bazel
index 9e0737a..88c27dd 100644
--- a/tests/core/go_library/BUILD.bazel
+++ b/tests/core/go_library/BUILD.bazel
@@ -1,4 +1,6 @@
-load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+load("//go:def.bzl", "go_binary", "go_library", "go_test")
+load("//go/tools/bazel_testing:def.bzl", "go_bazel_test")
+load(":def.bzl", "embedsrcs_files")
 
 go_library(
     name = "empty",
@@ -95,3 +97,46 @@
     importpath = "import_alias/b/v2",
     importpath_aliases = ["import_alias/b"],
 )
+
+go_test(
+    name = "embedsrcs_test",
+    srcs = [
+        "embedsrcs_gen_test.go",
+        "embedsrcs_test.go",
+    ],
+    embedsrcs = [
+        ":embedsrcs_dynamic",
+        "embedsrcs_test.go",
+    ] + glob(["embedsrcs_static/**"]),
+)
+
+genrule(
+    name = "embedsrcs_gen",
+    srcs = ["embedsrcs_gen_test.go.in"],
+    outs = ["embedsrcs_gen_test.go"],
+    cmd = "cp $< $@",
+)
+
+embedsrcs_files(
+    name = "embedsrcs_dynamic",
+    files = [
+        "dir/_no",
+        "dir/f",
+        "empty/",
+        "file",
+        "glob/_hidden",
+        "glob/f",
+        "no",
+    ],
+)
+
+go_binary(
+    name = "gen_embedsrcs_files",
+    srcs = ["gen_embedsrcs_files.go"],
+)
+
+go_bazel_test(
+    name = "embedsrcs_error_test",
+    size = "medium",
+    srcs = ["embedsrcs_error_test.go"],
+)
diff --git a/tests/core/go_library/README.rst b/tests/core/go_library/README.rst
index 415a19e..18c75e2 100644
--- a/tests/core/go_library/README.rst
+++ b/tests/core/go_library/README.rst
@@ -37,3 +37,14 @@
 Checks that a library may import another library using one of the strings
 listed in ``importpath_aliases``. This is the basic mechanism for minimal
 module compatibility. Verifies `#2058`_.
+
+embedsrcs_test
+--------------
+
+Checks that `go_library`_ can match ``//go:embed`` directives to files listed
+in the ``embedsrcs`` attribute and can pass those files to the compiler.
+
+embedsrcs_error_test
+--------------------
+
+Verifies common errors with ``//go:embed`` directives are correctly reported.
diff --git a/tests/core/go_library/def.bzl b/tests/core/go_library/def.bzl
new file mode 100644
index 0000000..613ea84
--- /dev/null
+++ b/tests/core/go_library/def.bzl
@@ -0,0 +1,22 @@
+def _embedsrcs_files_impl(ctx):
+    name = ctx.attr.name
+    dir = ctx.actions.declare_directory(name)
+    args = [dir.path] + ctx.attr.files
+    ctx.actions.run(
+        outputs = [dir],
+        executable = ctx.executable._gen,
+        arguments = args,
+    )
+    return [DefaultInfo(files = depset([dir]))]
+
+embedsrcs_files = rule(
+    implementation = _embedsrcs_files_impl,
+    attrs = {
+        "files": attr.string_list(),
+        "_gen": attr.label(
+            default = ":gen_embedsrcs_files",
+            executable = True,
+            cfg = "exec",
+        ),
+    },
+)
diff --git a/tests/core/go_library/embedsrcs_error_test.go b/tests/core/go_library/embedsrcs_error_test.go
new file mode 100644
index 0000000..61dc95d
--- /dev/null
+++ b/tests/core/go_library/embedsrcs_error_test.go
@@ -0,0 +1,118 @@
+// Copyright 2021 The Bazel Authors. 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 embedsrcs_errors
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/bazelbuild/rules_go/go/tools/bazel_testing"
+)
+
+func TestMain(m *testing.M) {
+	bazel_testing.TestMain(m, bazel_testing.Args{
+		Main: `
+-- BUILD.bazel --
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "invalid",
+    srcs = ["invalid.go"],
+    importpath = "invalid",
+)
+
+go_library(
+    name = "none",
+    srcs = ["none.go"],
+    importpath = "none",
+)
+
+go_library(
+    name = "multi_dir",
+    srcs = [
+        "a.go",
+        "b/b.go",
+    ],
+    embedsrcs = [
+        "a.txt",
+        "b/b.txt",
+    ],
+    importpath = "multi_dir",
+)
+-- invalid.go --
+package invalid
+
+import _ "embed"
+
+//go:embed ..
+var x string
+-- none.go --
+package none
+
+import _ "embed"
+
+//go:embed none
+var x string
+-- a.go --
+package a
+
+import _ "embed"
+
+//go:embed a.txt
+var x string
+-- a.txt --
+-- b/b.go --
+package a
+
+import _ "embed"
+
+//go:embed b.txt
+var y string
+-- b/b.txt --
+`,
+	})
+}
+
+func Test(t *testing.T) {
+	for _, test := range []struct {
+		desc, target, want string
+	}{
+		{
+			desc:   "invalid",
+			target: "//:invalid",
+			want:   "invalid pattern syntax",
+		},
+		{
+			desc:   "none",
+			target: "//:none",
+			want:   "could not embed none: no matching files found",
+		},
+		{
+			desc:   "multi_dir",
+			target: "//:multi_dir",
+			want:   "source files with //go:embed should be in same directory",
+		},
+	} {
+		t.Run(test.desc, func(t *testing.T) {
+			err := bazel_testing.RunBazel("build", test.target)
+			if err == nil {
+				t.Fatalf("expected error matching %q", test.want)
+			}
+			if errMsg := err.Error(); !strings.Contains(errMsg, test.want) {
+				t.Fatalf("expected error matching %q; got %v", test.want, errMsg)
+			}
+		})
+	}
+}
diff --git a/tests/core/go_library/embedsrcs_gen_test.go.in b/tests/core/go_library/embedsrcs_gen_test.go.in
new file mode 100644
index 0000000..a845b48
--- /dev/null
+++ b/tests/core/go_library/embedsrcs_gen_test.go.in
@@ -0,0 +1,20 @@
+// Copyright 2021 The Bazel Authors. 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 embedsrcs
+
+import "embed"
+
+//go:embed embedsrcs_test.go
+var gen embed.FS
diff --git a/tests/core/go_library/embedsrcs_static/dir/_no b/tests/core/go_library/embedsrcs_static/dir/_no
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/core/go_library/embedsrcs_static/dir/_no
diff --git a/tests/core/go_library/embedsrcs_static/dir/f b/tests/core/go_library/embedsrcs_static/dir/f
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/core/go_library/embedsrcs_static/dir/f
diff --git a/tests/core/go_library/embedsrcs_static/file b/tests/core/go_library/embedsrcs_static/file
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/core/go_library/embedsrcs_static/file
diff --git a/tests/core/go_library/embedsrcs_static/glob/_hidden b/tests/core/go_library/embedsrcs_static/glob/_hidden
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/core/go_library/embedsrcs_static/glob/_hidden
diff --git a/tests/core/go_library/embedsrcs_static/glob/f b/tests/core/go_library/embedsrcs_static/glob/f
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/core/go_library/embedsrcs_static/glob/f
diff --git a/tests/core/go_library/embedsrcs_static/no b/tests/core/go_library/embedsrcs_static/no
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/core/go_library/embedsrcs_static/no
diff --git a/tests/core/go_library/embedsrcs_test.go b/tests/core/go_library/embedsrcs_test.go
new file mode 100644
index 0000000..fd0f52e
--- /dev/null
+++ b/tests/core/go_library/embedsrcs_test.go
@@ -0,0 +1,148 @@
+// Copyright 2021 The Bazel Authors. 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 embedsrcs
+
+import (
+	"bytes"
+	"embed"
+	"io/fs"
+	"strings"
+	"testing"
+)
+
+//go:embed embedsrcs_test.go
+var self embed.FS
+
+//go:embed embedsrcs_static/file embedsrcs_static/dir embedsrcs_static/glob/*
+var static embed.FS
+
+//go:embed embedsrcs_dynamic/file embedsrcs_dynamic/dir embedsrcs_dynamic/glob/*
+var dynamic embed.FS
+
+//go:embed *
+var star embed.FS
+
+func TestFiles(t *testing.T) {
+	for _, test := range []struct {
+		desc string
+		fsys fs.FS
+		want []string
+	}{
+		{
+			desc: "self",
+			fsys: self,
+			want: []string{
+				".",
+				"embedsrcs_test.go",
+			},
+		},
+		{
+			desc: "gen",
+			fsys: gen,
+			want: []string{
+				".",
+				"embedsrcs_test.go",
+			},
+		},
+		{
+			desc: "static",
+			fsys: static,
+			want: []string{
+				".",
+				"embedsrcs_static",
+				"embedsrcs_static/dir",
+				"embedsrcs_static/dir/f",
+				"embedsrcs_static/file",
+				"embedsrcs_static/glob",
+				"embedsrcs_static/glob/_hidden",
+				"embedsrcs_static/glob/f",
+			},
+		},
+		{
+			desc: "dynamic",
+			fsys: dynamic,
+			want: []string{
+				".",
+				"embedsrcs_dynamic",
+				"embedsrcs_dynamic/dir",
+				"embedsrcs_dynamic/dir/f",
+				"embedsrcs_dynamic/file",
+				"embedsrcs_dynamic/glob",
+				"embedsrcs_dynamic/glob/_hidden",
+				"embedsrcs_dynamic/glob/f",
+			},
+		},
+		{
+			desc: "star",
+			fsys: star,
+			want: []string{
+				".",
+				"embedsrcs_dynamic",
+				"embedsrcs_dynamic/dir",
+				"embedsrcs_dynamic/dir/f",
+				"embedsrcs_dynamic/empty",
+				"embedsrcs_dynamic/file",
+				"embedsrcs_dynamic/glob",
+				"embedsrcs_dynamic/glob/f",
+				"embedsrcs_dynamic/no",
+				"embedsrcs_static",
+				"embedsrcs_static/dir",
+				"embedsrcs_static/dir/f",
+				"embedsrcs_static/file",
+				"embedsrcs_static/glob",
+				"embedsrcs_static/glob/f",
+				"embedsrcs_static/no",
+				"embedsrcs_test.go",
+			},
+		},
+	} {
+		t.Run(test.desc, func(t *testing.T) {
+			got, err := listFiles(test.fsys)
+			if err != nil {
+				t.Fatal(err)
+			}
+			gotStr := strings.Join(got, "\n")
+			wantStr := strings.Join(test.want, "\n")
+			if gotStr != wantStr {
+				t.Errorf("got:\n%s\nwant:\n%s", gotStr, wantStr)
+			}
+		})
+	}
+}
+
+func listFiles(fsys fs.FS) ([]string, error) {
+	var files []string
+	err := fs.WalkDir(fsys, ".", func(path string, _ fs.DirEntry, err error) error {
+		if err != nil {
+			return err
+		}
+		files = append(files, path)
+		return nil
+	})
+	if err != nil {
+		return nil, err
+	}
+	return files, nil
+}
+
+func TestContent(t *testing.T) {
+	data, err := fs.ReadFile(self, "embedsrcs_test.go")
+	if err != nil {
+		t.Fatal(err)
+	}
+	if !bytes.Contains(data, []byte("package embedsrcs")) {
+		t.Error("embedded content did not contain package declaration")
+	}
+}
diff --git a/tests/core/go_library/gen_embedsrcs_files.go b/tests/core/go_library/gen_embedsrcs_files.go
new file mode 100644
index 0000000..68ff2fe
--- /dev/null
+++ b/tests/core/go_library/gen_embedsrcs_files.go
@@ -0,0 +1,51 @@
+// Copyright 2021 The Bazel Authors. 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 (
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"strings"
+)
+
+func main() {
+	dir := os.Args[1]
+	files := os.Args[2:]
+	if err := run(dir, files); err != nil {
+		fmt.Fprintf(os.Stderr, "%v\n", err)
+		os.Exit(1)
+	}
+}
+
+func run(dir string, files []string) error {
+	for _, file := range files {
+		path := filepath.Join(dir, file)
+		if strings.HasSuffix(path, "/") {
+			if err := os.MkdirAll(path, 0777); err != nil {
+				return err
+			}
+		} else {
+			if err := os.MkdirAll(filepath.Dir(path), 0777); err != nil {
+				return err
+			}
+			if err := ioutil.WriteFile(path, nil, 0666); err != nil {
+				return err
+			}
+		}
+	}
+	return nil
+}