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 BuildEnv string
var Tags string var Tags string
var Target string var Target string
var Emulator bool
var AbiMode int var AbiMode int
var CheckLinkArgs bool var CheckLinkArgs bool
var CheckLLFiles bool var CheckLLFiles bool
@@ -39,6 +40,10 @@ func AddBuildFlags(fs *flag.FlagSet) {
var Gen bool var Gen bool
func AddRunFlags(fs *flag.FlagSet) {
fs.BoolVar(&Emulator, "emulator", false, "Run in emulator mode")
}
func AddCmpTestFlags(fs *flag.FlagSet) { func AddCmpTestFlags(fs *flag.FlagSet) {
fs.BoolVar(&Gen, "gen", false, "Generate llgo.expect file") fs.BoolVar(&Gen, "gen", false, "Generate llgo.expect file")
} }
@@ -51,6 +56,8 @@ func UpdateConfig(conf *build.Config) {
case build.ModeBuild: case build.ModeBuild:
conf.OutFile = OutputFile conf.OutFile = OutputFile
conf.FileFormat = FileFormat conf.FileFormat = FileFormat
case build.ModeRun:
conf.Emulator = Emulator
case build.ModeCmpTest: case build.ModeCmpTest:
conf.GenExpect = Gen conf.GenExpect = Gen
} }

View File

@@ -50,6 +50,7 @@ func init() {
base.PassBuildFlags(Cmd) base.PassBuildFlags(Cmd)
flags.AddBuildFlags(&Cmd.Flag) flags.AddBuildFlags(&Cmd.Flag)
flags.AddBuildFlags(&CmpTestCmd.Flag) flags.AddBuildFlags(&CmpTestCmd.Flag)
flags.AddRunFlags(&Cmd.Flag)
flags.AddCmpTestFlags(&CmpTestCmd.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 AppExt string // ".exe" on Windows, empty on Unix
OutFile string // only valid for ModeBuild when len(pkgs) == 1 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 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 RunArgs []string // only valid for ModeRun
Mode Mode Mode Mode
AbiMode AbiMode AbiMode AbiMode
@@ -628,83 +629,18 @@ func compileExtraFiles(ctx *context, verbose bool) ([]string, error) {
return objFiles, nil 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) { func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, global llssa.Package, conf *Config, mode Mode, verbose bool) {
pkgPath := pkg.PkgPath pkgPath := pkg.PkgPath
name := path.Base(pkgPath) name := path.Base(pkgPath)
binFmt := ctx.crossCompile.BinaryFormat binFmt := ctx.crossCompile.BinaryFormat
binExt := firmware.BinaryExt(binFmt)
// Determine final output extension from user-specified file format // Generate output configuration using the centralized function
outExt := binExt outputCfg, err := GenOutputs(conf, name, len(ctx.initial) > 1, ctx.crossCompile.Emulator, binFmt)
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,
)
check(err) check(err)
app := outputCfg.OutPath
orgApp := outputCfg.IntPath
needRuntime := false needRuntime := false
needPyInit := false needPyInit := false
pkgsMap := make(map[*packages.Package]*aPackage, len(pkgs)) 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 // Handle firmware conversion and file format conversion
currentApp := orgApp currentApp := orgApp
// Determine if firmware conversion is needed based on mode useEmulator := false
needFirmwareConversion := false if mode == ModeRun && conf.Emulator {
if mode == ModeBuild { if ctx.crossCompile.Emulator == "" {
// For build command, do firmware conversion if file-format is specified panic(fmt.Errorf("target %s does not have emulator configured", conf.Target))
needFirmwareConversion = conf.FileFormat != "" }
} else { useEmulator = true
// For run and install commands, do firmware conversion if binExt is set
needFirmwareConversion = binExt != ""
} }
// Step 1: Firmware conversion if needed // Step 1: Firmware conversion if needed
if needFirmwareConversion { if outputCfg.NeedFwGen {
if outExt == binExt { if outputCfg.DirectGen {
// Direct conversion to final output // Direct conversion to final output (including .img case)
if verbose { 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) check(err)
currentApp = app currentApp = app
} else { } else {
// Convert to intermediate file first // Convert to intermediate file first
binExt := firmware.BinaryExt(ctx.crossCompile.BinaryFormat)
tmpFile, err := os.CreateTemp("", "llgo-*"+binExt) tmpFile, err := os.CreateTemp("", "llgo-*"+binExt)
check(err) check(err)
tmpFile.Close() tmpFile.Close()
@@ -813,12 +748,12 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, global l
// Step 2: File format conversion if needed // Step 2: File format conversion if needed
if currentApp != app { if currentApp != app {
if conf.FileFormat != "" { if outputCfg.FileFmt != "" {
// File format conversion // File format conversion
if verbose { 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) check(err)
} else { } else {
// Just move/copy the file // Just move/copy the file
@@ -845,39 +780,46 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, global l
} }
} }
case ModeRun: case ModeRun:
args := make([]string, 0, len(conf.RunArgs)+1) if useEmulator {
copy(args, conf.RunArgs) if verbose {
if isWasmTarget(conf.Goos) { fmt.Fprintf(os.Stderr, "Using emulator: %s\n", ctx.crossCompile.Emulator)
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 runInEmulator(app, ctx.crossCompile.Emulator, conf.RunArgs, verbose)
} else { } else {
args = conf.RunArgs args := make([]string, 0, len(conf.RunArgs)+1)
} copy(args, conf.RunArgs)
cmd := exec.Command(app, args...) if isWasmTarget(conf.Goos) {
cmd.Stdin = os.Stdin wasmer := os.ExpandEnv(WasmRuntime())
cmd.Stdout = os.Stdout wasmerArgs := strings.Split(wasmer, " ")
cmd.Stderr = os.Stderr wasmerCmd := wasmerArgs[0]
err = cmd.Run() wasmerArgs = wasmerArgs[1:]
if err != nil { switch wasmer {
panic(err) case "wasmtime":
} args = append(args, "--wasm", "multi-memory=true", app)
if s := cmd.ProcessState; s != nil { args = append(args, conf.RunArgs...)
mockable.Exit(s.ExitCode()) 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: case ModeCmpTest:
cmpTest(filepath.Dir(pkg.GoFiles[0]), pkgPath, app, conf.GenExpect, conf.RunArgs) 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{} 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) { func check(err error) {
if err != nil { if err != nil {
panic(err) panic(err)

View File

@@ -8,7 +8,6 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"strings"
"testing" "testing"
"github.com/goplus/llgo/internal/mockable" "github.com/goplus/llgo/internal/mockable"
@@ -95,172 +94,4 @@ func TestCmpTest(t *testing.T) {
mockRun([]string{"../../cl/_testgo/runtest"}, &Config{Mode: ModeCmpTest}) mockRun([]string{"../../cl/_testgo/runtest"}, &Config{Mode: ModeCmpTest})
} }
func TestGenerateOutputFilenames(t *testing.T) { // TestGenerateOutputFilenames removed - functionality moved to filename_test.go
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)
}
}

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") BinaryFormat string // Binary format (e.g., "elf", "esp", "uf2")
FormatDetail string // For uf2, it's uf2FamilyID 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 // 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.ExtraFiles = config.ExtraFiles
export.BinaryFormat = config.BinaryFormat export.BinaryFormat = config.BinaryFormat
export.FormatDetail = config.FormatDetail() export.FormatDetail = config.FormatDetail()
export.Emulator = config.Emulator
// Build environment map for template variable expansion // Build environment map for template variable expansion
envs := buildEnvMap(env.LLGoROOT()) 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) 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 // GetFileExtFromFormat converts file format to file extension
func GetFileExtFromFormat(format string) string { func GetFileExtFromFormat(format string) string {
switch format { switch format {
@@ -33,6 +45,8 @@ func GetFileExtFromFormat(format string) string {
return ".uf2" return ".uf2"
case "zip": case "zip":
return ".zip" return ".zip"
case "img":
return ".img"
default: default:
return "" return ""
} }
@@ -47,7 +61,7 @@ func ConvertOutput(infile, outfile, binaryFormat, fileFormat string) error {
return nil return nil
} }
// Only support conversion to hex format // Only support conversion to hex and format
if fileFormat == "hex" { if fileFormat == "hex" {
return convertToHex(infile, outfile) return convertToHex(infile, outfile)
} }