2
.github/codecov.yml
vendored
2
.github/codecov.yml
vendored
@@ -10,4 +10,6 @@ coverage:
|
||||
- "internal/typepatch"
|
||||
- "internal/github"
|
||||
- "internal/firmware"
|
||||
- "internal/flash"
|
||||
- "internal/monitor"
|
||||
- "xtool"
|
||||
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -44,3 +44,11 @@ go.work*
|
||||
# GoReleaser
|
||||
.dist/
|
||||
.sysroot/
|
||||
|
||||
# Embedded firmware files
|
||||
*.bin
|
||||
*.hex
|
||||
*.elf
|
||||
*.uf2
|
||||
*.img
|
||||
*.zip
|
||||
|
||||
@@ -36,7 +36,11 @@ var Cmd = &base.Command{
|
||||
func init() {
|
||||
Cmd.Run = runCmd
|
||||
base.PassBuildFlags(Cmd)
|
||||
|
||||
flags.AddCommonFlags(&Cmd.Flag)
|
||||
flags.AddBuildFlags(&Cmd.Flag)
|
||||
flags.AddEmulatorFlags(&Cmd.Flag)
|
||||
flags.AddEmbeddedFlags(&Cmd.Flag)
|
||||
flags.AddOutputFlags(&Cmd.Flag)
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ var Cmd = &base.Command{
|
||||
|
||||
func init() {
|
||||
Cmd.Run = runCmd
|
||||
flags.AddCommonFlags(&Cmd.Flag)
|
||||
flags.AddBuildFlags(&Cmd.Flag)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,26 +8,41 @@ import (
|
||||
)
|
||||
|
||||
var OutputFile string
|
||||
var OutBin bool
|
||||
var OutHex bool
|
||||
var OutImg bool
|
||||
var OutUf2 bool
|
||||
var OutZip bool
|
||||
|
||||
func AddOutputFlags(fs *flag.FlagSet) {
|
||||
fs.StringVar(&OutputFile, "o", "", "Output file")
|
||||
fs.BoolVar(&OutBin, "obin", false, "Generate binary output (.bin)")
|
||||
fs.BoolVar(&OutHex, "ohex", false, "Generate Intel hex output (.hex)")
|
||||
fs.BoolVar(&OutImg, "oimg", false, "Generate image output (.img)")
|
||||
fs.BoolVar(&OutUf2, "ouf2", false, "Generate UF2 output (.uf2)")
|
||||
fs.BoolVar(&OutZip, "ozip", false, "Generate ZIP/DFU output (.zip)")
|
||||
}
|
||||
|
||||
var Verbose bool
|
||||
var BuildEnv string
|
||||
var Tags string
|
||||
var Target string
|
||||
var Emulator bool
|
||||
var Port string
|
||||
var BaudRate int
|
||||
var AbiMode int
|
||||
var CheckLinkArgs bool
|
||||
var CheckLLFiles bool
|
||||
var GenLLFiles bool
|
||||
var ForceEspClang bool
|
||||
|
||||
func AddCommonFlags(fs *flag.FlagSet) {
|
||||
fs.BoolVar(&Verbose, "v", false, "Verbose output")
|
||||
}
|
||||
|
||||
func AddBuildFlags(fs *flag.FlagSet) {
|
||||
fs.BoolVar(&Verbose, "v", false, "Verbose mode")
|
||||
fs.StringVar(&Tags, "tags", "", "Build tags")
|
||||
fs.StringVar(&BuildEnv, "buildenv", "", "Build environment")
|
||||
fs.StringVar(&Target, "target", "", "Target platform (e.g., rp2040, wasi)")
|
||||
if buildenv.Dev {
|
||||
fs.IntVar(&AbiMode, "abi", 2, "ABI mode (default 2). 0 = none, 1 = cfunc, 2 = allfunc.")
|
||||
fs.BoolVar(&CheckLinkArgs, "check-linkargs", false, "check link args valid")
|
||||
@@ -39,6 +54,16 @@ func AddBuildFlags(fs *flag.FlagSet) {
|
||||
|
||||
var Gen bool
|
||||
|
||||
func AddEmulatorFlags(fs *flag.FlagSet) {
|
||||
fs.BoolVar(&Emulator, "emulator", false, "Run in emulator mode")
|
||||
}
|
||||
|
||||
func AddEmbeddedFlags(fs *flag.FlagSet) {
|
||||
fs.StringVar(&Target, "target", "", "Target platform (e.g., rp2040, wasi)")
|
||||
fs.StringVar(&Port, "port", "", "Target port for flashing")
|
||||
fs.IntVar(&BaudRate, "baudrate", 115200, "Baudrate for serial communication")
|
||||
}
|
||||
|
||||
func AddCmpTestFlags(fs *flag.FlagSet) {
|
||||
fs.BoolVar(&Gen, "gen", false, "Generate llgo.expect file")
|
||||
}
|
||||
@@ -47,10 +72,24 @@ func UpdateConfig(conf *build.Config) {
|
||||
conf.Tags = Tags
|
||||
conf.Verbose = Verbose
|
||||
conf.Target = Target
|
||||
conf.Port = Port
|
||||
conf.BaudRate = BaudRate
|
||||
switch conf.Mode {
|
||||
case build.ModeBuild:
|
||||
conf.OutFile = OutputFile
|
||||
conf.OutFmts = build.OutFmts{
|
||||
Bin: OutBin,
|
||||
Hex: OutHex,
|
||||
Img: OutImg,
|
||||
Uf2: OutUf2,
|
||||
Zip: OutZip,
|
||||
}
|
||||
case build.ModeRun, build.ModeTest:
|
||||
conf.Emulator = Emulator
|
||||
case build.ModeInstall:
|
||||
|
||||
case build.ModeCmpTest:
|
||||
conf.Emulator = Emulator
|
||||
conf.GenExpect = Gen
|
||||
}
|
||||
if buildenv.Dev {
|
||||
|
||||
@@ -35,7 +35,9 @@ var Cmd = &base.Command{
|
||||
|
||||
func init() {
|
||||
Cmd.Run = runCmd
|
||||
flags.AddCommonFlags(&Cmd.Flag)
|
||||
flags.AddBuildFlags(&Cmd.Flag)
|
||||
flags.AddEmbeddedFlags(&Cmd.Flag)
|
||||
}
|
||||
|
||||
func runCmd(cmd *base.Command, args []string) {
|
||||
|
||||
77
cmd/internal/monitor/monitor.go
Normal file
77
cmd/internal/monitor/monitor.go
Normal file
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright (c) 2025 The GoPlus Authors (goplus.org). All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package monitor
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/goplus/llgo/cmd/internal/base"
|
||||
"github.com/goplus/llgo/cmd/internal/flags"
|
||||
"github.com/goplus/llgo/internal/crosscompile"
|
||||
"github.com/goplus/llgo/internal/monitor"
|
||||
)
|
||||
|
||||
// Cmd represents the monitor command.
|
||||
var Cmd = &base.Command{
|
||||
UsageLine: "llgo monitor [flags] [executable]",
|
||||
Short: "Monitor serial output from device",
|
||||
}
|
||||
|
||||
func init() {
|
||||
flags.AddCommonFlags(&Cmd.Flag)
|
||||
flags.AddEmbeddedFlags(&Cmd.Flag)
|
||||
Cmd.Run = runMonitor
|
||||
}
|
||||
|
||||
func runMonitor(cmd *base.Command, args []string) {
|
||||
cmd.Flag.Parse(args)
|
||||
args = cmd.Flag.Args()
|
||||
|
||||
if len(args) > 1 {
|
||||
fmt.Fprintf(os.Stderr, "llgo monitor: too many arguments\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var executable string
|
||||
if len(args) == 1 {
|
||||
executable = args[0]
|
||||
}
|
||||
|
||||
var serialPort []string
|
||||
if flags.Target != "" {
|
||||
conf, err := crosscompile.UseTarget(flags.Target)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "llgo monitor: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
serialPort = conf.Device.SerialPort
|
||||
}
|
||||
|
||||
config := monitor.MonitorConfig{
|
||||
Port: flags.Port,
|
||||
Target: flags.Target,
|
||||
BaudRate: flags.BaudRate,
|
||||
Executable: executable,
|
||||
SerialPort: serialPort,
|
||||
}
|
||||
|
||||
if err := monitor.Monitor(config, flags.Verbose); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "llgo monitor: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -48,8 +48,16 @@ func init() {
|
||||
Cmd.Run = runCmd
|
||||
CmpTestCmd.Run = runCmpTest
|
||||
base.PassBuildFlags(Cmd)
|
||||
flags.AddCommonFlags(&Cmd.Flag)
|
||||
flags.AddBuildFlags(&Cmd.Flag)
|
||||
flags.AddEmulatorFlags(&Cmd.Flag)
|
||||
flags.AddEmbeddedFlags(&Cmd.Flag) // for -target support
|
||||
|
||||
base.PassBuildFlags(CmpTestCmd)
|
||||
flags.AddCommonFlags(&CmpTestCmd.Flag)
|
||||
flags.AddBuildFlags(&CmpTestCmd.Flag)
|
||||
flags.AddEmulatorFlags(&CmpTestCmd.Flag)
|
||||
flags.AddEmbeddedFlags(&CmpTestCmd.Flag) // for -target support
|
||||
flags.AddCmpTestFlags(&CmpTestCmd.Flag)
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,10 @@ var Cmd = &base.Command{
|
||||
|
||||
func init() {
|
||||
Cmd.Run = runCmd
|
||||
flags.AddCommonFlags(&Cmd.Flag)
|
||||
flags.AddBuildFlags(&Cmd.Flag)
|
||||
flags.AddEmulatorFlags(&Cmd.Flag)
|
||||
flags.AddEmbeddedFlags(&Cmd.Flag)
|
||||
}
|
||||
|
||||
func runCmd(cmd *base.Command, args []string) {
|
||||
|
||||
29
cmd/llgo/monitor_cmd.gox
Normal file
29
cmd/llgo/monitor_cmd.gox
Normal file
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (c) 2025 The GoPlus Authors (goplus.org). All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import (
|
||||
self "github.com/goplus/llgo/cmd/internal/monitor"
|
||||
)
|
||||
|
||||
use "monitor [flags] [executable]"
|
||||
|
||||
short "Monitor serial output from device"
|
||||
|
||||
flagOff
|
||||
|
||||
run args => {
|
||||
self.Cmd.Run self.Cmd, args
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/goplus/llgo/cmd/internal/build"
|
||||
"github.com/goplus/llgo/cmd/internal/clean"
|
||||
"github.com/goplus/llgo/cmd/internal/install"
|
||||
"github.com/goplus/llgo/cmd/internal/monitor"
|
||||
"github.com/goplus/llgo/cmd/internal/run"
|
||||
"github.com/goplus/llgo/cmd/internal/test"
|
||||
"github.com/goplus/llgo/internal/env"
|
||||
@@ -40,6 +41,10 @@ type Cmd_install struct {
|
||||
type App struct {
|
||||
xcmd.App
|
||||
}
|
||||
type Cmd_monitor struct {
|
||||
xcmd.Command
|
||||
*App
|
||||
}
|
||||
type Cmd_run struct {
|
||||
xcmd.Command
|
||||
*App
|
||||
@@ -52,6 +57,7 @@ type Cmd_version struct {
|
||||
xcmd.Command
|
||||
*App
|
||||
}
|
||||
|
||||
//line cmd/llgo/main_app.gox:1
|
||||
func (this *App) MainEntry() {
|
||||
//line cmd/llgo/main_app.gox:1:1
|
||||
@@ -63,11 +69,13 @@ func (this *App) Main() {
|
||||
_xgo_obj2 := &Cmd_cmptest{App: this}
|
||||
_xgo_obj3 := &Cmd_get{App: this}
|
||||
_xgo_obj4 := &Cmd_install{App: this}
|
||||
_xgo_obj5 := &Cmd_run{App: this}
|
||||
_xgo_obj6 := &Cmd_test{App: this}
|
||||
_xgo_obj7 := &Cmd_version{App: this}
|
||||
xcmd.Gopt_App_Main(this, _xgo_obj0, _xgo_obj1, _xgo_obj2, _xgo_obj3, _xgo_obj4, _xgo_obj5, _xgo_obj6, _xgo_obj7)
|
||||
_xgo_obj5 := &Cmd_monitor{App: this}
|
||||
_xgo_obj6 := &Cmd_run{App: this}
|
||||
_xgo_obj7 := &Cmd_test{App: this}
|
||||
_xgo_obj8 := &Cmd_version{App: this}
|
||||
xcmd.Gopt_App_Main(this, _xgo_obj0, _xgo_obj1, _xgo_obj2, _xgo_obj3, _xgo_obj4, _xgo_obj5, _xgo_obj6, _xgo_obj7, _xgo_obj8)
|
||||
}
|
||||
|
||||
//line cmd/llgo/build_cmd.gox:20
|
||||
func (this *Cmd_build) Main(_xgo_arg0 string) {
|
||||
this.Command.Main(_xgo_arg0)
|
||||
@@ -86,6 +94,7 @@ func (this *Cmd_build) Main(_xgo_arg0 string) {
|
||||
func (this *Cmd_build) Classfname() string {
|
||||
return "build"
|
||||
}
|
||||
|
||||
//line cmd/llgo/clean_cmd.gox:20
|
||||
func (this *Cmd_clean) Main(_xgo_arg0 string) {
|
||||
this.Command.Main(_xgo_arg0)
|
||||
@@ -104,6 +113,7 @@ func (this *Cmd_clean) Main(_xgo_arg0 string) {
|
||||
func (this *Cmd_clean) Classfname() string {
|
||||
return "clean"
|
||||
}
|
||||
|
||||
//line cmd/llgo/cmptest_cmd.gox:20
|
||||
func (this *Cmd_cmptest) Main(_xgo_arg0 string) {
|
||||
this.Command.Main(_xgo_arg0)
|
||||
@@ -122,6 +132,7 @@ func (this *Cmd_cmptest) Main(_xgo_arg0 string) {
|
||||
func (this *Cmd_cmptest) Classfname() string {
|
||||
return "cmptest"
|
||||
}
|
||||
|
||||
//line cmd/llgo/get_cmd.gox:16
|
||||
func (this *Cmd_get) Main(_xgo_arg0 string) {
|
||||
this.Command.Main(_xgo_arg0)
|
||||
@@ -138,6 +149,7 @@ func (this *Cmd_get) Main(_xgo_arg0 string) {
|
||||
func (this *Cmd_get) Classfname() string {
|
||||
return "get"
|
||||
}
|
||||
|
||||
//line cmd/llgo/install_cmd.gox:20
|
||||
func (this *Cmd_install) Main(_xgo_arg0 string) {
|
||||
this.Command.Main(_xgo_arg0)
|
||||
@@ -156,6 +168,26 @@ func (this *Cmd_install) Main(_xgo_arg0 string) {
|
||||
func (this *Cmd_install) Classfname() string {
|
||||
return "install"
|
||||
}
|
||||
|
||||
//line cmd/llgo/monitor_cmd.gox:21
|
||||
func (this *Cmd_monitor) Main(_xgo_arg0 string) {
|
||||
this.Command.Main(_xgo_arg0)
|
||||
//line cmd/llgo/monitor_cmd.gox:21:1
|
||||
this.Use("monitor [flags] [executable]")
|
||||
//line cmd/llgo/monitor_cmd.gox:23:1
|
||||
this.Short("Monitor serial output from device")
|
||||
//line cmd/llgo/monitor_cmd.gox:25:1
|
||||
this.FlagOff()
|
||||
//line cmd/llgo/monitor_cmd.gox:27:1
|
||||
this.Run__1(func(args []string) {
|
||||
//line cmd/llgo/monitor_cmd.gox:28:1
|
||||
monitor.Cmd.Run(monitor.Cmd, args)
|
||||
})
|
||||
}
|
||||
func (this *Cmd_monitor) Classfname() string {
|
||||
return "monitor"
|
||||
}
|
||||
|
||||
//line cmd/llgo/run_cmd.gox:20
|
||||
func (this *Cmd_run) Main(_xgo_arg0 string) {
|
||||
this.Command.Main(_xgo_arg0)
|
||||
@@ -174,6 +206,7 @@ func (this *Cmd_run) Main(_xgo_arg0 string) {
|
||||
func (this *Cmd_run) Classfname() string {
|
||||
return "run"
|
||||
}
|
||||
|
||||
//line cmd/llgo/test_cmd.gox:20
|
||||
func (this *Cmd_test) Main(_xgo_arg0 string) {
|
||||
this.Command.Main(_xgo_arg0)
|
||||
@@ -192,6 +225,7 @@ func (this *Cmd_test) Main(_xgo_arg0 string) {
|
||||
func (this *Cmd_test) Classfname() string {
|
||||
return "test"
|
||||
}
|
||||
|
||||
//line cmd/llgo/version_cmd.gox:22
|
||||
func (this *Cmd_version) Main(_xgo_arg0 string) {
|
||||
this.Command.Main(_xgo_arg0)
|
||||
|
||||
78
doc/Embedded_Cmd.md
Normal file
78
doc/Embedded_Cmd.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# LLGo Embedded Development Command Line Options
|
||||
|
||||
## Flags
|
||||
|
||||
- `-o <file>` - Specify output file name
|
||||
- `-target <platform>` - Specify target platform for cross-compilation
|
||||
- `-obin` - Generate binary format output (requires `-target`)
|
||||
- `-ohex` - Generate Intel HEX format output (requires `-target`)
|
||||
- `-oimg` - Generate firmware image format output (requires `-target`)
|
||||
- `-ouf2` - Generate UF2 format output (requires `-target`)
|
||||
- `-ozip` - Generate ZIP/DFU format output (requires `-target`)
|
||||
- `-emulator` - Run using emulator (auto-detects required format)
|
||||
- `-port <port>` - Target port for flashing, testing, or monitoring
|
||||
- `-baudrate <rate>` - Baudrate for serial communication (default: 115200)
|
||||
|
||||
## Commands
|
||||
|
||||
### llgo build
|
||||
Compile program to output file.
|
||||
- No `-target`: Native executable
|
||||
- With `-target`: ELF executable (and additional formats if specified with `-obin`, `-ohex`, etc.)
|
||||
|
||||
### llgo run
|
||||
Compile and run program.
|
||||
- No `-target`: Run locally
|
||||
- With `-target`: Run on device or emulator (equivalent to `install` + `monitor`)
|
||||
|
||||
### llgo test
|
||||
Compile and run tests.
|
||||
- No `-target`: Run tests locally
|
||||
- With `-target`: Run tests on device or emulator
|
||||
- Supports `-emulator` and `-port` flags
|
||||
|
||||
### llgo install
|
||||
Install program or flash to device.
|
||||
- No `-target`: Install to `$GOPATH/bin`
|
||||
- With `-target`: Flash to device (use `-port` to specify port)
|
||||
|
||||
### llgo monitor
|
||||
Monitor serial output from embedded device.
|
||||
- `-port <device>`: Serial port device (e.g., `/dev/ttyUSB0`, `COM3`)
|
||||
- `-target <platform>`: Auto-detect port from target configuration
|
||||
- `-baudrate <rate>`: Communication speed (default: 115200)
|
||||
- `[executable]`: Optional ELF file for debug info (panic address resolution)
|
||||
|
||||
Features:
|
||||
- Real-time bidirectional serial communication
|
||||
- Automatic port detection from target configuration
|
||||
- Debug info integration for panic address resolution
|
||||
- Cross-platform support (Linux, macOS, Windows)
|
||||
- Graceful exit with Ctrl-C
|
||||
|
||||
## 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.elf
|
||||
llgo build -target esp32 -obin . # -> hello.elf + hello.bin
|
||||
llgo build -target esp32 -ohex . # -> hello.elf + hello.bin + hello.hex
|
||||
llgo build -target esp32 -obin -ohex -oimg . # -> hello.elf + hello.bin + hello.hex + hello.img
|
||||
llgo run -target esp32 . # run on ESP32 (guess a port)
|
||||
llgo run -target esp32 -emulator . # run in emulator
|
||||
llgo test -target esp32 -port /dev/ttyUSB0 . # run tests on device
|
||||
llgo test -target esp32 -emulator . # run tests in emulator
|
||||
llgo install -target esp32 -port /dev/ttyUSB0 . # flash to specific port
|
||||
|
||||
# Monitor device output
|
||||
llgo monitor -port /dev/ttyUSB0 # monitor with specific port
|
||||
llgo monitor -target esp32 # monitor with auto-detected port
|
||||
llgo monitor -target esp32 -baudrate 9600 # custom baudrate
|
||||
llgo monitor -port /dev/ttyUSB0 ./firmware.elf # with debug info for panic resolution
|
||||
```
|
||||
10
go.mod
10
go.mod
@@ -15,11 +15,19 @@ require (
|
||||
golang.org/x/tools v0.36.0
|
||||
)
|
||||
|
||||
require github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1
|
||||
require (
|
||||
github.com/marcinbor85/gohex v0.0.0-20210308104911-55fb1c624d84
|
||||
github.com/mattn/go-tty v0.0.7
|
||||
github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1
|
||||
go.bug.st/serial v1.6.4
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/creack/goselect v0.1.2 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
golang.org/x/mod v0.27.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
)
|
||||
|
||||
replace github.com/goplus/llgo/runtime => ./runtime
|
||||
|
||||
21
go.sum
21
go.sum
@@ -1,3 +1,7 @@
|
||||
github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0=
|
||||
github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/goplus/cobra v1.9.12 h1:0F9EdEbeGyITGz+mqoHoJ5KpUw97p1CkxV74IexHw5s=
|
||||
@@ -10,13 +14,30 @@ github.com/goplus/llvm v0.8.5 h1:DUnFeYC3Rco622tBEKGg8xkigRAV2fh5ZIfBCt7gOSs=
|
||||
github.com/goplus/llvm v0.8.5/go.mod h1:PeVK8GgzxwAYCiMiUAJb5wJR6xbhj989tu9oulKLLT4=
|
||||
github.com/goplus/mod v0.17.1 h1:ITovxDcc5zbURV/Wrp3/SBsYLgC1KrxY6pq1zMM2V94=
|
||||
github.com/goplus/mod v0.17.1/go.mod h1:iXEszBKqi38BAyQApBPyQeurLHmQN34YMgC2ZNdap50=
|
||||
github.com/marcinbor85/gohex v0.0.0-20210308104911-55fb1c624d84 h1:hyAgCuG5nqTMDeUD8KZs7HSPs6KprPgPP8QmGV8nyvk=
|
||||
github.com/marcinbor85/gohex v0.0.0-20210308104911-55fb1c624d84/go.mod h1:Pb6XcsXyropB9LNHhnqaknG/vEwYztLkQzVCHv8sQ3M=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-tty v0.0.7 h1:KJ486B6qI8+wBO7kQxYgmmEFDaFEE96JMBQ7h400N8Q=
|
||||
github.com/mattn/go-tty v0.0.7/go.mod h1:f2i5ZOvXBU/tCABmLmOfzLz9azMo5wdAaElRNnJKr+k=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/qiniu/x v1.15.1 h1:avE+YQaowp8ZExjylOeSM73rUo3MQKBAYVxh4NJ8dY8=
|
||||
github.com/qiniu/x v1.15.1/go.mod h1:AiovSOCaRijaf3fj+0CBOpR1457pn24b0Vdb1JpwhII=
|
||||
github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1 h1:NVK+OqnavpyFmUiKfUMHrpvbCi2VFoWTrcpI7aDaJ2I=
|
||||
github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
go.bug.st/serial v1.6.4 h1:7FmqNPgVp3pu2Jz5PoPtbZ9jJO5gnEnZIvnI1lzve8A=
|
||||
go.bug.st/serial v1.6.4/go.mod h1:nofMJxTeNVny/m6+KaafC6vJGj3miwQZ6vW4BZUGJPI=
|
||||
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
||||
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -18,7 +18,6 @@ package build
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"debug/macho"
|
||||
"errors"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
@@ -43,7 +42,9 @@ import (
|
||||
"github.com/goplus/llgo/internal/crosscompile"
|
||||
"github.com/goplus/llgo/internal/env"
|
||||
"github.com/goplus/llgo/internal/firmware"
|
||||
"github.com/goplus/llgo/internal/flash"
|
||||
"github.com/goplus/llgo/internal/mockable"
|
||||
"github.com/goplus/llgo/internal/monitor"
|
||||
"github.com/goplus/llgo/internal/packages"
|
||||
"github.com/goplus/llgo/internal/typepatch"
|
||||
"github.com/goplus/llgo/ssa/abi"
|
||||
@@ -71,14 +72,37 @@ const (
|
||||
debugBuild = packages.DebugPackagesLoad
|
||||
)
|
||||
|
||||
// OutFmts contains output format specifications for embedded targets
|
||||
type OutFmts struct {
|
||||
Bin bool // Generate binary output (.bin)
|
||||
Hex bool // Generate Intel hex output (.hex)
|
||||
Img bool // Generate image output (.img)
|
||||
Uf2 bool // Generate UF2 output (.uf2)
|
||||
Zip bool // Generate ZIP/DFU output (.zip)
|
||||
}
|
||||
|
||||
// OutFmtDetails contains detailed output file paths for each format
|
||||
type OutFmtDetails struct {
|
||||
Out string // Base output file path
|
||||
Bin string // Binary output file path (.bin)
|
||||
Hex string // Intel hex output file path (.hex)
|
||||
Img string // Image output file path (.img)
|
||||
Uf2 string // UF2 output file path (.uf2)
|
||||
Zip string // ZIP/DFU output file path (.zip)
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Goos string
|
||||
Goarch string
|
||||
Target string // target name (e.g., "rp2040", "wasi") - takes precedence over Goos/Goarch
|
||||
BinPath string
|
||||
AppExt string // ".exe" on Windows, empty on Unix
|
||||
OutFile string // only valid for ModeBuild when len(pkgs) == 1
|
||||
RunArgs []string // only valid for ModeRun
|
||||
AppExt string // ".exe" on Windows, empty on Unix
|
||||
OutFile string // only valid for ModeBuild when len(pkgs) == 1
|
||||
OutFmts OutFmts // Output format specifications (only for Target != "")
|
||||
Emulator bool // run in emulator mode
|
||||
Port string // target port for flashing
|
||||
BaudRate int // baudrate for serial communication
|
||||
RunArgs []string
|
||||
Mode Mode
|
||||
AbiMode AbiMode
|
||||
GenExpect bool // only valid for ModeCmpTest
|
||||
@@ -117,7 +141,6 @@ func NewDefaultConf(mode Mode) *Config {
|
||||
BinPath: bin,
|
||||
Mode: mode,
|
||||
AbiMode: cabi.ModeAllFunc,
|
||||
AppExt: DefaultAppExt(goos),
|
||||
}
|
||||
return conf
|
||||
}
|
||||
@@ -133,8 +156,15 @@ func envGOPATH() (string, error) {
|
||||
return filepath.Join(home, "go"), nil
|
||||
}
|
||||
|
||||
func DefaultAppExt(goos string) string {
|
||||
switch goos {
|
||||
func defaultAppExt(conf *Config) string {
|
||||
if conf.Target != "" {
|
||||
if strings.HasPrefix(conf.Target, "wasi") || strings.HasPrefix(conf.Target, "wasm") {
|
||||
return ".wasm"
|
||||
}
|
||||
return ".elf"
|
||||
}
|
||||
|
||||
switch conf.Goos {
|
||||
case "windows":
|
||||
return ".exe"
|
||||
case "wasi", "wasip1", "js":
|
||||
@@ -159,6 +189,9 @@ func Do(args []string, conf *Config) ([]Package, error) {
|
||||
if conf.Goarch == "" {
|
||||
conf.Goarch = runtime.GOARCH
|
||||
}
|
||||
if conf.AppExt == "" {
|
||||
conf.AppExt = defaultAppExt(conf)
|
||||
}
|
||||
// Handle crosscompile configuration first to set correct GOOS/GOARCH
|
||||
forceEspClang := conf.ForceEspClang || conf.Target != ""
|
||||
export, err := crosscompile.Use(conf.Goos, conf.Goarch, conf.Target, IsWasiThreadsEnabled(), forceEspClang)
|
||||
@@ -238,6 +271,10 @@ func Do(args []string, conf *Config) ([]Package, error) {
|
||||
if conf.OutFile != "" {
|
||||
return nil, fmt.Errorf("cannot build multiple packages with -o")
|
||||
}
|
||||
case ModeInstall:
|
||||
if conf.Target != "" {
|
||||
return nil, fmt.Errorf("cannot install multiple packages to embedded target")
|
||||
}
|
||||
case ModeRun:
|
||||
return nil, fmt.Errorf("cannot run multiple packages")
|
||||
case ModeTest:
|
||||
@@ -314,7 +351,67 @@ func Do(args []string, conf *Config) ([]Package, error) {
|
||||
|
||||
for _, pkg := range initial {
|
||||
if needLink(pkg, mode) {
|
||||
linkMainPkg(ctx, pkg, allPkgs, global, conf, mode, verbose)
|
||||
name := path.Base(pkg.PkgPath)
|
||||
|
||||
// Create output format details
|
||||
outFmts, err := buildOutFmts(name, conf, len(ctx.initial) > 1, &ctx.crossCompile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Link main package using the base output path
|
||||
err = linkMainPkg(ctx, pkg, allPkgs, global, outFmts.Out, verbose)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
envMap := outFmts.ToEnvMap()
|
||||
|
||||
// Only convert formats when Target is specified
|
||||
if conf.Target != "" {
|
||||
// Process format conversions for embedded targets
|
||||
err = firmware.ConvertFormats(ctx.crossCompile.BinaryFormat, ctx.crossCompile.FormatDetail, envMap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
switch mode {
|
||||
case ModeBuild:
|
||||
// Do nothing
|
||||
|
||||
case ModeInstall:
|
||||
// Native already installed in linkMainPkg
|
||||
if conf.Target != "" {
|
||||
err = flash.FlashDevice(ctx.crossCompile.Device, envMap, ctx.buildConf.Port, verbose)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
case ModeRun, ModeTest, ModeCmpTest:
|
||||
if conf.Target == "" {
|
||||
err = runNative(ctx, outFmts.Out, pkg.Dir, pkg.PkgPath, conf, mode)
|
||||
} else if conf.Emulator {
|
||||
err = runInEmulator(ctx.crossCompile.Emulator, envMap, pkg.Dir, pkg.PkgPath, conf, mode, verbose)
|
||||
} else {
|
||||
err = flash.FlashDevice(ctx.crossCompile.Device, envMap, ctx.buildConf.Port, verbose)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
monitorConfig := monitor.MonitorConfig{
|
||||
Port: ctx.buildConf.Port,
|
||||
Target: conf.Target,
|
||||
Executable: outFmts.Out,
|
||||
BaudRate: conf.BaudRate,
|
||||
SerialPort: ctx.crossCompile.Device.SerialPort,
|
||||
}
|
||||
err = monitor.Monitor(monitorConfig, verbose)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -629,76 +726,7 @@ 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)
|
||||
|
||||
// app: converted firmware output file or executable file
|
||||
// orgApp: before converted output file
|
||||
app, orgApp, err := generateOutputFilenames(
|
||||
conf.OutFile,
|
||||
conf.BinPath,
|
||||
conf.AppExt,
|
||||
binExt,
|
||||
name,
|
||||
mode,
|
||||
len(ctx.initial) > 1,
|
||||
)
|
||||
check(err)
|
||||
func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, global llssa.Package, outputPath string, verbose bool) error {
|
||||
|
||||
needRuntime := false
|
||||
needPyInit := false
|
||||
@@ -726,18 +754,24 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, global l
|
||||
}
|
||||
})
|
||||
entryObjFile, err := genMainModuleFile(ctx, llssa.PkgRuntime, pkg, needRuntime, needPyInit)
|
||||
check(err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// defer os.Remove(entryLLFile)
|
||||
objFiles = append(objFiles, entryObjFile)
|
||||
|
||||
// Compile extra files from target configuration
|
||||
extraObjFiles, err := compileExtraFiles(ctx, verbose)
|
||||
check(err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
objFiles = append(objFiles, extraObjFiles...)
|
||||
|
||||
if global != nil {
|
||||
export, err := exportObject(ctx, pkg.PkgPath+".global", pkg.ExportFile+"-global", []byte(global.String()))
|
||||
check(err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
objFiles = append(objFiles, export)
|
||||
}
|
||||
|
||||
@@ -758,66 +792,7 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, global l
|
||||
}
|
||||
}
|
||||
|
||||
err = linkObjFiles(ctx, orgApp, objFiles, linkArgs, verbose)
|
||||
check(err)
|
||||
|
||||
if orgApp != app {
|
||||
err = firmware.MakeFirmwareImage(orgApp, app, ctx.crossCompile.BinaryFormat, ctx.crossCompile.FormatDetail)
|
||||
check(err)
|
||||
}
|
||||
|
||||
switch mode {
|
||||
case ModeTest:
|
||||
cmd := exec.Command(app, conf.RunArgs...)
|
||||
cmd.Dir = pkg.Dir
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Run()
|
||||
if s := cmd.ProcessState; s != nil {
|
||||
exitCode := s.ExitCode()
|
||||
fmt.Fprintf(os.Stderr, "%s: exit code %d\n", app, exitCode)
|
||||
if !ctx.testFail && exitCode != 0 {
|
||||
ctx.testFail = true
|
||||
}
|
||||
}
|
||||
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...)
|
||||
}
|
||||
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)
|
||||
}
|
||||
return linkObjFiles(ctx, outputPath, objFiles, linkArgs, verbose)
|
||||
}
|
||||
|
||||
func linkObjFiles(ctx *context, app string, objFiles, linkArgs []string, verbose bool) error {
|
||||
@@ -1289,22 +1264,6 @@ func pkgExists(initial []*packages.Package, pkg *packages.Package) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// findDylibDep finds the dylib dependency in the executable. It returns empty
|
||||
// string if not found.
|
||||
func findDylibDep(exe, lib string) string {
|
||||
file, err := macho.Open(exe)
|
||||
check(err)
|
||||
defer file.Close()
|
||||
for _, load := range file.Loads {
|
||||
if dylib, ok := load.(*macho.Dylib); ok {
|
||||
if strings.HasPrefix(filepath.Base(dylib.Name), fmt.Sprintf("lib%s.", lib)) {
|
||||
return dylib.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type none struct{}
|
||||
|
||||
func check(err error) {
|
||||
|
||||
@@ -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
|
||||
|
||||
165
internal/build/outputs.go
Normal file
165
internal/build/outputs.go
Normal file
@@ -0,0 +1,165 @@
|
||||
package build
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/goplus/llgo/internal/crosscompile"
|
||||
"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
|
||||
}
|
||||
|
||||
func genTempOutputFile(prefix, ext string) (string, error) {
|
||||
tmpFile, err := os.CreateTemp("", prefix+"-*"+ext)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
tmpFile.Close()
|
||||
return tmpFile.Name(), nil
|
||||
}
|
||||
|
||||
// setOutFmt sets the appropriate OutFmt based on format name
|
||||
func setOutFmt(conf *Config, formatName string) {
|
||||
switch formatName {
|
||||
case "bin":
|
||||
conf.OutFmts.Bin = true
|
||||
case "hex":
|
||||
conf.OutFmts.Hex = true
|
||||
case "img":
|
||||
conf.OutFmts.Img = true
|
||||
case "uf2":
|
||||
conf.OutFmts.Uf2 = true
|
||||
case "zip":
|
||||
conf.OutFmts.Zip = true
|
||||
}
|
||||
}
|
||||
|
||||
// buildOutFmts creates OutFmtDetails based on package, configuration and multi-package status
|
||||
func buildOutFmts(pkgName string, conf *Config, multiPkg bool, crossCompile *crosscompile.Export) (*OutFmtDetails, error) {
|
||||
details := &OutFmtDetails{}
|
||||
var err error
|
||||
if conf.Target == "" {
|
||||
// Native target
|
||||
if conf.Mode == ModeInstall {
|
||||
details.Out = filepath.Join(conf.BinPath, pkgName+conf.AppExt)
|
||||
} else if conf.Mode == ModeBuild && !multiPkg && conf.OutFile != "" {
|
||||
base := strings.TrimSuffix(conf.OutFile, conf.AppExt)
|
||||
details.Out = base + conf.AppExt
|
||||
} else if conf.Mode == ModeBuild && !multiPkg {
|
||||
details.Out = pkgName + conf.AppExt
|
||||
} else {
|
||||
details.Out, err = genTempOutputFile(pkgName, conf.AppExt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return details, nil
|
||||
}
|
||||
|
||||
needRun := slices.Contains([]Mode{ModeRun, ModeTest, ModeCmpTest, ModeInstall}, conf.Mode)
|
||||
|
||||
if multiPkg {
|
||||
details.Out, err = genTempOutputFile(pkgName, conf.AppExt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if conf.OutFile != "" {
|
||||
base := strings.TrimSuffix(conf.OutFile, conf.AppExt)
|
||||
details.Out = base + conf.AppExt
|
||||
} else if conf.Mode == ModeBuild {
|
||||
details.Out = pkgName + conf.AppExt
|
||||
} else {
|
||||
details.Out, err = genTempOutputFile(pkgName, conf.AppExt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Check emulator format if emulator mode is enabled
|
||||
outFmt := ""
|
||||
if needRun {
|
||||
if conf.Emulator {
|
||||
if crossCompile.Emulator != "" {
|
||||
outFmt = firmware.ExtractFileFormatFromCommand(crossCompile.Emulator)
|
||||
}
|
||||
} else {
|
||||
if crossCompile.Device.Flash.Method == "command" {
|
||||
outFmt = firmware.ExtractFileFormatFromCommand(crossCompile.Device.Flash.Command)
|
||||
}
|
||||
}
|
||||
}
|
||||
if outFmt != "" {
|
||||
setOutFmt(conf, outFmt)
|
||||
}
|
||||
|
||||
// Check binary format and set corresponding format
|
||||
if crossCompile.BinaryFormat != "" && needRun {
|
||||
envName := firmware.BinaryFormatToEnvName(crossCompile.BinaryFormat)
|
||||
if envName != "" {
|
||||
setOutFmt(conf, envName)
|
||||
}
|
||||
}
|
||||
|
||||
// Generate format-specific paths based on base output
|
||||
if details.Out != "" {
|
||||
base := strings.TrimSuffix(details.Out, filepath.Ext(details.Out))
|
||||
|
||||
if conf.OutFmts.Bin || conf.OutFmts.Img || conf.OutFmts.Hex {
|
||||
details.Bin = base + ".bin"
|
||||
}
|
||||
if conf.OutFmts.Hex {
|
||||
details.Hex = base + ".hex"
|
||||
}
|
||||
if conf.OutFmts.Img {
|
||||
details.Img = base + ".img"
|
||||
}
|
||||
if conf.OutFmts.Uf2 {
|
||||
details.Uf2 = base + ".uf2"
|
||||
}
|
||||
if conf.OutFmts.Zip {
|
||||
details.Zip = base + ".zip"
|
||||
}
|
||||
}
|
||||
|
||||
return details, nil
|
||||
}
|
||||
|
||||
// ToEnvMap converts OutFmtDetails to a map for template substitution
|
||||
func (details *OutFmtDetails) ToEnvMap() map[string]string {
|
||||
envMap := make(map[string]string)
|
||||
|
||||
if details.Out != "" {
|
||||
envMap[""] = details.Out
|
||||
envMap["out"] = details.Out
|
||||
envMap["elf"] = details.Out // alias for compatibility
|
||||
}
|
||||
if details.Bin != "" {
|
||||
envMap["bin"] = details.Bin
|
||||
}
|
||||
if details.Hex != "" {
|
||||
envMap["hex"] = details.Hex
|
||||
}
|
||||
if details.Img != "" {
|
||||
envMap["img"] = details.Img
|
||||
}
|
||||
if details.Uf2 != "" {
|
||||
envMap["uf2"] = details.Uf2
|
||||
}
|
||||
if details.Zip != "" {
|
||||
envMap["zip"] = details.Zip
|
||||
}
|
||||
|
||||
return envMap
|
||||
}
|
||||
366
internal/build/outputs_test.go
Normal file
366
internal/build/outputs_test.go
Normal file
@@ -0,0 +1,366 @@
|
||||
//go:build !llgo
|
||||
// +build !llgo
|
||||
|
||||
package build
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/goplus/llgo/internal/crosscompile"
|
||||
"github.com/goplus/llgo/internal/flash"
|
||||
)
|
||||
|
||||
func TestBuildOutFmtsWithTarget(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
conf *Config
|
||||
pkgName string
|
||||
crossCompile crosscompile.Export
|
||||
wantOut string // use empty string to indicate temp file
|
||||
wantBin string
|
||||
wantHex string
|
||||
wantImg string
|
||||
wantUf2 string
|
||||
wantZip string
|
||||
}{
|
||||
{
|
||||
name: "build with -o",
|
||||
conf: &Config{
|
||||
Mode: ModeBuild,
|
||||
OutFile: "myapp",
|
||||
AppExt: "",
|
||||
},
|
||||
pkgName: "hello",
|
||||
crossCompile: crosscompile.Export{
|
||||
BinaryFormat: "",
|
||||
},
|
||||
wantOut: "myapp",
|
||||
},
|
||||
{
|
||||
name: "build hex format",
|
||||
conf: &Config{
|
||||
Mode: ModeBuild,
|
||||
Target: "esp32",
|
||||
AppExt: ".elf",
|
||||
OutFmts: OutFmts{Hex: true},
|
||||
},
|
||||
pkgName: "hello",
|
||||
crossCompile: crosscompile.Export{
|
||||
BinaryFormat: "esp32", // This will auto-set Bin: true
|
||||
},
|
||||
wantOut: "hello.elf",
|
||||
wantBin: "hello.bin", // Now expected due to esp32 BinaryFormat
|
||||
wantHex: "hello.hex",
|
||||
},
|
||||
{
|
||||
name: "emulator mode with bin format",
|
||||
conf: &Config{
|
||||
Mode: ModeRun,
|
||||
Target: "esp32",
|
||||
AppExt: ".elf",
|
||||
Emulator: true,
|
||||
},
|
||||
pkgName: "hello",
|
||||
crossCompile: crosscompile.Export{
|
||||
BinaryFormat: "esp32",
|
||||
Emulator: "qemu-system-xtensa -machine esp32 -kernel {bin}",
|
||||
},
|
||||
wantBin: ".bin", // Should be temp file with .bin extension
|
||||
},
|
||||
{
|
||||
name: "flash command with hex format",
|
||||
conf: &Config{
|
||||
Mode: ModeInstall,
|
||||
Target: "esp32",
|
||||
AppExt: ".elf",
|
||||
Emulator: false,
|
||||
},
|
||||
pkgName: "hello",
|
||||
crossCompile: crosscompile.Export{
|
||||
BinaryFormat: "esp32", // This will auto-set Bin: true
|
||||
Device: flash.Device{
|
||||
Flash: flash.Flash{
|
||||
Method: "command",
|
||||
Command: "esptool.py --chip esp32 write_flash 0x10000 {hex}",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantBin: ".bin", // Expected due to esp32 BinaryFormat
|
||||
wantHex: ".hex", // Should be temp file with .hex extension
|
||||
},
|
||||
{
|
||||
name: "multiple formats specified",
|
||||
conf: &Config{
|
||||
Mode: ModeBuild,
|
||||
Target: "esp32",
|
||||
AppExt: ".elf",
|
||||
OutFmts: OutFmts{
|
||||
Bin: true,
|
||||
Hex: true,
|
||||
Img: true,
|
||||
Uf2: true,
|
||||
},
|
||||
},
|
||||
pkgName: "hello",
|
||||
crossCompile: crosscompile.Export{
|
||||
BinaryFormat: "esp32",
|
||||
},
|
||||
wantOut: "hello.elf",
|
||||
wantBin: "hello.bin",
|
||||
wantHex: "hello.hex",
|
||||
wantImg: "hello.img",
|
||||
wantUf2: "hello.uf2",
|
||||
wantZip: "", // Not specified
|
||||
},
|
||||
{
|
||||
name: "no formats specified",
|
||||
conf: &Config{
|
||||
Mode: ModeBuild,
|
||||
Target: "esp32",
|
||||
AppExt: ".elf",
|
||||
OutFmts: OutFmts{}, // No formats specified
|
||||
},
|
||||
pkgName: "hello",
|
||||
crossCompile: crosscompile.Export{
|
||||
BinaryFormat: "esp32",
|
||||
},
|
||||
wantOut: "hello.elf",
|
||||
wantBin: "", // No bin file should be generated
|
||||
wantHex: "", // No hex file should be generated
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Determine if multi-package (mock single package for simplicity)
|
||||
multiPkg := false
|
||||
|
||||
result, err := buildOutFmts(tt.pkgName, tt.conf, multiPkg, &tt.crossCompile)
|
||||
if err != nil {
|
||||
t.Errorf("buildOutFmts() error = %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Check base output path
|
||||
if tt.wantOut != "" {
|
||||
if result.Out != tt.wantOut {
|
||||
t.Errorf("buildOutFmts().Out = %v, want %v", result.Out, tt.wantOut)
|
||||
}
|
||||
} else {
|
||||
// Should be a temp file
|
||||
if result.Out == "" || !strings.Contains(result.Out, tt.pkgName) {
|
||||
t.Errorf("buildOutFmts().Out should be temp file containing %v, got %v", tt.pkgName, result.Out)
|
||||
}
|
||||
}
|
||||
|
||||
// Check format-specific paths
|
||||
checkFormatPath := func(actual, expected, formatName string) {
|
||||
if expected == "" {
|
||||
// Empty means no file should be generated
|
||||
if actual != "" {
|
||||
t.Errorf("buildOutFmts().%s = %v, want empty (no file generated)", formatName, actual)
|
||||
}
|
||||
} else if strings.HasPrefix(expected, ".") && !strings.Contains(expected[1:], ".") {
|
||||
// ".xxx" means temp file with .xxx extension
|
||||
if actual == "" {
|
||||
t.Errorf("buildOutFmts().%s = empty, want temp file with %s extension", formatName, expected)
|
||||
} else if !strings.HasSuffix(actual, expected) || !strings.Contains(actual, tt.pkgName) {
|
||||
t.Errorf("buildOutFmts().%s should be temp file with %s extension containing %v, got %v", formatName, expected, tt.pkgName, actual)
|
||||
}
|
||||
} else {
|
||||
// "aaa.xxx" means exact file name
|
||||
if actual != expected {
|
||||
t.Errorf("buildOutFmts().%s = %v, want %v", formatName, actual, expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checkFormatPath(result.Bin, tt.wantBin, "Bin")
|
||||
checkFormatPath(result.Hex, tt.wantHex, "Hex")
|
||||
checkFormatPath(result.Img, tt.wantImg, "Img")
|
||||
checkFormatPath(result.Uf2, tt.wantUf2, "Uf2")
|
||||
checkFormatPath(result.Zip, tt.wantZip, "Zip")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildOutFmtsNativeTarget(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
mode Mode
|
||||
multiPkg bool
|
||||
outFile string
|
||||
binPath string
|
||||
appExt string
|
||||
goos string
|
||||
pkgName string
|
||||
wantOut string
|
||||
}{
|
||||
// ModeBuild scenarios
|
||||
{
|
||||
name: "build single pkg no outfile macos",
|
||||
mode: ModeBuild,
|
||||
multiPkg: false,
|
||||
outFile: "",
|
||||
appExt: "",
|
||||
goos: "darwin",
|
||||
pkgName: "hello",
|
||||
wantOut: "hello",
|
||||
},
|
||||
{
|
||||
name: "build single pkg no outfile linux",
|
||||
mode: ModeBuild,
|
||||
multiPkg: false,
|
||||
outFile: "",
|
||||
appExt: "",
|
||||
goos: "linux",
|
||||
pkgName: "hello",
|
||||
wantOut: "hello",
|
||||
},
|
||||
{
|
||||
name: "build single pkg no outfile windows",
|
||||
mode: ModeBuild,
|
||||
multiPkg: false,
|
||||
outFile: "",
|
||||
appExt: ".exe",
|
||||
goos: "windows",
|
||||
pkgName: "hello",
|
||||
wantOut: "hello.exe",
|
||||
},
|
||||
{
|
||||
name: "build single pkg with outfile",
|
||||
mode: ModeBuild,
|
||||
multiPkg: false,
|
||||
outFile: "myapp",
|
||||
appExt: "",
|
||||
goos: "linux",
|
||||
pkgName: "hello",
|
||||
wantOut: "myapp",
|
||||
},
|
||||
{
|
||||
name: "build single pkg with outfile and ext",
|
||||
mode: ModeBuild,
|
||||
multiPkg: false,
|
||||
outFile: "myapp.exe",
|
||||
appExt: ".exe",
|
||||
goos: "windows",
|
||||
pkgName: "hello",
|
||||
wantOut: "myapp.exe",
|
||||
},
|
||||
{
|
||||
name: "build multi pkg",
|
||||
mode: ModeBuild,
|
||||
multiPkg: true,
|
||||
outFile: "",
|
||||
appExt: "",
|
||||
goos: "linux",
|
||||
pkgName: "hello",
|
||||
wantOut: "", // Should be temp file
|
||||
},
|
||||
// ModeInstall scenarios
|
||||
{
|
||||
name: "install single pkg macos",
|
||||
mode: ModeInstall,
|
||||
multiPkg: false,
|
||||
outFile: "",
|
||||
binPath: "/go/bin",
|
||||
appExt: "",
|
||||
goos: "darwin",
|
||||
pkgName: "hello",
|
||||
wantOut: "/go/bin/hello",
|
||||
},
|
||||
{
|
||||
name: "install single pkg windows",
|
||||
mode: ModeInstall,
|
||||
multiPkg: false,
|
||||
outFile: "",
|
||||
binPath: "C:/go/bin", // Use forward slashes for cross-platform compatibility
|
||||
appExt: ".exe",
|
||||
goos: "windows",
|
||||
pkgName: "hello",
|
||||
wantOut: "C:/go/bin/hello.exe",
|
||||
},
|
||||
{
|
||||
name: "install multi pkg",
|
||||
mode: ModeInstall,
|
||||
multiPkg: true,
|
||||
outFile: "",
|
||||
binPath: "/go/bin",
|
||||
appExt: "",
|
||||
goos: "linux",
|
||||
pkgName: "hello",
|
||||
wantOut: "/go/bin/hello",
|
||||
},
|
||||
// Other modes should use temp files
|
||||
{
|
||||
name: "run mode",
|
||||
mode: ModeRun,
|
||||
multiPkg: false,
|
||||
outFile: "",
|
||||
appExt: "",
|
||||
goos: "linux",
|
||||
pkgName: "hello",
|
||||
wantOut: "", // Should be temp file
|
||||
},
|
||||
{
|
||||
name: "test mode",
|
||||
mode: ModeTest,
|
||||
multiPkg: false,
|
||||
outFile: "",
|
||||
appExt: "",
|
||||
goos: "linux",
|
||||
pkgName: "hello",
|
||||
wantOut: "", // Should be temp file
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
conf := &Config{
|
||||
Mode: tt.mode,
|
||||
OutFile: tt.outFile,
|
||||
BinPath: tt.binPath,
|
||||
AppExt: tt.appExt,
|
||||
Target: "", // Native target
|
||||
}
|
||||
|
||||
crossCompile := &crosscompile.Export{}
|
||||
|
||||
result, err := buildOutFmts(tt.pkgName, conf, tt.multiPkg, crossCompile)
|
||||
if err != nil {
|
||||
t.Errorf("buildOutFmts() error = %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Check base output path
|
||||
if tt.wantOut != "" {
|
||||
if result.Out != tt.wantOut {
|
||||
t.Errorf("buildOutFmts().Out = %v, want %v", result.Out, tt.wantOut)
|
||||
}
|
||||
} else {
|
||||
// Should be a temp file
|
||||
if result.Out == "" || !strings.Contains(result.Out, tt.pkgName) {
|
||||
t.Errorf("buildOutFmts().Out should be temp file containing %v, got %v", tt.pkgName, result.Out)
|
||||
}
|
||||
}
|
||||
|
||||
// For native targets, no format files should be generated
|
||||
if result.Bin != "" {
|
||||
t.Errorf("buildOutFmts().Bin = %v, want empty for native target", result.Bin)
|
||||
}
|
||||
if result.Hex != "" {
|
||||
t.Errorf("buildOutFmts().Hex = %v, want empty for native target", result.Hex)
|
||||
}
|
||||
if result.Img != "" {
|
||||
t.Errorf("buildOutFmts().Img = %v, want empty for native target", result.Img)
|
||||
}
|
||||
if result.Uf2 != "" {
|
||||
t.Errorf("buildOutFmts().Uf2 = %v, want empty for native target", result.Uf2)
|
||||
}
|
||||
if result.Zip != "" {
|
||||
t.Errorf("buildOutFmts().Zip = %v, want empty for native target", result.Zip)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
152
internal/build/run.go
Normal file
152
internal/build/run.go
Normal file
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* Copyright (c) 2024 The GoPlus Authors (goplus.org). All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package build
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/goplus/llgo/internal/mockable"
|
||||
"github.com/goplus/llgo/internal/shellparse"
|
||||
)
|
||||
|
||||
func runNative(ctx *context, app, pkgDir, pkgName string, conf *Config, mode Mode) error {
|
||||
switch mode {
|
||||
case ModeRun:
|
||||
args := make([]string, 0, len(conf.RunArgs)+1)
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
if s := cmd.ProcessState; s != nil {
|
||||
mockable.Exit(s.ExitCode())
|
||||
}
|
||||
case ModeTest:
|
||||
cmd := exec.Command(app, conf.RunArgs...)
|
||||
cmd.Dir = pkgDir
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
fmt.Fprintf(os.Stderr, "%s: exit code %d\n", app, exitErr.ExitCode())
|
||||
if !ctx.testFail {
|
||||
ctx.testFail = true
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "failed to run test %s: %v\n", app, err)
|
||||
if !ctx.testFail {
|
||||
ctx.testFail = true
|
||||
}
|
||||
}
|
||||
}
|
||||
case ModeCmpTest:
|
||||
cmpTest(pkgDir, pkgName, app, conf.GenExpect, conf.RunArgs)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runInEmulator(emulator string, envMap map[string]string, pkgDir, pkgName string, conf *Config, mode Mode, verbose bool) error {
|
||||
if emulator == "" {
|
||||
return fmt.Errorf("target %s does not have emulator configured", conf.Target)
|
||||
}
|
||||
if verbose {
|
||||
fmt.Fprintf(os.Stderr, "Using emulator: %s\n", emulator)
|
||||
}
|
||||
|
||||
switch mode {
|
||||
case ModeRun:
|
||||
return runEmuCmd(envMap, emulator, conf.RunArgs, verbose)
|
||||
case ModeTest:
|
||||
return runEmuCmd(envMap, emulator, conf.RunArgs, verbose)
|
||||
case ModeCmpTest:
|
||||
cmpTest(pkgDir, pkgName, envMap["out"], conf.GenExpect, conf.RunArgs)
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// runEmuCmd runs the application in emulator by formatting the emulator command template
|
||||
func runEmuCmd(envMap map[string]string, emulatorTemplate string, runArgs []string, verbose bool) error {
|
||||
// Expand the emulator command template
|
||||
emulatorCmd := emulatorTemplate
|
||||
for placeholder, path := range envMap {
|
||||
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 safely handling quoted strings
|
||||
cmdParts, err := shellparse.Parse(emulatorCmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse emulator command: %w", err)
|
||||
}
|
||||
if len(cmdParts) == 0 {
|
||||
return 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 {
|
||||
return err
|
||||
}
|
||||
if s := cmd.ProcessState; s != nil {
|
||||
mockable.Exit(s.ExitCode())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
"github.com/goplus/llgo/internal/crosscompile/compile"
|
||||
"github.com/goplus/llgo/internal/env"
|
||||
"github.com/goplus/llgo/internal/flash"
|
||||
"github.com/goplus/llgo/internal/targets"
|
||||
"github.com/goplus/llgo/internal/xtool/llvm"
|
||||
)
|
||||
@@ -34,6 +35,10 @@ 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 {}")
|
||||
|
||||
// Flashing/Debugging configuration
|
||||
Device flash.Device // Device configuration for flashing/debugging
|
||||
}
|
||||
|
||||
// URLs and configuration that can be overridden for testing
|
||||
@@ -54,65 +59,6 @@ func cacheDir() string {
|
||||
return filepath.Join(cacheRoot(), "crosscompile")
|
||||
}
|
||||
|
||||
// expandEnv expands template variables in a string
|
||||
// Supports variables like {port}, {hex}, {bin}, {root}, {tmpDir}, etc.
|
||||
// Special case: {} expands to the first available file variable (hex, bin, img, zip)
|
||||
func expandEnv(template string, envs map[string]string) string {
|
||||
return expandEnvWithDefault(template, envs)
|
||||
}
|
||||
|
||||
// expandEnvWithDefault expands template variables with optional default for {}
|
||||
func expandEnvWithDefault(template string, envs map[string]string, defaultValue ...string) string {
|
||||
if template == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
result := template
|
||||
|
||||
// Handle special case of {} - use provided default or first available file variable
|
||||
if strings.Contains(result, "{}") {
|
||||
defaultVal := ""
|
||||
if len(defaultValue) > 0 && defaultValue[0] != "" {
|
||||
defaultVal = defaultValue[0]
|
||||
} else {
|
||||
// Priority order: hex, bin, img, zip
|
||||
for _, key := range []string{"hex", "bin", "img", "zip"} {
|
||||
if value, exists := envs[key]; exists && value != "" {
|
||||
defaultVal = value
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
result = strings.ReplaceAll(result, "{}", defaultVal)
|
||||
}
|
||||
|
||||
// Replace named variables
|
||||
for key, value := range envs {
|
||||
if key != "" { // Skip empty key used for {} default
|
||||
result = strings.ReplaceAll(result, "{"+key+"}", value)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// expandEnvSlice expands template variables in a slice of strings
|
||||
func expandEnvSlice(templates []string, envs map[string]string) []string {
|
||||
return expandEnvSliceWithDefault(templates, envs)
|
||||
}
|
||||
|
||||
// expandEnvSliceWithDefault expands template variables in a slice with optional default for {}
|
||||
func expandEnvSliceWithDefault(templates []string, envs map[string]string, defaultValue ...string) []string {
|
||||
if len(templates) == 0 {
|
||||
return templates
|
||||
}
|
||||
|
||||
result := make([]string, len(templates))
|
||||
for i, template := range templates {
|
||||
result[i] = expandEnvWithDefault(template, envs, defaultValue...)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// buildEnvMap creates a map of template variables for the current context
|
||||
func buildEnvMap(llgoRoot string) map[string]string {
|
||||
envs := make(map[string]string)
|
||||
@@ -474,8 +420,8 @@ func use(goos, goarch string, wasiThreads, forceEspClang bool) (export Export, e
|
||||
return
|
||||
}
|
||||
|
||||
// useTarget loads configuration from a target name (e.g., "rp2040", "wasi")
|
||||
func useTarget(targetName string) (export Export, err error) {
|
||||
// UseTarget loads configuration from a target name (e.g., "rp2040", "wasi")
|
||||
func UseTarget(targetName string) (export Export, err error) {
|
||||
resolver := targets.NewDefaultResolver()
|
||||
|
||||
config, err := resolver.Resolve(targetName)
|
||||
@@ -510,6 +456,27 @@ func useTarget(targetName string) (export Export, err error) {
|
||||
export.ExtraFiles = config.ExtraFiles
|
||||
export.BinaryFormat = config.BinaryFormat
|
||||
export.FormatDetail = config.FormatDetail()
|
||||
export.Emulator = config.Emulator
|
||||
|
||||
// Set flashing/debugging configuration
|
||||
export.Device = flash.Device{
|
||||
Serial: config.Serial,
|
||||
SerialPort: config.SerialPort,
|
||||
Flash: flash.Flash{
|
||||
Method: config.FlashMethod,
|
||||
Command: config.FlashCommand,
|
||||
Flash1200BpsReset: config.Flash1200BpsReset == "true",
|
||||
},
|
||||
MSD: flash.MSD{
|
||||
VolumeName: config.MSDVolumeName,
|
||||
FirmwareName: config.MSDFirmwareName,
|
||||
},
|
||||
OpenOCD: flash.OpenOCD{
|
||||
Interface: config.OpenOCDInterface,
|
||||
Transport: config.OpenOCDTransport,
|
||||
Target: config.OpenOCDTarget,
|
||||
},
|
||||
}
|
||||
|
||||
// Build environment map for template variable expansion
|
||||
envs := buildEnvMap(env.LLGoROOT())
|
||||
@@ -524,7 +491,7 @@ func useTarget(targetName string) (export Export, err error) {
|
||||
ccflags = append(ccflags, "--target="+config.LLVMTarget)
|
||||
}
|
||||
// Expand template variables in cflags
|
||||
expandedCFlags := expandEnvSlice(config.CFlags, envs)
|
||||
expandedCFlags := env.ExpandEnvSlice(config.CFlags, envs)
|
||||
cflags = append(cflags, expandedCFlags...)
|
||||
|
||||
// The following parameters are inspired by tinygo/builder/library.go
|
||||
@@ -670,7 +637,7 @@ func useTarget(targetName string) (export Export, err error) {
|
||||
// Combine with config flags and expand template variables
|
||||
export.CFLAGS = cflags
|
||||
export.CCFLAGS = ccflags
|
||||
expandedLDFlags := expandEnvSlice(config.LDFlags, envs)
|
||||
expandedLDFlags := env.ExpandEnvSlice(config.LDFlags, envs)
|
||||
export.LDFLAGS = append(ldflags, expandedLDFlags...)
|
||||
|
||||
return export, nil
|
||||
@@ -680,7 +647,7 @@ func useTarget(targetName string) (export Export, err error) {
|
||||
// If targetName is provided, it takes precedence over goos/goarch
|
||||
func Use(goos, goarch, targetName string, wasiThreads, forceEspClang bool) (export Export, err error) {
|
||||
if targetName != "" && !strings.HasPrefix(targetName, "wasm") && !strings.HasPrefix(targetName, "wasi") {
|
||||
return useTarget(targetName)
|
||||
return UseTarget(targetName)
|
||||
}
|
||||
return use(goos, goarch, wasiThreads, forceEspClang)
|
||||
}
|
||||
|
||||
@@ -214,7 +214,7 @@ func TestUseTarget(t *testing.T) {
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
export, err := useTarget(tc.targetName)
|
||||
export, err := UseTarget(tc.targetName)
|
||||
|
||||
if tc.expectError {
|
||||
if err == nil {
|
||||
@@ -302,126 +302,3 @@ func TestUseWithTarget(t *testing.T) {
|
||||
t.Error("Expected LDFLAGS to be set for native build")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpandEnv(t *testing.T) {
|
||||
envs := map[string]string{
|
||||
"port": "/dev/ttyUSB0",
|
||||
"hex": "firmware.hex",
|
||||
"bin": "firmware.bin",
|
||||
"root": "/usr/local/llgo",
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
template string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
"avrdude -c arduino -p atmega328p -P {port} -U flash:w:{hex}:i",
|
||||
"avrdude -c arduino -p atmega328p -P /dev/ttyUSB0 -U flash:w:firmware.hex:i",
|
||||
},
|
||||
{
|
||||
"simavr -m atmega328p -f 16000000 {}",
|
||||
"simavr -m atmega328p -f 16000000 firmware.hex", // {} expands to hex (first priority)
|
||||
},
|
||||
{
|
||||
"-I{root}/lib/CMSIS/CMSIS/Include",
|
||||
"-I/usr/local/llgo/lib/CMSIS/CMSIS/Include",
|
||||
},
|
||||
{
|
||||
"no variables here",
|
||||
"no variables here",
|
||||
},
|
||||
{
|
||||
"",
|
||||
"",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
result := expandEnv(test.template, envs)
|
||||
if result != test.expected {
|
||||
t.Errorf("expandEnv(%q) = %q, want %q", test.template, result, test.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpandEnvSlice(t *testing.T) {
|
||||
envs := map[string]string{
|
||||
"root": "/usr/local/llgo",
|
||||
"port": "/dev/ttyUSB0",
|
||||
}
|
||||
|
||||
input := []string{
|
||||
"-I{root}/include",
|
||||
"-DPORT={port}",
|
||||
"static-flag",
|
||||
}
|
||||
|
||||
expected := []string{
|
||||
"-I/usr/local/llgo/include",
|
||||
"-DPORT=/dev/ttyUSB0",
|
||||
"static-flag",
|
||||
}
|
||||
|
||||
result := expandEnvSlice(input, envs)
|
||||
|
||||
if len(result) != len(expected) {
|
||||
t.Fatalf("expandEnvSlice length mismatch: got %d, want %d", len(result), len(expected))
|
||||
}
|
||||
|
||||
for i, exp := range expected {
|
||||
if result[i] != exp {
|
||||
t.Errorf("expandEnvSlice[%d] = %q, want %q", i, result[i], exp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpandEnvWithDefault(t *testing.T) {
|
||||
envs := map[string]string{
|
||||
"port": "/dev/ttyUSB0",
|
||||
"hex": "firmware.hex",
|
||||
"bin": "firmware.bin",
|
||||
"img": "image.img",
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
template string
|
||||
defaultValue string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
"simavr {}",
|
||||
"", // No default - should use hex (priority)
|
||||
"simavr firmware.hex",
|
||||
},
|
||||
{
|
||||
"simavr {}",
|
||||
"custom.elf", // Explicit default
|
||||
"simavr custom.elf",
|
||||
},
|
||||
{
|
||||
"qemu -kernel {}",
|
||||
"vmlinux", // Custom kernel
|
||||
"qemu -kernel vmlinux",
|
||||
},
|
||||
{
|
||||
"no braces here",
|
||||
"ignored",
|
||||
"no braces here",
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
var result string
|
||||
if test.defaultValue == "" {
|
||||
result = expandEnvWithDefault(test.template, envs)
|
||||
} else {
|
||||
result = expandEnvWithDefault(test.template, envs, test.defaultValue)
|
||||
}
|
||||
|
||||
if result != test.expected {
|
||||
t.Errorf("Test %d: expandEnvWithDefault(%q, envs, %q) = %q, want %q",
|
||||
i, test.template, test.defaultValue, result, test.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
63
internal/env/utils.go
vendored
Normal file
63
internal/env/utils.go
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright (c) 2024 The GoPlus Authors (goplus.org). All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package env
|
||||
|
||||
import "strings"
|
||||
|
||||
// ExpandEnvWithDefault expands template variables with optional default for {}
|
||||
func ExpandEnvWithDefault(template string, envs map[string]string, defaultValue ...string) string {
|
||||
if template == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
result := template
|
||||
|
||||
// Handle special case of {} - use provided default or first available file variable
|
||||
if strings.Contains(result, "{}") {
|
||||
defaultVal := ""
|
||||
if len(defaultValue) > 0 && defaultValue[0] != "" {
|
||||
defaultVal = defaultValue[0]
|
||||
}
|
||||
result = strings.ReplaceAll(result, "{}", defaultVal)
|
||||
}
|
||||
|
||||
// Replace named variables
|
||||
for key, value := range envs {
|
||||
if key != "" { // Skip empty key used for {} default
|
||||
result = strings.ReplaceAll(result, "{"+key+"}", value)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ExpandEnvSlice expands template variables in a slice of strings
|
||||
func ExpandEnvSlice(templates []string, envs map[string]string) []string {
|
||||
return ExpandEnvSliceWithDefault(templates, envs)
|
||||
}
|
||||
|
||||
// ExpandEnvSliceWithDefault expands template variables in a slice with optional default for {}
|
||||
func ExpandEnvSliceWithDefault(templates []string, envs map[string]string, defaultValue ...string) []string {
|
||||
if len(templates) == 0 {
|
||||
return templates
|
||||
}
|
||||
|
||||
result := make([]string, len(templates))
|
||||
for i, template := range templates {
|
||||
result[i] = ExpandEnvWithDefault(template, envs, defaultValue...)
|
||||
}
|
||||
return result
|
||||
}
|
||||
84
internal/env/utils_test.go
vendored
Normal file
84
internal/env/utils_test.go
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
package env
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestExpandEnvSlice(t *testing.T) {
|
||||
envs := map[string]string{
|
||||
"root": "/usr/local/llgo",
|
||||
"port": "/dev/ttyUSB0",
|
||||
}
|
||||
|
||||
input := []string{
|
||||
"-I{root}/include",
|
||||
"-DPORT={port}",
|
||||
"static-flag",
|
||||
}
|
||||
|
||||
expected := []string{
|
||||
"-I/usr/local/llgo/include",
|
||||
"-DPORT=/dev/ttyUSB0",
|
||||
"static-flag",
|
||||
}
|
||||
|
||||
result := ExpandEnvSlice(input, envs)
|
||||
|
||||
if len(result) != len(expected) {
|
||||
t.Fatalf("expandEnvSlice length mismatch: got %d, want %d", len(result), len(expected))
|
||||
}
|
||||
|
||||
for i, exp := range expected {
|
||||
if result[i] != exp {
|
||||
t.Errorf("expandEnvSlice[%d] = %q, want %q", i, result[i], exp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpandEnvWithDefault(t *testing.T) {
|
||||
envs := map[string]string{
|
||||
"port": "/dev/ttyUSB0",
|
||||
"hex": "firmware.hex",
|
||||
"bin": "firmware.bin",
|
||||
"img": "image.img",
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
template string
|
||||
defaultValue string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
"simavr {}",
|
||||
"firmware.hex",
|
||||
"simavr firmware.hex",
|
||||
},
|
||||
{
|
||||
"simavr {}",
|
||||
"custom.elf", // Explicit default
|
||||
"simavr custom.elf",
|
||||
},
|
||||
{
|
||||
"qemu -kernel {}",
|
||||
"vmlinux", // Custom kernel
|
||||
"qemu -kernel vmlinux",
|
||||
},
|
||||
{
|
||||
"no braces here",
|
||||
"ignored",
|
||||
"no braces here",
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
var result string
|
||||
if test.defaultValue == "" {
|
||||
result = ExpandEnvWithDefault(test.template, envs)
|
||||
} else {
|
||||
result = ExpandEnvWithDefault(test.template, envs, test.defaultValue)
|
||||
}
|
||||
|
||||
if result != test.expected {
|
||||
t.Errorf("Test %d: expandEnvWithDefault(%q, envs, %q) = %q, want %q",
|
||||
i, test.template, test.defaultValue, result, test.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
16
internal/firmware/env.go
Normal file
16
internal/firmware/env.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package firmware
|
||||
|
||||
import "strings"
|
||||
|
||||
// BinaryFormatToEnvName returns the environment variable name based on the binary format
|
||||
// Returns the format name for template expansion (e.g., "bin", "uf2", "zip")
|
||||
func BinaryFormatToEnvName(binaryFormat string) string {
|
||||
if strings.HasPrefix(binaryFormat, "esp") {
|
||||
return "bin"
|
||||
} else if strings.HasPrefix(binaryFormat, "uf2") {
|
||||
return "uf2"
|
||||
} else if strings.HasPrefix(binaryFormat, "nrf-dfu") {
|
||||
return "zip"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package firmware
|
||||
|
||||
import "strings"
|
||||
|
||||
// BinaryExt returns the binary file extension based on the binary format
|
||||
// Returns ".bin" for ESP-based formats, "" for others
|
||||
func BinaryExt(binaryFormat string) string {
|
||||
if strings.HasPrefix(binaryFormat, "esp") {
|
||||
return ".bin"
|
||||
} else if strings.HasPrefix(binaryFormat, "uf2") {
|
||||
return ".uf2"
|
||||
} else if strings.HasPrefix(binaryFormat, "nrf-dfu") {
|
||||
return ".zip"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
//go:build !llgo
|
||||
// +build !llgo
|
||||
|
||||
package firmware
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestBinaryExt(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
binaryFormat string
|
||||
expected string
|
||||
}{
|
||||
{"ESP32", "esp32", ".bin"},
|
||||
{"ESP8266", "esp8266", ".bin"},
|
||||
{"ESP32C3", "esp32c3", ".bin"},
|
||||
{"UF2", "uf2", ".uf2"},
|
||||
{"ELF", "elf", ""},
|
||||
{"Empty", "", ""},
|
||||
{"NRF-DFU", "nrf-dfu", ".zip"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := BinaryExt(tt.binaryFormat)
|
||||
if result != tt.expected {
|
||||
t.Errorf("BinaryExt() = %q, want %q", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,13 @@ package firmware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// MakeFirmwareImage creates a firmware image from the given input file.
|
||||
func MakeFirmwareImage(infile, outfile, format, fmtDetail string) error {
|
||||
// makeFirmwareImage creates a firmware image from the given input file.
|
||||
func makeFirmwareImage(infile, outfile, format, fmtDetail string) error {
|
||||
fmt.Fprintf(os.Stderr, "Generating firmware image: %s -> %s (format: %s, detail: %s)\n", infile, outfile, format, fmtDetail)
|
||||
if strings.HasPrefix(format, "esp") {
|
||||
return makeESPFirmareImage(infile, outfile, format)
|
||||
} else if format == "uf2" {
|
||||
@@ -17,3 +19,65 @@ func MakeFirmwareImage(infile, outfile, format, fmtDetail string) error {
|
||||
}
|
||||
return fmt.Errorf("unsupported firmware format: %s", format)
|
||||
}
|
||||
|
||||
// ExtractFileFormatFromCommand extracts file format from command template
|
||||
// Returns the format if found (e.g. "bin", "hex", "zip", "img"), empty string if not found
|
||||
func ExtractFileFormatFromCommand(cmd string) string {
|
||||
formats := []string{"bin", "hex", "zip", "img", "uf2"}
|
||||
for _, format := range formats {
|
||||
if strings.Contains(cmd, "{"+format+"}") {
|
||||
return format
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ConvertFormats processes format conversions for embedded targets only
|
||||
func ConvertFormats(binFmt, fmtDetail string, envMap map[string]string) error {
|
||||
var err error
|
||||
// Convert to bin format first (needed for img)
|
||||
if envMap["bin"] != "" {
|
||||
if strings.HasPrefix(binFmt, "esp") {
|
||||
err = makeFirmwareImage(envMap["out"], envMap["bin"], binFmt, fmtDetail)
|
||||
} else {
|
||||
err = objcopy(envMap["out"], envMap["bin"], "bin")
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert to bin format: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to hex format
|
||||
if envMap["hex"] != "" {
|
||||
err := objcopy(envMap["out"], envMap["hex"], "hex")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert to hex format: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to img format
|
||||
if envMap["img"] != "" {
|
||||
err = makeFirmwareImage(envMap["out"], envMap["img"], binFmt+"-img", fmtDetail)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert to img format: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to uf2 format
|
||||
if envMap["uf2"] != "" {
|
||||
err := makeFirmwareImage(envMap["out"], envMap["uf2"], binFmt, fmtDetail)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert to uf2 format: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to zip format
|
||||
if envMap["zip"] != "" {
|
||||
err := makeFirmwareImage(envMap["out"], envMap["zip"], binFmt, fmtDetail)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert to zip format: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -4,9 +4,12 @@ package firmware
|
||||
|
||||
import (
|
||||
"debug/elf"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sort"
|
||||
|
||||
"github.com/marcinbor85/gohex"
|
||||
)
|
||||
|
||||
// maxPadBytes is the maximum allowed bytes to be padded in a rom extraction
|
||||
@@ -105,6 +108,7 @@ func extractROM(path string) (uint64, []byte, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// From tinygo/builder/builder/objcopy.go objcopy
|
||||
// objcopy converts an ELF file to a different (simpler) output file format:
|
||||
// .bin or .hex. It extracts only the .text section.
|
||||
func objcopy(infile, outfile, binaryFormat string) error {
|
||||
@@ -115,14 +119,24 @@ func objcopy(infile, outfile, binaryFormat string) error {
|
||||
defer f.Close()
|
||||
|
||||
// Read the .text segment.
|
||||
_, data, err := extractROM(infile)
|
||||
addr, data, err := extractROM(infile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Write to the file, in the correct format.
|
||||
switch binaryFormat {
|
||||
case "hex":
|
||||
fmt.Fprintf(os.Stderr, "Converting firmware format: %s -> %s (intel hex format: %s)\n", infile, outfile, binaryFormat)
|
||||
// Intel hex file, includes the firmware start address.
|
||||
mem := gohex.NewMemory()
|
||||
err := mem.AddBinary(uint32(addr), data)
|
||||
if err != nil {
|
||||
return objcopyError{"failed to create .hex file", err}
|
||||
}
|
||||
return mem.DumpIntelHex(f, 16)
|
||||
case "bin":
|
||||
fmt.Fprintf(os.Stderr, "Converting firmware format: %s -> %s (format: %s)\n", infile, outfile, binaryFormat)
|
||||
// The start address is not stored in raw firmware files (therefore you
|
||||
// should use .hex files in most cases).
|
||||
_, err := f.Write(data)
|
||||
|
||||
440
internal/flash/flash.go
Normal file
440
internal/flash/flash.go
Normal file
@@ -0,0 +1,440 @@
|
||||
package flash
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/goplus/llgo/internal/env"
|
||||
"github.com/goplus/llgo/internal/shellparse"
|
||||
"go.bug.st/serial"
|
||||
"go.bug.st/serial/enumerator"
|
||||
)
|
||||
|
||||
// Device contains all flashing/debugging configuration for a device
|
||||
type Device struct {
|
||||
Serial string // Serial communication settings
|
||||
SerialPort []string // Available serial ports
|
||||
Flash Flash // Flash configuration for device programming
|
||||
MSD MSD // Mass Storage Device configuration
|
||||
OpenOCD OpenOCD // OpenOCD configuration for debugging/flashing
|
||||
}
|
||||
|
||||
// Flash contains configuration for device flashing
|
||||
type Flash struct {
|
||||
Method string // Flash method: "command", "openocd", "msd", "bmp"
|
||||
Command string // Flash command template
|
||||
Flash1200BpsReset bool // Whether to use 1200bps reset
|
||||
}
|
||||
|
||||
// MSD contains configuration for Mass Storage Device flashing
|
||||
type MSD struct {
|
||||
VolumeName []string // Names of the volumes
|
||||
FirmwareName string // Firmware file name pattern
|
||||
}
|
||||
|
||||
// OpenOCD contains configuration for OpenOCD debugging/flashing
|
||||
type OpenOCD struct {
|
||||
Interface string // Interface configuration (e.g., "stlink")
|
||||
Transport string // Transport protocol (e.g., "swd", "jtag")
|
||||
Target string // Target configuration (e.g., "stm32f4x")
|
||||
}
|
||||
|
||||
// From tinygo/main.go getDefaultPort
|
||||
// GetPort returns the default serial port depending on the operating system and USB interfaces.
|
||||
func GetPort(portFlag string, usbInterfaces []string) (string, error) {
|
||||
portCandidates := strings.FieldsFunc(portFlag, func(c rune) bool { return c == ',' })
|
||||
if len(portCandidates) == 1 {
|
||||
return portCandidates[0], nil
|
||||
}
|
||||
|
||||
var ports []string
|
||||
var err error
|
||||
switch runtime.GOOS {
|
||||
case "freebsd":
|
||||
ports, err = filepath.Glob("/dev/cuaU*")
|
||||
case "darwin", "linux", "windows":
|
||||
var portsList []*enumerator.PortDetails
|
||||
portsList, err = enumerator.GetDetailedPortsList()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var preferredPortIDs [][2]uint16
|
||||
for _, s := range usbInterfaces {
|
||||
parts := strings.Split(s, ":")
|
||||
if len(parts) != 2 {
|
||||
return "", fmt.Errorf("could not parse USB VID/PID pair %q", s)
|
||||
}
|
||||
vid, err := strconv.ParseUint(parts[0], 16, 16)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not parse USB vendor ID %q: %w", parts[0], err)
|
||||
}
|
||||
pid, err := strconv.ParseUint(parts[1], 16, 16)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not parse USB product ID %q: %w", parts[1], err)
|
||||
}
|
||||
preferredPortIDs = append(preferredPortIDs, [2]uint16{uint16(vid), uint16(pid)})
|
||||
}
|
||||
|
||||
var primaryPorts []string // ports picked from preferred USB VID/PID
|
||||
var secondaryPorts []string // other ports (as a fallback)
|
||||
for _, p := range portsList {
|
||||
if !p.IsUSB {
|
||||
continue
|
||||
}
|
||||
if p.VID != "" && p.PID != "" {
|
||||
foundPort := false
|
||||
vid, vidErr := strconv.ParseUint(p.VID, 16, 16)
|
||||
pid, pidErr := strconv.ParseUint(p.PID, 16, 16)
|
||||
if vidErr == nil && pidErr == nil {
|
||||
for _, id := range preferredPortIDs {
|
||||
if uint16(vid) == id[0] && uint16(pid) == id[1] {
|
||||
primaryPorts = append(primaryPorts, p.Name)
|
||||
foundPort = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if foundPort {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
secondaryPorts = append(secondaryPorts, p.Name)
|
||||
}
|
||||
if len(primaryPorts) == 1 {
|
||||
return primaryPorts[0], nil
|
||||
} else if len(primaryPorts) > 1 {
|
||||
ports = primaryPorts
|
||||
} else {
|
||||
ports = secondaryPorts
|
||||
}
|
||||
default:
|
||||
return "", errors.New("unable to search for a default USB device to be flashed on this OS")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
} else if ports == nil {
|
||||
return "", errors.New("unable to locate a serial port")
|
||||
} else if len(ports) == 0 {
|
||||
return "", errors.New("no serial ports available")
|
||||
}
|
||||
|
||||
if len(portCandidates) == 0 {
|
||||
if len(usbInterfaces) > 0 {
|
||||
return "", errors.New("unable to search for a default USB device - use -port flag, available ports are " + strings.Join(ports, ", "))
|
||||
} else if len(ports) == 1 {
|
||||
return ports[0], nil
|
||||
} else {
|
||||
return "", errors.New("multiple serial ports available - use -port flag, available ports are " + strings.Join(ports, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
for _, ps := range portCandidates {
|
||||
for _, p := range ports {
|
||||
if p == ps {
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", errors.New("port you specified '" + strings.Join(portCandidates, ",") + "' does not exist, available ports are " + strings.Join(ports, ", "))
|
||||
}
|
||||
|
||||
// From tinygo/main.go touchSerialPortAt1200bps
|
||||
// touchSerialPortAt1200bps triggers Arduino-compatible devices to enter bootloader mode.
|
||||
// This function implements the Arduino auto-reset mechanism used before flashing firmware.
|
||||
//
|
||||
// Working principle:
|
||||
// 1. Opens serial port at 1200 baud rate (special reset baudrate for Arduino)
|
||||
// 2. Sets DTR (Data Terminal Ready) signal to false
|
||||
// 3. This triggers the device to reset and enter bootloader mode for firmware upload
|
||||
//
|
||||
// Usage scenarios:
|
||||
// - Required for Arduino Uno, Leonardo, Micro and other compatible devices
|
||||
// - Executed when target config has "flash-1200-bps-reset": "true"
|
||||
// - Ensures device is in correct state to receive new firmware
|
||||
//
|
||||
// Retry mechanism:
|
||||
// - Retries up to 3 times due to potential temporary serial port access issues
|
||||
// - Windows special handling: InvalidSerialPort error during bootloader transition is normal
|
||||
func touchSerialPortAt1200bps(port string) (err error) {
|
||||
retryCount := 3
|
||||
for i := 0; i < retryCount; i++ {
|
||||
// Open port at 1200bps to trigger Arduino reset
|
||||
p, e := serial.Open(port, &serial.Mode{BaudRate: 1200})
|
||||
if e != nil {
|
||||
if runtime.GOOS == `windows` {
|
||||
se, ok := e.(*serial.PortError)
|
||||
if ok && se.Code() == serial.InvalidSerialPort {
|
||||
// InvalidSerialPort error occurs when transitioning to boot
|
||||
return nil
|
||||
}
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
err = e
|
||||
continue
|
||||
}
|
||||
defer p.Close()
|
||||
|
||||
// Set DTR to false to trigger reset
|
||||
p.SetDTR(false)
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("opening port: %s", err)
|
||||
}
|
||||
|
||||
// FlashDevice flashes firmware to a device based on the device configuration
|
||||
func FlashDevice(device Device, envMap map[string]string, port string, verbose bool) error {
|
||||
method := device.Flash.Method
|
||||
if method == "" {
|
||||
method = "command"
|
||||
}
|
||||
|
||||
// Resolve port for methods that need it (all except openocd)
|
||||
if method != "openocd" {
|
||||
var err error
|
||||
port, err = GetPort(port, device.SerialPort)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find port: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if verbose {
|
||||
fmt.Fprintf(os.Stderr, "Flashing using method: %s\n", method)
|
||||
fmt.Fprintf(os.Stderr, "Using port: %s\n", port)
|
||||
}
|
||||
|
||||
// Execute 1200bps reset before flashing if needed (except for openocd)
|
||||
if method != "openocd" && device.Flash.Flash1200BpsReset {
|
||||
if verbose {
|
||||
fmt.Fprintf(os.Stderr, "Triggering 1200bps reset on port: %s\n", port)
|
||||
}
|
||||
if err := touchSerialPortAt1200bps(port); err != nil {
|
||||
return fmt.Errorf("failed to trigger 1200bps reset: %w", err)
|
||||
}
|
||||
// Wait a bit for device to enter bootloader mode
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
|
||||
switch method {
|
||||
case "command":
|
||||
return flashCommand(device.Flash, envMap, port, verbose)
|
||||
case "openocd":
|
||||
return flashOpenOCD(device.OpenOCD, envMap, verbose)
|
||||
case "msd":
|
||||
return flashMSD(device.MSD, envMap, verbose)
|
||||
case "bmp":
|
||||
return flashBMP(envMap, port, verbose)
|
||||
default:
|
||||
return fmt.Errorf("unsupported flash method: %s", method)
|
||||
}
|
||||
}
|
||||
|
||||
// flashCommand handles command-based flashing
|
||||
func flashCommand(flash Flash, envMap map[string]string, port string, verbose bool) error {
|
||||
if flash.Command == "" {
|
||||
return fmt.Errorf("flash command not specified")
|
||||
}
|
||||
|
||||
// Build environment map for template variable expansion
|
||||
envs := buildFlashEnvMap(envMap, port)
|
||||
|
||||
// Expand template variables in command
|
||||
expandedCommand := env.ExpandEnvWithDefault(flash.Command, envs)
|
||||
|
||||
if verbose {
|
||||
fmt.Fprintf(os.Stderr, "Flash command: %s\n", expandedCommand)
|
||||
}
|
||||
|
||||
// Split command into parts for exec - safely handling quoted arguments
|
||||
parts, err := shellparse.Parse(expandedCommand)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse flash command: %w", err)
|
||||
}
|
||||
if len(parts) == 0 {
|
||||
return fmt.Errorf("empty flash command after expansion")
|
||||
}
|
||||
|
||||
// Execute flash command
|
||||
cmd := exec.Command(parts[0], parts[1:]...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// flashOpenOCD handles OpenOCD-based flashing
|
||||
func flashOpenOCD(openocd OpenOCD, envMap map[string]string, verbose bool) error {
|
||||
if openocd.Interface == "" {
|
||||
return fmt.Errorf("OpenOCD interface not specified")
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"-f", "interface/" + openocd.Interface + ".cfg",
|
||||
}
|
||||
|
||||
if openocd.Transport != "" {
|
||||
args = append(args, "-c", "transport select "+openocd.Transport)
|
||||
}
|
||||
|
||||
if openocd.Target != "" {
|
||||
args = append(args, "-f", "target/"+openocd.Target+".cfg")
|
||||
}
|
||||
|
||||
// Add programming commands
|
||||
args = append(args,
|
||||
"-c", "init",
|
||||
"-c", "reset init",
|
||||
"-c", fmt.Sprintf("flash write_image erase %s", envMap["elf"]),
|
||||
"-c", "reset",
|
||||
"-c", "shutdown",
|
||||
)
|
||||
|
||||
if verbose {
|
||||
fmt.Fprintf(os.Stderr, "OpenOCD command: openocd %s\n", strings.Join(args, " "))
|
||||
}
|
||||
|
||||
cmd := exec.Command("openocd", args...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// flashMSD handles Mass Storage Device flashing
|
||||
func flashMSD(msd MSD, envMap map[string]string, verbose bool) error {
|
||||
if len(msd.VolumeName) == 0 {
|
||||
return fmt.Errorf("MSD volume names not specified")
|
||||
}
|
||||
|
||||
if msd.FirmwareName == "" {
|
||||
return fmt.Errorf("MSD firmware name not specified")
|
||||
}
|
||||
|
||||
// Find the MSD volume
|
||||
var mountPoint string
|
||||
for _, volumeName := range msd.VolumeName {
|
||||
// Try platform-specific mount points
|
||||
var candidates []string
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
candidates = []string{
|
||||
filepath.Join("/Volumes", volumeName),
|
||||
}
|
||||
case "linux":
|
||||
candidates = []string{
|
||||
filepath.Join("/media", os.Getenv("USER"), volumeName),
|
||||
filepath.Join("/mnt", volumeName),
|
||||
}
|
||||
case "windows":
|
||||
candidates = []string{
|
||||
volumeName + ":",
|
||||
}
|
||||
default:
|
||||
candidates = []string{
|
||||
filepath.Join("/Volumes", volumeName), // macOS
|
||||
filepath.Join("/media", os.Getenv("USER"), volumeName), // Linux
|
||||
filepath.Join("/mnt", volumeName), // Linux alternative
|
||||
volumeName + ":", // Windows
|
||||
}
|
||||
}
|
||||
|
||||
for _, candidate := range candidates {
|
||||
if _, err := os.Stat(candidate); err == nil {
|
||||
mountPoint = candidate
|
||||
break
|
||||
}
|
||||
}
|
||||
if mountPoint != "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if mountPoint == "" {
|
||||
return fmt.Errorf("MSD volume not found. Expected volumes: %v", msd.VolumeName)
|
||||
}
|
||||
|
||||
// Copy firmware to MSD
|
||||
destPath := filepath.Join(mountPoint, msd.FirmwareName)
|
||||
|
||||
if verbose {
|
||||
fmt.Fprintf(os.Stderr, "Copying %s to %s\n", envMap["uf2"], destPath)
|
||||
}
|
||||
|
||||
return copyFile(envMap["uf2"], destPath)
|
||||
}
|
||||
|
||||
// flashBMP handles Black Magic Probe flashing
|
||||
func flashBMP(envMap map[string]string, port string, verbose bool) error {
|
||||
// BMP typically uses GDB for flashing
|
||||
args := []string{
|
||||
"-ex", "target extended-remote " + port,
|
||||
"-ex", "monitor swdp_scan",
|
||||
"-ex", "attach 1",
|
||||
"-ex", "load",
|
||||
"-ex", "compare-sections",
|
||||
"-ex", "kill",
|
||||
"-ex", "quit",
|
||||
envMap["elf"],
|
||||
}
|
||||
|
||||
if verbose {
|
||||
fmt.Fprintf(os.Stderr, "BMP command: arm-none-eabi-gdb %s\n", strings.Join(args, " "))
|
||||
}
|
||||
|
||||
cmd := exec.Command("arm-none-eabi-gdb", args...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// buildFlashEnvMap creates environment map for template expansion
|
||||
func buildFlashEnvMap(envMap map[string]string, port string) map[string]string {
|
||||
envs := make(map[string]string)
|
||||
|
||||
// Basic paths
|
||||
envs["root"] = env.LLGoROOT()
|
||||
envs["tmpDir"] = os.TempDir()
|
||||
|
||||
// Port information
|
||||
if port != "" {
|
||||
envs["port"] = port
|
||||
}
|
||||
|
||||
// Copy all format paths from envMap
|
||||
for key, value := range envMap {
|
||||
if value != "" {
|
||||
envs[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return envs
|
||||
}
|
||||
|
||||
// copyFile copies a file from src to dst
|
||||
func copyFile(src, dst string) error {
|
||||
sourceFile, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer sourceFile.Close()
|
||||
|
||||
destFile, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer destFile.Close()
|
||||
|
||||
_, err = destFile.ReadFrom(sourceFile)
|
||||
return err
|
||||
}
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/goplus/llgo/internal/build"
|
||||
@@ -48,7 +47,6 @@ func genFrom(pkgPath string, abiMode build.AbiMode) (build.Package, error) {
|
||||
conf := &build.Config{
|
||||
Mode: build.ModeGen,
|
||||
AbiMode: abiMode,
|
||||
AppExt: build.DefaultAppExt(runtime.GOOS),
|
||||
}
|
||||
pkgs, err := build.Do([]string{pkgPath}, conf)
|
||||
if err != nil {
|
||||
|
||||
280
internal/monitor/monitor.go
Normal file
280
internal/monitor/monitor.go
Normal file
@@ -0,0 +1,280 @@
|
||||
package monitor
|
||||
|
||||
import (
|
||||
"debug/dwarf"
|
||||
"debug/elf"
|
||||
"debug/macho"
|
||||
"debug/pe"
|
||||
"errors"
|
||||
"fmt"
|
||||
"go/token"
|
||||
"io"
|
||||
"os"
|
||||
"os/signal"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/goplus/llgo/internal/flash"
|
||||
"github.com/mattn/go-tty"
|
||||
"go.bug.st/serial"
|
||||
)
|
||||
|
||||
// MonitorConfig contains configuration for the monitor
|
||||
type MonitorConfig struct {
|
||||
Port string // Serial port device
|
||||
Target string // Target name for crosscompile config
|
||||
BaudRate int // Baudrate of serial monitor
|
||||
Executable string // Optional path to executable for debug info
|
||||
WaitTime int // Wait time for port connection (ms)
|
||||
SerialPort []string // List of serial ports to use
|
||||
}
|
||||
|
||||
// Monitor starts serial monitoring with the given configuration
|
||||
func Monitor(config MonitorConfig, verbose bool) error {
|
||||
// Set defaults
|
||||
if config.BaudRate == 0 {
|
||||
config.BaudRate = 115200
|
||||
}
|
||||
if config.WaitTime == 0 {
|
||||
config.WaitTime = 300
|
||||
}
|
||||
|
||||
// Resolve port using flash.GetPort
|
||||
port, err := flash.GetPort(config.Port, config.SerialPort)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find port: %w", err)
|
||||
}
|
||||
config.Port = port
|
||||
|
||||
if verbose {
|
||||
fmt.Fprintf(os.Stderr, "Connecting to %s at %d baud\n", config.Port, config.BaudRate)
|
||||
}
|
||||
|
||||
// Open serial port with retry
|
||||
var serialConn serial.Port
|
||||
for i := 0; i <= config.WaitTime; i++ {
|
||||
serialConn, err = serial.Open(config.Port, &serial.Mode{BaudRate: config.BaudRate})
|
||||
if err != nil {
|
||||
if i < config.WaitTime {
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("failed to open port %s: %w", config.Port, err)
|
||||
}
|
||||
break
|
||||
}
|
||||
defer serialConn.Close()
|
||||
|
||||
// Open TTY for input
|
||||
tty, err := tty.Open()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open TTY: %w", err)
|
||||
}
|
||||
defer tty.Close()
|
||||
|
||||
// Setup signal handling
|
||||
sig := make(chan os.Signal, 1)
|
||||
signal.Notify(sig, os.Interrupt)
|
||||
defer signal.Stop(sig)
|
||||
|
||||
// Create output writer with optional debug info
|
||||
var writer *outputWriter
|
||||
if config.Executable != "" {
|
||||
writer = newOutputWriter(os.Stdout, config.Executable)
|
||||
} else {
|
||||
writer = newOutputWriter(os.Stdout, "")
|
||||
}
|
||||
|
||||
fmt.Printf("Connected to %s. Press Ctrl-C to exit.\n", config.Port)
|
||||
|
||||
errCh := make(chan error, 2)
|
||||
|
||||
// Goroutine for reading from serial port
|
||||
go func() {
|
||||
buf := make([]byte, 100*1024)
|
||||
for {
|
||||
n, err := serialConn.Read(buf)
|
||||
if err != nil {
|
||||
errCh <- fmt.Errorf("serial read error: %w", err)
|
||||
return
|
||||
}
|
||||
writer.Write(buf[:n])
|
||||
}
|
||||
}()
|
||||
|
||||
// Goroutine for reading from TTY and writing to serial port
|
||||
go func() {
|
||||
for {
|
||||
r, err := tty.ReadRune()
|
||||
if err != nil {
|
||||
errCh <- fmt.Errorf("TTY read error: %w", err)
|
||||
return
|
||||
}
|
||||
if r == 0 {
|
||||
continue
|
||||
}
|
||||
serialConn.Write([]byte(string(r)))
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for signal or error
|
||||
select {
|
||||
case <-sig:
|
||||
if verbose {
|
||||
fmt.Fprintf(os.Stderr, "\nDisconnected from %s\n", config.Port)
|
||||
}
|
||||
return nil
|
||||
case err := <-errCh:
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var addressMatch = regexp.MustCompile(`^panic: runtime error at 0x([0-9a-f]+): `)
|
||||
|
||||
// Extract the address from the "panic: runtime error at" message.
|
||||
func extractPanicAddress(line []byte) uint64 {
|
||||
matches := addressMatch.FindSubmatch(line)
|
||||
if matches != nil {
|
||||
address, err := strconv.ParseUint(string(matches[1]), 16, 64)
|
||||
if err == nil {
|
||||
return address
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Convert an address in the binary to a source address location.
|
||||
func addressToLine(executable string, address uint64) (token.Position, error) {
|
||||
data, err := readDWARF(executable)
|
||||
if err != nil {
|
||||
return token.Position{}, err
|
||||
}
|
||||
r := data.Reader()
|
||||
|
||||
for {
|
||||
e, err := r.Next()
|
||||
if err != nil {
|
||||
return token.Position{}, err
|
||||
}
|
||||
if e == nil {
|
||||
break
|
||||
}
|
||||
switch e.Tag {
|
||||
case dwarf.TagCompileUnit:
|
||||
r.SkipChildren()
|
||||
lr, err := data.LineReader(e)
|
||||
if err != nil {
|
||||
return token.Position{}, err
|
||||
}
|
||||
var lineEntry = dwarf.LineEntry{
|
||||
EndSequence: true,
|
||||
}
|
||||
for {
|
||||
// Read the next .debug_line entry.
|
||||
prevLineEntry := lineEntry
|
||||
err := lr.Next(&lineEntry)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
return token.Position{}, err
|
||||
}
|
||||
|
||||
if prevLineEntry.EndSequence && lineEntry.Address == 0 {
|
||||
// Tombstone value. This symbol has been removed, for
|
||||
// example by the --gc-sections linker flag. It is still
|
||||
// here in the debug information because the linker can't
|
||||
// just remove this reference.
|
||||
// Read until the next EndSequence so that this sequence is
|
||||
// skipped.
|
||||
// For more details, see (among others):
|
||||
// https://reviews.llvm.org/D84825
|
||||
for {
|
||||
err := lr.Next(&lineEntry)
|
||||
if err != nil {
|
||||
return token.Position{}, err
|
||||
}
|
||||
if lineEntry.EndSequence {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !prevLineEntry.EndSequence {
|
||||
// The chunk describes the code from prevLineEntry to
|
||||
// lineEntry.
|
||||
if prevLineEntry.Address <= address && lineEntry.Address > address {
|
||||
return token.Position{
|
||||
Filename: prevLineEntry.File.Name,
|
||||
Line: prevLineEntry.Line,
|
||||
Column: prevLineEntry.Column,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return token.Position{}, nil // location not found
|
||||
}
|
||||
|
||||
// Read the DWARF debug information from a given file (in various formats).
|
||||
func readDWARF(executable string) (*dwarf.Data, error) {
|
||||
f, err := os.Open(executable)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if file, err := elf.NewFile(f); err == nil {
|
||||
return file.DWARF()
|
||||
} else if file, err := macho.NewFile(f); err == nil {
|
||||
return file.DWARF()
|
||||
} else if file, err := pe.NewFile(f); err == nil {
|
||||
return file.DWARF()
|
||||
} else {
|
||||
return nil, errors.New("unknown binary format")
|
||||
}
|
||||
}
|
||||
|
||||
type outputWriter struct {
|
||||
out io.Writer
|
||||
executable string
|
||||
line []byte
|
||||
}
|
||||
|
||||
// newOutputWriter returns an io.Writer that will intercept panic addresses and
|
||||
// will try to insert a source location in the output if the source location can
|
||||
// be found in the executable.
|
||||
func newOutputWriter(out io.Writer, executable string) *outputWriter {
|
||||
return &outputWriter{
|
||||
out: out,
|
||||
executable: executable,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *outputWriter) Write(p []byte) (n int, err error) {
|
||||
start := 0
|
||||
for i, c := range p {
|
||||
if c == '\n' {
|
||||
w.out.Write(p[start : i+1])
|
||||
start = i + 1
|
||||
if w.executable != "" {
|
||||
address := extractPanicAddress(w.line)
|
||||
if address != 0 {
|
||||
loc, err := addressToLine(w.executable, address)
|
||||
if err == nil && loc.Filename != "" {
|
||||
fmt.Printf("[llgo: panic at %s]\n", loc.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
w.line = w.line[:0]
|
||||
} else {
|
||||
w.line = append(w.line, c)
|
||||
}
|
||||
}
|
||||
w.out.Write(p[start:])
|
||||
n = len(p)
|
||||
return
|
||||
}
|
||||
86
internal/shellparse/shellparse.go
Normal file
86
internal/shellparse/shellparse.go
Normal file
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright (c) 2024 The GoPlus Authors (goplus.org). All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package shellparse
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// Parse parses a shell command string into command and arguments,
|
||||
// properly handling quoted arguments with spaces
|
||||
func Parse(cmd string) ([]string, error) {
|
||||
args := make([]string, 0)
|
||||
var current strings.Builder
|
||||
var inQuotes bool
|
||||
var quoteChar rune
|
||||
var hasContent bool // Track if we've seen content (including empty quotes)
|
||||
|
||||
runes := []rune(cmd)
|
||||
for i := 0; i < len(runes); i++ {
|
||||
r := runes[i]
|
||||
switch {
|
||||
case !inQuotes && (r == '"' || r == '\''):
|
||||
// Start of quoted string
|
||||
inQuotes = true
|
||||
quoteChar = r
|
||||
hasContent = true // Empty quotes still count as content
|
||||
case inQuotes && r == quoteChar:
|
||||
// End of quoted string
|
||||
inQuotes = false
|
||||
quoteChar = 0
|
||||
case !inQuotes && unicode.IsSpace(r):
|
||||
// Space outside quotes - end current argument
|
||||
if hasContent {
|
||||
args = append(args, current.String())
|
||||
current.Reset()
|
||||
hasContent = false
|
||||
}
|
||||
case inQuotes && r == '\\' && i+1 < len(runes):
|
||||
// Handle escape sequences in quotes
|
||||
if quoteChar == '"' {
|
||||
next := runes[i+1]
|
||||
if next == quoteChar || next == '\\' {
|
||||
current.WriteRune(next)
|
||||
i++ // Skip the next rune
|
||||
} else {
|
||||
current.WriteRune(r)
|
||||
}
|
||||
} else {
|
||||
// In single quotes, backslash is a literal character.
|
||||
current.WriteRune(r)
|
||||
}
|
||||
default:
|
||||
// Regular character
|
||||
current.WriteRune(r)
|
||||
hasContent = true
|
||||
}
|
||||
}
|
||||
|
||||
// Handle unterminated quotes
|
||||
if inQuotes {
|
||||
return nil, fmt.Errorf("unterminated quote in command: %s", cmd)
|
||||
}
|
||||
|
||||
// Add final argument if any
|
||||
if hasContent {
|
||||
args = append(args, current.String())
|
||||
}
|
||||
|
||||
return args, nil
|
||||
}
|
||||
115
internal/shellparse/shellparse_test.go
Normal file
115
internal/shellparse/shellparse_test.go
Normal file
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Copyright (c) 2024 The GoPlus Authors (goplus.org). All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package shellparse
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cmd string
|
||||
want []string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "simple command",
|
||||
cmd: "echo hello",
|
||||
want: []string{"echo", "hello"},
|
||||
},
|
||||
{
|
||||
name: "command with quoted argument",
|
||||
cmd: `echo "hello world"`,
|
||||
want: []string{"echo", "hello world"},
|
||||
},
|
||||
{
|
||||
name: "command with single quoted argument",
|
||||
cmd: `echo 'hello world'`,
|
||||
want: []string{"echo", "hello world"},
|
||||
},
|
||||
{
|
||||
name: "command with multiple quoted arguments",
|
||||
cmd: `cp "file with spaces.txt" "another file.txt"`,
|
||||
want: []string{"cp", "file with spaces.txt", "another file.txt"},
|
||||
},
|
||||
{
|
||||
name: "command with escaped quotes",
|
||||
cmd: `echo "He said \"hello\""`,
|
||||
want: []string{"echo", `He said "hello"`},
|
||||
},
|
||||
{
|
||||
name: "command with escaped backslash",
|
||||
cmd: `echo "path\\to\\file"`,
|
||||
want: []string{"echo", `path\to\file`},
|
||||
},
|
||||
{
|
||||
name: "mixed quotes",
|
||||
cmd: `echo "double quote" 'single quote' normal`,
|
||||
want: []string{"echo", "double quote", "single quote", "normal"},
|
||||
},
|
||||
{
|
||||
name: "empty arguments",
|
||||
cmd: `echo "" ''`,
|
||||
want: []string{"echo", "", ""},
|
||||
},
|
||||
{
|
||||
name: "multiple spaces",
|
||||
cmd: "echo hello world",
|
||||
want: []string{"echo", "hello", "world"},
|
||||
},
|
||||
{
|
||||
name: "emulator command example",
|
||||
cmd: `qemu-system-xtensa -machine esp32 -kernel "/path/with spaces/firmware.bin"`,
|
||||
want: []string{"qemu-system-xtensa", "-machine", "esp32", "-kernel", "/path/with spaces/firmware.bin"},
|
||||
},
|
||||
{
|
||||
name: "unterminated double quote",
|
||||
cmd: `echo "hello`,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "unterminated single quote",
|
||||
cmd: `echo 'hello`,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "empty command",
|
||||
cmd: "",
|
||||
want: []string{},
|
||||
},
|
||||
{
|
||||
name: "only spaces",
|
||||
cmd: " ",
|
||||
want: []string{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Parse(tt.cmd)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Parse() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -35,10 +35,14 @@ type Config struct {
|
||||
UF2FamilyID string `json:"uf2-family-id"`
|
||||
|
||||
// Flash and deployment configuration
|
||||
FlashCommand string `json:"flash-command"`
|
||||
FlashMethod string `json:"flash-method"`
|
||||
FlashMethod string `json:"flash-method"` // values: command, openocd, msd
|
||||
FlashCommand string `json:"flash-command"` // used when FlashMethod == "command"
|
||||
Flash1200BpsReset string `json:"flash-1200-bps-reset"`
|
||||
|
||||
// Serial configuration
|
||||
Serial string `json:"serial"` // Serial communication type (e.g., "usb")
|
||||
SerialPort []string `json:"serial-port"` // Serial port identifiers (e.g., vendor:product IDs)
|
||||
|
||||
// Mass storage device configuration
|
||||
MSDVolumeName []string `json:"msd-volume-name"`
|
||||
MSDFirmwareName string `json:"msd-firmware-name"`
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
package targets_test
|
||||
//go:build !llgo
|
||||
// +build !llgo
|
||||
|
||||
package targets
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"sort"
|
||||
|
||||
"github.com/goplus/llgo/internal/targets"
|
||||
)
|
||||
|
||||
func ExampleResolver_Resolve() {
|
||||
resolver := targets.NewDefaultResolver()
|
||||
resolver := NewDefaultResolver()
|
||||
|
||||
// Resolve a specific target
|
||||
config, err := resolver.Resolve("rp2040")
|
||||
@@ -34,7 +35,7 @@ func ExampleResolver_Resolve() {
|
||||
}
|
||||
|
||||
func ExampleResolver_ListAvailableTargets() {
|
||||
resolver := targets.NewDefaultResolver()
|
||||
resolver := NewDefaultResolver()
|
||||
|
||||
targets, err := resolver.ListAvailableTargets()
|
||||
if err != nil {
|
||||
@@ -50,7 +51,7 @@ func ExampleResolver_ListAvailableTargets() {
|
||||
}
|
||||
|
||||
func ExampleResolver_ResolveAll() {
|
||||
resolver := targets.NewDefaultResolver()
|
||||
resolver := NewDefaultResolver()
|
||||
|
||||
configs, err := resolver.ResolveAll()
|
||||
if err != nil {
|
||||
|
||||
@@ -167,6 +167,9 @@ func (l *Loader) mergeConfig(dst, src *Config) {
|
||||
if src.Flash1200BpsReset != "" {
|
||||
dst.Flash1200BpsReset = src.Flash1200BpsReset
|
||||
}
|
||||
if src.Serial != "" {
|
||||
dst.Serial = src.Serial
|
||||
}
|
||||
if src.MSDFirmwareName != "" {
|
||||
dst.MSDFirmwareName = src.MSDFirmwareName
|
||||
}
|
||||
@@ -202,6 +205,9 @@ func (l *Loader) mergeConfig(dst, src *Config) {
|
||||
if len(src.ExtraFiles) > 0 {
|
||||
dst.ExtraFiles = append(dst.ExtraFiles, src.ExtraFiles...)
|
||||
}
|
||||
if len(src.SerialPort) > 0 {
|
||||
dst.SerialPort = append(dst.SerialPort, src.SerialPort...)
|
||||
}
|
||||
if len(src.MSDVolumeName) > 0 {
|
||||
dst.MSDVolumeName = append(dst.MSDVolumeName, src.MSDVolumeName...)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user