refactor(toolchain)!: 工具链 plugindev 更名为 hmapdev,module path 改回 gitcode

- 目录 tools/plugindev → tools/hmapdev,可执行文件名/平台产物名同步
  (hmapdev_linux_amd64 等;包格式仍叫 .hmap)
- module path github.com/JianFeeeee/homeagent-sdk/tools/... → gitcode.com/...
  (与仓库实际托管一致;核心仓不依赖该 path,改动无外部影响)
- SDK 存储目录 ~/.homeagent/plugindev/sdk → ~/.homeagent/hmapdev/sdk
  新目录不存在而旧目录存在时沿用旧目录 → 已装 SDK 版本不会丢失
- 命令表/usage/--help/生成项目 README/示例 README/NSIS 安装器/
  package/build.sh/build-examples.sh 全部同步;PLUGINDEV 环境变量保留兼容
- sdk/ 目录零改动(公开接口不变)

验证:
- go build ./... ok;go test ./tools/hmapdev/ ok(含模板接线守卫 TestProcTemplate_CoversAllCoreMethods)
- bash -n package/{build,build-examples}.sh ok
- 端到端:hmapdev init demo && hmapdev build → dist/demo_bundle.hmap(linux+darwin)
- 本机安装 /usr/local/bin/hmapdev,旧名以软链保留;sdk list/current 正常
This commit is contained in:
JianFeeeee
2026-09-12 12:30:20 +08:00
parent 69ff3089a4
commit b237787c90
33 changed files with 135 additions and 122 deletions

295
tools/hmapdev/cmd_init.go Normal file
View File

@ -0,0 +1,295 @@
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新用户第一次 `hmapdev 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: hmapdev 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 && hmapdev 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)
}
}