Build and run for embeded

This commit is contained in:
Li Jie
2025-09-05 17:47:34 +08:00
parent df8f67db5a
commit 5e5d5c2a83
9 changed files with 828 additions and 288 deletions

View File

@@ -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
}

View File

@@ -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)
}

52
doc/Embedded_Cmd.md Normal file
View File

@@ -0,0 +1,52 @@
# LLGo Embedded Development Command Line Options
## Flags
- `-o <file>` - Specify output file name
- `-target <platform>` - Specify target platform for cross-compilation
- `-file-format <format>` - Convert to specified format (**requires `-target`**)
- Supported: `elf` (default), `bin`, `hex`, `uf2`, `zip`, `img`
- `-emulator` - Run using emulator (auto-detects required format)
- `-d <device>` - 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
```

View File

@@ -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)

View File

@@ -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

192
internal/build/outputs.go Normal file
View File

@@ -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
}

View File

@@ -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)
}
})
}
}

View File

@@ -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())

View File

@@ -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)
}