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"` // SDK 声明本插件针对的 SDK **接口版本**(中版本或完整版本,如 "1.2" / "1.2.1")。 // // 为何需要:工具链存储里可能装有多个 SDK 版本,而插件产物与内核是协议绑定的—— // 不给声明就只能猜(旧行为是直接用 current:谁改过 current 就拿谁的版本编, // 出错时表现为莫名其妙的编译错误)。写中版本表示「只要 1.2 这条接口线, // 补丁由工具链挑最新」(patch 只含工具链/打包修复,接口不变,见 README 版本语义)。 SDK string `json:"sdk,omitempty"` // ResolvedSDK 是本次构建实际选中的 SDK 版本(build 按 SDK 声明解析后回填), // 只写进产物里的 plugin.json,便于事后追溯「这个 .hmap 是哪版 SDK 编的」。 ResolvedSDK string `json:"-"` } // 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 // SDKLocalPath 是本机 SDK 源码绝对路径,写入生成的 go.mod 作为 replace 目标。 // // 为何必须写:gitcode 的模块不在 proxy.golang.org 上,只 require 一个 // 版本号的 go.mod 配上缺失的 go.sum,新用户第一次 `hmapdev build` // 必定死在 "missing go.sum entry",而 `go mod tidy` 又会去公共 proxy 拉 // 一个不存在的条目。有了本地 replace,go 完全不需要 go.sum 条目。 SDKLocalPath string } func cmdInit(args []string) { if len(args) < 1 { fmt.Println("Usage: hmapdev init [--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) } // 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" 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: // 否则 scaffold 出来的项目第一次 build 必定失败(详见 SDKLocalPath 注释)。 if isLua { // Lua 插件也要记录它按哪版 SDK 语义编写:Lua `sdk.*` 是公开契约, // 与内核能力版本挂钩;不写版本就只能靠“调用时才发现是 nil”。 if root := tryActiveSDKRoot(); root != "" { data.Plg.SDK = normalizeSDKVersion(readMetaVersion(root)) } } else { sdkMod, goVer, sdkRoot, sdkVer := detectSDKInfo() data.ModulePath = name data.GoVersion = goVer data.SDKModule = sdkMod data.SDKVersion = "v" + sdkVer data.SDKLocalPath = strings.ReplaceAll(sdkRoot, "\\", "/") // 声明**完整版本号**:SDK 版本跟随内核中版本、patch 位恒为 .0, // 一条内核线只对应一个 SDK 版本(build 时按此解析,见 ResolveSDKForProject)。 data.Plg.SDK = normalizeSDKVersion(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) // sdk.lua 是给 `lua main.lua` 离线测试用的 mock,单一事实源在 SDK 仓的 // sdk/lua/sdk.lua;优先从当前激活的 SDK 拷,拷不到才回退内嵌模板。 if !copyCanonicalLuaSDK(dir) { if err := os.WriteFile(filepath.Join(dir, "sdk.lua"), []byte(fallbackLuaSDK), 0644); err != nil { fmt.Printf("error: write sdk.lua: %v\n", err) os.Exit(1) } } } 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 && hmapdev build\n", dir) } // activeSDKRoot 返回当前激活 SDK 的根目录,未安装/未激活则报错退出。 // // 与 activeSDKRoot(fatal 版)区别:这里只探测,不退出。 // Lua 插件的 mock 是“锦上添花”,没装 SDK 不应该阻断 init。 func tryActiveSDKRoot() string { store := sdkStore() current := resolveCurrentVersion(store) if current == "" { return "" } root := sdkVersionDir(current) if _, err := os.Stat(root); err != nil { return "" } return root } // copyCanonicalLuaSDK 把激活 SDK 的 sdk/lua/sdk.lua 拷进新项目。 // 三份 sdk.lua(内核内嵌 / 工具链模板 / 项目副本)各自漂移是本工具链的历史债, // 单一事实源在 SDK 仓,工具链只负责搬运。返回是否成功。 func copyCanonicalLuaSDK(dir string) bool { root := tryActiveSDKRoot() if root == "" { return false } src := filepath.Join(root, "sdk", "lua", "sdk.lua") data, err := os.ReadFile(src) if err != nil { return false } if err := os.WriteFile(filepath.Join(dir, "sdk.lua"), data, 0644); err != nil { return false } fmt.Printf(" sdk.lua <- %s\n", src) return true } // 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) } }