From 07715a812598ea6e7768fe624a9b4285a390256a Mon Sep 17 00:00:00 2001 From: Inkeliz Date: Mon, 4 Jan 2021 11:25:08 +0000 Subject: [PATCH] cmd/gogio: remove windres dependency Now, it's possible to compile to Windows (`-target windows`) without having `windres`. The PNG icon, manifest and version info will be generated and include using `gogio`. Signed-off-by: Inkeliz --- cmd/go.mod | 2 + cmd/go.sum | 3 + cmd/gogio/windowsbuild.go | 429 +++++++++++++++++++++++--------------- 3 files changed, 267 insertions(+), 167 deletions(-) diff --git a/cmd/go.mod b/cmd/go.mod index 013d18ee..bf35ade4 100644 --- a/cmd/go.mod +++ b/cmd/go.mod @@ -4,9 +4,11 @@ go 1.13 require ( gioui.org v0.0.0-20201206220230-a87a520ae825 + github.com/akavel/rsrc v0.10.1 github.com/chromedp/cdproto v0.0.0-20191114225735-6626966fbae4 github.com/chromedp/chromedp v0.5.2 golang.org/x/image v0.0.0-20200618115811-c13761719519 golang.org/x/sync v0.0.0-20190423024810-112230192c58 + golang.org/x/text v0.3.0 golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e ) diff --git a/cmd/go.sum b/cmd/go.sum index 3e36137d..f71b2cf0 100644 --- a/cmd/go.sum +++ b/cmd/go.sum @@ -2,6 +2,8 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7 gioui.org v0.0.0-20201206220230-a87a520ae825 h1:8eQeFlQ0IL5sOX74YcwEBk3OtGNTRCqIU3Rz0z0U6vE= gioui.org v0.0.0-20201206220230-a87a520ae825/go.mod h1:Y+uS7hHMvku1Q+ooaoq6fYD5B2LGoT8JtFgvmYmRzTw= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/akavel/rsrc v0.10.1 h1:hCCPImjmFKVNGpeLZyTDRHEFC283DzyTXTo0cO0Rq9o= +github.com/akavel/rsrc v0.10.1/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= github.com/chromedp/cdproto v0.0.0-20191114225735-6626966fbae4 h1:QD3KxSJ59L2lxG6MXBjNHxiQO2RmxTQ3XcK+wO44WOg= github.com/chromedp/cdproto v0.0.0-20191114225735-6626966fbae4/go.mod h1:PfAWWKJqjlGFYJEidUM6aVIWPr0EpobeyVWEEmplX7g= github.com/chromedp/chromedp v0.5.2 h1:W8xBXQuUnd2dZK0SN/lyVwsQM7KgW+kY5HGnntms194= @@ -37,6 +39,7 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191113165036-4c7a9d0fe056/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9 h1:1/DFK4b7JH8DmkqhUk48onnSfrPzImPoVxuomtbT2nk= golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e h1:1xWUkZQQ9Z9UuZgNaIR6OQOE7rUFglXUUBZlO+dGg6I= golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= diff --git a/cmd/gogio/windowsbuild.go b/cmd/gogio/windowsbuild.go index bc1f1a87..53569efc 100644 --- a/cmd/gogio/windowsbuild.go +++ b/cmd/gogio/windowsbuild.go @@ -4,20 +4,23 @@ import ( "bytes" "encoding/binary" "fmt" - "image" + "github.com/akavel/rsrc/binutil" + "github.com/akavel/rsrc/coff" + "golang.org/x/text/encoding/unicode" "image/png" "io" "math" "os" "os/exec" "path/filepath" + "reflect" "strconv" "strings" "text/template" ) func buildWindows(tmpDir string, bi *buildInfo) error { - builder := &windowsBuilder{TempDir: tmpDir, BuildInfo: bi} + builder := &windowsBuilder{TempDir: tmpDir} builder.DestDir = *destPath if builder.DestDir == "" { builder.DestDir = bi.pkgPath @@ -40,29 +43,36 @@ func buildWindows(tmpDir string, bi *buildInfo) error { return fmt.Errorf("version (%d) is larger than the maximum (%d)", bi.version, math.MaxUint16) } - builder.Resources.Name = name - builder.Manifest.Name = name - builder.Manifest.WindowsVersion = sdk - builder.Resources.Version = "1,0,0," + version - builder.Manifest.Version = "1.0.0." + version + for _, arch := range bi.archs { + builder.Coff = coff.NewRSRC() + builder.Coff.Arch(arch) - if err := builder.createIcon(); err != nil { - return err - } + if err := builder.embedIcon(bi.iconPath); err != nil { + return err + } - if err := builder.createManifest(); err != nil { - return fmt.Errorf("can't create manifest: %v", err) - } - if err := builder.createResource(); err != nil { - return fmt.Errorf("can't create resource: %v", err) - } + if err := builder.embedManifest(windowsManifest{ + Version: "1.0.0." + version, + WindowsVersion: sdk, + Name: name, + }); err != nil { + return fmt.Errorf("can't create manifest: %v", err) + } - if err := builder.buildResource(); err != nil { - return fmt.Errorf("can't build the resources: %v", err) - } + if err := builder.embedInfo(windowsResources{ + Version: [2]uint32{uint32(1) << 16, uint32(bi.version)}, + VersionHuman: "1.0.0." + version, + Name: name, + Language: 0x0400, // Process Default Language: https://docs.microsoft.com/en-us/previous-versions/ms957130(v=msdn.10) + }); err != nil { + return fmt.Errorf("can't create info: %v", err) + } - for _, arch := range builder.BuildInfo.archs { - if err := builder.buildProgram(arch); err != nil { + if err := builder.buildResource(bi, name, arch); err != nil { + return fmt.Errorf("can't build the resources: %v", err) + } + + if err := builder.buildProgram(bi, name, arch); err != nil { return err } } @@ -72,157 +82,134 @@ func buildWindows(tmpDir string, bi *buildInfo) error { type ( windowsResources struct { - IconPath string - ManifestPath string - Version string + Version [2]uint32 + VersionHuman string + Language uint16 Name string - CompanyName string } windowsManifest struct { Version string WindowsVersion int Name string - Arch string } - windowsFiles struct { - Resources windowsResources - ResourcesPath string - Manifest windowsManifest + windowsBuilder struct { + TempDir string + DestDir string + Coff *coff.Coff } ) -type windowsBuilder struct { - TempDir string - DestDir string - BuildInfo *buildInfo - windowsFiles +const ( + // https://docs.microsoft.com/en-us/windows/win32/menurc/resource-types + windowsResourceIcon = 3 + windowsResourceIconGroup = windowsResourceIcon + 11 + windowsResourceManifest = 24 + windowsResourceVersion = 16 +) + +type bufferCoff struct { + bytes.Buffer } -func (b *windowsBuilder) createIcon() (err error) { - if _, err := os.Stat(b.BuildInfo.iconPath); err != nil { - return nil - } +func (b *bufferCoff) Size() int64 { + return int64(b.Len()) +} - iconFile, err := os.Open(b.BuildInfo.iconPath) +func (b *windowsBuilder) embedIcon(path string) (err error) { + iconFile, err := os.Open(path) if err != nil { - return fmt.Errorf("can't read the icon located at %s: %v", b.BuildInfo.iconPath, err) + return fmt.Errorf("can't read the icon located at %s: %v", path, err) } defer iconFile.Close() iconImage, err := png.Decode(iconFile) if err != nil { - return fmt.Errorf("can't decode the PNG file (%s): %v", b.BuildInfo.iconPath, err) + return fmt.Errorf("can't decode the PNG file (%s): %v", path, err) } - b.Resources.IconPath = filepath.Join(b.TempDir, "appicon.ico") - exeIcon, err := os.Create(b.Resources.IconPath) - if err != nil { - return fmt.Errorf("impossibe to create icon file at %s: %v", b.Resources.IconPath, err) - } - defer exeIcon.Close() - - return convertPNGtoICO(exeIcon, iconImage) -} - -func convertPNGtoICO(w io.Writer, img image.Image) error { - // The file must be in .ICO format. - const ( - OffsetICONDIR int = 2 * 3 - OffsetICONDIRENTRY int = (4 * 1) + (2 * 2) + (4 * 2) - ) - sizes := []int{16, 32, 48, 64, 128, 256} + var iconHeader bufferCoff - // ICONDIR structure - if err := binary.Write(w, binary.LittleEndian, [3]uint16{0, 1, uint16(len(sizes))}); err != nil { + // GRPICONDIR structure. + if err := binary.Write(&iconHeader, binary.LittleEndian, [3]uint16{0, 1, uint16(len(sizes))}); err != nil { return err } - var ( - headerOffset = OffsetICONDIR + (OffsetICONDIRENTRY * len(sizes)) - imageBuffer bytes.Buffer - ) for _, size := range sizes { - imageOffset := imageBuffer.Len() - scaledImage := resizeIcon(iconVariant{size: size, fill: false}, img) + var iconBuffer bufferCoff - if err := png.Encode(&imageBuffer, scaledImage); err != nil { + if err := png.Encode(&iconBuffer, resizeIcon(iconVariant{size: size, fill: false}, iconImage)); err != nil { return fmt.Errorf("can't encode image: %v", err) } - // ICONDIRENTRY 0-3 structure. - // The width/height is defined from 0 to 255 (uint8). But "0" means 256px. - if err := binary.Write(w, binary.LittleEndian, [4]uint8{uint8(size % 256), uint8(size % 256), 0, 0}); err != nil { - return err - } - // ICONDIRENTRY 4-6 structure - if err := binary.Write(w, binary.LittleEndian, [2]uint16{1, 32}); err != nil { - return err - } - // ICONDIRENTRY 8-12 structure - if err := binary.Write(w, binary.LittleEndian, [2]uint32{uint32(imageBuffer.Len() - imageOffset), uint32(headerOffset + imageOffset)}); err != nil { + b.Coff.AddResource(windowsResourceIcon, uint16(size), &iconBuffer) + + if err := binary.Write(&iconHeader, binary.LittleEndian, struct { + Size [2]uint8 + Color [2]uint8 + Planes uint16 + BitCount uint16 + Length uint32 + Id uint16 + }{ + Size: [2]uint8{uint8(size % 256), uint8(size % 256)}, // "0" means 256px. + Planes: 1, + BitCount: 32, + Length: uint32(iconBuffer.Len()), + Id: uint16(size), + }); err != nil { return err } } - _, err := io.Copy(w, &imageBuffer) + b.Coff.AddResource(windowsResourceIconGroup, 1, &iconHeader) + + return nil +} + +func (b *windowsBuilder) buildResource(buildInfo *buildInfo, name string, arch string) error { + out, err := os.Create(filepath.Join(buildInfo.pkgPath, name+"_windows_"+arch+".syso")) if err != nil { return err } + defer out.Close() + b.Coff.Freeze() + + // See https://github.com/akavel/rsrc/internal/write.go#L13. + w := binutil.Writer{W: out} + binutil.Walk(b.Coff, func(v reflect.Value, path string) error { + if binutil.Plain(v.Kind()) { + w.WriteLE(v.Interface()) + return nil + } + vv, ok := v.Interface().(binutil.SizedReader) + if ok { + w.WriteFromSized(vv) + return binutil.WALK_SKIP + } + return nil + }) + + if w.Err != nil { + return fmt.Errorf("error writing output file: %s", w.Err) + } return nil } -func (b *windowsBuilder) createManifest() error { - // The manifest have some information about the executable itself, - // such as the supported Windows and Execution Level/Permissions. - b.Resources.ManifestPath = filepath.Join(b.TempDir, "manifest_windows.xml") - manifest, err := os.Create(b.Resources.ManifestPath) - if err != nil { - return err - } - defer manifest.Close() - - return b.Manifest.encode(manifest) -} - -func (b *windowsBuilder) createResource() error { - // The resource includes the icon and manifest previously created - // it also defines the version and some other information about the - // program and the developer. - b.ResourcesPath = filepath.Join(b.TempDir, "main_windows.rc") - resources, err := os.Create(b.ResourcesPath) - if err != nil { - return err - } - defer resources.Close() - - return b.Resources.encode(resources) -} - -func (b *windowsBuilder) buildResource() error { - cmd := exec.Command( - "windres", - b.ResourcesPath, - filepath.Join(b.BuildInfo.pkgPath, "main_windows.syso"), - ) - _, err := runCmd(cmd) - return err -} - -func (b *windowsBuilder) buildProgram(arch string) error { +func (b *windowsBuilder) buildProgram(buildInfo *buildInfo, name string, arch string) error { dest := b.DestDir - if len(b.BuildInfo.archs) > 1 { - dest = filepath.Join(filepath.Dir(b.DestDir), b.Resources.Name+"_"+arch+".exe") + if len(buildInfo.archs) > 1 { + dest = filepath.Join(filepath.Dir(b.DestDir), name+"_"+arch+".exe") } cmd := exec.Command( "go", "build", - "-ldflags=-H=windowsgui "+b.BuildInfo.ldflags, - "-tags="+b.BuildInfo.tags, + "-ldflags=-H=windowsgui "+buildInfo.ldflags, + "-tags="+buildInfo.tags, "-o", dest, - b.BuildInfo.pkgPath, + buildInfo.pkgPath, ) cmd.Env = append( os.Environ(), @@ -233,8 +220,8 @@ func (b *windowsBuilder) buildProgram(arch string) error { return err } -func (f *windowsManifest) encode(w io.Writer) error { - t := ` +func (b *windowsBuilder) embedManifest(v windowsManifest) error { + t, err := template.New("manifest").Parse(` {{.Name}} @@ -264,53 +251,161 @@ func (f *windowsManifest) encode(w io.Writer) error { true -` - template, err := template.New("manifest").Parse(t) +`) if err != nil { return err } - return template.Execute(w, f) -} - -func (f *windowsResources) encode(w io.Writer) error { - const t = `{{if .IconPath}}#define IDI_ICON1 1 -IDI_ICON1 ICON "{{escapePath .IconPath}}"{{end}} - -#define IDI_MANIFEST 1 -IDI_MANIFEST 24 "{{escapePath .ManifestPath}}" - -#define IDI_VERSION 1 -IDI_VERSION VERSIONINFO -FILEVERSION {{.Version}} -PRODUCTVERSION {{.Version}} -FILEFLAGSMASK 0X3FL -FILEFLAGS 0x0L -FILEOS 0X40004L -FILETYPE 0X1L -FILESUBTYPE 0x0L -BEGIN - BLOCK "StringFileInfo" - BEGIN - BLOCK "04000400" - BEGIN - VALUE "ProductVersion", "{{.Version}}" - VALUE "FileVersion", "{{.Version}}" - VALUE "FileDescription", "{{.Name}}" - VALUE "ProductName", "{{.Name}}" - END - END - BLOCK "VarFileInfo" - BEGIN - VALUE "Translation", 0x0400, 0x0400 - END -END` - template, err := template.New("rc").Funcs(template.FuncMap{"escapePath": func(s string) string { - return strings.Replace(s, `\`, `\\`, -1) - }}).Parse(t) - if err != nil { + var manifest bufferCoff + if err := t.Execute(&manifest, v); err != nil { return err } - return template.Execute(w, f) + b.Coff.AddResource(windowsResourceManifest, 1, &manifest) + + return nil +} + +func (b *windowsBuilder) embedInfo(v windowsResources) error { + page := uint16(1) + + // https://docs.microsoft.com/pt-br/windows/win32/menurc/vs-versioninfo + t := newValue(valueBinary, "VS_VERSION_INFO", []io.WriterTo{ + // https://docs.microsoft.com/pt-br/windows/win32/api/VerRsrc/ns-verrsrc-vs_fixedfileinfo + windowsInfoValueFixed{ + Signature: 0xFEEF04BD, + StructVersion: 0x00010000, + FileVersion: v.Version, + ProductVersion: v.Version, + FileFlagMask: 0x3F, + FileFlags: 0, + FileOS: 0x40004, + FileType: 0x1, + FileSubType: 0, + }, + // https://docs.microsoft.com/pt-br/windows/win32/menurc/stringfileinfo + newValue(valueText, "StringFileInfo", []io.WriterTo{ + // https://docs.microsoft.com/pt-br/windows/win32/menurc/stringtable + newValue(valueText, fmt.Sprintf("%04X%04X", v.Language, page), []io.WriterTo{ + // https://docs.microsoft.com/pt-br/windows/win32/menurc/string-str + newValue(valueText, "ProductVersion", v.VersionHuman), + newValue(valueText, "FileVersion", v.VersionHuman), + newValue(valueText, "FileDescription", v.Name), + newValue(valueText, "ProductName", v.Name), + // TODO include more data: gogio must have some way to provide such information (like Company Name, Copyright...) + }), + }), + // https://docs.microsoft.com/pt-br/windows/win32/menurc/varfileinfo + newValue(valueBinary, "VarFileInfo", []io.WriterTo{ + // https://docs.microsoft.com/pt-br/windows/win32/menurc/var-str + newValue(valueBinary, "Translation", uint32(page)<<16|uint32(v.Language)), + }), + }) + + // For some reason the ValueLength of the VS_VERSIONINFO must be the byte-length of `windowsInfoValueFixed`: + t.ValueLength = 52 + + var verrsrc bufferCoff + if _, err := t.WriteTo(&verrsrc); err != nil { + return err + } + + b.Coff.AddResource(windowsResourceVersion, 1, &verrsrc) + + return nil +} + +type windowsInfoValueFixed struct { + Signature uint32 + StructVersion uint32 + FileVersion [2]uint32 + ProductVersion [2]uint32 + FileFlagMask uint32 + FileFlags uint32 + FileOS uint32 + FileType uint32 + FileSubType uint32 + FileDate [2]uint32 +} + +func (v windowsInfoValueFixed) WriteTo(w io.Writer) (_ int64, err error) { + return 0, binary.Write(w, binary.LittleEndian, v) +} + +type windowsInfoValue struct { + Length uint16 + ValueLength uint16 + Type uint16 + Key []byte + Value []byte +} + +func (v windowsInfoValue) WriteTo(w io.Writer) (_ int64, err error) { + // binary.Write doesn't support []byte inside struct. + if err = binary.Write(w, binary.LittleEndian, [3]uint16{v.Length, v.ValueLength, v.Type}); err != nil { + return 0, err + } + if _, err = w.Write(v.Key); err != nil { + return 0, err + } + if _, err = w.Write(v.Value); err != nil { + return 0, err + } + return 0, nil +} + +const ( + valueBinary uint16 = 0 + valueText uint16 = 1 +) + +func newValue(valueType uint16, key string, input interface{}) windowsInfoValue { + v := windowsInfoValue{ + Type: valueType, + Length: 6, + } + + padding := func(in []byte) []byte { + if l := uint16(len(in)) + v.Length; l%4 != 0 { + return append(in, make([]byte, 4-l%4)...) + } + return in + } + + v.Key = padding(utf16Encode(key)) + v.Length += uint16(len(v.Key)) + + switch in := input.(type) { + case string: + v.Value = padding(utf16Encode(in)) + v.ValueLength = uint16(len(v.Value) / 2) + case []io.WriterTo: + var buff bytes.Buffer + for k := range in { + if _, err := in[k].WriteTo(&buff); err != nil { + panic(err) + } + } + v.Value = buff.Bytes() + default: + var buff bytes.Buffer + if err := binary.Write(&buff, binary.LittleEndian, in); err != nil { + panic(err) + } + v.ValueLength = uint16(buff.Len()) + v.Value = buff.Bytes() + } + + v.Length += uint16(len(v.Value)) + + return v +} + +// utf16Encode encodes the string to UTF16 with null-termination. +func utf16Encode(s string) []byte { + b, err := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewEncoder().Bytes([]byte(s)) + if err != nil { + panic(err) + } + return append(b, 0x00, 0x00) // null-termination. }