feat(upgrade): cli $0 update work well!
This commit is contained in:
parent
ffd430285b
commit
0b4c26c1db
1 changed files with 469 additions and 329 deletions
|
|
@ -32,8 +32,27 @@ func DownloadAndExtractRelease(releaseURL, platform, arch string) (string, error
|
|||
return "", err
|
||||
}
|
||||
|
||||
// Download asset to temp file
|
||||
tmpFile, err := os.CreateTemp("", "picoclaw-release-*.archive")
|
||||
// Download asset to temp file. Use the asset URL extension so
|
||||
// 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 {
|
||||
return "", err
|
||||
}
|
||||
|
|
@ -119,8 +138,9 @@ func UpdateSelfFromRelease(releaseURL, platform, arch, programName string) error
|
|||
// UpdateSelf updates the running executable by fetching the latest release
|
||||
// and applying the binary matching programName.
|
||||
func UpdateSelf(programName string) error {
|
||||
// Use production repo by default.
|
||||
return UpdateSelfFromRelease(GetProdReleaseAPIURL(), runtime.GOOS, runtime.GOARCH, programName)
|
||||
// By default, let findAssetURL select the nightly build when no explicit
|
||||
// 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.
|
||||
|
|
@ -134,6 +154,17 @@ func GetProdReleaseAPIURL() string {
|
|||
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
|
||||
// selector. It accepts direct archive URLs as well as GitHub release URLs
|
||||
// or empty (latest release for the project).
|
||||
|
|
@ -144,9 +175,15 @@ func findAssetURL(releaseURL, platform, arch string) (string, error) {
|
|||
|
||||
apiURL := buildReleaseAPIURL(releaseURL)
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := http.Get(apiURL)
|
||||
if err != nil {
|
||||
|
|
@ -168,27 +205,104 @@ func findAssetURL(releaseURL, platform, arch string) (string, error) {
|
|||
return "", err
|
||||
}
|
||||
|
||||
// prefer exact platform+arch match
|
||||
var fallback string
|
||||
for _, a := range data.Assets {
|
||||
// Selection order: platform -> arch -> extension.
|
||||
platformLower := strings.ToLower(platform)
|
||||
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)
|
||||
if platform != "" && arch != "" {
|
||||
if strings.Contains(n, strings.ToLower(platform)) && strings.Contains(n, strings.ToLower(arch)) {
|
||||
return a.BrowserDownloadURL, nil
|
||||
}
|
||||
}
|
||||
if platform != "" && strings.Contains(n, strings.ToLower(platform)) {
|
||||
if fallback == "" {
|
||||
fallback = a.BrowserDownloadURL
|
||||
}
|
||||
}
|
||||
if fallback == "" {
|
||||
fallback = a.BrowserDownloadURL
|
||||
if platform == "" || strings.Contains(n, platformLower) {
|
||||
platformIdx = append(platformIdx, i)
|
||||
}
|
||||
}
|
||||
|
||||
if fallback != "" {
|
||||
return fallback, nil
|
||||
pickBest := func(idxs []int) (string, bool) {
|
||||
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")
|
||||
}
|
||||
|
|
@ -236,6 +350,32 @@ func buildReleaseAPIURL(releaseURL string) string {
|
|||
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 {
|
||||
lower := strings.ToLower(archivePath)
|
||||
if strings.HasSuffix(lower, ".zip") {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue