1package build 2 3import ( 4 "fmt" 5 "go/ast" 6 "go/build" 7 "go/parser" 8 "go/scanner" 9 "go/token" 10 "go/types" 11 "io" 12 "io/ioutil" 13 "os" 14 "os/exec" 15 "path" 16 "path/filepath" 17 "runtime" 18 "strconv" 19 "strings" 20 "time" 21 22 "github.com/fsnotify/fsnotify" 23 "github.com/gopherjs/gopherjs/compiler" 24 "github.com/gopherjs/gopherjs/compiler/gopherjspkg" 25 "github.com/gopherjs/gopherjs/compiler/natives" 26 "github.com/neelance/sourcemap" 27 "github.com/shurcooL/httpfs/vfsutil" 28 "golang.org/x/tools/go/buildutil" 29) 30 31type ImportCError struct { 32 pkgPath string 33} 34 35func (e *ImportCError) Error() string { 36 return e.pkgPath + `: importing "C" is not supported by GopherJS` 37} 38 39// NewBuildContext creates a build context for building Go packages 40// with GopherJS compiler. 41// 42// Core GopherJS packages (i.e., "github.com/gopherjs/gopherjs/js", "github.com/gopherjs/gopherjs/nosync") 43// are loaded from gopherjspkg.FS virtual filesystem rather than GOPATH. 44func NewBuildContext(installSuffix string, buildTags []string) *build.Context { 45 gopherjsRoot := filepath.Join(build.Default.GOROOT, "src", "github.com", "gopherjs", "gopherjs") 46 return &build.Context{ 47 GOROOT: build.Default.GOROOT, 48 GOPATH: build.Default.GOPATH, 49 GOOS: build.Default.GOOS, 50 GOARCH: "js", 51 InstallSuffix: installSuffix, 52 Compiler: "gc", 53 BuildTags: append(buildTags, 54 "netgo", // See https://godoc.org/net#hdr-Name_Resolution. 55 "purego", // See https://golang.org/issues/23172. 56 ), 57 ReleaseTags: build.Default.ReleaseTags, 58 CgoEnabled: true, // detect `import "C"` to throw proper error 59 60 IsDir: func(path string) bool { 61 if strings.HasPrefix(path, gopherjsRoot+string(filepath.Separator)) { 62 path = filepath.ToSlash(path[len(gopherjsRoot):]) 63 if fi, err := vfsutil.Stat(gopherjspkg.FS, path); err == nil { 64 return fi.IsDir() 65 } 66 } 67 fi, err := os.Stat(path) 68 return err == nil && fi.IsDir() 69 }, 70 ReadDir: func(path string) ([]os.FileInfo, error) { 71 if strings.HasPrefix(path, gopherjsRoot+string(filepath.Separator)) { 72 path = filepath.ToSlash(path[len(gopherjsRoot):]) 73 if fis, err := vfsutil.ReadDir(gopherjspkg.FS, path); err == nil { 74 return fis, nil 75 } 76 } 77 return ioutil.ReadDir(path) 78 }, 79 OpenFile: func(path string) (io.ReadCloser, error) { 80 if strings.HasPrefix(path, gopherjsRoot+string(filepath.Separator)) { 81 path = filepath.ToSlash(path[len(gopherjsRoot):]) 82 if f, err := gopherjspkg.FS.Open(path); err == nil { 83 return f, nil 84 } 85 } 86 return os.Open(path) 87 }, 88 } 89} 90 91// statFile returns an os.FileInfo describing the named file. 92// For files in "$GOROOT/src/github.com/gopherjs/gopherjs" directory, 93// gopherjspkg.FS is consulted first. 94func statFile(path string) (os.FileInfo, error) { 95 gopherjsRoot := filepath.Join(build.Default.GOROOT, "src", "github.com", "gopherjs", "gopherjs") 96 if strings.HasPrefix(path, gopherjsRoot+string(filepath.Separator)) { 97 path = filepath.ToSlash(path[len(gopherjsRoot):]) 98 if fi, err := vfsutil.Stat(gopherjspkg.FS, path); err == nil { 99 return fi, nil 100 } 101 } 102 return os.Stat(path) 103} 104 105// Import returns details about the Go package named by the import path. If the 106// path is a local import path naming a package that can be imported using 107// a standard import path, the returned package will set p.ImportPath to 108// that path. 109// 110// In the directory containing the package, .go and .inc.js files are 111// considered part of the package except for: 112// 113// - .go files in package documentation 114// - files starting with _ or . (likely editor temporary files) 115// - files with build constraints not satisfied by the context 116// 117// If an error occurs, Import returns a non-nil error and a nil 118// *PackageData. 119func Import(path string, mode build.ImportMode, installSuffix string, buildTags []string) (*PackageData, error) { 120 wd, err := os.Getwd() 121 if err != nil { 122 // Getwd may fail if we're in GOARCH=js mode. That's okay, handle 123 // it by falling back to empty working directory. It just means 124 // Import will not be able to resolve relative import paths. 125 wd = "" 126 } 127 bctx := NewBuildContext(installSuffix, buildTags) 128 return importWithSrcDir(*bctx, path, wd, mode, installSuffix) 129} 130 131func importWithSrcDir(bctx build.Context, path string, srcDir string, mode build.ImportMode, installSuffix string) (*PackageData, error) { 132 // bctx is passed by value, so it can be modified here. 133 var isVirtual bool 134 switch path { 135 case "syscall": 136 // syscall needs to use a typical GOARCH like amd64 to pick up definitions for _Socklen, BpfInsn, IFNAMSIZ, Timeval, BpfStat, SYS_FCNTL, Flock_t, etc. 137 bctx.GOARCH = runtime.GOARCH 138 bctx.InstallSuffix = "js" 139 if installSuffix != "" { 140 bctx.InstallSuffix += "_" + installSuffix 141 } 142 case "math/big": 143 // Use pure Go version of math/big; we don't want non-Go assembly versions. 144 bctx.BuildTags = append(bctx.BuildTags, "math_big_pure_go") 145 case "crypto/x509", "os/user": 146 // These stdlib packages have cgo and non-cgo versions (via build tags); we want the latter. 147 bctx.CgoEnabled = false 148 case "github.com/gopherjs/gopherjs/js", "github.com/gopherjs/gopherjs/nosync": 149 // These packages are already embedded via gopherjspkg.FS virtual filesystem (which can be 150 // safely vendored). Don't try to use vendor directory to resolve them. 151 mode |= build.IgnoreVendor 152 isVirtual = true 153 } 154 pkg, err := bctx.Import(path, srcDir, mode) 155 if err != nil { 156 return nil, err 157 } 158 159 switch path { 160 case "os": 161 pkg.GoFiles = excludeExecutable(pkg.GoFiles) // Need to exclude executable implementation files, because some of them contain package scope variables that perform (indirectly) syscalls on init. 162 case "runtime": 163 pkg.GoFiles = []string{"error.go"} 164 case "runtime/internal/sys": 165 pkg.GoFiles = []string{fmt.Sprintf("zgoos_%s.go", bctx.GOOS), "zversion.go"} 166 case "runtime/pprof": 167 pkg.GoFiles = nil 168 case "internal/poll": 169 pkg.GoFiles = exclude(pkg.GoFiles, "fd_poll_runtime.go") 170 case "crypto/rand": 171 pkg.GoFiles = []string{"rand.go", "util.go"} 172 pkg.TestGoFiles = exclude(pkg.TestGoFiles, "rand_linux_test.go") // Don't want linux-specific tests (since linux-specific package files are excluded too). 173 } 174 175 if len(pkg.CgoFiles) > 0 { 176 return nil, &ImportCError{path} 177 } 178 179 if pkg.IsCommand() { 180 pkg.PkgObj = filepath.Join(pkg.BinDir, filepath.Base(pkg.ImportPath)+".js") 181 } 182 183 if _, err := os.Stat(pkg.PkgObj); os.IsNotExist(err) && strings.HasPrefix(pkg.PkgObj, build.Default.GOROOT) { 184 // fall back to GOPATH 185 firstGopathWorkspace := filepath.SplitList(build.Default.GOPATH)[0] // TODO: Need to check inside all GOPATH workspaces. 186 gopathPkgObj := filepath.Join(firstGopathWorkspace, pkg.PkgObj[len(build.Default.GOROOT):]) 187 if _, err := os.Stat(gopathPkgObj); err == nil { 188 pkg.PkgObj = gopathPkgObj 189 } 190 } 191 192 jsFiles, err := jsFilesFromDir(&bctx, pkg.Dir) 193 if err != nil { 194 return nil, err 195 } 196 197 return &PackageData{Package: pkg, JSFiles: jsFiles, IsVirtual: isVirtual}, nil 198} 199 200// excludeExecutable excludes all executable implementation .go files. 201// They have "executable_" prefix. 202func excludeExecutable(goFiles []string) []string { 203 var s []string 204 for _, f := range goFiles { 205 if strings.HasPrefix(f, "executable_") { 206 continue 207 } 208 s = append(s, f) 209 } 210 return s 211} 212 213// exclude returns files, excluding specified files. 214func exclude(files []string, exclude ...string) []string { 215 var s []string 216Outer: 217 for _, f := range files { 218 for _, e := range exclude { 219 if f == e { 220 continue Outer 221 } 222 } 223 s = append(s, f) 224 } 225 return s 226} 227 228// ImportDir is like Import but processes the Go package found in the named 229// directory. 230func ImportDir(dir string, mode build.ImportMode, installSuffix string, buildTags []string) (*PackageData, error) { 231 bctx := NewBuildContext(installSuffix, buildTags) 232 pkg, err := bctx.ImportDir(dir, mode) 233 if err != nil { 234 return nil, err 235 } 236 237 jsFiles, err := jsFilesFromDir(bctx, pkg.Dir) 238 if err != nil { 239 return nil, err 240 } 241 242 return &PackageData{Package: pkg, JSFiles: jsFiles}, nil 243} 244 245// parseAndAugment parses and returns all .go files of given pkg. 246// Standard Go library packages are augmented with files in compiler/natives folder. 247// If isTest is true and pkg.ImportPath has no _test suffix, package is built for running internal tests. 248// If isTest is true and pkg.ImportPath has _test suffix, package is built for running external tests. 249// 250// The native packages are augmented by the contents of natives.FS in the following way. 251// The file names do not matter except the usual `_test` suffix. The files for 252// native overrides get added to the package (even if they have the same name 253// as an existing file from the standard library). For all identifiers that exist 254// in the original AND the overrides, the original identifier in the AST gets 255// replaced by `_`. New identifiers that don't exist in original package get added. 256func parseAndAugment(bctx *build.Context, pkg *build.Package, isTest bool, fileSet *token.FileSet) ([]*ast.File, error) { 257 var files []*ast.File 258 replacedDeclNames := make(map[string]bool) 259 funcName := func(d *ast.FuncDecl) string { 260 if d.Recv == nil || len(d.Recv.List) == 0 { 261 return d.Name.Name 262 } 263 recv := d.Recv.List[0].Type 264 if star, ok := recv.(*ast.StarExpr); ok { 265 recv = star.X 266 } 267 return recv.(*ast.Ident).Name + "." + d.Name.Name 268 } 269 isXTest := strings.HasSuffix(pkg.ImportPath, "_test") 270 importPath := pkg.ImportPath 271 if isXTest { 272 importPath = importPath[:len(importPath)-5] 273 } 274 275 nativesContext := &build.Context{ 276 GOROOT: "/", 277 GOOS: build.Default.GOOS, 278 GOARCH: "js", 279 Compiler: "gc", 280 JoinPath: path.Join, 281 SplitPathList: func(list string) []string { 282 if list == "" { 283 return nil 284 } 285 return strings.Split(list, "/") 286 }, 287 IsAbsPath: path.IsAbs, 288 IsDir: func(name string) bool { 289 dir, err := natives.FS.Open(name) 290 if err != nil { 291 return false 292 } 293 defer dir.Close() 294 info, err := dir.Stat() 295 if err != nil { 296 return false 297 } 298 return info.IsDir() 299 }, 300 HasSubdir: func(root, name string) (rel string, ok bool) { 301 panic("not implemented") 302 }, 303 ReadDir: func(name string) (fi []os.FileInfo, err error) { 304 dir, err := natives.FS.Open(name) 305 if err != nil { 306 return nil, err 307 } 308 defer dir.Close() 309 return dir.Readdir(0) 310 }, 311 OpenFile: func(name string) (r io.ReadCloser, err error) { 312 return natives.FS.Open(name) 313 }, 314 } 315 316 // reflect needs to tell Go 1.11 apart from Go 1.11.1 for https://github.com/gopherjs/gopherjs/issues/862, 317 // so provide it with the custom go1.11.1 build tag whenever we're on Go 1.11.1 or later. 318 // TODO: Remove this ad hoc special behavior in GopherJS 1.12. 319 if runtime.Version() != "go1.11" { 320 nativesContext.ReleaseTags = append(nativesContext.ReleaseTags, "go1.11.1") 321 } 322 323 if nativesPkg, err := nativesContext.Import(importPath, "", 0); err == nil { 324 names := nativesPkg.GoFiles 325 if isTest { 326 names = append(names, nativesPkg.TestGoFiles...) 327 } 328 if isXTest { 329 names = nativesPkg.XTestGoFiles 330 } 331 for _, name := range names { 332 fullPath := path.Join(nativesPkg.Dir, name) 333 r, err := nativesContext.OpenFile(fullPath) 334 if err != nil { 335 panic(err) 336 } 337 file, err := parser.ParseFile(fileSet, fullPath, r, parser.ParseComments) 338 if err != nil { 339 panic(err) 340 } 341 r.Close() 342 for _, decl := range file.Decls { 343 switch d := decl.(type) { 344 case *ast.FuncDecl: 345 replacedDeclNames[funcName(d)] = true 346 case *ast.GenDecl: 347 switch d.Tok { 348 case token.TYPE: 349 for _, spec := range d.Specs { 350 replacedDeclNames[spec.(*ast.TypeSpec).Name.Name] = true 351 } 352 case token.VAR, token.CONST: 353 for _, spec := range d.Specs { 354 for _, name := range spec.(*ast.ValueSpec).Names { 355 replacedDeclNames[name.Name] = true 356 } 357 } 358 } 359 } 360 } 361 files = append(files, file) 362 } 363 } 364 delete(replacedDeclNames, "init") 365 366 var errList compiler.ErrorList 367 for _, name := range pkg.GoFiles { 368 if !filepath.IsAbs(name) { // name might be absolute if specified directly. E.g., `gopherjs build /abs/file.go`. 369 name = filepath.Join(pkg.Dir, name) 370 } 371 r, err := buildutil.OpenFile(bctx, name) 372 if err != nil { 373 return nil, err 374 } 375 file, err := parser.ParseFile(fileSet, name, r, parser.ParseComments) 376 r.Close() 377 if err != nil { 378 if list, isList := err.(scanner.ErrorList); isList { 379 if len(list) > 10 { 380 list = append(list[:10], &scanner.Error{Pos: list[9].Pos, Msg: "too many errors"}) 381 } 382 for _, entry := range list { 383 errList = append(errList, entry) 384 } 385 continue 386 } 387 errList = append(errList, err) 388 continue 389 } 390 391 switch pkg.ImportPath { 392 case "crypto/rand", "encoding/gob", "encoding/json", "expvar", "go/token", "log", "math/big", "math/rand", "regexp", "testing", "time": 393 for _, spec := range file.Imports { 394 path, _ := strconv.Unquote(spec.Path.Value) 395 if path == "sync" { 396 if spec.Name == nil { 397 spec.Name = ast.NewIdent("sync") 398 } 399 spec.Path.Value = `"github.com/gopherjs/gopherjs/nosync"` 400 } 401 } 402 } 403 404 for _, decl := range file.Decls { 405 switch d := decl.(type) { 406 case *ast.FuncDecl: 407 if replacedDeclNames[funcName(d)] { 408 d.Name = ast.NewIdent("_") 409 } 410 case *ast.GenDecl: 411 switch d.Tok { 412 case token.TYPE: 413 for _, spec := range d.Specs { 414 s := spec.(*ast.TypeSpec) 415 if replacedDeclNames[s.Name.Name] { 416 s.Name = ast.NewIdent("_") 417 } 418 } 419 case token.VAR, token.CONST: 420 for _, spec := range d.Specs { 421 s := spec.(*ast.ValueSpec) 422 for i, name := range s.Names { 423 if replacedDeclNames[name.Name] { 424 s.Names[i] = ast.NewIdent("_") 425 } 426 } 427 } 428 } 429 } 430 } 431 files = append(files, file) 432 } 433 if errList != nil { 434 return nil, errList 435 } 436 return files, nil 437} 438 439type Options struct { 440 GOROOT string 441 GOPATH string 442 Verbose bool 443 Quiet bool 444 Watch bool 445 CreateMapFile bool 446 MapToLocalDisk bool 447 Minify bool 448 Color bool 449 BuildTags []string 450} 451 452func (o *Options) PrintError(format string, a ...interface{}) { 453 if o.Color { 454 format = "\x1B[31m" + format + "\x1B[39m" 455 } 456 fmt.Fprintf(os.Stderr, format, a...) 457} 458 459func (o *Options) PrintSuccess(format string, a ...interface{}) { 460 if o.Color { 461 format = "\x1B[32m" + format + "\x1B[39m" 462 } 463 fmt.Fprintf(os.Stderr, format, a...) 464} 465 466type PackageData struct { 467 *build.Package 468 JSFiles []string 469 IsTest bool // IsTest is true if the package is being built for running tests. 470 SrcModTime time.Time 471 UpToDate bool 472 IsVirtual bool // If true, the package does not have a corresponding physical directory on disk. 473} 474 475type Session struct { 476 options *Options 477 bctx *build.Context 478 Archives map[string]*compiler.Archive 479 Types map[string]*types.Package 480 Watcher *fsnotify.Watcher 481} 482 483func NewSession(options *Options) *Session { 484 if options.GOROOT == "" { 485 options.GOROOT = build.Default.GOROOT 486 } 487 if options.GOPATH == "" { 488 options.GOPATH = build.Default.GOPATH 489 } 490 options.Verbose = options.Verbose || options.Watch 491 492 s := &Session{ 493 options: options, 494 Archives: make(map[string]*compiler.Archive), 495 } 496 s.bctx = NewBuildContext(s.InstallSuffix(), s.options.BuildTags) 497 s.Types = make(map[string]*types.Package) 498 if options.Watch { 499 if out, err := exec.Command("ulimit", "-n").Output(); err == nil { 500 if n, err := strconv.Atoi(strings.TrimSpace(string(out))); err == nil && n < 1024 { 501 fmt.Printf("Warning: The maximum number of open file descriptors is very low (%d). Change it with 'ulimit -n 8192'.\n", n) 502 } 503 } 504 505 var err error 506 s.Watcher, err = fsnotify.NewWatcher() 507 if err != nil { 508 panic(err) 509 } 510 } 511 return s 512} 513 514// BuildContext returns the session's build context. 515func (s *Session) BuildContext() *build.Context { return s.bctx } 516 517func (s *Session) InstallSuffix() string { 518 if s.options.Minify { 519 return "min" 520 } 521 return "" 522} 523 524func (s *Session) BuildDir(packagePath string, importPath string, pkgObj string) error { 525 if s.Watcher != nil { 526 s.Watcher.Add(packagePath) 527 } 528 buildPkg, err := s.bctx.ImportDir(packagePath, 0) 529 if err != nil { 530 return err 531 } 532 pkg := &PackageData{Package: buildPkg} 533 jsFiles, err := jsFilesFromDir(s.bctx, pkg.Dir) 534 if err != nil { 535 return err 536 } 537 pkg.JSFiles = jsFiles 538 archive, err := s.BuildPackage(pkg) 539 if err != nil { 540 return err 541 } 542 if pkgObj == "" { 543 pkgObj = filepath.Base(packagePath) + ".js" 544 } 545 if pkg.IsCommand() && !pkg.UpToDate { 546 if err := s.WriteCommandPackage(archive, pkgObj); err != nil { 547 return err 548 } 549 } 550 return nil 551} 552 553func (s *Session) BuildFiles(filenames []string, pkgObj string, packagePath string) error { 554 pkg := &PackageData{ 555 Package: &build.Package{ 556 Name: "main", 557 ImportPath: "main", 558 Dir: packagePath, 559 }, 560 } 561 562 for _, file := range filenames { 563 if strings.HasSuffix(file, ".inc.js") { 564 pkg.JSFiles = append(pkg.JSFiles, file) 565 continue 566 } 567 pkg.GoFiles = append(pkg.GoFiles, file) 568 } 569 570 archive, err := s.BuildPackage(pkg) 571 if err != nil { 572 return err 573 } 574 if s.Types["main"].Name() != "main" { 575 return fmt.Errorf("cannot build/run non-main package") 576 } 577 return s.WriteCommandPackage(archive, pkgObj) 578} 579 580func (s *Session) BuildImportPath(path string) (*compiler.Archive, error) { 581 _, archive, err := s.buildImportPathWithSrcDir(path, "") 582 return archive, err 583} 584 585func (s *Session) buildImportPathWithSrcDir(path string, srcDir string) (*PackageData, *compiler.Archive, error) { 586 pkg, err := importWithSrcDir(*s.bctx, path, srcDir, 0, s.InstallSuffix()) 587 if s.Watcher != nil && pkg != nil { // add watch even on error 588 s.Watcher.Add(pkg.Dir) 589 } 590 if err != nil { 591 return nil, nil, err 592 } 593 594 archive, err := s.BuildPackage(pkg) 595 if err != nil { 596 return nil, nil, err 597 } 598 599 return pkg, archive, nil 600} 601 602func (s *Session) BuildPackage(pkg *PackageData) (*compiler.Archive, error) { 603 if archive, ok := s.Archives[pkg.ImportPath]; ok { 604 return archive, nil 605 } 606 607 if pkg.PkgObj != "" { 608 var fileInfo os.FileInfo 609 gopherjsBinary, err := os.Executable() 610 if err == nil { 611 fileInfo, err = os.Stat(gopherjsBinary) 612 if err == nil { 613 pkg.SrcModTime = fileInfo.ModTime() 614 } 615 } 616 if err != nil { 617 os.Stderr.WriteString("Could not get GopherJS binary's modification timestamp. Please report issue.\n") 618 pkg.SrcModTime = time.Now() 619 } 620 621 for _, importedPkgPath := range pkg.Imports { 622 // Ignore all imports that aren't mentioned in import specs of pkg. 623 // For example, this ignores imports such as runtime/internal/sys and runtime/internal/atomic. 624 ignored := true 625 for _, pos := range pkg.ImportPos[importedPkgPath] { 626 importFile := filepath.Base(pos.Filename) 627 for _, file := range pkg.GoFiles { 628 if importFile == file { 629 ignored = false 630 break 631 } 632 } 633 if !ignored { 634 break 635 } 636 } 637 638 if importedPkgPath == "unsafe" || ignored { 639 continue 640 } 641 importedPkg, _, err := s.buildImportPathWithSrcDir(importedPkgPath, pkg.Dir) 642 if err != nil { 643 return nil, err 644 } 645 impModTime := importedPkg.SrcModTime 646 if impModTime.After(pkg.SrcModTime) { 647 pkg.SrcModTime = impModTime 648 } 649 } 650 651 for _, name := range append(pkg.GoFiles, pkg.JSFiles...) { 652 fileInfo, err := statFile(filepath.Join(pkg.Dir, name)) 653 if err != nil { 654 return nil, err 655 } 656 if fileInfo.ModTime().After(pkg.SrcModTime) { 657 pkg.SrcModTime = fileInfo.ModTime() 658 } 659 } 660 661 pkgObjFileInfo, err := os.Stat(pkg.PkgObj) 662 if err == nil && !pkg.SrcModTime.After(pkgObjFileInfo.ModTime()) { 663 // package object is up to date, load from disk if library 664 pkg.UpToDate = true 665 if pkg.IsCommand() { 666 return nil, nil 667 } 668 669 objFile, err := os.Open(pkg.PkgObj) 670 if err != nil { 671 return nil, err 672 } 673 defer objFile.Close() 674 675 archive, err := compiler.ReadArchive(pkg.PkgObj, pkg.ImportPath, objFile, s.Types) 676 if err != nil { 677 return nil, err 678 } 679 680 s.Archives[pkg.ImportPath] = archive 681 return archive, err 682 } 683 } 684 685 fileSet := token.NewFileSet() 686 files, err := parseAndAugment(s.bctx, pkg.Package, pkg.IsTest, fileSet) 687 if err != nil { 688 return nil, err 689 } 690 691 localImportPathCache := make(map[string]*compiler.Archive) 692 importContext := &compiler.ImportContext{ 693 Packages: s.Types, 694 Import: func(path string) (*compiler.Archive, error) { 695 if archive, ok := localImportPathCache[path]; ok { 696 return archive, nil 697 } 698 _, archive, err := s.buildImportPathWithSrcDir(path, pkg.Dir) 699 if err != nil { 700 return nil, err 701 } 702 localImportPathCache[path] = archive 703 return archive, nil 704 }, 705 } 706 archive, err := compiler.Compile(pkg.ImportPath, files, fileSet, importContext, s.options.Minify) 707 if err != nil { 708 return nil, err 709 } 710 711 for _, jsFile := range pkg.JSFiles { 712 code, err := ioutil.ReadFile(filepath.Join(pkg.Dir, jsFile)) 713 if err != nil { 714 return nil, err 715 } 716 archive.IncJSCode = append(archive.IncJSCode, []byte("\t(function() {\n")...) 717 archive.IncJSCode = append(archive.IncJSCode, code...) 718 archive.IncJSCode = append(archive.IncJSCode, []byte("\n\t}).call($global);\n")...) 719 } 720 721 if s.options.Verbose { 722 fmt.Println(pkg.ImportPath) 723 } 724 725 s.Archives[pkg.ImportPath] = archive 726 727 if pkg.PkgObj == "" || pkg.IsCommand() { 728 return archive, nil 729 } 730 731 if err := s.writeLibraryPackage(archive, pkg.PkgObj); err != nil { 732 if strings.HasPrefix(pkg.PkgObj, s.options.GOROOT) { 733 // fall back to first GOPATH workspace 734 firstGopathWorkspace := filepath.SplitList(s.options.GOPATH)[0] 735 if err := s.writeLibraryPackage(archive, filepath.Join(firstGopathWorkspace, pkg.PkgObj[len(s.options.GOROOT):])); err != nil { 736 return nil, err 737 } 738 return archive, nil 739 } 740 return nil, err 741 } 742 743 return archive, nil 744} 745 746func (s *Session) writeLibraryPackage(archive *compiler.Archive, pkgObj string) error { 747 if err := os.MkdirAll(filepath.Dir(pkgObj), 0777); err != nil { 748 return err 749 } 750 751 objFile, err := os.Create(pkgObj) 752 if err != nil { 753 return err 754 } 755 defer objFile.Close() 756 757 return compiler.WriteArchive(archive, objFile) 758} 759 760func (s *Session) WriteCommandPackage(archive *compiler.Archive, pkgObj string) error { 761 if err := os.MkdirAll(filepath.Dir(pkgObj), 0777); err != nil { 762 return err 763 } 764 codeFile, err := os.Create(pkgObj) 765 if err != nil { 766 return err 767 } 768 defer codeFile.Close() 769 770 sourceMapFilter := &compiler.SourceMapFilter{Writer: codeFile} 771 if s.options.CreateMapFile { 772 m := &sourcemap.Map{File: filepath.Base(pkgObj)} 773 mapFile, err := os.Create(pkgObj + ".map") 774 if err != nil { 775 return err 776 } 777 778 defer func() { 779 m.WriteTo(mapFile) 780 mapFile.Close() 781 fmt.Fprintf(codeFile, "//# sourceMappingURL=%s.map\n", filepath.Base(pkgObj)) 782 }() 783 784 sourceMapFilter.MappingCallback = NewMappingCallback(m, s.options.GOROOT, s.options.GOPATH, s.options.MapToLocalDisk) 785 } 786 787 deps, err := compiler.ImportDependencies(archive, func(path string) (*compiler.Archive, error) { 788 if archive, ok := s.Archives[path]; ok { 789 return archive, nil 790 } 791 _, archive, err := s.buildImportPathWithSrcDir(path, "") 792 return archive, err 793 }) 794 if err != nil { 795 return err 796 } 797 return compiler.WriteProgramCode(deps, sourceMapFilter) 798} 799 800func NewMappingCallback(m *sourcemap.Map, goroot, gopath string, localMap bool) func(generatedLine, generatedColumn int, originalPos token.Position) { 801 return func(generatedLine, generatedColumn int, originalPos token.Position) { 802 if !originalPos.IsValid() { 803 m.AddMapping(&sourcemap.Mapping{GeneratedLine: generatedLine, GeneratedColumn: generatedColumn}) 804 return 805 } 806 807 file := originalPos.Filename 808 809 switch hasGopathPrefix, prefixLen := hasGopathPrefix(file, gopath); { 810 case localMap: 811 // no-op: keep file as-is 812 case hasGopathPrefix: 813 file = filepath.ToSlash(file[prefixLen+4:]) 814 case strings.HasPrefix(file, goroot): 815 file = filepath.ToSlash(file[len(goroot)+4:]) 816 default: 817 file = filepath.Base(file) 818 } 819 820 m.AddMapping(&sourcemap.Mapping{GeneratedLine: generatedLine, GeneratedColumn: generatedColumn, OriginalFile: file, OriginalLine: originalPos.Line, OriginalColumn: originalPos.Column}) 821 } 822} 823 824func jsFilesFromDir(bctx *build.Context, dir string) ([]string, error) { 825 files, err := buildutil.ReadDir(bctx, dir) 826 if err != nil { 827 return nil, err 828 } 829 var jsFiles []string 830 for _, file := range files { 831 if strings.HasSuffix(file.Name(), ".inc.js") && file.Name()[0] != '_' && file.Name()[0] != '.' { 832 jsFiles = append(jsFiles, file.Name()) 833 } 834 } 835 return jsFiles, nil 836} 837 838// hasGopathPrefix returns true and the length of the matched GOPATH workspace, 839// iff file has a prefix that matches one of the GOPATH workspaces. 840func hasGopathPrefix(file, gopath string) (hasGopathPrefix bool, prefixLen int) { 841 gopathWorkspaces := filepath.SplitList(gopath) 842 for _, gopathWorkspace := range gopathWorkspaces { 843 gopathWorkspace = filepath.Clean(gopathWorkspace) 844 if strings.HasPrefix(file, gopathWorkspace) { 845 return true, len(gopathWorkspace) 846 } 847 } 848 return false, 0 849} 850 851func (s *Session) WaitForChange() { 852 s.options.PrintSuccess("watching for changes...\n") 853 for { 854 select { 855 case ev := <-s.Watcher.Events: 856 if ev.Op&(fsnotify.Create|fsnotify.Write|fsnotify.Remove|fsnotify.Rename) == 0 || filepath.Base(ev.Name)[0] == '.' { 857 continue 858 } 859 if !strings.HasSuffix(ev.Name, ".go") && !strings.HasSuffix(ev.Name, ".inc.js") { 860 continue 861 } 862 s.options.PrintSuccess("change detected: %s\n", ev.Name) 863 case err := <-s.Watcher.Errors: 864 s.options.PrintError("watcher error: %s\n", err.Error()) 865 } 866 break 867 } 868 869 go func() { 870 for range s.Watcher.Events { 871 // consume, else Close() may deadlock 872 } 873 }() 874 s.Watcher.Close() 875} 876