diff options
author | Ayke van Laethem <[email protected]> | 2024-07-10 13:42:21 +0200 |
---|---|---|
committer | Ron Evans <[email protected]> | 2024-08-11 01:48:11 -0700 |
commit | 2e76cd3687df0f6a4ea8d042589ced1d22f4ea78 (patch) | |
tree | 0dca13e2a3642f2986e6ee341c24cea963f7c5dc /builder/tools.go | |
parent | 2eb39785fe9d0188e444fd1eb29f1ce2c7a89419 (diff) | |
download | tinygo-2e76cd3687df0f6a4ea8d042589ced1d22f4ea78.tar.gz tinygo-2e76cd3687df0f6a4ea8d042589ced1d22f4ea78.zip |
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.
Diffstat (limited to 'builder/tools.go')
-rw-r--r-- | builder/tools.go | 150 |
1 files changed, 134 insertions, 16 deletions
diff --git a/builder/tools.go b/builder/tools.go index a23714d60..e1c6443c6 100644 --- a/builder/tools.go +++ b/builder/tools.go @@ -1,10 +1,15 @@ package builder import ( + "bytes" + "fmt" + "go/scanner" + "go/token" "os" "os/exec" - - "github.com/tinygo-org/tinygo/goenv" + "regexp" + "strconv" + "strings" ) // 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. func link(linker string, flags ...string) error { - if hasBuiltinTools && (linker == "ld.lld" || linker == "wasm-ld") { - // Run command with internal linker. - cmd := exec.Command(os.Args[0], append([]string{linker}, flags...)...) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - return cmd.Run() + // We only support LLD. + if linker != "ld.lld" && linker != "wasm-ld" { + return fmt.Errorf("unexpected: linker %s should be ld.lld or wasm-ld", linker) } - // Fall back to external command. - if _, ok := commands[linker]; ok { - return execCommand(linker, flags...) + var cmd *exec.Cmd + if hasBuiltinTools { + 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...) } - - cmd := exec.Command(linker, flags...) + var buf bytes.Buffer cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - cmd.Dir = goenv.Get("TINYGOROOT") - return cmd.Run() + cmd.Stderr = &buf + err := 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 } |