github.com/cybriq/giocore@v0.0.7-0.20210703034601-cfb9cb5f3900/cmd/gogio/windowsbuild.go (about) 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 }