fix(cli): use .zip archive for Windows in multica update (#1075)

GoReleaser produces .zip for Windows and .tar.gz for other platforms,
but the update command hardcoded .tar.gz for all platforms, causing a
404 error on Windows.

- Select .zip extension when runtime.GOOS is "windows"
- Add extractBinaryFromZip() for zip archive extraction
- Use "multica.exe" as the binary name on Windows

Closes #1072
This commit is contained in:
Bohan Jiang
2026-04-15 17:16:36 +08:00
committed by GitHub
parent f76e3fb8f4
commit 970b7fd1d3

View File

@@ -2,6 +2,8 @@ package cli
import (
"archive/tar"
"archive/zip"
"bytes"
"compress/gzip"
"encoding/json"
"fmt"
@@ -104,15 +106,20 @@ func UpdateViaDownload(targetVersion string) (string, error) {
return "", fmt.Errorf("resolve symlink: %w", err)
}
// Build download URL: multica_{os}_{arch}.tar.gz
// Build download URL: multica_{os}_{arch}.{tar.gz|zip}
// GoReleaser produces .zip for Windows and .tar.gz for everything else.
tag := targetVersion
if !strings.HasPrefix(tag, "v") {
tag = "v" + tag
}
assetName := fmt.Sprintf("multica_%s_%s.tar.gz", runtime.GOOS, runtime.GOARCH)
ext := "tar.gz"
if runtime.GOOS == "windows" {
ext = "zip"
}
assetName := fmt.Sprintf("multica_%s_%s.%s", runtime.GOOS, runtime.GOARCH, ext)
downloadURL := fmt.Sprintf("https://github.com/multica-ai/multica/releases/download/%s/%s", tag, assetName)
// Download the tarball.
// Download the archive.
client := &http.Client{Timeout: 120 * time.Second}
resp, err := client.Get(downloadURL)
if err != nil {
@@ -124,8 +131,17 @@ func UpdateViaDownload(targetVersion string) (string, error) {
return "", fmt.Errorf("download failed: HTTP %d from %s", resp.StatusCode, downloadURL)
}
// Extract the "multica" binary from the tarball.
binaryData, err := extractBinaryFromTarGz(resp.Body, "multica")
// Extract the binary from the archive.
binaryName := "multica"
if runtime.GOOS == "windows" {
binaryName = "multica.exe"
}
var binaryData []byte
if runtime.GOOS == "windows" {
binaryData, err = extractBinaryFromZip(resp.Body, binaryName)
} else {
binaryData, err = extractBinaryFromTarGz(resp.Body, binaryName)
}
if err != nil {
return "", fmt.Errorf("extract binary: %w", err)
}
@@ -194,3 +210,35 @@ func extractBinaryFromTarGz(r io.Reader, name string) ([]byte, error) {
}
}
// extractBinaryFromZip reads a .zip stream and returns the contents of the
// named file entry. The zip format requires random access, so the full archive
// is buffered in memory.
func extractBinaryFromZip(r io.Reader, name string) ([]byte, error) {
buf, err := io.ReadAll(r)
if err != nil {
return nil, fmt.Errorf("read zip data: %w", err)
}
zr, err := zip.NewReader(bytes.NewReader(buf), int64(len(buf)))
if err != nil {
return nil, fmt.Errorf("zip reader: %w", err)
}
for _, f := range zr.File {
if filepath.Base(f.Name) == name && !f.FileInfo().IsDir() {
rc, err := f.Open()
if err != nil {
return nil, fmt.Errorf("open zip entry: %w", err)
}
defer rc.Close()
data, err := io.ReadAll(rc)
if err != nil {
return nil, fmt.Errorf("read binary: %w", err)
}
return data, nil
}
}
return nil, fmt.Errorf("binary %q not found in archive", name)
}