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