From 16c8402065de58a7b66903082972eba60ace01b2 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sun, 7 Sep 2025 13:32:11 +0800 Subject: [PATCH] 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 {