github.com/visualfc/goembed@v0.3.3/resolve/resolve.go (about)

     1  // Copyright 2020 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package resolve
     6  
     7  import (
     8  	"encoding/json"
     9  	"fmt"
    10  	"os"
    11  	"path"
    12  	"path/filepath"
    13  	"sort"
    14  
    15  	"github.com/visualfc/goembed/fs"
    16  	"github.com/visualfc/goembed/fsys"
    17  )
    18  
    19  // An EmbedError indicates a problem with a go:embed directive.
    20  type EmbedError struct {
    21  	Pattern string
    22  	Err     error
    23  }
    24  
    25  func (e *EmbedError) Error() string {
    26  	return fmt.Sprintf("pattern %s: %v", e.Pattern, e.Err)
    27  }
    28  
    29  func (e *EmbedError) Unwrap() error {
    30  	return e.Err
    31  }
    32  
    33  // ResolveEmbed resolves //go:embed patterns and returns only the file list.
    34  // For use by go mod vendor to find embedded files it should copy into the
    35  // vendor directory.
    36  // TODO(#42504): Once go mod vendor uses load.PackagesAndErrors, just
    37  // call (*Package).ResolveEmbed
    38  func ResolveEmbed(dir string, patterns []string) ([]string, error) {
    39  	files, _, err := resolveEmbed(dir, patterns)
    40  	return files, err
    41  }
    42  
    43  // resolveEmbed resolves //go:embed patterns to precise file lists.
    44  // It sets files to the list of unique files matched (for go list),
    45  // and it sets pmap to the more precise mapping from
    46  // patterns to files.
    47  func resolveEmbed(pkgdir string, patterns []string) (files []string, pmap map[string][]string, err error) {
    48  	var pattern string
    49  	defer func() {
    50  		if err != nil {
    51  			err = &EmbedError{
    52  				Pattern: pattern,
    53  				Err:     err,
    54  			}
    55  		}
    56  	}()
    57  
    58  	// TODO(rsc): All these messages need position information for better error reports.
    59  	pmap = make(map[string][]string)
    60  	have := make(map[string]int)
    61  	dirOK := make(map[string]bool)
    62  	pid := 0 // pattern ID, to allow reuse of have map
    63  	for _, pattern = range patterns {
    64  		pid++
    65  
    66  		// Check pattern is valid for //go:embed.
    67  		if _, err := path.Match(pattern, ""); err != nil || !validEmbedPattern(pattern) {
    68  			return nil, nil, fmt.Errorf("invalid pattern syntax")
    69  		}
    70  
    71  		// Glob to find matches.
    72  		match, err := fsys.Glob(pkgdir + string(filepath.Separator) + filepath.FromSlash(pattern))
    73  		if err != nil {
    74  			return nil, nil, err
    75  		}
    76  
    77  		// Filter list of matches down to the ones that will still exist when
    78  		// the directory is packaged up as a module. (If p.Dir is in the module cache,
    79  		// only those files exist already, but if p.Dir is in the current module,
    80  		// then there may be other things lying around, like symbolic links or .git directories.)
    81  		var list []string
    82  		for _, file := range match {
    83  			rel := filepath.ToSlash(file[len(pkgdir)+1:]) // file, relative to p.Dir
    84  
    85  			what := "file"
    86  			info, err := fsys.Lstat(file)
    87  			if err != nil {
    88  				return nil, nil, err
    89  			}
    90  			if info.IsDir() {
    91  				what = "directory"
    92  			}
    93  
    94  			// Check that directories along path do not begin a new module
    95  			// (do not contain a go.mod).
    96  			for dir := file; len(dir) > len(pkgdir)+1 && !dirOK[dir]; dir = filepath.Dir(dir) {
    97  				if _, err := fsys.Stat(filepath.Join(dir, "go.mod")); err == nil {
    98  					return nil, nil, fmt.Errorf("cannot embed %s %s: in different module", what, rel)
    99  				}
   100  				if dir != file {
   101  					if info, err := fsys.Lstat(dir); err == nil && !info.IsDir() {
   102  						return nil, nil, fmt.Errorf("cannot embed %s %s: in non-directory %s", what, rel, dir[len(pkgdir)+1:])
   103  					}
   104  				}
   105  				dirOK[dir] = true
   106  				if elem := filepath.Base(dir); isBadEmbedName(elem) {
   107  					if dir == file {
   108  						return nil, nil, fmt.Errorf("cannot embed %s %s: invalid name %s", what, rel, elem)
   109  					} else {
   110  						return nil, nil, fmt.Errorf("cannot embed %s %s: in invalid directory %s", what, rel, elem)
   111  					}
   112  				}
   113  			}
   114  
   115  			switch {
   116  			default:
   117  				return nil, nil, fmt.Errorf("cannot embed irregular file %s", rel)
   118  
   119  			case info.Mode().IsRegular():
   120  				if have[rel] != pid {
   121  					have[rel] = pid
   122  					list = append(list, rel)
   123  				}
   124  
   125  			case info.IsDir():
   126  				// Gather all files in the named directory, stopping at module boundaries
   127  				// and ignoring files that wouldn't be packaged into a module.
   128  				count := 0
   129  				err := fsys.Walk(file, func(path string, info os.FileInfo, err error) error {
   130  					if err != nil {
   131  						return err
   132  					}
   133  					rel := filepath.ToSlash(path[len(pkgdir)+1:])
   134  					name := info.Name()
   135  					if path != file && (isBadEmbedName(name) || name[0] == '.' || name[0] == '_') {
   136  						// Ignore bad names, assuming they won't go into modules.
   137  						// Also avoid hidden files that user may not know about.
   138  						// See golang.org/issue/42328.
   139  						if info.IsDir() {
   140  							return fs.SkipDir
   141  						}
   142  						return nil
   143  					}
   144  					if info.IsDir() {
   145  						if _, err := fsys.Stat(filepath.Join(path, "go.mod")); err == nil {
   146  							return filepath.SkipDir
   147  						}
   148  						return nil
   149  					}
   150  					if !info.Mode().IsRegular() {
   151  						return nil
   152  					}
   153  					count++
   154  					if have[rel] != pid {
   155  						have[rel] = pid
   156  						list = append(list, rel)
   157  					}
   158  					return nil
   159  				})
   160  				if err != nil {
   161  					return nil, nil, err
   162  				}
   163  				if count == 0 {
   164  					return nil, nil, fmt.Errorf("cannot embed directory %s: contains no embeddable files", rel)
   165  				}
   166  			}
   167  		}
   168  
   169  		if len(list) == 0 {
   170  			return nil, nil, fmt.Errorf("no matching files found")
   171  		}
   172  		sort.Strings(list)
   173  		pmap[pattern] = list
   174  	}
   175  
   176  	for file := range have {
   177  		files = append(files, file)
   178  	}
   179  	sort.Strings(files)
   180  	return files, pmap, nil
   181  }
   182  
   183  func validEmbedPattern(pattern string) bool {
   184  	return pattern != "." && fs.ValidPath(pattern)
   185  }
   186  
   187  // isBadEmbedName reports whether name is the base name of a file that
   188  // can't or won't be included in modules and therefore shouldn't be treated
   189  // as existing for embedding.
   190  func isBadEmbedName(name string) bool {
   191  	switch name {
   192  	// Empty string should be impossible but make it bad.
   193  	case "":
   194  		return true
   195  	// Version control directories won't be present in module.
   196  	case ".bzr", ".hg", ".git", ".svn":
   197  		return true
   198  	}
   199  	return false
   200  }
   201  
   202  func ToEmbedCfg(dir string, files []string, pmap map[string][]string) ([]byte, error) {
   203  	var embedcfg []byte
   204  	if len(pmap) > 0 {
   205  		var embed struct {
   206  			Patterns map[string][]string
   207  			Files    map[string]string
   208  		}
   209  		embed.Patterns = pmap
   210  		embed.Files = make(map[string]string)
   211  		for _, file := range files {
   212  			embed.Files[file] = filepath.Join(dir, file)
   213  		}
   214  		js, err := json.MarshalIndent(&embed, "", "\t")
   215  		if err != nil {
   216  			return nil, fmt.Errorf("marshal embedcfg: %v", err)
   217  		}
   218  		embedcfg = js
   219  	}
   220  	return embedcfg, nil
   221  }