mirror of
https://github.com/wailsapp/wails.git
synced 2026-03-14 14:45:49 +01:00
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:
parent
f8175363cf
commit
3594b77666
36 changed files with 5286 additions and 1330 deletions
|
|
@ -4,7 +4,7 @@ version: '3'
|
|||
|
||||
vars:
|
||||
# Change this to switch package managers: bun, npm, pnpm, yarn
|
||||
PKG_MANAGER: bun
|
||||
PKG_MANAGER: npm
|
||||
|
||||
tasks:
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
104
docs/src/content/docs/guides/build/building.mdx
Normal file
104
docs/src/content/docs/guides/build/building.mdx
Normal 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
|
||||
```
|
||||
298
docs/src/content/docs/guides/build/cross-platform.mdx
Normal file
298
docs/src/content/docs/guides/build/cross-platform.mdx
Normal 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
|
||||
|
|
@ -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";
|
||||
|
|
|
|||
147
docs/src/content/docs/guides/build/linux.mdx
Normal file
147
docs/src/content/docs/guides/build/linux.mdx
Normal 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.
|
||||
132
docs/src/content/docs/guides/build/macos.mdx
Normal file
132
docs/src/content/docs/guides/build/macos.mdx
Normal 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
|
||||
```
|
||||
823
docs/src/content/docs/guides/build/signing.mdx
Normal file
823
docs/src/content/docs/guides/build/signing.mdx
Normal 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)
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
564
docs/src/content/docs/guides/distribution/auto-updates.mdx
Normal file
564
docs/src/content/docs/guides/distribution/auto-updates.mdx
Normal 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
|
||||
|
|
@ -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 SDK’s *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!
|
||||
|
|
@ -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)
|
||||
501
docs/src/content/docs/reference/updater.mdx
Normal file
501
docs/src/content/docs/reference/updater.mdx
Normal 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.
|
||||
532
docs/src/content/docs/tutorials/04-distributing-your-app.mdx
Normal file
532
docs/src/content/docs/tutorials/04-distributing-your-app.mdx
Normal 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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
24
v3/go.mod
24
v3/go.mod
|
|
@ -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
|
||||
|
|
|
|||
67
v3/go.sum
67
v3/go.sum
|
|
@ -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=
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
195
v3/internal/commands/build_assets/docker/Dockerfile.cross
Normal file
195
v3/internal/commands/build_assets/docker/Dockerfile.cross
Normal 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"]
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
241
v3/internal/commands/entitlements_setup.go
Normal file
241
v3/internal/commands/entitlements_setup.go
Normal 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()
|
||||
}
|
||||
396
v3/internal/commands/sign.go
Normal file
396
v3/internal/commands/sign.go
Normal 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
|
||||
}
|
||||
588
v3/internal/commands/signing_setup.go
Normal file
588
v3/internal/commands/signing_setup.go
Normal 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(×tampServer),
|
||||
),
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
31
v3/internal/commands/tool_lipo.go
Normal file
31
v3/internal/commands/tool_lipo.go
Normal 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
|
||||
}
|
||||
|
|
@ -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
12
v3/internal/flags/lipo.go
Normal 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
26
v3/internal/flags/sign.go
Normal 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)"`
|
||||
}
|
||||
9
v3/internal/flags/signing.go
Normal file
9
v3/internal/flags/signing.go
Normal 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)"`
|
||||
}
|
||||
|
|
@ -11,3 +11,7 @@ type Dev struct {
|
|||
type Package struct {
|
||||
Common
|
||||
}
|
||||
|
||||
type SignWrapper struct {
|
||||
Common
|
||||
}
|
||||
|
|
|
|||
89
v3/internal/keychain/keychain.go
Normal file
89
v3/internal/keychain/keychain.go
Normal 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)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue