Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit bdd907e

Browse files
committed
Improved build cache.
Previously GopherJS used to store its build cache under $GOPATH/pkg/$GOOS_$GOARCH, and after Go Modules introduction some parts of it were stored under os.UserCacheDir(). Starting from this commit, *all* build cache is located under os.UserCacheDir(), in a manner similar to the modern Go tool. The cache is keyed by a set of build options (such as minification, compiler version, etc.) to ensure that incompatible archives aren't picked up (see gopherjs#440 for example). This change doesn't solve *all* possible cache-related issues (for example, it still relies on timestamps to invalidate the cache, see gopherjs#805), but should eliminate a large class of confusing failure modes.
1 parent 15ed2e8 commit bdd907e

File tree

9 files changed

+374
-202
lines changed

9 files changed

+374
-202
lines changed

build/build.go

Lines changed: 124 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
"github.com/shurcooL/httpfs/vfsutil"
3232
"golang.org/x/tools/go/buildutil"
3333

34+
"github.com/gopherjs/gopherjs/build/cache"
3435
_ "github.com/gopherjs/gopherjs/build/versionhack" // go/build release tags hack.
3536
)
3637

@@ -117,19 +118,6 @@ func importWithSrcDir(xctx XContext, path string, srcDir string, mode build.Impo
117118
return nil, err
118119
}
119120

120-
if pkg.IsCommand() {
121-
pkg.PkgObj = filepath.Join(pkg.BinDir, filepath.Base(pkg.ImportPath)+".js")
122-
}
123-
124-
if _, err := os.Stat(pkg.PkgObj); os.IsNotExist(err) && strings.HasPrefix(pkg.PkgObj, DefaultGOROOT) {
125-
// fall back to GOPATH
126-
firstGopathWorkspace := filepath.SplitList(build.Default.GOPATH)[0] // TODO: Need to check inside all GOPATH workspaces.
127-
gopathPkgObj := filepath.Join(firstGopathWorkspace, pkg.PkgObj[len(DefaultGOROOT):])
128-
if _, err := os.Stat(gopathPkgObj); err == nil {
129-
pkg.PkgObj = gopathPkgObj
130-
}
131-
}
132-
133121
return pkg, nil
134122
}
135123

@@ -374,11 +362,18 @@ func (o *Options) PrintSuccess(format string, a ...interface{}) {
374362
// GopherJS requires.
375363
type PackageData struct {
376364
*build.Package
377-
JSFiles []string
378-
IsTest bool // IsTest is true if the package is being built for running tests.
365+
JSFiles []string
366+
// IsTest is true if the package is being built for running tests.
367+
IsTest bool
379368
SrcModTime time.Time
380369
UpToDate bool
381-
IsVirtual bool // If true, the package does not have a corresponding physical directory on disk.
370+
// If true, the package does not have a corresponding physical directory on disk.
371+
IsVirtual bool
372+
// When importing a package present as a key in this map, will import the
373+
// package by the override package instead. This is primarily intended to
374+
// support importing "test" variants of packages from the test main and
375+
// "external test" packages.
376+
ImportOverrides map[string]string
382377

383378
bctx *build.Context // The original build context this package came from.
384379
}
@@ -395,7 +390,7 @@ func (p *PackageData) InternalBuildContext() *build.Context {
395390
func (p *PackageData) TestPackage() *PackageData {
396391
return &PackageData{
397392
Package: &build.Package{
398-
ImportPath: p.ImportPath,
393+
ImportPath: p.ImportPath + ".test",
399394
Dir: p.Dir,
400395
GoFiles: append(p.GoFiles, p.TestGoFiles...),
401396
Imports: append(p.Imports, p.TestImports...),
@@ -410,26 +405,51 @@ func (p *PackageData) TestPackage() *PackageData {
410405
func (p *PackageData) XTestPackage() *PackageData {
411406
return &PackageData{
412407
Package: &build.Package{
413-
ImportPath: p.ImportPath + "_test",
408+
ImportPath: p.ImportPath + ".xtest",
414409
Dir: p.Dir,
415410
GoFiles: p.XTestGoFiles,
416411
Imports: p.XTestImports,
417412
},
418413
IsTest: true,
419-
bctx: p.bctx,
414+
ImportOverrides: map[string]string{
415+
// Import the tested package variant built with _test.go sources included.
416+
p.ImportPath: p.ImportPath + ".test",
417+
},
418+
bctx: p.bctx,
419+
}
420+
}
421+
422+
// InstallPath returns the path where "gopherjs install" command should place the
423+
// generated output.
424+
func (p *PackageData) InstallPath() string {
425+
if p.IsCommand() {
426+
name := filepath.Base(p.ImportPath) + ".js"
427+
// For executable packages, mimic go tool behavior if possible.
428+
if gobin := os.Getenv("GOBIN"); gobin != "" {
429+
return filepath.Join(gobin, name)
430+
} else if gopath := os.Getenv("GOPATH"); gopath != "" {
431+
return filepath.Join(gopath, "bin", name)
432+
} else if home, err := os.UserHomeDir(); err == nil {
433+
return filepath.Join(home, "go", "bin", name)
434+
}
420435
}
436+
return p.PkgObj
421437
}
422438

423439
// Session manages internal state GopherJS requires to perform a build.
424440
//
425441
// This is the main interface to GopherJS build system. Session lifetime is
426442
// roughly equivalent to a single GopherJS tool invocation.
427443
type Session struct {
428-
options *Options
429-
xctx XContext
430-
Archives map[string]*compiler.Archive
431-
Types map[string]*types.Package
432-
Watcher *fsnotify.Watcher
444+
options *Options
445+
xctx XContext
446+
buildCache cache.BuildCache
447+
// Binary archives produced during the current session and assumed to be
448+
// up to date with input sources and dependencies. In the -w ("watch") mode
449+
// must be cleared upon entering watching.
450+
UpToDateArchives map[string]*compiler.Archive
451+
Types map[string]*types.Package
452+
Watcher *fsnotify.Watcher
433453
}
434454

435455
// NewSession creates a new GopherJS build session.
@@ -448,10 +468,18 @@ func NewSession(options *Options) (*Session, error) {
448468
}
449469

450470
s := &Session{
451-
options: options,
452-
Archives: make(map[string]*compiler.Archive),
471+
options: options,
472+
UpToDateArchives: make(map[string]*compiler.Archive),
453473
}
454474
s.xctx = NewBuildContext(s.InstallSuffix(), s.options.BuildTags)
475+
s.buildCache = cache.BuildCache{
476+
GOOS: s.xctx.GOOS(),
477+
GOARCH: "js",
478+
GOROOT: options.GOROOT,
479+
GOPATH: options.GOPATH,
480+
BuildTags: options.BuildTags,
481+
Minify: options.Minify,
482+
}
455483
s.Types = make(map[string]*types.Package)
456484
if options.Watch {
457485
if out, err := exec.Command("ulimit", "-n").Output(); err == nil {
@@ -493,10 +521,13 @@ func (s *Session) BuildFiles(filenames []string, pkgObj string, packagePath stri
493521
pkg := &PackageData{
494522
Package: &build.Package{
495523
Name: "main",
496-
ImportPath: "main",
524+
ImportPath: "command-line-arguments",
497525
Dir: packagePath,
498526
},
499-
bctx: &goCtx(s.InstallSuffix(), s.options.BuildTags).bctx,
527+
// This ephemeral package doesn't have a unique import path to be used as a
528+
// build cache key, so we never cache it.
529+
SrcModTime: time.Now().Add(time.Hour),
530+
bctx: &goCtx(s.InstallSuffix(), s.options.BuildTags).bctx,
500531
}
501532

502533
for _, file := range filenames {
@@ -511,7 +542,7 @@ func (s *Session) BuildFiles(filenames []string, pkgObj string, packagePath stri
511542
if err != nil {
512543
return err
513544
}
514-
if s.Types["main"].Name() != "main" {
545+
if s.Types["command-line-arguments"].Name() != "main" {
515546
return fmt.Errorf("cannot build/run non-main package")
516547
}
517548
return s.WriteCommandPackage(archive, pkgObj)
@@ -548,94 +579,70 @@ func (s *Session) buildImportPathWithSrcDir(path string, srcDir string) (*Packag
548579

549580
// BuildPackage compiles an already loaded package.
550581
func (s *Session) BuildPackage(pkg *PackageData) (*compiler.Archive, error) {
551-
if archive, ok := s.Archives[pkg.ImportPath]; ok {
582+
if archive, ok := s.UpToDateArchives[pkg.ImportPath]; ok {
552583
return archive, nil
553584
}
554585

555-
if pkg.PkgObj != "" {
556-
var fileInfo os.FileInfo
557-
gopherjsBinary, err := os.Executable()
586+
var fileInfo os.FileInfo
587+
gopherjsBinary, err := os.Executable()
588+
if err == nil {
589+
fileInfo, err = os.Stat(gopherjsBinary)
558590
if err == nil {
559-
fileInfo, err = os.Stat(gopherjsBinary)
560-
if err == nil {
561-
pkg.SrcModTime = fileInfo.ModTime()
562-
}
591+
pkg.SrcModTime = fileInfo.ModTime()
563592
}
593+
}
594+
if err != nil {
595+
os.Stderr.WriteString("Could not get GopherJS binary's modification timestamp. Please report issue.\n")
596+
pkg.SrcModTime = time.Now()
597+
}
598+
599+
for _, importedPkgPath := range pkg.Imports {
600+
if importedPkgPath == "unsafe" {
601+
continue
602+
}
603+
importedPkg, _, err := s.buildImportPathWithSrcDir(importedPkgPath, pkg.Dir)
564604
if err != nil {
565-
os.Stderr.WriteString("Could not get GopherJS binary's modification timestamp. Please report issue.\n")
566-
pkg.SrcModTime = time.Now()
605+
return nil, err
567606
}
568607

569-
for _, importedPkgPath := range pkg.Imports {
570-
if importedPkgPath == "unsafe" {
571-
continue
572-
}
573-
importedPkg, _, err := s.buildImportPathWithSrcDir(importedPkgPath, pkg.Dir)
574-
if err != nil {
575-
return nil, err
576-
}
577-
impModTime := importedPkg.SrcModTime
578-
if impModTime.After(pkg.SrcModTime) {
579-
pkg.SrcModTime = impModTime
580-
}
608+
impModTime := importedPkg.SrcModTime
609+
if impModTime.After(pkg.SrcModTime) {
610+
pkg.SrcModTime = impModTime
581611
}
612+
}
582613

583-
for _, name := range append(pkg.GoFiles, pkg.JSFiles...) {
584-
fileInfo, err := statFile(filepath.Join(pkg.Dir, name))
585-
if err != nil {
586-
return nil, err
587-
}
588-
if fileInfo.ModTime().After(pkg.SrcModTime) {
589-
pkg.SrcModTime = fileInfo.ModTime()
590-
}
614+
for _, name := range append(pkg.GoFiles, pkg.JSFiles...) {
615+
fileInfo, err := statFile(filepath.Join(pkg.Dir, name))
616+
if err != nil {
617+
return nil, err
591618
}
619+
if fileInfo.ModTime().After(pkg.SrcModTime) {
620+
pkg.SrcModTime = fileInfo.ModTime()
621+
}
622+
}
592623

593-
pkgObjFileInfo, err := os.Stat(pkg.PkgObj)
594-
if err == nil && !pkg.SrcModTime.After(pkgObjFileInfo.ModTime()) {
595-
// package object is up to date, load from disk if library
596-
pkg.UpToDate = true
597-
if pkg.IsCommand() {
598-
return nil, nil
599-
}
600-
601-
objFile, err := os.Open(pkg.PkgObj)
602-
if err != nil {
603-
return nil, err
604-
}
605-
defer objFile.Close()
606-
607-
archive, err := compiler.ReadArchive(pkg.PkgObj, pkg.ImportPath, objFile, s.Types)
608-
if err != nil {
609-
return nil, err
610-
}
611-
612-
s.Archives[pkg.ImportPath] = archive
613-
return archive, err
624+
archive := s.buildCache.LoadArchive(pkg.ImportPath)
625+
if archive != nil && !pkg.SrcModTime.After(archive.BuildTime) {
626+
if err := archive.RegisterTypes(s.Types); err != nil {
627+
panic(fmt.Errorf("Failed to load type information from %v: %w", archive, err))
614628
}
629+
s.UpToDateArchives[pkg.ImportPath] = archive
630+
// Existing archive is up to date, no need to build it from scratch.
631+
return archive, nil
615632
}
616633

634+
// Existing archive is out of date or doesn't exist, let's build the package.
617635
fileSet := token.NewFileSet()
618636
files, err := parseAndAugment(s.xctx, pkg, pkg.IsTest, fileSet)
619637
if err != nil {
620638
return nil, err
621639
}
622640

623-
localImportPathCache := make(map[string]*compiler.Archive)
624641
importContext := &compiler.ImportContext{
625642
Packages: s.Types,
626-
Import: func(path string) (*compiler.Archive, error) {
627-
if archive, ok := localImportPathCache[path]; ok {
628-
return archive, nil
629-
}
630-
_, archive, err := s.buildImportPathWithSrcDir(path, pkg.Dir)
631-
if err != nil {
632-
return nil, err
633-
}
634-
localImportPathCache[path] = archive
635-
return archive, nil
636-
},
643+
Import: s.ImportResolverFor(pkg),
637644
}
638-
archive, err := compiler.Compile(pkg.ImportPath, files, fileSet, importContext, s.options.Minify)
645+
archive, err = compiler.Compile(pkg.ImportPath, files, fileSet, importContext, s.options.Minify)
639646
if err != nil {
640647
return nil, err
641648
}
@@ -654,40 +661,30 @@ func (s *Session) BuildPackage(pkg *PackageData) (*compiler.Archive, error) {
654661
fmt.Println(pkg.ImportPath)
655662
}
656663

657-
s.Archives[pkg.ImportPath] = archive
664+
s.buildCache.StoreArchive(archive)
665+
s.UpToDateArchives[pkg.ImportPath] = archive
658666

659-
if pkg.PkgObj == "" || pkg.IsCommand() {
660-
return archive, nil
661-
}
667+
return archive, nil
668+
}
662669

663-
if err := s.writeLibraryPackage(archive, pkg.PkgObj); err != nil {
664-
if strings.HasPrefix(pkg.PkgObj, s.options.GOROOT) {
665-
// fall back to first GOPATH workspace
666-
firstGopathWorkspace := filepath.SplitList(s.options.GOPATH)[0]
667-
if err := s.writeLibraryPackage(archive, filepath.Join(firstGopathWorkspace, pkg.PkgObj[len(s.options.GOROOT):])); err != nil {
668-
return nil, err
670+
// ImportResolverFor returns a function which returns a compiled package archive
671+
// given an import path. Import paths are resolved relative to the |pkg|.
672+
func (s *Session) ImportResolverFor(pkg *PackageData) func(string) (*compiler.Archive, error) {
673+
return func(path string) (*compiler.Archive, error) {
674+
if pkg.ImportOverrides != nil {
675+
if override, ok := pkg.ImportOverrides[path]; ok {
676+
path = override
669677
}
678+
}
679+
if archive, ok := s.UpToDateArchives[path]; ok {
670680
return archive, nil
671681
}
672-
return nil, err
673-
}
674-
675-
return archive, nil
676-
}
677-
678-
// writeLibraryPackage writes a compiled package archive to disk at pkgObj path.
679-
func (s *Session) writeLibraryPackage(archive *compiler.Archive, pkgObj string) error {
680-
if err := os.MkdirAll(filepath.Dir(pkgObj), 0777); err != nil {
681-
return err
682-
}
683-
684-
objFile, err := os.Create(pkgObj)
685-
if err != nil {
686-
return err
682+
_, archive, err := s.buildImportPathWithSrcDir(path, pkg.Dir)
683+
if err != nil {
684+
return nil, err
685+
}
686+
return archive, nil
687687
}
688-
defer objFile.Close()
689-
690-
return compiler.WriteArchive(archive, objFile)
691688
}
692689

693690
// WriteCommandPackage writes the final JavaScript output file at pkgObj path.
@@ -719,7 +716,7 @@ func (s *Session) WriteCommandPackage(archive *compiler.Archive, pkgObj string)
719716
}
720717

721718
deps, err := compiler.ImportDependencies(archive, func(path string) (*compiler.Archive, error) {
722-
if archive, ok := s.Archives[path]; ok {
719+
if archive, ok := s.UpToDateArchives[path]; ok {
723720
return archive, nil
724721
}
725722
_, archive, err := s.buildImportPathWithSrcDir(path, "")
@@ -788,6 +785,11 @@ func hasGopathPrefix(file, gopath string) (hasGopathPrefix bool, prefixLen int)
788785
// WaitForChange watches file system events and returns if either when one of
789786
// the source files is modified.
790787
func (s *Session) WaitForChange() {
788+
// Will need to re-validate up-to-dateness of all archives, so flush them from
789+
// memory.
790+
s.UpToDateArchives = map[string]*compiler.Archive{}
791+
s.Types = map[string]*types.Package{}
792+
791793
s.options.PrintSuccess("watching for changes...\n")
792794
for {
793795
select {

0 commit comments

Comments
 (0)