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

     1  package main
     2  
     3  import (
     4  	"bytes"
     5  	"errors"
     6  	"fmt"
     7  	"go/ast"
     8  	"go/build"
     9  	"go/scanner"
    10  	"go/token"
    11  	"go/types"
    12  	"io"
    13  	"net"
    14  	"net/http"
    15  	"os"
    16  	"os/exec"
    17  	"path"
    18  	"path/filepath"
    19  	"runtime"
    20  	"runtime/pprof"
    21  	"strconv"
    22  	"strings"
    23  	"sync"
    24  	"syscall"
    25  	"text/template"
    26  	"time"
    27  
    28  	gbuild "github.com/gopherjs/gopherjs/build"
    29  	"github.com/gopherjs/gopherjs/build/cache"
    30  	"github.com/gopherjs/gopherjs/compiler"
    31  	"github.com/gopherjs/gopherjs/internal/sysutil"
    32  	"github.com/gopherjs/gopherjs/internal/testmain"
    33  	"github.com/neelance/sourcemap"
    34  	log "github.com/sirupsen/logrus"
    35  	"github.com/spf13/cobra"
    36  	"github.com/spf13/pflag"
    37  	"golang.org/x/sync/errgroup"
    38  	"golang.org/x/term"
    39  )
    40  
    41  var currentDirectory string
    42  
    43  func init() {
    44  	var err error
    45  	currentDirectory, err = os.Getwd()
    46  	if err != nil {
    47  		fmt.Fprintln(os.Stderr, err)
    48  		os.Exit(1)
    49  	}
    50  	currentDirectory, err = filepath.EvalSymlinks(currentDirectory)
    51  	if err != nil {
    52  		fmt.Fprintln(os.Stderr, err)
    53  		os.Exit(1)
    54  	}
    55  	gopaths := filepath.SplitList(build.Default.GOPATH)
    56  	if len(gopaths) == 0 {
    57  		fmt.Fprintf(os.Stderr, "$GOPATH not set. For more details see: go help gopath\n")
    58  		os.Exit(1)
    59  	}
    60  
    61  	e := gbuild.DefaultEnv()
    62  	if e.GOOS != "js" || e.GOARCH != "ecmascript" {
    63  		fmt.Fprintf(os.Stderr, "Using GOOS=%s and GOARCH=%s in GopherJS is deprecated and will be removed in future. Use GOOS=js GOARCH=ecmascript instead.\n", e.GOOS, e.GOARCH)
    64  	}
    65  }
    66  
    67  func main() {
    68  	var (
    69  		options = &gbuild.Options{}
    70  		pkgObj  string
    71  		tags    string
    72  	)
    73  
    74  	flagVerbose := pflag.NewFlagSet("", 0)
    75  	flagVerbose.BoolVarP(&options.Verbose, "verbose", "v", false, "print the names of packages as they are compiled")
    76  	flagQuiet := pflag.NewFlagSet("", 0)
    77  	flagQuiet.BoolVarP(&options.Quiet, "quiet", "q", false, "suppress non-fatal warnings")
    78  
    79  	compilerFlags := pflag.NewFlagSet("", 0)
    80  	compilerFlags.BoolVarP(&options.Minify, "minify", "m", false, "minify generated code")
    81  	compilerFlags.BoolVar(&options.Color, "color", term.IsTerminal(int(os.Stderr.Fd())) && os.Getenv("TERM") != "dumb", "colored output")
    82  	compilerFlags.StringVar(&tags, "tags", "", "a list of build tags to consider satisfied during the build")
    83  	compilerFlags.BoolVar(&options.MapToLocalDisk, "localmap", false, "use local paths for sourcemap")
    84  	compilerFlags.BoolVarP(&options.NoCache, "no_cache", "a", false, "rebuild all packages from scratch")
    85  	compilerFlags.BoolVarP(&options.CreateMapFile, "source_map", "s", true, "enable generation of source maps")
    86  
    87  	flagWatch := pflag.NewFlagSet("", 0)
    88  	flagWatch.BoolVarP(&options.Watch, "watch", "w", false, "watch for changes to the source files")
    89  
    90  	cmdBuild := &cobra.Command{
    91  		Use:   "build [packages]",
    92  		Short: "compile packages and dependencies",
    93  	}
    94  	cmdBuild.Flags().StringVarP(&pkgObj, "output", "o", "", "output file")
    95  	cmdBuild.Flags().AddFlagSet(flagVerbose)
    96  	cmdBuild.Flags().AddFlagSet(flagQuiet)
    97  	cmdBuild.Flags().AddFlagSet(compilerFlags)
    98  	cmdBuild.Flags().AddFlagSet(flagWatch)
    99  	cmdBuild.RunE = func(cmd *cobra.Command, args []string) error {
   100  		options.BuildTags = strings.Fields(tags)
   101  		for {
   102  			s, err := gbuild.NewSession(options)
   103  			if err != nil {
   104  				options.PrintError("%s\n", err)
   105  				return err
   106  			}
   107  
   108  			err = func() error {
   109  				// Handle "gopherjs build [files]" ad-hoc package mode.
   110  				if len(args) > 0 && (strings.HasSuffix(args[0], ".go") || strings.HasSuffix(args[0], ".inc.js")) {
   111  					for _, arg := range args {
   112  						if !strings.HasSuffix(arg, ".go") && !strings.HasSuffix(arg, ".inc.js") {
   113  							return fmt.Errorf("named files must be .go or .inc.js files")
   114  						}
   115  					}
   116  					if pkgObj == "" {
   117  						basename := filepath.Base(args[0])
   118  						pkgObj = basename[:len(basename)-3] + ".js"
   119  					}
   120  					names := make([]string, len(args))
   121  					for i, name := range args {
   122  						name = filepath.ToSlash(name)
   123  						names[i] = name
   124  						if s.Watcher != nil {
   125  							s.Watcher.Add(name)
   126  						}
   127  					}
   128  					err := s.BuildFiles(args, pkgObj, currentDirectory)
   129  					return err
   130  				}
   131  
   132  				xctx := gbuild.NewBuildContext(s.InstallSuffix(), options.BuildTags)
   133  				// Expand import path patterns.
   134  				pkgs, err := xctx.Match(args)
   135  				if err != nil {
   136  					return fmt.Errorf("failed to expand patterns %v: %w", args, err)
   137  				}
   138  				for _, pkgPath := range pkgs {
   139  					if s.Watcher != nil {
   140  						pkg, err := xctx.Import(pkgPath, currentDirectory, build.FindOnly)
   141  						if err != nil {
   142  							return err
   143  						}
   144  						s.Watcher.Add(pkg.Dir)
   145  					}
   146  					pkg, err := xctx.Import(pkgPath, currentDirectory, 0)
   147  					if err != nil {
   148  						return err
   149  					}
   150  					archive, err := s.BuildPackage(pkg)
   151  					if err != nil {
   152  						return err
   153  					}
   154  					if len(pkgs) == 1 { // Only consider writing output if single package specified.
   155  						if pkgObj == "" {
   156  							pkgObj = filepath.Base(pkg.Dir) + ".js"
   157  						}
   158  						if pkg.IsCommand() && !pkg.UpToDate {
   159  							if err := s.WriteCommandPackage(archive, pkgObj); err != nil {
   160  								return err
   161  							}
   162  						}
   163  					}
   164  				}
   165  				return nil
   166  			}()
   167  
   168  			if s.Watcher == nil {
   169  				return err
   170  			} else if err != nil {
   171  				handleError(err, options, nil)
   172  			}
   173  			s.WaitForChange()
   174  		}
   175  	}
   176  
   177  	cmdInstall := &cobra.Command{
   178  		Use:   "install [packages]",
   179  		Short: "compile and install packages and dependencies",
   180  	}
   181  	cmdInstall.Flags().AddFlagSet(flagVerbose)
   182  	cmdInstall.Flags().AddFlagSet(flagQuiet)
   183  	cmdInstall.Flags().AddFlagSet(compilerFlags)
   184  	cmdInstall.Flags().AddFlagSet(flagWatch)
   185  	cmdInstall.RunE = func(cmd *cobra.Command, args []string) error {
   186  		options.BuildTags = strings.Fields(tags)
   187  		for {
   188  			s, err := gbuild.NewSession(options)
   189  			if err != nil {
   190  				return err
   191  			}
   192  
   193  			err = func() error {
   194  				// Expand import path patterns.
   195  				xctx := gbuild.NewBuildContext(s.InstallSuffix(), options.BuildTags)
   196  				pkgs, err := xctx.Match(args)
   197  				if err != nil {
   198  					return fmt.Errorf("failed to expand patterns %v: %w", args, err)
   199  				}
   200  
   201  				if cmd.Name() == "get" {
   202  					goGet := exec.Command("go", append([]string{"get", "-d", "-tags=js"}, pkgs...)...)
   203  					goGet.Stdout = os.Stdout
   204  					goGet.Stderr = os.Stderr
   205  					if err := goGet.Run(); err != nil {
   206  						return err
   207  					}
   208  				}
   209  				for _, pkgPath := range pkgs {
   210  					pkg, err := xctx.Import(pkgPath, currentDirectory, 0)
   211  					if s.Watcher != nil && pkg != nil { // add watch even on error
   212  						s.Watcher.Add(pkg.Dir)
   213  					}
   214  					if err != nil {
   215  						return err
   216  					}
   217  
   218  					archive, err := s.BuildPackage(pkg)
   219  					if err != nil {
   220  						return err
   221  					}
   222  
   223  					if pkg.IsCommand() && !pkg.UpToDate {
   224  						if err := s.WriteCommandPackage(archive, pkg.InstallPath()); err != nil {
   225  							return err
   226  						}
   227  					}
   228  				}
   229  				return nil
   230  			}()
   231  
   232  			if s.Watcher == nil {
   233  				return err
   234  			} else if err != nil {
   235  				handleError(err, options, nil)
   236  			}
   237  			s.WaitForChange()
   238  		}
   239  	}
   240  
   241  	cmdDoc := &cobra.Command{
   242  		Use:   "doc [arguments]",
   243  		Short: "display documentation for the requested, package, method or symbol",
   244  	}
   245  	cmdDoc.RunE = func(cmd *cobra.Command, args []string) error {
   246  		goDoc := exec.Command("go", append([]string{"doc"}, args...)...)
   247  		goDoc.Stdout = os.Stdout
   248  		goDoc.Stderr = os.Stderr
   249  		goDoc.Env = append(os.Environ(), "GOARCH=js")
   250  		return goDoc.Run()
   251  	}
   252  
   253  	cmdGet := &cobra.Command{
   254  		Use:   "get [packages]",
   255  		Short: "download and install packages and dependencies",
   256  	}
   257  	cmdGet.Flags().AddFlagSet(flagVerbose)
   258  	cmdGet.Flags().AddFlagSet(flagQuiet)
   259  	cmdGet.Flags().AddFlagSet(compilerFlags)
   260  	cmdGet.Run = cmdInstall.Run
   261  
   262  	cmdRun := &cobra.Command{
   263  		Use:   "run [gofiles...] [arguments...]",
   264  		Short: "compile and run Go program",
   265  	}
   266  	cmdRun.Flags().AddFlagSet(flagVerbose)
   267  	cmdRun.Flags().AddFlagSet(flagQuiet)
   268  	cmdRun.Flags().AddFlagSet(compilerFlags)
   269  	cmdRun.RunE = func(cmd *cobra.Command, args []string) error {
   270  		options.BuildTags = strings.Fields(tags)
   271  		lastSourceArg := 0
   272  		for {
   273  			if lastSourceArg == len(args) || !(strings.HasSuffix(args[lastSourceArg], ".go") || strings.HasSuffix(args[lastSourceArg], ".inc.js")) {
   274  				break
   275  			}
   276  			lastSourceArg++
   277  		}
   278  		if lastSourceArg == 0 {
   279  			return fmt.Errorf("gopherjs run: no go files listed")
   280  		}
   281  
   282  		tempfile, err := os.CreateTemp(currentDirectory, filepath.Base(args[0])+".")
   283  		if err != nil && strings.HasPrefix(currentDirectory, runtime.GOROOT()) {
   284  			tempfile, err = os.CreateTemp("", filepath.Base(args[0])+".")
   285  		}
   286  		if err != nil {
   287  			return err
   288  		}
   289  		defer func() {
   290  			tempfile.Close()
   291  			os.Remove(tempfile.Name())
   292  			os.Remove(tempfile.Name() + ".map")
   293  		}()
   294  		s, err := gbuild.NewSession(options)
   295  		if err != nil {
   296  			return err
   297  		}
   298  		if err := s.BuildFiles(args[:lastSourceArg], tempfile.Name(), currentDirectory); err != nil {
   299  			return err
   300  		}
   301  		if err := runNode(tempfile.Name(), args[lastSourceArg:], "", options.Quiet, nil); err != nil {
   302  			return err
   303  		}
   304  		return nil
   305  	}
   306  
   307  	cmdTest := &cobra.Command{
   308  		Use:   "test [packages]",
   309  		Short: "test packages",
   310  	}
   311  	bench := cmdTest.Flags().String("bench", "", "Run benchmarks matching the regular expression. By default, no benchmarks run. To run all benchmarks, use '--bench=.'.")
   312  	benchtime := cmdTest.Flags().String("benchtime", "", "Run enough iterations of each benchmark to take t, specified as a time.Duration (for example, -benchtime 1h30s). The default is 1 second (1s).")
   313  	count := cmdTest.Flags().String("count", "", "Run each test and benchmark n times (default 1). Examples are always run once.")
   314  	run := cmdTest.Flags().String("run", "", "Run only those tests and examples matching the regular expression.")
   315  	short := cmdTest.Flags().Bool("short", false, "Tell long-running tests to shorten their run time.")
   316  	verbose := cmdTest.Flags().BoolP("verbose", "v", false, "Log all tests as they are run. Also print all text from Log and Logf calls even if the test succeeds.")
   317  	compileOnly := cmdTest.Flags().BoolP("compileonly", "c", false, "Compile the test binary to pkg.test.js but do not run it (where pkg is the last element of the package's import path). The file name can be changed with the -o flag.")
   318  	outputFilename := cmdTest.Flags().StringP("output", "o", "", "Compile the test binary to the named file. The test still runs (unless -c is specified).")
   319  	parallelTests := cmdTest.Flags().IntP("parallel", "p", runtime.NumCPU(), "Allow running tests in parallel for up to -p packages. Tests within the same package are still executed sequentially.")
   320  	cmdTest.Flags().AddFlagSet(compilerFlags)
   321  	cmdTest.RunE = func(cmd *cobra.Command, args []string) error {
   322  		options.BuildTags = strings.Fields(tags)
   323  
   324  		// Expand import path patterns.
   325  		patternContext := gbuild.NewBuildContext("", options.BuildTags)
   326  		matches, err := patternContext.Match(args)
   327  		if err != nil {
   328  			return fmt.Errorf("failed to expand patterns %v: %w", args, err)
   329  		}
   330  
   331  		if *compileOnly && len(matches) > 1 {
   332  			return errors.New("cannot use -c flag with multiple packages")
   333  		}
   334  		if *outputFilename != "" && len(matches) > 1 {
   335  			return errors.New("cannot use -o flag with multiple packages")
   336  		}
   337  		if *parallelTests < 1 {
   338  			return errors.New("--parallel cannot be less than 1")
   339  		}
   340  
   341  		parallelSlots := make(chan (bool), *parallelTests) // Semaphore for parallel test executions.
   342  		if len(matches) == 1 {
   343  			// Disable output buffering if testing only one package.
   344  			parallelSlots = make(chan (bool), 1)
   345  		}
   346  		executions := errgroup.Group{}
   347  
   348  		pkgs := make([]*gbuild.PackageData, len(matches))
   349  		for i, pkgPath := range matches {
   350  			var err error
   351  			pkgs[i], err = gbuild.Import(pkgPath, 0, "", options.BuildTags)
   352  			if err != nil {
   353  				return err
   354  			}
   355  		}
   356  
   357  		var (
   358  			exitErr   error
   359  			exitErrMu = &sync.Mutex{}
   360  		)
   361  		for _, pkg := range pkgs {
   362  			pkg := pkg // Capture for the goroutine.
   363  			if len(pkg.TestGoFiles) == 0 && len(pkg.XTestGoFiles) == 0 {
   364  				fmt.Printf("?   \t%s\t[no test files]\n", pkg.ImportPath)
   365  				continue
   366  			}
   367  			localOpts := options
   368  			localOpts.TestedPackage = pkg.ImportPath
   369  			s, err := gbuild.NewSession(localOpts)
   370  			if err != nil {
   371  				return err
   372  			}
   373  
   374  			_, err = s.BuildPackage(pkg.TestPackage())
   375  			if err != nil {
   376  				return err
   377  			}
   378  			_, err = s.BuildPackage(pkg.XTestPackage())
   379  			if err != nil {
   380  				return err
   381  			}
   382  
   383  			fset := token.NewFileSet()
   384  			tests := testmain.TestMain{Package: pkg}
   385  			tests.Scan(fset)
   386  			mainPkg, mainFile, err := tests.Synthesize(fset)
   387  			if err != nil {
   388  				return fmt.Errorf("failed to generate testmain package for %s: %w", pkg.ImportPath, err)
   389  			}
   390  			importContext := &compiler.ImportContext{
   391  				Packages: s.Types,
   392  				Import:   s.ImportResolverFor(mainPkg),
   393  			}
   394  			mainPkgArchive, err := compiler.Compile(mainPkg.ImportPath, []*ast.File{mainFile}, fset, importContext, options.Minify)
   395  			if err != nil {
   396  				return fmt.Errorf("failed to compile testmain package for %s: %w", pkg.ImportPath, err)
   397  			}
   398  
   399  			if *compileOnly && *outputFilename == "" {
   400  				*outputFilename = pkg.Package.Name + "_test.js"
   401  			}
   402  
   403  			var outfile *os.File
   404  			if *outputFilename != "" {
   405  				outfile, err = os.Create(*outputFilename)
   406  				if err != nil {
   407  					return err
   408  				}
   409  			} else {
   410  				outfile, err = os.CreateTemp(currentDirectory, pkg.Package.Name+"_test.*.js")
   411  				if err != nil {
   412  					return err
   413  				}
   414  				outfile.Close() // Release file handle early, we only need the name.
   415  			}
   416  			cleanupTemp := func() {
   417  				if *outputFilename == "" {
   418  					os.Remove(outfile.Name())
   419  					os.Remove(outfile.Name() + ".map")
   420  				}
   421  			}
   422  			defer cleanupTemp() // Safety net in case cleanup after execution doesn't happen.
   423  
   424  			if err := s.WriteCommandPackage(mainPkgArchive, outfile.Name()); err != nil {
   425  				return err
   426  			}
   427  
   428  			if *compileOnly {
   429  				continue
   430  			}
   431  
   432  			var args []string
   433  			if *bench != "" {
   434  				args = append(args, "-test.bench", *bench)
   435  			}
   436  			if *benchtime != "" {
   437  				args = append(args, "-test.benchtime", *benchtime)
   438  			}
   439  			if *count != "" {
   440  				args = append(args, "-test.count", *count)
   441  			}
   442  			if *run != "" {
   443  				args = append(args, "-test.run", *run)
   444  			}
   445  			if *short {
   446  				args = append(args, "-test.short")
   447  			}
   448  			if *verbose {
   449  				args = append(args, "-test.v")
   450  			}
   451  			executions.Go(func() error {
   452  				parallelSlots <- true              // Acquire slot
   453  				defer func() { <-parallelSlots }() // Release slot
   454  
   455  				status := "ok  "
   456  				start := time.Now()
   457  				var testOut io.ReadWriter
   458  				if cap(parallelSlots) > 1 {
   459  					// If running in parallel, capture test output in a temporary buffer to avoid mixing
   460  					// output from different tests and print it later.
   461  					testOut = &bytes.Buffer{}
   462  				}
   463  
   464  				err := runNode(outfile.Name(), args, runTestDir(pkg), options.Quiet, testOut)
   465  
   466  				cleanupTemp() // Eagerly cleanup temporary compiled files after execution.
   467  
   468  				if testOut != nil {
   469  					io.Copy(os.Stdout, testOut)
   470  				}
   471  
   472  				if err != nil {
   473  					if _, ok := err.(*exec.ExitError); !ok {
   474  						return err
   475  					}
   476  					exitErrMu.Lock()
   477  					exitErr = err
   478  					exitErrMu.Unlock()
   479  					status = "FAIL"
   480  				}
   481  				fmt.Printf("%s\t%s\t%.3fs\n", status, pkg.ImportPath, time.Since(start).Seconds())
   482  				return nil
   483  			})
   484  		}
   485  		if err := executions.Wait(); err != nil {
   486  			return err
   487  		}
   488  		return exitErr
   489  	}
   490  
   491  	cmdServe := &cobra.Command{
   492  		Use:   "serve [root]",
   493  		Short: "compile on-the-fly and serve",
   494  	}
   495  	cmdServe.Args = cobra.MaximumNArgs(1)
   496  	cmdServe.Flags().AddFlagSet(flagVerbose)
   497  	cmdServe.Flags().AddFlagSet(flagQuiet)
   498  	cmdServe.Flags().AddFlagSet(compilerFlags)
   499  	var addr string
   500  	cmdServe.Flags().StringVarP(&addr, "http", "", ":8080", "HTTP bind address to serve")
   501  	cmdServe.RunE = func(cmd *cobra.Command, args []string) error {
   502  		options.BuildTags = strings.Fields(tags)
   503  		var root string
   504  
   505  		if len(args) == 1 {
   506  			root = args[0]
   507  		}
   508  
   509  		// Create a new session eagerly to check if it fails, and report the error right away.
   510  		// Otherwise, users will see it only after trying to serve a package, which is a bad experience.
   511  		_, err := gbuild.NewSession(options)
   512  		if err != nil {
   513  			return err
   514  		}
   515  		sourceFiles := http.FileServer(serveCommandFileSystem{
   516  			serveRoot:  root,
   517  			options:    options,
   518  			sourceMaps: make(map[string][]byte),
   519  		})
   520  
   521  		ln, err := net.Listen("tcp", addr)
   522  		if err != nil {
   523  			return err
   524  		}
   525  		if tcpAddr := ln.Addr().(*net.TCPAddr); tcpAddr.IP.Equal(net.IPv4zero) || tcpAddr.IP.Equal(net.IPv6zero) { // Any available addresses.
   526  			fmt.Printf("serving at http://localhost:%d and on port %d of any available addresses\n", tcpAddr.Port, tcpAddr.Port)
   527  		} else { // Specific address.
   528  			fmt.Printf("serving at http://%s\n", tcpAddr)
   529  		}
   530  		fmt.Fprintln(os.Stderr, http.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)}, sourceFiles))
   531  		return nil
   532  	}
   533  
   534  	cmdVersion := &cobra.Command{
   535  		Use:   "version",
   536  		Short: "print GopherJS compiler version",
   537  		Args:  cobra.ExactArgs(0),
   538  	}
   539  	cmdVersion.Run = func(cmd *cobra.Command, args []string) {
   540  		fmt.Printf("GopherJS %s\n", compiler.Version)
   541  	}
   542  
   543  	cmdClean := &cobra.Command{
   544  		Use:   "clean",
   545  		Short: "clean GopherJS build cache",
   546  	}
   547  	cmdClean.RunE = func(cmd *cobra.Command, args []string) error {
   548  		return cache.Clear()
   549  	}
   550  
   551  	rootCmd := &cobra.Command{
   552  		Use:           "gopherjs",
   553  		Long:          "GopherJS is a tool for compiling Go source code to JavaScript.",
   554  		SilenceUsage:  true,
   555  		SilenceErrors: true,
   556  	}
   557  	rootCmd.AddCommand(cmdBuild, cmdGet, cmdInstall, cmdRun, cmdTest, cmdServe, cmdVersion, cmdDoc, cmdClean)
   558  
   559  	{
   560  		var logLevel string
   561  		var cpuProfile string
   562  		var allocProfile string
   563  		rootCmd.PersistentFlags().StringVar(&logLevel, "log_level", log.ErrorLevel.String(), "Compiler log level (debug, info, warn, error, fatal, panic).")
   564  		rootCmd.PersistentFlags().StringVar(&cpuProfile, "cpu_profile", "", "Save GopherJS compiler CPU profile at the given path. If unset, profiling is disabled.")
   565  		rootCmd.PersistentFlags().StringVar(&allocProfile, "alloc_profile", "", "Save GopherJS compiler allocation profile at the given path. If unset, profiling is disabled.")
   566  
   567  		rootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
   568  			lvl, err := log.ParseLevel(logLevel)
   569  			if err != nil {
   570  				return fmt.Errorf("invalid --log_level value %q: %w", logLevel, err)
   571  			}
   572  			log.SetLevel(lvl)
   573  
   574  			if cpuProfile != "" {
   575  				f, err := os.Create(cpuProfile)
   576  				if err != nil {
   577  					return fmt.Errorf("failed to create CPU profile file at %q: %w", cpuProfile, err)
   578  				}
   579  				if err := pprof.StartCPUProfile(f); err != nil {
   580  					return fmt.Errorf("failed to start CPU profile: %w", err)
   581  				}
   582  				// Not closing the file here, since we'll be writing to it throughout
   583  				// the lifetime of the process. It will be closed automatically when
   584  				// the process terminates.
   585  			}
   586  			return nil
   587  		}
   588  		rootCmd.PersistentPostRunE = func(cmd *cobra.Command, args []string) error {
   589  			if cpuProfile != "" {
   590  				pprof.StopCPUProfile()
   591  			}
   592  			if allocProfile != "" {
   593  				f, err := os.Create(allocProfile)
   594  				if err != nil {
   595  					return fmt.Errorf("failed to create alloc profile file at %q: %w", allocProfile, err)
   596  				}
   597  				if err := pprof.Lookup("allocs").WriteTo(f, 0); err != nil {
   598  					return fmt.Errorf("failed to write alloc profile: %w", err)
   599  				}
   600  				f.Close()
   601  			}
   602  			return nil
   603  		}
   604  	}
   605  	err := rootCmd.Execute()
   606  	if err != nil {
   607  		os.Exit(handleError(err, options, nil))
   608  	}
   609  }
   610  
   611  // tcpKeepAliveListener sets TCP keep-alive timeouts on accepted
   612  // connections. It's used by ListenAndServe and ListenAndServeTLS so
   613  // dead TCP connections (e.g. closing laptop mid-download) eventually
   614  // go away.
   615  type tcpKeepAliveListener struct {
   616  	*net.TCPListener
   617  }
   618  
   619  func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) {
   620  	tc, err := ln.AcceptTCP()
   621  	if err != nil {
   622  		return
   623  	}
   624  	tc.SetKeepAlive(true)
   625  	tc.SetKeepAlivePeriod(3 * time.Minute)
   626  	return tc, nil
   627  }
   628  
   629  type serveCommandFileSystem struct {
   630  	serveRoot  string
   631  	options    *gbuild.Options
   632  	sourceMaps map[string][]byte
   633  }
   634  
   635  func (fs serveCommandFileSystem) Open(requestName string) (http.File, error) {
   636  	name := path.Join(fs.serveRoot, requestName[1:]) // requestName[0] == '/'
   637  	log.Printf("Request: %s", name)
   638  
   639  	dir, file := path.Split(name)
   640  	base := path.Base(dir) // base is parent folder name, which becomes the output file name.
   641  
   642  	isPkg := file == base+".js"
   643  	isMap := file == base+".js.map"
   644  	isIndex := file == "index.html"
   645  
   646  	// Create a new session to pick up changes to source code on disk.
   647  	// TODO(dmitshur): might be possible to get a single session to detect changes to source code on disk
   648  	s, err := gbuild.NewSession(fs.options)
   649  	if err != nil {
   650  		return nil, err
   651  	}
   652  
   653  	if isPkg || isMap || isIndex {
   654  		// If we're going to be serving our special files, make sure there's a Go command in this folder.
   655  		pkg, err := gbuild.Import(path.Dir(name), 0, s.InstallSuffix(), fs.options.BuildTags)
   656  		if err != nil || pkg.Name != "main" {
   657  			isPkg = false
   658  			isMap = false
   659  			isIndex = false
   660  		}
   661  
   662  		switch {
   663  		case isPkg:
   664  			buf := new(bytes.Buffer)
   665  			browserErrors := new(bytes.Buffer)
   666  			err := func() error {
   667  				archive, err := s.BuildPackage(pkg)
   668  				if err != nil {
   669  					return err
   670  				}
   671  
   672  				sourceMapFilter := &compiler.SourceMapFilter{Writer: buf}
   673  				m := &sourcemap.Map{File: base + ".js"}
   674  				sourceMapFilter.MappingCallback = s.SourceMappingCallback(m)
   675  
   676  				deps, err := compiler.ImportDependencies(archive, s.BuildImportPath)
   677  				if err != nil {
   678  					return err
   679  				}
   680  				if err := compiler.WriteProgramCode(deps, sourceMapFilter, s.GoRelease()); err != nil {
   681  					return err
   682  				}
   683  
   684  				mapBuf := new(bytes.Buffer)
   685  				m.WriteTo(mapBuf)
   686  				buf.WriteString("//# sourceMappingURL=" + base + ".js.map\n")
   687  				fs.sourceMaps[name+".map"] = mapBuf.Bytes()
   688  
   689  				return nil
   690  			}()
   691  			handleError(err, fs.options, browserErrors)
   692  			if err != nil {
   693  				buf = browserErrors
   694  			}
   695  			return newFakeFile(base+".js", buf.Bytes()), nil
   696  
   697  		case isMap:
   698  			if content, ok := fs.sourceMaps[name]; ok {
   699  				return newFakeFile(base+".js.map", content), nil
   700  			}
   701  		}
   702  	}
   703  
   704  	// First try to serve the request with a root prefix supplied in the CLI.
   705  	if f, err := fs.serveSourceTree(s.XContext(), name); err == nil {
   706  		return f, nil
   707  	}
   708  
   709  	// If that didn't work, try without the prefix.
   710  	if f, err := fs.serveSourceTree(s.XContext(), requestName); err == nil {
   711  		return f, nil
   712  	}
   713  
   714  	if isIndex {
   715  		// If there was no index.html file in any dirs, supply our own.
   716  		return newFakeFile("index.html", []byte(`<html><head><meta charset="utf-8"><script src="`+base+`.js"></script></head><body></body></html>`)), nil
   717  	}
   718  
   719  	return nil, os.ErrNotExist
   720  }
   721  
   722  func (fs serveCommandFileSystem) serveSourceTree(xctx gbuild.XContext, reqPath string) (http.File, error) {
   723  	parts := strings.Split(path.Clean(reqPath), "/")
   724  	// Under Go Modules different packages can be located in different module
   725  	// directories, which no longer align with import paths.
   726  	//
   727  	// We don't know which part of the requested path is package import path and
   728  	// which is a path under the package directory, so we try different split
   729  	// points until the package is found successfully.
   730  	for i := len(parts); i > 0; i-- {
   731  		pkgPath := path.Clean(path.Join(parts[:i]...))
   732  		filePath := path.Clean(path.Join(parts[i:]...))
   733  		if pkg, err := xctx.Import(pkgPath, ".", build.FindOnly); err == nil {
   734  			return http.Dir(pkg.Dir).Open(filePath)
   735  		}
   736  	}
   737  	return nil, os.ErrNotExist
   738  }
   739  
   740  type fakeFile struct {
   741  	name string
   742  	size int
   743  	io.ReadSeeker
   744  }
   745  
   746  func newFakeFile(name string, content []byte) *fakeFile {
   747  	return &fakeFile{name: name, size: len(content), ReadSeeker: bytes.NewReader(content)}
   748  }
   749  
   750  func (f *fakeFile) Close() error {
   751  	return nil
   752  }
   753  
   754  func (f *fakeFile) Readdir(count int) ([]os.FileInfo, error) {
   755  	return nil, os.ErrInvalid
   756  }
   757  
   758  func (f *fakeFile) Stat() (os.FileInfo, error) {
   759  	return f, nil
   760  }
   761  
   762  func (f *fakeFile) Name() string {
   763  	return f.name
   764  }
   765  
   766  func (f *fakeFile) Size() int64 {
   767  	return int64(f.size)
   768  }
   769  
   770  func (f *fakeFile) Mode() os.FileMode {
   771  	return 0
   772  }
   773  
   774  func (f *fakeFile) ModTime() time.Time {
   775  	return time.Time{}
   776  }
   777  
   778  func (f *fakeFile) IsDir() bool {
   779  	return false
   780  }
   781  
   782  func (f *fakeFile) Sys() interface{} {
   783  	return nil
   784  }
   785  
   786  // handleError handles err and returns an appropriate exit code.
   787  // If browserErrors is non-nil, errors are written for presentation in browser.
   788  func handleError(err error, options *gbuild.Options, browserErrors *bytes.Buffer) int {
   789  	switch err := err.(type) {
   790  	case nil:
   791  		return 0
   792  	case compiler.ErrorList:
   793  		for _, entry := range err {
   794  			printError(entry, options, browserErrors)
   795  		}
   796  		return 1
   797  	case *exec.ExitError:
   798  		return err.Sys().(syscall.WaitStatus).ExitStatus()
   799  	default:
   800  		printError(err, options, browserErrors)
   801  		return 1
   802  	}
   803  }
   804  
   805  // printError prints err to Stderr with options. If browserErrors is non-nil, errors are also written for presentation in browser.
   806  func printError(err error, options *gbuild.Options, browserErrors *bytes.Buffer) {
   807  	e := sprintError(err)
   808  	options.PrintError("%s\n", e)
   809  	if browserErrors != nil {
   810  		fmt.Fprintln(browserErrors, `console.error("`+template.JSEscapeString(e)+`");`)
   811  	}
   812  }
   813  
   814  // sprintError returns an annotated error string without trailing newline.
   815  func sprintError(err error) string {
   816  	makeRel := func(name string) string {
   817  		if relname, err := filepath.Rel(currentDirectory, name); err == nil {
   818  			return relname
   819  		}
   820  		return name
   821  	}
   822  
   823  	switch e := err.(type) {
   824  	case *scanner.Error:
   825  		return fmt.Sprintf("%s:%d:%d: %s", makeRel(e.Pos.Filename), e.Pos.Line, e.Pos.Column, e.Msg)
   826  	case types.Error:
   827  		pos := e.Fset.Position(e.Pos)
   828  		return fmt.Sprintf("%s:%d:%d: %s", makeRel(pos.Filename), pos.Line, pos.Column, e.Msg)
   829  	default:
   830  		return fmt.Sprintf("%s", e)
   831  	}
   832  }
   833  
   834  // runNode runs script with args using Node.js in directory dir.
   835  // If dir is empty string, current directory is used.
   836  // Is out is not nil, process stderr and stdout are redirected to it, otherwise
   837  // os.Stdout and os.Stderr are used.
   838  func runNode(script string, args []string, dir string, quiet bool, out io.Writer) error {
   839  	var allArgs []string
   840  	if b, _ := strconv.ParseBool(os.Getenv("SOURCE_MAP_SUPPORT")); os.Getenv("SOURCE_MAP_SUPPORT") == "" || b {
   841  		allArgs = []string{"--require", "source-map-support/register"}
   842  		if err := exec.Command("node", "--require", "source-map-support/register", "--eval", "").Run(); err != nil {
   843  			if !quiet {
   844  				fmt.Fprintln(os.Stderr, "gopherjs: Source maps disabled. Install source-map-support module for nice stack traces. See https://github.com/gopherjs/gopherjs#gopherjs-run-gopherjs-test.")
   845  			}
   846  			allArgs = []string{}
   847  		}
   848  	}
   849  
   850  	if runtime.GOOS != "windows" {
   851  		// We've seen issues with stack space limits causing
   852  		// recursion-heavy standard library tests to fail (e.g., see
   853  		// https://github.com/gopherjs/gopherjs/pull/669#issuecomment-319319483).
   854  		//
   855  		// There are two separate limits in non-Windows environments:
   856  		//
   857  		// -	OS process limit
   858  		// -	Node.js (V8) limit
   859  		//
   860  		// GopherJS fetches the current OS process limit, and sets the Node.js limit
   861  		// to a value slightly below it (otherwise nodejs is likely to segfault).
   862  		// The backoff size has been determined experimentally on a linux machine,
   863  		// so it may not be 100% reliable. So both limits are kept in sync and can
   864  		// be controlled by setting OS process limit. E.g.:
   865  		//
   866  		// 	ulimit -s 10000 && gopherjs test
   867  		//
   868  		cur, err := sysutil.RlimitStack()
   869  		if err != nil {
   870  			return fmt.Errorf("failed to get stack size limit: %v", err)
   871  		}
   872  		cur = cur / 1024           // Convert bytes to KiB.
   873  		defaultSize := uint64(984) // --stack-size default value.
   874  		if backoff := uint64(64); cur > defaultSize+backoff {
   875  			cur = cur - backoff
   876  		}
   877  		allArgs = append(allArgs, fmt.Sprintf("--stack_size=%v", cur))
   878  	}
   879  
   880  	allArgs = append(allArgs, script)
   881  	allArgs = append(allArgs, args...)
   882  
   883  	node := exec.Command("node", allArgs...)
   884  	node.Dir = dir
   885  	node.Stdin = os.Stdin
   886  	if out != nil {
   887  		node.Stdout = out
   888  		node.Stderr = out
   889  	} else {
   890  		node.Stdout = os.Stdout
   891  		node.Stderr = os.Stderr
   892  	}
   893  	err := node.Run()
   894  	if _, ok := err.(*exec.ExitError); err != nil && !ok {
   895  		err = fmt.Errorf("could not run Node.js: %s", err.Error())
   896  	}
   897  	return err
   898  }
   899  
   900  // runTestDir returns the directory for Node.js to use when running tests for package p.
   901  // Empty string means current directory.
   902  func runTestDir(p *gbuild.PackageData) string {
   903  	if p.IsVirtual {
   904  		// The package is virtual and doesn't have a physical directory. Use current directory.
   905  		return ""
   906  	}
   907  	// Run tests in the package directory.
   908  	return p.Dir
   909  }