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  }