Compare commits

...

5 Commits

Author SHA1 Message Date
Ayke van Laethem 4e07fc569c main: add -json flag support to go env
This is needed for VS Code support.
2020-05-08 14:50:37 +02:00
Ayke van Laethem 25e13d887f loader: load packages using Go modules
This commit replaces the existing ad-hoc package loader with a package
loader that uses the x/tools/go/packages package to find all
to-be-loaded packages.
2020-05-08 14:50:26 +02:00
Ayke van Laethem 51daa2a044 main: fix test subcommand
It was broken for quite some time without anybody noticing...
2020-05-08 14:50:26 +02:00
Ayke van Laethem f70dc70247 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.
2020-05-05 00:06:53 +02:00
Ayke van Laethem f24bb1ceda builder: move Go version code to goenv package
This is necessary to avoid a circular dependency in the loader (which
soon will need to read the Go version) and because it seems like a
better place anyway.
2020-05-04 21:58:27 +02:00
10 changed files with 519 additions and 411 deletions
+1 -1
View File
@@ -21,7 +21,7 @@ func NewConfig(options *compileopts.Options) (*compileopts.Config, error) {
if goroot == "" { if goroot == "" {
return nil, errors.New("cannot locate $GOROOT, please set it manually") return nil, errors.New("cannot locate $GOROOT, please set it manually")
} }
major, minor, err := getGorootVersion(goroot) major, minor, err := goenv.GetGorootVersion(goroot)
if err != nil { if err != nil {
return nil, fmt.Errorf("could not read version from GOROOT (%v): %v", goroot, err) return nil, fmt.Errorf("could not read version from GOROOT (%v): %v", goroot, err)
} }
-58
View File
@@ -1,71 +1,13 @@
package builder package builder
import ( import (
"errors"
"fmt"
"io"
"io/ioutil" "io/ioutil"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"regexp"
"sort" "sort"
"strings"
) )
// getGorootVersion returns the major and minor version for a given GOROOT path.
// If the goroot cannot be determined, (0, 0) is returned.
func getGorootVersion(goroot string) (major, minor int, err error) {
s, err := GorootVersionString(goroot)
if err != nil {
return 0, 0, err
}
if s == "" || s[:2] != "go" {
return 0, 0, errors.New("could not parse Go version: version does not start with 'go' prefix")
}
parts := strings.Split(s[2:], ".")
if len(parts) < 2 {
return 0, 0, errors.New("could not parse Go version: version has less than two parts")
}
// Ignore the errors, we don't really handle errors here anyway.
var trailing string
n, err := fmt.Sscanf(s, "go%d.%d%s", &major, &minor, &trailing)
if n == 2 && err == io.EOF {
// Means there were no trailing characters (i.e., not an alpha/beta)
err = nil
}
if err != nil {
return 0, 0, fmt.Errorf("failed to parse version: %s", err)
}
return
}
// GorootVersionString returns the version string as reported by the Go
// toolchain for the given GOROOT path. It is usually of the form `go1.x.y` but
// can have some variations (for beta releases, for example).
func GorootVersionString(goroot string) (string, error) {
if data, err := ioutil.ReadFile(filepath.Join(
goroot, "src", "runtime", "internal", "sys", "zversion.go")); err == nil {
r := regexp.MustCompile("const TheVersion = `(.*)`")
matches := r.FindSubmatch(data)
if len(matches) != 2 {
return "", errors.New("Invalid go version output:\n" + string(data))
}
return string(matches[1]), nil
} else if data, err := ioutil.ReadFile(filepath.Join(goroot, "VERSION")); err == nil {
return string(data), nil
} else {
return "", err
}
}
// getClangHeaderPath returns the path to the built-in Clang headers. It tries // getClangHeaderPath returns the path to the built-in Clang headers. It tries
// multiple locations, which should make it find the directory when installed in // multiple locations, which should make it find the directory when installed in
// various ways. // various ways.
+14 -65
View File
@@ -137,64 +137,26 @@ func Compile(pkgName string, machine llvm.TargetMachine, config *compileopts.Con
c.funcPtrAddrSpace = dummyFunc.Type().PointerAddressSpace() c.funcPtrAddrSpace = dummyFunc.Type().PointerAddressSpace()
dummyFunc.EraseFromParentAsFunction() dummyFunc.EraseFromParentAsFunction()
// Prefix the GOPATH with the system GOROOT, as GOROOT is already set to
// the TinyGo root.
overlayGopath := goenv.Get("GOPATH")
if overlayGopath == "" {
overlayGopath = goenv.Get("GOROOT")
} else {
overlayGopath = goenv.Get("GOROOT") + string(filepath.ListSeparator) + overlayGopath
}
wd, err := os.Getwd() wd, err := os.Getwd()
if err != nil { if err != nil {
return c.mod, nil, []error{err} return c.mod, nil, []error{err}
} }
goroot, err := loader.GetCachedGoroot(c.Config)
if err != nil {
return c.mod, nil, []error{err}
}
lprogram := &loader.Program{ lprogram := &loader.Program{
Build: &build.Context{ Build: &build.Context{
GOARCH: c.GOARCH(), GOARCH: c.GOARCH(),
GOOS: c.GOOS(), GOOS: c.GOOS(),
GOROOT: goenv.Get("GOROOT"), GOROOT: goroot,
GOPATH: goenv.Get("GOPATH"), GOPATH: goenv.Get("GOPATH"),
CgoEnabled: c.CgoEnabled(), CgoEnabled: c.CgoEnabled(),
UseAllFiles: false, UseAllFiles: false,
Compiler: "gc", // must be one of the recognized compilers Compiler: "gc", // must be one of the recognized compilers
BuildTags: c.BuildTags(), BuildTags: c.BuildTags(),
}, },
OverlayBuild: &build.Context{ Tests: c.TestConfig.CompileTestBinary,
GOARCH: c.GOARCH(),
GOOS: c.GOOS(),
GOROOT: goenv.Get("TINYGOROOT"),
GOPATH: overlayGopath,
CgoEnabled: c.CgoEnabled(),
UseAllFiles: false,
Compiler: "gc", // must be one of the recognized compilers
BuildTags: c.BuildTags(),
},
OverlayPath: func(path string) string {
// Return the (overlay) import path when it should be overlaid, and
// "" if it should not.
if strings.HasPrefix(path, tinygoPath+"/src/") {
// Avoid issues with packages that are imported twice, one from
// GOPATH and one from TINYGOPATH.
path = path[len(tinygoPath+"/src/"):]
}
switch path {
case "machine", "os", "reflect", "runtime", "runtime/interrupt", "runtime/volatile", "sync", "testing", "internal/reflectlite", "internal/task":
return path
default:
if strings.HasPrefix(path, "device/") || strings.HasPrefix(path, "examples/") {
return path
} else if path == "syscall" {
for _, tag := range c.BuildTags() {
if tag == "baremetal" || tag == "darwin" {
return path
}
}
}
}
return ""
},
TypeChecker: types.Config{ TypeChecker: types.Config{
Sizes: &stdSizes{ Sizes: &stdSizes{
IntSize: int64(c.targetData.TypeAllocSize(c.intType)), IntSize: int64(c.targetData.TypeAllocSize(c.intType)),
@@ -208,33 +170,17 @@ func Compile(pkgName string, machine llvm.TargetMachine, config *compileopts.Con
ClangHeaders: c.ClangHeaders, ClangHeaders: c.ClangHeaders,
} }
if strings.HasSuffix(pkgName, ".go") { err = lprogram.Load(pkgName)
_, err = lprogram.ImportFile(pkgName)
if err != nil {
return c.mod, nil, []error{err}
}
} else {
_, err = lprogram.Import(pkgName, wd, token.Position{
Filename: "build command-line-arguments",
})
if err != nil {
return c.mod, nil, []error{err}
}
}
_, err = lprogram.Import("runtime", "", token.Position{
Filename: "build default import",
})
if err != nil { if err != nil {
return c.mod, nil, []error{err} return c.mod, nil, []error{err}
} }
err = lprogram.Parse(c.TestConfig.CompileTestBinary) err = lprogram.Parse()
if err != nil { if err != nil {
return c.mod, nil, []error{err} return c.mod, nil, []error{err}
} }
c.ir = ir.NewProgram(lprogram, pkgName) c.ir = ir.NewProgram(lprogram)
// Run a simple dead code elimination pass. // Run a simple dead code elimination pass.
err = c.ir.SimpleDCE() err = c.ir.SimpleDCE()
@@ -378,8 +324,11 @@ func Compile(pkgName string, machine llvm.TargetMachine, config *compileopts.Con
// Gather the list of (C) file paths that should be included in the build. // Gather the list of (C) file paths that should be included in the build.
var extraFiles []string var extraFiles []string
for _, pkg := range c.ir.LoaderProgram.Sorted() { for _, pkg := range c.ir.LoaderProgram.Sorted() {
for _, file := range pkg.CFiles { for _, file := range pkg.OtherFiles {
extraFiles = append(extraFiles, filepath.Join(pkg.Package.Dir, file)) switch strings.ToLower(filepath.Ext(file)) {
case ".c":
extraFiles = append(extraFiles, file)
}
} }
} }
+64
View File
@@ -0,0 +1,64 @@
package goenv
import (
"errors"
"fmt"
"io"
"io/ioutil"
"path/filepath"
"regexp"
"strings"
)
// GetGorootVersion returns the major and minor version for a given GOROOT path.
// If the goroot cannot be determined, (0, 0) is returned.
func GetGorootVersion(goroot string) (major, minor int, err error) {
s, err := GorootVersionString(goroot)
if err != nil {
return 0, 0, err
}
if s == "" || s[:2] != "go" {
return 0, 0, errors.New("could not parse Go version: version does not start with 'go' prefix")
}
parts := strings.Split(s[2:], ".")
if len(parts) < 2 {
return 0, 0, errors.New("could not parse Go version: version has less than two parts")
}
// Ignore the errors, we don't really handle errors here anyway.
var trailing string
n, err := fmt.Sscanf(s, "go%d.%d%s", &major, &minor, &trailing)
if n == 2 && err == io.EOF {
// Means there were no trailing characters (i.e., not an alpha/beta)
err = nil
}
if err != nil {
return 0, 0, fmt.Errorf("failed to parse version: %s", err)
}
return
}
// GorootVersionString returns the version string as reported by the Go
// toolchain for the given GOROOT path. It is usually of the form `go1.x.y` but
// can have some variations (for beta releases, for example).
func GorootVersionString(goroot string) (string, error) {
if data, err := ioutil.ReadFile(filepath.Join(
goroot, "src", "runtime", "internal", "sys", "zversion.go")); err == nil {
r := regexp.MustCompile("const TheVersion = `(.*)`")
matches := r.FindSubmatch(data)
if len(matches) != 2 {
return "", errors.New("Invalid go version output:\n" + string(data))
}
return string(matches[1]), nil
} else if data, err := ioutil.ReadFile(filepath.Join(goroot, "VERSION")); err == nil {
return string(data), nil
} else {
return "", err
}
}
+4 -25
View File
@@ -63,23 +63,13 @@ const (
) )
// Create and initialize a new *Program from a *ssa.Program. // Create and initialize a new *Program from a *ssa.Program.
func NewProgram(lprogram *loader.Program, mainPath string) *Program { func NewProgram(lprogram *loader.Program) *Program {
program := lprogram.LoadSSA() program := lprogram.LoadSSA()
program.Build() program.Build()
// Find the main package, which is a bit difficult when running a .go file // Find the main package, which is a bit difficult when running a .go file
// directly. // directly.
mainPkg := program.ImportedPackage(mainPath) mainPkg := program.ImportedPackage(lprogram.MainPkg.PkgPath)
if mainPkg == nil {
for _, pkgInfo := range program.AllPackages() {
if pkgInfo.Pkg.Name() == "main" {
if mainPkg != nil {
panic("more than one main package found")
}
mainPkg = pkgInfo
}
}
}
if mainPkg == nil { if mainPkg == nil {
panic("could not find main package") panic("could not find main package")
} }
@@ -87,21 +77,10 @@ func NewProgram(lprogram *loader.Program, mainPath string) *Program {
// Make a list of packages in import order. // Make a list of packages in import order.
packageList := []*ssa.Package{} packageList := []*ssa.Package{}
packageSet := map[string]struct{}{} packageSet := map[string]struct{}{}
worklist := []string{"runtime", mainPath} worklist := []string{"runtime", lprogram.MainPkg.PkgPath}
for len(worklist) != 0 { for len(worklist) != 0 {
pkgPath := worklist[0] pkgPath := worklist[0]
var pkg *ssa.Package pkg := program.ImportedPackage(pkgPath)
if pkgPath == mainPath {
pkg = mainPkg // necessary for compiling individual .go files
} else {
pkg = program.ImportedPackage(pkgPath)
}
if pkg == nil {
// Non-SSA package (e.g. cgo).
packageSet[pkgPath] = struct{}{}
worklist = worklist[1:]
continue
}
if _, ok := packageSet[pkgPath]; ok { if _, ok := packageSet[pkgPath]; ok {
// Package already in the final package list. // Package already in the final package list.
worklist = worklist[1:] worklist = worklist[1:]
-27
View File
@@ -1,10 +1,5 @@
package loader package loader
import (
"go/token"
"strings"
)
// Errors contains a list of parser errors or a list of typechecker errors for // Errors contains a list of parser errors or a list of typechecker errors for
// the given package. // the given package.
type Errors struct { type Errors struct {
@@ -15,25 +10,3 @@ type Errors struct {
func (e Errors) Error() string { func (e Errors) Error() string {
return "could not compile: " + e.Errs[0].Error() return "could not compile: " + e.Errs[0].Error()
} }
// ImportCycleErrors is returned when encountering an import cycle. The list of
// packages is a list from the root package to the leaf package that imports one
// of the packages in the list.
type ImportCycleError struct {
Packages []string
ImportPositions []token.Position
}
func (e *ImportCycleError) Error() string {
var msg strings.Builder
msg.WriteString("import cycle:\n\t")
msg.WriteString(strings.Join(e.Packages, "\n\t"))
msg.WriteString("\n at ")
for i, pos := range e.ImportPositions {
if i > 0 {
msg.WriteString(", ")
}
msg.WriteString(pos.String())
}
return msg.String()
}
+225
View File
@@ -0,0 +1,225 @@
package loader
// This file constructs a new temporary GOROOT directory by merging both the
// standard Go GOROOT and the GOROOT from TinyGo using symlinks.
import (
"crypto/sha512"
"encoding/hex"
"errors"
"io/ioutil"
"math/rand"
"os"
"os/exec"
"path"
"path/filepath"
"runtime"
"strconv"
"github.com/tinygo-org/tinygo/compileopts"
"github.com/tinygo-org/tinygo/goenv"
)
// GetCachedGoroot creates a new GOROOT by merging both the standard GOROOT and
// the GOROOT from TinyGo using lots of symbolic links.
func GetCachedGoroot(config *compileopts.Config) (string, error) {
goroot := goenv.Get("GOROOT")
if goroot == "" {
return "", errors.New("could not determine GOROOT")
}
tinygoroot := goenv.Get("TINYGOROOT")
if tinygoroot == "" {
return "", errors.New("could not determine TINYGOROOT")
}
needsSyscallPackage := false
for _, tag := range config.BuildTags() {
if tag == "baremetal" || tag == "darwin" {
needsSyscallPackage = true
}
}
// Determine the location of the cached GOROOT.
version, err := goenv.GorootVersionString(goroot)
if err != nil {
return "", err
}
gorootsHash := sha512.Sum512_256([]byte(goroot + "\x00" + tinygoroot))
gorootsHashHex := hex.EncodeToString(gorootsHash[:])
cachedgoroot := filepath.Join(goenv.Get("GOCACHE"), "goroot-"+version+"-"+gorootsHashHex)
if needsSyscallPackage {
cachedgoroot += "-syscall"
}
if _, err := os.Stat(cachedgoroot); err == nil {
return cachedgoroot, nil
}
tmpgoroot := cachedgoroot + ".tmp" + strconv.Itoa(rand.Int())
err = os.MkdirAll(tmpgoroot, 0777)
if err != nil {
return "", err
}
// Remove the temporary directory if it wasn't moved to the right place
// (for example, when there was an error).
defer os.RemoveAll(tmpgoroot)
for _, name := range []string{"bin", "lib", "pkg"} {
err = symlink(filepath.Join(goroot, name), filepath.Join(tmpgoroot, name))
if err != nil {
return "", err
}
}
err = mergeDirectory(goroot, tinygoroot, tmpgoroot, "", pathsToOverride(needsSyscallPackage))
if err != nil {
return "", err
}
err = os.Rename(tmpgoroot, cachedgoroot)
if err != nil {
if os.IsExist(err) {
// Another invocation of TinyGo also seems to have created a GOROOT.
// Use that one instead. Our new GOROOT will be automatically
// deleted by the defer above.
return cachedgoroot, nil
}
return "", err
}
return cachedgoroot, nil
}
// mergeDirectory merges two roots recursively. The tmpgoroot is the directory
// that will be created by this call by either symlinking the directory from
// goroot or tinygoroot, or by creating the directory and merging the contents.
func mergeDirectory(goroot, tinygoroot, tmpgoroot, importPath string, overrides map[string]bool) error {
if mergeSubdirs, ok := overrides[importPath+"/"]; ok {
if !mergeSubdirs {
// This directory and all subdirectories should come from the TinyGo
// root, so simply make a symlink.
newname := filepath.Join(tmpgoroot, "src", importPath)
oldname := filepath.Join(tinygoroot, "src", importPath)
return symlink(oldname, newname)
}
// Merge subdirectories. Start by making the directory to merge.
err := os.Mkdir(filepath.Join(tmpgoroot, "src", importPath), 0777)
if err != nil {
return err
}
// Symlink all files from TinyGo, and symlink directories from TinyGo
// that need to be overridden.
tinygoEntries, err := ioutil.ReadDir(filepath.Join(tinygoroot, "src", importPath))
if err != nil {
return err
}
for _, e := range tinygoEntries {
if e.IsDir() {
// A directory, so merge this thing.
err := mergeDirectory(goroot, tinygoroot, tmpgoroot, path.Join(importPath, e.Name()), overrides)
if err != nil {
return err
}
} else {
// A file, so symlink this.
newname := filepath.Join(tmpgoroot, "src", importPath, e.Name())
oldname := filepath.Join(tinygoroot, "src", importPath, e.Name())
err := symlink(oldname, newname)
if err != nil {
return err
}
}
}
// Symlink all directories from $GOROOT that are not part of the TinyGo
// overrides.
gorootEntries, err := ioutil.ReadDir(filepath.Join(goroot, "src", importPath))
if err != nil {
return err
}
for _, e := range gorootEntries {
if !e.IsDir() {
// Don't merge in files from Go. Otherwise we'd end up with a
// weird syscall package with files from both roots.
continue
}
if _, ok := overrides[path.Join(importPath, e.Name())+"/"]; ok {
// Already included above, so don't bother trying to create this
// symlink.
continue
}
newname := filepath.Join(tmpgoroot, "src", importPath, e.Name())
oldname := filepath.Join(goroot, "src", importPath, e.Name())
err := symlink(oldname, newname)
if err != nil {
return err
}
}
}
return nil
}
// The boolean indicates whether to merge the subdirs. True means merge, false
// means use the TinyGo version.
func pathsToOverride(needsSyscallPackage bool) map[string]bool {
paths := map[string]bool{
"/": true,
"device/": false,
"examples/": false,
"internal/": true,
"internal/reflectlite/": false,
"internal/task/": false,
"machine/": false,
"os/": true,
"reflect/": false,
"runtime/": false,
"sync/": true,
"testing/": false,
}
if needsSyscallPackage {
paths["syscall/"] = true // include syscall/js
}
return paths
}
// symlink creates a symlink or something similar. On Unix-like systems, it
// always creates a symlink. On Windows, it tries to create a symlink and if
// that fails, creates a hardlink or directory junction instead.
//
// Note that while Windows 10 does support symlinks and allows them to be
// created using os.Symlink, it requires developer mode to be enabled.
// Therefore provide a fallback for when symlinking is not possible.
// Unfortunately this fallback only works when TinyGo is installed on the same
// filesystem as the TinyGo cache and the Go installation (which is usually the
// C drive).
func symlink(oldname, newname string) error {
symlinkErr := os.Symlink(oldname, newname)
if runtime.GOOS == "windows" && symlinkErr != nil {
// Fallback for when developer mode is disabled.
// Note that we return the symlink error even if something else fails
// later on. This is because symlinks are the easiest to support
// (they're also used on Linux and MacOS) and enabling them is easy:
// just enable developer mode.
st, err := os.Stat(oldname)
if err != nil {
return symlinkErr
}
if st.IsDir() {
// Make a directory junction. There may be a way to do this
// programmatically, but it involves a lot of magic. Use the mklink
// command built into cmd instead (mklink is a builtin, not an
// external command).
err := exec.Command("cmd", "/k", "mklink", "/J", newname, oldname).Run()
if err != nil {
return symlinkErr
}
} else {
// Make a hard link.
err := os.Link(oldname, newname)
if err != nil {
return symlinkErr
}
}
return nil // success
}
return symlinkErr
}
+127 -219
View File
@@ -3,10 +3,10 @@ package loader
import ( import (
"bytes" "bytes"
"errors" "errors"
"fmt"
"go/ast" "go/ast"
"go/build" "go/build"
"go/parser" "go/parser"
"go/scanner"
"go/token" "go/token"
"go/types" "go/types"
"os" "os"
@@ -16,15 +16,16 @@ import (
"text/template" "text/template"
"github.com/tinygo-org/tinygo/cgo" "github.com/tinygo-org/tinygo/cgo"
"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. // Program holds all packages and some metadata about the program as a whole.
type Program struct { type Program struct {
mainPkg string
Build *build.Context Build *build.Context
OverlayBuild *build.Context Tests bool
OverlayPath func(path string) string
Packages map[string]*Package Packages map[string]*Package
MainPkg *Package
sorted []*Package sorted []*Package
fset *token.FileSet fset *token.FileSet
TypeChecker types.Config TypeChecker types.Config
@@ -37,89 +38,114 @@ type Program struct {
// Package holds a loaded package, its imports, and its parsed files. // Package holds a loaded package, its imports, and its parsed files.
type Package struct { type Package struct {
*Program *Program
*build.Package *packages.Package
Imports map[string]*Package Files []*ast.File
Importing bool Pkg *types.Package
Files []*ast.File
Pkg *types.Package
types.Info types.Info
} }
// Import loads the given package relative to srcDir (for the vendor directory). // Load loads the given package with all dependencies (including the runtime
// It only loads the current package without recursion. // package). Call .Parse() afterwards to parse all Go files (including CGo
func (p *Program) Import(path, srcDir string, pos token.Position) (*Package, error) { // processing, if necessary).
func (p *Program) Load(importPath string) error {
if p.Packages == nil { if p.Packages == nil {
p.Packages = make(map[string]*Package) p.Packages = make(map[string]*Package)
} }
// Load this package. err := p.loadPackage(importPath)
ctx := p.Build
if newPath := p.OverlayPath(path); newPath != "" {
ctx = p.OverlayBuild
path = newPath
}
buildPkg, err := ctx.Import(path, srcDir, build.ImportComment)
if err != nil { if err != nil {
return nil, scanner.Error{ return err
Pos: pos,
Msg: err.Error(), // TODO: define a new error type that will wrap the inner error
}
} }
if existingPkg, ok := p.Packages[buildPkg.ImportPath]; ok { p.MainPkg = p.sorted[len(p.sorted)-1]
// Already imported, or at least started the import. if _, ok := p.Packages["runtime"]; !ok {
return existingPkg, nil // 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")
} }
p.sorted = nil // invalidate the sorted order of packages return nil
pkg := p.newPackage(buildPkg)
p.Packages[buildPkg.ImportPath] = pkg
if p.mainPkg == "" {
p.mainPkg = buildPkg.ImportPath
}
return pkg, nil
} }
// ImportFile loads and parses the import statements in the given path and func (p *Program) loadPackage(importPath string) error {
// creates a pseudo-package out of it. cgoEnabled := "0"
func (p *Program) ImportFile(path string) (*Package, error) { if p.Build.CgoEnabled {
if p.Packages == nil { cgoEnabled = "1"
p.Packages = make(map[string]*Package)
} }
if _, ok := p.Packages[path]; ok { pkgs, err := packages.Load(&packages.Config{
// unlikely Mode: packages.NeedName | packages.NeedFiles | packages.NeedImports | packages.NeedDeps,
return nil, errors.New("loader: cannot import file that is already imported as package: " + path) 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,
file, err := p.parseFile(path, parser.ImportsOnly) }, importPath)
if err != nil { if err != nil {
return nil, err return err
} }
buildPkg := &build.Package{ var pkg *packages.Package
Dir: filepath.Dir(path), if p.Tests {
ImportPath: path, // We need the second package. Quoting from the docs:
GoFiles: []string{filepath.Base(path)}, // > 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]
} }
for _, importSpec := range file.Imports { var importError *Errors
buildPkg.Imports = append(buildPkg.Imports, importSpec.Path.Value[1:len(importSpec.Path.Value)-1]) var addPackages func(pkg *packages.Package)
} addPackages = func(pkg *packages.Package) {
p.sorted = nil // invalidate the sorted order of packages if _, ok := p.Packages[pkg.PkgPath]; ok {
pkg := p.newPackage(buildPkg) return
p.Packages[buildPkg.ImportPath] = pkg }
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 {
importError.Errs = append(importError.Errs, err)
}
return
}
if p.mainPkg == "" { // Get the list of imports (sorted alphabetically).
p.mainPkg = buildPkg.ImportPath names := make([]string, 0, len(pkg.Imports))
} for name := range pkg.Imports {
names = append(names, name)
}
sort.Strings(names)
return pkg, nil // 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
} }
// newPackage instantiates a new *Package object with initialized members. // newPackage instantiates a new *Package object with initialized members.
func (p *Program) newPackage(pkg *build.Package) *Package { func (p *Program) newPackage(pkg *packages.Package) *Package {
return &Package{ return &Package{
Program: p, Program: p,
Package: pkg, Package: pkg,
Imports: make(map[string]*Package, len(pkg.Imports)),
Info: types.Info{ Info: types.Info{
Types: make(map[ast.Expr]types.TypeAndValue), Types: make(map[ast.Expr]types.TypeAndValue),
Defs: make(map[*ast.Ident]types.Object), Defs: make(map[*ast.Ident]types.Object),
@@ -134,87 +160,25 @@ func (p *Program) newPackage(pkg *build.Package) *Package {
// Sorted returns a list of all packages, sorted in a way that no packages come // Sorted returns a list of all packages, sorted in a way that no packages come
// before the packages they depend upon. // before the packages they depend upon.
func (p *Program) Sorted() []*Package { func (p *Program) Sorted() []*Package {
if p.sorted == nil {
p.sort()
}
return p.sorted return p.sorted
} }
func (p *Program) sort() { // Parse parses all packages and typechecks them.
p.sorted = nil
packageList := make([]*Package, 0, len(p.Packages))
packageSet := make(map[string]struct{}, len(p.Packages))
worklist := make([]string, 0, len(p.Packages))
for path := range p.Packages {
worklist = append(worklist, path)
}
sort.Strings(worklist)
for len(worklist) != 0 {
pkgPath := worklist[0]
pkg := p.Packages[pkgPath]
if _, ok := packageSet[pkgPath]; ok {
// Package already in the final package list.
worklist = worklist[1:]
continue
}
unsatisfiedImports := make([]string, 0)
for _, pkg := range pkg.Imports {
if _, ok := packageSet[pkg.ImportPath]; ok {
continue
}
unsatisfiedImports = append(unsatisfiedImports, pkg.ImportPath)
}
sort.Strings(unsatisfiedImports)
if len(unsatisfiedImports) == 0 {
// All dependencies of this package are satisfied, so add this
// package to the list.
packageList = append(packageList, pkg)
packageSet[pkgPath] = struct{}{}
worklist = worklist[1:]
} else {
// Prepend all dependencies to the worklist and reconsider this
// package (by not removing it from the worklist). At that point, it
// must be possible to add it to packageList.
worklist = append(unsatisfiedImports, worklist...)
}
}
p.sorted = packageList
}
// Parse recursively imports all packages, parses them, and typechecks them.
// //
// The returned error may be an Errors error, which contains a list of errors. // The returned error may be an Errors error, which contains a list of errors.
// //
// Idempotent. // Idempotent.
func (p *Program) Parse(compileTestBinary bool) error { func (p *Program) Parse() error {
includeTests := compileTestBinary
// Load all imports
for _, pkg := range p.Sorted() {
err := pkg.importRecursively(includeTests)
if err != nil {
if err, ok := err.(*ImportCycleError); ok {
if pkg.ImportPath != err.Packages[0] {
err.Packages = append([]string{pkg.ImportPath}, err.Packages...)
}
}
return err
}
}
// Parse all packages. // Parse all packages.
for _, pkg := range p.Sorted() { for _, pkg := range p.Sorted() {
err := pkg.Parse(includeTests) err := pkg.Parse()
if err != nil { if err != nil {
return err return err
} }
} }
if compileTestBinary { if p.Tests {
err := p.SwapTestMain() err := p.swapTestMain()
if err != nil { if err != nil {
return err return err
} }
@@ -231,7 +195,7 @@ func (p *Program) Parse(compileTestBinary bool) error {
return nil return nil
} }
func (p *Program) SwapTestMain() error { func (p *Program) swapTestMain() error {
var tests []string var tests []string
isTestFunc := func(f *ast.FuncDecl) bool { isTestFunc := func(f *ast.FuncDecl) bool {
@@ -241,8 +205,7 @@ func (p *Program) SwapTestMain() error {
} }
return false return false
} }
mainPkg := p.Packages[p.mainPkg] for _, f := range p.MainPkg.Files {
for _, f := range mainPkg.Files {
for i, d := range f.Decls { for i, d := range f.Decls {
switch v := d.(type) { switch v := d.(type) {
case *ast.FuncDecl: case *ast.FuncDecl:
@@ -293,7 +256,7 @@ func main () {
if err != nil { if err != nil {
return err return err
} }
path := filepath.Join(p.mainPkg, "$testmain.go") path := filepath.Join(p.MainPkg.Dir, "$testmain.go")
if p.fset == nil { if p.fset == nil {
p.fset = token.NewFileSet() p.fset = token.NewFileSet()
@@ -303,7 +266,7 @@ func main () {
if err != nil { if err != nil {
return err return err
} }
mainPkg.Files = append(mainPkg.Files, newMain) p.MainPkg.Files = append(p.MainPkg.Files, newMain)
return nil return nil
} }
@@ -319,34 +282,41 @@ func (p *Program) parseFile(path string, mode parser.Mode) (*ast.File, error) {
return nil, err return nil, err
} }
defer rd.Close() defer rd.Close()
relpath := path diagnosticPath := path
if filepath.IsAbs(path) { if strings.HasPrefix(path, p.Build.GOROOT+string(filepath.Separator)) {
rp, err := filepath.Rel(p.Dir, path) // If this file is part of the synthetic GOROOT, try to infer the
if err == nil { // original path.
relpath = rp relpath := path[len(p.Build.GOROOT)+1:]
tinygoPath := filepath.Join(p.TINYGOROOT, relpath)
if _, err := os.Stat(tinygoPath); err == nil {
diagnosticPath = tinygoPath
}
realgorootPath := filepath.Join(goenv.Get("GOROOT"), relpath)
if _, err := os.Stat(realgorootPath); err == nil {
diagnosticPath = realgorootPath
} }
} }
return parser.ParseFile(p.fset, relpath, rd, mode) return parser.ParseFile(p.fset, diagnosticPath, rd, mode)
} }
// Parse parses and typechecks this package. // Parse parses and typechecks this package.
// //
// Idempotent. // Idempotent.
func (p *Package) Parse(includeTests bool) error { func (p *Package) Parse() error {
if len(p.Files) != 0 { if len(p.Files) != 0 {
return nil return nil
} }
// Load the AST. // Load the AST.
// TODO: do this in parallel. // TODO: do this in parallel.
if p.ImportPath == "unsafe" { if p.PkgPath == "unsafe" {
// Special case for the unsafe package. Don't even bother loading // Special case for the unsafe package. Don't even bother loading
// the files. // the files.
p.Pkg = types.Unsafe p.Pkg = types.Unsafe
return nil return nil
} }
files, err := p.parseFiles(includeTests) files, err := p.parseFiles()
if err != nil { if err != nil {
return err return err
} }
@@ -373,7 +343,7 @@ func (p *Package) Check() error {
// Do typechecking of the package. // Do typechecking of the package.
checker.Importer = p checker.Importer = p
typesPkg, err := checker.Check(p.ImportPath, p.fset, p.Files, &p.Info) typesPkg, err := checker.Check(p.PkgPath, p.fset, p.Files, &p.Info)
if err != nil { if err != nil {
if err, ok := err.(Errors); ok { if err, ok := err.(Errors); ok {
return err return err
@@ -385,22 +355,14 @@ func (p *Package) Check() error {
} }
// parseFiles parses the loaded list of files and returns this list. // parseFiles parses the loaded list of files and returns this list.
func (p *Package) parseFiles(includeTests bool) ([]*ast.File, error) { func (p *Package) parseFiles() ([]*ast.File, error) {
// TODO: do this concurrently. // TODO: do this concurrently.
var files []*ast.File var files []*ast.File
var fileErrs []error var fileErrs []error
var gofiles []string var cgoFiles []*ast.File
if includeTests { for _, file := range p.GoFiles {
gofiles = make([]string, 0, len(p.GoFiles)+len(p.TestGoFiles)) f, err := p.parseFile(file, parser.ParseComments)
gofiles = append(gofiles, p.GoFiles...)
gofiles = append(gofiles, p.TestGoFiles...)
} else {
gofiles = p.GoFiles
}
for _, file := range gofiles {
f, err := p.parseFile(filepath.Join(p.Package.Dir, file), parser.ParseComments)
if err != nil { if err != nil {
fileErrs = append(fileErrs, err) fileErrs = append(fileErrs, err)
continue continue
@@ -409,19 +371,15 @@ func (p *Package) parseFiles(includeTests bool) ([]*ast.File, error) {
fileErrs = append(fileErrs, err) fileErrs = append(fileErrs, err)
continue continue
} }
for _, importSpec := range f.Imports {
if importSpec.Path.Value == `"C"` {
cgoFiles = append(cgoFiles, f)
}
}
files = append(files, f) files = append(files, f)
} }
for _, file := range p.CgoFiles { if len(cgoFiles) != 0 {
path := filepath.Join(p.Package.Dir, file) cflags := append(p.CFlags, "-I"+filepath.Dir(p.GoFiles[0]))
f, err := p.parseFile(path, parser.ParseComments)
if err != nil {
fileErrs = append(fileErrs, err)
continue
}
files = append(files, f)
}
if len(p.CgoFiles) != 0 {
cflags := append(p.CFlags, "-I"+p.Package.Dir)
if p.ClangHeaders != "" { if p.ClangHeaders != "" {
cflags = append(cflags, "-Xclang", "-internal-isystem", "-Xclang", p.ClangHeaders) cflags = append(cflags, "-Xclang", "-internal-isystem", "-Xclang", p.ClangHeaders)
} }
@@ -445,58 +403,8 @@ func (p *Package) Import(to string) (*types.Package, error) {
return types.Unsafe, nil return types.Unsafe, nil
} }
if _, ok := p.Imports[to]; ok { if _, ok := p.Imports[to]; ok {
return p.Imports[to].Pkg, nil return p.Packages[p.Imports[to].PkgPath].Pkg, nil
} else { } else {
return nil, errors.New("package not imported: " + to) return nil, errors.New("package not imported: " + to)
} }
} }
// importRecursively calls Program.Import() on all imported packages, and calls
// importRecursively() on the imported packages as well.
//
// Idempotent.
func (p *Package) importRecursively(includeTests bool) error {
p.Importing = true
imports := p.Package.Imports
if includeTests {
imports = append(imports, p.Package.TestImports...)
}
for _, to := range imports {
if to == "C" {
// Do CGo processing in a later stage.
continue
}
if _, ok := p.Imports[to]; ok {
continue
}
// Find error location.
var pos token.Position
if len(p.Package.ImportPos[to]) > 0 {
pos = p.Package.ImportPos[to][0]
} else {
pos = token.Position{Filename: p.Package.ImportPath}
}
importedPkg, err := p.Program.Import(to, p.Package.Dir, pos)
if err != nil {
if err, ok := err.(*ImportCycleError); ok {
err.Packages = append([]string{p.ImportPath}, err.Packages...)
}
return err
}
if importedPkg.Importing {
return &ImportCycleError{[]string{p.ImportPath, importedPkg.ImportPath}, p.ImportPos[to]}
}
err = importedPkg.importRecursively(false)
if err != nil {
if err, ok := err.(*ImportCycleError); ok {
err.Packages = append([]string{p.ImportPath}, err.Packages...)
}
return err
}
p.Imports[to] = importedPkg
}
p.Importing = false
return nil
}
+83 -15
View File
@@ -2,6 +2,7 @@ package main
import ( import (
"bytes" "bytes"
"encoding/json"
"errors" "errors"
"flag" "flag"
"fmt" "fmt"
@@ -124,6 +125,7 @@ func Build(pkgName, outpath string, options *compileopts.Options) error {
// Test runs the tests in the given package. // Test runs the tests in the given package.
func Test(pkgName string, options *compileopts.Options) error { func Test(pkgName string, options *compileopts.Options) error {
options.TestConfig.CompileTestBinary = true
config, err := builder.NewConfig(options) config, err := builder.NewConfig(options)
if err != nil { if err != nil {
return err return err
@@ -135,7 +137,6 @@ func Test(pkgName string, options *compileopts.Options) error {
// For details: https://github.com/golang/go/issues/21360 // For details: https://github.com/golang/go/issues/21360
config.Target.BuildTags = append(config.Target.BuildTags, "test") config.Target.BuildTags = append(config.Target.BuildTags, "test")
options.TestConfig.CompileTestBinary = true
return builder.Build(pkgName, ".elf", config, func(tmppath string) error { return builder.Build(pkgName, ".elf", config, func(tmppath string) error {
cmd := exec.Command(tmppath) cmd := exec.Command(tmppath)
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
@@ -641,6 +642,36 @@ func getDefaultPort() (port string, err error) {
return d[0], nil return d[0], nil
} }
// runGoList runs the `go list` command but using the configuration used for
// TinyGo.
func runGoList(config *compileopts.Config, flagJSON, flagDeps bool, pkgs []string) error {
goroot, err := loader.GetCachedGoroot(config)
if err != nil {
return err
}
args := []string{"list"}
if flagJSON {
args = append(args, "-json")
}
if flagDeps {
args = append(args, "-deps")
}
if len(config.BuildTags()) != 0 {
args = append(args, "-tags", strings.Join(config.BuildTags(), " "))
}
args = append(args, pkgs...)
cgoEnabled := "0"
if config.CgoEnabled() {
cgoEnabled = "1"
}
cmd := exec.Command("go", args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Env = append(os.Environ(), "GOROOT="+goroot, "GOOS="+config.GOOS(), "GOARCH="+config.GOARCH(), "CGO_ENABLED="+cgoEnabled)
cmd.Run()
return nil
}
func usage() { func usage() {
fmt.Fprintln(os.Stderr, "TinyGo is a Go compiler for small places.") fmt.Fprintln(os.Stderr, "TinyGo is a Go compiler for small places.")
fmt.Fprintln(os.Stderr, "version:", version) fmt.Fprintln(os.Stderr, "version:", version)
@@ -652,6 +683,7 @@ func usage() {
fmt.Fprintln(os.Stderr, " flash: compile and flash to the device") fmt.Fprintln(os.Stderr, " flash: compile and flash to the device")
fmt.Fprintln(os.Stderr, " gdb: run/flash and immediately enter GDB") fmt.Fprintln(os.Stderr, " gdb: run/flash and immediately enter GDB")
fmt.Fprintln(os.Stderr, " env: list environment variables used during build") fmt.Fprintln(os.Stderr, " env: list environment variables used during build")
fmt.Fprintln(os.Stderr, " list: run go list using the TinyGo root")
fmt.Fprintln(os.Stderr, " clean: empty cache directory ("+goenv.Get("GOCACHE")+")") fmt.Fprintln(os.Stderr, " clean: empty cache directory ("+goenv.Get("GOCACHE")+")")
fmt.Fprintln(os.Stderr, " help: print this help text") fmt.Fprintln(os.Stderr, " help: print this help text")
fmt.Fprintln(os.Stderr, "\nflags:") fmt.Fprintln(os.Stderr, "\nflags:")
@@ -683,7 +715,7 @@ func printCompilerError(logln func(...interface{}), err error) {
} }
} }
case loader.Errors: case loader.Errors:
logln("#", err.Pkg.ImportPath) logln("#", err.Pkg.PkgPath)
for _, err := range err.Errs { for _, err := range err.Errs {
logln(err) logln(err)
} }
@@ -706,6 +738,13 @@ func handleCompilerError(err error) {
} }
func main() { func main() {
if len(os.Args) < 2 {
fmt.Fprintln(os.Stderr, "No command-line arguments supplied.")
usage()
os.Exit(1)
}
command := os.Args[1]
outpath := flag.String("o", "", "output filename") outpath := flag.String("o", "", "output filename")
opt := flag.String("opt", "z", "optimization level: 0, 1, 2, s, z") opt := flag.String("opt", "z", "optimization level: 0, 1, 2, s, z")
gc := flag.String("gc", "", "garbage collector to use (none, leaking, extalloc, conservative)") gc := flag.String("gc", "", "garbage collector to use (none, leaking, extalloc, conservative)")
@@ -726,12 +765,13 @@ func main() {
wasmAbi := flag.String("wasm-abi", "js", "WebAssembly ABI conventions: js (no i64 params) or generic") wasmAbi := flag.String("wasm-abi", "js", "WebAssembly ABI conventions: js (no i64 params) or generic")
heapSize := flag.String("heap-size", "1M", "default heap size in bytes (only supported by WebAssembly)") heapSize := flag.String("heap-size", "1M", "default heap size in bytes (only supported by WebAssembly)")
if len(os.Args) < 2 { var flagJSON, flagDeps *bool
fmt.Fprintln(os.Stderr, "No command-line arguments supplied.") if command == "list" || command == "env" {
usage() flagJSON = flag.Bool("json", false, "print data in JSON format")
os.Exit(1) }
if command == "list" {
flagDeps = flag.Bool("deps", false, "")
} }
command := os.Args[1]
// Early command processing, before commands are interpreted by the Go flag // Early command processing, before commands are interpreted by the Go flag
// library. // library.
@@ -895,6 +935,18 @@ func main() {
fmt.Printf("build tags: %s\n", strings.Join(config.BuildTags(), " ")) fmt.Printf("build tags: %s\n", strings.Join(config.BuildTags(), " "))
fmt.Printf("garbage collector: %s\n", config.GC()) fmt.Printf("garbage collector: %s\n", config.GC())
fmt.Printf("scheduler: %s\n", config.Scheduler()) fmt.Printf("scheduler: %s\n", config.Scheduler())
case "list":
config, err := builder.NewConfig(options)
if err != nil {
fmt.Fprintln(os.Stderr, err)
usage()
os.Exit(1)
}
err = runGoList(config, *flagJSON, *flagDeps, flag.Args())
if err != nil {
fmt.Fprintln(os.Stderr, "failed to run `go list`:", err)
os.Exit(1)
}
case "clean": case "clean":
// remove cache directory // remove cache directory
err := os.RemoveAll(goenv.Get("GOCACHE")) err := os.RemoveAll(goenv.Get("GOCACHE"))
@@ -906,20 +958,36 @@ func main() {
usage() usage()
case "version": case "version":
goversion := "<unknown>" goversion := "<unknown>"
if s, err := builder.GorootVersionString(goenv.Get("GOROOT")); err == nil { if s, err := goenv.GorootVersionString(goenv.Get("GOROOT")); err == nil {
goversion = s goversion = s
} }
fmt.Printf("tinygo version %s %s/%s (using go version %s and LLVM version %s)\n", version, runtime.GOOS, runtime.GOARCH, goversion, llvm.Version) fmt.Printf("tinygo version %s %s/%s (using go version %s and LLVM version %s)\n", version, runtime.GOOS, runtime.GOARCH, goversion, llvm.Version)
case "env": case "env":
if flag.NArg() == 0 { if *flagJSON {
// Show all environment variables. keys := goenv.Keys
for _, key := range goenv.Keys { if flag.NArg() != 0 {
fmt.Printf("%s=%#v\n", key, goenv.Get(key)) // Show only one (or a few) environment variables.
keys = flag.Args()
} }
// Show environment variables in JSON format.
env := make(map[string]string)
for _, key := range keys {
env[key] = goenv.Get(key)
}
encoder := json.NewEncoder(os.Stdout)
encoder.SetIndent("", "\t")
encoder.Encode(env)
} else { } else {
// Show only one (or a few) environment variables. if flag.NArg() == 0 {
for i := 0; i < flag.NArg(); i++ { // Show all environment variables.
fmt.Println(goenv.Get(flag.Arg(i))) for _, key := range goenv.Keys {
fmt.Printf("%s=%#v\n", key, goenv.Get(key))
}
} else {
// Show only one (or a few) environment variables.
for i := 0; i < flag.NArg(); i++ {
fmt.Println(goenv.Get(flag.Arg(i)))
}
} }
} }
default: default:
+1 -1
View File
@@ -73,7 +73,7 @@ func TestCompiler(t *testing.T) {
t.Run("ARM64Linux", func(t *testing.T) { t.Run("ARM64Linux", func(t *testing.T) {
runPlatTests("aarch64--linux-gnu", matches, t) runPlatTests("aarch64--linux-gnu", matches, t)
}) })
goVersion, err := builder.GorootVersionString(goenv.Get("GOROOT")) goVersion, err := goenv.GorootVersionString(goenv.Get("GOROOT"))
if err != nil { if err != nil {
t.Error("could not get Go version:", err) t.Error("could not get Go version:", err)
return return