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) // } // }