diff options
author | Ayke van Laethem <[email protected]> | 2022-04-15 17:00:24 +0200 |
---|---|---|
committer | Ron Evans <[email protected]> | 2022-04-16 18:37:03 +0200 |
commit | 85f5411d60313e00df2f12f0c9a70471d591dcda (patch) | |
tree | f9348d33b729a919b120fa74df63e953c06d1e2a | |
parent | c5de68622edca9f9c518521321fca0a46af7db95 (diff) | |
download | tinygo-85f5411d60313e00df2f12f0c9a70471d591dcda.tar.gz tinygo-85f5411d60313e00df2f12f0c9a70471d591dcda.zip |
main: unify how a given program runs
Refactor the code that runs a binary. With this change, the slightly
duplicated code between `tinygo run` and `TestBuild` is merged into one.
Apart from deduplication (which doesn't even gain much in terms of lines
removed), it makes it much easier to maintain this code. In particular,
passing command line arguments to programs to run now becomes trivial.
A future change might also merge `buildAndRun` and `runPackageTest`,
which currently have some overlap. In particular, flags like `-test.v`
don't need to be special-cased for wasmtime.
-rw-r--r-- | main.go | 140 | ||||
-rw-r--r-- | main_test.go | 105 |
2 files changed, 119 insertions, 126 deletions
@@ -2,6 +2,7 @@ package main import ( "bytes" + "context" "encoding/json" "errors" "flag" @@ -714,38 +715,121 @@ func Run(pkgName string, options *compileopts.Options) error { return err } - return builder.Build(pkgName, ".elf", config, func(result builder.BuildResult) error { - emulator := config.Emulator() - if len(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 buildAndRun(pkgName, config, os.Stdout, nil, nil, 0) +} + +// 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 evironment 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) error { + // make sure any special vars in the emulator definition are rewritten + emulator := config.Emulator() + + // 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, env []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, } - return nil + } + } else if len(emulator) != 0 && emulator[0] == "wasmtime" { + // Wasmtime needs some special flags to pass environment variables + // and allow reading from the current directory. + args = append(args, "--dir=.") + for _, v := range environmentVars { + args = append(args, "--env", v) + } + args = append(args, cmdArgs...) + } else { + // Pass environment variables and command line parameters as usual. + // This also works on qemu-aarch64 etc. + args = cmdArgs + env = environmentVars + } + + return builder.Build(pkgName, "", config, func(result builder.BuildResult) error { + // 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 len(emulator) == 0 { + name = result.Binary } else { - // Run in an emulator. - args := append(emulator[1:], result.Binary) - cmd := executeCommand(config.Options, 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} + name = emulator[0] + emuArgs := append([]string(nil), emulator[1:]...) + emuArgs = append(emuArgs, result.Binary) + 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 = env + + // Configure stdout/stderr. The stdout may go to a buffer, not a real + // stdout. + cmd.Stdout = stdout + cmd.Stderr = os.Stderr + if len(emulator) != 0 && emulator[0] == "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 := cmd.Run() + if err != nil { + if cerr := ctx.Err(); cerr == context.DeadlineExceeded { + stdout.Write([]byte(fmt.Sprintf("--- timeout of %s exceeded, terminating...\n", timeout))) + err = cerr } - return nil + return &commandError{"failed to run compiled binary", result.Binary, err} } + return nil }) } diff --git a/main_test.go b/main_test.go index b4210fd9b..4b043932c 100644 --- a/main_test.go +++ b/main_test.go @@ -6,7 +6,6 @@ package main import ( "bufio" "bytes" - "context" "errors" "flag" "fmt" @@ -14,7 +13,6 @@ import ( "io/ioutil" "os" "os/exec" - "path/filepath" "regexp" "runtime" "strings" @@ -324,110 +322,21 @@ func runTestWithConfig(name string, t *testing.T, options compileopts.Options, c t.Fatal("could not read expected output file:", err) } - // Create a temporary directory for test output files. - tmpdir := t.TempDir() - - // 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. - spec, err := compileopts.LoadTarget(&options) + config, err := builder.NewConfig(&options) if err != nil { - t.Fatal("failed to load target spec:", err) - } - needsEnvInVars := spec.GOOS == "js" - for _, tag := range spec.BuildTags { - if tag == "baremetal" { - needsEnvInVars = true - } - } - 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. - options.GlobalValues = map[string]map[string]string{ - "runtime": runtimeGlobals, - } - } + t.Fatal(err) } - // Build the test binary. - binary := filepath.Join(tmpdir, "test") - if spec.GOOS == "windows" { - binary += ".exe" - } - err = Build("./"+path, binary, &options) - if err != nil { - printCompilerError(t.Log, err) - t.Fail() - return - } - - // Reserve CPU time for the test to run. - // This attempts to ensure that the test is not CPU-starved. - options.Semaphore <- struct{}{} - defer func() { <-options.Semaphore }() - - // Create the test command, taking care of emulators etc. - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) - defer cancel() - var cmd *exec.Cmd - // make sure any special vars in the emulator definition are rewritten - config := compileopts.Config{Target: spec} emulator := config.Emulator() - if len(emulator) == 0 { - cmd = exec.CommandContext(ctx, binary) - } else { - args := append(emulator[1:], binary) - cmd = exec.CommandContext(ctx, emulator[0], args...) - } - - if len(emulator) != 0 && emulator[0] == "wasmtime" { - // Allow reading from the current directory. - cmd.Args = append(cmd.Args, "--dir=.") - for _, v := range environmentVars { - cmd.Args = append(cmd.Args, "--env", v) - } - cmd.Args = append(cmd.Args, cmdArgs...) - } else { - if !needsEnvInVars { - cmd.Args = append(cmd.Args, cmdArgs...) // works on qemu-aarch64 etc - cmd.Env = append(cmd.Env, environmentVars...) - } - } - - // Run the test. + // Build the test binary. stdout := &bytes.Buffer{} - if len(emulator) != 0 && emulator[0] == "simavr" { - cmd.Stdout = os.Stderr - cmd.Stderr = stdout - } else { - cmd.Stdout = stdout - cmd.Stderr = os.Stderr - } - err = cmd.Start() + err = buildAndRun("./"+path, config, stdout, cmdArgs, environmentVars, time.Minute) if err != nil { - t.Fatal("failed to start:", err) - } - err = cmd.Wait() - - if cerr := ctx.Err(); cerr == context.DeadlineExceeded { - stdout.WriteString("--- test ran too long, terminating...\n") - err = cerr + printCompilerError(t.Log, err) + t.Fail() + return } // putchar() prints CRLF, convert it to LF. |