Store/spm/installed_pacakges.go
2025-03-09 11:42:53 +01:00

214 lines
5.8 KiB
Go

package spm
import (
"fmt"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"gopkg.in/ini.v1"
)
// getInstalledPackagesPath determines the path for the installed.ini file.
func getInstalledPackagesPath(installDir string) string {
spmDir := filepath.Join(installDir, "spm")
_ = os.MkdirAll(spmDir, 0755)
return filepath.Join(spmDir, "installed.ini")
}
// loadInstalledPackages reads the installed.ini file and parses it.
func loadInstalledPackages(installDir string) ([]AppIndexEntry, error) {
installedFile := getInstalledPackagesPath(installDir)
cfg, err := ini.LoadSources(ini.LoadOptions{AllowNonUniqueSections: true}, installedFile)
if err != nil {
if os.IsNotExist(err) {
return []AppIndexEntry{}, nil // Return empty slice if file doesn't exist
}
return nil, err
}
var installed []AppIndexEntry
for _, section := range cfg.Sections() {
if section.Name() == "DEFAULT" {
continue
}
// Read fields
name := section.Key("P").String()
version := section.Key("V").String()
release := section.Key("R").String()
typeVal := section.Key("o").String()
arch := section.Key("A").MustString(runtime.GOARCH) // Default to system arch
osName := section.Key("p").MustString(runtime.GOOS) // Default to system OS
// Append to slice
installed = append(installed, AppIndexEntry{
Name: name,
Version: version,
Release: release,
Type: typeVal,
Arch: arch,
OS: osName,
})
}
return installed, nil
}
// saveInstalledPackages writes the installed packages into installed.ini.
func saveInstalledPackages(installDir string, pkgs []AppIndexEntry) error {
installedFile := getInstalledPackagesPath(installDir)
cfg := ini.Empty()
for _, pkg := range pkgs {
section, err := cfg.NewSection(pkg.Name)
if err != nil {
return err
}
section.Key("P").SetValue(pkg.Name)
section.Key("V").SetValue(pkg.Version)
section.Key("R").SetValue(pkg.Release)
section.Key("o").SetValue(pkg.Type)
// Save arch if different from current runtime architecture
if pkg.Arch != runtime.GOARCH {
section.Key("A").SetValue(pkg.Arch)
}
// Save OS if different from current runtime OS
if pkg.OS != runtime.GOOS {
section.Key("p").SetValue(pkg.OS)
}
}
return cfg.SaveTo(installedFile)
}
// isNewerVersion compares two version strings and determines if `newVer` is newer than `oldVer`.
func isNewerVersion(oldVer, newVer string) bool {
// Handle date-based versions (e.g., nightly: YYYY.MM.DD)
if isDateVersion(oldVer) && isDateVersion(newVer) {
return strings.Compare(newVer, oldVer) > 0
}
// Handle semantic versions (e.g., stable: v1.0.1)
if isSemVer(oldVer) && isSemVer(newVer) {
return compareSemVer(oldVer, newVer) > 0
}
// Fallback to lexicographical comparison for unknown formats
return strings.Compare(newVer, oldVer) > 0
}
// isDateVersion checks if a version string is in the format YYYY.MM.DD.
func isDateVersion(version string) bool {
parts := strings.Split(version, ".")
if len(parts) != 3 {
return false
}
for _, part := range parts {
if _, err := strconv.Atoi(part); err != nil {
return false
}
}
return true
}
// isSemVer checks if a version string is in the format vMAJOR.MINOR.PATCH.
func isSemVer(version string) bool {
if !strings.HasPrefix(version, "v") {
return false
}
parts := strings.Split(strings.TrimPrefix(version, "v"), ".")
if len(parts) != 3 {
return false
}
for _, part := range parts {
if _, err := strconv.Atoi(part); err != nil {
return false
}
}
return true
}
// compareSemVer compares two semantic version strings (vMAJOR.MINOR.PATCH).
// Returns:
// - 1 if `v2` is newer than `v1`
// - 0 if `v1` and `v2` are equal
// - -1 if `v1` is newer than `v2`
func compareSemVer(v1, v2 string) int {
parts1 := strings.Split(strings.TrimPrefix(v1, "v"), ".")
parts2 := strings.Split(strings.TrimPrefix(v2, "v"), ".")
for i := 0; i < len(parts1); i++ {
num1, _ := strconv.Atoi(parts1[i])
num2, _ := strconv.Atoi(parts2[i])
if num1 > num2 {
return 1
}
if num1 < num2 {
return -1
}
}
return 0
}
// IsUpdateNeeded checks if the given package version is newer than what's installed.
func IsUpdateNeeded(installDir, name, release, newVersion, arch, osName string) (bool, error) {
installed, err := loadInstalledPackages(installDir)
if err != nil {
return false, err
}
for _, pkg := range installed {
if pkg.Name == name && pkg.Release == release && pkg.Arch == arch && pkg.OS == osName {
fmt.Printf("Found installed package: %v\n", pkg)
if isNewerVersion(pkg.Version, newVersion) {
fmt.Println("Update is needed")
return true, nil
}
fmt.Println("No update needed")
return false, nil
}
}
fmt.Println("Package not installed, update needed")
return true, nil
}
// UpdateInstalledPackage writes/updates the new package version in installed.ini.
func UpdateInstalledPackage(installDir string, pkg AppIndexEntry) error {
installed, err := loadInstalledPackages(installDir)
if err != nil {
return err
}
updated := false
for i, p := range installed {
if p.Name == pkg.Name && p.Release == pkg.Release && p.Arch == pkg.Arch && p.OS == pkg.OS {
installed[i].Version = pkg.Version
updated = true
break
}
}
if !updated {
installed = append(installed, pkg)
}
return saveInstalledPackages(installDir, installed)
}
// // DebugInstalled prints installed packages for debugging.
// func DebugInstalled(installDir string) {
// pkgs, err := loadInstalledPackages(installDir)
// if err != nil {
// fmt.Println("DebugInstalled error:", err)
// return
// }
// for _, p := range pkgs {
// fmt.Printf("Installed: %s v%s [%s] (arch=%s, os=%s)\n", p.Name, p.Version, p.Release, p.Arch, p.OS)
// }
// }