Fixed SPM being stuck in loop if up-to-date package is already installed
All checks were successful
/ test-on-windows (push) Successful in 6s
/ test-on-alpine (push) Successful in 5s

This commit is contained in:
Internet Addict 2025-02-13 09:41:21 +00:00
parent b1e39f52ac
commit 5691c2b46c

View file

@ -1,261 +1,282 @@
package spm package spm
import ( import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
) "strings"
)
// pendingUpdates holds info about packages that have been downloaded/decompressed
// but not yet moved to the final install location, cuz Windows has this stupid file locking mechanism // pendingUpdates holds info about packages that have been downloaded/decompressed
var pendingUpdates []AppIndexEntry // but not yet moved to the final install location, cuz Windows has this stupid file locking mechanism
var pendingUpdates []AppIndexEntry
// AutoDownloadUpdates downloads the APPINDEX file, parses it, compares against
// currently installed packages, and if it finds a newer version, downloads // AutoDownloadUpdates downloads the APPINDEX file, parses it, compares against
// and decompresses it into a temporary folder. The result is stored in pendingUpdates, so it can be used by AutoInstallUpdates(). // currently installed packages, and if it finds a newer version, downloads
func AutoDownloadUpdates() error { // and decompresses it into a temporary folder. The result is stored in pendingUpdates, so it can be used by AutoInstallUpdates().
// 1) Download the APPINDEX file to a temporary location func AutoDownloadUpdates() error {
appIndexPath := filepath.Join(os.TempDir(), "APPINDEX") // 1) Download the APPINDEX file to a temporary location
fmt.Println("[INFO] Starting APPINDEX download to:", appIndexPath) appIndexPath := filepath.Join(os.TempDir(), "APPINDEX")
err := DownloadAppIndex(appIndexPath) fmt.Println("[INFO] Starting APPINDEX download to:", appIndexPath)
if err != nil { err := DownloadAppIndex(appIndexPath)
return fmt.Errorf("[ERROR] Failed to download APPINDEX: %w", err) if err != nil {
} return fmt.Errorf("[ERROR] Failed to download APPINDEX: %w", err)
fmt.Println("[INFO] APPINDEX downloaded successfully") }
fmt.Println("[INFO] APPINDEX downloaded successfully")
// 2) Parse the APPINDEX file
fmt.Println("[INFO] Parsing APPINDEX file:", appIndexPath) // 2) Parse the APPINDEX file
entries, err := ParseAppIndex(appIndexPath) fmt.Println("[INFO] Parsing APPINDEX file:", appIndexPath)
if err != nil { entries, err := ParseAppIndex(appIndexPath)
return fmt.Errorf("[ERROR] Failed to parse APPINDEX: %w", err) if err != nil {
} return fmt.Errorf("[ERROR] Failed to parse APPINDEX: %w", err)
fmt.Printf("[INFO] Parsed APPINDEX successfully, found %d entries\n", len(entries)) }
fmt.Printf("[INFO] Parsed APPINDEX successfully, found %d entries\n", len(entries))
// 3) Load installed packages
fmt.Println("[INFO] Loading installed packages") // 3) Load installed packages
installDir, err := GetInstallDir() fmt.Println("[INFO] Loading installed packages")
if err != nil { installDir, err := GetInstallDir()
return err if err != nil {
} return err
fmt.Println("[INFO] Install directory:", installDir) }
fmt.Println("[INFO] Install directory:", installDir)
installedPkgs, err := loadInstalledPackages(installDir)
if err != nil { installedPkgs, err := loadInstalledPackages(installDir)
return fmt.Errorf("[ERROR] Failed to load installed packages: %w", err) if err != nil {
} return fmt.Errorf("[ERROR] Failed to load installed packages: %w", err)
fmt.Printf("[INFO] Loaded %d installed packages\n", len(installedPkgs)) }
fmt.Printf("[INFO] Loaded %d installed packages\n", len(installedPkgs))
// 4) Process entries for installed packages only
for _, installed := range installedPkgs { // 4) Process entries for installed packages only
fmt.Printf("[INFO] Checking updates for installed package: %+v\n", installed) for _, installed := range installedPkgs {
fmt.Printf("[INFO] Checking updates for installed package: %+v\n", installed)
// Filter APPINDEX entries that match the installed package's attributes
var matchingEntry *AppIndexEntry // Filter APPINDEX entries that match the installed package's attributes
for _, entry := range entries { var matchingEntry *AppIndexEntry
if entry.Name == installed.Name && for _, entry := range entries {
entry.Release == installed.Release && if entry.Name == installed.Name &&
entry.Type == installed.Type && entry.Release == installed.Release &&
entry.OS == installed.OS && entry.Type == installed.Type &&
entry.Arch == installed.Arch { entry.OS == installed.OS &&
matchingEntry = &entry entry.Arch == installed.Arch {
break matchingEntry = &entry
} break
} }
}
if matchingEntry == nil {
fmt.Printf("[WARN] No matching APPINDEX entry found for installed package: %s (%s)\n", installed.Name, installed.Release) if matchingEntry == nil {
continue fmt.Printf("[WARN] No matching APPINDEX entry found for installed package: %s (%s)\n", installed.Name, installed.Release)
} continue
}
fmt.Printf("[INFO] Found matching APPINDEX entry: %+v\n", *matchingEntry)
fmt.Printf("[INFO] Found matching APPINDEX entry: %+v\n", *matchingEntry)
// Determine if an update is needed
updateNeeded, err := IsUpdateNeeded(installDir, matchingEntry.Name, matchingEntry.Release, matchingEntry.Version, matchingEntry.Arch, matchingEntry.OS) // Determine if an update is needed
if err != nil { updateNeeded, err := IsUpdateNeeded(installDir, matchingEntry.Name, matchingEntry.Release, matchingEntry.Version, matchingEntry.Arch, matchingEntry.OS)
return fmt.Errorf("[ERROR] Failed to check if update is needed for %s: %w", matchingEntry.Name, err) if err != nil {
} return fmt.Errorf("[ERROR] Failed to check if update is needed for %s: %w", matchingEntry.Name, err)
}
if !updateNeeded {
fmt.Printf("[INFO] No update needed for package '%s'\n", matchingEntry.Name) if !updateNeeded {
continue fmt.Printf("[INFO] No update needed for package '%s'\n", matchingEntry.Name)
} continue
}
// 5) Download the package into a temporary download folder
downloadDir := GetTempDir() // 5) Download the package into a temporary download folder
fmt.Printf("[INFO] Downloading package '%s' to temporary folder: %s\n", matchingEntry.Name, downloadDir) downloadDir := GetTempDir()
fmt.Printf("[INFO] Downloading package '%s' to temporary folder: %s\n",
err = DownloadPackageFromAppIndex(appIndexPath, matchingEntry.Name, matchingEntry.Release, matchingEntry.Type, downloadDir) matchingEntry.Name, downloadDir)
if err != nil {
return fmt.Errorf("[ERROR] Failed to download package '%s': %w", matchingEntry.Name, err) err = DownloadPackageFromAppIndex(
} appIndexPath,
matchingEntry.Name,
fmt.Printf("[INFO] Package '%s' downloaded successfully to: %s\n", matchingEntry.Name, downloadDir) matchingEntry.Release,
matchingEntry.Type,
// 6) Decompress the package into another temp folder downloadDir,
fmt.Printf("[INFO] Decompressing package '%s'\n", matchingEntry.Name) )
tempDir, err := DecompressPackage(downloadDir, matchingEntry.Name, matchingEntry.Arch, matchingEntry.OS, matchingEntry.Type, matchingEntry.Release, matchingEntry.Version) if err != nil {
if err != nil { // If the package is already up-to-date, skip it instead of erroring out
return fmt.Errorf("[ERROR] Failed to decompress package '%s': %w", matchingEntry.Name, err) if strings.Contains(err.Error(), "Already up-to-date") {
} fmt.Printf("[INFO] Package '%s' is already up-to-date, skipping.\n", matchingEntry.Name)
fmt.Printf("[INFO] Package '%s' decompressed successfully to: %s\n", matchingEntry.Name, tempDir) continue
}
// 7) Store in pendingUpdates so that AutoInstallUpdates can finish the job return fmt.Errorf("[ERROR] Failed to download package '%s': %w",
fmt.Printf("[INFO] Adding '%s' to pending updates\n", matchingEntry.Name) matchingEntry.Name, err)
pendingUpdates = append(pendingUpdates, AppIndexEntry{ }
Name: matchingEntry.Name,
Version: matchingEntry.Version, fmt.Printf("[INFO] Package '%s' downloaded successfully to: %s\n", matchingEntry.Name, downloadDir)
Release: matchingEntry.Release,
Arch: matchingEntry.Arch, // 6) Decompress the package into another temp folder
OS: matchingEntry.OS, fmt.Printf("[INFO] Decompressing package '%s'\n", matchingEntry.Name)
Type: matchingEntry.Type, tempDir, err := DecompressPackage(downloadDir, matchingEntry.Name, matchingEntry.Arch, matchingEntry.OS, matchingEntry.Type, matchingEntry.Release, matchingEntry.Version)
}) if err != nil {
} return fmt.Errorf("[ERROR] Failed to decompress package '%s': %w", matchingEntry.Name, err)
}
fmt.Println("[INFO] AutoDownloadUpdates completed successfully") fmt.Printf("[INFO] Package '%s' decompressed successfully to: %s\n", matchingEntry.Name, tempDir)
return nil
} // 7) Store in pendingUpdates so that AutoInstallUpdates can finish the job
fmt.Printf("[INFO] Adding '%s' to pending updates\n", matchingEntry.Name)
// AutoInstallUpdates installs any packages that were downloaded and decompressed by AutoDownloadUpdates. pendingUpdates = append(pendingUpdates, AppIndexEntry{
// It moves files from their temp directories to the final location and updates installed.ini. Name: matchingEntry.Name,
func AutoInstallUpdates() error { Version: matchingEntry.Version,
installDir, err := GetInstallDir() Release: matchingEntry.Release,
if err != nil { Arch: matchingEntry.Arch,
return err OS: matchingEntry.OS,
} Type: matchingEntry.Type,
})
for _, entry := range pendingUpdates { }
// 1) Construct the same .tar.gz name we used when decompressing
fileName := fmt.Sprintf("%s@%s@%s@%s@%s@%s", fmt.Println("[INFO] AutoDownloadUpdates completed successfully")
entry.Name, // no 'packageName' return nil
entry.Arch, // matches 'arch' }
entry.OS, // matches 'os'
entry.Type, // matches 'type' // AutoInstallUpdates installs any packages that were downloaded and decompressed by AutoDownloadUpdates.
entry.Release, // matches 'release' // It moves files from their temp directories to the final location and updates installed.ini.
entry.Version, // matches 'version' func AutoInstallUpdates() error {
) installDir, err := GetInstallDir()
if err != nil {
// 3) Combine with your global temp dir return err
tempBase := GetTempDir() // e.g. C:\Users\YourUser\AppData\Local\Temp\spm_temp_164326 }
decompressedDir := filepath.Join(tempBase, fileName)
for _, entry := range pendingUpdates {
// 4) Move files from that decompressedDir // 1) Construct the same .tar.gz name we used when decompressing
fmt.Printf("[INFO] Installing %s from %s\n", entry.Name, decompressedDir) fileName := fmt.Sprintf("%s@%s@%s@%s@%s@%s",
err := MoveFilesToInstallDir(decompressedDir, installDir, entry.Type) entry.Name, // no 'packageName'
if err != nil { entry.Arch, // matches 'arch'
return fmt.Errorf("failed to move files for %s: %w", entry.Name, err) entry.OS, // matches 'os'
} entry.Type, // matches 'type'
entry.Release, // matches 'release'
// 5) Finalize entry.Version, // matches 'version'
err = FinalizeInstall(entry.Name, entry.Release, entry.Version, entry.Arch, entry.OS) )
if err != nil {
return fmt.Errorf("failed to finalize install for %s: %w", entry.Name, err) // 3) Combine with your global temp dir
} tempBase := GetTempDir() // e.g. C:\Users\YourUser\AppData\Local\Temp\spm_temp_164326
} decompressedDir := filepath.Join(tempBase, fileName)
pendingUpdates = nil // 4) Move files from that decompressedDir
return nil fmt.Printf("[INFO] Installing %s from %s\n", entry.Name, decompressedDir)
} err := MoveFilesToInstallDir(decompressedDir, installDir, entry.Type)
if err != nil {
func AutoDownloadSpecified(specs []AppIndexEntry) error { return fmt.Errorf("failed to move files for %s: %w", entry.Name, err)
// 1) Download the APPINDEX file to a temporary location }
appIndexPath := filepath.Join(os.TempDir(), "APPINDEX")
fmt.Println("[INFO] Starting APPINDEX download to:", appIndexPath) // 5) Finalize
if err := DownloadAppIndex(appIndexPath); err != nil { err = FinalizeInstall(entry.Name, entry.Release, entry.Version, entry.Arch, entry.OS)
return fmt.Errorf("[ERROR] Failed to download APPINDEX: %w", err) if err != nil {
} return fmt.Errorf("failed to finalize install for %s: %w", entry.Name, err)
fmt.Println("[INFO] APPINDEX downloaded successfully") }
}
// 2) Parse the APPINDEX file
fmt.Println("[INFO] Parsing APPINDEX file:", appIndexPath) pendingUpdates = nil
entries, err := ParseAppIndex(appIndexPath) return nil
if err != nil { }
return fmt.Errorf("[ERROR] Failed to parse APPINDEX: %w", err)
} func AutoDownloadSpecified(specs []AppIndexEntry) error {
fmt.Printf("[INFO] Parsed APPINDEX successfully, found %d entries\n", len(entries)) // 1) Download the APPINDEX file to a temporary location
appIndexPath := filepath.Join(os.TempDir(), "APPINDEX")
// 3) Get install directory to check for updates fmt.Println("[INFO] Starting APPINDEX download to:", appIndexPath)
installDir, err := GetInstallDir() if err := DownloadAppIndex(appIndexPath); err != nil {
if err != nil { return fmt.Errorf("[ERROR] Failed to download APPINDEX: %w", err)
return err }
} fmt.Println("[INFO] APPINDEX downloaded successfully")
fmt.Println("[INFO] Install directory:", installDir)
// 2) Parse the APPINDEX file
// 4) For each item in the passed specs, attempt to download if update is needed fmt.Println("[INFO] Parsing APPINDEX file:", appIndexPath)
for _, spec := range specs { entries, err := ParseAppIndex(appIndexPath)
fmt.Printf("[INFO] Checking requested package: %+v\n", spec) if err != nil {
return fmt.Errorf("[ERROR] Failed to parse APPINDEX: %w", err)
// Find matching entry from the parsed APPINDEX }
var matchingEntry *AppIndexEntry fmt.Printf("[INFO] Parsed APPINDEX successfully, found %d entries\n", len(entries))
for _, e := range entries {
if e.Name == spec.Name && // 3) Get install directory to check for updates
e.Release == spec.Release && installDir, err := GetInstallDir()
e.Type == spec.Type && if err != nil {
e.OS == spec.OS && return err
e.Arch == spec.Arch { }
matchingEntry = &e fmt.Println("[INFO] Install directory:", installDir)
break
} // 4) For each item in the passed specs, attempt to download if update is needed
} for _, spec := range specs {
fmt.Printf("[INFO] Checking requested package: %+v\n", spec)
if matchingEntry == nil {
fmt.Printf("[WARN] No matching APPINDEX entry found for package: %s (%s)\n", spec.Name, spec.Release) // Find matching entry from the parsed APPINDEX
continue var matchingEntry *AppIndexEntry
} for _, e := range entries {
fmt.Printf("[INFO] Found matching APPINDEX entry: %+v\n", *matchingEntry) if e.Name == spec.Name &&
e.Release == spec.Release &&
// // Check if an update is needed e.Type == spec.Type &&
// updateNeeded, err := IsUpdateNeeded( e.OS == spec.OS &&
// installDir, e.Arch == spec.Arch {
// matchingEntry.Name, matchingEntry = &e
// matchingEntry.Release, break
// matchingEntry.Version, }
// matchingEntry.Arch, }
// matchingEntry.OS,
// ) if matchingEntry == nil {
// if err != nil { fmt.Printf("[WARN] No matching APPINDEX entry found for package: %s (%s)\n", spec.Name, spec.Release)
// return fmt.Errorf("[ERROR] Failed to check if update is needed for %s: %w", matchingEntry.Name, err) continue
// } }
fmt.Printf("[INFO] Found matching APPINDEX entry: %+v\n", *matchingEntry)
// if !updateNeeded {
// fmt.Printf("[INFO] No update needed for package '%s'\n", matchingEntry.Name) // // Check if an update is needed
// continue // updateNeeded, err := IsUpdateNeeded(
// } // installDir,
// matchingEntry.Name,
// 5) Download the package // matchingEntry.Release,
downloadDir := GetTempDir() // matchingEntry.Version,
fmt.Printf("[INFO] Downloading package '%s' to temporary folder: %s\n", matchingEntry.Name, downloadDir) // matchingEntry.Arch,
if err := DownloadPackageFromAppIndex( // matchingEntry.OS,
appIndexPath, // )
matchingEntry.Name, // if err != nil {
matchingEntry.Release, // return fmt.Errorf("[ERROR] Failed to check if update is needed for %s: %w", matchingEntry.Name, err)
matchingEntry.Type, // }
downloadDir,
); err != nil { // if !updateNeeded {
return fmt.Errorf("[ERROR] Failed to download package '%s': %w", matchingEntry.Name, err) // fmt.Printf("[INFO] No update needed for package '%s'\n", matchingEntry.Name)
} // continue
fmt.Printf("[INFO] Package '%s' downloaded successfully to: %s\n", matchingEntry.Name, downloadDir) // }
// 6) Decompress the package // 5) Download the package
fmt.Printf("[INFO] Decompressing package '%s'\n", matchingEntry.Name) downloadDir := GetTempDir()
tempDir, err := DecompressPackage( fmt.Printf("[INFO] Downloading package '%s' to temporary folder: %s\n",
downloadDir, matchingEntry.Name, downloadDir)
matchingEntry.Name, if err := DownloadPackageFromAppIndex(
matchingEntry.Arch, appIndexPath,
matchingEntry.OS, matchingEntry.Name,
matchingEntry.Type, matchingEntry.Release,
matchingEntry.Release, matchingEntry.Type,
matchingEntry.Version, downloadDir,
) ); err != nil {
if err != nil { // Again, if "Already up-to-date", skip
return fmt.Errorf("[ERROR] Failed to decompress package '%s': %w", matchingEntry.Name, err) if strings.Contains(err.Error(), "Already up-to-date") {
} fmt.Printf("[INFO] Package '%s' is already up-to-date, skipping.\n", matchingEntry.Name)
fmt.Printf("[INFO] Package '%s' decompressed successfully to: %s\n", matchingEntry.Name, tempDir) continue
}
// 7) Store in pendingUpdates for AutoInstallUpdates return fmt.Errorf("[ERROR] Failed to download package '%s': %w",
fmt.Printf("[INFO] Adding '%s' to pending updates\n", matchingEntry.Name) matchingEntry.Name, err)
pendingUpdates = append(pendingUpdates, *matchingEntry) }
} fmt.Printf("[INFO] Package '%s' downloaded successfully to: %s\n", matchingEntry.Name, downloadDir)
fmt.Println("[INFO] AutoDownloadSpecifiedPackages completed successfully") // 6) Decompress the package
return nil fmt.Printf("[INFO] Decompressing package '%s'\n", matchingEntry.Name)
} tempDir, err := DecompressPackage(
downloadDir,
matchingEntry.Name,
matchingEntry.Arch,
matchingEntry.OS,
matchingEntry.Type,
matchingEntry.Release,
matchingEntry.Version,
)
if err != nil {
return fmt.Errorf("[ERROR] Failed to decompress package '%s': %w", matchingEntry.Name, err)
}
fmt.Printf("[INFO] Package '%s' decompressed successfully to: %s\n", matchingEntry.Name, tempDir)
// 7) Store in pendingUpdates for AutoInstallUpdates
fmt.Printf("[INFO] Adding '%s' to pending updates\n", matchingEntry.Name)
pendingUpdates = append(pendingUpdates, *matchingEntry)
}
fmt.Println("[INFO] AutoDownloadSpecifiedPackages completed successfully")
return nil
}