mirror of
https://github.com/tinygo-org/tinygo.git
synced 2026-08-13 23:43:40 +00:00
builder: interpret linker error messages
This shows nicely formatted error messages for missing symbol names and for out-of-flash, out-of-RAM conditions (on microcontrollers with limited flash/RAM). Unfortunately the missing symbol name errors aren't available on Windows and WebAssembly because the linker doesn't report source locations yet. This is something that I could perhaps improve in LLD.
This commit is contained in:
committed by
Ron Evans
parent
2eb39785fe
commit
2e76cd3687
+1
-1
@@ -779,7 +779,7 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe
|
|||||||
}
|
}
|
||||||
err = link(config.Target.Linker, ldflags...)
|
err = link(config.Target.Linker, ldflags...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &commandError{"failed to link", result.Executable, err}
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var calculatedStacks []string
|
var calculatedStacks []string
|
||||||
|
|||||||
+2
-2
@@ -15,12 +15,12 @@ func (e *MultiError) Error() string {
|
|||||||
|
|
||||||
// newMultiError returns a *MultiError if there is more than one error, or
|
// newMultiError returns a *MultiError if there is more than one error, or
|
||||||
// returns that error directly when there is only one. Passing an empty slice
|
// returns that error directly when there is only one. Passing an empty slice
|
||||||
// will lead to a panic.
|
// will return nil (because there is no error).
|
||||||
// The importPath may be passed if this error is for a single package.
|
// The importPath may be passed if this error is for a single package.
|
||||||
func newMultiError(errs []error, importPath string) error {
|
func newMultiError(errs []error, importPath string) error {
|
||||||
switch len(errs) {
|
switch len(errs) {
|
||||||
case 0:
|
case 0:
|
||||||
panic("attempted to create empty MultiError")
|
return nil
|
||||||
case 1:
|
case 1:
|
||||||
return errs[0]
|
return errs[0]
|
||||||
default:
|
default:
|
||||||
|
|||||||
+134
-16
@@ -1,10 +1,15 @@
|
|||||||
package builder
|
package builder
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"go/scanner"
|
||||||
|
"go/token"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"regexp"
|
||||||
"github.com/tinygo-org/tinygo/goenv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// runCCompiler invokes a C compiler with the given arguments.
|
// runCCompiler invokes a C compiler with the given arguments.
|
||||||
@@ -23,22 +28,135 @@ func runCCompiler(flags ...string) error {
|
|||||||
|
|
||||||
// link invokes a linker with the given name and flags.
|
// link invokes a linker with the given name and flags.
|
||||||
func link(linker string, flags ...string) error {
|
func link(linker string, flags ...string) error {
|
||||||
if hasBuiltinTools && (linker == "ld.lld" || linker == "wasm-ld") {
|
// We only support LLD.
|
||||||
// Run command with internal linker.
|
if linker != "ld.lld" && linker != "wasm-ld" {
|
||||||
cmd := exec.Command(os.Args[0], append([]string{linker}, flags...)...)
|
return fmt.Errorf("unexpected: linker %s should be ld.lld or wasm-ld", linker)
|
||||||
cmd.Stdout = os.Stdout
|
|
||||||
cmd.Stderr = os.Stderr
|
|
||||||
return cmd.Run()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fall back to external command.
|
var cmd *exec.Cmd
|
||||||
if _, ok := commands[linker]; ok {
|
if hasBuiltinTools {
|
||||||
return execCommand(linker, flags...)
|
cmd = exec.Command(os.Args[0], append([]string{linker}, flags...)...)
|
||||||
|
} else {
|
||||||
|
name, err := LookupCommand(linker)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cmd = exec.Command(name, flags...)
|
||||||
}
|
}
|
||||||
|
var buf bytes.Buffer
|
||||||
cmd := exec.Command(linker, flags...)
|
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = &buf
|
||||||
cmd.Dir = goenv.Get("TINYGOROOT")
|
err := cmd.Run()
|
||||||
return cmd.Run()
|
if err != nil {
|
||||||
|
if buf.Len() == 0 {
|
||||||
|
// The linker failed but ther was no output.
|
||||||
|
// Therefore, show some output anyway.
|
||||||
|
return fmt.Errorf("failed to run linker: %w", err)
|
||||||
|
}
|
||||||
|
return parseLLDErrors(buf.String())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split LLD errors into individual erros (including errors that continue on the
|
||||||
|
// next line, using a ">>>" prefix). If possible, replace the raw errors with a
|
||||||
|
// more user-friendly version (and one that's more in a Go style).
|
||||||
|
func parseLLDErrors(text string) error {
|
||||||
|
// Split linker output in separate error messages.
|
||||||
|
lines := strings.Split(text, "\n")
|
||||||
|
var errorLines []string // one or more line (belonging to a single error) per line
|
||||||
|
for _, line := range lines {
|
||||||
|
line = strings.TrimRight(line, "\r") // needed for Windows
|
||||||
|
if len(errorLines) != 0 && strings.HasPrefix(line, ">>> ") {
|
||||||
|
errorLines[len(errorLines)-1] += "\n" + line
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
errorLines = append(errorLines, line)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse error messages.
|
||||||
|
var linkErrors []error
|
||||||
|
var flashOverflow, ramOverflow uint64
|
||||||
|
for _, message := range errorLines {
|
||||||
|
parsedError := false
|
||||||
|
|
||||||
|
// Check for undefined symbols.
|
||||||
|
// This can happen in some cases like with CGo and //go:linkname tricker.
|
||||||
|
if matches := regexp.MustCompile(`^ld.lld: error: undefined symbol: (.*)\n`).FindStringSubmatch(message); matches != nil {
|
||||||
|
symbolName := matches[1]
|
||||||
|
for _, line := range strings.Split(message, "\n") {
|
||||||
|
matches := regexp.MustCompile(`referenced by .* \(((.*):([0-9]+))\)`).FindStringSubmatch(line)
|
||||||
|
if matches != nil {
|
||||||
|
parsedError = true
|
||||||
|
line, _ := strconv.Atoi(matches[3])
|
||||||
|
// TODO: detect common mistakes like -gc=none?
|
||||||
|
linkErrors = append(linkErrors, scanner.Error{
|
||||||
|
Pos: token.Position{
|
||||||
|
Filename: matches[2],
|
||||||
|
Line: line,
|
||||||
|
},
|
||||||
|
Msg: "linker could not find symbol " + symbolName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for flash/RAM overflow.
|
||||||
|
if matches := regexp.MustCompile(`^ld.lld: error: section '(.*?)' will not fit in region '(.*?)': overflowed by ([0-9]+) bytes$`).FindStringSubmatch(message); matches != nil {
|
||||||
|
region := matches[2]
|
||||||
|
n, err := strconv.ParseUint(matches[3], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
// Should not happen at all (unless it overflows an uint64 for some reason).
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check which area overflowed.
|
||||||
|
// Some chips use differently named memory areas, but these are by
|
||||||
|
// far the most common.
|
||||||
|
switch region {
|
||||||
|
case "FLASH_TEXT":
|
||||||
|
if n > flashOverflow {
|
||||||
|
flashOverflow = n
|
||||||
|
}
|
||||||
|
parsedError = true
|
||||||
|
case "RAM":
|
||||||
|
if n > ramOverflow {
|
||||||
|
ramOverflow = n
|
||||||
|
}
|
||||||
|
parsedError = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we couldn't parse the linker error: show the error as-is to
|
||||||
|
// the user.
|
||||||
|
if !parsedError {
|
||||||
|
linkErrors = append(linkErrors, LinkerError{message})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if flashOverflow > 0 {
|
||||||
|
linkErrors = append(linkErrors, LinkerError{
|
||||||
|
Msg: fmt.Sprintf("program too large for this chip (flash overflowed by %d bytes)\n\toptimization guide: https://tinygo.org/docs/guides/optimizing-binaries/", flashOverflow),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if ramOverflow > 0 {
|
||||||
|
linkErrors = append(linkErrors, LinkerError{
|
||||||
|
Msg: fmt.Sprintf("program uses too much static RAM on this chip (RAM overflowed by %d bytes)", ramOverflow),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return newMultiError(linkErrors, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// LLD linker error that could not be parsed or doesn't refer to a source
|
||||||
|
// location.
|
||||||
|
type LinkerError struct {
|
||||||
|
Msg string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e LinkerError) Error() string {
|
||||||
|
return e.Msg
|
||||||
}
|
}
|
||||||
|
|||||||
+38
-22
@@ -7,7 +7,6 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/tinygo-org/tinygo/compileopts"
|
"github.com/tinygo-org/tinygo/compileopts"
|
||||||
"github.com/tinygo-org/tinygo/diagnostics"
|
"github.com/tinygo-org/tinygo/diagnostics"
|
||||||
@@ -15,38 +14,55 @@ import (
|
|||||||
|
|
||||||
// Test the error messages of the TinyGo compiler.
|
// Test the error messages of the TinyGo compiler.
|
||||||
func TestErrors(t *testing.T) {
|
func TestErrors(t *testing.T) {
|
||||||
for _, name := range []string{
|
// TODO: nicely formatted error messages for:
|
||||||
"cgo",
|
// - duplicate symbols in ld.lld (currently only prints bitcode file)
|
||||||
"compiler",
|
type errorTest struct {
|
||||||
"interp",
|
name string
|
||||||
"loader-importcycle",
|
target string
|
||||||
"loader-invaliddep",
|
}
|
||||||
"loader-invalidpackage",
|
for _, tc := range []errorTest{
|
||||||
"loader-nopackage",
|
{name: "cgo"},
|
||||||
"optimizer",
|
{name: "compiler"},
|
||||||
"syntax",
|
{name: "interp"},
|
||||||
"types",
|
{name: "linker-flashoverflow", target: "cortex-m-qemu"},
|
||||||
|
{name: "linker-ramoverflow", target: "cortex-m-qemu"},
|
||||||
|
{name: "linker-undefined", target: "darwin/arm64"},
|
||||||
|
{name: "linker-undefined", target: "linux/amd64"},
|
||||||
|
//{name: "linker-undefined", target: "windows/amd64"}, // TODO: no source location
|
||||||
|
{name: "linker-undefined", target: "cortex-m-qemu"},
|
||||||
|
//{name: "linker-undefined", target: "wasip1"}, // TODO: no source location
|
||||||
|
{name: "loader-importcycle"},
|
||||||
|
{name: "loader-invaliddep"},
|
||||||
|
{name: "loader-invalidpackage"},
|
||||||
|
{name: "loader-nopackage"},
|
||||||
|
{name: "optimizer"},
|
||||||
|
{name: "syntax"},
|
||||||
|
{name: "types"},
|
||||||
} {
|
} {
|
||||||
|
name := tc.name
|
||||||
|
if tc.target != "" {
|
||||||
|
name += "#" + tc.target
|
||||||
|
}
|
||||||
|
target := tc.target
|
||||||
|
if target == "" {
|
||||||
|
target = "wasip1"
|
||||||
|
}
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
testErrorMessages(t, "./testdata/errors/"+name+".go")
|
options := optionsFromTarget(target, sema)
|
||||||
|
testErrorMessages(t, "./testdata/errors/"+tc.name+".go", &options)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testErrorMessages(t *testing.T, filename string) {
|
func testErrorMessages(t *testing.T, filename string, options *compileopts.Options) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
// Parse expected error messages.
|
// Parse expected error messages.
|
||||||
expected := readErrorMessages(t, filename)
|
expected := readErrorMessages(t, filename)
|
||||||
|
|
||||||
// Try to build a binary (this should fail with an error).
|
// Try to build a binary (this should fail with an error).
|
||||||
tmpdir := t.TempDir()
|
tmpdir := t.TempDir()
|
||||||
err := Build(filename, tmpdir+"/out", &compileopts.Options{
|
err := Build(filename, tmpdir+"/out", options)
|
||||||
Target: "wasip1",
|
|
||||||
Semaphore: sema,
|
|
||||||
InterpTimeout: 180 * time.Second,
|
|
||||||
Debug: true,
|
|
||||||
VerifyIR: true,
|
|
||||||
Opt: "z",
|
|
||||||
})
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("expected to get a compiler error")
|
t.Fatal("expected to get a compiler error")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1467,7 +1467,8 @@ func main() {
|
|||||||
case "clang", "ld.lld", "wasm-ld":
|
case "clang", "ld.lld", "wasm-ld":
|
||||||
err := builder.RunTool(command, os.Args[2:]...)
|
err := builder.RunTool(command, os.Args[2:]...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintln(os.Stderr, err)
|
// The tool should have printed an error message already.
|
||||||
|
// Don't print another error message here.
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
|
|||||||
+2
-2
@@ -8,7 +8,6 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@@ -696,7 +695,8 @@ func TestMain(m *testing.M) {
|
|||||||
// Invoke a specific tool.
|
// Invoke a specific tool.
|
||||||
err := builder.RunTool(os.Args[1], os.Args[2:]...)
|
err := builder.RunTool(os.Args[1], os.Args[2:]...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintln(os.Stderr, err)
|
// The tool should have printed an error message already.
|
||||||
|
// Don't print another error message here.
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
|
|||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "unsafe"
|
||||||
|
|
||||||
|
const (
|
||||||
|
a = "0123456789abcdef" // 16 bytes
|
||||||
|
b = a + a + a + a + a + a + a + a // 128 bytes
|
||||||
|
c = b + b + b + b + b + b + b + b // 1024 bytes
|
||||||
|
d = c + c + c + c + c + c + c + c // 8192 bytes
|
||||||
|
e = d + d + d + d + d + d + d + d // 65536 bytes
|
||||||
|
f = e + e + e + e + e + e + e + e // 524288 bytes
|
||||||
|
)
|
||||||
|
|
||||||
|
var s = f
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
println(unsafe.StringData(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ERROR: program too large for this chip (flash overflowed by {{[0-9]+}} bytes)
|
||||||
|
// ERROR: optimization guide: https://tinygo.org/docs/guides/optimizing-binaries/
|
||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
var b [64 << 10]byte // 64kB
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
println("ptr:", &b[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
// ERROR: program uses too much static RAM on this chip (RAM overflowed by {{[0-9]+}} bytes)
|
||||||
Vendored
+11
@@ -0,0 +1,11 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
func foo()
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
foo()
|
||||||
|
foo()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ERROR: linker-undefined.go:6: linker could not find symbol {{_?}}main.foo
|
||||||
|
// ERROR: linker-undefined.go:7: linker could not find symbol {{_?}}main.foo
|
||||||
Reference in New Issue
Block a user