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
|
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") {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue