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 }