diff --git a/exp/cmd/wails/README.md b/exp/cmd/wails/README.md new file mode 100644 index 000000000..cb1b97df5 --- /dev/null +++ b/exp/cmd/wails/README.md @@ -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 +``` + +| 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. \ No newline at end of file diff --git a/exp/cmd/wails/main.go b/exp/cmd/wails/main.go index 453f62255..b870c8203 100644 --- a/exp/cmd/wails/main.go +++ b/exp/cmd/wails/main.go @@ -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) diff --git a/exp/go.mod b/exp/go.mod index 436147e58..f8401aead 100644 --- a/exp/go.mod +++ b/exp/go.mod @@ -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 ) diff --git a/exp/go.sum b/exp/go.sum index d82507a7f..872708ee1 100644 --- a/exp/go.sum +++ b/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= diff --git a/exp/internal/commands/examples/appicon.png b/exp/internal/commands/examples/appicon.png new file mode 100644 index 000000000..63617fe4f Binary files /dev/null and b/exp/internal/commands/examples/appicon.png differ diff --git a/exp/internal/commands/examples/examples.go b/exp/internal/commands/examples/examples.go new file mode 100644 index 000000000..01b1b9a2c --- /dev/null +++ b/exp/internal/commands/examples/examples.go @@ -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 diff --git a/exp/internal/commands/examples/icon.ico b/exp/internal/commands/examples/icon.ico new file mode 100644 index 000000000..f33479841 Binary files /dev/null and b/exp/internal/commands/examples/icon.ico differ diff --git a/exp/internal/commands/examples/info.json b/exp/internal/commands/examples/info.json new file mode 100644 index 000000000..1005eb5cb --- /dev/null +++ b/exp/internal/commands/examples/info.json @@ -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" + } + } +} \ No newline at end of file diff --git a/exp/internal/commands/examples/wails.exe.manifest b/exp/internal/commands/examples/wails.exe.manifest new file mode 100644 index 000000000..fb1ce5bde --- /dev/null +++ b/exp/internal/commands/examples/wails.exe.manifest @@ -0,0 +1,15 @@ + + + + + + + + + + + true/pm + permonitorv2,permonitor + + + \ No newline at end of file diff --git a/exp/internal/commands/icon.go b/exp/internal/commands/icon.go index f4e6480e1..a7a3f5dc9 100644 --- a/exp/internal/commands/icon.go +++ b/exp/internal/commands/icon.go @@ -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, ",") diff --git a/exp/internal/commands/icon_test.go b/exp/internal/commands/icon_test.go new file mode 100644 index 000000000..4a911bfc2 --- /dev/null +++ b/exp/internal/commands/icon_test.go @@ -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) + } + } + }) + } +} diff --git a/exp/internal/commands/syso.go b/exp/internal/commands/syso.go new file mode 100644 index 000000000..858367873 --- /dev/null +++ b/exp/internal/commands/syso.go @@ -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 +} diff --git a/exp/internal/commands/syso_test.go b/exp/internal/commands/syso_test.go new file mode 100644 index 000000000..a1e18e3dc --- /dev/null +++ b/exp/internal/commands/syso_test.go @@ -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) + } + } + }) + } +}