|
|
|
package loader
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"go/ast"
|
|
|
|
"go/build"
|
|
|
|
"go/parser"
|
|
|
|
"go/scanner"
|
|
|
|
"go/token"
|
|
|
|
"go/types"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
"sort"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
"text/template"
|
|
|
|
|
|
|
|
"github.com/tinygo-org/tinygo/cgo"
|
loader: merge roots from both Go and TinyGo in a cached directory
This commit changes the way that packages are looked up. Instead of
working around the loader package by modifying the GOROOT variable for
specific packages, create a new GOROOT using symlinks. This GOROOT is
cached for the specified configuration (Go version, underlying GOROOT
path, TinyGo path, whether to override the syscall package).
This will also enable go module support in the future.
Windows is a bit harder to support, because it only allows the creation
of symlinks when developer mode is enabled. This is worked around by
using symlinks and if that fails, using directory junctions or hardlinks
instead. This should work in the vast majority of cases. The only case
it doesn't work, is if developer mode is disabled and TinyGo, the Go
toolchain, and the cache directory are not all on the same filesystem.
If this is a problem, it is still possible to improve the code by using
file copies instead.
As a side effect, this also makes diagnostics use a relative file path
only when the file is not in GOROOT or in TINYGOROOT.
5 years ago
|
|
|
"github.com/tinygo-org/tinygo/goenv"
|
|
|
|
"golang.org/x/tools/go/packages"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Program holds all packages and some metadata about the program as a whole.
|
|
|
|
type Program struct {
|
|
|
|
Build *build.Context
|
|
|
|
Tests bool
|
|
|
|
Packages map[string]*Package
|
|
|
|
MainPkg *Package
|
|
|
|
sorted []*Package
|
|
|
|
fset *token.FileSet
|
|
|
|
TypeChecker types.Config
|
|
|
|
Dir string // current working directory (for error reporting)
|
|
|
|
TINYGOROOT string // root of the TinyGo installation or root of the source code
|
|
|
|
CFlags []string
|
|
|
|
LDFlags []string
|
|
|
|
ClangHeaders string
|
|
|
|
}
|
|
|
|
|
|
|
|
// Package holds a loaded package, its imports, and its parsed files.
|
|
|
|
type Package struct {
|
|
|
|
*Program
|
|
|
|
*packages.Package
|
|
|
|
Files []*ast.File
|
|
|
|
Pkg *types.Package
|
|
|
|
types.Info
|
|
|
|
}
|
|
|
|
|
|
|
|
// Load loads the given package with all dependencies (including the runtime
|
|
|
|
// package). Call .Parse() afterwards to parse all Go files (including CGo
|
|
|
|
// processing, if necessary).
|
|
|
|
func (p *Program) Load(importPath string) error {
|
|
|
|
if p.Packages == nil {
|
|
|
|
p.Packages = make(map[string]*Package)
|
|
|
|
}
|
|
|
|
|
|
|
|
err := p.loadPackage(importPath)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
p.MainPkg = p.sorted[len(p.sorted)-1]
|
|
|
|
if _, ok := p.Packages["runtime"]; !ok {
|
|
|
|
// The runtime package wasn't loaded. Although `go list -deps` seems to
|
|
|
|
// return the full dependency list, there is no way to get those
|
|
|
|
// packages from the go/packages package. Therefore load the runtime
|
|
|
|
// manually and add it to the list of to-be-compiled packages
|
|
|
|
// (duplicates are already filtered).
|
|
|
|
return p.loadPackage("runtime")
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p *Program) loadPackage(importPath string) error {
|
|
|
|
cgoEnabled := "0"
|
|
|
|
if p.Build.CgoEnabled {
|
|
|
|
cgoEnabled = "1"
|
|
|
|
}
|
|
|
|
pkgs, err := packages.Load(&packages.Config{
|
|
|
|
Mode: packages.NeedName | packages.NeedFiles | packages.NeedImports | packages.NeedDeps,
|
|
|
|
Env: append(os.Environ(), "GOROOT="+p.Build.GOROOT, "GOOS="+p.Build.GOOS, "GOARCH="+p.Build.GOARCH, "CGO_ENABLED="+cgoEnabled),
|
|
|
|
BuildFlags: []string{"-tags", strings.Join(p.Build.BuildTags, " ")},
|
|
|
|
Tests: p.Tests,
|
|
|
|
}, importPath)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
var pkg *packages.Package
|
|
|
|
if p.Tests {
|
|
|
|
// We need the second package. Quoting from the docs:
|
|
|
|
// > For example, when using the go command, loading "fmt" with Tests=true
|
|
|
|
// > returns four packages, with IDs "fmt" (the standard package),
|
|
|
|
// > "fmt [fmt.test]" (the package as compiled for the test),
|
|
|
|
// > "fmt_test" (the test functions from source files in package fmt_test),
|
|
|
|
// > and "fmt.test" (the test binary).
|
|
|
|
pkg = pkgs[1]
|
|
|
|
} else {
|
|
|
|
if len(pkgs) != 1 {
|
|
|
|
return fmt.Errorf("expected exactly one package while importing %s, got %d", importPath, len(pkgs))
|
|
|
|
}
|
|
|
|
pkg = pkgs[0]
|
|
|
|
}
|
|
|
|
var importError *Errors
|
|
|
|
var addPackages func(pkg *packages.Package)
|
|
|
|
addPackages = func(pkg *packages.Package) {
|
|
|
|
if _, ok := p.Packages[pkg.PkgPath]; ok {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
pkg2 := p.newPackage(pkg)
|
|
|
|
p.Packages[pkg.PkgPath] = pkg2
|
|
|
|
if len(pkg.Errors) != 0 {
|
|
|
|
if importError != nil {
|
|
|
|
// There was another error reported already. Do not report
|
|
|
|
// errors from multiple packages at once.
|
|
|
|
return
|
|
|
|
}
|
|
|
|
importError = &Errors{
|
|
|
|
Pkg: pkg2,
|
|
|
|
}
|
|
|
|
for _, err := range pkg.Errors {
|
|
|
|
pos := token.Position{}
|
|
|
|
fields := strings.Split(err.Pos, ":")
|
|
|
|
if len(fields) >= 2 {
|
|
|
|
// There is some file/line/column information.
|
|
|
|
if n, err := strconv.Atoi(fields[len(fields)-2]); err == nil {
|
|
|
|
// Format: filename.go:line:colum
|
|
|
|
pos.Filename = strings.Join(fields[:len(fields)-2], ":")
|
|
|
|
pos.Line = n
|
|
|
|
pos.Column, _ = strconv.Atoi(fields[len(fields)-1])
|
|
|
|
} else {
|
|
|
|
// Format: filename.go:line
|
|
|
|
pos.Filename = strings.Join(fields[:len(fields)-1], ":")
|
|
|
|
pos.Line, _ = strconv.Atoi(fields[len(fields)-1])
|
|
|
|
}
|
|
|
|
pos.Filename = p.getOriginalPath(pos.Filename)
|
|
|
|
}
|
|
|
|
importError.Errs = append(importError.Errs, scanner.Error{
|
|
|
|
Pos: pos,
|
|
|
|
Msg: err.Msg,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get the list of imports (sorted alphabetically).
|
|
|
|
names := make([]string, 0, len(pkg.Imports))
|
|
|
|
for name := range pkg.Imports {
|
|
|
|
names = append(names, name)
|
|
|
|
}
|
|
|
|
sort.Strings(names)
|
|
|
|
|
|
|
|
// Add all the imports.
|
|
|
|
for _, name := range names {
|
|
|
|
addPackages(pkg.Imports[name])
|
|
|
|
}
|
|
|
|
|
|
|
|
p.sorted = append(p.sorted, pkg2)
|
|
|
|
}
|
|
|
|
addPackages(pkg)
|
|
|
|
if importError != nil {
|
|
|
|
return *importError
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// getOriginalPath looks whether this path is in the generated GOROOT and if so,
|
|
|
|
// replaces the path with the original path (in GOROOT or TINYGOROOT). Otherwise
|
|
|
|
// the input path is returned.
|
|
|
|
func (p *Program) getOriginalPath(path string) string {
|
|
|
|
originalPath := path
|
|
|
|
if strings.HasPrefix(path, p.Build.GOROOT+string(filepath.Separator)) {
|
|
|
|
// If this file is part of the synthetic GOROOT, try to infer the
|
|
|
|
// original path.
|
|
|
|
relpath := path[len(filepath.Join(p.Build.GOROOT, "src"))+1:]
|
|
|
|
realgorootPath := filepath.Join(goenv.Get("GOROOT"), "src", relpath)
|
|
|
|
if _, err := os.Stat(realgorootPath); err == nil {
|
|
|
|
originalPath = realgorootPath
|
|
|
|
}
|
|
|
|
maybeInTinyGoRoot := false
|
|
|
|
for prefix := range pathsToOverride(needsSyscallPackage(p.Build.BuildTags)) {
|
|
|
|
if !strings.HasPrefix(relpath, prefix) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
maybeInTinyGoRoot = true
|
|
|
|
}
|
|
|
|
if maybeInTinyGoRoot {
|
|
|
|
tinygoPath := filepath.Join(p.TINYGOROOT, "src", relpath)
|
|
|
|
if _, err := os.Stat(tinygoPath); err == nil {
|
|
|
|
originalPath = tinygoPath
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return originalPath
|
|
|
|
}
|
|
|
|
|
|
|
|
// newPackage instantiates a new *Package object with initialized members.
|
|
|
|
func (p *Program) newPackage(pkg *packages.Package) *Package {
|
|
|
|
return &Package{
|
|
|
|
Program: p,
|
|
|
|
Package: pkg,
|
|
|
|
Info: types.Info{
|
|
|
|
Types: make(map[ast.Expr]types.TypeAndValue),
|
|
|
|
Defs: make(map[*ast.Ident]types.Object),
|
|
|
|
Uses: make(map[*ast.Ident]types.Object),
|
|
|
|
Implicits: make(map[ast.Node]types.Object),
|
|
|
|
Scopes: make(map[ast.Node]*types.Scope),
|
|
|
|
Selections: make(map[*ast.SelectorExpr]*types.Selection),
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Sorted returns a list of all packages, sorted in a way that no packages come
|
|
|
|
// before the packages they depend upon.
|
|
|
|
func (p *Program) Sorted() []*Package {
|
|
|
|
return p.sorted
|
|
|
|
}
|
|
|
|
|
|
|
|
// Parse parses all packages and typechecks them.
|
|
|
|
//
|
|
|
|
// The returned error may be an Errors error, which contains a list of errors.
|
|
|
|
//
|
|
|
|
// Idempotent.
|
|
|
|
func (p *Program) Parse() error {
|
|
|
|
// Parse all packages.
|
|
|
|
for _, pkg := range p.Sorted() {
|
|
|
|
err := pkg.Parse()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if p.Tests {
|
|
|
|
err := p.swapTestMain()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Typecheck all packages.
|
|
|
|
for _, pkg := range p.Sorted() {
|
|
|
|
err := pkg.Check()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p *Program) swapTestMain() error {
|
|
|
|
var tests []string
|
|
|
|
|
|
|
|
isTestFunc := func(f *ast.FuncDecl) bool {
|
|
|
|
// TODO: improve signature check
|
|
|
|
if strings.HasPrefix(f.Name.Name, "Test") && f.Name.Name != "TestMain" {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
for _, f := range p.MainPkg.Files {
|
|
|
|
for i, d := range f.Decls {
|
|
|
|
switch v := d.(type) {
|
|
|
|
case *ast.FuncDecl:
|
|
|
|
if isTestFunc(v) {
|
|
|
|
tests = append(tests, v.Name.Name)
|
|
|
|
}
|
|
|
|
if v.Name.Name == "main" {
|
|
|
|
// Remove main
|
|
|
|
if len(f.Decls) == 1 {
|
|
|
|
f.Decls = make([]ast.Decl, 0)
|
|
|
|
} else {
|
|
|
|
f.Decls[i] = f.Decls[len(f.Decls)-1]
|
|
|
|
f.Decls = f.Decls[:len(f.Decls)-1]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: Check if they defined a TestMain and call it instead of testing.TestMain
|
|
|
|
const mainBody = `package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"testing"
|
|
|
|
)
|
|
|
|
|
|
|
|
func main () {
|
|
|
|
m := &testing.M{
|
|
|
|
Tests: []testing.TestToCall{
|
|
|
|
{{range .TestFunctions}}
|
|
|
|
{Name: "{{.}}", Func: {{.}}},
|
|
|
|
{{end}}
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
testing.TestMain(m)
|
|
|
|
}
|
|
|
|
`
|
|
|
|
tmpl := template.Must(template.New("testmain").Parse(mainBody))
|
|
|
|
b := bytes.Buffer{}
|
|
|
|
tmplData := struct {
|
|
|
|
TestFunctions []string
|
|
|
|
}{
|
|
|
|
TestFunctions: tests,
|
|
|
|
}
|
|
|
|
|
|
|
|
err := tmpl.Execute(&b, tmplData)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
path := filepath.Join(p.MainPkg.Dir, "$testmain.go")
|
|
|
|
|
|
|
|
if p.fset == nil {
|
|
|
|
p.fset = token.NewFileSet()
|
|
|
|
}
|
|
|
|
|
|
|
|
newMain, err := parser.ParseFile(p.fset, path, b.Bytes(), parser.AllErrors)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
p.MainPkg.Files = append(p.MainPkg.Files, newMain)
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// parseFile is a wrapper around parser.ParseFile.
|
|
|
|
func (p *Program) parseFile(path string, mode parser.Mode) (*ast.File, error) {
|
|
|
|
if p.fset == nil {
|
|
|
|
p.fset = token.NewFileSet()
|
|
|
|
}
|
|
|
|
|
|
|
|
rd, err := os.Open(path)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
defer rd.Close()
|
|
|
|
return parser.ParseFile(p.fset, p.getOriginalPath(path), rd, mode)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Parse parses and typechecks this package.
|
|
|
|
//
|
|
|
|
// Idempotent.
|
|
|
|
func (p *Package) Parse() error {
|
|
|
|
if len(p.Files) != 0 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Load the AST.
|
|
|
|
// TODO: do this in parallel.
|
|
|
|
if p.PkgPath == "unsafe" {
|
|
|
|
// Special case for the unsafe package. Don't even bother loading
|
|
|
|
// the files.
|
|
|
|
p.Pkg = types.Unsafe
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
files, err := p.parseFiles()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
p.Files = files
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check runs the package through the typechecker. The package must already be
|
|
|
|
// loaded and all dependencies must have been checked already.
|
|
|
|
//
|
|
|
|
// Idempotent.
|
|
|
|
func (p *Package) Check() error {
|
|
|
|
if p.Pkg != nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
var typeErrors []error
|
|
|
|
checker := p.TypeChecker
|
|
|
|
checker.Error = func(err error) {
|
|
|
|
typeErrors = append(typeErrors, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Do typechecking of the package.
|
|
|
|
checker.Importer = p
|
|
|
|
|
|
|
|
typesPkg, err := checker.Check(p.PkgPath, p.fset, p.Files, &p.Info)
|
|
|
|
if err != nil {
|
|
|
|
if err, ok := err.(Errors); ok {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return Errors{p, typeErrors}
|
|
|
|
}
|
|
|
|
p.Pkg = typesPkg
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// parseFiles parses the loaded list of files and returns this list.
|
|
|
|
func (p *Package) parseFiles() ([]*ast.File, error) {
|
|
|
|
// TODO: do this concurrently.
|
|
|
|
var files []*ast.File
|
|
|
|
var fileErrs []error
|
|
|
|
|
|
|
|
var cgoFiles []*ast.File
|
|
|
|
for _, file := range p.GoFiles {
|
|
|
|
f, err := p.parseFile(file, parser.ParseComments)
|
|
|
|
if err != nil {
|
|
|
|
fileErrs = append(fileErrs, err)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
fileErrs = append(fileErrs, err)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
for _, importSpec := range f.Imports {
|
|
|
|
if importSpec.Path.Value == `"C"` {
|
|
|
|
cgoFiles = append(cgoFiles, f)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
files = append(files, f)
|
|
|
|
}
|
|
|
|
if len(cgoFiles) != 0 {
|
|
|
|
cflags := append(p.CFlags, "-I"+filepath.Dir(p.GoFiles[0]))
|
|
|
|
if p.ClangHeaders != "" {
|
|
|
|
cflags = append(cflags, "-Xclang", "-internal-isystem", "-Xclang", p.ClangHeaders)
|
|
|
|
}
|
|
|
|
generated, ldflags, errs := cgo.Process(files, p.Program.Dir, p.fset, cflags)
|
|
|
|
if errs != nil {
|
|
|
|
fileErrs = append(fileErrs, errs...)
|
|
|
|
}
|
|
|
|
files = append(files, generated)
|
|
|
|
p.LDFlags = append(p.LDFlags, ldflags...)
|
|
|
|
}
|
|
|
|
if len(fileErrs) != 0 {
|
|
|
|
return nil, Errors{p, fileErrs}
|
|
|
|
}
|
|
|
|
|
|
|
|
return files, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Import implements types.Importer. It loads and parses packages it encounters
|
|
|
|
// along the way, if needed.
|
|
|
|
func (p *Package) Import(to string) (*types.Package, error) {
|
|
|
|
if to == "unsafe" {
|
|
|
|
return types.Unsafe, nil
|
|
|
|
}
|
|
|
|
if _, ok := p.Imports[to]; ok {
|
|
|
|
return p.Packages[p.Imports[to].PkgPath].Pkg, nil
|
|
|
|
} else {
|
|
|
|
return nil, errors.New("package not imported: " + to)
|
|
|
|
}
|
|
|
|
}
|