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 }