diff --git a/README.md b/README.md
new file mode 100644
index 0000000..63f036c
--- /dev/null
+++ b/README.md
@@ -0,0 +1,88 @@
+
+
+
+
+
+ Spitfire Package Manager (SPM)
+
+
+
+A package management library designed to handle automatic updates for Spitfire browser and all of its components.
+
+
+### SPM is currently used in:
+- [Spitfire Luncher](https://weforge.xyz/Spitfire/Luncher)
+- [Spitfire Insaller](https://weforge.xyz/Spitfire/Installer)
+
+### Example usage:
+
+To use SPM for downloading, decompressing, and installing updates, implement the following example in your Go application:
+```go
+package main
+
+import (
+ "fmt"
+ "runtime"
+
+ spm "weforge.xyz/Spitfire/SPM"
+)
+
+func main() {
+ // Define the packages to download
+ specs := []spm.AppIndexEntry{
+ {
+ Name: "spitfire-launcher",
+ Release: "nightly",
+ OS: runtime.GOOS,
+ Arch: runtime.GOARCH,
+ Type: "launcher",
+ },
+ {
+ Name: "spitfire-browser",
+ Release: "nightly",
+ OS: runtime.GOOS,
+ Arch: runtime.GOARCH,
+ Type: "browser",
+ },
+ }
+
+ // -- Download --
+ // spm.DownloadSpecified(specs) downloads specified packages to temp dir and decompresses them, making them ready for install by running "spm.InstallUpdates()".
+ fmt.Println("Starting download and decompression...")
+ if err := spm.DownloadSpecified(specs); err != nil {
+ fmt.Println("Error downloading packages:", err)
+ return
+ }
+
+ fmt.Println("Download complete. Proceeding with installation...")
+
+ // -- Install --
+ // Install and Download are separate as you cannot replace running binaries on Windows. So the final move to the correct folder is done by "spm.InstallUpdates()".
+ if err := spm.InstallUpdates(); err != nil {
+ fmt.Println("Error during installation:", err)
+ return
+ }
+
+ // -- Register --
+ // spm.RegisterApp() is primarily used to modify the Windows registry so it recognizes Spitfire Browser as an installed program.
+ // You shouldn’t need to run it more than once during installation. Also this function requires administrative privileges on Windows to work correctly.
+ if err := spm.RegisterApp(); err != nil {
+ fmt.Println("Error registering app:", err)
+ return
+ }
+
+ fmt.Println("Installation completed successfully!")
+
+ // -- Run --
+ // This function detects the browser binary and runs it. Alternatively, you can also use spm.RunAndWait()
+ spm.Run()
+}
+```
+
+
+ License
+
+
+
+
+
diff --git a/appindex.go b/appindex.go
index 14a77be..848d59e 100644
--- a/appindex.go
+++ b/appindex.go
@@ -6,14 +6,46 @@ import (
"io"
"net/http"
"os"
+ "path/filepath"
+ "sort"
"strings"
+
+ "gopkg.in/ini.v1"
)
-const appIndexURL = "https://downloads.sourceforge.net/project/spitfire-browser/APPINDEX"
+// AppIndexEntry represents a single entry in an app index.
+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 DownloadAppIndex(dest string) error {
+// RemoteIndex represents a remote APPINDEX repository.
+type RemoteIndex struct {
+ Name string `json:"name"`
+ Link string `json:"link"`
+}
+
+var (
+ // defaultRemoteIndexes holds the default remote index.
+ defaultRemoteIndexes = []RemoteIndex{
+ {
+ Name: "default",
+ Link: "https://downloads.sourceforge.net/project/spitfire-browser/APPINDEX",
+ },
+ }
+ // remoteIndexes holds the current remote indexes in use.
+ remoteIndexes = defaultRemoteIndexes
+)
+
+// downloadAppIndex downloads an APPINDEX from the given URL and writes it to dest.
+func downloadAppIndex(url, dest string) error {
UpdateProgress(0, "Downloading APPINDEX")
- resp, err := http.Get(appIndexURL)
+ resp, err := http.Get(url)
if err != nil {
return err
}
@@ -52,25 +84,10 @@ func DownloadAppIndex(dest string) error {
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()
-
+// parseAppIndexFromReader parses an APPINDEX from any io.Reader.
+func parseAppIndexFromReader(r io.Reader) ([]AppIndexEntry, error) {
var entries []AppIndexEntry
- scanner := bufio.NewScanner(file)
+ scanner := bufio.NewScanner(r)
entry := AppIndexEntry{}
for scanner.Scan() {
@@ -112,12 +129,222 @@ func ParseAppIndex(filePath string) ([]AppIndexEntry, error) {
entries = append(entries, entry)
}
- // Log all parsed entries
- fmt.Printf("[INFO] Total parsed entries: %d\n", len(entries))
+ return entries, scanner.Err()
+}
+
+// parseAppIndex reads the APPINDEX file at filePath and parses its contents.
+func parseAppIndex(filePath string) ([]AppIndexEntry, error) {
+ file, err := os.Open(filePath)
+ if err != nil {
+ return nil, err
+ }
+ defer file.Close()
+
+ entries, err := parseAppIndexFromReader(file)
+ if err != nil {
+ return nil, err
+ }
+
+ fmt.Printf("[INFO] Total parsed entries from %s: %d\n", filePath, 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()
+ return entries, nil
+}
+
+// saveIndex saves the current list of remote indexes to an INI file located in
+// the spm directory under installDir, but only if it's different from the default.
+func saveIndex() error {
+ // Only save if remoteIndexes differs from defaultRemoteIndexes.
+ if len(remoteIndexes) == len(defaultRemoteIndexes) {
+ same := true
+ for i, ri := range remoteIndexes {
+ if ri != defaultRemoteIndexes[i] {
+ same = false
+ break
+ }
+ }
+ if same {
+ return nil
+ }
+ }
+
+ installDir, err := GetInstallDir()
+ if err != nil {
+ return err
+ }
+ spmDir := filepath.Join(installDir, "spm")
+ if err := os.MkdirAll(spmDir, 0755); err != nil {
+ return err
+ }
+ filePath := filepath.Join(spmDir, "sources.ini")
+
+ cfg := ini.Empty()
+ sec, err := cfg.NewSection("RemoteIndexes")
+ if err != nil {
+ return err
+ }
+ // Save each remote index as a key/value pair.
+ for _, ri := range remoteIndexes {
+ if _, err := sec.NewKey(ri.Name, ri.Link); err != nil {
+ return err
+ }
+ }
+
+ return cfg.SaveTo(filePath)
+}
+
+// loadIndex loads the list of remote indexes from an INI file located in
+// the spm directory under installDir. If the file is missing, it sets remoteIndexes to the default.
+func loadIndex() error {
+ installDir, err := GetInstallDir()
+ if err != nil {
+ return err
+ }
+ spmDir := filepath.Join(installDir, "spm")
+ filePath := filepath.Join(spmDir, "sources.ini")
+ cfg, err := ini.Load(filePath)
+ if err != nil {
+ // If file is missing or can't be loaded, use the default.
+ remoteIndexes = defaultRemoteIndexes
+ return nil
+ }
+ sec := cfg.Section("RemoteIndexes")
+ var loaded []RemoteIndex
+ for _, key := range sec.Keys() {
+ loaded = append(loaded, RemoteIndex{
+ Name: key.Name(),
+ Link: key.Value(),
+ })
+ }
+ remoteIndexes = loaded
+ return nil
+}
+
+// UpdateIndex downloads fresh APPINDEX files from all remote sources and saves them
+// into the temp directory. If the app is registered, it loads the remote indexes
+// from the INI file (or uses the default if not available) and saves them after updating.
+func UpdateIndex() error {
+ tempDir := GetTempDir()
+ var sources []RemoteIndex
+ if IsRegistered() {
+ // Try to load persisted remote indexes.
+ if err := loadIndex(); err != nil {
+ // If loading fails, fall back to the default remote indexes.
+ sources = defaultRemoteIndexes
+ } else {
+ sources = remoteIndexes
+ }
+ } else {
+ // Not registered: use default remote indexes.
+ sources = defaultRemoteIndexes
+ }
+
+ // Download each APPINDEX file.
+ for _, ri := range sources {
+ localPath := filepath.Join(tempDir, fmt.Sprintf("appindex_%s.txt", ri.Name))
+ if err := downloadAppIndex(ri.Link, localPath); err != nil {
+ return fmt.Errorf("[WARN] AppIndex: failed downloading %s: %v", ri.Link, err)
+ }
+ }
+
+ // If registered, save the current remote indexes.
+ if IsRegistered() {
+ if err := saveIndex(); err != nil {
+ return fmt.Errorf("[WARN] AppIndex: failed saving indexes: %v", err)
+ }
+ }
+ return nil
+}
+
+// GetIndex parses APPINDEX data from local files in the temp directory.
+// If a file is missing, it downloads the corresponding APPINDEX first.
+// If the app is registered, it loads remote indexes from the INI file.
+// Otherwise, it uses the default remote index.
+func GetIndex() ([]AppIndexEntry, error) {
+ var allEntries []AppIndexEntry
+ tempDir := GetTempDir()
+ var sources []RemoteIndex
+ if IsRegistered() {
+ if err := loadIndex(); err != nil {
+ sources = defaultRemoteIndexes
+ } else {
+ sources = remoteIndexes
+ }
+ } else {
+ sources = defaultRemoteIndexes
+ }
+
+ // For each remote source, ensure the APPINDEX file exists (downloading if needed),
+ // then parse its contents.
+ for _, ri := range sources {
+ localPath := filepath.Join(tempDir, fmt.Sprintf("appindex_%s.txt", ri.Name))
+ if _, err := os.Stat(localPath); os.IsNotExist(err) {
+ if err := downloadAppIndex(ri.Link, localPath); err != nil {
+ return nil, fmt.Errorf("[WARN] AppIndex: failed downloading %s: %v", ri.Link, err)
+ }
+ }
+ entries, err := parseAppIndex(localPath)
+ if err != nil {
+ return nil, fmt.Errorf("[WARN] AppIndex: failed parsing %s: %v", localPath, err)
+ }
+ allEntries = append(allEntries, entries...)
+ }
+ return allEntries, nil
+}
+
+// AddIndex adds a new remote index (name and link) into the list,
+// sorts the list by name, and if the app is registered, saves the updated list.
+func AddIndex(name, link string) error {
+ // If registered, load current indexes first.
+ if IsRegistered() {
+ if err := loadIndex(); err != nil {
+ return fmt.Errorf("[WARN] AppIndex: failed loading indexes: %w", err)
+ }
+ }
+
+ ri := RemoteIndex{
+ Name: name,
+ Link: link,
+ }
+ remoteIndexes = append(remoteIndexes, ri)
+ sort.Slice(remoteIndexes, func(i, j int) bool {
+ return remoteIndexes[i].Name < remoteIndexes[j].Name
+ })
+
+ // If registered, persist the changes.
+ if IsRegistered() {
+ if err := saveIndex(); err != nil {
+ return fmt.Errorf("[WARN] AppIndex: failed saving indexes: %w", err)
+ }
+ }
+ return nil
+}
+
+// RemoveIndex removes any remote index with the given name from the list,
+// and if the app is registered, saves the updated list.
+func RemoveIndex(name string) error {
+ // If registered, load current indexes first.
+ if IsRegistered() {
+ if err := loadIndex(); err != nil {
+ return fmt.Errorf("[WARN] AppIndex: failed loading indexes: %w", err)
+ }
+ }
+
+ var updated []RemoteIndex
+ for _, ri := range remoteIndexes {
+ if ri.Name != name {
+ updated = append(updated, ri)
+ }
+ }
+ remoteIndexes = updated
+
+ // If registered, persist the changes.
+ if IsRegistered() {
+ if err := saveIndex(); err != nil {
+ return fmt.Errorf("[WARN] AppIndex: failed saving indexes: %w", err)
+ }
+ }
+ return nil
}
diff --git a/auto.go b/auto.go
index 92e574a..d544a44 100644
--- a/auto.go
+++ b/auto.go
@@ -2,7 +2,6 @@ package spm
import (
"fmt"
- "os"
"path/filepath"
)
@@ -10,22 +9,19 @@ import (
// 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
+// DownloadUpdates 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 {
+// and decompresses it into a temporary folder. The result is stored in pendingUpdates, so it can be used by InstallUpdates().
+func DownloadUpdates() 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)
+ err := UpdateIndex()
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)
+ entries, err := GetIndex()
if err != nil {
return fmt.Errorf("[ERROR] Failed to parse APPINDEX: %w", err)
}
@@ -84,7 +80,7 @@ func AutoDownloadUpdates() error {
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)
+ err = DownloadPackageFromAppIndex(matchingEntry.Name, matchingEntry.Release, matchingEntry.Type, downloadDir)
if err != nil {
return fmt.Errorf("[ERROR] Failed to download package '%s': %w", matchingEntry.Name, err)
}
@@ -99,7 +95,7 @@ func AutoDownloadUpdates() error {
}
fmt.Printf("[INFO] Package '%s' decompressed successfully to: %s\n", matchingEntry.Name, tempDir)
- // 7) Store in pendingUpdates so that AutoInstallUpdates can finish the job
+ // 7) Store in pendingUpdates so that InstallUpdates can finish the job
fmt.Printf("[INFO] Adding '%s' to pending updates\n", matchingEntry.Name)
pendingUpdates = append(pendingUpdates, AppIndexEntry{
Name: matchingEntry.Name,
@@ -111,13 +107,13 @@ func AutoDownloadUpdates() error {
})
}
- fmt.Println("[INFO] AutoDownloadUpdates completed successfully")
+ fmt.Println("[INFO] DownloadUpdates completed successfully")
return nil
}
-// AutoInstallUpdates installs any packages that were downloaded and decompressed by AutoDownloadUpdates.
+// InstallUpdates installs any packages that were downloaded and decompressed by DownloadUpdates.
// It moves files from their temp directories to the final location and updates installed.ini.
-func AutoInstallUpdates() error {
+func InstallUpdates() error {
installDir, err := GetInstallDir()
if err != nil {
return err
@@ -146,7 +142,7 @@ func AutoInstallUpdates() error {
}
// 5) Finalize
- err = FinalizeInstall(entry.Name, entry.Release, entry.Version, entry.Arch, entry.OS)
+ 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)
}
@@ -156,18 +152,15 @@ func AutoInstallUpdates() error {
return nil
}
-func AutoDownloadSpecified(specs []AppIndexEntry) error {
+func DownloadSpecified(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 {
+ if err := UpdateIndex(); 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)
+ entries, err := GetIndex()
if err != nil {
return fmt.Errorf("[ERROR] Failed to parse APPINDEX: %w", err)
}
@@ -203,29 +196,27 @@ func AutoDownloadSpecified(specs []AppIndexEntry) error {
}
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)
- // }
+ 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
- // }
+ 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,
@@ -251,7 +242,7 @@ func AutoDownloadSpecified(specs []AppIndexEntry) error {
}
fmt.Printf("[INFO] Package '%s' decompressed successfully to: %s\n", matchingEntry.Name, tempDir)
- // 7) Store in pendingUpdates for AutoInstallUpdates
+ // Add to pendingUpdates for InstallUpdates
fmt.Printf("[INFO] Adding '%s' to pending updates\n", matchingEntry.Name)
pendingUpdates = append(pendingUpdates, *matchingEntry)
}
diff --git a/decompress.go b/decompress.go
index 7f7a207..00003e2 100644
--- a/decompress.go
+++ b/decompress.go
@@ -108,7 +108,11 @@ func decompressTarGz(srcFile, destDir string, updateProgress func(int, string))
// Update progress after extracting each file.
if updateProgress != nil {
percent := int((progressReader.BytesRead * 100) / totalSize)
- updateProgress(percent, fmt.Sprintf("Extracted: %s", header.Name))
+ name := header.Name
+ if len(name) > 50 {
+ name = name[len(name)-50:]
+ }
+ updateProgress(percent, fmt.Sprintf("Extracted: %s", name))
}
}
diff --git a/download.go b/download.go
index 30d239c..c6464f2 100644
--- a/download.go
+++ b/download.go
@@ -11,9 +11,9 @@ import (
)
// DownloadPackageFromAppIndex selects and downloads the correct package from the APPINDEX.
-func DownloadPackageFromAppIndex(appIndexPath string, packageName string, release string, pkgType string, destDir string) error {
+func DownloadPackageFromAppIndex(packageName string, release string, pkgType string, destDir string) error {
// Parse the APPINDEX
- entries, err := ParseAppIndex(appIndexPath)
+ entries, err := GetIndex()
if err != nil {
return fmt.Errorf("failed to parse APPINDEX: %w", err)
}
@@ -99,8 +99,8 @@ func DownloadPackageFromAppIndex(appIndexPath string, packageName string, releas
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
+ // I dont know why is this happening, I dont want to know but sometimes some process is helding up the downloaded files so thats why it retries here
+ maxRetries := 10
for i := 0; i < maxRetries; i++ {
err = os.Rename(downloadedFilePath, expectedFilePath)
if err == nil {
@@ -115,7 +115,7 @@ func DownloadPackageFromAppIndex(appIndexPath string, packageName string, releas
f.Close()
if i < maxRetries-1 {
- time.Sleep(500 * time.Millisecond) // Wait before retrying
+ time.Sleep(250 * time.Millisecond) // Wait before retrying
}
}
diff --git a/go.mod b/go.mod
index 8b67881..f4c1524 100644
--- a/go.mod
+++ b/go.mod
@@ -1,7 +1,12 @@
module weforge.xyz/Spitfire/SPM
-go 1.21
+go 1.23.0
-require gopkg.in/ini.v1 v1.67.0
+toolchain go1.24.1
+
+require (
+ golang.org/x/sys v0.31.0
+ gopkg.in/ini.v1 v1.67.0
+)
require github.com/stretchr/testify v1.10.0 // indirect
diff --git a/go.sum b/go.sum
index be248fb..bd54818 100644
--- a/go.sum
+++ b/go.sum
@@ -4,6 +4,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
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/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
+golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
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=
diff --git a/install.go b/install.go
index 9fbf274..a8d8521 100644
--- a/install.go
+++ b/install.go
@@ -287,8 +287,8 @@ func copyFile(src, dst string) error {
return os.Chmod(dst, info.Mode())
}
-// FinalizeInstall finalizes the installation by updating installed.ini.
-func FinalizeInstall(packageName, release, version, arch, osName string) error {
+// 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
diff --git a/installed_pacakges.go b/installed_pacakges.go
index 1f89eae..9f73aea 100644
--- a/installed_pacakges.go
+++ b/installed_pacakges.go
@@ -72,6 +72,16 @@ func saveInstalledPackages(installDir string, pkgs []AppIndexEntry) error {
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)
diff --git a/register_unix.go b/register_unix.go
index 6568edd..f9e35c9 100644
--- a/register_unix.go
+++ b/register_unix.go
@@ -4,14 +4,34 @@
package spm
-import "fmt"
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+)
// RegisterApp is not supported on non-Windows platforms.
func RegisterApp() error {
- return fmt.Errorf("RegisterApp is only available on Windows")
+ return fmt.Errorf("[WARN] RegisterApp() is only available on Windows")
}
// UnregisterApp is not supported on non-Windows platforms.
func UnregisterApp() error {
- return fmt.Errorf("UnregisterApp is only available on Windows")
+ return fmt.Errorf("[WARN] UnregisterApp() is only available on Windows")
+}
+
+// IsRegistered returns true if the application is detected as installed.
+// On Linux, we assume it is installed if the main executable exists in the install directory.
+func IsRegistered() bool {
+ installDir, err := GetInstallDir()
+ if err != nil {
+ return false
+ }
+
+ // Assume the executable is named "spitfire" and is located in installDir.
+ exePath := filepath.Join(installDir, "browser", "spitfire")
+ if _, err := os.Stat(exePath); err == nil {
+ return true
+ }
+ return false
}
diff --git a/register_win.go b/register_win.go
index b2cf47f..206f0ae 100644
--- a/register_win.go
+++ b/register_win.go
@@ -10,7 +10,7 @@ import (
"golang.org/x/sys/windows/registry"
)
-// RegisterApp writes the necessary registry keys, making it appear as offically installed app
+// RegisterApp writes the necessary registry keys, making it appear as officially installed app
func RegisterApp() error {
exePath, err := GetInstallDir()
if err != nil {
@@ -133,3 +133,15 @@ func deleteRegistryTree(root registry.Key, path string) error {
// Finally, delete the (now empty) key.
return registry.DeleteKey(root, path)
}
+
+// IsRegistered returns true if the application is registered (installed) in the registry.
+func IsRegistered() bool {
+ // Try to open the uninstall key with read-only access.
+ key, err := registry.OpenKey(registry.LOCAL_MACHINE, `Software\Microsoft\Windows\CurrentVersion\Uninstall\SpitfireBrowser`, registry.READ)
+ if err != nil {
+ // If the key cannot be opened, assume the app is not registered.
+ return false
+ }
+ defer key.Close()
+ return true
+}
diff --git a/run_unix.go b/run_unix.go
index cc82b79..aa580b2 100644
--- a/run_unix.go
+++ b/run_unix.go
@@ -9,7 +9,6 @@ import (
"os"
"os/exec"
"path/filepath"
- "runtime"
"syscall"
)
@@ -21,9 +20,6 @@ func Run() error {
}
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")
diff --git a/run_win.go b/run_win.go
index d1f2b38..706b6d4 100644
--- a/run_win.go
+++ b/run_win.go
@@ -9,8 +9,10 @@ import (
"os"
"os/exec"
"path/filepath"
- "runtime"
- "syscall"
+ "strings"
+ "time"
+
+ "golang.org/x/sys/windows"
)
// Run locates and starts the installed Spitfire browser without waiting for it to exit.
@@ -21,9 +23,6 @@ func Run() error {
}
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")
@@ -37,7 +36,6 @@ func RunAndWait() error {
return fmt.Errorf("failed to get install directory: %w", err)
}
- // Construct the browser executable path
exePath := filepath.Join(installDir, "browser", "spitfire.exe")
if _, err := os.Stat(exePath); err != nil {
return fmt.Errorf("browser executable not found at %s: %w", exePath, err)
@@ -46,23 +44,29 @@ func RunAndWait() error {
cmd := exec.Command(exePath)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
+ cmd.Dir = filepath.Join(installDir, "browser")
- // Use CREATE_NEW_PROCESS_GROUP flag for Windows
- cmd.SysProcAttr = &syscall.SysProcAttr{
- CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP,
+ // Create job object starting the process
+ job, err := windows.CreateJobObject(nil, nil)
+ if err != nil {
+ return fmt.Errorf("failed to create job object: %w", err)
}
+ defer windows.CloseHandle(job)
fmt.Printf("Starting browser: %s\n", exePath)
if err := cmd.Start(); err != nil {
return fmt.Errorf("failed to start browser: %w", err)
}
- fmt.Printf("Browser process started with PID %d\n", cmd.Process.Pid)
-
- if err := cmd.Wait(); err != nil {
- return fmt.Errorf("browser exited with error: %w", err)
+ for {
+ cmd := exec.Command("tasklist", "/FI", "IMAGENAME eq spitfire.exe")
+ output, _ := cmd.Output()
+ if !strings.Contains(string(output), "spitfire.exe") {
+ break
+ }
+ time.Sleep(1 * time.Second)
}
- fmt.Println("Browser exited successfully.")
+ fmt.Println("Browser exited.")
return nil
}