diff --git a/cmd/internal/flags/flags.go b/cmd/internal/flags/flags.go index c61927c3..9a16ffe0 100644 --- a/cmd/internal/flags/flags.go +++ b/cmd/internal/flags/flags.go @@ -8,9 +8,11 @@ import ( ) var OutputFile string +var FileFormat string func AddOutputFlags(fs *flag.FlagSet) { fs.StringVar(&OutputFile, "o", "", "Output file") + fs.StringVar(&FileFormat, "file-format", "", "File format for target output (e.g., bin, hex, elf, uf2, zip)") } var Verbose bool @@ -48,6 +50,7 @@ func UpdateConfig(conf *build.Config) { switch conf.Mode { case build.ModeBuild: conf.OutFile = OutputFile + conf.FileFormat = FileFormat case build.ModeCmpTest: conf.GenExpect = Gen } diff --git a/internal/build/build.go b/internal/build/build.go index f32c5f7e..bb4eda76 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -78,6 +78,7 @@ type Config struct { BinPath string AppExt string // ".exe" on Windows, empty on Unix OutFile string // only valid for ModeBuild when len(pkgs) == 1 + FileFormat string // File format override (e.g., "bin", "hex", "elf", "uf2", "zip") - takes precedence over target's default RunArgs []string // only valid for ModeRun Mode Mode AbiMode AbiMode @@ -685,13 +686,19 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, global l binFmt := ctx.crossCompile.BinaryFormat binExt := firmware.BinaryExt(binFmt) + // Determine final output extension from user-specified file format + outExt := binExt + if conf.FileFormat != "" { + outExt = firmware.GetFileExtFromFormat(conf.FileFormat) + } + // app: converted firmware output file or executable file // orgApp: before converted output file app, orgApp, err := generateOutputFilenames( conf.OutFile, conf.BinPath, conf.AppExt, - binExt, + outExt, name, mode, len(ctx.initial) > 1, @@ -759,10 +766,69 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, global l err = linkObjFiles(ctx, orgApp, objFiles, linkArgs, verbose) check(err) - if orgApp != app { + // Handle firmware conversion and file format conversion + currentApp := orgApp + + // Determine if firmware conversion is needed based on mode + needFirmwareConversion := false + if mode == ModeBuild { + // For build command, do firmware conversion if file-format is specified + needFirmwareConversion = conf.FileFormat != "" + } else { + // For run and install commands, do firmware conversion if binExt is set + needFirmwareConversion = binExt != "" + } + + // Step 1: Firmware conversion if needed + if needFirmwareConversion { + if outExt == binExt { + // Direct conversion to final output + if verbose { + fmt.Fprintf(os.Stderr, "Converting to firmware format: %s (%s -> %s)\n", ctx.crossCompile.BinaryFormat, currentApp, app) + } + err = firmware.MakeFirmwareImage(currentApp, app, ctx.crossCompile.BinaryFormat, ctx.crossCompile.FormatDetail) + check(err) + currentApp = app + } else { + // Convert to intermediate file first + tmpFile, err := os.CreateTemp("", "llgo-*"+binExt) + check(err) + tmpFile.Close() + intermediateApp := tmpFile.Name() + + if verbose { + fmt.Fprintf(os.Stderr, "Converting to firmware format: %s (%s -> %s)\n", ctx.crossCompile.BinaryFormat, currentApp, intermediateApp) + } + err = firmware.MakeFirmwareImage(currentApp, intermediateApp, ctx.crossCompile.BinaryFormat, ctx.crossCompile.FormatDetail) + check(err) + currentApp = intermediateApp + defer func() { + // Only remove if the intermediate file still exists (wasn't moved) + if _, err := os.Stat(intermediateApp); err == nil { + os.Remove(intermediateApp) + } + }() + } fmt.Printf("cross compile: %#v\n", ctx.crossCompile) - err = firmware.MakeFirmwareImage(orgApp, app, ctx.crossCompile.BinaryFormat, ctx.crossCompile.FormatDetail) - check(err) + } + + // Step 2: File format conversion if needed + if currentApp != app { + if conf.FileFormat != "" { + // File format conversion + if verbose { + fmt.Fprintf(os.Stderr, "Converting to file format: %s (%s -> %s)\n", conf.FileFormat, currentApp, app) + } + err = firmware.ConvertOutput(currentApp, app, binFmt, conf.FileFormat) + check(err) + } else { + // Just move/copy the file + if verbose { + fmt.Fprintf(os.Stderr, "Moving file: %s -> %s\n", currentApp, app) + } + err = os.Rename(currentApp, app) + check(err) + } } switch mode { diff --git a/internal/firmware/firmware.go b/internal/firmware/firmware.go index bbe42e4e..77813637 100644 --- a/internal/firmware/firmware.go +++ b/internal/firmware/firmware.go @@ -2,6 +2,8 @@ package firmware import ( "fmt" + "io" + "os" "strings" ) @@ -17,3 +19,73 @@ func MakeFirmwareImage(infile, outfile, format, fmtDetail string) error { } return fmt.Errorf("unsupported firmware format: %s", format) } + +// GetFileExtFromFormat converts file format to file extension +func GetFileExtFromFormat(format string) string { + switch format { + case "bin": + return ".bin" + case "hex": + return ".hex" + case "elf": + return "" + case "uf2": + return ".uf2" + case "zip": + return ".zip" + default: + return "" + } +} + +// ConvertOutput converts a binary file to the specified format. +// If binaryFormat == fileFormat, no conversion is needed. +// Otherwise, only hex format conversion is supported. +func ConvertOutput(infile, outfile, binaryFormat, fileFormat string) error { + // If formats match, no conversion needed + if binaryFormat == fileFormat { + return nil + } + + // Only support conversion to hex format + if fileFormat == "hex" { + return convertToHex(infile, outfile) + } + + return fmt.Errorf("unsupported format conversion from %s to %s", binaryFormat, fileFormat) +} + +// convertToHex converts binary file to hex format (each byte as two hex characters) +func convertToHex(infile, outfile string) error { + srcFile, err := os.Open(infile) + if err != nil { + return err + } + defer srcFile.Close() + + dstFile, err := os.Create(outfile) + if err != nil { + return err + } + defer dstFile.Close() + + // Read input file and convert each byte to two hex characters + buf := make([]byte, 4096) // Read in chunks + for { + n, err := srcFile.Read(buf) + if n > 0 { + for i := 0; i < n; i++ { + if _, writeErr := fmt.Fprintf(dstFile, "%02x", buf[i]); writeErr != nil { + return writeErr + } + } + } + if err == io.EOF { + break + } + if err != nil { + return err + } + } + return nil +}