package main import ( "archive/zip" "encoding/json" "fmt" "io" "os" "os/exec" "path/filepath" "runtime" "strings" ) type BuildConfig struct { OutDir string Targets []string // "linux/amd64", "windows/amd64", "lua" Bundle bool } func cmdBuild(args []string) { cfg := BuildConfig{OutDir: "dist"} for i := 0; i < len(args); i++ { switch args[i] { case "--outdir": if i+1 < len(args) { cfg.OutDir = args[i+1] i++ } case "--target": if i+1 < len(args) { cfg.Targets = append(cfg.Targets, args[i+1]) i++ } case "--bundle": cfg.Bundle = true } } // read plg.json plg, err := readPlgJSON("plg.json") if err != nil { fmt.Printf("error: read plg.json: %v\n", err) os.Exit(1) } if plg.IsLua() { buildTarget(plg, "lua", cfg.OutDir) return } if cfg.Bundle { buildBundle(plg, cfg.OutDir) return } // determine targets targets := cfg.Targets if len(targets) == 0 { targets = parseTargets(plg.Targets) } if len(targets) == 0 { targets = []string{"native"} } // build for each target for _, t := range targets { buildTarget(plg, t, cfg.OutDir) } } // allBundleTargets 是 --bundle 模式构建的全部平台。 // 每个 OS 只有一个架构(amd64),避免二进制文件名冲突。 var allBundleTargets = []struct { target string entry string // 二进制在 zip 中的文件名 }{ {"linux/amd64", "plugin.so"}, {"darwin/amd64", "plugin.dylib"}, {"windows/amd64", "plugin.dll"}, } func buildBundle(plg *PlgConfig, outDir string) { os.MkdirAll(outDir, 0755) buildDir := "build" os.MkdirAll(buildDir, 0755) // Auto-generate C ABI bridge for non-Windows bridgeCleanup := generateBridge("") defer bridgeCleanup() thirdpartCleanup := linkThirdpart("linux/amd64") defer thirdpartCleanup() var binaries []binEntry for _, bt := range allBundleTargets { cfg, errMsg := resolveBuild(bt.target) if cfg == nil { fmt.Printf(" error: %s\n", errMsg) return } outPath := filepath.Join(buildDir, cfg.entryFile) cmd := exec.Command("go", "build", "-buildmode=c-shared", "-o", outPath) cmd.Env = os.Environ() cmd.Env = append(cmd.Env, "GOOS="+cfg.goos, "GOARCH="+cfg.goarch, "CGO_ENABLED=1") if cfg.goos == "windows" { cc := detectWindowsCC() if cc != "" { cmd.Env = append(cmd.Env, "CC="+cc) } } cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr fmt.Printf(" compiling %s/%s (-buildmode=c-shared)...\n", cfg.goos, cfg.goarch) if err := cmd.Run(); err != nil { fmt.Printf(" error: build %s/%s: %v\n", cfg.goos, cfg.goarch, err) return } binaries = append(binaries, binEntry{src: outPath, zip: bt.entry}) } // Write plugin.json with all platforms declared platforms := map[string]bool{} for _, bt := range allBundleTargets { parts := strings.SplitN(bt.target, "/", 2) platforms[parts[0]] = true } plats := make([]string, 0, len(platforms)) for p := range platforms { plats = append(plats, p) } writePluginJSON(plg, plats, "plugin.so") // package single .hmap with correctly named entries hmapPath := filepath.Join(outDir, fmt.Sprintf("%s_bundle.hmap", toSnake(plg.NameEn))) createBundleHmap(hmapPath, "plugin.json", binaries) fmt.Printf(" packaged %s\n", filepath.Base(hmapPath)) } func (p *PlgConfig) IsLua() bool { return p.Entry == "main.lua" } func readPlgJSON(path string) (*PlgConfig, error) { data, err := os.ReadFile(path) if err != nil { return nil, err } var plg PlgConfig if err := json.Unmarshal(data, &plg); err != nil { return nil, err } return &plg, nil } func parseTargets(raw string) []string { raw = strings.TrimSpace(raw) if raw == "" || raw == "native" { return nil } var t []string for _, s := range strings.Split(raw, ",") { s = strings.TrimSpace(s) if s != "" { t = append(t, s) } } return t } func writePluginJSON(plg *PlgConfig, platforms []string, entry string) { m := map[string]interface{}{ "name": plg.Name, "name_zh": plg.NameZh, "name_en": plg.NameEn, "version": plg.Version, "description": plg.Description, "author": plg.Author, "entry": entry, } if len(platforms) > 0 { m["platforms"] = platforms } if len(plg.Tags) > 0 { m["tags"] = plg.Tags } data, _ := json.MarshalIndent(m, "", " ") os.WriteFile("plugin.json", data, 0644) } type buildConfig struct { goos string goarch string entryFile string // "plugin.so" or "plugin.dll" } func resolveBuild(target string) (*buildConfig, string) { if target == "lua" || target == "" { return nil, "lua" } goos, goarch, _ := strings.Cut(target, "/") if goos == "" { goos = runtime.GOOS if goarch == "" { goarch = runtime.GOARCH } } switch goos { case "linux": return &buildConfig{goos: goos, goarch: goarch, entryFile: "plugin.so"}, "" case "darwin": return &buildConfig{goos: goos, goarch: goarch, entryFile: "plugin.dylib"}, "" case "freebsd": return &buildConfig{goos: goos, goarch: goarch, entryFile: "plugin.so"}, "" case "windows": return &buildConfig{goos: goos, goarch: goarch, entryFile: "plugin.dll"}, "" default: return nil, fmt.Sprintf("unsupported OS %q", goos) } } func buildTarget(plg *PlgConfig, target, outDir string) { os.MkdirAll(outDir, 0755) // Lua: no compilation, package source directly if target == "lua" { writePluginJSON(plg, nil, "main.lua") pkgFiles := []string{"plugin.json", "main.lua"} for _, f := range []string{"README.md", "LICENSE"} { if _, err := os.Stat(f); err == nil { pkgFiles = append(pkgFiles, f) } } // Include thirdpart Lua files if entries, err := os.ReadDir("thirdpart"); err == nil { for _, e := range entries { if !e.IsDir() && strings.HasSuffix(e.Name(), ".lua") { path := filepath.Join("thirdpart", e.Name()) if _, err := os.Stat(path); err == nil { pkgFiles = append(pkgFiles, path) } } } } hmapName := fmt.Sprintf("%s_lua.hmap", toSnake(plg.NameEn)) createHmap(filepath.Join(outDir, hmapName), pkgFiles) fmt.Printf(" packaged %s\n", hmapName) return } // Resolve build config cfg, errMsg := resolveBuild(target) if cfg == nil { fmt.Printf(" error: %s\n", errMsg) return } buildDir := "build" os.MkdirAll(buildDir, 0755) outPath := filepath.Join(buildDir, cfg.entryFile) // Auto-generate C ABI bridge (all platforms use c-shared) bridgeCleanup := generateBridge(cfg.goos) _ = bridgeCleanup // DISABLED cleanup for debug // Auto-link thirdpart/ contents thirdpartCleanup := linkThirdpart(target) defer thirdpartCleanup() // Write plugin.json with the correct entry for this target writePluginJSON(plg, nil, cfg.entryFile) cmd := exec.Command("go", "build", "-buildmode=c-shared", "-o", outPath) cmd.Env = os.Environ() cmd.Env = append(cmd.Env, "GOOS="+cfg.goos, "GOARCH="+cfg.goarch, "CGO_ENABLED=1") // Auto-detect MinGW gcc on Windows if cfg.goos == "windows" { cc := detectWindowsCC() if cc != "" { cmd.Env = append(cmd.Env, "CC="+cc) } } cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr // DEBUG: list files before building entries, _ := os.ReadDir(".") for _, e := range entries { fmt.Printf(" [DEBUG] file: %s\n", e.Name()) } fmt.Printf(" compiling %s/%s (-buildmode=c-shared)...\n", cfg.goos, cfg.goarch) if err := cmd.Run(); err != nil { fmt.Printf(" error: build %s/%s: %v\n", cfg.goos, cfg.goarch, err) return } // package pkgFiles := []string{"plugin.json", outPath} for _, f := range []string{"README.md", "LICENSE"} { if _, err := os.Stat(f); err == nil { pkgFiles = append(pkgFiles, f) } } hmapName := fmt.Sprintf("%s_%s_%s.hmap", toSnake(plg.NameEn), cfg.goos, cfg.goarch) createHmap(filepath.Join(outDir, hmapName), pkgFiles) fmt.Printf(" packaged %s\n", hmapName) } type binEntry struct { src string // 磁盘路径,如 build/plugin.so zip string // zip 中条目名,如 plugin.so } // createBundleHmap 创建包含多平台二进制的 bundle .hmap 文件。 // jsonName 是 plugin.json 在 zip 中的条目名; // binaries 的 src 为磁盘路径,zip 为 zip 中的条目名。 func createBundleHmap(hmapPath, jsonName string, binaries []binEntry) { f, err := os.Create(hmapPath) if err != nil { fmt.Printf("error: create hmap %s: %v\n", hmapPath, err) return } defer f.Close() w := zip.NewWriter(f) defer w.Close() // Add plugin.json writeZipEntry := func(zipName, diskPath string) { info, err := os.Stat(diskPath) if err != nil { return } hdr, err := zip.FileInfoHeader(info) if err != nil { return } hdr.Method = zip.Deflate hdr.Name = zipName writer, err := w.CreateHeader(hdr) if err != nil { return } src, err := os.Open(diskPath) if err != nil { return } io.Copy(writer, src) src.Close() } writeZipEntry(jsonName, jsonName) // Add each platform binary with the correct zip entry name for _, b := range binaries { if _, err := os.Stat(b.src); err == nil { writeZipEntry(b.zip, b.src) } } // Add optional metadata files for _, fname := range []string{"README.md", "LICENSE"} { if _, err := os.Stat(fname); err == nil { writeZipEntry(fname, fname) } } } func createHmap(hmapPath string, files []string) { f, err := os.Create(hmapPath) if err != nil { fmt.Printf("error: create hmap %s: %v\n", hmapPath, err) return } defer f.Close() w := zip.NewWriter(f) defer w.Close() for _, path := range files { if path == "" { continue } info, err := os.Stat(path) if err != nil { continue } hdr, err := zip.FileInfoHeader(info) if err != nil { continue } hdr.Method = zip.Deflate hdr.Name = filepath.Base(path) writer, err := w.CreateHeader(hdr) if err != nil { continue } src, err := os.Open(path) if err != nil { continue } io.Copy(writer, src) src.Close() } } func toSnake(s string) string { return strings.ToLower(strings.ReplaceAll(s, " ", "_")) } // detectWindowsCC looks for a MinGW-w64 gcc on Windows for c-shared builds. func detectWindowsCC() string { // Check CC from environment first if cc := os.Getenv("CC"); cc != "" { if _, err := exec.LookPath(cc); err == nil { return cc } } // Check common MinGW install paths candidates := []string{ "C:\\mingw64\\bin\\gcc.exe", "C:\\MinGW\\bin\\gcc.exe", "C:\\msys64\\mingw64\\bin\\gcc.exe", "C:\\Users\\21989\\AppData\\Local\\Temp\\mingw64\\mingw64\\bin\\gcc.exe", } // Also search PATH for gcc if path, err := exec.LookPath("gcc"); err == nil { return path } for _, c := range candidates { if _, err := os.Stat(c); err == nil { return c } } return "" } // stripIncludeGuard strips preprocessor guards and C++ comments from a C header, // since these can confuse cgo's type resolution. func stripIncludeGuard(header string) string { lines := strings.Split(header, "\n") var out []string for _, line := range lines { trimmed := strings.TrimSpace(line) if trimmed == "#ifndef HOMEAGENT_CABI_H" || trimmed == "#define HOMEAGENT_CABI_H" { continue } if trimmed == "#endif" || strings.HasPrefix(trimmed, "#endif") { continue } if trimmed == "#ifdef __cplusplus" || trimmed == "extern \"C\" {" || trimmed == "}" { continue } // Strip C++-style comments (cgo parser may not handle them in /* */ blocks) if idx := strings.Index(line, "//"); idx >= 0 { line = line[:idx] } cleaned := strings.TrimSpace(line) if cleaned == "" { continue } out = append(out, line) } return strings.Join(out, "\n") } // generateBridge generates the C ABI bridge files for non-Lua builds. // Returns a cleanup function to remove generated files. func generateBridge(goos string) func() { const bridgeFile = "z_bridge_gen.go" const cEntryFile = "z_entry.c" os.Remove(bridgeFile) os.Remove(cEntryFile) var files []string if goos == "windows" { if err := os.WriteFile(bridgeFile, []byte(tmplBridge), 0644); err != nil { fmt.Printf(" error: write bridge: %v\n", err) return func() {} } files = append(files, bridgeFile) } else { if err := os.WriteFile(bridgeFile, []byte(tmplLinuxBridge), 0644); err != nil { fmt.Printf(" error: write bridge: %v\n", err) return func() {} } files = append(files, bridgeFile) // Write C entry point file if err := os.WriteFile(cEntryFile, []byte(tmplPluginInitC), 0644); err != nil { fmt.Printf(" error: write C entry: %v\n", err) return func() {} } files = append(files, cEntryFile) } return func() { for _, f := range files { os.Remove(f) } } } // linkThirdpart scans thirdpart/ for source files and generates auto-import stubs. // For Go plugins: if thirdpart/*.go exists, generate z_thirdpart.go with import. // For Lua plugins: no action needed (thirdpart/*.lua is packaged separately in buildTarget). // Returns cleanup function to remove generated files. func linkThirdpart(target string) func() { const thirdpartDir = "thirdpart" const importFile = "z_thirdpart.go" os.Remove(importFile) if info, err := os.Stat(thirdpartDir); err != nil || !info.IsDir() { return func() {} } entries, err := os.ReadDir(thirdpartDir) if err != nil { return func() {} } // For Go builds: check for .go files if target != "lua" { hasGo := false for _, e := range entries { if !e.IsDir() && strings.HasSuffix(e.Name(), ".go") { hasGo = true break } } if hasGo { // Read go.mod to get the module path gomodPath := "go.mod" data, err := os.ReadFile(gomodPath) if err != nil { return func() {} } modulePath := "" for _, line := range strings.Split(string(data), "\n") { if strings.HasPrefix(line, "module ") { modulePath = strings.TrimSpace(line[7:]) break } } if modulePath != "" { importPath := modulePath + "/" + thirdpartDir stub := "package main\nimport _ \"" + importPath + "\"\n" os.WriteFile(importFile, []byte(stub), 0644) } } } return func() { os.Remove(importFile) } }