github.com/tinygo-org/tinygo@v0.31.3-0.20240404173401-90b0bf646c27/loader/loader.go (about) 1 package loader 2 3 import ( 4 "bytes" 5 "crypto/sha512" 6 "encoding/json" 7 "errors" 8 "fmt" 9 "go/ast" 10 "go/constant" 11 "go/parser" 12 "go/scanner" 13 "go/token" 14 "go/types" 15 "io" 16 "os" 17 "os/exec" 18 "path" 19 "path/filepath" 20 "runtime" 21 "strconv" 22 "strings" 23 "unicode" 24 25 "github.com/tinygo-org/tinygo/cgo" 26 "github.com/tinygo-org/tinygo/compileopts" 27 "github.com/tinygo-org/tinygo/goenv" 28 ) 29 30 var initFileVersions = func(info *types.Info) {} 31 32 // Program holds all packages and some metadata about the program as a whole. 33 type Program struct { 34 config *compileopts.Config 35 typeChecker types.Config 36 goroot string // synthetic GOROOT 37 workingDir string 38 39 Packages map[string]*Package 40 sorted []*Package 41 fset *token.FileSet 42 43 // Information obtained during parsing. 44 LDFlags []string 45 } 46 47 // PackageJSON is a subset of the JSON struct returned from `go list`. 48 type PackageJSON struct { 49 Dir string 50 ImportPath string 51 Name string 52 ForTest string 53 Root string 54 Module struct { 55 Path string 56 Main bool 57 Dir string 58 GoMod string 59 GoVersion string 60 } 61 62 // Source files 63 GoFiles []string 64 CgoFiles []string 65 CFiles []string 66 67 // Embedded files 68 EmbedFiles []string 69 70 // Dependency information 71 Imports []string 72 ImportMap map[string]string 73 74 // Error information 75 Error *struct { 76 ImportStack []string 77 Pos string 78 Err string 79 } 80 } 81 82 // Package holds a loaded package, its imports, and its parsed files. 83 type Package struct { 84 PackageJSON 85 86 program *Program 87 Files []*ast.File 88 FileHashes map[string][]byte 89 CFlags []string // CFlags used during CGo preprocessing (only set if CGo is used) 90 CGoHeaders []string // text above 'import "C"' lines 91 EmbedGlobals map[string][]*EmbedFile 92 Pkg *types.Package 93 info types.Info 94 } 95 96 type EmbedFile struct { 97 Name string 98 Size uint64 99 Hash string // hash of the file (as a hex string) 100 NeedsData bool // true if this file is embedded as a byte slice 101 Data []byte // contents of this file (only if NeedsData is set) 102 } 103 104 // Load loads the given package with all dependencies (including the runtime 105 // package). Call .Parse() afterwards to parse all Go files (including CGo 106 // processing, if necessary). 107 func Load(config *compileopts.Config, inputPkg string, typeChecker types.Config) (*Program, error) { 108 goroot, err := GetCachedGoroot(config) 109 if err != nil { 110 return nil, err 111 } 112 var wd string 113 if config.Options.Directory != "" { 114 wd = config.Options.Directory 115 } else { 116 wd, err = os.Getwd() 117 if err != nil { 118 return nil, err 119 } 120 } 121 p := &Program{ 122 config: config, 123 typeChecker: typeChecker, 124 goroot: goroot, 125 workingDir: wd, 126 Packages: make(map[string]*Package), 127 fset: token.NewFileSet(), 128 } 129 130 // List the dependencies of this package, in raw JSON format. 131 extraArgs := []string{"-json", "-deps"} 132 if config.TestConfig.CompileTestBinary { 133 extraArgs = append(extraArgs, "-test") 134 } 135 cmd, err := List(config, extraArgs, []string{inputPkg}) 136 if err != nil { 137 return nil, err 138 } 139 buf := &bytes.Buffer{} 140 cmd.Stdout = buf 141 cmd.Stderr = os.Stderr 142 err = cmd.Run() 143 if err != nil { 144 if exitErr, ok := err.(*exec.ExitError); ok { 145 os.Exit(exitErr.ExitCode()) 146 } 147 return nil, fmt.Errorf("failed to run `go list`: %s", err) 148 } 149 150 // Parse the returned json from `go list`. 151 decoder := json.NewDecoder(buf) 152 for { 153 pkg := &Package{ 154 program: p, 155 FileHashes: make(map[string][]byte), 156 EmbedGlobals: make(map[string][]*EmbedFile), 157 info: types.Info{ 158 Types: make(map[ast.Expr]types.TypeAndValue), 159 Instances: make(map[*ast.Ident]types.Instance), 160 Defs: make(map[*ast.Ident]types.Object), 161 Uses: make(map[*ast.Ident]types.Object), 162 Implicits: make(map[ast.Node]types.Object), 163 Scopes: make(map[ast.Node]*types.Scope), 164 Selections: make(map[*ast.SelectorExpr]*types.Selection), 165 }, 166 } 167 err := decoder.Decode(&pkg.PackageJSON) 168 if err != nil { 169 if err == io.EOF { 170 break 171 } 172 return nil, err 173 } 174 if pkg.Error != nil { 175 // There was an error while importing (for example, a circular 176 // dependency). 177 pos := token.Position{} 178 fields := strings.Split(pkg.Error.Pos, ":") 179 if len(fields) >= 2 { 180 // There is some file/line/column information. 181 if n, err := strconv.Atoi(fields[len(fields)-2]); err == nil { 182 // Format: filename.go:line:colum 183 pos.Filename = strings.Join(fields[:len(fields)-2], ":") 184 pos.Line = n 185 pos.Column, _ = strconv.Atoi(fields[len(fields)-1]) 186 } else { 187 // Format: filename.go:line 188 pos.Filename = strings.Join(fields[:len(fields)-1], ":") 189 pos.Line, _ = strconv.Atoi(fields[len(fields)-1]) 190 } 191 pos.Filename = p.getOriginalPath(pos.Filename) 192 } 193 err := scanner.Error{ 194 Pos: pos, 195 Msg: pkg.Error.Err, 196 } 197 if len(pkg.Error.ImportStack) != 0 { 198 return nil, Error{ 199 ImportStack: pkg.Error.ImportStack, 200 Err: err, 201 } 202 } 203 return nil, err 204 } 205 if config.TestConfig.CompileTestBinary { 206 // When creating a test binary, `go list` will list two or three 207 // packages used for testing the package. The first is the original 208 // package as if it were built normally, the second is the same 209 // package but with the *_test.go files included. A possible third 210 // may be included for _test packages (such as math_test), used to 211 // test the external API with no access to internal functions. 212 // All packages that are necessary for testing (including the to be 213 // tested package with *_test.go files, but excluding the original 214 // unmodified package) have a suffix added to the import path, for 215 // example the math package has import path "math [math.test]" and 216 // test dependencies such as fmt will have an import path of the 217 // form "fmt [math.test]". 218 // The code below removes this suffix, and if this results in a 219 // duplicate (which happens with the to-be-tested package without 220 // *.test.go files) the previous package is removed from the list of 221 // packages included in this build. 222 // This is necessary because the change in import paths results in 223 // breakage to //go:linkname. Additionally, the duplicated package 224 // slows down the build and so is best removed. 225 if pkg.ForTest != "" && strings.HasSuffix(pkg.ImportPath, " ["+pkg.ForTest+".test]") { 226 newImportPath := pkg.ImportPath[:len(pkg.ImportPath)-len(" ["+pkg.ForTest+".test]")] 227 if _, ok := p.Packages[newImportPath]; ok { 228 // Delete the previous package (that this package overrides). 229 delete(p.Packages, newImportPath) 230 for i, pkg := range p.sorted { 231 if pkg.ImportPath == newImportPath { 232 p.sorted = append(p.sorted[:i], p.sorted[i+1:]...) // remove element from slice 233 break 234 } 235 } 236 } 237 pkg.ImportPath = newImportPath 238 } 239 } 240 p.sorted = append(p.sorted, pkg) 241 p.Packages[pkg.ImportPath] = pkg 242 } 243 244 if config.TestConfig.CompileTestBinary && !strings.HasSuffix(p.sorted[len(p.sorted)-1].ImportPath, ".test") { 245 // Trying to compile a test binary but there are no test files in this 246 // package. 247 return p, NoTestFilesError{p.sorted[len(p.sorted)-1].ImportPath} 248 } 249 250 return p, nil 251 } 252 253 // getOriginalPath looks whether this path is in the generated GOROOT and if so, 254 // replaces the path with the original path (in GOROOT or TINYGOROOT). Otherwise 255 // the input path is returned. 256 func (p *Program) getOriginalPath(path string) string { 257 originalPath := path 258 if strings.HasPrefix(path, p.goroot+string(filepath.Separator)) { 259 // If this file is part of the synthetic GOROOT, try to infer the 260 // original path. 261 relpath := path[len(filepath.Join(p.goroot, "src"))+1:] 262 realgorootPath := filepath.Join(goenv.Get("GOROOT"), "src", relpath) 263 if _, err := os.Stat(realgorootPath); err == nil { 264 originalPath = realgorootPath 265 } 266 maybeInTinyGoRoot := false 267 for prefix := range pathsToOverride(p.config.GoMinorVersion, needsSyscallPackage(p.config.BuildTags())) { 268 if runtime.GOOS == "windows" { 269 prefix = strings.ReplaceAll(prefix, "/", "\\") 270 } 271 if !strings.HasPrefix(relpath, prefix) { 272 continue 273 } 274 maybeInTinyGoRoot = true 275 } 276 if maybeInTinyGoRoot { 277 tinygoPath := filepath.Join(goenv.Get("TINYGOROOT"), "src", relpath) 278 if _, err := os.Stat(tinygoPath); err == nil { 279 originalPath = tinygoPath 280 } 281 } 282 } 283 return originalPath 284 } 285 286 // Sorted returns a list of all packages, sorted in a way that no packages come 287 // before the packages they depend upon. 288 func (p *Program) Sorted() []*Package { 289 return p.sorted 290 } 291 292 // MainPkg returns the last package in the Sorted() slice. This is the main 293 // package of the program. 294 func (p *Program) MainPkg() *Package { 295 return p.sorted[len(p.sorted)-1] 296 } 297 298 // Parse parses all packages and typechecks them. 299 // 300 // The returned error may be an Errors error, which contains a list of errors. 301 // 302 // Idempotent. 303 func (p *Program) Parse() error { 304 // Parse all packages. 305 // TODO: do this in parallel. 306 for _, pkg := range p.sorted { 307 err := pkg.Parse() 308 if err != nil { 309 return err 310 } 311 } 312 313 // Typecheck all packages. 314 for _, pkg := range p.sorted { 315 err := pkg.Check() 316 if err != nil { 317 return err 318 } 319 } 320 321 return nil 322 } 323 324 // OriginalDir returns the real directory name. It is the same as p.Dir except 325 // that if it is part of the cached GOROOT, its real location is returned. 326 func (p *Package) OriginalDir() string { 327 return strings.TrimSuffix(p.program.getOriginalPath(p.Dir+string(os.PathSeparator)), string(os.PathSeparator)) 328 } 329 330 // parseFile is a wrapper around parser.ParseFile. 331 func (p *Package) parseFile(path string, mode parser.Mode) (*ast.File, error) { 332 originalPath := p.program.getOriginalPath(path) 333 data, err := os.ReadFile(path) 334 if err != nil { 335 return nil, err 336 } 337 sum := sha512.Sum512_224(data) 338 p.FileHashes[originalPath] = sum[:] 339 return parser.ParseFile(p.program.fset, originalPath, data, mode) 340 } 341 342 // Parse parses and typechecks this package. 343 // 344 // Idempotent. 345 func (p *Package) Parse() error { 346 if len(p.Files) != 0 { 347 return nil // nothing to do (?) 348 } 349 350 // Load the AST. 351 if p.ImportPath == "unsafe" { 352 // Special case for the unsafe package, which is defined internally by 353 // the types package. 354 p.Pkg = types.Unsafe 355 return nil 356 } 357 358 files, err := p.parseFiles() 359 if err != nil { 360 return err 361 } 362 p.Files = files 363 364 return nil 365 } 366 367 // Check runs the package through the typechecker. The package must already be 368 // loaded and all dependencies must have been checked already. 369 // 370 // Idempotent. 371 func (p *Package) Check() error { 372 if p.Pkg != nil { 373 return nil // already typechecked 374 } 375 376 // Prepare some state used during type checking. 377 var typeErrors []error 378 checker := p.program.typeChecker // make a copy, because it will be modified 379 checker.Error = func(err error) { 380 typeErrors = append(typeErrors, err) 381 } 382 checker.Importer = p 383 if p.Module.GoVersion != "" { 384 // Setting the Go version for a module makes sure the type checker 385 // errors out on language features not supported in that particular 386 // version. 387 checker.GoVersion = "go" + p.Module.GoVersion 388 } else { 389 // Version is not known, so use the currently installed Go version. 390 // This is needed for `tinygo run` for example. 391 // Normally we'd use goenv.GorootVersionString(), but for compatibility 392 // with Go 1.20 and below we need a version in the form of "go1.12" (no 393 // patch version). 394 major, minor, err := goenv.GetGorootVersion() 395 if err != nil { 396 return err 397 } 398 checker.GoVersion = fmt.Sprintf("go%d.%d", major, minor) 399 } 400 initFileVersions(&p.info) 401 402 // Do typechecking of the package. 403 packageName := p.ImportPath 404 if p == p.program.MainPkg() { 405 if p.Name != "main" { 406 // Sanity check. Should not ever trigger. 407 panic("expected main package to have name 'main'") 408 } 409 packageName = "main" 410 } 411 typesPkg, err := checker.Check(packageName, p.program.fset, p.Files, &p.info) 412 if err != nil { 413 if err, ok := err.(Errors); ok { 414 return err 415 } 416 return Errors{p, typeErrors} 417 } 418 p.Pkg = typesPkg 419 420 p.extractEmbedLines(checker.Error) 421 if len(typeErrors) != 0 { 422 return Errors{p, typeErrors} 423 } 424 425 return nil 426 } 427 428 // parseFiles parses the loaded list of files and returns this list. 429 func (p *Package) parseFiles() ([]*ast.File, error) { 430 var files []*ast.File 431 var fileErrs []error 432 433 // Parse all files (incuding CgoFiles). 434 parseFile := func(file string) { 435 if !filepath.IsAbs(file) { 436 file = filepath.Join(p.Dir, file) 437 } 438 f, err := p.parseFile(file, parser.ParseComments) 439 if err != nil { 440 fileErrs = append(fileErrs, err) 441 return 442 } 443 files = append(files, f) 444 } 445 for _, file := range p.GoFiles { 446 parseFile(file) 447 } 448 for _, file := range p.CgoFiles { 449 parseFile(file) 450 } 451 452 // Do CGo processing. 453 // This is done when there are any CgoFiles at all. In that case, len(files) 454 // should be non-zero. However, if len(GoFiles) == 0 and len(CgoFiles) == 1 455 // and there is a syntax error in a CGo file, len(files) may be 0. Don't try 456 // to call cgo.Process in that case as it will only cause issues. 457 if len(p.CgoFiles) != 0 && len(files) != 0 { 458 var initialCFlags []string 459 initialCFlags = append(initialCFlags, p.program.config.CFlags(true)...) 460 initialCFlags = append(initialCFlags, "-I"+p.Dir) 461 generated, headerCode, cflags, ldflags, accessedFiles, errs := cgo.Process(files, p.program.workingDir, p.ImportPath, p.program.fset, initialCFlags) 462 p.CFlags = append(initialCFlags, cflags...) 463 p.CGoHeaders = headerCode 464 for path, hash := range accessedFiles { 465 p.FileHashes[path] = hash 466 } 467 if errs != nil { 468 fileErrs = append(fileErrs, errs...) 469 } 470 files = append(files, generated...) 471 p.program.LDFlags = append(p.program.LDFlags, ldflags...) 472 } 473 474 // Only return an error after CGo processing, so that errors in parsing and 475 // CGo can be reported together. 476 if len(fileErrs) != 0 { 477 return nil, Errors{p, fileErrs} 478 } 479 480 return files, nil 481 } 482 483 // extractEmbedLines finds all //go:embed lines in the package and matches them 484 // against EmbedFiles from `go list`. 485 func (p *Package) extractEmbedLines(addError func(error)) { 486 for _, file := range p.Files { 487 // Check for an `import "embed"` line at the start of the file. 488 // //go:embed lines are only valid if the given file itself imports the 489 // embed package. It is not valid if it is only imported in a separate 490 // Go file. 491 hasEmbed := false 492 for _, importSpec := range file.Imports { 493 if importSpec.Path.Value == `"embed"` { 494 hasEmbed = true 495 } 496 } 497 498 for _, decl := range file.Decls { 499 switch decl := decl.(type) { 500 case *ast.GenDecl: 501 if decl.Tok != token.VAR { 502 continue 503 } 504 for _, spec := range decl.Specs { 505 spec := spec.(*ast.ValueSpec) 506 var doc *ast.CommentGroup 507 if decl.Lparen == token.NoPos { 508 // Plain 'var' declaration, like: 509 // //go:embed hello.txt 510 // var hello string 511 doc = decl.Doc 512 } else { 513 // Bigger 'var' declaration like: 514 // var ( 515 // //go:embed hello.txt 516 // hello string 517 // ) 518 doc = spec.Doc 519 } 520 if doc == nil { 521 continue 522 } 523 524 // Look for //go:embed comments. 525 var allPatterns []string 526 for _, comment := range doc.List { 527 if comment.Text != "//go:embed" && !strings.HasPrefix(comment.Text, "//go:embed ") { 528 continue 529 } 530 if !hasEmbed { 531 addError(types.Error{ 532 Fset: p.program.fset, 533 Pos: comment.Pos() + 2, 534 Msg: "//go:embed only allowed in Go files that import \"embed\"", 535 }) 536 // Continue, because otherwise we might run into 537 // issues below. 538 continue 539 } 540 patterns, err := p.parseGoEmbed(comment.Text[len("//go:embed"):], comment.Slash) 541 if err != nil { 542 addError(err) 543 continue 544 } 545 if len(patterns) == 0 { 546 addError(types.Error{ 547 Fset: p.program.fset, 548 Pos: comment.Pos() + 2, 549 Msg: "usage: //go:embed pattern...", 550 }) 551 continue 552 } 553 for _, pattern := range patterns { 554 // Check that the pattern is well-formed. 555 // It must be valid: the Go toolchain has already 556 // checked for invalid patterns. But let's check 557 // anyway to be sure. 558 if _, err := path.Match(pattern, ""); err != nil { 559 addError(types.Error{ 560 Fset: p.program.fset, 561 Pos: comment.Pos(), 562 Msg: "invalid pattern syntax", 563 }) 564 continue 565 } 566 allPatterns = append(allPatterns, pattern) 567 } 568 } 569 570 if len(allPatterns) != 0 { 571 // This is a //go:embed global. Do a few more checks. 572 if len(spec.Names) != 1 { 573 addError(types.Error{ 574 Fset: p.program.fset, 575 Pos: spec.Names[1].NamePos, 576 Msg: "//go:embed cannot apply to multiple vars", 577 }) 578 } 579 if spec.Values != nil { 580 addError(types.Error{ 581 Fset: p.program.fset, 582 Pos: spec.Values[0].Pos(), 583 Msg: "//go:embed cannot apply to var with initializer", 584 }) 585 } 586 globalName := spec.Names[0].Name 587 globalType := p.Pkg.Scope().Lookup(globalName).Type() 588 valid, byteSlice := isValidEmbedType(globalType) 589 if !valid { 590 addError(types.Error{ 591 Fset: p.program.fset, 592 Pos: spec.Type.Pos(), 593 Msg: "//go:embed cannot apply to var of type " + globalType.String(), 594 }) 595 } 596 597 // Match all //go:embed patterns against the embed files 598 // provided by `go list`. 599 for _, name := range p.EmbedFiles { 600 for _, pattern := range allPatterns { 601 if matchPattern(pattern, name) { 602 p.EmbedGlobals[globalName] = append(p.EmbedGlobals[globalName], &EmbedFile{ 603 Name: name, 604 NeedsData: byteSlice, 605 }) 606 break 607 } 608 } 609 } 610 } 611 } 612 } 613 } 614 } 615 } 616 617 // matchPattern returns true if (and only if) the given pattern would match the 618 // filename. The pattern could also match a parent directory of name, in which 619 // case hidden files do not match. 620 func matchPattern(pattern, name string) bool { 621 // Match this file. 622 matched, _ := path.Match(pattern, name) 623 if matched { 624 return true 625 } 626 627 // Match parent directories. 628 dir := name 629 for { 630 dir, _ = path.Split(dir) 631 if dir == "" { 632 return false 633 } 634 dir = path.Clean(dir) 635 if matched, _ := path.Match(pattern, dir); matched { 636 // Pattern matches the directory. 637 suffix := name[len(dir):] 638 if strings.Contains(suffix, "/_") || strings.Contains(suffix, "/.") { 639 // Pattern matches a hidden file. 640 // Hidden files are included when listed directly as a 641 // pattern, but not when they are part of a directory tree. 642 // Source: 643 // > If a pattern names a directory, all files in the 644 // > subtree rooted at that directory are embedded 645 // > (recursively), except that files with names beginning 646 // > with ‘.’ or ‘_’ are excluded. 647 return false 648 } 649 return true 650 } 651 } 652 } 653 654 // parseGoEmbed is like strings.Fields but for a //go:embed line. It parses 655 // regular fields and quoted fields (that may contain spaces). 656 func (p *Package) parseGoEmbed(args string, pos token.Pos) (patterns []string, err error) { 657 args = strings.TrimSpace(args) 658 initialLen := len(args) 659 for args != "" { 660 patternPos := pos + token.Pos(initialLen-len(args)) 661 switch args[0] { 662 case '`', '"', '\\': 663 // Parse the next pattern using the Go scanner. 664 // This is perhaps a bit overkill, but it does correctly implement 665 // parsing of the various Go strings. 666 var sc scanner.Scanner 667 fset := &token.FileSet{} 668 file := fset.AddFile("", 0, len(args)) 669 sc.Init(file, []byte(args), nil, 0) 670 _, tok, lit := sc.Scan() 671 if tok != token.STRING || sc.ErrorCount != 0 { 672 // Calculate start of token 673 return nil, types.Error{ 674 Fset: p.program.fset, 675 Pos: patternPos, 676 Msg: "invalid quoted string in //go:embed", 677 } 678 } 679 pattern := constant.StringVal(constant.MakeFromLiteral(lit, tok, 0)) 680 patterns = append(patterns, pattern) 681 args = strings.TrimLeftFunc(args[len(lit):], unicode.IsSpace) 682 default: 683 // The value is just a regular value. 684 // Split it at the first white space. 685 index := strings.IndexFunc(args, unicode.IsSpace) 686 if index < 0 { 687 index = len(args) 688 } 689 pattern := args[:index] 690 patterns = append(patterns, pattern) 691 args = strings.TrimLeftFunc(args[len(pattern):], unicode.IsSpace) 692 } 693 if _, err := path.Match(patterns[len(patterns)-1], ""); err != nil { 694 return nil, types.Error{ 695 Fset: p.program.fset, 696 Pos: patternPos, 697 Msg: "invalid pattern syntax", 698 } 699 } 700 } 701 return patterns, nil 702 } 703 704 // isValidEmbedType returns whether the given Go type can be used as a 705 // //go:embed type. This is only true for embed.FS, strings, and byte slices. 706 // The second return value indicates that this is a byte slice, and therefore 707 // the contents of the file needs to be passed to the compiler. 708 func isValidEmbedType(typ types.Type) (valid, byteSlice bool) { 709 if typ.Underlying() == types.Typ[types.String] { 710 // string type 711 return true, false 712 } 713 if sliceType, ok := typ.Underlying().(*types.Slice); ok { 714 if elemType, ok := sliceType.Elem().Underlying().(*types.Basic); ok && elemType.Kind() == types.Byte { 715 // byte slice type 716 return true, true 717 } 718 } 719 if namedType, ok := typ.(*types.Named); ok && namedType.String() == "embed.FS" { 720 // embed.FS type 721 return true, false 722 } 723 return false, false 724 } 725 726 // Import implements types.Importer. It loads and parses packages it encounters 727 // along the way, if needed. 728 func (p *Package) Import(to string) (*types.Package, error) { 729 if to == "unsafe" { 730 return types.Unsafe, nil 731 } 732 if newTo, ok := p.ImportMap[to]; ok && !strings.HasSuffix(newTo, ".test]") { 733 to = newTo 734 } 735 if imported, ok := p.program.Packages[to]; ok { 736 return imported.Pkg, nil 737 } else { 738 return nil, errors.New("package not imported: " + to) 739 } 740 }