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  }