143 lines
3.9 KiB
Go
143 lines
3.9 KiB
Go
package installer
|
|
|
|
import (
|
|
"compress/gzip"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"github.com/kandev/kandev/internal/common/logger"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// GithubReleaseConfig configures a GitHub release download.
|
|
type GithubReleaseConfig struct {
|
|
Owner string // e.g. "rust-lang"
|
|
Repo string // e.g. "rust-analyzer"
|
|
AssetPattern string // e.g. "rust-analyzer-{target}.gz"
|
|
Targets map[string]string // "darwin/arm64" -> "aarch64-apple-darwin"
|
|
}
|
|
|
|
// GithubReleaseStrategy downloads prebuilt binaries from GitHub releases.
|
|
type GithubReleaseStrategy struct {
|
|
binDir string
|
|
binary string // "rust-analyzer"
|
|
config GithubReleaseConfig
|
|
logger *logger.Logger
|
|
}
|
|
|
|
// NewGithubReleaseStrategy creates a new GitHub release download strategy.
|
|
func NewGithubReleaseStrategy(binDir, binary string, config GithubReleaseConfig, log *logger.Logger) *GithubReleaseStrategy {
|
|
return &GithubReleaseStrategy{
|
|
binDir: binDir,
|
|
binary: binary,
|
|
config: config,
|
|
logger: log,
|
|
}
|
|
}
|
|
|
|
func (s *GithubReleaseStrategy) Name() string {
|
|
return fmt.Sprintf("github release %s/%s", s.config.Owner, s.config.Repo)
|
|
}
|
|
|
|
func (s *GithubReleaseStrategy) Install(ctx context.Context) (*InstallResult, error) {
|
|
// Resolve target from runtime OS/ARCH
|
|
targetKey := runtime.GOOS + "/" + runtime.GOARCH
|
|
target, ok := s.config.Targets[targetKey]
|
|
if !ok {
|
|
return nil, fmt.Errorf("unsupported platform: %s", targetKey)
|
|
}
|
|
|
|
// Build download URL
|
|
asset := strings.Replace(s.config.AssetPattern, "{target}", target, 1)
|
|
url := fmt.Sprintf("https://github.com/%s/%s/releases/latest/download/%s",
|
|
s.config.Owner, s.config.Repo, asset)
|
|
|
|
s.logger.Info("downloading from GitHub releases",
|
|
zap.String("url", url),
|
|
zap.String("target", target))
|
|
|
|
// Download
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
|
}
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("download failed: %w", err)
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("download failed with status %d", resp.StatusCode)
|
|
}
|
|
|
|
// Ensure bin directory exists
|
|
if err := os.MkdirAll(s.binDir, 0o755); err != nil {
|
|
return nil, fmt.Errorf("failed to create directory %s: %w", s.binDir, err)
|
|
}
|
|
|
|
outPath := filepath.Join(s.binDir, s.binary)
|
|
|
|
// Decompress .gz or write directly
|
|
if strings.HasSuffix(asset, ".gz") {
|
|
if err := WriteGzipBody(resp.Body, outPath); err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
if err := WriteBody(resp.Body, outPath); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Make executable
|
|
if err := os.Chmod(outPath, 0o755); err != nil {
|
|
return nil, fmt.Errorf("failed to chmod: %w", err)
|
|
}
|
|
|
|
s.logger.Info("GitHub release download completed", zap.String("binary", outPath))
|
|
return &InstallResult{
|
|
BinaryPath: outPath,
|
|
}, nil
|
|
}
|
|
|
|
// WriteGzipBody decompresses a gzip reader into outPath.
|
|
func WriteGzipBody(body io.Reader, outPath string) error {
|
|
gzReader, err := gzip.NewReader(body)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create gzip reader: %w", err)
|
|
}
|
|
defer func() { _ = gzReader.Close() }()
|
|
|
|
outFile, err := os.Create(outPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create output file: %w", err)
|
|
}
|
|
defer func() { _ = outFile.Close() }()
|
|
|
|
if _, err := io.Copy(outFile, gzReader); err != nil {
|
|
return fmt.Errorf("failed to decompress: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// WriteBody writes a plain reader into outPath.
|
|
func WriteBody(body io.Reader, outPath string) error {
|
|
outFile, err := os.Create(outPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create output file: %w", err)
|
|
}
|
|
defer func() { _ = outFile.Close() }()
|
|
|
|
if _, err := io.Copy(outFile, body); err != nil {
|
|
return fmt.Errorf("failed to write binary: %w", err)
|
|
}
|
|
return nil
|
|
}
|