github.com/gopherjs/gopherjs@v1.19.0-beta1.0.20240506212314-27071a8796e4/build/context.go (about)

     1  package build
     2  
     3  import (
     4  	"fmt"
     5  	"go/build"
     6  	"go/token"
     7  	"io"
     8  	"net/http"
     9  	"os"
    10  	"os/exec"
    11  	"path"
    12  	"path/filepath"
    13  	"sort"
    14  	"strings"
    15  
    16  	_ "github.com/gopherjs/gopherjs/build/versionhack" // go/build release tags hack.
    17  	"github.com/gopherjs/gopherjs/compiler"
    18  	"github.com/gopherjs/gopherjs/compiler/gopherjspkg"
    19  	"github.com/gopherjs/gopherjs/compiler/natives"
    20  	"golang.org/x/tools/go/buildutil"
    21  )
    22  
    23  // Env contains build environment configuration required to define an instance
    24  // of XContext.
    25  type Env struct {
    26  	GOROOT string
    27  	GOPATH string
    28  
    29  	GOOS   string
    30  	GOARCH string
    31  
    32  	BuildTags     []string
    33  	InstallSuffix string
    34  }
    35  
    36  // DefaultEnv creates a new instance of build Env according to environment
    37  // variables.
    38  //
    39  // By default, GopherJS will use GOOS=js GOARCH=ecmascript to build non-standard
    40  // library packages. If GOOS or GOARCH environment variables are set and not
    41  // empty, user-provided values will be used instead. This is done to facilitate
    42  // transition from the legacy GopherJS behavior, which used native GOOS, and may
    43  // be removed in future.
    44  func DefaultEnv() Env {
    45  	e := Env{}
    46  	e.GOROOT = DefaultGOROOT
    47  	e.GOPATH = build.Default.GOPATH
    48  
    49  	if val := os.Getenv("GOOS"); val != "" {
    50  		e.GOOS = val
    51  	} else {
    52  		e.GOOS = "js"
    53  	}
    54  
    55  	if val := os.Getenv("GOARCH"); val != "" {
    56  		e.GOARCH = val
    57  	} else {
    58  		e.GOARCH = "ecmascript"
    59  	}
    60  	return e
    61  }
    62  
    63  // XContext is an extension of go/build.Context with GopherJS-specific features.
    64  //
    65  // It abstracts away several different sources GopherJS can load its packages
    66  // from, with a minimal API.
    67  type XContext interface {
    68  	// Import returns details about the Go package named by the importPath,
    69  	// interpreting local import paths relative to the srcDir directory.
    70  	Import(path string, srcDir string, mode build.ImportMode) (*PackageData, error)
    71  
    72  	// Env returns build environment configuration this context has been set up for.
    73  	Env() Env
    74  
    75  	// Match explans build patterns into a set of matching import paths (see go help packages).
    76  	Match(patterns []string) ([]string, error)
    77  }
    78  
    79  // simpleCtx is a wrapper around go/build.Context with support for GopherJS-specific
    80  // features.
    81  type simpleCtx struct {
    82  	bctx         build.Context
    83  	isVirtual    bool // Imported packages don't have a physical directory on disk.
    84  	noPostTweaks bool // Don't apply post-load tweaks to packages. For tests only.
    85  }
    86  
    87  // Import implements XContext.Import().
    88  func (sc simpleCtx) Import(importPath string, srcDir string, mode build.ImportMode) (*PackageData, error) {
    89  	bctx, mode := sc.applyPreloadTweaks(importPath, srcDir, mode)
    90  	pkg, err := bctx.Import(importPath, srcDir, mode)
    91  	if err != nil {
    92  		return nil, err
    93  	}
    94  	jsFiles, err := jsFilesFromDir(&sc.bctx, pkg.Dir)
    95  	if err != nil {
    96  		return nil, fmt.Errorf("failed to enumerate .inc.js files in %s: %w", pkg.Dir, err)
    97  	}
    98  	if !path.IsAbs(pkg.Dir) {
    99  		pkg.Dir = mustAbs(pkg.Dir)
   100  	}
   101  	pkg = sc.applyPostloadTweaks(pkg)
   102  
   103  	return &PackageData{
   104  		Package:   pkg,
   105  		IsVirtual: sc.isVirtual,
   106  		JSFiles:   jsFiles,
   107  		bctx:      &sc.bctx,
   108  	}, nil
   109  }
   110  
   111  // Match implements XContext.Match.
   112  func (sc simpleCtx) Match(patterns []string) ([]string, error) {
   113  	if sc.isVirtual {
   114  		// We can't use go tool to enumerate packages in a virtual file system,
   115  		// so we fall back onto a simpler implementation provided by the buildutil
   116  		// package. It doesn't support all valid patterns, but should be good enough.
   117  		//
   118  		// Note: this code path will become unnecessary after
   119  		// https://github.com/gopherjs/gopherjs/issues/1021 is implemented.
   120  		args := []string{}
   121  		for _, p := range patterns {
   122  			switch p {
   123  			case "all":
   124  				args = append(args, "...")
   125  			case "std", "main", "cmd":
   126  				// These patterns are not supported by buildutil.ExpandPatterns(),
   127  				// but they would be matched by the real context correctly, so skip them.
   128  			default:
   129  				args = append(args, p)
   130  			}
   131  		}
   132  		matches := []string{}
   133  		for importPath := range buildutil.ExpandPatterns(&sc.bctx, args) {
   134  			if importPath[0] == '.' {
   135  				p, err := sc.Import(importPath, ".", build.FindOnly)
   136  				// Resolve relative patterns into canonical import paths.
   137  				if err != nil {
   138  					continue
   139  				}
   140  				importPath = p.ImportPath
   141  			}
   142  			matches = append(matches, importPath)
   143  		}
   144  		sort.Strings(matches)
   145  		return matches, nil
   146  	}
   147  
   148  	args := append([]string{
   149  		"-e", "-compiler=gc",
   150  		"-tags=" + strings.Join(sc.bctx.BuildTags, ","),
   151  		"-installsuffix=" + sc.bctx.InstallSuffix,
   152  		"-f={{.ImportPath}}",
   153  		"--",
   154  	}, patterns...)
   155  
   156  	out, err := sc.gotool("list", args...)
   157  	if err != nil {
   158  		return nil, fmt.Errorf("failed to list packages on FS: %w", err)
   159  	}
   160  	matches := strings.Split(strings.TrimSpace(out), "\n")
   161  	sort.Strings(matches)
   162  	return matches, nil
   163  }
   164  
   165  func (sc simpleCtx) Env() Env {
   166  	return Env{
   167  		GOROOT:        sc.bctx.GOROOT,
   168  		GOPATH:        sc.bctx.GOPATH,
   169  		GOOS:          sc.bctx.GOOS,
   170  		GOARCH:        sc.bctx.GOARCH,
   171  		BuildTags:     sc.bctx.BuildTags,
   172  		InstallSuffix: sc.bctx.InstallSuffix,
   173  	}
   174  }
   175  
   176  // gotool executes the go tool set up for the build context and returns standard output.
   177  func (sc simpleCtx) gotool(subcommand string, args ...string) (string, error) {
   178  	if sc.isVirtual {
   179  		panic(fmt.Errorf("can't use go tool with a virtual build context"))
   180  	}
   181  	args = append([]string{subcommand}, args...)
   182  	cmd := exec.Command(filepath.Join(sc.bctx.GOROOT, "bin", "go"), args...)
   183  
   184  	if sc.bctx.Dir != "" {
   185  		cmd.Dir = sc.bctx.Dir
   186  	}
   187  
   188  	var stdout, stderr strings.Builder
   189  	cmd.Stdout = &stdout
   190  	cmd.Stderr = &stderr
   191  
   192  	cgo := "0"
   193  	if sc.bctx.CgoEnabled {
   194  		cgo = "1"
   195  	}
   196  	cmd.Env = append(os.Environ(),
   197  		"GOOS="+sc.bctx.GOOS,
   198  		"GOARCH="+sc.bctx.GOARCH,
   199  		"GOROOT="+sc.bctx.GOROOT,
   200  		"GOPATH="+sc.bctx.GOPATH,
   201  		"CGO_ENABLED="+cgo,
   202  	)
   203  
   204  	if err := cmd.Run(); err != nil {
   205  		return "", fmt.Errorf("go tool error: %v: %w\n%s", cmd, err, stderr.String())
   206  	}
   207  	return stdout.String(), nil
   208  }
   209  
   210  // applyPreloadTweaks makes several package-specific adjustments to package importing.
   211  //
   212  // Ideally this method would not be necessary, but currently several packages
   213  // require special handing in order to be compatible with GopherJS. This method
   214  // returns a copy of the build context, keeping the original one intact.
   215  func (sc simpleCtx) applyPreloadTweaks(importPath string, srcDir string, mode build.ImportMode) (build.Context, build.ImportMode) {
   216  	bctx := sc.bctx
   217  	if sc.isStd(importPath, srcDir) {
   218  		// For most of the platform-dependent code in the standard library we simply
   219  		// reuse implementations targeting WebAssembly. For the user-supplied we use
   220  		// regular gopherjs-specific GOOS/GOARCH.
   221  		bctx.GOOS = "js"
   222  		bctx.GOARCH = "wasm"
   223  	}
   224  	switch importPath {
   225  	case "github.com/gopherjs/gopherjs/js", "github.com/gopherjs/gopherjs/nosync":
   226  		// These packages are already embedded via gopherjspkg.FS virtual filesystem
   227  		// (which can be safely vendored). Don't try to use vendor directory to
   228  		// resolve them.
   229  		mode |= build.IgnoreVendor
   230  	}
   231  
   232  	return bctx, mode
   233  }
   234  
   235  // applyPostloadTweaks makes adjustments to the contents of the loaded package.
   236  //
   237  // Some of the standard library packages require additional tweaks that are not
   238  // covered by our augmentation logic, for example excluding or including
   239  // particular source files. This method ensures that all such tweaks are applied
   240  // before the package is returned to the caller.
   241  func (sc simpleCtx) applyPostloadTweaks(pkg *build.Package) *build.Package {
   242  	if sc.isVirtual {
   243  		// GopherJS overlay package sources don't need tweaks to their content,
   244  		// since we already control them directly.
   245  		return pkg
   246  	}
   247  	if sc.noPostTweaks {
   248  		return pkg
   249  	}
   250  	switch pkg.ImportPath {
   251  	case "runtime":
   252  		pkg.GoFiles = []string{} // Package sources are completely replaced in natives.
   253  	case "runtime/pprof":
   254  		pkg.GoFiles = nil
   255  	case "sync":
   256  		// GopherJS completely replaces sync.Pool implementation with a simpler one,
   257  		// since it always executes in a single-threaded environment.
   258  		pkg.GoFiles = exclude(pkg.GoFiles, "pool.go")
   259  	case "syscall/js":
   260  		// Reuse upstream tests to ensure conformance, but completely replace
   261  		// implementation.
   262  		pkg.GoFiles = []string{}
   263  		pkg.TestGoFiles = []string{}
   264  	}
   265  
   266  	pkg.Imports, pkg.ImportPos = updateImports(pkg.GoFiles, pkg.ImportPos)
   267  	pkg.TestImports, pkg.TestImportPos = updateImports(pkg.TestGoFiles, pkg.TestImportPos)
   268  	pkg.XTestImports, pkg.XTestImportPos = updateImports(pkg.XTestGoFiles, pkg.XTestImportPos)
   269  
   270  	return pkg
   271  }
   272  
   273  // isStd returns true if the given importPath resolves into a standard library
   274  // package. Relative paths are interpreted relative to srcDir.
   275  func (sc simpleCtx) isStd(importPath, srcDir string) bool {
   276  	pkg, err := sc.bctx.Import(importPath, srcDir, build.FindOnly)
   277  	if err != nil {
   278  		return false
   279  	}
   280  	return pkg.Goroot
   281  }
   282  
   283  var defaultBuildTags = []string{
   284  	"netgo",            // See https://godoc.org/net#hdr-Name_Resolution.
   285  	"purego",           // See https://golang.org/issues/23172.
   286  	"math_big_pure_go", // Use pure Go version of math/big.
   287  	// We can't set compiler to gopherjs, since Go tooling doesn't support that,
   288  	// but, we can at least always set this build tag.
   289  	"gopherjs",
   290  }
   291  
   292  // embeddedCtx creates simpleCtx that imports from a virtual FS embedded into
   293  // the GopherJS compiler.
   294  func embeddedCtx(embedded http.FileSystem, e Env) *simpleCtx {
   295  	fs := &vfs{embedded}
   296  	ec := goCtx(e)
   297  	ec.bctx.GOPATH = ""
   298  
   299  	// Path functions must behave unix-like to work with the VFS.
   300  	ec.bctx.JoinPath = path.Join
   301  	ec.bctx.SplitPathList = splitPathList
   302  	ec.bctx.IsAbsPath = path.IsAbs
   303  	ec.bctx.HasSubdir = hasSubdir
   304  
   305  	// Substitute real FS with the embedded one.
   306  	ec.bctx.IsDir = fs.IsDir
   307  	ec.bctx.ReadDir = fs.ReadDir
   308  	ec.bctx.OpenFile = fs.OpenFile
   309  	ec.isVirtual = true
   310  	return ec
   311  }
   312  
   313  // overlayCtx creates simpleCtx that imports from the embedded standard library
   314  // overlays.
   315  func overlayCtx(e Env) *simpleCtx {
   316  	return embeddedCtx(&withPrefix{fs: http.FS(natives.FS), prefix: e.GOROOT}, e)
   317  }
   318  
   319  // gopherjsCtx creates a simpleCtx that imports from the embedded gopherjs
   320  // packages in case they are not present in the user's source tree.
   321  func gopherjsCtx(e Env) *simpleCtx {
   322  	gopherjsRoot := filepath.Join(e.GOROOT, "src", "github.com", "gopherjs", "gopherjs")
   323  	return embeddedCtx(&withPrefix{gopherjspkg.FS, gopherjsRoot}, e)
   324  }
   325  
   326  // goCtx creates simpleCtx that imports from the real file system GOROOT, GOPATH
   327  // or Go Modules.
   328  func goCtx(e Env) *simpleCtx {
   329  	gc := simpleCtx{
   330  		bctx: build.Context{
   331  			GOROOT:        e.GOROOT,
   332  			GOPATH:        e.GOPATH,
   333  			GOOS:          e.GOOS,
   334  			GOARCH:        e.GOARCH,
   335  			InstallSuffix: e.InstallSuffix,
   336  			Compiler:      "gc",
   337  			BuildTags:     append(append([]string{}, e.BuildTags...), defaultBuildTags...),
   338  			CgoEnabled:    false, // CGo is not supported by GopherJS.
   339  
   340  			// go/build supports modules, but only when no FS access functions are
   341  			// overridden and when provided ReleaseTags match those of the default
   342  			// context (matching Go compiler's version).
   343  			// This limitation stems from the fact that it will invoke the Go tool
   344  			// which can only see files on the real FS and will assume release tags
   345  			// based on the Go tool's version.
   346  			//
   347  			// See also comments to the versionhack package.
   348  			ReleaseTags: build.Default.ReleaseTags[:compiler.GoVersion],
   349  		},
   350  	}
   351  	return &gc
   352  }
   353  
   354  // chainedCtx combines two build contexts. Secondary context acts as a fallback
   355  // when a package is not found in the primary, and is ignored otherwise.
   356  //
   357  // This allows GopherJS to load its core "js" and "nosync" packages from the
   358  // embedded VFS whenever user's code doesn't directly depend on them, but
   359  // augmented stdlib does.
   360  type chainedCtx struct {
   361  	primary   XContext
   362  	secondary XContext
   363  }
   364  
   365  // Import implements buildCtx.Import().
   366  func (cc chainedCtx) Import(importPath string, srcDir string, mode build.ImportMode) (*PackageData, error) {
   367  	pkg, err := cc.primary.Import(importPath, srcDir, mode)
   368  	if err == nil {
   369  		return pkg, nil
   370  	} else if IsPkgNotFound(err) {
   371  		return cc.secondary.Import(importPath, srcDir, mode)
   372  	} else {
   373  		return nil, err
   374  	}
   375  }
   376  
   377  func (cc chainedCtx) Env() Env { return cc.primary.Env() }
   378  
   379  // Match implements XContext.Match().
   380  //
   381  // Packages from both contexts are included and returned as a deduplicated
   382  // sorted list.
   383  func (cc chainedCtx) Match(patterns []string) ([]string, error) {
   384  	m1, err := cc.primary.Match(patterns)
   385  	if err != nil {
   386  		return nil, fmt.Errorf("failed to list packages in the primary context: %s", err)
   387  	}
   388  	m2, err := cc.secondary.Match(patterns)
   389  	if err != nil {
   390  		return nil, fmt.Errorf("failed to list packages in the secondary context: %s", err)
   391  	}
   392  
   393  	seen := map[string]bool{}
   394  	matches := []string{}
   395  	for _, m := range append(m1, m2...) {
   396  		if seen[m] {
   397  			continue
   398  		}
   399  		seen[m] = true
   400  		matches = append(matches, m)
   401  	}
   402  	sort.Strings(matches)
   403  	return matches, nil
   404  }
   405  
   406  // IsPkgNotFound returns true if the error was caused by package not found.
   407  //
   408  // Unfortunately, go/build doesn't make use of typed errors, so we have to
   409  // rely on the error message.
   410  func IsPkgNotFound(err error) bool {
   411  	return err != nil &&
   412  		(strings.Contains(err.Error(), "cannot find package") || // Modules off.
   413  			strings.Contains(err.Error(), "is not in GOROOT")) // Modules on.
   414  }
   415  
   416  // updateImports package's list of import paths to only those present in sources
   417  // after post-load tweaks.
   418  func updateImports(sources []string, importPos map[string][]token.Position) (newImports []string, newImportPos map[string][]token.Position) {
   419  	if importPos == nil {
   420  		// Short-circuit for tests when no imports are loaded.
   421  		return nil, nil
   422  	}
   423  	sourceSet := map[string]bool{}
   424  	for _, source := range sources {
   425  		sourceSet[source] = true
   426  	}
   427  
   428  	newImportPos = map[string][]token.Position{}
   429  	for importPath, positions := range importPos {
   430  		for _, pos := range positions {
   431  			if sourceSet[filepath.Base(pos.Filename)] {
   432  				newImportPos[importPath] = append(newImportPos[importPath], pos)
   433  			}
   434  		}
   435  	}
   436  
   437  	for importPath := range newImportPos {
   438  		newImports = append(newImports, importPath)
   439  	}
   440  	sort.Strings(newImports)
   441  	return newImports, newImportPos
   442  }
   443  
   444  // jsFilesFromDir finds and loads any *.inc.js packages in the build context
   445  // directory.
   446  func jsFilesFromDir(bctx *build.Context, dir string) ([]JSFile, error) {
   447  	files, err := buildutil.ReadDir(bctx, dir)
   448  	if err != nil {
   449  		return nil, err
   450  	}
   451  	var jsFiles []JSFile
   452  	for _, file := range files {
   453  		if !strings.HasSuffix(file.Name(), ".inc.js") || file.IsDir() {
   454  			continue
   455  		}
   456  		if file.Name()[0] == '_' || file.Name()[0] == '.' {
   457  			continue // Skip "hidden" files that are typically ignored by the Go build system.
   458  		}
   459  
   460  		path := buildutil.JoinPath(bctx, dir, file.Name())
   461  		f, err := buildutil.OpenFile(bctx, path)
   462  		if err != nil {
   463  			return nil, fmt.Errorf("failed to open %s from %v: %w", path, bctx, err)
   464  		}
   465  		defer f.Close()
   466  
   467  		content, err := io.ReadAll(f)
   468  		if err != nil {
   469  			return nil, fmt.Errorf("failed to read %s from %v: %w", path, bctx, err)
   470  		}
   471  
   472  		jsFiles = append(jsFiles, JSFile{
   473  			Path:    path,
   474  			ModTime: file.ModTime(),
   475  			Content: content,
   476  		})
   477  	}
   478  	return jsFiles, nil
   479  }