mirror of
https://github.com/wailsapp/wails.git
synced 2026-03-14 14:45:49 +01:00
Add icon generation
Add syso generation Add tests
This commit is contained in:
parent
f4044ebcd9
commit
e5da0eb5f8
13 changed files with 742 additions and 16 deletions
56
exp/cmd/wails/README.md
Normal file
56
exp/cmd/wails/README.md
Normal 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.
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
16
exp/go.sum
16
exp/go.sum
|
|
@ -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=
|
||||
|
|
|
|||
BIN
exp/internal/commands/examples/appicon.png
Normal file
BIN
exp/internal/commands/examples/appicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 130 KiB |
15
exp/internal/commands/examples/examples.go
Normal file
15
exp/internal/commands/examples/examples.go
Normal 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
|
||||
BIN
exp/internal/commands/examples/icon.ico
Normal file
BIN
exp/internal/commands/examples/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
15
exp/internal/commands/examples/info.json
Normal file
15
exp/internal/commands/examples/info.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
15
exp/internal/commands/examples/wails.exe.manifest
Normal file
15
exp/internal/commands/examples/wails.exe.manifest
Normal 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>
|
||||
|
|
@ -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, ",")
|
||||
|
|
|
|||
285
exp/internal/commands/icon_test.go
Normal file
285
exp/internal/commands/icon_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
125
exp/internal/commands/syso.go
Normal file
125
exp/internal/commands/syso.go
Normal 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
|
||||
}
|
||||
189
exp/internal/commands/syso_test.go
Normal file
189
exp/internal/commands/syso_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue