mirror of https://github.com/tinygo-org/tinygo.git
wasmstm32webassemblymicrocontrollerarmavrspiwasiadafruitarduinocircuitplayground-expressgpioi2cllvmmicrobitnrf51nrf52nrf52840samd21tinygo
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1950 lines
57 KiB
1950 lines
57 KiB
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"regexp"
|
|
"runtime"
|
|
"runtime/pprof"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/google/shlex"
|
|
"github.com/inhies/go-bytesize"
|
|
"github.com/mattn/go-colorable"
|
|
"github.com/tinygo-org/tinygo/builder"
|
|
"github.com/tinygo-org/tinygo/compileopts"
|
|
"github.com/tinygo-org/tinygo/diagnostics"
|
|
"github.com/tinygo-org/tinygo/goenv"
|
|
"github.com/tinygo-org/tinygo/loader"
|
|
"golang.org/x/tools/go/buildutil"
|
|
"tinygo.org/x/go-llvm"
|
|
|
|
"go.bug.st/serial"
|
|
"go.bug.st/serial/enumerator"
|
|
)
|
|
|
|
// commandError is an error type to wrap os/exec.Command errors. This provides
|
|
// some more information regarding what went wrong while running a command.
|
|
type commandError struct {
|
|
Msg string
|
|
File string
|
|
Err error
|
|
}
|
|
|
|
func (e *commandError) Error() string {
|
|
return e.Msg + " " + e.File + ": " + e.Err.Error()
|
|
}
|
|
|
|
// moveFile renames the file from src to dst. If renaming doesn't work (for
|
|
// example, the rename crosses a filesystem boundary), the file is copied and
|
|
// the old file is removed.
|
|
func moveFile(src, dst string) error {
|
|
err := os.Rename(src, dst)
|
|
if err == nil {
|
|
// Success!
|
|
return nil
|
|
}
|
|
// Failed to move, probably a different filesystem.
|
|
// Do a copy + remove.
|
|
err = copyFile(src, dst)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return os.Remove(src)
|
|
}
|
|
|
|
// copyFile copies the given file or directory from src to dst. It can copy over
|
|
// a possibly already existing file (but not directory) at the destination.
|
|
func copyFile(src, dst string) error {
|
|
source, err := os.Open(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer source.Close()
|
|
|
|
st, err := source.Stat()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if st.IsDir() {
|
|
err := os.Mkdir(dst, st.Mode().Perm())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
names, err := source.Readdirnames(0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, name := range names {
|
|
err := copyFile(filepath.Join(src, name), filepath.Join(dst, name))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
} else {
|
|
destination, err := os.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, st.Mode())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer destination.Close()
|
|
|
|
_, err = io.Copy(destination, source)
|
|
return err
|
|
}
|
|
}
|
|
|
|
// executeCommand is a simple wrapper to exec.Cmd
|
|
func executeCommand(options *compileopts.Options, name string, arg ...string) *exec.Cmd {
|
|
if options.PrintCommands != nil {
|
|
options.PrintCommands(name, arg...)
|
|
}
|
|
return exec.Command(name, arg...)
|
|
}
|
|
|
|
// printCommand prints a command to stdout while formatting it like a real
|
|
// command (escaping characters etc). The resulting command should be easy to
|
|
// run directly in a shell, although it is not guaranteed to be a safe shell
|
|
// escape. That's not a problem as the primary use case is printing the command,
|
|
// not running it.
|
|
func printCommand(cmd string, args ...string) {
|
|
command := append([]string{cmd}, args...)
|
|
for i, arg := range command {
|
|
// Source: https://www.oreilly.com/library/view/learning-the-bash/1565923472/ch01s09.html
|
|
const specialChars = "~`#$&*()\\|[]{};'\"<>?! "
|
|
if strings.ContainsAny(arg, specialChars) {
|
|
// See: https://stackoverflow.com/questions/15783701/which-characters-need-to-be-escaped-when-using-bash
|
|
arg = "'" + strings.ReplaceAll(arg, `'`, `'\''`) + "'"
|
|
command[i] = arg
|
|
}
|
|
}
|
|
fmt.Fprintln(os.Stderr, strings.Join(command, " "))
|
|
}
|
|
|
|
// Build compiles and links the given package and writes it to outpath.
|
|
func Build(pkgName, outpath string, options *compileopts.Options) error {
|
|
config, err := builder.NewConfig(options)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if options.PrintJSON {
|
|
b, err := json.MarshalIndent(config, "", " ")
|
|
if err != nil {
|
|
handleCompilerError(err)
|
|
}
|
|
fmt.Printf("%s\n", string(b))
|
|
return nil
|
|
}
|
|
|
|
// Create a temporary directory for intermediary files.
|
|
tmpdir, err := os.MkdirTemp("", "tinygo")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !options.Work {
|
|
defer os.RemoveAll(tmpdir)
|
|
}
|
|
|
|
// Do the build.
|
|
result, err := builder.Build(pkgName, outpath, tmpdir, config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if result.Binary != "" {
|
|
// If result.Binary is set, it means there is a build output (elf, hex,
|
|
// etc) that we need to move to the outpath. If it isn't set, it means
|
|
// the build output was a .ll, .bc or .o file that has already been
|
|
// written to outpath and so we don't need to do anything.
|
|
|
|
if outpath == "" {
|
|
if strings.HasSuffix(pkgName, ".go") {
|
|
// A Go file was specified directly on the command line.
|
|
// Base the binary name off of it.
|
|
outpath = filepath.Base(pkgName[:len(pkgName)-3]) + config.DefaultBinaryExtension()
|
|
} else {
|
|
// Pick a default output path based on the main directory.
|
|
outpath = filepath.Base(result.MainDir) + config.DefaultBinaryExtension()
|
|
}
|
|
}
|
|
|
|
if err := os.Rename(result.Binary, outpath); err != nil {
|
|
// Moving failed. Do a file copy.
|
|
inf, err := os.Open(result.Binary)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer inf.Close()
|
|
outf, err := os.OpenFile(outpath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0777)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Copy data to output file.
|
|
_, err = io.Copy(outf, inf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Check whether file writing was successful.
|
|
return outf.Close()
|
|
}
|
|
}
|
|
|
|
// Move was successful.
|
|
return nil
|
|
}
|
|
|
|
// Test runs the tests in the given package. Returns whether the test passed and
|
|
// possibly an error if the test failed to run.
|
|
func Test(pkgName string, stdout, stderr io.Writer, options *compileopts.Options, outpath string) (bool, error) {
|
|
options.TestConfig.CompileTestBinary = true
|
|
config, err := builder.NewConfig(options)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
testConfig := &options.TestConfig
|
|
|
|
// Pass test flags to the test binary.
|
|
var flags []string
|
|
if testConfig.Verbose {
|
|
flags = append(flags, "-test.v")
|
|
}
|
|
if testConfig.Short {
|
|
flags = append(flags, "-test.short")
|
|
}
|
|
if testConfig.RunRegexp != "" {
|
|
flags = append(flags, "-test.run="+testConfig.RunRegexp)
|
|
}
|
|
if testConfig.SkipRegexp != "" {
|
|
flags = append(flags, "-test.skip="+testConfig.SkipRegexp)
|
|
}
|
|
if testConfig.BenchRegexp != "" {
|
|
flags = append(flags, "-test.bench="+testConfig.BenchRegexp)
|
|
}
|
|
if testConfig.BenchTime != "" {
|
|
flags = append(flags, "-test.benchtime="+testConfig.BenchTime)
|
|
}
|
|
if testConfig.BenchMem {
|
|
flags = append(flags, "-test.benchmem")
|
|
}
|
|
if testConfig.Count != nil && *testConfig.Count != 1 {
|
|
flags = append(flags, "-test.count="+strconv.Itoa(*testConfig.Count))
|
|
}
|
|
if testConfig.Shuffle != "" {
|
|
flags = append(flags, "-test.shuffle="+testConfig.Shuffle)
|
|
}
|
|
|
|
logToStdout := testConfig.Verbose || testConfig.BenchRegexp != ""
|
|
|
|
var buf bytes.Buffer
|
|
var output io.Writer = &buf
|
|
// Send the test output to stdout if -v or -bench
|
|
if logToStdout {
|
|
output = os.Stdout
|
|
}
|
|
|
|
passed := false
|
|
var duration time.Duration
|
|
result, err := buildAndRun(pkgName, config, output, flags, nil, 0, func(cmd *exec.Cmd, result builder.BuildResult) error {
|
|
if testConfig.CompileOnly || outpath != "" {
|
|
// Write test binary to the specified file name.
|
|
if outpath == "" {
|
|
// No -o path was given, so create one now.
|
|
// This matches the behavior of go test.
|
|
outpath = filepath.Base(result.MainDir) + ".test"
|
|
}
|
|
copyFile(result.Binary, outpath)
|
|
}
|
|
if testConfig.CompileOnly {
|
|
// Do not run the test.
|
|
passed = true
|
|
return nil
|
|
}
|
|
|
|
// Tests are always run in the package directory.
|
|
cmd.Dir = result.MainDir
|
|
|
|
// wasmtime is the default emulator used for `-target=wasip1`. wasmtime
|
|
// is a WebAssembly runtime CLI with WASI enabled by default. However,
|
|
// only stdio are allowed by default. For example, while STDOUT routes
|
|
// to the host, other files don't. It also does not inherit environment
|
|
// variables from the host. Some tests read testdata files, often from
|
|
// outside the package directory. Other tests require temporary
|
|
// writeable directories. We allow this by adding wasmtime flags below.
|
|
if config.EmulatorName() == "wasmtime" {
|
|
// At this point, The current working directory is at the package
|
|
// directory. Ex. $GOROOT/src/compress/flate for compress/flate.
|
|
// buildAndRun has already added arguments for wasmtime, that allow
|
|
// read-access to files such as "testdata/huffman-zero.in".
|
|
//
|
|
// Ex. main(.wasm) --dir=. -- -test.v
|
|
|
|
// Below adds additional wasmtime flags in case a test reads files
|
|
// outside its directory, like "../testdata/e.txt". This allows any
|
|
// relative directory up to the module root, even if the test never
|
|
// reads any files.
|
|
//
|
|
// Ex. run --dir=.. --dir=../.. --dir=../../..
|
|
var dirs []string
|
|
switch config.Options.Target {
|
|
case "wasip1":
|
|
dirs = dirsToModuleRootRel(result.MainDir, result.ModuleRoot)
|
|
case "wasip2":
|
|
dirs = dirsToModuleRootAbs(result.MainDir, result.ModuleRoot)
|
|
default:
|
|
return fmt.Errorf("unknown GOOS target: %v", config.Options.Target)
|
|
}
|
|
|
|
args := []string{"run"}
|
|
for _, d := range dirs {
|
|
args = append(args, "--dir="+d)
|
|
}
|
|
|
|
args = append(args, "--env=PWD="+cmd.Dir)
|
|
|
|
args = append(args, cmd.Args[1:]...)
|
|
cmd.Args = args
|
|
}
|
|
|
|
// Run the test.
|
|
start := time.Now()
|
|
err = cmd.Run()
|
|
duration = time.Since(start)
|
|
passed = err == nil
|
|
|
|
// if verbose or benchmarks, then output is already going to stdout
|
|
// However, if we failed and weren't printing to stdout, print the output we accumulated.
|
|
if !passed && !logToStdout {
|
|
buf.WriteTo(stdout)
|
|
}
|
|
|
|
if _, ok := err.(*exec.ExitError); ok {
|
|
// Binary exited with a non-zero exit code, which means the test
|
|
// failed. Return nil to avoid printing a useless "exited with
|
|
// error" error message.
|
|
return nil
|
|
}
|
|
return err
|
|
})
|
|
|
|
if testConfig.CompileOnly {
|
|
return true, nil
|
|
}
|
|
|
|
importPath := strings.TrimSuffix(result.ImportPath, ".test")
|
|
|
|
var w io.Writer = stdout
|
|
if logToStdout {
|
|
w = os.Stdout
|
|
}
|
|
if err, ok := err.(loader.NoTestFilesError); ok {
|
|
fmt.Fprintf(w, "? \t%s\t[no test files]\n", err.ImportPath)
|
|
// Pretend the test passed - it at least didn't fail.
|
|
return true, nil
|
|
} else if passed {
|
|
fmt.Fprintf(w, "ok \t%s\t%.3fs\n", importPath, duration.Seconds())
|
|
} else {
|
|
fmt.Fprintf(w, "FAIL\t%s\t%.3fs\n", importPath, duration.Seconds())
|
|
}
|
|
return passed, err
|
|
}
|
|
|
|
func dirsToModuleRootRel(maindir, modroot string) []string {
|
|
var dirs []string
|
|
last := ".."
|
|
// strip off path elements until we hit the module root
|
|
// adding `..`, `../..`, `../../..` until we're done
|
|
for maindir != modroot {
|
|
dirs = append(dirs, last)
|
|
last = filepath.Join(last, "..")
|
|
maindir = filepath.Dir(maindir)
|
|
}
|
|
dirs = append(dirs, ".")
|
|
return dirs
|
|
}
|
|
|
|
func dirsToModuleRootAbs(maindir, modroot string) []string {
|
|
var dirs = []string{maindir}
|
|
last := filepath.Join(maindir, "..")
|
|
// strip off path elements until we hit the module root
|
|
// adding `..`, `../..`, `../../..` until we're done
|
|
for maindir != modroot {
|
|
dirs = append(dirs, last)
|
|
last = filepath.Join(last, "..")
|
|
maindir = filepath.Dir(maindir)
|
|
}
|
|
return dirs
|
|
}
|
|
|
|
// Flash builds and flashes the built binary to the given serial port.
|
|
func Flash(pkgName, port string, options *compileopts.Options) error {
|
|
config, err := builder.NewConfig(options)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// determine the type of file to compile
|
|
var fileExt string
|
|
|
|
flashMethod, _ := config.Programmer()
|
|
switch flashMethod {
|
|
case "command", "":
|
|
switch {
|
|
case strings.Contains(config.Target.FlashCommand, "{hex}"):
|
|
fileExt = ".hex"
|
|
case strings.Contains(config.Target.FlashCommand, "{elf}"):
|
|
fileExt = ".elf"
|
|
case strings.Contains(config.Target.FlashCommand, "{bin}"):
|
|
fileExt = ".bin"
|
|
case strings.Contains(config.Target.FlashCommand, "{uf2}"):
|
|
fileExt = ".uf2"
|
|
case strings.Contains(config.Target.FlashCommand, "{zip}"):
|
|
fileExt = ".zip"
|
|
default:
|
|
return errors.New("invalid target file - did you forget the {hex} token in the 'flash-command' section?")
|
|
}
|
|
case "msd":
|
|
if config.Target.FlashFilename == "" {
|
|
return errors.New("invalid target file: flash-method was set to \"msd\" but no msd-firmware-name was set")
|
|
}
|
|
fileExt = filepath.Ext(config.Target.FlashFilename)
|
|
case "openocd":
|
|
fileExt = ".hex"
|
|
case "bmp":
|
|
fileExt = ".elf"
|
|
case "native":
|
|
return errors.New("unknown flash method \"native\" - did you miss a -target flag?")
|
|
default:
|
|
return errors.New("unknown flash method: " + flashMethod)
|
|
}
|
|
|
|
// Create a temporary directory for intermediary files.
|
|
tmpdir, err := os.MkdirTemp("", "tinygo")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !options.Work {
|
|
defer os.RemoveAll(tmpdir)
|
|
}
|
|
|
|
// Build the binary.
|
|
result, err := builder.Build(pkgName, fileExt, tmpdir, config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// do we need port reset to put MCU into bootloader mode?
|
|
if config.Target.PortReset == "true" && flashMethod != "openocd" {
|
|
port, err := getDefaultPort(port, config.Target.SerialPort)
|
|
if err == nil {
|
|
err = touchSerialPortAt1200bps(port)
|
|
if err != nil {
|
|
return &commandError{"failed to reset port", port, err}
|
|
}
|
|
// give the target MCU a chance to restart into bootloader
|
|
time.Sleep(3 * time.Second)
|
|
}
|
|
}
|
|
|
|
// Flash the binary to the MCU.
|
|
switch flashMethod {
|
|
case "", "command":
|
|
// Create the command.
|
|
flashCmd := config.Target.FlashCommand
|
|
flashCmdList, err := shlex.Split(flashCmd)
|
|
if err != nil {
|
|
return fmt.Errorf("could not parse flash command %#v: %w", flashCmd, err)
|
|
}
|
|
|
|
if strings.Contains(flashCmd, "{port}") {
|
|
var err error
|
|
port, err = getDefaultPort(port, config.Target.SerialPort)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Fill in fields in the command template.
|
|
fileToken := "{" + fileExt[1:] + "}"
|
|
for i, arg := range flashCmdList {
|
|
arg = strings.ReplaceAll(arg, fileToken, result.Binary)
|
|
arg = strings.ReplaceAll(arg, "{port}", port)
|
|
flashCmdList[i] = arg
|
|
}
|
|
|
|
// Execute the command.
|
|
if len(flashCmdList) < 2 {
|
|
return fmt.Errorf("invalid flash command: %#v", flashCmd)
|
|
}
|
|
cmd := executeCommand(config.Options, flashCmdList[0], flashCmdList[1:]...)
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
cmd.Dir = goenv.Get("TINYGOROOT")
|
|
err = cmd.Run()
|
|
if err != nil {
|
|
return &commandError{"failed to flash", result.Binary, err}
|
|
}
|
|
case "msd":
|
|
// this flashing method copies the binary data to a Mass Storage Device (msd)
|
|
switch fileExt {
|
|
case ".uf2":
|
|
err := flashUF2UsingMSD(config.Target.FlashVolume, result.Binary, config.Options)
|
|
if err != nil {
|
|
return &commandError{"failed to flash", result.Binary, err}
|
|
}
|
|
case ".hex":
|
|
err := flashHexUsingMSD(config.Target.FlashVolume, result.Binary, config.Options)
|
|
if err != nil {
|
|
return &commandError{"failed to flash", result.Binary, err}
|
|
}
|
|
default:
|
|
return errors.New("mass storage device flashing currently only supports uf2 and hex")
|
|
}
|
|
case "openocd":
|
|
args, err := config.OpenOCDConfiguration()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
exit := " reset exit"
|
|
if config.Target.OpenOCDVerify != nil && *config.Target.OpenOCDVerify {
|
|
exit = " verify" + exit
|
|
}
|
|
args = append(args, "-c", "program "+filepath.ToSlash(result.Binary)+exit)
|
|
cmd := executeCommand(config.Options, "openocd", args...)
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
err = cmd.Run()
|
|
if err != nil {
|
|
return &commandError{"failed to flash", result.Binary, err}
|
|
}
|
|
case "bmp":
|
|
gdb, err := config.Target.LookupGDB()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var bmpGDBPort string
|
|
bmpGDBPort, _, err = getBMPPorts()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
args := []string{"-ex", "target extended-remote " + bmpGDBPort, "-ex", "monitor swdp_scan", "-ex", "attach 1", "-ex", "load", filepath.ToSlash(result.Binary)}
|
|
cmd := executeCommand(config.Options, gdb, args...)
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
err = cmd.Run()
|
|
if err != nil {
|
|
return &commandError{"failed to flash", result.Binary, err}
|
|
}
|
|
default:
|
|
return fmt.Errorf("unknown flash method: %s", flashMethod)
|
|
}
|
|
if options.Monitor {
|
|
return Monitor(result.Executable, "", config)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Debug compiles and flashes a program to a microcontroller (just like Flash)
|
|
// but instead of resetting the target, it will drop into a debug shell like GDB
|
|
// or LLDB. You can then set breakpoints, run the `continue` command to start,
|
|
// hit Ctrl+C to break the running program, etc.
|
|
//
|
|
// Note: this command is expected to execute just before exiting, as it
|
|
// modifies global state.
|
|
func Debug(debugger, pkgName string, ocdOutput bool, options *compileopts.Options) error {
|
|
config, err := builder.NewConfig(options)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var cmdName string
|
|
switch debugger {
|
|
case "gdb":
|
|
cmdName, err = config.Target.LookupGDB()
|
|
case "lldb":
|
|
cmdName, err = builder.LookupCommand("lldb")
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create a temporary directory for intermediary files.
|
|
tmpdir, err := os.MkdirTemp("", "tinygo")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !options.Work {
|
|
defer os.RemoveAll(tmpdir)
|
|
}
|
|
|
|
// Build the binary to debug.
|
|
format, fileExt := config.EmulatorFormat()
|
|
result, err := builder.Build(pkgName, fileExt, tmpdir, config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Find a good way to run GDB.
|
|
gdbInterface, openocdInterface := config.Programmer()
|
|
switch gdbInterface {
|
|
case "msd", "command", "":
|
|
emulator := config.EmulatorName()
|
|
if emulator != "" {
|
|
if emulator == "mgba" {
|
|
gdbInterface = "mgba"
|
|
} else if emulator == "simavr" {
|
|
gdbInterface = "simavr"
|
|
} else if strings.HasPrefix(emulator, "qemu-system-") {
|
|
gdbInterface = "qemu"
|
|
} else {
|
|
// Assume QEMU as an emulator.
|
|
gdbInterface = "qemu-user"
|
|
}
|
|
} else if openocdInterface != "" && config.Target.OpenOCDTarget != "" {
|
|
gdbInterface = "openocd"
|
|
} else if config.Target.JLinkDevice != "" {
|
|
gdbInterface = "jlink"
|
|
} else {
|
|
gdbInterface = "native"
|
|
}
|
|
}
|
|
|
|
// Run the GDB server, if necessary.
|
|
port := ""
|
|
var gdbCommands []string
|
|
var daemon *exec.Cmd
|
|
emulator, err := config.Emulator(format, result.Binary)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
switch gdbInterface {
|
|
case "native":
|
|
// Run GDB directly.
|
|
case "bmp":
|
|
var bmpGDBPort string
|
|
bmpGDBPort, _, err = getBMPPorts()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
port = bmpGDBPort
|
|
gdbCommands = append(gdbCommands, "monitor swdp_scan", "compare-sections", "attach 1", "load")
|
|
case "openocd":
|
|
port = ":3333"
|
|
gdbCommands = append(gdbCommands, "monitor halt", "load", "monitor reset halt")
|
|
|
|
// We need a separate debugging daemon for on-chip debugging.
|
|
args, err := config.OpenOCDConfiguration()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
daemon = executeCommand(config.Options, "openocd", args...)
|
|
if ocdOutput {
|
|
// Make it clear which output is from the daemon.
|
|
w := &ColorWriter{
|
|
Out: colorable.NewColorableStderr(),
|
|
Prefix: "openocd: ",
|
|
Color: TermColorYellow,
|
|
}
|
|
daemon.Stdout = w
|
|
daemon.Stderr = w
|
|
}
|
|
case "jlink":
|
|
port = ":2331"
|
|
gdbCommands = append(gdbCommands, "load", "monitor reset halt")
|
|
|
|
// We need a separate debugging daemon for on-chip debugging.
|
|
daemon = executeCommand(config.Options, "JLinkGDBServer", "-device", config.Target.JLinkDevice)
|
|
if ocdOutput {
|
|
// Make it clear which output is from the daemon.
|
|
w := &ColorWriter{
|
|
Out: colorable.NewColorableStderr(),
|
|
Prefix: "jlink: ",
|
|
Color: TermColorYellow,
|
|
}
|
|
daemon.Stdout = w
|
|
daemon.Stderr = w
|
|
}
|
|
case "qemu":
|
|
port = ":1234"
|
|
// Run in an emulator.
|
|
args := append(emulator[1:], "-s", "-S")
|
|
daemon = executeCommand(config.Options, emulator[0], args...)
|
|
daemon.Stdout = os.Stdout
|
|
daemon.Stderr = os.Stderr
|
|
case "qemu-user":
|
|
port = ":1234"
|
|
// Run in an emulator.
|
|
args := append([]string{"-g", "1234"}, emulator[1:]...)
|
|
daemon = executeCommand(config.Options, emulator[0], args...)
|
|
daemon.Stdout = os.Stdout
|
|
daemon.Stderr = os.Stderr
|
|
case "mgba":
|
|
port = ":2345"
|
|
// Run in an emulator.
|
|
args := append(emulator[1:], "-g")
|
|
daemon = executeCommand(config.Options, emulator[0], args...)
|
|
daemon.Stdout = os.Stdout
|
|
daemon.Stderr = os.Stderr
|
|
case "simavr":
|
|
port = ":1234"
|
|
// Run in an emulator.
|
|
args := append(emulator[1:], "-g")
|
|
daemon = executeCommand(config.Options, emulator[0], args...)
|
|
daemon.Stdout = os.Stdout
|
|
daemon.Stderr = os.Stderr
|
|
case "msd":
|
|
return errors.New("gdb is not supported for drag-and-drop programmable devices")
|
|
default:
|
|
return fmt.Errorf("gdb is not supported with interface %#v", gdbInterface)
|
|
}
|
|
|
|
if daemon != nil {
|
|
// Make sure the daemon doesn't receive Ctrl-C that is intended for
|
|
// GDB (to break the currently executing program).
|
|
setCommandAsDaemon(daemon)
|
|
|
|
// Start now, and kill it on exit.
|
|
err = daemon.Start()
|
|
if err != nil {
|
|
return &commandError{"failed to run", daemon.Path, err}
|
|
}
|
|
defer func() {
|
|
daemon.Process.Signal(os.Interrupt)
|
|
var stopped uint32
|
|
go func() {
|
|
time.Sleep(time.Millisecond * 100)
|
|
if atomic.LoadUint32(&stopped) == 0 {
|
|
daemon.Process.Kill()
|
|
}
|
|
}()
|
|
daemon.Wait()
|
|
atomic.StoreUint32(&stopped, 1)
|
|
}()
|
|
}
|
|
|
|
// Ignore Ctrl-C, it must be passed on to GDB.
|
|
c := make(chan os.Signal, 1)
|
|
signal.Notify(c, os.Interrupt)
|
|
go func() {
|
|
for range c {
|
|
}
|
|
}()
|
|
|
|
// Construct and execute a gdb or lldb command.
|
|
// By default: gdb -ex run <binary>
|
|
// Exit the debugger with Ctrl-D.
|
|
params := []string{result.Executable}
|
|
switch debugger {
|
|
case "gdb":
|
|
if port != "" {
|
|
params = append(params, "-ex", "target extended-remote "+port)
|
|
}
|
|
for _, cmd := range gdbCommands {
|
|
params = append(params, "-ex", cmd)
|
|
}
|
|
case "lldb":
|
|
params = append(params, "--arch", config.Triple())
|
|
if port != "" {
|
|
if strings.HasPrefix(port, ":") {
|
|
params = append(params, "-o", "gdb-remote "+port[1:])
|
|
} else {
|
|
return fmt.Errorf("cannot use LLDB over a gdb-remote that isn't a TCP port: %s", port)
|
|
}
|
|
}
|
|
for _, cmd := range gdbCommands {
|
|
if strings.HasPrefix(cmd, "monitor ") {
|
|
params = append(params, "-o", "process plugin packet "+cmd)
|
|
} else if cmd == "load" {
|
|
params = append(params, "-o", "target modules load --load --slide 0")
|
|
} else {
|
|
return fmt.Errorf("don't know how to convert GDB command %#v to LLDB", cmd)
|
|
}
|
|
}
|
|
}
|
|
cmd := executeCommand(config.Options, cmdName, params...)
|
|
cmd.Stdin = os.Stdin
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
err = cmd.Run()
|
|
if err != nil {
|
|
return &commandError{"failed to run " + cmdName + " with", result.Executable, err}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Run compiles and runs the given program. Depending on the target provided in
|
|
// the options, it will run the program directly on the host or will run it in
|
|
// an emulator. For example, -target=wasm will cause the binary to be run inside
|
|
// of a WebAssembly VM.
|
|
func Run(pkgName string, options *compileopts.Options, cmdArgs []string) error {
|
|
config, err := builder.NewConfig(options)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = buildAndRun(pkgName, config, os.Stdout, cmdArgs, nil, 0, func(cmd *exec.Cmd, result builder.BuildResult) error {
|
|
return cmd.Run()
|
|
})
|
|
return err
|
|
}
|
|
|
|
// buildAndRun builds and runs the given program, writing output to stdout and
|
|
// errors to os.Stderr. It takes care of emulators (qemu, wasmtime, etc) and
|
|
// passes command line arguments and environment variables in a way appropriate
|
|
// for the given emulator.
|
|
func buildAndRun(pkgName string, config *compileopts.Config, stdout io.Writer, cmdArgs, environmentVars []string, timeout time.Duration, run func(cmd *exec.Cmd, result builder.BuildResult) error) (builder.BuildResult, error) {
|
|
|
|
isSingleFile := strings.HasSuffix(pkgName, ".go")
|
|
|
|
// Determine whether we're on a system that supports environment variables
|
|
// and command line parameters (operating systems, WASI) or not (baremetal,
|
|
// WebAssembly in the browser). If we're on a system without an environment,
|
|
// we need to pass command line arguments and environment variables through
|
|
// global variables (built into the binary directly) instead of the
|
|
// conventional way.
|
|
needsEnvInVars := config.GOOS() == "js"
|
|
for _, tag := range config.BuildTags() {
|
|
if tag == "baremetal" {
|
|
needsEnvInVars = true
|
|
}
|
|
}
|
|
var args, emuArgs, env []string
|
|
var extraCmdEnv []string
|
|
if needsEnvInVars {
|
|
runtimeGlobals := make(map[string]string)
|
|
if len(cmdArgs) != 0 {
|
|
runtimeGlobals["osArgs"] = strings.Join(cmdArgs, "\x00")
|
|
}
|
|
if len(environmentVars) != 0 {
|
|
runtimeGlobals["osEnv"] = strings.Join(environmentVars, "\x00")
|
|
}
|
|
if len(runtimeGlobals) != 0 {
|
|
// This sets the global variables like they would be set with
|
|
// `-ldflags="-X=runtime.osArgs=first\x00second`.
|
|
// The runtime package has two variables (osArgs and osEnv) that are
|
|
// both strings, from which the parameters and environment variables
|
|
// are read.
|
|
config.Options.GlobalValues = map[string]map[string]string{
|
|
"runtime": runtimeGlobals,
|
|
}
|
|
}
|
|
} else if config.EmulatorName() == "wasmtime" {
|
|
for _, v := range environmentVars {
|
|
emuArgs = append(emuArgs, "--env", v)
|
|
}
|
|
if len(cmdArgs) != 0 {
|
|
// Use of '--' argument no longer necessary as of Wasmtime v14:
|
|
// https://github.com/bytecodealliance/wasmtime/pull/6946
|
|
// args = append(args, "--")
|
|
args = append(args, cmdArgs...)
|
|
}
|
|
|
|
// Set this for nicer backtraces during tests, but don't override the user.
|
|
if _, ok := os.LookupEnv("WASMTIME_BACKTRACE_DETAILS"); !ok {
|
|
extraCmdEnv = append(extraCmdEnv, "WASMTIME_BACKTRACE_DETAILS=1")
|
|
}
|
|
} else {
|
|
// Pass environment variables and command line parameters as usual.
|
|
// This also works on qemu-aarch64 etc.
|
|
args = cmdArgs
|
|
env = environmentVars
|
|
}
|
|
|
|
// Create a temporary directory for intermediary files.
|
|
tmpdir, err := os.MkdirTemp("", "tinygo")
|
|
if err != nil {
|
|
return builder.BuildResult{}, err
|
|
}
|
|
if !config.Options.Work {
|
|
defer os.RemoveAll(tmpdir)
|
|
}
|
|
|
|
// Build the binary to be run.
|
|
format, fileExt := config.EmulatorFormat()
|
|
result, err := builder.Build(pkgName, fileExt, tmpdir, config)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
|
|
// If needed, set a timeout on the command. This is done in tests so
|
|
// they don't waste resources on a stalled test.
|
|
var ctx context.Context
|
|
if timeout != 0 {
|
|
var cancel context.CancelFunc
|
|
ctx, cancel = context.WithTimeout(context.Background(), timeout)
|
|
defer cancel()
|
|
}
|
|
|
|
// Set up the command.
|
|
var name string
|
|
if config.Target.Emulator == "" {
|
|
name = result.Binary
|
|
} else {
|
|
emulator, err := config.Emulator(format, result.Binary)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
|
|
name = emulator[0]
|
|
|
|
if name == "wasmtime" {
|
|
// Wasmtime needs some special flags to pass environment variables
|
|
// and allow reading from the current directory.
|
|
switch config.Options.Target {
|
|
case "wasip1":
|
|
emuArgs = append(emuArgs, "--dir=.")
|
|
case "wasip2":
|
|
dir := result.MainDir
|
|
if isSingleFile {
|
|
cwd, _ := os.Getwd()
|
|
dir = cwd
|
|
}
|
|
emuArgs = append(emuArgs, "--dir="+dir)
|
|
emuArgs = append(emuArgs, "--env=PWD="+dir)
|
|
}
|
|
}
|
|
|
|
emuArgs = append(emuArgs, emulator[1:]...)
|
|
args = append(emuArgs, args...)
|
|
}
|
|
var cmd *exec.Cmd
|
|
if ctx != nil {
|
|
cmd = exec.CommandContext(ctx, name, args...)
|
|
} else {
|
|
cmd = exec.Command(name, args...)
|
|
}
|
|
cmd.Env = append(cmd.Env, env...)
|
|
cmd.Env = append(cmd.Env, extraCmdEnv...)
|
|
|
|
// Configure stdout/stderr. The stdout may go to a buffer, not a real
|
|
// stdout.
|
|
cmd.Stdout = newOutputWriter(stdout, result.Executable)
|
|
cmd.Stderr = os.Stderr
|
|
if config.EmulatorName() == "simavr" {
|
|
cmd.Stdout = nil // don't print initial load commands
|
|
cmd.Stderr = stdout
|
|
}
|
|
|
|
// If this is a test, reserve CPU time for it so that increased
|
|
// parallelism doesn't blow up memory usage. If this isn't a test but
|
|
// simply `tinygo run`, then it is practically a no-op.
|
|
config.Options.Semaphore <- struct{}{}
|
|
defer func() {
|
|
<-config.Options.Semaphore
|
|
}()
|
|
|
|
// Run binary.
|
|
if config.Options.PrintCommands != nil {
|
|
config.Options.PrintCommands(cmd.Path, cmd.Args...)
|
|
}
|
|
err = run(cmd, result)
|
|
if err != nil {
|
|
if ctx != nil && ctx.Err() == context.DeadlineExceeded {
|
|
fmt.Fprintf(stdout, "--- timeout of %s exceeded, terminating...\n", timeout)
|
|
err = ctx.Err()
|
|
}
|
|
return result, &commandError{"failed to run compiled binary", result.Binary, err}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func touchSerialPortAt1200bps(port string) (err error) {
|
|
retryCount := 3
|
|
for i := 0; i < retryCount; i++ {
|
|
// Open port
|
|
p, e := serial.Open(port, &serial.Mode{BaudRate: 1200})
|
|
if e != nil {
|
|
if runtime.GOOS == `windows` {
|
|
se, ok := e.(*serial.PortError)
|
|
if ok && se.Code() == serial.InvalidSerialPort {
|
|
// InvalidSerialPort error occurs when transitioning to boot
|
|
return nil
|
|
}
|
|
}
|
|
time.Sleep(1 * time.Second)
|
|
err = e
|
|
continue
|
|
}
|
|
defer p.Close()
|
|
|
|
p.SetDTR(false)
|
|
return nil
|
|
}
|
|
return fmt.Errorf("opening port: %s", err)
|
|
}
|
|
|
|
func flashUF2UsingMSD(volumes []string, tmppath string, options *compileopts.Options) error {
|
|
for start := time.Now(); time.Since(start) < options.Timeout; {
|
|
// Find a UF2 mount point.
|
|
mounts, err := findFATMounts(options)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, mount := range mounts {
|
|
for _, volume := range volumes {
|
|
if mount.name != volume {
|
|
continue
|
|
}
|
|
if _, err := os.Stat(filepath.Join(mount.path, "INFO_UF2.TXT")); err != nil {
|
|
// No INFO_UF2.TXT found, which is expected on a UF2
|
|
// filesystem.
|
|
continue
|
|
}
|
|
// Found the filesystem, so flash the device!
|
|
return moveFile(tmppath, filepath.Join(mount.path, "flash.uf2"))
|
|
}
|
|
}
|
|
time.Sleep(500 * time.Millisecond)
|
|
}
|
|
return errors.New("unable to locate any volume: [" + strings.Join(volumes, ",") + "]")
|
|
}
|
|
|
|
func flashHexUsingMSD(volumes []string, tmppath string, options *compileopts.Options) error {
|
|
for start := time.Now(); time.Since(start) < options.Timeout; {
|
|
// Find all mount points.
|
|
mounts, err := findFATMounts(options)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, mount := range mounts {
|
|
for _, volume := range volumes {
|
|
if mount.name != volume {
|
|
continue
|
|
}
|
|
// Found the filesystem, so flash the device!
|
|
return moveFile(tmppath, filepath.Join(mount.path, "flash.hex"))
|
|
}
|
|
}
|
|
time.Sleep(500 * time.Millisecond)
|
|
}
|
|
return errors.New("unable to locate any volume: [" + strings.Join(volumes, ",") + "]")
|
|
}
|
|
|
|
type mountPoint struct {
|
|
name string
|
|
path string
|
|
}
|
|
|
|
// Find all the mount points on the system that use the FAT filesystem.
|
|
func findFATMounts(options *compileopts.Options) ([]mountPoint, error) {
|
|
var points []mountPoint
|
|
switch runtime.GOOS {
|
|
case "darwin":
|
|
list, err := os.ReadDir("/Volumes")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not list mount points: %w", err)
|
|
}
|
|
for _, elem := range list {
|
|
// TODO: find a way to check for the filesystem type.
|
|
// (Only return FAT filesystems).
|
|
points = append(points, mountPoint{
|
|
name: elem.Name(),
|
|
path: filepath.Join("/Volumes", elem.Name()),
|
|
})
|
|
}
|
|
sort.Slice(points, func(i, j int) bool {
|
|
return points[i].path < points[j].name
|
|
})
|
|
return points, nil
|
|
case "linux":
|
|
tab, err := os.ReadFile("/proc/mounts") // symlink to /proc/self/mounts on my system
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not list mount points: %w", err)
|
|
}
|
|
for _, line := range strings.Split(string(tab), "\n") {
|
|
fields := strings.Fields(line)
|
|
if len(fields) <= 2 {
|
|
continue
|
|
}
|
|
fstype := fields[2]
|
|
// chromeos bind mounts use 9p
|
|
if !(fstype == "vfat" || fstype == "9p") {
|
|
continue
|
|
}
|
|
fspath := strings.ReplaceAll(fields[1], "\\040", " ")
|
|
points = append(points, mountPoint{
|
|
name: filepath.Base(fspath),
|
|
path: fspath,
|
|
})
|
|
}
|
|
return points, nil
|
|
case "windows":
|
|
// Obtain a list of all currently mounted volumes.
|
|
cmd := executeCommand(options, "wmic",
|
|
"PATH", "Win32_LogicalDisk",
|
|
"get", "DeviceID,VolumeName,FileSystem,DriveType")
|
|
var out bytes.Buffer
|
|
cmd.Stdout = &out
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not list mount points: %w", err)
|
|
}
|
|
|
|
// Extract data to convert to a []mountPoint slice.
|
|
for _, line := range strings.Split(out.String(), "\n") {
|
|
words := strings.Fields(line)
|
|
if len(words) < 3 {
|
|
continue
|
|
}
|
|
if words[1] != "2" || words[2] != "FAT" {
|
|
// - DriveType 2 is removable (which we're looking for).
|
|
// - We only want to return FAT filesystems.
|
|
continue
|
|
}
|
|
points = append(points, mountPoint{
|
|
name: words[3],
|
|
path: words[0],
|
|
})
|
|
}
|
|
return points, nil
|
|
default:
|
|
return nil, fmt.Errorf("unknown GOOS for listing mount points: %s", runtime.GOOS)
|
|
}
|
|
}
|
|
|
|
// getDefaultPort returns the default serial port depending on the operating system.
|
|
func getDefaultPort(portFlag string, usbInterfaces []string) (port string, err error) {
|
|
portCandidates := strings.FieldsFunc(portFlag, func(c rune) bool { return c == ',' })
|
|
if len(portCandidates) == 1 {
|
|
return portCandidates[0], nil
|
|
}
|
|
|
|
var ports []string
|
|
switch runtime.GOOS {
|
|
case "freebsd":
|
|
ports, err = filepath.Glob("/dev/cuaU*")
|
|
case "darwin", "linux", "windows":
|
|
var portsList []*enumerator.PortDetails
|
|
portsList, err = enumerator.GetDetailedPortsList()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
var preferredPortIDs [][2]uint16
|
|
for _, s := range usbInterfaces {
|
|
parts := strings.Split(s, ":")
|
|
if len(parts) != 2 {
|
|
return "", fmt.Errorf("could not parse USB VID/PID pair %q", s)
|
|
}
|
|
vid, err := strconv.ParseUint(parts[0], 16, 16)
|
|
if err != nil {
|
|
return "", fmt.Errorf("could not parse USB vendor ID %q: %w", parts[1], err)
|
|
}
|
|
pid, err := strconv.ParseUint(parts[1], 16, 16)
|
|
if err != nil {
|
|
return "", fmt.Errorf("could not parse USB product ID %q: %w", parts[1], err)
|
|
}
|
|
preferredPortIDs = append(preferredPortIDs, [2]uint16{uint16(vid), uint16(pid)})
|
|
}
|
|
|
|
var primaryPorts []string // ports picked from preferred USB VID/PID
|
|
var secondaryPorts []string // other ports (as a fallback)
|
|
for _, p := range portsList {
|
|
if !p.IsUSB {
|
|
continue
|
|
}
|
|
if p.VID != "" && p.PID != "" {
|
|
foundPort := false
|
|
vid, vidErr := strconv.ParseUint(p.VID, 16, 16)
|
|
pid, pidErr := strconv.ParseUint(p.PID, 16, 16)
|
|
if vidErr == nil && pidErr == nil {
|
|
for _, id := range preferredPortIDs {
|
|
if uint16(vid) == id[0] && uint16(pid) == id[1] {
|
|
primaryPorts = append(primaryPorts, p.Name)
|
|
foundPort = true
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
if foundPort {
|
|
continue
|
|
}
|
|
}
|
|
|
|
secondaryPorts = append(secondaryPorts, p.Name)
|
|
}
|
|
if len(primaryPorts) == 1 {
|
|
// There is exactly one match in the set of preferred ports. Use
|
|
// this port, even if there may be others available. This allows
|
|
// flashing a specific board even if there are multiple available.
|
|
return primaryPorts[0], nil
|
|
} else if len(primaryPorts) > 1 {
|
|
// There are multiple preferred ports, probably because more than
|
|
// one device of the same type are connected (e.g. two Arduino
|
|
// Unos).
|
|
ports = primaryPorts
|
|
} else {
|
|
// No preferred ports found. Fall back to other serial ports
|
|
// available in the system.
|
|
ports = secondaryPorts
|
|
}
|
|
default:
|
|
return "", errors.New("unable to search for a default USB device to be flashed on this OS")
|
|
}
|
|
|
|
if err != nil {
|
|
return "", err
|
|
} else if ports == nil {
|
|
return "", errors.New("unable to locate a serial port")
|
|
} else if len(ports) == 0 {
|
|
return "", errors.New("no serial ports available")
|
|
}
|
|
|
|
if len(portCandidates) == 0 {
|
|
if len(usbInterfaces) > 0 {
|
|
return "", errors.New("unable to search for a default USB device - use -port flag, available ports are " + strings.Join(ports, ", "))
|
|
} else if len(ports) == 1 {
|
|
return ports[0], nil
|
|
} else {
|
|
return "", errors.New("multiple serial ports available - use -port flag, available ports are " + strings.Join(ports, ", "))
|
|
}
|
|
}
|
|
|
|
for _, ps := range portCandidates {
|
|
for _, p := range ports {
|
|
if p == ps {
|
|
return p, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return "", errors.New("port you specified '" + strings.Join(portCandidates, ",") + "' does not exist, available ports are " + strings.Join(ports, ", "))
|
|
}
|
|
|
|
// getBMPPorts returns BlackMagicProbe's serial ports if any
|
|
func getBMPPorts() (gdbPort, uartPort string, err error) {
|
|
var portsList []*enumerator.PortDetails
|
|
portsList, err = enumerator.GetDetailedPortsList()
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
var ports []string
|
|
for _, p := range portsList {
|
|
if !p.IsUSB {
|
|
continue
|
|
}
|
|
if p.VID != "" && p.PID != "" {
|
|
vid, vidErr := strconv.ParseUint(p.VID, 16, 16)
|
|
pid, pidErr := strconv.ParseUint(p.PID, 16, 16)
|
|
if vidErr == nil && pidErr == nil && vid == 0x1d50 && pid == 0x6018 {
|
|
ports = append(ports, p.Name)
|
|
}
|
|
}
|
|
}
|
|
if len(ports) == 2 {
|
|
return ports[0], ports[1], nil
|
|
} else if len(ports) == 0 {
|
|
return "", "", errors.New("no BMP detected")
|
|
} else {
|
|
return "", "", fmt.Errorf("expected 2 BMP serial ports, found %d - did you perhaps connect more than one BMP?", len(ports))
|
|
}
|
|
}
|
|
|
|
func usage(command string) {
|
|
switch command {
|
|
default:
|
|
fmt.Fprintln(os.Stderr, "TinyGo is a Go compiler for small places.")
|
|
fmt.Fprintln(os.Stderr, "version:", goenv.Version())
|
|
fmt.Fprintf(os.Stderr, "usage: %s <command> [arguments]\n", os.Args[0])
|
|
fmt.Fprintln(os.Stderr, "\ncommands:")
|
|
fmt.Fprintln(os.Stderr, " build: compile packages and dependencies")
|
|
fmt.Fprintln(os.Stderr, " run: compile and run immediately")
|
|
fmt.Fprintln(os.Stderr, " test: test packages")
|
|
fmt.Fprintln(os.Stderr, " flash: compile and flash to the device")
|
|
fmt.Fprintln(os.Stderr, " gdb: run/flash and immediately enter GDB")
|
|
fmt.Fprintln(os.Stderr, " lldb: run/flash and immediately enter LLDB")
|
|
fmt.Fprintln(os.Stderr, " monitor: open communication port")
|
|
fmt.Fprintln(os.Stderr, " ports: list available serial ports")
|
|
fmt.Fprintln(os.Stderr, " env: list environment variables used during build")
|
|
fmt.Fprintln(os.Stderr, " list: run go list using the TinyGo root")
|
|
fmt.Fprintln(os.Stderr, " clean: empty cache directory ("+goenv.Get("GOCACHE")+")")
|
|
fmt.Fprintln(os.Stderr, " targets: list targets")
|
|
fmt.Fprintln(os.Stderr, " info: show info for specified target")
|
|
fmt.Fprintln(os.Stderr, " version: show version")
|
|
fmt.Fprintln(os.Stderr, " help: print this help text")
|
|
|
|
if flag.Parsed() {
|
|
fmt.Fprintln(os.Stderr, "\nflags:")
|
|
flag.PrintDefaults()
|
|
}
|
|
|
|
fmt.Fprintln(os.Stderr, "\nfor more details, see https://tinygo.org/docs/reference/usage/")
|
|
}
|
|
}
|
|
|
|
func handleCompilerError(err error) {
|
|
if err != nil {
|
|
wd, getwdErr := os.Getwd()
|
|
if getwdErr != nil {
|
|
wd = ""
|
|
}
|
|
diagnostics.CreateDiagnostics(err).WriteTo(os.Stderr, wd)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// This is a special type for the -X flag to parse the pkgpath.Var=stringVal
|
|
// format. It has to be a special type to allow multiple variables to be defined
|
|
// this way.
|
|
type globalValuesFlag map[string]map[string]string
|
|
|
|
func (m globalValuesFlag) String() string {
|
|
return "pkgpath.Var=value"
|
|
}
|
|
|
|
func (m globalValuesFlag) Set(value string) error {
|
|
equalsIndex := strings.IndexByte(value, '=')
|
|
if equalsIndex < 0 {
|
|
return errors.New("expected format pkgpath.Var=value")
|
|
}
|
|
pathAndName := value[:equalsIndex]
|
|
pointIndex := strings.LastIndexByte(pathAndName, '.')
|
|
if pointIndex < 0 {
|
|
return errors.New("expected format pkgpath.Var=value")
|
|
}
|
|
path := pathAndName[:pointIndex]
|
|
name := pathAndName[pointIndex+1:]
|
|
stringValue := value[equalsIndex+1:]
|
|
if m[path] == nil {
|
|
m[path] = make(map[string]string)
|
|
}
|
|
m[path][name] = stringValue
|
|
return nil
|
|
}
|
|
|
|
// parseGoLinkFlag parses the -ldflags parameter. Its primary purpose right now
|
|
// is the -X flag, for setting the value of global string variables.
|
|
func parseGoLinkFlag(flagsString string) (map[string]map[string]string, error) {
|
|
set := flag.NewFlagSet("link", flag.ExitOnError)
|
|
globalVarValues := make(globalValuesFlag)
|
|
set.Var(globalVarValues, "X", "Set the value of the string variable to the given value.")
|
|
flags, err := shlex.Split(flagsString)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = set.Parse(flags)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return map[string]map[string]string(globalVarValues), nil
|
|
}
|
|
|
|
// getListOfPackages returns a standard list of packages for a given list that might
|
|
// include wildards using `go list`.
|
|
// For example [./...] => ["pkg1", "pkg1/pkg12", "pkg2"]
|
|
func getListOfPackages(pkgs []string, options *compileopts.Options) ([]string, error) {
|
|
config, err := builder.NewConfig(options)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cmd, err := loader.List(config, nil, pkgs)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to run `go list`: %w", err)
|
|
}
|
|
outputBuf := bytes.NewBuffer(nil)
|
|
cmd.Stdout = outputBuf
|
|
cmd.Stderr = os.Stderr
|
|
err = cmd.Run()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var pkgNames []string
|
|
sc := bufio.NewScanner(outputBuf)
|
|
for sc.Scan() {
|
|
pkgNames = append(pkgNames, sc.Text())
|
|
}
|
|
|
|
return pkgNames, nil
|
|
}
|
|
|
|
func main() {
|
|
if len(os.Args) < 2 {
|
|
fmt.Fprintln(os.Stderr, "No command-line arguments supplied.")
|
|
usage("")
|
|
os.Exit(1)
|
|
}
|
|
command := os.Args[1]
|
|
|
|
opt := flag.String("opt", "z", "optimization level: 0, 1, 2, s, z")
|
|
gc := flag.String("gc", "", "garbage collector to use (none, leaking, conservative)")
|
|
panicStrategy := flag.String("panic", "print", "panic strategy (print, trap)")
|
|
scheduler := flag.String("scheduler", "", "which scheduler to use (none, tasks, asyncify)")
|
|
serial := flag.String("serial", "", "which serial output to use (none, uart, usb, rtt)")
|
|
work := flag.Bool("work", false, "print the name of the temporary build directory and do not delete this directory on exit")
|
|
interpTimeout := flag.Duration("interp-timeout", 180*time.Second, "interp optimization pass timeout")
|
|
var tags buildutil.TagsFlag
|
|
flag.Var(&tags, "tags", "a space-separated list of extra build tags")
|
|
target := flag.String("target", "", "chip/board name or JSON target specification file")
|
|
var stackSize uint64
|
|
flag.Func("stack-size", "goroutine stack size (if unknown at compile time)", func(s string) error {
|
|
size, err := bytesize.Parse(s)
|
|
stackSize = uint64(size)
|
|
return err
|
|
})
|
|
printSize := flag.String("size", "", "print sizes (none, short, full)")
|
|
printStacks := flag.Bool("print-stacks", false, "print stack sizes of goroutines")
|
|
printAllocsString := flag.String("print-allocs", "", "regular expression of functions for which heap allocations should be printed")
|
|
printCommands := flag.Bool("x", false, "Print commands")
|
|
parallelism := flag.Int("p", runtime.GOMAXPROCS(0), "the number of build jobs that can run in parallel")
|
|
nodebug := flag.Bool("no-debug", false, "strip debug information")
|
|
ocdCommandsString := flag.String("ocd-commands", "", "OpenOCD commands, overriding target spec (can specify multiple separated by commas)")
|
|
ocdOutput := flag.Bool("ocd-output", false, "print OCD daemon output during debug")
|
|
port := flag.String("port", "", "flash port (can specify multiple candidates separated by commas)")
|
|
timeout := flag.Duration("timeout", 20*time.Second, "the length of time to retry locating the MSD volume to be used for flashing")
|
|
programmer := flag.String("programmer", "", "which hardware programmer to use")
|
|
ldflags := flag.String("ldflags", "", "Go link tool compatible ldflags")
|
|
llvmFeatures := flag.String("llvm-features", "", "comma separated LLVM features to enable")
|
|
cpuprofile := flag.String("cpuprofile", "", "cpuprofile output")
|
|
monitor := flag.Bool("monitor", false, "enable serial monitor")
|
|
baudrate := flag.Int("baudrate", 115200, "baudrate of serial monitor")
|
|
|
|
// Internal flags, that are only intended for TinyGo development.
|
|
printIR := flag.Bool("internal-printir", false, "print LLVM IR")
|
|
dumpSSA := flag.Bool("internal-dumpssa", false, "dump internal Go SSA")
|
|
verifyIR := flag.Bool("internal-verifyir", false, "run extra verification steps on LLVM IR")
|
|
// Don't generate debug information in the IR, to make IR more readable.
|
|
// You generally want debug information in IR for various features, like
|
|
// stack size calculation and features like -size=short, -print-allocs=,
|
|
// etc. The -no-debug flag is used to strip it at link time. But for TinyGo
|
|
// development it can be useful to not emit debug information at all.
|
|
skipDwarf := flag.Bool("internal-nodwarf", false, "internal flag, use -no-debug instead")
|
|
|
|
var flagJSON, flagDeps, flagTest bool
|
|
if command == "help" || command == "list" || command == "info" || command == "build" {
|
|
flag.BoolVar(&flagJSON, "json", false, "print data in JSON format")
|
|
}
|
|
if command == "help" || command == "list" {
|
|
flag.BoolVar(&flagDeps, "deps", false, "supply -deps flag to go list")
|
|
flag.BoolVar(&flagTest, "test", false, "supply -test flag to go list")
|
|
}
|
|
var outpath string
|
|
if command == "help" || command == "build" || command == "test" {
|
|
flag.StringVar(&outpath, "o", "", "output filename")
|
|
}
|
|
|
|
var witPackage, witWorld string
|
|
if command == "help" || command == "build" || command == "test" || command == "run" {
|
|
flag.StringVar(&witPackage, "wit-package", "", "wit package for wasm component embedding")
|
|
flag.StringVar(&witWorld, "wit-world", "", "wit world for wasm component embedding")
|
|
}
|
|
|
|
var testConfig compileopts.TestConfig
|
|
if command == "help" || command == "test" {
|
|
flag.BoolVar(&testConfig.CompileOnly, "c", false, "compile the test binary but do not run it")
|
|
flag.BoolVar(&testConfig.Verbose, "v", false, "verbose: print additional output")
|
|
flag.BoolVar(&testConfig.Short, "short", false, "short: run smaller test suite to save time")
|
|
flag.StringVar(&testConfig.RunRegexp, "run", "", "run: regexp of tests to run")
|
|
flag.StringVar(&testConfig.SkipRegexp, "skip", "", "skip: regexp of tests to skip")
|
|
testConfig.Count = flag.Int("count", 1, "count: number of times to run tests/benchmarks `count` times")
|
|
flag.StringVar(&testConfig.BenchRegexp, "bench", "", "bench: regexp of benchmarks to run")
|
|
flag.StringVar(&testConfig.BenchTime, "benchtime", "", "run each benchmark for duration `d`")
|
|
flag.BoolVar(&testConfig.BenchMem, "benchmem", false, "show memory stats for benchmarks")
|
|
flag.StringVar(&testConfig.Shuffle, "shuffle", "", "shuffle the order the tests and benchmarks run")
|
|
}
|
|
|
|
// Early command processing, before commands are interpreted by the Go flag
|
|
// library.
|
|
switch command {
|
|
case "clang", "ld.lld", "wasm-ld":
|
|
err := builder.RunTool(command, os.Args[2:]...)
|
|
if err != nil {
|
|
// The tool should have printed an error message already.
|
|
// Don't print another error message here.
|
|
os.Exit(1)
|
|
}
|
|
os.Exit(0)
|
|
}
|
|
|
|
flag.CommandLine.Parse(os.Args[2:])
|
|
globalVarValues, err := parseGoLinkFlag(*ldflags)
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
var printAllocs *regexp.Regexp
|
|
if *printAllocsString != "" {
|
|
printAllocs, err = regexp.Compile(*printAllocsString)
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
var ocdCommands []string
|
|
if *ocdCommandsString != "" {
|
|
ocdCommands = strings.Split(*ocdCommandsString, ",")
|
|
}
|
|
|
|
options := &compileopts.Options{
|
|
GOOS: goenv.Get("GOOS"),
|
|
GOARCH: goenv.Get("GOARCH"),
|
|
GOARM: goenv.Get("GOARM"),
|
|
GOMIPS: goenv.Get("GOMIPS"),
|
|
Target: *target,
|
|
StackSize: stackSize,
|
|
Opt: *opt,
|
|
GC: *gc,
|
|
PanicStrategy: *panicStrategy,
|
|
Scheduler: *scheduler,
|
|
Serial: *serial,
|
|
Work: *work,
|
|
InterpTimeout: *interpTimeout,
|
|
PrintIR: *printIR,
|
|
DumpSSA: *dumpSSA,
|
|
VerifyIR: *verifyIR,
|
|
SkipDWARF: *skipDwarf,
|
|
Semaphore: make(chan struct{}, *parallelism),
|
|
Debug: !*nodebug,
|
|
PrintSizes: *printSize,
|
|
PrintStacks: *printStacks,
|
|
PrintAllocs: printAllocs,
|
|
Tags: []string(tags),
|
|
TestConfig: testConfig,
|
|
GlobalValues: globalVarValues,
|
|
Programmer: *programmer,
|
|
OpenOCDCommands: ocdCommands,
|
|
LLVMFeatures: *llvmFeatures,
|
|
PrintJSON: flagJSON,
|
|
Monitor: *monitor,
|
|
BaudRate: *baudrate,
|
|
Timeout: *timeout,
|
|
WITPackage: witPackage,
|
|
WITWorld: witWorld,
|
|
}
|
|
if *printCommands {
|
|
options.PrintCommands = printCommand
|
|
}
|
|
|
|
err = options.Verify()
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, err.Error())
|
|
usage(command)
|
|
os.Exit(1)
|
|
}
|
|
|
|
if *cpuprofile != "" {
|
|
f, err := os.Create(*cpuprofile)
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, "could not create CPU profile: ", err)
|
|
os.Exit(1)
|
|
}
|
|
defer f.Close()
|
|
if err := pprof.StartCPUProfile(f); err != nil {
|
|
fmt.Fprintln(os.Stderr, "could not start CPU profile: ", err)
|
|
os.Exit(1)
|
|
}
|
|
defer pprof.StopCPUProfile()
|
|
}
|
|
|
|
switch command {
|
|
case "build":
|
|
pkgName := "."
|
|
if flag.NArg() == 1 {
|
|
pkgName = filepath.ToSlash(flag.Arg(0))
|
|
} else if flag.NArg() > 1 {
|
|
fmt.Fprintln(os.Stderr, "build only accepts a single positional argument: package name, but multiple were specified")
|
|
usage(command)
|
|
os.Exit(1)
|
|
}
|
|
if options.Target == "" && filepath.Ext(outpath) == ".wasm" {
|
|
options.Target = "wasm"
|
|
}
|
|
|
|
err := Build(pkgName, outpath, options)
|
|
handleCompilerError(err)
|
|
case "flash", "gdb", "lldb":
|
|
pkgName := filepath.ToSlash(flag.Arg(0))
|
|
if command == "flash" {
|
|
err := Flash(pkgName, *port, options)
|
|
handleCompilerError(err)
|
|
} else {
|
|
if !options.Debug {
|
|
fmt.Fprintln(os.Stderr, "Debug disabled while running debugger?")
|
|
usage(command)
|
|
os.Exit(1)
|
|
}
|
|
err := Debug(command, pkgName, *ocdOutput, options)
|
|
handleCompilerError(err)
|
|
}
|
|
case "run":
|
|
if flag.NArg() < 1 {
|
|
fmt.Fprintln(os.Stderr, "No package specified.")
|
|
usage(command)
|
|
os.Exit(1)
|
|
}
|
|
pkgName := filepath.ToSlash(flag.Arg(0))
|
|
err := Run(pkgName, options, flag.Args()[1:])
|
|
handleCompilerError(err)
|
|
case "test":
|
|
var pkgNames []string
|
|
for i := 0; i < flag.NArg(); i++ {
|
|
pkgNames = append(pkgNames, filepath.ToSlash(flag.Arg(i)))
|
|
}
|
|
if len(pkgNames) == 0 {
|
|
pkgNames = []string{"."}
|
|
}
|
|
|
|
explicitPkgNames, err := getListOfPackages(pkgNames, options)
|
|
if err != nil {
|
|
fmt.Printf("cannot resolve packages: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
if outpath != "" && len(explicitPkgNames) > 1 {
|
|
fmt.Println("cannot use -o flag with multiple packages")
|
|
os.Exit(1)
|
|
}
|
|
|
|
fail := make(chan struct{}, 1)
|
|
var wg sync.WaitGroup
|
|
bufs := make([]testOutputBuf, len(explicitPkgNames))
|
|
for i := range bufs {
|
|
bufs[i].done = make(chan struct{})
|
|
}
|
|
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
|
|
// Flush the output one test at a time.
|
|
// This ensures that outputs from different tests are not mixed together.
|
|
for i := range bufs {
|
|
err := bufs[i].flush(os.Stdout, os.Stderr)
|
|
if err != nil {
|
|
// There was an error writing to stdout or stderr, so we probably cannot print this.
|
|
select {
|
|
case fail <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Build and run the tests concurrently.
|
|
// This uses an additional semaphore to reduce the memory usage.
|
|
testSema := make(chan struct{}, cap(options.Semaphore))
|
|
for i, pkgName := range explicitPkgNames {
|
|
pkgName := pkgName
|
|
buf := &bufs[i]
|
|
testSema <- struct{}{}
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
defer func() { <-testSema }()
|
|
defer close(buf.done)
|
|
stdout := (*testStdout)(buf)
|
|
stderr := (*testStderr)(buf)
|
|
passed, err := Test(pkgName, stdout, stderr, options, outpath)
|
|
if err != nil {
|
|
wd, getwdErr := os.Getwd()
|
|
if getwdErr != nil {
|
|
wd = ""
|
|
}
|
|
diagnostics.CreateDiagnostics(err).WriteTo(os.Stderr, wd)
|
|
}
|
|
if !passed {
|
|
select {
|
|
case fail <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
// Wait for all tests to finish.
|
|
wg.Wait()
|
|
close(fail)
|
|
if _, fail := <-fail; fail {
|
|
os.Exit(1)
|
|
}
|
|
case "monitor":
|
|
config, err := builder.NewConfig(options)
|
|
handleCompilerError(err)
|
|
err = Monitor("", *port, config)
|
|
handleCompilerError(err)
|
|
case "ports":
|
|
serialPortInfo, err := ListSerialPorts()
|
|
handleCompilerError(err)
|
|
if len(serialPortInfo) == 0 {
|
|
fmt.Println("No serial ports found.")
|
|
}
|
|
fmt.Printf("%-20s %-9s %s\n", "Port", "ID", "Boards")
|
|
for _, s := range serialPortInfo {
|
|
fmt.Printf("%-20s %4s:%4s %s\n", s.Name, s.VID, s.PID, s.Target)
|
|
}
|
|
case "targets":
|
|
specs, err := compileopts.GetTargetSpecs()
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, "could not list targets:", err)
|
|
os.Exit(1)
|
|
return
|
|
}
|
|
names := []string{}
|
|
for key := range specs {
|
|
names = append(names, key)
|
|
}
|
|
sort.Strings(names)
|
|
for _, name := range names {
|
|
fmt.Println(name)
|
|
}
|
|
case "info":
|
|
if flag.NArg() == 1 {
|
|
options.Target = flag.Arg(0)
|
|
} else if flag.NArg() > 1 {
|
|
fmt.Fprintln(os.Stderr, "only one target name is accepted")
|
|
usage(command)
|
|
os.Exit(1)
|
|
}
|
|
config, err := builder.NewConfig(options)
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
usage(command)
|
|
os.Exit(1)
|
|
}
|
|
config.GoMinorVersion = 0 // this avoids creating the list of Go1.x build tags.
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(1)
|
|
}
|
|
cachedGOROOT, err := loader.GetCachedGoroot(config)
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(1)
|
|
}
|
|
if flagJSON {
|
|
json, _ := json.MarshalIndent(struct {
|
|
Target *compileopts.TargetSpec `json:"target"`
|
|
GOROOT string `json:"goroot"`
|
|
GOOS string `json:"goos"`
|
|
GOARCH string `json:"goarch"`
|
|
GOARM string `json:"goarm"`
|
|
GOMIPS string `json:"gomips"`
|
|
BuildTags []string `json:"build_tags"`
|
|
GC string `json:"garbage_collector"`
|
|
Scheduler string `json:"scheduler"`
|
|
LLVMTriple string `json:"llvm_triple"`
|
|
}{
|
|
Target: config.Target,
|
|
GOROOT: cachedGOROOT,
|
|
GOOS: config.GOOS(),
|
|
GOARCH: config.GOARCH(),
|
|
GOARM: config.GOARM(),
|
|
GOMIPS: config.GOMIPS(),
|
|
BuildTags: config.BuildTags(),
|
|
GC: config.GC(),
|
|
Scheduler: config.Scheduler(),
|
|
LLVMTriple: config.Triple(),
|
|
}, "", " ")
|
|
fmt.Println(string(json))
|
|
} else {
|
|
fmt.Printf("LLVM triple: %s\n", config.Triple())
|
|
fmt.Printf("GOOS: %s\n", config.GOOS())
|
|
fmt.Printf("GOARCH: %s\n", config.GOARCH())
|
|
fmt.Printf("build tags: %s\n", strings.Join(config.BuildTags(), " "))
|
|
fmt.Printf("garbage collector: %s\n", config.GC())
|
|
fmt.Printf("scheduler: %s\n", config.Scheduler())
|
|
fmt.Printf("cached GOROOT: %s\n", cachedGOROOT)
|
|
}
|
|
case "list":
|
|
config, err := builder.NewConfig(options)
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
usage(command)
|
|
os.Exit(1)
|
|
}
|
|
var extraArgs []string
|
|
if flagJSON {
|
|
extraArgs = append(extraArgs, "-json")
|
|
}
|
|
if flagDeps {
|
|
extraArgs = append(extraArgs, "-deps")
|
|
}
|
|
if flagTest {
|
|
extraArgs = append(extraArgs, "-test")
|
|
}
|
|
cmd, err := loader.List(config, extraArgs, flag.Args())
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, "failed to run `go list`:", err)
|
|
os.Exit(1)
|
|
}
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
err = cmd.Run()
|
|
if err != nil {
|
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
|
os.Exit(exitErr.ExitCode())
|
|
}
|
|
fmt.Fprintln(os.Stderr, "failed to run `go list`:", err)
|
|
os.Exit(1)
|
|
}
|
|
case "clean":
|
|
// remove cache directory
|
|
err := os.RemoveAll(goenv.Get("GOCACHE"))
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, "cannot clean cache:", err)
|
|
os.Exit(1)
|
|
}
|
|
case "help":
|
|
command := ""
|
|
if flag.NArg() >= 1 {
|
|
command = flag.Arg(0)
|
|
}
|
|
usage(command)
|
|
case "version":
|
|
goversion := "<unknown>"
|
|
if s, err := goenv.GorootVersionString(); err == nil {
|
|
goversion = s
|
|
}
|
|
fmt.Printf("tinygo version %s %s/%s (using go version %s and LLVM version %s)\n", goenv.Version(), runtime.GOOS, runtime.GOARCH, goversion, llvm.Version)
|
|
case "env":
|
|
if flag.NArg() == 0 {
|
|
// Show all environment variables.
|
|
for _, key := range goenv.Keys {
|
|
fmt.Printf("%s=%#v\n", key, goenv.Get(key))
|
|
}
|
|
} else {
|
|
// Show only one (or a few) environment variables.
|
|
for i := 0; i < flag.NArg(); i++ {
|
|
fmt.Println(goenv.Get(flag.Arg(i)))
|
|
}
|
|
}
|
|
default:
|
|
fmt.Fprintln(os.Stderr, "Unknown command:", command)
|
|
usage("")
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// testOutputBuf is used to buffer the output of concurrent tests.
|
|
type testOutputBuf struct {
|
|
mu sync.Mutex
|
|
output []outputEntry
|
|
stdout, stderr io.Writer
|
|
outerr, errerr error
|
|
done chan struct{}
|
|
}
|
|
|
|
// flush the output to stdout and stderr.
|
|
// This waits until done is closed.
|
|
func (b *testOutputBuf) flush(stdout, stderr io.Writer) error {
|
|
b.mu.Lock()
|
|
|
|
var err error
|
|
b.stdout = stdout
|
|
b.stderr = stderr
|
|
for _, e := range b.output {
|
|
var w io.Writer
|
|
var errDst *error
|
|
if e.stderr {
|
|
w = stderr
|
|
errDst = &b.errerr
|
|
} else {
|
|
w = stdout
|
|
errDst = &b.outerr
|
|
}
|
|
if *errDst != nil {
|
|
continue
|
|
}
|
|
|
|
_, werr := w.Write(e.data)
|
|
if werr != nil {
|
|
if err == nil {
|
|
err = werr
|
|
}
|
|
*errDst = err
|
|
}
|
|
}
|
|
|
|
b.mu.Unlock()
|
|
|
|
<-b.done
|
|
|
|
return err
|
|
}
|
|
|
|
// testStdout writes stdout from a test to the output buffer.
|
|
type testStdout testOutputBuf
|
|
|
|
func (out *testStdout) Write(data []byte) (int, error) {
|
|
buf := (*testOutputBuf)(out)
|
|
buf.mu.Lock()
|
|
|
|
if buf.stdout != nil {
|
|
// Write the output directly.
|
|
err := out.outerr
|
|
buf.mu.Unlock()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return buf.stdout.Write(data)
|
|
}
|
|
|
|
defer buf.mu.Unlock()
|
|
|
|
// Append the output.
|
|
if len(buf.output) == 0 || buf.output[len(buf.output)-1].stderr {
|
|
buf.output = append(buf.output, outputEntry{
|
|
stderr: false,
|
|
})
|
|
}
|
|
last := &buf.output[len(buf.output)-1]
|
|
last.data = append(last.data, data...)
|
|
|
|
return len(data), nil
|
|
}
|
|
|
|
// testStderr writes stderr from a test to the output buffer.
|
|
type testStderr testOutputBuf
|
|
|
|
func (out *testStderr) Write(data []byte) (int, error) {
|
|
buf := (*testOutputBuf)(out)
|
|
buf.mu.Lock()
|
|
|
|
if buf.stderr != nil {
|
|
// Write the output directly.
|
|
err := out.errerr
|
|
buf.mu.Unlock()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return buf.stderr.Write(data)
|
|
}
|
|
|
|
defer buf.mu.Unlock()
|
|
|
|
// Append the output.
|
|
if len(buf.output) == 0 || !buf.output[len(buf.output)-1].stderr {
|
|
buf.output = append(buf.output, outputEntry{
|
|
stderr: true,
|
|
})
|
|
}
|
|
last := &buf.output[len(buf.output)-1]
|
|
last.data = append(last.data, data...)
|
|
|
|
return len(data), nil
|
|
}
|
|
|
|
type outputEntry struct {
|
|
stderr bool
|
|
data []byte
|
|
}
|
|
|