package main import ( "errors" "flag" "fmt" "go/types" "io" "os" "os/exec" "os/signal" "path/filepath" "runtime" "strconv" "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" serial "go.bug.st/serial.v1" ) // 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. inf, err := os.Open(src) if err != nil { return err } defer inf.Close() outpath := dst + ".tmp" outf, err := os.Create(outpath) if err != nil { return err } _, err = io.Copy(outf, inf) if err != nil { os.Remove(outpath) return err } err = outf.Close() if err != nil { return err } return os.Rename(dst+".tmp", dst) } // 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(tmppath string) error { if err := os.Rename(tmppath, outpath); err != nil { // Moving failed. Do a file copy. inf, err := os.Open(tmppath) 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) error { config, err := builder.NewConfig(options) if err != nil { return err } // Add test build tag. This is incorrect: `go test` only looks at the // _test.go file suffix but does not add the test build tag in the process. // However, it's a simple fix right now. // For details: https://github.com/golang/go/issues/21360 config.Target.BuildTags = append(config.Target.BuildTags, "test") options.TestConfig.CompileTestBinary = true return builder.Build(pkgName, ".elf", config, func(tmppath string) error { cmd := exec.Command(tmppath) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr 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", tmppath, err} } return nil }) } // 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(tmppath string) error { // do we need port reset to put MCU into bootloader mode? if config.Target.PortReset == "true" { err := touchSerialPortAt1200bps(port) if err != nil { return &commandError{"failed to reset port", tmppath, 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, tmppath, -1) flashCmd = strings.Replace(flashCmd, "{port}", port, -1) // Execute the command. cmd := exec.Command("/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", tmppath, err} } return nil case "msd": switch fileExt { case ".uf2": err := flashUF2UsingMSD(config.Target.FlashVolume, tmppath) if err != nil { return &commandError{"failed to flash", tmppath, err} } return nil case ".hex": err := flashHexUsingMSD(config.Target.FlashVolume, tmppath) if err != nil { return &commandError{"failed to flash", tmppath, 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 "+tmppath+" reset exit") cmd := exec.Command("openocd", args...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr err = cmd.Run() if err != nil { return &commandError{"failed to flash", tmppath, 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, port 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(tmppath string) error { // Find a good way to run GDB. gdbInterface, openocdInterface := config.Programmer() switch gdbInterface { case "msd", "command", "": if gdbInterface == "" { gdbInterface = "command" } if openocdInterface != "" && config.Target.OpenOCDTarget != "" { gdbInterface = "openocd" } } // Run the GDB server, if necessary. var gdbCommands []string 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 := exec.Command("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 } // 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. daemon.Start() defer func() { daemon.Process.Signal(os.Interrupt) // Maybe we should send a .Kill() after x seconds? daemon.Wait() }() 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) } // 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 // Exit GDB with Ctrl-D. params := []string{tmppath} for _, cmd := range gdbCommands { params = append(params, "-ex", cmd) } cmd := exec.Command(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", tmppath, 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(tmppath string) error { if len(config.Target.Emulator) == 0 { // Run directly. cmd := exec.Command(tmppath) 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", tmppath, err} } return nil } else { // Run in an emulator. args := append(config.Target.Emulator[1:], tmppath) cmd := exec.Command(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", tmppath, err} } return nil } }) } func touchSerialPortAt1200bps(port string) error { // Open port p, err := serial.Open(port, &serial.Mode{BaudRate: 1200}) if err != nil { return fmt.Errorf("opening port: %s", err) } defer p.Close() p.SetDTR(false) return nil } func flashUF2UsingMSD(volume, tmppath string) error { // find standard UF2 info path infoPath := "/media/*/" + volume + "/INFO_UF2.TXT" if runtime.GOOS == "darwin" { infoPath = "/Volumes/" + volume + "/INFO_UF2.TXT" } d, err := filepath.Glob(infoPath) if err != nil { return err } if d == nil { return errors.New("unable to locate UF2 device: " + volume) } return moveFile(tmppath, filepath.Dir(d[0])+"/flash.uf2") } func flashHexUsingMSD(volume, tmppath string) error { // find expected volume path destPath := "/media/*/" + volume if runtime.GOOS == "darwin" { destPath = "/Volumes/" + volume } d, err := filepath.Glob(destPath) if err != nil { return err } if d == nil { return errors.New("unable to locate device: " + volume) } return moveFile(tmppath, d[0]+"/flash.hex") } // parseSize converts a human-readable size (with k/m/g suffix) into a plain // number. func parseSize(s string) (int64, error) { s = strings.ToLower(strings.TrimSpace(s)) if len(s) == 0 { return 0, errors.New("no size provided") } multiply := int64(1) switch s[len(s)-1] { case 'k': multiply = 1 << 10 case 'm': multiply = 1 << 20 case 'g': multiply = 1 << 30 } if multiply != 1 { s = s[:len(s)-1] } n, err := strconv.ParseInt(s, 0, 64) n *= multiply return n, err } func usage() { fmt.Fprintln(os.Stderr, "TinyGo is a Go compiler for small places.") fmt.Fprintln(os.Stderr, "version:", version) fmt.Fprintf(os.Stderr, "usage: %s command [-printir] [-target=] -o \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, " clean: empty cache directory ("+goenv.Get("GOCACHE")+")") fmt.Fprintln(os.Stderr, " help: print this help text") fmt.Fprintln(os.Stderr, "\nflags:") flag.PrintDefaults() } func handleCompilerError(err error) { if err != nil { switch err := err.(type) { case *interp.Unsupported: // hit an unknown/unsupported instruction fmt.Fprintln(os.Stderr, "unsupported instruction during init evaluation:") err.Inst.Dump() fmt.Fprintln(os.Stderr) case types.Error: fmt.Fprintln(os.Stderr, err) case loader.Errors: fmt.Fprintln(os.Stderr, "#", err.Pkg.ImportPath) for _, err := range err.Errs { fmt.Fprintln(os.Stderr, err) } case *builder.MultiError: for _, err := range err.Errs { fmt.Fprintln(os.Stderr, err) } default: fmt.Fprintln(os.Stderr, "error:", err) } os.Exit(1) } } func main() { outpath := flag.String("o", "", "output filename") 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 (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)") 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", "/dev/ttyACM0", "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", "js", "WebAssembly ABI conventions: js (no i64 params) or generic") heapSize := flag.String("heap-size", "1M", "default heap size in bytes (only supported by WebAssembly)") if len(os.Args) < 2 { fmt.Fprintln(os.Stderr, "No command-line arguments supplied.") usage() os.Exit(1) } command := os.Args[1] 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, Tags: *tags, WasmAbi: *wasmAbi, Programmer: *programmer, } if *cFlags != "" { options.CFlags = strings.Split(*cFlags, " ") } if *ldFlags != "" { options.LDFlags = strings.Split(*ldFlags, " ") } if *panicStrategy != "print" && *panicStrategy != "trap" { fmt.Fprintln(os.Stderr, "Panic strategy must be either print or trap.") usage() os.Exit(1) } var err error if options.HeapSize, err = parseSize(*heapSize); err != nil { fmt.Fprintln(os.Stderr, "Could not read heap size:", *heapSize) usage() os.Exit(1) } os.Setenv("CC", "clang -target="+*target) 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 = 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-builtins": // 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).") } err := builder.CompileBuiltins(*target, func(path string) error { return moveFile(path, *outpath) }) handleCompilerError(err) case "flash", "gdb": if *outpath != "" { fmt.Fprintln(os.Stderr, "Output cannot be specified with the flash command.") usage() os.Exit(1) } if command == "flash" { err := Flash(flag.Arg(0), *port, options) handleCompilerError(err) } else { if !options.Debug { fmt.Fprintln(os.Stderr, "Debug disabled while running gdb?") usage() os.Exit(1) } err := FlashGDB(flag.Arg(0), *port, *ocdOutput, options) handleCompilerError(err) } case "run": if flag.NArg() != 1 { fmt.Fprintln(os.Stderr, "No package specified.") usage() os.Exit(1) } err := Run(flag.Arg(0), options) handleCompilerError(err) case "test": pkgName := "." if flag.NArg() == 1 { pkgName = 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) handleCompilerError(err) 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) } 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()) 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 := "" if s, err := builder.GorootVersionString(goenv.Get("GOROOT")); err == nil { goversion = s } fmt.Printf("tinygo version %s %s/%s (using go version %s)\n", version, runtime.GOOS, runtime.GOARCH, goversion) 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) } }