Merge pull request #1271 from cpunion/embed-cmds

Embed cmds
This commit is contained in:
xushiwei
2025-09-08 16:00:08 +08:00
committed by GitHub
36 changed files with 2337 additions and 582 deletions

2
.github/codecov.yml vendored
View File

@@ -10,4 +10,6 @@ coverage:
- "internal/typepatch"
- "internal/github"
- "internal/firmware"
- "internal/flash"
- "internal/monitor"
- "xtool"

8
.gitignore vendored
View File

@@ -44,3 +44,11 @@ go.work*
# GoReleaser
.dist/
.sysroot/
# Embedded firmware files
*.bin
*.hex
*.elf
*.uf2
*.img
*.zip

View File

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

View File

@@ -31,6 +31,7 @@ var Cmd = &base.Command{
func init() {
Cmd.Run = runCmd
flags.AddCommonFlags(&Cmd.Flag)
flags.AddBuildFlags(&Cmd.Flag)
}

View File

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

View File

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

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

View File

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

View File

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

View File

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

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

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

View File

@@ -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,6 +72,25 @@ 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
@@ -78,7 +98,11 @@ type Config struct {
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
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) {

View File

@@ -8,7 +8,6 @@ import (
"fmt"
"io"
"os"
"strings"
"testing"
"github.com/goplus/llgo/internal/mockable"
@@ -95,172 +94,4 @@ func TestCmpTest(t *testing.T) {
mockRun([]string{"../../cl/_testgo/runtest"}, &Config{Mode: ModeCmpTest})
}
func TestGenerateOutputFilenames(t *testing.T) {
tests := []struct {
name string
outFile string
binPath string
appExt string
binExt string
pkgName string
mode Mode
isMultiplePkgs bool
wantAppSuffix string
wantOrgAppDiff bool // true if orgApp should be different from app
wantErr bool
}{
{
name: "empty outFile, single package",
outFile: "",
binPath: "/usr/local/bin",
appExt: "",
binExt: "",
pkgName: "hello",
mode: ModeBuild,
isMultiplePkgs: false,
wantAppSuffix: "/usr/local/bin/hello",
wantOrgAppDiff: false,
},
{
name: "empty outFile with appExt",
outFile: "",
binPath: "/usr/local/bin",
appExt: ".exe",
binExt: "",
pkgName: "hello",
mode: ModeBuild,
isMultiplePkgs: false,
wantAppSuffix: "/usr/local/bin/hello.exe",
wantOrgAppDiff: false,
},
{
name: "outFile without binExt",
outFile: "myapp",
binPath: "/usr/local/bin",
appExt: ".exe",
binExt: "",
pkgName: "hello",
mode: ModeBuild,
isMultiplePkgs: false,
wantAppSuffix: "myapp.exe",
wantOrgAppDiff: false,
},
{
name: "outFile with existing extension, no binExt",
outFile: "myapp.exe",
binPath: "/usr/local/bin",
appExt: ".exe",
binExt: "",
pkgName: "hello",
mode: ModeBuild,
isMultiplePkgs: false,
wantAppSuffix: "myapp.exe",
wantOrgAppDiff: false,
},
{
name: "outFile with binExt, different from existing extension",
outFile: "myapp",
binPath: "/usr/local/bin",
appExt: ".exe",
binExt: ".bin",
pkgName: "hello",
mode: ModeBuild,
isMultiplePkgs: false,
wantAppSuffix: "myapp.bin",
wantOrgAppDiff: true,
},
{
name: "outFile already ends with binExt",
outFile: "t.bin",
binPath: "/usr/local/bin",
appExt: ".exe",
binExt: ".bin",
pkgName: "hello",
mode: ModeBuild,
isMultiplePkgs: false,
wantAppSuffix: "t.bin",
wantOrgAppDiff: true,
},
{
name: "outFile with full path already ends with binExt",
outFile: "/path/to/t.bin",
binPath: "/usr/local/bin",
appExt: ".exe",
binExt: ".bin",
pkgName: "hello",
mode: ModeBuild,
isMultiplePkgs: false,
wantAppSuffix: "/path/to/t.bin",
wantOrgAppDiff: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
app, orgApp, err := generateOutputFilenames(
tt.outFile,
tt.binPath,
tt.appExt,
tt.binExt,
tt.pkgName,
tt.mode,
tt.isMultiplePkgs,
)
if (err != nil) != tt.wantErr {
t.Errorf("generateOutputFilenames() error = %v, wantErr %v", err, tt.wantErr)
return
}
if tt.wantAppSuffix != "" {
if app != tt.wantAppSuffix {
t.Errorf("generateOutputFilenames() app = %v, want %v", app, tt.wantAppSuffix)
}
}
if tt.wantOrgAppDiff {
if app == orgApp {
t.Errorf("generateOutputFilenames() orgApp should be different from app, but both are %v", app)
}
// Clean up temp file
if orgApp != "" && strings.Contains(orgApp, "llgo-") {
os.Remove(orgApp)
}
} else {
if app != orgApp {
t.Errorf("generateOutputFilenames() orgApp = %v, want %v (same as app)", orgApp, app)
}
}
})
}
}
func TestGenerateOutputFilenames_EdgeCases(t *testing.T) {
// Test case where outFile has same extension as binExt
app, orgApp, err := generateOutputFilenames(
"firmware.bin",
"/usr/local/bin",
".exe",
".bin",
"esp32app",
ModeBuild,
false,
)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if app != "firmware.bin" {
t.Errorf("Expected app to be 'firmware.bin', got '%s'", app)
}
if app == orgApp {
t.Errorf("Expected orgApp to be different from app when binExt is present, but both are '%s'", app)
}
// Clean up temp file
if orgApp != "" && strings.Contains(orgApp, "llgo-") {
os.Remove(orgApp)
}
}
// TestGenerateOutputFilenames removed - functionality moved to filename_test.go

165
internal/build/outputs.go Normal file
View 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
}

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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