diff --git a/internal/build/build.go b/internal/build/build.go index fabb54e1..073ac45a 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -18,7 +18,6 @@ package build import ( "bytes" - "debug/macho" "errors" "fmt" "go/ast" @@ -315,24 +314,51 @@ func Do(args []string, conf *Config) ([]Package, error) { for _, pkg := range initial { if needLink(pkg, mode) { - app, err := linkMainPkg(ctx, pkg, allPkgs, global, conf, mode, verbose) + name := path.Base(pkg.PkgPath) + binFmt := ctx.crossCompile.BinaryFormat + + outputCfg, err := genOutputs(conf, name, len(ctx.initial) > 1, ctx.crossCompile.Emulator, binFmt) if err != nil { return nil, err } - if slices.Contains([]Mode{ModeRun, ModeCmpTest, ModeTest, ModeInstall}, mode) { - // Flash to device if needed (for embedded targets) - if !conf.Emulator && conf.Target != "" { - err = flash(ctx, app, verbose) - if err != nil { - return nil, err - } - } else if mode != ModeInstall { - err = run(ctx, app, pkg.PkgPath, pkg.Dir, conf, mode, verbose) + err = linkMainPkg(ctx, pkg, allPkgs, global, outputCfg.IntPath, verbose) + if err != nil { + return nil, err + } + + finalApp := outputCfg.IntPath + if outputCfg.NeedFwGen || outputCfg.FileFmt != "" { + finalApp, err = convertFormat(ctx, finalApp, &outputCfg) + if err != nil { + return nil, err + } + } + + switch mode { + case ModeBuild: + // Do nothing + + case ModeInstall: + // Native already installed in linkMainPkg + if conf.Target != "" { + err = flash(ctx, finalApp, verbose) if err != nil { return nil, err } } + + case ModeRun, ModeTest, ModeCmpTest: + if conf.Target == "" { + err = runNative(ctx, finalApp, pkg.Dir, pkg.PkgPath, conf, mode) + } else if conf.Emulator { + err = runInEmulator(ctx.crossCompile.Emulator, finalApp, pkg.Dir, pkg.PkgPath, conf, mode, verbose) + } else { + err = flash(ctx, finalApp, verbose) + } + if err != nil { + return nil, err + } } } } @@ -648,19 +674,7 @@ func compileExtraFiles(ctx *context, verbose bool) ([]string, error) { return objFiles, nil } -func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, global llssa.Package, conf *Config, mode Mode, verbose bool) (string, error) { - pkgPath := pkg.PkgPath - name := path.Base(pkgPath) - binFmt := ctx.crossCompile.BinaryFormat - - // Generate output configuration using the centralized function - outputCfg, err := GenOutputs(conf, name, len(ctx.initial) > 1, ctx.crossCompile.Emulator, binFmt) - if err != nil { - return "", err - } - - app := outputCfg.OutPath - orgApp := outputCfg.IntPath +func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, global llssa.Package, outputPath string, verbose bool) error { needRuntime := false needPyInit := false @@ -689,7 +703,7 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, global l }) entryObjFile, err := genMainModuleFile(ctx, llssa.PkgRuntime, pkg, needRuntime, needPyInit) if err != nil { - return "", err + return err } // defer os.Remove(entryLLFile) objFiles = append(objFiles, entryObjFile) @@ -697,14 +711,14 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, global l // Compile extra files from target configuration extraObjFiles, err := compileExtraFiles(ctx, verbose) if err != nil { - return "", err + return err } objFiles = append(objFiles, extraObjFiles...) if global != nil { export, err := exportObject(ctx, pkg.PkgPath+".global", pkg.ExportFile+"-global", []byte(global.String())) if err != nil { - return "", err + return err } objFiles = append(objFiles, export) } @@ -726,28 +740,24 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, global l } } - err = linkObjFiles(ctx, orgApp, objFiles, linkArgs, verbose) - if err != nil { - return "", err + return linkObjFiles(ctx, outputPath, objFiles, linkArgs, verbose) +} + +func convertFormat(ctx *context, inputFile string, outputCfg *OutputCfg) (string, error) { + app := outputCfg.OutPath + currentApp := inputFile + if ctx.buildConf.Verbose { + fmt.Fprintf(os.Stderr, "Converting output format: %s -> %s\n", currentApp, app) } - // Handle firmware conversion and file format conversion - currentApp := orgApp - - // Step 1: Firmware conversion if needed if outputCfg.NeedFwGen { if outputCfg.DirectGen { - // Direct conversion to final output (including .img case) - if verbose { - fmt.Fprintf(os.Stderr, "Converting to firmware format: %s (%s -> %s)\n", outputCfg.BinFmt, currentApp, app) - } - err = firmware.MakeFirmwareImage(currentApp, app, outputCfg.BinFmt, ctx.crossCompile.FormatDetail) + err := firmware.MakeFirmwareImage(currentApp, app, outputCfg.BinFmt, ctx.crossCompile.FormatDetail) if err != nil { return "", err } currentApp = app } else { - // Convert to intermediate file first binExt := firmware.BinaryExt(ctx.crossCompile.BinaryFormat) tmpFile, err := os.CreateTemp("", "llgo-*"+binExt) if err != nil { @@ -756,16 +766,12 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, global l tmpFile.Close() intermediateApp := tmpFile.Name() - if verbose { - fmt.Fprintf(os.Stderr, "Converting to firmware format: %s (%s -> %s)\n", ctx.crossCompile.BinaryFormat, currentApp, intermediateApp) - } err = firmware.MakeFirmwareImage(currentApp, intermediateApp, ctx.crossCompile.BinaryFormat, ctx.crossCompile.FormatDetail) if err != nil { return "", err } currentApp = intermediateApp defer func() { - // Only remove if the intermediate file still exists (wasn't moved) if _, err := os.Stat(intermediateApp); err == nil { os.Remove(intermediateApp) } @@ -773,23 +779,15 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, global l } } - // Step 2: File format conversion if needed if currentApp != app { if outputCfg.FileFmt != "" { - // File format conversion - if verbose { - fmt.Fprintf(os.Stderr, "Converting to file format: %s (%s -> %s)\n", outputCfg.FileFmt, currentApp, app) - } - err = firmware.ConvertOutput(currentApp, app, binFmt, outputCfg.FileFmt) + binFmt := ctx.crossCompile.BinaryFormat + err := firmware.ConvertOutput(currentApp, app, binFmt, outputCfg.FileFmt) if err != nil { return "", err } } else { - // Just move/copy the file - if verbose { - fmt.Fprintf(os.Stderr, "Moving file: %s -> %s\n", currentApp, app) - } - err = os.Rename(currentApp, app) + err := os.Rename(currentApp, app) if err != nil { return "", err } @@ -799,87 +797,6 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, global l return app, nil } -func run(ctx *context, app string, pkgPath, pkgDir string, conf *Config, mode Mode, verbose bool) error { - useEmulator := false - if mode == ModeRun && conf.Emulator { - if ctx.crossCompile.Emulator == "" { - return fmt.Errorf("target %s does not have emulator configured", conf.Target) - } - useEmulator = true - } - - switch mode { - case ModeTest: - cmd := exec.Command(app, conf.RunArgs...) - cmd.Dir = pkgDir - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - if err := cmd.Run(); err != nil { - if exitErr, ok := err.(*exec.ExitError); ok { - // The command ran and exited with a non-zero status. - fmt.Fprintf(os.Stderr, "%s: exit code %d\n", app, exitErr.ExitCode()) - if !ctx.testFail { - ctx.testFail = true - } - } else { - // The command failed to start or other error. - fmt.Fprintf(os.Stderr, "failed to run test %s: %v\n", app, err) - if !ctx.testFail { - ctx.testFail = true - } - } - } - case ModeRun: - if useEmulator { - if verbose { - fmt.Fprintf(os.Stderr, "Using emulator: %s\n", ctx.crossCompile.Emulator) - } - err := runInEmulator(app, ctx.crossCompile.Emulator, conf.RunArgs, verbose) - if err != nil { - return err - } - } else { - args := make([]string, 0, len(conf.RunArgs)+1) - copy(args, conf.RunArgs) - if isWasmTarget(conf.Goos) { - wasmer := os.ExpandEnv(WasmRuntime()) - wasmerArgs := strings.Split(wasmer, " ") - wasmerCmd := wasmerArgs[0] - wasmerArgs = wasmerArgs[1:] - switch wasmer { - case "wasmtime": - args = append(args, "--wasm", "multi-memory=true", app) - args = append(args, conf.RunArgs...) - case "iwasm": - args = append(args, "--stack-size=819200000", "--heap-size=800000000", app) - args = append(args, conf.RunArgs...) - default: - args = append(args, wasmerArgs...) - args = append(args, app) - args = append(args, conf.RunArgs...) - } - app = wasmerCmd - } else { - args = conf.RunArgs - } - cmd := exec.Command(app, args...) - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - err := cmd.Run() - if err != nil { - return err - } - if s := cmd.ProcessState; s != nil { - mockable.Exit(s.ExitCode()) - } - } - case ModeCmpTest: - cmpTest(pkgDir, pkgPath, app, conf.GenExpect, conf.RunArgs) - } - return nil -} - func flash(ctx *context, app string, verbose bool) error { // TODO: Implement device flashing logic if verbose { @@ -1359,76 +1276,8 @@ func pkgExists(initial []*packages.Package, pkg *packages.Package) bool { return false } -// findDylibDep finds the dylib dependency in the executable. It returns empty -// string if not found. -func findDylibDep(exe, lib string) string { - file, err := macho.Open(exe) - check(err) - defer file.Close() - for _, load := range file.Loads { - if dylib, ok := load.(*macho.Dylib); ok { - if strings.HasPrefix(filepath.Base(dylib.Name), fmt.Sprintf("lib%s.", lib)) { - return dylib.Name - } - } - } - return "" -} - type none struct{} -// runInEmulator runs the application in emulator by formatting the emulator command template -func runInEmulator(appPath, emulatorTemplate string, runArgs []string, verbose bool) error { - // Build environment map for template variable expansion - envs := map[string]string{ - "": appPath, // {} expands to app path - "bin": appPath, - "hex": appPath, - "zip": appPath, - "img": appPath, - "uf2": appPath, - } - - // Expand the emulator command template - emulatorCmd := emulatorTemplate - for placeholder, path := range envs { - var target string - if placeholder == "" { - target = "{}" - } else { - target = "{" + placeholder + "}" - } - emulatorCmd = strings.ReplaceAll(emulatorCmd, target, path) - } - - if verbose { - fmt.Fprintf(os.Stderr, "Running in emulator: %s\n", emulatorCmd) - } - - // Parse command and arguments - cmdParts := strings.Fields(emulatorCmd) - if len(cmdParts) == 0 { - panic(fmt.Errorf("empty emulator command")) - } - - // Add run arguments to the end - cmdParts = append(cmdParts, runArgs...) - - // Execute the emulator command - cmd := exec.Command(cmdParts[0], cmdParts[1:]...) - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - err := cmd.Run() - if err != nil { - return err - } - if s := cmd.ProcessState; s != nil { - mockable.Exit(s.ExitCode()) - } - return nil -} - func check(err error) { if err != nil { panic(err) diff --git a/internal/build/outputs.go b/internal/build/outputs.go index 0fae21b0..3716ac8b 100644 --- a/internal/build/outputs.go +++ b/internal/build/outputs.go @@ -18,8 +18,8 @@ type OutputCfg struct { DirectGen bool // True if can generate firmware directly without intermediate file } -// GenOutputs generates appropriate output paths based on the configuration -func GenOutputs(conf *Config, pkgName string, multiPkg bool, emulator, binFmt string) (OutputCfg, error) { +// genOutputs generates appropriate output paths based on the configuration +func genOutputs(conf *Config, pkgName string, multiPkg bool, emulator, binFmt string) (OutputCfg, error) { var cfg OutputCfg // Calculate binary extension and set up format info diff --git a/internal/build/outputs_test.go b/internal/build/outputs_test.go index c677c8ee..725c19a5 100644 --- a/internal/build/outputs_test.go +++ b/internal/build/outputs_test.go @@ -4,7 +4,6 @@ package build import ( - "strings" "testing" ) @@ -246,12 +245,7 @@ func TestGenOutputs(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // Determine input binFmt - remove -img suffix if present as it will be added by the code - inputBinFmt := tt.wantBinFmt - if strings.HasSuffix(inputBinFmt, "-img") && tt.wantFileFmt == "img" { - inputBinFmt = strings.TrimSuffix(inputBinFmt, "-img") - } - result, err := GenOutputs(tt.conf, tt.pkgName, tt.multiPkg, tt.emulator, inputBinFmt) + result, err := genOutputs(tt.conf, tt.pkgName, tt.multiPkg, tt.crossCompile) if err != nil { t.Fatalf("GenOutputs() error = %v", err) } diff --git a/internal/build/run.go b/internal/build/run.go new file mode 100644 index 00000000..fc7d392d --- /dev/null +++ b/internal/build/run.go @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2024 The GoPlus Authors (goplus.org). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package build + +import ( + "fmt" + "os" + "os/exec" + "strings" + + "github.com/goplus/llgo/internal/mockable" +) + +func runNative(ctx *context, app, pkgDir, pkgName string, conf *Config, mode Mode) error { + switch mode { + case ModeRun: + args := make([]string, 0, len(conf.RunArgs)+1) + if isWasmTarget(conf.Goos) { + wasmer := os.ExpandEnv(WasmRuntime()) + wasmerArgs := strings.Split(wasmer, " ") + wasmerCmd := wasmerArgs[0] + wasmerArgs = wasmerArgs[1:] + switch wasmer { + case "wasmtime": + args = append(args, "--wasm", "multi-memory=true", app) + args = append(args, conf.RunArgs...) + case "iwasm": + args = append(args, "--stack-size=819200000", "--heap-size=800000000", app) + args = append(args, conf.RunArgs...) + default: + args = append(args, wasmerArgs...) + args = append(args, app) + args = append(args, conf.RunArgs...) + } + app = wasmerCmd + } else { + args = conf.RunArgs + } + cmd := exec.Command(app, args...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err := cmd.Run() + if err != nil { + return err + } + if s := cmd.ProcessState; s != nil { + mockable.Exit(s.ExitCode()) + } + case ModeTest: + cmd := exec.Command(app, conf.RunArgs...) + cmd.Dir = pkgDir + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + if exitErr, ok := err.(*exec.ExitError); ok { + fmt.Fprintf(os.Stderr, "%s: exit code %d\n", app, exitErr.ExitCode()) + if !ctx.testFail { + ctx.testFail = true + } + } else { + fmt.Fprintf(os.Stderr, "failed to run test %s: %v\n", app, err) + if !ctx.testFail { + ctx.testFail = true + } + } + } + case ModeCmpTest: + cmpTest(pkgDir, pkgName, app, conf.GenExpect, conf.RunArgs) + } + return nil +} + +func runInEmulator(emulator, app, pkgDir, pkgName string, conf *Config, mode Mode, verbose bool) error { + if emulator == "" { + return fmt.Errorf("target %s does not have emulator configured", conf.Target) + } + if verbose { + fmt.Fprintf(os.Stderr, "Using emulator: %s\n", emulator) + } + + switch mode { + case ModeRun: + return runEmuCmd(app, emulator, conf.RunArgs, verbose) + case ModeTest: + return runEmuCmd(app, emulator, conf.RunArgs, verbose) + case ModeCmpTest: + cmpTest(pkgDir, pkgName, app, conf.GenExpect, conf.RunArgs) + return nil + } + return nil +} + +// runEmuCmd runs the application in emulator by formatting the emulator command template +func runEmuCmd(appPath, emulatorTemplate string, runArgs []string, verbose bool) error { + // Build environment map for template variable expansion + envs := map[string]string{ + "": appPath, // {} expands to app path + "bin": appPath, + "hex": appPath, + "zip": appPath, + "img": appPath, + "uf2": appPath, + } + + // Expand the emulator command template + emulatorCmd := emulatorTemplate + for placeholder, path := range envs { + var target string + if placeholder == "" { + target = "{}" + } else { + target = "{" + placeholder + "}" + } + emulatorCmd = strings.ReplaceAll(emulatorCmd, target, path) + } + + if verbose { + fmt.Fprintf(os.Stderr, "Running in emulator: %s\n", emulatorCmd) + } + + // Parse command and arguments + cmdParts := strings.Fields(emulatorCmd) + if len(cmdParts) == 0 { + panic(fmt.Errorf("empty emulator command")) + } + + // Add run arguments to the end + cmdParts = append(cmdParts, runArgs...) + + // Execute the emulator command + cmd := exec.Command(cmdParts[0], cmdParts[1:]...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err := cmd.Run() + if err != nil { + return err + } + if s := cmd.ProcessState; s != nil { + mockable.Exit(s.ExitCode()) + } + return nil +}