github.com/aca02djr/gb@v0.4.1/importer/importer.go (about) 1 package importer 2 3 import ( 4 "bytes" 5 "fmt" 6 "io" 7 "os" 8 pathpkg "path" 9 "path/filepath" 10 "runtime" 11 "strings" 12 "unicode" 13 ) 14 15 type Context struct { 16 GOOS string // target architecture 17 GOARCH string // target operating system 18 CgoEnabled bool // whether cgo can be used 19 20 // The build and release tags specify build constraints 21 // that should be considered satisfied when processing +build lines. 22 // Clients creating a new context may customize BuildTags, which 23 // defaults to empty, but it is usually an error to customize ReleaseTags, 24 // which defaults to the list of Go releases the current release is compatible with. 25 // In addition to the BuildTags and ReleaseTags, build constraints 26 // consider the values of GOARCH and GOOS as satisfied tags. 27 BuildTags []string 28 ReleaseTags []string 29 } 30 31 type Importer struct { 32 *Context 33 Root string // root directory 34 } 35 36 func (i *Importer) Import(path string) (*Package, error) { 37 if path == "" { 38 return nil, fmt.Errorf("import %q: invalid import path", path) 39 } 40 41 if path == "." || path == ".." || strings.HasPrefix(path, "./") || strings.HasPrefix(path, "../") { 42 return nil, fmt.Errorf("import %q: relative import not supported", path) 43 } 44 45 if strings.HasPrefix(path, "/") { 46 return nil, fmt.Errorf("import %q: cannot import absolute path", path) 47 } 48 49 p := &Package{ 50 importer: i, 51 ImportPath: path, 52 Standard: i.Root == runtime.GOROOT(), 53 } 54 // if this is the stdlib, then search vendor first. 55 // this isn't real vendor support, just enough to make net/http compile. 56 if p.Standard { 57 path := pathpkg.Join("vendor", path) 58 dir := filepath.Join(i.Root, "src", filepath.FromSlash(path)) 59 fi, err := os.Stat(dir) 60 if err == nil && fi.IsDir() { 61 p.Dir = dir 62 p.Root = i.Root 63 p.ImportPath = path 64 p.SrcRoot = filepath.Join(p.Root, "src") 65 err = loadPackage(p) 66 return p, err 67 } 68 } 69 70 dir := filepath.Join(i.Root, "src", filepath.FromSlash(path)) 71 fi, err := os.Stat(dir) 72 if err == nil { 73 if fi.IsDir() { 74 p.Dir = dir 75 p.Root = i.Root 76 p.SrcRoot = filepath.Join(p.Root, "src") 77 err = loadPackage(p) 78 return p, err 79 } 80 err = fmt.Errorf("import %q: not a directory", path) 81 } 82 return nil, err 83 } 84 85 // matchFile determines whether the file with the given name in the given directory 86 // should be included in the package being constructed. 87 // It returns the data read from the file. 88 // If allTags is non-nil, matchFile records any encountered build tag 89 // by setting allTags[tag] = true. 90 func (i *Importer) matchFile(path string, allTags map[string]bool) (match bool, data []byte, err error) { 91 name := filepath.Base(path) 92 if name[0] == '_' || name[0] == '.' { 93 return 94 } 95 96 if !goodOSArchFile(i.GOOS, i.GOARCH, name, allTags) { 97 return 98 } 99 100 read := func(path string, fn func(r io.Reader) ([]byte, error)) ([]byte, error) { 101 f, err := os.Open(path) 102 if err != nil { 103 return nil, err 104 } 105 defer f.Close() 106 data, err := fn(f) 107 if err != nil { 108 err = fmt.Errorf("read %s: %v", path, err) 109 } 110 return data, err 111 } 112 113 switch filepath.Ext(name) { 114 case ".go": 115 data, err = read(path, readImports) 116 if err != nil { 117 return 118 } 119 // Look for +build comments to accept or reject the file. 120 if !i.shouldBuild(data, allTags) { 121 return 122 } 123 124 match = true 125 return 126 127 case ".c", ".cc", ".cxx", ".cpp", ".m", ".s", ".h", ".hh", ".hpp", ".hxx", ".S", ".swig", ".swigcxx": 128 // tentatively okay - read to make sure 129 data, err = read(path, readComments) 130 if err != nil { 131 return 132 } 133 // Look for +build comments to accept or reject the file. 134 if !i.shouldBuild(data, allTags) { 135 return 136 } 137 138 match = true 139 return 140 141 case ".syso": 142 // binary, no reading 143 match = true 144 return 145 default: 146 // skip 147 return 148 } 149 } 150 151 // shouldBuild reports whether it is okay to use this file, 152 // The rule is that in the file's leading run of // comments 153 // and blank lines, which must be followed by a blank line 154 // (to avoid including a Go package clause doc comment), 155 // lines beginning with '// +build' are taken as build directives. 156 // 157 // The file is accepted only if each such line lists something 158 // matching the file. For example: 159 // 160 // // +build windows linux 161 // 162 // marks the file as applicable only on Windows and Linux. 163 // 164 func (i *Importer) shouldBuild(content []byte, allTags map[string]bool) bool { 165 // Pass 1. Identify leading run of // comments and blank lines, 166 // which must be followed by a blank line. 167 end := 0 168 p := content 169 for len(p) > 0 { 170 line := p 171 if i := bytes.IndexByte(line, '\n'); i >= 0 { 172 line, p = line[:i], p[i+1:] 173 } else { 174 p = p[len(p):] 175 } 176 line = bytes.TrimSpace(line) 177 if len(line) == 0 { // Blank line 178 end = len(content) - len(p) 179 continue 180 } 181 if !bytes.HasPrefix(line, []byte{'/', '/'}) { // Not comment line 182 break 183 } 184 } 185 content = content[:end] 186 187 // Pass 2. Process each line in the run. 188 p = content 189 allok := true 190 for len(p) > 0 { 191 line := p 192 if i := bytes.IndexByte(line, '\n'); i >= 0 { 193 line, p = line[:i], p[i+1:] 194 } else { 195 p = p[len(p):] 196 } 197 line = bytes.TrimSpace(line) 198 if bytes.HasPrefix(line, []byte{'/', '/'}) { 199 line = bytes.TrimSpace(line[2:]) 200 if len(line) > 0 && line[0] == '+' { 201 // Looks like a comment +line. 202 f := strings.Fields(string(line)) 203 if f[0] == "+build" { 204 ok := false 205 for _, tok := range f[1:] { 206 if i.match(tok, allTags) { 207 ok = true 208 } 209 } 210 if !ok { 211 allok = false 212 } 213 } 214 } 215 } 216 } 217 218 return allok 219 } 220 221 // match reports whether the name is one of: 222 // 223 // $GOOS 224 // $GOARCH 225 // cgo (if cgo is enabled) 226 // !cgo (if cgo is disabled) 227 // ctxt.Compiler 228 // !ctxt.Compiler 229 // tag (if tag is listed in ctxt.BuildTags or ctxt.ReleaseTags) 230 // !tag (if tag is not listed in ctxt.BuildTags or ctxt.ReleaseTags) 231 // a comma-separated list of any of these 232 // 233 func (i *Importer) match(name string, allTags map[string]bool) bool { 234 if name == "" { 235 if allTags != nil { 236 allTags[name] = true 237 } 238 return false 239 } 240 if n := strings.Index(name, ","); n >= 0 { 241 // comma-separated list 242 ok1 := i.match(name[:n], allTags) 243 ok2 := i.match(name[n+1:], allTags) 244 return ok1 && ok2 245 } 246 if strings.HasPrefix(name, "!!") { // bad syntax, reject always 247 return false 248 } 249 if strings.HasPrefix(name, "!") { // negation 250 return len(name) > 1 && !i.match(name[1:], allTags) 251 } 252 253 if allTags != nil { 254 allTags[name] = true 255 } 256 257 // Tags must be letters, digits, underscores or dots. 258 // Unlike in Go identifiers, all digits are fine (e.g., "386"). 259 for _, c := range name { 260 if !unicode.IsLetter(c) && !unicode.IsDigit(c) && c != '_' && c != '.' { 261 return false 262 } 263 } 264 265 // special tags 266 if i.CgoEnabled && name == "cgo" { 267 return true 268 } 269 if name == i.GOOS || name == i.GOARCH || name == runtime.Compiler { 270 return true 271 } 272 if i.GOOS == "android" && name == "linux" { 273 return true 274 } 275 276 // other tags 277 for _, tag := range i.BuildTags { 278 if tag == name { 279 return true 280 } 281 } 282 for _, tag := range i.ReleaseTags { 283 if tag == name { 284 return true 285 } 286 } 287 288 return false 289 }