From 20e31307c5637f187e5dded81708a0bca3a76498 Mon Sep 17 00:00:00 2001 From: Andraz Vrhovec Date: Thu, 13 Aug 2026 10:23:11 +0000 Subject: [PATCH 1/8] feat(v3/linux): allow overriding the GTK application id appNew hardcoded the id as "org.wails." plus a sanitised Options.Name. WebKit derives the accessibility bus name it asks the portal to own from that id, and a flatpak may only own names prefixed with its own app id, so the request is refused and the web process aborts. The abort reaches Go as a SIGTRAP raised during the cgo call to g_application_run, which reads as a Wails or GTK crash rather than a permission refusal. Add Options.Linux.ApplicationID, defaulting to the derived id when unset, so an app can match the id its packaging requires. --- v3/examples/linux-flatpak-appid/README.md | 52 ++++++++++++++ v3/examples/linux-flatpak-appid/Taskfile.yml | 47 ++++++++++++ .../linux-flatpak-appid/flatpak/.gitignore | 5 ++ .../com.example.WailsFlatpakAppId.desktop | 7 ++ .../flatpak/com.example.WailsFlatpakAppId.png | Bin 0 -> 3270 bytes .../flatpak/com.example.WailsFlatpakAppId.yml | 37 ++++++++++ v3/examples/linux-flatpak-appid/main.go | 67 ++++++++++++++++++ v3/pkg/application/application_linux.go | 13 +++- v3/pkg/application/application_linux_gtk3.go | 13 +++- v3/pkg/application/application_options.go | 10 +++ v3/pkg/application/linux_cgo.go | 3 +- v3/pkg/application/linux_cgo_gtk3.go | 5 +- 12 files changed, 250 insertions(+), 9 deletions(-) create mode 100644 v3/examples/linux-flatpak-appid/README.md create mode 100644 v3/examples/linux-flatpak-appid/Taskfile.yml create mode 100644 v3/examples/linux-flatpak-appid/flatpak/.gitignore create mode 100644 v3/examples/linux-flatpak-appid/flatpak/com.example.WailsFlatpakAppId.desktop create mode 100644 v3/examples/linux-flatpak-appid/flatpak/com.example.WailsFlatpakAppId.png create mode 100644 v3/examples/linux-flatpak-appid/flatpak/com.example.WailsFlatpakAppId.yml create mode 100644 v3/examples/linux-flatpak-appid/main.go diff --git a/v3/examples/linux-flatpak-appid/README.md b/v3/examples/linux-flatpak-appid/README.md new file mode 100644 index 00000000000..f657c046227 --- /dev/null +++ b/v3/examples/linux-flatpak-appid/README.md @@ -0,0 +1,52 @@ +# linux-flatpak-appid (fix side) + +Fix verification for the startup crash reproduced by the example of the same +name in the unpatched checkout. Same manifest, same app id, same launch path. +`main.go` differs by one option: + +```go +Linux: application.LinuxOptions{ + ApplicationID: "com.example.WailsFlatpakAppId", +}, +``` + +Unlike the GTK4 uniqueness fix, this one is opt-in. The patched tree alone +changes nothing — an app that does not set `ApplicationID` still gets +`org.wails.` and still crashes, which is what keeps the patch backward +compatible. + +## Test + +```sh +cd v3/examples/linux-flatpak-appid +wails3 task bundle +flatpak install --user ./flatpak/com.example.WailsFlatpakAppId.flatpak +``` + +Then launch **Wails Flatpak App ID** from the desktop with `journalctl --user -f` +open. The window should open and the journal should be quiet. + +On the unpatched tree the same steps produce: + +``` +Portal call failed: Invalid sandbox a11y own name: + 'org.wails.linux-flatpak-appid.Sandboxed.WebProcess-' doesn't match app id +SIGTRAP: trace trap +``` + +Both bundles carry the same app id, so install one at a time — installing either +replaces the other, which is what makes them directly comparable. + +Launching from the menu matters. The web process only makes the portal call when +the accessibility bus is reachable inside the sandbox, so a terminal launch can +pass on the unpatched build and prove nothing. + +## The fix + +`Options.Linux.ApplicationID` overrides the id the GtkApplication is built with, +defaulting to the derived `org.wails.` when unset. Matching it to the +flatpak app id makes the accessibility name WebKit requests fall under the app +id prefix the sandbox permits. + +It also lets the desktop match windows to the installed `.desktop` entry, which +the derived id never does. diff --git a/v3/examples/linux-flatpak-appid/Taskfile.yml b/v3/examples/linux-flatpak-appid/Taskfile.yml new file mode 100644 index 00000000000..ff91d862ee8 --- /dev/null +++ b/v3/examples/linux-flatpak-appid/Taskfile.yml @@ -0,0 +1,47 @@ +version: '3' + +vars: + APP_ID: com.example.WailsFlatpakAppId + BINARY: wails-flatpak-appid + +tasks: + build: + summary: Builds the binary into the flatpak build context + dir: '{{.TASKFILE_DIR}}' + cmds: + - go build -o flatpak/{{.BINARY}} . + + package: + summary: Builds the flatpak + deps: [build] + dir: '{{.TASKFILE_DIR}}/flatpak' + cmds: + - flatpak-builder --force-clean --disable-rofiles-fuse build-dir {{.APP_ID}}.yml + + run: + summary: Runs the app inside the sandbox without installing it + deps: [package] + dir: '{{.TASKFILE_DIR}}/flatpak' + cmds: + - flatpak-builder --run build-dir {{.APP_ID}}.yml {{.BINARY}} + + install: + summary: Installs the flatpak so it can be launched from the desktop + deps: [build] + dir: '{{.TASKFILE_DIR}}/flatpak' + cmds: + - flatpak-builder --force-clean --disable-rofiles-fuse --user --install build-dir {{.APP_ID}}.yml + + bundle: + summary: Builds a single-file .flatpak to install elsewhere + deps: [build] + dir: '{{.TASKFILE_DIR}}/flatpak' + cmds: + - flatpak-builder --force-clean --disable-rofiles-fuse --repo=repo build-dir {{.APP_ID}}.yml + - flatpak build-bundle repo {{.APP_ID}}.flatpak {{.APP_ID}} + - 'echo "Install it with: flatpak install --user ./{{.APP_ID}}.flatpak"' + + clean: + dir: '{{.TASKFILE_DIR}}/flatpak' + cmds: + - rm -rf build-dir .flatpak-builder repo {{.BINARY}} {{.APP_ID}}.flatpak diff --git a/v3/examples/linux-flatpak-appid/flatpak/.gitignore b/v3/examples/linux-flatpak-appid/flatpak/.gitignore new file mode 100644 index 00000000000..356bdb03399 --- /dev/null +++ b/v3/examples/linux-flatpak-appid/flatpak/.gitignore @@ -0,0 +1,5 @@ +build-dir/ +.flatpak-builder/ +repo/ +wails-flatpak-appid +*.flatpak diff --git a/v3/examples/linux-flatpak-appid/flatpak/com.example.WailsFlatpakAppId.desktop b/v3/examples/linux-flatpak-appid/flatpak/com.example.WailsFlatpakAppId.desktop new file mode 100644 index 00000000000..33d499d4770 --- /dev/null +++ b/v3/examples/linux-flatpak-appid/flatpak/com.example.WailsFlatpakAppId.desktop @@ -0,0 +1,7 @@ +[Desktop Entry] +Type=Application +Name=Wails Flatpak App ID +Exec=wails-flatpak-appid +Icon=com.example.WailsFlatpakAppId +Categories=Development; +Terminal=false diff --git a/v3/examples/linux-flatpak-appid/flatpak/com.example.WailsFlatpakAppId.png b/v3/examples/linux-flatpak-appid/flatpak/com.example.WailsFlatpakAppId.png new file mode 100644 index 0000000000000000000000000000000000000000..61da111c4a3e9bce695e62525c684a750686589d GIT binary patch literal 3270 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 +

Window opened

+

Reaching this means the web process was allowed to start.

+ `)) + }), + }, + }) + + app.Window.NewWithOptions(application.WebviewWindowOptions{ + Title: "linux-flatpak-appid", + Width: 700, + Height: 400, + URL: "/", + }) + + log.Printf("[pid %d] calling app.Run()", os.Getpid()) + if err := app.Run(); err != nil { + log.Fatal(err) + } +} diff --git a/v3/pkg/application/application_linux.go b/v3/pkg/application/application_linux.go index db707e9e834..1284d713bbf 100644 --- a/v3/pkg/application/application_linux.go +++ b/v3/pkg/application/application_linux.go @@ -42,6 +42,16 @@ func sanitizeAppName(name string) string { return strings.ToLower(name) } +// applicationID returns the id to build the GtkApplication with. Options.Linux +// wins when it sets one, so sandboxed builds can match the id their runtime +// expects; everything else keeps the derived "org.wails.". +func applicationID(options Options) string { + if options.Linux.ApplicationID != "" { + return options.Linux.ApplicationID + } + return "org.wails." + sanitizeAppName(options.Name) +} + func init() { // Disable DMA-BUF renderer on any session type with NVIDIA to prevent blank windows and // "Error 71 (Protocol error)" crashes. NVIDIA proprietary drivers fail gbm_bo_map() when @@ -209,10 +219,9 @@ func (a *linuxApp) unregisterWindow(window windowPointer) { } func newPlatformApp(parent *App) *linuxApp { - name := sanitizeAppName(parent.options.Name) app := &linuxApp{ parent: parent, - application: appNew(name), + application: appNew(applicationID(parent.options)), activated: make(chan struct{}), windowMap: map[windowPointer]uint{}, } diff --git a/v3/pkg/application/application_linux_gtk3.go b/v3/pkg/application/application_linux_gtk3.go index e61fd53261f..1a292f1b225 100644 --- a/v3/pkg/application/application_linux_gtk3.go +++ b/v3/pkg/application/application_linux_gtk3.go @@ -50,6 +50,16 @@ func sanitizeAppName(name string) string { return strings.ToLower(name) } +// applicationID returns the id to build the GtkApplication with. Options.Linux +// wins when it sets one, so sandboxed builds can match the id their runtime +// expects; everything else keeps the derived "org.wails.". +func applicationID(options Options) string { + if options.Linux.ApplicationID != "" { + return options.Linux.ApplicationID + } + return "org.wails." + sanitizeAppName(options.Name) +} + func init() { // FIXME: This should be handled appropriately in the individual files most likely. // Set GDK_BACKEND=x11 if currently unset and XDG_SESSION_TYPE is unset, unspecified or x11 to prevent warnings @@ -227,10 +237,9 @@ func (a *linuxApp) getAccentColor() string { } func newPlatformApp(parent *App) *linuxApp { - name := sanitizeAppName(parent.options.Name) app := &linuxApp{ parent: parent, - application: appNew(name), + application: appNew(applicationID(parent.options)), windowMap: map[windowPointer]uint{}, } diff --git a/v3/pkg/application/application_options.go b/v3/pkg/application/application_options.go index 560436a36d8..a01136d89ee 100644 --- a/v3/pkg/application/application_options.go +++ b/v3/pkg/application/application_options.go @@ -329,6 +329,16 @@ type LinuxOptions struct { // //[see the docs]: https://docs.gtk.org/glib/func.set_prgname.html ProgramName string + + // ApplicationID overrides the GTK application id, which otherwise defaults + // to "org.wails." followed by a sanitised Name. + // + // Sandboxed builds have to set this. A flatpak may only own bus names + // prefixed with its app id, and WebKit asks the portal to own + // ".Sandboxed.WebProcess-" for the accessibility bus. + // With the default id that request is refused and the web process aborts, + // taking the application down from inside g_application_run. + ApplicationID string } /********* iOS Options *********/ diff --git a/v3/pkg/application/linux_cgo.go b/v3/pkg/application/linux_cgo.go index e4751c88262..111443c4de4 100644 --- a/v3/pkg/application/linux_cgo.go +++ b/v3/pkg/application/linux_cgo.go @@ -143,10 +143,9 @@ func appName() string { return C.GoString(name) } -func appNew(name string) pointer { +func appNew(appId string) pointer { C.install_signal_handlers() - appId := fmt.Sprintf("org.wails.%s", name) nameC := C.CString(appId) defer C.free(unsafe.Pointer(nameC)) return pointer(C.gtk_application_new(nameC, C.APPLICATION_DEFAULT_FLAGS)) diff --git a/v3/pkg/application/linux_cgo_gtk3.go b/v3/pkg/application/linux_cgo_gtk3.go index 4b4be6eaf28..78fcc2ac738 100644 --- a/v3/pkg/application/linux_cgo_gtk3.go +++ b/v3/pkg/application/linux_cgo_gtk3.go @@ -618,9 +618,8 @@ func appName() string { return C.GoString(name) } -func appNew(name string) pointer { - // Name is already sanitized by sanitizeAppName() in application_linux.go - appId := fmt.Sprintf("org.wails.%s", name) +func appNew(appId string) pointer { + // Already assembled and sanitized by applicationID() in application_linux_gtk3.go nameC := C.CString(appId) defer C.free(unsafe.Pointer(nameC)) return pointer(C.gtk_application_new(nameC, C.APPLICATION_DEFAULT_FLAGS)) From 4844cadcbfcdb4dfff7c68d1507c162e9880ddfc Mon Sep 17 00:00:00 2001 From: Andraz Vrhovec Date: Thu, 13 Aug 2026 11:44:05 +0000 Subject: [PATCH 2/8] docs(v3): document Options.Linux.ApplicationID The Linux options block listed fields with no explanation. Describe the three, and call out that sandboxed builds have to set the application id, since the symptom otherwise reads as a crash in Wails or GTK rather than a permission refusal. --- .../content/docs/reference/application.mdx | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/docs/src/content/docs/reference/application.mdx b/docs/src/content/docs/reference/application.mdx index a590cd17a65..aae8693d073 100644 --- a/docs/src/content/docs/reference/application.mdx +++ b/docs/src/content/docs/reference/application.mdx @@ -463,12 +463,37 @@ app := application.New(application.Options{ app := application.New(application.Options{ Name: "My App", Linux: application.LinuxOptions{ + ApplicationID: "com.myapp.myapplication", ProgramName: "my-app", DisableQuitOnLastWindowClosed: false, }, }) ``` +- `ApplicationID` - The GTK application id. Defaults to `org.wails.` followed by a sanitised `Name`. +- `ProgramName` - Sets the program name for the window manager via `g_set_prgname()`. +- `DisableQuitOnLastWindowClosed` - Keeps the application running after its last window closes. + +Set `ApplicationID` to match the identity your packaging declares. It should be +the same reverse-DNS id you use for the `.desktop` file, so the desktop +environment can associate the application's windows with its launcher. + +:::note +Sandboxed builds must set this. A flatpak may only own D-Bus names prefixed with +its own app id, and WebKit asks the portal to own +`.Sandboxed.WebProcess-` for the accessibility bus. With +the default id that request is refused and the web process aborts, taking the +application down during startup: + +``` +Portal call failed: Invalid sandbox a11y own name: + 'org.wails.myapp.Sandboxed.WebProcess-' doesn't match app id +``` + +Setting `ApplicationID` to the `app-id` in your flatpak manifest resolves it. +See the `linux-flatpak-appid` example. +::: + ## Complete Application Example ```go From f2eb75baa4e1f5f07188ae0222fa35cf6b52ecb5 Mon Sep 17 00:00:00 2001 From: Andraz Vrhovec Date: Thu, 13 Aug 2026 11:47:56 +0000 Subject: [PATCH 3/8] test(v3/linux): cover the application id derivation Assert that ApplicationID is used verbatim when set and that an application which leaves it unset keeps the id it had before the option existed, since that fallback is what makes the option backward compatible. The test file is tagged for linux && cgo without excluding gtk3, so it runs against both backends, which derive the id identically. --- v3/pkg/application/application_linux_test.go | 67 +++++++++++++++++++ .../application/application_options_test.go | 3 + 2 files changed, 70 insertions(+) create mode 100644 v3/pkg/application/application_linux_test.go diff --git a/v3/pkg/application/application_linux_test.go b/v3/pkg/application/application_linux_test.go new file mode 100644 index 00000000000..79da0d09f0a --- /dev/null +++ b/v3/pkg/application/application_linux_test.go @@ -0,0 +1,67 @@ +//go:build linux && cgo && !android && !server + +package application + +import "testing" + +// Both backends derive the id the same way, so this covers the GTK4 and GTK3 +// builds alike. +func TestApplicationID(t *testing.T) { + tests := []struct { + name string + options Options + want string + }{ + { + name: "derived from Name when unset", + options: Options{Name: "My App"}, + want: "org.wails.my_app", + }, + { + name: "hyphens are kept when derived", + options: Options{Name: "koofr-stage"}, + want: "org.wails.koofr-stage", + }, + { + name: "empty Name falls back to wailsapp", + options: Options{}, + want: "org.wails.wailsapp", + }, + { + name: "used verbatim when set", + options: Options{ + Name: "My App", + Linux: LinuxOptions{ApplicationID: "com.myapp.myapplication"}, + }, + want: "com.myapp.myapplication", + }, + { + // Sandboxed builds depend on this: the id has to be exactly the one + // the packaging declares, with nothing derived from Name mixed in. + name: "set id wins over Name entirely", + options: Options{ + Name: "something else", + Linux: LinuxOptions{ApplicationID: "com.example.WailsFlatpakAppId"}, + }, + want: "com.example.WailsFlatpakAppId", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := applicationID(tt.options); got != tt.want { + t.Errorf("applicationID() = %q, want %q", got, tt.want) + } + }) + } +} + +// The option is opt-in, so an application that does not set it has to keep the +// id it had before the option existed. +func TestApplicationIDUnsetIsBackwardCompatible(t *testing.T) { + options := Options{Name: "My App"} + + if got, want := applicationID(options), "org.wails."+sanitizeAppName(options.Name); got != want { + t.Errorf("applicationID() = %q, want the derived id %q", got, want) + } +} diff --git a/v3/pkg/application/application_options_test.go b/v3/pkg/application/application_options_test.go index 77ddbe522e9..47c48189d06 100644 --- a/v3/pkg/application/application_options_test.go +++ b/v3/pkg/application/application_options_test.go @@ -203,6 +203,9 @@ func TestLinuxOptions_Defaults(t *testing.T) { if opts.ProgramName != "" { t.Error("ProgramName should default to empty string") } + if opts.ApplicationID != "" { + t.Error("ApplicationID should default to empty string") + } } func TestIOSOptions_Defaults(t *testing.T) { From b7646473989d1582d966dff02b7285aaadc1b609 Mon Sep 17 00:00:00 2001 From: Andraz Vrhovec Date: Thu, 13 Aug 2026 11:48:57 +0000 Subject: [PATCH 4/8] cleanup, remove failing example linux-flatpak-appid --- v3/examples/linux-flatpak-appid/README.md | 52 -------------- v3/examples/linux-flatpak-appid/Taskfile.yml | 47 ------------ .../linux-flatpak-appid/flatpak/.gitignore | 5 -- .../com.example.WailsFlatpakAppId.desktop | 7 -- .../flatpak/com.example.WailsFlatpakAppId.png | Bin 3270 -> 0 bytes .../flatpak/com.example.WailsFlatpakAppId.yml | 37 ---------- v3/examples/linux-flatpak-appid/main.go | 67 ------------------ 7 files changed, 215 deletions(-) delete mode 100644 v3/examples/linux-flatpak-appid/README.md delete mode 100644 v3/examples/linux-flatpak-appid/Taskfile.yml delete mode 100644 v3/examples/linux-flatpak-appid/flatpak/.gitignore delete mode 100644 v3/examples/linux-flatpak-appid/flatpak/com.example.WailsFlatpakAppId.desktop delete mode 100644 v3/examples/linux-flatpak-appid/flatpak/com.example.WailsFlatpakAppId.png delete mode 100644 v3/examples/linux-flatpak-appid/flatpak/com.example.WailsFlatpakAppId.yml delete mode 100644 v3/examples/linux-flatpak-appid/main.go diff --git a/v3/examples/linux-flatpak-appid/README.md b/v3/examples/linux-flatpak-appid/README.md deleted file mode 100644 index f657c046227..00000000000 --- a/v3/examples/linux-flatpak-appid/README.md +++ /dev/null @@ -1,52 +0,0 @@ -# linux-flatpak-appid (fix side) - -Fix verification for the startup crash reproduced by the example of the same -name in the unpatched checkout. Same manifest, same app id, same launch path. -`main.go` differs by one option: - -```go -Linux: application.LinuxOptions{ - ApplicationID: "com.example.WailsFlatpakAppId", -}, -``` - -Unlike the GTK4 uniqueness fix, this one is opt-in. The patched tree alone -changes nothing — an app that does not set `ApplicationID` still gets -`org.wails.` and still crashes, which is what keeps the patch backward -compatible. - -## Test - -```sh -cd v3/examples/linux-flatpak-appid -wails3 task bundle -flatpak install --user ./flatpak/com.example.WailsFlatpakAppId.flatpak -``` - -Then launch **Wails Flatpak App ID** from the desktop with `journalctl --user -f` -open. The window should open and the journal should be quiet. - -On the unpatched tree the same steps produce: - -``` -Portal call failed: Invalid sandbox a11y own name: - 'org.wails.linux-flatpak-appid.Sandboxed.WebProcess-' doesn't match app id -SIGTRAP: trace trap -``` - -Both bundles carry the same app id, so install one at a time — installing either -replaces the other, which is what makes them directly comparable. - -Launching from the menu matters. The web process only makes the portal call when -the accessibility bus is reachable inside the sandbox, so a terminal launch can -pass on the unpatched build and prove nothing. - -## The fix - -`Options.Linux.ApplicationID` overrides the id the GtkApplication is built with, -defaulting to the derived `org.wails.` when unset. Matching it to the -flatpak app id makes the accessibility name WebKit requests fall under the app -id prefix the sandbox permits. - -It also lets the desktop match windows to the installed `.desktop` entry, which -the derived id never does. diff --git a/v3/examples/linux-flatpak-appid/Taskfile.yml b/v3/examples/linux-flatpak-appid/Taskfile.yml deleted file mode 100644 index ff91d862ee8..00000000000 --- a/v3/examples/linux-flatpak-appid/Taskfile.yml +++ /dev/null @@ -1,47 +0,0 @@ -version: '3' - -vars: - APP_ID: com.example.WailsFlatpakAppId - BINARY: wails-flatpak-appid - -tasks: - build: - summary: Builds the binary into the flatpak build context - dir: '{{.TASKFILE_DIR}}' - cmds: - - go build -o flatpak/{{.BINARY}} . - - package: - summary: Builds the flatpak - deps: [build] - dir: '{{.TASKFILE_DIR}}/flatpak' - cmds: - - flatpak-builder --force-clean --disable-rofiles-fuse build-dir {{.APP_ID}}.yml - - run: - summary: Runs the app inside the sandbox without installing it - deps: [package] - dir: '{{.TASKFILE_DIR}}/flatpak' - cmds: - - flatpak-builder --run build-dir {{.APP_ID}}.yml {{.BINARY}} - - install: - summary: Installs the flatpak so it can be launched from the desktop - deps: [build] - dir: '{{.TASKFILE_DIR}}/flatpak' - cmds: - - flatpak-builder --force-clean --disable-rofiles-fuse --user --install build-dir {{.APP_ID}}.yml - - bundle: - summary: Builds a single-file .flatpak to install elsewhere - deps: [build] - dir: '{{.TASKFILE_DIR}}/flatpak' - cmds: - - flatpak-builder --force-clean --disable-rofiles-fuse --repo=repo build-dir {{.APP_ID}}.yml - - flatpak build-bundle repo {{.APP_ID}}.flatpak {{.APP_ID}} - - 'echo "Install it with: flatpak install --user ./{{.APP_ID}}.flatpak"' - - clean: - dir: '{{.TASKFILE_DIR}}/flatpak' - cmds: - - rm -rf build-dir .flatpak-builder repo {{.BINARY}} {{.APP_ID}}.flatpak diff --git a/v3/examples/linux-flatpak-appid/flatpak/.gitignore b/v3/examples/linux-flatpak-appid/flatpak/.gitignore deleted file mode 100644 index 356bdb03399..00000000000 --- a/v3/examples/linux-flatpak-appid/flatpak/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -build-dir/ -.flatpak-builder/ -repo/ -wails-flatpak-appid -*.flatpak diff --git a/v3/examples/linux-flatpak-appid/flatpak/com.example.WailsFlatpakAppId.desktop b/v3/examples/linux-flatpak-appid/flatpak/com.example.WailsFlatpakAppId.desktop deleted file mode 100644 index 33d499d4770..00000000000 --- a/v3/examples/linux-flatpak-appid/flatpak/com.example.WailsFlatpakAppId.desktop +++ /dev/null @@ -1,7 +0,0 @@ -[Desktop Entry] -Type=Application -Name=Wails Flatpak App ID -Exec=wails-flatpak-appid -Icon=com.example.WailsFlatpakAppId -Categories=Development; -Terminal=false diff --git a/v3/examples/linux-flatpak-appid/flatpak/com.example.WailsFlatpakAppId.png b/v3/examples/linux-flatpak-appid/flatpak/com.example.WailsFlatpakAppId.png deleted file mode 100644 index 61da111c4a3e9bce695e62525c684a750686589d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3270 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 -

Window opened

-

Reaching this means the web process was allowed to start.

- `)) - }), - }, - }) - - app.Window.NewWithOptions(application.WebviewWindowOptions{ - Title: "linux-flatpak-appid", - Width: 700, - Height: 400, - URL: "/", - }) - - log.Printf("[pid %d] calling app.Run()", os.Getpid()) - if err := app.Run(); err != nil { - log.Fatal(err) - } -} From 28a26ce0b867c4331cc7f1250c06ba8c2a0d6110 Mon Sep 17 00:00:00 2001 From: Andraz Vrhovec Date: Fri, 14 Aug 2026 10:10:42 +0200 Subject: [PATCH 5/8] Remove reference to removed example from docs --- docs/src/content/docs/reference/application.mdx | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/src/content/docs/reference/application.mdx b/docs/src/content/docs/reference/application.mdx index aae8693d073..3be83142dbc 100644 --- a/docs/src/content/docs/reference/application.mdx +++ b/docs/src/content/docs/reference/application.mdx @@ -491,7 +491,6 @@ Portal call failed: Invalid sandbox a11y own name: ``` Setting `ApplicationID` to the `app-id` in your flatpak manifest resolves it. -See the `linux-flatpak-appid` example. ::: ## Complete Application Example From b3e890f21f9ca9b883fd1e3f9cc1c4663614a1d6 Mon Sep 17 00:00:00 2001 From: Andraz Vrhovec Date: Fri, 14 Aug 2026 10:58:52 +0200 Subject: [PATCH 6/8] feat(v3/linux): validate Options.Linux.ApplicationID GTK only asserts on the application id, so a value that g_application_id_is_valid() rejects made gtk_application_new() return NULL and took the process down during startup, far away from the option that caused it. Check the id against the same contract up front, report it through the error handler and start with the derived org.wails.. Both backends carried identical copies of sanitizeAppName and applicationID, so the id logic moves to application_linux_appid.go and is shared by the GTK3 and GTK4 builds rather than duplicating the validator as well. Deriving an id could not satisfy that contract either: sanitizeAppName prefixed a leading digit with an underscore and then trimmed it right back off, so Name "1Password" produced org.wails.1password, whose last element starts with a digit. Prefix after the trim, and clamp the derived id to the 255 character limit. --- .../content/docs/reference/application.mdx | 9 + v3/UNRELEASED_CHANGELOG.md | 3 + v3/pkg/application/application_linux.go | 35 +--- v3/pkg/application/application_linux_appid.go | 113 ++++++++++++ .../application_linux_appid_test.go | 167 ++++++++++++++++++ v3/pkg/application/application_linux_gtk3.go | 41 +---- v3/pkg/application/application_linux_test.go | 67 ------- v3/pkg/application/application_options.go | 9 + v3/pkg/application/linux_cgo_gtk3.go | 2 +- 9 files changed, 314 insertions(+), 132 deletions(-) create mode 100644 v3/pkg/application/application_linux_appid.go create mode 100644 v3/pkg/application/application_linux_appid_test.go delete mode 100644 v3/pkg/application/application_linux_test.go diff --git a/docs/src/content/docs/reference/application.mdx b/docs/src/content/docs/reference/application.mdx index 3be83142dbc..7070486a7ae 100644 --- a/docs/src/content/docs/reference/application.mdx +++ b/docs/src/content/docs/reference/application.mdx @@ -478,6 +478,15 @@ Set `ApplicationID` to match the identity your packaging declares. It should be the same reverse-DNS id you use for the `.desktop` file, so the desktop environment can associate the application's windows with its launcher. +The id has to satisfy [`g_application_id_is_valid()`](https://docs.gtk.org/gio/type_func.Application.id_is_valid.html): +two or more non-empty elements separated by a `.`, each holding only the ASCII +characters `A-Z`, `a-z`, `0-9`, `_` and `-`, none of them starting with a digit, +and at most 255 characters in total. So `com.example.MyApp` is fine, while +`MyApp`, `com.example.2ndApp` and `com.example.My App` are not. An id that GTK +would reject is reported through the application's error handler and replaced +with the derived default, because GTK only asserts on the id and would otherwise +abort the process during startup. + :::note Sandboxed builds must set this. A flatpak may only own D-Bus names prefixed with its own app id, and WebKit asks the portal to own diff --git a/v3/UNRELEASED_CHANGELOG.md b/v3/UNRELEASED_CHANGELOG.md index 33638e7fc86..f740b24e8c2 100644 --- a/v3/UNRELEASED_CHANGELOG.md +++ b/v3/UNRELEASED_CHANGELOG.md @@ -17,12 +17,15 @@ After processing, the content will be moved to the main changelog and this file ## Added +- Add `Options.Linux.ApplicationID` to override the GTK application id, which sandboxed (flatpak) builds have to set to the id their manifest declares (#5972) ## Changed ## Fixed +- Report a Linux `ApplicationID` that GTK would reject and fall back to the derived id, instead of letting `gtk_application_new()` assert and take the process down during startup (#5972) +- Fix Linux application ids derived from an application `Name` that starts with a digit, such as `1example`, producing `org.wails.1example`, which GTK rejects (#5972) ## Deprecated diff --git a/v3/pkg/application/application_linux.go b/v3/pkg/application/application_linux.go index 1284d713bbf..7f9547ab162 100644 --- a/v3/pkg/application/application_linux.go +++ b/v3/pkg/application/application_linux.go @@ -17,8 +17,6 @@ import ( "fmt" "os" "path/filepath" - "regexp" - "strings" "sync" "github.com/godbus/dbus/v5" @@ -26,32 +24,6 @@ import ( "github.com/wailsapp/wails/v3/pkg/events" ) -var invalidAppNameChars = regexp.MustCompile(`[^a-zA-Z0-9_-]`) -var leadingDigits = regexp.MustCompile(`^[0-9]+`) - -func sanitizeAppName(name string) string { - name = invalidAppNameChars.ReplaceAllString(name, "_") - name = leadingDigits.ReplaceAllString(name, "_$0") - for strings.Contains(name, "__") { - name = strings.ReplaceAll(name, "__", "_") - } - name = strings.Trim(name, "_") - if name == "" { - name = "wailsapp" - } - return strings.ToLower(name) -} - -// applicationID returns the id to build the GtkApplication with. Options.Linux -// wins when it sets one, so sandboxed builds can match the id their runtime -// expects; everything else keeps the derived "org.wails.". -func applicationID(options Options) string { - if options.Linux.ApplicationID != "" { - return options.Linux.ApplicationID - } - return "org.wails." + sanitizeAppName(options.Name) -} - func init() { // Disable DMA-BUF renderer on any session type with NVIDIA to prevent blank windows and // "Error 71 (Protocol error)" crashes. NVIDIA proprietary drivers fail gbm_bo_map() when @@ -219,9 +191,14 @@ func (a *linuxApp) unregisterWindow(window windowPointer) { } func newPlatformApp(parent *App) *linuxApp { + appID, err := applicationID(parent.options) + if err != nil { + parent.error("invalid Linux.ApplicationID: %w; falling back to %q", err, appID) + } + app := &linuxApp{ parent: parent, - application: appNew(applicationID(parent.options)), + application: appNew(appID), activated: make(chan struct{}), windowMap: map[windowPointer]uint{}, } diff --git a/v3/pkg/application/application_linux_appid.go b/v3/pkg/application/application_linux_appid.go new file mode 100644 index 00000000000..9592efdb680 --- /dev/null +++ b/v3/pkg/application/application_linux_appid.go @@ -0,0 +1,113 @@ +//go:build linux && cgo && !android && !server + +package application + +import ( + "errors" + "fmt" + "regexp" + "strings" +) + +var invalidAppNameChars = regexp.MustCompile(`[^a-zA-Z0-9_-]`) +var leadingDigits = regexp.MustCompile(`^[0-9]+`) + +// sanitizeAppName sanitizes the application name into a single element of a +// GTK/D-Bus application id: only alphanumeric characters, hyphens and +// underscores, and never a leading digit. +func sanitizeAppName(name string) string { + // Replace invalid characters with underscores + name = invalidAppNameChars.ReplaceAllString(name, "_") + // Remove consecutive underscores + for strings.Contains(name, "__") { + name = strings.ReplaceAll(name, "__", "_") + } + // Trim leading/trailing underscores + name = strings.Trim(name, "_") + if name == "" { + name = "wailsapp" + } + // Prefix with underscore if starts with digit. This has to happen after the + // trim, which would otherwise strip the prefix again and leave an element + // GTK refuses, e.g. "1Password" -> "org.wails.1password". + name = leadingDigits.ReplaceAllString(name, "_$0") + return strings.ToLower(name) +} + +// maxApplicationIDLength is the longest id GTK accepts, inherited from the +// D-Bus bus name limit. +const maxApplicationIDLength = 255 + +// validateApplicationID returns an error describing why GTK would refuse id, +// following the same contract as g_application_id_is_valid(): +// +// - the id is composed of two or more elements separated by a '.', and every +// element holds at least one character; +// - every element contains only the ASCII characters A-Z, a-z, 0-9, '_' and +// '-', and does not begin with a digit; +// - the id is at most 255 characters long. +// +// GTK only asserts on this, so an invalid id makes gtk_application_new() return +// NULL and takes the process down later, far away from the option that caused it. +// +// See: https://docs.gtk.org/gio/type_func.Application.id_is_valid.html +func validateApplicationID(id string) error { + if id == "" { + return errors.New("application id is empty") + } + if len(id) > maxApplicationIDLength { + return fmt.Errorf("application id %q is %d characters long, the maximum is %d", id, len(id), maxApplicationIDLength) + } + + elements := strings.Split(id, ".") + if len(elements) < 2 { + return fmt.Errorf("application id %q needs at least two elements separated by a '.', for example \"com.example.MyApp\"", id) + } + + for _, element := range elements { + if element == "" { + return fmt.Errorf("application id %q has an empty element: it must not start or end with a '.', or contain \"..\"", id) + } + if element[0] >= '0' && element[0] <= '9' { + return fmt.Errorf("application id %q has the element %q starting with a digit", id, element) + } + for i := 0; i < len(element); i++ { + if !isApplicationIDChar(element[i]) { + return fmt.Errorf("application id %q contains the invalid character %q: only A-Z, a-z, 0-9, '_' and '-' are allowed", id, rune(element[i])) + } + } + } + + return nil +} + +func isApplicationIDChar(c byte) bool { + return c >= 'A' && c <= 'Z' || + c >= 'a' && c <= 'z' || + c >= '0' && c <= '9' || + c == '_' || c == '-' +} + +// applicationID returns the id to build the GtkApplication with. Options.Linux +// wins when it sets one, so sandboxed builds can match the id their runtime +// expects; everything else keeps the derived "org.wails.". +// +// An id GTK would reject is reported as an error together with the derived id, +// so callers can carry on with an id that works instead of crashing inside GTK. +func applicationID(options Options) (string, error) { + derived := "org.wails." + sanitizeAppName(options.Name) + if len(derived) > maxApplicationIDLength { + // sanitizeAppName never emits a '.', so cutting the tail can only leave + // characters that are legal in the middle of an element. + derived = derived[:maxApplicationIDLength] + } + + id := options.Linux.ApplicationID + if id == "" { + return derived, nil + } + if err := validateApplicationID(id); err != nil { + return derived, err + } + return id, nil +} diff --git a/v3/pkg/application/application_linux_appid_test.go b/v3/pkg/application/application_linux_appid_test.go new file mode 100644 index 00000000000..15d1d84e678 --- /dev/null +++ b/v3/pkg/application/application_linux_appid_test.go @@ -0,0 +1,167 @@ +//go:build linux && cgo && !android && !server + +package application + +import ( + "strings" + "testing" +) + +// Both backends derive the id the same way, so this covers the GTK4 and GTK3 +// builds alike. +func TestApplicationID(t *testing.T) { + tests := []struct { + name string + options Options + want string + }{ + { + name: "derived from Name when unset", + options: Options{Name: "My App"}, + want: "org.wails.my_app", + }, + { + name: "hyphens are kept when derived", + options: Options{Name: "koofr-stage"}, + want: "org.wails.koofr-stage", + }, + { + name: "empty Name falls back to wailsapp", + options: Options{}, + want: "org.wails.wailsapp", + }, + { + name: "used verbatim when set", + options: Options{ + Name: "My App", + Linux: LinuxOptions{ApplicationID: "com.myapp.myapplication"}, + }, + want: "com.myapp.myapplication", + }, + { + // Sandboxed builds depend on this: the id has to be exactly the one + // the packaging declares, with nothing derived from Name mixed in. + name: "set id wins over Name entirely", + options: Options{ + Name: "something else", + Linux: LinuxOptions{ApplicationID: "com.example.WailsFlatpakAppId"}, + }, + want: "com.example.WailsFlatpakAppId", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := applicationID(tt.options) + if err != nil { + t.Fatalf("applicationID() returned an unexpected error: %v", err) + } + if got != tt.want { + t.Errorf("applicationID() = %q, want %q", got, tt.want) + } + }) + } +} + +// The option is opt-in, so an application that does not set it has to keep the +// id it had before the option existed. +func TestApplicationIDUnsetIsBackwardCompatible(t *testing.T) { + options := Options{Name: "My App"} + + got, err := applicationID(options) + if err != nil { + t.Fatalf("applicationID() returned an unexpected error: %v", err) + } + if want := "org.wails." + sanitizeAppName(options.Name); got != want { + t.Errorf("applicationID() = %q, want the derived id %q", got, want) + } +} + +// An id GTK would reject has to be reported rather than handed to +// gtk_application_new(), and the application still needs an id to start with. +func TestApplicationIDInvalidFallsBackToDerived(t *testing.T) { + options := Options{ + Name: "My App", + Linux: LinuxOptions{ApplicationID: "yeehaw"}, + } + + got, err := applicationID(options) + if err == nil { + t.Fatal("applicationID() accepted an id without a '.' separator") + } + if want := "org.wails.my_app"; got != want { + t.Errorf("applicationID() = %q, want the derived id %q", got, want) + } +} + +// The derived id is never routed through validateApplicationID at runtime, so +// make sure sanitizeAppName cannot produce one GTK would refuse. +func TestDerivedApplicationIDIsAlwaysValid(t *testing.T) { + names := []string{ + "My App", + "koofr-stage", + "", + "1Password", + "...", + "app.with.dots", + "ünïcodé", + "__leading__and__trailing__", + "9", + "-", + strings.Repeat("long", 200), + } + + for _, name := range names { + t.Run(name, func(t *testing.T) { + id, err := applicationID(Options{Name: name}) + if err != nil { + t.Fatalf("applicationID() returned an unexpected error: %v", err) + } + if err := validateApplicationID(id); err != nil { + t.Errorf("derived id %q is not a valid GTK application id: %v", id, err) + } + }) + } +} + +// Mirrors the contract of g_application_id_is_valid(). +// See: https://docs.gtk.org/gio/type_func.Application.id_is_valid.html +func TestValidateApplicationID(t *testing.T) { + tests := []struct { + name string + id string + valid bool + }{ + {name: "reverse dns", id: "com.example.MyApp", valid: true}, + {name: "two elements", id: "com.example", valid: true}, + {name: "underscores", id: "com.example.my_app", valid: true}, + {name: "hyphens are discouraged but legal", id: "org.wails.koofr-stage", valid: true}, + {name: "digits inside an element", id: "com.example.App2", valid: true}, + {name: "255 characters", id: "com." + strings.Repeat("a", 251), valid: true}, + + {name: "empty", id: "", valid: false}, + {name: "single element", id: "yeehaw", valid: false}, + {name: "leading dot", id: ".com.example", valid: false}, + {name: "trailing dot", id: "com.example.", valid: false}, + {name: "consecutive dots", id: "com..example", valid: false}, + {name: "element starting with a digit", id: "com.example.2ndApp", valid: false}, + {name: "first element starting with a digit", id: "2com.example", valid: false}, + {name: "slash", id: "com.example/MyApp", valid: false}, + {name: "space", id: "com.example.My App", valid: false}, + {name: "colon prefixed unique name", id: ":1.42", valid: false}, + {name: "non ascii", id: "com.example.Mü", valid: false}, + {name: "256 characters", id: "com." + strings.Repeat("a", 252), valid: false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateApplicationID(tt.id) + if tt.valid && err != nil { + t.Errorf("validateApplicationID(%q) = %v, want no error", tt.id, err) + } + if !tt.valid && err == nil { + t.Errorf("validateApplicationID(%q) = nil, want an error", tt.id) + } + }) + } +} diff --git a/v3/pkg/application/application_linux_gtk3.go b/v3/pkg/application/application_linux_gtk3.go index 1a292f1b225..06977da50a1 100644 --- a/v3/pkg/application/application_linux_gtk3.go +++ b/v3/pkg/application/application_linux_gtk3.go @@ -16,7 +16,6 @@ import "C" import ( "fmt" "os" - "regexp" "slices" "strings" "sync" @@ -27,39 +26,6 @@ import ( "github.com/wailsapp/wails/v3/pkg/events" ) -// sanitizeAppName sanitizes the application name to be a valid GTK/D-Bus application ID. -// Valid IDs contain only alphanumeric characters, hyphens, and underscores. -// They must not start with a digit. -var invalidAppNameChars = regexp.MustCompile(`[^a-zA-Z0-9_-]`) -var leadingDigits = regexp.MustCompile(`^[0-9]+`) - -func sanitizeAppName(name string) string { - // Replace invalid characters with underscores - name = invalidAppNameChars.ReplaceAllString(name, "_") - // Prefix with underscore if starts with digit - name = leadingDigits.ReplaceAllString(name, "_$0") - // Remove consecutive underscores - for strings.Contains(name, "__") { - name = strings.ReplaceAll(name, "__", "_") - } - // Trim leading/trailing underscores - name = strings.Trim(name, "_") - if name == "" { - name = "wailsapp" - } - return strings.ToLower(name) -} - -// applicationID returns the id to build the GtkApplication with. Options.Linux -// wins when it sets one, so sandboxed builds can match the id their runtime -// expects; everything else keeps the derived "org.wails.". -func applicationID(options Options) string { - if options.Linux.ApplicationID != "" { - return options.Linux.ApplicationID - } - return "org.wails." + sanitizeAppName(options.Name) -} - func init() { // FIXME: This should be handled appropriately in the individual files most likely. // Set GDK_BACKEND=x11 if currently unset and XDG_SESSION_TYPE is unset, unspecified or x11 to prevent warnings @@ -237,9 +203,14 @@ func (a *linuxApp) getAccentColor() string { } func newPlatformApp(parent *App) *linuxApp { + appID, err := applicationID(parent.options) + if err != nil { + parent.error("invalid Linux.ApplicationID: %w; falling back to %q", err, appID) + } + app := &linuxApp{ parent: parent, - application: appNew(applicationID(parent.options)), + application: appNew(appID), windowMap: map[windowPointer]uint{}, } diff --git a/v3/pkg/application/application_linux_test.go b/v3/pkg/application/application_linux_test.go deleted file mode 100644 index 79da0d09f0a..00000000000 --- a/v3/pkg/application/application_linux_test.go +++ /dev/null @@ -1,67 +0,0 @@ -//go:build linux && cgo && !android && !server - -package application - -import "testing" - -// Both backends derive the id the same way, so this covers the GTK4 and GTK3 -// builds alike. -func TestApplicationID(t *testing.T) { - tests := []struct { - name string - options Options - want string - }{ - { - name: "derived from Name when unset", - options: Options{Name: "My App"}, - want: "org.wails.my_app", - }, - { - name: "hyphens are kept when derived", - options: Options{Name: "koofr-stage"}, - want: "org.wails.koofr-stage", - }, - { - name: "empty Name falls back to wailsapp", - options: Options{}, - want: "org.wails.wailsapp", - }, - { - name: "used verbatim when set", - options: Options{ - Name: "My App", - Linux: LinuxOptions{ApplicationID: "com.myapp.myapplication"}, - }, - want: "com.myapp.myapplication", - }, - { - // Sandboxed builds depend on this: the id has to be exactly the one - // the packaging declares, with nothing derived from Name mixed in. - name: "set id wins over Name entirely", - options: Options{ - Name: "something else", - Linux: LinuxOptions{ApplicationID: "com.example.WailsFlatpakAppId"}, - }, - want: "com.example.WailsFlatpakAppId", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := applicationID(tt.options); got != tt.want { - t.Errorf("applicationID() = %q, want %q", got, tt.want) - } - }) - } -} - -// The option is opt-in, so an application that does not set it has to keep the -// id it had before the option existed. -func TestApplicationIDUnsetIsBackwardCompatible(t *testing.T) { - options := Options{Name: "My App"} - - if got, want := applicationID(options), "org.wails."+sanitizeAppName(options.Name); got != want { - t.Errorf("applicationID() = %q, want the derived id %q", got, want) - } -} diff --git a/v3/pkg/application/application_options.go b/v3/pkg/application/application_options.go index a01136d89ee..08d13c83de6 100644 --- a/v3/pkg/application/application_options.go +++ b/v3/pkg/application/application_options.go @@ -333,11 +333,20 @@ type LinuxOptions struct { // ApplicationID overrides the GTK application id, which otherwise defaults // to "org.wails." followed by a sanitised Name. // + // The id has to satisfy g_application_id_is_valid(): two or more non-empty + // elements separated by a '.', each holding only the ASCII characters A-Z, + // a-z, 0-9, '_' and '-', none of them starting with a digit, and at most + // 255 characters in total, e.g. "com.example.MyApp". An id that does not is + // reported through the error handler and replaced with the derived default, + // because GTK only asserts on it and would abort the process instead. + // // Sandboxed builds have to set this. A flatpak may only own bus names // prefixed with its app id, and WebKit asks the portal to own // ".Sandboxed.WebProcess-" for the accessibility bus. // With the default id that request is refused and the web process aborts, // taking the application down from inside g_application_run. + // + // See: https://docs.gtk.org/gio/type_func.Application.id_is_valid.html ApplicationID string } diff --git a/v3/pkg/application/linux_cgo_gtk3.go b/v3/pkg/application/linux_cgo_gtk3.go index 78fcc2ac738..55dbb1a5cc4 100644 --- a/v3/pkg/application/linux_cgo_gtk3.go +++ b/v3/pkg/application/linux_cgo_gtk3.go @@ -619,7 +619,7 @@ func appName() string { } func appNew(appId string) pointer { - // Already assembled and sanitized by applicationID() in application_linux_gtk3.go + // Already assembled and validated by applicationID() in application_linux_appid.go nameC := C.CString(appId) defer C.free(unsafe.Pointer(nameC)) return pointer(C.gtk_application_new(nameC, C.APPLICATION_DEFAULT_FLAGS)) From f708d8f94595d75917b035293ba89d65a475837c Mon Sep 17 00:00:00 2001 From: Andraz Vrhovec Date: Fri, 14 Aug 2026 11:13:09 +0200 Subject: [PATCH 7/8] feat(v3/linux): default ProgramName to ApplicationID when unset GTK takes the Wayland surface app_id from g_get_prgname(), not from the application id, so setting only ApplicationID left windows matching the executable name instead of the .desktop file. ProgramName now inherits the resolved application id. Setting it explicitly still wins, and applications that set neither option keep the executable name. --- .../content/docs/reference/application.mdx | 16 ++++- v3/UNRELEASED_CHANGELOG.md | 1 + v3/pkg/application/application_linux.go | 4 +- v3/pkg/application/application_linux_appid.go | 23 +++++++ .../application_linux_appid_test.go | 61 +++++++++++++++++++ v3/pkg/application/application_linux_gtk3.go | 4 +- v3/pkg/application/application_options.go | 5 ++ 7 files changed, 109 insertions(+), 5 deletions(-) diff --git a/docs/src/content/docs/reference/application.mdx b/docs/src/content/docs/reference/application.mdx index 7070486a7ae..8b467af108b 100644 --- a/docs/src/content/docs/reference/application.mdx +++ b/docs/src/content/docs/reference/application.mdx @@ -471,7 +471,7 @@ app := application.New(application.Options{ ``` - `ApplicationID` - The GTK application id. Defaults to `org.wails.` followed by a sanitised `Name`. -- `ProgramName` - Sets the program name for the window manager via `g_set_prgname()`. +- `ProgramName` - Sets the program name for the window manager via `g_set_prgname()`. Defaults to `ApplicationID` when that is set. - `DisableQuitOnLastWindowClosed` - Keeps the application running after its last window closes. Set `ApplicationID` to match the identity your packaging declares. It should be @@ -487,6 +487,20 @@ would reject is reported through the application's error handler and replaced with the derived default, because GTK only asserts on the id and would otherwise abort the process during startup. +On Wayland, GTK takes the surface `app_id` from the program name rather than the +application id, so `ProgramName` defaults to `ApplicationID` when you set one. +Setting the same string twice is not needed, and setting `ProgramName` +explicitly still wins: + +```go +app := application.New(application.Options{ + Name: "My App", + Linux: application.LinuxOptions{ + ApplicationID: "com.example.MyApp", // also becomes the program name + }, +}) +``` + :::note Sandboxed builds must set this. A flatpak may only own D-Bus names prefixed with its own app id, and WebKit asks the portal to own diff --git a/v3/UNRELEASED_CHANGELOG.md b/v3/UNRELEASED_CHANGELOG.md index f740b24e8c2..2f87c83f8e2 100644 --- a/v3/UNRELEASED_CHANGELOG.md +++ b/v3/UNRELEASED_CHANGELOG.md @@ -21,6 +21,7 @@ After processing, the content will be moved to the main changelog and this file ## Changed +- Default `Options.Linux.ProgramName` to `ApplicationID` when only the latter is set, so windows keep matching their `.desktop` file on Wayland, where GTK takes the surface `app_id` from the program name (#5972) ## Fixed diff --git a/v3/pkg/application/application_linux.go b/v3/pkg/application/application_linux.go index 7f9547ab162..7e213bbc461 100644 --- a/v3/pkg/application/application_linux.go +++ b/v3/pkg/application/application_linux.go @@ -203,8 +203,8 @@ func newPlatformApp(parent *App) *linuxApp { windowMap: map[windowPointer]uint{}, } - if parent.options.Linux.ProgramName != "" { - setProgramName(parent.options.Linux.ProgramName) + if name := programName(parent.options, appID); name != "" { + setProgramName(name) } return app diff --git a/v3/pkg/application/application_linux_appid.go b/v3/pkg/application/application_linux_appid.go index 9592efdb680..4945a90777b 100644 --- a/v3/pkg/application/application_linux_appid.go +++ b/v3/pkg/application/application_linux_appid.go @@ -111,3 +111,26 @@ func applicationID(options Options) (string, error) { } return id, nil } + +// programName returns the name to hand to g_set_prgname, or "" to leave the +// program name at whatever GTK picked up from the executable. +// +// GTK takes the Wayland surface app_id from g_get_prgname(), so a window is only +// matched with its .desktop file when the program name carries the application +// id as well. An application that sets Options.Linux.ApplicationID inherits it +// here rather than having to repeat the same string in ProgramName. +// +// What is inherited is appID, the id the GtkApplication was built with, so the +// program name cannot disagree with it: an ApplicationID that validation +// rejected falls back to the derived id in both places. Without an +// ApplicationID nothing is derived, keeping the program name of applications +// that set neither option as it was. +func programName(options Options, appID string) string { + if options.Linux.ProgramName != "" { + return options.Linux.ProgramName + } + if options.Linux.ApplicationID != "" { + return appID + } + return "" +} diff --git a/v3/pkg/application/application_linux_appid_test.go b/v3/pkg/application/application_linux_appid_test.go index 15d1d84e678..72cd977e91e 100644 --- a/v3/pkg/application/application_linux_appid_test.go +++ b/v3/pkg/application/application_linux_appid_test.go @@ -124,6 +124,67 @@ func TestDerivedApplicationIDIsAlwaysValid(t *testing.T) { } } +// On Wayland the surface app_id comes from g_get_prgname(), so setting only +// ApplicationID has to be enough to have windows match their .desktop file. +func TestProgramName(t *testing.T) { + tests := []struct { + name string + options Options + want string + }{ + { + name: "left alone when neither option is set", + options: Options{Name: "My App"}, + want: "", + }, + { + name: "inherits the application id", + options: Options{ + Name: "My App", + Linux: LinuxOptions{ApplicationID: "com.example.MyApp"}, + }, + want: "com.example.MyApp", + }, + { + name: "an explicit program name wins", + options: Options{ + Name: "My App", + Linux: LinuxOptions{ + ApplicationID: "com.example.MyApp", + ProgramName: "myapp", + }, + }, + want: "myapp", + }, + { + name: "kept without an application id", + options: Options{ + Name: "My App", + Linux: LinuxOptions{ProgramName: "myapp"}, + }, + want: "myapp", + }, + { + // The id GTK was given, not the one it would have rejected. + name: "inherits the fallback when the id is invalid", + options: Options{ + Name: "My App", + Linux: LinuxOptions{ApplicationID: "yeehaw"}, + }, + want: "org.wails.my_app", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + appID, _ := applicationID(tt.options) + if got := programName(tt.options, appID); got != tt.want { + t.Errorf("programName() = %q, want %q", got, tt.want) + } + }) + } +} + // Mirrors the contract of g_application_id_is_valid(). // See: https://docs.gtk.org/gio/type_func.Application.id_is_valid.html func TestValidateApplicationID(t *testing.T) { diff --git a/v3/pkg/application/application_linux_gtk3.go b/v3/pkg/application/application_linux_gtk3.go index 06977da50a1..02ca5dceb24 100644 --- a/v3/pkg/application/application_linux_gtk3.go +++ b/v3/pkg/application/application_linux_gtk3.go @@ -214,8 +214,8 @@ func newPlatformApp(parent *App) *linuxApp { windowMap: map[windowPointer]uint{}, } - if parent.options.Linux.ProgramName != "" { - setProgramName(parent.options.Linux.ProgramName) + if name := programName(parent.options, appID); name != "" { + setProgramName(name) } return app diff --git a/v3/pkg/application/application_options.go b/v3/pkg/application/application_options.go index 08d13c83de6..e1cd5810867 100644 --- a/v3/pkg/application/application_options.go +++ b/v3/pkg/application/application_options.go @@ -327,6 +327,11 @@ type LinuxOptions struct { //When a .desktop file is created this value helps with window grouping and desktop icons when the .desktop file's Name //property differs form the executable's filename. // + //Defaults to ApplicationID when that is set, because GTK takes the Wayland + //surface app_id from the program name: leaving this empty there would have + //windows fall back to the executable's name and stop matching the .desktop + //file. Applications that set neither option keep the executable's name. + // //[see the docs]: https://docs.gtk.org/glib/func.set_prgname.html ProgramName string From a3e751bb9502743881fab511e3d478e1de80235e Mon Sep 17 00:00:00 2001 From: taliesin-ai Date: Sun, 16 Aug 2026 17:21:39 +1000 Subject: [PATCH 8/8] docs: label implementation tracker file tree --- IMPLEMENTATION.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IMPLEMENTATION.md b/IMPLEMENTATION.md index d518e4a13aa..9b376f10dae 100644 --- a/IMPLEMENTATION.md +++ b/IMPLEMENTATION.md @@ -482,7 +482,7 @@ v3/internal/operatingsystem/ > **Historical note:** The Phase tracker blocks earlier in this document (Phases 1–4) reference the pre-flip filenames (`*_linux_gtk4.go`, `webkit6.go`, etc.) as a record of work done at the time. Those references are historical and have not been retconned; new work should reference the post-flip layout above. ### Shared Linux Files -``` +```text v3/pkg/application/ application_linux_appid.go # Shared GTK application-ID resolution and validation application_linux_appid_test.go # Shared GTK3/GTK4 identity parity tests