Permalink
Cannot retrieve contributors at this time
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
1094 lines (982 sloc)
31.8 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"bytes" | |
"errors" | |
"fmt" | |
"go/ast" | |
"go/build" | |
"go/doc" | |
"go/parser" | |
"go/scanner" | |
"go/token" | |
"go/types" | |
"io" | |
"io/ioutil" | |
"net" | |
"net/http" | |
"os" | |
"os/exec" | |
"path" | |
"path/filepath" | |
"runtime" | |
"runtime/pprof" | |
"sort" | |
"strconv" | |
"strings" | |
"sync" | |
"syscall" | |
"text/template" | |
"time" | |
"unicode" | |
"unicode/utf8" | |
gbuild "github.com/gopherjs/gopherjs/build" | |
"github.com/gopherjs/gopherjs/build/cache" | |
"github.com/gopherjs/gopherjs/compiler" | |
"github.com/gopherjs/gopherjs/internal/sysutil" | |
"github.com/neelance/sourcemap" | |
log "github.com/sirupsen/logrus" | |
"github.com/spf13/cobra" | |
"github.com/spf13/pflag" | |
"golang.org/x/crypto/ssh/terminal" | |
"golang.org/x/sync/errgroup" | |
"golang.org/x/tools/go/buildutil" | |
) | |
var currentDirectory string | |
func init() { | |
var err error | |
currentDirectory, err = os.Getwd() | |
if err != nil { | |
fmt.Fprintln(os.Stderr, err) | |
os.Exit(1) | |
} | |
currentDirectory, err = filepath.EvalSymlinks(currentDirectory) | |
if err != nil { | |
fmt.Fprintln(os.Stderr, err) | |
os.Exit(1) | |
} | |
gopaths := filepath.SplitList(build.Default.GOPATH) | |
if len(gopaths) == 0 { | |
fmt.Fprintf(os.Stderr, "$GOPATH not set. For more details see: go help gopath\n") | |
os.Exit(1) | |
} | |
e := gbuild.DefaultEnv() | |
if e.GOOS != "js" || e.GOARCH != "ecmascript" { | |
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) | |
} | |
} | |
func main() { | |
var ( | |
options = &gbuild.Options{CreateMapFile: true} | |
pkgObj string | |
tags string | |
) | |
flagVerbose := pflag.NewFlagSet("", 0) | |
flagVerbose.BoolVarP(&options.Verbose, "verbose", "v", false, "print the names of packages as they are compiled") | |
flagQuiet := pflag.NewFlagSet("", 0) | |
flagQuiet.BoolVarP(&options.Quiet, "quiet", "q", false, "suppress non-fatal warnings") | |
compilerFlags := pflag.NewFlagSet("", 0) | |
compilerFlags.BoolVarP(&options.Minify, "minify", "m", false, "minify generated code") | |
compilerFlags.BoolVar(&options.Color, "color", terminal.IsTerminal(int(os.Stderr.Fd())) && os.Getenv("TERM") != "dumb", "colored output") | |
compilerFlags.StringVar(&tags, "tags", "", "a list of build tags to consider satisfied during the build") | |
compilerFlags.BoolVar(&options.MapToLocalDisk, "localmap", false, "use local paths for sourcemap") | |
compilerFlags.BoolVarP(&options.NoCache, "no_cache", "a", false, "rebuild all packages from scratch") | |
flagWatch := pflag.NewFlagSet("", 0) | |
flagWatch.BoolVarP(&options.Watch, "watch", "w", false, "watch for changes to the source files") | |
cmdBuild := &cobra.Command{ | |
Use: "build [packages]", | |
Short: "compile packages and dependencies", | |
} | |
cmdBuild.Flags().StringVarP(&pkgObj, "output", "o", "", "output file") | |
cmdBuild.Flags().AddFlagSet(flagVerbose) | |
cmdBuild.Flags().AddFlagSet(flagQuiet) | |
cmdBuild.Flags().AddFlagSet(compilerFlags) | |
cmdBuild.Flags().AddFlagSet(flagWatch) | |
cmdBuild.RunE = func(cmd *cobra.Command, args []string) error { | |
options.BuildTags = strings.Fields(tags) | |
for { | |
s, err := gbuild.NewSession(options) | |
if err != nil { | |
options.PrintError("%s\n", err) | |
return err | |
} | |
err = func() error { | |
// Handle "gopherjs build [files]" ad-hoc package mode. | |
if len(args) > 0 && (strings.HasSuffix(args[0], ".go") || strings.HasSuffix(args[0], ".inc.js")) { | |
for _, arg := range args { | |
if !strings.HasSuffix(arg, ".go") && !strings.HasSuffix(arg, ".inc.js") { | |
return fmt.Errorf("named files must be .go or .inc.js files") | |
} | |
} | |
if pkgObj == "" { | |
basename := filepath.Base(args[0]) | |
pkgObj = basename[:len(basename)-3] + ".js" | |
} | |
names := make([]string, len(args)) | |
for i, name := range args { | |
name = filepath.ToSlash(name) | |
names[i] = name | |
if s.Watcher != nil { | |
s.Watcher.Add(name) | |
} | |
} | |
err := s.BuildFiles(args, pkgObj, currentDirectory) | |
return err | |
} | |
xctx := gbuild.NewBuildContext(s.InstallSuffix(), options.BuildTags) | |
// Expand import path patterns. | |
pkgs, err := xctx.Match(args) | |
if err != nil { | |
return fmt.Errorf("failed to expand patterns %v: %w", args, err) | |
} | |
for _, pkgPath := range pkgs { | |
if s.Watcher != nil { | |
pkg, err := xctx.Import(pkgPath, "", build.FindOnly) | |
if err != nil { | |
return err | |
} | |
s.Watcher.Add(pkg.Dir) | |
} | |
pkg, err := xctx.Import(pkgPath, ".", 0) | |
if err != nil { | |
return err | |
} | |
archive, err := s.BuildPackage(pkg) | |
if err != nil { | |
return err | |
} | |
if len(pkgs) == 1 { // Only consider writing output if single package specified. | |
if pkgObj == "" { | |
pkgObj = filepath.Base(pkg.Dir) + ".js" | |
} | |
if pkg.IsCommand() && !pkg.UpToDate { | |
if err := s.WriteCommandPackage(archive, pkgObj); err != nil { | |
return err | |
} | |
} | |
} | |
} | |
return nil | |
}() | |
if s.Watcher == nil { | |
return err | |
} else if err != nil { | |
handleError(err, options, nil) | |
} | |
s.WaitForChange() | |
} | |
} | |
cmdInstall := &cobra.Command{ | |
Use: "install [packages]", | |
Short: "compile and install packages and dependencies", | |
} | |
cmdInstall.Flags().AddFlagSet(flagVerbose) | |
cmdInstall.Flags().AddFlagSet(flagQuiet) | |
cmdInstall.Flags().AddFlagSet(compilerFlags) | |
cmdInstall.Flags().AddFlagSet(flagWatch) | |
cmdInstall.RunE = func(cmd *cobra.Command, args []string) error { | |
options.BuildTags = strings.Fields(tags) | |
for { | |
s, err := gbuild.NewSession(options) | |
if err != nil { | |
return err | |
} | |
err = func() error { | |
// Expand import path patterns. | |
xctx := gbuild.NewBuildContext(s.InstallSuffix(), options.BuildTags) | |
pkgs, err := xctx.Match(args) | |
if err != nil { | |
return fmt.Errorf("failed to expand patterns %v: %w", args, err) | |
} | |
if cmd.Name() == "get" { | |
goGet := exec.Command("go", append([]string{"get", "-d", "-tags=js"}, pkgs...)...) | |
goGet.Stdout = os.Stdout | |
goGet.Stderr = os.Stderr | |
if err := goGet.Run(); err != nil { | |
return err | |
} | |
} | |
for _, pkgPath := range pkgs { | |
pkg, err := xctx.Import(pkgPath, ".", 0) | |
if s.Watcher != nil && pkg != nil { // add watch even on error | |
s.Watcher.Add(pkg.Dir) | |
} | |
if err != nil { | |
return err | |
} | |
archive, err := s.BuildPackage(pkg) | |
if err != nil { | |
return err | |
} | |
if pkg.IsCommand() && !pkg.UpToDate { | |
if err := s.WriteCommandPackage(archive, pkg.InstallPath()); err != nil { | |
return err | |
} | |
} | |
} | |
return nil | |
}() | |
if s.Watcher == nil { | |
return err | |
} else if err != nil { | |
handleError(err, options, nil) | |
} | |
s.WaitForChange() | |
} | |
} | |
cmdDoc := &cobra.Command{ | |
Use: "doc [arguments]", | |
Short: "display documentation for the requested, package, method or symbol", | |
} | |
cmdDoc.RunE = func(cmd *cobra.Command, args []string) error { | |
goDoc := exec.Command("go", append([]string{"doc"}, args...)...) | |
goDoc.Stdout = os.Stdout | |
goDoc.Stderr = os.Stderr | |
goDoc.Env = append(os.Environ(), "GOARCH=js") | |
return goDoc.Run() | |
} | |
cmdGet := &cobra.Command{ | |
Use: "get [packages]", | |
Short: "download and install packages and dependencies", | |
} | |
cmdGet.Flags().AddFlagSet(flagVerbose) | |
cmdGet.Flags().AddFlagSet(flagQuiet) | |
cmdGet.Flags().AddFlagSet(compilerFlags) | |
cmdGet.Run = cmdInstall.Run | |
cmdRun := &cobra.Command{ | |
Use: "run [gofiles...] [arguments...]", | |
Short: "compile and run Go program", | |
} | |
cmdRun.Flags().AddFlagSet(flagVerbose) | |
cmdRun.Flags().AddFlagSet(flagQuiet) | |
cmdRun.Flags().AddFlagSet(compilerFlags) | |
cmdRun.RunE = func(cmd *cobra.Command, args []string) error { | |
options.BuildTags = strings.Fields(tags) | |
lastSourceArg := 0 | |
for { | |
if lastSourceArg == len(args) || !(strings.HasSuffix(args[lastSourceArg], ".go") || strings.HasSuffix(args[lastSourceArg], ".inc.js")) { | |
break | |
} | |
lastSourceArg++ | |
} | |
if lastSourceArg == 0 { | |
return fmt.Errorf("gopherjs run: no go files listed") | |
} | |
tempfile, err := ioutil.TempFile(currentDirectory, filepath.Base(args[0])+".") | |
if err != nil && strings.HasPrefix(currentDirectory, runtime.GOROOT()) { | |
tempfile, err = ioutil.TempFile("", filepath.Base(args[0])+".") | |
} | |
if err != nil { | |
return err | |
} | |
defer func() { | |
tempfile.Close() | |
os.Remove(tempfile.Name()) | |
os.Remove(tempfile.Name() + ".map") | |
}() | |
s, err := gbuild.NewSession(options) | |
if err != nil { | |
return err | |
} | |
if err := s.BuildFiles(args[:lastSourceArg], tempfile.Name(), currentDirectory); err != nil { | |
return err | |
} | |
if err := runNode(tempfile.Name(), args[lastSourceArg:], "", options.Quiet, nil); err != nil { | |
return err | |
} | |
return nil | |
} | |
cmdTest := &cobra.Command{ | |
Use: "test [packages]", | |
Short: "test packages", | |
} | |
bench := cmdTest.Flags().String("bench", "", "Run benchmarks matching the regular expression. By default, no benchmarks run. To run all benchmarks, use '--bench=.'.") | |
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).") | |
count := cmdTest.Flags().String("count", "", "Run each test and benchmark n times (default 1). Examples are always run once.") | |
run := cmdTest.Flags().String("run", "", "Run only those tests and examples matching the regular expression.") | |
short := cmdTest.Flags().Bool("short", false, "Tell long-running tests to shorten their run time.") | |
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.") | |
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.") | |
outputFilename := cmdTest.Flags().StringP("output", "o", "", "Compile the test binary to the named file. The test still runs (unless -c is specified).") | |
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.") | |
cmdTest.Flags().AddFlagSet(compilerFlags) | |
cmdTest.RunE = func(cmd *cobra.Command, args []string) error { | |
options.BuildTags = strings.Fields(tags) | |
// Expand import path patterns. | |
patternContext := gbuild.NewBuildContext("", options.BuildTags) | |
matches, err := patternContext.Match(args) | |
if err != nil { | |
return fmt.Errorf("failed to expand patterns %v: %w", args, err) | |
} | |
if *compileOnly && len(matches) > 1 { | |
return errors.New("cannot use -c flag with multiple packages") | |
} | |
if *outputFilename != "" && len(matches) > 1 { | |
return errors.New("cannot use -o flag with multiple packages") | |
} | |
if *parallelTests < 1 { | |
return errors.New("--parallel cannot be less than 1") | |
} | |
parallelSlots := make(chan (bool), *parallelTests) // Semaphore for parallel test executions. | |
if len(matches) == 1 { | |
// Disable output buffering if testing only one package. | |
parallelSlots = make(chan (bool), 1) | |
} | |
executions := errgroup.Group{} | |
pkgs := make([]*gbuild.PackageData, len(matches)) | |
for i, pkgPath := range matches { | |
var err error | |
pkgs[i], err = gbuild.Import(pkgPath, 0, "", options.BuildTags) | |
if err != nil { | |
return err | |
} | |
} | |
var ( | |
exitErr error | |
exitErrMu = &sync.Mutex{} | |
) | |
for _, pkg := range pkgs { | |
pkg := pkg // Capture for the goroutine. | |
if len(pkg.TestGoFiles) == 0 && len(pkg.XTestGoFiles) == 0 { | |
fmt.Printf("? \t%s\t[no test files]\n", pkg.ImportPath) | |
continue | |
} | |
localOpts := options | |
localOpts.TestedPackage = pkg.ImportPath | |
s, err := gbuild.NewSession(localOpts) | |
if err != nil { | |
return err | |
} | |
tests := &testFuncs{BuildContext: pkg.InternalBuildContext(), Package: pkg.Package} | |
collectTests := func(testPkg *gbuild.PackageData, testPkgName string, needVar *bool) error { | |
if testPkgName == "_test" { | |
for _, file := range pkg.TestGoFiles { | |
if err := tests.load(pkg.Package.Dir, file, testPkgName, &tests.ImportTest, &tests.NeedTest); err != nil { | |
return err | |
} | |
} | |
} else { | |
for _, file := range pkg.XTestGoFiles { | |
if err := tests.load(pkg.Package.Dir, file, "_xtest", &tests.ImportXtest, &tests.NeedXtest); err != nil { | |
return err | |
} | |
} | |
} | |
_, err := s.BuildPackage(testPkg) | |
return err | |
} | |
if err := collectTests(pkg.TestPackage(), "_test", &tests.NeedTest); err != nil { | |
return err | |
} | |
if err := collectTests(pkg.XTestPackage(), "_xtest", &tests.NeedXtest); err != nil { | |
return err | |
} | |
buf := new(bytes.Buffer) | |
if err := testmainTmpl.Execute(buf, tests); err != nil { | |
return err | |
} | |
fset := token.NewFileSet() | |
mainFile, err := parser.ParseFile(fset, "_testmain.go", buf, 0) | |
if err != nil { | |
return err | |
} | |
mainPkg := &gbuild.PackageData{ | |
Package: &build.Package{ | |
ImportPath: pkg.ImportPath + ".testmain", | |
Name: "main", | |
}, | |
} | |
importContext := &compiler.ImportContext{ | |
Packages: s.Types, | |
Import: s.ImportResolverFor(mainPkg), | |
} | |
mainPkgArchive, err := compiler.Compile(mainPkg.ImportPath, []*ast.File{mainFile}, fset, importContext, options.Minify) | |
if err != nil { | |
return err | |
} | |
if *compileOnly && *outputFilename == "" { | |
*outputFilename = pkg.Package.Name + "_test.js" | |
} | |
var outfile *os.File | |
if *outputFilename != "" { | |
outfile, err = os.Create(*outputFilename) | |
if err != nil { | |
return err | |
} | |
} else { | |
outfile, err = os.CreateTemp(currentDirectory, pkg.Package.Name+"_test.*.js") | |
if err != nil { | |
return err | |
} | |
outfile.Close() // Release file handle early, we only need the name. | |
} | |
cleanupTemp := func() { | |
if *outputFilename == "" { | |
os.Remove(outfile.Name()) | |
os.Remove(outfile.Name() + ".map") | |
} | |
} | |
defer cleanupTemp() // Safety net in case cleanup after execution doesn't happen. | |
if err := s.WriteCommandPackage(mainPkgArchive, outfile.Name()); err != nil { | |
return err | |
} | |
if *compileOnly { | |
continue | |
} | |
var args []string | |
if *bench != "" { | |
args = append(args, "-test.bench", *bench) | |
} | |
if *benchtime != "" { | |
args = append(args, "-test.benchtime", *benchtime) | |
} | |
if *count != "" { | |
args = append(args, "-test.count", *count) | |
} | |
if *run != "" { | |
args = append(args, "-test.run", *run) | |
} | |
if *short { | |
args = append(args, "-test.short") | |
} | |
if *verbose { | |
args = append(args, "-test.v") | |
} | |
executions.Go(func() error { | |
parallelSlots <- true // Acquire slot | |
defer func() { <-parallelSlots }() // Release slot | |
status := "ok " | |
start := time.Now() | |
var testOut io.ReadWriter | |
if cap(parallelSlots) > 1 { | |
// If running in parallel, capture test output in a temporary buffer to avoid mixing | |
// output from different tests and print it later. | |
testOut = &bytes.Buffer{} | |
} | |
err := runNode(outfile.Name(), args, runTestDir(pkg), options.Quiet, testOut) | |
cleanupTemp() // Eagerly cleanup temporary compiled files after execution. | |
if testOut != nil { | |
io.Copy(os.Stdout, testOut) | |
} | |
if err != nil { | |
if _, ok := err.(*exec.ExitError); !ok { | |
return err | |
} | |
exitErrMu.Lock() | |
exitErr = err | |
exitErrMu.Unlock() | |
status = "FAIL" | |
} | |
fmt.Printf("%s\t%s\t%.3fs\n", status, pkg.ImportPath, time.Since(start).Seconds()) | |
return nil | |
}) | |
} | |
if err := executions.Wait(); err != nil { | |
return err | |
} | |
return exitErr | |
} | |
cmdServe := &cobra.Command{ | |
Use: "serve [root]", | |
Short: "compile on-the-fly and serve", | |
} | |
cmdServe.Args = cobra.MaximumNArgs(1) | |
cmdServe.Flags().AddFlagSet(flagVerbose) | |
cmdServe.Flags().AddFlagSet(flagQuiet) | |
cmdServe.Flags().AddFlagSet(compilerFlags) | |
var addr string | |
cmdServe.Flags().StringVarP(&addr, "http", "", ":8080", "HTTP bind address to serve") | |
cmdServe.RunE = func(cmd *cobra.Command, args []string) error { | |
options.BuildTags = strings.Fields(tags) | |
var root string | |
if len(args) == 1 { | |
root = args[0] | |
} | |
// Create a new session eagerly to check if it fails, and report the error right away. | |
// Otherwise users will see it only after trying to serve a package, which is a bad experience. | |
_, err := gbuild.NewSession(options) | |
if err != nil { | |
return err | |
} | |
sourceFiles := http.FileServer(serveCommandFileSystem{ | |
serveRoot: root, | |
options: options, | |
sourceMaps: make(map[string][]byte), | |
}) | |
ln, err := net.Listen("tcp", addr) | |
if err != nil { | |
return err | |
} | |
if tcpAddr := ln.Addr().(*net.TCPAddr); tcpAddr.IP.Equal(net.IPv4zero) || tcpAddr.IP.Equal(net.IPv6zero) { // Any available addresses. | |
fmt.Printf("serving at http://localhost:%d and on port %d of any available addresses\n", tcpAddr.Port, tcpAddr.Port) | |
} else { // Specific address. | |
fmt.Printf("serving at http://%s\n", tcpAddr) | |
} | |
fmt.Fprintln(os.Stderr, http.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)}, sourceFiles)) | |
return nil | |
} | |
cmdVersion := &cobra.Command{ | |
Use: "version", | |
Short: "print GopherJS compiler version", | |
Args: cobra.ExactArgs(0), | |
} | |
cmdVersion.Run = func(cmd *cobra.Command, args []string) { | |
fmt.Printf("GopherJS %s\n", compiler.Version) | |
} | |
cmdClean := &cobra.Command{ | |
Use: "clean", | |
Short: "clean GopherJS build cache", | |
} | |
cmdClean.RunE = func(cmd *cobra.Command, args []string) error { | |
return cache.Clear() | |
} | |
rootCmd := &cobra.Command{ | |
Use: "gopherjs", | |
Long: "GopherJS is a tool for compiling Go source code to JavaScript.", | |
SilenceUsage: true, | |
SilenceErrors: true, | |
} | |
rootCmd.AddCommand(cmdBuild, cmdGet, cmdInstall, cmdRun, cmdTest, cmdServe, cmdVersion, cmdDoc, cmdClean) | |
{ | |
var logLevel string | |
var cpuProfile string | |
var allocProfile string | |
rootCmd.PersistentFlags().StringVar(&logLevel, "log_level", log.ErrorLevel.String(), "Compiler log level (debug, info, warn, error, fatal, panic).") | |
rootCmd.PersistentFlags().StringVar(&cpuProfile, "cpu_profile", "", "Save GopherJS compiler CPU profile at the given path. If unset, profiling is disabled.") | |
rootCmd.PersistentFlags().StringVar(&allocProfile, "alloc_profile", "", "Save GopherJS compiler allocation profile at the given path. If unset, profiling is disabled.") | |
rootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { | |
lvl, err := log.ParseLevel(logLevel) | |
if err != nil { | |
return fmt.Errorf("invalid --log_level value %q: %w", logLevel, err) | |
} | |
log.SetLevel(lvl) | |
if cpuProfile != "" { | |
f, err := os.Create(cpuProfile) | |
if err != nil { | |
return fmt.Errorf("failed to create CPU profile file at %q: %w", cpuProfile, err) | |
} | |
if err := pprof.StartCPUProfile(f); err != nil { | |
return fmt.Errorf("failed to start CPU profile: %w", err) | |
} | |
// Not closing the file here, since we'll be writing to it throughout | |
// the lifetime of the process. It will be closed automatically when | |
// the process terminates. | |
} | |
return nil | |
} | |
rootCmd.PersistentPostRunE = func(cmd *cobra.Command, args []string) error { | |
if cpuProfile != "" { | |
pprof.StopCPUProfile() | |
} | |
if allocProfile != "" { | |
f, err := os.Create(allocProfile) | |
if err != nil { | |
return fmt.Errorf("failed to create alloc profile file at %q: %w", allocProfile, err) | |
} | |
if err := pprof.Lookup("allocs").WriteTo(f, 0); err != nil { | |
return fmt.Errorf("failed to write alloc profile: %w", err) | |
} | |
f.Close() | |
} | |
return nil | |
} | |
} | |
err := rootCmd.Execute() | |
if err != nil { | |
os.Exit(handleError(err, options, nil)) | |
} | |
} | |
// tcpKeepAliveListener sets TCP keep-alive timeouts on accepted | |
// connections. It's used by ListenAndServe and ListenAndServeTLS so | |
// dead TCP connections (e.g. closing laptop mid-download) eventually | |
// go away. | |
type tcpKeepAliveListener struct { | |
*net.TCPListener | |
} | |
func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) { | |
tc, err := ln.AcceptTCP() | |
if err != nil { | |
return | |
} | |
tc.SetKeepAlive(true) | |
tc.SetKeepAlivePeriod(3 * time.Minute) | |
return tc, nil | |
} | |
type serveCommandFileSystem struct { | |
serveRoot string | |
options *gbuild.Options | |
sourceMaps map[string][]byte | |
} | |
func (fs serveCommandFileSystem) Open(requestName string) (http.File, error) { | |
name := path.Join(fs.serveRoot, requestName[1:]) // requestName[0] == '/' | |
log.Printf("Request: %s", name) | |
dir, file := path.Split(name) | |
base := path.Base(dir) // base is parent folder name, which becomes the output file name. | |
isPkg := file == base+".js" | |
isMap := file == base+".js.map" | |
isIndex := file == "index.html" | |
// Create a new session to pick up changes to source code on disk. | |
// TODO(dmitshur): might be possible to get a single session to detect changes to source code on disk | |
s, err := gbuild.NewSession(fs.options) | |
if err != nil { | |
return nil, err | |
} | |
if isPkg || isMap || isIndex { | |
// If we're going to be serving our special files, make sure there's a Go command in this folder. | |
pkg, err := gbuild.Import(path.Dir(name), 0, s.InstallSuffix(), fs.options.BuildTags) | |
if err != nil || pkg.Name != "main" { | |
isPkg = false | |
isMap = false | |
isIndex = false | |
} | |
switch { | |
case isPkg: | |
buf := new(bytes.Buffer) | |
browserErrors := new(bytes.Buffer) | |
err := func() error { | |
archive, err := s.BuildPackage(pkg) | |
if err != nil { | |
return err | |
} | |
sourceMapFilter := &compiler.SourceMapFilter{Writer: buf} | |
m := &sourcemap.Map{File: base + ".js"} | |
sourceMapFilter.MappingCallback = s.SourceMappingCallback(m) | |
deps, err := compiler.ImportDependencies(archive, s.BuildImportPath) | |
if err != nil { | |
return err | |
} | |
if err := compiler.WriteProgramCode(deps, sourceMapFilter, s.GoRelease()); err != nil { | |
return err | |
} | |
mapBuf := new(bytes.Buffer) | |
m.WriteTo(mapBuf) | |
buf.WriteString("//# sourceMappingURL=" + base + ".js.map\n") | |
fs.sourceMaps[name+".map"] = mapBuf.Bytes() | |
return nil | |
}() | |
handleError(err, fs.options, browserErrors) | |
if err != nil { | |
buf = browserErrors | |
} | |
return newFakeFile(base+".js", buf.Bytes()), nil | |
case isMap: | |
if content, ok := fs.sourceMaps[name]; ok { | |
return newFakeFile(base+".js.map", content), nil | |
} | |
} | |
} | |
// First try to serve the request with a root prefix supplied in the CLI. | |
if f, err := fs.serveSourceTree(s.XContext(), name); err == nil { | |
return f, nil | |
} | |
// If that didn't work, try without the prefix. | |
if f, err := fs.serveSourceTree(s.XContext(), requestName); err == nil { | |
return f, nil | |
} | |
if isIndex { | |
// If there was no index.html file in any dirs, supply our own. | |
return newFakeFile("index.html", []byte(`<html><head><meta charset="utf-8"><script src="`+base+`.js"></script></head><body></body></html>`)), nil | |
} | |
return nil, os.ErrNotExist | |
} | |
func (fs serveCommandFileSystem) serveSourceTree(xctx gbuild.XContext, reqPath string) (http.File, error) { | |
parts := strings.Split(path.Clean(reqPath), "/") | |
// Under Go Modules different packages can be located in different module | |
// directories, which no longer align with import paths. | |
// | |
// We don't know which part of the requested path is package import path and | |
// which is a path under the package directory, so we try different slipt | |
// points until the package is found successfully. | |
for i := len(parts); i > 0; i-- { | |
pkgPath := path.Clean(path.Join(parts[:i]...)) | |
filePath := path.Clean(path.Join(parts[i:]...)) | |
if pkg, err := xctx.Import(pkgPath, ".", build.FindOnly); err == nil { | |
return http.Dir(pkg.Dir).Open(filePath) | |
} | |
} | |
return nil, os.ErrNotExist | |
} | |
type fakeFile struct { | |
name string | |
size int | |
io.ReadSeeker | |
} | |
func newFakeFile(name string, content []byte) *fakeFile { | |
return &fakeFile{name: name, size: len(content), ReadSeeker: bytes.NewReader(content)} | |
} | |
func (f *fakeFile) Close() error { | |
return nil | |
} | |
func (f *fakeFile) Readdir(count int) ([]os.FileInfo, error) { | |
return nil, os.ErrInvalid | |
} | |
func (f *fakeFile) Stat() (os.FileInfo, error) { | |
return f, nil | |
} | |
func (f *fakeFile) Name() string { | |
return f.name | |
} | |
func (f *fakeFile) Size() int64 { | |
return int64(f.size) | |
} | |
func (f *fakeFile) Mode() os.FileMode { | |
return 0 | |
} | |
func (f *fakeFile) ModTime() time.Time { | |
return time.Time{} | |
} | |
func (f *fakeFile) IsDir() bool { | |
return false | |
} | |
func (f *fakeFile) Sys() interface{} { | |
return nil | |
} | |
// handleError handles err and returns an appropriate exit code. | |
// If browserErrors is non-nil, errors are written for presentation in browser. | |
func handleError(err error, options *gbuild.Options, browserErrors *bytes.Buffer) int { | |
switch err := err.(type) { | |
case nil: | |
return 0 | |
case compiler.ErrorList: | |
for _, entry := range err { | |
printError(entry, options, browserErrors) | |
} | |
return 1 | |
case *exec.ExitError: | |
return err.Sys().(syscall.WaitStatus).ExitStatus() | |
default: | |
printError(err, options, browserErrors) | |
return 1 | |
} | |
} | |
// printError prints err to Stderr with options. If browserErrors is non-nil, errors are also written for presentation in browser. | |
func printError(err error, options *gbuild.Options, browserErrors *bytes.Buffer) { | |
e := sprintError(err) | |
options.PrintError("%s\n", e) | |
if browserErrors != nil { | |
fmt.Fprintln(browserErrors, `console.error("`+template.JSEscapeString(e)+`");`) | |
} | |
} | |
// sprintError returns an annotated error string without trailing newline. | |
func sprintError(err error) string { | |
makeRel := func(name string) string { | |
if relname, err := filepath.Rel(currentDirectory, name); err == nil { | |
return relname | |
} | |
return name | |
} | |
switch e := err.(type) { | |
case *scanner.Error: | |
return fmt.Sprintf("%s:%d:%d: %s", makeRel(e.Pos.Filename), e.Pos.Line, e.Pos.Column, e.Msg) | |
case types.Error: | |
pos := e.Fset.Position(e.Pos) | |
return fmt.Sprintf("%s:%d:%d: %s", makeRel(pos.Filename), pos.Line, pos.Column, e.Msg) | |
default: | |
return fmt.Sprintf("%s", e) | |
} | |
} | |
// runNode runs script with args using Node.js in directory dir. | |
// If dir is empty string, current directory is used. | |
// Is out is not nil, process stderr and stdout are redirected to it, otherwise | |
// os.Stdout and os.Stderr are used. | |
func runNode(script string, args []string, dir string, quiet bool, out io.Writer) error { | |
var allArgs []string | |
if b, _ := strconv.ParseBool(os.Getenv("SOURCE_MAP_SUPPORT")); os.Getenv("SOURCE_MAP_SUPPORT") == "" || b { | |
allArgs = []string{"--require", "source-map-support/register"} | |
if err := exec.Command("node", "--require", "source-map-support/register", "--eval", "").Run(); err != nil { | |
if !quiet { | |
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.") | |
} | |
allArgs = []string{} | |
} | |
} | |
if runtime.GOOS != "windows" { | |
// We've seen issues with stack space limits causing | |
// recursion-heavy standard library tests to fail (e.g., see | |
// https://github.com/gopherjs/gopherjs/pull/669#issuecomment-319319483). | |
// | |
// There are two separate limits in non-Windows environments: | |
// | |
// - OS process limit | |
// - Node.js (V8) limit | |
// | |
// GopherJS fetches the current OS process limit, and sets the | |
// Node.js limit to the same value. So both limits are kept in sync | |
// and can be controlled by setting OS process limit. E.g.: | |
// | |
// ulimit -s 10000 && gopherjs test | |
// | |
cur, err := sysutil.RlimitStack() | |
if err != nil { | |
return fmt.Errorf("failed to get stack size limit: %v", err) | |
} | |
allArgs = append(allArgs, fmt.Sprintf("--stack_size=%v", cur/1000)) // Convert from bytes to KB. | |
} | |
allArgs = append(allArgs, script) | |
allArgs = append(allArgs, args...) | |
node := exec.Command("node", allArgs...) | |
node.Dir = dir | |
node.Stdin = os.Stdin | |
if out != nil { | |
node.Stdout = out | |
node.Stderr = out | |
} else { | |
node.Stdout = os.Stdout | |
node.Stderr = os.Stderr | |
} | |
err := node.Run() | |
if _, ok := err.(*exec.ExitError); err != nil && !ok { | |
err = fmt.Errorf("could not run Node.js: %s", err.Error()) | |
} | |
return err | |
} | |
// runTestDir returns the directory for Node.js to use when running tests for package p. | |
// Empty string means current directory. | |
func runTestDir(p *gbuild.PackageData) string { | |
if p.IsVirtual { | |
// The package is virtual and doesn't have a physical directory. Use current directory. | |
return "" | |
} | |
// Run tests in the package directory. | |
return p.Dir | |
} | |
type testFuncs struct { | |
BuildContext *build.Context | |
Tests []testFunc | |
Benchmarks []testFunc | |
Examples []testFunc | |
TestMain *testFunc | |
Package *build.Package | |
ImportTest bool | |
NeedTest bool | |
ImportXtest bool | |
NeedXtest bool | |
} | |
type testFunc struct { | |
Package string // imported package name (_test or _xtest) | |
Name string // function name | |
Output string // output, for examples | |
Unordered bool // output is allowed to be unordered. | |
} | |
var testFileSet = token.NewFileSet() | |
func (t *testFuncs) load(dir, file, pkg string, doImport, seen *bool) error { | |
f, err := buildutil.ParseFile(testFileSet, t.BuildContext, nil, dir, file, parser.ParseComments) | |
if err != nil { | |
return err | |
} | |
for _, d := range f.Decls { | |
n, ok := d.(*ast.FuncDecl) | |
if !ok { | |
continue | |
} | |
if n.Recv != nil { | |
continue | |
} | |
name := n.Name.String() | |
switch { | |
case isTestMain(n): | |
if t.TestMain != nil { | |
return errors.New("multiple definitions of TestMain") | |
} | |
t.TestMain = &testFunc{pkg, name, "", false} | |
*doImport, *seen = true, true | |
case isTest(name, "Test"): | |
t.Tests = append(t.Tests, testFunc{pkg, name, "", false}) | |
*doImport, *seen = true, true | |
case isTest(name, "Benchmark"): | |
t.Benchmarks = append(t.Benchmarks, testFunc{pkg, name, "", false}) | |
*doImport, *seen = true, true | |
} | |
} | |
ex := doc.Examples(f) | |
sort.Sort(byOrder(ex)) | |
for _, e := range ex { | |
*doImport = true // import test file whether executed or not | |
if e.Output == "" && !e.EmptyOutput { | |
// Don't run examples with no output. | |
continue | |
} | |
t.Examples = append(t.Examples, testFunc{pkg, "Example" + e.Name, e.Output, e.Unordered}) | |
*seen = true | |
} | |
return nil | |
} | |
type byOrder []*doc.Example | |
func (x byOrder) Len() int { return len(x) } | |
func (x byOrder) Swap(i, j int) { x[i], x[j] = x[j], x[i] } | |
func (x byOrder) Less(i, j int) bool { return x[i].Order < x[j].Order } | |
// isTestMain tells whether fn is a TestMain(m *testing.M) function. | |
func isTestMain(fn *ast.FuncDecl) bool { | |
if fn.Name.String() != "TestMain" || | |
fn.Type.Results != nil && len(fn.Type.Results.List) > 0 || | |
fn.Type.Params == nil || | |
len(fn.Type.Params.List) != 1 || | |
len(fn.Type.Params.List[0].Names) > 1 { | |
return false | |
} | |
ptr, ok := fn.Type.Params.List[0].Type.(*ast.StarExpr) | |
if !ok { | |
return false | |
} | |
// We can't easily check that the type is *testing.M | |
// because we don't know how testing has been imported, | |
// but at least check that it's *M or *something.M. | |
if name, ok := ptr.X.(*ast.Ident); ok && name.Name == "M" { | |
return true | |
} | |
if sel, ok := ptr.X.(*ast.SelectorExpr); ok && sel.Sel.Name == "M" { | |
return true | |
} | |
return false | |
} | |
// isTest tells whether name looks like a test (or benchmark, according to prefix). | |
// It is a Test (say) if there is a character after Test that is not a lower-case letter. | |
// We don't want TesticularCancer. | |
func isTest(name, prefix string) bool { | |
if !strings.HasPrefix(name, prefix) { | |
return false | |
} | |
if len(name) == len(prefix) { // "Test" is ok | |
return true | |
} | |
rune, _ := utf8.DecodeRuneInString(name[len(prefix):]) | |
return !unicode.IsLower(rune) | |
} | |
var testmainTmpl = template.Must(template.New("main").Parse(` | |
package main | |
import ( | |
{{if not .TestMain}} | |
"os" | |
{{end}} | |
"testing" | |
"testing/internal/testdeps" | |
{{if .ImportTest}} | |
{{if .NeedTest}}_test{{else}}_{{end}} {{.Package.ImportPath | printf "%q"}} | |
{{end}} | |
{{if .ImportXtest}} | |
{{if .NeedXtest}}_xtest{{else}}_{{end}} {{.Package.ImportPath | printf "%s_test" | printf "%q"}} | |
{{end}} | |
) | |
var tests = []testing.InternalTest{ | |
{{range .Tests}} | |
{"{{.Name}}", {{.Package}}.{{.Name}}}, | |
{{end}} | |
} | |
var benchmarks = []testing.InternalBenchmark{ | |
{{range .Benchmarks}} | |
{"{{.Name}}", {{.Package}}.{{.Name}}}, | |
{{end}} | |
} | |
var examples = []testing.InternalExample{ | |
{{range .Examples}} | |
{"{{.Name}}", {{.Package}}.{{.Name}}, {{.Output | printf "%q"}}, {{.Unordered}}}, | |
{{end}} | |
} | |
func main() { | |
m := testing.MainStart(testdeps.TestDeps{}, tests, benchmarks, examples) | |
{{with .TestMain}} | |
{{.Package}}.{{.Name}}(m) | |
{{else}} | |
os.Exit(m.Run()) | |
{{end}} | |
} | |
`)) |