updated spm package
This commit is contained in:
parent
b403befe74
commit
48473f98c5
16 changed files with 1454 additions and 505 deletions
8
go.mod
8
go.mod
|
@ -5,7 +5,15 @@ go 1.21.1
|
|||
require github.com/gen2brain/raylib-go/raylib v0.0.0-20241215043839-7a899c5e3aae
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/ebitengine/purego v0.7.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240707233637-46b078467d37 // indirect
|
||||
golang.org/x/sys v0.22.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
replace spitfire-installer/spm => ./spm
|
||||
|
||||
require spitfire-installer/spm v0.0.1
|
||||
|
|
11
go.sum
11
go.sum
|
@ -1,8 +1,19 @@
|
|||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/ebitengine/purego v0.7.1 h1:6/55d26lG3o9VCZX8lping+bZcmShseiqlh2bnUDiPA=
|
||||
github.com/ebitengine/purego v0.7.1/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ=
|
||||
github.com/gen2brain/raylib-go/raylib v0.0.0-20241215043839-7a899c5e3aae h1:zuCOZlow6XuDW6rPRRrbBx2PaS58igRW5zLaBpo5R70=
|
||||
github.com/gen2brain/raylib-go/raylib v0.0.0-20241215043839-7a899c5e3aae/go.mod h1:BaY76bZk7nw1/kVOSQObPY1v1iwVE1KHAGMfvI6oK1Q=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
golang.org/x/exp v0.0.0-20240707233637-46b078467d37 h1:uLDX+AfeFCct3a2C7uIWBKMJIR3CJMhcgfrUAqjRK6w=
|
||||
golang.org/x/exp v0.0.0-20240707233637-46b078467d37/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
77
installer.go
77
installer.go
|
@ -2,7 +2,7 @@ package main
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"spitfire-installer/spm"
|
||||
)
|
||||
|
||||
|
@ -19,10 +19,6 @@ type Installer struct {
|
|||
DoneInstall bool
|
||||
LastError error
|
||||
PendingInstall bool
|
||||
|
||||
// Paths
|
||||
DownloadDir string
|
||||
TempDir string
|
||||
}
|
||||
|
||||
// NewInstaller creates a new Installer with initial state.
|
||||
|
@ -36,66 +32,67 @@ func (inst *Installer) StartDownloadDecompress() {
|
|||
go func() {
|
||||
defer func() {
|
||||
inst.IsDownloading = false
|
||||
// Signal that download phase is complete.
|
||||
inst.DoneDownload = (inst.LastError == nil)
|
||||
|
||||
// If user requested install while we were downloading (PendingInstall),
|
||||
// automatically do the install now that we're done decompressing.
|
||||
// If a final install was requested, go ahead.
|
||||
if inst.PendingInstall && inst.DoneDownload && !inst.IsInstalling && !inst.DoneInstall {
|
||||
inst.doFinalInstall()
|
||||
}
|
||||
}()
|
||||
|
||||
spm.UpdateProgress(0, "Preparing to download...")
|
||||
inst.DownloadDir = spm.GetTempDownloadDir()
|
||||
|
||||
// 1) Download APPINDEX
|
||||
appIndexPath := filepath.Join(inst.DownloadDir, "APPINDEX")
|
||||
spm.UpdateProgress(0, "Downloading APPINDEX")
|
||||
if err := spm.DownloadAppIndex(appIndexPath); err != nil {
|
||||
// Define the package specifications.
|
||||
specs := []spm.AppIndexEntry{
|
||||
{
|
||||
Name: "spitfire-luncher",
|
||||
Release: "nightly",
|
||||
OS: runtime.GOOS,
|
||||
Arch: runtime.GOARCH,
|
||||
Type: "luncher",
|
||||
},
|
||||
{
|
||||
Name: "spitfire-browser",
|
||||
Release: "nightly",
|
||||
OS: runtime.GOOS,
|
||||
Arch: runtime.GOARCH,
|
||||
Type: "browser",
|
||||
},
|
||||
}
|
||||
|
||||
spm.UpdateProgress(0, "Downloading specified packages...")
|
||||
if err := spm.AutoDownloadSpecified(specs); err != nil {
|
||||
fmt.Println("AutoDownloadSpecifiedPackages failed:", err)
|
||||
inst.LastError = err
|
||||
return
|
||||
}
|
||||
|
||||
// 2) Download package
|
||||
packageName := "spitfire-browser"
|
||||
release := "nightly"
|
||||
spm.UpdateProgress(0, "Downloading package...")
|
||||
if err := spm.DownloadPackageFromAppIndex(appIndexPath, packageName, release, inst.DownloadDir); err != nil {
|
||||
inst.LastError = err
|
||||
return
|
||||
}
|
||||
|
||||
// 3) Decompress
|
||||
spm.UpdateProgress(0, "Decompressing...")
|
||||
tempDir, err := spm.DecompressPackage(inst.DownloadDir)
|
||||
if err != nil {
|
||||
inst.LastError = err
|
||||
return
|
||||
}
|
||||
inst.TempDir = tempDir
|
||||
spm.UpdateProgress(0, "Download and decompression complete!")
|
||||
// Here, update your installer state so FinalInstall() can proceed.
|
||||
inst.DoneDownload = true
|
||||
}()
|
||||
}
|
||||
|
||||
// FinalInstall is called by the UI to request installation.
|
||||
// If download is done, it runs immediately, otherwise sets PendingInstall=true.
|
||||
func (inst *Installer) FinalInstall() {
|
||||
// Already installed or installing => ignore repeated calls
|
||||
// Already installed or installing => ignore repeated calls.
|
||||
if inst.IsInstalling || inst.DoneInstall {
|
||||
return
|
||||
}
|
||||
|
||||
// If not done downloading, just mark that we want to install once finished
|
||||
// If not done downloading, mark that we want to install once finished.
|
||||
if !inst.DoneDownload {
|
||||
fmt.Println("Cannot install now: download and decompression not complete -> pending install.")
|
||||
inst.PendingInstall = true
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise, go ahead and install now
|
||||
// Otherwise, go ahead and install now.
|
||||
inst.doFinalInstall()
|
||||
}
|
||||
|
||||
// doFinalInstall does the actual file move and sets states
|
||||
// doFinalInstall does the actual installation by invoking AutoInstallUpdates.
|
||||
func (inst *Installer) doFinalInstall() {
|
||||
inst.IsInstalling = true
|
||||
inst.PendingInstall = false // we are fulfilling the install now
|
||||
|
@ -106,16 +103,8 @@ func (inst *Installer) doFinalInstall() {
|
|||
inst.DoneInstall = (inst.LastError == nil)
|
||||
}()
|
||||
|
||||
// Generate default install directory
|
||||
installDir, err := spm.GetDefaultInstallDir()
|
||||
if err != nil {
|
||||
inst.LastError = fmt.Errorf("failed to determine default install directory: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Move files
|
||||
spm.UpdateProgress(0, "Installing...")
|
||||
if err := spm.MoveFilesToInstallDir(inst.TempDir, installDir); err != nil {
|
||||
spm.UpdateProgress(0, "Installing updates...")
|
||||
if err := spm.AutoInstallUpdates(); err != nil {
|
||||
inst.LastError = err
|
||||
return
|
||||
}
|
||||
|
|
3
main.go
3
main.go
|
@ -4,6 +4,7 @@ import (
|
|||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"spitfire-installer/spm"
|
||||
|
||||
rl "github.com/gen2brain/raylib-go/raylib"
|
||||
)
|
||||
|
@ -29,6 +30,8 @@ var (
|
|||
const finalStep = 3
|
||||
|
||||
func main() {
|
||||
spm.Run()
|
||||
|
||||
monitor := rl.GetCurrentMonitor()
|
||||
if monitor < 0 {
|
||||
monitor = 0 // Fallback to the primary monitor
|
||||
|
|
231
spm/appindex.go
231
spm/appindex.go
|
@ -1,108 +1,123 @@
|
|||
package spm
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const appIndexURL = "https://downloads.sourceforge.net/project/spitfire-browser/APPINDEX"
|
||||
|
||||
func DownloadAppIndex(dest string) error {
|
||||
UpdateProgress(0, "Downloading APPINDEX")
|
||||
resp, err := http.Get(appIndexURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
out, err := os.Create(dest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
totalSize := resp.ContentLength
|
||||
var downloaded int64
|
||||
|
||||
// Track progress as bytes are downloaded
|
||||
buf := make([]byte, 1024)
|
||||
for {
|
||||
n, err := resp.Body.Read(buf)
|
||||
if n > 0 {
|
||||
downloaded += int64(n)
|
||||
percentage := int(float64(downloaded) / float64(totalSize) * 100)
|
||||
UpdateProgress(percentage, "Downloading APPINDEX")
|
||||
if _, err := out.Write(buf[:n]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
UpdateProgress(100, "APPINDEX downloaded")
|
||||
return nil
|
||||
}
|
||||
|
||||
type AppIndexEntry struct {
|
||||
Name string
|
||||
Version string
|
||||
Release string
|
||||
Arch string
|
||||
OS string
|
||||
DownloadURL string
|
||||
}
|
||||
|
||||
func ParseAppIndex(filePath string) ([]AppIndexEntry, error) {
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var entries []AppIndexEntry
|
||||
scanner := bufio.NewScanner(file)
|
||||
entry := AppIndexEntry{}
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if strings.HasPrefix(line, "C:") {
|
||||
if entry.Name != "" {
|
||||
entries = append(entries, entry)
|
||||
entry = AppIndexEntry{}
|
||||
}
|
||||
}
|
||||
parts := strings.SplitN(line, ":", 2)
|
||||
if len(parts) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
switch parts[0] {
|
||||
case "P":
|
||||
entry.Name = parts[1]
|
||||
case "R":
|
||||
entry.Release = parts[1]
|
||||
case "V":
|
||||
entry.Version = parts[1]
|
||||
case "A":
|
||||
entry.Arch = parts[1]
|
||||
case "p":
|
||||
entry.OS = parts[1]
|
||||
case "d":
|
||||
entry.DownloadURL = parts[1]
|
||||
}
|
||||
}
|
||||
|
||||
if entry.Name != "" {
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
|
||||
return entries, scanner.Err()
|
||||
}
|
||||
package spm
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const appIndexURL = "https://downloads.sourceforge.net/project/spitfire-browser/APPINDEX"
|
||||
|
||||
func DownloadAppIndex(dest string) error {
|
||||
UpdateProgress(0, "Downloading APPINDEX")
|
||||
resp, err := http.Get(appIndexURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
out, err := os.Create(dest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
totalSize := resp.ContentLength
|
||||
var downloaded int64
|
||||
|
||||
// Track progress as bytes are downloaded
|
||||
buf := make([]byte, 1024)
|
||||
for {
|
||||
n, err := resp.Body.Read(buf)
|
||||
if n > 0 {
|
||||
downloaded += int64(n)
|
||||
percentage := int(float64(downloaded) / float64(totalSize) * 100)
|
||||
UpdateProgress(percentage, "Downloading APPINDEX")
|
||||
if _, err := out.Write(buf[:n]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
UpdateProgress(100, "APPINDEX downloaded")
|
||||
return nil
|
||||
}
|
||||
|
||||
type AppIndexEntry struct {
|
||||
Name string
|
||||
Version string
|
||||
Release string // "nightly" / "stable" / etc.
|
||||
Arch string // e.g. "amd64", "386"
|
||||
OS string // e.g. "windows", "linux"
|
||||
Type string // "browser", "addon", "theme", etc.
|
||||
DownloadURL string
|
||||
}
|
||||
|
||||
func ParseAppIndex(filePath string) ([]AppIndexEntry, error) {
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var entries []AppIndexEntry
|
||||
scanner := bufio.NewScanner(file)
|
||||
entry := AppIndexEntry{}
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if strings.HasPrefix(line, "C:") {
|
||||
// Start of a new entry
|
||||
if entry.Name != "" {
|
||||
entries = append(entries, entry)
|
||||
entry = AppIndexEntry{}
|
||||
}
|
||||
}
|
||||
|
||||
parts := strings.SplitN(line, ":", 2)
|
||||
if len(parts) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
key, value := parts[0], parts[1]
|
||||
switch key {
|
||||
case "P":
|
||||
entry.Name = value
|
||||
case "R":
|
||||
entry.Release = value
|
||||
case "V":
|
||||
entry.Version = value
|
||||
case "A":
|
||||
entry.Arch = value
|
||||
case "p":
|
||||
entry.OS = value
|
||||
case "d":
|
||||
entry.DownloadURL = value
|
||||
case "o":
|
||||
entry.Type = value
|
||||
}
|
||||
}
|
||||
|
||||
// Append the last entry if any
|
||||
if entry.Name != "" {
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
|
||||
// Log all parsed entries
|
||||
fmt.Printf("[INFO] Total parsed entries: %d\n", len(entries))
|
||||
for _, e := range entries {
|
||||
fmt.Printf(" - Name: %s, Release: %s, Type: %s, OS: %s, Arch: %s, Version: %s, URL: %s\n",
|
||||
e.Name, e.Release, e.Type, e.OS, e.Arch, e.Version, e.DownloadURL)
|
||||
}
|
||||
|
||||
return entries, scanner.Err()
|
||||
}
|
||||
|
|
261
spm/auto.go
Normal file
261
spm/auto.go
Normal file
|
@ -0,0 +1,261 @@
|
|||
package spm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// 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
|
||||
var pendingUpdates []AppIndexEntry
|
||||
|
||||
// AutoDownloadUpdates downloads the APPINDEX file, parses it, compares against
|
||||
// currently installed packages, and if it finds a newer version, downloads
|
||||
// and decompresses it into a temporary folder. The result is stored in pendingUpdates, so it can be used by AutoInstallUpdates().
|
||||
func AutoDownloadUpdates() error {
|
||||
// 1) Download the APPINDEX file to a temporary location
|
||||
appIndexPath := filepath.Join(os.TempDir(), "APPINDEX")
|
||||
fmt.Println("[INFO] Starting APPINDEX download to:", appIndexPath)
|
||||
err := DownloadAppIndex(appIndexPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("[ERROR] Failed to download APPINDEX: %w", err)
|
||||
}
|
||||
fmt.Println("[INFO] APPINDEX downloaded successfully")
|
||||
|
||||
// 2) Parse the APPINDEX file
|
||||
fmt.Println("[INFO] Parsing APPINDEX file:", appIndexPath)
|
||||
entries, err := ParseAppIndex(appIndexPath)
|
||||
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))
|
||||
|
||||
// 3) Load installed packages
|
||||
fmt.Println("[INFO] Loading installed packages")
|
||||
installDir, err := GetInstallDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("[INFO] Install directory:", installDir)
|
||||
|
||||
installedPkgs, err := loadInstalledPackages(installDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("[ERROR] Failed to load installed packages: %w", err)
|
||||
}
|
||||
fmt.Printf("[INFO] Loaded %d installed packages\n", len(installedPkgs))
|
||||
|
||||
// 4) Process entries for installed packages only
|
||||
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
|
||||
for _, entry := range entries {
|
||||
if entry.Name == installed.Name &&
|
||||
entry.Release == installed.Release &&
|
||||
entry.Type == installed.Type &&
|
||||
entry.OS == installed.OS &&
|
||||
entry.Arch == installed.Arch {
|
||||
matchingEntry = &entry
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if matchingEntry == nil {
|
||||
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)
|
||||
|
||||
// Determine if an update is needed
|
||||
updateNeeded, err := IsUpdateNeeded(installDir, matchingEntry.Name, matchingEntry.Release, matchingEntry.Version, matchingEntry.Arch, matchingEntry.OS)
|
||||
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)
|
||||
continue
|
||||
}
|
||||
|
||||
// 5) Download the package into a temporary download folder
|
||||
downloadDir := GetTempDir()
|
||||
fmt.Printf("[INFO] Downloading package '%s' to temporary folder: %s\n", matchingEntry.Name, downloadDir)
|
||||
|
||||
err = DownloadPackageFromAppIndex(appIndexPath, matchingEntry.Name, matchingEntry.Release, matchingEntry.Type, downloadDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("[ERROR] Failed to download package '%s': %w", matchingEntry.Name, err)
|
||||
}
|
||||
|
||||
fmt.Printf("[INFO] Package '%s' downloaded successfully to: %s\n", matchingEntry.Name, downloadDir)
|
||||
|
||||
// 6) Decompress the package into another temp folder
|
||||
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 so that AutoInstallUpdates can finish the job
|
||||
fmt.Printf("[INFO] Adding '%s' to pending updates\n", matchingEntry.Name)
|
||||
pendingUpdates = append(pendingUpdates, AppIndexEntry{
|
||||
Name: matchingEntry.Name,
|
||||
Version: matchingEntry.Version,
|
||||
Release: matchingEntry.Release,
|
||||
Arch: matchingEntry.Arch,
|
||||
OS: matchingEntry.OS,
|
||||
Type: matchingEntry.Type,
|
||||
})
|
||||
}
|
||||
|
||||
fmt.Println("[INFO] AutoDownloadUpdates completed successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// AutoInstallUpdates installs any packages that were downloaded and decompressed by AutoDownloadUpdates.
|
||||
// It moves files from their temp directories to the final location and updates installed.ini.
|
||||
func AutoInstallUpdates() error {
|
||||
installDir, err := GetInstallDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, entry := range pendingUpdates {
|
||||
// 1) Construct the same .tar.gz name we used when decompressing
|
||||
fileName := fmt.Sprintf("%s@%s@%s@%s@%s@%s",
|
||||
entry.Name, // no 'packageName'
|
||||
entry.Arch, // matches 'arch'
|
||||
entry.OS, // matches 'os'
|
||||
entry.Type, // matches 'type'
|
||||
entry.Release, // matches 'release'
|
||||
entry.Version, // matches 'version'
|
||||
)
|
||||
|
||||
// 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)
|
||||
|
||||
// 4) Move files from that decompressedDir
|
||||
fmt.Printf("[INFO] Installing %s from %s\n", entry.Name, decompressedDir)
|
||||
err := MoveFilesToInstallDir(decompressedDir, installDir, entry.Type)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to move files for %s: %w", entry.Name, err)
|
||||
}
|
||||
|
||||
// 5) Finalize
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
pendingUpdates = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func AutoDownloadSpecified(specs []AppIndexEntry) error {
|
||||
// 1) Download the APPINDEX file to a temporary location
|
||||
appIndexPath := filepath.Join(os.TempDir(), "APPINDEX")
|
||||
fmt.Println("[INFO] Starting APPINDEX download to:", appIndexPath)
|
||||
if err := DownloadAppIndex(appIndexPath); err != nil {
|
||||
return fmt.Errorf("[ERROR] Failed to download APPINDEX: %w", err)
|
||||
}
|
||||
fmt.Println("[INFO] APPINDEX downloaded successfully")
|
||||
|
||||
// 2) Parse the APPINDEX file
|
||||
fmt.Println("[INFO] Parsing APPINDEX file:", appIndexPath)
|
||||
entries, err := ParseAppIndex(appIndexPath)
|
||||
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))
|
||||
|
||||
// 3) Get install directory to check for updates
|
||||
installDir, err := GetInstallDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("[INFO] Install directory:", installDir)
|
||||
|
||||
// 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)
|
||||
|
||||
// Find matching entry from the parsed APPINDEX
|
||||
var matchingEntry *AppIndexEntry
|
||||
for _, e := range entries {
|
||||
if e.Name == spec.Name &&
|
||||
e.Release == spec.Release &&
|
||||
e.Type == spec.Type &&
|
||||
e.OS == spec.OS &&
|
||||
e.Arch == spec.Arch {
|
||||
matchingEntry = &e
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if matchingEntry == nil {
|
||||
fmt.Printf("[WARN] No matching APPINDEX entry found for package: %s (%s)\n", spec.Name, spec.Release)
|
||||
continue
|
||||
}
|
||||
fmt.Printf("[INFO] Found matching APPINDEX entry: %+v\n", *matchingEntry)
|
||||
|
||||
// // Check if an update is needed
|
||||
// updateNeeded, err := IsUpdateNeeded(
|
||||
// installDir,
|
||||
// matchingEntry.Name,
|
||||
// matchingEntry.Release,
|
||||
// matchingEntry.Version,
|
||||
// matchingEntry.Arch,
|
||||
// matchingEntry.OS,
|
||||
// )
|
||||
// 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)
|
||||
// continue
|
||||
// }
|
||||
|
||||
// 5) Download the package
|
||||
downloadDir := GetTempDir()
|
||||
fmt.Printf("[INFO] Downloading package '%s' to temporary folder: %s\n", matchingEntry.Name, downloadDir)
|
||||
if err := DownloadPackageFromAppIndex(
|
||||
appIndexPath,
|
||||
matchingEntry.Name,
|
||||
matchingEntry.Release,
|
||||
matchingEntry.Type,
|
||||
downloadDir,
|
||||
); err != nil {
|
||||
return fmt.Errorf("[ERROR] Failed to download package '%s': %w", matchingEntry.Name, err)
|
||||
}
|
||||
fmt.Printf("[INFO] Package '%s' downloaded successfully to: %s\n", matchingEntry.Name, downloadDir)
|
||||
|
||||
// 6) Decompress the package
|
||||
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
|
||||
}
|
90
spm/decompress.go
Normal file
90
spm/decompress.go
Normal file
|
@ -0,0 +1,90 @@
|
|||
package spm
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// DecompressPackage determines the package format and decompresses it
|
||||
func DecompressPackage(downloadDir, packageName, arch, osName, pkgType, release, version string) (string, error) {
|
||||
// 1) Construct the .tar.gz name
|
||||
expectedFileName := fmt.Sprintf(
|
||||
"%s@%s@%s@%s@%s@%s.tar.gz",
|
||||
packageName, arch, osName, pkgType, release, version,
|
||||
)
|
||||
packagePath := filepath.Join(downloadDir, expectedFileName)
|
||||
|
||||
// Check that file exists
|
||||
if _, err := os.Stat(packagePath); os.IsNotExist(err) {
|
||||
return "", fmt.Errorf("package file not found: %s", packagePath)
|
||||
}
|
||||
|
||||
// 2) Build the folder path (minus ".tar.gz")
|
||||
folderName := strings.TrimSuffix(expectedFileName, ".tar.gz")
|
||||
tempDir := GetTempDir() // e.g. C:\Users\<User>\AppData\Local\Temp\spm_temp_164326
|
||||
decompressDir := filepath.Join(tempDir, folderName)
|
||||
|
||||
// Ensure the folder
|
||||
if err := os.MkdirAll(decompressDir, 0755); err != nil {
|
||||
return "", fmt.Errorf("failed to create decompressDir: %w", err)
|
||||
}
|
||||
|
||||
// 3) Decompress everything into `decompressDir`
|
||||
if err := decompressTarGz(packagePath, decompressDir); err != nil {
|
||||
return "", fmt.Errorf("failed to decompress: %w", err)
|
||||
}
|
||||
|
||||
// Return the folder path we used
|
||||
return decompressDir, nil
|
||||
}
|
||||
|
||||
func decompressTarGz(srcFile, destDir string) error {
|
||||
f, err := os.Open(srcFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
gzr, err := gzip.NewReader(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer gzr.Close()
|
||||
|
||||
tarReader := tar.NewReader(gzr)
|
||||
for {
|
||||
header, err := tarReader.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
outPath := filepath.Join(destDir, header.Name)
|
||||
switch header.Typeflag {
|
||||
case tar.TypeDir:
|
||||
if err := os.MkdirAll(outPath, os.FileMode(header.Mode)); err != nil {
|
||||
return err
|
||||
}
|
||||
case tar.TypeReg:
|
||||
outFile, err := os.Create(outPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(outFile, tarReader)
|
||||
outFile.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
// huh
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
144
spm/dirs.go
Normal file
144
spm/dirs.go
Normal file
|
@ -0,0 +1,144 @@
|
|||
package spm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// global sync and variable for generated temp dir
|
||||
var (
|
||||
tempDirOnce sync.Once
|
||||
tempDirPath string
|
||||
)
|
||||
|
||||
// global variables for install dir
|
||||
var (
|
||||
installMu sync.Mutex
|
||||
installedDir string
|
||||
installEnvVar = "SPITFIRE_INSTALL_DIR"
|
||||
)
|
||||
|
||||
// GetTempDir generates or retrieves a unique temp dir.
|
||||
func GetTempDir() string {
|
||||
tempDirOnce.Do(func() {
|
||||
// Generate a unique temp dir name
|
||||
tempDirPath = filepath.Join(os.TempDir(), fmt.Sprintf("spm_temp_%d", rand.Intn(1000000)))
|
||||
|
||||
// Ensure the dir exists
|
||||
if err := os.MkdirAll(tempDirPath, os.ModePerm); err != nil {
|
||||
fmt.Printf("[ERROR] Failed to create temp directory: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf("[INFO] Using temp directory: %s\n", tempDirPath)
|
||||
}
|
||||
})
|
||||
return tempDirPath
|
||||
}
|
||||
|
||||
// GetDefaultInstallDir generates the default installation dir
|
||||
// based on the OS and environment, then also sets it via SetInstallDir.
|
||||
//
|
||||
// Please use GetInstallDir() instead of GetDefaultInstallDir() when interacting with spm.
|
||||
func GetDefaultInstallDir() (string, error) {
|
||||
var installDir string
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
// Use C:\Program Files
|
||||
programFiles := os.Getenv("ProgramFiles")
|
||||
if programFiles == "" {
|
||||
return "", fmt.Errorf("unable to determine default install directory on Windows")
|
||||
}
|
||||
installDir = filepath.Join(programFiles, "Spitfire")
|
||||
|
||||
case "darwin":
|
||||
// Use ~/Library/Application Support on macOS
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to determine home directory on macOS: %w", err)
|
||||
}
|
||||
installDir = filepath.Join(homeDir, "Library", "Application Support", "Spitfire")
|
||||
|
||||
case "linux":
|
||||
// Use ~/.local/share/Spitfire on Linux
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to determine home directory on Linux: %w", err)
|
||||
}
|
||||
installDir = filepath.Join(homeDir, ".local", "share", "Spitfire")
|
||||
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
|
||||
}
|
||||
|
||||
// Also store it globally so future calls to GetInstallDir() return the same
|
||||
SetInstallDir(installDir)
|
||||
return installDir, nil
|
||||
}
|
||||
|
||||
// SetDownloadFolder ensures customDir exists, returns it
|
||||
func SetDownloadFolder(customDir string) (string, error) {
|
||||
if err := os.MkdirAll(customDir, os.ModePerm); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return customDir, nil
|
||||
}
|
||||
|
||||
// SetInstallDir sets the global install dir variable and updates the persistent environment variable.
|
||||
func SetInstallDir(path string) error {
|
||||
installMu.Lock()
|
||||
defer installMu.Unlock()
|
||||
|
||||
installedDir = path
|
||||
|
||||
// Persist the environment variable on Windows
|
||||
if runtime.GOOS == "windows" {
|
||||
err := persistSystemEnvVar(installEnvVar, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// For non-Windows platforms, just set it in the current process environment
|
||||
err := os.Setenv(installEnvVar, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetInstallDir returns the currently set install dir if available.
|
||||
// Otherwise, it calls GetDefaultInstallDir() and sets that.
|
||||
func GetInstallDir() (string, error) {
|
||||
|
||||
// If already set, return it
|
||||
if installedDir != "" {
|
||||
return installedDir, nil
|
||||
}
|
||||
|
||||
// Check if it's stored in the system environment variable
|
||||
if envDir := os.Getenv(installEnvVar); envDir != "" {
|
||||
installedDir = envDir
|
||||
return installedDir, nil
|
||||
}
|
||||
|
||||
// Compute and store the default dir if not already set
|
||||
defDir, err := GetDefaultInstallDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
installedDir = defDir
|
||||
|
||||
// Persist the default dir as an environment variable on Windows
|
||||
if runtime.GOOS == "windows" {
|
||||
_ = persistSystemEnvVar(installEnvVar, defDir)
|
||||
} else {
|
||||
_ = os.Setenv(installEnvVar, defDir)
|
||||
}
|
||||
|
||||
return defDir, nil
|
||||
}
|
219
spm/download.go
219
spm/download.go
|
@ -1,91 +1,128 @@
|
|||
package spm
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
type Package struct {
|
||||
Name string
|
||||
Arch string
|
||||
OS string
|
||||
DownloadURL string
|
||||
}
|
||||
|
||||
func DownloadPackage(pkg Package, destDir string) error {
|
||||
UpdateProgress(0, fmt.Sprintf("Downloading %s", pkg.Name))
|
||||
resp, err := http.Get(pkg.DownloadURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
filePath := filepath.Join(destDir, filepath.Base(pkg.DownloadURL))
|
||||
out, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
totalSize := resp.ContentLength
|
||||
var downloaded int64
|
||||
|
||||
// Track progress as bytes are downloaded
|
||||
buf := make([]byte, 1024)
|
||||
for {
|
||||
n, err := resp.Body.Read(buf)
|
||||
if n > 0 {
|
||||
downloaded += int64(n)
|
||||
percentage := int(float64(downloaded) / float64(totalSize) * 100)
|
||||
UpdateProgress(percentage, fmt.Sprintf("Downloading %s", pkg.Name))
|
||||
if _, err := out.Write(buf[:n]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
UpdateProgress(100, fmt.Sprintf("%s downloaded", pkg.Name))
|
||||
return nil
|
||||
}
|
||||
|
||||
func FindPackage(entries []AppIndexEntry, name, release, arch, os string) (*AppIndexEntry, error) {
|
||||
for _, entry := range entries {
|
||||
if entry.Name == name && entry.Release == release && entry.Arch == arch && entry.OS == os {
|
||||
return &entry, nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("package not found")
|
||||
}
|
||||
|
||||
func DownloadPackageFromAppIndex(appIndexPath, packageName, release, destDir string) error {
|
||||
arch := runtime.GOARCH
|
||||
osName := runtime.GOOS
|
||||
|
||||
entries, err := ParseAppIndex(appIndexPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
entry, err := FindPackage(entries, packageName, release, arch, osName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return DownloadPackage(Package{
|
||||
Name: entry.Name,
|
||||
Arch: entry.Arch,
|
||||
OS: entry.OS,
|
||||
DownloadURL: entry.DownloadURL,
|
||||
}, destDir)
|
||||
}
|
||||
package spm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DownloadPackageFromAppIndex selects and downloads the correct package from the APPINDEX.
|
||||
func DownloadPackageFromAppIndex(appIndexPath string, packageName string, release string, pkgType string, destDir string) error {
|
||||
// Parse the APPINDEX
|
||||
entries, err := ParseAppIndex(appIndexPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse APPINDEX: %w", err)
|
||||
}
|
||||
|
||||
// Find the right entry
|
||||
var selected *AppIndexEntry
|
||||
for _, e := range entries {
|
||||
if e.Name == packageName &&
|
||||
e.Release == release &&
|
||||
e.Type == pkgType &&
|
||||
e.OS == runtime.GOOS &&
|
||||
e.Arch == runtime.GOARCH {
|
||||
selected = &e
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Handle no matching entry
|
||||
if selected == nil {
|
||||
return fmt.Errorf("package not found in APPINDEX: %s (release: %s, type: %s, os: %s, arch: %s)", packageName, release, pkgType, runtime.GOOS, runtime.GOARCH)
|
||||
}
|
||||
|
||||
// Check if the package is already installed and up-to-date
|
||||
installDir, err := GetInstallDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get install directory: %w", err)
|
||||
}
|
||||
needsUpdate, err := IsUpdateNeeded(installDir, packageName, release, selected.Version, selected.Arch, selected.OS)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check update status: %w", err)
|
||||
}
|
||||
if !needsUpdate {
|
||||
UpdateProgress(0, "Already up-to-date, skipping download.")
|
||||
return nil // Skip download
|
||||
}
|
||||
|
||||
// Download the package
|
||||
UpdateProgress(0, fmt.Sprintf("Downloading %s %s (%s)...", packageName, selected.Version, selected.Type))
|
||||
resp, err := http.Get(selected.DownloadURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to download package: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Save the downloaded file
|
||||
downloadedFileName := filepath.Base(selected.DownloadURL)
|
||||
downloadedFilePath := filepath.Join(destDir, downloadedFileName)
|
||||
|
||||
out, err := os.OpenFile(downloadedFilePath, os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create output file: %w", err)
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
totalSize := resp.ContentLength
|
||||
var downloaded int64
|
||||
buf := make([]byte, 32*1024) // Use a larger buffer for efficiency
|
||||
|
||||
for {
|
||||
n, errRead := resp.Body.Read(buf)
|
||||
if n > 0 {
|
||||
downloaded += int64(n)
|
||||
percentage := int(float64(downloaded) / float64(totalSize) * 100)
|
||||
UpdateProgress(percentage, fmt.Sprintf("Downloading %s %s (%s)...", packageName, selected.Version, selected.Type))
|
||||
if _, errWrite := out.Write(buf[:n]); errWrite != nil {
|
||||
return fmt.Errorf("failed to write to output file: %w", errWrite)
|
||||
}
|
||||
}
|
||||
if errRead == io.EOF {
|
||||
break
|
||||
}
|
||||
if errRead != nil {
|
||||
return fmt.Errorf("error while reading response: %w", errRead)
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure the file handle is closed before renaming
|
||||
out.Close()
|
||||
|
||||
// Construct the expected filename
|
||||
expectedFileName := fmt.Sprintf("%s@%s@%s@%s@%s@%s.tar.gz",
|
||||
packageName, selected.Arch, selected.OS, selected.Type, selected.Release, selected.Version)
|
||||
|
||||
expectedFilePath := filepath.Join(destDir, expectedFileName)
|
||||
|
||||
// I dont know why is this happening, I dont want to know but sometimes some process is helding up the donwloaded files so thats why it retries here
|
||||
maxRetries := 5
|
||||
for i := 0; i < maxRetries; i++ {
|
||||
err = os.Rename(downloadedFilePath, expectedFilePath)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
|
||||
// Check if file is in use
|
||||
f, checkErr := os.Open(downloadedFilePath)
|
||||
if checkErr != nil {
|
||||
return fmt.Errorf("file is locked by another process: %w", checkErr)
|
||||
}
|
||||
f.Close()
|
||||
|
||||
if i < maxRetries-1 {
|
||||
time.Sleep(500 * time.Millisecond) // Wait before retrying
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to rename downloaded file after retries: %w", err)
|
||||
}
|
||||
|
||||
UpdateProgress(100, fmt.Sprintf("Downloaded %s %s (%s).", packageName, selected.Version, selected.Type))
|
||||
return nil
|
||||
}
|
||||
|
|
7
spm/go.mod
Normal file
7
spm/go.mod
Normal file
|
@ -0,0 +1,7 @@
|
|||
module spm
|
||||
|
||||
go 1.21
|
||||
|
||||
require gopkg.in/ini.v1 v1.67.0
|
||||
|
||||
require github.com/stretchr/testify v1.10.0 // indirect
|
10
spm/go.sum
Normal file
10
spm/go.sum
Normal file
|
@ -0,0 +1,10 @@
|
|||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
465
spm/install.go
465
spm/install.go
|
@ -1,161 +1,304 @@
|
|||
package spm
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func DecompressToTemp(filePath string) (string, error) {
|
||||
UpdateProgress(0, "Decompressing package")
|
||||
tempDir, err := os.MkdirTemp("", "spm_decompress")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
gzr, err := gzip.NewReader(file)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer gzr.Close()
|
||||
|
||||
tarReader := tar.NewReader(gzr)
|
||||
var totalFiles, processedFiles int
|
||||
// Count total files
|
||||
for {
|
||||
_, err := tarReader.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
totalFiles++
|
||||
}
|
||||
|
||||
file.Seek(0, io.SeekStart)
|
||||
gzr.Reset(file)
|
||||
tarReader = tar.NewReader(gzr)
|
||||
|
||||
for {
|
||||
header, err := tarReader.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
targetPath := filepath.Join(tempDir, header.Name)
|
||||
switch header.Typeflag {
|
||||
case tar.TypeDir:
|
||||
if err := os.MkdirAll(targetPath, os.FileMode(header.Mode)); err != nil {
|
||||
return "", err
|
||||
}
|
||||
case tar.TypeReg:
|
||||
outFile, err := os.Create(targetPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, err := io.Copy(outFile, tarReader); err != nil {
|
||||
return "", err
|
||||
}
|
||||
outFile.Close()
|
||||
}
|
||||
|
||||
processedFiles++
|
||||
UpdateProgress(int(float64(processedFiles)/float64(totalFiles)*100), "Decompressing package")
|
||||
}
|
||||
|
||||
UpdateProgress(100, "Package decompressed")
|
||||
return tempDir, nil
|
||||
}
|
||||
|
||||
func MoveFilesToInstallDir(tempDir, installDir string) error {
|
||||
// Count total files to copy
|
||||
var totalFiles, copiedFiles int
|
||||
err := filepath.Walk(tempDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
totalFiles++
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Copy files and track progress
|
||||
err = filepath.Walk(tempDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
relPath, err := filepath.Rel(tempDir, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
targetPath := filepath.Join(installDir, relPath)
|
||||
if info.IsDir() {
|
||||
// Create directories in the install directory
|
||||
if err := os.MkdirAll(targetPath, os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// Copy files to the install directory
|
||||
if err := copyFile(path, targetPath); err != nil {
|
||||
return err
|
||||
}
|
||||
copiedFiles++
|
||||
UpdateProgress(int(float64(copiedFiles)/float64(totalFiles)*100), "Copying files to install directory")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Clean up temporary directory
|
||||
UpdateProgress(100, "Cleaning up temporary files")
|
||||
return os.RemoveAll(tempDir)
|
||||
}
|
||||
|
||||
// copyFile copies the contents of the source file to the destination file.
|
||||
func copyFile(src, dst string) error {
|
||||
sourceFile, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer sourceFile.Close()
|
||||
|
||||
// Create the destination file
|
||||
destinationFile, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer destinationFile.Close()
|
||||
|
||||
// Copy the file content
|
||||
if _, err := io.Copy(destinationFile, sourceFile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Preserve file permissions
|
||||
info, err := sourceFile.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Chmod(dst, info.Mode())
|
||||
}
|
||||
package spm
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bufio"
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
func DecompressToTemp(filePath string) (string, error) {
|
||||
UpdateProgress(0, "Decompressing package")
|
||||
|
||||
// 1) Base temp dir
|
||||
baseTempDir := GetTempDir()
|
||||
|
||||
// 2) Create a unique subfolder inside the base temp dir
|
||||
subfolderName := fmt.Sprintf("spm_decompress_%d", rand.Intn(1000000))
|
||||
decompressDir := filepath.Join(baseTempDir, subfolderName)
|
||||
if err := os.MkdirAll(decompressDir, 0755); err != nil {
|
||||
return "", fmt.Errorf("failed to create decompress dir: %w", err)
|
||||
}
|
||||
|
||||
// 3) Open the tar.gz file
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
gzr, err := gzip.NewReader(file)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer gzr.Close()
|
||||
|
||||
tarReader := tar.NewReader(gzr)
|
||||
|
||||
// 4) Count total files
|
||||
var totalFiles, processedFiles int
|
||||
for {
|
||||
_, err := tarReader.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
totalFiles++
|
||||
}
|
||||
|
||||
// 5) Reset file position and tar reader
|
||||
if _, err := file.Seek(0, io.SeekStart); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := gzr.Reset(file); err != nil {
|
||||
return "", err
|
||||
}
|
||||
tarReader = tar.NewReader(gzr)
|
||||
|
||||
// 6) Extract into `decompressDir`
|
||||
for {
|
||||
header, err := tarReader.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
targetPath := filepath.Join(decompressDir, header.Name)
|
||||
switch header.Typeflag {
|
||||
case tar.TypeDir:
|
||||
if err := os.MkdirAll(targetPath, os.FileMode(header.Mode)); err != nil {
|
||||
return "", err
|
||||
}
|
||||
case tar.TypeReg:
|
||||
outFile, err := os.Create(targetPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, err := io.Copy(outFile, tarReader); err != nil {
|
||||
outFile.Close()
|
||||
return "", err
|
||||
}
|
||||
outFile.Close()
|
||||
}
|
||||
|
||||
processedFiles++
|
||||
UpdateProgress(int(float64(processedFiles)/float64(totalFiles)*100), "Decompressing package")
|
||||
}
|
||||
|
||||
UpdateProgress(100, "Package decompressed")
|
||||
return decompressDir, nil
|
||||
}
|
||||
|
||||
// tailLogFile continuously reads new lines from logFile until done is closed.
|
||||
// It prints each new line and searches for a percentage (e.g. " 47%") to call UpdateProgress.
|
||||
func tailLogFile(logFile string, done <-chan struct{}, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
|
||||
var offset int64
|
||||
re := regexp.MustCompile(`\s+(\d+)%`)
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
default:
|
||||
f, err := os.Open(logFile)
|
||||
if err != nil {
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
// Seek to the last known offset.
|
||||
f.Seek(offset, io.SeekStart)
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
fmt.Printf("[ROBOPROGRESS-LOG] %s\n", line)
|
||||
// Look for a percentage.
|
||||
if matches := re.FindStringSubmatch(line); len(matches) == 2 {
|
||||
var percent int
|
||||
_, err := fmt.Sscanf(matches[1], "%d", &percent)
|
||||
if err == nil {
|
||||
fmt.Printf("[ROBOPROGRESS] Parsed progress: %d%%\n", percent)
|
||||
UpdateProgress(percent, "Copying files to install directory")
|
||||
}
|
||||
}
|
||||
}
|
||||
// Update the offset.
|
||||
newOffset, _ := f.Seek(0, io.SeekCurrent)
|
||||
offset = newOffset
|
||||
f.Close()
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MoveFilesToInstallDir copies files from tempDir to installDir.
|
||||
// On Windows it uses robocopy with logging so that as much output as possible is printed,
|
||||
// and a separate goroutine tails the log file to update progress.
|
||||
// The log file is saved (not automatically deleted) so you can inspect it.
|
||||
func MoveFilesToInstallDir(tempDir, installDir, pkgType string) error {
|
||||
// Ensure tempDir exists.
|
||||
if _, err := os.Stat(tempDir); os.IsNotExist(err) {
|
||||
return fmt.Errorf("tempDir does not exist: %s", tempDir)
|
||||
}
|
||||
|
||||
// If package type is "browser", adjust installDir.
|
||||
if pkgType == "browser" {
|
||||
installDir = filepath.Join(installDir, "browser")
|
||||
}
|
||||
|
||||
// Ensure destination exists.
|
||||
if err := os.MkdirAll(installDir, os.ModePerm); err != nil {
|
||||
return fmt.Errorf("failed to create installDir: %w", err)
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
// Create a temporary log file.
|
||||
logFile := filepath.Join(os.TempDir(), fmt.Sprintf("robocopy_%d.log", time.Now().UnixNano()))
|
||||
// Print out the log file path so you can locate it manually.
|
||||
fmt.Printf("[INFO] Robocopy log file: %s\n", logFile)
|
||||
|
||||
// Build robocopy command.
|
||||
// /E: copy subdirectories (including empty ones)
|
||||
// /TEE: output to console as well as to the log file.
|
||||
// /LOG:<logFile>: write output to the log file.
|
||||
// We remove extra suppression flags so that robocopy prints as much as possible.
|
||||
cmd := exec.Command("robocopy", tempDir, installDir, "/E", "/TEE", fmt.Sprintf("/LOG:%s", logFile))
|
||||
|
||||
// Start robocopy.
|
||||
if err := cmd.Start(); err != nil {
|
||||
return fmt.Errorf("failed to start robocopy: %w", err)
|
||||
}
|
||||
|
||||
// Set up a goroutine to tail the log file.
|
||||
doneTail := make(chan struct{})
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go tailLogFile(logFile, doneTail, &wg)
|
||||
|
||||
// Wait for robocopy to complete.
|
||||
err := cmd.Wait()
|
||||
// Signal the tail goroutine to stop.
|
||||
close(doneTail)
|
||||
wg.Wait()
|
||||
|
||||
// Robocopy returns exit codes less than 8 as success.
|
||||
if err != nil {
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
if exitCode := exitErr.ExitCode(); exitCode >= 8 {
|
||||
return fmt.Errorf("robocopy failed: exit status %d", exitCode)
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("robocopy failed: %w", err)
|
||||
}
|
||||
}
|
||||
// Mark progress as complete.
|
||||
UpdateProgress(100, "Copying files to install directory")
|
||||
|
||||
// (Optional) If you want the log file to be removed automatically, uncomment the next line.
|
||||
// os.Remove(logFile)
|
||||
} else {
|
||||
// Non-Windows fallback: copy files one-by-one.
|
||||
var totalFiles, copiedFiles int
|
||||
err := filepath.Walk(tempDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
totalFiles++
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = filepath.Walk(tempDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
relPath, err := filepath.Rel(tempDir, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
targetPath := filepath.Join(installDir, relPath)
|
||||
if info.IsDir() {
|
||||
if err := os.MkdirAll(targetPath, os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := copyFile(path, targetPath); err != nil {
|
||||
return err
|
||||
}
|
||||
copiedFiles++
|
||||
UpdateProgress(int(float64(copiedFiles)/float64(totalFiles)*100), "Copying files to install directory")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up temporary directory.
|
||||
UpdateProgress(100, "Cleaning up temporary files")
|
||||
return os.RemoveAll(tempDir)
|
||||
}
|
||||
|
||||
// copyFile copies the contents of the source file to the destination file.
|
||||
func copyFile(src, dst string) error {
|
||||
sourceFile, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer sourceFile.Close()
|
||||
|
||||
// Create the destination file
|
||||
destinationFile, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer destinationFile.Close()
|
||||
|
||||
// Copy the file content
|
||||
if _, err := io.Copy(destinationFile, sourceFile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Preserve file permissions
|
||||
info, err := sourceFile.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Chmod(dst, info.Mode())
|
||||
}
|
||||
|
||||
// FinalizeInstall finalizes the installation by updating installed.ini.
|
||||
func FinalizeInstall(packageName, release, version, arch, osName string) error {
|
||||
installDir, err := GetInstallDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pkgInfo := AppIndexEntry{
|
||||
Name: packageName,
|
||||
Version: version,
|
||||
Release: release,
|
||||
Arch: arch,
|
||||
OS: osName,
|
||||
}
|
||||
return UpdateInstalledPackage(installDir, pkgInfo)
|
||||
}
|
||||
|
|
204
spm/installed_pacakges.go
Normal file
204
spm/installed_pacakges.go
Normal file
|
@ -0,0 +1,204 @@
|
|||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
// }
|
||||
// }
|
|
@ -1,30 +1,31 @@
|
|||
package spm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Progress struct {
|
||||
mu sync.Mutex
|
||||
percentage int
|
||||
task string
|
||||
}
|
||||
|
||||
var progress = Progress{}
|
||||
|
||||
// UpdateProgress sets the current percentage and task.
|
||||
func UpdateProgress(percentage int, task string) {
|
||||
progress.mu.Lock()
|
||||
defer progress.mu.Unlock()
|
||||
progress.percentage = percentage
|
||||
progress.task = task
|
||||
fmt.Printf("\r[%3d%%] %s", percentage, task) // Print progress to the terminal
|
||||
}
|
||||
|
||||
// GetProgress returns the current progress state.
|
||||
func GetProgress() (int, string) {
|
||||
progress.mu.Lock()
|
||||
defer progress.mu.Unlock()
|
||||
return progress.percentage, progress.task
|
||||
}
|
||||
package spm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Progress struct {
|
||||
mu sync.Mutex
|
||||
percentage int
|
||||
task string
|
||||
}
|
||||
|
||||
var progress = Progress{}
|
||||
|
||||
// UpdateProgress sets the current percentage and task.
|
||||
func UpdateProgress(percentage int, task string) {
|
||||
progress.mu.Lock()
|
||||
defer progress.mu.Unlock()
|
||||
progress.percentage = percentage
|
||||
progress.task = task
|
||||
fmt.Printf("\r[%3d%%] %s", percentage, task) // Print progress to the terminal
|
||||
// Next line on 100% ?
|
||||
}
|
||||
|
||||
// GetProgress returns the current progress state.
|
||||
func GetProgress() (int, string) {
|
||||
progress.mu.Lock()
|
||||
defer progress.mu.Unlock()
|
||||
return progress.percentage, progress.task
|
||||
}
|
||||
|
|
71
spm/run.go
Normal file
71
spm/run.go
Normal file
|
@ -0,0 +1,71 @@
|
|||
package spm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// Run locates and starts the installed Spitfire browser without waiting for it to exit.
|
||||
func Run() error {
|
||||
installDir, err := GetInstallDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
exePath := filepath.Join(installDir, "browser", "spitfire.exe")
|
||||
if runtime.GOOS != "windows" {
|
||||
exePath = filepath.Join(installDir, "browser", "spitfire")
|
||||
}
|
||||
|
||||
cmd := exec.Command(exePath)
|
||||
cmd.Dir = filepath.Join(installDir, "browser")
|
||||
return cmd.Start()
|
||||
}
|
||||
|
||||
// RunAndWait locates and starts the installed Spitfire browser and waits for it to exit.
|
||||
func RunAndWait() error {
|
||||
installDir, err := GetInstallDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get install directory: %w", err)
|
||||
}
|
||||
|
||||
// Construct the browser executable path
|
||||
exePath := filepath.Join(installDir, "browser", "spitfire.exe")
|
||||
if runtime.GOOS != "windows" {
|
||||
exePath = filepath.Join(installDir, "browser", "spitfire")
|
||||
}
|
||||
|
||||
// Check if the browser executable exists
|
||||
if _, err := os.Stat(exePath); err != nil {
|
||||
return fmt.Errorf("browser executable not found at %s: %w", exePath, err)
|
||||
}
|
||||
|
||||
// Create the command
|
||||
cmd := exec.Command(exePath)
|
||||
|
||||
// Attach standard output and error for debugging
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
// Start the browser process
|
||||
fmt.Printf("Starting browser: %s\n", exePath)
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start browser: %w", err)
|
||||
}
|
||||
|
||||
// Print the PID for debugging
|
||||
fmt.Printf("Browser process started with PID %d\n", cmd.Process.Pid)
|
||||
|
||||
// Wait for the process to exit
|
||||
err = cmd.Wait()
|
||||
if err != nil {
|
||||
return fmt.Errorf("browser exited with error: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("Browser exited successfully.")
|
||||
return nil
|
||||
}
|
97
spm/utils.go
97
spm/utils.go
|
@ -1,71 +1,26 @@
|
|||
package spm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
func GetTempDownloadDir() string {
|
||||
dir, err := os.MkdirTemp("", "spm_downloads")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
func SetDownloadFolder(customDir string) (string, error) {
|
||||
if err := os.MkdirAll(customDir, os.ModePerm); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return customDir, nil
|
||||
}
|
||||
|
||||
// GetDefaultInstallDir generates the default installation directory based on the OS and environment.
|
||||
func GetDefaultInstallDir() (string, error) {
|
||||
var installDir string
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
// Use %APPDATA% or Program Files on Windows
|
||||
appData := os.Getenv("APPDATA")
|
||||
if appData != "" {
|
||||
installDir = filepath.Join(appData, "Spitfire")
|
||||
} else {
|
||||
programFiles := os.Getenv("ProgramFiles")
|
||||
if programFiles == "" {
|
||||
return "", fmt.Errorf("unable to determine default install directory on Windows")
|
||||
}
|
||||
installDir = filepath.Join(programFiles, "Spitfire")
|
||||
}
|
||||
|
||||
case "darwin":
|
||||
// Use ~/Library/Application Support on macOS
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to determine home directory on macOS: %w", err)
|
||||
}
|
||||
installDir = filepath.Join(homeDir, "Library", "Application Support", "Spitfire")
|
||||
|
||||
case "linux":
|
||||
// Use ~/.local/share or /opt on Linux
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to determine home directory on Linux: %w", err)
|
||||
}
|
||||
installDir = filepath.Join(homeDir, ".local", "share", "Spitfire")
|
||||
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
|
||||
}
|
||||
|
||||
return installDir, nil
|
||||
}
|
||||
|
||||
// DecompressPackage determines the appropriate package format and decompresses it.
|
||||
func DecompressPackage(downloadDir string) (string, error) {
|
||||
osName := runtime.GOOS
|
||||
packagePath := filepath.Join(downloadDir, fmt.Sprintf("browser-amd64-nightly-%s.tar.gz", osName)) // If file naming changes this will break!!
|
||||
return DecompressToTemp(packagePath)
|
||||
}
|
||||
package spm
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
// persistSystemEnvVar sets a persistent environment variable on Windows using the `setx` command.
|
||||
// Perhaps support for other systems would be needed, but all of this "Launcher" thingy is probably going to end up
|
||||
// being Windows-specific, as other superior systems have their own package managers.
|
||||
func persistSystemEnvVar(key, value string) error {
|
||||
cmd := exec.Command("cmd", "/C", "setx", key, value)
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsMatchingEntry checks if a package entry matches the requested specs
|
||||
func IsMatchingEntry(e AppIndexEntry, name, release, arch, osName, pkgType string) bool {
|
||||
return e.Name == name &&
|
||||
e.Release == release &&
|
||||
e.Arch == arch &&
|
||||
e.OS == osName &&
|
||||
e.Type == pkgType
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue