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.
1100 lines
31 KiB
1100 lines
31 KiB
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"go/scanner"
|
|
"go/types"
|
|
"io"
|
|
"io/ioutil"
|
|
"os"
|
|
"os/exec"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/tinygo-org/tinygo/builder"
|
|
"github.com/tinygo-org/tinygo/compileopts"
|
|
"github.com/tinygo-org/tinygo/goenv"
|
|
"github.com/tinygo-org/tinygo/interp"
|
|
"github.com/tinygo-org/tinygo/loader"
|
|
"tinygo.org/x/go-llvm"
|
|
|
|
"go.bug.st/serial"
|
|
)
|
|
|
|
var (
|
|
// This variable is set at build time using -ldflags parameters.
|
|
// See: https://stackoverflow.com/a/11355611
|
|
gitSha1 string
|
|
)
|
|
|
|
// 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 from src to dst. It can copy over
|
|
// a possibly already existing file 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
|
|
}
|
|
|
|
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 {
|
|
fmt.Printf("%s %s\n ", name, strings.Join(arg, " "))
|
|
}
|
|
return exec.Command(name, arg...)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
return builder.Build(pkgName, outpath, config, func(result builder.BuildResult) error {
|
|
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()
|
|
} else {
|
|
// Move was successful.
|
|
return nil
|
|
}
|
|
})
|
|
}
|
|
|
|
// Test runs the tests in the given package.
|
|
func Test(pkgName string, options *compileopts.Options, testCompileOnly bool, outpath string) error {
|
|
options.TestConfig.CompileTestBinary = true
|
|
config, err := builder.NewConfig(options)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return builder.Build(pkgName, outpath, config, func(result builder.BuildResult) error {
|
|
if testCompileOnly || 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 testCompileOnly {
|
|
// Do not run the test.
|
|
return nil
|
|
}
|
|
if len(config.Target.Emulator) == 0 {
|
|
// Run directly.
|
|
cmd := executeCommand(config.Options, result.Binary)
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
cmd.Dir = result.MainDir
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
// Propagate the exit code
|
|
if err, ok := err.(*exec.ExitError); ok {
|
|
if status, ok := err.Sys().(syscall.WaitStatus); ok {
|
|
os.Exit(status.ExitStatus())
|
|
}
|
|
os.Exit(1)
|
|
}
|
|
return &commandError{"failed to run compiled binary", result.Binary, err}
|
|
}
|
|
return nil
|
|
} else {
|
|
// Run in an emulator.
|
|
args := append(config.Target.Emulator[1:], result.Binary)
|
|
cmd := executeCommand(config.Options, config.Target.Emulator[0], args...)
|
|
buf := &bytes.Buffer{}
|
|
w := io.MultiWriter(os.Stdout, buf)
|
|
cmd.Stdout = w
|
|
cmd.Stderr = os.Stderr
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
if err, ok := err.(*exec.ExitError); !ok || !err.Exited() {
|
|
// Workaround for QEMU which always exits with an error.
|
|
return &commandError{"failed to run emulator with", result.Binary, err}
|
|
}
|
|
}
|
|
testOutput := string(buf.Bytes())
|
|
if testOutput == "PASS\n" || strings.HasSuffix(testOutput, "\nPASS\n") {
|
|
// Test passed.
|
|
return nil
|
|
} else {
|
|
// Test failed, either by ending with the word "FAIL" or with a
|
|
// panic of some sort.
|
|
os.Exit(1)
|
|
return nil // unreachable
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// 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"
|
|
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 "native":
|
|
return errors.New("unknown flash method \"native\" - did you miss a -target flag?")
|
|
default:
|
|
return errors.New("unknown flash method: " + flashMethod)
|
|
}
|
|
|
|
return builder.Build(pkgName, fileExt, config, func(result builder.BuildResult) error {
|
|
// do we need port reset to put MCU into bootloader mode?
|
|
if config.Target.PortReset == "true" && flashMethod != "openocd" {
|
|
if port == "" {
|
|
var err error
|
|
port, err = getDefaultPort()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
err := touchSerialPortAt1200bps(port)
|
|
if err != nil {
|
|
return &commandError{"failed to reset port", result.Binary, err}
|
|
}
|
|
// give the target MCU a chance to restart into bootloader
|
|
time.Sleep(3 * time.Second)
|
|
}
|
|
|
|
// this flashing method copies the binary data to a Mass Storage Device (msd)
|
|
switch flashMethod {
|
|
case "", "command":
|
|
// Create the command.
|
|
flashCmd := config.Target.FlashCommand
|
|
fileToken := "{" + fileExt[1:] + "}"
|
|
flashCmd = strings.Replace(flashCmd, fileToken, result.Binary, -1)
|
|
|
|
if port == "" && strings.Contains(flashCmd, "{port}") {
|
|
var err error
|
|
port, err = getDefaultPort()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
flashCmd = strings.Replace(flashCmd, "{port}", port, -1)
|
|
|
|
// Execute the command.
|
|
var cmd *exec.Cmd
|
|
switch runtime.GOOS {
|
|
case "windows":
|
|
command := strings.Split(flashCmd, " ")
|
|
if len(command) < 2 {
|
|
return errors.New("invalid flash command")
|
|
}
|
|
cmd = executeCommand(config.Options, command[0], command[1:]...)
|
|
default:
|
|
cmd = executeCommand(config.Options, "/bin/sh", "-c", flashCmd)
|
|
}
|
|
|
|
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}
|
|
}
|
|
return nil
|
|
case "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}
|
|
}
|
|
return nil
|
|
case ".hex":
|
|
err := flashHexUsingMSD(config.Target.FlashVolume, result.Binary, config.Options)
|
|
if err != nil {
|
|
return &commandError{"failed to flash", result.Binary, err}
|
|
}
|
|
return nil
|
|
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
|
|
}
|
|
args = append(args, "-c", "program "+filepath.ToSlash(result.Binary)+" reset 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}
|
|
}
|
|
return nil
|
|
default:
|
|
return fmt.Errorf("unknown flash method: %s", flashMethod)
|
|
}
|
|
})
|
|
}
|
|
|
|
// FlashGDB compiles and flashes a program to a microcontroller (just like
|
|
// Flash) but instead of resetting the target, it will drop into a GDB shell.
|
|
// You can then set breakpoints, run the GDB `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 FlashGDB(pkgName string, ocdOutput bool, options *compileopts.Options) error {
|
|
config, err := builder.NewConfig(options)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if config.Target.GDB == "" {
|
|
return errors.New("gdb not configured in the target specification")
|
|
}
|
|
|
|
return builder.Build(pkgName, "", config, func(result builder.BuildResult) error {
|
|
// Find a good way to run GDB.
|
|
gdbInterface, openocdInterface := config.Programmer()
|
|
switch gdbInterface {
|
|
case "msd", "command", "":
|
|
if len(config.Target.Emulator) != 0 {
|
|
if config.Target.Emulator[0] == "mgba" {
|
|
gdbInterface = "mgba"
|
|
} else if config.Target.Emulator[0] == "simavr" {
|
|
gdbInterface = "simavr"
|
|
} else if strings.HasPrefix(config.Target.Emulator[0], "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.
|
|
var gdbCommands []string
|
|
var daemon *exec.Cmd
|
|
switch gdbInterface {
|
|
case "native":
|
|
// Run GDB directly.
|
|
case "openocd":
|
|
gdbCommands = append(gdbCommands, "target remote :3333", "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: os.Stderr,
|
|
Prefix: "openocd: ",
|
|
Color: TermColorYellow,
|
|
}
|
|
daemon.Stdout = w
|
|
daemon.Stderr = w
|
|
}
|
|
case "jlink":
|
|
gdbCommands = append(gdbCommands, "target remote :2331", "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: os.Stderr,
|
|
Prefix: "jlink: ",
|
|
Color: TermColorYellow,
|
|
}
|
|
daemon.Stdout = w
|
|
daemon.Stderr = w
|
|
}
|
|
case "qemu":
|
|
gdbCommands = append(gdbCommands, "target remote :1234")
|
|
|
|
// Run in an emulator.
|
|
args := append(config.Target.Emulator[1:], result.Binary, "-s", "-S")
|
|
daemon = executeCommand(config.Options, config.Target.Emulator[0], args...)
|
|
daemon.Stdout = os.Stdout
|
|
daemon.Stderr = os.Stderr
|
|
case "qemu-user":
|
|
gdbCommands = append(gdbCommands, "target remote :1234")
|
|
|
|
// Run in an emulator.
|
|
args := append(config.Target.Emulator[1:], "-g", "1234", result.Binary)
|
|
daemon = executeCommand(config.Options, config.Target.Emulator[0], args...)
|
|
daemon.Stdout = os.Stdout
|
|
daemon.Stderr = os.Stderr
|
|
case "mgba":
|
|
gdbCommands = append(gdbCommands, "target remote :2345")
|
|
|
|
// Run in an emulator.
|
|
args := append(config.Target.Emulator[1:], result.Binary, "-g")
|
|
daemon = executeCommand(config.Options, config.Target.Emulator[0], args...)
|
|
daemon.Stdout = os.Stdout
|
|
daemon.Stderr = os.Stderr
|
|
case "simavr":
|
|
gdbCommands = append(gdbCommands, "target remote :1234")
|
|
|
|
// Run in an emulator.
|
|
args := append(config.Target.Emulator[1:], "-g", result.Binary)
|
|
daemon = executeCommand(config.Options, config.Target.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)
|
|
// Maybe we should send a .Kill() after x seconds?
|
|
daemon.Wait()
|
|
}()
|
|
}
|
|
|
|
// 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 command.
|
|
// By default: gdb -ex run <binary>
|
|
// Exit GDB with Ctrl-D.
|
|
params := []string{result.Binary}
|
|
for _, cmd := range gdbCommands {
|
|
params = append(params, "-ex", cmd)
|
|
}
|
|
cmd := executeCommand(config.Options, config.Target.GDB, params...)
|
|
cmd.Stdin = os.Stdin
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
return &commandError{"failed to run gdb with", result.Binary, 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) error {
|
|
config, err := builder.NewConfig(options)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return builder.Build(pkgName, ".elf", config, func(result builder.BuildResult) error {
|
|
if len(config.Target.Emulator) == 0 {
|
|
// Run directly.
|
|
cmd := executeCommand(config.Options, result.Binary)
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
if err, ok := err.(*exec.ExitError); ok && err.Exited() {
|
|
// Workaround for QEMU which always exits with an error.
|
|
return nil
|
|
}
|
|
return &commandError{"failed to run compiled binary", result.Binary, err}
|
|
}
|
|
return nil
|
|
} else {
|
|
// Run in an emulator.
|
|
args := append(config.Target.Emulator[1:], result.Binary)
|
|
cmd := executeCommand(config.Options, config.Target.Emulator[0], args...)
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
if err, ok := err.(*exec.ExitError); ok && err.Exited() {
|
|
// Workaround for QEMU which always exits with an error.
|
|
return nil
|
|
}
|
|
return &commandError{"failed to run emulator with", result.Binary, err}
|
|
}
|
|
return 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)
|
|
}
|
|
|
|
const maxMSDRetries = 10
|
|
|
|
func flashUF2UsingMSD(volume, tmppath string, options *compileopts.Options) error {
|
|
// find standard UF2 info path
|
|
var infoPath string
|
|
switch runtime.GOOS {
|
|
case "linux", "freebsd":
|
|
infoPath = "/media/*/" + volume + "/INFO_UF2.TXT"
|
|
case "darwin":
|
|
infoPath = "/Volumes/" + volume + "/INFO_UF2.TXT"
|
|
case "windows":
|
|
path, err := windowsFindUSBDrive(volume, options)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
infoPath = path + "/INFO_UF2.TXT"
|
|
}
|
|
|
|
d, err := locateDevice(volume, infoPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return moveFile(tmppath, filepath.Dir(d)+"/flash.uf2")
|
|
}
|
|
|
|
func flashHexUsingMSD(volume, tmppath string, options *compileopts.Options) error {
|
|
// find expected volume path
|
|
var destPath string
|
|
switch runtime.GOOS {
|
|
case "linux", "freebsd":
|
|
destPath = "/media/*/" + volume
|
|
case "darwin":
|
|
destPath = "/Volumes/" + volume
|
|
case "windows":
|
|
path, err := windowsFindUSBDrive(volume, options)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
destPath = path + "/"
|
|
}
|
|
|
|
d, err := locateDevice(volume, destPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return moveFile(tmppath, d+"/flash.hex")
|
|
}
|
|
|
|
func locateDevice(volume, path string) (string, error) {
|
|
var d []string
|
|
var err error
|
|
for i := 0; i < maxMSDRetries; i++ {
|
|
d, err = filepath.Glob(path)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if d != nil {
|
|
break
|
|
}
|
|
time.Sleep(500 * time.Millisecond)
|
|
}
|
|
if d == nil {
|
|
return "", errors.New("unable to locate device: " + volume)
|
|
}
|
|
return d[0], nil
|
|
}
|
|
|
|
func windowsFindUSBDrive(volume string, options *compileopts.Options) (string, error) {
|
|
cmd := executeCommand(options, "wmic",
|
|
"PATH", "Win32_LogicalDisk", "WHERE", "VolumeName = '"+volume+"'",
|
|
"get", "DeviceID,VolumeName,FileSystem,DriveType")
|
|
|
|
var out bytes.Buffer
|
|
cmd.Stdout = &out
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
for _, line := range strings.Split(out.String(), "\n") {
|
|
words := strings.Fields(line)
|
|
if len(words) >= 3 {
|
|
if words[1] == "2" && words[2] == "FAT" {
|
|
return words[0], nil
|
|
}
|
|
}
|
|
}
|
|
return "", errors.New("unable to locate a USB device to be flashed")
|
|
}
|
|
|
|
// getDefaultPort returns the default serial port depending on the operating system.
|
|
func getDefaultPort() (port string, err error) {
|
|
var portPath string
|
|
switch runtime.GOOS {
|
|
case "darwin":
|
|
portPath = "/dev/cu.usb*"
|
|
case "linux":
|
|
portPath = "/dev/ttyACM*"
|
|
case "freebsd":
|
|
portPath = "/dev/cuaU*"
|
|
case "windows":
|
|
ports, err := serial.GetPortsList()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if len(ports) == 0 {
|
|
return "", errors.New("no serial ports available")
|
|
} else if len(ports) > 1 {
|
|
return "", errors.New("multiple serial ports available - use -port flag")
|
|
}
|
|
|
|
return ports[0], nil
|
|
default:
|
|
return "", errors.New("unable to search for a default USB device to be flashed on this OS")
|
|
}
|
|
|
|
d, err := filepath.Glob(portPath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if d == nil {
|
|
return "", errors.New("unable to locate a serial port")
|
|
}
|
|
|
|
return d[0], nil
|
|
}
|
|
|
|
func usage() {
|
|
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 [-printir] [-target=<target>] -o <output> <input>\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, " 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, " help: print this help text")
|
|
fmt.Fprintln(os.Stderr, "\nflags:")
|
|
flag.PrintDefaults()
|
|
}
|
|
|
|
// try to make the path relative to the current working directory. If any error
|
|
// occurs, this error is ignored and the absolute path is returned instead.
|
|
func tryToMakePathRelative(dir string) string {
|
|
wd, err := os.Getwd()
|
|
if err != nil {
|
|
return dir
|
|
}
|
|
relpath, err := filepath.Rel(wd, dir)
|
|
if err != nil {
|
|
return dir
|
|
}
|
|
return relpath
|
|
}
|
|
|
|
// printCompilerError prints compiler errors using the provided logger function
|
|
// (similar to fmt.Println).
|
|
//
|
|
// There is one exception: interp errors may print to stderr unconditionally due
|
|
// to limitations in the LLVM bindings.
|
|
func printCompilerError(logln func(...interface{}), err error) {
|
|
switch err := err.(type) {
|
|
case types.Error:
|
|
printCompilerError(logln, scanner.Error{
|
|
Pos: err.Fset.Position(err.Pos),
|
|
Msg: err.Msg,
|
|
})
|
|
case scanner.Error:
|
|
if !strings.HasPrefix(err.Pos.Filename, filepath.Join(goenv.Get("GOROOT"), "src")) && !strings.HasPrefix(err.Pos.Filename, filepath.Join(goenv.Get("TINYGOROOT"), "src")) {
|
|
// This file is not from the standard library (either the GOROOT or
|
|
// the TINYGOROOT). Make the path relative, for easier reading.
|
|
// Ignore any errors in the process (falling back to the absolute
|
|
// path).
|
|
err.Pos.Filename = tryToMakePathRelative(err.Pos.Filename)
|
|
}
|
|
logln(err)
|
|
case scanner.ErrorList:
|
|
for _, scannerErr := range err {
|
|
printCompilerError(logln, *scannerErr)
|
|
}
|
|
case *interp.Error:
|
|
logln("#", err.ImportPath)
|
|
logln(err.Error())
|
|
if !err.Inst.IsNil() {
|
|
err.Inst.Dump()
|
|
logln()
|
|
}
|
|
if len(err.Traceback) > 0 {
|
|
logln("\ntraceback:")
|
|
for _, line := range err.Traceback {
|
|
logln(line.Pos.String() + ":")
|
|
line.Inst.Dump()
|
|
logln()
|
|
}
|
|
}
|
|
case loader.Errors:
|
|
logln("#", err.Pkg.ImportPath)
|
|
for _, err := range err.Errs {
|
|
printCompilerError(logln, err)
|
|
}
|
|
case loader.Error:
|
|
logln(err.Err.Error())
|
|
logln("package", err.ImportStack[0])
|
|
for _, pkgPath := range err.ImportStack[1:] {
|
|
logln("\timports", pkgPath)
|
|
}
|
|
case *builder.MultiError:
|
|
for _, err := range err.Errs {
|
|
printCompilerError(logln, err)
|
|
}
|
|
default:
|
|
logln("error:", err)
|
|
}
|
|
}
|
|
|
|
func handleCompilerError(err error) {
|
|
if err != nil {
|
|
printCompilerError(func(args ...interface{}) {
|
|
fmt.Fprintln(os.Stderr, args...)
|
|
}, err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
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, extalloc, conservative)")
|
|
panicStrategy := flag.String("panic", "print", "panic strategy (print, trap)")
|
|
scheduler := flag.String("scheduler", "", "which scheduler to use (none, coroutines, tasks)")
|
|
printIR := flag.Bool("printir", false, "print LLVM IR")
|
|
dumpSSA := flag.Bool("dumpssa", false, "dump internal Go SSA")
|
|
verifyIR := flag.Bool("verifyir", false, "run extra verification steps on LLVM IR")
|
|
tags := flag.String("tags", "", "a space-separated list of extra build tags")
|
|
target := flag.String("target", "", "LLVM target | .json file with TargetSpec")
|
|
printSize := flag.String("size", "", "print sizes (none, short, full)")
|
|
printStacks := flag.Bool("print-stacks", false, "print stack sizes of goroutines")
|
|
printCommands := flag.Bool("x", false, "Print commands")
|
|
nodebug := flag.Bool("no-debug", false, "disable DWARF debug symbol generation")
|
|
ocdOutput := flag.Bool("ocd-output", false, "print OCD daemon output during debug")
|
|
port := flag.String("port", "", "flash port")
|
|
programmer := flag.String("programmer", "", "which hardware programmer to use")
|
|
cFlags := flag.String("cflags", "", "additional cflags for compiler")
|
|
ldFlags := flag.String("ldflags", "", "additional ldflags for linker")
|
|
wasmAbi := flag.String("wasm-abi", "", "WebAssembly ABI conventions: js (no i64 params) or generic")
|
|
|
|
var flagJSON, flagDeps *bool
|
|
if command == "help" || command == "list" {
|
|
flagJSON = flag.Bool("json", false, "print data in JSON format")
|
|
flagDeps = flag.Bool("deps", false, "")
|
|
}
|
|
var outpath string
|
|
if command == "help" || command == "build" || command == "build-library" || command == "test" {
|
|
flag.StringVar(&outpath, "o", "", "output filename")
|
|
}
|
|
var testCompileOnlyFlag *bool
|
|
if command == "help" || command == "test" {
|
|
testCompileOnlyFlag = flag.Bool("c", false, "compile the test binary but do not run it")
|
|
}
|
|
|
|
// 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 {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(1)
|
|
}
|
|
os.Exit(0)
|
|
}
|
|
|
|
flag.CommandLine.Parse(os.Args[2:])
|
|
options := &compileopts.Options{
|
|
Target: *target,
|
|
Opt: *opt,
|
|
GC: *gc,
|
|
PanicStrategy: *panicStrategy,
|
|
Scheduler: *scheduler,
|
|
PrintIR: *printIR,
|
|
DumpSSA: *dumpSSA,
|
|
VerifyIR: *verifyIR,
|
|
Debug: !*nodebug,
|
|
PrintSizes: *printSize,
|
|
PrintStacks: *printStacks,
|
|
PrintCommands: *printCommands,
|
|
Tags: *tags,
|
|
WasmAbi: *wasmAbi,
|
|
Programmer: *programmer,
|
|
}
|
|
|
|
if *cFlags != "" {
|
|
options.CFlags = strings.Split(*cFlags, " ")
|
|
}
|
|
|
|
if *ldFlags != "" {
|
|
options.LDFlags = strings.Split(*ldFlags, " ")
|
|
}
|
|
|
|
os.Setenv("CC", "clang -target="+*target)
|
|
|
|
err := options.Verify()
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, err.Error())
|
|
usage()
|
|
os.Exit(1)
|
|
}
|
|
|
|
switch command {
|
|
case "build":
|
|
if outpath == "" {
|
|
fmt.Fprintln(os.Stderr, "No output filename supplied (-o).")
|
|
usage()
|
|
os.Exit(1)
|
|
}
|
|
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()
|
|
os.Exit(1)
|
|
}
|
|
if options.Target == "" && filepath.Ext(outpath) == ".wasm" {
|
|
options.Target = "wasm"
|
|
}
|
|
|
|
err := Build(pkgName, outpath, options)
|
|
handleCompilerError(err)
|
|
case "build-library":
|
|
// Note: this command is only meant to be used while making a release!
|
|
if outpath == "" {
|
|
fmt.Fprintln(os.Stderr, "No output filename supplied (-o).")
|
|
usage()
|
|
os.Exit(1)
|
|
}
|
|
if *target == "" {
|
|
fmt.Fprintln(os.Stderr, "No target (-target).")
|
|
}
|
|
if flag.NArg() != 1 {
|
|
fmt.Fprintf(os.Stderr, "Build-library only accepts exactly one library name as argument, %d given\n", flag.NArg())
|
|
usage()
|
|
os.Exit(1)
|
|
}
|
|
var lib *builder.Library
|
|
switch name := flag.Arg(0); name {
|
|
case "compiler-rt":
|
|
lib = &builder.CompilerRT
|
|
case "picolibc":
|
|
lib = &builder.Picolibc
|
|
default:
|
|
fmt.Fprintf(os.Stderr, "Unknown library: %s\n", name)
|
|
os.Exit(1)
|
|
}
|
|
path, err := lib.Load(*target)
|
|
handleCompilerError(err)
|
|
copyFile(path, outpath)
|
|
case "flash", "gdb":
|
|
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 gdb?")
|
|
usage()
|
|
os.Exit(1)
|
|
}
|
|
err := FlashGDB(pkgName, *ocdOutput, options)
|
|
handleCompilerError(err)
|
|
}
|
|
case "run":
|
|
if flag.NArg() != 1 {
|
|
fmt.Fprintln(os.Stderr, "No package specified.")
|
|
usage()
|
|
os.Exit(1)
|
|
}
|
|
pkgName := filepath.ToSlash(flag.Arg(0))
|
|
err := Run(pkgName, options)
|
|
handleCompilerError(err)
|
|
case "test":
|
|
pkgName := "."
|
|
if flag.NArg() == 1 {
|
|
pkgName = filepath.ToSlash(flag.Arg(0))
|
|
} else if flag.NArg() > 1 {
|
|
fmt.Fprintln(os.Stderr, "test only accepts a single positional argument: package name, but multiple were specified")
|
|
usage()
|
|
os.Exit(1)
|
|
}
|
|
err := Test(pkgName, options, *testCompileOnlyFlag, outpath)
|
|
handleCompilerError(err)
|
|
case "targets":
|
|
dir := filepath.Join(goenv.Get("TINYGOROOT"), "targets")
|
|
entries, err := ioutil.ReadDir(dir)
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, "could not list targets:", err)
|
|
os.Exit(1)
|
|
return
|
|
}
|
|
for _, entry := range entries {
|
|
if !entry.Mode().IsRegular() || !strings.HasSuffix(entry.Name(), ".json") {
|
|
// Only inspect JSON files.
|
|
continue
|
|
}
|
|
path := filepath.Join(dir, entry.Name())
|
|
spec, err := compileopts.LoadTarget(path)
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, "could not list target:", err)
|
|
os.Exit(1)
|
|
return
|
|
}
|
|
if spec.FlashMethod == "" && spec.FlashCommand == "" && spec.Emulator == nil {
|
|
// This doesn't look like a regular target file, but rather like
|
|
// a parent target (such as targets/cortex-m.json).
|
|
continue
|
|
}
|
|
name := entry.Name()
|
|
name = name[:len(name)-5]
|
|
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()
|
|
os.Exit(1)
|
|
}
|
|
config, err := builder.NewConfig(options)
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
usage()
|
|
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)
|
|
}
|
|
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()
|
|
os.Exit(1)
|
|
}
|
|
var extraArgs []string
|
|
if *flagJSON {
|
|
extraArgs = append(extraArgs, "-json")
|
|
}
|
|
if *flagDeps {
|
|
extraArgs = append(extraArgs, "-deps")
|
|
}
|
|
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 {
|
|
if status, ok := exitErr.Sys().(syscall.WaitStatus); ok {
|
|
os.Exit(status.ExitStatus())
|
|
}
|
|
os.Exit(1)
|
|
}
|
|
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":
|
|
usage()
|
|
case "version":
|
|
goversion := "<unknown>"
|
|
if s, err := goenv.GorootVersionString(goenv.Get("GOROOT")); err == nil {
|
|
goversion = s
|
|
}
|
|
version := goenv.Version
|
|
if strings.HasSuffix(goenv.Version, "-dev") && gitSha1 != "" {
|
|
version += "-" + gitSha1
|
|
}
|
|
fmt.Printf("tinygo version %s %s/%s (using go version %s and LLVM version %s)\n", 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)
|
|
}
|
|
}
|
|
|