Skip to content
Merged
5 changes: 3 additions & 2 deletions v3/UNRELEASED_CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,14 @@ 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
- Added tests for build-assets

## Changed
<!-- Changes in existing functionality -->

## Fixed
<!-- Bug fixes -->
- Fixed linux desktop.tmpl protocol range, by removing `<.Info.Protocol>` to `<.Protocol>`

## Deprecated
<!-- Soon-to-be removed features -->
Expand Down
267 changes: 267 additions & 0 deletions v3/internal/commands/build-assets_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ Section
CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"

!insertmacro wails.associateFiles

!insertmacro wails.associateCustomProtocols

!insertmacro wails.writeUninstaller
SectionEnd

Expand All @@ -107,6 +108,7 @@ Section "uninstall"
Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk"

!insertmacro wails.unassociateFiles
!insertmacro wails.unassociateCustomProtocols

!insertmacro wails.deleteUninstaller
SectionEnd
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +222 to +228
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Quote macro parameters with backticks to safely handle spaces and special chars

The existing APP_ASSOCIATE macro uses backticks when writing ${DESCRIPTION}, ${ICON}, ${COMMAND}. Mirror that here to avoid issues with spaces/quotes in descriptions or paths.

Apply this diff:

-  WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "" "${DESCRIPTION}"
+  WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "" `${DESCRIPTION}`
-  WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\DefaultIcon" "" "${ICON}"
+  WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\DefaultIcon" "" `${ICON}`
-  WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open\command" "" "${COMMAND}"
+  WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open\command" "" `${COMMAND}`

If you adopt SHCTX per the previous comment, replace SHELL_CONTEXT with SHCTX accordingly in the same diff.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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
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
🤖 Prompt for AI Agents
In v3/internal/commands/updatable_build_assets/windows/nsis/wails_tools.nsh.tmpl
around lines 222 to 228, the registry WriteRegStr calls use unquoted macro
parameters which can break on spaces/special characters; update each WriteRegStr
to quote the macro parameters with backticks (e.g. replace ${DESCRIPTION},
${ICON}, ${COMMAND} with `${DESCRIPTION}`, `${ICON}`, `${COMMAND}`) and, if you
adopted the SHCTX name from the previous comment, also replace SHELL_CONTEXT
with SHCTX consistently in these lines.


!macro CUSTOM_PROTOCOL_UNASSOCIATE PROTOCOL
DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
!macroend

Comment on lines +220 to +233
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Use SHCTX for registry root (or define SHELL_CONTEXT) to ensure correct per-user/all-users context

NSIS supports the SHCTX root, which respects SetShellVarContext. Using unrecognized roots can break builds. This file’s earlier macros also use SHELL_CONTEXT; if that isn’t defined elsewhere, builds may fail. Two options:

  • Define SHELL_CONTEXT as SHCTX once near the top (preferred for consistency).
  • Or change these new macros to use SHCTX directly.

Minimal global alias (outside the selected range; add near the top after includes):

!ifndef SHELL_CONTEXT
  !define SHELL_CONTEXT SHCTX
!endif

Alternatively, change within this block:

-  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}"
+  DeleteRegKey SHCTX "Software\Classes\${PROTOCOL}"
+  WriteRegStr SHCTX "Software\Classes\${PROTOCOL}" "" "${DESCRIPTION}"
+  WriteRegStr SHCTX "Software\Classes\${PROTOCOL}" "URL Protocol" ""
+  WriteRegStr SHCTX "Software\Classes\${PROTOCOL}\DefaultIcon" "" "${ICON}"
+  WriteRegStr SHCTX "Software\Classes\${PROTOCOL}\shell" "" ""
+  WriteRegStr SHCTX "Software\Classes\${PROTOCOL}\shell\open" "" ""
+  WriteRegStr SHCTX "Software\Classes\${PROTOCOL}\shell\open\command" "" "${COMMAND}"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
!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 CUSTOM_PROTOCOL_ASSOCIATE PROTOCOL DESCRIPTION ICON COMMAND
DeleteRegKey SHCTX "Software\Classes\${PROTOCOL}"
WriteRegStr SHCTX "Software\Classes\${PROTOCOL}" "" "${DESCRIPTION}"
WriteRegStr SHCTX "Software\Classes\${PROTOCOL}" "URL Protocol" ""
WriteRegStr SHCTX "Software\Classes\${PROTOCOL}\DefaultIcon" "" "${ICON}"
WriteRegStr SHCTX "Software\Classes\${PROTOCOL}\shell" "" ""
WriteRegStr SHCTX "Software\Classes\${PROTOCOL}\shell\open" "" ""
WriteRegStr SHCTX "Software\Classes\${PROTOCOL}\shell\open\command" "" "${COMMAND}"
!macroend
!macro CUSTOM_PROTOCOL_UNASSOCIATE PROTOCOL
DeleteRegKey SHCTX "Software\Classes\${PROTOCOL}"
!macroend
🤖 Prompt for AI Agents
v3/internal/commands/updatable_build_assets/windows/nsis/wails_tools.nsh.tmpl
around lines 220-233: the new CUSTOM_PROTOCOL_* macros use SHELL_CONTEXT which
may be undefined and NSIS expects SHCTX (or a defined alias) for correct
per-user/all-users registry root; fix by defining SHELL_CONTEXT as SHCTX near
the top of the file (after includes) using a conditional !ifndef/!define block,
or alternatively replace SHELL_CONTEXT in these macros with SHCTX directly so
the registry root respects SetShellVarContext.

!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
Loading