mirror of
https://github.com/goplus/llgo.git
synced 2025-09-26 19:51:21 +08:00

- Add BuildMode type with three build modes: exe, c-archive, c-shared - Restrict buildmode flag to llgo build command only (not run/install/test) - Implement build mode specific linker arguments: - c-shared: use -shared -fPIC flags - c-archive: use ar tool to create static archive - exe: default executable mode - Add normalizeOutputPath function for platform-specific file naming conventions - Generate C header files for library modes - Fix buildmode flag conflict by removing from PassArgs - Add comprehensive test coverage for all build modes - Resolve duplicate logic between defaultAppExt and normalizeOutputPath 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
250 lines
6.4 KiB
Go
250 lines
6.4 KiB
Go
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
|
|
// determineBaseNameAndDir extracts the base name and directory from configuration
|
|
func determineBaseNameAndDir(pkgName string, conf *Config, multiPkg bool) (baseName, dir string) {
|
|
switch conf.Mode {
|
|
case ModeInstall:
|
|
return pkgName, conf.BinPath
|
|
case ModeBuild:
|
|
if !multiPkg && conf.OutFile != "" {
|
|
dir = filepath.Dir(conf.OutFile)
|
|
baseName = strings.TrimSuffix(filepath.Base(conf.OutFile), conf.AppExt)
|
|
if dir == "." {
|
|
dir = ""
|
|
}
|
|
return baseName, dir
|
|
}
|
|
return pkgName, ""
|
|
}
|
|
// Other modes (run, test, etc.)
|
|
return pkgName, ""
|
|
}
|
|
|
|
// applyPrefix applies build mode specific naming conventions
|
|
func applyPrefix(baseName string, buildMode BuildMode, target string, goos string) string {
|
|
// Determine the effective OS for naming conventions
|
|
effectiveGoos := goos
|
|
if target != "" {
|
|
// Embedded targets follow Linux conventions
|
|
effectiveGoos = "linux"
|
|
}
|
|
|
|
switch buildMode {
|
|
case BuildModeCArchive:
|
|
// Static libraries: libname.a (add lib prefix if missing)
|
|
if !strings.HasPrefix(baseName, "lib") {
|
|
return "lib" + baseName
|
|
}
|
|
return baseName
|
|
|
|
case BuildModeCShared:
|
|
// Shared libraries: libname.so/libname.dylib (add lib prefix if missing, except on Windows)
|
|
if effectiveGoos != "windows" && !strings.HasPrefix(baseName, "lib") {
|
|
return "lib" + baseName
|
|
}
|
|
return baseName
|
|
|
|
case BuildModeExe:
|
|
// Executables: name or name.exe (no lib prefix)
|
|
if strings.HasPrefix(baseName, "lib") {
|
|
return strings.TrimPrefix(baseName, "lib")
|
|
}
|
|
return baseName
|
|
}
|
|
|
|
return baseName
|
|
}
|
|
|
|
// buildOutputPath creates the final output path from baseName, dir and other parameters
|
|
func buildOutputPath(baseName, dir string, conf *Config, multiPkg bool, appExt string) (string, error) {
|
|
baseName = applyPrefix(baseName, conf.BuildMode, conf.Target, conf.Goos)
|
|
|
|
if dir != "" {
|
|
return filepath.Join(dir, baseName+appExt), nil
|
|
} else if (conf.Mode == ModeBuild && multiPkg) || (conf.Mode != ModeBuild && conf.Mode != ModeInstall) {
|
|
return genTempOutputFile(baseName, appExt)
|
|
} else {
|
|
return baseName + appExt, nil
|
|
}
|
|
}
|
|
|
|
func buildOutFmts(pkgName string, conf *Config, multiPkg bool, crossCompile *crosscompile.Export) (*OutFmtDetails, error) {
|
|
details := &OutFmtDetails{}
|
|
|
|
// Determine base name and directory
|
|
baseName, dir := determineBaseNameAndDir(pkgName, conf, multiPkg)
|
|
|
|
// Build output path
|
|
outputPath, err := buildOutputPath(baseName, dir, conf, multiPkg, conf.AppExt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
details.Out = outputPath
|
|
|
|
if conf.Target == "" {
|
|
// Native target - we're done
|
|
return details, nil
|
|
}
|
|
|
|
needRun := slices.Contains([]Mode{ModeRun, ModeTest, ModeCmpTest, ModeInstall}, conf.Mode)
|
|
|
|
// 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
|
|
}
|
|
|
|
func defaultAppExt(conf *Config) string {
|
|
// Handle build mode specific extensions first
|
|
switch conf.BuildMode {
|
|
case BuildModeCArchive:
|
|
return ".a"
|
|
case BuildModeCShared:
|
|
switch conf.Goos {
|
|
case "windows":
|
|
return ".dll"
|
|
case "darwin":
|
|
return ".dylib"
|
|
default:
|
|
return ".so"
|
|
}
|
|
case BuildModeExe:
|
|
// For executable mode, handle target-specific logic
|
|
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":
|
|
return ".wasm"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// This should not be reached, but kept for safety
|
|
return ""
|
|
}
|