Add icon generation

Add syso generation
Add tests
This commit is contained in:
Lea Anthony 2023-01-02 21:23:44 +11:00
commit e5da0eb5f8
No known key found for this signature in database
GPG key ID: 33DAF7BB90A58405
13 changed files with 742 additions and 16 deletions

56
exp/cmd/wails/README.md Normal file
View file

@ -0,0 +1,56 @@
# The Wails CLI
The Wails CLI is a command line tool that allows you to create, build and run Wails applications.
There are a number of commands related to tooling, such as icon generation and asset bundling.
## Commands
### icon
The `icon` command generates icons for your project. It takes a single argument which is the path to the icon file.
| Flag | Type | Description | Default |
|--------------------|--------|------------------------------------------------------|-----------------------|
| `-example` | bool | Generates example icon file (appicon.png) | |
| `-input` | string | The input image file | |
| `-sizes` | string | The sizes to generate in .ico file (comma separated) | "256,128,64,48,32,16" |
| `-windowsFilename` | string | The output filename for the Windows icon | icons.ico |
| `-macFilename` | string | The output filename for the Mac icon bundle | icons.icns |
```bash
wails icon -input myicon.png -sizes "32,64,128" -windowsFilename myicon.ico -macFilename myicon.icns
```
This will generate icons for mac and windows and save them in the current directory as `myicon.ico`
and `myicons.icns`.
### syso
The `syso` command generates a Windows resource file (aka `.syso`).
```bash
wails syso <options>
```
| Flag | Type | Description | Default |
|-------------|--------|--------------------------------------------|------------------|
| `-example` | bool | Generates example manifest & info files | |
| `-manifest` | string | The manifest file | |
| `-info` | string | The info.json file | |
| `-icon` | string | The icon file | |
| `-out` | string | The output filename for the syso file | `wails.exe.syso` |
| `-arch` | string | The target architecture (amd64,arm64,386) | `runtime.GOOS` |
If `-example` is provided, the command will generate example manifest and info files
in the current directory and exit.
If `-manifest` is provided, the command will use the provided manifest file to generate
the syso file.
If `-info` is provided, the command will use the provided info.json file to set the version
information in the syso file.
NOTE: We use [winres](https://github.com/tc-hib/winres) to generate the syso file. Please
refer to the winres documentation for more information.
NOTE: Whilst the tool will work for 32-bit Windows, it is not supported. Please use 64-bit.

View file

@ -12,9 +12,9 @@ func main() {
app := clir.NewCli("wails", "The Wails CLI", "v3")
app.NewSubCommandFunction("init", "Initialise a new project", commands.Init)
app.NewSubCommandFunction("build", "Build the project", commands.Build)
tool := app.NewSubCommand("tool", "Various build tools")
tool.NewSubCommandFunction("icon", "Generate icons", commands.Icon)
generate := app.NewSubCommand("generate", "Generation tools")
generate.NewSubCommandFunction("icon", "Generate icons", commands.GenerateIcon)
generate.NewSubCommandFunction("syso", "Generate Windows .syso file", commands.GenerateSyso)
err := app.Run()
if err != nil {
log.Fatalln(err)

View file

@ -7,24 +7,27 @@ require (
github.com/jackmordaunt/icns/v2 v2.2.1
github.com/leaanthony/clir v1.3.0
github.com/leaanthony/winicon v1.0.0
github.com/tc-hib/winres v0.1.6
)
require (
github.com/fatih/color v1.13.0 // indirect
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
github.com/joho/godotenv v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.9 // indirect
github.com/mattn/go-colorable v0.1.11 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-zglob v0.0.4 // indirect
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/radovskyb/watcher v1.0.7 // indirect
github.com/sajari/fuzzy v1.0.0 // indirect
golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9 // indirect
golang.org/x/image v0.0.0-20200430140353-33d19683fad8 // indirect
golang.org/x/image v0.0.0-20201208152932-35266b937fa6 // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
mvdan.cc/sh/v3 v3.6.0-0.dev.0.20220704111049-a6e3029cd899 // indirect
)

View file

@ -16,14 +16,17 @@ github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/leaanthony/clir v1.0.4/go.mod h1:k/RBkdkFl18xkkACMCLt09bhiZnrGORoxmomeMvDpE0=
github.com/leaanthony/clir v1.3.0 h1:L9nPDWrmc/qU9UWZZvRaFajWYuO0np9V5p+5gxyYno0=
github.com/leaanthony/clir v1.3.0/go.mod h1:k/RBkdkFl18xkkACMCLt09bhiZnrGORoxmomeMvDpE0=
github.com/leaanthony/winicon v1.0.0 h1:ZNt5U5dY71oEoKZ97UVwJRT4e+5xo5o/ieKuHuk8NqQ=
github.com/leaanthony/winicon v1.0.0/go.mod h1:en5xhijl92aphrJdmRPlh4NI1L6wq3gEm0LpXAPghjU=
github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs=
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
@ -33,6 +36,8 @@ github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@ -48,12 +53,15 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/tc-hib/winres v0.1.6 h1:qgsYHze+BxQPEYilxIz/KCQGaClvI2+yLBAZs+3+0B8=
github.com/tc-hib/winres v0.1.6/go.mod h1:pe6dOR40VOrGz8PkzreVKNvEKnlE8t4yR8A8naL+t7A=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9 h1:RjggHMcaTVp0LOVZcW0bo8alwHrOaCrGUDgfWUHhnN4=
golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
golang.org/x/image v0.0.0-20200430140353-33d19683fad8 h1:6WW6V3x1P/jokJBpRQYUJnMHRP6isStQwCozxnU7XQw=
golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20201208152932-35266b937fa6 h1:nfeHNc1nAqecKCy2FCy4HY+soOOe5sDLJ/gZLbx6GYI=
golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -62,6 +70,7 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
@ -71,7 +80,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

View file

@ -0,0 +1,15 @@
package examples
import _ "embed"
//go:embed info.json
var Info []byte
//go:embed wails.exe.manifest
var Manifest []byte
//go:embed appicon.png
var AppIcon []byte
//go:embed icon.ico
var IconIco []byte

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View file

@ -0,0 +1,15 @@
{
"fixed": {
"file_version": "v1.0.0"
},
"info": {
"0000": {
"ProductVersion": "v1.0.0",
"CompanyName": "My Company Name",
"FileDescription": "A thing that does a thing",
"LegalCopyright": "(c) 2023 My Company Name",
"ProductName": "My Product Name",
"Comments": "This is a comment"
}
}
}

View file

@ -0,0 +1,15 @@
<?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="com.wails.myproductname" version="v1.0.0.0" processorArchitecture="*"/>
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
</dependentAssembly>
</dependency>
<asmv3:application>
<asmv3:windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware> <!-- fallback for Windows 7 and 8 -->
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2,permonitor</dpiAwareness> <!-- falls back to per-monitor if per-monitor v2 is not supported -->
</asmv3:windowsSettings>
</asmv3:application>
</assembly>

View file

@ -9,14 +9,14 @@ import (
"strings"
"github.com/jackmordaunt/icns/v2"
"github.com/leaanthony/winicon"
"github.com/wailsapp/wails/exp/internal/commands/examples"
)
type IconOptions struct {
Input string `description:"The input image file"`
Sizes string `description:"The sizes to generate in .ico file (comma separated)"`
Example bool `description:"Generate example icon file (appicon.png) in the current directory"`
Input string `description:"The input image file"`
Sizes string `description:"The sizes to generate in .ico file (comma separated)"`
WindowsFilename string `description:"The output filename for the Windows icon"`
MacFilename string `description:"The output filename for the Mac icon bundle"`
}
@ -29,7 +29,12 @@ func (i *IconOptions) Default() *IconOptions {
}
}
func Icon(options *IconOptions) error {
func GenerateIcon(options *IconOptions) error {
if options.Example {
return generateExampleIcon()
}
if options.Input == "" {
return fmt.Errorf("input is required")
}
@ -39,9 +44,13 @@ func Icon(options *IconOptions) error {
}
// Parse sizes
sizes, err := parseSizes(options.Sizes)
if err != nil {
return err
var sizes = []int{256, 128, 64, 48, 32, 16}
var err error
if options.Sizes != "" {
sizes, err = parseSizes(options.Sizes)
if err != nil {
return err
}
}
iconData, err := os.ReadFile(options.Input)
@ -66,6 +75,10 @@ func Icon(options *IconOptions) error {
return nil
}
func generateExampleIcon() error {
return os.WriteFile("appicon.png", []byte(examples.AppIcon), 0644)
}
func parseSizes(sizes string) ([]int, error) {
// split the input string by comma and confirm that each one is an integer
parsedSizes := strings.Split(sizes, ",")

View file

@ -0,0 +1,285 @@
package commands
import (
"fmt"
"os"
"path/filepath"
"runtime"
"testing"
)
func TestGenerateIcon(t *testing.T) {
tests := []struct {
name string
setup func() *IconOptions
wantErr bool
test func() error
}{
{
name: "should generate an icon when using the `example` flag",
setup: func() *IconOptions {
return &IconOptions{
Example: true,
}
},
wantErr: false,
test: func() error {
// the file `appicon.png` should be created in the current directory
// check for the existence of the file
f, err := os.Stat("appicon.png")
if err != nil {
return err
}
defer func() {
err := os.Remove("appicon.png")
if err != nil {
panic(err)
}
}()
if f.IsDir() {
return fmt.Errorf("appicon.png is a directory")
}
if f.Size() == 0 {
return fmt.Errorf("appicon.png is empty")
}
return nil
},
},
{
name: "should generate a .ico file when using the `input` flag and `windowsfilena me` flag",
setup: func() *IconOptions {
// Get the directory of this file
_, thisFile, _, _ := runtime.Caller(1)
localDir := filepath.Dir(thisFile)
// Get the path to the example icon
exampleIcon := filepath.Join(localDir, "examples", "appicon.png")
return &IconOptions{
Input: exampleIcon,
WindowsFilename: "appicon.ico",
}
},
wantErr: false,
test: func() error {
// the file `appicon.ico` should be created in the current directory
// check for the existence of the file
f, err := os.Stat("appicon.ico")
if err != nil {
return err
}
defer func() {
// Remove the file
err = os.Remove("appicon.ico")
if err != nil {
return
}
}()
if f.IsDir() {
return fmt.Errorf("appicon.ico is a directory")
}
if f.Size() == 0 {
return fmt.Errorf("appicon.ico is empty")
}
// Remove the file
err = os.Remove("appicon.ico")
if err != nil {
return err
}
return nil
},
},
{
name: "should generate a .icns file when using the `input` flag and `macfilename` flag",
setup: func() *IconOptions {
// Get the directory of this file
_, thisFile, _, _ := runtime.Caller(1)
localDir := filepath.Dir(thisFile)
// Get the path to the example icon
exampleIcon := filepath.Join(localDir, "examples", "appicon.png")
return &IconOptions{
Input: exampleIcon,
MacFilename: "appicon.icns",
}
},
wantErr: false,
test: func() error {
// the file `appicon.icns` should be created in the current directory
// check for the existence of the file
f, err := os.Stat("appicon.icns")
if err != nil {
return err
}
defer func() {
// Remove the file
err = os.Remove("appicon.icns")
if err != nil {
panic(err)
}
}()
if f.IsDir() {
return fmt.Errorf("appicon.icns is a directory")
}
if f.Size() == 0 {
return fmt.Errorf("appicon.icns is empty")
}
// Remove the file
return nil
},
},
{
name: "should generate a small .ico file when using the `input` flag and `sizes` flag",
setup: func() *IconOptions {
// Get the directory of this file
_, thisFile, _, _ := runtime.Caller(1)
localDir := filepath.Dir(thisFile)
// Get the path to the example icon
exampleIcon := filepath.Join(localDir, "examples", "appicon.png")
return &IconOptions{
Input: exampleIcon,
Sizes: "16",
WindowsFilename: "appicon.ico",
}
},
wantErr: false,
test: func() error {
// the file `appicon.ico` should be created in the current directory
// check for the existence of the file
f, err := os.Stat("appicon.ico")
if err != nil {
return err
}
defer func() {
err := os.Remove("appicon.ico")
if err != nil {
panic(err)
}
}()
// The size of the file should be 571 bytes
if f.Size() != 571 {
return fmt.Errorf("appicon.ico is not the correct size. Got %d", f.Size())
}
if f.IsDir() {
return fmt.Errorf("appicon.ico is a directory")
}
if f.Size() == 0 {
return fmt.Errorf("appicon.ico is empty")
}
return nil
},
},
{
name: "should error if no input file is provided",
setup: func() *IconOptions {
return &IconOptions{}
},
wantErr: true,
},
{
name: "should error if neither mac or windows filename is provided",
setup: func() *IconOptions {
// Get the directory of this file
_, thisFile, _, _ := runtime.Caller(1)
localDir := filepath.Dir(thisFile)
// Get the path to the example icon
exampleIcon := filepath.Join(localDir, "examples", "appicon.png")
return &IconOptions{
Input: exampleIcon,
}
},
wantErr: true,
},
{
name: "should error if bad sizes provided",
setup: func() *IconOptions {
// Get the directory of this file
_, thisFile, _, _ := runtime.Caller(1)
localDir := filepath.Dir(thisFile)
// Get the path to the example icon
exampleIcon := filepath.Join(localDir, "examples", "appicon.png")
return &IconOptions{
Input: exampleIcon,
WindowsFilename: "appicon.ico",
Sizes: "bad",
}
},
wantErr: true,
},
{
name: "should ignore 0 size",
setup: func() *IconOptions {
// Get the directory of this file
_, thisFile, _, _ := runtime.Caller(1)
localDir := filepath.Dir(thisFile)
// Get the path to the example icon
exampleIcon := filepath.Join(localDir, "examples", "appicon.png")
return &IconOptions{
Input: exampleIcon,
WindowsFilename: "appicon.ico",
Sizes: "0,16",
}
},
wantErr: false,
test: func() error {
// Test the file exists and has 571 bytes
f, err := os.Stat("appicon.ico")
if err != nil {
return err
}
defer func() {
err := os.Remove("appicon.ico")
if err != nil {
panic(err)
}
}()
if f.Size() != 571 {
return fmt.Errorf("appicon.ico is not the correct size. Got %d", f.Size())
}
if f.IsDir() {
return fmt.Errorf("appicon.ico is a directory")
}
if f.Size() == 0 {
return fmt.Errorf("appicon.ico is empty")
}
return nil
},
},
{
name: "should error if the input file does not exist",
setup: func() *IconOptions {
return &IconOptions{
Input: "doesnotexist.png",
WindowsFilename: "appicon.ico",
}
},
wantErr: true,
},
{
name: "should error if the input file is not a png",
setup: func() *IconOptions {
// Get the directory of this file
_, thisFile, _, _ := runtime.Caller(1)
return &IconOptions{
Input: thisFile,
WindowsFilename: "appicon.ico",
}
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
options := tt.setup()
err := GenerateIcon(options)
if (err != nil) != tt.wantErr {
t.Errorf("GenerateIcon() error = %v, wantErr %v", err, tt.wantErr)
return
}
if tt.test != nil {
if err := tt.test(); err != nil {
t.Errorf("GenerateIcon() test error = %v", err)
}
}
})
}
}

View file

@ -0,0 +1,125 @@
package commands
import (
"fmt"
"os"
"runtime"
"github.com/tc-hib/winres"
"github.com/tc-hib/winres/version"
"github.com/wailsapp/wails/exp/internal/commands/examples"
)
type SysoOptions struct {
Example bool `description:"Generate example manifest & info files"`
Manifest string `description:"The manifest file"`
Info string `description:"The info.json file"`
Icon string `description:"The icon file"`
Out string `description:"The output filename for the syso file"`
Arch string `description:"The target architecture"`
}
func (i *SysoOptions) Default() *SysoOptions {
return &SysoOptions{
Arch: runtime.GOOS,
Out: "wails-res.syso",
}
}
func GenerateSyso(options *SysoOptions) error {
// Generate example files?
if options.Example {
return generateExampleSyso()
}
if options.Manifest == "" {
return fmt.Errorf("manifest is required")
}
if options.Icon == "" {
return fmt.Errorf("icon is required")
}
rs := winres.ResourceSet{}
// Process Icon
iconFile, err := os.Open(options.Icon)
if err != nil {
return err
}
defer iconFile.Close()
ico, err := winres.LoadICO(iconFile)
if err != nil {
return fmt.Errorf("couldn't load icon '%s': %v", options.Icon, err)
}
err = rs.SetIcon(winres.RT_ICON, ico)
if err != nil {
return err
}
// Process Manifest
manifestData, err := os.ReadFile(options.Manifest)
if err != nil {
return err
}
xmlData, err := winres.AppManifestFromXML(manifestData)
if err != nil {
return err
}
rs.SetManifest(xmlData)
if options.Info != "" {
infoData, err := os.ReadFile(options.Info)
if err != nil {
return err
}
if len(infoData) != 0 {
var v version.Info
if err := v.UnmarshalJSON(infoData); err != nil {
return err
}
rs.SetVersionInfo(v)
}
}
targetFile := options.Out
if targetFile == "" {
targetFile = "wails-res.syso"
}
fout, err := os.Create(targetFile)
if err != nil {
return err
}
defer fout.Close()
archs := map[string]winres.Arch{
"amd64": winres.ArchAMD64,
"arm64": winres.ArchARM64,
"386": winres.ArchI386,
}
targetArch, supported := archs[options.Arch]
if !supported {
return fmt.Errorf("arch '%s' not supported", options.Arch)
}
err = rs.WriteObject(fout, targetArch)
if err != nil {
return err
}
return nil
}
func generateExampleSyso() error {
// Generate example info.json
err := os.WriteFile("info.json", examples.Info, 0644)
if err != nil {
return err
}
// Generate example manifest
err = os.WriteFile("wails.exe.manifest", examples.Manifest, 0644)
if err != nil {
return err
}
return nil
}

View file

@ -0,0 +1,189 @@
package commands
import (
"fmt"
"os"
"path/filepath"
"runtime"
"testing"
)
func TestGenerateSyso(t *testing.T) {
tests := []struct {
name string
setup func() *SysoOptions
wantErr bool
test func() error
}{
{
name: "should generate example info and manifest files when using the `example` flag",
setup: func() *SysoOptions {
return &SysoOptions{
Example: true,
}
},
wantErr: false,
test: func() error {
// the file `info.json` should be created in the current directory
// check for the existence of the file
f, err := os.Stat("info.json")
if err != nil {
return err
}
m, err := os.Stat("wails.exe.manifest")
if err != nil {
return err
}
defer func() {
err := os.Remove("info.json")
err2 := os.Remove("wails.exe.manifest")
if err != nil {
panic(err)
}
if err2 != nil {
panic(err2)
}
}()
if f.IsDir() {
return fmt.Errorf("info.json is a directory")
}
if f.Size() == 0 {
return fmt.Errorf("info.json is empty")
}
if m.IsDir() {
return fmt.Errorf("wails.exe.manifest is a directory")
}
if m.Size() == 0 {
return fmt.Errorf("wails.exe.manifest is empty")
}
return nil
},
},
{
name: "should error if manifest filename is not provided",
setup: func() *SysoOptions {
return &SysoOptions{
Manifest: "",
}
},
wantErr: true,
},
{
name: "should error if icon filename is not provided",
setup: func() *SysoOptions {
return &SysoOptions{
Manifest: "test.manifest",
Icon: "",
}
},
wantErr: true,
},
{
name: "should error if icon filename does not exist",
setup: func() *SysoOptions {
return &SysoOptions{
Manifest: "test.manifest",
Icon: "icon.ico",
}
},
wantErr: true,
},
{
name: "should error if icon is wrong format",
setup: func() *SysoOptions {
_, thisFile, _, _ := runtime.Caller(1)
return &SysoOptions{
Manifest: "test.manifest",
Icon: thisFile,
}
},
wantErr: true,
},
{
name: "should error if manifest filename does not exist",
setup: func() *SysoOptions {
// Get the directory of this file
_, thisFile, _, _ := runtime.Caller(1)
localDir := filepath.Dir(thisFile)
// Get the path to the example icon
exampleIcon := filepath.Join(localDir, "examples", "icon.ico")
return &SysoOptions{
Manifest: "test.manifest",
Icon: exampleIcon,
}
},
wantErr: true,
},
{
name: "should error if manifest is wrong format",
setup: func() *SysoOptions {
// Get the directory of this file
_, thisFile, _, _ := runtime.Caller(1)
localDir := filepath.Dir(thisFile)
// Get the path to the example icon
exampleIcon := filepath.Join(localDir, "examples", "icon.ico")
return &SysoOptions{
Manifest: exampleIcon,
Icon: exampleIcon,
}
},
wantErr: true,
},
{
name: "should error if info file does not exist",
setup: func() *SysoOptions {
// Get the directory of this file
_, thisFile, _, _ := runtime.Caller(1)
localDir := filepath.Dir(thisFile)
// Get the path to the example icon
exampleIcon := filepath.Join(localDir, "examples", "icon.ico")
// Get the path to the example manifest
exampleManifest := filepath.Join(localDir, "examples", "wails.exe.manifest")
return &SysoOptions{
Manifest: exampleManifest,
Icon: exampleIcon,
Info: "doesnotexist.json",
}
},
wantErr: true,
},
{
name: "should error if info file is wrong format",
setup: func() *SysoOptions {
// Get the directory of this file
_, thisFile, _, _ := runtime.Caller(1)
localDir := filepath.Dir(thisFile)
// Get the path to the example icon
exampleIcon := filepath.Join(localDir, "examples", "icon.ico")
// Get the path to the example manifest
exampleManifest := filepath.Join(localDir, "examples", "wails.exe.manifest")
return &SysoOptions{
Manifest: exampleManifest,
Icon: exampleIcon,
Info: thisFile,
}
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
options := tt.setup()
err := GenerateSyso(options)
if (err != nil) != tt.wantErr {
t.Errorf("GenerateSyso() error = %v, wantErr %v", err, tt.wantErr)
return
}
if (err != nil) && tt.wantErr {
println(err.Error())
return
}
if tt.test != nil {
if err := tt.test(); err != nil {
t.Errorf("GenerateSyso() test error = %v", err)
}
}
})
}
}