From e9170e4cef5953ec510b7df950c26c3d8b352c73 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Fri, 5 Sep 2025 13:50:51 +0800 Subject: [PATCH 01/24] feat: add -file-format flag for flexible output formats MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add support for -file-format flag to llgo build command allowing users to specify output format independently of target configuration. Changes: - Add -file-format flag supporting bin, hex, elf, uf2, zip formats - Implement two-stage conversion: firmware format → file format - Add ConvertOutput function with hex format conversion support - Update build logic to handle different modes (build vs run/install) - Add verbose logging for conversion operations For build command: only convert firmware when -file-format is specified For run/install commands: always convert firmware when target requires it 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- cmd/internal/flags/flags.go | 3 ++ internal/build/build.go | 74 +++++++++++++++++++++++++++++++++-- internal/firmware/firmware.go | 72 ++++++++++++++++++++++++++++++++++ 3 files changed, 145 insertions(+), 4 deletions(-) diff --git a/cmd/internal/flags/flags.go b/cmd/internal/flags/flags.go index c61927c3..9a16ffe0 100644 --- a/cmd/internal/flags/flags.go +++ b/cmd/internal/flags/flags.go @@ -8,9 +8,11 @@ import ( ) var OutputFile string +var FileFormat string func AddOutputFlags(fs *flag.FlagSet) { fs.StringVar(&OutputFile, "o", "", "Output file") + fs.StringVar(&FileFormat, "file-format", "", "File format for target output (e.g., bin, hex, elf, uf2, zip)") } var Verbose bool @@ -48,6 +50,7 @@ func UpdateConfig(conf *build.Config) { switch conf.Mode { case build.ModeBuild: conf.OutFile = OutputFile + conf.FileFormat = FileFormat case build.ModeCmpTest: conf.GenExpect = Gen } diff --git a/internal/build/build.go b/internal/build/build.go index f32c5f7e..bb4eda76 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -78,6 +78,7 @@ type Config struct { BinPath string AppExt string // ".exe" on Windows, empty on Unix OutFile string // only valid for ModeBuild when len(pkgs) == 1 + FileFormat string // File format override (e.g., "bin", "hex", "elf", "uf2", "zip") - takes precedence over target's default RunArgs []string // only valid for ModeRun Mode Mode AbiMode AbiMode @@ -685,13 +686,19 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, global l binFmt := ctx.crossCompile.BinaryFormat binExt := firmware.BinaryExt(binFmt) + // Determine final output extension from user-specified file format + outExt := binExt + if conf.FileFormat != "" { + outExt = firmware.GetFileExtFromFormat(conf.FileFormat) + } + // app: converted firmware output file or executable file // orgApp: before converted output file app, orgApp, err := generateOutputFilenames( conf.OutFile, conf.BinPath, conf.AppExt, - binExt, + outExt, name, mode, len(ctx.initial) > 1, @@ -759,10 +766,69 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, global l err = linkObjFiles(ctx, orgApp, objFiles, linkArgs, verbose) check(err) - if orgApp != app { + // Handle firmware conversion and file format conversion + currentApp := orgApp + + // Determine if firmware conversion is needed based on mode + needFirmwareConversion := false + if mode == ModeBuild { + // For build command, do firmware conversion if file-format is specified + needFirmwareConversion = conf.FileFormat != "" + } else { + // For run and install commands, do firmware conversion if binExt is set + needFirmwareConversion = binExt != "" + } + + // Step 1: Firmware conversion if needed + if needFirmwareConversion { + if outExt == binExt { + // Direct conversion to final output + if verbose { + fmt.Fprintf(os.Stderr, "Converting to firmware format: %s (%s -> %s)\n", ctx.crossCompile.BinaryFormat, currentApp, app) + } + err = firmware.MakeFirmwareImage(currentApp, app, ctx.crossCompile.BinaryFormat, ctx.crossCompile.FormatDetail) + check(err) + currentApp = app + } else { + // Convert to intermediate file first + tmpFile, err := os.CreateTemp("", "llgo-*"+binExt) + check(err) + 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) + check(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) + } + }() + } fmt.Printf("cross compile: %#v\n", ctx.crossCompile) - err = firmware.MakeFirmwareImage(orgApp, app, ctx.crossCompile.BinaryFormat, ctx.crossCompile.FormatDetail) - check(err) + } + + // Step 2: File format conversion if needed + if currentApp != app { + if conf.FileFormat != "" { + // File format conversion + if verbose { + fmt.Fprintf(os.Stderr, "Converting to file format: %s (%s -> %s)\n", conf.FileFormat, currentApp, app) + } + err = firmware.ConvertOutput(currentApp, app, binFmt, conf.FileFormat) + check(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) + check(err) + } } switch mode { diff --git a/internal/firmware/firmware.go b/internal/firmware/firmware.go index bbe42e4e..77813637 100644 --- a/internal/firmware/firmware.go +++ b/internal/firmware/firmware.go @@ -2,6 +2,8 @@ package firmware import ( "fmt" + "io" + "os" "strings" ) @@ -17,3 +19,73 @@ func MakeFirmwareImage(infile, outfile, format, fmtDetail string) error { } return fmt.Errorf("unsupported firmware format: %s", format) } + +// GetFileExtFromFormat converts file format to file extension +func GetFileExtFromFormat(format string) string { + switch format { + case "bin": + return ".bin" + case "hex": + return ".hex" + case "elf": + return "" + case "uf2": + return ".uf2" + case "zip": + return ".zip" + default: + return "" + } +} + +// ConvertOutput converts a binary file to the specified format. +// If binaryFormat == fileFormat, no conversion is needed. +// Otherwise, only hex format conversion is supported. +func ConvertOutput(infile, outfile, binaryFormat, fileFormat string) error { + // If formats match, no conversion needed + if binaryFormat == fileFormat { + return nil + } + + // Only support conversion to hex format + if fileFormat == "hex" { + return convertToHex(infile, outfile) + } + + return fmt.Errorf("unsupported format conversion from %s to %s", binaryFormat, fileFormat) +} + +// convertToHex converts binary file to hex format (each byte as two hex characters) +func convertToHex(infile, outfile string) error { + srcFile, err := os.Open(infile) + if err != nil { + return err + } + defer srcFile.Close() + + dstFile, err := os.Create(outfile) + if err != nil { + return err + } + defer dstFile.Close() + + // Read input file and convert each byte to two hex characters + buf := make([]byte, 4096) // Read in chunks + for { + n, err := srcFile.Read(buf) + if n > 0 { + for i := 0; i < n; i++ { + if _, writeErr := fmt.Fprintf(dstFile, "%02x", buf[i]); writeErr != nil { + return writeErr + } + } + } + if err == io.EOF { + break + } + if err != nil { + return err + } + } + return nil +} From df8f67db5a3207a683ca0815bc15cdeaa03807fc Mon Sep 17 00:00:00 2001 From: Li Jie Date: Fri, 5 Sep 2025 13:51:45 +0800 Subject: [PATCH 02/24] remove debug log --- internal/build/build.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/build/build.go b/internal/build/build.go index bb4eda76..d263501f 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -809,7 +809,6 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, global l } }() } - fmt.Printf("cross compile: %#v\n", ctx.crossCompile) } // Step 2: File format conversion if needed From 5e5d5c2a83ddea3d496ecfe9dd7b2e87fc916c85 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Fri, 5 Sep 2025 17:47:34 +0800 Subject: [PATCH 03/24] Build and run for embeded --- cmd/internal/flags/flags.go | 7 + cmd/internal/run/run.go | 1 + doc/Embedded_Cmd.md | 52 +++ internal/build/build.go | 227 +++++++------ internal/build/build_test.go | 171 +--------- internal/build/outputs.go | 192 +++++++++++ internal/build/outputs_test.go | 448 ++++++++++++++++++++++++++ internal/crosscompile/crosscompile.go | 2 + internal/firmware/firmware.go | 16 +- 9 files changed, 828 insertions(+), 288 deletions(-) create mode 100644 doc/Embedded_Cmd.md create mode 100644 internal/build/outputs.go create mode 100644 internal/build/outputs_test.go diff --git a/cmd/internal/flags/flags.go b/cmd/internal/flags/flags.go index 9a16ffe0..c06bb7f1 100644 --- a/cmd/internal/flags/flags.go +++ b/cmd/internal/flags/flags.go @@ -19,6 +19,7 @@ var Verbose bool var BuildEnv string var Tags string var Target string +var Emulator bool var AbiMode int var CheckLinkArgs bool var CheckLLFiles bool @@ -39,6 +40,10 @@ func AddBuildFlags(fs *flag.FlagSet) { var Gen bool +func AddRunFlags(fs *flag.FlagSet) { + fs.BoolVar(&Emulator, "emulator", false, "Run in emulator mode") +} + func AddCmpTestFlags(fs *flag.FlagSet) { fs.BoolVar(&Gen, "gen", false, "Generate llgo.expect file") } @@ -51,6 +56,8 @@ func UpdateConfig(conf *build.Config) { case build.ModeBuild: conf.OutFile = OutputFile conf.FileFormat = FileFormat + case build.ModeRun: + conf.Emulator = Emulator case build.ModeCmpTest: conf.GenExpect = Gen } diff --git a/cmd/internal/run/run.go b/cmd/internal/run/run.go index b6380ab9..b90add5e 100644 --- a/cmd/internal/run/run.go +++ b/cmd/internal/run/run.go @@ -50,6 +50,7 @@ func init() { base.PassBuildFlags(Cmd) flags.AddBuildFlags(&Cmd.Flag) flags.AddBuildFlags(&CmpTestCmd.Flag) + flags.AddRunFlags(&Cmd.Flag) flags.AddCmpTestFlags(&CmpTestCmd.Flag) } diff --git a/doc/Embedded_Cmd.md b/doc/Embedded_Cmd.md new file mode 100644 index 00000000..1909893c --- /dev/null +++ b/doc/Embedded_Cmd.md @@ -0,0 +1,52 @@ +# LLGo Embedded Development Command Line Options + +## Flags + +- `-o ` - Specify output file name +- `-target ` - Specify target platform for cross-compilation +- `-file-format ` - Convert to specified format (**requires `-target`**) + - Supported: `elf` (default), `bin`, `hex`, `uf2`, `zip`, `img` +- `-emulator` - Run using emulator (auto-detects required format) +- `-d ` - Target device for flashing or testing + +## Commands + +### llgo build +Compile program to output file. +- No `-target`: Native executable +- With `-target`: ELF executable (or `-file-format` if specified) + +### llgo run +Compile and run program. +- No `-target`: Run locally +- With `-target`: Run on device or emulator + +### llgo test +Compile and run tests. +- No `-target`: Run tests locally +- With `-target`: Run tests on device or emulator +- Supports `-emulator` and `-d` flags + +### llgo install +Install program or flash to device. +- No `-target`: Install to `$GOPATH/bin` +- With `-target`: Flash to device (use `-d` to specify device) + +## Examples + +```bash +# Native development +llgo build hello.go # -> hello +llgo build -o myapp hello.go # -> myapp +llgo run hello.go # run locally +llgo install hello.go # install to bin + +# Cross-compilation +llgo build -target esp32 hello.go # -> hello (ELF) +llgo build -target esp32 -file-format bin hello.go # -> hello.bin +llgo run -target esp32 hello.go # run on ESP32 +llgo run -target esp32 -emulator hello.go # run in emulator +llgo test -target esp32 -d /dev/ttyUSB0 # run tests on device +llgo test -target esp32 -emulator # run tests in emulator +llgo install -target esp32 -d /dev/ttyUSB0 hello.go # flash to specific device +``` \ No newline at end of file diff --git a/internal/build/build.go b/internal/build/build.go index d263501f..c8a80579 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -79,6 +79,7 @@ type Config struct { AppExt string // ".exe" on Windows, empty on Unix OutFile string // only valid for ModeBuild when len(pkgs) == 1 FileFormat string // File format override (e.g., "bin", "hex", "elf", "uf2", "zip") - takes precedence over target's default + Emulator bool // only valid for ModeRun - run in emulator mode RunArgs []string // only valid for ModeRun Mode Mode AbiMode AbiMode @@ -628,83 +629,18 @@ func compileExtraFiles(ctx *context, verbose bool) ([]string, error) { return objFiles, nil } -// generateOutputFilenames generates the final output filename (app) and intermediate filename (orgApp) -// based on configuration and build context. -func generateOutputFilenames(outFile, binPath, appExt, binExt, pkgName string, mode Mode, isMultiplePkgs bool) (app, orgApp string, err error) { - if outFile == "" { - if mode == ModeBuild && isMultiplePkgs { - // For multiple packages in ModeBuild mode, use temporary file - name := pkgName - if binExt != "" { - name += "*" + binExt - } else { - name += "*" + appExt - } - tmpFile, err := os.CreateTemp("", name) - if err != nil { - return "", "", err - } - app = tmpFile.Name() - tmpFile.Close() - } else { - app = filepath.Join(binPath, pkgName+appExt) - } - orgApp = app - } else { - // outFile is not empty, use it as base part - base := outFile - if binExt != "" { - // If binExt has value, use temporary file as orgApp for firmware conversion - tmpFile, err := os.CreateTemp("", "llgo-*"+appExt) - if err != nil { - return "", "", err - } - orgApp = tmpFile.Name() - tmpFile.Close() - // Check if base already ends with binExt, if so, don't add it again - if strings.HasSuffix(base, binExt) { - app = base - } else { - app = base + binExt - } - } else { - // No binExt, use base + AppExt directly - if filepath.Ext(base) == "" { - app = base + appExt - } else { - app = base - } - orgApp = app - } - } - return app, orgApp, nil -} - func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, global llssa.Package, conf *Config, mode Mode, verbose bool) { pkgPath := pkg.PkgPath name := path.Base(pkgPath) binFmt := ctx.crossCompile.BinaryFormat - binExt := firmware.BinaryExt(binFmt) - // Determine final output extension from user-specified file format - outExt := binExt - if conf.FileFormat != "" { - outExt = firmware.GetFileExtFromFormat(conf.FileFormat) - } - - // app: converted firmware output file or executable file - // orgApp: before converted output file - app, orgApp, err := generateOutputFilenames( - conf.OutFile, - conf.BinPath, - conf.AppExt, - outExt, - name, - mode, - len(ctx.initial) > 1, - ) + // Generate output configuration using the centralized function + outputCfg, err := GenOutputs(conf, name, len(ctx.initial) > 1, ctx.crossCompile.Emulator, binFmt) check(err) + app := outputCfg.OutPath + orgApp := outputCfg.IntPath + needRuntime := false needPyInit := false pkgsMap := make(map[*packages.Package]*aPackage, len(pkgs)) @@ -769,28 +705,27 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, global l // Handle firmware conversion and file format conversion currentApp := orgApp - // Determine if firmware conversion is needed based on mode - needFirmwareConversion := false - if mode == ModeBuild { - // For build command, do firmware conversion if file-format is specified - needFirmwareConversion = conf.FileFormat != "" - } else { - // For run and install commands, do firmware conversion if binExt is set - needFirmwareConversion = binExt != "" + useEmulator := false + if mode == ModeRun && conf.Emulator { + if ctx.crossCompile.Emulator == "" { + panic(fmt.Errorf("target %s does not have emulator configured", conf.Target)) + } + useEmulator = true } // Step 1: Firmware conversion if needed - if needFirmwareConversion { - if outExt == binExt { - // Direct conversion to final output + 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", ctx.crossCompile.BinaryFormat, currentApp, app) + fmt.Fprintf(os.Stderr, "Converting to firmware format: %s (%s -> %s)\n", outputCfg.BinFmt, currentApp, app) } - err = firmware.MakeFirmwareImage(currentApp, app, ctx.crossCompile.BinaryFormat, ctx.crossCompile.FormatDetail) + err = firmware.MakeFirmwareImage(currentApp, app, outputCfg.BinFmt, ctx.crossCompile.FormatDetail) check(err) currentApp = app } else { // Convert to intermediate file first + binExt := firmware.BinaryExt(ctx.crossCompile.BinaryFormat) tmpFile, err := os.CreateTemp("", "llgo-*"+binExt) check(err) tmpFile.Close() @@ -813,12 +748,12 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, global l // Step 2: File format conversion if needed if currentApp != app { - if conf.FileFormat != "" { + if outputCfg.FileFmt != "" { // File format conversion if verbose { - fmt.Fprintf(os.Stderr, "Converting to file format: %s (%s -> %s)\n", conf.FileFormat, currentApp, app) + fmt.Fprintf(os.Stderr, "Converting to file format: %s (%s -> %s)\n", outputCfg.FileFmt, currentApp, app) } - err = firmware.ConvertOutput(currentApp, app, binFmt, conf.FileFormat) + err = firmware.ConvertOutput(currentApp, app, binFmt, outputCfg.FileFmt) check(err) } else { // Just move/copy the file @@ -845,39 +780,46 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, global l } } case ModeRun: - 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...) + if useEmulator { + if verbose { + fmt.Fprintf(os.Stderr, "Using emulator: %s\n", ctx.crossCompile.Emulator) } - app = wasmerCmd + runInEmulator(app, ctx.crossCompile.Emulator, conf.RunArgs, verbose) } 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 { - panic(err) - } - if s := cmd.ProcessState; s != nil { - mockable.Exit(s.ExitCode()) + 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 { + panic(err) + } + if s := cmd.ProcessState; s != nil { + mockable.Exit(s.ExitCode()) + } } case ModeCmpTest: cmpTest(filepath.Dir(pkg.GoFiles[0]), pkgPath, app, conf.GenExpect, conf.RunArgs) @@ -1371,6 +1313,57 @@ func findDylibDep(exe, lib string) string { type none struct{} +// runInEmulator runs the application in emulator by formatting the emulator command template +func runInEmulator(appPath, emulatorTemplate string, runArgs []string, verbose bool) { + // 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 { + panic(err) + } + if s := cmd.ProcessState; s != nil { + mockable.Exit(s.ExitCode()) + } +} + func check(err error) { if err != nil { panic(err) diff --git a/internal/build/build_test.go b/internal/build/build_test.go index b43ff6a4..ead07db0 100644 --- a/internal/build/build_test.go +++ b/internal/build/build_test.go @@ -8,7 +8,6 @@ import ( "fmt" "io" "os" - "strings" "testing" "github.com/goplus/llgo/internal/mockable" @@ -95,172 +94,4 @@ func TestCmpTest(t *testing.T) { mockRun([]string{"../../cl/_testgo/runtest"}, &Config{Mode: ModeCmpTest}) } -func TestGenerateOutputFilenames(t *testing.T) { - tests := []struct { - name string - outFile string - binPath string - appExt string - binExt string - pkgName string - mode Mode - isMultiplePkgs bool - wantAppSuffix string - wantOrgAppDiff bool // true if orgApp should be different from app - wantErr bool - }{ - { - name: "empty outFile, single package", - outFile: "", - binPath: "/usr/local/bin", - appExt: "", - binExt: "", - pkgName: "hello", - mode: ModeBuild, - isMultiplePkgs: false, - wantAppSuffix: "/usr/local/bin/hello", - wantOrgAppDiff: false, - }, - { - name: "empty outFile with appExt", - outFile: "", - binPath: "/usr/local/bin", - appExt: ".exe", - binExt: "", - pkgName: "hello", - mode: ModeBuild, - isMultiplePkgs: false, - wantAppSuffix: "/usr/local/bin/hello.exe", - wantOrgAppDiff: false, - }, - { - name: "outFile without binExt", - outFile: "myapp", - binPath: "/usr/local/bin", - appExt: ".exe", - binExt: "", - pkgName: "hello", - mode: ModeBuild, - isMultiplePkgs: false, - wantAppSuffix: "myapp.exe", - wantOrgAppDiff: false, - }, - { - name: "outFile with existing extension, no binExt", - outFile: "myapp.exe", - binPath: "/usr/local/bin", - appExt: ".exe", - binExt: "", - pkgName: "hello", - mode: ModeBuild, - isMultiplePkgs: false, - wantAppSuffix: "myapp.exe", - wantOrgAppDiff: false, - }, - { - name: "outFile with binExt, different from existing extension", - outFile: "myapp", - binPath: "/usr/local/bin", - appExt: ".exe", - binExt: ".bin", - pkgName: "hello", - mode: ModeBuild, - isMultiplePkgs: false, - wantAppSuffix: "myapp.bin", - wantOrgAppDiff: true, - }, - { - name: "outFile already ends with binExt", - outFile: "t.bin", - binPath: "/usr/local/bin", - appExt: ".exe", - binExt: ".bin", - pkgName: "hello", - mode: ModeBuild, - isMultiplePkgs: false, - wantAppSuffix: "t.bin", - wantOrgAppDiff: true, - }, - { - name: "outFile with full path already ends with binExt", - outFile: "/path/to/t.bin", - binPath: "/usr/local/bin", - appExt: ".exe", - binExt: ".bin", - pkgName: "hello", - mode: ModeBuild, - isMultiplePkgs: false, - wantAppSuffix: "/path/to/t.bin", - wantOrgAppDiff: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - app, orgApp, err := generateOutputFilenames( - tt.outFile, - tt.binPath, - tt.appExt, - tt.binExt, - tt.pkgName, - tt.mode, - tt.isMultiplePkgs, - ) - - if (err != nil) != tt.wantErr { - t.Errorf("generateOutputFilenames() error = %v, wantErr %v", err, tt.wantErr) - return - } - - if tt.wantAppSuffix != "" { - if app != tt.wantAppSuffix { - t.Errorf("generateOutputFilenames() app = %v, want %v", app, tt.wantAppSuffix) - } - } - - if tt.wantOrgAppDiff { - if app == orgApp { - t.Errorf("generateOutputFilenames() orgApp should be different from app, but both are %v", app) - } - // Clean up temp file - if orgApp != "" && strings.Contains(orgApp, "llgo-") { - os.Remove(orgApp) - } - } else { - if app != orgApp { - t.Errorf("generateOutputFilenames() orgApp = %v, want %v (same as app)", orgApp, app) - } - } - }) - } -} - -func TestGenerateOutputFilenames_EdgeCases(t *testing.T) { - // Test case where outFile has same extension as binExt - app, orgApp, err := generateOutputFilenames( - "firmware.bin", - "/usr/local/bin", - ".exe", - ".bin", - "esp32app", - ModeBuild, - false, - ) - - if err != nil { - t.Fatalf("Unexpected error: %v", err) - } - - if app != "firmware.bin" { - t.Errorf("Expected app to be 'firmware.bin', got '%s'", app) - } - - if app == orgApp { - t.Errorf("Expected orgApp to be different from app when binExt is present, but both are '%s'", app) - } - - // Clean up temp file - if orgApp != "" && strings.Contains(orgApp, "llgo-") { - os.Remove(orgApp) - } -} +// TestGenerateOutputFilenames removed - functionality moved to filename_test.go diff --git a/internal/build/outputs.go b/internal/build/outputs.go new file mode 100644 index 00000000..dd67e069 --- /dev/null +++ b/internal/build/outputs.go @@ -0,0 +1,192 @@ +package build + +import ( + "os" + "path/filepath" + + "github.com/goplus/llgo/internal/firmware" +) + +// OutputCfg contains the generated output paths and conversion configuration +type OutputCfg struct { + OutPath string // Final output file path + IntPath string // Intermediate file path (for two-stage conversion) + OutExt string // Output file extension + FileFmt string // File format (from conf.FileFormat or extracted from emulator) + BinFmt string // Binary format for firmware conversion (may have -img suffix) + NeedFwGen bool // Whether firmware image generation is needed + 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) { + var cfg OutputCfg + + // Calculate binary extension and set up format info + binExt := firmware.BinaryExt(binFmt) + cfg.BinFmt = binFmt + + // Determine output format and extension + cfg.FileFmt, cfg.OutExt = determineFormat(conf, emulator) + + // Handle special .img case and set conversion flags + cfg.DirectGen = shouldDirectGen(cfg.OutExt, binExt) + if cfg.OutExt == ".img" { + cfg.BinFmt = binFmt + "-img" + } + + // Determine if firmware generation is needed + cfg.NeedFwGen = needsFwGen(conf, cfg.OutExt, binExt) + + // Generate paths based on mode + switch conf.Mode { + case ModeBuild: + return genBuildOutputs(conf, pkgName, multiPkg, cfg) + case ModeRun, ModeTest, ModeCmpTest: + return genRunOutputs(pkgName, cfg, conf.AppExt) + case ModeInstall: + return genInstallOutputs(conf, pkgName, cfg, binExt) + default: + return cfg, nil + } +} + +// determineFormat determines the file format and extension +func determineFormat(conf *Config, emulator string) (format, ext string) { + if conf.FileFormat != "" { + // User specified file format + return conf.FileFormat, firmware.GetFileExtFromFormat(conf.FileFormat) + } + + if conf.Mode == ModeRun && conf.Emulator && emulator != "" { + // Emulator mode - extract format from emulator command + if emulatorFmt := firmware.ExtractFileFormatFromEmulator(emulator); emulatorFmt != "" { + return emulatorFmt, firmware.GetFileExtFromFormat(emulatorFmt) + } + } + + return "", "" +} + +// shouldDirectGen determines if direct firmware generation is possible +func shouldDirectGen(outExt, binExt string) bool { + return outExt == "" || outExt == binExt || outExt == ".img" +} + +// needsFwGen determines if firmware generation is needed +func needsFwGen(conf *Config, outExt, binExt string) bool { + switch conf.Mode { + case ModeBuild: + return conf.FileFormat != "" + case ModeRun, ModeTest, ModeCmpTest: + if conf.Emulator { + return outExt != "" + } + return binExt != "" + case ModeInstall: + return binExt != "" + default: + return false + } +} + +// genBuildOutputs generates output paths for build mode +func genBuildOutputs(conf *Config, pkgName string, multiPkg bool, cfg OutputCfg) (OutputCfg, error) { + if conf.OutFile == "" && multiPkg { + // Multiple packages, use temp file + return genTempOutputs(pkgName, cfg, conf.AppExt) + } + + // Single package build + baseName := pkgName + if conf.OutFile != "" { + baseName = conf.OutFile + } + + if cfg.OutExt != "" { + // Need format conversion: ELF -> format + if err := setupTwoStageGen(&cfg, baseName, conf.AppExt); err != nil { + return cfg, err + } + } else { + // Direct output + cfg.OutPath = baseName + conf.AppExt + cfg.IntPath = cfg.OutPath + } + + return cfg, nil +} + +// genRunOutputs generates output paths for run mode +func genRunOutputs(pkgName string, cfg OutputCfg, appExt string) (OutputCfg, error) { + // Always use temp files for run mode + return genTempOutputs(pkgName, cfg, appExt) +} + +// genInstallOutputs generates output paths for install mode (flashing to device) +func genInstallOutputs(conf *Config, pkgName string, cfg OutputCfg, binExt string) (OutputCfg, error) { + // Install mode with target means flashing to device, use temp files like run mode + if binExt != "" || cfg.OutExt != "" { + // Flash to device - use temp files for firmware generation + return genTempOutputs(pkgName, cfg, conf.AppExt) + } else { + // Install to BinPath (traditional install without target) + cfg.OutPath = filepath.Join(conf.BinPath, pkgName+conf.AppExt) + cfg.IntPath = cfg.OutPath + } + + return cfg, nil +} + +// setupTwoStageGen sets up paths for two-stage generation +func setupTwoStageGen(cfg *OutputCfg, baseName, appExt string) error { + // Create temp file for intermediate ELF + tmpFile, err := os.CreateTemp("", "llgo-*"+appExt) + if err != nil { + return err + } + tmpFile.Close() + cfg.IntPath = tmpFile.Name() + + // Set final output path + if baseName != "" { + if filepath.Ext(baseName) == cfg.OutExt { + cfg.OutPath = baseName + } else { + cfg.OutPath = baseName + cfg.OutExt + } + } + + return nil +} + +// genTempOutputs creates temporary output file paths +func genTempOutputs(pkgName string, cfg OutputCfg, appExt string) (OutputCfg, error) { + if cfg.OutExt != "" { + // Need format conversion: create temp ELF, then convert to final format + tmpFile, err := os.CreateTemp("", "llgo-*"+appExt) + if err != nil { + return cfg, err + } + tmpFile.Close() + cfg.IntPath = tmpFile.Name() + + finalTmp, err := os.CreateTemp("", pkgName+"-*"+cfg.OutExt) + if err != nil { + return cfg, err + } + finalTmp.Close() + cfg.OutPath = finalTmp.Name() + } else { + // Direct output + tmpFile, err := os.CreateTemp("", pkgName+"-*"+appExt) + if err != nil { + return cfg, err + } + tmpFile.Close() + cfg.OutPath = tmpFile.Name() + cfg.IntPath = cfg.OutPath + } + + return cfg, nil +} diff --git a/internal/build/outputs_test.go b/internal/build/outputs_test.go new file mode 100644 index 00000000..66cb1330 --- /dev/null +++ b/internal/build/outputs_test.go @@ -0,0 +1,448 @@ +//go:build !llgo +// +build !llgo + +package build + +import ( + "strings" + "testing" +) + +func TestGenOutputs(t *testing.T) { + tests := []struct { + name string + conf *Config + pkgName string + multiPkg bool + emulator string + wantOutPath string // use empty string to indicate temp file + wantIntPath string // use empty string to indicate same as outPath + wantOutExt string + wantFileFmt string + wantBinFmt string + wantDirectGen bool + }{ + { + name: "build without target", + conf: &Config{ + Mode: ModeBuild, + BinPath: "/go/bin", + AppExt: "", + }, + pkgName: "hello", + wantOutPath: "hello", + wantOutExt: "", + wantFileFmt: "", + wantBinFmt: "esp32", + wantDirectGen: true, + }, + { + name: "build with -o", + conf: &Config{ + Mode: ModeBuild, + OutFile: "myapp", + AppExt: "", + }, + pkgName: "hello", + wantOutPath: "myapp", + wantOutExt: "", + wantFileFmt: "", + wantBinFmt: "esp32", + wantDirectGen: true, + }, + { + name: "build with target and file-format", + conf: &Config{ + Mode: ModeBuild, + BinPath: "/go/bin", + AppExt: "", + FileFormat: "bin", + Target: "esp32", + }, + pkgName: "hello", + wantOutPath: "hello.bin", + wantOutExt: ".bin", + wantFileFmt: "bin", + wantBinFmt: "esp32", + wantDirectGen: true, + }, + { + name: "build with target, -o and file-format", + conf: &Config{ + Mode: ModeBuild, + OutFile: "myapp", + AppExt: "", + FileFormat: "hex", + Target: "esp32", + }, + pkgName: "hello", + wantOutPath: "myapp.hex", + wantOutExt: ".hex", + wantFileFmt: "hex", + wantBinFmt: "esp32", + wantDirectGen: false, + }, + { + name: "build with target, -o has correct extension and file-format", + conf: &Config{ + Mode: ModeBuild, + OutFile: "myapp.hex", + AppExt: "", + FileFormat: "hex", + Target: "esp32", + }, + pkgName: "hello", + wantOutPath: "myapp.hex", + wantOutExt: ".hex", + wantFileFmt: "hex", + wantBinFmt: "esp32", + wantDirectGen: false, + }, + { + name: "run without target", + conf: &Config{ + Mode: ModeRun, + AppExt: "", + }, + pkgName: "hello", + wantOutPath: "", // temp file + wantOutExt: "", + wantFileFmt: "", + wantBinFmt: "esp32", + wantDirectGen: true, + }, + { + name: "run with target", + conf: &Config{ + Mode: ModeRun, + AppExt: "", + Target: "esp32", + }, + pkgName: "hello", + wantOutPath: "", // temp file + wantOutExt: "", + wantFileFmt: "", + wantBinFmt: "esp32", + wantDirectGen: true, + }, + { + name: "run with target and emulator", + conf: &Config{ + Mode: ModeRun, + AppExt: "", + Target: "esp32", + Emulator: true, + }, + pkgName: "hello", + emulator: "qemu-system-xtensa -machine esp32 -drive file={hex},if=mtd,format=raw", + wantOutPath: "", // temp file + wantOutExt: ".hex", + wantFileFmt: "hex", + wantBinFmt: "esp32", + wantDirectGen: false, + }, + { + name: "build with img file-format", + conf: &Config{ + Mode: ModeBuild, + BinPath: "/go/bin", + AppExt: "", + FileFormat: "img", + Target: "esp32", + }, + pkgName: "hello", + wantOutPath: "hello.img", + wantOutExt: ".img", + wantFileFmt: "img", + wantBinFmt: "esp32-img", + wantDirectGen: true, + }, + { + name: "test without target", + conf: &Config{ + Mode: ModeTest, + AppExt: "", + }, + pkgName: "hello", + wantOutPath: "", // temp file + wantOutExt: "", + wantFileFmt: "", + wantBinFmt: "esp32", + wantDirectGen: true, + }, + { + name: "test with target", + conf: &Config{ + Mode: ModeTest, + AppExt: "", + Target: "esp32", + }, + pkgName: "hello", + wantOutPath: "", // temp file + wantOutExt: "", + wantFileFmt: "", + wantBinFmt: "esp32", + wantDirectGen: true, + }, + { + name: "cmptest without target", + conf: &Config{ + Mode: ModeCmpTest, + AppExt: "", + }, + pkgName: "hello", + wantOutPath: "", // temp file + wantOutExt: "", + wantFileFmt: "", + wantBinFmt: "esp32", + wantDirectGen: true, + }, + { + name: "install without target", + conf: &Config{ + Mode: ModeInstall, + BinPath: "/go/bin", + AppExt: "", + }, + pkgName: "hello", + wantOutPath: "/go/bin/hello", + wantOutExt: "", + wantFileFmt: "", + wantBinFmt: "", + wantDirectGen: true, + }, + { + name: "install with esp32 target (flash to device)", + conf: &Config{ + Mode: ModeInstall, + BinPath: "/go/bin", + AppExt: "", + Target: "esp32", + }, + pkgName: "hello", + wantOutPath: "", // temp file for flashing + wantOutExt: "", + wantFileFmt: "", + wantBinFmt: "esp32", + wantDirectGen: true, + }, + { + name: "install with file format (flash to device)", + conf: &Config{ + Mode: ModeInstall, + BinPath: "/go/bin", + AppExt: "", + FileFormat: "hex", + Target: "esp32", + }, + pkgName: "hello", + wantOutPath: "", // temp file for flashing + wantOutExt: ".hex", + wantFileFmt: "hex", + wantBinFmt: "esp32", + wantDirectGen: false, + }, + } + + 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) + if err != nil { + t.Fatalf("GenOutputs() error = %v", err) + } + + if tt.wantOutExt != result.OutExt { + t.Errorf("GenOutputs() OutExt = %v, want %v", result.OutExt, tt.wantOutExt) + } + + if tt.wantFileFmt != result.FileFmt { + t.Errorf("GenOutputs() FileFmt = %v, want %v", result.FileFmt, tt.wantFileFmt) + } + + if tt.wantBinFmt != result.BinFmt { + t.Errorf("GenOutputs() BinFmt = %v, want %v", result.BinFmt, tt.wantBinFmt) + } + + if tt.wantDirectGen != result.DirectGen { + t.Errorf("GenOutputs() DirectGen = %v, want %v", result.DirectGen, tt.wantDirectGen) + } + + if tt.wantOutPath != "" { + // Check exact match for non-temp files + if result.OutPath != tt.wantOutPath { + t.Errorf("GenOutputs() OutPath = %v, want %v", result.OutPath, tt.wantOutPath) + } + } else { + // Check temp file pattern for temp files + if result.OutPath == "" { + t.Errorf("GenOutputs() OutPath should not be empty for temp file") + } + } + + // Check IntPath logic + if tt.wantIntPath != "" { + // Check exact IntPath match when specified + if result.IntPath != tt.wantIntPath { + t.Errorf("GenOutputs() IntPath = %v, want %v", result.IntPath, tt.wantIntPath) + } + } else if tt.wantOutExt != "" && !tt.wantDirectGen { + // Should have different IntPath for format conversion + if result.IntPath == result.OutPath { + t.Errorf("GenOutputs() IntPath should be different from OutPath when format conversion is needed") + } + } else if tt.conf.Mode == ModeRun && tt.wantOutExt == "" { + // Run mode without conversion should have same IntPath and OutPath + if result.IntPath != result.OutPath { + t.Errorf("GenOutputs() IntPath should equal OutPath for run mode without conversion") + } + } else if tt.conf.Mode == ModeInstall { + // Install mode: check based on whether it's device flashing or traditional install + isDeviceFlash := tt.conf.Target != "" || tt.wantOutExt != "" + if isDeviceFlash { + // Device flashing - should use temp files (like run mode) + if result.OutPath == "" { + // This is expected for temp files, no additional check needed + } + } else { + // Traditional install to BinPath - should have fixed paths + if result.IntPath != result.OutPath { + t.Errorf("GenOutputs() IntPath should equal OutPath for traditional install mode") + } + } + } + }) + } +} + +func TestDetermineFormat(t *testing.T) { + tests := []struct { + name string + conf *Config + emulator string + wantFmt string + wantExt string + }{ + { + name: "user specified format", + conf: &Config{FileFormat: "hex"}, + wantFmt: "hex", + wantExt: ".hex", + }, + { + name: "emulator format extraction", + conf: &Config{Mode: ModeRun, Emulator: true}, + emulator: "qemu-system-xtensa -machine esp32 -drive file={bin},if=mtd,format=raw", + wantFmt: "bin", + wantExt: ".bin", + }, + { + name: "no format", + conf: &Config{}, + wantFmt: "", + wantExt: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotFmt, gotExt := determineFormat(tt.conf, tt.emulator) + if gotFmt != tt.wantFmt { + t.Errorf("determineFormat() format = %v, want %v", gotFmt, tt.wantFmt) + } + if gotExt != tt.wantExt { + t.Errorf("determineFormat() ext = %v, want %v", gotExt, tt.wantExt) + } + }) + } +} + +func TestShouldDirectGen(t *testing.T) { + tests := []struct { + name string + outExt string + binExt string + want bool + }{ + {"no extension", "", ".bin", true}, + {"same extension", ".bin", ".bin", true}, + {"img format", ".img", ".bin", true}, + {"different extension", ".hex", ".bin", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := shouldDirectGen(tt.outExt, tt.binExt); got != tt.want { + t.Errorf("shouldDirectGen() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNeedsFwGen(t *testing.T) { + tests := []struct { + name string + conf *Config + outExt string + binExt string + want bool + }{ + { + name: "build mode with file format", + conf: &Config{Mode: ModeBuild, FileFormat: "hex"}, + outExt: ".hex", + want: true, + }, + { + name: "build mode without file format", + conf: &Config{Mode: ModeBuild}, + outExt: "", + want: false, + }, + { + name: "run mode with emulator", + conf: &Config{Mode: ModeRun, Emulator: true}, + outExt: ".hex", + want: true, + }, + { + name: "run mode with binExt", + conf: &Config{Mode: ModeRun}, + outExt: "", + binExt: ".bin", + want: true, + }, + { + name: "test mode with emulator", + conf: &Config{Mode: ModeTest, Emulator: true}, + outExt: ".hex", + want: true, + }, + { + name: "test mode with binExt", + conf: &Config{Mode: ModeTest}, + outExt: "", + binExt: ".bin", + want: true, + }, + { + name: "cmptest mode with binExt", + conf: &Config{Mode: ModeCmpTest}, + outExt: "", + binExt: ".bin", + want: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := needsFwGen(tt.conf, tt.outExt, tt.binExt); got != tt.want { + t.Errorf("needsFwGen() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/crosscompile/crosscompile.go b/internal/crosscompile/crosscompile.go index c128d8f1..6a4ee4a9 100644 --- a/internal/crosscompile/crosscompile.go +++ b/internal/crosscompile/crosscompile.go @@ -34,6 +34,7 @@ type Export struct { BinaryFormat string // Binary format (e.g., "elf", "esp", "uf2") FormatDetail string // For uf2, it's uf2FamilyID + Emulator string // Emulator command template (e.g., "qemu-system-arm -M {} -kernel {}") } // URLs and configuration that can be overridden for testing @@ -499,6 +500,7 @@ func useTarget(targetName string) (export Export, err error) { export.ExtraFiles = config.ExtraFiles export.BinaryFormat = config.BinaryFormat export.FormatDetail = config.FormatDetail() + export.Emulator = config.Emulator // Build environment map for template variable expansion envs := buildEnvMap(env.LLGoROOT()) diff --git a/internal/firmware/firmware.go b/internal/firmware/firmware.go index 77813637..02a35108 100644 --- a/internal/firmware/firmware.go +++ b/internal/firmware/firmware.go @@ -20,6 +20,18 @@ func MakeFirmwareImage(infile, outfile, format, fmtDetail string) error { return fmt.Errorf("unsupported firmware format: %s", format) } +// ExtractFileFormatFromEmulator extracts file format from emulator command template +// Returns the format if found (e.g. "bin", "hex", "zip", "img"), empty string if not found +func ExtractFileFormatFromEmulator(emulatorCmd string) string { + formats := []string{"bin", "hex", "zip", "img", "uf2"} + for _, format := range formats { + if strings.Contains(emulatorCmd, "{"+format+"}") { + return format + } + } + return "" +} + // GetFileExtFromFormat converts file format to file extension func GetFileExtFromFormat(format string) string { switch format { @@ -33,6 +45,8 @@ func GetFileExtFromFormat(format string) string { return ".uf2" case "zip": return ".zip" + case "img": + return ".img" default: return "" } @@ -47,7 +61,7 @@ func ConvertOutput(infile, outfile, binaryFormat, fileFormat string) error { return nil } - // Only support conversion to hex format + // Only support conversion to hex and format if fileFormat == "hex" { return convertToHex(infile, outfile) } From 1033452e8f19e452b794c42f561535cae2cb6b4a Mon Sep 17 00:00:00 2001 From: Li Jie Date: Fri, 5 Sep 2025 18:39:47 +0800 Subject: [PATCH 04/24] extract run from linkMainPkg, add flash scaffold --- cmd/internal/flags/flags.go | 14 +++- cmd/internal/install/install.go | 1 + cmd/internal/run/run.go | 7 +- cmd/internal/test/test.go | 2 + doc/Embedded_Cmd.md | 16 ++-- internal/build/build.go | 133 ++++++++++++++++++++++++-------- internal/build/outputs.go | 5 +- internal/build/outputs_test.go | 10 +-- 8 files changed, 137 insertions(+), 51 deletions(-) diff --git a/cmd/internal/flags/flags.go b/cmd/internal/flags/flags.go index c06bb7f1..adb7c219 100644 --- a/cmd/internal/flags/flags.go +++ b/cmd/internal/flags/flags.go @@ -20,6 +20,7 @@ var BuildEnv string var Tags string var Target string var Emulator bool +var Port string var AbiMode int var CheckLinkArgs bool var CheckLLFiles bool @@ -40,10 +41,14 @@ func AddBuildFlags(fs *flag.FlagSet) { var Gen bool -func AddRunFlags(fs *flag.FlagSet) { +func AddEmulatorFlags(fs *flag.FlagSet) { fs.BoolVar(&Emulator, "emulator", false, "Run in emulator mode") } +func AddEmbeddedFlags(fs *flag.FlagSet) { + fs.StringVar(&Port, "port", "", "Target port for flashing") +} + func AddCmpTestFlags(fs *flag.FlagSet) { fs.BoolVar(&Gen, "gen", false, "Generate llgo.expect file") } @@ -56,9 +61,14 @@ func UpdateConfig(conf *build.Config) { case build.ModeBuild: conf.OutFile = OutputFile conf.FileFormat = FileFormat - case build.ModeRun: + case build.ModeRun, build.ModeTest: conf.Emulator = Emulator + conf.Port = Port + case build.ModeInstall: + conf.Port = Port case build.ModeCmpTest: + conf.Emulator = Emulator + conf.Port = Port conf.GenExpect = Gen } if buildenv.Dev { diff --git a/cmd/internal/install/install.go b/cmd/internal/install/install.go index 7c50c509..d986853c 100644 --- a/cmd/internal/install/install.go +++ b/cmd/internal/install/install.go @@ -36,6 +36,7 @@ var Cmd = &base.Command{ func init() { Cmd.Run = runCmd flags.AddBuildFlags(&Cmd.Flag) + flags.AddEmbeddedFlags(&Cmd.Flag) } func runCmd(cmd *base.Command, args []string) { diff --git a/cmd/internal/run/run.go b/cmd/internal/run/run.go index b90add5e..b1614715 100644 --- a/cmd/internal/run/run.go +++ b/cmd/internal/run/run.go @@ -49,8 +49,13 @@ func init() { CmpTestCmd.Run = runCmpTest base.PassBuildFlags(Cmd) flags.AddBuildFlags(&Cmd.Flag) + flags.AddEmulatorFlags(&Cmd.Flag) + flags.AddEmbeddedFlags(&Cmd.Flag) // for -target support + + base.PassBuildFlags(CmpTestCmd) flags.AddBuildFlags(&CmpTestCmd.Flag) - flags.AddRunFlags(&Cmd.Flag) + flags.AddEmulatorFlags(&CmpTestCmd.Flag) + flags.AddEmbeddedFlags(&CmpTestCmd.Flag) // for -target support flags.AddCmpTestFlags(&CmpTestCmd.Flag) } diff --git a/cmd/internal/test/test.go b/cmd/internal/test/test.go index b05f1e69..5a259922 100644 --- a/cmd/internal/test/test.go +++ b/cmd/internal/test/test.go @@ -18,6 +18,8 @@ var Cmd = &base.Command{ func init() { Cmd.Run = runCmd flags.AddBuildFlags(&Cmd.Flag) + flags.AddEmulatorFlags(&Cmd.Flag) + flags.AddEmbeddedFlags(&Cmd.Flag) } func runCmd(cmd *base.Command, args []string) { diff --git a/doc/Embedded_Cmd.md b/doc/Embedded_Cmd.md index 1909893c..d4616484 100644 --- a/doc/Embedded_Cmd.md +++ b/doc/Embedded_Cmd.md @@ -7,7 +7,7 @@ - `-file-format ` - Convert to specified format (**requires `-target`**) - Supported: `elf` (default), `bin`, `hex`, `uf2`, `zip`, `img` - `-emulator` - Run using emulator (auto-detects required format) -- `-d ` - Target device for flashing or testing +- `-port ` - Target port for flashing or testing ## Commands @@ -16,7 +16,7 @@ Compile program to output file. - No `-target`: Native executable - With `-target`: ELF executable (or `-file-format` if specified) -### llgo run +### llgo run Compile and run program. - No `-target`: Run locally - With `-target`: Run on device or emulator @@ -25,12 +25,12 @@ Compile and run program. Compile and run tests. - No `-target`: Run tests locally - With `-target`: Run tests on device or emulator -- Supports `-emulator` and `-d` flags +- Supports `-emulator` and `-port` flags ### llgo install Install program or flash to device. - No `-target`: Install to `$GOPATH/bin` -- With `-target`: Flash to device (use `-d` to specify device) +- With `-target`: Flash to device (use `-port` to specify port) ## Examples @@ -46,7 +46,7 @@ llgo build -target esp32 hello.go # -> hello (ELF) llgo build -target esp32 -file-format bin hello.go # -> hello.bin llgo run -target esp32 hello.go # run on ESP32 llgo run -target esp32 -emulator hello.go # run in emulator -llgo test -target esp32 -d /dev/ttyUSB0 # run tests on device -llgo test -target esp32 -emulator # run tests in emulator -llgo install -target esp32 -d /dev/ttyUSB0 hello.go # flash to specific device -``` \ No newline at end of file +llgo test -target esp32 -port /dev/ttyUSB0 . # run tests on device +llgo test -target esp32 -emulator . # run tests in emulator +llgo install -target esp32 -port /dev/ttyUSB0 hello.go # flash to specific port +``` diff --git a/internal/build/build.go b/internal/build/build.go index c8a80579..fabb54e1 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -79,7 +79,8 @@ type Config struct { AppExt string // ".exe" on Windows, empty on Unix OutFile string // only valid for ModeBuild when len(pkgs) == 1 FileFormat string // File format override (e.g., "bin", "hex", "elf", "uf2", "zip") - takes precedence over target's default - Emulator bool // only valid for ModeRun - run in emulator mode + Emulator bool // only valid for ModeRun/ModeTest - run in emulator mode + Port string // only valid for ModeRun/ModeTest/ModeInstall/ModeCmpTest - target port for flashing RunArgs []string // only valid for ModeRun Mode Mode AbiMode AbiMode @@ -314,7 +315,25 @@ func Do(args []string, conf *Config) ([]Package, error) { for _, pkg := range initial { if needLink(pkg, mode) { - linkMainPkg(ctx, pkg, allPkgs, global, conf, mode, verbose) + app, err := linkMainPkg(ctx, pkg, allPkgs, global, conf, mode, verbose) + 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) + if err != nil { + return nil, err + } + } + } } } @@ -629,14 +648,16 @@ 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) { +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) - check(err) + if err != nil { + return "", err + } app := outputCfg.OutPath orgApp := outputCfg.IntPath @@ -667,18 +688,24 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, global l } }) entryObjFile, err := genMainModuleFile(ctx, llssa.PkgRuntime, pkg, needRuntime, needPyInit) - check(err) + if err != nil { + return "", err + } // defer os.Remove(entryLLFile) objFiles = append(objFiles, entryObjFile) // Compile extra files from target configuration extraObjFiles, err := compileExtraFiles(ctx, verbose) - check(err) + if err != nil { + return "", err + } objFiles = append(objFiles, extraObjFiles...) if global != nil { export, err := exportObject(ctx, pkg.PkgPath+".global", pkg.ExportFile+"-global", []byte(global.String())) - check(err) + if err != nil { + return "", err + } objFiles = append(objFiles, export) } @@ -700,19 +727,13 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, global l } err = linkObjFiles(ctx, orgApp, objFiles, linkArgs, verbose) - check(err) + if err != nil { + return "", err + } // Handle firmware conversion and file format conversion currentApp := orgApp - useEmulator := false - if mode == ModeRun && conf.Emulator { - if ctx.crossCompile.Emulator == "" { - panic(fmt.Errorf("target %s does not have emulator configured", conf.Target)) - } - useEmulator = true - } - // Step 1: Firmware conversion if needed if outputCfg.NeedFwGen { if outputCfg.DirectGen { @@ -721,13 +742,17 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, global l 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) - check(err) + 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) - check(err) + if err != nil { + return "", err + } tmpFile.Close() intermediateApp := tmpFile.Name() @@ -735,7 +760,9 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, global l 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) - check(err) + if err != nil { + return "", err + } currentApp = intermediateApp defer func() { // Only remove if the intermediate file still exists (wasn't moved) @@ -754,29 +781,52 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, global l fmt.Fprintf(os.Stderr, "Converting to file format: %s (%s -> %s)\n", outputCfg.FileFmt, currentApp, app) } err = firmware.ConvertOutput(currentApp, app, binFmt, outputCfg.FileFmt) - check(err) + 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) - check(err) + if err != nil { + return "", err + } } } + 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 = pkg.Dir + cmd.Dir = pkgDir cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr - cmd.Run() - if s := cmd.ProcessState; s != nil { - exitCode := s.ExitCode() - fmt.Fprintf(os.Stderr, "%s: exit code %d\n", app, exitCode) - if !ctx.testFail && exitCode != 0 { - ctx.testFail = true + 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: @@ -784,7 +834,10 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, global l if verbose { fmt.Fprintf(os.Stderr, "Using emulator: %s\n", ctx.crossCompile.Emulator) } - runInEmulator(app, ctx.crossCompile.Emulator, conf.RunArgs, verbose) + 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) @@ -813,17 +866,28 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, global l cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr - err = cmd.Run() + err := cmd.Run() if err != nil { - panic(err) + return err } if s := cmd.ProcessState; s != nil { mockable.Exit(s.ExitCode()) } } case ModeCmpTest: - cmpTest(filepath.Dir(pkg.GoFiles[0]), pkgPath, app, conf.GenExpect, conf.RunArgs) + 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 { + fmt.Fprintf(os.Stderr, "Flashing %s to port %s\n", app, ctx.buildConf.Port) + } + fmt.Printf("conf: %#v\n", ctx.buildConf) + fmt.Printf("crosscompile: %#v\n", ctx.crossCompile) + return nil } func linkObjFiles(ctx *context, app string, objFiles, linkArgs []string, verbose bool) error { @@ -1314,7 +1378,7 @@ func findDylibDep(exe, lib string) string { type none struct{} // runInEmulator runs the application in emulator by formatting the emulator command template -func runInEmulator(appPath, emulatorTemplate string, runArgs []string, verbose bool) { +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 @@ -1357,11 +1421,12 @@ func runInEmulator(appPath, emulatorTemplate string, runArgs []string, verbose b cmd.Stderr = os.Stderr err := cmd.Run() if err != nil { - panic(err) + return err } if s := cmd.ProcessState; s != nil { mockable.Exit(s.ExitCode()) } + return nil } func check(err error) { diff --git a/internal/build/outputs.go b/internal/build/outputs.go index dd67e069..0fae21b0 100644 --- a/internal/build/outputs.go +++ b/internal/build/outputs.go @@ -110,7 +110,10 @@ func genBuildOutputs(conf *Config, pkgName string, multiPkg bool, cfg OutputCfg) } } else { // Direct output - cfg.OutPath = baseName + conf.AppExt + cfg.OutPath = baseName + if filepath.Ext(cfg.OutPath) != conf.AppExt { + cfg.OutPath += conf.AppExt + } cfg.IntPath = cfg.OutPath } diff --git a/internal/build/outputs_test.go b/internal/build/outputs_test.go index 66cb1330..c677c8ee 100644 --- a/internal/build/outputs_test.go +++ b/internal/build/outputs_test.go @@ -33,7 +33,7 @@ func TestGenOutputs(t *testing.T) { wantOutPath: "hello", wantOutExt: "", wantFileFmt: "", - wantBinFmt: "esp32", + wantBinFmt: "", wantDirectGen: true, }, { @@ -47,7 +47,7 @@ func TestGenOutputs(t *testing.T) { wantOutPath: "myapp", wantOutExt: "", wantFileFmt: "", - wantBinFmt: "esp32", + wantBinFmt: "", wantDirectGen: true, }, { @@ -108,7 +108,7 @@ func TestGenOutputs(t *testing.T) { wantOutPath: "", // temp file wantOutExt: "", wantFileFmt: "", - wantBinFmt: "esp32", + wantBinFmt: "", wantDirectGen: true, }, { @@ -167,7 +167,7 @@ func TestGenOutputs(t *testing.T) { wantOutPath: "", // temp file wantOutExt: "", wantFileFmt: "", - wantBinFmt: "esp32", + wantBinFmt: "", wantDirectGen: true, }, { @@ -194,7 +194,7 @@ func TestGenOutputs(t *testing.T) { wantOutPath: "", // temp file wantOutExt: "", wantFileFmt: "", - wantBinFmt: "esp32", + wantBinFmt: "", wantDirectGen: true, }, { From da9865104fc44d3ca52bf2beeff1fa5b037b7020 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sat, 6 Sep 2025 16:21:15 +0800 Subject: [PATCH 05/24] export flash, openocd, msd --- doc/Embedded_Cmd.md | 12 ++++---- internal/crosscompile/crosscompile.go | 43 +++++++++++++++++++++++++++ internal/targets/config.go | 8 +++-- internal/targets/loader.go | 6 ++++ 4 files changed, 61 insertions(+), 8 deletions(-) diff --git a/doc/Embedded_Cmd.md b/doc/Embedded_Cmd.md index d4616484..58d9386d 100644 --- a/doc/Embedded_Cmd.md +++ b/doc/Embedded_Cmd.md @@ -42,11 +42,11 @@ llgo run hello.go # run locally llgo install hello.go # install to bin # Cross-compilation -llgo build -target esp32 hello.go # -> hello (ELF) -llgo build -target esp32 -file-format bin hello.go # -> hello.bin -llgo run -target esp32 hello.go # run on ESP32 -llgo run -target esp32 -emulator hello.go # run in emulator -llgo test -target esp32 -port /dev/ttyUSB0 . # run tests on device +llgo build -target esp32 . # -> hello (ELF) +llgo build -target esp32 -file-format bin , # -> hello.bin +llgo run -target esp32 . # run on ESP32 (guess a port) +llgo run -target esp32 -emulator . # run in emulator +llgo test -target esp32 -port /dev/ttyUSB0 . # run tests on device llgo test -target esp32 -emulator . # run tests in emulator -llgo install -target esp32 -port /dev/ttyUSB0 hello.go # flash to specific port +llgo install -target esp32 -port /dev/ttyUSB0 . # flash to specific port ``` diff --git a/internal/crosscompile/crosscompile.go b/internal/crosscompile/crosscompile.go index 6a4ee4a9..6ce60529 100644 --- a/internal/crosscompile/crosscompile.go +++ b/internal/crosscompile/crosscompile.go @@ -16,6 +16,27 @@ import ( "github.com/goplus/llgo/internal/xtool/llvm" ) +// Flash contains configuration for device flashing +type Flash struct { + Command string // Flash command template + Serial string // Serial communication settings + SerialPort []string // Available serial ports + Flash1200BpsReset bool // Whether to use 1200bps reset +} + +// MSD contains configuration for Mass Storage Device flashing +type MSD struct { + VolumeName []string // Names of the volumes + FirmwareName string // Firmware file name pattern +} + +// OpenOCD contains configuration for OpenOCD debugging/flashing +type OpenOCD struct { + Interface string // Interface configuration (e.g., "stlink") + Transport string // Transport protocol (e.g., "swd", "jtag") + Target string // Target configuration (e.g., "stm32f4x") +} + type Export struct { CC string // Compiler to use CCFLAGS []string @@ -35,6 +56,11 @@ type Export struct { BinaryFormat string // Binary format (e.g., "elf", "esp", "uf2") FormatDetail string // For uf2, it's uf2FamilyID Emulator string // Emulator command template (e.g., "qemu-system-arm -M {} -kernel {}") + + // Flashing/Debugging configuration + Flash Flash // Flash configuration for device programming + MSD MSD // Mass Storage Device configuration + OpenOCD OpenOCD // OpenOCD configuration for debugging/flashing } // URLs and configuration that can be overridden for testing @@ -502,6 +528,23 @@ func useTarget(targetName string) (export Export, err error) { export.FormatDetail = config.FormatDetail() export.Emulator = config.Emulator + // Set flashing/debugging configuration + export.Flash = Flash{ + Command: config.FlashCommand, + Serial: config.Serial, + SerialPort: config.SerialPort, + Flash1200BpsReset: config.Flash1200BpsReset == "true", + } + export.MSD = MSD{ + VolumeName: config.MSDVolumeName, + FirmwareName: config.MSDFirmwareName, + } + export.OpenOCD = OpenOCD{ + Interface: config.OpenOCDInterface, + Transport: config.OpenOCDTransport, + Target: config.OpenOCDTarget, + } + // Build environment map for template variable expansion envs := buildEnvMap(env.LLGoROOT()) diff --git a/internal/targets/config.go b/internal/targets/config.go index d75d1f45..1d56e7d6 100644 --- a/internal/targets/config.go +++ b/internal/targets/config.go @@ -35,10 +35,14 @@ type Config struct { UF2FamilyID string `json:"uf2-family-id"` // Flash and deployment configuration - FlashCommand string `json:"flash-command"` - FlashMethod string `json:"flash-method"` + FlashMethod string `json:"flash-method"` // values: command, openocd, msd + FlashCommand string `json:"flash-command"` // used when FlashMethod == "command" Flash1200BpsReset string `json:"flash-1200-bps-reset"` + // Serial configuration + Serial string `json:"serial"` // Serial communication type (e.g., "usb") + SerialPort []string `json:"serial-port"` // Serial port identifiers (e.g., vendor:product IDs) + // Mass storage device configuration MSDVolumeName []string `json:"msd-volume-name"` MSDFirmwareName string `json:"msd-firmware-name"` diff --git a/internal/targets/loader.go b/internal/targets/loader.go index 95d1e73a..5603ddcd 100644 --- a/internal/targets/loader.go +++ b/internal/targets/loader.go @@ -167,6 +167,9 @@ func (l *Loader) mergeConfig(dst, src *Config) { if src.Flash1200BpsReset != "" { dst.Flash1200BpsReset = src.Flash1200BpsReset } + if src.Serial != "" { + dst.Serial = src.Serial + } if src.MSDFirmwareName != "" { dst.MSDFirmwareName = src.MSDFirmwareName } @@ -202,6 +205,9 @@ func (l *Loader) mergeConfig(dst, src *Config) { if len(src.ExtraFiles) > 0 { dst.ExtraFiles = append(dst.ExtraFiles, src.ExtraFiles...) } + if len(src.SerialPort) > 0 { + dst.SerialPort = append(dst.SerialPort, src.SerialPort...) + } if len(src.MSDVolumeName) > 0 { dst.MSDVolumeName = append(dst.MSDVolumeName, src.MSDVolumeName...) } From 7cad146013b8f4d8cddc8465079b835309ef6228 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sat, 6 Sep 2025 17:58:40 +0800 Subject: [PATCH 06/24] refactor build/install/run pipeline --- internal/build/build.go | 257 +++++++-------------------------- internal/build/outputs.go | 4 +- internal/build/outputs_test.go | 8 +- internal/build/run.go | 158 ++++++++++++++++++++ 4 files changed, 214 insertions(+), 213 deletions(-) create mode 100644 internal/build/run.go 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 +} From 549beeb1013fea760b1f15ccae73cb16766fd7ef Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sat, 6 Sep 2025 18:58:43 +0800 Subject: [PATCH 07/24] test output format for all target/emuator/flash-method --- cmd/llgo/xgo_autogen.go | 9 + internal/build/build.go | 3 +- internal/build/outputs.go | 61 +++++- internal/build/outputs_test.go | 301 +++++++++++++++++++++++--- internal/crosscompile/crosscompile.go | 2 + 5 files changed, 335 insertions(+), 41 deletions(-) diff --git a/cmd/llgo/xgo_autogen.go b/cmd/llgo/xgo_autogen.go index 3833800f..1bf5cb7d 100644 --- a/cmd/llgo/xgo_autogen.go +++ b/cmd/llgo/xgo_autogen.go @@ -52,6 +52,7 @@ type Cmd_version struct { xcmd.Command *App } + //line cmd/llgo/main_app.gox:1 func (this *App) MainEntry() { //line cmd/llgo/main_app.gox:1:1 @@ -68,6 +69,7 @@ func (this *App) Main() { _xgo_obj7 := &Cmd_version{App: this} xcmd.Gopt_App_Main(this, _xgo_obj0, _xgo_obj1, _xgo_obj2, _xgo_obj3, _xgo_obj4, _xgo_obj5, _xgo_obj6, _xgo_obj7) } + //line cmd/llgo/build_cmd.gox:20 func (this *Cmd_build) Main(_xgo_arg0 string) { this.Command.Main(_xgo_arg0) @@ -86,6 +88,7 @@ func (this *Cmd_build) Main(_xgo_arg0 string) { func (this *Cmd_build) Classfname() string { return "build" } + //line cmd/llgo/clean_cmd.gox:20 func (this *Cmd_clean) Main(_xgo_arg0 string) { this.Command.Main(_xgo_arg0) @@ -104,6 +107,7 @@ func (this *Cmd_clean) Main(_xgo_arg0 string) { func (this *Cmd_clean) Classfname() string { return "clean" } + //line cmd/llgo/cmptest_cmd.gox:20 func (this *Cmd_cmptest) Main(_xgo_arg0 string) { this.Command.Main(_xgo_arg0) @@ -122,6 +126,7 @@ func (this *Cmd_cmptest) Main(_xgo_arg0 string) { func (this *Cmd_cmptest) Classfname() string { return "cmptest" } + //line cmd/llgo/get_cmd.gox:16 func (this *Cmd_get) Main(_xgo_arg0 string) { this.Command.Main(_xgo_arg0) @@ -138,6 +143,7 @@ func (this *Cmd_get) Main(_xgo_arg0 string) { func (this *Cmd_get) Classfname() string { return "get" } + //line cmd/llgo/install_cmd.gox:20 func (this *Cmd_install) Main(_xgo_arg0 string) { this.Command.Main(_xgo_arg0) @@ -156,6 +162,7 @@ func (this *Cmd_install) Main(_xgo_arg0 string) { func (this *Cmd_install) Classfname() string { return "install" } + //line cmd/llgo/run_cmd.gox:20 func (this *Cmd_run) Main(_xgo_arg0 string) { this.Command.Main(_xgo_arg0) @@ -174,6 +181,7 @@ func (this *Cmd_run) Main(_xgo_arg0 string) { func (this *Cmd_run) Classfname() string { return "run" } + //line cmd/llgo/test_cmd.gox:20 func (this *Cmd_test) Main(_xgo_arg0 string) { this.Command.Main(_xgo_arg0) @@ -192,6 +200,7 @@ func (this *Cmd_test) Main(_xgo_arg0 string) { func (this *Cmd_test) Classfname() string { return "test" } + //line cmd/llgo/version_cmd.gox:22 func (this *Cmd_version) Main(_xgo_arg0 string) { this.Command.Main(_xgo_arg0) diff --git a/internal/build/build.go b/internal/build/build.go index 073ac45a..0d6c99c4 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -315,9 +315,8 @@ func Do(args []string, conf *Config) ([]Package, error) { for _, pkg := range initial { if needLink(pkg, mode) { name := path.Base(pkg.PkgPath) - binFmt := ctx.crossCompile.BinaryFormat - outputCfg, err := genOutputs(conf, name, len(ctx.initial) > 1, ctx.crossCompile.Emulator, binFmt) + outputCfg, err := genOutputs(conf, name, len(ctx.initial) > 1, &ctx.crossCompile) if err != nil { return nil, err } diff --git a/internal/build/outputs.go b/internal/build/outputs.go index 3716ac8b..0eaa2e6f 100644 --- a/internal/build/outputs.go +++ b/internal/build/outputs.go @@ -3,7 +3,9 @@ package build import ( "os" "path/filepath" + "strings" + "github.com/goplus/llgo/internal/crosscompile" "github.com/goplus/llgo/internal/firmware" ) @@ -19,15 +21,16 @@ type OutputCfg struct { } // genOutputs generates appropriate output paths based on the configuration -func genOutputs(conf *Config, pkgName string, multiPkg bool, emulator, binFmt string) (OutputCfg, error) { +func genOutputs(conf *Config, pkgName string, multiPkg bool, crossCompile *crosscompile.Export) (OutputCfg, error) { var cfg OutputCfg // Calculate binary extension and set up format info + binFmt := crossCompile.BinaryFormat binExt := firmware.BinaryExt(binFmt) cfg.BinFmt = binFmt // Determine output format and extension - cfg.FileFmt, cfg.OutExt = determineFormat(conf, emulator) + cfg.FileFmt, cfg.OutExt = determineFormat(conf, crossCompile) // Handle special .img case and set conversion flags cfg.DirectGen = shouldDirectGen(cfg.OutExt, binExt) @@ -52,22 +55,70 @@ func genOutputs(conf *Config, pkgName string, multiPkg bool, emulator, binFmt st } // determineFormat determines the file format and extension -func determineFormat(conf *Config, emulator string) (format, ext string) { +func determineFormat(conf *Config, crossCompile *crosscompile.Export) (format, ext string) { if conf.FileFormat != "" { // User specified file format return conf.FileFormat, firmware.GetFileExtFromFormat(conf.FileFormat) } - if conf.Mode == ModeRun && conf.Emulator && emulator != "" { + if conf.Mode == ModeRun && conf.Emulator && crossCompile.Emulator != "" { // Emulator mode - extract format from emulator command - if emulatorFmt := firmware.ExtractFileFormatFromEmulator(emulator); emulatorFmt != "" { + if emulatorFmt := firmware.ExtractFileFormatFromEmulator(crossCompile.Emulator); emulatorFmt != "" { return emulatorFmt, firmware.GetFileExtFromFormat(emulatorFmt) } } + // Device flashing - determine format based on flash method and target + if conf.Target != "" && (conf.Mode == ModeInstall || conf.Mode == ModeRun || conf.Mode == ModeTest || conf.Mode == ModeCmpTest) { + if flashExt := determineFlashFormat(crossCompile); flashExt != "" { + return flashExt[1:], flashExt // Remove the dot for format, keep for ext + } + } + return "", "" } +// determineFlashFormat determines the required file format for flashing based on flash method +func determineFlashFormat(crossCompile *crosscompile.Export) string { + if crossCompile == nil { + return "" + } + + flashMethod := crossCompile.Flash.Method + switch flashMethod { + case "command", "": + // Extract format from flash command tokens + flashCommand := crossCompile.Flash.Command + switch { + case strings.Contains(flashCommand, "{hex}"): + return ".hex" + case strings.Contains(flashCommand, "{elf}"): + return ".elf" + case strings.Contains(flashCommand, "{bin}"): + return ".bin" + case strings.Contains(flashCommand, "{uf2}"): + return ".uf2" + case strings.Contains(flashCommand, "{zip}"): + return ".zip" + case strings.Contains(flashCommand, "{img}"): + return ".img" + default: + return "" + } + case "msd": + if crossCompile.MSD.FirmwareName == "" { + return "" + } + return filepath.Ext(crossCompile.MSD.FirmwareName) + case "openocd": + return ".hex" + case "bmp": + return ".elf" + default: + return "" + } +} + // shouldDirectGen determines if direct firmware generation is possible func shouldDirectGen(outExt, binExt string) bool { return outExt == "" || outExt == binExt || outExt == ".img" diff --git a/internal/build/outputs_test.go b/internal/build/outputs_test.go index 725c19a5..c37638dd 100644 --- a/internal/build/outputs_test.go +++ b/internal/build/outputs_test.go @@ -5,6 +5,8 @@ package build import ( "testing" + + "github.com/goplus/llgo/internal/crosscompile" ) func TestGenOutputs(t *testing.T) { @@ -13,7 +15,7 @@ func TestGenOutputs(t *testing.T) { conf *Config pkgName string multiPkg bool - emulator string + crossCompile *crosscompile.Export wantOutPath string // use empty string to indicate temp file wantIntPath string // use empty string to indicate same as outPath wantOutExt string @@ -28,7 +30,10 @@ func TestGenOutputs(t *testing.T) { BinPath: "/go/bin", AppExt: "", }, - pkgName: "hello", + pkgName: "hello", + crossCompile: &crosscompile.Export{ + BinaryFormat: "", + }, wantOutPath: "hello", wantOutExt: "", wantFileFmt: "", @@ -42,7 +47,10 @@ func TestGenOutputs(t *testing.T) { OutFile: "myapp", AppExt: "", }, - pkgName: "hello", + pkgName: "hello", + crossCompile: &crosscompile.Export{ + BinaryFormat: "", + }, wantOutPath: "myapp", wantOutExt: "", wantFileFmt: "", @@ -58,7 +66,10 @@ func TestGenOutputs(t *testing.T) { FileFormat: "bin", Target: "esp32", }, - pkgName: "hello", + pkgName: "hello", + crossCompile: &crosscompile.Export{ + BinaryFormat: "esp32", + }, wantOutPath: "hello.bin", wantOutExt: ".bin", wantFileFmt: "bin", @@ -74,7 +85,10 @@ func TestGenOutputs(t *testing.T) { FileFormat: "hex", Target: "esp32", }, - pkgName: "hello", + pkgName: "hello", + crossCompile: &crosscompile.Export{ + BinaryFormat: "esp32", + }, wantOutPath: "myapp.hex", wantOutExt: ".hex", wantFileFmt: "hex", @@ -90,7 +104,10 @@ func TestGenOutputs(t *testing.T) { FileFormat: "hex", Target: "esp32", }, - pkgName: "hello", + pkgName: "hello", + crossCompile: &crosscompile.Export{ + BinaryFormat: "esp32", + }, wantOutPath: "myapp.hex", wantOutExt: ".hex", wantFileFmt: "hex", @@ -103,7 +120,10 @@ func TestGenOutputs(t *testing.T) { Mode: ModeRun, AppExt: "", }, - pkgName: "hello", + pkgName: "hello", + crossCompile: &crosscompile.Export{ + BinaryFormat: "", + }, wantOutPath: "", // temp file wantOutExt: "", wantFileFmt: "", @@ -117,7 +137,10 @@ func TestGenOutputs(t *testing.T) { AppExt: "", Target: "esp32", }, - pkgName: "hello", + pkgName: "hello", + crossCompile: &crosscompile.Export{ + BinaryFormat: "esp32", + }, wantOutPath: "", // temp file wantOutExt: "", wantFileFmt: "", @@ -132,8 +155,11 @@ func TestGenOutputs(t *testing.T) { Target: "esp32", Emulator: true, }, - pkgName: "hello", - emulator: "qemu-system-xtensa -machine esp32 -drive file={hex},if=mtd,format=raw", + pkgName: "hello", + crossCompile: &crosscompile.Export{ + BinaryFormat: "esp32", + Emulator: "qemu-system-xtensa -machine esp32 -drive file={hex},if=mtd,format=raw", + }, wantOutPath: "", // temp file wantOutExt: ".hex", wantFileFmt: "hex", @@ -149,7 +175,10 @@ func TestGenOutputs(t *testing.T) { FileFormat: "img", Target: "esp32", }, - pkgName: "hello", + pkgName: "hello", + crossCompile: &crosscompile.Export{ + BinaryFormat: "esp32", + }, wantOutPath: "hello.img", wantOutExt: ".img", wantFileFmt: "img", @@ -162,7 +191,10 @@ func TestGenOutputs(t *testing.T) { Mode: ModeTest, AppExt: "", }, - pkgName: "hello", + pkgName: "hello", + crossCompile: &crosscompile.Export{ + BinaryFormat: "", + }, wantOutPath: "", // temp file wantOutExt: "", wantFileFmt: "", @@ -176,7 +208,10 @@ func TestGenOutputs(t *testing.T) { AppExt: "", Target: "esp32", }, - pkgName: "hello", + pkgName: "hello", + crossCompile: &crosscompile.Export{ + BinaryFormat: "esp32", + }, wantOutPath: "", // temp file wantOutExt: "", wantFileFmt: "", @@ -189,7 +224,10 @@ func TestGenOutputs(t *testing.T) { Mode: ModeCmpTest, AppExt: "", }, - pkgName: "hello", + pkgName: "hello", + crossCompile: &crosscompile.Export{ + BinaryFormat: "", + }, wantOutPath: "", // temp file wantOutExt: "", wantFileFmt: "", @@ -203,7 +241,10 @@ func TestGenOutputs(t *testing.T) { BinPath: "/go/bin", AppExt: "", }, - pkgName: "hello", + pkgName: "hello", + crossCompile: &crosscompile.Export{ + BinaryFormat: "", + }, wantOutPath: "/go/bin/hello", wantOutExt: "", wantFileFmt: "", @@ -218,7 +259,10 @@ func TestGenOutputs(t *testing.T) { AppExt: "", Target: "esp32", }, - pkgName: "hello", + pkgName: "hello", + crossCompile: &crosscompile.Export{ + BinaryFormat: "esp32", + }, wantOutPath: "", // temp file for flashing wantOutExt: "", wantFileFmt: "", @@ -234,13 +278,138 @@ func TestGenOutputs(t *testing.T) { FileFormat: "hex", Target: "esp32", }, - pkgName: "hello", + pkgName: "hello", + crossCompile: &crosscompile.Export{ + BinaryFormat: "esp32", + }, wantOutPath: "", // temp file for flashing wantOutExt: ".hex", wantFileFmt: "hex", wantBinFmt: "esp32", wantDirectGen: false, }, + { + name: "run with target non-emulator (should use .bin from binary format)", + conf: &Config{ + Mode: ModeRun, + AppExt: "", + Target: "esp32", + }, + pkgName: "hello", + crossCompile: &crosscompile.Export{ + BinaryFormat: "esp32", + }, + wantOutPath: "", // temp file + wantOutExt: "", + wantFileFmt: "", + wantBinFmt: "esp32", + wantDirectGen: true, + }, + { + name: "run with flash method command - extract hex from command", + conf: &Config{ + Mode: ModeRun, + AppExt: "", + Target: "esp32", + }, + pkgName: "hello", + crossCompile: &crosscompile.Export{ + BinaryFormat: "esp32", + Flash: crosscompile.Flash{ + Method: "command", + Command: "esptool.py --chip esp32 write_flash 0x10000 {hex}", + }, + }, + wantOutPath: "", // temp file + wantOutExt: ".hex", + wantFileFmt: "hex", + wantBinFmt: "esp32", + wantDirectGen: false, + }, + { + name: "run with flash method command - extract bin from command", + conf: &Config{ + Mode: ModeRun, + AppExt: "", + Target: "esp32", + }, + pkgName: "hello", + crossCompile: &crosscompile.Export{ + BinaryFormat: "esp32", + Flash: crosscompile.Flash{ + Method: "command", + Command: "esptool.py --chip esp32 write_flash 0x10000 {bin}", + }, + }, + wantOutPath: "", // temp file + wantOutExt: ".bin", + wantFileFmt: "bin", + wantBinFmt: "esp32", + wantDirectGen: true, + }, + { + name: "run with flash method openocd - should use .hex", + conf: &Config{ + Mode: ModeRun, + AppExt: "", + Target: "stm32", + }, + pkgName: "hello", + crossCompile: &crosscompile.Export{ + BinaryFormat: "arm", + Flash: crosscompile.Flash{ + Method: "openocd", + }, + }, + wantOutPath: "", // temp file + wantOutExt: ".hex", + wantFileFmt: "hex", + wantBinFmt: "arm", + wantDirectGen: false, + }, + { + name: "run with flash method msd - extract extension from firmware name", + conf: &Config{ + Mode: ModeRun, + AppExt: "", + Target: "rp2040", + }, + pkgName: "hello", + crossCompile: &crosscompile.Export{ + BinaryFormat: "uf2", + Flash: crosscompile.Flash{ + Method: "msd", + }, + MSD: crosscompile.MSD{ + FirmwareName: "firmware.uf2", + }, + }, + wantOutPath: "", // temp file + wantOutExt: ".uf2", + wantFileFmt: "uf2", + wantBinFmt: "uf2", + wantDirectGen: true, + }, + { + name: "run with flash method bmp - should use .elf", + conf: &Config{ + Mode: ModeRun, + AppExt: "", + Target: "stm32", + }, + pkgName: "hello", + crossCompile: &crosscompile.Export{ + BinaryFormat: "arm", + Flash: crosscompile.Flash{ + Method: "bmp", + }, + }, + wantOutPath: "", // temp file + wantOutExt: ".elf", + wantFileFmt: "elf", + wantBinFmt: "arm", + wantDirectGen: false, + }, } for _, tt := range tests { @@ -315,36 +484,100 @@ func TestGenOutputs(t *testing.T) { func TestDetermineFormat(t *testing.T) { tests := []struct { - name string - conf *Config - emulator string - wantFmt string - wantExt string + name string + conf *Config + crossCompile *crosscompile.Export + wantFmt string + wantExt string }{ { - name: "user specified format", - conf: &Config{FileFormat: "hex"}, + name: "user specified format", + conf: &Config{FileFormat: "hex"}, + crossCompile: &crosscompile.Export{}, + wantFmt: "hex", + wantExt: ".hex", + }, + { + name: "emulator format extraction", + conf: &Config{Mode: ModeRun, Emulator: true}, + crossCompile: &crosscompile.Export{ + Emulator: "qemu-system-xtensa -machine esp32 -drive file={bin},if=mtd,format=raw", + }, + wantFmt: "bin", + wantExt: ".bin", + }, + { + name: "flash method command - extract hex", + conf: &Config{Mode: ModeRun, Target: "esp32"}, + crossCompile: &crosscompile.Export{ + Flash: crosscompile.Flash{ + Method: "command", + Command: "esptool.py --chip esp32 write_flash 0x10000 {hex}", + }, + }, wantFmt: "hex", wantExt: ".hex", }, { - name: "emulator format extraction", - conf: &Config{Mode: ModeRun, Emulator: true}, - emulator: "qemu-system-xtensa -machine esp32 -drive file={bin},if=mtd,format=raw", - wantFmt: "bin", - wantExt: ".bin", + name: "flash method command - extract bin", + conf: &Config{Mode: ModeRun, Target: "esp32"}, + crossCompile: &crosscompile.Export{ + Flash: crosscompile.Flash{ + Method: "command", + Command: "esptool.py --chip esp32 write_flash 0x10000 {bin}", + }, + }, + wantFmt: "bin", + wantExt: ".bin", }, { - name: "no format", - conf: &Config{}, - wantFmt: "", - wantExt: "", + name: "flash method openocd", + conf: &Config{Mode: ModeRun, Target: "stm32"}, + crossCompile: &crosscompile.Export{ + Flash: crosscompile.Flash{ + Method: "openocd", + }, + }, + wantFmt: "hex", + wantExt: ".hex", + }, + { + name: "flash method msd - extract from firmware name", + conf: &Config{Mode: ModeRun, Target: "rp2040"}, + crossCompile: &crosscompile.Export{ + Flash: crosscompile.Flash{ + Method: "msd", + }, + MSD: crosscompile.MSD{ + FirmwareName: "firmware.uf2", + }, + }, + wantFmt: "uf2", + wantExt: ".uf2", + }, + { + name: "flash method bmp", + conf: &Config{Mode: ModeRun, Target: "stm32"}, + crossCompile: &crosscompile.Export{ + Flash: crosscompile.Flash{ + Method: "bmp", + }, + }, + wantFmt: "elf", + wantExt: ".elf", + }, + { + name: "no format", + conf: &Config{}, + crossCompile: &crosscompile.Export{}, + wantFmt: "", + wantExt: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gotFmt, gotExt := determineFormat(tt.conf, tt.emulator) + gotFmt, gotExt := determineFormat(tt.conf, tt.crossCompile) if gotFmt != tt.wantFmt { t.Errorf("determineFormat() format = %v, want %v", gotFmt, tt.wantFmt) } diff --git a/internal/crosscompile/crosscompile.go b/internal/crosscompile/crosscompile.go index 6ce60529..ebbb3e3c 100644 --- a/internal/crosscompile/crosscompile.go +++ b/internal/crosscompile/crosscompile.go @@ -18,6 +18,7 @@ import ( // Flash contains configuration for device flashing type Flash struct { + Method string // Flash method: "command", "openocd", "msd", "bmp" Command string // Flash command template Serial string // Serial communication settings SerialPort []string // Available serial ports @@ -530,6 +531,7 @@ func useTarget(targetName string) (export Export, err error) { // Set flashing/debugging configuration export.Flash = Flash{ + Method: config.FlashMethod, Command: config.FlashCommand, Serial: config.Serial, SerialPort: config.SerialPort, From 1a3bca40bc37b207fcb37ea272c2575a6de94099 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sat, 6 Sep 2025 20:53:28 +0800 Subject: [PATCH 08/24] feat: implement flash functionality --- internal/build/build.go | 15 +-- internal/flash/flash.go | 278 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 281 insertions(+), 12 deletions(-) create mode 100644 internal/flash/flash.go diff --git a/internal/build/build.go b/internal/build/build.go index 0d6c99c4..83775596 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -42,6 +42,7 @@ import ( "github.com/goplus/llgo/internal/crosscompile" "github.com/goplus/llgo/internal/env" "github.com/goplus/llgo/internal/firmware" + "github.com/goplus/llgo/internal/flash" "github.com/goplus/llgo/internal/mockable" "github.com/goplus/llgo/internal/packages" "github.com/goplus/llgo/internal/typepatch" @@ -341,7 +342,7 @@ func Do(args []string, conf *Config) ([]Package, error) { case ModeInstall: // Native already installed in linkMainPkg if conf.Target != "" { - err = flash(ctx, finalApp, verbose) + err = flash.Flash(ctx.crossCompile, finalApp, ctx.buildConf.Port, verbose) if err != nil { return nil, err } @@ -353,7 +354,7 @@ func Do(args []string, conf *Config) ([]Package, error) { } else if conf.Emulator { err = runInEmulator(ctx.crossCompile.Emulator, finalApp, pkg.Dir, pkg.PkgPath, conf, mode, verbose) } else { - err = flash(ctx, finalApp, verbose) + err = flash.Flash(ctx.crossCompile, finalApp, ctx.buildConf.Port, verbose) } if err != nil { return nil, err @@ -796,16 +797,6 @@ func convertFormat(ctx *context, inputFile string, outputCfg *OutputCfg) (string return app, nil } -func flash(ctx *context, app string, verbose bool) error { - // TODO: Implement device flashing logic - if verbose { - fmt.Fprintf(os.Stderr, "Flashing %s to port %s\n", app, ctx.buildConf.Port) - } - fmt.Printf("conf: %#v\n", ctx.buildConf) - fmt.Printf("crosscompile: %#v\n", ctx.crossCompile) - return nil -} - func linkObjFiles(ctx *context, app string, objFiles, linkArgs []string, verbose bool) error { buildArgs := []string{"-o", app} buildArgs = append(buildArgs, linkArgs...) diff --git a/internal/flash/flash.go b/internal/flash/flash.go new file mode 100644 index 00000000..d5fc55ab --- /dev/null +++ b/internal/flash/flash.go @@ -0,0 +1,278 @@ +package flash + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "time" + + "github.com/goplus/llgo/internal/crosscompile" + "github.com/goplus/llgo/internal/env" +) + +// Flash flashes firmware to a device based on the crosscompile configuration +func Flash(crossCompile crosscompile.Export, app string, port string, verbose bool) error { + if verbose { + fmt.Fprintf(os.Stderr, "Flashing %s to port %s\n", app, port) + } + + method := crossCompile.Flash.Method + if method == "" { + method = "command" + } + + if verbose { + fmt.Fprintf(os.Stderr, "Flashing %s using method: %s\n", app, method) + } + + switch method { + case "command": + return flashCommand(crossCompile.Flash, app, port, verbose) + case "openocd": + return flashOpenOCD(crossCompile.OpenOCD, app, verbose) + case "msd": + return flashMSD(crossCompile.MSD, app, verbose) + case "bmp": + return flashBMP(app, verbose) + default: + return fmt.Errorf("unsupported flash method: %s", method) + } +} + +// flashCommand handles command-based flashing +func flashCommand(flash crosscompile.Flash, app string, port string, verbose bool) error { + if flash.Command == "" { + return fmt.Errorf("flash command not specified") + } + + // Build environment map for template variable expansion + envs := buildFlashEnvMap(app, port) + + // Expand template variables in command + expandedCommand := expandEnv(flash.Command, envs) + + if verbose { + fmt.Fprintf(os.Stderr, "Flash command: %s\n", expandedCommand) + } + + // Split command into parts for exec + parts := strings.Fields(expandedCommand) + if len(parts) == 0 { + return fmt.Errorf("empty flash command after expansion") + } + + // Handle 1200bps reset if required + if flash.Flash1200BpsReset && port != "" { + if err := reset1200bps(port, verbose); err != nil { + if verbose { + fmt.Fprintf(os.Stderr, "Warning: 1200bps reset failed: %v\n", err) + } + } + // Wait for bootloader + time.Sleep(2 * time.Second) + } + + // Execute flash command + cmd := exec.Command(parts[0], parts[1:]...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + return cmd.Run() +} + +// flashOpenOCD handles OpenOCD-based flashing +func flashOpenOCD(openocd crosscompile.OpenOCD, app string, verbose bool) error { + if openocd.Interface == "" { + return fmt.Errorf("OpenOCD interface not specified") + } + + args := []string{ + "-f", "interface/" + openocd.Interface + ".cfg", + } + + if openocd.Transport != "" { + args = append(args, "-c", "transport select "+openocd.Transport) + } + + if openocd.Target != "" { + args = append(args, "-f", "target/"+openocd.Target+".cfg") + } + + // Add programming commands + args = append(args, + "-c", "init", + "-c", "reset init", + "-c", fmt.Sprintf("flash write_image erase %s", app), + "-c", "reset", + "-c", "shutdown", + ) + + if verbose { + fmt.Fprintf(os.Stderr, "OpenOCD command: openocd %s\n", strings.Join(args, " ")) + } + + cmd := exec.Command("openocd", args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + return cmd.Run() +} + +// flashMSD handles Mass Storage Device flashing +func flashMSD(msd crosscompile.MSD, app string, verbose bool) error { + if len(msd.VolumeName) == 0 { + return fmt.Errorf("MSD volume names not specified") + } + + if msd.FirmwareName == "" { + return fmt.Errorf("MSD firmware name not specified") + } + + // Find the MSD volume + var mountPoint string + for _, volumeName := range msd.VolumeName { + // Try common mount points on different platforms + candidates := []string{ + filepath.Join("/Volumes", volumeName), // macOS + filepath.Join("/media", os.Getenv("USER"), volumeName), // Linux + filepath.Join("/mnt", volumeName), // Linux alternative + volumeName + ":", // Windows (assuming drive letter) + } + + for _, candidate := range candidates { + if _, err := os.Stat(candidate); err == nil { + mountPoint = candidate + break + } + } + if mountPoint != "" { + break + } + } + + if mountPoint == "" { + return fmt.Errorf("MSD volume not found. Expected volumes: %v", msd.VolumeName) + } + + // Copy firmware to MSD + destPath := filepath.Join(mountPoint, msd.FirmwareName) + + if verbose { + fmt.Fprintf(os.Stderr, "Copying %s to %s\n", app, destPath) + } + + return copyFile(app, destPath) +} + +// flashBMP handles Black Magic Probe flashing +func flashBMP(app string, verbose bool) error { + // BMP typically uses GDB for flashing + args := []string{ + "-ex", "target extended-remote /dev/ttyACM0", // Default BMP port + "-ex", "monitor swdp_scan", + "-ex", "attach 1", + "-ex", "load", + "-ex", "compare-sections", + "-ex", "kill", + "-ex", "quit", + app, + } + + if verbose { + fmt.Fprintf(os.Stderr, "BMP command: arm-none-eabi-gdb %s\n", strings.Join(args, " ")) + } + + cmd := exec.Command("arm-none-eabi-gdb", args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + return cmd.Run() +} + +// buildFlashEnvMap creates environment map for template expansion +func buildFlashEnvMap(app string, port string) map[string]string { + envs := make(map[string]string) + + // Basic paths + envs["root"] = env.LLGoROOT() + envs["tmpDir"] = os.TempDir() + + // Port information + if port != "" { + envs["port"] = port + } + + // File paths based on extension + ext := strings.ToLower(filepath.Ext(app)) + switch ext { + case ".hex": + envs["hex"] = app + case ".bin": + envs["bin"] = app + case ".elf": + envs["elf"] = app + case ".uf2": + envs["uf2"] = app + case ".zip": + envs["zip"] = app + case ".img": + envs["img"] = app + default: + // Default to binary for unknown extensions + envs["bin"] = app + } + + return envs +} + +// expandEnv expands template variables in a string +// Supports variables like {port}, {hex}, {bin}, {root}, {tmpDir}, etc. +func expandEnv(template string, envs map[string]string) string { + if template == "" { + return "" + } + + result := template + + // Replace named variables + for key, value := range envs { + if key != "" { + result = strings.ReplaceAll(result, "{"+key+"}", value) + } + } + + return result +} + +// reset1200bps performs 1200bps reset for Arduino-compatible boards +func reset1200bps(port string, verbose bool) error { + if verbose { + fmt.Fprintf(os.Stderr, "Performing 1200bps reset on %s\n", port) + } + + // This is a simplified implementation + // In practice, this would need platform-specific serial port handling + // For now, just try to touch the port to trigger reset + _, err := os.Stat(port) + return err +} + +// copyFile copies a file from src to dst +func copyFile(src, dst string) error { + sourceFile, err := os.Open(src) + if err != nil { + return err + } + defer sourceFile.Close() + + destFile, err := os.Create(dst) + if err != nil { + return err + } + defer destFile.Close() + + _, err = destFile.ReadFrom(sourceFile) + return err +} From 519faabfe194efbf366f6721bd611d349eec6a44 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sat, 6 Sep 2025 20:54:39 +0800 Subject: [PATCH 09/24] feat: ignore firmware files --- .gitignore | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.gitignore b/.gitignore index 7c0b0d92..c388e11d 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,11 @@ go.work* # GoReleaser .dist/ .sysroot/ + +# Embedded firmware files +*.bin +*.hex +*.elf +*.uf2 +*.img +*.zip From 9a5b231c88ff3d82240b9165154268db86a42ff1 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sat, 6 Sep 2025 21:29:48 +0800 Subject: [PATCH 10/24] feat: llgo monitor -target target -port port --- cmd/internal/flags/flags.go | 10 +- cmd/internal/monitor/monitor.go | 69 ++++++++ cmd/llgo/monitor_cmd.gox | 29 +++ cmd/llgo/xgo_autogen.go | 33 +++- doc/Embedded_Cmd.md | 2 +- go.mod | 9 +- go.sum | 19 ++ internal/monitor/monitor.go | 303 ++++++++++++++++++++++++++++++++ 8 files changed, 464 insertions(+), 10 deletions(-) create mode 100644 cmd/internal/monitor/monitor.go create mode 100644 cmd/llgo/monitor_cmd.gox create mode 100644 internal/monitor/monitor.go diff --git a/cmd/internal/flags/flags.go b/cmd/internal/flags/flags.go index adb7c219..d2a7aadf 100644 --- a/cmd/internal/flags/flags.go +++ b/cmd/internal/flags/flags.go @@ -21,6 +21,7 @@ var Tags string var Target string var Emulator bool var Port string +var BaudRate int var AbiMode int var CheckLinkArgs bool var CheckLLFiles bool @@ -30,7 +31,6 @@ func AddBuildFlags(fs *flag.FlagSet) { fs.BoolVar(&Verbose, "v", false, "Verbose mode") fs.StringVar(&Tags, "tags", "", "Build tags") fs.StringVar(&BuildEnv, "buildenv", "", "Build environment") - fs.StringVar(&Target, "target", "", "Target platform (e.g., rp2040, wasi)") if buildenv.Dev { fs.IntVar(&AbiMode, "abi", 2, "ABI mode (default 2). 0 = none, 1 = cfunc, 2 = allfunc.") fs.BoolVar(&CheckLinkArgs, "check-linkargs", false, "check link args valid") @@ -46,7 +46,9 @@ func AddEmulatorFlags(fs *flag.FlagSet) { } func AddEmbeddedFlags(fs *flag.FlagSet) { + fs.StringVar(&Target, "target", "", "Target platform (e.g., rp2040, wasi)") fs.StringVar(&Port, "port", "", "Target port for flashing") + fs.IntVar(&BaudRate, "baudrate", 115200, "Baudrate for serial communication") } func AddCmpTestFlags(fs *flag.FlagSet) { @@ -57,18 +59,18 @@ func UpdateConfig(conf *build.Config) { conf.Tags = Tags conf.Verbose = Verbose conf.Target = Target + conf.Port = Port + conf.BaudRate = BaudRate switch conf.Mode { case build.ModeBuild: conf.OutFile = OutputFile conf.FileFormat = FileFormat case build.ModeRun, build.ModeTest: conf.Emulator = Emulator - conf.Port = Port case build.ModeInstall: - conf.Port = Port + case build.ModeCmpTest: conf.Emulator = Emulator - conf.Port = Port conf.GenExpect = Gen } if buildenv.Dev { diff --git a/cmd/internal/monitor/monitor.go b/cmd/internal/monitor/monitor.go new file mode 100644 index 00000000..9392d024 --- /dev/null +++ b/cmd/internal/monitor/monitor.go @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2025 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 monitor + +import ( + "fmt" + "os" + + "github.com/goplus/llgo/cmd/internal/base" + "github.com/goplus/llgo/cmd/internal/flags" + "github.com/goplus/llgo/internal/monitor" +) + +// Cmd represents the monitor command. +var Cmd = &base.Command{ + UsageLine: "llgo monitor [flags] [executable]", + Short: "Monitor serial output from device", +} + +func init() { + flags.AddEmbeddedFlags(&Cmd.Flag) + Cmd.Run = runMonitor +} + +func runMonitor(cmd *base.Command, args []string) { + cmd.Flag.Parse(args) + args = cmd.Flag.Args() + + if len(args) > 1 { + fmt.Fprintf(os.Stderr, "llgo monitor: too many arguments\n") + os.Exit(1) + } + + if flags.Port == "" && flags.Target == "" { + fmt.Fprintf(os.Stderr, "llgo monitor: must specify either -port or -target\n") + return + } + + var executable string + if len(args) == 1 { + executable = args[0] + } + + config := monitor.MonitorConfig{ + Port: flags.Port, + Target: flags.Target, + BaudRate: flags.BaudRate, + Executable: executable, + } + + if err := monitor.Monitor(config, true); err != nil { + fmt.Fprintf(os.Stderr, "llgo monitor: %v\n", err) + os.Exit(1) + } +} diff --git a/cmd/llgo/monitor_cmd.gox b/cmd/llgo/monitor_cmd.gox new file mode 100644 index 00000000..f87a4e85 --- /dev/null +++ b/cmd/llgo/monitor_cmd.gox @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2025 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. + */ + +import ( + self "github.com/goplus/llgo/cmd/internal/monitor" +) + +use "monitor [flags] [executable]" + +short "Monitor serial output from device" + +flagOff + +run args => { + self.Cmd.Run self.Cmd, args +} \ No newline at end of file diff --git a/cmd/llgo/xgo_autogen.go b/cmd/llgo/xgo_autogen.go index 1bf5cb7d..c42e19b6 100644 --- a/cmd/llgo/xgo_autogen.go +++ b/cmd/llgo/xgo_autogen.go @@ -8,6 +8,7 @@ import ( "github.com/goplus/llgo/cmd/internal/build" "github.com/goplus/llgo/cmd/internal/clean" "github.com/goplus/llgo/cmd/internal/install" + "github.com/goplus/llgo/cmd/internal/monitor" "github.com/goplus/llgo/cmd/internal/run" "github.com/goplus/llgo/cmd/internal/test" "github.com/goplus/llgo/internal/env" @@ -40,6 +41,10 @@ type Cmd_install struct { type App struct { xcmd.App } +type Cmd_monitor struct { + xcmd.Command + *App +} type Cmd_run struct { xcmd.Command *App @@ -64,10 +69,11 @@ func (this *App) Main() { _xgo_obj2 := &Cmd_cmptest{App: this} _xgo_obj3 := &Cmd_get{App: this} _xgo_obj4 := &Cmd_install{App: this} - _xgo_obj5 := &Cmd_run{App: this} - _xgo_obj6 := &Cmd_test{App: this} - _xgo_obj7 := &Cmd_version{App: this} - xcmd.Gopt_App_Main(this, _xgo_obj0, _xgo_obj1, _xgo_obj2, _xgo_obj3, _xgo_obj4, _xgo_obj5, _xgo_obj6, _xgo_obj7) + _xgo_obj5 := &Cmd_monitor{App: this} + _xgo_obj6 := &Cmd_run{App: this} + _xgo_obj7 := &Cmd_test{App: this} + _xgo_obj8 := &Cmd_version{App: this} + xcmd.Gopt_App_Main(this, _xgo_obj0, _xgo_obj1, _xgo_obj2, _xgo_obj3, _xgo_obj4, _xgo_obj5, _xgo_obj6, _xgo_obj7, _xgo_obj8) } //line cmd/llgo/build_cmd.gox:20 @@ -163,6 +169,25 @@ func (this *Cmd_install) Classfname() string { return "install" } +//line cmd/llgo/monitor_cmd.gox:21 +func (this *Cmd_monitor) Main(_xgo_arg0 string) { + this.Command.Main(_xgo_arg0) +//line cmd/llgo/monitor_cmd.gox:21:1 + this.Use("monitor [flags] [executable]") +//line cmd/llgo/monitor_cmd.gox:23:1 + this.Short("Monitor serial output from device") +//line cmd/llgo/monitor_cmd.gox:25:1 + this.FlagOff() +//line cmd/llgo/monitor_cmd.gox:27:1 + this.Run__1(func(args []string) { +//line cmd/llgo/monitor_cmd.gox:28:1 + monitor.Cmd.Run(monitor.Cmd, args) + }) +} +func (this *Cmd_monitor) Classfname() string { + return "monitor" +} + //line cmd/llgo/run_cmd.gox:20 func (this *Cmd_run) Main(_xgo_arg0 string) { this.Command.Main(_xgo_arg0) diff --git a/doc/Embedded_Cmd.md b/doc/Embedded_Cmd.md index 58d9386d..d689eae7 100644 --- a/doc/Embedded_Cmd.md +++ b/doc/Embedded_Cmd.md @@ -19,7 +19,7 @@ Compile program to output file. ### llgo run Compile and run program. - No `-target`: Run locally -- With `-target`: Run on device or emulator +- With `-target`: Run on device or emulator (equivalent to `install` + `monitor`) ### llgo test Compile and run tests. diff --git a/go.mod b/go.mod index 0324e027..0bd566d3 100644 --- a/go.mod +++ b/go.mod @@ -15,11 +15,18 @@ require ( golang.org/x/tools v0.36.0 ) -require github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1 +require ( + github.com/mattn/go-tty v0.0.7 + github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1 + go.bug.st/serial v1.6.4 +) require ( + github.com/creack/goselect v0.1.2 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect golang.org/x/mod v0.27.0 // indirect golang.org/x/sync v0.16.0 // indirect + golang.org/x/sys v0.35.0 // indirect ) replace github.com/goplus/llgo/runtime => ./runtime diff --git a/go.sum b/go.sum index b5da526b..ab56b0c4 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,7 @@ +github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0= +github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/goplus/cobra v1.9.12 h1:0F9EdEbeGyITGz+mqoHoJ5KpUw97p1CkxV74IexHw5s= @@ -10,13 +14,28 @@ github.com/goplus/llvm v0.8.5 h1:DUnFeYC3Rco622tBEKGg8xkigRAV2fh5ZIfBCt7gOSs= github.com/goplus/llvm v0.8.5/go.mod h1:PeVK8GgzxwAYCiMiUAJb5wJR6xbhj989tu9oulKLLT4= github.com/goplus/mod v0.17.1 h1:ITovxDcc5zbURV/Wrp3/SBsYLgC1KrxY6pq1zMM2V94= github.com/goplus/mod v0.17.1/go.mod h1:iXEszBKqi38BAyQApBPyQeurLHmQN34YMgC2ZNdap50= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-tty v0.0.7 h1:KJ486B6qI8+wBO7kQxYgmmEFDaFEE96JMBQ7h400N8Q= +github.com/mattn/go-tty v0.0.7/go.mod h1:f2i5ZOvXBU/tCABmLmOfzLz9azMo5wdAaElRNnJKr+k= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/qiniu/x v1.15.1 h1:avE+YQaowp8ZExjylOeSM73rUo3MQKBAYVxh4NJ8dY8= github.com/qiniu/x v1.15.1/go.mod h1:AiovSOCaRijaf3fj+0CBOpR1457pn24b0Vdb1JpwhII= github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1 h1:NVK+OqnavpyFmUiKfUMHrpvbCi2VFoWTrcpI7aDaJ2I= github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +go.bug.st/serial v1.6.4 h1:7FmqNPgVp3pu2Jz5PoPtbZ9jJO5gnEnZIvnI1lzve8A= +go.bug.st/serial v1.6.4/go.mod h1:nofMJxTeNVny/m6+KaafC6vJGj3miwQZ6vW4BZUGJPI= golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/monitor/monitor.go b/internal/monitor/monitor.go new file mode 100644 index 00000000..8622b5ae --- /dev/null +++ b/internal/monitor/monitor.go @@ -0,0 +1,303 @@ +package monitor + +import ( + "debug/dwarf" + "debug/elf" + "debug/macho" + "debug/pe" + "errors" + "fmt" + "go/token" + "io" + "os" + "os/signal" + "regexp" + "strconv" + "time" + + "github.com/goplus/llgo/internal/crosscompile" + "github.com/mattn/go-tty" + "go.bug.st/serial" +) + +// MonitorConfig contains configuration for the monitor +type MonitorConfig struct { + Port string // Serial port device + Target string // Target name for crosscompile config + BaudRate int // Baudrate of serial monitor + Executable string // Optional path to executable for debug info + WaitTime int // Wait time for port connection (ms) +} + +// Monitor starts serial monitoring with the given configuration +func Monitor(config MonitorConfig, verbose bool) error { + // Set defaults + if config.BaudRate == 0 { + config.BaudRate = 115200 + } + if config.WaitTime == 0 { + config.WaitTime = 300 + } + + // If target is specified, try to get port from crosscompile config + if config.Target != "" && config.Port == "" { + port, err := getPortFromTarget(config.Target) + if err != nil && verbose { + fmt.Fprintf(os.Stderr, "Warning: could not get port from target: %v\n", err) + } + if port != "" { + config.Port = port + } + } + + if config.Port == "" { + return fmt.Errorf("port not specified and could not determine from target") + } + + if verbose { + fmt.Fprintf(os.Stderr, "Connecting to %s at %d baud\n", config.Port, config.BaudRate) + } + + // Open serial port with retry + var serialConn serial.Port + var err error + for i := 0; i <= config.WaitTime; i++ { + serialConn, err = serial.Open(config.Port, &serial.Mode{BaudRate: config.BaudRate}) + if err != nil { + if i < config.WaitTime { + time.Sleep(10 * time.Millisecond) + continue + } + return fmt.Errorf("failed to open port %s: %w", config.Port, err) + } + break + } + defer serialConn.Close() + + // Open TTY for input + tty, err := tty.Open() + if err != nil { + return fmt.Errorf("failed to open TTY: %w", err) + } + defer tty.Close() + + // Setup signal handling + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt) + defer signal.Stop(sig) + + // Create output writer with optional debug info + var writer *outputWriter + if config.Executable != "" { + writer = newOutputWriter(os.Stdout, config.Executable) + } else { + writer = newOutputWriter(os.Stdout, "") + } + + fmt.Printf("Connected to %s. Press Ctrl-C to exit.\n", config.Port) + + errCh := make(chan error, 2) + + // Goroutine for reading from serial port + go func() { + buf := make([]byte, 100*1024) + for { + n, err := serialConn.Read(buf) + if err != nil { + errCh <- fmt.Errorf("serial read error: %w", err) + return + } + writer.Write(buf[:n]) + } + }() + + // Goroutine for reading from TTY and writing to serial port + go func() { + for { + r, err := tty.ReadRune() + if err != nil { + errCh <- fmt.Errorf("TTY read error: %w", err) + return + } + if r == 0 { + continue + } + serialConn.Write([]byte(string(r))) + } + }() + + // Wait for signal or error + select { + case <-sig: + if verbose { + fmt.Fprintf(os.Stderr, "\nDisconnected from %s\n", config.Port) + } + return nil + case err := <-errCh: + return err + } +} + +// getPortFromTarget tries to get serial port from target configuration +func getPortFromTarget(target string) (string, error) { + export, err := crosscompile.Use("", "", false, target) + if err != nil { + return "", err + } + + // Try to get port from serial port list + if len(export.Flash.SerialPort) > 0 { + return export.Flash.SerialPort[0], nil + } + + return "", fmt.Errorf("no serial port found in target configuration") +} + +var addressMatch = regexp.MustCompile(`^panic: runtime error at 0x([0-9a-f]+): `) + +// Extract the address from the "panic: runtime error at" message. +func extractPanicAddress(line []byte) uint64 { + matches := addressMatch.FindSubmatch(line) + if matches != nil { + address, err := strconv.ParseUint(string(matches[1]), 16, 64) + if err == nil { + return address + } + } + return 0 +} + +// Convert an address in the binary to a source address location. +func addressToLine(executable string, address uint64) (token.Position, error) { + data, err := readDWARF(executable) + if err != nil { + return token.Position{}, err + } + r := data.Reader() + + for { + e, err := r.Next() + if err != nil { + return token.Position{}, err + } + if e == nil { + break + } + switch e.Tag { + case dwarf.TagCompileUnit: + r.SkipChildren() + lr, err := data.LineReader(e) + if err != nil { + return token.Position{}, err + } + var lineEntry = dwarf.LineEntry{ + EndSequence: true, + } + for { + // Read the next .debug_line entry. + prevLineEntry := lineEntry + err := lr.Next(&lineEntry) + if err != nil { + if err == io.EOF { + break + } + return token.Position{}, err + } + + if prevLineEntry.EndSequence && lineEntry.Address == 0 { + // Tombstone value. This symbol has been removed, for + // example by the --gc-sections linker flag. It is still + // here in the debug information because the linker can't + // just remove this reference. + // Read until the next EndSequence so that this sequence is + // skipped. + // For more details, see (among others): + // https://reviews.llvm.org/D84825 + for { + err := lr.Next(&lineEntry) + if err != nil { + return token.Position{}, err + } + if lineEntry.EndSequence { + break + } + } + } + + if !prevLineEntry.EndSequence { + // The chunk describes the code from prevLineEntry to + // lineEntry. + if prevLineEntry.Address <= address && lineEntry.Address > address { + return token.Position{ + Filename: prevLineEntry.File.Name, + Line: prevLineEntry.Line, + Column: prevLineEntry.Column, + }, nil + } + } + } + } + } + + return token.Position{}, nil // location not found +} + +// Read the DWARF debug information from a given file (in various formats). +func readDWARF(executable string) (*dwarf.Data, error) { + f, err := os.Open(executable) + if err != nil { + return nil, err + } + defer f.Close() + + if file, err := elf.NewFile(f); err == nil { + return file.DWARF() + } else if file, err := macho.NewFile(f); err == nil { + return file.DWARF() + } else if file, err := pe.NewFile(f); err == nil { + return file.DWARF() + } else { + return nil, errors.New("unknown binary format") + } +} + +type outputWriter struct { + out io.Writer + executable string + line []byte +} + +// newOutputWriter returns an io.Writer that will intercept panic addresses and +// will try to insert a source location in the output if the source location can +// be found in the executable. +func newOutputWriter(out io.Writer, executable string) *outputWriter { + return &outputWriter{ + out: out, + executable: executable, + } +} + +func (w *outputWriter) Write(p []byte) (n int, err error) { + start := 0 + for i, c := range p { + if c == '\n' { + w.out.Write(p[start : i+1]) + start = i + 1 + if w.executable != "" { + address := extractPanicAddress(w.line) + if address != 0 { + loc, err := addressToLine(w.executable, address) + if err == nil && loc.Filename != "" { + fmt.Printf("[llgo: panic at %s]\n", loc.String()) + } + } + } + w.line = w.line[:0] + } else { + w.line = append(w.line, c) + } + } + w.out.Write(p[start:]) + n = len(p) + return +} From a2c9c7f10a9b9cce1f1dcb70738571e949cacee9 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sat, 6 Sep 2025 21:30:37 +0800 Subject: [PATCH 11/24] feat: llgo run -target target -port port auto start monitor --- doc/Embedded_Cmd.md | 25 +++++++++++++++++++++++-- internal/build/build.go | 23 +++++++++++++++++------ 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/doc/Embedded_Cmd.md b/doc/Embedded_Cmd.md index d689eae7..e708ea27 100644 --- a/doc/Embedded_Cmd.md +++ b/doc/Embedded_Cmd.md @@ -7,7 +7,8 @@ - `-file-format ` - Convert to specified format (**requires `-target`**) - Supported: `elf` (default), `bin`, `hex`, `uf2`, `zip`, `img` - `-emulator` - Run using emulator (auto-detects required format) -- `-port ` - Target port for flashing or testing +- `-port ` - Target port for flashing, testing, or monitoring +- `-baudrate ` - Baudrate for serial communication (default: 115200) ## Commands @@ -32,6 +33,20 @@ Install program or flash to device. - No `-target`: Install to `$GOPATH/bin` - With `-target`: Flash to device (use `-port` to specify port) +### llgo monitor +Monitor serial output from embedded device. +- `-port `: Serial port device (e.g., `/dev/ttyUSB0`, `COM3`) +- `-target `: Auto-detect port from target configuration +- `-baudrate `: Communication speed (default: 115200) +- `[executable]`: Optional ELF file for debug info (panic address resolution) + +Features: +- Real-time bidirectional serial communication +- Automatic port detection from target configuration +- Debug info integration for panic address resolution +- Cross-platform support (Linux, macOS, Windows) +- Graceful exit with Ctrl-C + ## Examples ```bash @@ -43,10 +58,16 @@ llgo install hello.go # install to bin # Cross-compilation llgo build -target esp32 . # -> hello (ELF) -llgo build -target esp32 -file-format bin , # -> hello.bin +llgo build -target esp32 -file-format bin . # -> hello.bin llgo run -target esp32 . # run on ESP32 (guess a port) llgo run -target esp32 -emulator . # run in emulator llgo test -target esp32 -port /dev/ttyUSB0 . # run tests on device llgo test -target esp32 -emulator . # run tests in emulator llgo install -target esp32 -port /dev/ttyUSB0 . # flash to specific port + +# Monitor device output +llgo monitor -port /dev/ttyUSB0 # monitor with specific port +llgo monitor -target esp32 # monitor with auto-detected port +llgo monitor -target esp32 -baudrate 9600 # custom baudrate +llgo monitor -port /dev/ttyUSB0 ./firmware.elf # with debug info for panic resolution ``` diff --git a/internal/build/build.go b/internal/build/build.go index 83775596..aad02f41 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -44,6 +44,7 @@ import ( "github.com/goplus/llgo/internal/firmware" "github.com/goplus/llgo/internal/flash" "github.com/goplus/llgo/internal/mockable" + "github.com/goplus/llgo/internal/monitor" "github.com/goplus/llgo/internal/packages" "github.com/goplus/llgo/internal/typepatch" "github.com/goplus/llgo/ssa/abi" @@ -76,12 +77,13 @@ type Config struct { Goarch string Target string // target name (e.g., "rp2040", "wasi") - takes precedence over Goos/Goarch BinPath string - AppExt string // ".exe" on Windows, empty on Unix - OutFile string // only valid for ModeBuild when len(pkgs) == 1 - FileFormat string // File format override (e.g., "bin", "hex", "elf", "uf2", "zip") - takes precedence over target's default - Emulator bool // only valid for ModeRun/ModeTest - run in emulator mode - Port string // only valid for ModeRun/ModeTest/ModeInstall/ModeCmpTest - target port for flashing - RunArgs []string // only valid for ModeRun + AppExt string // ".exe" on Windows, empty on Unix + OutFile string // only valid for ModeBuild when len(pkgs) == 1 + FileFormat string // File format override (e.g., "bin", "hex", "elf", "uf2", "zip") - takes precedence over target's default + Emulator bool // run in emulator mode + Port string // target port for flashing + BaudRate int // baudrate for serial communication + RunArgs []string Mode Mode AbiMode AbiMode GenExpect bool // only valid for ModeCmpTest @@ -355,6 +357,15 @@ func Do(args []string, conf *Config) ([]Package, error) { err = runInEmulator(ctx.crossCompile.Emulator, finalApp, pkg.Dir, pkg.PkgPath, conf, mode, verbose) } else { err = flash.Flash(ctx.crossCompile, finalApp, ctx.buildConf.Port, verbose) + if err != nil { + return nil, err + } + err = monitor.Monitor(monitor.MonitorConfig{ + Port: ctx.buildConf.Port, + Target: conf.Target, + Executable: finalApp, + BaudRate: conf.BaudRate, + }, verbose) } if err != nil { return nil, err From 88e0844ada71baa22df9f5ed1e0163f7706d34bb Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sat, 6 Sep 2025 21:56:15 +0800 Subject: [PATCH 12/24] ignore codecov from internal/{monitor,flash} --- .github/codecov.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/codecov.yml b/.github/codecov.yml index a1559ecf..778c88a4 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -10,4 +10,6 @@ coverage: - "internal/typepatch" - "internal/github" - "internal/firmware" + - "internal/flash" + - "internal/monitor" - "xtool" From c6676917b308ca23e1154fa1b12bc3a73da471ac Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sat, 6 Sep 2025 22:05:29 +0800 Subject: [PATCH 13/24] refine: check msd paths --- internal/flash/flash.go | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/internal/flash/flash.go b/internal/flash/flash.go index d5fc55ab..c00818db 100644 --- a/internal/flash/flash.go +++ b/internal/flash/flash.go @@ -5,6 +5,7 @@ import ( "os" "os/exec" "path/filepath" + "runtime" "strings" "time" @@ -133,12 +134,29 @@ func flashMSD(msd crosscompile.MSD, app string, verbose bool) error { // Find the MSD volume var mountPoint string for _, volumeName := range msd.VolumeName { - // Try common mount points on different platforms - candidates := []string{ - filepath.Join("/Volumes", volumeName), // macOS - filepath.Join("/media", os.Getenv("USER"), volumeName), // Linux - filepath.Join("/mnt", volumeName), // Linux alternative - volumeName + ":", // Windows (assuming drive letter) + // Try platform-specific mount points + var candidates []string + switch runtime.GOOS { + case "darwin": + candidates = []string{ + filepath.Join("/Volumes", volumeName), + } + case "linux": + candidates = []string{ + filepath.Join("/media", os.Getenv("USER"), volumeName), + filepath.Join("/mnt", volumeName), + } + case "windows": + candidates = []string{ + volumeName + ":", + } + default: + candidates = []string{ + filepath.Join("/Volumes", volumeName), // macOS + filepath.Join("/media", os.Getenv("USER"), volumeName), // Linux + filepath.Join("/mnt", volumeName), // Linux alternative + volumeName + ":", // Windows + } } for _, candidate := range candidates { From 228d7cee31454ccb8351868f3b2fc44c7ae225d4 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sat, 6 Sep 2025 22:14:44 +0800 Subject: [PATCH 14/24] feat: make -port optional --- internal/flash/flash.go | 120 ++++++++++++++++++++++++++++++++++-- internal/monitor/monitor.go | 36 ++--------- 2 files changed, 122 insertions(+), 34 deletions(-) diff --git a/internal/flash/flash.go b/internal/flash/flash.go index c00818db..4f5830f3 100644 --- a/internal/flash/flash.go +++ b/internal/flash/flash.go @@ -1,31 +1,143 @@ package flash import ( + "errors" "fmt" "os" "os/exec" "path/filepath" "runtime" + "strconv" "strings" "time" "github.com/goplus/llgo/internal/crosscompile" "github.com/goplus/llgo/internal/env" + "go.bug.st/serial/enumerator" ) +// From tinygo/main.go getDefaultPort +// GetPort returns the default serial port depending on the operating system and USB interfaces. +func GetPort(portFlag string, usbInterfaces []string) (string, error) { + portCandidates := strings.FieldsFunc(portFlag, func(c rune) bool { return c == ',' }) + if len(portCandidates) == 1 { + return portCandidates[0], nil + } + + var ports []string + var err error + 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[0], 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 + break + } + } + } + if foundPort { + continue + } + } + + secondaryPorts = append(secondaryPorts, p.Name) + } + if len(primaryPorts) == 1 { + return primaryPorts[0], nil + } else if len(primaryPorts) > 1 { + ports = primaryPorts + } else { + 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, ", ")) +} + // Flash flashes firmware to a device based on the crosscompile configuration func Flash(crossCompile crosscompile.Export, app string, port string, verbose bool) error { - if verbose { - fmt.Fprintf(os.Stderr, "Flashing %s to port %s\n", app, port) - } - method := crossCompile.Flash.Method if method == "" { method = "command" } + // Resolve port for methods that need it (all except openocd) + if method != "openocd" { + var err error + port, err = GetPort(port, crossCompile.Flash.SerialPort) + if err != nil { + return fmt.Errorf("failed to find port: %w", err) + } + } + if verbose { fmt.Fprintf(os.Stderr, "Flashing %s using method: %s\n", app, method) + fmt.Fprintf(os.Stderr, "Using port: %s\n", port) } switch method { diff --git a/internal/monitor/monitor.go b/internal/monitor/monitor.go index 8622b5ae..2596cd8b 100644 --- a/internal/monitor/monitor.go +++ b/internal/monitor/monitor.go @@ -15,7 +15,7 @@ import ( "strconv" "time" - "github.com/goplus/llgo/internal/crosscompile" + "github.com/goplus/llgo/internal/flash" "github.com/mattn/go-tty" "go.bug.st/serial" ) @@ -39,20 +39,12 @@ func Monitor(config MonitorConfig, verbose bool) error { config.WaitTime = 300 } - // If target is specified, try to get port from crosscompile config - if config.Target != "" && config.Port == "" { - port, err := getPortFromTarget(config.Target) - if err != nil && verbose { - fmt.Fprintf(os.Stderr, "Warning: could not get port from target: %v\n", err) - } - if port != "" { - config.Port = port - } - } - - if config.Port == "" { - return fmt.Errorf("port not specified and could not determine from target") + // Resolve port using flash.GetPort + port, err := flash.GetPort(config.Port, nil) + if err != nil { + return fmt.Errorf("failed to find port: %w", err) } + config.Port = port if verbose { fmt.Fprintf(os.Stderr, "Connecting to %s at %d baud\n", config.Port, config.BaudRate) @@ -60,7 +52,6 @@ func Monitor(config MonitorConfig, verbose bool) error { // Open serial port with retry var serialConn serial.Port - var err error for i := 0; i <= config.WaitTime; i++ { serialConn, err = serial.Open(config.Port, &serial.Mode{BaudRate: config.BaudRate}) if err != nil { @@ -138,21 +129,6 @@ func Monitor(config MonitorConfig, verbose bool) error { } } -// getPortFromTarget tries to get serial port from target configuration -func getPortFromTarget(target string) (string, error) { - export, err := crosscompile.Use("", "", false, target) - if err != nil { - return "", err - } - - // Try to get port from serial port list - if len(export.Flash.SerialPort) > 0 { - return export.Flash.SerialPort[0], nil - } - - return "", fmt.Errorf("no serial port found in target configuration") -} - var addressMatch = regexp.MustCompile(`^panic: runtime error at 0x([0-9a-f]+): `) // Extract the address from the "panic: runtime error at" message. From 4e590d42bea6f2c79ee72b2557f201b19e009fe8 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sat, 6 Sep 2025 22:25:00 +0800 Subject: [PATCH 15/24] feat: auto guess port for monitoring --- cmd/internal/build/build.go | 2 ++ cmd/internal/clean/clean.go | 1 + cmd/internal/flags/flags.go | 5 ++++- cmd/internal/install/install.go | 1 + cmd/internal/monitor/monitor.go | 20 ++++++++++++++------ cmd/internal/run/run.go | 2 ++ cmd/internal/test/test.go | 1 + internal/build/build.go | 8 ++++++-- internal/crosscompile/crosscompile.go | 6 +++--- internal/crosscompile/crosscompile_test.go | 2 +- internal/monitor/monitor.go | 13 +++++++------ 11 files changed, 42 insertions(+), 19 deletions(-) diff --git a/cmd/internal/build/build.go b/cmd/internal/build/build.go index af53ab62..41c65f11 100644 --- a/cmd/internal/build/build.go +++ b/cmd/internal/build/build.go @@ -36,6 +36,8 @@ var Cmd = &base.Command{ func init() { Cmd.Run = runCmd base.PassBuildFlags(Cmd) + + flags.AddCommonFlags(&Cmd.Flag) flags.AddBuildFlags(&Cmd.Flag) flags.AddOutputFlags(&Cmd.Flag) } diff --git a/cmd/internal/clean/clean.go b/cmd/internal/clean/clean.go index 907ec849..096ca421 100644 --- a/cmd/internal/clean/clean.go +++ b/cmd/internal/clean/clean.go @@ -31,6 +31,7 @@ var Cmd = &base.Command{ func init() { Cmd.Run = runCmd + flags.AddCommonFlags(&Cmd.Flag) flags.AddBuildFlags(&Cmd.Flag) } diff --git a/cmd/internal/flags/flags.go b/cmd/internal/flags/flags.go index d2a7aadf..2a2f80d8 100644 --- a/cmd/internal/flags/flags.go +++ b/cmd/internal/flags/flags.go @@ -27,8 +27,11 @@ var CheckLinkArgs bool var CheckLLFiles bool var GenLLFiles bool +func AddCommonFlags(fs *flag.FlagSet) { + fs.BoolVar(&Verbose, "v", false, "Verbose output") +} + func AddBuildFlags(fs *flag.FlagSet) { - fs.BoolVar(&Verbose, "v", false, "Verbose mode") fs.StringVar(&Tags, "tags", "", "Build tags") fs.StringVar(&BuildEnv, "buildenv", "", "Build environment") if buildenv.Dev { diff --git a/cmd/internal/install/install.go b/cmd/internal/install/install.go index d986853c..037c4c68 100644 --- a/cmd/internal/install/install.go +++ b/cmd/internal/install/install.go @@ -35,6 +35,7 @@ var Cmd = &base.Command{ func init() { Cmd.Run = runCmd + flags.AddCommonFlags(&Cmd.Flag) flags.AddBuildFlags(&Cmd.Flag) flags.AddEmbeddedFlags(&Cmd.Flag) } diff --git a/cmd/internal/monitor/monitor.go b/cmd/internal/monitor/monitor.go index 9392d024..af7bab30 100644 --- a/cmd/internal/monitor/monitor.go +++ b/cmd/internal/monitor/monitor.go @@ -22,6 +22,7 @@ import ( "github.com/goplus/llgo/cmd/internal/base" "github.com/goplus/llgo/cmd/internal/flags" + "github.com/goplus/llgo/internal/crosscompile" "github.com/goplus/llgo/internal/monitor" ) @@ -32,6 +33,7 @@ var Cmd = &base.Command{ } func init() { + flags.AddCommonFlags(&Cmd.Flag) flags.AddEmbeddedFlags(&Cmd.Flag) Cmd.Run = runMonitor } @@ -45,24 +47,30 @@ func runMonitor(cmd *base.Command, args []string) { os.Exit(1) } - if flags.Port == "" && flags.Target == "" { - fmt.Fprintf(os.Stderr, "llgo monitor: must specify either -port or -target\n") - return - } - var executable string if len(args) == 1 { executable = args[0] } + var serialPort []string + if flags.Target != "" { + conf, err := crosscompile.UseTarget(flags.Target) + if err != nil { + fmt.Fprintf(os.Stderr, "llgo monitor: %v\n", err) + os.Exit(1) + } + serialPort = conf.Flash.SerialPort + } + config := monitor.MonitorConfig{ Port: flags.Port, Target: flags.Target, BaudRate: flags.BaudRate, Executable: executable, + SerialPort: serialPort, } - if err := monitor.Monitor(config, true); err != nil { + if err := monitor.Monitor(config, flags.Verbose); err != nil { fmt.Fprintf(os.Stderr, "llgo monitor: %v\n", err) os.Exit(1) } diff --git a/cmd/internal/run/run.go b/cmd/internal/run/run.go index b1614715..68b8ce83 100644 --- a/cmd/internal/run/run.go +++ b/cmd/internal/run/run.go @@ -48,11 +48,13 @@ func init() { Cmd.Run = runCmd CmpTestCmd.Run = runCmpTest base.PassBuildFlags(Cmd) + flags.AddCommonFlags(&Cmd.Flag) flags.AddBuildFlags(&Cmd.Flag) flags.AddEmulatorFlags(&Cmd.Flag) flags.AddEmbeddedFlags(&Cmd.Flag) // for -target support base.PassBuildFlags(CmpTestCmd) + flags.AddCommonFlags(&CmpTestCmd.Flag) flags.AddBuildFlags(&CmpTestCmd.Flag) flags.AddEmulatorFlags(&CmpTestCmd.Flag) flags.AddEmbeddedFlags(&CmpTestCmd.Flag) // for -target support diff --git a/cmd/internal/test/test.go b/cmd/internal/test/test.go index 5a259922..0a8a5a05 100644 --- a/cmd/internal/test/test.go +++ b/cmd/internal/test/test.go @@ -17,6 +17,7 @@ var Cmd = &base.Command{ func init() { Cmd.Run = runCmd + flags.AddCommonFlags(&Cmd.Flag) flags.AddBuildFlags(&Cmd.Flag) flags.AddEmulatorFlags(&Cmd.Flag) flags.AddEmbeddedFlags(&Cmd.Flag) diff --git a/internal/build/build.go b/internal/build/build.go index aad02f41..68242e55 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -360,12 +360,16 @@ func Do(args []string, conf *Config) ([]Package, error) { if err != nil { return nil, err } - err = monitor.Monitor(monitor.MonitorConfig{ + monitorConfig := monitor.MonitorConfig{ Port: ctx.buildConf.Port, Target: conf.Target, Executable: finalApp, BaudRate: conf.BaudRate, - }, verbose) + } + if ctx.crossCompile.Flash.Method != "openocd" { + monitorConfig.SerialPort = ctx.crossCompile.Flash.SerialPort + } + err = monitor.Monitor(monitorConfig, verbose) } if err != nil { return nil, err diff --git a/internal/crosscompile/crosscompile.go b/internal/crosscompile/crosscompile.go index ebbb3e3c..9da1cf98 100644 --- a/internal/crosscompile/crosscompile.go +++ b/internal/crosscompile/crosscompile.go @@ -491,8 +491,8 @@ func use(goos, goarch string, wasiThreads bool) (export Export, err error) { return } -// useTarget loads configuration from a target name (e.g., "rp2040", "wasi") -func useTarget(targetName string) (export Export, err error) { +// UseTarget loads configuration from a target name (e.g., "rp2040", "wasi") +func UseTarget(targetName string) (export Export, err error) { resolver := targets.NewDefaultResolver() config, err := resolver.Resolve(targetName) @@ -716,7 +716,7 @@ func useTarget(targetName string) (export Export, err error) { // If targetName is provided, it takes precedence over goos/goarch func Use(goos, goarch string, wasiThreads bool, targetName string) (export Export, err error) { if targetName != "" && !strings.HasPrefix(targetName, "wasm") && !strings.HasPrefix(targetName, "wasi") { - return useTarget(targetName) + return UseTarget(targetName) } return use(goos, goarch, wasiThreads) } diff --git a/internal/crosscompile/crosscompile_test.go b/internal/crosscompile/crosscompile_test.go index 8ea3f146..42152a58 100644 --- a/internal/crosscompile/crosscompile_test.go +++ b/internal/crosscompile/crosscompile_test.go @@ -214,7 +214,7 @@ func TestUseTarget(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - export, err := useTarget(tc.targetName) + export, err := UseTarget(tc.targetName) if tc.expectError { if err == nil { diff --git a/internal/monitor/monitor.go b/internal/monitor/monitor.go index 2596cd8b..4b205b54 100644 --- a/internal/monitor/monitor.go +++ b/internal/monitor/monitor.go @@ -22,11 +22,12 @@ import ( // MonitorConfig contains configuration for the monitor type MonitorConfig struct { - Port string // Serial port device - Target string // Target name for crosscompile config - BaudRate int // Baudrate of serial monitor - Executable string // Optional path to executable for debug info - WaitTime int // Wait time for port connection (ms) + Port string // Serial port device + Target string // Target name for crosscompile config + BaudRate int // Baudrate of serial monitor + Executable string // Optional path to executable for debug info + WaitTime int // Wait time for port connection (ms) + SerialPort []string // List of serial ports to use } // Monitor starts serial monitoring with the given configuration @@ -40,7 +41,7 @@ func Monitor(config MonitorConfig, verbose bool) error { } // Resolve port using flash.GetPort - port, err := flash.GetPort(config.Port, nil) + port, err := flash.GetPort(config.Port, config.SerialPort) if err != nil { return fmt.Errorf("failed to find port: %w", err) } From 1c2aea10f020e19cbb43a60da1cb4416b26f82b4 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sat, 6 Sep 2025 22:44:23 +0800 Subject: [PATCH 16/24] feat: add Arduino 1200bps reset support before flashing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add touchSerialPortAt1200bps function from TinyGo for Arduino bootloader reset - Update Flash function to trigger 1200bps reset when flash-1200-bps-reset is true - Add 2-second wait after reset for device to enter bootloader mode - Support retry mechanism with Windows-specific error handling 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- internal/flash/flash.go | 80 ++++++++++++++++++++++++++++------------- 1 file changed, 56 insertions(+), 24 deletions(-) diff --git a/internal/flash/flash.go b/internal/flash/flash.go index 4f5830f3..68b75e1a 100644 --- a/internal/flash/flash.go +++ b/internal/flash/flash.go @@ -13,6 +13,7 @@ import ( "github.com/goplus/llgo/internal/crosscompile" "github.com/goplus/llgo/internal/env" + "go.bug.st/serial" "go.bug.st/serial/enumerator" ) @@ -119,6 +120,49 @@ func GetPort(portFlag string, usbInterfaces []string) (string, error) { return "", errors.New("port you specified '" + strings.Join(portCandidates, ",") + "' does not exist, available ports are " + strings.Join(ports, ", ")) } +// From tinygo/main.go touchSerialPortAt1200bps +// touchSerialPortAt1200bps triggers Arduino-compatible devices to enter bootloader mode. +// This function implements the Arduino auto-reset mechanism used before flashing firmware. +// +// Working principle: +// 1. Opens serial port at 1200 baud rate (special reset baudrate for Arduino) +// 2. Sets DTR (Data Terminal Ready) signal to false +// 3. This triggers the device to reset and enter bootloader mode for firmware upload +// +// Usage scenarios: +// - Required for Arduino Uno, Leonardo, Micro and other compatible devices +// - Executed when target config has "flash-1200-bps-reset": "true" +// - Ensures device is in correct state to receive new firmware +// +// Retry mechanism: +// - Retries up to 3 times due to potential temporary serial port access issues +// - Windows special handling: InvalidSerialPort error during bootloader transition is normal +func touchSerialPortAt1200bps(port string) (err error) { + retryCount := 3 + for i := 0; i < retryCount; i++ { + // Open port at 1200bps to trigger Arduino reset + 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() + + // Set DTR to false to trigger reset + p.SetDTR(false) + return nil + } + return fmt.Errorf("opening port: %s", err) +} + // Flash flashes firmware to a device based on the crosscompile configuration func Flash(crossCompile crosscompile.Export, app string, port string, verbose bool) error { method := crossCompile.Flash.Method @@ -140,6 +184,18 @@ func Flash(crossCompile crosscompile.Export, app string, port string, verbose bo fmt.Fprintf(os.Stderr, "Using port: %s\n", port) } + // Execute 1200bps reset before flashing if needed (except for openocd) + if method != "openocd" && crossCompile.Flash.Flash1200BpsReset { + if verbose { + fmt.Fprintf(os.Stderr, "Triggering 1200bps reset on port: %s\n", port) + } + if err := touchSerialPortAt1200bps(port); err != nil { + return fmt.Errorf("failed to trigger 1200bps reset: %w", err) + } + // Wait a bit for device to enter bootloader mode + time.Sleep(2 * time.Second) + } + switch method { case "command": return flashCommand(crossCompile.Flash, app, port, verbose) @@ -176,17 +232,6 @@ func flashCommand(flash crosscompile.Flash, app string, port string, verbose boo return fmt.Errorf("empty flash command after expansion") } - // Handle 1200bps reset if required - if flash.Flash1200BpsReset && port != "" { - if err := reset1200bps(port, verbose); err != nil { - if verbose { - fmt.Fprintf(os.Stderr, "Warning: 1200bps reset failed: %v\n", err) - } - } - // Wait for bootloader - time.Sleep(2 * time.Second) - } - // Execute flash command cmd := exec.Command(parts[0], parts[1:]...) cmd.Stdout = os.Stdout @@ -376,19 +421,6 @@ func expandEnv(template string, envs map[string]string) string { return result } -// reset1200bps performs 1200bps reset for Arduino-compatible boards -func reset1200bps(port string, verbose bool) error { - if verbose { - fmt.Fprintf(os.Stderr, "Performing 1200bps reset on %s\n", port) - } - - // This is a simplified implementation - // In practice, this would need platform-specific serial port handling - // For now, just try to touch the port to trigger reset - _, err := os.Stat(port) - return err -} - // copyFile copies a file from src to dst func copyFile(src, dst string) error { sourceFile, err := os.Open(src) From c0afe199c22bba60412f00392845107fab8f57d6 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sun, 7 Sep 2025 10:06:01 +0800 Subject: [PATCH 17/24] refactor: move device types definition into flash --- cmd/internal/monitor/monitor.go | 2 +- internal/build/build.go | 8 +-- internal/build/outputs.go | 8 +-- internal/build/outputs_test.go | 81 +++++++++++++++++---------- internal/crosscompile/crosscompile.go | 59 +++++++------------ internal/flash/flash.go | 52 +++++++++++++---- 6 files changed, 118 insertions(+), 92 deletions(-) diff --git a/cmd/internal/monitor/monitor.go b/cmd/internal/monitor/monitor.go index af7bab30..6c5bd5af 100644 --- a/cmd/internal/monitor/monitor.go +++ b/cmd/internal/monitor/monitor.go @@ -59,7 +59,7 @@ func runMonitor(cmd *base.Command, args []string) { fmt.Fprintf(os.Stderr, "llgo monitor: %v\n", err) os.Exit(1) } - serialPort = conf.Flash.SerialPort + serialPort = conf.Device.SerialPort } config := monitor.MonitorConfig{ diff --git a/internal/build/build.go b/internal/build/build.go index 68242e55..05451b20 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -344,7 +344,7 @@ func Do(args []string, conf *Config) ([]Package, error) { case ModeInstall: // Native already installed in linkMainPkg if conf.Target != "" { - err = flash.Flash(ctx.crossCompile, finalApp, ctx.buildConf.Port, verbose) + err = flash.FlashDevice(ctx.crossCompile.Device, finalApp, ctx.buildConf.Port, verbose) if err != nil { return nil, err } @@ -356,7 +356,7 @@ func Do(args []string, conf *Config) ([]Package, error) { } else if conf.Emulator { err = runInEmulator(ctx.crossCompile.Emulator, finalApp, pkg.Dir, pkg.PkgPath, conf, mode, verbose) } else { - err = flash.Flash(ctx.crossCompile, finalApp, ctx.buildConf.Port, verbose) + err = flash.FlashDevice(ctx.crossCompile.Device, finalApp, ctx.buildConf.Port, verbose) if err != nil { return nil, err } @@ -365,9 +365,7 @@ func Do(args []string, conf *Config) ([]Package, error) { Target: conf.Target, Executable: finalApp, BaudRate: conf.BaudRate, - } - if ctx.crossCompile.Flash.Method != "openocd" { - monitorConfig.SerialPort = ctx.crossCompile.Flash.SerialPort + SerialPort: ctx.crossCompile.Device.SerialPort, } err = monitor.Monitor(monitorConfig, verbose) } diff --git a/internal/build/outputs.go b/internal/build/outputs.go index 0eaa2e6f..d00c0e63 100644 --- a/internal/build/outputs.go +++ b/internal/build/outputs.go @@ -84,11 +84,11 @@ func determineFlashFormat(crossCompile *crosscompile.Export) string { return "" } - flashMethod := crossCompile.Flash.Method + flashMethod := crossCompile.Device.Flash.Method switch flashMethod { case "command", "": // Extract format from flash command tokens - flashCommand := crossCompile.Flash.Command + flashCommand := crossCompile.Device.Flash.Command switch { case strings.Contains(flashCommand, "{hex}"): return ".hex" @@ -106,10 +106,10 @@ func determineFlashFormat(crossCompile *crosscompile.Export) string { return "" } case "msd": - if crossCompile.MSD.FirmwareName == "" { + if crossCompile.Device.MSD.FirmwareName == "" { return "" } - return filepath.Ext(crossCompile.MSD.FirmwareName) + return filepath.Ext(crossCompile.Device.MSD.FirmwareName) case "openocd": return ".hex" case "bmp": diff --git a/internal/build/outputs_test.go b/internal/build/outputs_test.go index c37638dd..5f5a528f 100644 --- a/internal/build/outputs_test.go +++ b/internal/build/outputs_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/goplus/llgo/internal/crosscompile" + "github.com/goplus/llgo/internal/flash" ) func TestGenOutputs(t *testing.T) { @@ -315,9 +316,11 @@ func TestGenOutputs(t *testing.T) { pkgName: "hello", crossCompile: &crosscompile.Export{ BinaryFormat: "esp32", - Flash: crosscompile.Flash{ - Method: "command", - Command: "esptool.py --chip esp32 write_flash 0x10000 {hex}", + Device: flash.Device{ + Flash: flash.Flash{ + Method: "command", + Command: "esptool.py --chip esp32 write_flash 0x10000 {hex}", + }, }, }, wantOutPath: "", // temp file @@ -336,9 +339,11 @@ func TestGenOutputs(t *testing.T) { pkgName: "hello", crossCompile: &crosscompile.Export{ BinaryFormat: "esp32", - Flash: crosscompile.Flash{ - Method: "command", - Command: "esptool.py --chip esp32 write_flash 0x10000 {bin}", + Device: flash.Device{ + Flash: flash.Flash{ + Method: "command", + Command: "esptool.py --chip esp32 write_flash 0x10000 {bin}", + }, }, }, wantOutPath: "", // temp file @@ -357,8 +362,10 @@ func TestGenOutputs(t *testing.T) { pkgName: "hello", crossCompile: &crosscompile.Export{ BinaryFormat: "arm", - Flash: crosscompile.Flash{ - Method: "openocd", + Device: flash.Device{ + Flash: flash.Flash{ + Method: "openocd", + }, }, }, wantOutPath: "", // temp file @@ -377,11 +384,13 @@ func TestGenOutputs(t *testing.T) { pkgName: "hello", crossCompile: &crosscompile.Export{ BinaryFormat: "uf2", - Flash: crosscompile.Flash{ - Method: "msd", - }, - MSD: crosscompile.MSD{ - FirmwareName: "firmware.uf2", + Device: flash.Device{ + Flash: flash.Flash{ + Method: "msd", + }, + MSD: flash.MSD{ + FirmwareName: "firmware.uf2", + }, }, }, wantOutPath: "", // temp file @@ -400,8 +409,10 @@ func TestGenOutputs(t *testing.T) { pkgName: "hello", crossCompile: &crosscompile.Export{ BinaryFormat: "arm", - Flash: crosscompile.Flash{ - Method: "bmp", + Device: flash.Device{ + Flash: flash.Flash{ + Method: "bmp", + }, }, }, wantOutPath: "", // temp file @@ -510,9 +521,11 @@ func TestDetermineFormat(t *testing.T) { name: "flash method command - extract hex", conf: &Config{Mode: ModeRun, Target: "esp32"}, crossCompile: &crosscompile.Export{ - Flash: crosscompile.Flash{ - Method: "command", - Command: "esptool.py --chip esp32 write_flash 0x10000 {hex}", + Device: flash.Device{ + Flash: flash.Flash{ + Method: "command", + Command: "esptool.py --chip esp32 write_flash 0x10000 {hex}", + }, }, }, wantFmt: "hex", @@ -522,9 +535,11 @@ func TestDetermineFormat(t *testing.T) { name: "flash method command - extract bin", conf: &Config{Mode: ModeRun, Target: "esp32"}, crossCompile: &crosscompile.Export{ - Flash: crosscompile.Flash{ - Method: "command", - Command: "esptool.py --chip esp32 write_flash 0x10000 {bin}", + Device: flash.Device{ + Flash: flash.Flash{ + Method: "command", + Command: "esptool.py --chip esp32 write_flash 0x10000 {bin}", + }, }, }, wantFmt: "bin", @@ -534,8 +549,10 @@ func TestDetermineFormat(t *testing.T) { name: "flash method openocd", conf: &Config{Mode: ModeRun, Target: "stm32"}, crossCompile: &crosscompile.Export{ - Flash: crosscompile.Flash{ - Method: "openocd", + Device: flash.Device{ + Flash: flash.Flash{ + Method: "openocd", + }, }, }, wantFmt: "hex", @@ -545,11 +562,13 @@ func TestDetermineFormat(t *testing.T) { name: "flash method msd - extract from firmware name", conf: &Config{Mode: ModeRun, Target: "rp2040"}, crossCompile: &crosscompile.Export{ - Flash: crosscompile.Flash{ - Method: "msd", - }, - MSD: crosscompile.MSD{ - FirmwareName: "firmware.uf2", + Device: flash.Device{ + Flash: flash.Flash{ + Method: "msd", + }, + MSD: flash.MSD{ + FirmwareName: "firmware.uf2", + }, }, }, wantFmt: "uf2", @@ -559,8 +578,10 @@ func TestDetermineFormat(t *testing.T) { name: "flash method bmp", conf: &Config{Mode: ModeRun, Target: "stm32"}, crossCompile: &crosscompile.Export{ - Flash: crosscompile.Flash{ - Method: "bmp", + Device: flash.Device{ + Flash: flash.Flash{ + Method: "bmp", + }, }, }, wantFmt: "elf", diff --git a/internal/crosscompile/crosscompile.go b/internal/crosscompile/crosscompile.go index 9da1cf98..d22624e5 100644 --- a/internal/crosscompile/crosscompile.go +++ b/internal/crosscompile/crosscompile.go @@ -12,32 +12,11 @@ import ( "github.com/goplus/llgo/internal/crosscompile/compile" "github.com/goplus/llgo/internal/env" + "github.com/goplus/llgo/internal/flash" "github.com/goplus/llgo/internal/targets" "github.com/goplus/llgo/internal/xtool/llvm" ) -// Flash contains configuration for device flashing -type Flash struct { - Method string // Flash method: "command", "openocd", "msd", "bmp" - Command string // Flash command template - Serial string // Serial communication settings - SerialPort []string // Available serial ports - Flash1200BpsReset bool // Whether to use 1200bps reset -} - -// MSD contains configuration for Mass Storage Device flashing -type MSD struct { - VolumeName []string // Names of the volumes - FirmwareName string // Firmware file name pattern -} - -// OpenOCD contains configuration for OpenOCD debugging/flashing -type OpenOCD struct { - Interface string // Interface configuration (e.g., "stlink") - Transport string // Transport protocol (e.g., "swd", "jtag") - Target string // Target configuration (e.g., "stm32f4x") -} - type Export struct { CC string // Compiler to use CCFLAGS []string @@ -59,9 +38,7 @@ type Export struct { Emulator string // Emulator command template (e.g., "qemu-system-arm -M {} -kernel {}") // Flashing/Debugging configuration - Flash Flash // Flash configuration for device programming - MSD MSD // Mass Storage Device configuration - OpenOCD OpenOCD // OpenOCD configuration for debugging/flashing + Device flash.Device // Device configuration for flashing/debugging } // URLs and configuration that can be overridden for testing @@ -530,21 +507,23 @@ func UseTarget(targetName string) (export Export, err error) { export.Emulator = config.Emulator // Set flashing/debugging configuration - export.Flash = Flash{ - Method: config.FlashMethod, - Command: config.FlashCommand, - Serial: config.Serial, - SerialPort: config.SerialPort, - Flash1200BpsReset: config.Flash1200BpsReset == "true", - } - export.MSD = MSD{ - VolumeName: config.MSDVolumeName, - FirmwareName: config.MSDFirmwareName, - } - export.OpenOCD = OpenOCD{ - Interface: config.OpenOCDInterface, - Transport: config.OpenOCDTransport, - Target: config.OpenOCDTarget, + export.Device = flash.Device{ + Serial: config.Serial, + SerialPort: config.SerialPort, + Flash: flash.Flash{ + Method: config.FlashMethod, + Command: config.FlashCommand, + Flash1200BpsReset: config.Flash1200BpsReset == "true", + }, + MSD: flash.MSD{ + VolumeName: config.MSDVolumeName, + FirmwareName: config.MSDFirmwareName, + }, + OpenOCD: flash.OpenOCD{ + Interface: config.OpenOCDInterface, + Transport: config.OpenOCDTransport, + Target: config.OpenOCDTarget, + }, } // Build environment map for template variable expansion diff --git a/internal/flash/flash.go b/internal/flash/flash.go index 68b75e1a..5d20b029 100644 --- a/internal/flash/flash.go +++ b/internal/flash/flash.go @@ -11,12 +11,40 @@ import ( "strings" "time" - "github.com/goplus/llgo/internal/crosscompile" "github.com/goplus/llgo/internal/env" "go.bug.st/serial" "go.bug.st/serial/enumerator" ) +// Device contains all flashing/debugging configuration for a device +type Device struct { + Serial string // Serial communication settings + SerialPort []string // Available serial ports + Flash Flash // Flash configuration for device programming + MSD MSD // Mass Storage Device configuration + OpenOCD OpenOCD // OpenOCD configuration for debugging/flashing +} + +// Flash contains configuration for device flashing +type Flash struct { + Method string // Flash method: "command", "openocd", "msd", "bmp" + Command string // Flash command template + Flash1200BpsReset bool // Whether to use 1200bps reset +} + +// MSD contains configuration for Mass Storage Device flashing +type MSD struct { + VolumeName []string // Names of the volumes + FirmwareName string // Firmware file name pattern +} + +// OpenOCD contains configuration for OpenOCD debugging/flashing +type OpenOCD struct { + Interface string // Interface configuration (e.g., "stlink") + Transport string // Transport protocol (e.g., "swd", "jtag") + Target string // Target configuration (e.g., "stm32f4x") +} + // From tinygo/main.go getDefaultPort // GetPort returns the default serial port depending on the operating system and USB interfaces. func GetPort(portFlag string, usbInterfaces []string) (string, error) { @@ -163,9 +191,9 @@ func touchSerialPortAt1200bps(port string) (err error) { return fmt.Errorf("opening port: %s", err) } -// Flash flashes firmware to a device based on the crosscompile configuration -func Flash(crossCompile crosscompile.Export, app string, port string, verbose bool) error { - method := crossCompile.Flash.Method +// FlashDevice flashes firmware to a device based on the device configuration +func FlashDevice(device Device, app string, port string, verbose bool) error { + method := device.Flash.Method if method == "" { method = "command" } @@ -173,7 +201,7 @@ func Flash(crossCompile crosscompile.Export, app string, port string, verbose bo // Resolve port for methods that need it (all except openocd) if method != "openocd" { var err error - port, err = GetPort(port, crossCompile.Flash.SerialPort) + port, err = GetPort(port, device.SerialPort) if err != nil { return fmt.Errorf("failed to find port: %w", err) } @@ -185,7 +213,7 @@ func Flash(crossCompile crosscompile.Export, app string, port string, verbose bo } // Execute 1200bps reset before flashing if needed (except for openocd) - if method != "openocd" && crossCompile.Flash.Flash1200BpsReset { + if method != "openocd" && device.Flash.Flash1200BpsReset { if verbose { fmt.Fprintf(os.Stderr, "Triggering 1200bps reset on port: %s\n", port) } @@ -198,11 +226,11 @@ func Flash(crossCompile crosscompile.Export, app string, port string, verbose bo switch method { case "command": - return flashCommand(crossCompile.Flash, app, port, verbose) + return flashCommand(device.Flash, app, port, verbose) case "openocd": - return flashOpenOCD(crossCompile.OpenOCD, app, verbose) + return flashOpenOCD(device.OpenOCD, app, verbose) case "msd": - return flashMSD(crossCompile.MSD, app, verbose) + return flashMSD(device.MSD, app, verbose) case "bmp": return flashBMP(app, verbose) default: @@ -211,7 +239,7 @@ func Flash(crossCompile crosscompile.Export, app string, port string, verbose bo } // flashCommand handles command-based flashing -func flashCommand(flash crosscompile.Flash, app string, port string, verbose bool) error { +func flashCommand(flash Flash, app string, port string, verbose bool) error { if flash.Command == "" { return fmt.Errorf("flash command not specified") } @@ -241,7 +269,7 @@ func flashCommand(flash crosscompile.Flash, app string, port string, verbose boo } // flashOpenOCD handles OpenOCD-based flashing -func flashOpenOCD(openocd crosscompile.OpenOCD, app string, verbose bool) error { +func flashOpenOCD(openocd OpenOCD, app string, verbose bool) error { if openocd.Interface == "" { return fmt.Errorf("OpenOCD interface not specified") } @@ -279,7 +307,7 @@ func flashOpenOCD(openocd crosscompile.OpenOCD, app string, verbose bool) error } // flashMSD handles Mass Storage Device flashing -func flashMSD(msd crosscompile.MSD, app string, verbose bool) error { +func flashMSD(msd MSD, app string, verbose bool) error { if len(msd.VolumeName) == 0 { return fmt.Errorf("MSD volume names not specified") } From 16c8402065de58a7b66903082972eba60ace01b2 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sun, 7 Sep 2025 13:32:11 +0800 Subject: [PATCH 18/24] refactor: multi format generation and llgo build flags --- cmd/internal/build/build.go | 2 + cmd/internal/flags/flags.go | 20 +- internal/build/build.go | 129 +++-- internal/build/outputs.go | 333 +++++-------- internal/build/outputs_test.go | 832 ++++++++++----------------------- internal/build/run.go | 22 +- internal/firmware/env.go | 16 + internal/firmware/ext.go | 16 - internal/firmware/ext_test.go | 31 -- internal/firmware/firmware.go | 59 ++- internal/flash/flash.go | 58 +-- internal/llgen/llgenf.go | 2 - 12 files changed, 541 insertions(+), 979 deletions(-) create mode 100644 internal/firmware/env.go delete mode 100644 internal/firmware/ext.go delete mode 100644 internal/firmware/ext_test.go diff --git a/cmd/internal/build/build.go b/cmd/internal/build/build.go index 41c65f11..98d4748c 100644 --- a/cmd/internal/build/build.go +++ b/cmd/internal/build/build.go @@ -39,6 +39,8 @@ func init() { flags.AddCommonFlags(&Cmd.Flag) flags.AddBuildFlags(&Cmd.Flag) + flags.AddEmulatorFlags(&Cmd.Flag) + flags.AddEmbeddedFlags(&Cmd.Flag) flags.AddOutputFlags(&Cmd.Flag) } diff --git a/cmd/internal/flags/flags.go b/cmd/internal/flags/flags.go index 2a2f80d8..c888b28c 100644 --- a/cmd/internal/flags/flags.go +++ b/cmd/internal/flags/flags.go @@ -8,11 +8,19 @@ import ( ) var OutputFile string -var FileFormat string +var OutBin bool +var OutHex bool +var OutImg bool +var OutUf2 bool +var OutZip bool func AddOutputFlags(fs *flag.FlagSet) { fs.StringVar(&OutputFile, "o", "", "Output file") - fs.StringVar(&FileFormat, "file-format", "", "File format for target output (e.g., bin, hex, elf, uf2, zip)") + fs.BoolVar(&OutBin, "obin", false, "Generate binary output (.bin)") + fs.BoolVar(&OutHex, "ohex", false, "Generate Intel hex output (.hex)") + fs.BoolVar(&OutImg, "oimg", false, "Generate image output (.img)") + fs.BoolVar(&OutUf2, "ouf2", false, "Generate UF2 output (.uf2)") + fs.BoolVar(&OutZip, "ozip", false, "Generate ZIP/DFU output (.zip)") } var Verbose bool @@ -67,7 +75,13 @@ func UpdateConfig(conf *build.Config) { switch conf.Mode { case build.ModeBuild: conf.OutFile = OutputFile - conf.FileFormat = FileFormat + conf.OutFmts = build.OutFmts{ + Bin: OutBin, + Hex: OutHex, + Img: OutImg, + Uf2: OutUf2, + Zip: OutZip, + } case build.ModeRun, build.ModeTest: conf.Emulator = Emulator case build.ModeInstall: diff --git a/internal/build/build.go b/internal/build/build.go index 05451b20..b084e482 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -72,17 +72,36 @@ const ( debugBuild = packages.DebugPackagesLoad ) +// OutFmts contains output format specifications for embedded targets +type OutFmts struct { + Bin bool // Generate binary output (.bin) + Hex bool // Generate Intel hex output (.hex) + Img bool // Generate image output (.img) + Uf2 bool // Generate UF2 output (.uf2) + Zip bool // Generate ZIP/DFU output (.zip) +} + +// OutFmtDetails contains detailed output file paths for each format +type OutFmtDetails struct { + Out string // Base output file path + Bin string // Binary output file path (.bin) + Hex string // Intel hex output file path (.hex) + Img string // Image output file path (.img) + Uf2 string // UF2 output file path (.uf2) + Zip string // ZIP/DFU output file path (.zip) +} + type Config struct { Goos string Goarch string Target string // target name (e.g., "rp2040", "wasi") - takes precedence over Goos/Goarch BinPath string - AppExt string // ".exe" on Windows, empty on Unix - OutFile string // only valid for ModeBuild when len(pkgs) == 1 - FileFormat string // File format override (e.g., "bin", "hex", "elf", "uf2", "zip") - takes precedence over target's default - Emulator bool // run in emulator mode - Port string // target port for flashing - BaudRate int // baudrate for serial communication + AppExt string // ".exe" on Windows, empty on Unix + OutFile string // only valid for ModeBuild when len(pkgs) == 1 + OutFmts OutFmts // Output format specifications (only for Target != "") + Emulator bool // run in emulator mode + Port string // target port for flashing + BaudRate int // baudrate for serial communication RunArgs []string Mode Mode AbiMode AbiMode @@ -121,7 +140,6 @@ func NewDefaultConf(mode Mode) *Config { BinPath: bin, Mode: mode, AbiMode: cabi.ModeAllFunc, - AppExt: DefaultAppExt(goos), } return conf } @@ -137,8 +155,15 @@ func envGOPATH() (string, error) { return filepath.Join(home, "go"), nil } -func DefaultAppExt(goos string) string { - switch goos { +func defaultAppExt(conf *Config) string { + if conf.Target != "" { + if strings.HasPrefix(conf.Target, "wasi") || strings.HasPrefix(conf.Target, "wasm") { + return ".wasm" + } + return ".elf" + } + + switch conf.Goos { case "windows": return ".exe" case "wasi", "wasip1", "js": @@ -163,6 +188,9 @@ func Do(args []string, conf *Config) ([]Package, error) { if conf.Goarch == "" { conf.Goarch = runtime.GOARCH } + if conf.AppExt == "" { + conf.AppExt = defaultAppExt(conf) + } // Handle crosscompile configuration first to set correct GOOS/GOARCH export, err := crosscompile.Use(conf.Goos, conf.Goarch, IsWasiThreadsEnabled(), conf.Target) if err != nil { @@ -241,6 +269,10 @@ func Do(args []string, conf *Config) ([]Package, error) { if conf.OutFile != "" { return nil, fmt.Errorf("cannot build multiple packages with -o") } + case ModeInstall: + if conf.Target != "" { + return nil, fmt.Errorf("cannot install multiple packages to embedded target") + } case ModeRun: return nil, fmt.Errorf("cannot run multiple packages") case ModeTest: @@ -319,19 +351,24 @@ func Do(args []string, conf *Config) ([]Package, error) { if needLink(pkg, mode) { name := path.Base(pkg.PkgPath) - outputCfg, err := genOutputs(conf, name, len(ctx.initial) > 1, &ctx.crossCompile) + // Create output format details + outFmts, err := buildOutFmts(name, conf, len(ctx.initial) > 1, &ctx.crossCompile) if err != nil { return nil, err } - err = linkMainPkg(ctx, pkg, allPkgs, global, outputCfg.IntPath, verbose) + // Link main package using the base output path + err = linkMainPkg(ctx, pkg, allPkgs, global, outFmts.Out, verbose) if err != nil { return nil, err } - finalApp := outputCfg.IntPath - if outputCfg.NeedFwGen || outputCfg.FileFmt != "" { - finalApp, err = convertFormat(ctx, finalApp, &outputCfg) + envMap := outFmts.ToEnvMap() + + // Only convert formats when Target is specified + if conf.Target != "" { + // Process format conversions for embedded targets + err = firmware.ConvertFormats(ctx.crossCompile.BinaryFormat, ctx.crossCompile.FormatDetail, envMap) if err != nil { return nil, err } @@ -344,7 +381,7 @@ func Do(args []string, conf *Config) ([]Package, error) { case ModeInstall: // Native already installed in linkMainPkg if conf.Target != "" { - err = flash.FlashDevice(ctx.crossCompile.Device, finalApp, ctx.buildConf.Port, verbose) + err = flash.FlashDevice(ctx.crossCompile.Device, envMap, ctx.buildConf.Port, verbose) if err != nil { return nil, err } @@ -352,18 +389,18 @@ func Do(args []string, conf *Config) ([]Package, error) { case ModeRun, ModeTest, ModeCmpTest: if conf.Target == "" { - err = runNative(ctx, finalApp, pkg.Dir, pkg.PkgPath, conf, mode) + err = runNative(ctx, outFmts.Out, pkg.Dir, pkg.PkgPath, conf, mode) } else if conf.Emulator { - err = runInEmulator(ctx.crossCompile.Emulator, finalApp, pkg.Dir, pkg.PkgPath, conf, mode, verbose) + err = runInEmulator(ctx.crossCompile.Emulator, envMap, pkg.Dir, pkg.PkgPath, conf, mode, verbose) } else { - err = flash.FlashDevice(ctx.crossCompile.Device, finalApp, ctx.buildConf.Port, verbose) + err = flash.FlashDevice(ctx.crossCompile.Device, envMap, ctx.buildConf.Port, verbose) if err != nil { return nil, err } monitorConfig := monitor.MonitorConfig{ Port: ctx.buildConf.Port, Target: conf.Target, - Executable: finalApp, + Executable: outFmts.Out, BaudRate: conf.BaudRate, SerialPort: ctx.crossCompile.Device.SerialPort, } @@ -756,60 +793,6 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, global l 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) - } - - if outputCfg.NeedFwGen { - if outputCfg.DirectGen { - err := firmware.MakeFirmwareImage(currentApp, app, outputCfg.BinFmt, ctx.crossCompile.FormatDetail) - if err != nil { - return "", err - } - currentApp = app - } else { - binExt := firmware.BinaryExt(ctx.crossCompile.BinaryFormat) - tmpFile, err := os.CreateTemp("", "llgo-*"+binExt) - if err != nil { - return "", err - } - tmpFile.Close() - intermediateApp := tmpFile.Name() - - err = firmware.MakeFirmwareImage(currentApp, intermediateApp, ctx.crossCompile.BinaryFormat, ctx.crossCompile.FormatDetail) - if err != nil { - return "", err - } - currentApp = intermediateApp - defer func() { - if _, err := os.Stat(intermediateApp); err == nil { - os.Remove(intermediateApp) - } - }() - } - } - - if currentApp != app { - if outputCfg.FileFmt != "" { - binFmt := ctx.crossCompile.BinaryFormat - err := firmware.ConvertOutput(currentApp, app, binFmt, outputCfg.FileFmt) - if err != nil { - return "", err - } - } else { - err := os.Rename(currentApp, app) - if err != nil { - return "", err - } - } - } - - return app, nil -} - func linkObjFiles(ctx *context, app string, objFiles, linkArgs []string, verbose bool) error { buildArgs := []string{"-o", app} buildArgs = append(buildArgs, linkArgs...) diff --git a/internal/build/outputs.go b/internal/build/outputs.go index d00c0e63..0fa64b9e 100644 --- a/internal/build/outputs.go +++ b/internal/build/outputs.go @@ -3,6 +3,7 @@ package build import ( "os" "path/filepath" + "slices" "strings" "github.com/goplus/llgo/internal/crosscompile" @@ -20,227 +21,143 @@ 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, crossCompile *crosscompile.Export) (OutputCfg, error) { - var cfg OutputCfg - - // Calculate binary extension and set up format info - binFmt := crossCompile.BinaryFormat - binExt := firmware.BinaryExt(binFmt) - cfg.BinFmt = binFmt - - // Determine output format and extension - cfg.FileFmt, cfg.OutExt = determineFormat(conf, crossCompile) - - // Handle special .img case and set conversion flags - cfg.DirectGen = shouldDirectGen(cfg.OutExt, binExt) - if cfg.OutExt == ".img" { - cfg.BinFmt = binFmt + "-img" - } - - // Determine if firmware generation is needed - cfg.NeedFwGen = needsFwGen(conf, cfg.OutExt, binExt) - - // Generate paths based on mode - switch conf.Mode { - case ModeBuild: - return genBuildOutputs(conf, pkgName, multiPkg, cfg) - case ModeRun, ModeTest, ModeCmpTest: - return genRunOutputs(pkgName, cfg, conf.AppExt) - case ModeInstall: - return genInstallOutputs(conf, pkgName, cfg, binExt) - default: - return cfg, nil - } -} - -// determineFormat determines the file format and extension -func determineFormat(conf *Config, crossCompile *crosscompile.Export) (format, ext string) { - if conf.FileFormat != "" { - // User specified file format - return conf.FileFormat, firmware.GetFileExtFromFormat(conf.FileFormat) - } - - if conf.Mode == ModeRun && conf.Emulator && crossCompile.Emulator != "" { - // Emulator mode - extract format from emulator command - if emulatorFmt := firmware.ExtractFileFormatFromEmulator(crossCompile.Emulator); emulatorFmt != "" { - return emulatorFmt, firmware.GetFileExtFromFormat(emulatorFmt) - } - } - - // Device flashing - determine format based on flash method and target - if conf.Target != "" && (conf.Mode == ModeInstall || conf.Mode == ModeRun || conf.Mode == ModeTest || conf.Mode == ModeCmpTest) { - if flashExt := determineFlashFormat(crossCompile); flashExt != "" { - return flashExt[1:], flashExt // Remove the dot for format, keep for ext - } - } - - return "", "" -} - -// determineFlashFormat determines the required file format for flashing based on flash method -func determineFlashFormat(crossCompile *crosscompile.Export) string { - if crossCompile == nil { - return "" - } - - flashMethod := crossCompile.Device.Flash.Method - switch flashMethod { - case "command", "": - // Extract format from flash command tokens - flashCommand := crossCompile.Device.Flash.Command - switch { - case strings.Contains(flashCommand, "{hex}"): - return ".hex" - case strings.Contains(flashCommand, "{elf}"): - return ".elf" - case strings.Contains(flashCommand, "{bin}"): - return ".bin" - case strings.Contains(flashCommand, "{uf2}"): - return ".uf2" - case strings.Contains(flashCommand, "{zip}"): - return ".zip" - case strings.Contains(flashCommand, "{img}"): - return ".img" - default: - return "" - } - case "msd": - if crossCompile.Device.MSD.FirmwareName == "" { - return "" - } - return filepath.Ext(crossCompile.Device.MSD.FirmwareName) - case "openocd": - return ".hex" - case "bmp": - return ".elf" - default: - return "" - } -} - -// shouldDirectGen determines if direct firmware generation is possible -func shouldDirectGen(outExt, binExt string) bool { - return outExt == "" || outExt == binExt || outExt == ".img" -} - -// needsFwGen determines if firmware generation is needed -func needsFwGen(conf *Config, outExt, binExt string) bool { - switch conf.Mode { - case ModeBuild: - return conf.FileFormat != "" - case ModeRun, ModeTest, ModeCmpTest: - if conf.Emulator { - return outExt != "" - } - return binExt != "" - case ModeInstall: - return binExt != "" - default: - return false - } -} - -// genBuildOutputs generates output paths for build mode -func genBuildOutputs(conf *Config, pkgName string, multiPkg bool, cfg OutputCfg) (OutputCfg, error) { - if conf.OutFile == "" && multiPkg { - // Multiple packages, use temp file - return genTempOutputs(pkgName, cfg, conf.AppExt) - } - - // Single package build - baseName := pkgName - if conf.OutFile != "" { - baseName = conf.OutFile - } - - if cfg.OutExt != "" { - // Need format conversion: ELF -> format - if err := setupTwoStageGen(&cfg, baseName, conf.AppExt); err != nil { - return cfg, err - } - } else { - // Direct output - cfg.OutPath = baseName - if filepath.Ext(cfg.OutPath) != conf.AppExt { - cfg.OutPath += conf.AppExt - } - cfg.IntPath = cfg.OutPath - } - - return cfg, nil -} - -// genRunOutputs generates output paths for run mode -func genRunOutputs(pkgName string, cfg OutputCfg, appExt string) (OutputCfg, error) { - // Always use temp files for run mode - return genTempOutputs(pkgName, cfg, appExt) -} - -// genInstallOutputs generates output paths for install mode (flashing to device) -func genInstallOutputs(conf *Config, pkgName string, cfg OutputCfg, binExt string) (OutputCfg, error) { - // Install mode with target means flashing to device, use temp files like run mode - if binExt != "" || cfg.OutExt != "" { - // Flash to device - use temp files for firmware generation - return genTempOutputs(pkgName, cfg, conf.AppExt) - } else { - // Install to BinPath (traditional install without target) - cfg.OutPath = filepath.Join(conf.BinPath, pkgName+conf.AppExt) - cfg.IntPath = cfg.OutPath - } - - return cfg, nil -} - -// setupTwoStageGen sets up paths for two-stage generation -func setupTwoStageGen(cfg *OutputCfg, baseName, appExt string) error { - // Create temp file for intermediate ELF - tmpFile, err := os.CreateTemp("", "llgo-*"+appExt) +func genTempOutputFile(prefix, ext string) (string, error) { + tmpFile, err := os.CreateTemp("", prefix+"-*"+ext) if err != nil { - return err + return "", err } tmpFile.Close() - cfg.IntPath = tmpFile.Name() + return tmpFile.Name(), nil +} - // Set final output path - if baseName != "" { - if filepath.Ext(baseName) == cfg.OutExt { - cfg.OutPath = baseName +// setOutFmt sets the appropriate OutFmt based on format name +func setOutFmt(conf *Config, formatName string) { + switch formatName { + case "bin": + conf.OutFmts.Bin = true + case "hex": + conf.OutFmts.Hex = true + case "img": + conf.OutFmts.Img = true + case "uf2": + conf.OutFmts.Uf2 = true + case "zip": + conf.OutFmts.Zip = true + } +} + +// buildOutFmts creates OutFmtDetails based on package, configuration and multi-package status +func buildOutFmts(pkgName string, conf *Config, multiPkg bool, crossCompile *crosscompile.Export) (*OutFmtDetails, error) { + details := &OutFmtDetails{} + var err error + if conf.Target == "" { + // Native target + if conf.Mode == ModeInstall { + details.Out = filepath.Join(conf.BinPath, pkgName+conf.AppExt) + } else if conf.Mode == ModeBuild && !multiPkg && conf.OutFile != "" { + base := strings.TrimSuffix(conf.OutFile, conf.AppExt) + details.Out = base + conf.AppExt + } else if conf.Mode == ModeBuild && !multiPkg { + details.Out = pkgName + conf.AppExt } else { - cfg.OutPath = baseName + cfg.OutExt + details.Out, err = genTempOutputFile(pkgName, conf.AppExt) + if err != nil { + return nil, err + } } + return details, nil } - return nil -} - -// genTempOutputs creates temporary output file paths -func genTempOutputs(pkgName string, cfg OutputCfg, appExt string) (OutputCfg, error) { - if cfg.OutExt != "" { - // Need format conversion: create temp ELF, then convert to final format - tmpFile, err := os.CreateTemp("", "llgo-*"+appExt) + if multiPkg { + details.Out, err = genTempOutputFile(pkgName, conf.AppExt) if err != nil { - return cfg, err + return nil, err } - tmpFile.Close() - cfg.IntPath = tmpFile.Name() - - finalTmp, err := os.CreateTemp("", pkgName+"-*"+cfg.OutExt) - if err != nil { - return cfg, err - } - finalTmp.Close() - cfg.OutPath = finalTmp.Name() + } else if conf.OutFile != "" { + base := strings.TrimSuffix(conf.OutFile, conf.AppExt) + details.Out = base + conf.AppExt + } else if conf.Mode == ModeBuild { + details.Out = pkgName + conf.AppExt } else { - // Direct output - tmpFile, err := os.CreateTemp("", pkgName+"-*"+appExt) + details.Out, err = genTempOutputFile(pkgName, conf.AppExt) if err != nil { - return cfg, err + return nil, err } - tmpFile.Close() - cfg.OutPath = tmpFile.Name() - cfg.IntPath = cfg.OutPath } - return cfg, nil + // Check emulator format if emulator mode is enabled + outFmt := "" + if conf.Emulator { + if crossCompile.Emulator != "" { + outFmt = firmware.ExtractFileFormatFromCommand(crossCompile.Emulator) + } + } else { + if crossCompile.Device.Flash.Method == "command" { + outFmt = firmware.ExtractFileFormatFromCommand(crossCompile.Device.Flash.Command) + } + } + if outFmt != "" { + setOutFmt(conf, outFmt) + } + + // Check binary format and set corresponding format + if crossCompile.BinaryFormat != "" && slices.Contains([]Mode{ModeRun, ModeTest, ModeCmpTest, ModeInstall}, conf.Mode) { + envName := firmware.BinaryFormatToEnvName(crossCompile.BinaryFormat) + if envName != "" { + setOutFmt(conf, envName) + } + } + + // Generate format-specific paths based on base output + if details.Out != "" { + base := strings.TrimSuffix(details.Out, filepath.Ext(details.Out)) + + if conf.OutFmts.Bin || conf.OutFmts.Img { + details.Bin = base + ".bin" + } + if conf.OutFmts.Hex { + details.Bin = base + ".bin" // hex depends on bin + details.Hex = base + ".hex" + } + if conf.OutFmts.Img { + details.Bin = base + ".bin" // img depends on bin + details.Img = base + ".img" + } + if conf.OutFmts.Uf2 { + details.Uf2 = base + ".uf2" + } + if conf.OutFmts.Zip { + details.Zip = base + ".zip" + } + } + + return details, nil +} + +// ToEnvMap converts OutFmtDetails to a map for template substitution +func (details *OutFmtDetails) ToEnvMap() map[string]string { + envMap := make(map[string]string) + + if details.Out != "" { + envMap[""] = details.Out + envMap["out"] = details.Out + envMap["elf"] = details.Out // alias for compatibility + } + if details.Bin != "" { + envMap["bin"] = details.Bin + } + if details.Hex != "" { + envMap["hex"] = details.Hex + } + if details.Img != "" { + envMap["img"] = details.Img + } + if details.Uf2 != "" { + envMap["uf2"] = details.Uf2 + } + if details.Zip != "" { + envMap["zip"] = details.Zip + } + + return envMap } diff --git a/internal/build/outputs_test.go b/internal/build/outputs_test.go index 5f5a528f..e76b33d3 100644 --- a/internal/build/outputs_test.go +++ b/internal/build/outputs_test.go @@ -4,43 +4,26 @@ package build import ( + "strings" "testing" "github.com/goplus/llgo/internal/crosscompile" "github.com/goplus/llgo/internal/flash" ) -func TestGenOutputs(t *testing.T) { +func TestBuildOutFmtsWithTarget(t *testing.T) { tests := []struct { - name string - conf *Config - pkgName string - multiPkg bool - crossCompile *crosscompile.Export - wantOutPath string // use empty string to indicate temp file - wantIntPath string // use empty string to indicate same as outPath - wantOutExt string - wantFileFmt string - wantBinFmt string - wantDirectGen bool + name string + conf *Config + pkgName string + crossCompile crosscompile.Export + wantOut string // use empty string to indicate temp file + wantBin string + wantHex string + wantImg string + wantUf2 string + wantZip string }{ - { - name: "build without target", - conf: &Config{ - Mode: ModeBuild, - BinPath: "/go/bin", - AppExt: "", - }, - pkgName: "hello", - crossCompile: &crosscompile.Export{ - BinaryFormat: "", - }, - wantOutPath: "hello", - wantOutExt: "", - wantFileFmt: "", - wantBinFmt: "", - wantDirectGen: true, - }, { name: "build with -o", conf: &Config{ @@ -49,273 +32,53 @@ func TestGenOutputs(t *testing.T) { AppExt: "", }, pkgName: "hello", - crossCompile: &crosscompile.Export{ + crossCompile: crosscompile.Export{ BinaryFormat: "", }, - wantOutPath: "myapp", - wantOutExt: "", - wantFileFmt: "", - wantBinFmt: "", - wantDirectGen: true, + wantOut: "myapp", }, { - name: "build with target and file-format", + name: "build hex format", conf: &Config{ - Mode: ModeBuild, - BinPath: "/go/bin", - AppExt: "", - FileFormat: "bin", - Target: "esp32", + Mode: ModeBuild, + Target: "esp32", + AppExt: ".elf", + OutFmts: OutFmts{Hex: true}, }, pkgName: "hello", - crossCompile: &crosscompile.Export{ - BinaryFormat: "esp32", + crossCompile: crosscompile.Export{ + BinaryFormat: "esp32", // This will auto-set Bin: true }, - wantOutPath: "hello.bin", - wantOutExt: ".bin", - wantFileFmt: "bin", - wantBinFmt: "esp32", - wantDirectGen: true, + wantOut: "hello.elf", + wantBin: "hello.bin", // Now expected due to esp32 BinaryFormat + wantHex: "hello.hex", }, { - name: "build with target, -o and file-format", - conf: &Config{ - Mode: ModeBuild, - OutFile: "myapp", - AppExt: "", - FileFormat: "hex", - Target: "esp32", - }, - pkgName: "hello", - crossCompile: &crosscompile.Export{ - BinaryFormat: "esp32", - }, - wantOutPath: "myapp.hex", - wantOutExt: ".hex", - wantFileFmt: "hex", - wantBinFmt: "esp32", - wantDirectGen: false, - }, - { - name: "build with target, -o has correct extension and file-format", - conf: &Config{ - Mode: ModeBuild, - OutFile: "myapp.hex", - AppExt: "", - FileFormat: "hex", - Target: "esp32", - }, - pkgName: "hello", - crossCompile: &crosscompile.Export{ - BinaryFormat: "esp32", - }, - wantOutPath: "myapp.hex", - wantOutExt: ".hex", - wantFileFmt: "hex", - wantBinFmt: "esp32", - wantDirectGen: false, - }, - { - name: "run without target", - conf: &Config{ - Mode: ModeRun, - AppExt: "", - }, - pkgName: "hello", - crossCompile: &crosscompile.Export{ - BinaryFormat: "", - }, - wantOutPath: "", // temp file - wantOutExt: "", - wantFileFmt: "", - wantBinFmt: "", - wantDirectGen: true, - }, - { - name: "run with target", - conf: &Config{ - Mode: ModeRun, - AppExt: "", - Target: "esp32", - }, - pkgName: "hello", - crossCompile: &crosscompile.Export{ - BinaryFormat: "esp32", - }, - wantOutPath: "", // temp file - wantOutExt: "", - wantFileFmt: "", - wantBinFmt: "esp32", - wantDirectGen: true, - }, - { - name: "run with target and emulator", + name: "emulator mode with bin format", conf: &Config{ Mode: ModeRun, - AppExt: "", Target: "esp32", + AppExt: ".elf", Emulator: true, }, pkgName: "hello", - crossCompile: &crosscompile.Export{ + crossCompile: crosscompile.Export{ BinaryFormat: "esp32", - Emulator: "qemu-system-xtensa -machine esp32 -drive file={hex},if=mtd,format=raw", + Emulator: "qemu-system-xtensa -machine esp32 -kernel {bin}", }, - wantOutPath: "", // temp file - wantOutExt: ".hex", - wantFileFmt: "hex", - wantBinFmt: "esp32", - wantDirectGen: false, + wantBin: ".bin", // Should be temp file with .bin extension }, { - name: "build with img file-format", + name: "flash command with hex format", conf: &Config{ - Mode: ModeBuild, - BinPath: "/go/bin", - AppExt: "", - FileFormat: "img", - Target: "esp32", + Mode: ModeInstall, + Target: "esp32", + AppExt: ".elf", + Emulator: false, }, pkgName: "hello", - crossCompile: &crosscompile.Export{ - BinaryFormat: "esp32", - }, - wantOutPath: "hello.img", - wantOutExt: ".img", - wantFileFmt: "img", - wantBinFmt: "esp32-img", - wantDirectGen: true, - }, - { - name: "test without target", - conf: &Config{ - Mode: ModeTest, - AppExt: "", - }, - pkgName: "hello", - crossCompile: &crosscompile.Export{ - BinaryFormat: "", - }, - wantOutPath: "", // temp file - wantOutExt: "", - wantFileFmt: "", - wantBinFmt: "", - wantDirectGen: true, - }, - { - name: "test with target", - conf: &Config{ - Mode: ModeTest, - AppExt: "", - Target: "esp32", - }, - pkgName: "hello", - crossCompile: &crosscompile.Export{ - BinaryFormat: "esp32", - }, - wantOutPath: "", // temp file - wantOutExt: "", - wantFileFmt: "", - wantBinFmt: "esp32", - wantDirectGen: true, - }, - { - name: "cmptest without target", - conf: &Config{ - Mode: ModeCmpTest, - AppExt: "", - }, - pkgName: "hello", - crossCompile: &crosscompile.Export{ - BinaryFormat: "", - }, - wantOutPath: "", // temp file - wantOutExt: "", - wantFileFmt: "", - wantBinFmt: "", - wantDirectGen: true, - }, - { - name: "install without target", - conf: &Config{ - Mode: ModeInstall, - BinPath: "/go/bin", - AppExt: "", - }, - pkgName: "hello", - crossCompile: &crosscompile.Export{ - BinaryFormat: "", - }, - wantOutPath: "/go/bin/hello", - wantOutExt: "", - wantFileFmt: "", - wantBinFmt: "", - wantDirectGen: true, - }, - { - name: "install with esp32 target (flash to device)", - conf: &Config{ - Mode: ModeInstall, - BinPath: "/go/bin", - AppExt: "", - Target: "esp32", - }, - pkgName: "hello", - crossCompile: &crosscompile.Export{ - BinaryFormat: "esp32", - }, - wantOutPath: "", // temp file for flashing - wantOutExt: "", - wantFileFmt: "", - wantBinFmt: "esp32", - wantDirectGen: true, - }, - { - name: "install with file format (flash to device)", - conf: &Config{ - Mode: ModeInstall, - BinPath: "/go/bin", - AppExt: "", - FileFormat: "hex", - Target: "esp32", - }, - pkgName: "hello", - crossCompile: &crosscompile.Export{ - BinaryFormat: "esp32", - }, - wantOutPath: "", // temp file for flashing - wantOutExt: ".hex", - wantFileFmt: "hex", - wantBinFmt: "esp32", - wantDirectGen: false, - }, - { - name: "run with target non-emulator (should use .bin from binary format)", - conf: &Config{ - Mode: ModeRun, - AppExt: "", - Target: "esp32", - }, - pkgName: "hello", - crossCompile: &crosscompile.Export{ - BinaryFormat: "esp32", - }, - wantOutPath: "", // temp file - wantOutExt: "", - wantFileFmt: "", - wantBinFmt: "esp32", - wantDirectGen: true, - }, - { - name: "run with flash method command - extract hex from command", - conf: &Config{ - Mode: ModeRun, - AppExt: "", - Target: "esp32", - }, - pkgName: "hello", - crossCompile: &crosscompile.Export{ - BinaryFormat: "esp32", + crossCompile: crosscompile.Export{ + BinaryFormat: "esp32", // This will auto-set Bin: true Device: flash.Device{ Flash: flash.Flash{ Method: "command", @@ -323,373 +86,280 @@ func TestGenOutputs(t *testing.T) { }, }, }, - wantOutPath: "", // temp file - wantOutExt: ".hex", - wantFileFmt: "hex", - wantBinFmt: "esp32", - wantDirectGen: false, + wantBin: ".bin", // Expected due to esp32 BinaryFormat + wantHex: ".hex", // Should be temp file with .hex extension }, { - name: "run with flash method command - extract bin from command", + name: "multiple formats specified", conf: &Config{ - Mode: ModeRun, - AppExt: "", + Mode: ModeBuild, Target: "esp32", + AppExt: ".elf", + OutFmts: OutFmts{ + Bin: true, + Hex: true, + Img: true, + Uf2: true, + }, }, pkgName: "hello", - crossCompile: &crosscompile.Export{ + crossCompile: crosscompile.Export{ BinaryFormat: "esp32", - Device: flash.Device{ - Flash: flash.Flash{ - Method: "command", - Command: "esptool.py --chip esp32 write_flash 0x10000 {bin}", - }, - }, }, - wantOutPath: "", // temp file - wantOutExt: ".bin", - wantFileFmt: "bin", - wantBinFmt: "esp32", - wantDirectGen: true, + wantOut: "hello.elf", + wantBin: "hello.bin", + wantHex: "hello.hex", + wantImg: "hello.img", + wantUf2: "hello.uf2", + wantZip: "", // Not specified }, { - name: "run with flash method openocd - should use .hex", + name: "no formats specified", conf: &Config{ - Mode: ModeRun, - AppExt: "", - Target: "stm32", + Mode: ModeBuild, + Target: "esp32", + AppExt: ".elf", + OutFmts: OutFmts{}, // No formats specified }, pkgName: "hello", - crossCompile: &crosscompile.Export{ - BinaryFormat: "arm", - Device: flash.Device{ - Flash: flash.Flash{ - Method: "openocd", - }, - }, + crossCompile: crosscompile.Export{ + BinaryFormat: "esp32", }, - wantOutPath: "", // temp file - wantOutExt: ".hex", - wantFileFmt: "hex", - wantBinFmt: "arm", - wantDirectGen: false, - }, - { - name: "run with flash method msd - extract extension from firmware name", - conf: &Config{ - Mode: ModeRun, - AppExt: "", - Target: "rp2040", - }, - pkgName: "hello", - crossCompile: &crosscompile.Export{ - BinaryFormat: "uf2", - Device: flash.Device{ - Flash: flash.Flash{ - Method: "msd", - }, - MSD: flash.MSD{ - FirmwareName: "firmware.uf2", - }, - }, - }, - wantOutPath: "", // temp file - wantOutExt: ".uf2", - wantFileFmt: "uf2", - wantBinFmt: "uf2", - wantDirectGen: true, - }, - { - name: "run with flash method bmp - should use .elf", - conf: &Config{ - Mode: ModeRun, - AppExt: "", - Target: "stm32", - }, - pkgName: "hello", - crossCompile: &crosscompile.Export{ - BinaryFormat: "arm", - Device: flash.Device{ - Flash: flash.Flash{ - Method: "bmp", - }, - }, - }, - wantOutPath: "", // temp file - wantOutExt: ".elf", - wantFileFmt: "elf", - wantBinFmt: "arm", - wantDirectGen: false, + wantOut: "hello.elf", + wantBin: "", // No bin file should be generated + wantHex: "", // No hex file should be generated }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result, err := genOutputs(tt.conf, tt.pkgName, tt.multiPkg, tt.crossCompile) + // Determine if multi-package (mock single package for simplicity) + multiPkg := false + + result, err := buildOutFmts(tt.pkgName, tt.conf, multiPkg, &tt.crossCompile) if err != nil { - t.Fatalf("GenOutputs() error = %v", err) + t.Errorf("buildOutFmts() error = %v", err) + return } - if tt.wantOutExt != result.OutExt { - t.Errorf("GenOutputs() OutExt = %v, want %v", result.OutExt, tt.wantOutExt) - } - - if tt.wantFileFmt != result.FileFmt { - t.Errorf("GenOutputs() FileFmt = %v, want %v", result.FileFmt, tt.wantFileFmt) - } - - if tt.wantBinFmt != result.BinFmt { - t.Errorf("GenOutputs() BinFmt = %v, want %v", result.BinFmt, tt.wantBinFmt) - } - - if tt.wantDirectGen != result.DirectGen { - t.Errorf("GenOutputs() DirectGen = %v, want %v", result.DirectGen, tt.wantDirectGen) - } - - if tt.wantOutPath != "" { - // Check exact match for non-temp files - if result.OutPath != tt.wantOutPath { - t.Errorf("GenOutputs() OutPath = %v, want %v", result.OutPath, tt.wantOutPath) + // Check base output path + if tt.wantOut != "" { + if result.Out != tt.wantOut { + t.Errorf("buildOutFmts().Out = %v, want %v", result.Out, tt.wantOut) } } else { - // Check temp file pattern for temp files - if result.OutPath == "" { - t.Errorf("GenOutputs() OutPath should not be empty for temp file") + // Should be a temp file + if result.Out == "" || !strings.Contains(result.Out, tt.pkgName) { + t.Errorf("buildOutFmts().Out should be temp file containing %v, got %v", tt.pkgName, result.Out) } } - // Check IntPath logic - if tt.wantIntPath != "" { - // Check exact IntPath match when specified - if result.IntPath != tt.wantIntPath { - t.Errorf("GenOutputs() IntPath = %v, want %v", result.IntPath, tt.wantIntPath) - } - } else if tt.wantOutExt != "" && !tt.wantDirectGen { - // Should have different IntPath for format conversion - if result.IntPath == result.OutPath { - t.Errorf("GenOutputs() IntPath should be different from OutPath when format conversion is needed") - } - } else if tt.conf.Mode == ModeRun && tt.wantOutExt == "" { - // Run mode without conversion should have same IntPath and OutPath - if result.IntPath != result.OutPath { - t.Errorf("GenOutputs() IntPath should equal OutPath for run mode without conversion") - } - } else if tt.conf.Mode == ModeInstall { - // Install mode: check based on whether it's device flashing or traditional install - isDeviceFlash := tt.conf.Target != "" || tt.wantOutExt != "" - if isDeviceFlash { - // Device flashing - should use temp files (like run mode) - if result.OutPath == "" { - // This is expected for temp files, no additional check needed + // Check format-specific paths + checkFormatPath := func(actual, expected, formatName string) { + if expected == "" { + // Empty means no file should be generated + if actual != "" { + t.Errorf("buildOutFmts().%s = %v, want empty (no file generated)", formatName, actual) + } + } else if strings.HasPrefix(expected, ".") && !strings.Contains(expected[1:], ".") { + // ".xxx" means temp file with .xxx extension + if actual == "" { + t.Errorf("buildOutFmts().%s = empty, want temp file with %s extension", formatName, expected) + } else if !strings.HasSuffix(actual, expected) || !strings.Contains(actual, tt.pkgName) { + t.Errorf("buildOutFmts().%s should be temp file with %s extension containing %v, got %v", formatName, expected, tt.pkgName, actual) } } else { - // Traditional install to BinPath - should have fixed paths - if result.IntPath != result.OutPath { - t.Errorf("GenOutputs() IntPath should equal OutPath for traditional install mode") + // "aaa.xxx" means exact file name + if actual != expected { + t.Errorf("buildOutFmts().%s = %v, want %v", formatName, actual, expected) } } } + + checkFormatPath(result.Bin, tt.wantBin, "Bin") + checkFormatPath(result.Hex, tt.wantHex, "Hex") + checkFormatPath(result.Img, tt.wantImg, "Img") + checkFormatPath(result.Uf2, tt.wantUf2, "Uf2") + checkFormatPath(result.Zip, tt.wantZip, "Zip") }) } } -func TestDetermineFormat(t *testing.T) { +func TestBuildOutFmtsNativeTarget(t *testing.T) { tests := []struct { - name string - conf *Config - crossCompile *crosscompile.Export - wantFmt string - wantExt string + name string + mode Mode + multiPkg bool + outFile string + binPath string + appExt string + goos string + pkgName string + wantOut string }{ + // ModeBuild scenarios { - name: "user specified format", - conf: &Config{FileFormat: "hex"}, - crossCompile: &crosscompile.Export{}, - wantFmt: "hex", - wantExt: ".hex", + name: "build single pkg no outfile macos", + mode: ModeBuild, + multiPkg: false, + outFile: "", + appExt: "", + goos: "darwin", + pkgName: "hello", + wantOut: "hello", }, { - name: "emulator format extraction", - conf: &Config{Mode: ModeRun, Emulator: true}, - crossCompile: &crosscompile.Export{ - Emulator: "qemu-system-xtensa -machine esp32 -drive file={bin},if=mtd,format=raw", - }, - wantFmt: "bin", - wantExt: ".bin", + name: "build single pkg no outfile linux", + mode: ModeBuild, + multiPkg: false, + outFile: "", + appExt: "", + goos: "linux", + pkgName: "hello", + wantOut: "hello", }, { - name: "flash method command - extract hex", - conf: &Config{Mode: ModeRun, Target: "esp32"}, - crossCompile: &crosscompile.Export{ - Device: flash.Device{ - Flash: flash.Flash{ - Method: "command", - Command: "esptool.py --chip esp32 write_flash 0x10000 {hex}", - }, - }, - }, - wantFmt: "hex", - wantExt: ".hex", + name: "build single pkg no outfile windows", + mode: ModeBuild, + multiPkg: false, + outFile: "", + appExt: ".exe", + goos: "windows", + pkgName: "hello", + wantOut: "hello.exe", }, { - name: "flash method command - extract bin", - conf: &Config{Mode: ModeRun, Target: "esp32"}, - crossCompile: &crosscompile.Export{ - Device: flash.Device{ - Flash: flash.Flash{ - Method: "command", - Command: "esptool.py --chip esp32 write_flash 0x10000 {bin}", - }, - }, - }, - wantFmt: "bin", - wantExt: ".bin", + name: "build single pkg with outfile", + mode: ModeBuild, + multiPkg: false, + outFile: "myapp", + appExt: "", + goos: "linux", + pkgName: "hello", + wantOut: "myapp", }, { - name: "flash method openocd", - conf: &Config{Mode: ModeRun, Target: "stm32"}, - crossCompile: &crosscompile.Export{ - Device: flash.Device{ - Flash: flash.Flash{ - Method: "openocd", - }, - }, - }, - wantFmt: "hex", - wantExt: ".hex", + name: "build single pkg with outfile and ext", + mode: ModeBuild, + multiPkg: false, + outFile: "myapp.exe", + appExt: ".exe", + goos: "windows", + pkgName: "hello", + wantOut: "myapp.exe", }, { - name: "flash method msd - extract from firmware name", - conf: &Config{Mode: ModeRun, Target: "rp2040"}, - crossCompile: &crosscompile.Export{ - Device: flash.Device{ - Flash: flash.Flash{ - Method: "msd", - }, - MSD: flash.MSD{ - FirmwareName: "firmware.uf2", - }, - }, - }, - wantFmt: "uf2", - wantExt: ".uf2", + name: "build multi pkg", + mode: ModeBuild, + multiPkg: true, + outFile: "", + appExt: "", + goos: "linux", + pkgName: "hello", + wantOut: "", // Should be temp file + }, + // ModeInstall scenarios + { + name: "install single pkg macos", + mode: ModeInstall, + multiPkg: false, + outFile: "", + binPath: "/go/bin", + appExt: "", + goos: "darwin", + pkgName: "hello", + wantOut: "/go/bin/hello", }, { - name: "flash method bmp", - conf: &Config{Mode: ModeRun, Target: "stm32"}, - crossCompile: &crosscompile.Export{ - Device: flash.Device{ - Flash: flash.Flash{ - Method: "bmp", - }, - }, - }, - wantFmt: "elf", - wantExt: ".elf", + name: "install single pkg windows", + mode: ModeInstall, + multiPkg: false, + outFile: "", + binPath: "C:/go/bin", // Use forward slashes for cross-platform compatibility + appExt: ".exe", + goos: "windows", + pkgName: "hello", + wantOut: "C:/go/bin/hello.exe", }, { - name: "no format", - conf: &Config{}, - crossCompile: &crosscompile.Export{}, - wantFmt: "", - wantExt: "", + name: "install multi pkg", + mode: ModeInstall, + multiPkg: true, + outFile: "", + binPath: "/go/bin", + appExt: "", + goos: "linux", + pkgName: "hello", + wantOut: "/go/bin/hello", + }, + // Other modes should use temp files + { + name: "run mode", + mode: ModeRun, + multiPkg: false, + outFile: "", + appExt: "", + goos: "linux", + pkgName: "hello", + wantOut: "", // Should be temp file + }, + { + name: "test mode", + mode: ModeTest, + multiPkg: false, + outFile: "", + appExt: "", + goos: "linux", + pkgName: "hello", + wantOut: "", // Should be temp file }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gotFmt, gotExt := determineFormat(tt.conf, tt.crossCompile) - if gotFmt != tt.wantFmt { - t.Errorf("determineFormat() format = %v, want %v", gotFmt, tt.wantFmt) + conf := &Config{ + Mode: tt.mode, + OutFile: tt.outFile, + BinPath: tt.binPath, + AppExt: tt.appExt, + Target: "", // Native target } - if gotExt != tt.wantExt { - t.Errorf("determineFormat() ext = %v, want %v", gotExt, tt.wantExt) - } - }) - } -} - -func TestShouldDirectGen(t *testing.T) { - tests := []struct { - name string - outExt string - binExt string - want bool - }{ - {"no extension", "", ".bin", true}, - {"same extension", ".bin", ".bin", true}, - {"img format", ".img", ".bin", true}, - {"different extension", ".hex", ".bin", false}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := shouldDirectGen(tt.outExt, tt.binExt); got != tt.want { - t.Errorf("shouldDirectGen() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestNeedsFwGen(t *testing.T) { - tests := []struct { - name string - conf *Config - outExt string - binExt string - want bool - }{ - { - name: "build mode with file format", - conf: &Config{Mode: ModeBuild, FileFormat: "hex"}, - outExt: ".hex", - want: true, - }, - { - name: "build mode without file format", - conf: &Config{Mode: ModeBuild}, - outExt: "", - want: false, - }, - { - name: "run mode with emulator", - conf: &Config{Mode: ModeRun, Emulator: true}, - outExt: ".hex", - want: true, - }, - { - name: "run mode with binExt", - conf: &Config{Mode: ModeRun}, - outExt: "", - binExt: ".bin", - want: true, - }, - { - name: "test mode with emulator", - conf: &Config{Mode: ModeTest, Emulator: true}, - outExt: ".hex", - want: true, - }, - { - name: "test mode with binExt", - conf: &Config{Mode: ModeTest}, - outExt: "", - binExt: ".bin", - want: true, - }, - { - name: "cmptest mode with binExt", - conf: &Config{Mode: ModeCmpTest}, - outExt: "", - binExt: ".bin", - want: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := needsFwGen(tt.conf, tt.outExt, tt.binExt); got != tt.want { - t.Errorf("needsFwGen() = %v, want %v", got, tt.want) + + crossCompile := &crosscompile.Export{} + + result, err := buildOutFmts(tt.pkgName, conf, tt.multiPkg, crossCompile) + if err != nil { + t.Errorf("buildOutFmts() error = %v", err) + return + } + + // Check base output path + if tt.wantOut != "" { + if result.Out != tt.wantOut { + t.Errorf("buildOutFmts().Out = %v, want %v", result.Out, tt.wantOut) + } + } else { + // Should be a temp file + if result.Out == "" || !strings.Contains(result.Out, tt.pkgName) { + t.Errorf("buildOutFmts().Out should be temp file containing %v, got %v", tt.pkgName, result.Out) + } + } + + // For native targets, no format files should be generated + if result.Bin != "" { + t.Errorf("buildOutFmts().Bin = %v, want empty for native target", result.Bin) + } + if result.Hex != "" { + t.Errorf("buildOutFmts().Hex = %v, want empty for native target", result.Hex) + } + if result.Img != "" { + t.Errorf("buildOutFmts().Img = %v, want empty for native target", result.Img) + } + if result.Uf2 != "" { + t.Errorf("buildOutFmts().Uf2 = %v, want empty for native target", result.Uf2) + } + if result.Zip != "" { + t.Errorf("buildOutFmts().Zip = %v, want empty for native target", result.Zip) } }) } diff --git a/internal/build/run.go b/internal/build/run.go index fc7d392d..824af3fc 100644 --- a/internal/build/run.go +++ b/internal/build/run.go @@ -85,7 +85,7 @@ func runNative(ctx *context, app, pkgDir, pkgName string, conf *Config, mode Mod return nil } -func runInEmulator(emulator, app, pkgDir, pkgName string, conf *Config, mode Mode, verbose bool) error { +func runInEmulator(emulator string, envMap map[string]string, pkgDir, pkgName string, conf *Config, mode Mode, verbose bool) error { if emulator == "" { return fmt.Errorf("target %s does not have emulator configured", conf.Target) } @@ -95,31 +95,21 @@ func runInEmulator(emulator, app, pkgDir, pkgName string, conf *Config, mode Mod switch mode { case ModeRun: - return runEmuCmd(app, emulator, conf.RunArgs, verbose) + return runEmuCmd(envMap, emulator, conf.RunArgs, verbose) case ModeTest: - return runEmuCmd(app, emulator, conf.RunArgs, verbose) + return runEmuCmd(envMap, emulator, conf.RunArgs, verbose) case ModeCmpTest: - cmpTest(pkgDir, pkgName, app, conf.GenExpect, conf.RunArgs) + cmpTest(pkgDir, pkgName, envMap["out"], 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, - } - +func runEmuCmd(envMap map[string]string, emulatorTemplate string, runArgs []string, verbose bool) error { // Expand the emulator command template emulatorCmd := emulatorTemplate - for placeholder, path := range envs { + for placeholder, path := range envMap { var target string if placeholder == "" { target = "{}" diff --git a/internal/firmware/env.go b/internal/firmware/env.go new file mode 100644 index 00000000..6b6db553 --- /dev/null +++ b/internal/firmware/env.go @@ -0,0 +1,16 @@ +package firmware + +import "strings" + +// BinaryFormatToEnvName returns the environment variable name based on the binary format +// Returns the format name for template expansion (e.g., "bin", "uf2", "zip") +func BinaryFormatToEnvName(binaryFormat string) string { + if strings.HasPrefix(binaryFormat, "esp") { + return "bin" + } else if strings.HasPrefix(binaryFormat, "uf2") { + return "uf2" + } else if strings.HasPrefix(binaryFormat, "nrf-dfu") { + return "zip" + } + return "" +} diff --git a/internal/firmware/ext.go b/internal/firmware/ext.go deleted file mode 100644 index 3a90ca97..00000000 --- a/internal/firmware/ext.go +++ /dev/null @@ -1,16 +0,0 @@ -package firmware - -import "strings" - -// BinaryExt returns the binary file extension based on the binary format -// Returns ".bin" for ESP-based formats, "" for others -func BinaryExt(binaryFormat string) string { - if strings.HasPrefix(binaryFormat, "esp") { - return ".bin" - } else if strings.HasPrefix(binaryFormat, "uf2") { - return ".uf2" - } else if strings.HasPrefix(binaryFormat, "nrf-dfu") { - return ".zip" - } - return "" -} diff --git a/internal/firmware/ext_test.go b/internal/firmware/ext_test.go deleted file mode 100644 index cfbe1f13..00000000 --- a/internal/firmware/ext_test.go +++ /dev/null @@ -1,31 +0,0 @@ -//go:build !llgo -// +build !llgo - -package firmware - -import "testing" - -func TestBinaryExt(t *testing.T) { - tests := []struct { - name string - binaryFormat string - expected string - }{ - {"ESP32", "esp32", ".bin"}, - {"ESP8266", "esp8266", ".bin"}, - {"ESP32C3", "esp32c3", ".bin"}, - {"UF2", "uf2", ".uf2"}, - {"ELF", "elf", ""}, - {"Empty", "", ""}, - {"NRF-DFU", "nrf-dfu", ".zip"}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := BinaryExt(tt.binaryFormat) - if result != tt.expected { - t.Errorf("BinaryExt() = %q, want %q", result, tt.expected) - } - }) - } -} diff --git a/internal/firmware/firmware.go b/internal/firmware/firmware.go index 02a35108..0528378c 100644 --- a/internal/firmware/firmware.go +++ b/internal/firmware/firmware.go @@ -20,12 +20,12 @@ func MakeFirmwareImage(infile, outfile, format, fmtDetail string) error { return fmt.Errorf("unsupported firmware format: %s", format) } -// ExtractFileFormatFromEmulator extracts file format from emulator command template +// ExtractFileFormatFromCommand extracts file format from command template // Returns the format if found (e.g. "bin", "hex", "zip", "img"), empty string if not found -func ExtractFileFormatFromEmulator(emulatorCmd string) string { +func ExtractFileFormatFromCommand(cmd string) string { formats := []string{"bin", "hex", "zip", "img", "uf2"} for _, format := range formats { - if strings.Contains(emulatorCmd, "{"+format+"}") { + if strings.Contains(cmd, "{"+format+"}") { return format } } @@ -52,21 +52,52 @@ func GetFileExtFromFormat(format string) string { } } -// ConvertOutput converts a binary file to the specified format. -// If binaryFormat == fileFormat, no conversion is needed. -// Otherwise, only hex format conversion is supported. -func ConvertOutput(infile, outfile, binaryFormat, fileFormat string) error { - // If formats match, no conversion needed - if binaryFormat == fileFormat { - return nil +// ConvertFormats processes format conversions for embedded targets only +func ConvertFormats(binFmt, fmtDetail string, envMap map[string]string) error { + fmt.Printf("Converting formats based on binary format: %s\n", binFmt) + fmt.Printf("Format details: %s\n", fmtDetail) + fmt.Printf("Environment map: %+v\n", envMap) + // Convert to bin format first (needed for img) + if envMap["bin"] != "" { + err := MakeFirmwareImage(envMap["out"], envMap["bin"], binFmt, fmtDetail) + if err != nil { + return fmt.Errorf("failed to convert to bin format: %w", err) + } } - // Only support conversion to hex and format - if fileFormat == "hex" { - return convertToHex(infile, outfile) + // Convert to hex format + if envMap["hex"] != "" { + err := MakeFirmwareImage(envMap["out"], envMap["hex"], binFmt, fmtDetail) + if err != nil { + return fmt.Errorf("failed to convert to hex format: %w", err) + } } - return fmt.Errorf("unsupported format conversion from %s to %s", binaryFormat, fileFormat) + // Convert to img format (depends on bin) + if envMap["img"] != "" { + err := MakeFirmwareImage(envMap["out"], envMap["img"], binFmt+"-img", fmtDetail) + if err != nil { + return fmt.Errorf("failed to convert to img format: %w", err) + } + } + + // Convert to uf2 format + if envMap["uf2"] != "" { + err := MakeFirmwareImage(envMap["out"], envMap["uf2"], binFmt, fmtDetail) + if err != nil { + return fmt.Errorf("failed to convert to uf2 format: %w", err) + } + } + + // Convert to zip format + if envMap["zip"] != "" { + err := MakeFirmwareImage(envMap["out"], envMap["zip"], binFmt, fmtDetail) + if err != nil { + return fmt.Errorf("failed to convert to zip format: %w", err) + } + } + + return nil } // convertToHex converts binary file to hex format (each byte as two hex characters) diff --git a/internal/flash/flash.go b/internal/flash/flash.go index 5d20b029..b4e790f0 100644 --- a/internal/flash/flash.go +++ b/internal/flash/flash.go @@ -192,7 +192,7 @@ func touchSerialPortAt1200bps(port string) (err error) { } // FlashDevice flashes firmware to a device based on the device configuration -func FlashDevice(device Device, app string, port string, verbose bool) error { +func FlashDevice(device Device, envMap map[string]string, port string, verbose bool) error { method := device.Flash.Method if method == "" { method = "command" @@ -208,7 +208,7 @@ func FlashDevice(device Device, app string, port string, verbose bool) error { } if verbose { - fmt.Fprintf(os.Stderr, "Flashing %s using method: %s\n", app, method) + fmt.Fprintf(os.Stderr, "Flashing using method: %s\n", method) fmt.Fprintf(os.Stderr, "Using port: %s\n", port) } @@ -226,26 +226,26 @@ func FlashDevice(device Device, app string, port string, verbose bool) error { switch method { case "command": - return flashCommand(device.Flash, app, port, verbose) + return flashCommand(device.Flash, envMap, port, verbose) case "openocd": - return flashOpenOCD(device.OpenOCD, app, verbose) + return flashOpenOCD(device.OpenOCD, envMap, verbose) case "msd": - return flashMSD(device.MSD, app, verbose) + return flashMSD(device.MSD, envMap, verbose) case "bmp": - return flashBMP(app, verbose) + return flashBMP(envMap, port, verbose) default: return fmt.Errorf("unsupported flash method: %s", method) } } // flashCommand handles command-based flashing -func flashCommand(flash Flash, app string, port string, verbose bool) error { +func flashCommand(flash Flash, envMap map[string]string, port string, verbose bool) error { if flash.Command == "" { return fmt.Errorf("flash command not specified") } // Build environment map for template variable expansion - envs := buildFlashEnvMap(app, port) + envs := buildFlashEnvMap(envMap, port) // Expand template variables in command expandedCommand := expandEnv(flash.Command, envs) @@ -269,7 +269,7 @@ func flashCommand(flash Flash, app string, port string, verbose bool) error { } // flashOpenOCD handles OpenOCD-based flashing -func flashOpenOCD(openocd OpenOCD, app string, verbose bool) error { +func flashOpenOCD(openocd OpenOCD, envMap map[string]string, verbose bool) error { if openocd.Interface == "" { return fmt.Errorf("OpenOCD interface not specified") } @@ -290,7 +290,7 @@ func flashOpenOCD(openocd OpenOCD, app string, verbose bool) error { args = append(args, "-c", "init", "-c", "reset init", - "-c", fmt.Sprintf("flash write_image erase %s", app), + "-c", fmt.Sprintf("flash write_image erase %s", envMap["elf"]), "-c", "reset", "-c", "shutdown", ) @@ -307,7 +307,7 @@ func flashOpenOCD(openocd OpenOCD, app string, verbose bool) error { } // flashMSD handles Mass Storage Device flashing -func flashMSD(msd MSD, app string, verbose bool) error { +func flashMSD(msd MSD, envMap map[string]string, verbose bool) error { if len(msd.VolumeName) == 0 { return fmt.Errorf("MSD volume names not specified") } @@ -363,24 +363,24 @@ func flashMSD(msd MSD, app string, verbose bool) error { destPath := filepath.Join(mountPoint, msd.FirmwareName) if verbose { - fmt.Fprintf(os.Stderr, "Copying %s to %s\n", app, destPath) + fmt.Fprintf(os.Stderr, "Copying %s to %s\n", envMap["uf2"], destPath) } - return copyFile(app, destPath) + return copyFile(envMap["uf2"], destPath) } // flashBMP handles Black Magic Probe flashing -func flashBMP(app string, verbose bool) error { +func flashBMP(envMap map[string]string, port string, verbose bool) error { // BMP typically uses GDB for flashing args := []string{ - "-ex", "target extended-remote /dev/ttyACM0", // Default BMP port + "-ex", "target extended-remote " + port, "-ex", "monitor swdp_scan", "-ex", "attach 1", "-ex", "load", "-ex", "compare-sections", "-ex", "kill", "-ex", "quit", - app, + envMap["elf"], } if verbose { @@ -395,7 +395,7 @@ func flashBMP(app string, verbose bool) error { } // buildFlashEnvMap creates environment map for template expansion -func buildFlashEnvMap(app string, port string) map[string]string { +func buildFlashEnvMap(envMap map[string]string, port string) map[string]string { envs := make(map[string]string) // Basic paths @@ -407,24 +407,11 @@ func buildFlashEnvMap(app string, port string) map[string]string { envs["port"] = port } - // File paths based on extension - ext := strings.ToLower(filepath.Ext(app)) - switch ext { - case ".hex": - envs["hex"] = app - case ".bin": - envs["bin"] = app - case ".elf": - envs["elf"] = app - case ".uf2": - envs["uf2"] = app - case ".zip": - envs["zip"] = app - case ".img": - envs["img"] = app - default: - // Default to binary for unknown extensions - envs["bin"] = app + // Copy all format paths from envMap + for key, value := range envMap { + if value != "" { + envs[key] = value + } } return envs @@ -433,6 +420,7 @@ func buildFlashEnvMap(app string, port string) map[string]string { // expandEnv expands template variables in a string // Supports variables like {port}, {hex}, {bin}, {root}, {tmpDir}, etc. func expandEnv(template string, envs map[string]string) string { + fmt.Fprintf(os.Stderr, "Expanding template: %s with envs: %v\n", template, envs) if template == "" { return "" } diff --git a/internal/llgen/llgenf.go b/internal/llgen/llgenf.go index 4ac61b50..2dc23f6f 100644 --- a/internal/llgen/llgenf.go +++ b/internal/llgen/llgenf.go @@ -20,7 +20,6 @@ import ( "os" "os/exec" "path/filepath" - "runtime" "strings" "github.com/goplus/llgo/internal/build" @@ -48,7 +47,6 @@ func genFrom(pkgPath string, abiMode build.AbiMode) (build.Package, error) { conf := &build.Config{ Mode: build.ModeGen, AbiMode: abiMode, - AppExt: build.DefaultAppExt(runtime.GOOS), } pkgs, err := build.Do([]string{pkgPath}, conf) if err != nil { From df07513a37eb1a71ebed1fae797e98fe915301bc Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sun, 7 Sep 2025 14:31:59 +0800 Subject: [PATCH 19/24] refine: reduce unnecessary format conversion --- internal/build/outputs.go | 24 +++++++++++---------- internal/firmware/firmware.go | 40 ++++++++--------------------------- internal/firmware/objcopy.go | 28 ------------------------ 3 files changed, 22 insertions(+), 70 deletions(-) diff --git a/internal/build/outputs.go b/internal/build/outputs.go index 0fa64b9e..0fc4ab60 100644 --- a/internal/build/outputs.go +++ b/internal/build/outputs.go @@ -68,6 +68,8 @@ func buildOutFmts(pkgName string, conf *Config, multiPkg bool, crossCompile *cro return details, nil } + needRun := slices.Contains([]Mode{ModeRun, ModeTest, ModeCmpTest, ModeInstall}, conf.Mode) + if multiPkg { details.Out, err = genTempOutputFile(pkgName, conf.AppExt) if err != nil { @@ -87,13 +89,15 @@ func buildOutFmts(pkgName string, conf *Config, multiPkg bool, crossCompile *cro // Check emulator format if emulator mode is enabled outFmt := "" - if conf.Emulator { - if crossCompile.Emulator != "" { - outFmt = firmware.ExtractFileFormatFromCommand(crossCompile.Emulator) - } - } else { - if crossCompile.Device.Flash.Method == "command" { - outFmt = firmware.ExtractFileFormatFromCommand(crossCompile.Device.Flash.Command) + if needRun { + if conf.Emulator { + if crossCompile.Emulator != "" { + outFmt = firmware.ExtractFileFormatFromCommand(crossCompile.Emulator) + } + } else { + if crossCompile.Device.Flash.Method == "command" { + outFmt = firmware.ExtractFileFormatFromCommand(crossCompile.Device.Flash.Command) + } } } if outFmt != "" { @@ -101,7 +105,7 @@ func buildOutFmts(pkgName string, conf *Config, multiPkg bool, crossCompile *cro } // Check binary format and set corresponding format - if crossCompile.BinaryFormat != "" && slices.Contains([]Mode{ModeRun, ModeTest, ModeCmpTest, ModeInstall}, conf.Mode) { + if crossCompile.BinaryFormat != "" && needRun { envName := firmware.BinaryFormatToEnvName(crossCompile.BinaryFormat) if envName != "" { setOutFmt(conf, envName) @@ -112,15 +116,13 @@ func buildOutFmts(pkgName string, conf *Config, multiPkg bool, crossCompile *cro if details.Out != "" { base := strings.TrimSuffix(details.Out, filepath.Ext(details.Out)) - if conf.OutFmts.Bin || conf.OutFmts.Img { + if conf.OutFmts.Bin || conf.OutFmts.Img || conf.OutFmts.Hex { details.Bin = base + ".bin" } if conf.OutFmts.Hex { - details.Bin = base + ".bin" // hex depends on bin details.Hex = base + ".hex" } if conf.OutFmts.Img { - details.Bin = base + ".bin" // img depends on bin details.Img = base + ".img" } if conf.OutFmts.Uf2 { diff --git a/internal/firmware/firmware.go b/internal/firmware/firmware.go index 0528378c..0b23d8a8 100644 --- a/internal/firmware/firmware.go +++ b/internal/firmware/firmware.go @@ -7,8 +7,9 @@ import ( "strings" ) -// MakeFirmwareImage creates a firmware image from the given input file. -func MakeFirmwareImage(infile, outfile, format, fmtDetail string) error { +// makeFirmwareImage creates a firmware image from the given input file. +func makeFirmwareImage(infile, outfile, format, fmtDetail string) error { + fmt.Fprintf(os.Stderr, "Generating firmware image: %s -> %s (format: %s, detail: %s)\n", infile, outfile, format, fmtDetail) if strings.HasPrefix(format, "esp") { return makeESPFirmareImage(infile, outfile, format) } else if format == "uf2" { @@ -32,34 +33,11 @@ func ExtractFileFormatFromCommand(cmd string) string { return "" } -// GetFileExtFromFormat converts file format to file extension -func GetFileExtFromFormat(format string) string { - switch format { - case "bin": - return ".bin" - case "hex": - return ".hex" - case "elf": - return "" - case "uf2": - return ".uf2" - case "zip": - return ".zip" - case "img": - return ".img" - default: - return "" - } -} - // ConvertFormats processes format conversions for embedded targets only func ConvertFormats(binFmt, fmtDetail string, envMap map[string]string) error { - fmt.Printf("Converting formats based on binary format: %s\n", binFmt) - fmt.Printf("Format details: %s\n", fmtDetail) - fmt.Printf("Environment map: %+v\n", envMap) // Convert to bin format first (needed for img) if envMap["bin"] != "" { - err := MakeFirmwareImage(envMap["out"], envMap["bin"], binFmt, fmtDetail) + err := makeFirmwareImage(envMap["out"], envMap["bin"], binFmt, fmtDetail) if err != nil { return fmt.Errorf("failed to convert to bin format: %w", err) } @@ -67,15 +45,15 @@ func ConvertFormats(binFmt, fmtDetail string, envMap map[string]string) error { // Convert to hex format if envMap["hex"] != "" { - err := MakeFirmwareImage(envMap["out"], envMap["hex"], binFmt, fmtDetail) + err := makeFirmwareImage(envMap["out"], envMap["hex"], binFmt, fmtDetail) if err != nil { return fmt.Errorf("failed to convert to hex format: %w", err) } } - // Convert to img format (depends on bin) + // Convert to img format if envMap["img"] != "" { - err := MakeFirmwareImage(envMap["out"], envMap["img"], binFmt+"-img", fmtDetail) + err := makeFirmwareImage(envMap["out"], envMap["img"], binFmt+"-img", fmtDetail) if err != nil { return fmt.Errorf("failed to convert to img format: %w", err) } @@ -83,7 +61,7 @@ func ConvertFormats(binFmt, fmtDetail string, envMap map[string]string) error { // Convert to uf2 format if envMap["uf2"] != "" { - err := MakeFirmwareImage(envMap["out"], envMap["uf2"], binFmt, fmtDetail) + err := makeFirmwareImage(envMap["out"], envMap["uf2"], binFmt, fmtDetail) if err != nil { return fmt.Errorf("failed to convert to uf2 format: %w", err) } @@ -91,7 +69,7 @@ func ConvertFormats(binFmt, fmtDetail string, envMap map[string]string) error { // Convert to zip format if envMap["zip"] != "" { - err := MakeFirmwareImage(envMap["out"], envMap["zip"], binFmt, fmtDetail) + err := makeFirmwareImage(envMap["out"], envMap["zip"], binFmt, fmtDetail) if err != nil { return fmt.Errorf("failed to convert to zip format: %w", err) } diff --git a/internal/firmware/objcopy.go b/internal/firmware/objcopy.go index e163f41f..dd978667 100644 --- a/internal/firmware/objcopy.go +++ b/internal/firmware/objcopy.go @@ -5,7 +5,6 @@ package firmware import ( "debug/elf" "io" - "os" "sort" ) @@ -104,30 +103,3 @@ func extractROM(path string) (uint64, []byte, error) { return progs[0].Paddr, rom, nil } } - -// objcopy converts an ELF file to a different (simpler) output file format: -// .bin or .hex. It extracts only the .text section. -func objcopy(infile, outfile, binaryFormat string) error { - f, err := os.OpenFile(outfile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) - if err != nil { - return err - } - defer f.Close() - - // Read the .text segment. - _, data, err := extractROM(infile) - if err != nil { - return err - } - - // Write to the file, in the correct format. - switch binaryFormat { - case "bin": - // The start address is not stored in raw firmware files (therefore you - // should use .hex files in most cases). - _, err := f.Write(data) - return err - default: - panic("unreachable") - } -} From 9eeb14ae31151fff40a211ece0e18281106e470b Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sun, 7 Sep 2025 14:56:39 +0800 Subject: [PATCH 20/24] feat: support generic bin and intel hex firmware --- go.mod | 1 + go.sum | 2 ++ internal/firmware/firmware.go | 47 ++++++----------------------------- internal/firmware/objcopy.go | 42 +++++++++++++++++++++++++++++++ 4 files changed, 53 insertions(+), 39 deletions(-) diff --git a/go.mod b/go.mod index 0bd566d3..03269ef9 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( ) require ( + github.com/marcinbor85/gohex v0.0.0-20210308104911-55fb1c624d84 github.com/mattn/go-tty v0.0.7 github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1 go.bug.st/serial v1.6.4 diff --git a/go.sum b/go.sum index ab56b0c4..b3d12fd7 100644 --- a/go.sum +++ b/go.sum @@ -14,6 +14,8 @@ github.com/goplus/llvm v0.8.5 h1:DUnFeYC3Rco622tBEKGg8xkigRAV2fh5ZIfBCt7gOSs= github.com/goplus/llvm v0.8.5/go.mod h1:PeVK8GgzxwAYCiMiUAJb5wJR6xbhj989tu9oulKLLT4= github.com/goplus/mod v0.17.1 h1:ITovxDcc5zbURV/Wrp3/SBsYLgC1KrxY6pq1zMM2V94= github.com/goplus/mod v0.17.1/go.mod h1:iXEszBKqi38BAyQApBPyQeurLHmQN34YMgC2ZNdap50= +github.com/marcinbor85/gohex v0.0.0-20210308104911-55fb1c624d84 h1:hyAgCuG5nqTMDeUD8KZs7HSPs6KprPgPP8QmGV8nyvk= +github.com/marcinbor85/gohex v0.0.0-20210308104911-55fb1c624d84/go.mod h1:Pb6XcsXyropB9LNHhnqaknG/vEwYztLkQzVCHv8sQ3M= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-tty v0.0.7 h1:KJ486B6qI8+wBO7kQxYgmmEFDaFEE96JMBQ7h400N8Q= diff --git a/internal/firmware/firmware.go b/internal/firmware/firmware.go index 0b23d8a8..b5da6f6b 100644 --- a/internal/firmware/firmware.go +++ b/internal/firmware/firmware.go @@ -2,7 +2,6 @@ package firmware import ( "fmt" - "io" "os" "strings" ) @@ -35,9 +34,14 @@ func ExtractFileFormatFromCommand(cmd string) string { // ConvertFormats processes format conversions for embedded targets only func ConvertFormats(binFmt, fmtDetail string, envMap map[string]string) error { + var err error // Convert to bin format first (needed for img) if envMap["bin"] != "" { - err := makeFirmwareImage(envMap["out"], envMap["bin"], binFmt, fmtDetail) + if strings.HasPrefix(binFmt, "esp") { + err = makeFirmwareImage(envMap["out"], envMap["bin"], binFmt, fmtDetail) + } else { + err = objcopy(envMap["out"], envMap["bin"], "bin") + } if err != nil { return fmt.Errorf("failed to convert to bin format: %w", err) } @@ -45,7 +49,7 @@ func ConvertFormats(binFmt, fmtDetail string, envMap map[string]string) error { // Convert to hex format if envMap["hex"] != "" { - err := makeFirmwareImage(envMap["out"], envMap["hex"], binFmt, fmtDetail) + err := objcopy(envMap["out"], envMap["hex"], "hex") if err != nil { return fmt.Errorf("failed to convert to hex format: %w", err) } @@ -53,7 +57,7 @@ func ConvertFormats(binFmt, fmtDetail string, envMap map[string]string) error { // Convert to img format if envMap["img"] != "" { - err := makeFirmwareImage(envMap["out"], envMap["img"], binFmt+"-img", fmtDetail) + err = makeFirmwareImage(envMap["out"], envMap["img"], binFmt+"-img", fmtDetail) if err != nil { return fmt.Errorf("failed to convert to img format: %w", err) } @@ -77,38 +81,3 @@ func ConvertFormats(binFmt, fmtDetail string, envMap map[string]string) error { return nil } - -// convertToHex converts binary file to hex format (each byte as two hex characters) -func convertToHex(infile, outfile string) error { - srcFile, err := os.Open(infile) - if err != nil { - return err - } - defer srcFile.Close() - - dstFile, err := os.Create(outfile) - if err != nil { - return err - } - defer dstFile.Close() - - // Read input file and convert each byte to two hex characters - buf := make([]byte, 4096) // Read in chunks - for { - n, err := srcFile.Read(buf) - if n > 0 { - for i := 0; i < n; i++ { - if _, writeErr := fmt.Fprintf(dstFile, "%02x", buf[i]); writeErr != nil { - return writeErr - } - } - } - if err == io.EOF { - break - } - if err != nil { - return err - } - } - return nil -} diff --git a/internal/firmware/objcopy.go b/internal/firmware/objcopy.go index dd978667..c301f75a 100644 --- a/internal/firmware/objcopy.go +++ b/internal/firmware/objcopy.go @@ -4,8 +4,12 @@ package firmware import ( "debug/elf" + "fmt" "io" + "os" "sort" + + "github.com/marcinbor85/gohex" ) // maxPadBytes is the maximum allowed bytes to be padded in a rom extraction @@ -103,3 +107,41 @@ func extractROM(path string) (uint64, []byte, error) { return progs[0].Paddr, rom, nil } } + +// From tinygo/builder/builder/objcopy.go objcopy +// objcopy converts an ELF file to a different (simpler) output file format: +// .bin or .hex. It extracts only the .text section. +func objcopy(infile, outfile, binaryFormat string) error { + f, err := os.OpenFile(outfile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) + if err != nil { + return err + } + defer f.Close() + + // Read the .text segment. + addr, data, err := extractROM(infile) + if err != nil { + return err + } + + // Write to the file, in the correct format. + switch binaryFormat { + case "hex": + fmt.Fprintf(os.Stderr, "Converting firmware format: %s -> %s (intel hex format: %s)\n", infile, outfile, binaryFormat) + // Intel hex file, includes the firmware start address. + mem := gohex.NewMemory() + err := mem.AddBinary(uint32(addr), data) + if err != nil { + return objcopyError{"failed to create .hex file", err} + } + return mem.DumpIntelHex(f, 16) + case "bin": + fmt.Fprintf(os.Stderr, "Converting firmware format: %s -> %s (format: %s)\n", infile, outfile, binaryFormat) + // The start address is not stored in raw firmware files (therefore you + // should use .hex files in most cases). + _, err := f.Write(data) + return err + default: + panic("unreachable") + } +} From 806b16c2c860249feec26181e839243e237aa9ac Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sun, 7 Sep 2025 15:13:58 +0800 Subject: [PATCH 21/24] refine: reduce duplicated env expand funcs --- internal/crosscompile/crosscompile.go | 63 +---------- internal/crosscompile/crosscompile_test.go | 123 --------------------- internal/env/utils.go | 63 +++++++++++ internal/env/utils_test.go | 84 ++++++++++++++ internal/flash/flash.go | 22 +--- 5 files changed, 150 insertions(+), 205 deletions(-) create mode 100644 internal/env/utils.go create mode 100644 internal/env/utils_test.go diff --git a/internal/crosscompile/crosscompile.go b/internal/crosscompile/crosscompile.go index d22624e5..48a5d9f5 100644 --- a/internal/crosscompile/crosscompile.go +++ b/internal/crosscompile/crosscompile.go @@ -59,65 +59,6 @@ func cacheDir() string { return filepath.Join(cacheRoot(), "crosscompile") } -// expandEnv expands template variables in a string -// Supports variables like {port}, {hex}, {bin}, {root}, {tmpDir}, etc. -// Special case: {} expands to the first available file variable (hex, bin, img, zip) -func expandEnv(template string, envs map[string]string) string { - return expandEnvWithDefault(template, envs) -} - -// expandEnvWithDefault expands template variables with optional default for {} -func expandEnvWithDefault(template string, envs map[string]string, defaultValue ...string) string { - if template == "" { - return "" - } - - result := template - - // Handle special case of {} - use provided default or first available file variable - if strings.Contains(result, "{}") { - defaultVal := "" - if len(defaultValue) > 0 && defaultValue[0] != "" { - defaultVal = defaultValue[0] - } else { - // Priority order: hex, bin, img, zip - for _, key := range []string{"hex", "bin", "img", "zip"} { - if value, exists := envs[key]; exists && value != "" { - defaultVal = value - break - } - } - } - result = strings.ReplaceAll(result, "{}", defaultVal) - } - - // Replace named variables - for key, value := range envs { - if key != "" { // Skip empty key used for {} default - result = strings.ReplaceAll(result, "{"+key+"}", value) - } - } - return result -} - -// expandEnvSlice expands template variables in a slice of strings -func expandEnvSlice(templates []string, envs map[string]string) []string { - return expandEnvSliceWithDefault(templates, envs) -} - -// expandEnvSliceWithDefault expands template variables in a slice with optional default for {} -func expandEnvSliceWithDefault(templates []string, envs map[string]string, defaultValue ...string) []string { - if len(templates) == 0 { - return templates - } - - result := make([]string, len(templates)) - for i, template := range templates { - result[i] = expandEnvWithDefault(template, envs, defaultValue...) - } - return result -} - // buildEnvMap creates a map of template variables for the current context func buildEnvMap(llgoRoot string) map[string]string { envs := make(map[string]string) @@ -539,7 +480,7 @@ func UseTarget(targetName string) (export Export, err error) { ccflags = append(ccflags, "--target="+config.LLVMTarget) } // Expand template variables in cflags - expandedCFlags := expandEnvSlice(config.CFlags, envs) + expandedCFlags := env.ExpandEnvSlice(config.CFlags, envs) cflags = append(cflags, expandedCFlags...) // The following parameters are inspired by tinygo/builder/library.go @@ -685,7 +626,7 @@ func UseTarget(targetName string) (export Export, err error) { // Combine with config flags and expand template variables export.CFLAGS = cflags export.CCFLAGS = ccflags - expandedLDFlags := expandEnvSlice(config.LDFlags, envs) + expandedLDFlags := env.ExpandEnvSlice(config.LDFlags, envs) export.LDFLAGS = append(ldflags, expandedLDFlags...) return export, nil diff --git a/internal/crosscompile/crosscompile_test.go b/internal/crosscompile/crosscompile_test.go index 42152a58..b779dc6d 100644 --- a/internal/crosscompile/crosscompile_test.go +++ b/internal/crosscompile/crosscompile_test.go @@ -302,126 +302,3 @@ func TestUseWithTarget(t *testing.T) { t.Error("Expected LDFLAGS to be set for native build") } } - -func TestExpandEnv(t *testing.T) { - envs := map[string]string{ - "port": "/dev/ttyUSB0", - "hex": "firmware.hex", - "bin": "firmware.bin", - "root": "/usr/local/llgo", - } - - tests := []struct { - template string - expected string - }{ - { - "avrdude -c arduino -p atmega328p -P {port} -U flash:w:{hex}:i", - "avrdude -c arduino -p atmega328p -P /dev/ttyUSB0 -U flash:w:firmware.hex:i", - }, - { - "simavr -m atmega328p -f 16000000 {}", - "simavr -m atmega328p -f 16000000 firmware.hex", // {} expands to hex (first priority) - }, - { - "-I{root}/lib/CMSIS/CMSIS/Include", - "-I/usr/local/llgo/lib/CMSIS/CMSIS/Include", - }, - { - "no variables here", - "no variables here", - }, - { - "", - "", - }, - } - - for _, test := range tests { - result := expandEnv(test.template, envs) - if result != test.expected { - t.Errorf("expandEnv(%q) = %q, want %q", test.template, result, test.expected) - } - } -} - -func TestExpandEnvSlice(t *testing.T) { - envs := map[string]string{ - "root": "/usr/local/llgo", - "port": "/dev/ttyUSB0", - } - - input := []string{ - "-I{root}/include", - "-DPORT={port}", - "static-flag", - } - - expected := []string{ - "-I/usr/local/llgo/include", - "-DPORT=/dev/ttyUSB0", - "static-flag", - } - - result := expandEnvSlice(input, envs) - - if len(result) != len(expected) { - t.Fatalf("expandEnvSlice length mismatch: got %d, want %d", len(result), len(expected)) - } - - for i, exp := range expected { - if result[i] != exp { - t.Errorf("expandEnvSlice[%d] = %q, want %q", i, result[i], exp) - } - } -} - -func TestExpandEnvWithDefault(t *testing.T) { - envs := map[string]string{ - "port": "/dev/ttyUSB0", - "hex": "firmware.hex", - "bin": "firmware.bin", - "img": "image.img", - } - - tests := []struct { - template string - defaultValue string - expected string - }{ - { - "simavr {}", - "", // No default - should use hex (priority) - "simavr firmware.hex", - }, - { - "simavr {}", - "custom.elf", // Explicit default - "simavr custom.elf", - }, - { - "qemu -kernel {}", - "vmlinux", // Custom kernel - "qemu -kernel vmlinux", - }, - { - "no braces here", - "ignored", - "no braces here", - }, - } - - for i, test := range tests { - var result string - if test.defaultValue == "" { - result = expandEnvWithDefault(test.template, envs) - } else { - result = expandEnvWithDefault(test.template, envs, test.defaultValue) - } - - if result != test.expected { - t.Errorf("Test %d: expandEnvWithDefault(%q, envs, %q) = %q, want %q", - i, test.template, test.defaultValue, result, test.expected) - } - } -} diff --git a/internal/env/utils.go b/internal/env/utils.go new file mode 100644 index 00000000..ec3d4f3e --- /dev/null +++ b/internal/env/utils.go @@ -0,0 +1,63 @@ +/* + * 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 env + +import "strings" + +// ExpandEnvWithDefault expands template variables with optional default for {} +func ExpandEnvWithDefault(template string, envs map[string]string, defaultValue ...string) string { + if template == "" { + return "" + } + + result := template + + // Handle special case of {} - use provided default or first available file variable + if strings.Contains(result, "{}") { + defaultVal := "" + if len(defaultValue) > 0 && defaultValue[0] != "" { + defaultVal = defaultValue[0] + } + result = strings.ReplaceAll(result, "{}", defaultVal) + } + + // Replace named variables + for key, value := range envs { + if key != "" { // Skip empty key used for {} default + result = strings.ReplaceAll(result, "{"+key+"}", value) + } + } + return result +} + +// ExpandEnvSlice expands template variables in a slice of strings +func ExpandEnvSlice(templates []string, envs map[string]string) []string { + return ExpandEnvSliceWithDefault(templates, envs) +} + +// ExpandEnvSliceWithDefault expands template variables in a slice with optional default for {} +func ExpandEnvSliceWithDefault(templates []string, envs map[string]string, defaultValue ...string) []string { + if len(templates) == 0 { + return templates + } + + result := make([]string, len(templates)) + for i, template := range templates { + result[i] = ExpandEnvWithDefault(template, envs, defaultValue...) + } + return result +} diff --git a/internal/env/utils_test.go b/internal/env/utils_test.go new file mode 100644 index 00000000..e12297f2 --- /dev/null +++ b/internal/env/utils_test.go @@ -0,0 +1,84 @@ +package env + +import "testing" + +func TestExpandEnvSlice(t *testing.T) { + envs := map[string]string{ + "root": "/usr/local/llgo", + "port": "/dev/ttyUSB0", + } + + input := []string{ + "-I{root}/include", + "-DPORT={port}", + "static-flag", + } + + expected := []string{ + "-I/usr/local/llgo/include", + "-DPORT=/dev/ttyUSB0", + "static-flag", + } + + result := ExpandEnvSlice(input, envs) + + if len(result) != len(expected) { + t.Fatalf("expandEnvSlice length mismatch: got %d, want %d", len(result), len(expected)) + } + + for i, exp := range expected { + if result[i] != exp { + t.Errorf("expandEnvSlice[%d] = %q, want %q", i, result[i], exp) + } + } +} + +func TestExpandEnvWithDefault(t *testing.T) { + envs := map[string]string{ + "port": "/dev/ttyUSB0", + "hex": "firmware.hex", + "bin": "firmware.bin", + "img": "image.img", + } + + tests := []struct { + template string + defaultValue string + expected string + }{ + { + "simavr {}", + "firmware.hex", + "simavr firmware.hex", + }, + { + "simavr {}", + "custom.elf", // Explicit default + "simavr custom.elf", + }, + { + "qemu -kernel {}", + "vmlinux", // Custom kernel + "qemu -kernel vmlinux", + }, + { + "no braces here", + "ignored", + "no braces here", + }, + } + + for i, test := range tests { + var result string + if test.defaultValue == "" { + result = ExpandEnvWithDefault(test.template, envs) + } else { + result = ExpandEnvWithDefault(test.template, envs, test.defaultValue) + } + + if result != test.expected { + t.Errorf("Test %d: expandEnvWithDefault(%q, envs, %q) = %q, want %q", + i, test.template, test.defaultValue, result, test.expected) + } + } +} diff --git a/internal/flash/flash.go b/internal/flash/flash.go index b4e790f0..d5ac65b0 100644 --- a/internal/flash/flash.go +++ b/internal/flash/flash.go @@ -248,7 +248,7 @@ func flashCommand(flash Flash, envMap map[string]string, port string, verbose bo envs := buildFlashEnvMap(envMap, port) // Expand template variables in command - expandedCommand := expandEnv(flash.Command, envs) + expandedCommand := env.ExpandEnvWithDefault(flash.Command, envs) if verbose { fmt.Fprintf(os.Stderr, "Flash command: %s\n", expandedCommand) @@ -417,26 +417,6 @@ func buildFlashEnvMap(envMap map[string]string, port string) map[string]string { return envs } -// expandEnv expands template variables in a string -// Supports variables like {port}, {hex}, {bin}, {root}, {tmpDir}, etc. -func expandEnv(template string, envs map[string]string) string { - fmt.Fprintf(os.Stderr, "Expanding template: %s with envs: %v\n", template, envs) - if template == "" { - return "" - } - - result := template - - // Replace named variables - for key, value := range envs { - if key != "" { - result = strings.ReplaceAll(result, "{"+key+"}", value) - } - } - - return result -} - // copyFile copies a file from src to dst func copyFile(src, dst string) error { sourceFile, err := os.Open(src) From dba719a8d7b0ce4da0fdec4ea92e45f1673333e8 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sun, 7 Sep 2025 15:14:34 +0800 Subject: [PATCH 22/24] doc: update embeded doc of llgo cmds --- doc/Embedded_Cmd.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/doc/Embedded_Cmd.md b/doc/Embedded_Cmd.md index e708ea27..dc4dd762 100644 --- a/doc/Embedded_Cmd.md +++ b/doc/Embedded_Cmd.md @@ -4,8 +4,11 @@ - `-o ` - Specify output file name - `-target ` - Specify target platform for cross-compilation -- `-file-format ` - Convert to specified format (**requires `-target`**) - - Supported: `elf` (default), `bin`, `hex`, `uf2`, `zip`, `img` +- `-obin` - Generate binary format output (requires `-target`) +- `-ohex` - Generate Intel HEX format output (requires `-target`) +- `-oimg` - Generate firmware image format output (requires `-target`) +- `-ouf2` - Generate UF2 format output (requires `-target`) +- `-ozip` - Generate ZIP/DFU format output (requires `-target`) - `-emulator` - Run using emulator (auto-detects required format) - `-port ` - Target port for flashing, testing, or monitoring - `-baudrate ` - Baudrate for serial communication (default: 115200) @@ -15,7 +18,7 @@ ### llgo build Compile program to output file. - No `-target`: Native executable -- With `-target`: ELF executable (or `-file-format` if specified) +- With `-target`: ELF executable (and additional formats if specified with `-obin`, `-ohex`, etc.) ### llgo run Compile and run program. @@ -57,8 +60,10 @@ llgo run hello.go # run locally llgo install hello.go # install to bin # Cross-compilation -llgo build -target esp32 . # -> hello (ELF) -llgo build -target esp32 -file-format bin . # -> hello.bin +llgo build -target esp32 . # -> hello.elf +llgo build -target esp32 -obin . # -> hello.elf + hello.bin +llgo build -target esp32 -ohex . # -> hello.elf + hello.bin + hello.hex +llgo build -target esp32 -obin -ohex -oimg . # -> hello.elf + hello.bin + hello.hex + hello.img llgo run -target esp32 . # run on ESP32 (guess a port) llgo run -target esp32 -emulator . # run in emulator llgo test -target esp32 -port /dev/ttyUSB0 . # run tests on device From 3ecf9b35f32ab4f59626018385b6231ebfe67b4d Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sun, 7 Sep 2025 15:31:17 +0800 Subject: [PATCH 23/24] refine: safe shell cmd line parse --- internal/build/run.go | 12 ++- internal/flash/flash.go | 8 +- internal/shellparse/shellparse.go | 86 ++++++++++++++++++ internal/shellparse/shellparse_test.go | 115 +++++++++++++++++++++++++ 4 files changed, 215 insertions(+), 6 deletions(-) create mode 100644 internal/shellparse/shellparse.go create mode 100644 internal/shellparse/shellparse_test.go diff --git a/internal/build/run.go b/internal/build/run.go index 824af3fc..a37a894c 100644 --- a/internal/build/run.go +++ b/internal/build/run.go @@ -23,6 +23,7 @@ import ( "strings" "github.com/goplus/llgo/internal/mockable" + "github.com/goplus/llgo/internal/shellparse" ) func runNative(ctx *context, app, pkgDir, pkgName string, conf *Config, mode Mode) error { @@ -123,10 +124,13 @@ func runEmuCmd(envMap map[string]string, emulatorTemplate string, runArgs []stri fmt.Fprintf(os.Stderr, "Running in emulator: %s\n", emulatorCmd) } - // Parse command and arguments - cmdParts := strings.Fields(emulatorCmd) + // Parse command and arguments safely handling quoted strings + cmdParts, err := shellparse.Parse(emulatorCmd) + if err != nil { + return fmt.Errorf("failed to parse emulator command: %w", err) + } if len(cmdParts) == 0 { - panic(fmt.Errorf("empty emulator command")) + return fmt.Errorf("empty emulator command") } // Add run arguments to the end @@ -137,7 +141,7 @@ func runEmuCmd(envMap map[string]string, emulatorTemplate string, runArgs []stri cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr - err := cmd.Run() + err = cmd.Run() if err != nil { return err } diff --git a/internal/flash/flash.go b/internal/flash/flash.go index d5ac65b0..281686bd 100644 --- a/internal/flash/flash.go +++ b/internal/flash/flash.go @@ -12,6 +12,7 @@ import ( "time" "github.com/goplus/llgo/internal/env" + "github.com/goplus/llgo/internal/shellparse" "go.bug.st/serial" "go.bug.st/serial/enumerator" ) @@ -254,8 +255,11 @@ func flashCommand(flash Flash, envMap map[string]string, port string, verbose bo fmt.Fprintf(os.Stderr, "Flash command: %s\n", expandedCommand) } - // Split command into parts for exec - parts := strings.Fields(expandedCommand) + // Split command into parts for exec - safely handling quoted arguments + parts, err := shellparse.Parse(expandedCommand) + if err != nil { + return fmt.Errorf("failed to parse flash command: %w", err) + } if len(parts) == 0 { return fmt.Errorf("empty flash command after expansion") } diff --git a/internal/shellparse/shellparse.go b/internal/shellparse/shellparse.go new file mode 100644 index 00000000..ab13d9d3 --- /dev/null +++ b/internal/shellparse/shellparse.go @@ -0,0 +1,86 @@ +/* + * 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 shellparse + +import ( + "fmt" + "strings" + "unicode" +) + +// Parse parses a shell command string into command and arguments, +// properly handling quoted arguments with spaces +func Parse(cmd string) ([]string, error) { + args := make([]string, 0) + var current strings.Builder + var inQuotes bool + var quoteChar rune + var hasContent bool // Track if we've seen content (including empty quotes) + + runes := []rune(cmd) + for i := 0; i < len(runes); i++ { + r := runes[i] + switch { + case !inQuotes && (r == '"' || r == '\''): + // Start of quoted string + inQuotes = true + quoteChar = r + hasContent = true // Empty quotes still count as content + case inQuotes && r == quoteChar: + // End of quoted string + inQuotes = false + quoteChar = 0 + case !inQuotes && unicode.IsSpace(r): + // Space outside quotes - end current argument + if hasContent { + args = append(args, current.String()) + current.Reset() + hasContent = false + } + case inQuotes && r == '\\' && i+1 < len(runes): + // Handle escape sequences in quotes + if quoteChar == '"' { + next := runes[i+1] + if next == quoteChar || next == '\\' { + current.WriteRune(next) + i++ // Skip the next rune + } else { + current.WriteRune(r) + } + } else { + // In single quotes, backslash is a literal character. + current.WriteRune(r) + } + default: + // Regular character + current.WriteRune(r) + hasContent = true + } + } + + // Handle unterminated quotes + if inQuotes { + return nil, fmt.Errorf("unterminated quote in command: %s", cmd) + } + + // Add final argument if any + if hasContent { + args = append(args, current.String()) + } + + return args, nil +} diff --git a/internal/shellparse/shellparse_test.go b/internal/shellparse/shellparse_test.go new file mode 100644 index 00000000..cb195532 --- /dev/null +++ b/internal/shellparse/shellparse_test.go @@ -0,0 +1,115 @@ +/* + * 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 shellparse + +import ( + "reflect" + "testing" +) + +func TestParse(t *testing.T) { + tests := []struct { + name string + cmd string + want []string + wantErr bool + }{ + { + name: "simple command", + cmd: "echo hello", + want: []string{"echo", "hello"}, + }, + { + name: "command with quoted argument", + cmd: `echo "hello world"`, + want: []string{"echo", "hello world"}, + }, + { + name: "command with single quoted argument", + cmd: `echo 'hello world'`, + want: []string{"echo", "hello world"}, + }, + { + name: "command with multiple quoted arguments", + cmd: `cp "file with spaces.txt" "another file.txt"`, + want: []string{"cp", "file with spaces.txt", "another file.txt"}, + }, + { + name: "command with escaped quotes", + cmd: `echo "He said \"hello\""`, + want: []string{"echo", `He said "hello"`}, + }, + { + name: "command with escaped backslash", + cmd: `echo "path\\to\\file"`, + want: []string{"echo", `path\to\file`}, + }, + { + name: "mixed quotes", + cmd: `echo "double quote" 'single quote' normal`, + want: []string{"echo", "double quote", "single quote", "normal"}, + }, + { + name: "empty arguments", + cmd: `echo "" ''`, + want: []string{"echo", "", ""}, + }, + { + name: "multiple spaces", + cmd: "echo hello world", + want: []string{"echo", "hello", "world"}, + }, + { + name: "emulator command example", + cmd: `qemu-system-xtensa -machine esp32 -kernel "/path/with spaces/firmware.bin"`, + want: []string{"qemu-system-xtensa", "-machine", "esp32", "-kernel", "/path/with spaces/firmware.bin"}, + }, + { + name: "unterminated double quote", + cmd: `echo "hello`, + wantErr: true, + }, + { + name: "unterminated single quote", + cmd: `echo 'hello`, + wantErr: true, + }, + { + name: "empty command", + cmd: "", + want: []string{}, + }, + { + name: "only spaces", + cmd: " ", + want: []string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Parse(tt.cmd) + if (err != nil) != tt.wantErr { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Parse() = %v, want %v", got, tt.want) + } + }) + } +} From 20ce823b685a19f45fbc4362611e93a0b901c034 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sun, 7 Sep 2025 16:13:08 +0800 Subject: [PATCH 24/24] fix: targets tests --- internal/targets/example_test.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/internal/targets/example_test.go b/internal/targets/example_test.go index 93bb8bb9..7a82d648 100644 --- a/internal/targets/example_test.go +++ b/internal/targets/example_test.go @@ -1,15 +1,16 @@ -package targets_test +//go:build !llgo +// +build !llgo + +package targets import ( "fmt" "log" "sort" - - "github.com/goplus/llgo/internal/targets" ) func ExampleResolver_Resolve() { - resolver := targets.NewDefaultResolver() + resolver := NewDefaultResolver() // Resolve a specific target config, err := resolver.Resolve("rp2040") @@ -34,7 +35,7 @@ func ExampleResolver_Resolve() { } func ExampleResolver_ListAvailableTargets() { - resolver := targets.NewDefaultResolver() + resolver := NewDefaultResolver() targets, err := resolver.ListAvailableTargets() if err != nil { @@ -50,7 +51,7 @@ func ExampleResolver_ListAvailableTargets() { } func ExampleResolver_ResolveAll() { - resolver := targets.NewDefaultResolver() + resolver := NewDefaultResolver() configs, err := resolver.ResolveAll() if err != nil {