package crosscompile import ( "errors" "fmt" "io/fs" "os" "os/exec" "path/filepath" "runtime" "strings" "github.com/goplus/llgo/internal/env" "github.com/goplus/llgo/internal/targets" "github.com/goplus/llgo/internal/xtool/llvm" ) type Export struct { CC string // Compiler to use CCFLAGS []string 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") ClangRoot string // Root directory of custom clang installation ClangBinPath string // Path to clang binary directory } const wasiSdkUrl = "https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-25/wasi-sdk-25.0-x86_64-macos.tar.gz" const ( espClangBaseUrl = "https://github.com/goplus/espressif-llvm-project-prebuilt/releases/download/19.1.2_20250805" espClangVersion = "19.1.2_20250805" ) func cacheDir() string { return filepath.Join(env.LLGoCacheDir(), "crosscompile") } // getMacOSSysroot returns the macOS SDK path using xcrun func getMacOSSysroot() (string, error) { cmd := exec.Command("xcrun", "--sdk", "macosx", "--show-sdk-path") output, err := cmd.Output() if err != nil { return "", err } return strings.TrimSpace(string(output)), nil } // getESPClangRoot returns the ESP Clang root directory, checking LLGoROOT first, // then downloading if needed and platform is supported func getESPClangRoot() (clangRoot string, err error) { llgoRoot := env.LLGoROOT() // First check if clang exists in LLGoROOT espClangRoot := filepath.Join(llgoRoot, "crosscompile", "clang") if _, err = os.Stat(espClangRoot); err == nil { clangRoot = espClangRoot return } // Try to download ESP Clang if platform is supported platformSuffix := getESPClangPlatform(runtime.GOOS, runtime.GOARCH) if platformSuffix != "" { cacheClangDir := filepath.Join(env.LLGoCacheDir(), "crosscompile", "clang-"+espClangVersion) if _, err = os.Stat(cacheClangDir); err != nil { if !errors.Is(err, fs.ErrNotExist) { return } if err = downloadAndExtractESPClang(platformSuffix, cacheClangDir); err != nil { return } } clangRoot = cacheClangDir return } err = fmt.Errorf("ESP Clang not found in LLGoROOT and platform %s/%s is not supported for download", runtime.GOOS, runtime.GOARCH) return } // getESPClangPlatform returns the platform suffix for ESP Clang downloads func getESPClangPlatform(goos, goarch string) string { switch goos { case "darwin": switch goarch { case "amd64": return "x86_64-apple-darwin" case "arm64": return "aarch64-apple-darwin" } case "linux": switch goarch { case "amd64": return "x86_64-linux-gnu" case "arm64": return "aarch64-linux-gnu" case "arm": return "arm-linux-gnueabihf" } case "windows": switch goarch { case "amd64": return "x86_64-w64-mingw32" } } return "" } func use(goos, goarch string, wasiThreads bool) (export Export, err error) { targetTriple := llvm.GetTargetTriple(goos, goarch) llgoRoot := env.LLGoROOT() if runtime.GOOS == goos && runtime.GOARCH == goarch { // not cross compile // Set up basic flags for non-cross-compile export.LDFLAGS = []string{ "-target", targetTriple, "-Wno-override-module", "-Wl,--error-limit=0", "-fuse-ld=lld", } // Add sysroot for macOS only if goos == "darwin" { sysrootPath, sysrootErr := getMacOSSysroot() if sysrootErr != nil { err = fmt.Errorf("failed to get macOS SDK path: %w", sysrootErr) return } export.LDFLAGS = append([]string{"--sysroot=" + sysrootPath}, export.LDFLAGS...) } // Add OS-specific flags switch goos { case "darwin": // ld64.lld (macOS) export.LDFLAGS = append( export.LDFLAGS, "-Xlinker", "-dead_strip", ) case "windows": // lld-link (Windows) // TODO(lijie): Add options for Windows. default: // ld.lld (Unix) export.CCFLAGS = append( export.CCFLAGS, "-fdata-sections", "-ffunction-sections", ) export.LDFLAGS = append( export.LDFLAGS, "-fdata-sections", "-ffunction-sections", "-Xlinker", "--gc-sections", "-lm", "-latomic", "-lpthread", // libpthread is built-in since glibc 2.34 (2021-08-01); we need to support earlier versions. ) } return } if goarch != "wasm" { return } // Configure based on GOOS switch goos { case "wasip1": // Set wasiSdkRoot path wasiSdkRoot := filepath.Join(llgoRoot, "crosscompile", "wasi-libc") // If not exists in LLGoROOT, download and use cached wasiSdkRoot if _, err = os.Stat(wasiSdkRoot); err != nil { sdkDir := filepath.Join(cacheDir(), llvm.GetTargetTriple(goos, goarch)) if wasiSdkRoot, err = checkDownloadAndExtract(wasiSdkUrl, sdkDir); err != nil { return } } // WASI-SDK configuration triple := "wasm32-wasip1" if wasiThreads { triple = "wasm32-wasip1-threads" } // Set up flags for the WASI-SDK or wasi-libc sysrootDir := filepath.Join(wasiSdkRoot, "share", "wasi-sysroot") libclangDir := filepath.Join(wasiSdkRoot, "lib", "clang", "19") includeDir := filepath.Join(sysrootDir, "include", triple) libDir := filepath.Join(sysrootDir, "lib", triple) // Use system clang and sysroot of wasi-sdk // Add compiler flags export.CCFLAGS = []string{ "-target", targetTriple, "--sysroot=" + sysrootDir, "-resource-dir=" + libclangDir, "-matomics", "-mbulk-memory", } export.CFLAGS = []string{ "-I" + includeDir, } // Add WebAssembly linker flags export.LDFLAGS = append(export.LDFLAGS, export.CCFLAGS...) export.LDFLAGS = append(export.LDFLAGS, []string{ "-Wno-override-module", "-Wl,--error-limit=0", "-L" + libDir, "-Wl,--allow-undefined", "-Wl,--import-memory,", // unknown import: `env::memory` has not been defined "-Wl,--export-memory", "-Wl,--initial-memory=67108864", // 64MB "-mbulk-memory", "-mmultimemory", "-z", "stack-size=10485760", // 10MB "-Wl,--export=malloc", "-Wl,--export=free", "-lc", "-lcrypt", "-lm", "-lrt", "-lutil", "-lsetjmp", "-lwasi-emulated-mman", "-lwasi-emulated-getpid", "-lwasi-emulated-process-clocks", "-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", ) } case "js": targetTriple := "wasm32-unknown-emscripten" // Emscripten configuration using system installation // Specify emcc as the compiler export.CC = "emcc" // Add compiler flags export.CCFLAGS = []string{ "-target", targetTriple, } export.CFLAGS = []string{} // Add WebAssembly linker flags for Emscripten export.LDFLAGS = []string{ "-target", targetTriple, "-Wno-override-module", "-Wl,--error-limit=0", "-s", "ALLOW_MEMORY_GROWTH=1", "-Wl,--allow-undefined", // "-Wl,--import-memory,", // "-Wl,--export-memory", // "-Wl,--initial-memory=67108864", // 64MB // "-mbulk-memory", // "-mmultimemory", // "-z", "stack-size=10485760", // 10MB // "-Wl,--export=malloc", "-Wl,--export=free", } export.EXTRAFLAGS = []string{ "-sENVIRONMENT=web,worker", "-DPLATFORM_WEB", "-sEXPORT_KEEPALIVE=1", "-sEXPORT_ES6=1", "-sALLOW_MEMORY_GROWTH=1", "-sRESERVED_FUNCTION_POINTERS=1", "-sEXPORTED_RUNTIME_METHODS=cwrap,allocateUTF8,stringToUTF8,UTF8ToString,FS,setValue,getValue", "-sWASM=1", "-sEXPORT_ALL=1", "-sASYNCIFY=1", "-sSTACK_SIZE=5242880", // 50MB } default: err = errors.New("unsupported GOOS for WebAssembly: " + goos) return } 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) } // Check for ESP Clang support for target-based builds clangRoot, err := getESPClangRoot() if err != nil { return } // Set ClangRoot and CC if clang is available export.ClangRoot = clangRoot export.CC = filepath.Join(clangRoot, "bin", "clang") // 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 if config.Linker != "" { export.Linker = filepath.Join(clangRoot, "bin", config.Linker) } if config.LinkerScript != "" { ldflags = append(ldflags, "-T", config.LinkerScript) } ldflags = append(ldflags, "-L", env.LLGoROOT()) // search targets/*.ld // Combine with config flags export.CFLAGS = config.CFlags export.CCFLAGS = ccflags export.LDFLAGS = append(ldflags, config.LDFlags...) export.EXTRAFLAGS = []string{} return export, nil } // Use extends the original Use function to support target-based configuration // If targetName is provided, it takes precedence over goos/goarch func Use(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 }