Files
homeagent-sdk/tools/plugindev/cmd_init.go
JianFeeeee e256023399 docs+plugindev: README 同步子进程架构,scaffold 修 entry 与 go.sum 死路
三类问题,都会让新用户第一次上手就走错:

1. README 仍写 plugin.so
   plugin.json 示例的 entry、entry 字段说明、.hmap 包格式三处描述的都是
   已退场的 C ABI 产物。改为 plugin.bin,并补 bundle 模式下
   plugin.bin.<goos>.<goarch> 的命名与安装时挑平台的行为。

2. cmd_init.go 的 entry := "plugin.so"
   scaffold 出来的 plg.json 带着一个已退场的 entry 值。build 实际不看这个
   值(只用它区分 Lua),但跟着模板走会误以为自己在做 C ABI 插件。

3. 生成的项目第一次 build 必定失败
   go.mod 只 require 一个 gitcode 模块版本号且不生成 go.sum。gitcode 不在
   proxy.golang.org 上,于是:
     go build     → missing go.sum entry
     go mod tidy  → 去公共 proxy 拉一个不存在的条目,超时
   原来 cmdBuild 里那句 `go mod download <mod>` 走的正是这条死路,失败后
   只打一行 warn 就继续编译,紧接着死在同一个错误上——用户看到两段无关报错。

   修法分两处:
   - 生成的 go.mod 直接写指向本机 SDK 的 replace(replace 到目录时 go 不
     需要也不校验 go.sum)
   - 新增 ensureSDKResolvable:三级策略(已有本地 replace → 探测本机 SDK
     并写入 → 兜底 go mod tidy 带 -mod=mod,失败给可操作提示)。存量项目
     go.mod 无 replace 时走第二级救回。

bin/ 5 个预编译二进制不再进仓库(改为 release 附件):
5 个平台各 26-28MB,每次重编都在 git 历史里再叠一份,而它们本质是可从源码
复现的产物。README 的下载说明同步改为 release 附件 URL + 从源码编译。
2026-09-03 19:27:03 +08:00

296 lines
8.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
// SDKLocalPath 是本机 SDK 源码绝对路径,写入生成的 go.mod 作为 replace 目标。
//
// 为何必须写gitcode 的模块不在 proxy.golang.org 上,只 require 一个
// 版本号的 go.mod 配上缺失的 go.sum新用户第一次 `plugindev build`
// 必定死在 "missing go.sum entry",而 `go mod tidy` 又会去公共 proxy 拉
// 一个不存在的条目。有了本地 replacego 完全不需要 go.sum 条目。
SDKLocalPath 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)
}
// Go 插件统一产出 plugin.binv1.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 {
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 {
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)
}
}