cmd/gogio: add support for Windows

Now, gogio can build the program for Windows, using the `-target
windows`.

It will build with `-H=windowsgui`, by default. Also, it can compile for
multiple platforms if specified using `-target` (e.g. `-target arm, 386,
amd64`), the executable will have the respective suffix (i.e.
`_386.exe`).

gogio will also attach (any) appicon.png as executable icon resource and
include some information about the file and supported operating system.

Signed-off-by: Inkeliz <inkeliz@inkeliz.com>
This commit is contained in:
Inkeliz
2020-12-10 15:17:30 +00:00
committed by Elias Naur
parent 0b2a2d6c2e
commit fa96e12b6d
6 changed files with 385 additions and 45 deletions
+12 -9
View File
@@ -97,7 +97,6 @@ func buildAndroid(tmpDir string, bi *buildInfo) error {
buildtools: buildtools, buildtools: buildtools,
androidjar: filepath.Join(platform, "android.jar"), androidjar: filepath.Join(platform, "android.jar"),
} }
perms := []string{"default"} perms := []string{"default"}
const permPref = "gioui.org/app/permission/" const permPref = "gioui.org/app/permission/"
cfg := &packages.Config{ cfg := &packages.Config{
@@ -176,11 +175,15 @@ func compileAndroid(tmpDir string, tools *androidTools, bi *buildInfo) (err erro
if err != nil { if err != nil {
return err return err
} }
minSDK := 16
if bi.minsdk > minSDK {
minSDK = bi.minsdk
}
tcRoot := filepath.Join(ndkRoot, "toolchains", "llvm", "prebuilt", archNDK()) tcRoot := filepath.Join(ndkRoot, "toolchains", "llvm", "prebuilt", archNDK())
var builds errgroup.Group var builds errgroup.Group
for _, a := range bi.archs { for _, a := range bi.archs {
arch := allArchs[a] arch := allArchs[a]
clang, err := latestCompiler(tcRoot, a, bi.minsdk) clang, err := latestCompiler(tcRoot, a, minSDK)
if err != nil { if err != nil {
return fmt.Errorf("%s. Please make sure you have NDK >= r19c installed. Use the command `sdkmanager ndk-bundle` to install it.", err) return fmt.Errorf("%s. Please make sure you have NDK >= r19c installed. Use the command `sdkmanager ndk-bundle` to install it.", err)
} }
@@ -351,13 +354,9 @@ func exeAndroid(tmpDir string, tools *androidTools, bi *buildInfo, extraJars, pe
return err return err
} }
} }
icon := *iconPath
if icon == "" {
icon = filepath.Join(bi.pkgDir, "appicon.png")
}
iconSnip := "" iconSnip := ""
if _, err := os.Stat(icon); err == nil { if _, err := os.Stat(bi.iconPath); err == nil {
err := buildIcons(resDir, icon, []iconVariant{ err := buildIcons(resDir, bi.iconPath, []iconVariant{
{path: filepath.Join("mipmap-hdpi", "ic_launcher.png"), size: 72}, {path: filepath.Join("mipmap-hdpi", "ic_launcher.png"), size: 72},
{path: filepath.Join("mipmap-xhdpi", "ic_launcher.png"), size: 96}, {path: filepath.Join("mipmap-xhdpi", "ic_launcher.png"), size: 96},
{path: filepath.Join("mipmap-xxhdpi", "ic_launcher.png"), size: 144}, {path: filepath.Join("mipmap-xxhdpi", "ic_launcher.png"), size: 144},
@@ -394,12 +393,16 @@ func exeAndroid(tmpDir string, tools *androidTools, bi *buildInfo, extraJars, pe
if bi.minsdk > targetSDK { if bi.minsdk > targetSDK {
targetSDK = bi.minsdk targetSDK = bi.minsdk
} }
minSDK := 16
if bi.minsdk > minSDK {
minSDK = bi.minsdk
}
permissions, features := getPermissions(perms) permissions, features := getPermissions(perms)
appName := strings.Title(bi.name) appName := strings.Title(bi.name)
manifestSrc := manifestData{ manifestSrc := manifestData{
AppID: bi.appID, AppID: bi.appID,
Version: bi.version, Version: bi.version,
MinSDK: bi.minsdk, MinSDK: minSDK,
TargetSDK: targetSDK, TargetSDK: targetSDK,
Permissions: permissions, Permissions: permissions,
Features: features, Features: features,
+35 -20
View File
@@ -3,22 +3,26 @@ package main
import ( import (
"flag" "flag"
"fmt" "fmt"
"os"
"os/exec" "os/exec"
"path" "path"
"path/filepath"
"runtime"
"strings" "strings"
) )
type buildInfo struct { type buildInfo struct {
appID string appID string
archs []string archs []string
ldflags string ldflags string
minsdk int minsdk int
name string name string
pkgDir string pkgDir string
pkgPath string pkgPath string
tags string iconPath string
target string tags string
version int target string
version int
} }
func newBuildInfo(pkgPath string) (*buildInfo, error) { func newBuildInfo(pkgPath string) (*buildInfo, error) {
@@ -27,17 +31,22 @@ func newBuildInfo(pkgPath string) (*buildInfo, error) {
return nil, err return nil, err
} }
appID := getAppID(pkgMetadata) appID := getAppID(pkgMetadata)
appIcon := filepath.Join(pkgMetadata.Dir, "appicon.png")
if *iconPath != "" {
appIcon = *iconPath
}
bi := &buildInfo{ bi := &buildInfo{
appID: appID, appID: appID,
archs: getArchs(), archs: getArchs(),
ldflags: getLdFlags(appID), ldflags: getLdFlags(appID),
minsdk: *minsdk, minsdk: *minsdk,
name: getPkgName(pkgMetadata), name: getPkgName(pkgMetadata),
pkgDir: pkgMetadata.Dir, pkgDir: pkgMetadata.Dir,
pkgPath: pkgPath, pkgPath: pkgPath,
tags: *extraTags, iconPath: appIcon,
target: *target, tags: *extraTags,
version: *version, target: *target,
version: *version,
} }
return bi, nil return bi, nil
} }
@@ -54,6 +63,12 @@ func getArchs() []string {
return []string{"arm64", "amd64"} return []string{"arm64", "amd64"}
case "android": case "android":
return []string{"arm", "arm64", "386", "amd64"} return []string{"arm", "arm64", "386", "amd64"}
case "windows":
goarch := os.Getenv("GOARCH")
if goarch == "" {
goarch = runtime.GOARCH
}
return []string{goarch}
default: default:
// TODO: Add flag tests. // TODO: Add flag tests.
panic("The target value has already been validated, this will never execute.") panic("The target value has already been validated, this will never execute.")
+3
View File
@@ -52,6 +52,9 @@ component of the 1.0.X version for iOS and tvOS.
For Android builds the -minsdk flag specify the minimum SDK level. For example, For Android builds the -minsdk flag specify the minimum SDK level. For example,
use -minsdk 22 to target Android 5.1 (Lollipop) and later. use -minsdk 22 to target Android 5.1 (Lollipop) and later.
For Windows builds the -minsdk flag specify the minimum OS version. For example,
use -mindk 10 to target Windows 10 only, -minsdk 6 for Windows Vista and later.
The -work flag prints the path to the working directory and suppress The -work flag prints the path to the working directory and suppress
its deletion. its deletion.
+2 -6
View File
@@ -222,12 +222,8 @@ int main(int argc, char * argv[]) {
if err := ioutil.WriteFile(plistFile, []byte(infoPlist), 0660); err != nil { if err := ioutil.WriteFile(plistFile, []byte(infoPlist), 0660); err != nil {
return err return err
} }
icon := *iconPath if _, err := os.Stat(bi.iconPath); err == nil {
if icon == "" { assetPlist, err := iosIcons(bi, tmpDir, app, bi.iconPath)
icon = filepath.Join(bi.pkgDir, "appicon.png")
}
if _, err := os.Stat(icon); err == nil {
assetPlist, err := iosIcons(bi, tmpDir, app, icon)
if err != nil { if err != nil {
return err return err
} }
+17 -10
View File
@@ -24,7 +24,7 @@ import (
var ( var (
target = flag.String("target", "", "specify target (ios, tvos, android, js).\n") target = flag.String("target", "", "specify target (ios, tvos, android, js).\n")
archNames = flag.String("arch", "", "specify architecture(s) to include (arm, arm64, amd64).") archNames = flag.String("arch", "", "specify architecture(s) to include (arm, arm64, amd64).")
minsdk = flag.Int("minsdk", 16, "specify minimum supported Android platform sdk version (e.g. 28 for android28 a.k.a. Android 9 Pie).") minsdk = flag.Int("minsdk", 0, "specify the minimum supported operating system level")
buildMode = flag.String("buildmode", "exe", "specify buildmode (archive, exe)") buildMode = flag.String("buildmode", "exe", "specify buildmode (archive, exe)")
destPath = flag.String("o", "", "output file or directory.\nFor -target ios or tvos, use the .app suffix to target simulators.") destPath = flag.String("o", "", "output file or directory.\nFor -target ios or tvos, use the .app suffix to target simulators.")
appID = flag.String("appid", "", "app identifier (for -buildmode=exe)") appID = flag.String("appid", "", "app identifier (for -buildmode=exe)")
@@ -67,7 +67,7 @@ func flagValidate() error {
return errors.New("please specify -target") return errors.New("please specify -target")
} }
switch *target { switch *target {
case "ios", "tvos", "android", "js": case "ios", "tvos", "android", "js", "windows":
default: default:
return fmt.Errorf("invalid -target %s", *target) return fmt.Errorf("invalid -target %s", *target)
} }
@@ -96,6 +96,8 @@ func build(bi *buildInfo) error {
return buildIOS(tmpDir, *target, bi) return buildIOS(tmpDir, *target, bi)
case "android": case "android":
return buildAndroid(tmpDir, bi) return buildAndroid(tmpDir, bi)
case "windows":
return buildWindows(tmpDir, bi)
default: default:
panic("unreachable") panic("unreachable")
} }
@@ -188,13 +190,6 @@ func buildIcons(baseDir, icon string, variants []iconVariant) error {
for _, v := range variants { for _, v := range variants {
v := v v := v
resizes.Go(func() (err error) { resizes.Go(func() (err error) {
scaled := image.NewNRGBA(image.Rectangle{Max: image.Point{X: v.size, Y: v.size}})
op := draw.Src
if v.fill {
op = draw.Over
draw.Draw(scaled, scaled.Bounds(), &image.Uniform{color.White}, image.Point{}, draw.Src)
}
draw.CatmullRom.Scale(scaled, scaled.Bounds(), img, img.Bounds(), op, nil)
path := filepath.Join(baseDir, v.path) path := filepath.Join(baseDir, v.path)
if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
return err return err
@@ -208,8 +203,20 @@ func buildIcons(baseDir, icon string, variants []iconVariant) error {
err = cerr err = cerr
} }
}() }()
return png.Encode(f, scaled) return png.Encode(f, resizeIcon(v, img))
}) })
} }
return resizes.Wait() return resizes.Wait()
} }
func resizeIcon(v iconVariant, img image.Image) *image.NRGBA {
scaled := image.NewNRGBA(image.Rectangle{Max: image.Point{X: v.size, Y: v.size}})
op := draw.Src
if v.fill {
op = draw.Over
draw.Draw(scaled, scaled.Bounds(), &image.Uniform{color.White}, image.Point{}, draw.Src)
}
draw.CatmullRom.Scale(scaled, scaled.Bounds(), img, img.Bounds(), op, nil)
return scaled
}
+316
View File
@@ -0,0 +1,316 @@
package main
import (
"bytes"
"encoding/binary"
"fmt"
"image"
"image/png"
"io"
"math"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"text/template"
)
func buildWindows(tmpDir string, bi *buildInfo) error {
builder := &windowsBuilder{TempDir: tmpDir, BuildInfo: bi}
builder.DestDir = *destPath
if builder.DestDir == "" {
builder.DestDir = bi.pkgPath
}
name := bi.name
if *destPath != "" {
if filepath.Ext(*destPath) != ".exe" {
return fmt.Errorf("invalid output name %q, it must end with `.exe`", *destPath)
}
name = filepath.Base(*destPath)
}
name = strings.TrimSuffix(name, ".exe")
sdk := bi.minsdk
if sdk > 10 {
return fmt.Errorf("invalid minsdk (%d) it's higher than Windows 10", sdk)
}
version := strconv.Itoa(bi.version)
if bi.version > math.MaxUint16 {
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
if err := builder.createIcon(); 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.buildResource(); err != nil {
return fmt.Errorf("can't build the resources: %v", err)
}
for _, arch := range builder.BuildInfo.archs {
if err := builder.buildProgram(arch); err != nil {
return err
}
}
return nil
}
type (
windowsResources struct {
IconPath string
ManifestPath string
Version string
Name string
CompanyName string
}
windowsManifest struct {
Version string
WindowsVersion int
Name string
Arch string
}
windowsFiles struct {
Resources windowsResources
ResourcesPath string
Manifest windowsManifest
}
)
type windowsBuilder struct {
TempDir string
DestDir string
BuildInfo *buildInfo
windowsFiles
}
func (b *windowsBuilder) createIcon() (err error) {
if _, err := os.Stat(b.BuildInfo.iconPath); err != nil {
return nil
}
iconFile, err := os.Open(b.BuildInfo.iconPath)
if err != nil {
return fmt.Errorf("can't read the icon located at %s: %v", b.BuildInfo.iconPath, 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)
}
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}
// ICONDIR structure
if err := binary.Write(w, 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)
if err := png.Encode(&imageBuffer, scaledImage); 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 {
return err
}
}
_, err := io.Copy(w, &imageBuffer)
if err != nil {
return 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 {
dest := b.DestDir
if len(b.BuildInfo.archs) > 1 {
dest = filepath.Join(filepath.Dir(b.DestDir), b.Resources.Name+"_"+arch+".exe")
}
cmd := exec.Command(
"go",
"build",
"-ldflags=-H=windowsgui "+b.BuildInfo.ldflags,
"-tags="+b.BuildInfo.tags,
"-o", dest,
b.BuildInfo.pkgPath,
)
cmd.Env = append(
os.Environ(),
"GOOS=windows",
"GOARCH="+arch,
)
_, err := runCmd(cmd)
return err
}
func (f *windowsManifest) encode(w io.Writer) error {
t := `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<assemblyIdentity type="win32" name="{{.Name}}" version="{{.Version}}" />
<description>{{.Name}}</description>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
{{if (le .WindowsVersion 10)}}<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
{{end}}
{{if (le .WindowsVersion 9)}}<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
{{end}}
{{if (le .WindowsVersion 8)}}<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
{{end}}
{{if (le .WindowsVersion 7)}}<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
{{end}}
{{if (le .WindowsVersion 6)}}<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
{{end}}
</application>
</compatibility>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
<asmv3:application>
<asmv3:windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
</asmv3:windowsSettings>
</asmv3:application>
</assembly>`
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 {
return err
}
return template.Execute(w, f)
}