Implement llgo build mode support (#1197)

- 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>
This commit is contained in:
Li Jie
2025-09-09 17:29:13 +08:00
parent d5ad4d997d
commit e05c8b9f46
9 changed files with 510 additions and 63 deletions

View File

@@ -75,7 +75,7 @@ func PassBuildFlags(cmd *Command) *PassArgs {
p.Bool("a") p.Bool("a")
p.Bool("linkshared", "race", "msan", "asan", p.Bool("linkshared", "race", "msan", "asan",
"trimpath", "work") "trimpath", "work")
p.Var("p", "asmflags", "compiler", "buildmode", p.Var("p", "asmflags", "compiler",
"gcflags", "gccgoflags", "installsuffix", "gcflags", "gccgoflags", "installsuffix",
"ldflags", "pkgdir", "toolexec", "buildvcs") "ldflags", "pkgdir", "toolexec", "buildvcs")
return p return p

View File

@@ -39,6 +39,7 @@ func init() {
flags.AddCommonFlags(&Cmd.Flag) flags.AddCommonFlags(&Cmd.Flag)
flags.AddBuildFlags(&Cmd.Flag) flags.AddBuildFlags(&Cmd.Flag)
flags.AddBuildModeFlags(&Cmd.Flag)
flags.AddEmulatorFlags(&Cmd.Flag) flags.AddEmulatorFlags(&Cmd.Flag)
flags.AddEmbeddedFlags(&Cmd.Flag) flags.AddEmbeddedFlags(&Cmd.Flag)
flags.AddOutputFlags(&Cmd.Flag) flags.AddOutputFlags(&Cmd.Flag)
@@ -51,7 +52,10 @@ func runCmd(cmd *base.Command, args []string) {
} }
conf := build.NewDefaultConf(build.ModeBuild) conf := build.NewDefaultConf(build.ModeBuild)
flags.UpdateConfig(conf) if err := flags.UpdateBuildConfig(conf); err != nil {
fmt.Fprintln(os.Stderr, err)
mockable.Exit(1)
}
args = cmd.Flag.Args() args = cmd.Flag.Args()

View File

@@ -25,6 +25,7 @@ func AddOutputFlags(fs *flag.FlagSet) {
var Verbose bool var Verbose bool
var BuildEnv string var BuildEnv string
var BuildMode string
var Tags string var Tags string
var Target string var Target string
var Emulator bool var Emulator bool
@@ -52,6 +53,10 @@ func AddBuildFlags(fs *flag.FlagSet) {
} }
} }
func AddBuildModeFlags(fs *flag.FlagSet) {
fs.StringVar(&BuildMode, "buildmode", "exe", "Build mode (exe, c-archive, c-shared)")
}
var Gen bool var Gen bool
func AddEmulatorFlags(fs *flag.FlagSet) { func AddEmulatorFlags(fs *flag.FlagSet) {
@@ -68,12 +73,13 @@ func AddCmpTestFlags(fs *flag.FlagSet) {
fs.BoolVar(&Gen, "gen", false, "Generate llgo.expect file") fs.BoolVar(&Gen, "gen", false, "Generate llgo.expect file")
} }
func UpdateConfig(conf *build.Config) { func UpdateConfig(conf *build.Config) error {
conf.Tags = Tags conf.Tags = Tags
conf.Verbose = Verbose conf.Verbose = Verbose
conf.Target = Target conf.Target = Target
conf.Port = Port conf.Port = Port
conf.BaudRate = BaudRate conf.BaudRate = BaudRate
switch conf.Mode { switch conf.Mode {
case build.ModeBuild: case build.ModeBuild:
conf.OutFile = OutputFile conf.OutFile = OutputFile
@@ -99,4 +105,20 @@ func UpdateConfig(conf *build.Config) {
conf.GenLL = GenLLFiles conf.GenLL = GenLLFiles
conf.ForceEspClang = ForceEspClang conf.ForceEspClang = ForceEspClang
} }
return nil
}
func UpdateBuildConfig(conf *build.Config) error {
// First apply common config
if err := UpdateConfig(conf); err != nil {
return err
}
// Validate and set build mode
if err := build.ValidateBuildMode(BuildMode); err != nil {
return err
}
conf.BuildMode = build.BuildMode(BuildMode)
return nil
} }

View File

@@ -47,7 +47,10 @@ func runCmd(cmd *base.Command, args []string) {
} }
conf := build.NewDefaultConf(build.ModeInstall) conf := build.NewDefaultConf(build.ModeInstall)
flags.UpdateConfig(conf) if err := flags.UpdateConfig(conf); err != nil {
fmt.Fprintln(os.Stderr, err)
mockable.Exit(1)
}
args = cmd.Flag.Args() args = cmd.Flag.Args()
_, err := build.Do(args, conf) _, err := build.Do(args, conf)

View File

@@ -76,7 +76,10 @@ func runCmdEx(cmd *base.Command, args []string, mode build.Mode) {
} }
conf := build.NewDefaultConf(mode) conf := build.NewDefaultConf(mode)
flags.UpdateConfig(conf) if err := flags.UpdateConfig(conf); err != nil {
fmt.Fprintln(os.Stderr, err)
mockable.Exit(1)
}
args = cmd.Flag.Args() args = cmd.Flag.Args()
args, runArgs, err := parseRunArgs(args) args, runArgs, err := parseRunArgs(args)

View File

@@ -7,6 +7,7 @@ import (
"github.com/goplus/llgo/cmd/internal/base" "github.com/goplus/llgo/cmd/internal/base"
"github.com/goplus/llgo/cmd/internal/flags" "github.com/goplus/llgo/cmd/internal/flags"
"github.com/goplus/llgo/internal/build" "github.com/goplus/llgo/internal/build"
"github.com/goplus/llgo/internal/mockable"
) )
// llgo test // llgo test
@@ -30,12 +31,15 @@ func runCmd(cmd *base.Command, args []string) {
} }
conf := build.NewDefaultConf(build.ModeTest) conf := build.NewDefaultConf(build.ModeTest)
flags.UpdateConfig(conf) if err := flags.UpdateConfig(conf); err != nil {
fmt.Fprintln(os.Stderr, err)
mockable.Exit(1)
}
args = cmd.Flag.Args() args = cmd.Flag.Args()
_, err := build.Do(args, conf) _, err := build.Do(args, conf)
if err != nil { if err != nil {
fmt.Fprintln(os.Stderr, err) fmt.Fprintln(os.Stderr, err)
os.Exit(1) mockable.Exit(1)
} }
} }

View File

@@ -66,6 +66,24 @@ const (
ModeGen ModeGen
) )
type BuildMode string
const (
BuildModeExe BuildMode = "exe"
BuildModeCArchive BuildMode = "c-archive"
BuildModeCShared BuildMode = "c-shared"
)
// ValidateBuildMode checks if the build mode is valid
func ValidateBuildMode(mode string) error {
switch BuildMode(mode) {
case BuildModeExe, BuildModeCArchive, BuildModeCShared:
return nil
default:
return fmt.Errorf("invalid build mode %q, must be one of: exe, c-archive, c-shared", mode)
}
}
type AbiMode = cabi.Mode type AbiMode = cabi.Mode
const ( const (
@@ -104,6 +122,7 @@ type Config struct {
BaudRate int // baudrate for serial communication BaudRate int // baudrate for serial communication
RunArgs []string RunArgs []string
Mode Mode Mode Mode
BuildMode BuildMode // Build mode: exe, c-archive, c-shared
AbiMode AbiMode AbiMode AbiMode
GenExpect bool // only valid for ModeCmpTest GenExpect bool // only valid for ModeCmpTest
Verbose bool Verbose bool
@@ -136,11 +155,12 @@ func NewDefaultConf(mode Mode) *Config {
goarch = runtime.GOARCH goarch = runtime.GOARCH
} }
conf := &Config{ conf := &Config{
Goos: goos, Goos: goos,
Goarch: goarch, Goarch: goarch,
BinPath: bin, BinPath: bin,
Mode: mode, Mode: mode,
AbiMode: cabi.ModeAllFunc, BuildMode: BuildModeExe,
AbiMode: cabi.ModeAllFunc,
} }
return conf return conf
} }
@@ -156,23 +176,6 @@ func envGOPATH() (string, error) {
return filepath.Join(home, "go"), nil return filepath.Join(home, "go"), nil
} }
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":
return ".wasm"
}
return ""
}
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
const ( const (
@@ -361,12 +364,21 @@ func Do(args []string, conf *Config) ([]Package, error) {
return nil, err return nil, err
} }
// Link main package using the base output path // Link main package using the output path from buildOutFmts
err = linkMainPkg(ctx, pkg, allPkgs, global, outFmts.Out, verbose) err = linkMainPkg(ctx, pkg, allPkgs, global, outFmts.Out, verbose)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Generate C headers for c-archive and c-shared modes before linking
if ctx.buildConf.BuildMode == BuildModeCArchive || ctx.buildConf.BuildMode == BuildModeCShared {
headerErr := generateCHeader(ctx, pkg, outFmts.Out, verbose)
if headerErr != nil {
return nil, headerErr
}
continue
}
envMap := outFmts.ToEnvMap() envMap := outFmts.ToEnvMap()
// Only convert formats when Target is specified // Only convert formats when Target is specified
@@ -794,13 +806,69 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, global l
} }
} }
return linkObjFiles(ctx, outputPath, objFiles, linkArgs, verbose) err = linkObjFiles(ctx, outputPath, objFiles, linkArgs, verbose)
if err != nil {
return err
}
return nil
}
// TODO(lijie): export C header from function list of the pkg
func generateCHeader(ctx *context, pkg *packages.Package, outputPath string, verbose bool) error {
// Determine header file path
headerPath := strings.TrimSuffix(outputPath, filepath.Ext(outputPath)) + ".h"
// Generate header content
headerContent := fmt.Sprintf(`/* Code generated by llgo; DO NOT EDIT. */
#ifndef __%s_H_
#define __%s_H_
#ifdef __cplusplus
extern "C" {
#endif
#ifdef __cplusplus
}
#endif
#endif /* __%s_H_ */
`,
strings.ToUpper(strings.ReplaceAll(pkg.Name, "-", "_")),
strings.ToUpper(strings.ReplaceAll(pkg.Name, "-", "_")),
strings.ToUpper(strings.ReplaceAll(pkg.Name, "-", "_")))
// Write header file
err := os.WriteFile(headerPath, []byte(headerContent), 0644)
if err != nil {
return fmt.Errorf("failed to write header file %s: %w", headerPath, err)
}
if verbose {
fmt.Fprintf(os.Stderr, "Generated C header: %s\n", headerPath)
}
return nil
} }
func linkObjFiles(ctx *context, app string, objFiles, linkArgs []string, verbose bool) error { func linkObjFiles(ctx *context, app string, objFiles, linkArgs []string, verbose bool) error {
// Handle c-archive mode differently - use ar tool instead of linker
if ctx.buildConf.BuildMode == BuildModeCArchive {
return createStaticArchive(ctx, app, objFiles, verbose)
}
buildArgs := []string{"-o", app} buildArgs := []string{"-o", app}
buildArgs = append(buildArgs, linkArgs...) buildArgs = append(buildArgs, linkArgs...)
// Add build mode specific linker arguments
switch ctx.buildConf.BuildMode {
case BuildModeCShared:
buildArgs = append(buildArgs, "-shared", "-fPIC")
case BuildModeExe:
// Default executable mode, no additional flags needed
}
// Add common linker arguments based on target OS and architecture // Add common linker arguments based on target OS and architecture
if IsDbgSymsEnabled() { if IsDbgSymsEnabled() {
buildArgs = append(buildArgs, "-gdwarf-4") buildArgs = append(buildArgs, "-gdwarf-4")
@@ -813,6 +881,27 @@ func linkObjFiles(ctx *context, app string, objFiles, linkArgs []string, verbose
return cmd.Link(buildArgs...) return cmd.Link(buildArgs...)
} }
func createStaticArchive(ctx *context, archivePath string, objFiles []string, verbose bool) error {
// Use ar tool to create static archive
args := []string{"rcs", archivePath}
args = append(args, objFiles...)
if verbose {
fmt.Fprintf(os.Stderr, "ar %s\n", strings.Join(args, " "))
}
cmd := exec.Command("ar", args...)
output, err := cmd.CombinedOutput()
if err != nil {
if verbose && len(output) > 0 {
fmt.Fprintf(os.Stderr, "ar output: %s\n", output)
}
return fmt.Errorf("ar command failed: %w", err)
}
return nil
}
func isWasmTarget(goos string) bool { func isWasmTarget(goos string) bool {
return slices.Contains([]string{"wasi", "js", "wasip1"}, goos) return slices.Contains([]string{"wasi", "js", "wasip1"}, goos)
} }

View File

@@ -47,46 +47,94 @@ func setOutFmt(conf *Config, formatName string) {
} }
// buildOutFmts creates OutFmtDetails based on package, configuration and multi-package status // 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) { func buildOutFmts(pkgName string, conf *Config, multiPkg bool, crossCompile *crosscompile.Export) (*OutFmtDetails, error) {
details := &OutFmtDetails{} details := &OutFmtDetails{}
var err error
// 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 == "" { if conf.Target == "" {
// Native target // Native target - we're done
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 return details, nil
} }
needRun := slices.Contains([]Mode{ModeRun, ModeTest, ModeCmpTest, ModeInstall}, conf.Mode) 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 // Check emulator format if emulator mode is enabled
outFmt := "" outFmt := ""
if needRun { if needRun {
@@ -163,3 +211,39 @@ func (details *OutFmtDetails) ToEnvMap() map[string]string {
return envMap 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 ""
}

View File

@@ -364,3 +364,241 @@ func TestBuildOutFmtsNativeTarget(t *testing.T) {
}) })
} }
} }
func TestBuildOutFmtsBuildModes(t *testing.T) {
tests := []struct {
name string
pkgName string
buildMode BuildMode
outFile string
mode Mode
target string
goos string
appExt string
expectedOut string
}{
// C-Archive tests
{
name: "c_archive_build_linux",
pkgName: "mylib",
buildMode: BuildModeCArchive,
outFile: "",
mode: ModeBuild,
target: "",
goos: "linux",
appExt: ".a",
expectedOut: "libmylib.a",
},
{
name: "c_archive_build_with_outfile",
pkgName: "mylib",
buildMode: BuildModeCArchive,
outFile: "custom.a",
mode: ModeBuild,
target: "",
goos: "linux",
appExt: ".a",
expectedOut: "libcustom.a",
},
{
name: "c_archive_build_with_path",
pkgName: "mylib",
buildMode: BuildModeCArchive,
outFile: "build/custom.a",
mode: ModeBuild,
target: "",
goos: "linux",
appExt: ".a",
expectedOut: "build/libcustom.a",
},
// C-Shared tests
{
name: "c_shared_build_linux",
pkgName: "mylib",
buildMode: BuildModeCShared,
outFile: "",
mode: ModeBuild,
target: "",
goos: "linux",
appExt: ".so",
expectedOut: "libmylib.so",
},
{
name: "c_shared_build_windows",
pkgName: "mylib",
buildMode: BuildModeCShared,
outFile: "",
mode: ModeBuild,
target: "",
goos: "windows",
appExt: ".dll",
expectedOut: "mylib.dll",
},
{
name: "c_shared_build_darwin",
pkgName: "mylib",
buildMode: BuildModeCShared,
outFile: "",
mode: ModeBuild,
target: "",
goos: "darwin",
appExt: ".dylib",
expectedOut: "libmylib.dylib",
},
{
name: "c_shared_embedded_target",
pkgName: "mylib",
buildMode: BuildModeCShared,
outFile: "",
mode: ModeBuild,
target: "rp2040",
goos: "windows",
appExt: ".so", // embedded follows linux rules
expectedOut: "libmylib.so",
},
// Executable tests
{
name: "exe_build_linux",
pkgName: "myapp",
buildMode: BuildModeExe,
outFile: "",
mode: ModeBuild,
target: "",
goos: "linux",
appExt: "",
expectedOut: "myapp",
},
{
name: "exe_build_windows",
pkgName: "myapp",
buildMode: BuildModeExe,
outFile: "",
mode: ModeBuild,
target: "",
goos: "windows",
appExt: ".exe",
expectedOut: "myapp.exe",
},
{
name: "exe_remove_lib_prefix",
pkgName: "libmyapp",
buildMode: BuildModeExe,
outFile: "",
mode: ModeBuild,
target: "",
goos: "linux",
appExt: "",
expectedOut: "myapp",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
conf := &Config{
Mode: tt.mode,
BuildMode: tt.buildMode,
Target: tt.target,
Goos: tt.goos,
OutFile: tt.outFile,
AppExt: tt.appExt,
}
crossCompile := &crosscompile.Export{}
result, err := buildOutFmts(tt.pkgName, conf, false, crossCompile)
if err != nil {
t.Fatalf("buildOutFmts failed: %v", err)
}
if result.Out != tt.expectedOut {
t.Errorf("buildOutFmts(%q, buildMode=%v, target=%q, goos=%q) = %q, want %q",
tt.pkgName, tt.buildMode, tt.target, tt.goos, result.Out, tt.expectedOut)
}
})
}
}
func TestApplyBuildModeNaming(t *testing.T) {
tests := []struct {
name string
baseName string
buildMode BuildMode
target string
goos string
expected string
}{
// Executable tests
{
name: "exe_linux",
baseName: "myapp",
buildMode: BuildModeExe,
target: "",
goos: "linux",
expected: "myapp",
},
{
name: "exe_remove_lib_prefix",
baseName: "libmyapp",
buildMode: BuildModeExe,
target: "",
goos: "linux",
expected: "myapp",
},
// C-Archive tests
{
name: "c_archive_linux",
baseName: "mylib",
buildMode: BuildModeCArchive,
target: "",
goos: "linux",
expected: "libmylib",
},
{
name: "c_archive_existing_prefix",
baseName: "libmylib",
buildMode: BuildModeCArchive,
target: "",
goos: "linux",
expected: "libmylib",
},
// C-Shared tests
{
name: "c_shared_linux",
baseName: "mylib",
buildMode: BuildModeCShared,
target: "",
goos: "linux",
expected: "libmylib",
},
{
name: "c_shared_windows",
baseName: "mylib",
buildMode: BuildModeCShared,
target: "",
goos: "windows",
expected: "mylib", // Windows doesn't use lib prefix
},
{
name: "c_shared_embedded_rp2040",
baseName: "mylib",
buildMode: BuildModeCShared,
target: "rp2040",
goos: "darwin",
expected: "libmylib", // embedded follows linux rules
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := applyPrefix(tt.baseName, tt.buildMode, tt.target, tt.goos)
if result != tt.expected {
t.Errorf("applyBuildModeNaming(%q, %v, %q, %q) = %q, want %q",
tt.baseName, tt.buildMode, tt.target, tt.goos, result, tt.expected)
}
})
}
}