tools.go raw

   1  package builder
   2  
   3  import (
   4  	"bytes"
   5  	"fmt"
   6  	"go/scanner"
   7  	"go/token"
   8  	"os"
   9  	"os/exec"
  10  	"regexp"
  11  	"strconv"
  12  	"strings"
  13  )
  14  
  15  // runCCompiler invokes a C compiler with the given arguments.
  16  func runCCompiler(flags ...string) error {
  17  	// Find the right command to run Clang.
  18  	var cmd *exec.Cmd
  19  	if hasBuiltinTools {
  20  		// Compile this with the internal Clang compiler.
  21  		cmd = exec.Command(os.Args[0], append([]string{"clang"}, flags...)...)
  22  	} else {
  23  		// Compile this with an external invocation of the Clang compiler.
  24  		name, err := LookupCommand("clang")
  25  		if err != nil {
  26  			return err
  27  		}
  28  		cmd = exec.Command(name, flags...)
  29  	}
  30  
  31  	cmd.Stdout = os.Stdout
  32  	cmd.Stderr = os.Stderr
  33  
  34  	// Make sure the command doesn't use any environmental variables.
  35  	// Most importantly, it should not use C_INCLUDE_PATH and the like.
  36  	cmd.Env = []string{}
  37  
  38  	// Let some environment variables through. One important one is the
  39  	// temporary directory, especially on Windows it looks like Clang breaks if
  40  	// the temporary directory has not been set.
  41  	// See: https://moxie/issues/4557
  42  	// Also see: https://github.com/llvm/llvm-project/blob/release/18.x/llvm/lib/Support/Unix/Path.inc#L1435
  43  	for _, env := range os.Environ() {
  44  		// We could parse the key and look it up in a map, but since there are
  45  		// only a few keys iterating through them is easier and maybe even
  46  		// faster.
  47  		for _, prefix := range []string{"TMPDIR=", "TMP=", "TEMP=", "TEMPDIR="} {
  48  			if strings.HasPrefix(env, prefix) {
  49  				cmd.Env = append(cmd.Env, env)
  50  				break
  51  			}
  52  		}
  53  	}
  54  
  55  	return cmd.Run()
  56  }
  57  
  58  // link invokes a linker with the given name and flags.
  59  func link(linker string, flags ...string) error {
  60  	// We only support LLD.
  61  	if linker != "ld.lld" && linker != "wasm-ld" {
  62  		return fmt.Errorf("unexpected: linker %s should be ld.lld or wasm-ld", linker)
  63  	}
  64  
  65  	var cmd *exec.Cmd
  66  	if hasBuiltinTools {
  67  		cmd = exec.Command(os.Args[0], append([]string{linker}, flags...)...)
  68  	} else {
  69  		name, err := LookupCommand(linker)
  70  		if err != nil {
  71  			return err
  72  		}
  73  		cmd = exec.Command(name, flags...)
  74  	}
  75  	var buf bytes.Buffer
  76  	cmd.Stdout = os.Stdout
  77  	cmd.Stderr = &buf
  78  	err := cmd.Run()
  79  	if err != nil {
  80  		if buf.Len() == 0 {
  81  			// The linker failed but there was no output.
  82  			// Therefore, show some output anyway.
  83  			return fmt.Errorf("failed to run linker: %w", err)
  84  		}
  85  		return parseLLDErrors(buf.String())
  86  	}
  87  	return nil
  88  }
  89  
  90  // Split LLD errors into individual erros (including errors that continue on the
  91  // next line, using a ">>>" prefix). If possible, replace the raw errors with a
  92  // more user-friendly version (and one that's more in a Go style).
  93  func parseLLDErrors(text string) error {
  94  	// Split linker output in separate error messages.
  95  	lines := strings.Split(text, "\n")
  96  	var errorLines []string // one or more line (belonging to a single error) per line
  97  	for _, line := range lines {
  98  		line = strings.TrimRight(line, "\r") // needed for Windows
  99  		if len(errorLines) != 0 && strings.HasPrefix(line, ">>> ") {
 100  			errorLines[len(errorLines)-1] += "\n" + line
 101  			continue
 102  		}
 103  		if line == "" {
 104  			continue
 105  		}
 106  		errorLines = append(errorLines, line)
 107  	}
 108  
 109  	// Parse error messages.
 110  	var linkErrors []error
 111  	var flashOverflow, ramOverflow uint64
 112  	for _, message := range errorLines {
 113  		parsedError := false
 114  
 115  		// Check for undefined symbols.
 116  		// This can happen in some cases like with CGo and //go:linkname tricker.
 117  		if matches := regexp.MustCompile(`^ld.lld(-[0-9]+)?: error: undefined symbol: (.*)\n`).FindStringSubmatch(message); matches != nil {
 118  			symbolName := matches[2]
 119  			for _, line := range strings.Split(message, "\n") {
 120  				matches := regexp.MustCompile(`referenced by .* \(((.*):([0-9]+))\)`).FindStringSubmatch(line)
 121  				if matches != nil {
 122  					parsedError = true
 123  					line, _ := strconv.Atoi(matches[3])
 124  					// TODO: detect common mistakes like -gc=none?
 125  					linkErrors = append(linkErrors, scanner.Error{
 126  						Pos: token.Position{
 127  							Filename: matches[2],
 128  							Line:     line,
 129  						},
 130  						Msg: "linker could not find symbol " + symbolName,
 131  					})
 132  				}
 133  			}
 134  		}
 135  
 136  		// Check for flash/RAM overflow.
 137  		if matches := regexp.MustCompile(`^ld.lld(-[0-9]+)?: error: section '(.*?)' will not fit in region '(.*?)': overflowed by ([0-9]+) bytes$`).FindStringSubmatch(message); matches != nil {
 138  			region := matches[3]
 139  			n, err := strconv.ParseUint(matches[4], 10, 64)
 140  			if err != nil {
 141  				// Should not happen at all (unless it overflows an uint64 for some reason).
 142  				continue
 143  			}
 144  
 145  			// Check which area overflowed.
 146  			// Some chips use differently named memory areas, but these are by
 147  			// far the most common.
 148  			switch region {
 149  			case "FLASH_TEXT":
 150  				if n > flashOverflow {
 151  					flashOverflow = n
 152  				}
 153  				parsedError = true
 154  			case "RAM":
 155  				if n > ramOverflow {
 156  					ramOverflow = n
 157  				}
 158  				parsedError = true
 159  			}
 160  		}
 161  
 162  		// If we couldn't parse the linker error: show the error as-is to
 163  		// the user.
 164  		if !parsedError {
 165  			linkErrors = append(linkErrors, LinkerError{message})
 166  		}
 167  	}
 168  
 169  	if flashOverflow > 0 {
 170  		linkErrors = append(linkErrors, LinkerError{
 171  			Msg: fmt.Sprintf("program too large for this chip (flash overflowed by %d bytes)\n\toptimization guide: https://moxie.dev/docs/guides/optimizing-binaries/", flashOverflow),
 172  		})
 173  	}
 174  	if ramOverflow > 0 {
 175  		linkErrors = append(linkErrors, LinkerError{
 176  			Msg: fmt.Sprintf("program uses too much static RAM on this chip (RAM overflowed by %d bytes)", ramOverflow),
 177  		})
 178  	}
 179  
 180  	return newMultiError(linkErrors, "")
 181  }
 182  
 183  // LLD linker error that could not be parsed or doesn't refer to a source
 184  // location.
 185  type LinkerError struct {
 186  	Msg string
 187  }
 188  
 189  func (e LinkerError) Error() string {
 190  	return e.Msg
 191  }
 192