github.com/tinygo-org/tinygo@v0.31.3-0.20240404173401-90b0bf646c27/compileopts/config.go (about) 1 // Package compileopts contains the configuration for a single to-be-built 2 // binary. 3 package compileopts 4 5 import ( 6 "errors" 7 "fmt" 8 "os" 9 "path/filepath" 10 "regexp" 11 "strings" 12 13 "github.com/google/shlex" 14 "github.com/tinygo-org/tinygo/goenv" 15 ) 16 17 // Config keeps all configuration affecting the build in a single struct. 18 type Config struct { 19 Options *Options 20 Target *TargetSpec 21 GoMinorVersion int 22 TestConfig TestConfig 23 } 24 25 // Triple returns the LLVM target triple, like armv6m-unknown-unknown-eabi. 26 func (c *Config) Triple() string { 27 return c.Target.Triple 28 } 29 30 // CPU returns the LLVM CPU name, like atmega328p or arm7tdmi. It may return an 31 // empty string if the CPU name is not known. 32 func (c *Config) CPU() string { 33 return c.Target.CPU 34 } 35 36 // Features returns a list of features this CPU supports. For example, for a 37 // RISC-V processor, that could be "+a,+c,+m". For many targets, an empty list 38 // will be returned. 39 func (c *Config) Features() string { 40 if c.Target.Features == "" { 41 return c.Options.LLVMFeatures 42 } 43 if c.Options.LLVMFeatures == "" { 44 return c.Target.Features 45 } 46 return c.Target.Features + "," + c.Options.LLVMFeatures 47 } 48 49 // ABI returns the -mabi= flag for this target (like -mabi=lp64). A zero-length 50 // string is returned if the target doesn't specify an ABI. 51 func (c *Config) ABI() string { 52 return c.Target.ABI 53 } 54 55 // GOOS returns the GOOS of the target. This might not always be the actual OS: 56 // for example, bare-metal targets will usually pretend to be linux to get the 57 // standard library to compile. 58 func (c *Config) GOOS() string { 59 return c.Target.GOOS 60 } 61 62 // GOARCH returns the GOARCH of the target. This might not always be the actual 63 // archtecture: for example, the AVR target is not supported by the Go standard 64 // library so such targets will usually pretend to be linux/arm. 65 func (c *Config) GOARCH() string { 66 return c.Target.GOARCH 67 } 68 69 // GOARM will return the GOARM environment variable given to the compiler when 70 // building a program. 71 func (c *Config) GOARM() string { 72 return c.Options.GOARM 73 } 74 75 // BuildTags returns the complete list of build tags used during this build. 76 func (c *Config) BuildTags() []string { 77 tags := append(c.Target.BuildTags, []string{ 78 "tinygo", // that's the compiler 79 "purego", // to get various crypto packages to work 80 "math_big_pure_go", // to get math/big to work 81 "gc." + c.GC(), "scheduler." + c.Scheduler(), // used inside the runtime package 82 "serial." + c.Serial()}...) // used inside the machine package 83 for i := 1; i <= c.GoMinorVersion; i++ { 84 tags = append(tags, fmt.Sprintf("go1.%d", i)) 85 } 86 tags = append(tags, c.Options.Tags...) 87 return tags 88 } 89 90 // GC returns the garbage collection strategy in use on this platform. Valid 91 // values are "none", "leaking", "conservative" and "precise". 92 func (c *Config) GC() string { 93 if c.Options.GC != "" { 94 return c.Options.GC 95 } 96 if c.Target.GC != "" { 97 return c.Target.GC 98 } 99 return "conservative" 100 } 101 102 // NeedsStackObjects returns true if the compiler should insert stack objects 103 // that can be traced by the garbage collector. 104 func (c *Config) NeedsStackObjects() bool { 105 switch c.GC() { 106 case "conservative", "custom", "precise": 107 for _, tag := range c.BuildTags() { 108 if tag == "tinygo.wasm" { 109 return true 110 } 111 } 112 113 return false 114 default: 115 return false 116 } 117 } 118 119 // Scheduler returns the scheduler implementation. Valid values are "none", 120 // "asyncify" and "tasks". 121 func (c *Config) Scheduler() string { 122 if c.Options.Scheduler != "" { 123 return c.Options.Scheduler 124 } 125 if c.Target.Scheduler != "" { 126 return c.Target.Scheduler 127 } 128 // Fall back to none. 129 return "none" 130 } 131 132 // Serial returns the serial implementation for this build configuration: uart, 133 // usb (meaning USB-CDC), or none. 134 func (c *Config) Serial() string { 135 if c.Options.Serial != "" { 136 return c.Options.Serial 137 } 138 if c.Target.Serial != "" { 139 return c.Target.Serial 140 } 141 return "none" 142 } 143 144 // OptLevels returns the optimization level (0-2), size level (0-2), and inliner 145 // threshold as used in the LLVM optimization pipeline. 146 func (c *Config) OptLevel() (level string, speedLevel, sizeLevel int) { 147 switch c.Options.Opt { 148 case "none", "0": 149 return "O0", 0, 0 150 case "1": 151 return "O1", 1, 0 152 case "2": 153 return "O2", 2, 0 154 case "s": 155 return "Os", 2, 1 156 case "z": 157 return "Oz", 2, 2 // default 158 default: 159 // This is not shown to the user: valid choices are already checked as 160 // part of Options.Verify(). It is here as a sanity check. 161 panic("unknown optimization level: -opt=" + c.Options.Opt) 162 } 163 } 164 165 // PanicStrategy returns the panic strategy selected for this target. Valid 166 // values are "print" (print the panic value, then exit) or "trap" (issue a trap 167 // instruction). 168 func (c *Config) PanicStrategy() string { 169 return c.Options.PanicStrategy 170 } 171 172 // AutomaticStackSize returns whether goroutine stack sizes should be determined 173 // automatically at compile time, if possible. If it is false, no attempt is 174 // made. 175 func (c *Config) AutomaticStackSize() bool { 176 if c.Target.AutoStackSize != nil && c.Scheduler() == "tasks" { 177 return *c.Target.AutoStackSize 178 } 179 return false 180 } 181 182 // StackSize returns the default stack size to be used for goroutines, if the 183 // stack size could not be determined automatically at compile time. 184 func (c *Config) StackSize() uint64 { 185 if c.Options.StackSize != 0 { 186 return c.Options.StackSize 187 } 188 return c.Target.DefaultStackSize 189 } 190 191 // MaxStackAlloc returns the size of the maximum allocation to put on the stack vs heap. 192 func (c *Config) MaxStackAlloc() uint64 { 193 if c.StackSize() > 32*1024 { 194 return 1024 195 } 196 197 return 256 198 } 199 200 // RP2040BootPatch returns whether the RP2040 boot patch should be applied that 201 // calculates and patches in the checksum for the 2nd stage bootloader. 202 func (c *Config) RP2040BootPatch() bool { 203 if c.Target.RP2040BootPatch != nil { 204 return *c.Target.RP2040BootPatch 205 } 206 return false 207 } 208 209 // MuslArchitecture returns the architecture name as used in musl libc. It is 210 // usually the same as the first part of the LLVM triple, but not always. 211 func MuslArchitecture(triple string) string { 212 arch := strings.Split(triple, "-")[0] 213 if strings.HasPrefix(arch, "arm") || strings.HasPrefix(arch, "thumb") { 214 arch = "arm" 215 } 216 return arch 217 } 218 219 // LibcPath returns the path to the libc directory. The libc path will be either 220 // a precompiled libc shipped with a TinyGo build, or a libc path in the cache 221 // directory (which might not yet be built). 222 func (c *Config) LibcPath(name string) (path string, precompiled bool) { 223 archname := c.Triple() 224 if c.CPU() != "" { 225 archname += "-" + c.CPU() 226 } 227 if c.ABI() != "" { 228 archname += "-" + c.ABI() 229 } 230 231 // Try to load a precompiled library. 232 precompiledDir := filepath.Join(goenv.Get("TINYGOROOT"), "pkg", archname, name) 233 if _, err := os.Stat(precompiledDir); err == nil { 234 // Found a precompiled library for this OS/architecture. Return the path 235 // directly. 236 return precompiledDir, true 237 } 238 239 // No precompiled library found. Determine the path name that will be used 240 // in the build cache. 241 return filepath.Join(goenv.Get("GOCACHE"), name+"-"+archname), false 242 } 243 244 // DefaultBinaryExtension returns the default extension for binaries, such as 245 // .exe, .wasm, or no extension (depending on the target). 246 func (c *Config) DefaultBinaryExtension() string { 247 parts := strings.Split(c.Triple(), "-") 248 if parts[0] == "wasm32" { 249 // WebAssembly files always have the .wasm file extension. 250 return ".wasm" 251 } 252 if len(parts) >= 3 && parts[2] == "windows" { 253 // Windows uses .exe. 254 return ".exe" 255 } 256 if len(parts) >= 3 && parts[2] == "unknown" { 257 // There appears to be a convention to use the .elf file extension for 258 // ELF files intended for microcontrollers. I'm not aware of the origin 259 // of this, it's just something that is used by many projects. 260 // I think it's a good tradition, so let's keep it. 261 return ".elf" 262 } 263 // Linux, MacOS, etc, don't use a file extension. Use it as a fallback. 264 return "" 265 } 266 267 // CFlags returns the flags to pass to the C compiler. This is necessary for CGo 268 // preprocessing. 269 func (c *Config) CFlags(libclang bool) []string { 270 var cflags []string 271 for _, flag := range c.Target.CFlags { 272 cflags = append(cflags, strings.ReplaceAll(flag, "{root}", goenv.Get("TINYGOROOT"))) 273 } 274 resourceDir := goenv.ClangResourceDir(libclang) 275 if resourceDir != "" { 276 // The resource directory contains the built-in clang headers like 277 // stdbool.h, stdint.h, float.h, etc. 278 // It is left empty if we're using an external compiler (that already 279 // knows these headers). 280 cflags = append(cflags, 281 "-resource-dir="+resourceDir, 282 ) 283 } 284 switch c.Target.Libc { 285 case "darwin-libSystem": 286 root := goenv.Get("TINYGOROOT") 287 cflags = append(cflags, 288 "-nostdlibinc", 289 "-isystem", filepath.Join(root, "lib/macos-minimal-sdk/src/usr/include"), 290 ) 291 case "picolibc": 292 root := goenv.Get("TINYGOROOT") 293 picolibcDir := filepath.Join(root, "lib", "picolibc", "newlib", "libc") 294 path, _ := c.LibcPath("picolibc") 295 cflags = append(cflags, 296 "-nostdlibinc", 297 "-isystem", filepath.Join(path, "include"), 298 "-isystem", filepath.Join(picolibcDir, "include"), 299 "-isystem", filepath.Join(picolibcDir, "tinystdio"), 300 ) 301 case "musl": 302 root := goenv.Get("TINYGOROOT") 303 path, _ := c.LibcPath("musl") 304 arch := MuslArchitecture(c.Triple()) 305 cflags = append(cflags, 306 "-nostdlibinc", 307 "-isystem", filepath.Join(path, "include"), 308 "-isystem", filepath.Join(root, "lib", "musl", "arch", arch), 309 "-isystem", filepath.Join(root, "lib", "musl", "include"), 310 ) 311 case "wasi-libc": 312 root := goenv.Get("TINYGOROOT") 313 cflags = append(cflags, 314 "-nostdlibinc", 315 "-isystem", root+"/lib/wasi-libc/sysroot/include") 316 case "wasmbuiltins": 317 // nothing to add (library is purely for builtins) 318 case "mingw-w64": 319 root := goenv.Get("TINYGOROOT") 320 path, _ := c.LibcPath("mingw-w64") 321 cflags = append(cflags, 322 "-nostdlibinc", 323 "-isystem", filepath.Join(path, "include"), 324 "-isystem", filepath.Join(root, "lib", "mingw-w64", "mingw-w64-headers", "crt"), 325 "-isystem", filepath.Join(root, "lib", "mingw-w64", "mingw-w64-headers", "defaults", "include"), 326 "-D_UCRT", 327 ) 328 case "": 329 // No libc specified, nothing to add. 330 default: 331 // Incorrect configuration. This could be handled in a better way, but 332 // usually this will be found by developers (not by TinyGo users). 333 panic("unknown libc: " + c.Target.Libc) 334 } 335 // Always emit debug information. It is optionally stripped at link time. 336 cflags = append(cflags, "-gdwarf-4") 337 // Use the same optimization level as TinyGo. 338 cflags = append(cflags, "-O"+c.Options.Opt) 339 // Set the LLVM target triple. 340 cflags = append(cflags, "--target="+c.Triple()) 341 // Set the -mcpu (or similar) flag. 342 if c.Target.CPU != "" { 343 if c.GOARCH() == "amd64" || c.GOARCH() == "386" { 344 // x86 prefers the -march flag (-mcpu is deprecated there). 345 cflags = append(cflags, "-march="+c.Target.CPU) 346 } else if strings.HasPrefix(c.Triple(), "avr") { 347 // AVR MCUs use -mmcu instead of -mcpu. 348 cflags = append(cflags, "-mmcu="+c.Target.CPU) 349 } else { 350 // The rest just uses -mcpu. 351 cflags = append(cflags, "-mcpu="+c.Target.CPU) 352 } 353 } 354 // Set the -mabi flag, if needed. 355 if c.ABI() != "" { 356 cflags = append(cflags, "-mabi="+c.ABI()) 357 } 358 return cflags 359 } 360 361 // LDFlags returns the flags to pass to the linker. A few more flags are needed 362 // (like the one for the compiler runtime), but this represents the majority of 363 // the flags. 364 func (c *Config) LDFlags() []string { 365 root := goenv.Get("TINYGOROOT") 366 // Merge and adjust LDFlags. 367 var ldflags []string 368 for _, flag := range c.Target.LDFlags { 369 ldflags = append(ldflags, strings.ReplaceAll(flag, "{root}", root)) 370 } 371 ldflags = append(ldflags, "-L", root) 372 if c.Target.LinkerScript != "" { 373 ldflags = append(ldflags, "-T", c.Target.LinkerScript) 374 } 375 return ldflags 376 } 377 378 // ExtraFiles returns the list of extra files to be built and linked with the 379 // executable. This can include extra C and assembly files. 380 func (c *Config) ExtraFiles() []string { 381 return c.Target.ExtraFiles 382 } 383 384 // DumpSSA returns whether to dump Go SSA while compiling (-dumpssa flag). Only 385 // enable this for debugging. 386 func (c *Config) DumpSSA() bool { 387 return c.Options.DumpSSA 388 } 389 390 // VerifyIR returns whether to run extra checks on the IR. This is normally 391 // disabled but enabled during testing. 392 func (c *Config) VerifyIR() bool { 393 return c.Options.VerifyIR 394 } 395 396 // Debug returns whether debug (DWARF) information should be retained by the 397 // linker. By default, debug information is retained, but it can be removed 398 // with the -no-debug flag. 399 func (c *Config) Debug() bool { 400 return c.Options.Debug 401 } 402 403 // BinaryFormat returns an appropriate binary format, based on the file 404 // extension and the configured binary format in the target JSON file. 405 func (c *Config) BinaryFormat(ext string) string { 406 switch ext { 407 case ".bin", ".gba", ".nro": 408 // The simplest format possible: dump everything in a raw binary file. 409 if c.Target.BinaryFormat != "" { 410 return c.Target.BinaryFormat 411 } 412 return "bin" 413 case ".img": 414 // Image file. Only defined for the ESP32 at the moment, where it is a 415 // full (runnable) image that can be used in the Espressif QEMU fork. 416 if c.Target.BinaryFormat != "" { 417 return c.Target.BinaryFormat + "-img" 418 } 419 return "bin" 420 case ".hex": 421 // Similar to bin, but includes the start address and is thus usually a 422 // better format. 423 return "hex" 424 case ".uf2": 425 // Special purpose firmware format, mainly used on Adafruit boards. 426 // More information: 427 // https://github.com/Microsoft/uf2 428 return "uf2" 429 case ".zip": 430 if c.Target.BinaryFormat != "" { 431 return c.Target.BinaryFormat 432 } 433 return "zip" 434 default: 435 // Use the ELF format for unrecognized file formats. 436 return "elf" 437 } 438 } 439 440 // Programmer returns the flash method and OpenOCD interface name given a 441 // particular configuration. It may either be all configured in the target JSON 442 // file or be modified using the -programmmer command-line option. 443 func (c *Config) Programmer() (method, openocdInterface string) { 444 switch c.Options.Programmer { 445 case "": 446 // No configuration supplied. 447 return c.Target.FlashMethod, c.Target.OpenOCDInterface 448 case "openocd", "msd", "command": 449 // The -programmer flag only specifies the flash method. 450 return c.Options.Programmer, c.Target.OpenOCDInterface 451 case "bmp": 452 // The -programmer flag only specifies the flash method. 453 return c.Options.Programmer, "" 454 default: 455 // The -programmer flag specifies something else, assume it specifies 456 // the OpenOCD interface name. 457 return "openocd", c.Options.Programmer 458 } 459 } 460 461 // OpenOCDConfiguration returns a list of command line arguments to OpenOCD. 462 // This list of command-line arguments is based on the various OpenOCD-related 463 // flags in the target specification. 464 func (c *Config) OpenOCDConfiguration() (args []string, err error) { 465 _, openocdInterface := c.Programmer() 466 if openocdInterface == "" { 467 return nil, errors.New("OpenOCD programmer not set") 468 } 469 if !regexp.MustCompile(`^[\p{L}0-9_-]+$`).MatchString(openocdInterface) { 470 return nil, fmt.Errorf("OpenOCD programmer has an invalid name: %#v", openocdInterface) 471 } 472 if c.Target.OpenOCDTarget == "" { 473 return nil, errors.New("OpenOCD chip not set") 474 } 475 if !regexp.MustCompile(`^[\p{L}0-9_-]+$`).MatchString(c.Target.OpenOCDTarget) { 476 return nil, fmt.Errorf("OpenOCD target has an invalid name: %#v", c.Target.OpenOCDTarget) 477 } 478 if c.Target.OpenOCDTransport != "" && c.Target.OpenOCDTransport != "swd" { 479 return nil, fmt.Errorf("unknown OpenOCD transport: %#v", c.Target.OpenOCDTransport) 480 } 481 args = []string{"-f", "interface/" + openocdInterface + ".cfg"} 482 for _, cmd := range c.Target.OpenOCDCommands { 483 args = append(args, "-c", cmd) 484 } 485 if c.Target.OpenOCDTransport != "" { 486 transport := c.Target.OpenOCDTransport 487 if transport == "swd" { 488 switch openocdInterface { 489 case "stlink-dap": 490 transport = "dapdirect_swd" 491 } 492 } 493 args = append(args, "-c", "transport select "+transport) 494 } 495 args = append(args, "-f", "target/"+c.Target.OpenOCDTarget+".cfg") 496 return args, nil 497 } 498 499 // CodeModel returns the code model used on this platform. 500 func (c *Config) CodeModel() string { 501 if c.Target.CodeModel != "" { 502 return c.Target.CodeModel 503 } 504 505 return "default" 506 } 507 508 // RelocationModel returns the relocation model in use on this platform. Valid 509 // values are "static", "pic", "dynamicnopic". 510 func (c *Config) RelocationModel() string { 511 if c.Target.RelocationModel != "" { 512 return c.Target.RelocationModel 513 } 514 515 return "static" 516 } 517 518 // EmulatorName is a shorthand to get the command for this emulator, something 519 // like qemu-system-arm or simavr. 520 func (c *Config) EmulatorName() string { 521 parts := strings.SplitN(c.Target.Emulator, " ", 2) 522 if len(parts) > 1 { 523 return parts[0] 524 } 525 return "" 526 } 527 528 // EmulatorFormat returns the binary format for the emulator and the associated 529 // file extension. An empty string means to pass directly whatever the linker 530 // produces directly without conversion (usually ELF format). 531 func (c *Config) EmulatorFormat() (format, fileExt string) { 532 switch { 533 case strings.Contains(c.Target.Emulator, "{img}"): 534 return "img", ".img" 535 default: 536 return "", "" 537 } 538 } 539 540 // Emulator returns a ready-to-run command to run the given binary in an 541 // emulator. Give it the format (returned by EmulatorFormat()) and the path to 542 // the compiled binary. 543 func (c *Config) Emulator(format, binary string) ([]string, error) { 544 parts, err := shlex.Split(c.Target.Emulator) 545 if err != nil { 546 return nil, fmt.Errorf("could not parse emulator command: %w", err) 547 } 548 var emulator []string 549 for _, s := range parts { 550 s = strings.ReplaceAll(s, "{root}", goenv.Get("TINYGOROOT")) 551 // Allow replacement of what's usually /tmp except notably Windows. 552 s = strings.ReplaceAll(s, "{tmpDir}", os.TempDir()) 553 s = strings.ReplaceAll(s, "{"+format+"}", binary) 554 emulator = append(emulator, s) 555 } 556 return emulator, nil 557 } 558 559 type TestConfig struct { 560 CompileTestBinary bool 561 CompileOnly bool 562 Verbose bool 563 Short bool 564 RunRegexp string 565 SkipRegexp string 566 Count *int 567 BenchRegexp string 568 BenchTime string 569 BenchMem bool 570 Shuffle string 571 }