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