feat(upgrade): cli $0 update work well!

This commit is contained in:
sky5454 2026-03-31 04:27:33 +08:00
parent ffd430285b
commit 0b4c26c1db

View file

@ -32,8 +32,27 @@ func DownloadAndExtractRelease(releaseURL, platform, arch string) (string, error
return "", err return "", err
} }
// Download asset to temp file // Download asset to temp file. Use the asset URL extension so
tmpFile, err := os.CreateTemp("", "picoclaw-release-*.archive") // extractArchive can detect the archive format (zip/tar.gz/tar).
tmpPattern := "picoclaw-release-*"
if u, perr := url.Parse(assetURL); perr == nil {
base := filepath.Base(u.Path)
lbase := strings.ToLower(base)
switch {
case strings.HasSuffix(lbase, ".zip"):
tmpPattern += ".zip"
case strings.HasSuffix(lbase, ".tar.gz") || strings.HasSuffix(lbase, ".tgz"):
tmpPattern += ".tar.gz"
case strings.HasSuffix(lbase, ".tar"):
tmpPattern += ".tar"
default:
tmpPattern += ".archive"
}
} else {
tmpPattern += ".archive"
}
tmpFile, err := os.CreateTemp("", tmpPattern)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -119,8 +138,9 @@ func UpdateSelfFromRelease(releaseURL, platform, arch, programName string) error
// UpdateSelf updates the running executable by fetching the latest release // UpdateSelf updates the running executable by fetching the latest release
// and applying the binary matching programName. // and applying the binary matching programName.
func UpdateSelf(programName string) error { func UpdateSelf(programName string) error {
// Use production repo by default. // By default, let findAssetURL select the nightly build when no explicit
return UpdateSelfFromRelease(GetProdReleaseAPIURL(), runtime.GOOS, runtime.GOARCH, programName) // release URL is provided. Passing an empty releaseURL triggers that path.
return UpdateSelfFromRelease("", runtime.GOOS, runtime.GOARCH, programName)
} }
// GetReleaseAPIURL returns the GitHub Releases API URL for the given repo owner. // GetReleaseAPIURL returns the GitHub Releases API URL for the given repo owner.
@ -134,6 +154,17 @@ func GetProdReleaseAPIURL() string {
return GetReleaseAPIURL("sipeed") return GetReleaseAPIURL("sipeed")
} }
// GetReleaseTagAPIURL returns the GitHub Releases API URL for a specific tag.
// Example: owner="sipeed", tag="nightly" -> https://api.github.com/repos/sipeed/picoclaw/releases/tags/nightly
func GetReleaseTagAPIURL(owner, tag string) string {
return fmt.Sprintf("https://api.github.com/repos/%s/picoclaw/releases/tags/%s", owner, tag)
}
// GetNightlyReleaseAPIURL returns the nightly release API URL for the production repo.
func GetNightlyReleaseAPIURL() string {
return GetReleaseTagAPIURL("sipeed", "nightly")
}
// findAssetURL resolves the appropriate asset URL for the given release // findAssetURL resolves the appropriate asset URL for the given release
// selector. It accepts direct archive URLs as well as GitHub release URLs // selector. It accepts direct archive URLs as well as GitHub release URLs
// or empty (latest release for the project). // or empty (latest release for the project).
@ -144,9 +175,15 @@ func findAssetURL(releaseURL, platform, arch string) (string, error) {
apiURL := buildReleaseAPIURL(releaseURL) apiURL := buildReleaseAPIURL(releaseURL)
if apiURL == "" { if apiURL == "" {
// Default to production repo API URL when no explicit release URL is provided. // If caller provided an empty releaseURL, default to the nightly tag
// from the production repo. Otherwise fall back to the production
// latest release API URL.
if strings.TrimSpace(releaseURL) == "" {
apiURL = GetNightlyReleaseAPIURL()
} else {
apiURL = GetProdReleaseAPIURL() apiURL = GetProdReleaseAPIURL()
} }
}
resp, err := http.Get(apiURL) resp, err := http.Get(apiURL)
if err != nil { if err != nil {
@ -168,27 +205,104 @@ func findAssetURL(releaseURL, platform, arch string) (string, error) {
return "", err return "", err
} }
// prefer exact platform+arch match // Selection order: platform -> arch -> extension.
var fallback string platformLower := strings.ToLower(platform)
for _, a := range data.Assets { archLower := strings.ToLower(arch)
isZip := func(name string) bool {
return strings.HasSuffix(name, ".zip")
}
isTarGz := func(name string) bool {
return strings.HasSuffix(name, ".tar.gz") || strings.HasSuffix(name, ".tgz")
}
isTar := func(name string) bool { return strings.HasSuffix(name, ".tar") }
// collect indices of assets that contain platform (if provided)
var platformIdx []int
for i, a := range data.Assets {
n := strings.ToLower(a.Name) n := strings.ToLower(a.Name)
if platform != "" && arch != "" { if platform == "" || strings.Contains(n, platformLower) {
if strings.Contains(n, strings.ToLower(platform)) && strings.Contains(n, strings.ToLower(arch)) { platformIdx = append(platformIdx, i)
return a.BrowserDownloadURL, nil
}
}
if platform != "" && strings.Contains(n, strings.ToLower(platform)) {
if fallback == "" {
fallback = a.BrowserDownloadURL
}
}
if fallback == "" {
fallback = a.BrowserDownloadURL
} }
} }
if fallback != "" { pickBest := func(idxs []int) (string, bool) {
return fallback, nil if len(idxs) == 0 {
return "", false
}
// prefer arch matches within idxs
var archIdx []int
if arch != "" {
aliases := archAliases(archLower)
for _, i := range idxs {
n := strings.ToLower(data.Assets[i].Name)
for _, ali := range aliases {
if strings.Contains(n, ali) {
archIdx = append(archIdx, i)
break
}
}
}
}
candidates := archIdx
if len(candidates) == 0 {
candidates = idxs
}
// extension preference
if platformLower == "windows" {
// prefer .zip only
for _, i := range candidates {
if isZip(strings.ToLower(data.Assets[i].Name)) {
return data.Assets[i].BrowserDownloadURL, true
}
}
// if no zip found, fallthrough to first candidate
return data.Assets[candidates[0]].BrowserDownloadURL, true
}
// non-windows: prefer tar.gz/tgz, then tar, then zip
for _, i := range candidates {
if isTarGz(strings.ToLower(data.Assets[i].Name)) {
return data.Assets[i].BrowserDownloadURL, true
}
}
for _, i := range candidates {
if isTar(strings.ToLower(data.Assets[i].Name)) {
return data.Assets[i].BrowserDownloadURL, true
}
}
for _, i := range candidates {
if isZip(strings.ToLower(data.Assets[i].Name)) {
return data.Assets[i].BrowserDownloadURL, true
}
}
// fallback to first candidate
return data.Assets[candidates[0]].BrowserDownloadURL, true
}
// Try platform matches first
if url, ok := pickBest(platformIdx); ok {
return url, nil
}
// If no platform matches, try arch-only matches
if arch != "" {
var archOnlyIdx []int
for i, a := range data.Assets {
n := strings.ToLower(a.Name)
if strings.Contains(n, archLower) {
archOnlyIdx = append(archOnlyIdx, i)
}
}
if url, ok := pickBest(archOnlyIdx); ok {
return url, nil
}
}
// Fallback to first asset
if len(data.Assets) > 0 {
return data.Assets[0].BrowserDownloadURL, nil
} }
return "", errors.New("no suitable release asset found") return "", errors.New("no suitable release asset found")
} }
@ -236,6 +350,32 @@ func buildReleaseAPIURL(releaseURL string) string {
return fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", owner, repo) return fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", owner, repo)
} }
// archAliases returns common name variants for an architecture string
// so we can match release asset names like "x86_64" vs Go's "amd64".
// archAliases returns name variants for an architecture string.
// If `arch` is empty or matches the local runtime.GOARCH, prefer the
// compile-time architecture aliases provided by archAliasesForLocal
// (implemented per-architecture via build tags). For other `arch`
// values we use a small synonyms map.
func archAliases(arch string) []string {
a := strings.ToLower(arch)
if syns, ok := archSynonyms[a]; ok {
return syns
}
return []string{a}
}
var archSynonyms = map[string][]string{
"amd64": {"amd64", "x86_64", "x64"},
"x86_64": {"amd64", "x86_64", "x64"},
"x64": {"amd64", "x86_64", "x64"},
"386": {"386", "x86"},
"x86": {"386", "x86"},
"arm64": {"arm64", "aarch64"},
"aarch64": {"arm64", "aarch64"},
"arm": {"arm"},
}
func extractArchive(archivePath, destDir string) error { func extractArchive(archivePath, destDir string) error {
lower := strings.ToLower(archivePath) lower := strings.ToLower(archivePath)
if strings.HasSuffix(lower, ".zip") { if strings.HasSuffix(lower, ".zip") {