mirror of
https://gitcode.com/JianFeeeee/homeagent-sdk.git
synced 2026-09-20 00:48:12 +00:00
## entry 不再是通道开关 —— 外部插件零改动的关键 17 个存量插件的 plg.json 都写着 "entry": "plugin.so"。若把 entry 当通道 开关,迁移就得改 17 个文件,而「外部插件零改动」是本次迁移的硬约束。 改法:Go 插件一律产出 plugin.bin,不看 entry 值。isProcEntry 删除, resolveBuild 去掉 proc 参数。entry 现在只剩区分 Lua(main.lua)一个用途。 实测:weather 的 plg.json 一行不改(仍写 plugin.so),plugindev build 直接产出三平台 plugin.bin。 ## Windows 不再是能力退化的第三套实现(§9.2 的正解) C ABI 时代 Windows 是独立的第三套 ABI:dynamic_dll_windows.go 的 stage 只下发 3 个字段(raw_message/user_id/phase)且完全没有写回,sanitizer 这类改写型插件在 Windows 上静默失效,且无任何运行时警告。 现在 Windows 与 Unix 共用同一份 RPC 逻辑与同一份共享段布局。平台差异 收敛到三个挂载函数: - Unix(linux/darwin/freebsd):内核经 ExtraFiles 传继承 fd(3=StageContext 段,4=事件环段,5=eventfd/pipe) - Windows:没有 fd 继承语义(os/exec 的 ExtraFiles 在 Windows 不支持), 改用命名内核对象——父进程 CreateFileMapping/CreateEvent 建带名字的对象, 子进程 OpenFileMappingW/OpenEventW 按同名打开。名字经环境变量传入而非 硬编码:多个 homed 实例并存时不能撞名。 Windows 绑定用 syscall.NewLazyDLL 而非 golang.org/x/sys/windows: OpenFileMappingW/OpenEventW 未被标准库 syscall 导出,而引入 x/sys 会给 **每个插件的 go.mod** 加一个新依赖,违反「插件仅依赖公开 SDK」。 LazyDLL 属标准库,零新增依赖。 新增 evtWaiter 接口抽象等待语义:eventfd 是计数器(多事件合并成一次 唤醒),Windows Event 是二元信号。不影响正确性——消费者被唤醒后按 readSeq 追 writeSeq 批量 drain,一次唤醒能处理累积的全部事件。 模板拆成三个文件: proc_main.go.tmpl 平台无关(RPC + 共享段布局 + stage + 事件环消费) proc_shm_unix.go.tmpl 继承 fd 挂载 proc_shm_windows.go.tmpl 命名对象挂载 ## 删除 C ABI 工具链 templates.go 1296 → 516 行: - tmplBridge(Windows DLL bridge) -265 行 - tmplLinuxBridge(Linux c-shared) -457 行 - tmplPluginInitC(C 入口) -57 行 另删 generateBridge / detectWindowsCC(MinGW 探测)/ tmplCABIHeader / InitData.CABIVersion+CABIHeader。 交叉编译不再需要目标平台 C 工具链——这是 -buildmode=c-shared 退场的 连带收益(§3.1)。 ## 测试 15 项全过,新增 4 项守护迁移不变量: - AllPlatformsProduceBin:6 个 GOOS/GOARCH 组合统一产出 plugin.bin - LuaIsSeparatePath:Lua 仍走解释器路径 - UnsupportedOSErrors:不支持平台明确报错,不静默产出错误产物 - NoCABIResiduals:代码中不得再出现 c-shared / CGO_ENABLED=1 / detectWindowsCC / tmplLinuxBridge / tmplPluginInitC(注释除外) - IgnoresEntryForGoPlugins:isProcEntry 必须已删除 验证:go build/vet/test 全通过;三平台交叉编译产出 plugin.bin; git diff sdk/ 为空(接口冻结)。 Ref: docs/zh/架构迁移评估.md §3.1/§9.2、docs/zh/plugin-migration-plan.md Part 6
282 lines
7.4 KiB
Go
282 lines
7.4 KiB
Go
package main
|
||
|
||
import (
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
"sort"
|
||
"strings"
|
||
"text/template"
|
||
)
|
||
|
||
func (p *PlgConfig) ReplacesToSlice() []string {
|
||
var s []string
|
||
for from, to := range p.Replaces {
|
||
s = append(s, from+"="+to)
|
||
}
|
||
sort.Strings(s) // deterministic order
|
||
return s
|
||
}
|
||
|
||
type PlgConfig struct {
|
||
Name string `json:"name"`
|
||
NameZh string `json:"name_zh"`
|
||
NameEn string `json:"name_en"`
|
||
Version string `json:"version"`
|
||
Description string `json:"description"`
|
||
Author string `json:"author"`
|
||
Entry string `json:"entry"`
|
||
Tags []string `json:"tags"`
|
||
Targets string `json:"targets"`
|
||
OutDir string `json:"outdir,omitempty"`
|
||
Bundle *bool `json:"bundle,omitempty"`
|
||
SDKPath string `json:"sdk_path,omitempty"`
|
||
GoVersion string `json:"go_version,omitempty"`
|
||
Replaces map[string]string `json:"replaces,omitempty"`
|
||
SourceDirs []string `json:"source_dirs,omitempty"`
|
||
}
|
||
|
||
// TargetList parses the Targets string into a slice.
|
||
func (p *PlgConfig) TargetList() []string { return parseTargets(p.Targets) }
|
||
|
||
// BundleDefault returns true if bundle mode is not explicitly disabled.
|
||
func (p *PlgConfig) BundleDefault() bool { return p.Bundle == nil || *p.Bundle }
|
||
|
||
// OutDirDefault returns the output directory, defaulting to "dist".
|
||
func (p *PlgConfig) OutDirDefault() string {
|
||
if p.OutDir != "" {
|
||
return p.OutDir
|
||
}
|
||
return "dist"
|
||
}
|
||
|
||
type TemplateData struct {
|
||
Plg PlgConfig
|
||
IsLua bool
|
||
|
||
// Go module info (for go.mod)
|
||
ModulePath string
|
||
GoVersion string
|
||
SDKModule string
|
||
SDKVersion string
|
||
}
|
||
|
||
func cmdInit(args []string) {
|
||
if len(args) < 1 {
|
||
fmt.Println("Usage: plugindev init <name> [--lua] [--type remotedevice]")
|
||
os.Exit(1)
|
||
}
|
||
|
||
name := args[0]
|
||
isLua := false
|
||
isRemoteDevice := false
|
||
for _, a := range args[1:] {
|
||
switch a {
|
||
case "--lua":
|
||
isLua = true
|
||
case "--type", "-t":
|
||
// handled in next iteration
|
||
}
|
||
}
|
||
// also check --type remotedevice as a single arg
|
||
for i, a := range args[1:] {
|
||
if a == "--type" || a == "-t" {
|
||
if i+1 < len(args[1:]) {
|
||
if args[1:][i+1] == "remotedevice" {
|
||
isRemoteDevice = true
|
||
}
|
||
}
|
||
}
|
||
if a == "--type=remotedevice" || a == "-t=remotedevice" {
|
||
isRemoteDevice = true
|
||
}
|
||
}
|
||
|
||
if isRemoteDevice && isLua {
|
||
fmt.Println("error: --type remotedevice and --lua are mutually exclusive")
|
||
os.Exit(1)
|
||
}
|
||
|
||
// Remote device projects use different scaffold
|
||
if isRemoteDevice {
|
||
scaffoldRemoteDevice(name)
|
||
return
|
||
}
|
||
|
||
dir := name
|
||
if _, err := os.Stat(dir); !os.IsNotExist(err) {
|
||
fmt.Printf("error: directory %q already exists\n", dir)
|
||
os.Exit(1)
|
||
}
|
||
|
||
entry := "plugin.so"
|
||
var targets string
|
||
if isLua {
|
||
entry = "main.lua"
|
||
targets = "lua"
|
||
} else {
|
||
targets = "linux/amd64,windows/amd64"
|
||
}
|
||
|
||
nameEn := strings.ReplaceAll(name, "-", " ")
|
||
nameEn = strings.Title(nameEn)
|
||
|
||
data := TemplateData{
|
||
Plg: PlgConfig{
|
||
Name: name,
|
||
NameZh: "中文名",
|
||
NameEn: nameEn,
|
||
Version: "0.1.0",
|
||
Description: name + " plugin",
|
||
Author: "HomeAgent",
|
||
Entry: entry,
|
||
Tags: []string{name},
|
||
Targets: targets,
|
||
},
|
||
IsLua: isLua,
|
||
}
|
||
|
||
// Detect SDK info for Go plugin go.mod.
|
||
// 生成的 go.mod 只 require SDK 线上模块版本,不写本地路径 replace;
|
||
// 本地调试请用 `plugindev build --sdk-path <path>` 或手动加 replace。
|
||
if !isLua {
|
||
sdkMod, goVer, _, sdkVer := detectSDKInfo()
|
||
data.ModulePath = name
|
||
data.GoVersion = goVer
|
||
data.SDKModule = sdkMod
|
||
data.SDKVersion = "v" + sdkVer
|
||
}
|
||
|
||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||
fmt.Printf("error: create dir: %v\n", err)
|
||
os.Exit(1)
|
||
}
|
||
|
||
// write plg.json
|
||
writeTemplate(filepath.Join(dir, "plg.json"), tmplPlgJSON, data)
|
||
|
||
// Lua plugins get main.lua + sdk.lua; Go plugins get plugin.go only
|
||
if isLua {
|
||
writeTemplate(filepath.Join(dir, "main.lua"), tmplMainLua, data)
|
||
writeTemplate(filepath.Join(dir, "sdk.lua"), tmplSDKLua, data)
|
||
} else {
|
||
writeTemplate(filepath.Join(dir, "plugin.go"), tmplPluginGo, data)
|
||
}
|
||
|
||
// write README.md
|
||
writeTemplate(filepath.Join(dir, "README.md"), tmplReadme, data)
|
||
|
||
// write go.mod for Go plugins
|
||
if !isLua {
|
||
writeTemplate(filepath.Join(dir, "go.mod"), tmplGoMod, data)
|
||
}
|
||
|
||
// create thirdpart directory for external library sources
|
||
os.MkdirAll(filepath.Join(dir, "thirdpart"), 0755)
|
||
|
||
fmt.Printf("Created plugin project %q (%s)\n", dir, entry)
|
||
if isLua {
|
||
fmt.Printf(" cd %s && lua main.lua (standalone test)\n", dir)
|
||
}
|
||
fmt.Printf(" cd %s && plugindev build\n", dir)
|
||
}
|
||
|
||
// detectSDKInfo reads the HomeAgent SDK's go.mod and meta to get module path, go version, and SDK version.
|
||
func detectSDKInfo() (modulePath, goVersion, sdkPath, sdkVersion string) {
|
||
root := activeSDKRoot()
|
||
gomodPath := filepath.Join(root, "go.mod")
|
||
data, err := os.ReadFile(gomodPath)
|
||
if err != nil {
|
||
fmt.Printf("error: cannot read SDK go.mod at %s: %v\n", gomodPath, err)
|
||
os.Exit(1)
|
||
}
|
||
|
||
for _, line := range strings.Split(string(data), "\n") {
|
||
line = strings.TrimSpace(line)
|
||
if strings.HasPrefix(line, "module ") {
|
||
modulePath = strings.TrimSpace(line[7:])
|
||
}
|
||
if strings.HasPrefix(line, "go ") {
|
||
goVersion = strings.TrimSpace(line[3:])
|
||
}
|
||
}
|
||
if modulePath == "" {
|
||
fmt.Printf("error: no module directive in %s\n", gomodPath)
|
||
os.Exit(1)
|
||
}
|
||
if goVersion == "" {
|
||
goVersion = "1.21"
|
||
}
|
||
sdkVersion = readMetaVersion(root)
|
||
return modulePath, goVersion, root, sdkVersion
|
||
}
|
||
|
||
// scaffoldRemoteDevice 创建远程设备适配器项目脚手架
|
||
func scaffoldRemoteDevice(name string) {
|
||
dir := name
|
||
if _, err := os.Stat(dir); !os.IsNotExist(err) {
|
||
fmt.Printf("error: directory %q already exists\n", dir)
|
||
os.Exit(1)
|
||
}
|
||
|
||
nameEn := strings.Title(strings.ReplaceAll(name, "-", " "))
|
||
|
||
data := TemplateData{
|
||
Plg: PlgConfig{
|
||
Name: name,
|
||
NameZh: "中文名",
|
||
NameEn: nameEn,
|
||
Version: "0.1.0",
|
||
Description: name + " remote device adapter",
|
||
Author: "HomeAgent",
|
||
Entry: name,
|
||
Tags: []string{name, "remotedevice"},
|
||
},
|
||
}
|
||
|
||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||
fmt.Printf("error: create dir: %v\n", err)
|
||
os.Exit(1)
|
||
}
|
||
|
||
// 写入 main.c
|
||
writeTemplate(filepath.Join(dir, "main.c"), tmplRemoteDeviceMain, data)
|
||
|
||
// 写入 CMakeLists.txt
|
||
writeTemplate(filepath.Join(dir, "CMakeLists.txt"), tmplRemoteDeviceCMake, data)
|
||
|
||
// 创建 SDK 目录(symlink/copy)
|
||
sdkSrc := filepath.Join("..", "remotedevice")
|
||
sdkDst := filepath.Join(dir, "ha_remotedevice")
|
||
if _, err := os.Stat(sdkDst); os.IsNotExist(err) {
|
||
// 尝试创建符号链接,失败则提示
|
||
if err := os.Symlink(sdkSrc, sdkDst); err != nil {
|
||
fmt.Printf(" note: could not create symlink to SDK, copy manually:\n")
|
||
fmt.Printf(" cp -r %s %s\n", sdkSrc, sdkDst)
|
||
}
|
||
}
|
||
|
||
fmt.Printf("Created remote device adapter project %q\n", dir)
|
||
fmt.Printf(" cd %s && mkdir build && cd build && cmake .. && make\n", dir)
|
||
fmt.Printf(" Or include as subdirectory in your project:\n")
|
||
fmt.Printf(" add_subdirectory(%s)\n", dir)
|
||
}
|
||
|
||
func writeTemplate(path, content string, data TemplateData) {
|
||
tmpl, err := template.New("").Parse(content)
|
||
if err != nil {
|
||
fmt.Printf("error: parse template: %v\n", err)
|
||
os.Exit(1)
|
||
}
|
||
f, err := os.Create(path)
|
||
if err != nil {
|
||
fmt.Printf("error: create %s: %v\n", path, err)
|
||
os.Exit(1)
|
||
}
|
||
defer f.Close()
|
||
if err := tmpl.Execute(f, data); err != nil {
|
||
fmt.Printf("error: execute template: %v\n", err)
|
||
os.Exit(1)
|
||
}
|
||
}
|