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

     1  // Package build implements GopherJS build system.
     2  //
     3  // WARNING: This package's API is treated as internal and currently doesn't
     4  // provide any API stability guarantee, use it at your own risk. If you need a
     5  // stable interface, prefer invoking the gopherjs CLI tool as a subprocess.
     6  package build
     7  
     8  import (
     9  	"fmt"
    10  	"go/ast"
    11  	"go/build"
    12  	"go/parser"
    13  	"go/scanner"
    14  	"go/token"
    15  	"go/types"
    16  	"io/fs"
    17  	"os"
    18  	"os/exec"
    19  	"path"
    20  	"path/filepath"
    21  	"sort"
    22  	"strconv"
    23  	"strings"
    24  	"time"
    25  
    26  	"github.com/fsnotify/fsnotify"
    27  	"github.com/gopherjs/gopherjs/compiler"
    28  	"github.com/gopherjs/gopherjs/compiler/astutil"
    29  	log "github.com/sirupsen/logrus"
    30  
    31  	"github.com/neelance/sourcemap"
    32  	"golang.org/x/tools/go/buildutil"
    33  
    34  	"github.com/gopherjs/gopherjs/build/cache"
    35  )
    36  
    37  // DefaultGOROOT is the default GOROOT value for builds.
    38  //
    39  // It uses the GOPHERJS_GOROOT environment variable if it is set,
    40  // or else the default GOROOT value of the system Go distribution.
    41  var DefaultGOROOT = func() string {
    42  	if goroot, ok := os.LookupEnv("GOPHERJS_GOROOT"); ok {
    43  		// GopherJS-specific GOROOT value takes precedence.
    44  		return goroot
    45  	}
    46  	// The usual default GOROOT.
    47  	return build.Default.GOROOT
    48  }()
    49  
    50  // NewBuildContext creates a build context for building Go packages
    51  // with GopherJS compiler.
    52  //
    53  // Core GopherJS packages (i.e., "github.com/gopherjs/gopherjs/js", "github.com/gopherjs/gopherjs/nosync")
    54  // are loaded from gopherjspkg.FS virtual filesystem if not present in GOPATH or
    55  // go.mod.
    56  func NewBuildContext(installSuffix string, buildTags []string) XContext {
    57  	e := DefaultEnv()
    58  	e.InstallSuffix = installSuffix
    59  	e.BuildTags = buildTags
    60  	realGOROOT := goCtx(e)
    61  	return &chainedCtx{
    62  		primary:   realGOROOT,
    63  		secondary: gopherjsCtx(e),
    64  	}
    65  }
    66  
    67  // Import returns details about the Go package named by the import path. If the
    68  // path is a local import path naming a package that can be imported using
    69  // a standard import path, the returned package will set p.ImportPath to
    70  // that path.
    71  //
    72  // In the directory containing the package, .go and .inc.js files are
    73  // considered part of the package except for:
    74  //
    75  //   - .go files in package documentation
    76  //   - files starting with _ or . (likely editor temporary files)
    77  //   - files with build constraints not satisfied by the context
    78  //
    79  // If an error occurs, Import returns a non-nil error and a nil
    80  // *PackageData.
    81  func Import(path string, mode build.ImportMode, installSuffix string, buildTags []string) (*PackageData, error) {
    82  	wd, err := os.Getwd()
    83  	if err != nil {
    84  		// Getwd may fail if we're in GOOS=js mode. That's okay, handle
    85  		// it by falling back to empty working directory. It just means
    86  		// Import will not be able to resolve relative import paths.
    87  		wd = ""
    88  	}
    89  	xctx := NewBuildContext(installSuffix, buildTags)
    90  	return xctx.Import(path, wd, mode)
    91  }
    92  
    93  // exclude returns files, excluding specified files.
    94  func exclude(files []string, exclude ...string) []string {
    95  	var s []string
    96  Outer:
    97  	for _, f := range files {
    98  		for _, e := range exclude {
    99  			if f == e {
   100  				continue Outer
   101  			}
   102  		}
   103  		s = append(s, f)
   104  	}
   105  	return s
   106  }
   107  
   108  // ImportDir is like Import but processes the Go package found in the named
   109  // directory.
   110  func ImportDir(dir string, mode build.ImportMode, installSuffix string, buildTags []string) (*PackageData, error) {
   111  	xctx := NewBuildContext(installSuffix, buildTags)
   112  	pkg, err := xctx.Import(".", dir, mode)
   113  	if err != nil {
   114  		return nil, err
   115  	}
   116  
   117  	return pkg, nil
   118  }
   119  
   120  // overrideInfo is used by parseAndAugment methods to manage
   121  // directives and how the overlay and original are merged.
   122  type overrideInfo struct {
   123  	// KeepOriginal indicates that the original code should be kept
   124  	// but the identifier will be prefixed by `_gopherjs_original_foo`.
   125  	// If false the original code is removed.
   126  	keepOriginal bool
   127  
   128  	// purgeMethods indicates that this info is for a type and
   129  	// if a method has this type as a receiver should also be removed.
   130  	// If the method is defined in the overlays and therefore has its
   131  	// own overrides, this will be ignored.
   132  	purgeMethods bool
   133  
   134  	// overrideSignature is the function definition given in the overlays
   135  	// that should be used to replace the signature in the originals.
   136  	// Only receivers, type parameters, parameters, and results will be used.
   137  	overrideSignature *ast.FuncDecl
   138  }
   139  
   140  // parseAndAugment parses and returns all .go files of given pkg.
   141  // Standard Go library packages are augmented with files in compiler/natives folder.
   142  // If isTest is true and pkg.ImportPath has no _test suffix, package is built for running internal tests.
   143  // If isTest is true and pkg.ImportPath has _test suffix, package is built for running external tests.
   144  //
   145  // The native packages are augmented by the contents of natives.FS in the following way.
   146  // The file names do not matter except the usual `_test` suffix. The files for
   147  // native overrides get added to the package (even if they have the same name
   148  // as an existing file from the standard library).
   149  //
   150  //   - For function identifiers that exist in the original and the overrides
   151  //     and have the directive `gopherjs:keep-original`, the original identifier
   152  //     in the AST gets prefixed by `_gopherjs_original_`.
   153  //   - For identifiers that exist in the original and the overrides, and have
   154  //     the directive `gopherjs:purge`, both the original and override are
   155  //     removed. This is for completely removing something which is currently
   156  //     invalid for GopherJS. For any purged types any methods with that type as
   157  //     the receiver are also removed.
   158  //   - For function identifiers that exist in the original and the overrides,
   159  //     and have the directive `gopherjs:override-signature`, the overridden
   160  //     function is removed and the original function's signature is changed
   161  //     to match the overridden function signature. This allows the receiver,
   162  //     type parameters, parameter, and return values to be modified as needed.
   163  //   - Otherwise for identifiers that exist in the original and the overrides,
   164  //     the original is removed.
   165  //   - New identifiers that don't exist in original package get added.
   166  func parseAndAugment(xctx XContext, pkg *PackageData, isTest bool, fileSet *token.FileSet) ([]*ast.File, []JSFile, error) {
   167  	jsFiles, overlayFiles := parseOverlayFiles(xctx, pkg, isTest, fileSet)
   168  
   169  	originalFiles, err := parserOriginalFiles(pkg, fileSet)
   170  	if err != nil {
   171  		return nil, nil, err
   172  	}
   173  
   174  	overrides := make(map[string]overrideInfo)
   175  	for _, file := range overlayFiles {
   176  		augmentOverlayFile(file, overrides)
   177  	}
   178  	delete(overrides, "init")
   179  
   180  	for _, file := range originalFiles {
   181  		augmentOriginalImports(pkg.ImportPath, file)
   182  	}
   183  
   184  	if len(overrides) > 0 {
   185  		for _, file := range originalFiles {
   186  			augmentOriginalFile(file, overrides)
   187  		}
   188  	}
   189  
   190  	return append(overlayFiles, originalFiles...), jsFiles, nil
   191  }
   192  
   193  // parseOverlayFiles loads and parses overlay files
   194  // to augment the original files with.
   195  func parseOverlayFiles(xctx XContext, pkg *PackageData, isTest bool, fileSet *token.FileSet) ([]JSFile, []*ast.File) {
   196  	isXTest := strings.HasSuffix(pkg.ImportPath, "_test")
   197  	importPath := pkg.ImportPath
   198  	if isXTest {
   199  		importPath = importPath[:len(importPath)-5]
   200  	}
   201  
   202  	nativesContext := overlayCtx(xctx.Env())
   203  	nativesPkg, err := nativesContext.Import(importPath, "", 0)
   204  	if err != nil {
   205  		return nil, nil
   206  	}
   207  
   208  	jsFiles := nativesPkg.JSFiles
   209  	var files []*ast.File
   210  	names := nativesPkg.GoFiles
   211  	if isTest {
   212  		names = append(names, nativesPkg.TestGoFiles...)
   213  	}
   214  	if isXTest {
   215  		names = nativesPkg.XTestGoFiles
   216  	}
   217  
   218  	for _, name := range names {
   219  		fullPath := path.Join(nativesPkg.Dir, name)
   220  		r, err := nativesContext.bctx.OpenFile(fullPath)
   221  		if err != nil {
   222  			panic(err)
   223  		}
   224  		// Files should be uniquely named and in the original package directory in order to be
   225  		// ordered correctly
   226  		newPath := path.Join(pkg.Dir, "gopherjs__"+name)
   227  		file, err := parser.ParseFile(fileSet, newPath, r, parser.ParseComments)
   228  		if err != nil {
   229  			panic(err)
   230  		}
   231  		r.Close()
   232  
   233  		files = append(files, file)
   234  	}
   235  	return jsFiles, files
   236  }
   237  
   238  // parserOriginalFiles loads and parses the original files to augment.
   239  func parserOriginalFiles(pkg *PackageData, fileSet *token.FileSet) ([]*ast.File, error) {
   240  	var files []*ast.File
   241  	var errList compiler.ErrorList
   242  	for _, name := range pkg.GoFiles {
   243  		if !filepath.IsAbs(name) { // name might be absolute if specified directly. E.g., `gopherjs build /abs/file.go`.
   244  			name = filepath.Join(pkg.Dir, name)
   245  		}
   246  
   247  		r, err := buildutil.OpenFile(pkg.bctx, name)
   248  		if err != nil {
   249  			return nil, err
   250  		}
   251  
   252  		file, err := parser.ParseFile(fileSet, name, r, parser.ParseComments)
   253  		r.Close()
   254  		if err != nil {
   255  			if list, isList := err.(scanner.ErrorList); isList {
   256  				if len(list) > 10 {
   257  					list = append(list[:10], &scanner.Error{Pos: list[9].Pos, Msg: "too many errors"})
   258  				}
   259  				for _, entry := range list {
   260  					errList = append(errList, entry)
   261  				}
   262  				continue
   263  			}
   264  			errList = append(errList, err)
   265  			continue
   266  		}
   267  
   268  		files = append(files, file)
   269  	}
   270  
   271  	if errList != nil {
   272  		return nil, errList
   273  	}
   274  	return files, nil
   275  }
   276  
   277  // augmentOverlayFile is the part of parseAndAugment that processes
   278  // an overlay file AST to collect information such as compiler directives
   279  // and perform any initial augmentation needed to the overlay.
   280  func augmentOverlayFile(file *ast.File, overrides map[string]overrideInfo) {
   281  	anyChange := false
   282  	for i, decl := range file.Decls {
   283  		purgeDecl := astutil.Purge(decl)
   284  		switch d := decl.(type) {
   285  		case *ast.FuncDecl:
   286  			k := astutil.FuncKey(d)
   287  			oi := overrideInfo{
   288  				keepOriginal: astutil.KeepOriginal(d),
   289  			}
   290  			if astutil.OverrideSignature(d) {
   291  				oi.overrideSignature = d
   292  				purgeDecl = true
   293  			}
   294  			overrides[k] = oi
   295  		case *ast.GenDecl:
   296  			for j, spec := range d.Specs {
   297  				purgeSpec := purgeDecl || astutil.Purge(spec)
   298  				switch s := spec.(type) {
   299  				case *ast.TypeSpec:
   300  					overrides[s.Name.Name] = overrideInfo{
   301  						purgeMethods: purgeSpec,
   302  					}
   303  				case *ast.ValueSpec:
   304  					for _, name := range s.Names {
   305  						overrides[name.Name] = overrideInfo{}
   306  					}
   307  				}
   308  				if purgeSpec {
   309  					anyChange = true
   310  					d.Specs[j] = nil
   311  				}
   312  			}
   313  		}
   314  		if purgeDecl {
   315  			anyChange = true
   316  			file.Decls[i] = nil
   317  		}
   318  	}
   319  	if anyChange {
   320  		finalizeRemovals(file)
   321  		pruneImports(file)
   322  	}
   323  }
   324  
   325  // augmentOriginalImports is the part of parseAndAugment that processes
   326  // an original file AST to modify the imports for that file.
   327  func augmentOriginalImports(importPath string, file *ast.File) {
   328  	switch importPath {
   329  	case "crypto/rand", "encoding/gob", "encoding/json", "expvar", "go/token", "log", "math/big", "math/rand", "regexp", "time":
   330  		for _, spec := range file.Imports {
   331  			path, _ := strconv.Unquote(spec.Path.Value)
   332  			if path == "sync" {
   333  				if spec.Name == nil {
   334  					spec.Name = ast.NewIdent("sync")
   335  				}
   336  				spec.Path.Value = `"github.com/gopherjs/gopherjs/nosync"`
   337  			}
   338  		}
   339  	}
   340  }
   341  
   342  // augmentOriginalFile is the part of parseAndAugment that processes an
   343  // original file AST to augment the source code using the overrides from
   344  // the overlay files.
   345  func augmentOriginalFile(file *ast.File, overrides map[string]overrideInfo) {
   346  	anyChange := false
   347  	for i, decl := range file.Decls {
   348  		switch d := decl.(type) {
   349  		case *ast.FuncDecl:
   350  			if info, ok := overrides[astutil.FuncKey(d)]; ok {
   351  				anyChange = true
   352  				removeFunc := true
   353  				if info.keepOriginal {
   354  					// Allow overridden function calls
   355  					// The standard library implementation of foo() becomes _gopherjs_original_foo()
   356  					d.Name.Name = "_gopherjs_original_" + d.Name.Name
   357  					removeFunc = false
   358  				}
   359  				if overSig := info.overrideSignature; overSig != nil {
   360  					d.Recv = overSig.Recv
   361  					d.Type.TypeParams = overSig.Type.TypeParams
   362  					d.Type.Params = overSig.Type.Params
   363  					d.Type.Results = overSig.Type.Results
   364  					removeFunc = false
   365  				}
   366  				if removeFunc {
   367  					file.Decls[i] = nil
   368  				}
   369  			} else if recvKey := astutil.FuncReceiverKey(d); len(recvKey) > 0 {
   370  				// check if the receiver has been purged, if so, remove the method too.
   371  				if info, ok := overrides[recvKey]; ok && info.purgeMethods {
   372  					anyChange = true
   373  					file.Decls[i] = nil
   374  				}
   375  			}
   376  		case *ast.GenDecl:
   377  			for j, spec := range d.Specs {
   378  				switch s := spec.(type) {
   379  				case *ast.TypeSpec:
   380  					if _, ok := overrides[s.Name.Name]; ok {
   381  						anyChange = true
   382  						d.Specs[j] = nil
   383  					}
   384  				case *ast.ValueSpec:
   385  					if len(s.Names) == len(s.Values) {
   386  						// multi-value context
   387  						// e.g. var a, b = 2, foo[int]()
   388  						// A removal will also remove the value which may be from a
   389  						// function call. This allows us to remove unwanted statements.
   390  						// However, if that call has a side effect which still needs
   391  						// to be run, add the call into the overlay.
   392  						for k, name := range s.Names {
   393  							if _, ok := overrides[name.Name]; ok {
   394  								anyChange = true
   395  								s.Names[k] = nil
   396  								s.Values[k] = nil
   397  							}
   398  						}
   399  					} else {
   400  						// single-value context
   401  						// e.g. var a, b = foo[int]()
   402  						// If a removal from the overlays makes all returned values unused,
   403  						// then remove the function call as well. This allows us to stop
   404  						// unwanted calls if needed. If that call has a side effect which
   405  						// still needs to be run, add the call into the overlay.
   406  						nameRemoved := false
   407  						for _, name := range s.Names {
   408  							if _, ok := overrides[name.Name]; ok {
   409  								nameRemoved = true
   410  								name.Name = `_`
   411  							}
   412  						}
   413  						if nameRemoved {
   414  							removeSpec := true
   415  							for _, name := range s.Names {
   416  								if name.Name != `_` {
   417  									removeSpec = false
   418  									break
   419  								}
   420  							}
   421  							if removeSpec {
   422  								anyChange = true
   423  								d.Specs[j] = nil
   424  							}
   425  						}
   426  					}
   427  				}
   428  			}
   429  		}
   430  	}
   431  	if anyChange {
   432  		finalizeRemovals(file)
   433  		pruneImports(file)
   434  	}
   435  }
   436  
   437  // isOnlyImports determines if this file is empty except for imports.
   438  func isOnlyImports(file *ast.File) bool {
   439  	for _, decl := range file.Decls {
   440  		if gen, ok := decl.(*ast.GenDecl); ok && gen.Tok == token.IMPORT {
   441  			continue
   442  		}
   443  
   444  		// The decl was either a FuncDecl or a non-import GenDecl.
   445  		return false
   446  	}
   447  	return true
   448  }
   449  
   450  // pruneImports will remove any unused imports from the file.
   451  //
   452  // This will not remove any dot (`.`) or blank (`_`) imports, unless
   453  // there are no declarations or directives meaning that all the imports
   454  // should be cleared.
   455  // If the removal of code causes an import to be removed, the init's from that
   456  // import may not be run anymore. If we still need to run an init for an import
   457  // which is no longer used, add it to the overlay as a blank (`_`) import.
   458  //
   459  // This uses the given name or guesses at the name using the import path,
   460  // meaning this doesn't work for packages which have a different package name
   461  // from the path, including those paths which are versioned
   462  // (e.g. `github.com/foo/bar/v2` where the package name is `bar`)
   463  // or if the import is defined using a relative path (e.g. `./..`).
   464  // Those cases don't exist in the native for Go, so we should only run
   465  // this pruning when we have native overlays, but not for unknown packages.
   466  func pruneImports(file *ast.File) {
   467  	if isOnlyImports(file) && !astutil.HasDirectivePrefix(file, `//go:linkname `) {
   468  		// The file is empty, remove all imports including any `.` or `_` imports.
   469  		file.Imports = nil
   470  		file.Decls = nil
   471  		return
   472  	}
   473  
   474  	unused := make(map[string]int, len(file.Imports))
   475  	for i, in := range file.Imports {
   476  		if name := astutil.ImportName(in); len(name) > 0 {
   477  			unused[name] = i
   478  		}
   479  	}
   480  
   481  	// Remove "unused imports" for any import which is used.
   482  	ast.Inspect(file, func(n ast.Node) bool {
   483  		if sel, ok := n.(*ast.SelectorExpr); ok {
   484  			if id, ok := sel.X.(*ast.Ident); ok && id.Obj == nil {
   485  				delete(unused, id.Name)
   486  			}
   487  		}
   488  		return len(unused) > 0
   489  	})
   490  	if len(unused) == 0 {
   491  		return
   492  	}
   493  
   494  	// Remove "unused imports" for any import used for a directive.
   495  	directiveImports := map[string]string{
   496  		`unsafe`: `//go:linkname `,
   497  		`embed`:  `//go:embed `,
   498  	}
   499  	for name, index := range unused {
   500  		in := file.Imports[index]
   501  		path, _ := strconv.Unquote(in.Path.Value)
   502  		directivePrefix, hasPath := directiveImports[path]
   503  		if hasPath && astutil.HasDirectivePrefix(file, directivePrefix) {
   504  			// since the import is otherwise unused set the name to blank.
   505  			in.Name = ast.NewIdent(`_`)
   506  			delete(unused, name)
   507  		}
   508  	}
   509  	if len(unused) == 0 {
   510  		return
   511  	}
   512  
   513  	// Remove all unused import specifications
   514  	isUnusedSpec := map[*ast.ImportSpec]bool{}
   515  	for _, index := range unused {
   516  		isUnusedSpec[file.Imports[index]] = true
   517  	}
   518  	for _, decl := range file.Decls {
   519  		if d, ok := decl.(*ast.GenDecl); ok {
   520  			for i, spec := range d.Specs {
   521  				if other, ok := spec.(*ast.ImportSpec); ok && isUnusedSpec[other] {
   522  					d.Specs[i] = nil
   523  				}
   524  			}
   525  		}
   526  	}
   527  
   528  	// Remove the unused import copies in the file
   529  	for _, index := range unused {
   530  		file.Imports[index] = nil
   531  	}
   532  
   533  	finalizeRemovals(file)
   534  }
   535  
   536  // finalizeRemovals fully removes any declaration, specification, imports
   537  // that have been set to nil. This will also remove any unassociated comment
   538  // groups, including the comments from removed code.
   539  func finalizeRemovals(file *ast.File) {
   540  	fileChanged := false
   541  	for i, decl := range file.Decls {
   542  		switch d := decl.(type) {
   543  		case nil:
   544  			fileChanged = true
   545  		case *ast.GenDecl:
   546  			declChanged := false
   547  			for j, spec := range d.Specs {
   548  				switch s := spec.(type) {
   549  				case nil:
   550  					declChanged = true
   551  				case *ast.ValueSpec:
   552  					specChanged := false
   553  					for _, name := range s.Names {
   554  						if name == nil {
   555  							specChanged = true
   556  							break
   557  						}
   558  					}
   559  					if specChanged {
   560  						s.Names = astutil.Squeeze(s.Names)
   561  						s.Values = astutil.Squeeze(s.Values)
   562  						if len(s.Names) == 0 {
   563  							declChanged = true
   564  							d.Specs[j] = nil
   565  						}
   566  					}
   567  				}
   568  			}
   569  			if declChanged {
   570  				d.Specs = astutil.Squeeze(d.Specs)
   571  				if len(d.Specs) == 0 {
   572  					fileChanged = true
   573  					file.Decls[i] = nil
   574  				}
   575  			}
   576  		}
   577  	}
   578  	if fileChanged {
   579  		file.Decls = astutil.Squeeze(file.Decls)
   580  	}
   581  
   582  	file.Imports = astutil.Squeeze(file.Imports)
   583  
   584  	file.Comments = nil // clear this first so ast.Inspect doesn't walk it.
   585  	remComments := []*ast.CommentGroup{}
   586  	ast.Inspect(file, func(n ast.Node) bool {
   587  		if cg, ok := n.(*ast.CommentGroup); ok {
   588  			remComments = append(remComments, cg)
   589  		}
   590  		return true
   591  	})
   592  	file.Comments = remComments
   593  }
   594  
   595  // Options controls build process behavior.
   596  type Options struct {
   597  	Verbose        bool
   598  	Quiet          bool
   599  	Watch          bool
   600  	CreateMapFile  bool
   601  	MapToLocalDisk bool
   602  	Minify         bool
   603  	Color          bool
   604  	BuildTags      []string
   605  	TestedPackage  string
   606  	NoCache        bool
   607  }
   608  
   609  // PrintError message to the terminal.
   610  func (o *Options) PrintError(format string, a ...interface{}) {
   611  	if o.Color {
   612  		format = "\x1B[31m" + format + "\x1B[39m"
   613  	}
   614  	fmt.Fprintf(os.Stderr, format, a...)
   615  }
   616  
   617  // PrintSuccess message to the terminal.
   618  func (o *Options) PrintSuccess(format string, a ...interface{}) {
   619  	if o.Color {
   620  		format = "\x1B[32m" + format + "\x1B[39m"
   621  	}
   622  	fmt.Fprintf(os.Stderr, format, a...)
   623  }
   624  
   625  // JSFile represents a *.inc.js file metadata and content.
   626  type JSFile struct {
   627  	Path    string // Full file path for the build context the file came from.
   628  	ModTime time.Time
   629  	Content []byte
   630  }
   631  
   632  // PackageData is an extension of go/build.Package with additional metadata
   633  // GopherJS requires.
   634  type PackageData struct {
   635  	*build.Package
   636  	JSFiles []JSFile
   637  	// IsTest is true if the package is being built for running tests.
   638  	IsTest     bool
   639  	SrcModTime time.Time
   640  	UpToDate   bool
   641  	// If true, the package does not have a corresponding physical directory on disk.
   642  	IsVirtual bool
   643  
   644  	bctx *build.Context // The original build context this package came from.
   645  }
   646  
   647  func (p PackageData) String() string {
   648  	return fmt.Sprintf("%s [is_test=%v]", p.ImportPath, p.IsTest)
   649  }
   650  
   651  // FileModTime returns the most recent modification time of the package's source
   652  // files. This includes all .go and .inc.js that would be included in the build,
   653  // but excludes any dependencies.
   654  func (p PackageData) FileModTime() time.Time {
   655  	newest := time.Time{}
   656  	for _, file := range p.JSFiles {
   657  		if file.ModTime.After(newest) {
   658  			newest = file.ModTime
   659  		}
   660  	}
   661  
   662  	// Unfortunately, build.Context methods don't allow us to Stat and individual
   663  	// file, only to enumerate a directory. So we first get mtimes for all files
   664  	// in the package directory, and then pick the newest for the relevant GoFiles.
   665  	mtimes := map[string]time.Time{}
   666  	files, err := buildutil.ReadDir(p.bctx, p.Dir)
   667  	if err != nil {
   668  		log.Errorf("Failed to enumerate files in the %q in context %v: %s. Assuming time.Now().", p.Dir, p.bctx, err)
   669  		return time.Now()
   670  	}
   671  	for _, file := range files {
   672  		mtimes[file.Name()] = file.ModTime()
   673  	}
   674  
   675  	for _, file := range p.GoFiles {
   676  		t, ok := mtimes[file]
   677  		if !ok {
   678  			log.Errorf("No mtime found for source file %q of package %q, assuming time.Now().", file, p.Name)
   679  			return time.Now()
   680  		}
   681  		if t.After(newest) {
   682  			newest = t
   683  		}
   684  	}
   685  	return newest
   686  }
   687  
   688  // InternalBuildContext returns the build context that produced the package.
   689  //
   690  // WARNING: This function is a part of internal API and will be removed in
   691  // future.
   692  func (p *PackageData) InternalBuildContext() *build.Context {
   693  	return p.bctx
   694  }
   695  
   696  // TestPackage returns a variant of the package with "internal" tests.
   697  func (p *PackageData) TestPackage() *PackageData {
   698  	return &PackageData{
   699  		Package: &build.Package{
   700  			Name:            p.Name,
   701  			ImportPath:      p.ImportPath,
   702  			Dir:             p.Dir,
   703  			GoFiles:         append(p.GoFiles, p.TestGoFiles...),
   704  			Imports:         append(p.Imports, p.TestImports...),
   705  			EmbedPatternPos: joinEmbedPatternPos(p.EmbedPatternPos, p.TestEmbedPatternPos),
   706  		},
   707  		IsTest:  true,
   708  		JSFiles: p.JSFiles,
   709  		bctx:    p.bctx,
   710  	}
   711  }
   712  
   713  // XTestPackage returns a variant of the package with "external" tests.
   714  func (p *PackageData) XTestPackage() *PackageData {
   715  	return &PackageData{
   716  		Package: &build.Package{
   717  			Name:            p.Name + "_test",
   718  			ImportPath:      p.ImportPath + "_test",
   719  			Dir:             p.Dir,
   720  			GoFiles:         p.XTestGoFiles,
   721  			Imports:         p.XTestImports,
   722  			EmbedPatternPos: p.XTestEmbedPatternPos,
   723  		},
   724  		IsTest: true,
   725  		bctx:   p.bctx,
   726  	}
   727  }
   728  
   729  // InstallPath returns the path where "gopherjs install" command should place the
   730  // generated output.
   731  func (p *PackageData) InstallPath() string {
   732  	if p.IsCommand() {
   733  		name := filepath.Base(p.ImportPath) + ".js"
   734  		// For executable packages, mimic go tool behavior if possible.
   735  		if gobin := os.Getenv("GOBIN"); gobin != "" {
   736  			return filepath.Join(gobin, name)
   737  		} else if gopath := os.Getenv("GOPATH"); gopath != "" {
   738  			return filepath.Join(gopath, "bin", name)
   739  		} else if home, err := os.UserHomeDir(); err == nil {
   740  			return filepath.Join(home, "go", "bin", name)
   741  		}
   742  	}
   743  	return p.PkgObj
   744  }
   745  
   746  // Session manages internal state GopherJS requires to perform a build.
   747  //
   748  // This is the main interface to GopherJS build system. Session lifetime is
   749  // roughly equivalent to a single GopherJS tool invocation.
   750  type Session struct {
   751  	options    *Options
   752  	xctx       XContext
   753  	buildCache cache.BuildCache
   754  
   755  	// Binary archives produced during the current session and assumed to be
   756  	// up to date with input sources and dependencies. In the -w ("watch") mode
   757  	// must be cleared upon entering watching.
   758  	UpToDateArchives map[string]*compiler.Archive
   759  	Types            map[string]*types.Package
   760  	Watcher          *fsnotify.Watcher
   761  }
   762  
   763  // NewSession creates a new GopherJS build session.
   764  func NewSession(options *Options) (*Session, error) {
   765  	options.Verbose = options.Verbose || options.Watch
   766  
   767  	s := &Session{
   768  		options:          options,
   769  		UpToDateArchives: make(map[string]*compiler.Archive),
   770  	}
   771  	s.xctx = NewBuildContext(s.InstallSuffix(), s.options.BuildTags)
   772  	env := s.xctx.Env()
   773  
   774  	// Go distribution version check.
   775  	if err := compiler.CheckGoVersion(env.GOROOT); err != nil {
   776  		return nil, err
   777  	}
   778  
   779  	s.buildCache = cache.BuildCache{
   780  		GOOS:          env.GOOS,
   781  		GOARCH:        env.GOARCH,
   782  		GOROOT:        env.GOROOT,
   783  		GOPATH:        env.GOPATH,
   784  		BuildTags:     append([]string{}, env.BuildTags...),
   785  		Minify:        options.Minify,
   786  		TestedPackage: options.TestedPackage,
   787  	}
   788  	s.Types = make(map[string]*types.Package)
   789  	if options.Watch {
   790  		if out, err := exec.Command("ulimit", "-n").Output(); err == nil {
   791  			if n, err := strconv.Atoi(strings.TrimSpace(string(out))); err == nil && n < 1024 {
   792  				fmt.Printf("Warning: The maximum number of open file descriptors is very low (%d). Change it with 'ulimit -n 8192'.\n", n)
   793  			}
   794  		}
   795  
   796  		var err error
   797  		s.Watcher, err = fsnotify.NewWatcher()
   798  		if err != nil {
   799  			return nil, err
   800  		}
   801  	}
   802  	return s, nil
   803  }
   804  
   805  // XContext returns the session's build context.
   806  func (s *Session) XContext() XContext { return s.xctx }
   807  
   808  // InstallSuffix returns the suffix added to the generated output file.
   809  func (s *Session) InstallSuffix() string {
   810  	if s.options.Minify {
   811  		return "min"
   812  	}
   813  	return ""
   814  }
   815  
   816  // GoRelease returns Go release version this session is building with.
   817  func (s *Session) GoRelease() string {
   818  	return compiler.GoRelease(s.xctx.Env().GOROOT)
   819  }
   820  
   821  // BuildFiles passed to the GopherJS tool as if they were a package.
   822  //
   823  // A ephemeral package will be created with only the provided files. This
   824  // function is intended for use with, for example, `gopherjs run main.go`.
   825  func (s *Session) BuildFiles(filenames []string, pkgObj string, cwd string) error {
   826  	if len(filenames) == 0 {
   827  		return fmt.Errorf("no input sources are provided")
   828  	}
   829  
   830  	normalizedDir := func(filename string) string {
   831  		d := filepath.Dir(filename)
   832  		if !filepath.IsAbs(d) {
   833  			d = filepath.Join(cwd, d)
   834  		}
   835  		return filepath.Clean(d)
   836  	}
   837  
   838  	// Ensure all source files are in the same directory.
   839  	dirSet := map[string]bool{}
   840  	for _, file := range filenames {
   841  		dirSet[normalizedDir(file)] = true
   842  	}
   843  	dirList := []string{}
   844  	for dir := range dirSet {
   845  		dirList = append(dirList, dir)
   846  	}
   847  	sort.Strings(dirList)
   848  	if len(dirList) != 1 {
   849  		return fmt.Errorf("named files must all be in one directory; have: %v", strings.Join(dirList, ", "))
   850  	}
   851  
   852  	root := dirList[0]
   853  	ctx := build.Default
   854  	ctx.UseAllFiles = true
   855  	ctx.ReadDir = func(dir string) ([]fs.FileInfo, error) {
   856  		n := len(filenames)
   857  		infos := make([]fs.FileInfo, n)
   858  		for i := 0; i < n; i++ {
   859  			info, err := os.Stat(filenames[i])
   860  			if err != nil {
   861  				return nil, err
   862  			}
   863  			infos[i] = info
   864  		}
   865  		return infos, nil
   866  	}
   867  	p, err := ctx.Import(".", root, 0)
   868  	if err != nil {
   869  		return err
   870  	}
   871  	p.Name = "main"
   872  	p.ImportPath = "main"
   873  
   874  	pkg := &PackageData{
   875  		Package: p,
   876  		// This ephemeral package doesn't have a unique import path to be used as a
   877  		// build cache key, so we never cache it.
   878  		SrcModTime: time.Now().Add(time.Hour),
   879  		bctx:       &goCtx(s.xctx.Env()).bctx,
   880  	}
   881  
   882  	for _, file := range filenames {
   883  		if !strings.HasSuffix(file, ".inc.js") {
   884  			continue
   885  		}
   886  
   887  		content, err := os.ReadFile(file)
   888  		if err != nil {
   889  			return fmt.Errorf("failed to read %s: %w", file, err)
   890  		}
   891  		info, err := os.Stat(file)
   892  		if err != nil {
   893  			return fmt.Errorf("failed to stat %s: %w", file, err)
   894  		}
   895  		pkg.JSFiles = append(pkg.JSFiles, JSFile{
   896  			Path:    filepath.Join(pkg.Dir, filepath.Base(file)),
   897  			ModTime: info.ModTime(),
   898  			Content: content,
   899  		})
   900  	}
   901  
   902  	archive, err := s.BuildPackage(pkg)
   903  	if err != nil {
   904  		return err
   905  	}
   906  	if s.Types["main"].Name() != "main" {
   907  		return fmt.Errorf("cannot build/run non-main package")
   908  	}
   909  	return s.WriteCommandPackage(archive, pkgObj)
   910  }
   911  
   912  // BuildImportPath loads and compiles package with the given import path.
   913  //
   914  // Relative paths are interpreted relative to the current working dir.
   915  func (s *Session) BuildImportPath(path string) (*compiler.Archive, error) {
   916  	_, archive, err := s.buildImportPathWithSrcDir(path, "")
   917  	return archive, err
   918  }
   919  
   920  // buildImportPathWithSrcDir builds the package specified by the import path.
   921  //
   922  // Relative import paths are interpreted relative to the passed srcDir. If
   923  // srcDir is empty, current working directory is assumed.
   924  func (s *Session) buildImportPathWithSrcDir(path string, srcDir string) (*PackageData, *compiler.Archive, error) {
   925  	pkg, err := s.xctx.Import(path, srcDir, 0)
   926  	if s.Watcher != nil && pkg != nil { // add watch even on error
   927  		s.Watcher.Add(pkg.Dir)
   928  	}
   929  	if err != nil {
   930  		return nil, nil, err
   931  	}
   932  
   933  	archive, err := s.BuildPackage(pkg)
   934  	if err != nil {
   935  		return nil, nil, err
   936  	}
   937  
   938  	return pkg, archive, nil
   939  }
   940  
   941  // BuildPackage compiles an already loaded package.
   942  func (s *Session) BuildPackage(pkg *PackageData) (*compiler.Archive, error) {
   943  	if archive, ok := s.UpToDateArchives[pkg.ImportPath]; ok {
   944  		return archive, nil
   945  	}
   946  
   947  	var fileInfo os.FileInfo
   948  	gopherjsBinary, err := os.Executable()
   949  	if err == nil {
   950  		fileInfo, err = os.Stat(gopherjsBinary)
   951  		if err == nil && fileInfo.ModTime().After(pkg.SrcModTime) {
   952  			pkg.SrcModTime = fileInfo.ModTime()
   953  		}
   954  	}
   955  	if err != nil {
   956  		os.Stderr.WriteString("Could not get GopherJS binary's modification timestamp. Please report issue.\n")
   957  		pkg.SrcModTime = time.Now()
   958  	}
   959  
   960  	for _, importedPkgPath := range pkg.Imports {
   961  		if importedPkgPath == "unsafe" {
   962  			continue
   963  		}
   964  		importedPkg, _, err := s.buildImportPathWithSrcDir(importedPkgPath, pkg.Dir)
   965  		if err != nil {
   966  			return nil, err
   967  		}
   968  
   969  		impModTime := importedPkg.SrcModTime
   970  		if impModTime.After(pkg.SrcModTime) {
   971  			pkg.SrcModTime = impModTime
   972  		}
   973  	}
   974  
   975  	if pkg.FileModTime().After(pkg.SrcModTime) {
   976  		pkg.SrcModTime = pkg.FileModTime()
   977  	}
   978  
   979  	if !s.options.NoCache {
   980  		archive := s.buildCache.LoadArchive(pkg.ImportPath)
   981  		if archive != nil && !pkg.SrcModTime.After(archive.BuildTime) {
   982  			if err := archive.RegisterTypes(s.Types); err != nil {
   983  				panic(fmt.Errorf("failed to load type information from %v: %w", archive, err))
   984  			}
   985  			s.UpToDateArchives[pkg.ImportPath] = archive
   986  			// Existing archive is up to date, no need to build it from scratch.
   987  			return archive, nil
   988  		}
   989  	}
   990  
   991  	// Existing archive is out of date or doesn't exist, let's build the package.
   992  	fileSet := token.NewFileSet()
   993  	files, overlayJsFiles, err := parseAndAugment(s.xctx, pkg, pkg.IsTest, fileSet)
   994  	if err != nil {
   995  		return nil, err
   996  	}
   997  	embed, err := embedFiles(pkg, fileSet, files)
   998  	if err != nil {
   999  		return nil, err
  1000  	}
  1001  	if embed != nil {
  1002  		files = append(files, embed)
  1003  	}
  1004  
  1005  	importContext := &compiler.ImportContext{
  1006  		Packages: s.Types,
  1007  		Import:   s.ImportResolverFor(pkg),
  1008  	}
  1009  	archive, err := compiler.Compile(pkg.ImportPath, files, fileSet, importContext, s.options.Minify)
  1010  	if err != nil {
  1011  		return nil, err
  1012  	}
  1013  
  1014  	for _, jsFile := range append(pkg.JSFiles, overlayJsFiles...) {
  1015  		archive.IncJSCode = append(archive.IncJSCode, []byte("\t(function() {\n")...)
  1016  		archive.IncJSCode = append(archive.IncJSCode, jsFile.Content...)
  1017  		archive.IncJSCode = append(archive.IncJSCode, []byte("\n\t}).call($global);\n")...)
  1018  	}
  1019  
  1020  	if s.options.Verbose {
  1021  		fmt.Println(pkg.ImportPath)
  1022  	}
  1023  
  1024  	s.buildCache.StoreArchive(archive)
  1025  	s.UpToDateArchives[pkg.ImportPath] = archive
  1026  
  1027  	return archive, nil
  1028  }
  1029  
  1030  // ImportResolverFor returns a function which returns a compiled package archive
  1031  // given an import path.
  1032  func (s *Session) ImportResolverFor(pkg *PackageData) func(string) (*compiler.Archive, error) {
  1033  	return func(path string) (*compiler.Archive, error) {
  1034  		if archive, ok := s.UpToDateArchives[path]; ok {
  1035  			return archive, nil
  1036  		}
  1037  		_, archive, err := s.buildImportPathWithSrcDir(path, pkg.Dir)
  1038  		return archive, err
  1039  	}
  1040  }
  1041  
  1042  // SourceMappingCallback returns a call back for compiler.SourceMapFilter
  1043  // configured for the current build session.
  1044  func (s *Session) SourceMappingCallback(m *sourcemap.Map) func(generatedLine, generatedColumn int, originalPos token.Position) {
  1045  	return NewMappingCallback(m, s.xctx.Env().GOROOT, s.xctx.Env().GOPATH, s.options.MapToLocalDisk)
  1046  }
  1047  
  1048  // WriteCommandPackage writes the final JavaScript output file at pkgObj path.
  1049  func (s *Session) WriteCommandPackage(archive *compiler.Archive, pkgObj string) error {
  1050  	if err := os.MkdirAll(filepath.Dir(pkgObj), 0o777); err != nil {
  1051  		return err
  1052  	}
  1053  	codeFile, err := os.Create(pkgObj)
  1054  	if err != nil {
  1055  		return err
  1056  	}
  1057  	defer codeFile.Close()
  1058  
  1059  	sourceMapFilter := &compiler.SourceMapFilter{Writer: codeFile}
  1060  	if s.options.CreateMapFile {
  1061  		m := &sourcemap.Map{File: filepath.Base(pkgObj)}
  1062  		mapFile, err := os.Create(pkgObj + ".map")
  1063  		if err != nil {
  1064  			return err
  1065  		}
  1066  
  1067  		defer func() {
  1068  			m.WriteTo(mapFile)
  1069  			mapFile.Close()
  1070  			fmt.Fprintf(codeFile, "//# sourceMappingURL=%s.map\n", filepath.Base(pkgObj))
  1071  		}()
  1072  
  1073  		sourceMapFilter.MappingCallback = s.SourceMappingCallback(m)
  1074  	}
  1075  
  1076  	deps, err := compiler.ImportDependencies(archive, func(path string) (*compiler.Archive, error) {
  1077  		if archive, ok := s.UpToDateArchives[path]; ok {
  1078  			return archive, nil
  1079  		}
  1080  		_, archive, err := s.buildImportPathWithSrcDir(path, "")
  1081  		return archive, err
  1082  	})
  1083  	if err != nil {
  1084  		return err
  1085  	}
  1086  	return compiler.WriteProgramCode(deps, sourceMapFilter, s.GoRelease())
  1087  }
  1088  
  1089  // NewMappingCallback creates a new callback for source map generation.
  1090  func NewMappingCallback(m *sourcemap.Map, goroot, gopath string, localMap bool) func(generatedLine, generatedColumn int, originalPos token.Position) {
  1091  	return func(generatedLine, generatedColumn int, originalPos token.Position) {
  1092  		if !originalPos.IsValid() {
  1093  			m.AddMapping(&sourcemap.Mapping{GeneratedLine: generatedLine, GeneratedColumn: generatedColumn})
  1094  			return
  1095  		}
  1096  
  1097  		file := originalPos.Filename
  1098  
  1099  		switch hasGopathPrefix, prefixLen := hasGopathPrefix(file, gopath); {
  1100  		case localMap:
  1101  			// no-op:  keep file as-is
  1102  		case hasGopathPrefix:
  1103  			file = filepath.ToSlash(file[prefixLen+4:])
  1104  		case strings.HasPrefix(file, goroot):
  1105  			file = filepath.ToSlash(file[len(goroot)+4:])
  1106  		default:
  1107  			file = filepath.Base(file)
  1108  		}
  1109  
  1110  		m.AddMapping(&sourcemap.Mapping{GeneratedLine: generatedLine, GeneratedColumn: generatedColumn, OriginalFile: file, OriginalLine: originalPos.Line, OriginalColumn: originalPos.Column})
  1111  	}
  1112  }
  1113  
  1114  // hasGopathPrefix returns true and the length of the matched GOPATH workspace,
  1115  // iff file has a prefix that matches one of the GOPATH workspaces.
  1116  func hasGopathPrefix(file, gopath string) (hasGopathPrefix bool, prefixLen int) {
  1117  	gopathWorkspaces := filepath.SplitList(gopath)
  1118  	for _, gopathWorkspace := range gopathWorkspaces {
  1119  		gopathWorkspace = filepath.Clean(gopathWorkspace)
  1120  		if strings.HasPrefix(file, gopathWorkspace) {
  1121  			return true, len(gopathWorkspace)
  1122  		}
  1123  	}
  1124  	return false, 0
  1125  }
  1126  
  1127  // WaitForChange watches file system events and returns if either when one of
  1128  // the source files is modified.
  1129  func (s *Session) WaitForChange() {
  1130  	// Will need to re-validate up-to-dateness of all archives, so flush them from
  1131  	// memory.
  1132  	s.UpToDateArchives = map[string]*compiler.Archive{}
  1133  	s.Types = map[string]*types.Package{}
  1134  
  1135  	s.options.PrintSuccess("watching for changes...\n")
  1136  	for {
  1137  		select {
  1138  		case ev := <-s.Watcher.Events:
  1139  			if ev.Op&(fsnotify.Create|fsnotify.Write|fsnotify.Remove|fsnotify.Rename) == 0 || filepath.Base(ev.Name)[0] == '.' {
  1140  				continue
  1141  			}
  1142  			if !strings.HasSuffix(ev.Name, ".go") && !strings.HasSuffix(ev.Name, ".inc.js") {
  1143  				continue
  1144  			}
  1145  			s.options.PrintSuccess("change detected: %s\n", ev.Name)
  1146  		case err := <-s.Watcher.Errors:
  1147  			s.options.PrintError("watcher error: %s\n", err.Error())
  1148  		}
  1149  		break
  1150  	}
  1151  
  1152  	go func() {
  1153  		for range s.Watcher.Events {
  1154  			// consume, else Close() may deadlock
  1155  		}
  1156  	}()
  1157  	s.Watcher.Close()
  1158  }