diff --git a/docs/en/PLUGIN_DEV.md b/docs/en/PLUGIN_DEV.md index 2314569..164aed1 100644 --- a/docs/en/PLUGIN_DEV.md +++ b/docs/en/PLUGIN_DEV.md @@ -112,26 +112,50 @@ Execution process: 2. **Go plugin**: Runs `go build -buildmode=c-shared` (produces `.so` + C ABI header) 3. **Lua plugin**: Packages source code directly, no compilation needed 4. Generates `plugin.json` manifest file -5. Packages as `.hmap` distribution (zip format, containing `plugin.json` + `plugin.so` + `plugin.dll` + `main.lua`) +5. Packages as `.hmap` distribution (zip format, containing `plugin.json` + platform binary) + +The `platforms` field in `plugin.json` declares supported platforms; the build includes the corresponding binary: + +| Platform | Binary name | +|----------|-------------| +| Linux | `plugin.so` | +| macOS | `plugin.dylib` | +| Windows | `plugin.dll` | + +> Use `--bundle` to build a multi-platform bundle — the resulting `.hmap` contains binaries for all platforms. +> During installation, the kernel automatically selects the correct binary for the current OS, skipping others. Output in `dist/` directory: ``` dist/ ├── myplugin_linux_amd64.hmap # Go plugin Linux version ├── myplugin_windows_amd64.hmap # Go plugin Windows version +├── myplugin_darwin_amd64.hmap # Go plugin macOS version └── myplugin_lua.hmap # Lua plugin ``` ### Deployment -Install via PluginMgr HTTP API: +Install via PluginMgr HTTP API (three methods): ```bash -# Kernel PluginMgr listens on :9876 +# 1. Install from URL (auto-cleanup) curl -X POST http://127.0.0.1:9876/plugins \ - -F "file=@dist/myplugin_linux_amd64.hmap" + -H "Content-Type: application/json" \ + -d '{"url": "https://example.com/myplugin.hmap"}' + +# 2. Install from local path (keeps source file) +curl -X POST http://127.0.0.1:9876/plugins \ + -H "Content-Type: application/json" \ + -d '{"path": "/path/to/myplugin.hmap"}' + +# 3. Upload binary directly +curl -X POST http://127.0.0.1:9876/plugins \ + --data-binary @dist/myplugin.hmap ``` +Reload plugins via `/api/v1/plugins/reload` or restart the kernel to activate. + Or upload via WebUI plugin management page. --- diff --git a/docs/zh/PLUGIN_DEV.md b/docs/zh/PLUGIN_DEV.md index dd11ae2..9e7853c 100644 --- a/docs/zh/PLUGIN_DEV.md +++ b/docs/zh/PLUGIN_DEV.md @@ -113,27 +113,51 @@ plugindev build 2. **Go 插件**:执行 `go build -buildmode=c-shared`(生成 `.so` + C ABI header) 3. **Lua 插件**:直接打包源码,无需编译 4. 生成 `plugin.json` 清单文件 -5. 打包为 `.hmap` 分发包(zip 格式,内含 `plugin.json` + `plugin.so` + `plugin.dll` + `main.lua`) +5. 打包为 `.hmap` 分发包(zip 格式,内含 `plugin.json` + 平台二进制) + +`plugin.json` 的 `platforms` 字段声明支持的平台,打包时自动包含对应二进制: + +| 平台 | 二进制文件名 | +|------|-------------| +| Linux | `plugin.so` | +| macOS | `plugin.dylib` | +| Windows | `plugin.dll` | + +> 使用 `--bundle` 可一次打包多平台,生成的 `.hmap` 内含所有平台的二进制。 +> 安装时核心自动选择当前平台的文件,跳过其他平台。 输出在 `dist/` 目录: ``` dist/ ├── myplugin_linux_amd64.hmap # Go 插件 Linux 版 ├── myplugin_windows_amd64.hmap # Go 插件 Windows 版 +├── myplugin_darwin_amd64.hmap # Go 插件 macOS 版 └── myplugin_lua.hmap # Lua 插件 ``` ### 安装部署 -通过 PluginMgr HTTP API 安装: +通过 PluginMgr HTTP API 安装,支持三种方式: ```bash -# 内核 PluginMgr 监听 :9876 +# 1. 从 URL 安装(自动清理安装包) curl -X POST http://127.0.0.1:9876/plugins \ - -F "file=@dist/myplugin_linux_amd64.hmap" + -H "Content-Type: application/json" \ + -d '{"url": "https://example.com/myplugin.hmap"}' + +# 2. 从本地路径安装(保留安装包) +curl -X POST http://127.0.0.1:9876/plugins \ + -H "Content-Type: application/json" \ + -d '{"path": "/path/to/myplugin.hmap"}' + +# 3. 直接上传二进制 +curl -X POST http://127.0.0.1:9876/plugins \ + --data-binary @dist/myplugin.hmap ``` -或通过 WebUI 插件管理页面上传安装。 +安装后需调用 `/api/v1/plugins/reload` 或重启内核生效。 + +也可通过 WebUI 插件管理页面上传安装。 --- diff --git a/internal/plugin/cabi/loader.go b/internal/plugin/cabi/loader.go index 3701581..11b3f64 100644 --- a/internal/plugin/cabi/loader.go +++ b/internal/plugin/cabi/loader.go @@ -1,3 +1,5 @@ +//go:build linux || darwin + package cabi /* diff --git a/internal/plugin/dynamic.go b/internal/plugin/dynamic.go index 12f04fc..41beba7 100644 --- a/internal/plugin/dynamic.go +++ b/internal/plugin/dynamic.go @@ -1,26 +1,11 @@ package plugin import ( - "crypto/sha256" - "encoding/hex" "encoding/json" - "fmt" "os" "path/filepath" - "plugin" - "reflect" - - sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk" - pubsdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk" - - "gitcode.com/JianFeeeee/HomeAgent/internal/plugin/cabi" ) -// .so 插件必须导出函数 NewPlugin,签名与 NativeFactory 一致: -// -// func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) { -// return &myPlugin{name: name}, nil -// } const ( soEntry = "plugin.so" dllEntry = "plugin.dll" @@ -28,18 +13,6 @@ const ( metaEntry = "plugin.json" ) -type dynamicPlugin struct { - name string - impl pubsdk.Plugin -} - -func (p *dynamicPlugin) Name() string { return p.name } -func (p *dynamicPlugin) Start(s *sdk.PluginSDK) error { - return p.impl.Start(s.PluginSDK) -} -func (p *dynamicPlugin) Stop() error { return p.impl.Stop() } - -// readManifest 读取插件目录下的 plugin.json。文件不存在时不报错。 func readManifest(dir string) *PluginManifest { data, err := os.ReadFile(filepath.Join(dir, metaEntry)) if err != nil { @@ -52,91 +25,4 @@ func readManifest(dir string) *PluginManifest { return &m } -// cabiPlugin wraps a C ABI loaded plugin (.so via -buildmode=c-shared). -type cabiPlugin struct { - name string - handle *cabi.Handle -} - -func (p *cabiPlugin) Name() string { return p.name } -func (p *cabiPlugin) Start(s *sdk.PluginSDK) error { - corePtr := p.handle.CreateCoreAPI(s) - if corePtr == nil { - return fmt.Errorf("cabi: failed to create CoreAPI for %s", p.name) - } - if err := p.handle.Start(corePtr); err != nil { - return fmt.Errorf("cabi: start %s: %w", p.name, err) - } - return nil -} - -func (p *cabiPlugin) Stop() error { - _ = p.handle.Stop() - // Note: intentionally NOT calling p.handle.Close() (dlclose). - // The .so stays loaded because plugin goroutines (HTTP server, tickers) - // may still be running. dlclose would unmap their code and cause SIGSEGV. - return nil -} - -// tryLoadSO 尝试从插件目录加载 plugin.so。 -// 优先尝试 C ABI 加载(-buildmode=c-shared),失败时回退到 Go plugin.Open。 -func tryLoadSO(dir, name string, config map[string]interface{}) (sdk.Plugin, error) { - soPath := filepath.Join(dir, soEntry) - if _, err := os.Stat(soPath); os.IsNotExist(err) { - return nil, nil - } - - // Try C ABI first - handle, err := cabi.Load(soPath, name, config) - if err == nil { - return &cabiPlugin{name: name, handle: handle}, nil - } - - // Fall back to Go plugin.Open - data, err := os.ReadFile(soPath) - if err != nil { - return nil, fmt.Errorf("read %s: %w", soPath, err) - } - h := sha256.Sum256(data) - cacheKey := fmt.Sprintf("plugin_%s_%s.so", name, hex.EncodeToString(h[:8])) - cachePath := filepath.Join(os.TempDir(), cacheKey) - if _, err := os.Stat(cachePath); os.IsNotExist(err) { - if err := os.WriteFile(cachePath, data, 0644); err != nil { - return nil, fmt.Errorf("write cache %s: %w", cachePath, err) - } - } - - p, err := plugin.Open(cachePath) - if err != nil { - return nil, fmt.Errorf("plugin.Open %s: %w", cachePath, err) - } - - sym, err := p.Lookup("NewPlugin") - if err != nil { - return nil, fmt.Errorf(".so %s must export NewPlugin: %w", soPath, err) - } - - rv := reflect.ValueOf(sym) - if rv.Kind() != reflect.Func { - return nil, fmt.Errorf("NewPlugin in %s is not a function (type=%T)", soPath, sym) - } - if rv.Type().NumIn() != 2 || rv.Type().NumOut() != 2 { - return nil, fmt.Errorf("NewPlugin in %s has wrong arity", soPath) - } - outs := rv.Call([]reflect.Value{reflect.ValueOf(name), reflect.ValueOf(config)}) - if len(outs) != 2 { - return nil, fmt.Errorf("NewPlugin in %s returned unexpected values", soPath) - } - if !outs[1].IsNil() { - if err, ok := outs[1].Interface().(error); ok { - return nil, fmt.Errorf("NewPlugin %s: %w", name, err) - } - return nil, fmt.Errorf("NewPlugin %s returned non-error second value", name) - } - plg, ok := outs[0].Interface().(pubsdk.Plugin) - if !ok { - return nil, fmt.Errorf("NewPlugin in %s does not implement pubsdk.Plugin", soPath) - } - - return &dynamicPlugin{name: name, impl: plg}, nil -} +var _ = json.Marshal diff --git a/internal/plugin/dynamic_loader_unix.go b/internal/plugin/dynamic_loader_unix.go new file mode 100644 index 0000000..2546d53 --- /dev/null +++ b/internal/plugin/dynamic_loader_unix.go @@ -0,0 +1,119 @@ +//go:build linux || darwin + +package plugin + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "os" + "path/filepath" + "plugin" + "reflect" + + pubsdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk" + sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk" + + "gitcode.com/JianFeeeee/HomeAgent/internal/plugin/cabi" +) + +type dynamicPlugin struct { + name string + impl pubsdk.Plugin +} + +func (p *dynamicPlugin) Name() string { return p.name } +func (p *dynamicPlugin) Start(s *sdk.PluginSDK) error { + return p.impl.Start(s.PluginSDK) +} +func (p *dynamicPlugin) Stop() error { return p.impl.Stop() } + +type cabiPlugin struct { + name string + handle *cabi.Handle +} + +func (p *cabiPlugin) Name() string { return p.name } +func (p *cabiPlugin) Start(s *sdk.PluginSDK) error { + corePtr := p.handle.CreateCoreAPI(s) + if corePtr == nil { + return fmt.Errorf("cabi: failed to create CoreAPI for %s", p.name) + } + if err := p.handle.Start(corePtr); err != nil { + return fmt.Errorf("cabi: start %s: %w", p.name, err) + } + return nil +} + +func (p *cabiPlugin) Stop() error { + _ = p.handle.Stop() + return nil +} + +func tryLoadSO(dir, name string, config map[string]interface{}) (sdk.Plugin, error) { + soPath := filepath.Join(dir, soEntry) + if _, err := os.Stat(soPath); os.IsNotExist(err) { + // 回退尝试 plugin.dylib (macOS 原生扩展名) + dylibPath := filepath.Join(dir, "plugin.dylib") + if _, err2 := os.Stat(dylibPath); err2 == nil { + soPath = dylibPath + } else { + return nil, nil + } + } + + handle, err := cabi.Load(soPath, name, config) + if err == nil { + return &cabiPlugin{name: name, handle: handle}, nil + } + + data, err := os.ReadFile(soPath) + if err != nil { + return nil, fmt.Errorf("read %s: %w", soPath, err) + } + h := sha256.Sum256(data) + cacheKey := fmt.Sprintf("plugin_%s_%s.so", name, hex.EncodeToString(h[:8])) + cachePath := filepath.Join(os.TempDir(), cacheKey) + if _, err := os.Stat(cachePath); os.IsNotExist(err) { + if err := os.WriteFile(cachePath, data, 0644); err != nil { + return nil, fmt.Errorf("write cache %s: %w", cachePath, err) + } + } + + p, err := plugin.Open(cachePath) + if err != nil { + return nil, fmt.Errorf("plugin.Open %s: %w", cachePath, err) + } + + sym, err := p.Lookup("NewPlugin") + if err != nil { + return nil, fmt.Errorf(".so %s must export NewPlugin: %w", soPath, err) + } + + rv := reflect.ValueOf(sym) + if rv.Kind() != reflect.Func { + return nil, fmt.Errorf("NewPlugin in %s is not a function (type=%T)", soPath, sym) + } + if rv.Type().NumIn() != 2 || rv.Type().NumOut() != 2 { + return nil, fmt.Errorf("NewPlugin in %s has wrong arity", soPath) + } + outs := rv.Call([]reflect.Value{reflect.ValueOf(name), reflect.ValueOf(config)}) + if len(outs) != 2 { + return nil, fmt.Errorf("NewPlugin in %s returned unexpected values", soPath) + } + if !outs[1].IsNil() { + if err, ok := outs[1].Interface().(error); ok { + return nil, fmt.Errorf("NewPlugin %s: %w", name, err) + } + return nil, fmt.Errorf("NewPlugin %s returned non-error second value", name) + } + plg, ok := outs[0].Interface().(pubsdk.Plugin) + if !ok { + return nil, fmt.Errorf("NewPlugin in %s does not implement pubsdk.Plugin", soPath) + } + + return &dynamicPlugin{name: name, impl: plg}, nil +} + +var _ = json.Marshal diff --git a/internal/plugin/dynamic_loader_windows.go b/internal/plugin/dynamic_loader_windows.go new file mode 100644 index 0000000..c7db6cf --- /dev/null +++ b/internal/plugin/dynamic_loader_windows.go @@ -0,0 +1,11 @@ +//go:build windows + +package plugin + +import ( + sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk" +) + +func tryLoadSO(dir, name string, config map[string]interface{}) (sdk.Plugin, error) { + return nil, nil +} diff --git a/internal/plugin/manifest.go b/internal/plugin/manifest.go index 14d2416..095ae68 100644 --- a/internal/plugin/manifest.go +++ b/internal/plugin/manifest.go @@ -19,7 +19,8 @@ type PluginManifest struct { License string `json:"license,omitempty"` Homepage string `json:"homepage,omitempty"` Repository string `json:"repository,omitempty"` - Entry string `json:"entry"` // "plugin.so" | "main.lua" | "SKILL.md" + Entry string `json:"entry"` // "plugin.so" | "plugin.dll" | "main.lua" | "SKILL.md" + Platforms []string `json:"platforms,omitempty"` // 声明的支持平台: ["linux","darwin","windows"] MinVersion string `json:"min_version,omitempty"` Tags []string `json:"tags,omitempty"` Deprecated bool `json:"deprecated,omitempty"` diff --git a/internal/plugins/pluginmgr/plugin.go b/internal/plugins/pluginmgr/plugin.go index b4fead5..8c2d429 100644 --- a/internal/plugins/pluginmgr/plugin.go +++ b/internal/plugins/pluginmgr/plugin.go @@ -13,6 +13,7 @@ import ( "net/url" "os" "path/filepath" + "runtime" "strings" "sync" "time" @@ -21,6 +22,30 @@ import ( sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk" ) +// platformBinary 按当前 OS 选择正确的插件二进制文件名。 +// 返回 (zip内文件名, 安装后重命名). +func platformBinary() (zipName, canonicalName string) { + switch runtime.GOOS { + case "linux": + return "plugin.so", "plugin.so" + case "darwin": + return "plugin.dylib", "plugin.so" // dlopen 兼容 .so 名称 + case "windows": + return "plugin.dll", "plugin.dll" + default: + return "", "" + } +} + +// validBinaries 是 .hmap 中所有可识别的平台二进制文件名。 +var validBinaries = map[string]bool{ + "plugin.so": true, + "plugin.dylib": true, + "plugin.dll": true, + "main.lua": true, + "SKILL.md": true, +} + var downloadClient = &http.Client{ Timeout: 30 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { @@ -210,22 +235,32 @@ func (p *Plugin) handlePlugins(w http.ResponseWriter, r *http.Request) { ct := r.Header.Get("Content-Type") if strings.HasPrefix(ct, "application/json") { var body struct { - URL string `json:"url"` + URL string `json:"url"` + Path string `json:"path"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { http.Error(w, "invalid json", http.StatusBadRequest) return } - if body.URL == "" { - http.Error(w, "url is required", http.StatusBadRequest) + switch { + case body.URL != "": + result, err := p.installFromURL(body.URL) + if err != nil { + writeJSON(w, http.StatusInternalServerError, map[string]interface{}{"error": err.Error()}) + return + } + writeJSON(w, http.StatusOK, result) + case body.Path != "": + result, err := p.installFromPath(body.Path) + if err != nil { + writeJSON(w, http.StatusInternalServerError, map[string]interface{}{"error": err.Error()}) + return + } + writeJSON(w, http.StatusOK, result) + default: + http.Error(w, "url or path is required", http.StatusBadRequest) return } - result, err := p.installFromURL(body.URL) - if err != nil { - writeJSON(w, http.StatusInternalServerError, map[string]interface{}{"error": err.Error()}) - return - } - writeJSON(w, http.StatusOK, result) } else { data, err := io.ReadAll(r.Body) if err != nil { @@ -277,6 +312,14 @@ func (p *Plugin) handlePluginByID(w http.ResponseWriter, r *http.Request) { // ======== Core Logic ======== +func (p *Plugin) installFromPath(path string) (interface{}, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("read file: %w", err) + } + return p.installFromData(data) +} + func (p *Plugin) installFromURL(rawURL string) (interface{}, error) { log.Printf("[pluginmgr] downloading: %s", rawURL) @@ -303,7 +346,16 @@ func (p *Plugin) installFromURL(rawURL string) (interface{}, error) { return nil, fmt.Errorf("read response: %w", err) } - return p.installFromData(data) + result, err := p.installFromData(data) + if err != nil { + return nil, err + } + + if m, ok := result.(map[string]interface{}); ok { + m["source"] = "url" + result = m + } + return result, nil } func (p *Plugin) installFromData(data []byte) (interface{}, error) { @@ -438,9 +490,10 @@ func (p *Plugin) pluginInfo(name string) (interface{}, error) { // ======== Package Validation ======== type pluginPackage struct { - Name string `json:"name"` - Version string `json:"version"` - Entry string `json:"entry"` + Name string `json:"name"` + Version string `json:"version"` + Entry string `json:"entry"` + Platforms []string `json:"platforms,omitempty"` } func validatePackage(data []byte) (*pluginPackage, error) { @@ -495,43 +548,82 @@ func validatePackage(data []byte) (*pluginPackage, error) { return nil, fmt.Errorf("manifest: entry required") } - hasEntry := false + // verify at least one valid binary exists for any platform + hasBinary := false + zipEntries := map[string]bool{} for _, f := range reader.File { - if f.Name == pkg.Entry && !f.FileInfo().IsDir() { - hasEntry = true - break + if !f.FileInfo().IsDir() { + zipEntries[f.Name] = true } } - if !hasEntry { - return nil, fmt.Errorf("entry %q not found in package", pkg.Entry) + + if len(pkg.Platforms) > 0 { + // bundle mode: check each declared platform has a matching binary + for _, plat := range pkg.Platforms { + bin, ok := map[string]string{ + "linux": "plugin.so", + "darwin": "plugin.dylib", + "windows": "plugin.dll", + }[plat] + if !ok { + return nil, fmt.Errorf("unsupported platform: %q", plat) + } + if zipEntries[bin] { + hasBinary = true + } + } + } else { + // legacy mode: check entry exists and is a known binary + if zipEntries[pkg.Entry] && validBinaries[pkg.Entry] { + hasBinary = true + } } - valid := map[string]bool{"plugin.so": true, "plugin.dll": true, "main.lua": true, "SKILL.md": true} - if !valid[pkg.Entry] { - return nil, fmt.Errorf("unsupported entry: %q", pkg.Entry) + if !hasBinary { + return nil, fmt.Errorf("no valid binary found in package (entry=%q, platforms=%v)", pkg.Entry, pkg.Platforms) } return &pkg, nil } +// copyZipEntry 解压 zip 中的单个文件到目标路径。 +func copyZipEntry(f *zip.File, dest string) error { + os.MkdirAll(filepath.Dir(dest), 0755) + rc, err := f.Open() + if err != nil { + return fmt.Errorf("open %s: %w", f.Name, err) + } + defer rc.Close() + out, err := os.OpenFile(dest, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) + if err != nil { + return fmt.Errorf("create %s: %w", dest, err) + } + defer out.Close() + _, err = io.Copy(out, rc) + return err +} + func extractPackage(data []byte, pluginDir string) error { reader, err := zip.NewReader(bytes.NewReader(data), int64(len(data))) if err != nil { return err } - // 先读 manifest 确定插件名 + // 先读 manifest 确定插件名和平台信息 var pkgName string + var declaredPlatforms []string for _, f := range reader.File { if f.Name == "plugin.json" && !f.FileInfo().IsDir() { rc, _ := f.Open() mData, _ := io.ReadAll(rc) rc.Close() var m struct { - Name string `json:"name"` + Name string `json:"name"` + Platforms []string `json:"platforms"` } json.Unmarshal(mData, &m) pkgName = m.Name + declaredPlatforms = m.Platforms break } } @@ -542,6 +634,9 @@ func extractPackage(data []byte, pluginDir string) error { target := filepath.Join(pluginDir, pkgName) os.MkdirAll(target, 0755) + zipBin, canonicalName := platformBinary() + isBundle := declaredPlatforms != nil + for _, f := range reader.File { fpath := filepath.Join(target, f.Name) if !strings.HasPrefix(filepath.Clean(fpath), filepath.Clean(target)+string(os.PathSeparator)) { @@ -551,19 +646,21 @@ func extractPackage(data []byte, pluginDir string) error { os.MkdirAll(fpath, 0755) continue } - os.MkdirAll(filepath.Dir(fpath), 0755) - rc, err := f.Open() - if err != nil { - return fmt.Errorf("open %s: %w", f.Name, err) + + // bundle mode: skip other platforms' binaries, keep only current OS + if isBundle && validBinaries[f.Name] && f.Name != zipBin { + continue } - out, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) - if err != nil { - rc.Close() - return fmt.Errorf("create %s: %w", f.Name, err) + + // rename platform binary to canonical name (e.g. plugin.dylib → plugin.so) + dest := fpath + if isBundle && f.Name == zipBin && canonicalName != zipBin { + dest = filepath.Join(target, canonicalName) + } + + if err := copyZipEntry(f, dest); err != nil { + return err } - io.Copy(out, rc) - rc.Close() - out.Close() } return nil diff --git a/package/build.sh b/package/build.sh index 4aac81f..4b759fa 100755 --- a/package/build.sh +++ b/package/build.sh @@ -13,18 +13,18 @@ TARGET="${1:-native}" COMPONENT="${2:-all}" # ---- platform matrix ---- -# homed: linux/amd64 + linux/arm64 (CGO), macOS native-only (no osxcross) -# windows: blocked — gojieba CXX flags + dlfcn.h not available in MinGW +# homed: linux/amd64 + linux/arm64 (CGO), macOS native-only (no osxcross), +# windows/amd64 (MinGW) # waiter: all platforms (CGO-free, raw terminal mode is a no-op on non-Linux) # gui: electron-builder handles cross-platform natively case "$TARGET" in native) GOOS="" GOARCH="" ;; linux/amd64) GOOS=linux GOARCH=amd64 CC="${CC:-}" ;; - linux/arm64) GOOS=linux GOARCH=arm64 CC="${CC:-aarch64-linux-gnu-gcc}" ;; + linux/arm64) GOOS=linux GOARCH=arm64 CC="${CC:-aarch64-linux-gnu-gcc}" CXX="${CXX:-aarch64-linux-gnu-g++}" ;; darwin/amd64) GOOS=darwin GOARCH=amd64 CC="${CC:-}" ;; darwin/arm64) GOOS=darwin GOARCH=arm64 CC="${CC:-}" ;; - windows/amd64) GOOS=windows GOARCH=amd64 CC="${CC:-x86_64-w64-mingw32-gcc}" ;; + windows/amd64) GOOS=windows GOARCH=amd64 CC="${CC:-x86_64-w64-mingw32-gcc}" CXX="${CXX:-x86_64-w64-mingw32-g++}" ;; all) "$0" linux/amd64 "$COMPONENT" "$0" linux/arm64 "$COMPONENT" @@ -47,6 +47,9 @@ fi if [ -n "${CC:-}" ]; then export CC fi +if [ -n "${CXX:-}" ]; then + export CXX +fi export CGO_ENABLED="${CGO_ENABLED:-1}" mkdir -p "$BUILD_DIR" @@ -61,11 +64,8 @@ build_homed() { return fi if [ "$GOOS" = "windows" ]; then - echo "[SKIP] homed ${plat} — gojieba CXX flags + dlfcn.h unavailable in MinGW cross-compiler" - return + out="${out}.exe" fi - - if [ "$GOOS" = "windows" ]; then out="${out}.exe"; fi echo "[BUILD] homed ${plat} → $out" CGO_ENABLED=1 "$GO" build -trimpath -installsuffix dynlink \ -ldflags "$LDFLAGS" -o "$out" ./cmd/homed/