diff --git a/internal/crosscompile/cosscompile.go b/internal/crosscompile/crosscompile.go similarity index 58% rename from internal/crosscompile/cosscompile.go rename to internal/crosscompile/crosscompile.go index f066c8ac..17bd05aa 100644 --- a/internal/crosscompile/cosscompile.go +++ b/internal/crosscompile/crosscompile.go @@ -2,12 +2,15 @@ package crosscompile import ( "errors" + "fmt" "io/fs" "os" "path/filepath" "runtime" + "strings" "github.com/goplus/llgo/internal/env" + "github.com/goplus/llgo/internal/targets" "github.com/goplus/llgo/internal/xtool/llvm" ) @@ -17,6 +20,15 @@ type Export struct { CFLAGS []string LDFLAGS []string EXTRAFLAGS []string + + // Additional fields from target configuration + LLVMTarget string + CPU string + Features string + BuildTags []string + GOOS string + GOARCH string + Linker string // Linker to use (e.g., "ld.lld", "avr-ld") } const wasiSdkUrl = "https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-25/wasi-sdk-25.0-x86_64-macos.tar.gz" @@ -109,8 +121,8 @@ func Use(goos, goarch string, wasiThreads bool) (export Export, err error) { "-I" + includeDir, } // Add WebAssembly linker flags - export.LDFLAGS = []string{ - "-target", targetTriple, + export.LDFLAGS = append(export.LDFLAGS, export.CCFLAGS...) + export.LDFLAGS = append(export.LDFLAGS, []string{ "-Wno-override-module", "-Wl,--error-limit=0", "-L" + libDir, @@ -134,18 +146,18 @@ func Use(goos, goarch string, wasiThreads bool) (export Export, err error) { "-lwasi-emulated-signal", "-fwasm-exceptions", "-mllvm", "-wasm-enable-sjlj", - } + }...) // Add thread support if enabled if wasiThreads { export.CCFLAGS = append( export.CCFLAGS, "-pthread", ) + export.LDFLAGS = append(export.LDFLAGS, export.CCFLAGS...) export.LDFLAGS = append( export.LDFLAGS, "-lwasi-emulated-pthread", "-lpthread", - "-pthread", // global is immutable if -pthread is not specified ) } @@ -194,3 +206,114 @@ func Use(goos, goarch string, wasiThreads bool) (export Export, err error) { } return } + +// useTarget loads configuration from a target name (e.g., "rp2040", "wasi") +func useTarget(targetName string) (export Export, err error) { + resolver := targets.NewDefaultResolver() + + config, err := resolver.Resolve(targetName) + if err != nil { + return export, fmt.Errorf("failed to resolve target %s: %w", targetName, err) + } + + // Convert target config to Export - only export necessary fields + export.BuildTags = config.BuildTags + export.GOOS = config.GOOS + export.GOARCH = config.GOARCH + + // Convert LLVMTarget, CPU, Features to CCFLAGS/LDFLAGS + var ccflags []string + var ldflags []string + + target := config.LLVMTarget + if target == "" { + target = llvm.GetTargetTriple(config.GOOS, config.GOARCH) + } + + ccflags = append(ccflags, "-Wno-override-module", "--target="+config.LLVMTarget) + + // Inspired by tinygo + cpu := config.CPU + if cpu != "" { + if strings.HasPrefix(target, "i386") || strings.HasPrefix(target, "x86_64") { + ccflags = append(ccflags, "-march="+cpu) + } else if strings.HasPrefix(target, "avr") { + ccflags = append(ccflags, "-mmcu="+cpu) + } else { + ccflags = append(ccflags, "-mcpu="+cpu) + } + // Only add -mllvm flags for non-WebAssembly linkers + if config.Linker == "ld.lld" { + ldflags = append(ldflags, "-mllvm", "-mcpu="+cpu) + } + } + + // Handle Features + if config.Features != "" { + // Only add -mllvm flags for non-WebAssembly linkers + if config.Linker == "ld.lld" { + ldflags = append(ldflags, "-mllvm", "-mattr="+config.Features) + } + } + + // Handle Linker - keep it for external usage + export.Linker = config.Linker + + // Combine with config flags + export.CFLAGS = config.CFlags + export.CCFLAGS = ccflags + export.LDFLAGS = append(ldflags, filterCompatibleLDFlags(config.LDFlags)...) + export.EXTRAFLAGS = []string{} + + return export, nil +} + +// UseWithTarget extends the original Use function to support target-based configuration +// If targetName is provided, it takes precedence over goos/goarch +func UseWithTarget(goos, goarch string, wasiThreads bool, targetName string) (export Export, err error) { + if targetName != "" { + return useTarget(targetName) + } + return Use(goos, goarch, wasiThreads) +} + +// filterCompatibleLDFlags filters out linker flags that are incompatible with clang/lld +func filterCompatibleLDFlags(ldflags []string) []string { + if len(ldflags) == 0 { + return ldflags + } + + var filtered []string + + incompatiblePrefixes := []string{ + "--defsym=", // Use -Wl,--defsym= instead + "-T", // Linker script, needs special handling + } + + i := 0 + for i < len(ldflags) { + flag := ldflags[i] + + // Check incompatible prefixes + skip := false + for _, prefix := range incompatiblePrefixes { + if strings.HasPrefix(flag, prefix) { + skip = true + break + } + } + if skip { + // Skip -T and its argument if separate + if flag == "-T" && i+1 < len(ldflags) { + i += 2 // Skip both -T and the script path + } else { + i++ + } + continue + } + filtered = append(filtered, flag) + i++ + } + + return filtered +} diff --git a/internal/crosscompile/crosscompile_test.go b/internal/crosscompile/crosscompile_test.go index 9ce9bbe9..53161cd6 100644 --- a/internal/crosscompile/crosscompile_test.go +++ b/internal/crosscompile/crosscompile_test.go @@ -6,6 +6,7 @@ package crosscompile import ( "os" "runtime" + "slices" "testing" ) @@ -154,3 +155,181 @@ func TestUseCrossCompileSDK(t *testing.T) { }) } } + +func TestUseTarget(t *testing.T) { + // Test cases for target-based configuration + testCases := []struct { + name string + targetName string + expectError bool + expectLLVM string + expectCPU string + }{ + { + name: "WASI Target", + targetName: "wasi", + expectError: false, + expectLLVM: "", + expectCPU: "generic", + }, + { + name: "RP2040 Target", + targetName: "rp2040", + expectError: false, + expectLLVM: "thumbv6m-unknown-unknown-eabi", + expectCPU: "cortex-m0plus", + }, + { + name: "Cortex-M Target", + targetName: "cortex-m", + expectError: false, + expectLLVM: "", + expectCPU: "", + }, + { + name: "Arduino Target (with filtered flags)", + targetName: "arduino", + expectError: false, + expectLLVM: "avr", + expectCPU: "atmega328p", + }, + { + name: "Nonexistent Target", + targetName: "nonexistent-target", + expectError: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + export, err := useTarget(tc.targetName) + + if tc.expectError { + if err == nil { + t.Errorf("Expected error for target %s, but got none", tc.targetName) + } + return + } + + if err != nil { + t.Fatalf("Unexpected error for target %s: %v", tc.targetName, err) + } + + // Check if LLVM target is in CCFLAGS + if tc.expectLLVM != "" { + found := false + expectedFlag := "--target=" + tc.expectLLVM + for _, flag := range export.CCFLAGS { + if flag == expectedFlag { + found = true + break + } + } + if !found { + t.Errorf("Expected LLVM target %s in CCFLAGS, got %v", expectedFlag, export.CCFLAGS) + } + } + + // Check if CPU is in CCFLAGS + if tc.expectCPU != "" { + found := false + expectedFlags := []string{"-mmcu=" + tc.expectCPU, "-mcpu=" + tc.expectCPU} + for _, flag := range export.CCFLAGS { + for _, expectedFlag := range expectedFlags { + if flag == expectedFlag { + found = true + break + } + } + } + if !found { + t.Errorf("Expected CPU %s in CCFLAGS, got %v", tc.expectCPU, export.CCFLAGS) + } + } + + t.Logf("Target %s: BuildTags=%v, CFlags=%v, CCFlags=%v, LDFlags=%v", + tc.targetName, export.BuildTags, export.CFLAGS, export.CCFLAGS, export.LDFLAGS) + }) + } +} + +func TestUseWithTarget(t *testing.T) { + // Test target-based configuration takes precedence + export, err := UseWithTarget("linux", "amd64", false, "wasi") + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + // Check if LLVM target is in CCFLAGS + found := slices.Contains(export.CCFLAGS, "-mcpu=generic") + if !found { + t.Errorf("Expected CPU generic in CCFLAGS, got %v", export.CCFLAGS) + } + + // Test fallback to goos/goarch when no target specified + export, err = UseWithTarget(runtime.GOOS, runtime.GOARCH, false, "") + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + // Should use native configuration (only check for macOS since that's where tests run) + if runtime.GOOS == "darwin" && len(export.LDFLAGS) == 0 { + t.Error("Expected LDFLAGS to be set for native build") + } +} + +func TestFilterCompatibleLDFlags(t *testing.T) { + testCases := []struct { + name string + input []string + expected []string + }{ + { + name: "Empty flags", + input: []string{}, + expected: []string{}, + }, + { + name: "Compatible flags only", + input: []string{"-lm", "-lpthread"}, + expected: []string{"-lm", "-lpthread"}, + }, + { + name: "Incompatible flags filtered", + input: []string{"--gc-sections", "-lm", "--emit-relocs", "-lpthread"}, + expected: []string{"--gc-sections", "-lm", "--emit-relocs", "-lpthread"}, + }, + { + name: "Defsym flags filtered", + input: []string{"--defsym=_stack_size=512", "-lm", "--defsym=_bootloader_size=512"}, + expected: []string{"-lm"}, + }, + { + name: "Linker script flags filtered", + input: []string{"-T", "script.ld", "-lm"}, + expected: []string{"-lm"}, + }, + { + name: "Mixed compatible and incompatible", + input: []string{"-lm", "--gc-sections", "--defsym=test=1", "-lpthread", "--no-demangle"}, + expected: []string{"-lm", "--gc-sections", "-lpthread", "--no-demangle"}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := filterCompatibleLDFlags(tc.input) + + if len(result) != len(tc.expected) { + t.Errorf("Expected %d flags, got %d: %v", len(tc.expected), len(result), result) + return + } + + for i, expected := range tc.expected { + if result[i] != expected { + t.Errorf("Expected flag[%d] = %s, got %s", i, expected, result[i]) + } + } + }) + } +}