diff --git a/.gitignore b/.gitignore index 6b2c52c..b01f7b3 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,11 @@ plugin.json build/ dist/ +# plugindev 预编译二进制:只作为 release 附件分发,不进仓库历史。 +# 此前 5 个平台各 26-28MB 被 git 跟踪(约 137MB),每次重编都在历史里 +# 再叠一份,而它们本质是可从源码复现的产物。 +bin/ + # Test artifacts testdist/ diff --git a/README.md b/README.md index 4f821d0..4568b88 100644 --- a/README.md +++ b/README.md @@ -142,13 +142,21 @@ func New(name string, sett SettingsAPI, regTool ToolRegistrar, regStage StageReg ## plugindev 工具链 -`plugindev` 提供插件开发全流程支持。仓库 `bin/` 提供各平台预制二进制(linux/darwin/windows × amd64/arm64),下载后直接加入 PATH 即可: +`plugindev` 提供插件开发全流程支持。预编译二进制作为 **release 附件**分发(linux/darwin/windows × amd64/arm64),从 +[Releases](https://gitcode.com/JianFeeeee/homeagent-sdk/releases) 下载后加入 PATH 即可: ```bash -curl -o plugindev https://gitcode.com/JianFeeeee/homeagent-sdk/-/raw/main/bin/plugindev_linux_amd64 +# 从 release 附件下载(以 v1.0.0 / linux amd64 为例) +curl -Lo plugindev https://gitcode.com/JianFeeeee/homeagent-sdk/releases/download/v1.0.0/plugindev_linux_amd64 chmod +x plugindev + +# 或从源码自己编 +cd tools/plugindev && go build -o plugindev . ``` +> 二进制不再随仓库分发(旧的 `bin/` 目录已停用):5 个平台各 26-28MB, +> 每次重编都在 git 历史里再叠一份,而它们本质是可从源码复现的产物。 + | 命令 | 说明 | |------|------| | `plugindev init [--lua]` | 初始化插件项目(生成 plg.json、plugin.go 或 main.lua、go.mod、README.md) | @@ -180,7 +188,7 @@ chmod +x plugindev "version": "1.0.0", "description": "天气查询插件", "author": "HomeAgent", - "entry": "plugin.so", + "entry": "plugin.bin", "tags": ["weather", "forecast"], "targets": "linux/amd64,windows/amd64", "outdir": "dist", @@ -202,7 +210,7 @@ chmod +x plugindev | `version` | string | 版本号 | | `description` | string | 插件描述 | | `author` | string | 作者 | -| `entry` | string | 入口文件(`plugin.so` / `plugin.dll` / `main.lua`) | +| `entry` | string | 入口文件(`plugin.bin` / `main.lua`)。v1.0.0 起 Go 插件统一为 `plugin.bin`,不再区分平台后缀 | | `tags` | string[] | 标签 | | `targets` | string | 构建目标,逗号分隔(如 `linux/amd64,windows/amd64`,Lua 插件为 `lua`) | | `outdir` | string | 输出目录(默认 `dist`) | @@ -217,11 +225,14 @@ chmod +x plugindev `.hmap` 为 ZIP 归档,包含: - `plugin.json` — 插件元数据 -- `plugin.so` — Go 编译产物(Linux) -- `plugin.dll` — Go 编译产物(Windows) -- `plugin.dylib` — Go 编译产物(macOS,bundle 模式) +- `plugin.bin` — Go 编译产物(单平台构建) +- `plugin.bin..` — 多平台 bundle 模式下每平台一份, + 安装时 pluginmgr 挑当前平台那份重命名为 `plugin.bin` - `main.lua` — Lua 插件入口(Lua 插件时) +> v1.0.0 起不再使用 `plugin.so`/`plugin.dll`/`plugin.dylib`——进程边界即 ABI 边界, +> 不存在平台特定的动态库区分。旧产物新内核不会加载,会给出明确的重编提示。 + ## 插件生命周期 ### 入口函数 diff --git a/README_EN.md b/README_EN.md index ba5acc5..c20dce6 100644 --- a/README_EN.md +++ b/README_EN.md @@ -142,14 +142,30 @@ Plugin developers only need to implement the `Plugin` interface and export a `Ne ## plugindev Toolchain -`plugindev` provides full development workflow support: +`plugindev` provides full development workflow support. Prebuilt binaries ship as **release assets** +(linux/darwin/windows × amd64/arm64); download from +[Releases](https://gitcode.com/JianFeeeee/homeagent-sdk/releases) and put it on your PATH: + +```bash +# From release assets (v1.0.0 / linux amd64 shown) +curl -Lo plugindev https://gitcode.com/JianFeeeee/homeagent-sdk/releases/download/v1.0.0/plugindev_linux_amd64 +chmod +x plugindev + +# Or build from source +cd tools/plugindev && go build -o plugindev . +``` + +> Binaries no longer ship inside the repository (the old `bin/` directory is retired): five +> platforms at 26-28MB each piled another copy into git history on every rebuild, and they are +> reproducible from source anyway. | Command | Description | |---------|-------------| -| `plugindev init` | Initialize plugin project (generates plg.json, entry template) | -| `plugindev build` | Build plugin, output .hmap package | -| `plugindev clean` | Clean build artifacts | -| `plugindev debug` | Run plugin in local debug mode | +| `plugindev init [--lua]` | Initialize plugin project (generates plg.json, plugin.go or main.lua, go.mod, README.md) | +| `plugindev build [flags]` | Build and package into a `.hmap` (supports cross-compilation and bundle mode) | +| `plugindev clean` | Clean `build/` and `dist/` plus generated files | +| `plugindev debug [dir]` | Load plugin source through the Yaegi Go interpreter and start an interactive REPL | +| `plugindev sdk ` | SDK version management (list/install/use/path/current/latest) | Supports both **Go** and **Lua** plugin languages. @@ -163,7 +179,7 @@ Supports both **Go** and **Lua** plugin languages. "version": "1.0.0", "description": "Weather plugin", "author": "HomeAgent", - "entry": "plugin.so", + "entry": "plugin.bin", "tags": ["weather", "forecast"], "targets": "linux/amd64,windows/amd64", "outdir": "dist", @@ -185,7 +201,7 @@ Supports both **Go** and **Lua** plugin languages. | `version` | string | Version | | `description` | string | Plugin description | | `author` | string | Author | -| `entry` | string | Entry file (`plugin.so` / `main.lua`) | +| `entry` | string | Entry file (`plugin.bin` / `main.lua`). Since v1.0.0 Go plugins uniformly build to `plugin.bin`—no per-platform suffix | | `tags` | string[] | Tags | | `targets` | string | Build targets, comma-separated (e.g. `linux/amd64,windows/amd64`) | | `outdir` | string | Output directory (default `dist`) | @@ -198,10 +214,15 @@ Supports both **Go** and **Lua** plugin languages. `.hmap` is a ZIP archive containing: - `plugin.json` — plugin metadata -- `plugin.so` — Go compiled artifact (Linux) -- `plugin.dll` — Go compiled artifact (Windows) +- `plugin.bin` — Go compiled artifact (single-platform build) +- `plugin.bin..` — one per platform in bundle mode; on install pluginmgr picks + the one matching the current platform and renames it to `plugin.bin` - `main.lua` — Lua plugin entry (for Lua plugins) +> Since v1.0.0 `plugin.so`/`plugin.dll`/`plugin.dylib` are no longer used—the process boundary +> *is* the ABI boundary, so there is no platform-specific shared-library distinction. The new +> kernel will not load old artifacts; it emits an explicit rebuild hint instead. + ## Plugin Lifecycle ### Start & Stop diff --git a/bin/plugindev_darwin_amd64 b/bin/plugindev_darwin_amd64 deleted file mode 100755 index e5b9264..0000000 Binary files a/bin/plugindev_darwin_amd64 and /dev/null differ diff --git a/bin/plugindev_darwin_arm64 b/bin/plugindev_darwin_arm64 deleted file mode 100755 index 7e17007..0000000 Binary files a/bin/plugindev_darwin_arm64 and /dev/null differ diff --git a/bin/plugindev_linux_amd64 b/bin/plugindev_linux_amd64 deleted file mode 100755 index 51274e0..0000000 Binary files a/bin/plugindev_linux_amd64 and /dev/null differ diff --git a/bin/plugindev_linux_arm64 b/bin/plugindev_linux_arm64 deleted file mode 100755 index 27a4cd9..0000000 Binary files a/bin/plugindev_linux_arm64 and /dev/null differ diff --git a/bin/plugindev_windows_amd64.exe b/bin/plugindev_windows_amd64.exe deleted file mode 100755 index a771468..0000000 Binary files a/bin/plugindev_windows_amd64.exe and /dev/null differ diff --git a/tools/plugindev/cmd_build.go b/tools/plugindev/cmd_build.go index 2e974de..5701c9b 100644 --- a/tools/plugindev/cmd_build.go +++ b/tools/plugindev/cmd_build.go @@ -73,18 +73,9 @@ func cmdBuild(args []string) { // Ensure go.mod exists with correct SDK path sdkModule := ensureGoMod(plg, sdkPath) - // First build: fetch the SDK module (generates go.sum with zip hash) + // 保证 SDK 模块可解析,否则编译必死在 "missing go.sum entry"。 if sdkModule != "" { - if _, err := os.Stat("go.sum"); os.IsNotExist(err) { - dl := exec.Command("go", "mod", "download", sdkModule) - dl.Env = os.Environ() - dl.Stdout = os.Stdout - dl.Stderr = os.Stderr - fmt.Println(" downloading SDK module deps...") - if err := dl.Run(); err != nil { - fmt.Printf(" error: go mod download: %v\n", err) - } - } + ensureSDKResolvable(plg, sdkModule, sdkPath) } // Merge plg.json replaces + CLI overrides @@ -343,6 +334,128 @@ func ensureGoMod(plg *PlgConfig, sdkPath string) string { return sdkModule } +// ensureSDKResolvable 保证 SDK 模块在编译前可解析。 +// +// 为何需要这个函数:gitcode 的模块不在 proxy.golang.org 上。只要 go.mod +// 里的 SDK 靠 require 版本号解析,而本地又没 go.sum 条目,go build 就报 +// "missing go.sum entry";而原来那句 `go mod download ` 会去公共 proxy +// 拉一个永远拉不到的条目,超时后只打一行 warn 就继继编译,紧接着死在 +// 同一个错误上——新用户拿到的是两段无关的报错。 +// +// 三级策略,按代价递增: +// 1. go.mod 已有指向本地目录的 replace —— 什么都不用做(replace 到目录时 +// go 不需要也不校验 go.sum)。 +// 2. 能定位到本机 SDK 源码 —— 写入 replace。这是存量项目(go.mod 旧、 +// 无 replace)的救场路径。 +// 3. 都不行 —— 跑 `go mod tidy`(带 -mod=mod)让它自己去试,失败则给 +// 可操作的提示而不是让用户去猜。 +func ensureSDKResolvable(plg *PlgConfig, sdkModule, sdkPath string) { + data, err := os.ReadFile("go.mod") + if err != nil { + return + } + + // 策略 1:已有指向本地目录的 replace。 + // replace 目标带 / 或 . 开头的才是路径;指向另一个模块的 replace 不算。 + for _, line := range strings.Split(string(data), "\n") { + line = strings.TrimSpace(line) + if !strings.HasPrefix(line, "replace ") || !strings.Contains(line, sdkModule) { + continue + } + parts := strings.Fields(line) + if len(parts) < 4 { + continue + } + target := parts[3] + if strings.HasPrefix(target, ".") || strings.HasPrefix(target, "/") || + strings.Contains(target, ":/") || strings.Contains(target, ":\\") { + return // 已指向本地目录,无需 go.sum + } + } + + // 策略 2:能定位到本机 SDK 就写 replace。 + // resolveSDKPath 失败会 os.Exit,所以只在能确定拿到路径时调用它背后的探测。 + if root := findLocalSDK(sdkPath); root != "" { + if appendGoModReplace(sdkModule, root) { + fmt.Printf(" SDK 指向本机源码(已写入 go.mod replace):%s\n", root) + return + } + } + + // 策略 3:交给 go mod tidy。 + if _, err := os.Stat("go.sum"); err == nil { + return // 已有 go.sum,不插手 + } + fmt.Println(" 解析 SDK 依赖(go mod tidy)...") + tidy := exec.Command("go", "mod", "tidy") + tidy.Env = append(os.Environ(), "GOFLAGS=-mod=mod") + if out, err := tidy.CombinedOutput(); err != nil { + fmt.Printf(" warn: go mod tidy 失败:%v\n", err) + if len(out) > 0 { + fmt.Printf(" %s\n", strings.TrimSpace(string(out))) + } + fmt.Printf(" 提示:%s 不在公共 proxy 上。用以下任一方式指向本机 SDK:\n", sdkModule) + fmt.Printf(" plugindev sdk install latest # 装一份到 ~/.homeagent/plugindev/sdk\n") + fmt.Printf(" plugindev build --sdk-path <路径> # 或直接指定源码目录\n") + } +} + +// findLocalSDK 探测本机 SDK 源码根目录,找不到返回空串。 +// +// 与 resolveSDKPath 的区别:后者找不到就 os.Exit,适合“必须有”的调用点; +// 这里是“有则更好”的探测,不能把构建搞挂。 +func findLocalSDK(sdkPath string) string { + candidates := []string{} + if sdkPath != "" { + if abs, err := filepath.Abs(sdkPath); err == nil { + candidates = append(candidates, abs) + } + } + // plugindev 自身所在位置往上三级(tools/plugindev/plugindev → SDK 根) + if self, err := os.Executable(); err == nil { + candidates = append(candidates, filepath.Dir(filepath.Dir(filepath.Dir(self)))) + } + // plugindev sdk use 选定的版本 + store := os.Getenv("HOMEAGENT_SDK_DIR") + if store == "" { + if home, err := os.UserHomeDir(); err == nil { + store = filepath.Join(home, ".homeagent", "plugindev", "sdk") + } + } + if store != "" { + if d, err := os.ReadFile(filepath.Join(store, "current")); err == nil { + if ver := strings.TrimSpace(string(d)); ver != "" { + candidates = append(candidates, filepath.Join(store, ver)) + } + } + } + for _, c := range candidates { + if c == "" { + continue + } + if _, err := os.Stat(filepath.Join(c, "sdk", "plugin.go")); err == nil { + return c + } + } + return "" +} + +// appendGoModReplace 向 go.mod 追加一条 replace,成功返回 true。 +func appendGoModReplace(module, localPath string) bool { + data, err := os.ReadFile("go.mod") + if err != nil { + return false + } + abs, err := filepath.Abs(localPath) + if err != nil { + return false + } + abs = strings.ReplaceAll(abs, "\\", "/") + s := strings.TrimRight(string(data), "\r\n") + s += fmt.Sprintf("\n\nreplace %s => %s\n", module, abs) + return os.WriteFile("go.mod", []byte(s), 0644) == nil +} + func resolveSDKPath(sdkPath string) string { if sdkPath != "" { abs, _ := filepath.Abs(sdkPath) @@ -465,8 +578,8 @@ func buildTarget(plg *PlgConfig, target, outDir, sdkPath string) { } type binEntry struct { - src string // 磁盘路径,如 build/plugin.so - zip string // zip 中条目名,如 plugin.so + src string // 磁盘路径,如 build/plugin.bin + zip string // zip 中条目名,如 plugin.bin.linux.amd64 } // createBundleHmap 创建包含多平台二进制的 bundle .hmap 文件。 diff --git a/tools/plugindev/cmd_init.go b/tools/plugindev/cmd_init.go index 5f03abe..4934955 100644 --- a/tools/plugindev/cmd_init.go +++ b/tools/plugindev/cmd_init.go @@ -59,6 +59,14 @@ type TemplateData struct { GoVersion string SDKModule string SDKVersion string + + // SDKLocalPath 是本机 SDK 源码绝对路径,写入生成的 go.mod 作为 replace 目标。 + // + // 为何必须写:gitcode 的模块不在 proxy.golang.org 上,只 require 一个 + // 版本号的 go.mod 配上缺失的 go.sum,新用户第一次 `plugindev build` + // 必定死在 "missing go.sum entry",而 `go mod tidy` 又会去公共 proxy 拉 + // 一个不存在的条目。有了本地 replace,go 完全不需要 go.sum 条目。 + SDKLocalPath string } func cmdInit(args []string) { @@ -109,7 +117,12 @@ func cmdInit(args []string) { os.Exit(1) } - entry := "plugin.so" + // Go 插件统一产出 plugin.bin(v1.0.0 子进程模式)。 + // + // 此前这里写 "plugin.so",scaffold 出来的 plg.json 就带着一个已退场的 + // entry 值,新手跟着模板走会误以为自己在做 C ABI 插件。 + // build 实际不看这个值(只用它区分 Lua),但模板不应误导。 + entry := "plugin.bin" var targets string if isLua { entry = "main.lua" @@ -137,14 +150,15 @@ func cmdInit(args []string) { } // Detect SDK info for Go plugin go.mod. - // 生成的 go.mod 只 require SDK 线上模块版本,不写本地路径 replace; - // 本地调试请用 `plugindev build --sdk-path ` 或手动加 replace。 + // 生成的 go.mod 除 require 外还写一条指向本机 SDK 的 replace: + // 否则 scaffold 出来的项目第一次 build 必定失败(详见 SDKLocalPath 注释)。 if !isLua { - sdkMod, goVer, _, sdkVer := detectSDKInfo() + sdkMod, goVer, sdkRoot, sdkVer := detectSDKInfo() data.ModulePath = name data.GoVersion = goVer data.SDKModule = sdkMod data.SDKVersion = "v" + sdkVer + data.SDKLocalPath = strings.ReplaceAll(sdkRoot, "\\", "/") } if err := os.MkdirAll(dir, 0755); err != nil { diff --git a/tools/plugindev/templates.go b/tools/plugindev/templates.go index bcc018c..ff8f97e 100644 --- a/tools/plugindev/templates.go +++ b/tools/plugindev/templates.go @@ -19,7 +19,12 @@ const tmplGoMod = `module {{.ModulePath}} go {{.GoVersion}} require {{.SDKModule}} {{.SDKVersion}} -` +{{if .SDKLocalPath}} +// SDK 指向本机源码。gitcode 的模块不在 proxy.golang.org 上, +// 没有这条 replace 就需要 go.sum 条目,而那个条目无处可拉。 +// 若你已有可访问的私有 proxy,可删掉本行。 +replace {{.SDKModule}} => {{.SDKLocalPath}} +{{end}}` const tmplPluginGo = `package main