Files
wails_tools/pkg/wails3kit/updates/platform/installer.go
2026-03-12 22:16:34 +01:00

173 lines
4.5 KiB
Go

package platform
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"time"
"github.com/Eriyc/rules_wails/pkg/wails3kit/updates"
)
const helperFlag = "--wails3kit-update-helper"
type Detector func(app updates.AppDescriptor) (updates.InstallRoot, error)
type Installer struct {
detector Detector
}
func New(detector Detector) *Installer {
return &Installer{detector: detector}
}
func (installer *Installer) DetectInstallRoot(app updates.AppDescriptor) (updates.InstallRoot, error) {
if installer.detector == nil {
return updates.InstallRoot{}, errors.New("missing install root detector")
}
return installer.detector(app)
}
func (installer *Installer) Stage(ctx context.Context, root updates.InstallRoot, file updates.DownloadedFile, release updates.Release) (updates.StagedArtifact, error) {
stageDir, bundleManifest, err := extractBundle(ctx, file.Path, release.Artifact.Format)
if err != nil {
return updates.StagedArtifact{}, err
}
return updates.StagedArtifact{
Path: stageDir,
Root: root,
Release: release,
Bundle: bundleManifest,
}, nil
}
func (installer *Installer) SpawnApplyAndRestart(_ context.Context, req updates.ApplyRequest) error {
helperDir, err := os.MkdirTemp("", "wails3kit-helper-*")
if err != nil {
return err
}
helperBinary := filepath.Join(helperDir, filepath.Base(req.App.ExecutablePath))
if err := copyFile(req.App.ExecutablePath, helperBinary, 0o755); err != nil {
return err
}
payload := helperRequest{
InstallRoot: req.Root.Path,
StagedPath: req.Staged.Path,
Bundle: req.Staged.Bundle,
Args: append([]string(nil), req.App.Args...),
WorkingDirectory: req.App.WorkingDirectory,
}
requestPath := filepath.Join(helperDir, "request.json")
encoded, err := json.Marshal(payload)
if err != nil {
return err
}
if err := os.WriteFile(requestPath, encoded, 0o600); err != nil {
return err
}
command := exec.Command(helperBinary, helperFlag, requestPath)
command.Dir = helperDir
command.Stdout = os.Stdout
command.Stderr = os.Stderr
return command.Start()
}
func MaybeRun(args []string) (bool, error) {
if len(args) < 3 || args[1] != helperFlag {
return false, nil
}
return true, runHelper(args[2])
}
func NewCurrent() (*Installer, error) {
switch runtime.GOOS {
case "darwin":
return New(detectDarwinInstallRoot), nil
case "windows":
return New(detectDirectoryInstallRoot), nil
case "linux":
return New(detectDirectoryInstallRoot), nil
default:
return nil, fmt.Errorf("unsupported platform: %s", runtime.GOOS)
}
}
type helperRequest struct {
InstallRoot string `json:"installRoot"`
StagedPath string `json:"stagedPath"`
Bundle updates.BundleManifest `json:"bundle"`
Args []string `json:"args,omitempty"`
WorkingDirectory string `json:"workingDirectory,omitempty"`
}
func runHelper(requestPath string) error {
bytes, err := os.ReadFile(requestPath)
if err != nil {
return err
}
var request helperRequest
if err := json.Unmarshal(bytes, &request); err != nil {
return err
}
deadline := time.Now().Add(15 * time.Second)
var applyErr error
for time.Now().Before(deadline) {
applyErr = applyBundle(request)
if applyErr == nil {
break
}
time.Sleep(100 * time.Millisecond)
}
if applyErr != nil {
return applyErr
}
relaunch := filepath.Join(request.InstallRoot, filepath.FromSlash(request.Bundle.EntryPoint))
command := exec.Command(relaunch, request.Args...)
if request.WorkingDirectory != "" {
command.Dir = request.WorkingDirectory
} else {
command.Dir = request.InstallRoot
}
command.Stdout = os.Stdout
command.Stderr = os.Stderr
return command.Start()
}
func detectDirectoryInstallRoot(app updates.AppDescriptor) (updates.InstallRoot, error) {
if app.ExecutablePath == "" {
return updates.InstallRoot{}, errors.New("missing executable path")
}
return updates.InstallRoot{Path: filepath.Dir(app.ExecutablePath)}, nil
}
func detectDarwinInstallRoot(app updates.AppDescriptor) (updates.InstallRoot, error) {
if app.ExecutablePath == "" {
return updates.InstallRoot{}, errors.New("missing executable path")
}
current := filepath.Dir(app.ExecutablePath)
for {
if filepath.Ext(current) == ".app" {
return updates.InstallRoot{Path: current}, nil
}
parent := filepath.Dir(current)
if parent == current {
return updates.InstallRoot{}, fmt.Errorf("unable to locate .app bundle from %s", app.ExecutablePath)
}
current = parent
}
}