//go:build windows package builder import ( "encoding/json" "fmt" "log" "os" "os/exec" "path/filepath" "strings" ) // applyPrepResourcesToEXE copies icon + version info from prepPath onto exePath (post-build). func (h *Handler) applyPrepResourcesToEXE(prepPath, exePath string) error { if err := h.patchEXEResourcesFromPrepExtract(prepPath, exePath); err == nil { log.Printf("[Fusion] Applied icon + version info from %s", filepath.Base(prepPath)) return nil } else { log.Printf("[Fusion] resource extract/patch failed, trying icon fallback: %v", err) } if err := h.patchEXEWithExtractedICO(prepPath, exePath); err == nil { log.Printf("[Fusion] Applied icon from %s (fallback ico)", filepath.Base(prepPath)) return nil } return fmt.Errorf("could not copy icon/resources from prep exe") } func (h *Handler) patchEXEResourcesFromPrepExtract(prepPath, exePath string) error { workDir, err := os.MkdirTemp(filepath.Dir(exePath), "prep-resources-*") if err != nil { return err } defer os.RemoveAll(workDir) if _, err := h.runGoWinres("", "extract", "--dir", workDir, prepPath); err != nil { return err } filteredJSON, err := writeIconAndVersionWinresJSON(filepath.Join(workDir, "winres.json")) if err != nil { return err } if _, err := h.runGoWinres(filepath.Dir(filteredJSON), "patch", "--in", filteredJSON, "--no-backup", exePath); err != nil { return err } return nil } func (h *Handler) patchEXEWithExtractedICO(prepPath, exePath string) error { workDir, err := os.MkdirTemp(filepath.Dir(exePath), "prep-ico-*") if err != nil { return err } defer os.RemoveAll(workDir) iconPath := filepath.Join(workDir, "prep-icon.ico") if err := extractIconFromEXE(prepPath, iconPath); err != nil { return err } doc := map[string]any{ "RT_GROUP_ICON": map[string]any{ "APP": map[string]any{ "0409": "prep-icon.ico", }, }, } jsonPath := filepath.Join(workDir, "icons-only.json") raw, err := json.MarshalIndent(doc, "", " ") if err != nil { return err } if err := os.WriteFile(jsonPath, raw, 0644); err != nil { return err } if _, err := h.runGoWinres(workDir, "patch", "--in", jsonPath, "--no-backup", exePath); err != nil { return err } return nil } // extractIconFromEXE writes the primary icon from a Windows PE file to a .ico path. func extractIconFromEXE(exePath, icoPath string) error { exeEsc := strings.ReplaceAll(exePath, `'`, `''`) icoEsc := strings.ReplaceAll(icoPath, `'`, `''`) script := fmt.Sprintf(` $ErrorActionPreference = 'Stop' Add-Type -AssemblyName System.Drawing $icon = [System.Drawing.Icon]::ExtractAssociatedIcon('%s') if ($null -eq $icon) { throw 'no icon on executable' } $dir = Split-Path -Parent '%s' if ($dir -and -not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null } $fs = [System.IO.File]::Create('%s') $icon.Save($fs) $fs.Close() `, exeEsc, icoEsc, icoEsc) cmd := exec.Command("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script) out, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf("extract icon: %w (%s)", err, strings.TrimSpace(string(out))) } if _, err := os.Stat(icoPath); err != nil { return fmt.Errorf("icon file not created: %w", err) } return nil } func (h *Handler) prepareFusionWinres(fusionDir, prepPath string) error { return nil } func peSubsystem(exePath string) int { data, err := os.ReadFile(exePath) if err != nil || len(data) < 128 { return 2 } peOff := int(uint32(data[0x3c]) | uint32(data[0x3d])<<8 | uint32(data[0x3e])<<16 | uint32(data[0x3f])<<24) if peOff+24+68+2 > len(data) { return 2 } if string(data[peOff:peOff+4]) != "PE\x00\x00" { return 2 } opt := peOff + 24 sub := int(uint16(data[opt+68]) | uint16(data[opt+69])<<8) return sub } func fusionLdflags(prepPath string) string { flags := "-s -w" if peSubsystem(prepPath) == 2 { flags += " -H windowsgui" } return flags }