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