From fa96e12b6d7a877585291ac40ecdcb4bf8b82faf Mon Sep 17 00:00:00 2001 From: Inkeliz Date: Thu, 10 Dec 2020 15:17:30 +0000 Subject: [PATCH] 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 --- cmd/gogio/androidbuild.go | 21 +-- cmd/gogio/build_info.go | 55 ++++--- cmd/gogio/help.go | 3 + cmd/gogio/iosbuild.go | 8 +- cmd/gogio/main.go | 27 ++-- cmd/gogio/windowsbuild.go | 316 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 385 insertions(+), 45 deletions(-) create mode 100644 cmd/gogio/windowsbuild.go diff --git a/cmd/gogio/androidbuild.go b/cmd/gogio/androidbuild.go index 946b3eef..3ece6527 100644 --- a/cmd/gogio/androidbuild.go +++ b/cmd/gogio/androidbuild.go @@ -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, diff --git a/cmd/gogio/build_info.go b/cmd/gogio/build_info.go index 85303b3c..05dca316 100644 --- a/cmd/gogio/build_info.go +++ b/cmd/gogio/build_info.go @@ -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.") diff --git a/cmd/gogio/help.go b/cmd/gogio/help.go index 8c045090..d6769b82 100644 --- a/cmd/gogio/help.go +++ b/cmd/gogio/help.go @@ -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. diff --git a/cmd/gogio/iosbuild.go b/cmd/gogio/iosbuild.go index ae68a369..5a349c63 100644 --- a/cmd/gogio/iosbuild.go +++ b/cmd/gogio/iosbuild.go @@ -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 } diff --git a/cmd/gogio/main.go b/cmd/gogio/main.go index 81727287..a3732587 100644 --- a/cmd/gogio/main.go +++ b/cmd/gogio/main.go @@ -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 +} diff --git a/cmd/gogio/windowsbuild.go b/cmd/gogio/windowsbuild.go new file mode 100644 index 00000000..bc1f1a87 --- /dev/null +++ b/cmd/gogio/windowsbuild.go @@ -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 := ` + + + {{.Name}} + + + {{if (le .WindowsVersion 10)}} +{{end}} + {{if (le .WindowsVersion 9)}} +{{end}} + {{if (le .WindowsVersion 8)}} +{{end}} + {{if (le .WindowsVersion 7)}} +{{end}} + {{if (le .WindowsVersion 6)}} +{{end}} + + + + + + + + + + + + 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 { + return err + } + + return template.Execute(w, f) +}