github.com/gernest/nezuko@v0.1.2/internal/build/build.go (about) 1 package build 2 3 import ( 4 "errors" 5 "fmt" 6 "io" 7 "io/ioutil" 8 "os" 9 pathpkg "path" 10 "path/filepath" 11 "runtime" 12 "strings" 13 ) 14 15 // An ImportMode controls the behavior of the Import method. 16 type ImportMode uint 17 18 const ( 19 // If FindOnly is set, Import stops after locating the directory 20 // that should contain the sources for a package. It does not 21 // read any files in the directory. 22 FindOnly ImportMode = 1 << iota 23 24 // If AllowBinary is set, Import can be satisfied by a compiled 25 // package object without corresponding sources. 26 // 27 // Deprecated: 28 // The supported way to create a compiled-only package is to 29 // write source code containing a //go:binary-only-package comment at 30 // the top of the file. Such a package will be recognized 31 // regardless of this flag setting (because it has source code) 32 // and will have BinaryOnly set to true in the returned Package. 33 AllowBinary 34 35 // If ImportComment is set, parse import comments on package statements. 36 // Import returns an error if it finds a comment it cannot understand 37 // or finds conflicting comments in multiple source files. 38 // See golang.org/s/go14customimport for more information. 39 ImportComment 40 41 // By default, Import searches vendor directories 42 // that apply in the given source directory before searching 43 // the GOROOT and GOPATH roots. 44 // If an Import finds and returns a package using a vendor 45 // directory, the resulting ImportPath is the complete path 46 // to the package, including the path elements leading up 47 // to and including "vendor". 48 // For example, if Import("y", "x/subdir", 0) finds 49 // "x/vendor/y", the returned package's ImportPath is "x/vendor/y", 50 // not plain "y". 51 // See golang.org/s/go15vendor for more information. 52 // 53 // Setting IgnoreVendor ignores vendor directories. 54 // 55 // In contrast to the package's ImportPath, 56 // the returned package's Imports, TestImports, and XTestImports 57 // are always the exact import paths from the source files: 58 // Import makes no attempt to resolve or check those paths. 59 IgnoreVendor 60 ) 61 62 var Default Context = defaultContext() 63 64 type Context struct { 65 ZIGPATH string // Go path 66 67 JoinPath func(elem ...string) string 68 69 // SplitPathList splits the path list into a slice of individual paths. 70 // If SplitPathList is nil, Import uses filepath.SplitList. 71 SplitPathList func(list string) []string 72 73 // IsAbsPath reports whether path is an absolute path. 74 // If IsAbsPath is nil, Import uses filepath.IsAbs. 75 IsAbsPath func(path string) bool 76 77 // IsDir reports whether the path names a directory. 78 // If IsDir is nil, Import calls os.Stat and uses the result's IsDir method. 79 IsDir func(path string) bool 80 81 // HasSubdir reports whether dir is lexically a subdirectory of 82 // root, perhaps multiple levels below. It does not try to check 83 // whether dir exists. 84 // If so, HasSubdir sets rel to a slash-separated path that 85 // can be joined to root to produce a path equivalent to dir. 86 // If HasSubdir is nil, Import uses an implementation built on 87 // filepath.EvalSymlinks. 88 HasSubdir func(root, dir string) (rel string, ok bool) 89 90 // ReadDir returns a slice of os.FileInfo, sorted by Name, 91 // describing the content of the named directory. 92 // If ReadDir is nil, Import uses ioutil.ReadDir. 93 ReadDir func(dir string) ([]os.FileInfo, error) 94 95 // OpenFile opens a file (not a directory) for reading. 96 // If OpenFile is nil, Import uses os.Open. 97 OpenFile func(path string) (io.ReadCloser, error) 98 } 99 100 func envOr(name, def string) string { 101 s := os.Getenv(name) 102 if s == "" { 103 return def 104 } 105 return s 106 } 107 108 func defaultGOPATH() string { 109 env := "HOME" 110 if runtime.GOOS == "windows" { 111 env = "USERPROFILE" 112 } else if runtime.GOOS == "plan9" { 113 env = "home" 114 } 115 if home := os.Getenv(env); home != "" { 116 def := filepath.Join(home, "zig") 117 return def 118 } 119 return "" 120 } 121 122 func defaultContext() Context { 123 var c Context 124 c.ZIGPATH = envOr("ZIGPATH", defaultGOPATH()) 125 return c 126 } 127 128 func (ctxt *Context) ImportDir(dir string, mode ImportMode) (*Package, error) { 129 return ctxt.Import(".", dir, mode) 130 } 131 132 func (ctxt *Context) isAbsPath(path string) bool { 133 if f := ctxt.IsAbsPath; f != nil { 134 return f(path) 135 } 136 return filepath.IsAbs(path) 137 } 138 139 // joinPath calls ctxt.JoinPath (if not nil) or else filepath.Join. 140 func (ctxt *Context) joinPath(elem ...string) string { 141 if f := ctxt.JoinPath; f != nil { 142 return f(elem...) 143 } 144 return filepath.Join(elem...) 145 } 146 147 // splitPathList calls ctxt.SplitPathList (if not nil) or else filepath.SplitList. 148 func (ctxt *Context) splitPathList(s string) []string { 149 if f := ctxt.SplitPathList; f != nil { 150 return f(s) 151 } 152 return filepath.SplitList(s) 153 } 154 155 func (ctxt *Context) gopath() []string { 156 var all []string 157 for _, p := range ctxt.splitPathList(ctxt.ZIGPATH) { 158 if p == "" { 159 // Empty paths are uninteresting. 160 // If the path is the GOROOT, ignore it. 161 // People sometimes set GOPATH=$GOROOT. 162 // Do not get confused by this common mistake. 163 continue 164 } 165 if strings.HasPrefix(p, "~") { 166 // Path segments starting with ~ on Unix are almost always 167 // users who have incorrectly quoted ~ while setting GOPATH, 168 // preventing it from expanding to $HOME. 169 // The situation is made more confusing by the fact that 170 // bash allows quoted ~ in $PATH (most shells do not). 171 // Do not get confused by this, and do not try to use the path. 172 // It does not exist, and printing errors about it confuses 173 // those users even more, because they think "sure ~ exists!". 174 // The go command diagnoses this situation and prints a 175 // useful error. 176 // On Windows, ~ is used in short names, such as c:\progra~1 177 // for c:\program files. 178 continue 179 } 180 all = append(all, p) 181 } 182 return all 183 } 184 185 // hasSubdir calls ctxt.HasSubdir (if not nil) or else uses 186 // the local file system to answer the question. 187 func (ctxt *Context) hasSubdir(root, dir string) (rel string, ok bool) { 188 if f := ctxt.HasSubdir; f != nil { 189 return f(root, dir) 190 } 191 192 // Try using paths we received. 193 if rel, ok = hasSubdir(root, dir); ok { 194 return 195 } 196 197 // Try expanding symlinks and comparing 198 // expanded against unexpanded and 199 // expanded against expanded. 200 rootSym, _ := filepath.EvalSymlinks(root) 201 dirSym, _ := filepath.EvalSymlinks(dir) 202 203 if rel, ok = hasSubdir(rootSym, dir); ok { 204 return 205 } 206 if rel, ok = hasSubdir(root, dirSym); ok { 207 return 208 } 209 return hasSubdir(rootSym, dirSym) 210 } 211 212 // hasSubdir reports if dir is within root by performing lexical analysis only. 213 func hasSubdir(root, dir string) (rel string, ok bool) { 214 const sep = string(filepath.Separator) 215 root = filepath.Clean(root) 216 if !strings.HasSuffix(root, sep) { 217 root += sep 218 } 219 dir = filepath.Clean(dir) 220 if !strings.HasPrefix(dir, root) { 221 return "", false 222 } 223 return filepath.ToSlash(dir[len(root):]), true 224 } 225 226 // isDir calls ctxt.IsDir (if not nil) or else uses os.Stat. 227 func (ctxt *Context) isDir(path string) bool { 228 if f := ctxt.IsDir; f != nil { 229 return f(path) 230 } 231 fi, err := os.Stat(path) 232 return err == nil && fi.IsDir() 233 } 234 235 var errNoModules = errors.New("not using modules") 236 237 func (ctxt *Context) importGo(p *Package, path, srcDir string, mode ImportMode, gopath []string) error { 238 const debugImportGo = false 239 240 // To invoke the go command, we must know the source directory, 241 // we must not being doing special things like AllowBinary or IgnoreVendor, 242 // and all the file system callbacks must be nil (we're meant to use the local file system). 243 if srcDir == "" || 244 ctxt.JoinPath != nil || ctxt.SplitPathList != nil || ctxt.IsAbsPath != nil || ctxt.IsDir != nil || ctxt.HasSubdir != nil || ctxt.ReadDir != nil || ctxt.OpenFile != nil { 245 return errNoModules 246 } 247 248 // Look to see if there is a go.mod. 249 abs, err := filepath.Abs(srcDir) 250 if err != nil { 251 return errNoModules 252 } 253 for { 254 info, err := os.Stat(filepath.Join(abs, "go.mod")) 255 if err == nil && !info.IsDir() { 256 break 257 } 258 d := filepath.Dir(abs) 259 if len(d) >= len(abs) { 260 return errNoModules // reached top of file system, no go.mod 261 } 262 abs = d 263 } 264 265 return nil 266 } 267 268 // readDir calls ctxt.ReadDir (if not nil) or else ioutil.ReadDir. 269 func (ctxt *Context) readDir(path string) ([]os.FileInfo, error) { 270 if f := ctxt.ReadDir; f != nil { 271 return f(path) 272 } 273 return ioutil.ReadDir(path) 274 } 275 276 // hasGoFiles reports whether dir contains any files with names ending in .go. 277 // For a vendor check we must exclude directories that contain no .go files. 278 // Otherwise it is not possible to vendor just a/b/c and still import the 279 // non-vendored a/b. See golang.org/issue/13832. 280 func hasGoFiles(ctxt *Context, dir string) bool { 281 ents, _ := ctxt.readDir(dir) 282 for _, ent := range ents { 283 if !ent.IsDir() && strings.HasSuffix(ent.Name(), ".zig") { 284 return true 285 } 286 } 287 return false 288 } 289 290 // openFile calls ctxt.OpenFile (if not nil) or else os.Open. 291 func (ctxt *Context) openFile(path string) (io.ReadCloser, error) { 292 if fn := ctxt.OpenFile; fn != nil { 293 return fn(path) 294 } 295 296 f, err := os.Open(path) 297 if err != nil { 298 return nil, err // nil interface 299 } 300 return f, nil 301 } 302 303 func (ctxt *Context) IsFile(path string) bool { 304 return ctxt.isFile(path) 305 } 306 307 // isFile determines whether path is a file by trying to open it. 308 // It reuses openFile instead of adding another function to the 309 // list in Context. 310 func (ctxt *Context) isFile(path string) bool { 311 f, err := ctxt.openFile(path) 312 if err != nil { 313 return false 314 } 315 f.Close() 316 return true 317 } 318 319 // Import imports package 320 func (ctxt *Context) Import(path string, srcDir string, mode ImportMode) (*Package, error) { 321 p := &Package{ 322 ImportPath: path, 323 } 324 if path == "" { 325 return p, fmt.Errorf("import %q: invalid import path", path) 326 } 327 328 var pkgerr error 329 binaryOnly := false 330 if IsLocalImport(path) { 331 if srcDir == "" { 332 return p, fmt.Errorf("import %q: import relative to unknown directory", path) 333 } 334 if !ctxt.isAbsPath(path) { 335 p.Dir = ctxt.joinPath(srcDir, path) 336 } 337 // p.Dir directory may or may not exist. Gather partial information first, check if it exists later. 338 // Determine canonical import path, if any. 339 // Exclude results where the import path would include /testdata/. 340 inTestdata := func(sub string) bool { 341 return strings.Contains(sub, "/testdata/") || strings.HasSuffix(sub, "/testdata") || strings.HasPrefix(sub, "testdata/") || sub == "testdata" 342 } 343 all := ctxt.gopath() 344 for i, root := range all { 345 rootsrc := ctxt.joinPath(root, "src") 346 if sub, ok := ctxt.hasSubdir(rootsrc, p.Dir); ok && !inTestdata(sub) { 347 // We found a potential import path for dir, 348 // but check that using it wouldn't find something 349 // else first. 350 for _, earlyRoot := range all[:i] { 351 if dir := ctxt.joinPath(earlyRoot, "src", sub); ctxt.isDir(dir) { 352 p.ConflictDir = dir 353 goto Found 354 } 355 } 356 357 // sub would not name some other directory instead of this one. 358 // Record it. 359 p.ImportPath = sub 360 p.Root = root 361 goto Found 362 } 363 } 364 // It's okay that we didn't find a root containing dir. 365 // Keep going with the information we have. 366 } else { 367 if strings.HasPrefix(path, "/") { 368 return p, fmt.Errorf("import %q: cannot import absolute path", path) 369 } 370 371 gopath := ctxt.gopath() // needed by both importGo and below; avoid computing twice 372 if err := ctxt.importGo(p, path, srcDir, mode, gopath); err == nil { 373 goto Found 374 } else if err != errNoModules { 375 return p, err 376 } 377 378 // tried records the location of unsuccessful package lookups 379 var tried struct { 380 vendor []string 381 gopath []string 382 } 383 384 // Vendor directories get first chance to satisfy import. 385 if mode&IgnoreVendor == 0 && srcDir != "" { 386 searchVendor := func(root string, isGoroot bool) bool { 387 sub, ok := ctxt.hasSubdir(root, srcDir) 388 if !ok || !strings.HasPrefix(sub, "src/") || strings.Contains(sub, "/testdata/") { 389 return false 390 } 391 for { 392 vendor := ctxt.joinPath(root, sub, "vendor") 393 if ctxt.isDir(vendor) { 394 dir := ctxt.joinPath(vendor, path) 395 if ctxt.isDir(dir) && hasGoFiles(ctxt, dir) { 396 p.Dir = dir 397 p.ImportPath = strings.TrimPrefix(pathpkg.Join(sub, "vendor", path), "src/") 398 p.Root = root 399 return true 400 } 401 tried.vendor = append(tried.vendor, dir) 402 } 403 i := strings.LastIndex(sub, "/") 404 if i < 0 { 405 break 406 } 407 sub = sub[:i] 408 } 409 return false 410 } 411 for _, root := range gopath { 412 if searchVendor(root, false) { 413 goto Found 414 } 415 } 416 } 417 418 for _, root := range gopath { 419 dir := ctxt.joinPath(root, "src", path) 420 isDir := ctxt.isDir(dir) 421 if isDir { 422 p.Dir = dir 423 p.Root = root 424 goto Found 425 } 426 tried.gopath = append(tried.gopath, dir) 427 } 428 429 // package was not found 430 var paths []string 431 format := "\t%s (vendor tree)" 432 for _, dir := range tried.vendor { 433 paths = append(paths, fmt.Sprintf(format, dir)) 434 format = "\t%s" 435 } 436 437 paths = append(paths, "\t($GOROOT not set)") 438 format = "\t%s (from $GOPATH)" 439 for _, dir := range tried.gopath { 440 paths = append(paths, fmt.Sprintf(format, dir)) 441 format = "\t%s" 442 } 443 if len(tried.gopath) == 0 { 444 paths = append(paths, "\t($GOPATH not set. For more details see: 'go help gopath')") 445 } 446 return p, fmt.Errorf("cannot find package %q in any of:\n%s", path, strings.Join(paths, "\n")) 447 } 448 449 Found: 450 if p.Root != "" { 451 p.SrcRoot = ctxt.joinPath(p.Root, "src") 452 p.PkgRoot = ctxt.joinPath(p.Root, "pkg") 453 } 454 455 // If it's a local import path, by the time we get here, we still haven't checked 456 // that p.Dir directory exists. This is the right time to do that check. 457 // We can't do it earlier, because we want to gather partial information for the 458 // non-nil *Package returned when an error occurs. 459 // We need to do this before we return early on FindOnly flag. 460 if IsLocalImport(path) && !ctxt.isDir(p.Dir) { 461 // package was not found 462 return p, fmt.Errorf("cannot find package %q in:\n\t%s", path, p.Dir) 463 } 464 465 if mode&FindOnly != 0 { 466 return p, pkgerr 467 } 468 if binaryOnly && (mode&AllowBinary) != 0 { 469 return p, pkgerr 470 } 471 472 dirs, err := ctxt.readDir(p.Dir) 473 if err != nil { 474 return p, err 475 } 476 477 // TODO(gernest) load package from z.mod 478 for _, d := range dirs { 479 if d.IsDir() { 480 continue 481 } 482 } 483 return p, pkgerr 484 } 485 486 // A Package describes the Go package found in a directory. 487 type Package struct { 488 Dir string // directory containing package sources 489 Name string // package name 490 ImportComment string // path in import comment on package statement 491 Doc string // documentation synopsis 492 ImportPath string // import path of package ("" if unknown) 493 Root string // root of Go tree where this package lives 494 SrcRoot string // package source root directory ("" if unknown) 495 PkgRoot string // package install root directory ("" if unknown) 496 ConflictDir string // this directory shadows Dir in $GOPATH 497 498 // Source files 499 GoFiles []string // .go source files (excluding CgoFiles, TestGoFiles, XTestGoFiles) 500 501 // Dependency information 502 Imports []string // import paths from GoFiles, CgoFiles 503 } 504 505 // IsLocalImport reports whether the import path is 506 // a local import path, like ".", "..", "./foo", or "../foo". 507 func IsLocalImport(path string) bool { 508 return path == "." || path == ".." || 509 strings.HasPrefix(path, "./") || strings.HasPrefix(path, "../") 510 }