diff --git a/internal/build/build.go b/internal/build/build.go index b2bf418a..d29b130d 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -37,6 +37,7 @@ import ( "golang.org/x/tools/go/ssa" "github.com/goplus/llgo/cl" + "github.com/goplus/llgo/internal/crosscompile" "github.com/goplus/llgo/internal/env" "github.com/goplus/llgo/internal/mockable" "github.com/goplus/llgo/internal/packages" @@ -291,7 +292,9 @@ func Do(args []string, conf *Config) ([]Package, error) { os.Setenv("PATH", env.BinDir()+":"+os.Getenv("PATH")) // TODO(xsw): check windows output := conf.OutFile != "" - ctx := &context{env, cfg, progSSA, prog, dedup, patches, make(map[string]none), initial, mode, 0, output, make(map[*packages.Package]bool), make(map[*packages.Package]bool), conf} + export, err := crosscompile.UseCrossCompileSDK(conf.Goos, conf.Goarch) + check(err) + ctx := &context{env, cfg, progSSA, prog, dedup, patches, make(map[string]none), initial, mode, 0, output, make(map[*packages.Package]bool), make(map[*packages.Package]bool), conf, export} pkgs, err := buildAllPkgs(ctx, initial, verbose) check(err) if mode == ModeGen { @@ -357,7 +360,8 @@ type context struct { needRt map[*packages.Package]bool needPyInit map[*packages.Package]bool - buildConf *Config + buildConf *Config + crossCompile crosscompile.Export } func buildAllPkgs(ctx *context, initial []*packages.Package, verbose bool) (pkgs []*aPackage, err error) { @@ -543,6 +547,9 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, linkArgs args = append(args, "-gdwarf-4") } + args = append(args, ctx.crossCompile.CCFLAGS...) + args = append(args, ctx.crossCompile.LDFLAGS...) + cmd := ctx.env.Clang() cmd.Verbose = verbose err = cmd.Link(args...) @@ -1022,6 +1029,8 @@ func clFile(ctx *context, args []string, cFile, expFile string, procFile func(li cflags := buildCflags(ctx.buildConf.Goos, ctx.buildConf.Goarch, targetTriple) args = append(cflags, args...) args = append(args, "-emit-llvm", "-S", "-o", llFile, "-c", cFile) + args = append(args, ctx.crossCompile.CCFLAGS...) + args = append(args, ctx.crossCompile.CFLAGS...) if verbose { fmt.Fprintln(os.Stderr, "clang", args) } diff --git a/internal/crosscompile/cosscompile.go b/internal/crosscompile/cosscompile.go new file mode 100644 index 00000000..f71a96a9 --- /dev/null +++ b/internal/crosscompile/cosscompile.go @@ -0,0 +1,62 @@ +package crosscompile + +import ( + "errors" + "io/fs" + "os" + "path/filepath" + "runtime" + + "github.com/goplus/llgo/internal/env" + "github.com/goplus/llgo/internal/xtool/llvm" +) + +type Export struct { + CCFLAGS []string + CFLAGS []string + LDFLAGS []string +} + +const wasiSdkUrl = "https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-25/wasi-sdk-25.0-x86_64-macos.tar.gz" + +func cacheDir() string { + return filepath.Join(env.LLGoCacheDir(), "crosscompile") +} + +func UseCrossCompileSDK(goos, goarch string) (export Export, err error) { + if runtime.GOOS == goos && runtime.GOARCH == goarch { + // not cross compile + return + } + if goarch == "wasm" { + sdkDir := filepath.Join(cacheDir(), llvm.GetTargetTriple(goos, goarch)) + if _, err = os.Stat(sdkDir); err != nil { + if !errors.Is(err, fs.ErrNotExist) { + return + } + if err = downloadAndExtract(wasiSdkUrl, sdkDir); err != nil { + return + } + } + // Set up flags for the SDK + wasiSdkRoot := filepath.Join(sdkDir, "wasi-sdk-25.0-x86_64-macos") + sysrootDir := filepath.Join(wasiSdkRoot, "share", "wasi-sysroot") + libclangDir := filepath.Join(wasiSdkRoot, "lib", "clang", "19") + includeDir := filepath.Join(sysrootDir, "include", "wasm32-wasip1") + libDir := filepath.Join(sysrootDir, "lib", "wasm32-wasip1") + + export.CCFLAGS = []string{ + "--sysroot=" + sysrootDir, + "-resource-dir=" + libclangDir, + } + export.CFLAGS = []string{ + "-I" + includeDir, + } + export.LDFLAGS = []string{ + "-L" + libDir, + } + return + } + // TODO(lijie): supports other platforms + return +} diff --git a/internal/crosscompile/crosscompile_test.go b/internal/crosscompile/crosscompile_test.go new file mode 100644 index 00000000..35ffa192 --- /dev/null +++ b/internal/crosscompile/crosscompile_test.go @@ -0,0 +1,156 @@ +//go:build !llgo +// +build !llgo + +package crosscompile + +import ( + "os" + "runtime" + "testing" +) + +const ( + sysrootPrefix = "--sysroot=" + resourceDirPrefix = "-resource-dir=" + includePrefix = "-I" + libPrefix = "-L" +) + +func TestUseCrossCompileSDK(t *testing.T) { + // Skip long-running tests unless explicitly enabled + if testing.Short() { + t.Skip("Skipping test in short mode") + } + + // Test cases + testCases := []struct { + name string + goos string + goarch string + expectSDK bool + expectCCFlags bool + expectCFlags bool + expectLDFlags bool + }{ + { + name: "Same Platform", + goos: runtime.GOOS, + goarch: runtime.GOARCH, + expectSDK: false, + expectCCFlags: false, + expectCFlags: false, + expectLDFlags: false, + }, + { + name: "WASM Target", + goos: "wasip1", + goarch: "wasm", + expectSDK: true, + expectCCFlags: true, + expectCFlags: true, + expectLDFlags: true, + }, + { + name: "Unsupported Target", + goos: "windows", + goarch: "amd64", + expectSDK: false, + expectCCFlags: false, + expectCFlags: false, + expectLDFlags: false, + }, + } + + // Create a temporary directory for the cache + tempDir, err := os.MkdirTemp("", "crosscompile_test") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tempDir) + + // Set environment variable for cache directory + oldEnv := os.Getenv("LLGO_CACHE_DIR") + os.Setenv("LLGO_CACHE_DIR", tempDir) + defer os.Setenv("LLGO_CACHE_DIR", oldEnv) + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + export, err := UseCrossCompileSDK(tc.goos, tc.goarch) + + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + t.Logf("export: %+v", export) + + if tc.expectSDK { + // Check if flags are set correctly + if tc.expectCCFlags && len(export.CCFLAGS) == 0 { + t.Error("Expected CCFLAGS to be set, but they are empty") + } + + if tc.expectCFlags && len(export.CFLAGS) == 0 { + t.Error("Expected CFLAGS to be set, but they are empty") + } + + if tc.expectLDFlags && len(export.LDFLAGS) == 0 { + t.Error("Expected LDFLAGS to be set, but they are empty") + } + + // Check for specific flags + if tc.expectCCFlags { + hasSysroot := false + hasResourceDir := false + + for _, flag := range export.CCFLAGS { + if len(flag) >= len(sysrootPrefix) && flag[:len(sysrootPrefix)] == sysrootPrefix { + hasSysroot = true + } + if len(flag) >= len(resourceDirPrefix) && flag[:len(resourceDirPrefix)] == resourceDirPrefix { + hasResourceDir = true + } + } + + if !hasSysroot { + t.Error("Missing --sysroot flag in CCFLAGS") + } + if !hasResourceDir { + t.Error("Missing -resource-dir flag in CCFLAGS") + } + } + + if tc.expectCFlags { + hasInclude := false + + for _, flag := range export.CFLAGS { + if len(flag) >= len(includePrefix) && flag[:len(includePrefix)] == includePrefix { + hasInclude = true + } + } + + if !hasInclude { + t.Error("Missing -I flag in CFLAGS") + } + } + + if tc.expectLDFlags { + hasLib := false + + for _, flag := range export.LDFLAGS { + if len(flag) >= len(libPrefix) && flag[:len(libPrefix)] == libPrefix { + hasLib = true + } + } + + if !hasLib { + t.Error("Missing -L flag in LDFLAGS") + } + } + } else { + if len(export.CCFLAGS) != 0 || len(export.CFLAGS) != 0 || len(export.LDFLAGS) != 0 { + t.Errorf("Expected empty export, got CCFLAGS=%v, CFLAGS=%v, LDFLAGS=%v", + export.CCFLAGS, export.CFLAGS, export.LDFLAGS) + } + } + }) + } +} diff --git a/internal/crosscompile/fetch.go b/internal/crosscompile/fetch.go new file mode 100644 index 00000000..edd556a7 --- /dev/null +++ b/internal/crosscompile/fetch.go @@ -0,0 +1,109 @@ +package crosscompile + +import ( + "archive/tar" + "compress/gzip" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "strings" +) + +func downloadAndExtract(url, dir string) (err error) { + if _, err = os.Stat(dir); err == nil { + os.RemoveAll(dir) + } + tempDir := dir + ".temp" + os.RemoveAll(tempDir) + if err = os.MkdirAll(tempDir, 0755); err != nil { + return fmt.Errorf("failed to create temporary directory: %w", err) + } + + urlPath := strings.Split(url, "/") + filename := urlPath[len(urlPath)-1] + localFile := filepath.Join(tempDir, filename) + if err = downloadFile(url, localFile); err != nil { + return fmt.Errorf("failed to download file: %w", err) + } + defer os.Remove(localFile) + + if strings.HasSuffix(filename, ".tar.gz") || strings.HasSuffix(filename, ".tgz") { + err = extractTarGz(localFile, tempDir) + } else { + return fmt.Errorf("unsupported archive format: %s", filename) + } + if err != nil { + return fmt.Errorf("failed to extract archive: %w", err) + } + if err = os.Rename(tempDir, dir); err != nil { + return fmt.Errorf("failed to rename directory: %w", err) + } + return nil +} + +func downloadFile(url, filepath string) error { + out, err := os.Create(filepath) + if err != nil { + return err + } + defer out.Close() + resp, err := http.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("bad status: %s", resp.Status) + } + _, err = io.Copy(out, resp.Body) + return err +} + +func extractTarGz(tarGzFile, dest string) error { + file, err := os.Open(tarGzFile) + if err != nil { + return err + } + defer file.Close() + gzr, err := gzip.NewReader(file) + if err != nil { + return err + } + defer gzr.Close() + tr := tar.NewReader(gzr) + for { + header, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + return err + } + target := filepath.Join(dest, header.Name) + if !strings.HasPrefix(target, filepath.Clean(dest)+string(os.PathSeparator)) { + return fmt.Errorf("%s: illegal file path", target) + } + switch header.Typeflag { + case tar.TypeDir: + if err := os.MkdirAll(target, 0755); err != nil { + return err + } + case tar.TypeReg: + if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil { + return err + } + f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode)) + if err != nil { + return err + } + if _, err := io.Copy(f, tr); err != nil { + f.Close() + return err + } + f.Close() + } + } + return nil +} diff --git a/internal/env/env.go b/internal/env/env.go index f28fd8ae..5d602e90 100644 --- a/internal/env/env.go +++ b/internal/env/env.go @@ -32,6 +32,14 @@ func GOROOT() string { panic("cannot get GOROOT: " + err.Error()) } +func LLGoCacheDir() string { + userCacheDir, err := os.UserCacheDir() + if err != nil { + panic(err) + } + return filepath.Join(userCacheDir, "llgo") +} + func LLGoRuntimeDir() string { root := LLGoROOT() if root != "" {