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