Files
llgo/internal/crosscompile/crosscompile.go

652 lines
19 KiB
Go

package crosscompile
import (
"errors"
"fmt"
"io/fs"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"github.com/goplus/llgo/internal/crosscompile/compile"
"github.com/goplus/llgo/internal/env"
"github.com/goplus/llgo/internal/flash"
"github.com/goplus/llgo/internal/targets"
"github.com/goplus/llgo/internal/xtool/llvm"
envllvm "github.com/goplus/llgo/xtool/env/llvm"
)
type Export struct {
CC string // Compiler to use
CCFLAGS []string
CFLAGS []string
LDFLAGS []string
// Additional fields from target configuration
BuildTags []string
GOOS string
GOARCH string
Libc string
Linker string // Linker to use (e.g., "ld.lld", "avr-ld")
ExtraFiles []string // Extra files to compile and link (e.g., .s, .c files)
ClangRoot string // Root directory of custom clang installation
ClangBinPath string // Path to clang binary directory
BinaryFormat string // Binary format (e.g., "elf", "esp", "uf2")
FormatDetail string // For uf2, it's uf2FamilyID
Emulator string // Emulator command template (e.g., "qemu-system-arm -M {} -kernel {}")
// Flashing/Debugging configuration
Device flash.Device // Device configuration for flashing/debugging
}
// URLs and configuration that can be overridden for testing
var (
wasiSdkUrl = "https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-25/wasi-sdk-25.0-x86_64-macos.tar.gz"
wasiMacosSubdir = "wasi-sdk-25.0-x86_64-macos"
)
var (
espClangBaseUrl = "https://github.com/goplus/espressif-llvm-project-prebuilt/releases/download/19.1.2_20250905-3"
espClangVersion = "19.1.2_20250905-3"
)
// cacheRoot can be overridden for testing
var cacheRoot = env.LLGoCacheDir
func cacheDir() string {
return filepath.Join(cacheRoot(), "crosscompile")
}
// buildEnvMap creates a map of template variables for the current context
func buildEnvMap(llgoRoot string) map[string]string {
envs := make(map[string]string)
// Basic paths
envs["root"] = llgoRoot
envs["tmpDir"] = os.TempDir()
// These will typically be set by calling code when actual values are known
// envs["port"] = "" // Serial port (e.g., "/dev/ttyUSB0", "COM3")
// envs["hex"] = "" // Path to hex file
// envs["bin"] = "" // Path to binary file
// envs["img"] = "" // Path to image file
// envs["zip"] = "" // Path to zip file
return envs
}
// getCanonicalArchName returns the canonical architecture name for a target triple
func getCanonicalArchName(triple string) string {
arch := strings.Split(triple, "-")[0]
if arch == "arm64" {
return "aarch64"
}
if strings.HasPrefix(arch, "arm") || strings.HasPrefix(arch, "thumb") {
return "arm"
}
if arch == "mipsel" {
return "mips"
}
return arch
}
// 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(forceEspClang bool) (clangRoot string, err error) {
llgoRoot := env.LLGoROOT()
// First check if clang exists in LLGoROOT
espClangRoot := filepath.Join(llgoRoot, envllvm.CrosscompileClangPath)
if _, err = os.Stat(espClangRoot); err == nil {
clangRoot = espClangRoot
return
}
if !forceEspClang {
return "", nil
}
// Try to download ESP Clang if platform is supported
platformSuffix := getESPClangPlatform(runtime.GOOS, runtime.GOARCH)
if platformSuffix != "" {
cacheClangDir := filepath.Join(cacheRoot(), "crosscompile", "esp-clang-"+espClangVersion)
if _, err = os.Stat(cacheClangDir); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return
}
fmt.Fprintln(os.Stderr, "ESP Clang not found in LLGO_ROOT or cache, will download.")
if err = checkDownloadAndExtractESPClang(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 ""
}
// ldFlagsFromFileName extracts the library name from a filename for use in linker flags
// For example, "libmath.a" becomes "math" for use with "-lmath"
func ldFlagsFromFileName(fileName string) string {
return strings.TrimPrefix(strings.TrimSuffix(fileName, ".a"), "lib")
}
// compileWithConfig compiles libraries according to the provided configuration
// and returns the necessary linker flags for linking against the compiled libraries
func compileWithConfig(
compileConfig compile.CompileConfig,
outputDir string, options compile.CompileOptions,
) (ldflags []string, err error) {
ldflags = append(ldflags, "-nostdlib", "-L"+outputDir)
for _, group := range compileConfig.Groups {
err = group.Compile(outputDir, options)
if err != nil {
break
}
if filepath.Ext(group.OutputFileName) == ".o" {
continue
}
ldflags = append(ldflags, "-l"+ldFlagsFromFileName(group.OutputFileName))
}
return
}
func use(goos, goarch string, wasiThreads, forceEspClang bool) (export Export, err error) {
targetTriple := llvm.GetTargetTriple(goos, goarch)
llgoRoot := env.LLGoROOT()
// Check for ESP Clang support for target-based builds
clangRoot, err := getESPClangRoot(forceEspClang)
if err != nil {
return
}
// Set ClangRoot and CC if clang is available
export.ClangRoot = clangRoot
if clangRoot != "" {
export.CC = filepath.Join(clangRoot, "bin", "clang++")
} else {
export.CC = "clang++"
}
if runtime.GOOS == goos && runtime.GOARCH == goarch {
// not cross compile
// Set up basic flags for non-cross-compile
export.LDFLAGS = []string{
"-target", targetTriple,
"-Qunused-arguments",
"-Wno-unused-command-line-argument",
"-Wl,--error-limit=0",
"-fuse-ld=lld",
}
if clangRoot != "" {
clangLib := filepath.Join(clangRoot, "lib")
clangInc := filepath.Join(clangRoot, "include")
export.CFLAGS = append(export.CFLAGS, "-I"+clangInc)
export.LDFLAGS = append(export.LDFLAGS, "-L"+clangLib)
// Add platform-specific rpath flags
switch goos {
case "darwin":
export.LDFLAGS = append(export.LDFLAGS, "-Wl,-rpath,"+clangLib)
case "linux":
export.LDFLAGS = append(export.LDFLAGS, "-Wl,-rpath,"+clangLib)
case "windows":
// Windows doesn't support rpath, DLLs should be in PATH or same directory
default:
// For other Unix-like systems, try the generic rpath
export.LDFLAGS = append(export.LDFLAGS, "-Wl,-rpath,"+clangLib)
}
}
export.CCFLAGS = []string{
"-Qunused-arguments",
"-Wno-unused-command-line-argument",
}
// 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.CCFLAGS = append(export.CCFLAGS, []string{"--sysroot=" + sysrootPath}...)
export.LDFLAGS = append(export.LDFLAGS, []string{"--sysroot=" + sysrootPath}...)
}
// 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",
// libpthread & libdl is built-in since glibc 2.34 (2021-08-01); we need to support earlier versions.
"-lpthread",
"-ldl",
)
}
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 = checkDownloadAndExtractWasiSDK(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,
"-Qunused-arguments",
"-Wno-unused-command-line-argument",
}
// 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,
"-Qunused-arguments",
"-Wno-unused-command-line-argument",
}
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.LDFLAGS = append(export.LDFLAGS, []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)
}
target := config.LLVMTarget
if target == "" {
return export, fmt.Errorf("target '%s' does not have a valid LLVM target triple", targetName)
}
cpu := config.CPU
if cpu == "" {
return export, fmt.Errorf("target '%s' does not have a valid CPU configuration", targetName)
}
// Check for ESP Clang support for target-based builds
clangRoot, err := getESPClangRoot(true)
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
export.ExtraFiles = config.ExtraFiles
export.BinaryFormat = config.BinaryFormat
export.FormatDetail = config.FormatDetail()
export.Emulator = config.Emulator
// Set flashing/debugging configuration
export.Device = flash.Device{
Serial: config.Serial,
SerialPort: config.SerialPort,
Flash: flash.Flash{
Method: config.FlashMethod,
Command: config.FlashCommand,
Flash1200BpsReset: config.Flash1200BpsReset == "true",
},
MSD: flash.MSD{
VolumeName: config.MSDVolumeName,
FirmwareName: config.MSDFirmwareName,
},
OpenOCD: flash.OpenOCD{
Interface: config.OpenOCDInterface,
Transport: config.OpenOCDTransport,
Target: config.OpenOCDTarget,
},
}
// Build environment map for template variable expansion
envs := buildEnvMap(env.LLGoROOT())
// Convert LLVMTarget, CPU, Features to CCFLAGS/LDFLAGS
ldflags := []string{"-S"}
ccflags := []string{"-Oz"}
cflags := []string{"-Wno-override-module", "-Qunused-arguments", "-Wno-unused-command-line-argument"}
if config.LLVMTarget != "" {
cflags = append(cflags, "--target="+config.LLVMTarget)
ccflags = append(ccflags, "--target="+config.LLVMTarget)
}
// Expand template variables in cflags
expandedCFlags := env.ExpandEnvSlice(config.CFlags, envs)
cflags = append(cflags, expandedCFlags...)
// The following parameters are inspired by tinygo/builder/library.go
// Handle CPU configuration
if cpu != "" {
// X86 has deprecated the -mcpu flag, so we need to use -march instead.
// However, ARM has not done this.
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)
}
// For ld.lld linker, also add CPU info to linker flags
if config.Linker == "ld.lld" {
ldflags = append(ldflags, "-mllvm", "-mcpu="+cpu)
}
}
// Handle architecture-specific flags
canonicalArch := getCanonicalArchName(target)
switch canonicalArch {
case "arm":
if strings.Split(target, "-")[2] == "linux" {
ccflags = append(ccflags, "-fno-unwind-tables", "-fno-asynchronous-unwind-tables")
} else {
ccflags = append(ccflags, "-fshort-enums", "-fomit-frame-pointer", "-mfloat-abi=soft", "-fno-unwind-tables", "-fno-asynchronous-unwind-tables")
}
case "avr":
// AVR defaults to C float and double both being 32-bit. This deviates
// from what most code (and certainly compiler-rt) expects. So we need
// to force the compiler to use 64-bit floating point numbers for
// double.
ccflags = append(ccflags, "-mdouble=64")
case "riscv32":
ccflags = append(ccflags, "-march=rv32imac", "-fforce-enable-int128")
case "riscv64":
ccflags = append(ccflags, "-march=rv64gc")
case "mips":
ccflags = append(ccflags, "-fno-pic")
}
// Handle soft float
if strings.Contains(config.Features, "soft-float") || strings.Contains(strings.Join(config.CFlags, " "), "soft-float") {
// Use softfloat instead of floating point instructions. This is
// supported on many architectures.
ccflags = append(ccflags, "-msoft-float")
} else {
if strings.HasPrefix(target, "armv5") {
// On ARMv5 we need to explicitly enable hardware floating point
// instructions: Clang appears to assume the hardware doesn't have a
// FPU otherwise.
ccflags = append(ccflags, "-mfpu=vfpv2")
}
}
// 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 code generation configuration
if config.CodeModel != "" {
ccflags = append(ccflags, "-mcmodel="+config.CodeModel)
}
if config.TargetABI != "" {
ccflags = append(ccflags, "-mabi="+config.TargetABI)
}
if config.RelocationModel != "" {
switch config.RelocationModel {
case "pic":
ccflags = append(ccflags, "-fPIC")
case "static":
ccflags = append(ccflags, "-fno-pic")
}
}
// 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
var libcIncludeDir []string
if config.Libc != "" {
var outputDir string
var libcLDFlags []string
var compileConfig compile.CompileConfig
baseDir := filepath.Join(cacheRoot(), "crosscompile")
outputDir, compileConfig, err = getLibcCompileConfigByName(baseDir, config.Libc, config.LLVMTarget, config.CPU)
if err != nil {
return
}
libcLDFlags, err = compileWithConfig(compileConfig, outputDir, compile.CompileOptions{
CC: export.CC,
Linker: export.Linker,
CCFLAGS: ccflags,
LDFLAGS: ldflags,
})
if err != nil {
return
}
cflags = append(cflags, compileConfig.ExportCFlags...)
ldflags = append(ldflags, libcLDFlags...)
libcIncludeDir = compileConfig.ExportCFlags
export.Libc = config.Libc
}
if config.RTLib != "" {
var outputDir string
var rtLibLDFlags []string
var compileConfig compile.CompileConfig
baseDir := filepath.Join(cacheRoot(), "crosscompile")
outputDir, compileConfig, err = getRTCompileConfigByName(baseDir, config.RTLib, config.LLVMTarget)
if err != nil {
return
}
rtLibLDFlags, err = compileWithConfig(compileConfig, outputDir, compile.CompileOptions{
CC: export.CC,
Linker: export.Linker,
CCFLAGS: ccflags,
LDFLAGS: ldflags,
CFLAGS: libcIncludeDir,
})
if err != nil {
return
}
ldflags = append(ldflags, rtLibLDFlags...)
}
// Combine with config flags and expand template variables
export.CFLAGS = cflags
export.CCFLAGS = ccflags
expandedLDFlags := env.ExpandEnvSlice(config.LDFlags, envs)
export.LDFLAGS = append(ldflags, expandedLDFlags...)
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, targetName string, wasiThreads, forceEspClang bool) (export Export, err error) {
if targetName != "" && !strings.HasPrefix(targetName, "wasm") && !strings.HasPrefix(targetName, "wasi") {
return UseTarget(targetName)
}
return use(goos, goarch, wasiThreads, forceEspClang)
}