mirror of
https://gitcode.com/JianFeeeee/homeagent-sdk.git
synced 2026-09-20 17:08:01 +00:00
- Plugin.Start(sdk *PluginSDK) 接口签名改为指针 - 方法表重写: 移除 CallLLM/QueryKnowledge/SetMemory 等不存在方法 - IOInjector 参数顺序修正为 (source, channel, text) - 删除虚构 SDKConfig, 替换为实际 New() 构造函数签名 - .hmap 内容描述一致化 (plugin.so + plugin.dll + main.lua) - 添加 meta/ 包元数据文件
416 lines
9.7 KiB
Go
416 lines
9.7 KiB
Go
package main
|
|
|
|
import (
|
|
"archive/zip"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
)
|
|
|
|
type BuildConfig struct {
|
|
OutDir string
|
|
Targets []string // "linux/amd64", "windows/amd64", "lua"
|
|
}
|
|
|
|
func cmdBuild(args []string) {
|
|
cfg := BuildConfig{OutDir: "dist"}
|
|
for i := 0; i < len(args); i++ {
|
|
switch args[i] {
|
|
case "--outdir":
|
|
if i+1 < len(args) {
|
|
cfg.OutDir = args[i+1]
|
|
i++
|
|
}
|
|
case "--target":
|
|
if i+1 < len(args) {
|
|
cfg.Targets = append(cfg.Targets, args[i+1])
|
|
i++
|
|
}
|
|
}
|
|
}
|
|
|
|
// read plg.json
|
|
plg, err := readPlgJSON("plg.json")
|
|
if err != nil {
|
|
fmt.Printf("error: read plg.json: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// determine targets
|
|
targets := cfg.Targets
|
|
if len(targets) == 0 {
|
|
targets = parseTargets(plg.Targets)
|
|
}
|
|
if len(targets) == 0 {
|
|
targets = []string{"native"}
|
|
}
|
|
|
|
// build for each target
|
|
for _, t := range targets {
|
|
buildTarget(plg, t, cfg.OutDir)
|
|
}
|
|
}
|
|
|
|
func readPlgJSON(path string) (*PlgConfig, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var plg PlgConfig
|
|
if err := json.Unmarshal(data, &plg); err != nil {
|
|
return nil, err
|
|
}
|
|
return &plg, nil
|
|
}
|
|
|
|
func parseTargets(raw string) []string {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" || raw == "native" {
|
|
return nil
|
|
}
|
|
var t []string
|
|
for _, s := range strings.Split(raw, ",") {
|
|
s = strings.TrimSpace(s)
|
|
if s != "" {
|
|
t = append(t, s)
|
|
}
|
|
}
|
|
return t
|
|
}
|
|
|
|
func writePluginJSON(plg *PlgConfig, entry string) {
|
|
m := map[string]interface{}{
|
|
"name": plg.Name,
|
|
"name_zh": plg.NameZh,
|
|
"name_en": plg.NameEn,
|
|
"version": plg.Version,
|
|
"description": plg.Description,
|
|
"author": plg.Author,
|
|
"entry": entry,
|
|
}
|
|
if len(plg.Tags) > 0 {
|
|
m["tags"] = plg.Tags
|
|
}
|
|
data, _ := json.MarshalIndent(m, "", " ")
|
|
os.WriteFile("plugin.json", data, 0644)
|
|
}
|
|
|
|
type buildConfig struct {
|
|
goos string
|
|
goarch string
|
|
entryFile string // "plugin.so" or "plugin.dll"
|
|
}
|
|
|
|
func resolveBuild(target string) (*buildConfig, string) {
|
|
if target == "lua" || target == "" {
|
|
return nil, "lua"
|
|
}
|
|
|
|
goos, goarch, _ := strings.Cut(target, "/")
|
|
if goos == "" {
|
|
goos = runtime.GOOS
|
|
if goarch == "" {
|
|
goarch = runtime.GOARCH
|
|
}
|
|
}
|
|
|
|
switch goos {
|
|
case "linux", "darwin", "freebsd", "windows":
|
|
ext := ".so"
|
|
if goos == "windows" {
|
|
ext = ".dll"
|
|
}
|
|
return &buildConfig{
|
|
goos: goos,
|
|
goarch: goarch,
|
|
entryFile: "plugin" + ext,
|
|
}, ""
|
|
default:
|
|
return nil, fmt.Sprintf("unsupported OS %q", goos)
|
|
}
|
|
}
|
|
|
|
func buildTarget(plg *PlgConfig, target, outDir string) {
|
|
os.MkdirAll(outDir, 0755)
|
|
|
|
// Lua: no compilation, package source directly
|
|
if target == "lua" {
|
|
writePluginJSON(plg, "main.lua")
|
|
pkgFiles := []string{"plugin.json", "main.lua"}
|
|
for _, f := range []string{"README.md", "LICENSE"} {
|
|
if _, err := os.Stat(f); err == nil {
|
|
pkgFiles = append(pkgFiles, f)
|
|
}
|
|
}
|
|
// Include thirdpart Lua files
|
|
if entries, err := os.ReadDir("thirdpart"); err == nil {
|
|
for _, e := range entries {
|
|
if !e.IsDir() && strings.HasSuffix(e.Name(), ".lua") {
|
|
path := filepath.Join("thirdpart", e.Name())
|
|
if _, err := os.Stat(path); err == nil {
|
|
pkgFiles = append(pkgFiles, path)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
hmapName := fmt.Sprintf("%s_lua.hmap", toSnake(plg.NameEn))
|
|
createHmap(filepath.Join(outDir, hmapName), pkgFiles)
|
|
fmt.Printf(" packaged %s\n", hmapName)
|
|
return
|
|
}
|
|
|
|
// Resolve build config
|
|
cfg, errMsg := resolveBuild(target)
|
|
if cfg == nil {
|
|
fmt.Printf(" error: %s\n", errMsg)
|
|
return
|
|
}
|
|
|
|
buildDir := "build"
|
|
os.MkdirAll(buildDir, 0755)
|
|
outPath := filepath.Join(buildDir, cfg.entryFile)
|
|
|
|
// Auto-generate C ABI bridge (all platforms use c-shared)
|
|
bridgeCleanup := generateBridge(cfg.goos)
|
|
_ = bridgeCleanup // DISABLED cleanup for debug
|
|
|
|
// Auto-link thirdpart/ contents
|
|
thirdpartCleanup := linkThirdpart(target)
|
|
defer thirdpartCleanup()
|
|
|
|
// Write plugin.json with the correct entry for this target
|
|
writePluginJSON(plg, cfg.entryFile)
|
|
|
|
cmd := exec.Command("go", "build", "-buildmode=c-shared", "-o", outPath)
|
|
cmd.Env = os.Environ()
|
|
cmd.Env = append(cmd.Env, "GOOS="+cfg.goos, "GOARCH="+cfg.goarch, "CGO_ENABLED=1")
|
|
|
|
// Auto-detect MinGW gcc on Windows
|
|
if cfg.goos == "windows" {
|
|
cc := detectWindowsCC()
|
|
if cc != "" {
|
|
cmd.Env = append(cmd.Env, "CC="+cc)
|
|
}
|
|
}
|
|
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
|
|
// DEBUG: list files before building
|
|
entries, _ := os.ReadDir(".")
|
|
for _, e := range entries {
|
|
fmt.Printf(" [DEBUG] file: %s\n", e.Name())
|
|
}
|
|
|
|
fmt.Printf(" compiling %s/%s (-buildmode=c-shared)...\n", cfg.goos, cfg.goarch)
|
|
if err := cmd.Run(); err != nil {
|
|
fmt.Printf(" error: build %s/%s: %v\n", cfg.goos, cfg.goarch, err)
|
|
return
|
|
}
|
|
|
|
// package
|
|
pkgFiles := []string{"plugin.json", outPath}
|
|
for _, f := range []string{"README.md", "LICENSE"} {
|
|
if _, err := os.Stat(f); err == nil {
|
|
pkgFiles = append(pkgFiles, f)
|
|
}
|
|
}
|
|
hmapName := fmt.Sprintf("%s_%s_%s.hmap", toSnake(plg.NameEn), cfg.goos, cfg.goarch)
|
|
createHmap(filepath.Join(outDir, hmapName), pkgFiles)
|
|
fmt.Printf(" packaged %s\n", hmapName)
|
|
}
|
|
|
|
func createHmap(hmapPath string, files []string) {
|
|
f, err := os.Create(hmapPath)
|
|
if err != nil {
|
|
fmt.Printf("error: create hmap %s: %v\n", hmapPath, err)
|
|
return
|
|
}
|
|
defer f.Close()
|
|
|
|
w := zip.NewWriter(f)
|
|
defer w.Close()
|
|
|
|
for _, path := range files {
|
|
if path == "" {
|
|
continue
|
|
}
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
hdr, err := zip.FileInfoHeader(info)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
hdr.Method = zip.Deflate
|
|
hdr.Name = filepath.Base(path)
|
|
writer, err := w.CreateHeader(hdr)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
src, err := os.Open(path)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
io.Copy(writer, src)
|
|
src.Close()
|
|
}
|
|
}
|
|
|
|
func toSnake(s string) string {
|
|
return strings.ToLower(strings.ReplaceAll(s, " ", "_"))
|
|
}
|
|
|
|
// detectWindowsCC looks for a MinGW-w64 gcc on Windows for c-shared builds.
|
|
func detectWindowsCC() string {
|
|
// Check CC from environment first
|
|
if cc := os.Getenv("CC"); cc != "" {
|
|
if _, err := exec.LookPath(cc); err == nil {
|
|
return cc
|
|
}
|
|
}
|
|
// Check common MinGW install paths
|
|
candidates := []string{
|
|
"C:\\mingw64\\bin\\gcc.exe",
|
|
"C:\\MinGW\\bin\\gcc.exe",
|
|
"C:\\msys64\\mingw64\\bin\\gcc.exe",
|
|
"C:\\Users\\21989\\AppData\\Local\\Temp\\mingw64\\mingw64\\bin\\gcc.exe",
|
|
}
|
|
// Also search PATH for gcc
|
|
if path, err := exec.LookPath("gcc"); err == nil {
|
|
return path
|
|
}
|
|
for _, c := range candidates {
|
|
if _, err := os.Stat(c); err == nil {
|
|
return c
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// stripIncludeGuard strips preprocessor guards and C++ comments from a C header,
|
|
// since these can confuse cgo's type resolution.
|
|
func stripIncludeGuard(header string) string {
|
|
lines := strings.Split(header, "\n")
|
|
var out []string
|
|
for _, line := range lines {
|
|
trimmed := strings.TrimSpace(line)
|
|
if trimmed == "#ifndef HOMEAGENT_CABI_H" || trimmed == "#define HOMEAGENT_CABI_H" {
|
|
continue
|
|
}
|
|
if trimmed == "#endif" || strings.HasPrefix(trimmed, "#endif") {
|
|
continue
|
|
}
|
|
if trimmed == "#ifdef __cplusplus" || trimmed == "extern \"C\" {" || trimmed == "}" {
|
|
continue
|
|
}
|
|
// Strip C++-style comments (cgo parser may not handle them in /* */ blocks)
|
|
if idx := strings.Index(line, "//"); idx >= 0 {
|
|
line = line[:idx]
|
|
}
|
|
cleaned := strings.TrimSpace(line)
|
|
if cleaned == "" {
|
|
continue
|
|
}
|
|
out = append(out, line)
|
|
}
|
|
return strings.Join(out, "\n")
|
|
}
|
|
|
|
// generateBridge generates the C ABI bridge files for non-Lua builds.
|
|
// Returns a cleanup function to remove generated files.
|
|
func generateBridge(goos string) func() {
|
|
const bridgeFile = "z_bridge_gen.go"
|
|
const cEntryFile = "z_entry.c"
|
|
os.Remove(bridgeFile)
|
|
os.Remove(cEntryFile)
|
|
|
|
var files []string
|
|
|
|
if goos == "windows" {
|
|
if err := os.WriteFile(bridgeFile, []byte(tmplBridge), 0644); err != nil {
|
|
fmt.Printf(" error: write bridge: %v\n", err)
|
|
return func() {}
|
|
}
|
|
files = append(files, bridgeFile)
|
|
} else {
|
|
if err := os.WriteFile(bridgeFile, []byte(tmplLinuxBridge), 0644); err != nil {
|
|
fmt.Printf(" error: write bridge: %v\n", err)
|
|
return func() {}
|
|
}
|
|
files = append(files, bridgeFile)
|
|
// Write C entry point file
|
|
if err := os.WriteFile(cEntryFile, []byte(tmplPluginInitC), 0644); err != nil {
|
|
fmt.Printf(" error: write C entry: %v\n", err)
|
|
return func() {}
|
|
}
|
|
files = append(files, cEntryFile)
|
|
}
|
|
|
|
return func() {
|
|
for _, f := range files {
|
|
os.Remove(f)
|
|
}
|
|
}
|
|
}
|
|
|
|
// linkThirdpart scans thirdpart/ for source files and generates auto-import stubs.
|
|
// For Go plugins: if thirdpart/*.go exists, generate z_thirdpart.go with import.
|
|
// For Lua plugins: no action needed (thirdpart/*.lua is packaged separately in buildTarget).
|
|
// Returns cleanup function to remove generated files.
|
|
func linkThirdpart(target string) func() {
|
|
const thirdpartDir = "thirdpart"
|
|
const importFile = "z_thirdpart.go"
|
|
os.Remove(importFile)
|
|
|
|
if info, err := os.Stat(thirdpartDir); err != nil || !info.IsDir() {
|
|
return func() {}
|
|
}
|
|
|
|
entries, err := os.ReadDir(thirdpartDir)
|
|
if err != nil {
|
|
return func() {}
|
|
}
|
|
|
|
// For Go builds: check for .go files
|
|
if target != "lua" {
|
|
hasGo := false
|
|
for _, e := range entries {
|
|
if !e.IsDir() && strings.HasSuffix(e.Name(), ".go") {
|
|
hasGo = true
|
|
break
|
|
}
|
|
}
|
|
if hasGo {
|
|
// Read go.mod to get the module path
|
|
gomodPath := "go.mod"
|
|
data, err := os.ReadFile(gomodPath)
|
|
if err != nil {
|
|
return func() {}
|
|
}
|
|
modulePath := ""
|
|
for _, line := range strings.Split(string(data), "\n") {
|
|
if strings.HasPrefix(line, "module ") {
|
|
modulePath = strings.TrimSpace(line[7:])
|
|
break
|
|
}
|
|
}
|
|
if modulePath != "" {
|
|
importPath := modulePath + "/" + thirdpartDir
|
|
stub := "package main\nimport _ \"" + importPath + "\"\n"
|
|
os.WriteFile(importFile, []byte(stub), 0644)
|
|
}
|
|
}
|
|
}
|
|
|
|
return func() {
|
|
os.Remove(importFile)
|
|
}
|
|
}
|