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

@@ -66,6 +66,24 @@ const (
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
const (
@@ -104,6 +122,7 @@ type Config struct {
BaudRate int // baudrate for serial communication
RunArgs []string
Mode Mode
BuildMode BuildMode // Build mode: exe, c-archive, c-shared
AbiMode AbiMode
GenExpect bool // only valid for ModeCmpTest
Verbose bool
@@ -136,11 +155,12 @@ func NewDefaultConf(mode Mode) *Config {
goarch = runtime.GOARCH
}
conf := &Config{
Goos: goos,
Goarch: goarch,
BinPath: bin,
Mode: mode,
AbiMode: cabi.ModeAllFunc,
Goos: goos,
Goarch: goarch,
BinPath: bin,
Mode: mode,
BuildMode: BuildModeExe,
AbiMode: cabi.ModeAllFunc,
}
return conf
}
@@ -156,23 +176,6 @@ func envGOPATH() (string, error) {
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 (
@@ -361,12 +364,21 @@ func Do(args []string, conf *Config) ([]Package, error) {
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)
if err != nil {
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()
// 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 {
// 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 = 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
if IsDbgSymsEnabled() {
buildArgs = append(buildArgs, "-gdwarf-4")
@@ -813,6 +881,27 @@ func linkObjFiles(ctx *context, app string, objFiles, linkArgs []string, verbose
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 {
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
// 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{}
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 == "" {
// 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
}
}
// Native target - we're done
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 {
@@ -163,3 +211,39 @@ func (details *OutFmtDetails) ToEnvMap() map[string]string {
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)
}
})
}
}