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