feat(v3): add cross-platform build system and signing support

Add Docker-based cross-compilation for building Wails apps on any platform:
- Linux builds from macOS/Windows using Docker with Zig
- Windows builds with CGO from Linux/macOS using Docker
- macOS builds from Linux/Windows using Docker with osxcross

Add wails3 tool lipo command using konoui/lipo library for creating
macOS universal binaries on any platform.

Add code signing infrastructure:
- wails3 sign wrapper command (like build/package)
- wails3 tool sign low-level command for Taskfiles
- wails3 setup signing interactive wizard
- wails3 setup entitlements for macOS entitlements
- Keychain integration for secure credential storage

Update all platform Taskfiles with signing tasks:
- darwin:sign, darwin:sign:notarize
- windows:sign, windows:sign:installer
- linux:sign:deb, linux:sign:rpm, linux:sign:packages

Reorganize documentation:
- Move building/signing guides to guides/build/
- Add platform-specific packaging guides (macos, linux, windows)
- Add cross-platform build documentation
- Add comprehensive signing guide with CI/CD examples
- Add auto-updates guide and updater reference
- Add distribution tutorial

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Lea Anthony 2025-12-06 13:53:37 +11:00
commit 3594b77666
36 changed files with 5286 additions and 1330 deletions

View file

@ -4,7 +4,7 @@ version: '3'
vars:
# Change this to switch package managers: bun, npm, pnpm, yarn
PKG_MANAGER: bun
PKG_MANAGER: npm
tasks:

View file

@ -1,171 +0,0 @@
---
title: Auto-Updates
description: Implement automatic application updates
sidebar:
order: 4
---
## Overview
Keep your application up-to-date with automatic updates.
## Update Strategies
### 1. Check on Startup
```go
func (a *App) checkForUpdates() {
latest, err := getLatestVersion()
if err != nil {
return
}
if isNewer(latest, currentVersion) {
a.promptUpdate(latest)
}
}
```
### 2. Periodic Checks
```go
func (a *App) startUpdateChecker() {
ticker := time.NewTicker(24 * time.Hour)
go func() {
for range ticker.C {
a.checkForUpdates()
}
}()
}
```
### 3. Manual Check
```go
func (a *App) CheckForUpdates() {
result, _ := a.app.QuestionDialog().
SetMessage("Check for updates?").
SetButtons("Check", "Cancel").
Show()
if result == "Check" {
a.checkForUpdates()
}
}
```
## Implementation
### Version Checking
```go
type UpdateInfo struct {
Version string `json:"version"`
DownloadURL string `json:"download_url"`
ReleaseNotes string `json:"release_notes"`
}
func getLatestVersion() (*UpdateInfo, error) {
resp, err := http.Get("https://api.example.com/latest")
if err != nil {
return nil, err
}
defer resp.Body.Close()
var info UpdateInfo
if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
return nil, err
}
return &info, nil
}
```
### Download and Install
```go
func (a *App) downloadUpdate(url string) error {
// Download update
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
// Save to temp file
tmpFile, err := os.CreateTemp("", "update-*.exe")
if err != nil {
return err
}
defer tmpFile.Close()
// Copy data
if _, err := io.Copy(tmpFile, resp.Body); err != nil {
return err
}
// Launch installer and quit
return a.installUpdate(tmpFile.Name())
}
```
## Third-Party Solutions
### Squirrel
```go
import "github.com/Squirrel/go-squirrel"
func setupUpdater() {
updater := squirrel.NewUpdater(squirrel.Options{
URL: "https://updates.example.com",
})
updater.CheckForUpdates()
}
```
### Self-Hosted
Host update manifests on your own server:
```json
{
"version": "1.0.1",
"platforms": {
"windows": {
"url": "https://example.com/myapp-1.0.1-windows.exe",
"sha256": "..."
},
"darwin": {
"url": "https://example.com/myapp-1.0.1-macos.dmg",
"sha256": "..."
}
},
"release_notes": "Bug fixes and improvements"
}
```
## Best Practices
### ✅ Do
- Verify update signatures
- Show release notes
- Allow users to skip versions
- Test update process thoroughly
- Provide rollback mechanism
- Use HTTPS for downloads
### ❌ Don't
- Don't force immediate updates
- Don't skip signature verification
- Don't interrupt user work
- Don't forget error handling
- Don't lose user data
## Next Steps
- [Creating Installers](/guides/installers) - Package your application
- [Testing](/guides/testing) - Test your application

View file

@ -0,0 +1,104 @@
---
title: Building Applications
description: Build and package your Wails application
sidebar:
order: 1
---
import { Tabs, TabItem, Aside } from "@astrojs/starlight/components";
Wails v3 uses [Task](https://taskfile.dev) as its build system. The `wails3 build` and `wails3 package` commands are convenient wrappers around Task.
## Building
Build for the current platform:
```bash
wails3 build
```
Build for a specific platform:
```bash
wails3 build GOOS=windows
wails3 build GOOS=darwin
wails3 build GOOS=linux
# With architecture
wails3 build GOOS=darwin GOARCH=arm64
# Environment variable style works too
GOOS=windows wails3 build
```
Output goes to the `bin/` directory.
<Aside type="tip">
Cross-compiling to macOS or Linux from another platform requires Docker. See [Cross-Platform Builds](/guides/build/cross-platform) for setup.
</Aside>
## Development
Run your application with hot reload:
```bash
wails3 dev
```
This starts a file watcher that rebuilds and restarts your app on changes. The frontend dev server runs on port 9245 by default.
```bash
# Custom port
wails3 dev -port 3000
# Enable HTTPS
wails3 dev -s
```
## Packaging
Package your app for distribution:
```bash
wails3 package
wails3 package GOOS=windows
wails3 package GOOS=darwin
wails3 package GOOS=linux
```
This creates platform-specific packages:
- **Windows**: NSIS installer — see [Windows Packaging](/guides/build/windows)
- **macOS**: Application bundle (`.app`) — see [macOS Packaging](/guides/build/macos)
- **Linux**: AppImage, deb, and rpm — see [Linux Packaging](/guides/build/linux)
## Using Task Directly
For more control, use Task directly:
```bash
# List available tasks
wails3 task --list
# Verbose output
wails3 task build -v
# Dry run
wails3 task --dry
# Force rebuild
wails3 task build -f
# Pass variables
wails3 task darwin:build ARCH=amd64
```
Platform-specific tasks like `linux:create:deb` or `darwin:build:universal` are only available through Task.
## Generating Assets
Regenerate icons or update build configuration:
```bash
wails3 generate icons -input build/appicon.png
wails3 update build-assets -name "MyApp" -config build/config.yml -dir build
```

View file

@ -0,0 +1,298 @@
---
title: Cross-Platform Building
description: Build for multiple platforms from a single machine
sidebar:
order: 2
---
import { Tabs, TabItem, Aside } from "@astrojs/starlight/components";
## Quick Start
Wails v3 supports building for Windows, macOS, and Linux from any host operating system. The build system automatically detects your environment and chooses the right compilation method.
**Want to cross-compile to macOS and Linux?** Run this once to set up the Docker images (~800MB download):
```bash
wails3 task setup:docker
```
Then build for any platform:
```bash
# Build for current platform (production by default)
wails3 build
# Build for specific platforms
wails3 build GOOS=windows
wails3 build GOOS=darwin
wails3 build GOOS=linux
# Build for ARM64 architecture
wails3 build GOOS=windows GOARCH=arm64
wails3 build GOOS=darwin GOARCH=arm64
wails3 build GOOS=linux GOARCH=arm64
# Environment variable style also works
GOOS=darwin GOARCH=arm64 wails3 build
```
### Windows
Windows is the simplest cross-compilation target because it doesn't require CGO by default.
```bash
wails3 build GOOS=windows
```
This works from any host OS with no additional setup. Go's built-in cross-compilation handles everything.
**If your app requires CGO** (e.g., you're using a C library or CGO-dependent package), you'll need Docker when building from macOS or Linux:
```bash
# One-time setup
wails3 task setup:docker
# Build with CGO enabled
wails3 task windows:build CGO_ENABLED=1
```
The Taskfile detects `CGO_ENABLED=1` on non-Windows hosts and automatically uses the Docker image.
### macOS
macOS builds require CGO for WebView integration, which means cross-compilation needs special tooling.
```bash
# Build for Apple Silicon (arm64) - default
wails3 build GOOS=darwin
# Build for Intel (amd64)
wails3 build GOOS=darwin GOARCH=amd64
# Build universal binary (both architectures)
wails3 task darwin:build:universal
```
**From Linux or Windows**, you'll need to set up Docker first:
```bash
wails3 task setup:docker
```
Once the images are built, the build system detects that you're not on macOS and uses Docker automatically. You don't need to change your build commands.
Note that cross-compiled macOS binaries are not code-signed. You'll need to sign them on macOS or in CI before distribution.
### Linux
Linux builds require CGO for WebView integration.
```bash
wails3 build GOOS=linux
# Build for specific architecture
wails3 build GOOS=linux GOARCH=amd64
wails3 build GOOS=linux GOARCH=arm64
```
**From macOS or Windows**, you'll need to set up Docker first:
```bash
wails3 task setup:docker
```
The build system detects that you're not on Linux and uses Docker automatically.
**On Linux without a C compiler**, the build system checks for `gcc` or `clang`. If neither is found, it falls back to Docker. This is useful for minimal containers or systems without build tools installed. You can either:
1. Install a C compiler: `sudo apt install build-essential` (Debian/Ubuntu) or `sudo pacman -S base-devel` (Arch)
2. Build the Docker image and let it be used automatically
### ARM Architecture
All platforms support ARM64 cross-compilation using `GOARCH`:
```bash
# Windows ARM64 (Surface Pro X, Windows on ARM)
wails3 build GOOS=windows GOARCH=arm64
# Linux ARM64 (Raspberry Pi 4/5, AWS Graviton)
wails3 build GOOS=linux GOARCH=arm64
# macOS ARM64 (Apple Silicon - this is the default on macOS)
wails3 build GOOS=darwin GOARCH=arm64
# macOS Intel (amd64)
wails3 build GOOS=darwin GOARCH=amd64
```
The Docker image includes Zig cross-compiler targets for both amd64 and arm64 on all platforms, so ARM builds work from any host:
| Build ARM64 for | From Windows | From macOS | From Linux |
|-----------------|--------------|------------|------------|
| **Windows ARM64** | Native Go | Native Go | Native Go |
| **macOS ARM64** | Docker | Native | Docker |
| **Linux ARM64** | Docker | Docker | Docker* |
*Linux ARM64 from Linux x86_64 uses Docker because CGO cross-compilation requires a different toolchain.
## How It Works
### Cross-Compilation Matrix
| Host → Target | Windows | macOS | Linux |
|---------------|---------|-------|-------|
| **Windows** | Native | Docker | Docker |
| **macOS** | Native Go | Native | Docker |
| **Linux** | Native Go | Docker | Native |
- **Native** = Platform's native toolchain, no additional setup
- **Native Go** = Go's built-in cross-compilation (`CGO_ENABLED=0`)
- **Docker** = Docker image with Zig cross-compiler
### CGO Requirements
| Target | CGO Required | Cross-Compilation Method |
|--------|--------------|--------------------------|
| Windows | No (default) | Native Go. Docker only if `CGO_ENABLED=1` |
| macOS | Yes | Docker with macOS SDK |
| Linux | Yes | Docker, or native if C compiler available |
### Auto-Detection
The Taskfiles automatically choose the right build method based on your environment:
- **Windows target:** Uses native Go cross-compilation by default. If you explicitly set `CGO_ENABLED=1` on a non-Windows host, it switches to Docker.
- **macOS target:** Uses Docker automatically when not on macOS. No manual intervention needed.
- **Linux target:** Checks for `gcc` or `clang`. Uses native compilation if found, otherwise falls back to Docker.
### Docker Image
Wails uses a single Docker image (`wails-cross`) that can build for all platforms. It uses [Zig](https://ziglang.org/) as the cross-compiler, which can target any platform from any host. The macOS SDK is included for darwin targets.
```bash
wails3 task setup:docker
```
You can check if the image is ready by running `wails3 doctor`.
### macOS SDK
The Docker image downloads the macOS SDK from [joseluisq/macosx-sdks](https://github.com/joseluisq/macosx-sdks) during the image build process. This is required because macOS headers are needed for CGO compilation.
By default, the image uses **macOS SDK 14.5** (Sonoma). To use a different version, rebuild with:
```bash
docker build -t wails-cross \
--build-arg MACOS_SDK_VERSION=15.0 \
-f build/docker/Dockerfile.cross build/docker/
```
See the [available SDK versions](https://github.com/joseluisq/macosx-sdks/releases) for options.
**Important:** Wails does not distribute the macOS SDK. Users are responsible for reviewing Apple's SDK license terms before using this feature.
## CI/CD Integration
For production releases, we recommend using CI/CD with native runners for each platform. This avoids cross-compilation entirely and ensures you get properly signed binaries.
```yaml
name: Build
on:
push:
branches: [main]
jobs:
build:
strategy:
matrix:
include:
- os: ubuntu-latest
goos: linux
- os: macos-latest
goos: darwin
- os: windows-latest
goos: windows
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.24'
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install Wails CLI
run: go install github.com/wailsapp/wails/v3/cmd/wails3@latest
- name: Install Task
uses: arduino/setup-task@v2
- name: Build
run: wails3 build
- uses: actions/upload-artifact@v4
with:
name: app-${{ matrix.goos }}
path: bin/
```
## Troubleshooting
### Docker image not found
```
Docker image 'wails-cross' not found.
```
Run `wails3 task setup:docker` to build the Docker image. You only need to do this once.
### Docker daemon not running
```
Docker is required for cross-compilation. Please install Docker.
```
Start Docker Desktop or the Docker daemon. On Linux, you may need to run `sudo systemctl start docker`.
### No C compiler on Linux
If you see CGO-related errors when building on Linux, you have two options:
1. **Install a C compiler:**
- Debian/Ubuntu: `sudo apt install build-essential`
- Arch Linux: `sudo pacman -S base-devel`
- Fedora: `sudo dnf install gcc`
2. **Use Docker instead:** Run `wails3 task setup:docker` and the Taskfile will use it automatically when no compiler is detected.
### macOS binaries not signed
Cross-compiled macOS binaries are not code-signed. Apple requires code signing for distribution, so you'll need to:
1. Sign the binary on a macOS machine, or
2. Sign in CI using a macOS runner
See [Signing Applications](/guides/build/signing) for details.
### Universal binary creation
Universal binaries (arm64 + amd64 combined) can be built on any platform:
```bash
wails3 task darwin:build:universal
```
On Linux and Windows, Wails uses its built-in `wails3 tool lipo` command (powered by [konoui/lipo](https://github.com/konoui/lipo)) to combine the binaries. This creates a single binary that runs natively on both Apple Silicon and Intel Macs.
## Next Steps
- [Building Applications](/guides/build/building) - Basic build commands and options
- [Signing Applications](/guides/build/signing) - Code signing for distribution

View file

@ -2,7 +2,7 @@
title: Build Customization
description: Customize your build process using Task and Taskfile.yml
sidebar:
order: 1
order: 7
---
import { FileTree } from "@astrojs/starlight/components";

View file

@ -0,0 +1,147 @@
---
title: Linux Packaging
description: Package your Wails application for Linux distribution
sidebar:
order: 5
---
import { Aside } from '@astrojs/starlight/components';
## Package Formats
Package your app for Linux distribution:
```bash
wails3 package GOOS=linux
```
This creates multiple formats in the `bin/` directory:
- **AppImage**: Portable, runs on any Linux distribution
- **DEB**: For Debian, Ubuntu, and derivatives
- **RPM**: For Fedora, RHEL, and derivatives
- **Arch**: For Arch Linux and derivatives
### Individual Formats
Build specific formats:
```bash
wails3 task linux:create:appimage
wails3 task linux:create:deb
wails3 task linux:create:rpm
wails3 task linux:create:aur
```
## Customizing Packages
### Desktop Entry
The `.desktop` file controls how your app appears in application menus. It's generated from values in `build/linux/Taskfile.yml`:
```yaml
vars:
APP_NAME: 'MyApp'
EXEC: 'MyApp'
ICON: 'MyApp'
CATEGORIES: 'Development;'
```
### Package Metadata
Edit `build/linux/nfpm/nfpm.yaml` to customize DEB and RPM packages:
```yaml
name: myapp
version: 1.0.0
maintainer: Your Name <you@example.com>
description: My awesome Wails application
homepage: https://example.com
license: MIT
```
### AppImage
AppImage configuration is in `build/linux/appimage/`. The app icon comes from `build/appicon.png`.
## Signing Packages
Sign DEB and RPM packages with a PGP key:
```bash
# Using the wrapper (auto-detects platform)
wails3 sign GOOS=linux
# Or using tasks directly
wails3 task linux:sign:deb
wails3 task linux:sign:rpm
wails3 task linux:sign:packages # Both
```
Configure signing in `build/linux/Taskfile.yml`:
```yaml
vars:
PGP_KEY: "path/to/signing-key.asc"
SIGN_ROLE: "builder" # origin, maint, archive, or builder
```
Store your key password:
```bash
wails3 setup signing
```
See [Signing Applications](/guides/build/signing) for details.
## Building for ARM
```bash
wails3 build GOOS=linux GOARCH=arm64
wails3 package GOOS=linux GOARCH=arm64
```
<Aside type="note">
ARM64 builds from x86_64 hosts use Docker for CGO cross-compilation.
</Aside>
## Troubleshooting
### AppImage won't run
Make it executable:
```bash
chmod +x MyApp-x86_64.AppImage
```
### Missing dependencies
If the app fails to start, check for missing WebKit dependencies:
```bash
# Debian/Ubuntu
sudo apt install libwebkit2gtk-4.1-0
# Fedora
sudo dnf install webkit2gtk4.1
# Arch
sudo pacman -S webkit2gtk-4.1
```
### No C compiler found
The build system needs GCC or Clang for CGO:
```bash
# Debian/Ubuntu
sudo apt install build-essential
# Fedora
sudo dnf install gcc
# Arch
sudo pacman -S base-devel
```
Alternatively, run `wails3 task setup:docker` and the build system will use Docker automatically.

View file

@ -0,0 +1,132 @@
---
title: macOS Packaging
description: Package your Wails application for macOS distribution
sidebar:
order: 4
---
import { Aside } from '@astrojs/starlight/components';
## Application Bundle
Package your app as a standard macOS `.app` bundle:
```bash
wails3 package GOOS=darwin
```
This creates `bin/<AppName>.app` containing:
- The compiled binary in `Contents/MacOS/`
- App icon in `Contents/Resources/`
- `Info.plist` with app metadata
### Universal Binary
Build for both Apple Silicon and Intel Macs:
```bash
wails3 task darwin:package:universal
```
This creates a single `.app` that runs natively on both architectures. Universal binaries can be built on any platform — on Linux and Windows, `wails3 tool lipo` is used automatically.
## Customizing the Bundle
Edit `build/darwin/Info.plist` to customize:
- Bundle identifier (`CFBundleIdentifier`)
- App name and version
- Minimum macOS version
- File associations
- URL schemes
The app icon is generated from `build/appicon.png`. Regenerate with:
```bash
wails3 generate icons -input build/appicon.png
```
## Code Signing
Sign your app for distribution:
```bash
# Using the wrapper (auto-detects platform)
wails3 sign GOOS=darwin
# Or using the task directly
wails3 task darwin:sign
```
Configure signing in `build/darwin/Taskfile.yml`:
```yaml
vars:
SIGN_IDENTITY: "Developer ID Application: Your Company (TEAMID)"
ENTITLEMENTS: "build/darwin/entitlements.plist"
```
### Notarization
For apps distributed outside the Mac App Store, Apple requires notarization:
```bash
wails3 task darwin:sign:notarize
```
First, store your credentials:
```bash
wails3 signing credentials \
--apple-id "you@email.com" \
--team-id "TEAMID" \
--password "app-specific-password" \
--profile "my-notarize-profile"
```
Configure in `build/darwin/Taskfile.yml`:
```yaml
vars:
SIGN_IDENTITY: "Developer ID Application: Your Company (TEAMID)"
KEYCHAIN_PROFILE: "my-notarize-profile"
```
See [Signing Applications](/guides/build/signing) for details.
## DMG Installer
Create a DMG disk image for distribution:
```bash
wails3 task darwin:create:dmg
```
<Aside type="tip">
Customize the DMG background and layout by editing `build/darwin/dmg/` assets.
</Aside>
## Troubleshooting
### "App is damaged and can't be opened"
The app isn't signed. Either sign it with a Developer ID certificate, or users can bypass Gatekeeper:
```bash
xattr -cr /path/to/YourApp.app
```
### Notarization fails
Common issues:
- **Invalid credentials**: Re-run `wails3 signing credentials`
- **Hardened runtime required**: Ensure entitlements include `com.apple.security.cs.allow-unsigned-executable-memory` if needed
- **Missing timestamp**: The signing process should include a timestamp automatically
### Cross-compiled app won't run
Cross-compiled macOS binaries aren't signed. Transfer to a Mac and sign before testing:
```bash
codesign --force --deep --sign - YourApp.app
```

View file

@ -0,0 +1,823 @@
---
title: Code Signing
description: Guide for signing your Wails applications on all platforms
sidebar:
order: 6
---
import { Tabs, TabItem, Steps, Aside } from '@astrojs/starlight/components';
# Code Signing Your Application
This guide covers how to sign your Wails applications for macOS, Windows, and Linux. Wails v3 provides built-in CLI tools for code signing, notarization, and PGP key management.
- **macOS** - Sign and notarize your macOS applications
- **Windows** - Sign your Windows executables and packages
- **Linux** - Sign DEB and RPM packages with PGP keys
## Cross-Platform Signing Matrix
This matrix shows what you can sign from each source platform:
| Target Format | From Windows | From macOS | From Linux |
|---------------|:------------:|:----------:|:----------:|
| Windows EXE/MSI | ✅ | ✅ | ✅ |
| macOS .app bundle | ❌ | ✅ | ❌ |
| macOS notarization | ❌ | ✅ | ❌ |
| Linux DEB | ✅ | ✅ | ✅ |
| Linux RPM | ✅ | ✅ | ✅ |
<Aside type="tip">
Windows and Linux packages can be signed from **any platform**. macOS signing requires a Mac due to Apple's tooling requirements.
</Aside>
### Signing Backends
Wails automatically selects the best available signing backend:
| Platform | Native Backend | Cross-Platform Backend |
|----------|----------------|------------------------|
| Windows | `signtool.exe` (Windows SDK) | Built-in |
| macOS | `codesign` (Xcode) | Not available |
| Linux | N/A | Built-in |
When running on the native platform, Wails uses the native tools for maximum compatibility. When cross-compiling, it uses the built-in signing support.
## Quick Start
The easiest way to configure signing is using the interactive setup wizard:
```bash
wails3 setup signing
```
This command:
- Walks you through configuring signing credentials for each platform
- On macOS, lists available Developer ID certificates from your keychain
- For Linux, can generate a new PGP key if you don't have one
- **Stores passwords securely in your system keychain** (not in Taskfiles)
- Updates the `vars` section in each platform's Taskfile with non-sensitive config
<Aside type="tip">
Passwords are stored in your system's native credential store (macOS Keychain, Windows Credential Manager, or Linux Secret Service). This means your signing configuration is secure and works across all your Wails projects.
</Aside>
To configure only specific platforms:
```bash
wails3 setup signing --platform darwin
wails3 setup signing --platform windows --platform linux
```
### Manual Configuration
Alternatively, you can manually edit the platform-specific Taskfiles. Edit the `vars` section at the top of each file:
<Tabs>
<TabItem label="macOS">
Edit `build/darwin/Taskfile.yml`:
```yaml
vars:
SIGN_IDENTITY: "Developer ID Application: Your Company (TEAMID)"
KEYCHAIN_PROFILE: "my-notarize-profile"
# ENTITLEMENTS: "build/darwin/entitlements.plist"
```
Then run:
```bash
wails3 task darwin:sign # Sign only
wails3 task darwin:sign:notarize # Sign and notarize
```
</TabItem>
<TabItem label="Windows">
Edit `build/windows/Taskfile.yml`:
```yaml
vars:
SIGN_CERTIFICATE: "path/to/certificate.pfx"
# Or use thumbprint instead:
# SIGN_THUMBPRINT: "certificate-thumbprint"
# TIMESTAMP_SERVER: "http://timestamp.digicert.com"
```
Password is retrieved from system keychain (run `wails3 setup signing` to configure).
Then run:
```bash
wails3 task windows:sign # Sign executable
wails3 task windows:sign:installer # Sign NSIS installer
```
</TabItem>
<TabItem label="Linux">
Edit `build/linux/Taskfile.yml`:
```yaml
vars:
PGP_KEY: "path/to/signing-key.asc"
# SIGN_ROLE: "builder" # Options: origin, maint, archive, builder
```
Password is retrieved from system keychain (run `wails3 setup signing` to configure).
Then run:
```bash
wails3 task linux:sign:deb # Sign DEB package
wails3 task linux:sign:rpm # Sign RPM package
wails3 task linux:sign:packages # Sign all packages
```
</TabItem>
</Tabs>
You can also use the CLI directly:
```bash
# Check signing capabilities on your system
wails3 signing info
# List available signing identities (macOS/Windows)
wails3 signing list
```
## macOS Code Signing
### Prerequisites
- Apple Developer Account ($99/year)
- Developer ID Application certificate
- Xcode Command Line Tools installed
### Signing Identities
Check available signing identities:
```bash
wails3 signing list
```
Output:
```
Found 2 signing identities:
Developer ID Application: Your Company (ABCD1234) [valid]
Hash: ABC123DEF456...
Apple Development: your@email.com (XYZ789) [valid]
Hash: DEF789ABC123...
```
<Aside type="tip">
For distribution outside the App Store, you need a **Developer ID Application** certificate.
</Aside>
### Configuration
Edit `build/darwin/Taskfile.yml` and set the signing variables:
```yaml
vars:
SIGN_IDENTITY: "Developer ID Application: Your Company (TEAMID)"
KEYCHAIN_PROFILE: "my-notarize-profile"
ENTITLEMENTS: "build/darwin/entitlements.plist"
```
| Variable | Required | Description |
|----------|----------|-------------|
| `SIGN_IDENTITY` | Yes | Your Developer ID (e.g., "Developer ID Application: Your Company (TEAMID)") |
| `KEYCHAIN_PROFILE` | For notarization | Keychain profile name with stored credentials |
| `ENTITLEMENTS` | No | Path to entitlements file |
Then run:
```bash
wails3 task darwin:sign # Build, package, and sign
wails3 task darwin:sign:notarize # Build, package, sign, and notarize
```
### Entitlements
Entitlements control what capabilities your app has access to. Wails apps typically need different entitlements for development vs production:
- **Development**: Requires JIT, unsigned memory, and debugging entitlements
- **Production**: Minimal entitlements (just network access)
Use the interactive setup wizard to generate both files:
```bash
wails3 setup entitlements
```
This creates:
- `build/darwin/entitlements.dev.plist` - For development builds
- `build/darwin/entitlements.plist` - For production/signed builds
**Presets available:**
| Preset | Description |
|--------|-------------|
| Development | JIT, unsigned memory, debugging, network |
| Production | Network only (minimal, most secure) |
| Both | Creates both dev and production files (recommended) |
| App Store | Sandbox enabled with network and file access |
| Custom | Choose individual entitlements |
<Aside type="note">
The `run` task in the darwin Taskfile uses `entitlements.dev.plist` automatically. The `sign` tasks use `entitlements.plist` for production builds.
</Aside>
Then set `ENTITLEMENTS` in your Taskfile vars to point to the appropriate file.
### Notarization
Apple requires all distributed apps to be notarized.
<Steps>
1. **Store your credentials in the keychain** (one-time setup):
```bash
wails3 signing credentials \
--apple-id "your@email.com" \
--team-id "ABCD1234" \
--password "app-specific-password" \
--profile "my-notarize-profile"
```
2. **Set KEYCHAIN_PROFILE in your Taskfile** to match the profile name above.
3. **Sign and notarize your app**:
```bash
wails3 task darwin:sign:notarize
```
4. **Verify notarization**:
```bash
spctl --assess --verbose=2 bin/MyApp.app
```
</Steps>
<Aside type="note">
Notarization typically takes 1-2 minutes. The ticket is automatically stapled to your app.
</Aside>
## Windows Code Signing
### Prerequisites
- Code signing certificate (from DigiCert, Sectigo, etc.)
- For native signing: Windows SDK installed (for signtool.exe)
- For cross-platform: Just the certificate file
### Configuration
Edit `build/windows/Taskfile.yml` and set the signing variables:
```yaml
vars:
SIGN_CERTIFICATE: "path/to/certificate.pfx"
# Or use thumbprint instead:
# SIGN_THUMBPRINT: "certificate-thumbprint"
# TIMESTAMP_SERVER: "http://timestamp.digicert.com"
```
| Variable | Required | Description |
|----------|----------|-------------|
| `SIGN_CERTIFICATE` | One of these | Path to .pfx/.p12 certificate file |
| `SIGN_THUMBPRINT` | One of these | Certificate thumbprint in Windows cert store |
| `TIMESTAMP_SERVER` | No | Timestamp server URL (default: http://timestamp.digicert.com) |
<Aside type="note">
The certificate password is stored in your system keychain, not in the Taskfile. Run `wails3 setup signing` to configure it, or set the `WAILS_WINDOWS_CERT_PASSWORD` environment variable in CI.
</Aside>
Then run:
```bash
wails3 task windows:sign # Build and sign executable
wails3 task windows:sign:installer # Build and sign NSIS installer
```
### Cross-Platform Signing
Windows executables can be signed from any platform. The same Taskfile configuration and commands work on macOS and Linux.
### Supported Windows Formats
| Format | Extension | Notes |
|--------|-----------|-------|
| Executables | .exe | Standard PE signing |
| Installers | .msi | Windows Installer packages |
| App Packages | .msix, .appx | Modern Windows apps |
## Linux Package Signing
Linux packages (DEB and RPM) are signed using PGP/GPG keys. Unlike Windows and macOS code signing, Linux package signing proves the package came from a trusted source rather than that the code is trusted by the OS.
### Prerequisites
- PGP key pair (can be generated with Wails)
### Generating a PGP Key
If you don't have a PGP key, Wails can generate one for you:
```bash
wails3 signing generate-key \
--name "Your Name" \
--email "your@email.com" \
--comment "Package Signing Key" \
--output-private signing-key.asc \
--output-public signing-key.pub.asc
```
Options:
- `--bits`: Key size (default: 4096)
- `--expiry`: Key expiry duration (e.g., "1y", "6m", "0" for no expiry)
- `--password`: Password to protect the private key
<Aside type="caution">
Keep your private key secure! Store it encrypted and back it up safely.
</Aside>
### Configuration
Edit `build/linux/Taskfile.yml` and set the signing variables:
```yaml
vars:
PGP_KEY: "path/to/signing-key.asc"
# SIGN_ROLE: "builder" # Options: origin, maint, archive, builder
```
| Variable | Required | Description |
|----------|----------|-------------|
| `PGP_KEY` | Yes | Path to PGP private key file |
| `SIGN_ROLE` | No | DEB signing role (default: builder) |
<Aside type="note">
The PGP key password is stored in your system keychain, not in the Taskfile. Run `wails3 setup signing` to configure it, or set the `WAILS_PGP_PASSWORD` environment variable in CI.
</Aside>
Then run:
```bash
wails3 task linux:sign:deb # Build and sign DEB package
wails3 task linux:sign:rpm # Build and sign RPM package
wails3 task linux:sign:packages # Build and sign all packages
```
### DEB Signing Roles
For DEB packages, you can specify the signing role via `SIGN_ROLE`:
- `origin`: Signature from the package origin
- `maint`: Signature from the package maintainer
- `archive`: Signature from the archive maintainer
- `builder`: Signature from the package builder (default)
### Cross-Platform Signing
Linux packages can be signed from any platform. The same Taskfile configuration and commands work on Windows and macOS.
### Viewing Key Information
```bash
wails3 signing key-info --key signing-key.asc
```
Output:
```
PGP Key Information:
Key ID: ABC123DEF456
Fingerprint: 1234 5678 90AB CDEF ...
User IDs: Your Name <your@email.com>
Created: 2024-01-15
Expires: 2025-01-15
Has Private: Yes
Encrypted: Yes
```
### Verifying Linux Packages
```bash
# Verify DEB signature
dpkg-sig --verify myapp_1.0.0_amd64.deb
# Verify RPM signature
rpm --checksig myapp-1.0.0.x86_64.rpm
```
### Distributing Your Public Key
Users need your public key to verify packages:
```bash
# Export public key for distribution
wails3 signing key-info --key signing-key.asc --export-public > myapp-signing.pub.asc
# Users can import it:
# For DEB (apt):
sudo apt-key add myapp-signing.pub.asc
# Or for modern apt:
sudo cp myapp-signing.pub.asc /etc/apt/trusted.gpg.d/
# For RPM:
sudo rpm --import myapp-signing.pub.asc
```
## GitHub Actions Integration
In CI environments, passwords are provided via environment variables instead of the system keychain:
| Environment Variable | Description |
|---------------------|-------------|
| `WAILS_WINDOWS_CERT_PASSWORD` | Windows certificate password |
| `WAILS_PGP_PASSWORD` | PGP key password for Linux packages |
You can also pass Taskfile variables directly:
```bash
wails3 task darwin:sign SIGN_IDENTITY="$SIGN_IDENTITY" KEYCHAIN_PROFILE="$KEYCHAIN_PROFILE"
```
### macOS Workflow
```yaml
name: Build and Sign macOS
on:
push:
tags: ['v*']
jobs:
build:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.23'
- name: Install Wails
run: go install github.com/wailsapp/wails/v3/cmd/wails3@latest
- name: Import Certificate
env:
CERTIFICATE_BASE64: ${{ secrets.MACOS_CERTIFICATE }}
CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
run: |
echo $CERTIFICATE_BASE64 | base64 --decode > certificate.p12
security create-keychain -p "" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "" build.keychain
security import certificate.p12 -k build.keychain -P "$CERTIFICATE_PASSWORD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "" build.keychain
- name: Store Notarization Credentials
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }}
run: |
wails3 signing credentials \
--apple-id "$APPLE_ID" \
--team-id "$APPLE_TEAM_ID" \
--password "$APPLE_APP_PASSWORD" \
--profile "notarize-profile"
- name: Build, Sign, and Notarize
env:
SIGN_IDENTITY: ${{ secrets.MACOS_SIGN_IDENTITY }}
run: |
wails3 task darwin:sign:notarize \
SIGN_IDENTITY="$SIGN_IDENTITY" \
KEYCHAIN_PROFILE="notarize-profile"
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: MyApp-macOS
path: bin/*.app
```
### Windows Workflow
```yaml
name: Build and Sign Windows
on:
push:
tags: ['v*']
jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.23'
- name: Install Wails
run: go install github.com/wailsapp/wails/v3/cmd/wails3@latest
- name: Import Certificate
env:
CERTIFICATE_BASE64: ${{ secrets.WINDOWS_CERTIFICATE }}
run: |
$certBytes = [Convert]::FromBase64String($env:CERTIFICATE_BASE64)
[IO.File]::WriteAllBytes("certificate.pfx", $certBytes)
- name: Build and Sign
env:
WAILS_WINDOWS_CERT_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
run: |
wails3 task windows:sign SIGN_CERTIFICATE=certificate.pfx
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: MyApp-Windows
path: bin/*.exe
```
### Cross-Platform Workflow (Linux Runner)
Sign Windows and Linux packages from a single Linux runner:
```yaml
name: Build and Sign (Cross-Platform)
on:
push:
tags: ['v*']
jobs:
build-and-sign:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.23'
- name: Install Wails
run: go install github.com/wailsapp/wails/v3/cmd/wails3@latest
- name: Install Build Dependencies
run: |
sudo apt-get update
sudo apt-get install -y nsis rpm
# Import certificates
- name: Import Certificates
env:
WINDOWS_CERT_BASE64: ${{ secrets.WINDOWS_CERTIFICATE }}
PGP_KEY_BASE64: ${{ secrets.PGP_PRIVATE_KEY }}
run: |
echo "$WINDOWS_CERT_BASE64" | base64 -d > certificate.pfx
echo "$PGP_KEY_BASE64" | base64 -d > signing-key.asc
# Build and sign Windows
- name: Build and Sign Windows
env:
WAILS_WINDOWS_CERT_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
run: |
wails3 task windows:sign SIGN_CERTIFICATE=certificate.pfx
# Build and sign Linux packages
- name: Build and Sign Linux Packages
env:
WAILS_PGP_PASSWORD: ${{ secrets.PGP_PASSWORD }}
run: |
wails3 task linux:sign:packages PGP_KEY=signing-key.asc
# Cleanup secrets
- name: Cleanup
if: always()
run: rm -f certificate.pfx signing-key.asc
- name: Upload Artifacts
uses: actions/upload-artifact@v4
with:
name: signed-binaries
path: |
bin/*.exe
bin/*.deb
bin/*.rpm
```
<Aside type="note">
Using a Linux runner for cross-platform signing simplifies CI/CD by eliminating the need for separate Windows runners. macOS still requires a macOS runner for signing due to Apple's native tooling requirements.
</Aside>
## CLI Reference
### wails3 setup signing
Interactive wizard to configure signing for your project.
```bash
wails3 setup signing [flags]
Flags:
--platform Platform(s) to configure (darwin, windows, linux)
If not specified, configures all platforms
```
The wizard guides you through:
- **macOS**: Selecting a Developer ID certificate, configuring notarization profile
- **Windows**: Choosing between certificate file or thumbprint, setting password and timestamp server
- **Linux**: Using an existing PGP key or generating a new one, configuring signing role
### wails3 setup entitlements
Interactive wizard to configure macOS entitlements.
```bash
wails3 setup entitlements [flags]
Flags:
--output Output directory (default: build/darwin)
```
**Presets:**
- **Development**: Creates `entitlements.dev.plist` with JIT, debugging, and network
- **Production**: Creates `entitlements.plist` with minimal entitlements
- **Both**: Creates both files (recommended)
- **App Store**: Creates sandboxed entitlements for Mac App Store
- **Custom**: Choose individual entitlements and target file
### wails3 sign
Sign binaries and packages for the current or specified platform. This is a wrapper that calls the appropriate platform-specific signing task.
```bash
wails3 sign
wails3 sign GOOS=darwin
wails3 sign GOOS=windows
wails3 sign GOOS=linux
```
This runs the corresponding `<platform>:sign` task which uses the signing configuration from your Taskfile.
### wails3 tool sign
Low-level command to sign a specific file directly. Used internally by the Taskfiles.
```bash
wails3 tool sign [flags]
```
**Common Flags:**
| Flag | Description |
|------|-------------|
| `--input` | Path to the file to sign |
| `--output` | Output path (optional, defaults to in-place) |
| `--verbose` | Enable verbose output |
**Windows/macOS Flags:**
| Flag | Description |
|------|-------------|
| `--certificate` | Path to PKCS#12 (.pfx/.p12) certificate |
| `--password` | Certificate password |
| `--timestamp` | Timestamp server URL |
**macOS-Specific Flags:**
| Flag | Description |
|------|-------------|
| `--identity` | Signing identity (use '-' for ad-hoc) |
| `--entitlements` | Path to entitlements plist |
| `--hardened-runtime` | Enable hardened runtime (default: true) |
| `--notarize` | Submit for notarization |
| `--keychain-profile` | Keychain profile for notarization |
**Windows-Specific Flags:**
| Flag | Description |
|------|-------------|
| `--thumbprint` | Certificate thumbprint in Windows store |
| `--description` | Application description |
| `--url` | Application URL |
**Linux-Specific Flags:**
| Flag | Description |
|------|-------------|
| `--pgp-key` | Path to PGP private key |
| `--pgp-password` | PGP key password |
| `--role` | DEB signing role (origin/maint/archive/builder) |
### wails3 signing info
Display signing capabilities on the current system.
```bash
wails3 signing info
```
### wails3 signing list
List available signing identities.
```bash
wails3 signing list
```
### wails3 signing credentials
Store notarization credentials in keychain (macOS only).
```bash
wails3 signing credentials [flags]
Flags:
--apple-id Apple ID email
--team-id Apple Developer Team ID
--password App-specific password
--profile Keychain profile name
```
### wails3 signing generate-key
Generate a PGP key pair for Linux package signing.
```bash
wails3 signing generate-key [flags]
Flags:
--name Name for the key (required)
--email Email for the key (required)
--comment Comment for the key
--bits Key size in bits (default: 4096)
--expiry Key expiry (e.g., "1y", "6m", "0" for never)
--password Password to encrypt private key
--output-private Path for private key output
--output-public Path for public key output
```
### wails3 signing key-info
Display information about a PGP key.
```bash
wails3 signing key-info --key <path-to-key>
```
## Troubleshooting
### macOS Issues
**"No Developer ID certificate found"**
- Ensure your certificate is installed in the Keychain
- Check it hasn't expired with `wails3 signing list`
- Make sure you have a "Developer ID Application" certificate (not just "Apple Development")
**"Notarization failed"**
- Check the notarization log: `xcrun notarytool log <submission-id> --keychain-profile <profile>`
- Ensure hardened runtime is enabled
- Verify your app doesn't include unsigned binaries
**"Codesign failed"**
- Make sure the keychain is unlocked: `security unlock-keychain`
- Check file permissions on the app bundle
### Windows Issues
**"Certificate not found"**
- Verify the certificate path is correct
- Check the certificate password
- Ensure the certificate is valid (not expired or revoked)
**"Timestamp server error"**
- Try a different timestamp server:
- `http://timestamp.digicert.com`
- `http://timestamp.sectigo.com`
- `http://timestamp.comodoca.com`
### Linux Issues
**"Invalid PGP key"**
- Ensure the key file is in ASCII-armored format
- Check the key hasn't expired with `wails3 signing key-info`
- Verify the password is correct
**"Signature verification failed"**
- Ensure the public key is properly imported
- Check that the package wasn't modified after signing
## Additional Resources
### Official Documentation
- [Apple Code Signing Guide](https://developer.apple.com/support/code-signing/)
- [Apple Notarization Documentation](https://developer.apple.com/documentation/security/notarizing_macos_software_before_distribution)
- [Microsoft Code Signing](https://docs.microsoft.com/en-us/windows-hardware/drivers/dashboard/get-a-code-signing-certificate)
- [Debian Package Signing](https://wiki.debian.org/SecureApt)
- [RPM Package Signing](https://rpm-software-management.github.io/rpm/manual/signatures.html)

View file

@ -2,414 +2,123 @@
title: Windows Packaging
description: Package your Wails application for Windows distribution
sidebar:
order: 4
order: 3
---
import { Tabs, TabItem } from '@astrojs/starlight/components';
Learn how to package your Wails application for Windows distribution using various installer formats.
## Overview
Wails provides several packaging options for Windows:
- **NSIS** - Nullsoft Scriptable Install System (recommended)
- **MSI** - Windows Installer Package
- **Portable** - Standalone executable (no installation)
- **MSIX** - Modern Windows app package
import { Aside } from '@astrojs/starlight/components';
## NSIS Installer
NSIS creates professional installers with customization options and automatic system integration.
### Quick Start
Build an NSIS installer:
The default packaging format creates an NSIS installer:
```bash
wails3 build
wails3 task nsis
wails3 package GOOS=windows
```
This generates `build/bin/<AppName>-<version>-<arch>-installer.exe`.
This runs `wails3 task windows:package` which:
1. Builds the application
2. Generates the WebView2 bootstrapper
3. Creates an NSIS installer
### Features
Output: `build/windows/nsis/<AppName>-installer.exe`
**Automatic Integration:**
- ✅ Start Menu shortcuts
- ✅ Desktop shortcuts (optional)
- ✅ File associations
- ✅ **Custom URL protocol registration** (NEW)
- ✅ Uninstaller creation
- ✅ Registry entries
### MSIX Package
**Custom Protocols:**
For Microsoft Store distribution or modern Windows deployment:
Wails automatically registers custom URL protocols defined in your application:
```go
app := application.New(application.Options{
Name: "My Application",
Protocols: []application.Protocol{
{
Scheme: "myapp",
Description: "My Application Protocol",
},
},
})
```bash
wails3 package GOOS=windows FORMAT=msix
```
The NSIS installer:
1. Registers all protocols during installation
2. Associates them with your application executable
3. Removes them during uninstallation
Output: `bin/<AppName>-<arch>.msix`
**No additional configuration needed!**
<Aside type="note">
MSIX requires either `makeappx.exe` (Windows SDK) or the MSIX tool. Run `wails3 tool msix-install-tools` to set up.
</Aside>
### Customization
## Customizing the Installer
Customize the installer via `wails.json`:
NSIS configuration is in `build/windows/nsis/project.nsi`. Edit this file to customize:
- Installer UI and branding
- Installation directory
- Start menu and desktop shortcuts
- File associations
- License agreement
Application metadata comes from `build/windows/info.json`:
```json
{
"name": "MyApp",
"outputfilename": "myapp.exe",
"nsis": {
"companyName": "My Company",
"productName": "My Application",
"productDescription": "An amazing desktop application",
"productVersion": "1.0.0",
"installerIcon": "build/appicon.ico",
"license": "LICENSE.txt",
"allowInstallDirCustomization": true,
"installDirectory": "$PROGRAMFILES64\\MyApp",
"createDesktopShortcut": true,
"runAfterInstall": true,
"adminPrivileges": false
}
}
```
### NSIS Options
| Option | Type | Description | Default |
|--------|------|-------------|---------|
| `companyName` | string | Company name shown in installer | Project name |
| `productName` | string | Product name | App name |
| `productDescription` | string | Description shown in installer | App description |
| `productVersion` | string | Version number (e.g., "1.0.0") | "1.0.0" |
| `installerIcon` | string | Path to .ico file for installer | App icon |
| `license` | string | Path to license file (txt) | None |
| `allowInstallDirCustomization` | boolean | Let users choose install location | true |
| `installDirectory` | string | Default installation directory | `$PROGRAMFILES64\<ProductName>` |
| `createDesktopShortcut` | boolean | Create desktop shortcut | true |
| `runAfterInstall` | boolean | Run app after installation | false |
| `adminPrivileges` | boolean | Require admin rights to install | false |
### Advanced NSIS
#### Custom NSIS Script
For advanced customization, provide your own NSIS script:
```bash
# Create custom template
cp build/nsis/installer.nsi build/nsis/installer.custom.nsi
# Edit build/nsis/installer.custom.nsi
# Add custom sections, pages, or logic
# Build with custom script
wails3 task nsis --script build/nsis/installer.custom.nsi
```
#### Available Macros
Wails provides NSIS macros for common tasks:
**wails.associateCustomProtocols**
- Registers all custom URL protocols defined in `application.Options.Protocols`
- Called automatically during installation
- Creates registry entries under `HKEY_CURRENT_USER\SOFTWARE\Classes\`
**wails.unassociateCustomProtocols**
- Removes custom URL protocol registrations
- Called automatically during uninstallation
- Cleans up all protocol-related registry entries
**Example usage in custom NSIS script:**
```nsis
Section "Install"
# ... your installation code ...
# Register custom protocols
!insertmacro wails.associateCustomProtocols
# ... more installation code ...
SectionEnd
Section "Uninstall"
# Remove custom protocols
!insertmacro wails.unassociateCustomProtocols
# ... your uninstallation code ...
SectionEnd
```
## MSI Installer
Windows Installer Package format with Windows logo certification support.
### Build MSI
```bash
wails3 build
wails3 task msi
```
Generates `build/bin/<AppName>-<version>-<arch>.msi`.
### Customization
Configure via `wails.json`:
```json
{
"msi": {
"productCode": "{GUID}",
"upgradeCode": "{GUID}",
"manufacturer": "My Company",
"installScope": "perMachine",
"shortcuts": {
"desktop": true,
"startMenu": true
"fixed": {
"file_version": "1.0.0"
},
"info": {
"0000": {
"ProductVersion": "1.0.0",
"CompanyName": "My Company",
"FileDescription": "My Application",
"ProductName": "MyApp"
}
}
}
```
### MSI vs NSIS
| Feature | NSIS | MSI |
|---------|------|-----|
| Customization | ✅ High | ⚠️ Limited |
| File Size | ✅ Smaller | ⚠️ Larger |
| Corporate Deployment | ⚠️ Less common | ✅ Preferred |
| Custom UI | ✅ Full control | ⚠️ Restricted |
| Windows Logo | ❌ No | ✅ Yes |
| Protocol Registration | ✅ Automatic | ⚠️ Manual |
**Use NSIS when:**
- You want maximum customization
- You need custom branding and UI
- You want automatic protocol registration
- File size matters
**Use MSI when:**
- You need Windows logo certification
- You're deploying in enterprise environments
- You need Group Policy support
- You want Windows Update integration
## Portable Executable
Single executable with no installation required.
### Build Portable
```bash
wails3 build
```
Output: `build/bin/<appname>.exe`
### Characteristics
- No installation required
- No registry changes
- No administrator privileges needed
- Can run from USB drives
- No automatic updates
- No Start Menu integration
### Use Cases
- Trial versions
- USB stick applications
- Corporate environments with restricted installations
- Quick testing and demos
## MSIX Packages
Modern Windows app package format for Microsoft Store and sideloading.
See [MSIX Packaging Guide](/guides/build/msix) for detailed information.
## Code Signing
Sign your executables and installers to:
- Avoid Windows SmartScreen warnings
- Establish publisher identity
- Enable automatic updates
- Meet corporate security requirements
See [Code Signing Guide](/guides/build/signing) for details.
## Icon Requirements
### Application Icon
**Format:** `.ico` file with multiple resolutions
**Recommended sizes:**
- 16x16, 32x32, 48x48, 64x64, 128x128, 256x256
**Create from PNG:**
Sign your executable and installer to avoid SmartScreen warnings:
```bash
# Using ImageMagick
magick convert icon.png -define icon:auto-resize=256,128,64,48,32,16 appicon.ico
# Using the wrapper (auto-detects platform)
wails3 sign GOOS=windows
# Using online tools
# https://icoconvert.com/
# Or using tasks directly
wails3 task windows:sign
wails3 task windows:sign:installer
```
### Installer Icon
Configure signing in `build/windows/Taskfile.yml`:
Same `.ico` file can be used for both application and installer.
Configure in `wails.json`:
```json
{
"nsis": {
"installerIcon": "build/appicon.ico"
},
"windows": {
"applicationIcon": "build/appicon.ico"
}
}
```yaml
vars:
SIGN_CERTIFICATE: "path/to/certificate.pfx"
# Or use thumbprint for certificates in Windows store
SIGN_THUMBPRINT: "certificate-thumbprint"
TIMESTAMP_SERVER: "http://timestamp.digicert.com"
```
## Building for Different Architectures
### AMD64 (64-bit)
Store your certificate password securely:
```bash
wails3 build -platform windows/amd64
wails3 setup signing
```
### ARM64
See [Signing Applications](/guides/build/signing) for details.
## Building for ARM
```bash
wails3 build -platform windows/arm64
wails3 build GOOS=windows GOARCH=arm64
wails3 package GOOS=windows GOARCH=arm64
```
### Universal Build
Build for all architectures:
```bash
wails3 build -platform windows/amd64,windows/arm64
```
## Distribution Checklist
Before distributing your Windows application:
- [ ] Test installer on clean Windows installation
- [ ] Verify application icon displays correctly
- [ ] Test uninstaller completely removes application
- [ ] Verify Start Menu shortcuts work
- [ ] Test custom protocol handlers (if used)
- [ ] Check SmartScreen behavior (sign your app if possible)
- [ ] Test on Windows 10 and Windows 11
- [ ] Verify file associations work (if used)
- [ ] Test with antivirus software
- [ ] Include license and documentation
## Troubleshooting
### NSIS Build Fails
### makensis not found
**Error:** `makensis: command not found`
Install NSIS:
**Solution:**
```bash
# Install NSIS
# Windows
winget install NSIS.NSIS
# Or download from https://nsis.sourceforge.io/
```
### Custom Protocols Not Working
### SmartScreen warning
**Check registration:**
```powershell
# Check registry
Get-ItemProperty -Path "HKCU:\SOFTWARE\Classes\myapp"
Your executable isn't signed. See [Code Signing](#code-signing) above.
# Test protocol
Start-Process "myapp://test"
```
### WebView2 missing
**Fix:**
1. Reinstall with NSIS installer
2. Run installer as administrator if needed
3. Verify `Protocols` configuration in Go code
### SmartScreen Warning
**Cause:** Unsigned executable
**Solutions:**
1. **Code sign your application** (recommended)
2. Build reputation (downloads over time)
3. Submit to Microsoft for analysis
4. Use Extended Validation (EV) certificate for immediate trust
### Installer Won't Run
**Possible causes:**
- Antivirus blocking
- Missing dependencies
- Corrupted download
- User permissions
**Solutions:**
1. Temporarily disable antivirus for testing
2. Run as administrator
3. Re-download installer
4. Check Windows Event Viewer for errors
## Best Practices
### ✅ Do
- **Code sign your releases** - Avoids SmartScreen warnings
- **Test on clean Windows installations** - Don't rely on dev environment
- **Provide both installer and portable versions** - Give users choice
- **Include comprehensive uninstaller** - Remove all traces
- **Use semantic versioning** - Clear version numbering
- **Test protocol handlers thoroughly** - Validate all URL inputs
- **Provide clear installation instructions** - Help users succeed
### ❌ Don't
- **Don't skip code signing** - Users will see scary warnings
- **Don't require admin for normal apps** - Only if truly necessary
- **Don't install to non-standard locations** - Use `$PROGRAMFILES64`
- **Don't leave orphaned registry entries** - Clean up properly
- **Don't forget to test uninstaller** - Broken uninstallers frustrate users
- **Don't hardcode paths** - Use Windows environment variables
## Next Steps
- [Code Signing](/guides/build/signing) - Sign your executables and installers
- [MSIX Packaging](/guides/build/msix) - Modern Windows app packages
- [Custom Protocols](/guides/distribution/custom-protocols) - Deep linking and URL schemes
- [Auto-Updates](/guides/distribution/auto-updates) - Keep your app current
---
**Questions?** Ask in [Discord](https://discord.gg/JDdSxwjhGf) or check the [Windows packaging examples](https://github.com/wailsapp/wails/tree/v3-alpha/v3/examples).
The installer includes a WebView2 bootstrapper that downloads the runtime if needed. If you need offline installation, download the Evergreen Standalone Installer from Microsoft.

View file

@ -1,176 +0,0 @@
---
title: Building Applications
description: Build and package your Wails application
sidebar:
order: 1
---
import { Card, CardGrid, Tabs, TabItem } from "@astrojs/starlight/components";
## Overview
Wails provides simple commands to build your application for development and production.
## Development Build
### Quick Start
```bash
wails3 dev
```
**Features:**
- Hot reload for frontend changes
- Automatic Go rebuild on changes
- Debug mode enabled
- Fast iteration
### Dev Options
```bash
# Specify frontend dev server
wails3 dev -devserver http://localhost:5173
# Skip frontend dev server
wails3 dev -nofrontend
# Custom build flags
wails3 dev -tags dev
```
## Production Build
### Basic Build
```bash
wails3 build
```
**Output:** Optimized binary in `build/bin/`
### Build Options
```bash
# Build for specific platform
wails3 build -platform windows/amd64
# Custom output directory
wails3 build -o ./dist/myapp
# Skip frontend build
wails3 build -nofrontend
# Production optimizations
wails3 build -ldflags "-s -w"
```
## Build Configuration
### wails.json
```json
{
"name": "myapp",
"frontend": {
"dir": "./frontend",
"install": "npm install",
"build": "npm run build",
"dev": "npm run dev",
"devServerUrl": "http://localhost:5173"
},
"build": {
"output": "myapp",
"ldflags": "-s -w"
}
}
```
## Platform-Specific Builds
<Tabs syncKey="platform">
<TabItem label="Windows" icon="seti:windows">
```bash
# Windows executable
wails3 build -platform windows/amd64
# With icon
wails3 build -platform windows/amd64 -icon icon.ico
# Console app (shows terminal)
wails3 build -platform windows/amd64 -windowsconsole
```
</TabItem>
<TabItem label="macOS" icon="apple">
```bash
# macOS app bundle
wails3 build -platform darwin/amd64
# Universal binary (Intel + Apple Silicon)
wails3 build -platform darwin/universal
# With icon
wails3 build -platform darwin/amd64 -icon icon.icns
```
</TabItem>
<TabItem label="Linux" icon="linux">
```bash
# Linux executable
wails3 build -platform linux/amd64
# With icon
wails3 build -platform linux/amd64 -icon icon.png
```
</TabItem>
</Tabs>
## Optimization
### Binary Size
```bash
# Strip debug symbols
wails3 build -ldflags "-s -w"
# UPX compression (external tool)
upx --best --lzma build/bin/myapp
```
### Performance
```bash
# Enable optimizations
wails3 build -tags production
# Disable debug features
wails3 build -ldflags "-X main.debug=false"
```
## Troubleshooting
### Build Fails
**Problem:** Build errors
**Solutions:**
- Check `go.mod` is up to date
- Run `go mod tidy`
- Verify frontend builds: `cd frontend && npm run build`
- Check wails.json configuration
### Large Binary Size
**Problem:** Binary is too large
**Solutions:**
- Use `-ldflags "-s -w"` to strip symbols
- Remove unused dependencies
- Use UPX compression
- Check embedded assets size
## Next Steps
- [Cross-Platform Building](/guides/cross-platform) - Build for multiple platforms
- [Distribution](/guides/installers) - Create installers and packages
- [Build System](/concepts/build-system) - Understand the build system

View file

@ -1,202 +0,0 @@
---
title: Cross-Platform Building
description: Build for multiple platforms from a single machine
sidebar:
order: 2
---
## Overview
Wails supports cross-platform compilation, allowing you to build for Windows, macOS, and Linux from any platform.
## Supported Platforms
```bash
# List available platforms
wails3 build -platform list
# Common platforms:
# - windows/amd64
# - darwin/amd64
# - darwin/arm64
# - darwin/universal
# - linux/amd64
# - linux/arm64
```
## Building for Windows
### From macOS/Linux
```bash
# Install dependencies (one-time)
# macOS: brew install mingw-w64
# Linux: apt-get install mingw-w64
# Build for Windows
wails3 build -platform windows/amd64
```
### Windows-Specific Options
```bash
# With custom icon
wails3 build -platform windows/amd64 -icon app.ico
# Console application
wails3 build -platform windows/amd64 -windowsconsole
# With manifest
wails3 build -platform windows/amd64 -manifest app.manifest
```
## Building for macOS
### From Windows/Linux
```bash
# Note: macOS builds from other platforms have limitations
# Recommended: Use macOS for macOS builds
# Build for Intel Macs
wails3 build -platform darwin/amd64
# Build for Apple Silicon
wails3 build -platform darwin/arm64
# Universal binary (both architectures)
wails3 build -platform darwin/universal
```
### macOS-Specific Options
```bash
# With custom icon
wails3 build -platform darwin/universal -icon app.icns
# Code signing (macOS only)
wails3 build -platform darwin/universal -codesign "Developer ID"
```
## Building for Linux
### From Any Platform
```bash
# Build for Linux AMD64
wails3 build -platform linux/amd64
# Build for Linux ARM64
wails3 build -platform linux/arm64
```
### Linux-Specific Options
```bash
# With custom icon
wails3 build -platform linux/amd64 -icon app.png
```
## Build Matrix
### Build All Platforms
```bash
#!/bin/bash
# build-all.sh
platforms=("windows/amd64" "darwin/universal" "linux/amd64")
for platform in "${platforms[@]}"; do
echo "Building for $platform..."
wails3 build -platform "$platform" -o "dist/myapp-$platform"
done
```
### Using Taskfile
```yaml
# Taskfile.yml
version: '3'
tasks:
build:all:
desc: Build for all platforms
cmds:
- task: build:windows
- task: build:macos
- task: build:linux
build:windows:
desc: Build for Windows
cmds:
- wails3 build -platform windows/amd64 -o dist/myapp-windows.exe
build:macos:
desc: Build for macOS
cmds:
- wails3 build -platform darwin/universal -o dist/myapp-macos
build:linux:
desc: Build for Linux
cmds:
- wails3 build -platform linux/amd64 -o dist/myapp-linux
```
## CI/CD Integration
### GitHub Actions
```yaml
name: Build
on: [push]
jobs:
build:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: '1.21'
- name: Install Wails
run: go install github.com/wailsapp/wails/v3/cmd/wails3@latest
- name: Build
run: wails3 build
- name: Upload artifact
uses: actions/upload-artifact@v3
with:
name: app-${{ matrix.os }}
path: build/bin/
```
## Best Practices
### ✅ Do
- Test on target platforms
- Use CI/CD for builds
- Version your builds
- Include platform in filename
- Document build requirements
### ❌ Don't
- Don't skip testing on target platform
- Don't forget platform-specific icons
- Don't hardcode paths
- Don't ignore build warnings
## Next Steps
- [Creating Installers](/guides/installers) - Package your application
- [Building](/guides/building) - Basic building guide

View file

@ -0,0 +1,564 @@
---
title: Auto-Updates
description: Implement automatic application updates with Wails v3
sidebar:
order: 5
---
import { Tabs, TabItem } from '@astrojs/starlight/components';
import { Steps } from '@astrojs/starlight/components';
import { Card, CardGrid } from '@astrojs/starlight/components';
import { Aside } from '@astrojs/starlight/components';
# Automatic Updates
Wails v3 provides a built-in updater system that supports automatic update checking, downloading, and installation. The updater includes support for binary delta updates (patches) for minimal download sizes.
<CardGrid>
<Card title="Automatic Checking" icon="rocket">
Configure periodic update checks in the background
</Card>
<Card title="Delta Updates" icon="down-caret">
Download only what changed with bsdiff patches
</Card>
<Card title="Cross-Platform" icon="laptop">
Works on macOS, Windows, and Linux
</Card>
<Card title="Secure" icon="approve-check">
SHA256 checksums and optional signature verification
</Card>
</CardGrid>
## Quick Start
Add the updater service to your application:
```go title="main.go"
package main
import (
"github.com/wailsapp/wails/v3/pkg/application"
"time"
)
func main() {
// Create the updater service
updater, err := application.CreateUpdaterService(
"1.0.0", // Current version
application.WithUpdateURL("https://updates.example.com/myapp/"),
application.WithCheckInterval(24 * time.Hour),
)
if err != nil {
panic(err)
}
app := application.New(application.Options{
Name: "MyApp",
Services: []application.Service{
application.NewService(updater),
},
})
// ... rest of your app
app.Run()
}
```
Then use it from your frontend:
```typescript title="App.tsx"
import { updater } from './bindings/myapp';
async function checkForUpdates() {
const update = await updater.CheckForUpdate();
if (update) {
console.log(`New version available: ${update.version}`);
console.log(`Release notes: ${update.releaseNotes}`);
// Download and install
await updater.DownloadAndApply();
}
}
```
## Configuration Options
The updater supports various configuration options:
```go
updater, err := application.CreateUpdaterService(
"1.0.0",
// Required: URL where update manifests are hosted
application.WithUpdateURL("https://updates.example.com/myapp/"),
// Optional: Check for updates automatically every 24 hours
application.WithCheckInterval(24 * time.Hour),
// Optional: Allow pre-release versions
application.WithAllowPrerelease(true),
// Optional: Update channel (stable, beta, canary)
application.WithChannel("stable"),
// Optional: Require signed updates
application.WithRequireSignature(true),
application.WithPublicKey("your-ed25519-public-key"),
)
```
## Update Manifest Format
Host an `update.json` file on your server:
```json title="update.json"
{
"version": "1.2.0",
"release_date": "2025-01-15T00:00:00Z",
"release_notes": "## What's New\n\n- Feature A\n- Bug fix B",
"platforms": {
"macos-arm64": {
"url": "https://updates.example.com/myapp/myapp-1.2.0-macos-arm64.tar.gz",
"size": 12582912,
"checksum": "sha256:abc123...",
"patches": [
{
"from": "1.1.0",
"url": "https://updates.example.com/myapp/patches/1.1.0-to-1.2.0-macos-arm64.patch",
"size": 14336,
"checksum": "sha256:def456..."
}
]
},
"macos-amd64": {
"url": "https://updates.example.com/myapp/myapp-1.2.0-macos-amd64.tar.gz",
"size": 13107200,
"checksum": "sha256:789xyz..."
},
"windows-amd64": {
"url": "https://updates.example.com/myapp/myapp-1.2.0-windows-amd64.zip",
"size": 14680064,
"checksum": "sha256:ghi789..."
},
"linux-amd64": {
"url": "https://updates.example.com/myapp/myapp-1.2.0-linux-amd64.tar.gz",
"size": 11534336,
"checksum": "sha256:jkl012..."
}
},
"minimum_version": "1.0.0",
"mandatory": false
}
```
### Platform Keys
| Platform | Key |
|----------|-----|
| macOS (Apple Silicon) | `macos-arm64` |
| macOS (Intel) | `macos-amd64` |
| Windows (64-bit) | `windows-amd64` |
| Linux (64-bit) | `linux-amd64` |
| Linux (ARM64) | `linux-arm64` |
## Frontend API
The updater exposes methods that are automatically bound to your frontend:
### TypeScript Types
```typescript
interface UpdateInfo {
version: string;
releaseDate: Date;
releaseNotes: string;
size: number;
patchSize?: number;
mandatory: boolean;
hasPatch: boolean;
}
interface Updater {
// Get the current application version
GetCurrentVersion(): string;
// Check if an update is available
CheckForUpdate(): Promise<UpdateInfo | null>;
// Download the update (emits progress events)
DownloadUpdate(): Promise<void>;
// Apply the downloaded update (restarts app)
ApplyUpdate(): Promise<void>;
// Download and apply in one call
DownloadAndApply(): Promise<void>;
// Get current state: idle, checking, available, downloading, ready, installing, error
GetState(): string;
// Get the available update info
GetUpdateInfo(): UpdateInfo | null;
// Get the last error message
GetLastError(): string;
// Reset the updater state
Reset(): void;
}
```
### Progress Events
Listen for download progress events:
```typescript
import { Events } from '@wailsio/runtime';
Events.On('updater:progress', (data) => {
console.log(`Downloaded: ${data.downloaded} / ${data.total}`);
console.log(`Progress: ${data.percentage.toFixed(1)}%`);
console.log(`Speed: ${(data.bytesPerSecond / 1024 / 1024).toFixed(2)} MB/s`);
});
```
## Complete Example
Here's a complete example with a React component:
```tsx title="UpdateChecker.tsx"
import { useState, useEffect } from 'react';
import { updater } from './bindings/myapp';
import { Events } from '@wailsio/runtime';
interface Progress {
downloaded: number;
total: number;
percentage: number;
bytesPerSecond: number;
}
export function UpdateChecker() {
const [checking, setChecking] = useState(false);
const [updateInfo, setUpdateInfo] = useState<any>(null);
const [downloading, setDownloading] = useState(false);
const [progress, setProgress] = useState<Progress | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
// Listen for progress events
const cleanup = Events.On('updater:progress', (data: Progress) => {
setProgress(data);
});
// Check for updates on mount
checkForUpdates();
return () => cleanup();
}, []);
async function checkForUpdates() {
setChecking(true);
setError(null);
try {
const info = await updater.CheckForUpdate();
setUpdateInfo(info);
} catch (err) {
setError(err.message);
} finally {
setChecking(false);
}
}
async function downloadAndInstall() {
setDownloading(true);
setError(null);
try {
await updater.DownloadAndApply();
// App will restart automatically
} catch (err) {
setError(err.message);
setDownloading(false);
}
}
if (checking) {
return <div>Checking for updates...</div>;
}
if (error) {
return (
<div>
<p>Error: {error}</p>
<button onClick={checkForUpdates}>Retry</button>
</div>
);
}
if (!updateInfo) {
return (
<div>
<p>You're up to date! (v{updater.GetCurrentVersion()})</p>
<button onClick={checkForUpdates}>Check Again</button>
</div>
);
}
if (downloading) {
return (
<div>
<p>Downloading update...</p>
{progress && (
<div>
<progress value={progress.percentage} max={100} />
<p>{progress.percentage.toFixed(1)}%</p>
<p>{(progress.bytesPerSecond / 1024 / 1024).toFixed(2)} MB/s</p>
</div>
)}
</div>
);
}
return (
<div>
<h3>Update Available!</h3>
<p>Version {updateInfo.version} is available</p>
<p>Size: {updateInfo.hasPatch
? `${(updateInfo.patchSize / 1024).toFixed(0)} KB (patch)`
: `${(updateInfo.size / 1024 / 1024).toFixed(1)} MB`}
</p>
<div dangerouslySetInnerHTML={{ __html: updateInfo.releaseNotes }} />
<button onClick={downloadAndInstall}>Download & Install</button>
<button onClick={() => setUpdateInfo(null)}>Skip</button>
</div>
);
}
```
## Update Strategies
### Check on Startup
```go
func (a *App) OnStartup(ctx context.Context) {
// Check for updates after a short delay
go func() {
time.Sleep(5 * time.Second)
info, err := a.updater.CheckForUpdate()
if err == nil && info != nil {
// Emit event to frontend
application.Get().EmitEvent("update-available", info)
}
}()
}
```
### Background Checking
Configure automatic background checks:
```go
updater, _ := application.CreateUpdaterService(
"1.0.0",
application.WithUpdateURL("https://updates.example.com/myapp/"),
application.WithCheckInterval(6 * time.Hour), // Check every 6 hours
)
```
### Manual Check Menu Item
```go
menu := application.NewMenu()
menu.Add("Check for Updates...").OnClick(func(ctx *application.Context) {
info, err := updater.CheckForUpdate()
if err != nil {
application.InfoDialog().SetMessage("Error checking for updates").Show()
return
}
if info == nil {
application.InfoDialog().SetMessage("You're up to date!").Show()
return
}
// Show update dialog...
})
```
## Delta Updates (Patches)
Delta updates (patches) allow users to download only the changes between versions, dramatically reducing download sizes.
### How It Works
1. When building a new version, generate patches from previous versions
2. Host patches alongside full updates on your server
3. The updater automatically downloads patches when available
4. If patching fails, it falls back to the full download
### Generating Patches
Patches are generated using the bsdiff algorithm. You'll need the `bsdiff` tool:
```bash
# Install bsdiff (macOS)
brew install bsdiff
# Install bsdiff (Ubuntu/Debian)
sudo apt-get install bsdiff
# Generate a patch
bsdiff old-binary new-binary patch.bsdiff
```
### Patch File Naming
Organize your patches in your update directory:
```
updates/
├── update.json
├── myapp-1.2.0-macos-arm64.tar.gz
├── myapp-1.2.0-windows-amd64.zip
└── patches/
├── 1.0.0-to-1.2.0-macos-arm64.patch
├── 1.1.0-to-1.2.0-macos-arm64.patch
├── 1.0.0-to-1.2.0-windows-amd64.patch
└── 1.1.0-to-1.2.0-windows-amd64.patch
```
<Aside type="tip">
Keep patches from the last few versions. Users on very old versions will automatically download the full update.
</Aside>
## Hosting Updates
### Static File Hosting
Updates can be hosted on any static file server:
- **Amazon S3** / **Cloudflare R2**
- **Google Cloud Storage**
- **GitHub Releases**
- **Any CDN or web server**
Example S3 bucket structure:
```
s3://my-updates-bucket/myapp/
├── stable/
│ ├── update.json
│ ├── myapp-1.2.0-macos-arm64.tar.gz
│ └── patches/
│ └── 1.1.0-to-1.2.0-macos-arm64.patch
└── beta/
└── update.json
```
### CORS Configuration
If hosting on a different domain, configure CORS:
```json
{
"CORSRules": [
{
"AllowedOrigins": ["*"],
"AllowedMethods": ["GET"],
"AllowedHeaders": ["*"]
}
]
}
```
## Security
### Checksum Verification
All downloads are verified against SHA256 checksums in the manifest:
```json
{
"checksum": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
}
```
### Signature Verification
For additional security, enable signature verification:
<Steps>
1. **Generate a key pair**:
```bash
# Generate Ed25519 key pair
openssl genpkey -algorithm Ed25519 -out private.pem
openssl pkey -in private.pem -pubout -out public.pem
```
2. **Sign your update manifest**:
```bash
openssl pkeyutl -sign -inkey private.pem -in update.json -out update.json.sig
```
3. **Configure the updater**:
```go
updater, _ := application.CreateUpdaterService(
"1.0.0",
application.WithUpdateURL("https://updates.example.com/myapp/"),
application.WithRequireSignature(true),
application.WithPublicKey("MCowBQYDK2VwAyEA..."), // Base64-encoded public key
)
```
</Steps>
## Best Practices
### Do
- Test updates thoroughly before deploying
- Keep previous versions available for rollback
- Show release notes to users
- Allow users to skip non-mandatory updates
- Use HTTPS for all downloads
- Verify checksums before applying updates
- Handle errors gracefully
### Don't
- Force immediate restarts without warning
- Skip checksum verification
- Interrupt users during important work
- Delete the previous version immediately
- Ignore update failures
## Troubleshooting
### Update Not Found
- Verify the manifest URL is correct
- Check the platform key matches (e.g., `macos-arm64`)
- Ensure the version in the manifest is newer
### Download Fails
- Check network connectivity
- Verify the download URL is accessible
- Check CORS configuration if cross-origin
### Patch Fails
- The updater automatically falls back to full download
- Ensure `bspatch` is available on the system
- Verify the patch checksum is correct
### Application Won't Restart
- On macOS, ensure the app is properly code-signed
- On Windows, check for file locks
- On Linux, verify file permissions
## Next Steps
- [Code Signing](/guides/build/signing) - Sign your updates
- [Creating Installers](/guides/installers) - Package your application
- [CI/CD Integration](/guides/ci-cd) - Automate your release process

View file

@ -1,138 +0,0 @@
---
title: MSIX Packaging (Windows)
description: Guide for creating MSIX packages with Wails v3
---
# MSIX Packaging (Windows)
Wails v3 can generate modern **MSIX** installers for Windows applications, providing a cleaner, safer and Store-ready alternative to traditional **NSIS** or plain `.exe` bundles.
This guide walks through:
* Prerequisites & tool installation
* Building your app as an MSIX package
* Signing the package
* Command-line reference
* Troubleshooting
---
## 1. Prerequisites
| Requirement | Notes |
|-------------|-------|
| **Windows 10 1809+ / Windows 11** | MSIX is only supported on Windows. |
| **Windows SDK** (for `MakeAppx.exe` & `signtool.exe`) | Install from the [Windows SDK download page](https://developer.microsoft.com/windows/downloads/windows-sdk/). |
| **Microsoft MSIX Packaging Tool** (optional) | Available from the Microsoft Store provides a GUI & CLI. |
| **Code-signing certificate** (recommended) | A `.pfx` file generated by your CA or `New-SelfSignedCertificate`. |
> **Tip:** Wails ships a Task that opens the download pages for you:
```bash
# installs MakeAppx / signtool (via Windows SDK) and the MSIX Packaging Tool
wails3 task windows:install:msix:tools
```
---
## 2. Building an MSIX package
### 2.1 Quick CLI
```bash
# Production build + MSIX
wails3 tool msix \
--name MyApp \ # executable name
--executable build/bin/MyApp.exe \
--arch x64 \ # x64, x86 or arm64
--out build/bin/MyApp-x64.msix
```
The command will:
1. Create a temporary layout (`AppxManifest.xml`, `Assets/`).
2. Call **MakeAppx.exe** (default) or **MsixPackagingTool.exe** if `--use-msix-tool` is passed.
3. Optionally sign the package (see §3).
### 2.2 Using the generated Taskfile
When you ran `wails init`, Wails created `build/windows/Taskfile.yml`.
Packaging with MSIX is a one-liner:
```bash
# default=nsis, override FORMAT
wails3 task windows:package FORMAT=msix
```
Output goes to `build/bin/MyApp-<arch>.msix`.
---
## 3. Signing the package
Windows will refuse unsigned MSIX packages unless you enable developer-mode, so signing is strongly recommended.
```bash
wails3 tool msix \
--cert build/cert/CodeSign.pfx \
--cert-password "pfx-password" \
--publisher "CN=MyCompany" \
--out build/bin/MyApp.msix
```
* If you pass `--cert`, Wails automatically runs `signtool sign …`.
* `--publisher` sets the `Publisher` field inside `AppxManifest.xml`.
It **must** match the subject of your certificate.
---
## 4. Command-line reference
| Flag | Default | Description |
|------|---------|-------------|
| `--config` | `wails.json` | Project config with **Info** & `fileAssociations`. |
| `--name` | — | Executable name inside the package (no spaces). |
| `--executable` | — | Path to the built `.exe`. |
| `--arch` | `x64` | `x64`, `x86`, or `arm64`. |
| `--out` | `<name>.msix` | Output path / filename. |
| `--publisher` | `CN=<CompanyName>` | Publisher string in the manifest. |
| `--cert` | ― | Path to `.pfx` certificate for signing. |
| `--cert-password` | ― | Password for the `.pfx`. |
| `--use-msix-tool` | `false` | Use **MsixPackagingTool.exe** instead of **MakeAppx.exe**. |
| `--use-makeappx` | `true` | Force MakeAppx even if the MSIX Tool is installed. |
---
## 5. File associations
Wails automatically injects file associations declared in `wails.json` into the package manifest:
```json
"fileAssociations": [
{ "ext": "wails", "name": "Wails Project", "description": "Wails file", "role": "Editor" }
]
```
After installation, Windows will offer your app as a handler for these extensions.
---
## 6. Troubleshooting
| Problem | Solution |
|---------|----------|
| `MakeAppx.exe not found` | Install the Windows SDK and restart the terminal. |
| `signtool.exe not found` | Same as above both live in the SDKs *bin* folder. |
| *Package cannot be installed because publisher mismatch* | The certificate subject (CN) must match `--publisher`. |
| *The certificate is not trusted* | Import the certificate into **Trusted Root Certification Authorities** or use a publicly trusted code-signing cert. |
| Need GUI | Install **MSIX Packaging Tool** from the store and run `MsixPackagingTool.exe`. The template generated by Wails is fully compatible. |
---
## 7. Next steps
* [Windows Installer (NSIS) guide](./windows-installer.mdx) legacy format.
* [Cross-platform update mechanism](../updates.mdx) coming soon.
* Join the community on Discord to share feedback!
Happy packaging!

View file

@ -1,228 +0,0 @@
---
title: Code Signing
description: Guide for signing your Wails applications on macOS and Windows
sidebar:
order: 4
---
import { Tabs, TabItem } from '@astrojs/starlight/components';
import { Steps } from '@astrojs/starlight/components';
import { Card, CardGrid } from '@astrojs/starlight/components';
# Code Signing Your Application
This guide covers how to sign your Wails applications for both macOS and Windows, with a focus on automated signing using GitHub Actions.
<CardGrid>
<Card title="Windows Signing" icon="windows">
Sign your Windows executables with certificates
</Card>
<Card title="macOS Signing" icon="apple">
Sign and notarize your macOS applications
</Card>
</CardGrid>
## Windows Code Signing
<Steps>
1. **Obtain a Code Signing Certificate**
- Get from a trusted provider listed on [Microsoft's documentation](https://docs.microsoft.com/en-us/windows-hardware/drivers/dashboard/get-a-code-signing-certificate)
- Standard code signing certificate is sufficient (EV not required)
- Test signing locally before setting up CI
2. **Prepare for GitHub Actions**
- Convert your certificate to Base64
- Store in GitHub Secrets
- Set up signing workflow
3. **Configure GitHub Actions**
```yaml
name: Sign Windows Binary
on:
workflow_dispatch:
release:
types: [created]
jobs:
sign:
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
- name: Import Certificate
run: |
New-Item -ItemType directory -Path certificate
Set-Content -Path certificate\certificate.txt -Value ${{ secrets.WINDOWS_CERTIFICATE }}
certutil -decode certificate\certificate.txt certificate\certificate.pfx
- name: Sign Binary
run: |
& 'C:\Program Files (x86)\Windows Kits\10\bin\10.0.17763.0\x86\signtool.exe' sign /f certificate\certificate.pfx /t http://timestamp.sectigo.com /p ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }} /v /fd sha256 .\build\bin\app.exe
```
</Steps>
### Important Windows Parameters
- **Signing Algorithm**: Usually `sha256`
- **Timestamp Server**: Valid timestamping server URL
- **Certificate Password**: Stored in GitHub Secrets
- **Binary Path**: Path to your compiled executable
## macOS Code Signing
<Steps>
1. **Prerequisites**
- Apple Developer Account
- Developer ID Certificate
- App Store Connect API Key
- [gon](https://github.com/mitchellh/gon) for notarization
2. **Certificate Setup**
- Generate Developer ID Certificate
- Download and install certificate
- Export certificate for CI
3. **Configure Notarization**
```json title="gon-sign.json"
{
"source": ["./build/bin/app"],
"bundle_id": "com.company.app",
"apple_id": {
"username": "dev@company.com",
"password": "@env:AC_PASSWORD"
},
"sign": {
"application_identity": "Developer ID Application: Company Name"
}
}
```
4. **GitHub Actions Configuration**
```yaml
name: Sign macOS Binary
on:
workflow_dispatch:
release:
types: [created]
jobs:
sign:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Import Certificate
env:
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_PWD }}
run: |
echo $MACOS_CERTIFICATE | base64 --decode > certificate.p12
security create-keychain -p "" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "" build.keychain
security import certificate.p12 -k build.keychain -P $MACOS_CERTIFICATE_PWD -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "" build.keychain
- name: Sign and Notarize
env:
AC_USERNAME: ${{ secrets.AC_USERNAME }}
AC_PASSWORD: ${{ secrets.AC_PASSWORD }}
run: |
gon -log-level=info ./build/darwin/gon-sign.json
```
</Steps>
### Important macOS Parameters
- **Bundle ID**: Unique identifier for your app
- **Developer ID**: Your Developer ID Application certificate
- **Apple ID**: Developer account credentials
- **ASC API Key**: App Store Connect API credentials
## Best Practices
1. **Security**
- Store all credentials in GitHub Secrets
- Use environment variables for sensitive data
- Regularly rotate certificates and credentials
2. **Workflow**
- Test signing locally first
- Use conditional signing based on platform
- Implement proper error handling
3. **Verification**
- Verify signatures after signing
- Test notarization process
- Check timestamp validity
## Troubleshooting
### Windows Issues
- Certificate not found
- Invalid timestamp server
- Signing tool errors
### macOS Issues
- Keychain access issues
- Notarization failures
- Certificate validation errors
## Complete GitHub Actions Workflow
```yaml
name: Sign Binaries
on:
workflow_dispatch:
release:
types: [created]
jobs:
sign:
strategy:
matrix:
platform: [windows-latest, macos-latest]
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v3
# Windows Signing
- name: Sign Windows Binary
if: matrix.platform == 'windows-latest'
env:
CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
run: |
New-Item -ItemType directory -Path certificate
Set-Content -Path certificate\certificate.txt -Value $env:CERTIFICATE
certutil -decode certificate\certificate.txt certificate\certificate.pfx
& 'C:\Program Files (x86)\Windows Kits\10\bin\10.0.17763.0\x86\signtool.exe' sign /f certificate\certificate.pfx /t http://timestamp.sectigo.com /p $env:CERTIFICATE_PASSWORD /v /fd sha256 .\build\bin\app.exe
# macOS Signing
- name: Sign macOS Binary
if: matrix.platform == 'macos-latest'
env:
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_PWD }}
AC_USERNAME: ${{ secrets.AC_USERNAME }}
AC_PASSWORD: ${{ secrets.AC_PASSWORD }}
run: |
echo $MACOS_CERTIFICATE | base64 --decode > certificate.p12
security create-keychain -p "" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "" build.keychain
security import certificate.p12 -k build.keychain -P $MACOS_CERTIFICATE_PWD -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "" build.keychain
gon -log-level=info ./build/darwin/gon-sign.json
```
## Additional Resources
- [Apple Code Signing Documentation](https://developer.apple.com/support/code-signing/)
- [Microsoft Code Signing Documentation](https://docs.microsoft.com/en-us/windows-hardware/drivers/dashboard/get-a-code-signing-certificate)
- [Gon Documentation](https://github.com/mitchellh/gon)
- [GitHub Actions Documentation](https://docs.github.com/en/actions)

View file

@ -0,0 +1,501 @@
---
title: Updater API
description: Reference documentation for the Wails v3 Updater API
sidebar:
order: 8
---
import { Tabs, TabItem } from '@astrojs/starlight/components';
# Updater API Reference
The Updater API provides automatic update functionality for Wails applications, including update checking, downloading, and installation.
## Go API
### CreateUpdaterService
Creates a new updater service.
```go
func CreateUpdaterService(currentVersion string, opts ...UpdaterOption) (*UpdaterService, error)
```
**Parameters:**
- `currentVersion` - The current version of your application (e.g., "1.0.0")
- `opts` - Configuration options
**Returns:**
- `*UpdaterService` - The updater service instance
- `error` - Error if creation fails
**Example:**
```go
updater, err := application.CreateUpdaterService(
"1.0.0",
application.WithUpdateURL("https://updates.example.com/myapp/"),
)
```
### UpdaterOption Functions
#### WithUpdateURL
Sets the base URL for update manifests.
```go
func WithUpdateURL(url string) UpdaterOption
```
**Required.** The URL should point to the directory containing your `update.json` manifest.
#### WithCheckInterval
Sets the interval for automatic background update checks.
```go
func WithCheckInterval(interval time.Duration) UpdaterOption
```
Set to `0` to disable automatic checking. Default is `0` (disabled).
#### WithAllowPrerelease
Allows pre-release versions to be considered as updates.
```go
func WithAllowPrerelease(allow bool) UpdaterOption
```
Default is `false`.
#### WithChannel
Sets the update channel (e.g., "stable", "beta", "canary").
```go
func WithChannel(channel string) UpdaterOption
```
The channel is appended to the update URL path.
#### WithPublicKey
Sets the public key for signature verification.
```go
func WithPublicKey(key string) UpdaterOption
```
The key should be a base64-encoded Ed25519 public key.
#### WithRequireSignature
Requires all updates to be signed.
```go
func WithRequireSignature(require bool) UpdaterOption
```
Default is `false`.
### UpdaterService Methods
#### GetCurrentVersion
Returns the current application version.
```go
func (u *UpdaterService) GetCurrentVersion() string
```
#### CheckForUpdate
Checks if a new version is available.
```go
func (u *UpdaterService) CheckForUpdate() (*UpdateInfo, error)
```
**Returns:**
- `*UpdateInfo` - Update information, or `nil` if no update is available
- `error` - Error if the check fails
#### DownloadUpdate
Downloads the available update.
```go
func (u *UpdaterService) DownloadUpdate() error
```
Emits `updater:progress` events during download.
#### ApplyUpdate
Applies the downloaded update.
```go
func (u *UpdaterService) ApplyUpdate() error
```
This will restart the application.
#### DownloadAndApply
Downloads and applies an update in one call.
```go
func (u *UpdaterService) DownloadAndApply() error
```
#### GetState
Returns the current updater state.
```go
func (u *UpdaterService) GetState() string
```
**Possible values:**
- `"idle"` - No update in progress
- `"checking"` - Checking for updates
- `"available"` - Update available
- `"downloading"` - Downloading update
- `"ready"` - Update downloaded, ready to install
- `"installing"` - Installing update
- `"error"` - Error occurred
#### GetUpdateInfo
Returns information about the available update.
```go
func (u *UpdaterService) GetUpdateInfo() *UpdateInfo
```
#### GetLastError
Returns the last error message.
```go
func (u *UpdaterService) GetLastError() string
```
#### Reset
Resets the updater state.
```go
func (u *UpdaterService) Reset()
```
### UpdateInfo
Information about an available update.
```go
type UpdateInfo struct {
Version string `json:"version"`
ReleaseDate time.Time `json:"releaseDate"`
ReleaseNotes string `json:"releaseNotes"`
Size int64 `json:"size"`
PatchSize int64 `json:"patchSize,omitempty"`
Mandatory bool `json:"mandatory"`
HasPatch bool `json:"hasPatch"`
}
```
| Field | Type | Description |
|-------|------|-------------|
| `Version` | `string` | The new version number |
| `ReleaseDate` | `time.Time` | When this version was released |
| `ReleaseNotes` | `string` | Release notes (markdown) |
| `Size` | `int64` | Full download size in bytes |
| `PatchSize` | `int64` | Patch size in bytes (if available) |
| `Mandatory` | `bool` | Whether this update is required |
| `HasPatch` | `bool` | Whether a patch is available |
### UpdaterConfig
Configuration for the updater.
```go
type UpdaterConfig struct {
UpdateURL string
CheckInterval time.Duration
AllowPrerelease bool
PublicKey string
RequireSignature bool
Channel string
}
```
## JavaScript/TypeScript API
The updater service is automatically bound to the frontend when added as a service.
### Types
```typescript
interface UpdateInfo {
version: string;
releaseDate: string; // ISO 8601 date string
releaseNotes: string;
size: number;
patchSize?: number;
mandatory: boolean;
hasPatch: boolean;
}
interface DownloadProgress {
downloaded: number;
total: number;
percentage: number;
bytesPerSecond: number;
}
```
### Methods
#### GetCurrentVersion
```typescript
function GetCurrentVersion(): string
```
Returns the current application version.
#### CheckForUpdate
```typescript
function CheckForUpdate(): Promise<UpdateInfo | null>
```
Checks for available updates.
**Returns:** `UpdateInfo` if an update is available, `null` otherwise.
#### DownloadUpdate
```typescript
function DownloadUpdate(): Promise<void>
```
Downloads the available update. Emits `updater:progress` events.
#### ApplyUpdate
```typescript
function ApplyUpdate(): Promise<void>
```
Applies the downloaded update. The application will restart.
#### DownloadAndApply
```typescript
function DownloadAndApply(): Promise<void>
```
Downloads and applies the update in one call.
#### GetState
```typescript
function GetState(): string
```
Returns the current updater state.
#### GetUpdateInfo
```typescript
function GetUpdateInfo(): UpdateInfo | null
```
Returns the current update information.
#### GetLastError
```typescript
function GetLastError(): string
```
Returns the last error message.
#### Reset
```typescript
function Reset(): void
```
Resets the updater state.
### Events
#### updater:progress
Emitted during download with progress information.
```typescript
import { Events } from '@wailsio/runtime';
Events.On('updater:progress', (data: DownloadProgress) => {
console.log(`${data.percentage}% downloaded`);
});
```
## Update Manifest Format
The update manifest (`update.json`) must be hosted at your update URL.
### Schema
```typescript
interface Manifest {
version: string;
release_date: string; // ISO 8601
release_notes?: string;
platforms: {
[platform: string]: PlatformUpdate;
};
minimum_version?: string;
mandatory?: boolean;
}
interface PlatformUpdate {
url: string;
size: number;
checksum: string; // "sha256:..."
signature?: string;
patches?: PatchInfo[];
}
interface PatchInfo {
from: string; // Version this patch applies from
url: string;
size: number;
checksum: string;
signature?: string;
}
```
### Platform Keys
| Platform | Key |
|----------|-----|
| macOS (Apple Silicon) | `macos-arm64` |
| macOS (Intel) | `macos-amd64` |
| Windows (64-bit) | `windows-amd64` |
| Linux (64-bit) | `linux-amd64` |
| Linux (ARM64) | `linux-arm64` |
### Example Manifest
```json
{
"version": "1.2.0",
"release_date": "2025-01-15T00:00:00Z",
"release_notes": "## Changes\n\n- New feature\n- Bug fixes",
"platforms": {
"macos-arm64": {
"url": "https://example.com/app-1.2.0-macos-arm64.tar.gz",
"size": 12582912,
"checksum": "sha256:abc123def456...",
"patches": [
{
"from": "1.1.0",
"url": "https://example.com/patches/1.1.0-to-1.2.0.patch",
"size": 14336,
"checksum": "sha256:789xyz..."
}
]
},
"windows-amd64": {
"url": "https://example.com/app-1.2.0-windows.zip",
"size": 14680064,
"checksum": "sha256:ghi789jkl012..."
}
},
"minimum_version": "1.0.0",
"mandatory": false
}
```
## Service Integration
### Registering the Service
```go
app := application.New(application.Options{
Name: "MyApp",
Services: []application.Service{
application.NewService(updater),
},
})
```
### Service Lifecycle
The updater service implements the `Service` interface:
```go
type Service interface {
ServiceName() string
ServiceStartup(ctx context.Context, options ServiceOptions) error
ServiceShutdown() error
}
```
- **ServiceStartup**: Starts background update checking if configured
- **ServiceShutdown**: Stops background checking
### Event Handlers
Register handlers in Go:
```go
updater.OnUpdateAvailable(func(info *UpdateInfo) {
log.Printf("Update available: %s", info.Version)
})
updater.OnDownloadProgress(func(downloaded, total int64, percentage float64) {
log.Printf("Download: %.1f%%", percentage)
})
updater.OnError(func(err string) {
log.Printf("Update error: %s", err)
})
```
## Security Considerations
### HTTPS
Always use HTTPS for update URLs to prevent man-in-the-middle attacks.
### Checksum Verification
All downloads are verified against SHA256 checksums before installation.
### Signature Verification
Enable signature verification for production applications:
```go
updater, _ := application.CreateUpdaterService(
"1.0.0",
application.WithUpdateURL("https://updates.example.com/myapp/"),
application.WithRequireSignature(true),
application.WithPublicKey("MCowBQYDK2VwAyEA..."),
)
```
### Code Signing
Ensure your application binaries are properly code-signed:
- macOS: Developer ID Application certificate
- Windows: Code signing certificate with timestamp
See [Code Signing Guide](/guides/build/signing) for details.

View file

@ -0,0 +1,532 @@
---
title: "Tutorial: Distributing Your App"
description: Learn how to build, sign, and distribute your Wails application with automatic updates
sidebar:
order: 5
---
import { Steps } from '@astrojs/starlight/components';
import { Tabs, TabItem } from '@astrojs/starlight/components';
import { Aside } from '@astrojs/starlight/components';
# Distributing Your Wails Application
This tutorial walks you through the complete process of building, signing, and distributing a Wails application with automatic updates. By the end, you'll have a production-ready app that can update itself.
## What You'll Learn
- Building production binaries for multiple platforms
- Code signing for macOS and Windows
- Setting up an update server
- Implementing automatic updates in your app
- Creating a release workflow
## Prerequisites
- A working Wails v3 application
- For macOS: Apple Developer Account and Developer ID certificate
- For Windows: A code signing certificate (optional but recommended)
- A place to host files (S3, GitHub Releases, or any web server)
## Step 1: Prepare Your Application
First, let's configure your app for distribution.
### Update your build configuration
Edit `build/config.yml`:
```yaml title="build/config.yml"
version: '3'
info:
companyName: "Your Company"
productName: "My App"
productIdentifier: "com.yourcompany.myapp"
description: "An awesome desktop application"
copyright: "(c) 2025 Your Company"
version: "1.0.0" # Update this for each release
# Optional: Configure signing
signing:
macos:
identity: "Developer ID Application: Your Company (TEAMID)"
entitlements: "build/darwin/entitlements.plist"
hardened_runtime: true
```
### Add version tracking
Create a version file that your app can read:
```go title="version.go"
package main
// Version is set at build time
var Version = "1.0.0"
// Set via: go build -ldflags="-X main.Version=1.0.0"
```
## Step 2: Add the Updater Service
Now let's integrate automatic updates.
### Create the updater
```go title="main.go"
package main
import (
"log"
"time"
"github.com/wailsapp/wails/v3/pkg/application"
)
func main() {
// Create the updater service
updater, err := application.CreateUpdaterService(
Version, // Use build-time version
application.WithUpdateURL("https://updates.yourcompany.com/myapp/"),
application.WithCheckInterval(6 * time.Hour),
)
if err != nil {
log.Fatal(err)
}
app := application.New(application.Options{
Name: "My App",
Version: Version,
Services: []application.Service{
application.NewService(updater),
},
})
// ... configure windows, menus, etc.
if err := app.Run(); err != nil {
log.Fatal(err)
}
}
```
### Add update UI
Create a simple update checker component:
```tsx title="frontend/src/UpdateChecker.tsx"
import { useState, useEffect } from 'react';
import { updater } from './bindings/main';
import { Events } from '@wailsio/runtime';
export function UpdateChecker() {
const [update, setUpdate] = useState<any>(null);
const [downloading, setDownloading] = useState(false);
const [progress, setProgress] = useState(0);
useEffect(() => {
// Check for updates on mount
checkForUpdates();
// Listen for progress
const cleanup = Events.On('updater:progress', (data: any) => {
setProgress(data.percentage);
});
return () => cleanup();
}, []);
async function checkForUpdates() {
const info = await updater.CheckForUpdate();
setUpdate(info);
}
async function installUpdate() {
setDownloading(true);
try {
await updater.DownloadAndApply();
} catch (err) {
console.error(err);
setDownloading(false);
}
}
if (!update) return null;
return (
<div className="update-banner">
{downloading ? (
<p>Downloading update... {progress.toFixed(0)}%</p>
) : (
<>
<p>Version {update.version} is available!</p>
<button onClick={installUpdate}>Update Now</button>
</>
)}
</div>
);
}
```
## Step 3: Build for Production
<Tabs>
<TabItem label="macOS">
### Build and sign for macOS
```bash
# Build with production flags
wails3 task package:signed
# Or build with notarization (recommended)
wails3 task package:notarize KEYCHAIN_PROFILE="my-notarize-profile"
```
The output will be in `bin/MyApp.app`.
### Create a DMG for distribution
```bash
# Install create-dmg if needed
brew install create-dmg
# Create DMG
create-dmg \
--volname "My App" \
--window-pos 200 120 \
--window-size 600 400 \
--icon-size 100 \
--icon "My App.app" 150 185 \
--app-drop-link 450 185 \
"MyApp-1.0.0-macos.dmg" \
"bin/MyApp.app"
```
</TabItem>
<TabItem label="Windows">
### Build for Windows
```bash
# Build production binary
wails3 build -production
# Sign the executable (if you have a certificate)
signtool sign /f certificate.pfx /p "password" /fd SHA256 /t http://timestamp.digicert.com bin\MyApp.exe
```
### Create an installer
Use the built-in NSIS task:
```bash
wails3 task package
```
Or create an MSIX package:
```bash
wails3 tool msix --certificate-path certificate.pfx --certificate-password "password"
```
</TabItem>
<TabItem label="Linux">
### Build for Linux
```bash
# Build production binary
wails3 build -production
```
### Create an AppImage
```bash
wails3 generate appimage
```
### Create distribution packages
```bash
# Create DEB package
wails3 tool package --format deb
# Create RPM package
wails3 tool package --format rpm
```
</TabItem>
</Tabs>
## Step 4: Create Update Archives
For the updater, you need to create update archives:
<Tabs>
<TabItem label="macOS">
```bash
# Create update archive
cd bin
tar -czvf myapp-1.0.0-macos-arm64.tar.gz MyApp.app
# Calculate checksum
shasum -a 256 myapp-1.0.0-macos-arm64.tar.gz
```
</TabItem>
<TabItem label="Windows">
```bash
# Create update archive
cd bin
zip -r myapp-1.0.0-windows-amd64.zip MyApp.exe
# Calculate checksum
certutil -hashfile myapp-1.0.0-windows-amd64.zip SHA256
```
</TabItem>
<TabItem label="Linux">
```bash
# Create update archive
cd bin
tar -czvf myapp-1.0.0-linux-amd64.tar.gz myapp
# Calculate checksum
sha256sum myapp-1.0.0-linux-amd64.tar.gz
```
</TabItem>
</Tabs>
## Step 5: Create the Update Manifest
Create an `update.json` file:
```json title="update.json"
{
"version": "1.0.0",
"release_date": "2025-01-15T00:00:00Z",
"release_notes": "## Initial Release\n\n- Feature one\n- Feature two",
"platforms": {
"macos-arm64": {
"url": "https://updates.yourcompany.com/myapp/myapp-1.0.0-macos-arm64.tar.gz",
"size": 12582912,
"checksum": "sha256:YOUR_CHECKSUM_HERE"
},
"macos-amd64": {
"url": "https://updates.yourcompany.com/myapp/myapp-1.0.0-macos-amd64.tar.gz",
"size": 13107200,
"checksum": "sha256:YOUR_CHECKSUM_HERE"
},
"windows-amd64": {
"url": "https://updates.yourcompany.com/myapp/myapp-1.0.0-windows-amd64.zip",
"size": 14680064,
"checksum": "sha256:YOUR_CHECKSUM_HERE"
},
"linux-amd64": {
"url": "https://updates.yourcompany.com/myapp/myapp-1.0.0-linux-amd64.tar.gz",
"size": 11534336,
"checksum": "sha256:YOUR_CHECKSUM_HERE"
}
}
}
```
## Step 6: Set Up Hosting
### Option A: GitHub Releases
Upload your files as release assets:
1. Create a new release in your repository
2. Upload the update archives and manifest
3. Use raw URLs for the update manifest
```go
application.WithUpdateURL("https://github.com/yourname/myapp/releases/latest/download/")
```
### Option B: S3 or Cloud Storage
<Steps>
1. Create a bucket for your updates
2. Upload files:
```bash
aws s3 cp update.json s3://my-updates-bucket/myapp/
aws s3 cp myapp-1.0.0-macos-arm64.tar.gz s3://my-updates-bucket/myapp/
# ... upload other platforms
```
3. Configure public access or CloudFront distribution
</Steps>
### Option C: Any Web Server
Simply serve the files from any web server. Ensure CORS is configured if serving from a different domain.
## Step 7: Automate with GitHub Actions
Create a release workflow:
```yaml title=".github/workflows/release.yml"
name: Release
on:
push:
tags:
- 'v*'
jobs:
build:
strategy:
matrix:
include:
- os: macos-latest
platform: macos
arch: arm64
- os: macos-latest
platform: macos
arch: amd64
- os: windows-latest
platform: windows
arch: amd64
- os: ubuntu-latest
platform: linux
arch: amd64
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.23'
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install Wails
run: go install github.com/wailsapp/wails/v3/cmd/wails@latest
- name: Build
run: wails3 build -production
env:
GOARCH: ${{ matrix.arch }}
# macOS signing
- name: Sign macOS
if: matrix.platform == 'macos'
env:
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
run: |
# Import certificate
echo $MACOS_CERTIFICATE | base64 --decode > certificate.p12
security create-keychain -p "" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "" build.keychain
security import certificate.p12 -k build.keychain -P "$MACOS_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "" build.keychain
# Sign the app bundle
wails3 task darwin:sign
# Create archive
- name: Create Archive
run: |
cd bin
if [ "${{ matrix.platform }}" = "windows" ]; then
zip -r ../myapp-${{ github.ref_name }}-${{ matrix.platform }}-${{ matrix.arch }}.zip .
else
tar -czvf ../myapp-${{ github.ref_name }}-${{ matrix.platform }}-${{ matrix.arch }}.tar.gz .
fi
shell: bash
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: myapp-${{ matrix.platform }}-${{ matrix.arch }}
path: myapp-*
release:
needs: build
runs-on: ubuntu-latest
steps:
- name: Download Artifacts
uses: actions/download-artifact@v4
- name: Create Release
uses: softprops/action-gh-release@v1
with:
files: |
myapp-*/myapp-*
```
## Step 8: Release a New Version
When you're ready to release:
<Steps>
1. Update the version in `build/config.yml`
2. Update the `Version` variable in your code
3. Commit and tag:
```bash
git add .
git commit -m "Release v1.1.0"
git tag v1.1.0
git push origin main --tags
```
4. Wait for CI to build and create the release
5. Update your `update.json` manifest with new checksums
6. Upload the new manifest to your update server
</Steps>
## Testing Updates
Before releasing to production:
1. Build version 1.0.0 and install it
2. Build version 1.1.0 and create the update files
3. Host the update files locally:
```bash
python -m http.server 8080
```
4. Update your app to check `http://localhost:8080/`
5. Verify the update process works correctly
## Troubleshooting
### Update not detected
- Check the manifest URL is correct
- Verify the version in the manifest is newer than the installed version
- Check browser console for network errors
### Download fails
- Verify all URLs in the manifest are accessible
- Check CORS headers if hosting on a different domain
- Verify checksums are correct
### Installation fails
- On macOS, ensure proper code signing
- On Windows, check for file locks
- Check application logs for error details
## Next Steps
Congratulations! You now have a fully distributable Wails application with automatic updates.
- [Code Signing Guide](/guides/build/signing) - Learn more about code signing
- [Auto-Updates Guide](/guides/distribution/auto-updates) - Advanced update configurations
- [Creating Installers](/guides/installers) - More installer options

View file

@ -93,6 +93,34 @@ func main() {
tool.NewSubCommandFunction("buildinfo", "Show Build Info", commands.BuildInfo)
tool.NewSubCommandFunction("package", "Generate Linux packages (deb, rpm, archlinux)", commands.ToolPackage)
tool.NewSubCommandFunction("version", "Bump semantic version", commands.ToolVersion)
tool.NewSubCommandFunction("lipo", "Create macOS universal binary from multiple architectures", commands.ToolLipo)
// Low-level sign tool (used by Taskfiles)
toolSign := tool.NewSubCommand("sign", "Sign a binary or package directly")
var toolSignFlags flags.Sign
toolSign.AddFlags(&toolSignFlags)
toolSign.Action(func() error {
return commands.Sign(&toolSignFlags)
})
// Setup commands
setup := app.NewSubCommand("setup", "Project setup wizards")
setupSigning := setup.NewSubCommand("signing", "Configure code signing")
var setupSigningFlags flags.SigningSetup
setupSigning.AddFlags(&setupSigningFlags)
setupSigning.Action(func() error {
return commands.SigningSetup(&setupSigningFlags)
})
setupEntitlements := setup.NewSubCommand("entitlements", "Configure macOS entitlements")
var setupEntitlementsFlags flags.EntitlementsSetup
setupEntitlements.AddFlags(&setupEntitlementsFlags)
setupEntitlements.Action(func() error {
return commands.EntitlementsSetup(&setupEntitlementsFlags)
})
// Sign command (wrapper that calls platform-specific tasks)
app.NewSubCommandFunction("sign", "Sign binaries and packages for current or specified platform", commands.SignWrapper)
app.NewSubCommandFunction("version", "Print the version", commands.Version)
app.NewSubCommand("sponsor", "Sponsor the project").Action(openSponsor)

View file

@ -9,6 +9,7 @@ require (
github.com/atterpac/refresh v0.8.6
github.com/bep/debounce v1.2.1
github.com/charmbracelet/glamour v0.9.0
github.com/charmbracelet/huh v0.8.0
github.com/ebitengine/purego v0.8.2
github.com/go-git/go-git/v5 v5.13.2
github.com/go-ole/go-ole v1.3.0
@ -19,6 +20,7 @@ require (
github.com/goreleaser/nfpm/v2 v2.41.3
github.com/jackmordaunt/icns/v2 v2.2.7
github.com/jaypipes/ghw v0.17.0
github.com/konoui/lipo v0.10.0
github.com/leaanthony/clir v1.7.0
github.com/leaanthony/go-ansi-parser v1.6.1
github.com/leaanthony/gosod v1.0.4
@ -37,7 +39,8 @@ require (
github.com/wailsapp/go-webview2 v1.0.22
github.com/wailsapp/mimetype v1.4.1
github.com/wailsapp/task/v3 v3.40.1-patched3
golang.org/x/sys v0.31.0
github.com/zalando/go-keyring v0.2.6
golang.org/x/sys v0.33.0
golang.org/x/term v0.30.0
golang.org/x/tools v0.31.0
gopkg.in/ini.v1 v1.67.0
@ -46,10 +49,21 @@ require (
)
require (
al.essio.dev/pkg/shellescape v1.5.1 // indirect
atomicgo.dev/schedule v0.1.0 // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/catppuccin/go v0.3.0 // indirect
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 // indirect
github.com/charmbracelet/bubbletea v1.3.6 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/danieljoos/wincred v1.2.2 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/konoui/go-qsort v0.1.0 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
)
@ -65,7 +79,7 @@ require (
github.com/Masterminds/semver/v3 v3.3.1 // indirect
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.1.6 // indirect
github.com/ProtonMail/go-crypto v1.1.6
github.com/StackExchange/wmi v1.2.1 // indirect
github.com/alecthomas/chroma/v2 v2.15.0 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
@ -74,7 +88,7 @@ require (
github.com/cavaliergopher/cpio v1.0.1 // indirect
github.com/chainguard-dev/git-urls v1.0.2 // indirect
github.com/charmbracelet/lipgloss v1.1.0 // indirect
github.com/charmbracelet/x/ansi v0.8.0 // indirect
github.com/charmbracelet/x/ansi v0.9.3 // indirect
github.com/cloudflare/circl v1.6.0 // indirect
github.com/containerd/console v1.0.4 // indirect
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
@ -141,7 +155,7 @@ require (
golang.org/x/image v0.24.0
golang.org/x/mod v0.24.0 // indirect
golang.org/x/net v0.37.0 // indirect
golang.org/x/sync v0.12.0 // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/text v0.23.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect

View file

@ -1,3 +1,5 @@
al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho=
al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=
atomicgo.dev/assert v0.0.2 h1:FiKeMiZSgRrZsPo9qn/7vmr7mCsh5SZyXY4YGYiYwrg=
atomicgo.dev/assert v0.0.2/go.mod h1:ut4NcI3QDdJtlmAxQULOmA13Gz6e2DWbSAS8RUOmNYQ=
atomicgo.dev/cursor v0.2.0 h1:H6XN5alUJ52FZZUkI7AlJbUc1aW38GWZalpYRPpoPOw=
@ -18,6 +20,8 @@ github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ=
github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
github.com/Ladicle/tabwriter v1.0.0 h1:DZQqPvMumBDwVNElso13afjYLNp0Z7pHqHnu0r4t9Dg=
github.com/Ladicle/tabwriter v1.0.0/go.mod h1:c4MdCjxQyTbGuQO/gvqJ+IA/89UEwrsD6hUCW98dyp4=
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs=
github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8=
github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII=
@ -59,12 +63,14 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuW
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/atterpac/refresh v0.8.6 h1:Q5miKV2qs9jW+USw8WZ/54Zz8/RSh/bOz5U6JvvDZmM=
github.com/atterpac/refresh v0.8.6/go.mod h1:fJpWySLdpbANS8Ej5OvfZVZIVvi/9bmnhTjKS5EjQes=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
@ -73,33 +79,53 @@ github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI=
github.com/caarlos0/testfs v0.4.4 h1:3PHvzHi5Lt+g332CiShwS8ogTgS3HjrmzZxCm6JCDr8=
github.com/caarlos0/testfs v0.4.4/go.mod h1:bRN55zgG4XCUVVHZCeU+/Tz1Q6AxEJOEJTliBy+1DMk=
github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
github.com/cavaliergopher/cpio v1.0.1 h1:KQFSeKmZhv0cr+kawA3a0xTQCU4QxXF1vhU7P7av2KM=
github.com/cavaliergopher/cpio v1.0.1/go.mod h1:pBdaqQjnvXxdS/6CvNDwIANIFSP0xRKI16PX4xejRQc=
github.com/chainguard-dev/git-urls v1.0.2 h1:pSpT7ifrpc5X55n4aTTm7FFUE+ZQHKiqpiwNkJrVcKQ=
github.com/chainguard-dev/git-urls v1.0.2/go.mod h1:rbGgj10OS7UgZlbzdUQIQpT0k/D4+An04HJY7Ol+Y/o=
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/xnwFfbezlUnFMJy0nusZvytYysV4SCS2cYbvws=
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7/go.mod h1:ISC1gtLcVilLOf23wvTfoQuYbW2q0JevFxPfUzZ9Ybw=
github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU=
github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
github.com/charmbracelet/glamour v0.9.0 h1:1Hm3wxww7qXvGI+Fb3zDmIZo5oDOvVOWJ4OrIB+ef7c=
github.com/charmbracelet/glamour v0.9.0/go.mod h1:+SHvIS8qnwhgTpVMiXwn7OfGomSqff1cHBCI8jLOetk=
github.com/charmbracelet/huh v0.8.0 h1:Xz/Pm2h64cXQZn/Jvele4J3r7DDiqFCNIVteYukxDvY=
github.com/charmbracelet/huh v0.8.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30=
github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0=
github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U=
github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ=
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA=
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4=
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI=
github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4=
github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk=
github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro=
github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
github.com/creack/pty v1.1.23 h1:4M6+isWdcStXEf15G/RbrMPOQj1dZ7HPZCGwE4kOeP0=
github.com/creack/pty v1.1.23/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0=
github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -115,6 +141,8 @@ github.com/elazarl/goproxy v1.4.0 h1:4GyuSbFa+s26+3rmYNSuUVsx+HgPrV1bk1jXI0l9wjM
github.com/elazarl/goproxy v1.4.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
@ -198,6 +226,10 @@ github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kK
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/konoui/go-qsort v0.1.0 h1:0Os/0X0Fce6B54jqN26aR+J5uOExN+0t7nb9zs6zzzE=
github.com/konoui/go-qsort v0.1.0/go.mod h1:UOsvdDPBzyQDk9Tb21hETK6KYXGYQTnoZB5qeKA1ARs=
github.com/konoui/lipo v0.10.0 h1:1P2VkBSB6I38kgmyznvAjy9gmAqybK22pJt9iyx5CgY=
github.com/konoui/lipo v0.10.0/go.mod h1:R+0EgDVrLKKS37SumAO8zhpEprjjoKEkrT3QqKQE35k=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@ -231,6 +263,8 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
@ -247,6 +281,8 @@ github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
@ -342,6 +378,8 @@ github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk=
github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s=
github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI=
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
@ -375,8 +413,8 @@ golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -387,6 +425,7 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -395,8 +434,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=

View file

@ -22,7 +22,7 @@ tasks:
- npm install
build:frontend:
label: build:frontend (PRODUCTION={{ "{{.PRODUCTION}}" }})
label: build:frontend (DEV={{ "{{.DEV}}" }})
summary: Build the frontend project
dir: frontend
sources:
@ -38,9 +38,9 @@ tasks:
cmds:
- npm run {{ "{{.BUILD_COMMAND}}" }} -q
env:
PRODUCTION: {{ "'{{.PRODUCTION | default \"false\"}}'" }}
PRODUCTION: {{ "'{{if eq .DEV \"true\"}}false{{else}}true{{end}}'" }}
vars:
BUILD_COMMAND: {{ "'{{if eq .PRODUCTION \"true\"}}build{{else}}build:dev{{end}}'" }}
BUILD_COMMAND: {{ "'{{if eq .DEV \"true\"}}build:dev{{else}}build{{end}}'" }}
generate:bindings:
@ -84,3 +84,15 @@ tasks:
dir: build
cmds:
- wails3 update build-assets -name {{ "\"{{.APP_NAME}}\"" }} -binaryname {{ "\"{{.APP_NAME}}\"" }} -config config.yml -dir .
setup:docker:
summary: Builds Docker image for cross-compilation (~800MB download)
desc: |
Builds the Docker image needed for cross-compiling to any platform.
Run this once to enable cross-platform builds from any OS.
cmds:
- docker build -t wails-cross -f build/docker/Dockerfile.cross build/docker/
preconditions:
- sh: docker info > /dev/null 2>&1
msg: "Docker is required. Please install Docker first."

View file

@ -3,22 +3,44 @@ version: '3'
includes:
common: ../Taskfile.yml
vars:
# Signing configuration - edit these values for your project
# SIGN_IDENTITY: "Developer ID Application: Your Company (TEAMID)"
# KEYCHAIN_PROFILE: "my-notarize-profile"
# ENTITLEMENTS: "build/darwin/entitlements.plist"
# Docker image for cross-compilation (used when building on non-macOS)
CROSS_IMAGE: wails-cross
tasks:
build:
summary: Creates a production build of the application
summary: Builds the application
cmds:
- task: '{{if eq OS "darwin"}}build:native{{else}}build:docker{{end}}'
vars:
ARCH: '{{.ARCH}}'
DEV: '{{.DEV}}'
OUTPUT: '{{.OUTPUT}}'
vars:
DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
build:native:
summary: Builds the application natively on macOS
internal: true
deps:
- task: common:go:mod:tidy
- task: common:build:frontend
vars:
BUILD_FLAGS:
ref: .BUILD_FLAGS
PRODUCTION:
ref: .PRODUCTION
DEV:
ref: .DEV
- task: common:generate:icons
cmds:
- go build {{.BUILD_FLAGS}} -o {{.OUTPUT}}
vars:
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -buildvcs=false -ldflags="-w -s"{{else}}-buildvcs=false -gcflags=all="-l"{{end}}'
BUILD_FLAGS: '{{if eq .DEV "true"}}-buildvcs=false -gcflags=all="-l"{{else}}-tags production -trimpath -buildvcs=false -ldflags="-w -s"{{end}}'
DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
env:
@ -28,7 +50,47 @@ tasks:
CGO_CFLAGS: "-mmacosx-version-min=10.15"
CGO_LDFLAGS: "-mmacosx-version-min=10.15"
MACOSX_DEPLOYMENT_TARGET: "10.15"
PRODUCTION: '{{.PRODUCTION | default "false"}}'
build:docker:
summary: Cross-compiles for macOS using Docker (for Linux/Windows hosts)
internal: true
deps:
- task: common:build:frontend
- task: common:generate:icons
preconditions:
- sh: docker info > /dev/null 2>&1
msg: "Docker is required for cross-compilation. Please install Docker."
- sh: docker image inspect {{.CROSS_IMAGE}} > /dev/null 2>&1
msg: |
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" alpine chown -R $(id -u):$(id -g) /app/bin
- mkdir -p {{.BIN_DIR}}
- mv bin/{{.APP_NAME}}-darwin-{{.DOCKER_ARCH}} {{.OUTPUT}}
vars:
DOCKER_ARCH: '{{if eq .ARCH "arm64"}}arm64{{else if eq .ARCH "amd64"}}amd64{{else}}arm64{{end}}'
DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
# Mount Go module cache for faster builds
GO_CACHE_MOUNT:
sh: 'echo "-v ${GOPATH:-$HOME/go}/pkg/mod:/go/pkg/mod"'
# Extract replace directives from go.mod and create -v mounts for each
# Handles both relative (=> ../) and absolute (=> /) paths
REPLACE_MOUNTS:
sh: |
grep -E '^replace .* => ' go.mod 2>/dev/null | while read -r line; do
path=$(echo "$line" | sed -E 's/^replace .* => //' | tr -d '\r')
# Convert relative paths to absolute
if [ "${path#/}" = "$path" ]; then
path="$(cd "$(dirname "$path")" 2>/dev/null && pwd)/$(basename "$path")"
fi
# Only mount if directory exists
if [ -d "$path" ]; then
echo "-v $path:$path:ro"
fi
done | tr '\n' ' '
build:universal:
summary: Builds darwin universal binary (arm64 + amd64)
@ -41,16 +103,27 @@ tasks:
vars:
ARCH: arm64
OUTPUT: "{{.BIN_DIR}}/{{.APP_NAME}}-arm64"
cmds:
- task: '{{if eq OS "darwin"}}build:universal:lipo:native{{else}}build:universal:lipo:go{{end}}'
build:universal:lipo:native:
summary: Creates universal binary using native lipo (macOS)
internal: true
cmds:
- lipo -create -output "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64"
- rm "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64"
build:universal:lipo:go:
summary: Creates universal binary using wails3 tool lipo (Linux/Windows)
internal: true
cmds:
- wails3 tool lipo -o "{{.BIN_DIR}}/{{.APP_NAME}}" -i "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" -i "{{.BIN_DIR}}/{{.APP_NAME}}-arm64"
- rm -f "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64"
package:
summary: Packages a production build of the application into a `.app` bundle
summary: Packages the application into a `.app` bundle
deps:
- task: build
vars:
PRODUCTION: "true"
cmds:
- task: create:app:bundle
@ -69,8 +142,20 @@ tasks:
- cp build/darwin/icons.icns {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources
- cp {{.BIN_DIR}}/{{.APP_NAME}} {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/MacOS
- cp build/darwin/Info.plist {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents
- task: '{{if eq OS "darwin"}}codesign:adhoc{{else}}codesign:skip{{end}}'
codesign:adhoc:
summary: Ad-hoc signs the app bundle (macOS only)
internal: true
cmds:
- codesign --force --deep --sign - {{.BIN_DIR}}/{{.APP_NAME}}.app
codesign:skip:
summary: Skips codesigning when cross-compiling
internal: true
cmds:
- 'echo "Skipping codesign (not available on {{OS}}). Sign the .app on macOS before distribution."'
run:
cmds:
- mkdir -p {{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/{MacOS,Resources}
@ -79,3 +164,34 @@ tasks:
- cp build/darwin/Info.dev.plist {{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Info.plist
- codesign --force --deep --sign - {{.BIN_DIR}}/{{.APP_NAME}}.dev.app
- '{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS/{{.APP_NAME}}'
sign:
summary: Signs the application bundle with Developer ID
desc: |
Signs the .app bundle for distribution.
Configure SIGN_IDENTITY in the vars section at the top of this file.
deps:
- task: package
cmds:
- wails3 tool sign --input {{.BIN_DIR}}/{{.APP_NAME}}.app --identity "{{.SIGN_IDENTITY}}" {{if .ENTITLEMENTS}}--entitlements {{.ENTITLEMENTS}}{{end}}
preconditions:
- sh: '[ -n "{{.SIGN_IDENTITY}}" ]'
msg: "SIGN_IDENTITY is required. Set it in the vars section at the top of build/darwin/Taskfile.yml"
sign:notarize:
summary: Signs and notarizes the application bundle
desc: |
Signs the .app bundle and submits it for notarization.
Configure SIGN_IDENTITY and KEYCHAIN_PROFILE in the vars section at the top of this file.
Setup (one-time):
wails3 signing credentials --apple-id "you@email.com" --team-id "TEAMID" --password "app-specific-password" --profile "my-profile"
deps:
- task: package
cmds:
- wails3 tool sign --input {{.BIN_DIR}}/{{.APP_NAME}}.app --identity "{{.SIGN_IDENTITY}}" {{if .ENTITLEMENTS}}--entitlements {{.ENTITLEMENTS}}{{end}} --notarize --keychain-profile {{.KEYCHAIN_PROFILE}}
preconditions:
- sh: '[ -n "{{.SIGN_IDENTITY}}" ]'
msg: "SIGN_IDENTITY is required. Set it in the vars section at the top of build/darwin/Taskfile.yml"
- sh: '[ -n "{{.KEYCHAIN_PROFILE}}" ]'
msg: "KEYCHAIN_PROFILE is required. Set it in the vars section at the top of build/darwin/Taskfile.yml"

View file

@ -0,0 +1,195 @@
# Cross-compile Wails v3 apps to any platform
#
# Uses Zig as C compiler + macOS SDK for darwin targets
#
# Usage:
# docker build -t wails-cross -f Dockerfile.cross .
# docker run --rm -v $(pwd):/app wails-cross darwin arm64
# docker run --rm -v $(pwd):/app wails-cross darwin amd64
# docker run --rm -v $(pwd):/app wails-cross linux amd64
# docker run --rm -v $(pwd):/app wails-cross linux arm64
# docker run --rm -v $(pwd):/app wails-cross windows amd64
# docker run --rm -v $(pwd):/app wails-cross windows arm64
FROM golang:1.25-alpine
RUN apk add --no-cache curl xz nodejs npm
# Install Zig
ARG ZIG_VERSION=0.14.0
RUN curl -L "https://ziglang.org/download/${ZIG_VERSION}/zig-linux-x86_64-${ZIG_VERSION}.tar.xz" \
| tar -xJ -C /opt \
&& ln -s /opt/zig-linux-x86_64-${ZIG_VERSION}/zig /usr/local/bin/zig
# Download macOS SDK (required for darwin targets)
ARG MACOS_SDK_VERSION=14.5
RUN curl -L "https://github.com/joseluisq/macosx-sdks/releases/download/${MACOS_SDK_VERSION}/MacOSX${MACOS_SDK_VERSION}.sdk.tar.xz" \
| tar -xJ -C /opt \
&& mv /opt/MacOSX${MACOS_SDK_VERSION}.sdk /opt/macos-sdk
ENV MACOS_SDK_PATH=/opt/macos-sdk
# Create zig cc wrappers for each target
# Darwin arm64
COPY <<'ZIGWRAP' /usr/local/bin/zcc-darwin-arm64
#!/bin/sh
ARGS=""
SKIP_NEXT=0
for arg in "$@"; do
if [ $SKIP_NEXT -eq 1 ]; then
SKIP_NEXT=0
continue
fi
case "$arg" in
-target) SKIP_NEXT=1 ;;
-mmacosx-version-min=*) ;;
*) ARGS="$ARGS $arg" ;;
esac
done
exec zig cc -target aarch64-macos-none -isysroot /opt/macos-sdk -I/opt/macos-sdk/usr/include -L/opt/macos-sdk/usr/lib -F/opt/macos-sdk/System/Library/Frameworks -w $ARGS
ZIGWRAP
RUN chmod +x /usr/local/bin/zcc-darwin-arm64
# Darwin amd64
COPY <<'ZIGWRAP' /usr/local/bin/zcc-darwin-amd64
#!/bin/sh
ARGS=""
SKIP_NEXT=0
for arg in "$@"; do
if [ $SKIP_NEXT -eq 1 ]; then
SKIP_NEXT=0
continue
fi
case "$arg" in
-target) SKIP_NEXT=1 ;;
-mmacosx-version-min=*) ;;
*) ARGS="$ARGS $arg" ;;
esac
done
exec zig cc -target x86_64-macos-none -isysroot /opt/macos-sdk -I/opt/macos-sdk/usr/include -L/opt/macos-sdk/usr/lib -F/opt/macos-sdk/System/Library/Frameworks -w $ARGS
ZIGWRAP
RUN chmod +x /usr/local/bin/zcc-darwin-amd64
# Linux amd64
COPY <<'ZIGWRAP' /usr/local/bin/zcc-linux-amd64
#!/bin/sh
ARGS=""
SKIP_NEXT=0
for arg in "$@"; do
if [ $SKIP_NEXT -eq 1 ]; then
SKIP_NEXT=0
continue
fi
case "$arg" in
-target) SKIP_NEXT=1 ;;
*) ARGS="$ARGS $arg" ;;
esac
done
exec zig cc -target x86_64-linux-musl $ARGS
ZIGWRAP
RUN chmod +x /usr/local/bin/zcc-linux-amd64
# Linux arm64
COPY <<'ZIGWRAP' /usr/local/bin/zcc-linux-arm64
#!/bin/sh
ARGS=""
SKIP_NEXT=0
for arg in "$@"; do
if [ $SKIP_NEXT -eq 1 ]; then
SKIP_NEXT=0
continue
fi
case "$arg" in
-target) SKIP_NEXT=1 ;;
*) ARGS="$ARGS $arg" ;;
esac
done
exec zig cc -target aarch64-linux-musl $ARGS
ZIGWRAP
RUN chmod +x /usr/local/bin/zcc-linux-arm64
# Windows amd64
COPY <<'ZIGWRAP' /usr/local/bin/zcc-windows-amd64
#!/bin/sh
ARGS=""
SKIP_NEXT=0
for arg in "$@"; do
if [ $SKIP_NEXT -eq 1 ]; then
SKIP_NEXT=0
continue
fi
case "$arg" in
-target) SKIP_NEXT=1 ;;
-Wl,*) ;;
*) ARGS="$ARGS $arg" ;;
esac
done
exec zig cc -target x86_64-windows-gnu $ARGS
ZIGWRAP
RUN chmod +x /usr/local/bin/zcc-windows-amd64
# Windows arm64
COPY <<'ZIGWRAP' /usr/local/bin/zcc-windows-arm64
#!/bin/sh
ARGS=""
SKIP_NEXT=0
for arg in "$@"; do
if [ $SKIP_NEXT -eq 1 ]; then
SKIP_NEXT=0
continue
fi
case "$arg" in
-target) SKIP_NEXT=1 ;;
-Wl,*) ;;
*) ARGS="$ARGS $arg" ;;
esac
done
exec zig cc -target aarch64-windows-gnu $ARGS
ZIGWRAP
RUN chmod +x /usr/local/bin/zcc-windows-arm64
# Build script
COPY <<'SCRIPT' /usr/local/bin/build.sh
#!/bin/sh
set -e
OS=${1:-darwin}
ARCH=${2:-arm64}
case "${OS}-${ARCH}" in
darwin-arm64|darwin-aarch64) export CC=zcc-darwin-arm64; export GOARCH=arm64; export GOOS=darwin ;;
darwin-amd64|darwin-x86_64) export CC=zcc-darwin-amd64; export GOARCH=amd64; export GOOS=darwin ;;
linux-arm64|linux-aarch64) export CC=zcc-linux-arm64; export GOARCH=arm64; export GOOS=linux ;;
linux-amd64|linux-x86_64) export CC=zcc-linux-amd64; export GOARCH=amd64; export GOOS=linux ;;
windows-arm64|windows-aarch64) export CC=zcc-windows-arm64; export GOARCH=arm64; export GOOS=windows ;;
windows-amd64|windows-x86_64) export CC=zcc-windows-amd64; export GOARCH=amd64; export GOOS=windows ;;
*) echo "Usage: <os> <arch>"; echo " os: darwin, linux, windows"; echo " arch: amd64, arm64"; exit 1 ;;
esac
export CGO_ENABLED=1
export CGO_CFLAGS="-w"
# Build frontend if exists and not already built (host may have built it)
if [ -d "frontend" ] && [ -f "frontend/package.json" ] && [ ! -d "frontend/dist" ]; then
(cd frontend && npm install --silent && npm run build --silent)
fi
# Build
APP=${APP_NAME:-$(basename $(pwd))}
mkdir -p bin
EXT=""
LDFLAGS="-s -w"
if [ "$GOOS" = "windows" ]; then
EXT=".exe"
LDFLAGS="-s -w -H windowsgui"
fi
go build -ldflags="$LDFLAGS" -o bin/${APP}-${GOOS}-${GOARCH}${EXT} .
echo "Built: bin/${APP}-${GOOS}-${GOARCH}${EXT}"
SCRIPT
RUN chmod +x /usr/local/bin/build.sh
WORKDIR /app
ENTRYPOINT ["/usr/local/bin/build.sh"]
CMD ["darwin", "arm64"]

View file

@ -3,34 +3,100 @@ version: '3'
includes:
common: ../Taskfile.yml
vars:
# Signing configuration - edit these values for your project
# PGP_KEY: "path/to/signing-key.asc"
# SIGN_ROLE: "builder" # Options: origin, maint, archive, builder
#
# Password is stored securely in system keychain. Run: wails3 setup signing
# Docker image for cross-compilation (used when building on non-Linux or no CC available)
CROSS_IMAGE: wails-cross
tasks:
build:
summary: Builds the application for Linux
cmds:
# Linux requires CGO - use Docker when cross-compiling from non-Linux OR when no C compiler is available
- task: '{{if and (eq OS "linux") (eq .HAS_CC "true")}}build:native{{else}}build:docker{{end}}'
vars:
ARCH: '{{.ARCH}}'
DEV: '{{.DEV}}'
OUTPUT: '{{.OUTPUT}}'
vars:
DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
# Check if a C compiler is available (gcc or clang)
HAS_CC:
sh: '(command -v gcc >/dev/null 2>&1 || command -v clang >/dev/null 2>&1) && echo "true" || echo "false"'
build:native:
summary: Builds the application natively on Linux
internal: true
deps:
- task: common:go:mod:tidy
- task: common:build:frontend
vars:
BUILD_FLAGS:
ref: .BUILD_FLAGS
PRODUCTION:
ref: .PRODUCTION
DEV:
ref: .DEV
- task: common:generate:icons
cmds:
- go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}}
- go build {{.BUILD_FLAGS}} -o {{.OUTPUT}}
vars:
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -buildvcs=false -ldflags="-w -s"{{else}}-buildvcs=false -gcflags=all="-l"{{end}}'
BUILD_FLAGS: '{{if eq .DEV "true"}}-buildvcs=false -gcflags=all="-l"{{else}}-tags production -trimpath -buildvcs=false -ldflags="-w -s"{{end}}'
DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
env:
GOOS: linux
CGO_ENABLED: 1
GOARCH: '{{.ARCH | default ARCH}}'
PRODUCTION: '{{.PRODUCTION | default "false"}}'
build:docker:
summary: Cross-compiles for Linux using Docker with Zig (for macOS/Windows hosts)
internal: true
deps:
- task: common:build:frontend
- task: common:generate:icons
preconditions:
- sh: docker info > /dev/null 2>&1
msg: "Docker is required for cross-compilation to Linux. Please install Docker."
- sh: docker image inspect {{.CROSS_IMAGE}} > /dev/null 2>&1
msg: |
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" alpine chown -R $(id -u):$(id -g) /app/bin
- mkdir -p {{.BIN_DIR}}
- mv bin/{{.APP_NAME}}-linux-{{.DOCKER_ARCH}} {{.OUTPUT}}
vars:
DOCKER_ARCH: '{{.ARCH | default "amd64"}}'
DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
# Mount Go module cache for faster builds
GO_CACHE_MOUNT:
sh: 'echo "-v ${GOPATH:-$HOME/go}/pkg/mod:/go/pkg/mod"'
# Extract replace directives from go.mod and create -v mounts for each
REPLACE_MOUNTS:
sh: |
grep -E '^replace .* => ' go.mod 2>/dev/null | while read -r line; do
path=$(echo "$line" | sed -E 's/^replace .* => //' | tr -d '\r')
# Convert relative paths to absolute
if [ "${path#/}" = "$path" ]; then
path="$(cd "$(dirname "$path")" 2>/dev/null && pwd)/$(basename "$path")"
fi
# Only mount if directory exists
if [ -d "$path" ]; then
echo "-v $path:$path:ro"
fi
done | tr '\n' ' '
package:
summary: Packages a production build of the application for Linux
summary: Packages the application for Linux
deps:
- task: build
vars:
PRODUCTION: "true"
cmds:
- task: create:appimage
- task: create:deb
@ -42,8 +108,6 @@ tasks:
dir: build/linux/appimage
deps:
- task: build
vars:
PRODUCTION: "true"
- task: generate:dotdesktop
cmds:
- cp {{.APP_BINARY}} {{.APP_NAME}}
@ -60,8 +124,6 @@ tasks:
summary: Creates a deb package
deps:
- task: build
vars:
PRODUCTION: "true"
cmds:
- task: generate:dotdesktop
- task: generate:deb
@ -70,8 +132,6 @@ tasks:
summary: Creates a rpm package
deps:
- task: build
vars:
PRODUCTION: "true"
cmds:
- task: generate:dotdesktop
- task: generate:rpm
@ -80,8 +140,6 @@ tasks:
summary: Creates a arch linux packager package
deps:
- task: build
vars:
PRODUCTION: "true"
cmds:
- task: generate:dotdesktop
- task: generate:aur
@ -117,3 +175,44 @@ tasks:
run:
cmds:
- '{{.BIN_DIR}}/{{.APP_NAME}}'
sign:deb:
summary: Signs the DEB package
desc: |
Signs the .deb package with a PGP key.
Configure PGP_KEY in the vars section at the top of this file.
Password is retrieved from system keychain (run: wails3 setup signing)
deps:
- task: create:deb
cmds:
- wails3 tool sign --input {{.BIN_DIR}}/{{.APP_NAME}}*.deb --pgp-key {{.PGP_KEY}} {{if .SIGN_ROLE}}--role {{.SIGN_ROLE}}{{end}}
preconditions:
- sh: '[ -n "{{.PGP_KEY}}" ]'
msg: "PGP_KEY is required. Set it in the vars section at the top of build/linux/Taskfile.yml"
sign:rpm:
summary: Signs the RPM package
desc: |
Signs the .rpm package with a PGP key.
Configure PGP_KEY in the vars section at the top of this file.
Password is retrieved from system keychain (run: wails3 setup signing)
deps:
- task: create:rpm
cmds:
- wails3 tool sign --input {{.BIN_DIR}}/{{.APP_NAME}}*.rpm --pgp-key {{.PGP_KEY}}
preconditions:
- sh: '[ -n "{{.PGP_KEY}}" ]'
msg: "PGP_KEY is required. Set it in the vars section at the top of build/linux/Taskfile.yml"
sign:packages:
summary: Signs all Linux packages (DEB and RPM)
desc: |
Signs both .deb and .rpm packages with a PGP key.
Configure PGP_KEY in the vars section at the top of this file.
Password is retrieved from system keychain (run: wails3 setup signing)
cmds:
- task: sign:deb
- task: sign:rpm
preconditions:
- sh: '[ -n "{{.PGP_KEY}}" ]'
msg: "PGP_KEY is required. Set it in the vars section at the top of build/linux/Taskfile.yml"

View file

@ -3,17 +3,41 @@ version: '3'
includes:
common: ../Taskfile.yml
vars:
# Signing configuration - edit these values for your project
# SIGN_CERTIFICATE: "path/to/certificate.pfx"
# SIGN_THUMBPRINT: "certificate-thumbprint" # Alternative to SIGN_CERTIFICATE
# TIMESTAMP_SERVER: "http://timestamp.digicert.com"
#
# Password is stored securely in system keychain. Run: wails3 setup signing
# Docker image for cross-compilation with CGO (used when CGO_ENABLED=1 on non-Windows)
CROSS_IMAGE: wails-cross
tasks:
build:
summary: Builds the application for Windows
cmds:
# Auto-detect CGO: if CGO_ENABLED=1, use Docker; otherwise use native Go cross-compile
- task: '{{if and (ne OS "windows") (eq .CGO_ENABLED "1")}}build:docker{{else}}build:native{{end}}'
vars:
ARCH: '{{.ARCH}}'
DEV: '{{.DEV}}'
vars:
# Default to CGO_ENABLED=0 if not explicitly set
CGO_ENABLED: '{{.CGO_ENABLED | default "0"}}'
build:native:
summary: Builds the application using native Go cross-compilation
internal: true
deps:
- task: common:go:mod:tidy
- task: common:build:frontend
vars:
BUILD_FLAGS:
ref: .BUILD_FLAGS
PRODUCTION:
ref: .PRODUCTION
DEV:
ref: .DEV
- task: common:generate:icons
cmds:
- task: generate:syso
@ -23,15 +47,52 @@ tasks:
- cmd: rm -f *.syso
platforms: [linux, darwin]
vars:
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -buildvcs=false -ldflags="-w -s -H windowsgui"{{else}}-buildvcs=false -gcflags=all="-l"{{end}}'
BUILD_FLAGS: '{{if eq .DEV "true"}}-buildvcs=false -gcflags=all="-l"{{else}}-tags production -trimpath -buildvcs=false -ldflags="-w -s -H windowsgui"{{end}}'
env:
GOOS: windows
CGO_ENABLED: 0
CGO_ENABLED: '{{.CGO_ENABLED | default "0"}}'
GOARCH: '{{.ARCH | default ARCH}}'
PRODUCTION: '{{.PRODUCTION | default "false"}}'
build:docker:
summary: Cross-compiles for Windows using Docker with Zig (for CGO builds on non-Windows)
internal: true
deps:
- task: common:build:frontend
- task: common:generate:icons
preconditions:
- sh: docker info > /dev/null 2>&1
msg: "Docker is required for CGO cross-compilation. Please install Docker."
- sh: docker image inspect {{.CROSS_IMAGE}} > /dev/null 2>&1
msg: |
Docker image '{{.CROSS_IMAGE}}' not found.
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" alpine chown -R $(id -u):$(id -g) /app/bin
- rm -f *.syso
vars:
DOCKER_ARCH: '{{.ARCH | default "amd64"}}'
# Mount Go module cache for faster builds
GO_CACHE_MOUNT:
sh: 'echo "-v ${GOPATH:-$HOME/go}/pkg/mod:/go/pkg/mod"'
# Extract replace directives from go.mod and create -v mounts for each
REPLACE_MOUNTS:
sh: |
grep -E '^replace .* => ' go.mod 2>/dev/null | while read -r line; do
path=$(echo "$line" | sed -E 's/^replace .* => //' | tr -d '\r')
# Convert relative paths to absolute
if [ "${path#/}" = "$path" ]; then
path="$(cd "$(dirname "$path")" 2>/dev/null && pwd)/$(basename "$path")"
fi
# Only mount if directory exists
if [ -d "$path" ]; then
echo "-v $path:$path:ro"
fi
done | tr '\n' ' '
package:
summary: Packages a production build of the application
summary: Packages the application
cmds:
- task: '{{if eq (.FORMAT | default "nsis") "msix"}}create:msix:package{{else}}create:nsis:installer{{end}}'
vars:
@ -50,8 +111,6 @@ tasks:
dir: build/windows/nsis
deps:
- task: build
vars:
PRODUCTION: "true"
cmds:
# Create the Microsoft WebView2 bootstrapper if it doesn't exist
- wails3 generate webview2bootstrapper -dir "{{.ROOT_DIR}}/build/windows/nsis"
@ -69,8 +128,6 @@ tasks:
summary: Creates an MSIX package
deps:
- task: build
vars:
PRODUCTION: "true"
cmds:
- |-
wails3 tool msix \
@ -96,3 +153,31 @@ tasks:
run:
cmds:
- '{{.BIN_DIR}}/{{.APP_NAME}}.exe'
sign:
summary: Signs the Windows executable
desc: |
Signs the .exe with an Authenticode certificate.
Configure SIGN_CERTIFICATE or SIGN_THUMBPRINT in the vars section at the top of this file.
Password is retrieved from system keychain (run: wails3 setup signing)
deps:
- task: build
cmds:
- wails3 tool sign --input {{.BIN_DIR}}/{{.APP_NAME}}.exe {{if .SIGN_CERTIFICATE}}--certificate {{.SIGN_CERTIFICATE}}{{end}} {{if .SIGN_THUMBPRINT}}--thumbprint {{.SIGN_THUMBPRINT}}{{end}} {{if .TIMESTAMP_SERVER}}--timestamp {{.TIMESTAMP_SERVER}}{{end}}
preconditions:
- sh: '[ -n "{{.SIGN_CERTIFICATE}}" ] || [ -n "{{.SIGN_THUMBPRINT}}" ]'
msg: "Either SIGN_CERTIFICATE or SIGN_THUMBPRINT is required. Set it in the vars section at the top of build/windows/Taskfile.yml"
sign:installer:
summary: Signs the NSIS installer
desc: |
Creates and signs the NSIS installer.
Configure SIGN_CERTIFICATE or SIGN_THUMBPRINT in the vars section at the top of this file.
Password is retrieved from system keychain (run: wails3 setup signing)
deps:
- task: create:nsis:installer
cmds:
- wails3 tool sign --input build/windows/nsis/{{.APP_NAME}}-installer.exe {{if .SIGN_CERTIFICATE}}--certificate {{.SIGN_CERTIFICATE}}{{end}} {{if .SIGN_THUMBPRINT}}--thumbprint {{.SIGN_THUMBPRINT}}{{end}} {{if .TIMESTAMP_SERVER}}--timestamp {{.TIMESTAMP_SERVER}}{{end}}
preconditions:
- sh: '[ -n "{{.SIGN_CERTIFICATE}}" ] || [ -n "{{.SIGN_THUMBPRINT}}" ]'
msg: "Either SIGN_CERTIFICATE or SIGN_THUMBPRINT is required. Set it in the vars section at the top of build/windows/Taskfile.yml"

View file

@ -0,0 +1,241 @@
package commands
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/charmbracelet/huh"
"github.com/pterm/pterm"
"github.com/wailsapp/wails/v3/internal/flags"
)
// Entitlement represents a macOS entitlement
type Entitlement struct {
Key string
Name string
Description string
Category string
}
// Common macOS entitlements organized by category
var availableEntitlements = []Entitlement{
// Hardened Runtime - Code Execution
{Key: "com.apple.security.cs.allow-jit", Name: "Allow JIT", Description: "Allow creating writable/executable memory using MAP_JIT (most secure option for JIT)", Category: "Code Execution"},
{Key: "com.apple.security.cs.allow-unsigned-executable-memory", Name: "Allow Unsigned Executable Memory", Description: "Allow writable/executable memory without MAP_JIT restrictions", Category: "Code Execution"},
{Key: "com.apple.security.cs.disable-executable-page-protection", Name: "Disable Executable Page Protection", Description: "Disable all executable memory protections (least secure)", Category: "Code Execution"},
{Key: "com.apple.security.cs.disable-library-validation", Name: "Disable Library Validation", Description: "Allow loading unsigned or differently-signed libraries/frameworks", Category: "Code Execution"},
{Key: "com.apple.security.cs.allow-dyld-environment-variables", Name: "Allow DYLD Environment Variables", Description: "Allow DYLD_* environment variables to modify library loading", Category: "Code Execution"},
// Hardened Runtime - Resource Access
{Key: "com.apple.security.device.audio-input", Name: "Audio Input (Microphone)", Description: "Access to audio input devices", Category: "Resource Access"},
{Key: "com.apple.security.device.camera", Name: "Camera", Description: "Access to the camera", Category: "Resource Access"},
{Key: "com.apple.security.personal-information.location", Name: "Location", Description: "Access to location services", Category: "Resource Access"},
{Key: "com.apple.security.personal-information.addressbook", Name: "Address Book", Description: "Access to contacts", Category: "Resource Access"},
{Key: "com.apple.security.personal-information.calendars", Name: "Calendars", Description: "Access to calendar data", Category: "Resource Access"},
{Key: "com.apple.security.personal-information.photos-library", Name: "Photos Library", Description: "Access to the Photos library", Category: "Resource Access"},
{Key: "com.apple.security.automation.apple-events", Name: "Apple Events", Description: "Send Apple Events to other apps (AppleScript)", Category: "Resource Access"},
// App Sandbox - Basic
{Key: "com.apple.security.app-sandbox", Name: "Enable App Sandbox", Description: "Enable the App Sandbox (required for Mac App Store)", Category: "App Sandbox"},
// App Sandbox - Network
{Key: "com.apple.security.network.client", Name: "Outgoing Network Connections", Description: "Allow outgoing network connections (client)", Category: "Network"},
{Key: "com.apple.security.network.server", Name: "Incoming Network Connections", Description: "Allow incoming network connections (server)", Category: "Network"},
// App Sandbox - Files
{Key: "com.apple.security.files.user-selected.read-only", Name: "User-Selected Files (Read)", Description: "Read access to files the user selects", Category: "File Access"},
{Key: "com.apple.security.files.user-selected.read-write", Name: "User-Selected Files (Read/Write)", Description: "Read/write access to files the user selects", Category: "File Access"},
{Key: "com.apple.security.files.downloads.read-only", Name: "Downloads Folder (Read)", Description: "Read access to the Downloads folder", Category: "File Access"},
{Key: "com.apple.security.files.downloads.read-write", Name: "Downloads Folder (Read/Write)", Description: "Read/write access to the Downloads folder", Category: "File Access"},
// Development
{Key: "com.apple.security.get-task-allow", Name: "Debugging", Description: "Allow debugging (disable for production)", Category: "Development"},
}
// EntitlementsSetup runs the interactive entitlements configuration wizard
func EntitlementsSetup(options *flags.EntitlementsSetup) error {
pterm.DefaultHeader.Println("macOS Entitlements Setup")
fmt.Println()
// Build all options for custom selection
var allOptions []huh.Option[string]
for _, e := range availableEntitlements {
label := fmt.Sprintf("[%s] %s", e.Category, e.Name)
allOptions = append(allOptions, huh.NewOption(label, e.Key))
}
// Show quick presets first
var preset string
presetForm := huh.NewForm(
huh.NewGroup(
huh.NewSelect[string]().
Title("Which entitlements profile?").
Description("Development and production use separate files").
Options(
huh.NewOption("Development (entitlements.dev.plist)", "dev"),
huh.NewOption("Production (entitlements.plist)", "prod"),
huh.NewOption("Both (recommended)", "both"),
huh.NewOption("App Store (entitlements.plist with sandbox)", "appstore"),
huh.NewOption("Custom", "custom"),
).
Value(&preset),
),
)
if err := presetForm.Run(); err != nil {
return err
}
devEntitlements := []string{
"com.apple.security.cs.allow-jit",
"com.apple.security.cs.allow-unsigned-executable-memory",
"com.apple.security.cs.disable-library-validation",
"com.apple.security.get-task-allow",
"com.apple.security.network.client",
}
prodEntitlements := []string{
"com.apple.security.network.client",
}
appStoreEntitlements := []string{
"com.apple.security.app-sandbox",
"com.apple.security.network.client",
"com.apple.security.files.user-selected.read-write",
}
baseDir := "build/darwin"
if options.Output != "" {
baseDir = filepath.Dir(options.Output)
}
switch preset {
case "dev":
return writeEntitlementsFile(filepath.Join(baseDir, "entitlements.dev.plist"), devEntitlements)
case "prod":
return writeEntitlementsFile(filepath.Join(baseDir, "entitlements.plist"), prodEntitlements)
case "both":
if err := writeEntitlementsFile(filepath.Join(baseDir, "entitlements.dev.plist"), devEntitlements); err != nil {
return err
}
return writeEntitlementsFile(filepath.Join(baseDir, "entitlements.plist"), prodEntitlements)
case "appstore":
return writeEntitlementsFile(filepath.Join(baseDir, "entitlements.plist"), appStoreEntitlements)
case "custom":
// Let user choose which file and entitlements
var targetFile string
var selected []string
customForm := huh.NewForm(
huh.NewGroup(
huh.NewSelect[string]().
Title("Target file").
Options(
huh.NewOption("entitlements.plist (production)", "entitlements.plist"),
huh.NewOption("entitlements.dev.plist (development)", "entitlements.dev.plist"),
).
Value(&targetFile),
),
huh.NewGroup(
huh.NewMultiSelect[string]().
Title("Select entitlements").
Description("Use space to select, enter to confirm").
Options(allOptions...).
Value(&selected),
),
)
if err := customForm.Run(); err != nil {
return err
}
return writeEntitlementsFile(filepath.Join(baseDir, targetFile), selected)
}
return nil
}
func writeEntitlementsFile(path string, entitlements []string) error {
// Ensure directory exists
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("failed to create directory: %w", err)
}
// Generate and write the plist
plist := generateEntitlementsPlist(entitlements)
if err := os.WriteFile(path, []byte(plist), 0644); err != nil {
return fmt.Errorf("failed to write entitlements file: %w", err)
}
pterm.Success.Printfln("Wrote %s", path)
// Show summary
pterm.Info.Println("Entitlements:")
for _, key := range entitlements {
for _, e := range availableEntitlements {
if e.Key == key {
fmt.Printf(" - %s\n", e.Name)
break
}
}
}
fmt.Println()
return nil
}
func parseExistingEntitlements(path string) (map[string]bool, error) {
content, err := os.ReadFile(path)
if err != nil {
return nil, err
}
result := make(map[string]bool)
lines := strings.Split(string(content), "\n")
for i, line := range lines {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "<key>") && strings.HasSuffix(line, "</key>") {
key := strings.TrimPrefix(line, "<key>")
key = strings.TrimSuffix(key, "</key>")
// Check if next line is <true/>
if i+1 < len(lines) {
nextLine := strings.TrimSpace(lines[i+1])
if nextLine == "<true/>" {
result[key] = true
}
}
}
}
return result, nil
}
func generateEntitlementsPlist(entitlements []string) string {
var sb strings.Builder
sb.WriteString(`<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
`)
for _, key := range entitlements {
sb.WriteString(fmt.Sprintf("\t<key>%s</key>\n", key))
sb.WriteString("\t<true/>\n")
}
sb.WriteString(`</dict>
</plist>
`)
return sb.String()
}

View file

@ -0,0 +1,396 @@
package commands
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"github.com/pterm/pterm"
"github.com/wailsapp/wails/v3/internal/flags"
"github.com/wailsapp/wails/v3/internal/keychain"
)
// Sign signs a binary or package
func Sign(options *flags.Sign) error {
if options.Input == "" {
return fmt.Errorf("--input is required")
}
// Check input file exists
info, err := os.Stat(options.Input)
if err != nil {
return fmt.Errorf("input file not found: %w", err)
}
// Determine what type of signing to do based on file extension and flags
ext := strings.ToLower(filepath.Ext(options.Input))
// macOS app bundle (directory)
if info.IsDir() && strings.HasSuffix(options.Input, ".app") {
return signMacOSApp(options)
}
// macOS binary or Windows executable
if ext == ".exe" || ext == ".msi" || ext == ".msix" || ext == ".appx" {
return signWindows(options)
}
// Linux packages
if ext == ".deb" {
return signDEB(options)
}
if ext == ".rpm" {
return signRPM(options)
}
// macOS binary (no extension typically)
if runtime.GOOS == "darwin" && options.Identity != "" {
return signMacOSBinary(options)
}
return fmt.Errorf("unsupported file type: %s", ext)
}
func signMacOSApp(options *flags.Sign) error {
if options.Identity == "" {
return fmt.Errorf("--identity is required for macOS signing")
}
if options.Verbose {
pterm.Info.Printfln("Signing macOS app bundle: %s", options.Input)
}
// Build codesign command
args := []string{
"--force",
"--deep",
"--sign", options.Identity,
}
if options.Entitlements != "" {
args = append(args, "--entitlements", options.Entitlements)
}
if options.HardenedRuntime || options.Notarize {
args = append(args, "--options", "runtime")
}
args = append(args, options.Input)
cmd := exec.Command("codesign", args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("codesign failed: %w", err)
}
pterm.Success.Printfln("Signed: %s", options.Input)
// Notarize if requested
if options.Notarize {
return notarizeMacOSApp(options)
}
return nil
}
func signMacOSBinary(options *flags.Sign) error {
if options.Identity == "" {
return fmt.Errorf("--identity is required for macOS signing")
}
if options.Verbose {
pterm.Info.Printfln("Signing macOS binary: %s", options.Input)
}
args := []string{
"--force",
"--sign", options.Identity,
}
if options.Entitlements != "" {
args = append(args, "--entitlements", options.Entitlements)
}
if options.HardenedRuntime {
args = append(args, "--options", "runtime")
}
args = append(args, options.Input)
cmd := exec.Command("codesign", args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("codesign failed: %w", err)
}
pterm.Success.Printfln("Signed: %s", options.Input)
return nil
}
func notarizeMacOSApp(options *flags.Sign) error {
if options.KeychainProfile == "" {
return fmt.Errorf("--keychain-profile is required for notarization")
}
if options.Verbose {
pterm.Info.Println("Submitting for notarization...")
}
// Create a zip for notarization
zipPath := options.Input + ".zip"
zipCmd := exec.Command("ditto", "-c", "-k", "--keepParent", options.Input, zipPath)
if err := zipCmd.Run(); err != nil {
return fmt.Errorf("failed to create zip for notarization: %w", err)
}
defer os.Remove(zipPath)
// Submit for notarization
args := []string{
"notarytool", "submit",
zipPath,
"--keychain-profile", options.KeychainProfile,
"--wait",
}
cmd := exec.Command("xcrun", args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("notarization failed: %w", err)
}
// Staple the ticket
stapleCmd := exec.Command("xcrun", "stapler", "staple", options.Input)
stapleCmd.Stdout = os.Stdout
stapleCmd.Stderr = os.Stderr
if err := stapleCmd.Run(); err != nil {
return fmt.Errorf("stapling failed: %w", err)
}
pterm.Success.Println("Notarization complete and ticket stapled")
return nil
}
func signWindows(options *flags.Sign) error {
// Get password from keychain if not provided
password := options.Password
if password == "" && options.Certificate != "" {
var err error
password, err = keychain.Get(keychain.KeyWindowsCertPassword)
if err != nil {
pterm.Warning.Printfln("Could not get password from keychain: %v", err)
// Continue without password - might work for some certificates
}
}
if options.Verbose {
pterm.Info.Printfln("Signing Windows executable: %s", options.Input)
}
// Try native signtool first on Windows
if runtime.GOOS == "windows" {
err := signWindowsNative(options, password)
if err == nil {
return nil
}
if options.Verbose {
pterm.Warning.Printfln("Native signing failed, trying built-in: %v", err)
}
}
// Use built-in signing (works cross-platform)
return signWindowsBuiltin(options, password)
}
func signWindowsNative(options *flags.Sign, password string) error {
// Find signtool.exe
signtool, err := findSigntool()
if err != nil {
return err
}
args := []string{"sign"}
if options.Certificate != "" {
args = append(args, "/f", options.Certificate)
if password != "" {
args = append(args, "/p", password)
}
} else if options.Thumbprint != "" {
args = append(args, "/sha1", options.Thumbprint)
} else {
return fmt.Errorf("either --certificate or --thumbprint is required")
}
// Add timestamp server
timestamp := options.Timestamp
if timestamp == "" {
timestamp = "http://timestamp.digicert.com"
}
args = append(args, "/tr", timestamp, "/td", "SHA256", "/fd", "SHA256")
args = append(args, options.Input)
cmd := exec.Command(signtool, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("signtool failed: %w", err)
}
pterm.Success.Printfln("Signed: %s", options.Input)
return nil
}
func findSigntool() (string, error) {
// Check if signtool is in PATH
path, err := exec.LookPath("signtool.exe")
if err == nil {
return path, nil
}
// Common Windows SDK locations
sdkPaths := []string{
`C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64\signtool.exe`,
`C:\Program Files (x86)\Windows Kits\10\bin\10.0.19041.0\x64\signtool.exe`,
`C:\Program Files (x86)\Windows Kits\10\bin\x64\signtool.exe`,
}
for _, p := range sdkPaths {
if _, err := os.Stat(p); err == nil {
return p, nil
}
}
return "", fmt.Errorf("signtool.exe not found")
}
func signWindowsBuiltin(options *flags.Sign, password string) error {
// This would use a Go library for Authenticode signing
// For now, we'll return an error indicating it needs implementation
// In a full implementation, you'd use something like:
// - github.com/AkarinLiu/osslsigncode-go
// - or implement PE signing directly
if options.Certificate == "" {
return fmt.Errorf("--certificate is required for cross-platform signing")
}
return fmt.Errorf("built-in Windows signing not yet implemented - please use signtool.exe on Windows, or install osslsigncode")
}
func signDEB(options *flags.Sign) error {
if options.PGPKey == "" {
return fmt.Errorf("--pgp-key is required for DEB signing")
}
// Get password from keychain if not provided
password := options.PGPPassword
if password == "" {
var err error
password, err = keychain.Get(keychain.KeyPGPPassword)
if err != nil {
// Password might not be required if key is unencrypted
if options.Verbose {
pterm.Warning.Printfln("Could not get PGP password from keychain: %v", err)
}
}
}
if options.Verbose {
pterm.Info.Printfln("Signing DEB package: %s", options.Input)
}
role := options.Role
if role == "" {
role = "builder"
}
// Use dpkg-sig for signing
args := []string{
"-k", options.PGPKey,
"--sign", role,
}
if password != "" {
// dpkg-sig reads from GPG_TTY or gpg-agent
// For scripted use, we need to use gpg with passphrase
args = append(args, "--gpg-options", fmt.Sprintf("--batch --passphrase %s", password))
}
args = append(args, options.Input)
cmd := exec.Command("dpkg-sig", args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
// Fallback: try using gpg directly to sign
return signDEBWithGPG(options, password, role)
}
pterm.Success.Printfln("Signed: %s", options.Input)
return nil
}
func signDEBWithGPG(options *flags.Sign, password, role string) error {
// Alternative approach using ar and gpg directly
// This is more portable but more complex
return fmt.Errorf("dpkg-sig not found - please install dpkg-sig or use a Linux system")
}
func signRPM(options *flags.Sign) error {
if options.PGPKey == "" {
return fmt.Errorf("--pgp-key is required for RPM signing")
}
// Get password from keychain if not provided
password := options.PGPPassword
if password == "" {
var err error
password, err = keychain.Get(keychain.KeyPGPPassword)
if err != nil {
if options.Verbose {
pterm.Warning.Printfln("Could not get PGP password from keychain: %v", err)
}
}
}
if options.Verbose {
pterm.Info.Printfln("Signing RPM package: %s", options.Input)
}
// RPM signing requires the key to be imported to GPG keyring
// and uses rpmsign command
args := []string{
"--addsign",
options.Input,
}
cmd := exec.Command("rpmsign", args...)
// Set up passphrase via environment if needed
if password != "" {
cmd.Env = append(os.Environ(), fmt.Sprintf("GPG_PASSPHRASE=%s", password))
}
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("rpmsign failed: %w", err)
}
pterm.Success.Printfln("Signed: %s", options.Input)
return nil
}

View file

@ -0,0 +1,588 @@
package commands
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"github.com/ProtonMail/go-crypto/openpgp"
"github.com/ProtonMail/go-crypto/openpgp/armor"
"github.com/ProtonMail/go-crypto/openpgp/packet"
"github.com/charmbracelet/huh"
"github.com/pterm/pterm"
"github.com/wailsapp/wails/v3/internal/flags"
"github.com/wailsapp/wails/v3/internal/keychain"
)
// SigningSetup configures signing variables in platform Taskfiles
func SigningSetup(options *flags.SigningSetup) error {
// Determine which platforms to configure
platforms := options.Platforms
if len(platforms) == 0 {
// Auto-detect based on existing Taskfiles
platforms = detectPlatforms()
if len(platforms) == 0 {
return fmt.Errorf("no platform Taskfiles found in build/ directory")
}
}
for _, platform := range platforms {
var err error
switch platform {
case "darwin":
err = setupDarwinSigning()
case "windows":
err = setupWindowsSigning()
case "linux":
err = setupLinuxSigning()
default:
pterm.Warning.Printfln("Unknown platform: %s", platform)
continue
}
if err != nil {
return err
}
}
return nil
}
func detectPlatforms() []string {
var platforms []string
for _, p := range []string{"darwin", "windows", "linux"} {
taskfile := filepath.Join("build", p, "Taskfile.yml")
if _, err := os.Stat(taskfile); err == nil {
platforms = append(platforms, p)
}
}
return platforms
}
func setupDarwinSigning() error {
pterm.DefaultHeader.Println("macOS Code Signing Setup")
fmt.Println()
// Get available signing identities
identities, err := getMacOSSigningIdentities()
if err != nil {
pterm.Warning.Printfln("Could not list signing identities: %v", err)
identities = []string{}
}
var signIdentity string
var keychainProfile string
var entitlements string
var configureNotarization bool
// Build identity options
var identityOptions []huh.Option[string]
for _, id := range identities {
identityOptions = append(identityOptions, huh.NewOption(id, id))
}
identityOptions = append(identityOptions, huh.NewOption("Enter manually...", "manual"))
form := huh.NewForm(
huh.NewGroup(
huh.NewSelect[string]().
Title("Select signing identity").
Description("Choose your Developer ID certificate").
Options(identityOptions...).
Value(&signIdentity),
).WithHideFunc(func() bool {
return len(identities) == 0
}),
huh.NewGroup(
huh.NewInput().
Title("Signing identity").
Description("e.g., Developer ID Application: Your Company (TEAMID)").
Placeholder("Developer ID Application: ...").
Value(&signIdentity),
).WithHideFunc(func() bool {
return len(identities) > 0 && signIdentity != "manual"
}),
huh.NewGroup(
huh.NewConfirm().
Title("Configure notarization?").
Description("Required for distributing apps outside the App Store").
Value(&configureNotarization),
),
huh.NewGroup(
huh.NewInput().
Title("Keychain profile name").
Description("The profile name used with 'wails3 signing credentials'").
Placeholder("my-notarize-profile").
Value(&keychainProfile),
).WithHideFunc(func() bool {
return !configureNotarization
}),
huh.NewGroup(
huh.NewInput().
Title("Entitlements file (optional)").
Description("Path to entitlements plist, leave empty to skip").
Placeholder("build/darwin/entitlements.plist").
Value(&entitlements),
),
)
err = form.Run()
if err != nil {
return err
}
// Handle manual entry
if signIdentity == "manual" {
signIdentity = ""
}
// Update Taskfile
taskfilePath := filepath.Join("build", "darwin", "Taskfile.yml")
err = updateTaskfileVars(taskfilePath, map[string]string{
"SIGN_IDENTITY": signIdentity,
"KEYCHAIN_PROFILE": keychainProfile,
"ENTITLEMENTS": entitlements,
})
if err != nil {
return err
}
pterm.Success.Printfln("Updated %s", taskfilePath)
if configureNotarization && keychainProfile != "" {
fmt.Println()
pterm.Info.Println("Next step: Store your notarization credentials:")
fmt.Println()
pterm.Println(pterm.LightBlue(fmt.Sprintf(` wails3 signing credentials \
--apple-id "your@email.com" \
--team-id "TEAMID" \
--password "app-specific-password" \
--profile "%s"`, keychainProfile)))
fmt.Println()
}
return nil
}
func setupWindowsSigning() error {
pterm.DefaultHeader.Println("Windows Code Signing Setup")
fmt.Println()
var certSource string
var certPath string
var certPassword string
var thumbprint string
var timestampServer string
form := huh.NewForm(
huh.NewGroup(
huh.NewSelect[string]().
Title("Certificate source").
Options(
huh.NewOption("Certificate file (.pfx/.p12)", "file"),
huh.NewOption("Windows certificate store (thumbprint)", "store"),
).
Value(&certSource),
),
huh.NewGroup(
huh.NewInput().
Title("Certificate path").
Description("Path to your .pfx or .p12 file").
Placeholder("certs/signing.pfx").
Value(&certPath),
huh.NewInput().
Title("Certificate password").
Description("Stored securely in system keychain").
EchoMode(huh.EchoModePassword).
Value(&certPassword),
).WithHideFunc(func() bool {
return certSource != "file"
}),
huh.NewGroup(
huh.NewInput().
Title("Certificate thumbprint").
Description("SHA-1 thumbprint of the certificate in Windows store").
Placeholder("ABC123DEF456...").
Value(&thumbprint),
).WithHideFunc(func() bool {
return certSource != "store"
}),
huh.NewGroup(
huh.NewInput().
Title("Timestamp server (optional)").
Description("Leave empty for default: http://timestamp.digicert.com").
Placeholder("http://timestamp.digicert.com").
Value(&timestampServer),
),
)
err := form.Run()
if err != nil {
return err
}
// Store password in keychain if provided
if certPassword != "" {
err = keychain.Set(keychain.KeyWindowsCertPassword, certPassword)
if err != nil {
return fmt.Errorf("failed to store password in keychain: %w", err)
}
pterm.Success.Println("Certificate password stored in system keychain")
}
// Update Taskfile (no passwords stored here)
taskfilePath := filepath.Join("build", "windows", "Taskfile.yml")
vars := map[string]string{
"TIMESTAMP_SERVER": timestampServer,
}
if certSource == "file" {
vars["SIGN_CERTIFICATE"] = certPath
} else {
vars["SIGN_THUMBPRINT"] = thumbprint
}
err = updateTaskfileVars(taskfilePath, vars)
if err != nil {
return err
}
pterm.Success.Printfln("Updated %s", taskfilePath)
return nil
}
func setupLinuxSigning() error {
pterm.DefaultHeader.Println("Linux Package Signing Setup")
fmt.Println()
var keySource string
var keyPath string
var keyPassword string
var signRole string
// For key generation
var genName string
var genEmail string
var genPassword string
form := huh.NewForm(
huh.NewGroup(
huh.NewSelect[string]().
Title("PGP key source").
Options(
huh.NewOption("Use existing key", "existing"),
huh.NewOption("Generate new key", "generate"),
).
Value(&keySource),
),
// Existing key options
huh.NewGroup(
huh.NewInput().
Title("PGP private key path").
Description("Path to your ASCII-armored private key file").
Placeholder("signing-key.asc").
Value(&keyPath),
huh.NewInput().
Title("Key password (if encrypted)").
Description("Stored securely in system keychain").
EchoMode(huh.EchoModePassword).
Value(&keyPassword),
).WithHideFunc(func() bool {
return keySource != "existing"
}),
// Key generation options
huh.NewGroup(
huh.NewInput().
Title("Name").
Description("Name for the PGP key").
Placeholder("Your Name").
Value(&genName).
Validate(func(s string) error {
if keySource == "generate" && s == "" {
return fmt.Errorf("name is required")
}
return nil
}),
huh.NewInput().
Title("Email").
Description("Email for the PGP key").
Placeholder("you@example.com").
Value(&genEmail).
Validate(func(s string) error {
if keySource == "generate" && s == "" {
return fmt.Errorf("email is required")
}
return nil
}),
huh.NewInput().
Title("Key password (optional but recommended)").
Description("Stored securely in system keychain").
EchoMode(huh.EchoModePassword).
Value(&genPassword),
).WithHideFunc(func() bool {
return keySource != "generate"
}),
huh.NewGroup(
huh.NewSelect[string]().
Title("DEB signing role").
Description("Role for signing Debian packages").
Options(
huh.NewOption("builder (default)", "builder"),
huh.NewOption("origin", "origin"),
huh.NewOption("maint", "maint"),
huh.NewOption("archive", "archive"),
).
Value(&signRole),
),
)
err := form.Run()
if err != nil {
return err
}
// Generate key if requested
if keySource == "generate" {
keyPath = "signing-key.asc"
pubKeyPath := "signing-key.pub.asc"
pterm.Info.Println("Generating PGP key pair...")
// Call the key generation
err = generatePGPKeyForSetup(genName, genEmail, genPassword, keyPath, pubKeyPath)
if err != nil {
return fmt.Errorf("failed to generate key: %w", err)
}
keyPassword = genPassword
pterm.Success.Printfln("Generated %s and %s", keyPath, pubKeyPath)
fmt.Println()
pterm.Info.Println("Distribute the public key to users so they can verify your packages:")
pterm.Println(pterm.LightBlue(fmt.Sprintf(" # For apt: sudo cp %s /etc/apt/trusted.gpg.d/", pubKeyPath)))
pterm.Println(pterm.LightBlue(fmt.Sprintf(" # For rpm: sudo rpm --import %s", pubKeyPath)))
fmt.Println()
}
// Store password in keychain if provided
if keyPassword != "" {
err = keychain.Set(keychain.KeyPGPPassword, keyPassword)
if err != nil {
return fmt.Errorf("failed to store password in keychain: %w", err)
}
pterm.Success.Println("PGP key password stored in system keychain")
}
// Update Taskfile (no passwords stored here)
taskfilePath := filepath.Join("build", "linux", "Taskfile.yml")
vars := map[string]string{
"PGP_KEY": keyPath,
}
if signRole != "" && signRole != "builder" {
vars["SIGN_ROLE"] = signRole
}
err = updateTaskfileVars(taskfilePath, vars)
if err != nil {
return err
}
pterm.Success.Printfln("Updated %s", taskfilePath)
return nil
}
// getMacOSSigningIdentities returns available signing identities on macOS
func getMacOSSigningIdentities() ([]string, error) {
if runtime.GOOS != "darwin" {
return nil, fmt.Errorf("not running on macOS")
}
// Run security find-identity to get available codesigning identities
cmd := exec.Command("security", "find-identity", "-v", "-p", "codesigning")
output, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("failed to run security find-identity: %w", err)
}
var identities []string
lines := strings.Split(string(output), "\n")
for _, line := range lines {
// Lines look like: 1) ABC123... "Developer ID Application: Company Name (TEAMID)"
// We want to extract the quoted part
if strings.Contains(line, "\"") {
start := strings.Index(line, "\"")
end := strings.LastIndex(line, "\"")
if start != -1 && end > start {
identity := line[start+1 : end]
// Filter for Developer ID certificates (most useful for distribution)
if strings.Contains(identity, "Developer ID") {
identities = append(identities, identity)
}
}
}
}
return identities, nil
}
// updateTaskfileVars updates the vars section of a Taskfile
func updateTaskfileVars(path string, vars map[string]string) error {
content, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("failed to read %s: %w", path, err)
}
lines := strings.Split(string(content), "\n")
var result []string
inVars := false
varsInserted := false
remainingVars := make(map[string]string)
for k, v := range vars {
remainingVars[k] = v
}
for i, line := range lines {
trimmed := strings.TrimSpace(line)
// Detect vars section
if trimmed == "vars:" {
inVars = true
result = append(result, line)
continue
}
// Detect end of vars section (next top-level key or tasks:)
if inVars && len(line) > 0 && line[0] != ' ' && line[0] != '\t' && !strings.HasPrefix(trimmed, "#") {
// Insert any remaining vars before leaving vars section
for k, v := range remainingVars {
if v != "" {
result = append(result, fmt.Sprintf(" %s: %q", k, v))
}
}
remainingVars = make(map[string]string)
varsInserted = true
inVars = false
}
if inVars {
// Check if this line is a var we want to update
updated := false
for k, v := range remainingVars {
commentedKey := "# " + k + ":"
uncommentedKey := k + ":"
if strings.Contains(trimmed, commentedKey) || strings.HasPrefix(trimmed, uncommentedKey) {
if v != "" {
// Uncomment and set value
result = append(result, fmt.Sprintf(" %s: %q", k, v))
} else {
// Keep as comment
result = append(result, line)
}
delete(remainingVars, k)
updated = true
break
}
}
if !updated {
result = append(result, line)
}
} else {
result = append(result, line)
}
// If we're at the end and haven't inserted vars yet, we need to add vars section
if i == len(lines)-1 && !varsInserted && len(remainingVars) > 0 {
// Find where to insert (after includes, before tasks)
// For simplicity, just append warning
pterm.Warning.Println("Could not find vars section in Taskfile, please add manually")
}
}
return os.WriteFile(path, []byte(strings.Join(result, "\n")), 0644)
}
// generatePGPKeyForSetup generates a PGP key pair for signing packages
func generatePGPKeyForSetup(name, email, password, privatePath, publicPath string) error {
// Create a new entity (key pair)
config := &packet.Config{
DefaultHash: 0, // Use default
DefaultCipher: 0, // Use default
DefaultCompressionAlgo: 0, // Use default
}
entity, err := openpgp.NewEntity(name, "", email, config)
if err != nil {
return fmt.Errorf("failed to create PGP entity: %w", err)
}
// Encrypt the private key if password is provided
if password != "" {
err = entity.PrivateKey.Encrypt([]byte(password))
if err != nil {
return fmt.Errorf("failed to encrypt private key: %w", err)
}
// Also encrypt subkeys
for _, subkey := range entity.Subkeys {
if subkey.PrivateKey != nil {
err = subkey.PrivateKey.Encrypt([]byte(password))
if err != nil {
return fmt.Errorf("failed to encrypt subkey: %w", err)
}
}
}
}
// Write private key
privateFile, err := os.Create(privatePath)
if err != nil {
return fmt.Errorf("failed to create private key file: %w", err)
}
defer privateFile.Close()
privateArmor, err := armor.Encode(privateFile, openpgp.PrivateKeyType, nil)
if err != nil {
return fmt.Errorf("failed to create armor encoder: %w", err)
}
err = entity.SerializePrivate(privateArmor, config)
if err != nil {
return fmt.Errorf("failed to serialize private key: %w", err)
}
privateArmor.Close()
// Write public key
publicFile, err := os.Create(publicPath)
if err != nil {
return fmt.Errorf("failed to create public key file: %w", err)
}
defer publicFile.Close()
publicArmor, err := armor.Encode(publicFile, openpgp.PublicKeyType, nil)
if err != nil {
return fmt.Errorf("failed to create armor encoder: %w", err)
}
err = entity.Serialize(publicArmor)
if err != nil {
return fmt.Errorf("failed to serialize public key: %w", err)
}
publicArmor.Close()
return nil
}

View file

@ -1,8 +1,9 @@
package commands
import (
"github.com/wailsapp/wails/v3/internal/term"
"os"
"runtime"
"strings"
"github.com/wailsapp/wails/v3/internal/flags"
)
@ -10,6 +11,13 @@ import (
// runTaskFunc is a variable to allow mocking in tests
var runTaskFunc = RunTask
// validPlatforms for GOOS
var validPlatforms = map[string]bool{
"windows": true,
"darwin": true,
"linux": true,
}
func Build(_ *flags.Build, otherArgs []string) error {
return wrapTask("build", otherArgs)
}
@ -18,12 +26,48 @@ func Package(_ *flags.Package, otherArgs []string) error {
return wrapTask("package", otherArgs)
}
func wrapTask(command string, otherArgs []string) error {
term.Warningf("`wails3 %s` is an alias for `wails3 task %s`. Use `wails task` for better control and more options.\n", command, command)
// Rebuild os.Args to include the command and all additional arguments
newArgs := []string{"wails3", "task", command}
newArgs = append(newArgs, otherArgs...)
os.Args = newArgs
// Pass the task name via options and otherArgs as CLI variables
return runTaskFunc(&RunTaskOptions{Name: command}, otherArgs)
func SignWrapper(_ *flags.SignWrapper, otherArgs []string) error {
return wrapTask("sign", otherArgs)
}
func wrapTask(action string, otherArgs []string) error {
// Check environment first, then allow args to override
goos := os.Getenv("GOOS")
if goos == "" {
goos = runtime.GOOS
}
goarch := os.Getenv("GOARCH")
if goarch == "" {
goarch = runtime.GOARCH
}
var remainingArgs []string
// Args override environment
for _, arg := range otherArgs {
switch {
case strings.HasPrefix(arg, "GOOS="):
goos = strings.TrimPrefix(arg, "GOOS=")
case strings.HasPrefix(arg, "GOARCH="):
goarch = strings.TrimPrefix(arg, "GOARCH=")
default:
remainingArgs = append(remainingArgs, arg)
}
}
// Determine task name based on GOOS
taskName := action
if validPlatforms[goos] {
taskName = goos + ":" + action
}
// Pass ARCH to task (always set, defaults to current architecture)
remainingArgs = append(remainingArgs, "ARCH="+goarch)
// Rebuild os.Args to include the command and all additional arguments
newArgs := []string{"wails3", "task", taskName}
newArgs = append(newArgs, remainingArgs...)
os.Args = newArgs
// Pass the task name via options and remainingArgs as CLI variables
return runTaskFunc(&RunTaskOptions{Name: taskName}, remainingArgs)
}

View file

@ -0,0 +1,31 @@
package commands
import (
"fmt"
"github.com/konoui/lipo/pkg/lipo"
"github.com/wailsapp/wails/v3/internal/flags"
)
func ToolLipo(options *flags.Lipo) error {
DisableFooter = true
if len(options.Inputs) < 2 {
return fmt.Errorf("lipo requires at least 2 input files")
}
if options.Output == "" {
return fmt.Errorf("output file is required (-output)")
}
l := lipo.New(
lipo.WithInputs(options.Inputs...),
lipo.WithOutput(options.Output),
)
if err := l.Create(); err != nil {
return fmt.Errorf("failed to create universal binary: %w", err)
}
return nil
}

View file

@ -29,4 +29,33 @@ func checkCommonDependencies(result map[string]string, ok *bool) {
}
}
result["npm"] = string(npmVersion)
// Check for Docker (optional - used for macOS cross-compilation from Linux)
checkDocker(result)
}
func checkDocker(result map[string]string) {
dockerVersion, err := exec.Command("docker", "--version").Output()
if err != nil {
result["docker"] = "*Not installed (optional - for cross-compilation)"
return
}
// Check if Docker daemon is running
_, err = exec.Command("docker", "info").Output()
if err != nil {
version := strings.TrimSpace(string(dockerVersion))
result["docker"] = "*" + version + " (daemon not running)"
return
}
version := strings.TrimSpace(string(dockerVersion))
// Check for the unified cross-compilation image
imageCheck, _ := exec.Command("docker", "image", "inspect", "wails-cross").Output()
if len(imageCheck) == 0 {
result["docker"] = "*" + version + " (wails-cross image not built - run: wails3 task setup:docker)"
} else {
result["docker"] = "*" + version + " (cross-compilation ready)"
}
}

12
v3/internal/flags/lipo.go Normal file
View file

@ -0,0 +1,12 @@
package flags
// Lipo represents the options for creating macOS universal binaries
type Lipo struct {
Common
// Output is the path for the universal binary
Output string `name:"output" short:"o" description:"Output path for the universal binary" default:""`
// Inputs are the architecture-specific binaries to combine
Inputs []string `name:"input" short:"i" description:"Input binaries to combine (specify multiple times)" default:""`
}

26
v3/internal/flags/sign.go Normal file
View file

@ -0,0 +1,26 @@
package flags
// Sign contains flags for the sign command
type Sign struct {
Input string `name:"input" description:"Path to the file to sign"`
Output string `name:"output" description:"Output path (optional, defaults to in-place signing)"`
Verbose bool `name:"verbose" description:"Enable verbose output"`
// Windows/macOS certificate signing
Certificate string `name:"certificate" description:"Path to PKCS#12 (.pfx/.p12) certificate file"`
Password string `name:"password" description:"Certificate password (reads from keychain if not provided)"`
Thumbprint string `name:"thumbprint" description:"Certificate thumbprint in Windows certificate store"`
Timestamp string `name:"timestamp" description:"Timestamp server URL"`
// macOS specific
Identity string `name:"identity" description:"Signing identity (e.g., 'Developer ID Application: ...')"`
Entitlements string `name:"entitlements" description:"Path to entitlements plist file"`
HardenedRuntime bool `name:"hardened-runtime" description:"Enable hardened runtime (default: true for notarization)"`
Notarize bool `name:"notarize" description:"Submit for Apple notarization after signing"`
KeychainProfile string `name:"keychain-profile" description:"Keychain profile for notarization credentials"`
// Linux PGP signing
PGPKey string `name:"pgp-key" description:"Path to PGP private key file"`
PGPPassword string `name:"pgp-password" description:"PGP key password (reads from keychain if not provided)"`
Role string `name:"role" description:"DEB signing role (origin, maint, archive, builder)"`
}

View file

@ -0,0 +1,9 @@
package flags
type SigningSetup struct {
Platforms []string `name:"platform" description:"Platform(s) to configure (darwin, windows, linux). If not specified, auto-detects from build directory."`
}
type EntitlementsSetup struct {
Output string `name:"output" description:"Output path for entitlements.plist (default: build/darwin/entitlements.plist)"`
}

View file

@ -11,3 +11,7 @@ type Dev struct {
type Package struct {
Common
}
type SignWrapper struct {
Common
}

View file

@ -0,0 +1,89 @@
// Package keychain provides secure credential storage using the system keychain.
// On macOS it uses Keychain, on Windows it uses Credential Manager,
// and on Linux it uses Secret Service (via D-Bus).
package keychain
import (
"fmt"
"os"
"github.com/zalando/go-keyring"
)
const (
// ServiceName is the service identifier used for all Wails credentials
ServiceName = "wails"
// Credential keys
KeyWindowsCertPassword = "windows-cert-password"
KeyPGPPassword = "pgp-password"
)
// Set stores a credential in the system keychain.
// The credential is identified by a key and can be retrieved later with Get.
func Set(key, value string) error {
err := keyring.Set(ServiceName, key, value)
if err != nil {
return fmt.Errorf("failed to store credential in keychain: %w", err)
}
return nil
}
// Get retrieves a credential from the system keychain.
// Returns the value and nil error if found, or empty string and error if not found.
// Also checks environment variables as a fallback (useful for CI).
func Get(key string) (string, error) {
// First check environment variable (for CI/automation)
envKey := "WAILS_" + toEnvName(key)
if val := os.Getenv(envKey); val != "" {
return val, nil
}
// Try keychain
value, err := keyring.Get(ServiceName, key)
if err != nil {
if err == keyring.ErrNotFound {
return "", fmt.Errorf("credential %q not found in keychain (set with: wails3 setup signing, or set env var %s)", key, envKey)
}
return "", fmt.Errorf("failed to retrieve credential from keychain: %w", err)
}
return value, nil
}
// Delete removes a credential from the system keychain.
func Delete(key string) error {
err := keyring.Delete(ServiceName, key)
if err != nil && err != keyring.ErrNotFound {
return fmt.Errorf("failed to delete credential from keychain: %w", err)
}
return nil
}
// Exists checks if a credential exists in the keychain or environment.
func Exists(key string) bool {
// Check environment variable first
envKey := "WAILS_" + toEnvName(key)
if os.Getenv(envKey) != "" {
return true
}
// Check keychain
_, err := keyring.Get(ServiceName, key)
return err == nil
}
// toEnvName converts a key to an environment variable name.
// e.g., "windows-cert-password" -> "WINDOWS_CERT_PASSWORD"
func toEnvName(key string) string {
result := make([]byte, len(key))
for i, c := range key {
if c == '-' {
result[i] = '_'
} else if c >= 'a' && c <= 'z' {
result[i] = byte(c - 'a' + 'A')
} else {
result[i] = byte(c)
}
}
return string(result)
}

View file

@ -32,3 +32,7 @@ tasks:
cmds:
- wails3 dev -config ./build/config.yml -port {{ "{{.VITE_PORT}}" }}
setup:docker:
summary: Builds Docker image for cross-compilation (~800MB download)
cmds:
- task: common:setup:docker