refactor build/install/run pipeline

This commit is contained in:
Li Jie
2025-09-06 17:58:40 +08:00
parent da9865104f
commit 7cad146013
4 changed files with 214 additions and 213 deletions

View File

@@ -18,7 +18,6 @@ package build
import (
"bytes"
"debug/macho"
"errors"
"fmt"
"go/ast"
@@ -315,24 +314,51 @@ func Do(args []string, conf *Config) ([]Package, error) {
for _, pkg := range initial {
if needLink(pkg, mode) {
app, err := linkMainPkg(ctx, pkg, allPkgs, global, conf, mode, verbose)
name := path.Base(pkg.PkgPath)
binFmt := ctx.crossCompile.BinaryFormat
outputCfg, err := genOutputs(conf, name, len(ctx.initial) > 1, ctx.crossCompile.Emulator, binFmt)
if err != nil {
return nil, err
}
if slices.Contains([]Mode{ModeRun, ModeCmpTest, ModeTest, ModeInstall}, mode) {
// Flash to device if needed (for embedded targets)
if !conf.Emulator && conf.Target != "" {
err = flash(ctx, app, verbose)
err = linkMainPkg(ctx, pkg, allPkgs, global, outputCfg.IntPath, verbose)
if err != nil {
return nil, err
}
} else if mode != ModeInstall {
err = run(ctx, app, pkg.PkgPath, pkg.Dir, conf, mode, verbose)
finalApp := outputCfg.IntPath
if outputCfg.NeedFwGen || outputCfg.FileFmt != "" {
finalApp, err = convertFormat(ctx, finalApp, &outputCfg)
if err != nil {
return nil, err
}
}
switch mode {
case ModeBuild:
// Do nothing
case ModeInstall:
// Native already installed in linkMainPkg
if conf.Target != "" {
err = flash(ctx, finalApp, verbose)
if err != nil {
return nil, err
}
}
case ModeRun, ModeTest, ModeCmpTest:
if conf.Target == "" {
err = runNative(ctx, finalApp, pkg.Dir, pkg.PkgPath, conf, mode)
} else if conf.Emulator {
err = runInEmulator(ctx.crossCompile.Emulator, finalApp, pkg.Dir, pkg.PkgPath, conf, mode, verbose)
} else {
err = flash(ctx, finalApp, verbose)
}
if err != nil {
return nil, err
}
}
}
}
@@ -648,19 +674,7 @@ func compileExtraFiles(ctx *context, verbose bool) ([]string, error) {
return objFiles, nil
}
func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, global llssa.Package, conf *Config, mode Mode, verbose bool) (string, error) {
pkgPath := pkg.PkgPath
name := path.Base(pkgPath)
binFmt := ctx.crossCompile.BinaryFormat
// Generate output configuration using the centralized function
outputCfg, err := GenOutputs(conf, name, len(ctx.initial) > 1, ctx.crossCompile.Emulator, binFmt)
if err != nil {
return "", err
}
app := outputCfg.OutPath
orgApp := outputCfg.IntPath
func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, global llssa.Package, outputPath string, verbose bool) error {
needRuntime := false
needPyInit := false
@@ -689,7 +703,7 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, global l
})
entryObjFile, err := genMainModuleFile(ctx, llssa.PkgRuntime, pkg, needRuntime, needPyInit)
if err != nil {
return "", err
return err
}
// defer os.Remove(entryLLFile)
objFiles = append(objFiles, entryObjFile)
@@ -697,14 +711,14 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, global l
// Compile extra files from target configuration
extraObjFiles, err := compileExtraFiles(ctx, verbose)
if err != nil {
return "", err
return err
}
objFiles = append(objFiles, extraObjFiles...)
if global != nil {
export, err := exportObject(ctx, pkg.PkgPath+".global", pkg.ExportFile+"-global", []byte(global.String()))
if err != nil {
return "", err
return err
}
objFiles = append(objFiles, export)
}
@@ -726,28 +740,24 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, global l
}
}
err = linkObjFiles(ctx, orgApp, objFiles, linkArgs, verbose)
if err != nil {
return "", err
return linkObjFiles(ctx, outputPath, objFiles, linkArgs, verbose)
}
// Handle firmware conversion and file format conversion
currentApp := orgApp
func convertFormat(ctx *context, inputFile string, outputCfg *OutputCfg) (string, error) {
app := outputCfg.OutPath
currentApp := inputFile
if ctx.buildConf.Verbose {
fmt.Fprintf(os.Stderr, "Converting output format: %s -> %s\n", currentApp, app)
}
// Step 1: Firmware conversion if needed
if outputCfg.NeedFwGen {
if outputCfg.DirectGen {
// Direct conversion to final output (including .img case)
if verbose {
fmt.Fprintf(os.Stderr, "Converting to firmware format: %s (%s -> %s)\n", outputCfg.BinFmt, currentApp, app)
}
err = firmware.MakeFirmwareImage(currentApp, app, outputCfg.BinFmt, ctx.crossCompile.FormatDetail)
err := firmware.MakeFirmwareImage(currentApp, app, outputCfg.BinFmt, ctx.crossCompile.FormatDetail)
if err != nil {
return "", err
}
currentApp = app
} else {
// Convert to intermediate file first
binExt := firmware.BinaryExt(ctx.crossCompile.BinaryFormat)
tmpFile, err := os.CreateTemp("", "llgo-*"+binExt)
if err != nil {
@@ -756,16 +766,12 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, global l
tmpFile.Close()
intermediateApp := tmpFile.Name()
if verbose {
fmt.Fprintf(os.Stderr, "Converting to firmware format: %s (%s -> %s)\n", ctx.crossCompile.BinaryFormat, currentApp, intermediateApp)
}
err = firmware.MakeFirmwareImage(currentApp, intermediateApp, ctx.crossCompile.BinaryFormat, ctx.crossCompile.FormatDetail)
if err != nil {
return "", err
}
currentApp = intermediateApp
defer func() {
// Only remove if the intermediate file still exists (wasn't moved)
if _, err := os.Stat(intermediateApp); err == nil {
os.Remove(intermediateApp)
}
@@ -773,23 +779,15 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, global l
}
}
// Step 2: File format conversion if needed
if currentApp != app {
if outputCfg.FileFmt != "" {
// File format conversion
if verbose {
fmt.Fprintf(os.Stderr, "Converting to file format: %s (%s -> %s)\n", outputCfg.FileFmt, currentApp, app)
}
err = firmware.ConvertOutput(currentApp, app, binFmt, outputCfg.FileFmt)
binFmt := ctx.crossCompile.BinaryFormat
err := firmware.ConvertOutput(currentApp, app, binFmt, outputCfg.FileFmt)
if err != nil {
return "", err
}
} else {
// Just move/copy the file
if verbose {
fmt.Fprintf(os.Stderr, "Moving file: %s -> %s\n", currentApp, app)
}
err = os.Rename(currentApp, app)
err := os.Rename(currentApp, app)
if err != nil {
return "", err
}
@@ -799,87 +797,6 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, global l
return app, nil
}
func run(ctx *context, app string, pkgPath, pkgDir string, conf *Config, mode Mode, verbose bool) error {
useEmulator := false
if mode == ModeRun && conf.Emulator {
if ctx.crossCompile.Emulator == "" {
return fmt.Errorf("target %s does not have emulator configured", conf.Target)
}
useEmulator = true
}
switch mode {
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 {
// The command ran and exited with a non-zero status.
fmt.Fprintf(os.Stderr, "%s: exit code %d\n", app, exitErr.ExitCode())
if !ctx.testFail {
ctx.testFail = true
}
} else {
// The command failed to start or other error.
fmt.Fprintf(os.Stderr, "failed to run test %s: %v\n", app, err)
if !ctx.testFail {
ctx.testFail = true
}
}
}
case ModeRun:
if useEmulator {
if verbose {
fmt.Fprintf(os.Stderr, "Using emulator: %s\n", ctx.crossCompile.Emulator)
}
err := runInEmulator(app, ctx.crossCompile.Emulator, conf.RunArgs, verbose)
if err != nil {
return err
}
} else {
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 {
return err
}
if s := cmd.ProcessState; s != nil {
mockable.Exit(s.ExitCode())
}
}
case ModeCmpTest:
cmpTest(pkgDir, pkgPath, app, conf.GenExpect, conf.RunArgs)
}
return nil
}
func flash(ctx *context, app string, verbose bool) error {
// TODO: Implement device flashing logic
if verbose {
@@ -1359,76 +1276,8 @@ 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{}
// runInEmulator runs the application in emulator by formatting the emulator command template
func runInEmulator(appPath, emulatorTemplate string, runArgs []string, verbose bool) error {
// Build environment map for template variable expansion
envs := map[string]string{
"": appPath, // {} expands to app path
"bin": appPath,
"hex": appPath,
"zip": appPath,
"img": appPath,
"uf2": appPath,
}
// Expand the emulator command template
emulatorCmd := emulatorTemplate
for placeholder, path := range envs {
var target string
if placeholder == "" {
target = "{}"
} else {
target = "{" + placeholder + "}"
}
emulatorCmd = strings.ReplaceAll(emulatorCmd, target, path)
}
if verbose {
fmt.Fprintf(os.Stderr, "Running in emulator: %s\n", emulatorCmd)
}
// Parse command and arguments
cmdParts := strings.Fields(emulatorCmd)
if len(cmdParts) == 0 {
panic(fmt.Errorf("empty emulator command"))
}
// Add run arguments to the end
cmdParts = append(cmdParts, runArgs...)
// Execute the emulator command
cmd := exec.Command(cmdParts[0], cmdParts[1:]...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
if err != nil {
return err
}
if s := cmd.ProcessState; s != nil {
mockable.Exit(s.ExitCode())
}
return nil
}
func check(err error) {
if err != nil {
panic(err)

View File

@@ -18,8 +18,8 @@ type OutputCfg struct {
DirectGen bool // True if can generate firmware directly without intermediate file
}
// GenOutputs generates appropriate output paths based on the configuration
func GenOutputs(conf *Config, pkgName string, multiPkg bool, emulator, binFmt string) (OutputCfg, error) {
// genOutputs generates appropriate output paths based on the configuration
func genOutputs(conf *Config, pkgName string, multiPkg bool, emulator, binFmt string) (OutputCfg, error) {
var cfg OutputCfg
// Calculate binary extension and set up format info

View File

@@ -4,7 +4,6 @@
package build
import (
"strings"
"testing"
)
@@ -246,12 +245,7 @@ func TestGenOutputs(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Determine input binFmt - remove -img suffix if present as it will be added by the code
inputBinFmt := tt.wantBinFmt
if strings.HasSuffix(inputBinFmt, "-img") && tt.wantFileFmt == "img" {
inputBinFmt = strings.TrimSuffix(inputBinFmt, "-img")
}
result, err := GenOutputs(tt.conf, tt.pkgName, tt.multiPkg, tt.emulator, inputBinFmt)
result, err := genOutputs(tt.conf, tt.pkgName, tt.multiPkg, tt.crossCompile)
if err != nil {
t.Fatalf("GenOutputs() error = %v", err)
}

158
internal/build/run.go Normal file
View File

@@ -0,0 +1,158 @@
/*
* 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"
)
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, app, 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(app, emulator, conf.RunArgs, verbose)
case ModeTest:
return runEmuCmd(app, emulator, conf.RunArgs, verbose)
case ModeCmpTest:
cmpTest(pkgDir, pkgName, app, conf.GenExpect, conf.RunArgs)
return nil
}
return nil
}
// runEmuCmd runs the application in emulator by formatting the emulator command template
func runEmuCmd(appPath, emulatorTemplate string, runArgs []string, verbose bool) error {
// Build environment map for template variable expansion
envs := map[string]string{
"": appPath, // {} expands to app path
"bin": appPath,
"hex": appPath,
"zip": appPath,
"img": appPath,
"uf2": appPath,
}
// Expand the emulator command template
emulatorCmd := emulatorTemplate
for placeholder, path := range envs {
var target string
if placeholder == "" {
target = "{}"
} else {
target = "{" + placeholder + "}"
}
emulatorCmd = strings.ReplaceAll(emulatorCmd, target, path)
}
if verbose {
fmt.Fprintf(os.Stderr, "Running in emulator: %s\n", emulatorCmd)
}
// Parse command and arguments
cmdParts := strings.Fields(emulatorCmd)
if len(cmdParts) == 0 {
panic(fmt.Errorf("empty emulator command"))
}
// Add run arguments to the end
cmdParts = append(cmdParts, runArgs...)
// Execute the emulator command
cmd := exec.Command(cmdParts[0], cmdParts[1:]...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
if err != nil {
return err
}
if s := cmd.ProcessState; s != nil {
mockable.Exit(s.ExitCode())
}
return nil
}