From 208639b5c7cd60504b4241420de0bc06b1e63974 Mon Sep 17 00:00:00 2001 From: Lea Anthony Date: Sat, 7 Feb 2026 13:55:31 +1100 Subject: [PATCH 1/4] fix(ci): add libwayland-dev to Linux CI dependencies Ubuntu 24.04 no longer transitively installs libwayland-server.so.0 via libwebkit2gtk-4.1-dev, causing all Linux template tests to fail with "cannot open shared object file". Add libwayland-dev explicitly to all workflow files. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/build-and-test-v3.yml | 4 ++-- .github/workflows/build-and-test.yml | 4 ++-- .github/workflows/build-cross-image.yml | 2 +- .github/workflows/cross-compile-test-v3.yml | 2 +- .github/workflows/pr-master.yml | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build-and-test-v3.yml b/.github/workflows/build-and-test-v3.yml index e41488bef..cc2c6ded0 100644 --- a/.github/workflows/build-and-test-v3.yml +++ b/.github/workflows/build-and-test-v3.yml @@ -115,7 +115,7 @@ jobs: uses: awalsh128/cache-apt-pkgs-action@latest if: matrix.os == 'ubuntu-latest' with: - packages: libgtk-3-dev libwebkit2gtk-4.1-dev build-essential pkg-config xvfb x11-xserver-utils at-spi2-core xdg-desktop-portal-gtk + packages: libgtk-3-dev libwebkit2gtk-4.1-dev libwayland-dev build-essential pkg-config xvfb x11-xserver-utils at-spi2-core xdg-desktop-portal-gtk version: 1.0 - name: Install linux dependencies (GTK4) - webkit-gtk6-support branch only @@ -240,7 +240,7 @@ jobs: uses: awalsh128/cache-apt-pkgs-action@latest if: matrix.os == 'ubuntu-latest' with: - packages: libgtk-3-dev libwebkit2gtk-4.1-dev build-essential pkg-config + packages: libgtk-3-dev libwebkit2gtk-4.1-dev libwayland-dev build-essential pkg-config version: 1.0 - name: Install linux dependencies (GTK4) - webkit-gtk6-support branch only diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index cd9cfa42a..a6ee1a0bb 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -28,7 +28,7 @@ jobs: - uses: awalsh128/cache-apt-pkgs-action@latest if: matrix.os == 'ubuntu-24.04' with: - packages: libgtk-3-dev libwebkit2gtk-4.1-dev build-essential pkg-config + packages: libgtk-3-dev libwebkit2gtk-4.1-dev libwayland-dev build-essential pkg-config version: 1.0 - name: Setup Go @@ -126,7 +126,7 @@ jobs: - name: Install linux dependencies ( 24.04 ) if: matrix.os == 'ubuntu-24.04' - run: sudo apt-get update -y && sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev build-essential pkg-config + run: sudo apt-get update -y && sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libwayland-dev build-essential pkg-config - name: Generate & Build template '${{ matrix.template }}' if: matrix.os != 'ubuntu-24.04' diff --git a/.github/workflows/build-cross-image.yml b/.github/workflows/build-cross-image.yml index 004831139..1af95a59b 100644 --- a/.github/workflows/build-cross-image.yml +++ b/.github/workflows/build-cross-image.yml @@ -153,7 +153,7 @@ jobs: - name: Install Linux dev dependencies run: | sudo apt-get update - sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev + sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libwayland-dev - name: Install wails3 CLI run: | diff --git a/.github/workflows/cross-compile-test-v3.yml b/.github/workflows/cross-compile-test-v3.yml index 092b4e4c7..2d7c3dbcb 100644 --- a/.github/workflows/cross-compile-test-v3.yml +++ b/.github/workflows/cross-compile-test-v3.yml @@ -83,7 +83,7 @@ jobs: - name: Install Linux dependencies uses: awalsh128/cache-apt-pkgs-action@latest with: - packages: libgtk-3-dev libwebkit2gtk-4.1-dev build-essential pkg-config + packages: libgtk-3-dev libwebkit2gtk-4.1-dev libwayland-dev build-essential pkg-config version: 1.0 - name: Install Wails3 CLI diff --git a/.github/workflows/pr-master.yml b/.github/workflows/pr-master.yml index 1f9fe5622..1de533199 100644 --- a/.github/workflows/pr-master.yml +++ b/.github/workflows/pr-master.yml @@ -52,7 +52,7 @@ jobs: - name: Install linux dependencies (24.04) if: matrix.os == 'ubuntu-24.04' - run: sudo apt-get update -y && sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev build-essential pkg-config + run: sudo apt-get update -y && sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libwayland-dev build-essential pkg-config - name: Setup Go uses: actions/setup-go@v3 From 18a9d79446be31a0b7a7b34fa870d4364c40839c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 7 Feb 2026 03:01:28 +0000 Subject: [PATCH 2/4] chore(v3): bump to v3.0.0-alpha.68 and update changelog [skip ci] --- docs/src/content/docs/changelog.mdx | 22 ++++++++++++++++++++++ v3/UNRELEASED_CHANGELOG.md | 14 -------------- v3/internal/version/version.txt | 2 +- 3 files changed, 23 insertions(+), 15 deletions(-) diff --git a/docs/src/content/docs/changelog.mdx b/docs/src/content/docs/changelog.mdx index 457b19492..a44e73055 100644 --- a/docs/src/content/docs/changelog.mdx +++ b/docs/src/content/docs/changelog.mdx @@ -30,6 +30,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 */ ## [Unreleased] +## v3.0.0-alpha.68 - 2026-02-07 + +## Added +- Add Web API examples in `v3/examples/web-apis/` demonstrating 41 browser APIs including Storage (localStorage, sessionStorage, IndexedDB, Cache API), Network (Fetch, WebSocket, XMLHttpRequest, EventSource, Beacon), Media (Canvas, WebGL, Web Audio, MediaDevices, MediaRecorder, Speech Synthesis), Device (Geolocation, Clipboard, Fullscreen, Device Orientation, Vibration, Gamepad), Performance (Performance API, Mutation Observer, Intersection/Resize Observer), UI (Web Components, Pointer Events, Selection, Dialog, Drag and Drop), and more +- Add WebView API compatibility checker example (`v3/examples/webview-api-check/`) that tests 200+ browser APIs across platforms +- Add `internal/libpath` package for finding native library paths on Linux with parallel search, caching, and support for Flatpak/Snap/Nix +- **WIP:** Add experimental WebKitGTK 6.0 / GTK4 support for Linux, available via `-tags gtk4` (GTK3/WebKit2GTK 4.1 remains the default) + - Note: On tiling window managers (e.g., Hyprland, Sway), Minimize/Maximize operations may not work as expected since the WM controls window geometry + +## Changed +- **BREAKING**: Map keys in generated JS/TS bindings are now marked optional to accurately reflect Go map semantics. Map value access in Typescript now returns `T | undefined` instead of `T`, requiring null checks or assertions (#4943) by `@fbbdev` + +## Fixed +- Fix file drag-and-drop on Windows not working at non-100% display scaling +- Fix HTML5 internal drag-and-drop being broken when file drop was enabled on Windows +- Fix file drop coordinates being in wrong pixel space on Windows (physical vs CSS pixels) +- Fix file drag-and-drop on Linux not working reliably with hover effects +- Fix HTML5 internal drag-and-drop being broken when file drop was enabled on Linux +- Fix DPI scaling on Linux/GTK4 by implementing proper PhysicalBounds calculation and fractional scaling support via `gdk_monitor_get_scale` (GTK 4.14+) +- Fix menu items duplicating when creating new windows on Linux/GTK4 +- Fix generation of mapped types with enum keys in JS/TS bindings (#4437) by @fbbdev + ## v3.0.0-alpha.67 - 2026-02-04 ## Added diff --git a/v3/UNRELEASED_CHANGELOG.md b/v3/UNRELEASED_CHANGELOG.md index 7fceefca9..8e4648038 100644 --- a/v3/UNRELEASED_CHANGELOG.md +++ b/v3/UNRELEASED_CHANGELOG.md @@ -17,26 +17,12 @@ After processing, the content will be moved to the main changelog and this file ## Added -- Add Web API examples in `v3/examples/web-apis/` demonstrating 41 browser APIs including Storage (localStorage, sessionStorage, IndexedDB, Cache API), Network (Fetch, WebSocket, XMLHttpRequest, EventSource, Beacon), Media (Canvas, WebGL, Web Audio, MediaDevices, MediaRecorder, Speech Synthesis), Device (Geolocation, Clipboard, Fullscreen, Device Orientation, Vibration, Gamepad), Performance (Performance API, Mutation Observer, Intersection/Resize Observer), UI (Web Components, Pointer Events, Selection, Dialog, Drag and Drop), and more -- Add WebView API compatibility checker example (`v3/examples/webview-api-check/`) that tests 200+ browser APIs across platforms -- Add `internal/libpath` package for finding native library paths on Linux with parallel search, caching, and support for Flatpak/Snap/Nix -- **WIP:** Add experimental WebKitGTK 6.0 / GTK4 support for Linux, available via `-tags gtk4` (GTK3/WebKit2GTK 4.1 remains the default) - - Note: On tiling window managers (e.g., Hyprland, Sway), Minimize/Maximize operations may not work as expected since the WM controls window geometry ## Changed -- **BREAKING**: Map keys in generated JS/TS bindings are now marked optional to accurately reflect Go map semantics. Map value access in Typescript now returns `T | undefined` instead of `T`, requiring null checks or assertions (#4943) by `@fbbdev` ## Fixed -- Fix file drag-and-drop on Windows not working at non-100% display scaling -- Fix HTML5 internal drag-and-drop being broken when file drop was enabled on Windows -- Fix file drop coordinates being in wrong pixel space on Windows (physical vs CSS pixels) -- Fix file drag-and-drop on Linux not working reliably with hover effects -- Fix HTML5 internal drag-and-drop being broken when file drop was enabled on Linux -- Fix DPI scaling on Linux/GTK4 by implementing proper PhysicalBounds calculation and fractional scaling support via `gdk_monitor_get_scale` (GTK 4.14+) -- Fix menu items duplicating when creating new windows on Linux/GTK4 -- Fix generation of mapped types with enum keys in JS/TS bindings (#4437) by @fbbdev ## Deprecated diff --git a/v3/internal/version/version.txt b/v3/internal/version/version.txt index a3cbab1c2..776e58d05 100644 --- a/v3/internal/version/version.txt +++ b/v3/internal/version/version.txt @@ -1 +1 @@ -v3.0.0-alpha.67 \ No newline at end of file +v3.0.0-alpha.68 \ No newline at end of file From 716507b488556b70d68ad4fdd954d774c89b5e2a Mon Sep 17 00:00:00 2001 From: Lea Anthony Date: Sat, 7 Feb 2026 16:27:20 +1100 Subject: [PATCH 3/4] fix(linux/gtk4): window position, max size, and show/hide on X11 (#4965) * fix(linux/gtk4): window position, max size, and show/hide on X11 (#4957) - Use gtk_window_present() in windowShow() to properly restore windows after hide instead of gtk_widget_set_visible() which could leave windows minimized - Add X11-conditional window position get/set using XTranslateCoordinates and XMoveWindow, gated behind GDK_IS_X11_DISPLAY runtime checks and GDK_WINDOWING_X11 compile-time guards. Wayland continues to return 0,0 gracefully since position is not available there. - Implement max window size enforcement via notify::default-width/height signal handlers that clamp dimensions, replacing the removed gtk_window_set_geometry_hints from GTK3 Co-Authored-By: Claude Opus 4.6 * fix: treat skipped cross-compile job as success in CI The cross_compile_results job treats "skipped" as a failure, but the cross_compile job is legitimately skipped when the PR hasn't been approved yet. Accept "skipped" alongside "success". Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 --- v3/UNRELEASED_CHANGELOG.md | 11 +++ v3/pkg/application/linux_cgo_gtk4.c | 121 +++++++++++++++++++++++++++ v3/pkg/application/linux_cgo_gtk4.go | 26 ++++-- v3/pkg/application/linux_cgo_gtk4.h | 13 +++ 4 files changed, 165 insertions(+), 6 deletions(-) diff --git a/v3/UNRELEASED_CHANGELOG.md b/v3/UNRELEASED_CHANGELOG.md index 8e4648038..54e8a7df5 100644 --- a/v3/UNRELEASED_CHANGELOG.md +++ b/v3/UNRELEASED_CHANGELOG.md @@ -23,6 +23,17 @@ After processing, the content will be moved to the main changelog and this file ## Fixed +- Fix file drag-and-drop on Windows not working at non-100% display scaling +- Fix HTML5 internal drag-and-drop being broken when file drop was enabled on Windows +- Fix file drop coordinates being in wrong pixel space on Windows (physical vs CSS pixels) +- Fix file drag-and-drop on Linux not working reliably with hover effects +- Fix HTML5 internal drag-and-drop being broken when file drop was enabled on Linux +- Fix window show/hide on Linux/GTK4 sometimes restoring to minimized state by using `gtk_window_present()` (#4957) +- Fix window position get/set on Linux/GTK4 always returning 0,0 by adding X11-conditional support via `XTranslateCoordinates`/`XMoveWindow` (#4957) +- Fix max window size not being enforced on Linux/GTK4 by adding signal-based size clamping to replace removed `gtk_window_set_geometry_hints` (#4957) +- Fix DPI scaling on Linux/GTK4 by implementing proper PhysicalBounds calculation and fractional scaling support via `gdk_monitor_get_scale` (GTK 4.14+) +- Fix menu items duplicating when creating new windows on Linux/GTK4 +- Fix generation of mapped types with enum keys in JS/TS bindings (#4437) by @fbbdev ## Deprecated diff --git a/v3/pkg/application/linux_cgo_gtk4.c b/v3/pkg/application/linux_cgo_gtk4.c index e6f6ce499..92f6ef5aa 100644 --- a/v3/pkg/application/linux_cgo_gtk4.c +++ b/v3/pkg/application/linux_cgo_gtk4.c @@ -2,6 +2,31 @@ #include "linux_cgo_gtk4.h" +#ifdef GDK_WINDOWING_X11 +#include +#include + +// Function pointer types for Xlib functions loaded at runtime via dlsym. +// This avoids a direct link dependency on libX11 - the symbols are resolved +// from GTK4's already-loaded X11 backend. +typedef int (*WailsXMoveWindowFunc)(Display*, Window, int, int); +typedef int (*WailsXFlushFunc)(Display*); +typedef Bool (*WailsXTranslateCoordinatesFunc)(Display*, Window, Window, int, int, int*, int*, Window*); + +static WailsXMoveWindowFunc wails_XMoveWindow = NULL; +static WailsXFlushFunc wails_XFlush = NULL; +static WailsXTranslateCoordinatesFunc wails_XTranslateCoordinates = NULL; +static gboolean x11_funcs_resolved = FALSE; + +static void resolve_x11_funcs(void) { + if (x11_funcs_resolved) return; + x11_funcs_resolved = TRUE; + wails_XMoveWindow = (WailsXMoveWindowFunc)dlsym(RTLD_DEFAULT, "XMoveWindow"); + wails_XFlush = (WailsXFlushFunc)dlsym(RTLD_DEFAULT, "XFlush"); + wails_XTranslateCoordinates = (WailsXTranslateCoordinatesFunc)dlsym(RTLD_DEFAULT, "XTranslateCoordinates"); +} +#endif + #ifdef WAILS_GTK_DEBUG #define DEBUG_LOG(fmt, ...) fprintf(stderr, "[GTK4] " fmt "\n", ##__VA_ARGS__) #else @@ -867,6 +892,102 @@ void clipboard_free_text(char *text) { } } +// ============================================================================ +// Window position (X11 only) +// ============================================================================ + +void window_move_x11(GtkWindow *window, int x, int y) { +#ifdef GDK_WINDOWING_X11 + GtkNative *native = gtk_widget_get_native(GTK_WIDGET(window)); + if (native == NULL) return; + + GdkSurface *surface = gtk_native_get_surface(native); + if (surface == NULL) return; + + GdkDisplay *display = gdk_surface_get_display(surface); + if (!GDK_IS_X11_DISPLAY(display)) return; + + resolve_x11_funcs(); + if (wails_XMoveWindow == NULL) return; + + Display *xdisplay = gdk_x11_display_get_xdisplay(display); + Window xwindow = gdk_x11_surface_get_xid(GDK_X11_SURFACE(surface)); + wails_XMoveWindow(xdisplay, xwindow, x, y); + if (wails_XFlush != NULL) wails_XFlush(xdisplay); +#endif +} + +void window_get_position_x11(GtkWindow *window, int *x, int *y) { + *x = 0; + *y = 0; +#ifdef GDK_WINDOWING_X11 + GtkNative *native = gtk_widget_get_native(GTK_WIDGET(window)); + if (native == NULL) return; + + GdkSurface *surface = gtk_native_get_surface(native); + if (surface == NULL) return; + + GdkDisplay *display = gdk_surface_get_display(surface); + if (!GDK_IS_X11_DISPLAY(display)) return; + + resolve_x11_funcs(); + if (wails_XTranslateCoordinates == NULL) return; + + Display *xdisplay = gdk_x11_display_get_xdisplay(display); + Window xwindow = gdk_x11_surface_get_xid(GDK_X11_SURFACE(surface)); + + Window child; + Window root = DefaultRootWindow(xdisplay); + int abs_x, abs_y; + if (wails_XTranslateCoordinates(xdisplay, xwindow, root, 0, 0, &abs_x, &abs_y, &child)) { + *x = abs_x; + *y = abs_y; + } +#endif +} + +// ============================================================================ +// Window size constraints (max size enforcement for GTK4) +// ============================================================================ + +static void on_window_size_changed(GObject *object, GParamSpec *pspec, gpointer data) { + GtkWindow *window = GTK_WINDOW(object); + + // Don't clamp during fullscreen or maximize - these should bypass max size + // constraints, matching V2 behaviour where geometry hints are suspended. + if (gtk_window_is_fullscreen(window) || gtk_window_is_maximized(window)) return; + + int maxW = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(window), "wails-max-width")); + int maxH = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(window), "wails-max-height")); + + if (maxW <= 0 && maxH <= 0) return; + + int w = gtk_widget_get_width(GTK_WIDGET(window)); + int h = gtk_widget_get_height(GTK_WIDGET(window)); + + gboolean needs_clamp = FALSE; + if (maxW > 0 && w > maxW) { w = maxW; needs_clamp = TRUE; } + if (maxH > 0 && h > maxH) { h = maxH; needs_clamp = TRUE; } + + if (needs_clamp) { + gtk_window_set_default_size(window, w, h); + } +} + +void window_set_max_size(GtkWindow *window, int maxWidth, int maxHeight) { + g_object_set_data(G_OBJECT(window), "wails-max-width", GINT_TO_POINTER(maxWidth)); + g_object_set_data(G_OBJECT(window), "wails-max-height", GINT_TO_POINTER(maxHeight)); + + // Check if we already connected the signal + gpointer connected = g_object_get_data(G_OBJECT(window), "wails-max-size-connected"); + if (connected == NULL) { + g_signal_connect(window, "notify::default-width", G_CALLBACK(on_window_size_changed), NULL); + g_signal_connect(window, "notify::default-height", G_CALLBACK(on_window_size_changed), NULL); + g_object_set_data(G_OBJECT(window), "wails-max-size-connected", GINT_TO_POINTER(1)); + } + +} + // ============================================================================ // Misc // ============================================================================ diff --git a/v3/pkg/application/linux_cgo_gtk4.go b/v3/pkg/application/linux_cgo_gtk4.go index c077e2fc9..7dbde24f1 100644 --- a/v3/pkg/application/linux_cgo_gtk4.go +++ b/v3/pkg/application/linux_cgo_gtk4.go @@ -1063,9 +1063,14 @@ func (w *linuxWebviewWindow) size() (int, int) { } func (w *linuxWebviewWindow) relativePosition() (int, int) { - // GTK4/Wayland: Window positioning is not reliable - // This is a documented limitation - return 0, 0 + x, y := w.position() + monitor := w.getCurrentMonitor() + if monitor == nil { + return x, y + } + var geometry C.GdkRectangle + C.gdk_monitor_get_geometry(monitor, &geometry) + return x - int(geometry.x), y - int(geometry.y) } func (w *linuxWebviewWindow) gtkWidget() *C.GtkWidget { @@ -1228,6 +1233,9 @@ func windowSetGeometryHints(window pointer, minWidth, minHeight, maxWidth, maxHe if minWidth > 0 && minHeight > 0 { C.gtk_widget_set_size_request(w, C.int(minWidth), C.int(minHeight)) } + if maxWidth > 0 || maxHeight > 0 { + C.window_set_max_size((*C.GtkWindow)(window), C.int(maxWidth), C.int(maxHeight)) + } } func (w *linuxWebviewWindow) setResizable(resizable bool) { @@ -1235,11 +1243,17 @@ func (w *linuxWebviewWindow) setResizable(resizable bool) { } func (w *linuxWebviewWindow) move(x, y int) { + // C-side GDK_IS_X11_DISPLAY check handles X11 vs Wayland correctly, + // including XWayland and GDK_BACKEND=x11 scenarios. + C.window_move_x11(w.gtkWindow(), C.int(x), C.int(y)) } func (w *linuxWebviewWindow) position() (int, int) { - // GTK4/Wayland: Cannot reliably get window position - return 0, 0 + // C-side GDK_IS_X11_DISPLAY check handles X11 vs Wayland correctly, + // returning 0,0 on non-X11 displays. + var x, y C.int + C.window_get_position_x11(w.gtkWindow(), &x, &y) + return int(x), int(y) } func (w *linuxWebviewWindow) unfullscreen() { @@ -1255,7 +1269,7 @@ func (w *linuxWebviewWindow) windowShow() { if w.gtkWidget() == nil { return } - C.gtk_widget_set_visible(w.gtkWidget(), gtkBool(true)) + C.gtk_window_present(w.gtkWindow()) } func (w *linuxWebviewWindow) setAlwaysOnTop(alwaysOnTop bool) { diff --git a/v3/pkg/application/linux_cgo_gtk4.h b/v3/pkg/application/linux_cgo_gtk4.h index 777f4d42e..ff1d6a93a 100644 --- a/v3/pkg/application/linux_cgo_gtk4.h +++ b/v3/pkg/application/linux_cgo_gtk4.h @@ -155,6 +155,13 @@ void setupWindowEventControllers(GtkWindow *window, GtkWidget *webview, uintptr_ void beginWindowDrag(GtkWindow *window, int button, double x, double y, guint32 timestamp); void beginWindowResize(GtkWindow *window, GdkSurfaceEdge edge, int button, double x, double y, guint32 timestamp); +// ============================================================================ +// Window position (X11 only) +// ============================================================================ + +void window_move_x11(GtkWindow *window, int x, int y); +void window_get_position_x11(GtkWindow *window, int *x, int *y); + // ============================================================================ // Drag and drop (GtkDropTarget for GTK4) // ============================================================================ @@ -189,6 +196,12 @@ void show_message_dialog(GtkWindow *parent, const char *heading, const char *bod char* clipboard_get_text_sync(void); void clipboard_free_text(char *text); +// ============================================================================ +// Window size constraints +// ============================================================================ + +void window_set_max_size(GtkWindow *window, int maxWidth, int maxHeight); + // ============================================================================ // Misc // ============================================================================ From 4097aa363b69847f626ac585e4eb2b759a35e8ce Mon Sep 17 00:00:00 2001 From: Lea Anthony Date: Sat, 7 Feb 2026 18:48:34 +1100 Subject: [PATCH 4/4] feat(v3): add -tags flag to wails3 build command (#4968) Allow users to pass custom build tags via `wails3 build -tags gtk4` instead of requiring Taskfile modifications. Tags are forwarded as EXTRA_TAGS to platform Taskfiles and appended to the go build command alongside the existing production tag. Works for both native and Docker cross-compilation builds. Closes #4957 Co-authored-by: Claude Opus 4.6 --- v3/UNRELEASED_CHANGELOG.md | 1 + .../commands/build_assets/darwin/Taskfile.yml | 5 +- .../build_assets/docker/Dockerfile.cross | 7 +- .../commands/build_assets/linux/Taskfile.yml | 5 +- .../build_assets/windows/Taskfile.yml | 5 +- v3/internal/commands/task_wrapper.go | 5 +- v3/internal/commands/task_wrapper_test.go | 144 ++++++++++++++++++ v3/internal/flags/task_wrapper.go | 1 + 8 files changed, 165 insertions(+), 8 deletions(-) diff --git a/v3/UNRELEASED_CHANGELOG.md b/v3/UNRELEASED_CHANGELOG.md index 54e8a7df5..7814bcfaa 100644 --- a/v3/UNRELEASED_CHANGELOG.md +++ b/v3/UNRELEASED_CHANGELOG.md @@ -17,6 +17,7 @@ After processing, the content will be moved to the main changelog and this file ## Added +- Add `-tags` flag to `wails3 build` command for passing custom Go build tags (e.g., `wails3 build -tags gtk4`) (#4957) ## Changed diff --git a/v3/internal/commands/build_assets/darwin/Taskfile.yml b/v3/internal/commands/build_assets/darwin/Taskfile.yml index 041bd2091..e4a2d03a9 100644 --- a/v3/internal/commands/build_assets/darwin/Taskfile.yml +++ b/v3/internal/commands/build_assets/darwin/Taskfile.yml @@ -21,6 +21,7 @@ tasks: ARCH: '{{.ARCH}}' DEV: '{{.DEV}}' OUTPUT: '{{.OUTPUT}}' + EXTRA_TAGS: '{{.EXTRA_TAGS}}' vars: DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}' OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}' @@ -40,7 +41,7 @@ tasks: cmds: - go build {{.BUILD_FLAGS}} -o {{.OUTPUT}} vars: - BUILD_FLAGS: '{{if eq .DEV "true"}}-buildvcs=false -gcflags=all="-l"{{else}}-tags production -trimpath -buildvcs=false -ldflags="-w -s"{{end}}' + BUILD_FLAGS: '{{if eq .DEV "true"}}{{if .EXTRA_TAGS}}-tags {{.EXTRA_TAGS}} {{end}}-buildvcs=false -gcflags=all="-l"{{else}}-tags production{{if .EXTRA_TAGS}},{{.EXTRA_TAGS}}{{end}} -trimpath -buildvcs=false -ldflags="-w -s"{{end}}' DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}' OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}' env: @@ -65,7 +66,7 @@ tasks: Docker image '{{.CROSS_IMAGE}}' not found. Build it first: wails3 task setup:docker cmds: - - docker run --rm -v "{{.ROOT_DIR}}:/app" {{.GO_CACHE_MOUNT}} {{.REPLACE_MOUNTS}} -e APP_NAME="{{.APP_NAME}}" {{.CROSS_IMAGE}} darwin {{.DOCKER_ARCH}} + - docker run --rm -v "{{.ROOT_DIR}}:/app" {{.GO_CACHE_MOUNT}} {{.REPLACE_MOUNTS}} -e APP_NAME="{{.APP_NAME}}" {{if .EXTRA_TAGS}}-e EXTRA_TAGS="{{.EXTRA_TAGS}}"{{end}} {{.CROSS_IMAGE}} darwin {{.DOCKER_ARCH}} - docker run --rm -v "{{.ROOT_DIR}}:/app" alpine chown -R $(id -u):$(id -g) /app/bin - mkdir -p {{.BIN_DIR}} - mv "bin/{{.APP_NAME}}-darwin-{{.DOCKER_ARCH}}" "{{.OUTPUT}}" diff --git a/v3/internal/commands/build_assets/docker/Dockerfile.cross b/v3/internal/commands/build_assets/docker/Dockerfile.cross index ddae7aa03..a3c01f2da 100644 --- a/v3/internal/commands/build_assets/docker/Dockerfile.cross +++ b/v3/internal/commands/build_assets/docker/Dockerfile.cross @@ -188,7 +188,12 @@ if [ "$GOOS" = "windows" ]; then LDFLAGS="-s -w -H windowsgui" fi -go build -ldflags="$LDFLAGS" -o bin/${APP}-${GOOS}-${GOARCH}${EXT} . +TAGS="production" +if [ -n "$EXTRA_TAGS" ]; then + TAGS="${TAGS},${EXTRA_TAGS}" +fi + +go build -tags "$TAGS" -trimpath -buildvcs=false -ldflags="$LDFLAGS" -o bin/${APP}-${GOOS}-${GOARCH}${EXT} . echo "Built: bin/${APP}-${GOOS}-${GOARCH}${EXT}" SCRIPT RUN chmod +x /usr/local/bin/build.sh diff --git a/v3/internal/commands/build_assets/linux/Taskfile.yml b/v3/internal/commands/build_assets/linux/Taskfile.yml index 12854847f..a5e583d19 100644 --- a/v3/internal/commands/build_assets/linux/Taskfile.yml +++ b/v3/internal/commands/build_assets/linux/Taskfile.yml @@ -26,6 +26,7 @@ tasks: ARCH: '{{.ARCH}}' DEV: '{{.DEV}}' OUTPUT: '{{.OUTPUT}}' + EXTRA_TAGS: '{{.EXTRA_TAGS}}' vars: DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}' OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}' @@ -51,7 +52,7 @@ tasks: cmds: - go build {{.BUILD_FLAGS}} -o {{.OUTPUT}} vars: - BUILD_FLAGS: '{{if eq .DEV "true"}}-buildvcs=false -gcflags=all="-l"{{else}}-tags production -trimpath -buildvcs=false -ldflags="-w -s"{{end}}' + BUILD_FLAGS: '{{if eq .DEV "true"}}{{if .EXTRA_TAGS}}-tags {{.EXTRA_TAGS}} {{end}}-buildvcs=false -gcflags=all="-l"{{else}}-tags production{{if .EXTRA_TAGS}},{{.EXTRA_TAGS}}{{end}} -trimpath -buildvcs=false -ldflags="-w -s"{{end}}' DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}' OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}' env: @@ -74,7 +75,7 @@ tasks: Docker image '{{.CROSS_IMAGE}}' not found. Build it first: wails3 task setup:docker cmds: - - docker run --rm -v "{{.ROOT_DIR}}:/app" {{.GO_CACHE_MOUNT}} {{.REPLACE_MOUNTS}} -e APP_NAME="{{.APP_NAME}}" "{{.CROSS_IMAGE}}" linux {{.DOCKER_ARCH}} + - docker run --rm -v "{{.ROOT_DIR}}:/app" {{.GO_CACHE_MOUNT}} {{.REPLACE_MOUNTS}} -e APP_NAME="{{.APP_NAME}}" {{if .EXTRA_TAGS}}-e EXTRA_TAGS="{{.EXTRA_TAGS}}"{{end}} "{{.CROSS_IMAGE}}" linux {{.DOCKER_ARCH}} - docker run --rm -v "{{.ROOT_DIR}}:/app" alpine chown -R $(id -u):$(id -g) /app/bin - mkdir -p {{.BIN_DIR}} - mv "bin/{{.APP_NAME}}-linux-{{.DOCKER_ARCH}}" "{{.OUTPUT}}" diff --git a/v3/internal/commands/build_assets/windows/Taskfile.yml b/v3/internal/commands/build_assets/windows/Taskfile.yml index 77b620b7d..a579f85f2 100644 --- a/v3/internal/commands/build_assets/windows/Taskfile.yml +++ b/v3/internal/commands/build_assets/windows/Taskfile.yml @@ -23,6 +23,7 @@ tasks: vars: ARCH: '{{.ARCH}}' DEV: '{{.DEV}}' + EXTRA_TAGS: '{{.EXTRA_TAGS}}' vars: # Default to CGO_ENABLED=0 if not explicitly set CGO_ENABLED: '{{.CGO_ENABLED | default "0"}}' @@ -47,7 +48,7 @@ tasks: - cmd: rm -f *.syso platforms: [linux, darwin] vars: - BUILD_FLAGS: '{{if eq .DEV "true"}}-buildvcs=false -gcflags=all="-l"{{else}}-tags production -trimpath -buildvcs=false -ldflags="-w -s -H windowsgui"{{end}}' + BUILD_FLAGS: '{{if eq .DEV "true"}}{{if .EXTRA_TAGS}}-tags {{.EXTRA_TAGS}} {{end}}-buildvcs=false -gcflags=all="-l"{{else}}-tags production{{if .EXTRA_TAGS}},{{.EXTRA_TAGS}}{{end}} -trimpath -buildvcs=false -ldflags="-w -s -H windowsgui"{{end}}' env: GOOS: windows CGO_ENABLED: '{{.CGO_ENABLED | default "0"}}' @@ -68,7 +69,7 @@ tasks: Build it first: wails3 task setup:docker cmds: - task: generate:syso - - docker run --rm -v "{{.ROOT_DIR}}:/app" {{.GO_CACHE_MOUNT}} {{.REPLACE_MOUNTS}} -e APP_NAME="{{.APP_NAME}}" {{.CROSS_IMAGE}} windows {{.DOCKER_ARCH}} + - docker run --rm -v "{{.ROOT_DIR}}:/app" {{.GO_CACHE_MOUNT}} {{.REPLACE_MOUNTS}} -e APP_NAME="{{.APP_NAME}}" {{if .EXTRA_TAGS}}-e EXTRA_TAGS="{{.EXTRA_TAGS}}"{{end}} {{.CROSS_IMAGE}} windows {{.DOCKER_ARCH}} - docker run --rm -v "{{.ROOT_DIR}}:/app" alpine chown -R $(id -u):$(id -g) /app/bin - rm -f *.syso vars: diff --git a/v3/internal/commands/task_wrapper.go b/v3/internal/commands/task_wrapper.go index 908c0e53c..028c9f4a3 100644 --- a/v3/internal/commands/task_wrapper.go +++ b/v3/internal/commands/task_wrapper.go @@ -18,7 +18,10 @@ var validPlatforms = map[string]bool{ "linux": true, } -func Build(_ *flags.Build, otherArgs []string) error { +func Build(buildFlags *flags.Build, otherArgs []string) error { + if buildFlags.Tags != "" { + otherArgs = append(otherArgs, "EXTRA_TAGS="+buildFlags.Tags) + } return wrapTask("build", otherArgs) } diff --git a/v3/internal/commands/task_wrapper_test.go b/v3/internal/commands/task_wrapper_test.go index 5a7d8c3a0..e4322d1fa 100644 --- a/v3/internal/commands/task_wrapper_test.go +++ b/v3/internal/commands/task_wrapper_test.go @@ -199,6 +199,150 @@ func TestBuildCommand(t *testing.T) { assert.Equal(t, []string{"CONFIG=release", "ARCH=" + currentArch}, capturedOtherArgs) } +func TestBuildCommandWithTags(t *testing.T) { + currentOS := runtime.GOOS + currentArch := runtime.GOARCH + + // Save original RunTask + originalRunTask := runTaskFunc + defer func() { runTaskFunc = originalRunTask }() + + // Mock RunTask to capture the arguments + var capturedOptions *RunTaskOptions + var capturedOtherArgs []string + runTaskFunc = func(options *RunTaskOptions, otherArgs []string) error { + capturedOptions = options + capturedOtherArgs = otherArgs + return nil + } + + // Save original os.Args and environment + originalArgs := os.Args + defer func() { os.Args = originalArgs }() + + originalGOOS := os.Getenv("GOOS") + originalGOARCH := os.Getenv("GOARCH") + defer func() { + if originalGOOS == "" { + os.Unsetenv("GOOS") + } else { + os.Setenv("GOOS", originalGOOS) + } + if originalGOARCH == "" { + os.Unsetenv("GOARCH") + } else { + os.Setenv("GOARCH", originalGOARCH) + } + }() + os.Unsetenv("GOOS") + os.Unsetenv("GOARCH") + + // Test Build command with tags + buildFlags := &flags.Build{} + buildFlags.Tags = "gtk4" + otherArgs := []string{"CONFIG=release"} + + err := Build(buildFlags, otherArgs) + assert.NoError(t, err) + assert.Equal(t, currentOS+":build", capturedOptions.Name) + assert.Equal(t, []string{"CONFIG=release", "EXTRA_TAGS=gtk4", "ARCH=" + currentArch}, capturedOtherArgs) +} + +func TestBuildCommandWithMultipleTags(t *testing.T) { + currentOS := runtime.GOOS + currentArch := runtime.GOARCH + + // Save original RunTask + originalRunTask := runTaskFunc + defer func() { runTaskFunc = originalRunTask }() + + // Mock RunTask to capture the arguments + var capturedOptions *RunTaskOptions + var capturedOtherArgs []string + runTaskFunc = func(options *RunTaskOptions, otherArgs []string) error { + capturedOptions = options + capturedOtherArgs = otherArgs + return nil + } + + // Save original os.Args and environment + originalArgs := os.Args + defer func() { os.Args = originalArgs }() + + originalGOOS := os.Getenv("GOOS") + originalGOARCH := os.Getenv("GOARCH") + defer func() { + if originalGOOS == "" { + os.Unsetenv("GOOS") + } else { + os.Setenv("GOOS", originalGOOS) + } + if originalGOARCH == "" { + os.Unsetenv("GOARCH") + } else { + os.Setenv("GOARCH", originalGOARCH) + } + }() + os.Unsetenv("GOOS") + os.Unsetenv("GOARCH") + + // Test Build command with multiple comma-separated tags + buildFlags := &flags.Build{} + buildFlags.Tags = "gtk4,server" + + err := Build(buildFlags, nil) + assert.NoError(t, err) + assert.Equal(t, currentOS+":build", capturedOptions.Name) + assert.Equal(t, []string{"EXTRA_TAGS=gtk4,server", "ARCH=" + currentArch}, capturedOtherArgs) +} + +func TestBuildCommandWithoutTags(t *testing.T) { + currentOS := runtime.GOOS + currentArch := runtime.GOARCH + + // Save original RunTask + originalRunTask := runTaskFunc + defer func() { runTaskFunc = originalRunTask }() + + // Mock RunTask to capture the arguments + var capturedOptions *RunTaskOptions + var capturedOtherArgs []string + runTaskFunc = func(options *RunTaskOptions, otherArgs []string) error { + capturedOptions = options + capturedOtherArgs = otherArgs + return nil + } + + // Save original os.Args and environment + originalArgs := os.Args + defer func() { os.Args = originalArgs }() + + originalGOOS := os.Getenv("GOOS") + originalGOARCH := os.Getenv("GOARCH") + defer func() { + if originalGOOS == "" { + os.Unsetenv("GOOS") + } else { + os.Setenv("GOOS", originalGOOS) + } + if originalGOARCH == "" { + os.Unsetenv("GOARCH") + } else { + os.Setenv("GOARCH", originalGOARCH) + } + }() + os.Unsetenv("GOOS") + os.Unsetenv("GOARCH") + + // Test Build command without tags - no EXTRA_TAGS should be present + buildFlags := &flags.Build{} + + err := Build(buildFlags, nil) + assert.NoError(t, err) + assert.Equal(t, currentOS+":build", capturedOptions.Name) + assert.Equal(t, []string{"ARCH=" + currentArch}, capturedOtherArgs) +} + func TestPackageCommand(t *testing.T) { currentOS := runtime.GOOS currentArch := runtime.GOARCH diff --git a/v3/internal/flags/task_wrapper.go b/v3/internal/flags/task_wrapper.go index d77535f6c..e65f22c09 100644 --- a/v3/internal/flags/task_wrapper.go +++ b/v3/internal/flags/task_wrapper.go @@ -2,6 +2,7 @@ package flags type Build struct { Common + Tags string `name:"tags" description:"Additional build tags to pass to the Go compiler (comma-separated)"` } type Dev struct {