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,
androidjar: filepath.Join(platform, "android.jar"),
}
perms := []string{"default"}
const permPref = "gioui.org/app/permission/"
cfg := &packages.Config{
@@ -176,11 +175,15 @@ func compileAndroid(tmpDir string, tools *androidTools, bi *buildInfo) (err erro
if err != nil {
return err
}
minSDK := 16
if bi.minsdk > minSDK {
minSDK = bi.minsdk
}
tcRoot := filepath.Join(ndkRoot, "toolchains", "llvm", "prebuilt", archNDK())
var builds errgroup.Group
for _, a := range bi.archs {
arch := allArchs[a]
clang, err := latestCompiler(tcRoot, a, bi.minsdk)
clang, err := latestCompiler(tcRoot, a, minSDK)
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)
}
@@ -351,13 +354,9 @@ func exeAndroid(tmpDir string, tools *androidTools, bi *buildInfo, extraJars, pe
return err
}
}
icon := *iconPath
if icon == "" {
icon = filepath.Join(bi.pkgDir, "appicon.png")
}
iconSnip := ""
if _, err := os.Stat(icon); err == nil {
err := buildIcons(resDir, icon, []iconVariant{
if _, err := os.Stat(bi.iconPath); err == nil {
err := buildIcons(resDir, bi.iconPath, []iconVariant{
{path: filepath.Join("mipmap-hdpi", "ic_launcher.png"), size: 72},
{path: filepath.Join("mipmap-xhdpi", "ic_launcher.png"), size: 96},
{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 {
targetSDK = bi.minsdk
}
minSDK := 16
if bi.minsdk > minSDK {
minSDK = bi.minsdk
}
permissions, features := getPermissions(perms)
appName := strings.Title(bi.name)
manifestSrc := manifestData{
AppID: bi.appID,
Version: bi.version,
MinSDK: bi.minsdk,
MinSDK: minSDK,
TargetSDK: targetSDK,
Permissions: permissions,
Features: features,
+35 -20
View File
@@ -3,22 +3,26 @@ package main
import (
"flag"
"fmt"
"os"
"os/exec"
"path"
"path/filepath"
"runtime"
"strings"
)
type buildInfo struct {
appID string
archs []string
ldflags string
minsdk int
name string
pkgDir string
pkgPath string
tags string
target string
version int
appID string
archs []string
ldflags string
minsdk int
name string
pkgDir string
pkgPath string
iconPath string
tags string
target string
version int
}
func newBuildInfo(pkgPath string) (*buildInfo, error) {
@@ -27,17 +31,22 @@ func newBuildInfo(pkgPath string) (*buildInfo, error) {
return nil, err
}
appID := getAppID(pkgMetadata)
appIcon := filepath.Join(pkgMetadata.Dir, "appicon.png")
if *iconPath != "" {
appIcon = *iconPath
}
bi := &buildInfo{
appID: appID,
archs: getArchs(),
ldflags: getLdFlags(appID),
minsdk: *minsdk,
name: getPkgName(pkgMetadata),
pkgDir: pkgMetadata.Dir,
pkgPath: pkgPath,
tags: *extraTags,
target: *target,
version: *version,
appID: appID,
archs: getArchs(),
ldflags: getLdFlags(appID),
minsdk: *minsdk,
name: getPkgName(pkgMetadata),
pkgDir: pkgMetadata.Dir,
pkgPath: pkgPath,
iconPath: appIcon,
tags: *extraTags,
target: *target,
version: *version,
}
return bi, nil
}
@@ -54,6 +63,12 @@ func getArchs() []string {
return []string{"arm64", "amd64"}
case "android":
return []string{"arm", "arm64", "386", "amd64"}
case "windows":
goarch := os.Getenv("GOARCH")
if goarch == "" {
goarch = runtime.GOARCH
}
return []string{goarch}
default:
// TODO: Add flag tests.
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,
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
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 {
return err
}
icon := *iconPath
if icon == "" {
icon = filepath.Join(bi.pkgDir, "appicon.png")
}
if _, err := os.Stat(icon); err == nil {
assetPlist, err := iosIcons(bi, tmpDir, app, icon)
if _, err := os.Stat(bi.iconPath); err == nil {
assetPlist, err := iosIcons(bi, tmpDir, app, bi.iconPath)
if err != nil {
return err
}
+17 -10
View File
@@ -24,7 +24,7 @@ import (
var (
target = flag.String("target", "", "specify target (ios, tvos, android, js).\n")
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)")
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)")
@@ -67,7 +67,7 @@ func flagValidate() error {
return errors.New("please specify -target")
}
switch *target {
case "ios", "tvos", "android", "js":
case "ios", "tvos", "android", "js", "windows":
default:
return fmt.Errorf("invalid -target %s", *target)
}
@@ -96,6 +96,8 @@ func build(bi *buildInfo) error {
return buildIOS(tmpDir, *target, bi)
case "android":
return buildAndroid(tmpDir, bi)
case "windows":
return buildWindows(tmpDir, bi)
default:
panic("unreachable")
}
@@ -188,13 +190,6 @@ func buildIcons(baseDir, icon string, variants []iconVariant) error {
for _, v := range variants {
v := v
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)
if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
return err
@@ -208,8 +203,20 @@ func buildIcons(baseDir, icon string, variants []iconVariant) error {
err = cerr
}
}()
return png.Encode(f, scaled)
return png.Encode(f, resizeIcon(v, img))
})
}
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)
}