diff --git a/internal/build/build.go b/internal/build/build.go index 9cea6b4b..c9d11fe2 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -151,7 +151,7 @@ func Do(args []string, conf *Config) ([]Package, error) { conf.Goarch = runtime.GOARCH } // Handle crosscompile configuration first to set correct GOOS/GOARCH - export, err := crosscompile.UseWithTarget(conf.Goos, conf.Goarch, IsWasiThreadsEnabled(), conf.Target) + export, err := crosscompile.Use(conf.Goos, conf.Goarch, IsWasiThreadsEnabled(), conf.Target) if err != nil { return nil, fmt.Errorf("failed to setup crosscompile: %w", err) } @@ -385,7 +385,7 @@ func (c *context) compiler() *clang.Cmd { } func (c *context) linker() *clang.Cmd { - cmd := c.env.Clang() + cmd := c.compiler() if c.crossCompile.Linker != "" { cmd = clang.New(c.crossCompile.Linker) } diff --git a/internal/crosscompile/crosscompile.go b/internal/crosscompile/crosscompile.go index 6f9b64a9..733132a6 100644 --- a/internal/crosscompile/crosscompile.go +++ b/internal/crosscompile/crosscompile.go @@ -22,23 +22,59 @@ type Export struct { 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") + 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/espressif/llvm-project/releases/download/esp-19.1.2_20250312" + espClangVersion = "esp-19.1.2_20250312" +) + func cacheDir() string { return filepath.Join(env.LLGoCacheDir(), "crosscompile") } -func Use(goos, goarch string, wasiThreads bool) (export Export, err error) { +// 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 @@ -86,7 +122,6 @@ func Use(goos, goarch string, wasiThreads bool) (export Export, err error) { switch goos { case "wasip1": // Set wasiSdkRoot path - llgoRoot := env.LLGoROOT() wasiSdkRoot := filepath.Join(llgoRoot, "crosscompile", "wasi-libc") // If not exists in LLGoROOT, download and use cached wasiSdkRoot @@ -222,6 +257,38 @@ func useTarget(targetName string) (export Export, err error) { return export, fmt.Errorf("failed to resolve target %s: %w", targetName, err) } + // Check for ESP Clang support for target-based builds + llgoRoot := env.LLGoROOT() + var clangRoot string + + // First check if clang exists in LLGoROOT + espClangRoot := filepath.Join(llgoRoot, "crosscompile", "clang") + if _, err = os.Stat(espClangRoot); err == nil { + clangRoot = espClangRoot + } else { + // Try to download ESP Clang if platform is supported + platformSuffix := getESPClangPlatform(runtime.GOOS, runtime.GOARCH) + if platformSuffix != "" { + cacheClangDir := filepath.Join(env.LLGoCacheDir(), "crosscompile", "clang") + if _, err = os.Stat(cacheClangDir); err != nil { + if !errors.Is(err, fs.ErrNotExist) { + return + } + if err = downloadAndExtractESPClang(platformSuffix, cacheClangDir); err != nil { + return + } + } + clangRoot = cacheClangDir + } else { + err = fmt.Errorf("ESP Clang not found in LLGoROOT and platform %s/%s is not supported for download", runtime.GOOS, runtime.GOARCH) + 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 @@ -274,13 +341,13 @@ func useTarget(targetName string) (export Export, err error) { return export, nil } -// UseWithTarget extends the original Use function to support target-based configuration +// Use 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) { +func Use(goos, goarch string, wasiThreads bool, targetName string) (export Export, err error) { if targetName != "" { return useTarget(targetName) } - return Use(goos, goarch, wasiThreads) + return use(goos, goarch, wasiThreads) } // filterCompatibleLDFlags filters out linker flags that are incompatible with clang/lld diff --git a/internal/crosscompile/crosscompile_test.go b/internal/crosscompile/crosscompile_test.go index 53161cd6..26cfeed5 100644 --- a/internal/crosscompile/crosscompile_test.go +++ b/internal/crosscompile/crosscompile_test.go @@ -76,7 +76,7 @@ func TestUseCrossCompileSDK(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - export, err := Use(tc.goos, tc.goarch, false) + export, err := use(tc.goos, tc.goarch, false) if err != nil { t.Fatalf("Unexpected error: %v", err) @@ -255,7 +255,7 @@ func TestUseTarget(t *testing.T) { func TestUseWithTarget(t *testing.T) { // Test target-based configuration takes precedence - export, err := UseWithTarget("linux", "amd64", false, "wasi") + export, err := Use("linux", "amd64", false, "wasi") if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -267,7 +267,7 @@ func TestUseWithTarget(t *testing.T) { } // Test fallback to goos/goarch when no target specified - export, err = UseWithTarget(runtime.GOOS, runtime.GOARCH, false, "") + export, err = Use(runtime.GOOS, runtime.GOARCH, false, "") if err != nil { t.Fatalf("Unexpected error: %v", err) } diff --git a/internal/crosscompile/fetch.go b/internal/crosscompile/fetch.go index 045ac9a6..598b8f02 100644 --- a/internal/crosscompile/fetch.go +++ b/internal/crosscompile/fetch.go @@ -7,6 +7,7 @@ import ( "io" "net/http" "os" + "os/exec" "path/filepath" "strings" ) @@ -31,6 +32,8 @@ func downloadAndExtract(url, dir string) (wasiSdkRoot string, err error) { if strings.HasSuffix(filename, ".tar.gz") || strings.HasSuffix(filename, ".tgz") { err = extractTarGz(localFile, tempDir) + } else if strings.HasSuffix(filename, ".tar.xz") { + err = extractTarXz(localFile, tempDir) } else { return "", fmt.Errorf("unsupported archive format: %s", filename) } @@ -108,3 +111,61 @@ func extractTarGz(tarGzFile, dest string) error { } return nil } + +func extractTarXz(tarXzFile, dest string) error { + // Use external tar command to extract .tar.xz files + cmd := exec.Command("tar", "-xf", tarXzFile, "-C", dest) + return cmd.Run() +} + +// downloadAndExtractESPClang downloads and extracts ESP Clang binaries and libraries +func downloadAndExtractESPClang(platformSuffix, dir string) error { + if _, err := os.Stat(dir); err == nil { + os.RemoveAll(dir) + } + + // Create download temp directory + downloadDir := dir + "-download" + os.RemoveAll(downloadDir) + if err := os.MkdirAll(downloadDir, 0755); err != nil { + return fmt.Errorf("failed to create download directory: %w", err) + } + defer os.RemoveAll(downloadDir) + + // Download clang binary package + clangUrl := fmt.Sprintf("%s/clang-%s-%s.tar.xz", espClangBaseUrl, espClangVersion, platformSuffix) + clangFile := filepath.Join(downloadDir, fmt.Sprintf("clang-%s-%s.tar.xz", espClangVersion, platformSuffix)) + if err := downloadFile(clangUrl, clangFile); err != nil { + return fmt.Errorf("failed to download clang: %w", err) + } + + // Download libs package + libsUrl := fmt.Sprintf("%s/libs-clang-%s-%s.tar.xz", espClangBaseUrl, espClangVersion, platformSuffix) + libsFile := filepath.Join(downloadDir, fmt.Sprintf("libs-clang-%s-%s.tar.xz", espClangVersion, platformSuffix)) + if err := downloadFile(libsUrl, libsFile); err != nil { + return fmt.Errorf("failed to download libs: %w", err) + } + + // Create extract temp directory + extractDir := dir + "-extract" + os.RemoveAll(extractDir) + if err := os.MkdirAll(extractDir, 0755); err != nil { + return fmt.Errorf("failed to create extract directory: %w", err) + } + defer os.RemoveAll(extractDir) + + // Extract both packages to extract directory + if err := extractTarXz(clangFile, extractDir); err != nil { + return fmt.Errorf("failed to extract clang: %w", err) + } + if err := extractTarXz(libsFile, extractDir); err != nil { + return fmt.Errorf("failed to extract libs: %w", err) + } + + // Rename esp-clang directory to final destination + espClangDir := filepath.Join(extractDir, "esp-clang") + if err := os.Rename(espClangDir, dir); err != nil { + return fmt.Errorf("failed to rename esp-clang directory: %w", err) + } + return nil +}