From 5e5d5c2a83ddea3d496ecfe9dd7b2e87fc916c85 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Fri, 5 Sep 2025 17:47:34 +0800 Subject: [PATCH] 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) }