windowsbuild.go raw
1 package main
2
3 import (
4 "bytes"
5 "encoding/binary"
6 "fmt"
7 "image/png"
8 "io"
9 "math"
10 "os"
11 "os/exec"
12 "path/filepath"
13 "reflect"
14 "strconv"
15 "strings"
16 "text/template"
17
18 "github.com/akavel/rsrc/binutil"
19 "github.com/akavel/rsrc/coff"
20 "golang.org/x/text/encoding/unicode"
21 )
22
23 func buildWindows(tmpDir string, bi *buildInfo) error {
24 builder := &windowsBuilder{TempDir: tmpDir}
25 builder.DestDir = *destPath
26 if builder.DestDir == "" {
27 builder.DestDir = bi.pkgPath
28 }
29
30 name := bi.name
31 if *destPath != "" {
32 if filepath.Ext(*destPath) != ".exe" {
33 return fmt.Errorf("invalid output name %q, it must end with `.exe`", *destPath)
34 }
35 name = filepath.Base(*destPath)
36 }
37 name = strings.TrimSuffix(name, ".exe")
38 sdk := bi.minsdk
39 if sdk > 10 {
40 return fmt.Errorf("invalid minsdk (%d) it's higher than Windows 10", sdk)
41 }
42 version := strconv.Itoa(bi.version)
43 if bi.version > math.MaxUint16 {
44 return fmt.Errorf("version (%d) is larger than the maximum (%d)", bi.version, math.MaxUint16)
45 }
46
47 for _, arch := range bi.archs {
48 builder.Coff = coff.NewRSRC()
49 builder.Coff.Arch(arch)
50
51 if err := builder.embedIcon(bi.iconPath); err != nil {
52 return err
53 }
54
55 if err := builder.embedManifest(windowsManifest{
56 Version: "1.0.0." + version,
57 WindowsVersion: sdk,
58 Name: name,
59 }); err != nil {
60 return fmt.Errorf("can't create manifest: %v", err)
61 }
62
63 if err := builder.embedInfo(windowsResources{
64 Version: [2]uint32{uint32(1) << 16, uint32(bi.version)},
65 VersionHuman: "1.0.0." + version,
66 Name: name,
67 Language: 0x0400, // Process Default Language: https://docs.microsoft.com/en-us/previous-versions/ms957130(v=msdn.10)
68 }); err != nil {
69 return fmt.Errorf("can't create info: %v", err)
70 }
71
72 if err := builder.buildResource(bi, name, arch); err != nil {
73 return fmt.Errorf("can't build the resources: %v", err)
74 }
75
76 if err := builder.buildProgram(bi, name, arch); err != nil {
77 return err
78 }
79 }
80
81 return nil
82 }
83
84 type (
85 windowsResources struct {
86 Version [2]uint32
87 VersionHuman string
88 Language uint16
89 Name string
90 }
91 windowsManifest struct {
92 Version string
93 WindowsVersion int
94 Name string
95 }
96 windowsBuilder struct {
97 TempDir string
98 DestDir string
99 Coff *coff.Coff
100 }
101 )
102
103 const (
104 // https://docs.microsoft.com/en-us/windows/win32/menurc/resource-types
105 windowsResourceIcon = 3
106 windowsResourceIconGroup = windowsResourceIcon + 11
107 windowsResourceManifest = 24
108 windowsResourceVersion = 16
109 )
110
111 type bufferCoff struct {
112 bytes.Buffer
113 }
114
115 func (b *bufferCoff) Size() int64 {
116 return int64(b.Len())
117 }
118
119 func (b *windowsBuilder) embedIcon(path string) (err error) {
120 iconFile, err := os.Open(path)
121 if err != nil {
122 return fmt.Errorf("can't read the icon located at %s: %v", path, err)
123 }
124 defer iconFile.Close()
125
126 iconImage, err := png.Decode(iconFile)
127 if err != nil {
128 return fmt.Errorf("can't decode the PNG file (%s): %v", path, err)
129 }
130
131 sizes := []int{16, 32, 48, 64, 128, 256}
132 var iconHeader bufferCoff
133
134 // GRPICONDIR structure.
135 if err := binary.Write(&iconHeader, binary.LittleEndian, [3]uint16{0, 1, uint16(len(sizes))}); err != nil {
136 return err
137 }
138
139 for _, size := range sizes {
140 var iconBuffer bufferCoff
141
142 if err := png.Encode(&iconBuffer, resizeIcon(iconVariant{size: size, fill: false}, iconImage)); err != nil {
143 return fmt.Errorf("can't encode image: %v", err)
144 }
145
146 b.Coff.AddResource(windowsResourceIcon, uint16(size), &iconBuffer)
147
148 if err := binary.Write(&iconHeader, binary.LittleEndian, struct {
149 Size [2]uint8
150 Color [2]uint8
151 Planes uint16
152 BitCount uint16
153 Length uint32
154 Id uint16
155 }{
156 Size: [2]uint8{uint8(size % 256), uint8(size % 256)}, // "0" means 256px.
157 Planes: 1,
158 BitCount: 32,
159 Length: uint32(iconBuffer.Len()),
160 Id: uint16(size),
161 }); err != nil {
162 return err
163 }
164 }
165
166 b.Coff.AddResource(windowsResourceIconGroup, 1, &iconHeader)
167
168 return nil
169 }
170
171 func (b *windowsBuilder) buildResource(buildInfo *buildInfo, name string, arch string) error {
172 out, err := os.Create(filepath.Join(buildInfo.pkgPath, name+"_windows_"+arch+".syso"))
173 if err != nil {
174 return err
175 }
176 defer out.Close()
177 b.Coff.Freeze()
178
179 // See https://github.com/akavel/rsrc/internal/write.go#L13.
180 w := binutil.Writer{W: out}
181 binutil.Walk(b.Coff, func(v reflect.Value, path string) error {
182 if binutil.Plain(v.Kind()) {
183 w.WriteLE(v.Interface())
184 return nil
185 }
186 vv, ok := v.Interface().(binutil.SizedReader)
187 if ok {
188 w.WriteFromSized(vv)
189 return binutil.WALK_SKIP
190 }
191 return nil
192 })
193
194 if w.Err != nil {
195 return fmt.Errorf("error writing output file: %s", w.Err)
196 }
197
198 return nil
199 }
200
201 func (b *windowsBuilder) buildProgram(buildInfo *buildInfo, name string, arch string) error {
202 dest := b.DestDir
203 if len(buildInfo.archs) > 1 {
204 dest = filepath.Join(filepath.Dir(b.DestDir), name+"_"+arch+".exe")
205 }
206
207 cmd := exec.Command(
208 "go",
209 "build",
210 "-ldflags=-H=windowsgui "+buildInfo.ldflags,
211 "-tags="+buildInfo.tags,
212 "-o", dest,
213 buildInfo.pkgPath,
214 )
215 cmd.Env = append(
216 os.Environ(),
217 "GOOS=windows",
218 "GOARCH="+arch,
219 )
220 _, err := runCmd(cmd)
221 return err
222 }
223
224 func (b *windowsBuilder) embedManifest(v windowsManifest) error {
225 t, err := template.New("manifest").Parse(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
226 <assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
227 <assemblyIdentity type="win32" name="{{.Name}}" version="{{.Version}}" />
228 <description>{{.Name}}</description>
229 <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
230 <application>
231 {{if (le .WindowsVersion 10)}}<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
232 {{end}}
233 {{if (le .WindowsVersion 9)}}<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
234 {{end}}
235 {{if (le .WindowsVersion 8)}}<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
236 {{end}}
237 {{if (le .WindowsVersion 7)}}<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
238 {{end}}
239 {{if (le .WindowsVersion 6)}}<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
240 {{end}}
241 </application>
242 </compatibility>
243 <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
244 <security>
245 <requestedPrivileges>
246 <requestedExecutionLevel level="asInvoker" uiAccess="false" />
247 </requestedPrivileges>
248 </security>
249 </trustInfo>
250 <asmv3:application>
251 <asmv3:windowsSettings>
252 <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
253 </asmv3:windowsSettings>
254 </asmv3:application>
255 </assembly>`)
256 if err != nil {
257 return err
258 }
259
260 var manifest bufferCoff
261 if err := t.Execute(&manifest, v); err != nil {
262 return err
263 }
264
265 b.Coff.AddResource(windowsResourceManifest, 1, &manifest)
266
267 return nil
268 }
269
270 func (b *windowsBuilder) embedInfo(v windowsResources) error {
271 page := uint16(1)
272
273 // https://docs.microsoft.com/pt-br/windows/win32/menurc/vs-versioninfo
274 t := newValue(valueBinary, "VS_VERSION_INFO", []io.WriterTo{
275 // https://docs.microsoft.com/pt-br/windows/win32/api/VerRsrc/ns-verrsrc-vs_fixedfileinfo
276 windowsInfoValueFixed{
277 Signature: 0xFEEF04BD,
278 StructVersion: 0x00010000,
279 FileVersion: v.Version,
280 ProductVersion: v.Version,
281 FileFlagMask: 0x3F,
282 FileFlags: 0,
283 FileOS: 0x40004,
284 FileType: 0x1,
285 FileSubType: 0,
286 },
287 // https://docs.microsoft.com/pt-br/windows/win32/menurc/stringfileinfo
288 newValue(valueText, "StringFileInfo", []io.WriterTo{
289 // https://docs.microsoft.com/pt-br/windows/win32/menurc/stringtable
290 newValue(valueText, fmt.Sprintf("%04X%04X", v.Language, page), []io.WriterTo{
291 // https://docs.microsoft.com/pt-br/windows/win32/menurc/string-str
292 newValue(valueText, "ProductVersion", v.VersionHuman),
293 newValue(valueText, "FileVersion", v.VersionHuman),
294 newValue(valueText, "FileDescription", v.Name),
295 newValue(valueText, "ProductName", v.Name),
296 // TODO include more data: gogio must have some way to provide such information (like Company Name, Copyright...)
297 }),
298 }),
299 // https://docs.microsoft.com/pt-br/windows/win32/menurc/varfileinfo
300 newValue(valueBinary, "VarFileInfo", []io.WriterTo{
301 // https://docs.microsoft.com/pt-br/windows/win32/menurc/var-str
302 newValue(valueBinary, "Translation", uint32(page)<<16|uint32(v.Language)),
303 }),
304 })
305
306 // For some reason the ValueLength of the VS_VERSIONINFO must be the byte-length of `windowsInfoValueFixed`:
307 t.ValueLength = 52
308
309 var verrsrc bufferCoff
310 if _, err := t.WriteTo(&verrsrc); err != nil {
311 return err
312 }
313
314 b.Coff.AddResource(windowsResourceVersion, 1, &verrsrc)
315
316 return nil
317 }
318
319 type windowsInfoValueFixed struct {
320 Signature uint32
321 StructVersion uint32
322 FileVersion [2]uint32
323 ProductVersion [2]uint32
324 FileFlagMask uint32
325 FileFlags uint32
326 FileOS uint32
327 FileType uint32
328 FileSubType uint32
329 FileDate [2]uint32
330 }
331
332 func (v windowsInfoValueFixed) WriteTo(w io.Writer) (_ int64, err error) {
333 return 0, binary.Write(w, binary.LittleEndian, v)
334 }
335
336 type windowsInfoValue struct {
337 Length uint16
338 ValueLength uint16
339 Type uint16
340 Key []byte
341 Value []byte
342 }
343
344 func (v windowsInfoValue) WriteTo(w io.Writer) (_ int64, err error) {
345 // binary.Write doesn't support []byte inside struct.
346 if err = binary.Write(w, binary.LittleEndian, [3]uint16{v.Length, v.ValueLength, v.Type}); err != nil {
347 return 0, err
348 }
349 if _, err = w.Write(v.Key); err != nil {
350 return 0, err
351 }
352 if _, err = w.Write(v.Value); err != nil {
353 return 0, err
354 }
355 return 0, nil
356 }
357
358 const (
359 valueBinary uint16 = 0
360 valueText uint16 = 1
361 )
362
363 func newValue(valueType uint16, key string, input interface{}) windowsInfoValue {
364 v := windowsInfoValue{
365 Type: valueType,
366 Length: 6,
367 }
368
369 padding := func(in []byte) []byte {
370 if l := uint16(len(in)) + v.Length; l%4 != 0 {
371 return append(in, make([]byte, 4-l%4)...)
372 }
373 return in
374 }
375
376 v.Key = padding(utf16Encode(key))
377 v.Length += uint16(len(v.Key))
378
379 switch in := input.(type) {
380 case string:
381 v.Value = padding(utf16Encode(in))
382 v.ValueLength = uint16(len(v.Value) / 2)
383 case []io.WriterTo:
384 var buff bytes.Buffer
385 for k := range in {
386 if _, err := in[k].WriteTo(&buff); err != nil {
387 panic(err)
388 }
389 }
390 v.Value = buff.Bytes()
391 default:
392 var buff bytes.Buffer
393 if err := binary.Write(&buff, binary.LittleEndian, in); err != nil {
394 panic(err)
395 }
396 v.ValueLength = uint16(buff.Len())
397 v.Value = buff.Bytes()
398 }
399
400 v.Length += uint16(len(v.Value))
401
402 return v
403 }
404
405 // utf16Encode encodes the string to UTF16 with null-termination.
406 func utf16Encode(s string) []byte {
407 b, err := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewEncoder().Bytes([]byte(s))
408 if err != nil {
409 panic(err)
410 }
411 return append(b, 0x00, 0x00) // null-termination.
412 }
413