blob: 2de4f3b911acc5fda1b22fe7385aed3cfcd161d4 [file] [log] [blame]
// 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, 0o666); 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)
}
}()
// Remove optional "all:" prefix from pattern and set matchAll flag if present.
// See https://pkg.go.dev/embed#hdr-Directives for details.
pattern := embed.pattern
var matchAll bool
if strings.HasPrefix(pattern, "all:") {
matchAll = true
pattern = pattern[4:]
}
// Check that the pattern has valid syntax.
if _, err := path.Match(pattern, ""); err != nil || !validEmbedPattern(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(pattern, matchRel); !ok {
// Non-matching file or directory.
return nil
}
// TODO: Should check that directories along path do not begin a new module
// (do not contain a go.mod).
// https://cs.opensource.google/go/go/+/master:src/cmd/go/internal/load/pkg.go;l=2158;drc=261fe25c83a94fc3defe064baed3944cd3d16959
for dir := matchRel; len(dir) > 1; dir = filepath.Dir(dir) {
if base := path.Base(matchRel); isBadEmbedName(base) {
what := "file"
if matchNode.isDir() {
what = "directory"
}
if dir == matchRel {
return fmt.Errorf("cannot embed %s %s: invalid name %s", what, matchRel, base)
} else {
return fmt.Errorf("cannot embed %s %s: in invalid directory %s", what, matchRel, base)
}
}
}
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 "_"),
// unless "all:" prefix was set.
// See golang/go#42328.
matchTreeErr := matchNode.walk(func(childRel string, childNode *embedNode) error {
// TODO: Should check that directories along path do not begin a new module
// https://cs.opensource.google/go/go/+/master:src/cmd/go/internal/load/pkg.go;l=2158;drc=261fe25c83a94fc3defe064baed3944cd3d16959
if childRel != "" {
base := path.Base(childRel)
if isBadEmbedName(base) || (!matchAll && (strings.HasPrefix(base, ".") || strings.HasPrefix(base, "_"))) {
if childNode.isDir() {
return errSkip
}
return nil
}
}
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:]
}
}
// isBadEmbedName reports whether name is the base name of a file that
// can't or won't be included in modules and therefore shouldn't be treated
// as existing for embedding.
//
// TODO: This should use the equivalent of golang.org/x/mod/module.CheckFilePath instead of fsValidPath.
// https://cs.opensource.google/go/go/+/master:src/cmd/go/internal/load/pkg.go;l=2200;drc=261fe25c83a94fc3defe064baed3944cd3d16959
func isBadEmbedName(name string) bool {
if !fsValidPath(name) {
return true
}
switch name {
// Empty string should be impossible but make it bad.
case "":
return true
// Version control directories won't be present in module.
case ".bzr", ".hg", ".git", ".svn":
return true
}
return false
}