返回 DeepSeek-Reasonix
main.go
1 // Command windows-resource stamps Reasonix branding and metadata into a Windows
2 // support executable after it has been built. Wails already uses the same
3 // winres library for the desktop executable; keeping the support binaries on the
4 // same resource path avoids generic Explorer, shortcut, and taskbar icons.
5 package main
6
7 import (
8 "bytes"
9 "errors"
10 "flag"
11 "fmt"
12 "io"
13 "os"
14 "path/filepath"
15 "strconv"
16 "strings"
17
18 "github.com/tc-hib/winres"
19 "github.com/tc-hib/winres/version"
20 )
21
22 const (
23 companyName = "Reasonix"
24 copyright = "Copyright © 2026 Reasonix Contributors"
25 )
26
27 type resourceOptions struct {
28 executable string
29 icon string
30 numericVersion string
31 fileDescription string
32 internalName string
33 originalName string
34 }
35
36 func main() {
37 if err := run(os.Args[1:]); err != nil {
38 fmt.Fprintln(os.Stderr, "windows-resource:", err)
39 os.Exit(1)
40 }
41 }
42
43 func run(args []string) error {
44 fs := flag.NewFlagSet("windows-resource", flag.ContinueOnError)
45 fs.SetOutput(io.Discard)
46 var opts resourceOptions
47 fs.StringVar(&opts.executable, "exe", "", "Windows executable to stamp")
48 fs.StringVar(&opts.icon, "icon", "", "ICO file to embed")
49 fs.StringVar(&opts.numericVersion, "version", "", "numeric X.Y.Z version")
50 fs.StringVar(&opts.fileDescription, "description", "", "Windows FileDescription")
51 fs.StringVar(&opts.internalName, "internal-name", "", "Windows InternalName")
52 fs.StringVar(&opts.originalName, "original-filename", "", "Windows OriginalFilename")
53 if err := fs.Parse(args); err != nil {
54 return err
55 }
56 if fs.NArg() != 0 {
57 return fmt.Errorf("unexpected arguments: %s", strings.Join(fs.Args(), " "))
58 }
59 for name, value := range map[string]string{
60 "-exe": opts.executable,
61 "-icon": opts.icon,
62 "-version": opts.numericVersion,
63 "-description": opts.fileDescription,
64 "-internal-name": opts.internalName,
65 "-original-filename": opts.originalName,
66 } {
67 if strings.TrimSpace(value) == "" {
68 return fmt.Errorf("%s is required", name)
69 }
70 }
71 return stampExecutable(opts)
72 }
73
74 func stampExecutable(opts resourceOptions) error {
75 numericVersion, err := parseNumericVersion(opts.numericVersion)
76 if err != nil {
77 return err
78 }
79
80 iconFile, err := os.Open(opts.icon)
81 if err != nil {
82 return fmt.Errorf("open icon: %w", err)
83 }
84 ico, err := winres.LoadICO(iconFile)
85 closeErr := iconFile.Close()
86 if err != nil {
87 return fmt.Errorf("load icon: %w", err)
88 }
89 if closeErr != nil {
90 return fmt.Errorf("close icon: %w", closeErr)
91 }
92
93 rs := winres.ResourceSet{}
94 if err := rs.SetIcon(winres.ID(1), ico); err != nil {
95 return fmt.Errorf("set icon: %w", err)
96 }
97 rs.SetManifest(winres.AppManifest{
98 Identity: winres.AssemblyIdentity{
99 Name: "Reasonix." + opts.internalName,
100 Version: numericVersion,
101 },
102 Description: opts.fileDescription,
103 ExecutionLevel: winres.AsInvoker,
104 DPIAwareness: winres.DPIPerMonitorV2,
105 LongPathAware: true,
106 UseCommonControlsV6: true,
107 })
108
109 info := version.Info{
110 FileVersion: numericVersion,
111 ProductVersion: numericVersion,
112 }
113 for key, value := range map[string]string{
114 version.CompanyName: companyName,
115 version.FileDescription: opts.fileDescription,
116 version.FileVersion: opts.numericVersion,
117 version.InternalName: opts.internalName,
118 version.LegalCopyright: copyright,
119 version.OriginalFilename: opts.originalName,
120 version.ProductName: "Reasonix",
121 version.ProductVersion: opts.numericVersion,
122 version.Comments: "Reasonix desktop support component.",
123 } {
124 if err := info.Set(version.LangDefault, key, value); err != nil {
125 return fmt.Errorf("set version field %s: %w", key, err)
126 }
127 }
128 rs.SetVersionInfo(info)
129
130 source, err := os.ReadFile(opts.executable)
131 if err != nil {
132 return fmt.Errorf("read executable: %w", err)
133 }
134 var stamped bytes.Buffer
135 if err := rs.WriteToEXE(&stamped, bytes.NewReader(source)); err != nil {
136 return fmt.Errorf("write resources: %w", err)
137 }
138 if err := verifyResources(stamped.Bytes(), opts, numericVersion); err != nil {
139 return fmt.Errorf("verify resources: %w", err)
140 }
141
142 mode := os.FileMode(0o755)
143 if stat, err := os.Stat(opts.executable); err == nil {
144 mode = stat.Mode()
145 }
146 if err := replaceFile(opts.executable, stamped.Bytes(), mode); err != nil {
147 return fmt.Errorf("replace executable: %w", err)
148 }
149 return nil
150 }
151
152 func parseNumericVersion(value string) ([4]uint16, error) {
153 parts := strings.Split(strings.TrimSpace(value), ".")
154 if len(parts) != 3 && len(parts) != 4 {
155 return [4]uint16{}, fmt.Errorf("version %q must have three or four numeric parts", value)
156 }
157 var parsed [4]uint16
158 for i, part := range parts {
159 if part == "" {
160 return [4]uint16{}, fmt.Errorf("version %q contains an empty part", value)
161 }
162 n, err := strconv.ParseUint(part, 10, 16)
163 if err != nil {
164 return [4]uint16{}, fmt.Errorf("version %q is not a Windows numeric version: %w", value, err)
165 }
166 parsed[i] = uint16(n)
167 }
168 return parsed, nil
169 }
170
171 func verifyResources(executable []byte, opts resourceOptions, numericVersion [4]uint16) error {
172 rs, err := winres.LoadFromEXE(bytes.NewReader(executable))
173 if err != nil {
174 return err
175 }
176 if _, err := rs.GetIcon(winres.ID(1)); err != nil {
177 return fmt.Errorf("application icon: %w", err)
178 }
179 manifest := rs.Get(winres.RT_MANIFEST, winres.ID(1), winres.LCIDDefault)
180 if len(manifest) == 0 || !bytes.Contains(manifest, []byte(`requestedExecutionLevel level="asInvoker"`)) {
181 return errors.New("asInvoker application manifest is missing")
182 }
183 versionData := rs.Get(winres.RT_VERSION, winres.ID(1), version.LangDefault)
184 info, err := version.FromBytes(versionData)
185 if err != nil {
186 return fmt.Errorf("version info: %w", err)
187 }
188 if info.FileVersion != numericVersion || info.ProductVersion != numericVersion {
189 return fmt.Errorf("numeric version mismatch: file=%v product=%v want=%v", info.FileVersion, info.ProductVersion, numericVersion)
190 }
191 table := info.Table()[version.LangDefault]
192 if table == nil {
193 return errors.New("English version string table is missing")
194 }
195 for key, want := range map[string]string{
196 version.CompanyName: companyName,
197 version.FileDescription: opts.fileDescription,
198 version.InternalName: opts.internalName,
199 version.OriginalFilename: opts.originalName,
200 version.ProductName: "Reasonix",
201 } {
202 if got := (*table)[key]; got != want {
203 return fmt.Errorf("version field %s=%q, want %q", key, got, want)
204 }
205 }
206 return nil
207 }
208
209 func replaceFile(path string, data []byte, mode os.FileMode) error {
210 temp, err := os.CreateTemp(filepath.Dir(path), ".reasonix-resource-*.exe")
211 if err != nil {
212 return err
213 }
214 tempName := temp.Name()
215 defer os.Remove(tempName)
216 if err := temp.Chmod(mode); err != nil {
217 temp.Close()
218 return err
219 }
220 if _, err := temp.Write(data); err != nil {
221 temp.Close()
222 return err
223 }
224 if err := temp.Sync(); err != nil {
225 temp.Close()
226 return err
227 }
228 if err := temp.Close(); err != nil {
229 return err
230 }
231 return os.Rename(tempName, path)
232 }
233
233 lines GO