[v3] Fix Linux Desktop Template & Windows NSIS Template Added (#4510)

* feat: added custom protocols to tmpl for windows wails tools

* fix:: .Info.Protocols, .Info doesn't exist and causes templater to error

* test: tests for build assets

* feat: updated changelog

* feat: insert macro for custom protocols

* Update changelog

---------

Co-authored-by: Lea Anthony <lea.anthony@gmail.com>
This commit is contained in:
Matthew 2025-10-05 12:58:54 +02:00 committed by GitHub
commit f1037c8e22
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 303 additions and 4 deletions

View file

@ -16,13 +16,15 @@ After processing, the content will be moved to the main changelog and this file
-->
## Added
<!-- New features, capabilities, or enhancements -->
- Added NSIS Protocol template for Windows by @Tolfx in #4510
- Added tests for build-assets by @Tolfx in #4510
## Changed
<!-- Changes in existing functionality -->
## Fixed
<!-- Bug fixes -->
- Fixed linux desktop.tmpl protocol range, by removing `<.Info.Protocol>` to `<.Protocol>` by @Tolfx in #4510
- Fixed redefinition error for liquid glass demo in [#4542](https://github.com/wailsapp/wails/pull/4542) by @Etesam913
## Deprecated
<!-- Soon-to-be removed features -->

View file

@ -0,0 +1,267 @@
package commands
import (
"os"
"path/filepath"
"testing"
"gopkg.in/yaml.v3"
)
func TestGenerateBuildAssets(t *testing.T) {
// Create a temporary directory for testing
tempDir, err := os.MkdirTemp("", "wails-build-assets-test-*")
if err != nil {
t.Fatalf("Failed to create temp directory: %v", err)
}
defer os.RemoveAll(tempDir)
tests := []struct {
name string
options *BuildAssetsOptions
wantErr bool
}{
{
name: "Basic build assets generation",
options: &BuildAssetsOptions{
Dir: "testbuild",
Name: "TestApp",
BinaryName: "",
ProductName: "Test Application",
ProductDescription: "A test application",
ProductVersion: "1.0.0",
ProductCompany: "Test Company",
ProductCopyright: "© 2024 Test Company",
ProductComments: "Test comments",
ProductIdentifier: "",
Silent: true,
},
wantErr: false,
},
{
name: "Build assets with custom binary name",
options: &BuildAssetsOptions{
Dir: "testbuild2",
Name: "Custom App",
BinaryName: "custom-binary",
ProductName: "Custom Application",
ProductDescription: "A custom application",
ProductVersion: "2.0.0",
ProductCompany: "Custom Company",
ProductIdentifier: "com.custom.app",
Silent: true,
},
wantErr: false,
},
{
name: "Build assets with MSIX options",
options: &BuildAssetsOptions{
Dir: "testbuild3",
Name: "MSIX App",
ProductName: "MSIX Application",
ProductDescription: "An MSIX application",
ProductVersion: "3.0.0",
ProductCompany: "MSIX Company",
Publisher: "CN=MSIX Company",
ProcessorArchitecture: "x64",
ExecutablePath: "msix-app.exe",
ExecutableName: "msix-app.exe",
OutputPath: "msix-app.msix",
Silent: true,
},
wantErr: false,
},
{
name: "Build assets with TypeScript",
options: &BuildAssetsOptions{
Dir: "testbuild4",
Name: "TypeScript App",
ProductName: "TypeScript Application",
ProductDescription: "A TypeScript application",
ProductVersion: "4.0.0",
ProductCompany: "TypeScript Company",
Typescript: true,
Silent: true,
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Set the directory to be under our temp directory
buildDir := filepath.Join(tempDir, tt.options.Dir)
tt.options.Dir = buildDir
err := GenerateBuildAssets(tt.options)
if (err != nil) != tt.wantErr {
t.Errorf("GenerateBuildAssets() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr {
// Verify that the build directory was created
if _, err := os.Stat(buildDir); os.IsNotExist(err) {
t.Errorf("Build directory %s was not created", buildDir)
}
// List all files that were actually created for debugging
files, err := os.ReadDir(buildDir)
if err != nil {
t.Errorf("Failed to read build directory: %v", err)
} else {
t.Logf("Files created in %s:", buildDir)
for _, file := range files {
t.Logf(" - %s", file.Name())
}
}
// Verify some expected files were created - check what actually exists
expectedFiles := []string{
"config.yml",
"appicon.png",
"Taskfile.yml",
}
for _, file := range expectedFiles {
filePath := filepath.Join(buildDir, file)
if _, err := os.Stat(filePath); os.IsNotExist(err) {
t.Errorf("Expected file %s was not created", file)
}
}
// Test that defaults were applied correctly
if tt.options.ProductIdentifier == "" && tt.options.Name != "" {
expectedIdentifier := "com.wails." + normaliseName(tt.options.Name)
// We can't easily check this without modifying the function to return the config
// but we know the logic is there
_ = expectedIdentifier
}
}
})
}
}
func TestUpdateBuildAssets(t *testing.T) {
// Create a temporary directory for testing
tempDir, err := os.MkdirTemp("", "wails-update-assets-test-*")
if err != nil {
t.Fatalf("Failed to create temp directory: %v", err)
}
defer os.RemoveAll(tempDir)
// Create a sample wails config file
configDir := filepath.Join(tempDir, "config")
err = os.MkdirAll(configDir, 0755)
if err != nil {
t.Fatalf("Failed to create config directory: %v", err)
}
configFile := filepath.Join(configDir, "wails.yaml")
config := WailsConfig{
Info: struct {
CompanyName string `yaml:"companyName"`
ProductName string `yaml:"productName"`
ProductIdentifier string `yaml:"productIdentifier"`
Description string `yaml:"description"`
Copyright string `yaml:"copyright"`
Comments string `yaml:"comments"`
Version string `yaml:"version"`
}{
CompanyName: "Config Company",
ProductName: "Config Product",
ProductIdentifier: "com.config.product",
Description: "Config Description",
Copyright: "© 2024 Config Company",
Comments: "Config Comments",
Version: "1.0.0",
},
FileAssociations: []FileAssociation{
{
Ext: ".test",
Name: "Test File",
Description: "Test file association",
IconName: "test-icon",
Role: "Editor",
MimeType: "application/test",
},
},
Protocols: []ProtocolConfig{
{
Scheme: "testapp",
Description: "Test App Protocol",
},
},
}
configBytes, err := yaml.Marshal(config)
if err != nil {
t.Fatalf("Failed to marshal config: %v", err)
}
err = os.WriteFile(configFile, configBytes, 0644)
if err != nil {
t.Fatalf("Failed to write config file: %v", err)
}
tests := []struct {
name string
options *UpdateBuildAssetsOptions
wantErr bool
}{
{
name: "Update with config file",
options: &UpdateBuildAssetsOptions{
Dir: "updatebuild1",
Name: "UpdateApp",
Config: configFile,
Silent: true,
},
wantErr: false,
},
{
name: "Update without config file",
options: &UpdateBuildAssetsOptions{
Dir: "updatebuild2",
Name: "UpdateApp2",
ProductName: "Update Application 2",
ProductDescription: "An update application 2",
ProductVersion: "2.0.0",
ProductCompany: "Update Company 2",
Silent: true,
},
wantErr: false,
},
{
name: "Update with non-existent config file",
options: &UpdateBuildAssetsOptions{
Dir: "updatebuild3",
Name: "UpdateApp3",
Config: "non-existent-config.yaml",
Silent: true,
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Set the directory to be under our temp directory
updateDir := filepath.Join(tempDir, tt.options.Dir)
tt.options.Dir = updateDir
err := UpdateBuildAssets(tt.options)
if (err != nil) != tt.wantErr {
t.Errorf("UpdateBuildAssets() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr {
// Verify that the update directory was created
if _, err := os.Stat(updateDir); os.IsNotExist(err) {
t.Errorf("Update directory %s was not created", updateDir)
}
}
})
}
}

View file

@ -92,7 +92,8 @@ Section
CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
!insertmacro wails.associateFiles
!insertmacro wails.associateCustomProtocols
!insertmacro wails.writeUninstaller
SectionEnd
@ -107,6 +108,7 @@ Section "uninstall"
Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk"
!insertmacro wails.unassociateFiles
!insertmacro wails.unassociateCustomProtocols
!insertmacro wails.deleteUninstaller
SectionEnd

View file

@ -11,5 +11,5 @@ Categories=Utility;
StartupWMClass={{.BinaryName}}
{{if .Protocols -}}
MimeType={{range $index, $protocol := .Info.Protocols}}x-scheme-handler/{{$protocol.Scheme}};{{end}}
MimeType={{range $index, $protocol := .Protocols}}x-scheme-handler/{{$protocol.Scheme}};{{end}}
{{- end}}

View file

@ -215,4 +215,32 @@ RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}"
!insertmacro APP_UNASSOCIATE "{{.Ext}}" "{{.Name}}"
Delete "$INSTDIR\{{.IconName}}.ico"
{{end}}
!macroend
!macro CUSTOM_PROTOCOL_ASSOCIATE PROTOCOL DESCRIPTION ICON COMMAND
DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "" "${DESCRIPTION}"
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "URL Protocol" ""
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\DefaultIcon" "" "${ICON}"
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell" "" ""
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open" "" ""
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open\command" "" "${COMMAND}"
!macroend
!macro CUSTOM_PROTOCOL_UNASSOCIATE PROTOCOL
DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
!macroend
!macro wails.associateCustomProtocols
; Create custom protocols associations
{{range .Protocols}}
!insertmacro CUSTOM_PROTOCOL_ASSOCIATE "{{.Scheme}}" "{{.Description}}" "$INSTDIR\${PRODUCT_EXECUTABLE},0" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\""
{{end}}
!macroend
!macro wails.unassociateCustomProtocols
; Delete app custom protocol associations
{{range .Protocols}}
!insertmacro CUSTOM_PROTOCOL_UNASSOCIATE "{{.Scheme}}"
{{end}}
!macroend