Files
homeagent-sdk/tools/hmapdev/sdk_resolve.go
JianFeeeee 9206353858 feat(hmapdev): 项目声明 SDK 版本,工具链据此自动选(plg.json 的 sdk 字段)
此前项目里没有任何「我要哪版 SDK」的声明:go.mod 的 require 是个 Go 模块版本,
而工具链实际用的是存储里的 current——谁改过 current 就拿谁的版本编,出错时
表现为莫名其妙的编译错误(本轮就踩过:存储里只有陈旧的 v0.8.0,模板项目
首次构建报 undefined: sdk.InjectOptions)。

- `plg.json` 新增 `sdk` 字段:本插件针对的 SDK 版本。`hmapdev init` 生成时写入
  **完整版本号**(如 "1.2.0")。
- `hmapdev build` 按声明的版本在本地 SDK 存储里定位:命中则用它并把 go.mod 的
  require/replace 同步到该版本;未命中则报**可执行**的错误(列出已装版本 +
  `hmapdev sdk install vX.Y.Z`),绝不静默退化成 current。
- **区间写法("1.2")被拒绝**并说明规矩:SDK 版本跟随内核中版本、patch 位恒为 .0,
  一条内核线只有一个 SDK 版本(写区间会让人误以为同一条线里还能挑不同 SDK)。
- 产物 `plugin.json` 记录实际选中的版本(`sdk`),便于追溯「这个 .hmap 是哪版编的」。
- 显式 `--sdk-path` / `plg.json sdk_path` 优先(本机改 SDK 联调的路径),此路径下也尽力记录版本。
- 存量项目(plg.json 无 sdk 字段)行为不变,向后兼容。

顺带修一处自相矛盾:解析出 1.2.0 之外的版本时,原先只改 replace 而 require 保持旧版本,
一旦有人删掉 replace 就会静默用回旧 SDK 编译(`go list -m` 报的也是假版本)。

验证:单测 10 例(精确命中/带 v 前缀目录/区间写法被拒并说明规矩/未命中给可执行命令/
空存储给安装指引/非法值拒绝/杂项目录不干扰)+ 反向验证(把版本比较退化成字典序,
「1.2.x 取最新」用例立刻变红,证明判据能发现缺陷)。
E2E:init → plg.json `"sdk": "1.2.0"`;build → 精确解析、go.mod require/replace 一致、
产物 plugin.json 记录 sdk;声明不存在的版本 → 可执行报错;无 sdk 字段 → 照旧构建。
2026-09-12 15:51:23 +08:00

136 lines
4.2 KiB
Go
Raw Permalink 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"
"strconv"
"strings"
)
// sdkVersionEntry 是本地 SDK 存储里的一个版本。
//
// Dir 是存储目录名(历史上有 "v0.8.0" 与 "1.2.0" 两种写法都出现过,所以
// 目录名与规范化版本号要分开存Version 是去掉 v 前缀的 x.y.z。
type sdkVersionEntry struct {
Dir string
Version string
}
// normalizeSDKVersion 去掉常见的前缀写法,得到 x.y.z。
func normalizeSDKVersion(v string) string {
return strings.TrimPrefix(strings.TrimSpace(v), "v")
}
// parseSDKVersion 解析 x.y.z / x.y后者补 0
func parseSDKVersion(v string) (maj, min, patch int, ok bool) {
v = normalizeSDKVersion(v)
parts := strings.Split(v, ".")
if len(parts) < 2 || len(parts) > 3 {
return 0, 0, 0, false
}
nums := make([]int, 0, 3)
for _, p := range parts {
n, err := strconv.Atoi(p)
if err != nil || n < 0 {
return 0, 0, 0, false
}
nums = append(nums, n)
}
for len(nums) < 3 {
nums = append(nums, 0)
}
return nums[0], nums[1], nums[2], true
}
// compareSDKVersion 比较两个 x.y.za<b 返回 -1相等 0a>b 返回 1
func compareSDKVersion(a, b string) int {
amaj, amin, apat, aok := parseSDKVersion(a)
bmaj, bmin, bpat, bok := parseSDKVersion(b)
if !aok || !bok {
return strings.Compare(normalizeSDKVersion(a), normalizeSDKVersion(b))
}
for _, d := range [][2]int{{amaj, bmaj}, {amin, bmin}, {apat, bpat}} {
switch {
case d[0] < d[1]:
return -1
case d[0] > d[1]:
return 1
}
}
return 0
}
// listInstalledSDKs 列出存储里已安装的 SDK按版本升序。
func listInstalledSDKs() []sdkVersionEntry {
store := sdkStore()
entries, err := os.ReadDir(store)
if err != nil {
return nil
}
var out []sdkVersionEntry
for _, e := range entries {
if !e.IsDir() {
continue
}
name := e.Name()
if name == "current" || strings.HasPrefix(name, ".") {
continue
}
v := normalizeSDKVersion(name)
if _, _, _, ok := parseSDKVersion(v); !ok {
continue // 非版本目录(用户放别的东西进去时不误判)
}
out = append(out, sdkVersionEntry{Dir: name, Version: v})
}
sort.Slice(out, func(i, j int) bool { return compareSDKVersion(out[i].Version, out[j].Version) < 0 })
return out
}
// ResolveSDKForProject 按项目声明的 SDK 版本plg.json 的 "sdk" 字段)在本地存储里定位 SDK。
//
// 声明必须是**完整版本号**x.y.z如 "1.2.0"SDK 的版本纪律是「跟随内核中版本,
// patch 位恒为 .0」(内核的 patch 不碰公开接口 → SDK 不跟版),所以一条内核线
// 只对应一个 SDK 版本号,写 "1.2" 这种区间写法既不必要、又容易让人以为
// 「同一条线里还能挑不同 SDK」。工具链直接拒它顺便把这条规矩说清楚。
//
// 找不到时必须报**可执行**的错误:列出已装版本 + 可直接粘贴的安装命令 ——
// 只说 "not found" 会让人以为是工具链坏了。
func ResolveSDKForProject(declared string) (dir, version string, err error) {
declared = normalizeSDKVersion(declared)
if strings.Count(declared, ".") != 2 {
return "", "", fmt.Errorf(
"plg.json 的 sdk 字段 %q 必须是完整版本号(如 \"1.2.0\")——\n"+
" SDK 版本跟随内核中版本、patch 位恒为 .0,一条内核线只有一个 SDK 版本", declared)
}
maj, min, pat, ok := parseSDKVersion(declared)
if !ok {
return "", "", fmt.Errorf("plg.json 的 sdk 字段 %q 不是合法版本号(写法:\"1.2.0\"", declared)
}
installed := listInstalledSDKs()
for i := range installed {
e := installed[i]
emaj, emin, epat, _ := parseSDKVersion(e.Version)
if emaj == maj && emin == min && epat == pat {
return filepath.Join(sdkStore(), e.Dir), e.Version, nil
}
}
// 未命中:给出可执行的下一步
var have []string
for _, e := range installed {
have = append(have, e.Version)
}
avail := "(存储里还没有任何 SDK"
if len(have) > 0 {
avail = "已安装:" + strings.Join(have, ", ")
}
return "", "", fmt.Errorf(
"项目声明 sdk=%s但本地 SDK 存储里没有这个版本;%s\n"+
" 安装hmapdev sdk install v%s\n"+
" 查看hmapdev sdk list",
declared, avail, declared)
}