diff --git a/docs/Taskfile.yml b/docs/Taskfile.yml index be83682f4..9f7c01fd6 100644 --- a/docs/Taskfile.yml +++ b/docs/Taskfile.yml @@ -4,7 +4,7 @@ version: '3' vars: # Change this to switch package managers: bun, npm, pnpm, yarn - PKG_MANAGER: bun + PKG_MANAGER: npm tasks: diff --git a/docs/src/content/docs/guides/auto-updates.mdx b/docs/src/content/docs/guides/auto-updates.mdx deleted file mode 100644 index 685850413..000000000 --- a/docs/src/content/docs/guides/auto-updates.mdx +++ /dev/null @@ -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 diff --git a/docs/src/content/docs/guides/build/building.mdx b/docs/src/content/docs/guides/build/building.mdx new file mode 100644 index 000000000..64a82b552 --- /dev/null +++ b/docs/src/content/docs/guides/build/building.mdx @@ -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. + + + +## 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 +``` diff --git a/docs/src/content/docs/guides/build/cross-platform.mdx b/docs/src/content/docs/guides/build/cross-platform.mdx new file mode 100644 index 000000000..0685834d4 --- /dev/null +++ b/docs/src/content/docs/guides/build/cross-platform.mdx @@ -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 diff --git a/docs/src/content/docs/guides/build/customization.mdx b/docs/src/content/docs/guides/build/customization.mdx index 041738ddf..993fb1ee5 100644 --- a/docs/src/content/docs/guides/build/customization.mdx +++ b/docs/src/content/docs/guides/build/customization.mdx @@ -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"; diff --git a/docs/src/content/docs/guides/build/linux.mdx b/docs/src/content/docs/guides/build/linux.mdx new file mode 100644 index 000000000..9d8fac890 --- /dev/null +++ b/docs/src/content/docs/guides/build/linux.mdx @@ -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 +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 +``` + + + +## 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. diff --git a/docs/src/content/docs/guides/build/macos.mdx b/docs/src/content/docs/guides/build/macos.mdx new file mode 100644 index 000000000..1d820daef --- /dev/null +++ b/docs/src/content/docs/guides/build/macos.mdx @@ -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/.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 +``` + + + +## 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 +``` diff --git a/docs/src/content/docs/guides/build/signing.mdx b/docs/src/content/docs/guides/build/signing.mdx new file mode 100644 index 000000000..3f6a03382 --- /dev/null +++ b/docs/src/content/docs/guides/build/signing.mdx @@ -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 | ✅ | ✅ | ✅ | + + + +### 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 + + + +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: + + + +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 +``` + + +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 +``` + + +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 +``` + + + +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... +``` + + + +### 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 | + + + +Then set `ENTITLEMENTS` in your Taskfile vars to point to the appropriate file. + +### Notarization + +Apple requires all distributed apps to be notarized. + + +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 + ``` + + + + +## 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) | + + + +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 + + + +### 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) | + + + +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 + 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 +``` + + + +## 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 `: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 +``` + +## 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 --keychain-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) diff --git a/docs/src/content/docs/guides/build/windows.mdx b/docs/src/content/docs/guides/build/windows.mdx index 8e04ea0bd..fe4183cb4 100644 --- a/docs/src/content/docs/guides/build/windows.mdx +++ b/docs/src/content/docs/guides/build/windows.mdx @@ -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/---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/-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/-.msix` -**No additional configuration needed!** + -### 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\` | -| `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/--.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/.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. diff --git a/docs/src/content/docs/guides/building.mdx b/docs/src/content/docs/guides/building.mdx deleted file mode 100644 index cf6e9ef97..000000000 --- a/docs/src/content/docs/guides/building.mdx +++ /dev/null @@ -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 - - - - ```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 - ``` - - - - ```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 - ``` - - - - ```bash - # Linux executable - wails3 build -platform linux/amd64 - - # With icon - wails3 build -platform linux/amd64 -icon icon.png - ``` - - - -## 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 diff --git a/docs/src/content/docs/guides/cross-platform.mdx b/docs/src/content/docs/guides/cross-platform.mdx deleted file mode 100644 index 190a2a11f..000000000 --- a/docs/src/content/docs/guides/cross-platform.mdx +++ /dev/null @@ -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 diff --git a/docs/src/content/docs/guides/distribution/auto-updates.mdx b/docs/src/content/docs/guides/distribution/auto-updates.mdx new file mode 100644 index 000000000..273915653 --- /dev/null +++ b/docs/src/content/docs/guides/distribution/auto-updates.mdx @@ -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. + + + + Configure periodic update checks in the background + + + Download only what changed with bsdiff patches + + + Works on macOS, Windows, and Linux + + + SHA256 checksums and optional signature verification + + + +## 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; + + // Download the update (emits progress events) + DownloadUpdate(): Promise; + + // Apply the downloaded update (restarts app) + ApplyUpdate(): Promise; + + // Download and apply in one call + DownloadAndApply(): Promise; + + // 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(null); + const [downloading, setDownloading] = useState(false); + const [progress, setProgress] = useState(null); + const [error, setError] = useState(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
Checking for updates...
; + } + + if (error) { + return ( +
+

Error: {error}

+ +
+ ); + } + + if (!updateInfo) { + return ( +
+

You're up to date! (v{updater.GetCurrentVersion()})

+ +
+ ); + } + + if (downloading) { + return ( +
+

Downloading update...

+ {progress && ( +
+ +

{progress.percentage.toFixed(1)}%

+

{(progress.bytesPerSecond / 1024 / 1024).toFixed(2)} MB/s

+
+ )} +
+ ); + } + + return ( +
+

Update Available!

+

Version {updateInfo.version} is available

+

Size: {updateInfo.hasPatch + ? `${(updateInfo.patchSize / 1024).toFixed(0)} KB (patch)` + : `${(updateInfo.size / 1024 / 1024).toFixed(1)} MB`} +

+
+ + +
+ ); +} +``` + +## 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 +``` + + + +## 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: + + +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 + ) + ``` + + +## 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 diff --git a/docs/src/content/docs/guides/msix-packaging.mdx b/docs/src/content/docs/guides/msix-packaging.mdx deleted file mode 100644 index 8b9ac57a1..000000000 --- a/docs/src/content/docs/guides/msix-packaging.mdx +++ /dev/null @@ -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-.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` | `.msix` | Output path / filename. | -| `--publisher` | `CN=` | 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! diff --git a/docs/src/content/docs/guides/signing.mdx b/docs/src/content/docs/guides/signing.mdx deleted file mode 100644 index b5de768c9..000000000 --- a/docs/src/content/docs/guides/signing.mdx +++ /dev/null @@ -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. - - - - Sign your Windows executables with certificates - - - Sign and notarize your macOS applications - - - -## Windows Code Signing - - -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 - ``` - - -### 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 - - -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 - ``` - - -### 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) diff --git a/docs/src/content/docs/reference/updater.mdx b/docs/src/content/docs/reference/updater.mdx new file mode 100644 index 000000000..e4b2204c3 --- /dev/null +++ b/docs/src/content/docs/reference/updater.mdx @@ -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 +``` + +Checks for available updates. + +**Returns:** `UpdateInfo` if an update is available, `null` otherwise. + +#### DownloadUpdate + +```typescript +function DownloadUpdate(): Promise +``` + +Downloads the available update. Emits `updater:progress` events. + +#### ApplyUpdate + +```typescript +function ApplyUpdate(): Promise +``` + +Applies the downloaded update. The application will restart. + +#### DownloadAndApply + +```typescript +function DownloadAndApply(): Promise +``` + +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. diff --git a/docs/src/content/docs/tutorials/04-distributing-your-app.mdx b/docs/src/content/docs/tutorials/04-distributing-your-app.mdx new file mode 100644 index 000000000..111e9f1c4 --- /dev/null +++ b/docs/src/content/docs/tutorials/04-distributing-your-app.mdx @@ -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(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 ( +
+ {downloading ? ( +

Downloading update... {progress.toFixed(0)}%

+ ) : ( + <> +

Version {update.version} is available!

+ + + )} +
+ ); +} +``` + +## Step 3: Build for Production + + + + +### 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" +``` + + + + +### 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" +``` + + + + +### 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 +``` + + + + +## Step 4: Create Update Archives + +For the updater, you need to create update archives: + + + + +```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 +``` + + + + +```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 +``` + + + + +```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 +``` + + + + +## 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 + + +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 + + +### 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: + + +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 + + +## 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 diff --git a/v3/cmd/wails3/main.go b/v3/cmd/wails3/main.go index c5e966278..cbf181572 100644 --- a/v3/cmd/wails3/main.go +++ b/v3/cmd/wails3/main.go @@ -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) diff --git a/v3/go.mod b/v3/go.mod index 897fb9abf..c72ffbb80 100644 --- a/v3/go.mod +++ b/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 diff --git a/v3/go.sum b/v3/go.sum index 2d287bdf8..ace8e0bc9 100644 --- a/v3/go.sum +++ b/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= diff --git a/v3/internal/commands/build_assets/Taskfile.tmpl.yml b/v3/internal/commands/build_assets/Taskfile.tmpl.yml index c72601659..8deb266f2 100644 --- a/v3/internal/commands/build_assets/Taskfile.tmpl.yml +++ b/v3/internal/commands/build_assets/Taskfile.tmpl.yml @@ -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." + diff --git a/v3/internal/commands/build_assets/darwin/Taskfile.yml b/v3/internal/commands/build_assets/darwin/Taskfile.yml index f0791fea9..3458c96ef 100644 --- a/v3/internal/commands/build_assets/darwin/Taskfile.yml +++ b/v3/internal/commands/build_assets/darwin/Taskfile.yml @@ -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" diff --git a/v3/internal/commands/build_assets/docker/Dockerfile.cross b/v3/internal/commands/build_assets/docker/Dockerfile.cross new file mode 100644 index 000000000..e1c05a05d --- /dev/null +++ b/v3/internal/commands/build_assets/docker/Dockerfile.cross @@ -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: "; 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"] diff --git a/v3/internal/commands/build_assets/linux/Taskfile.yml b/v3/internal/commands/build_assets/linux/Taskfile.yml index 7ddf9f359..ce78c16dd 100644 --- a/v3/internal/commands/build_assets/linux/Taskfile.yml +++ b/v3/internal/commands/build_assets/linux/Taskfile.yml @@ -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" diff --git a/v3/internal/commands/build_assets/windows/Taskfile.yml b/v3/internal/commands/build_assets/windows/Taskfile.yml index c81aa66a9..1ca955f67 100644 --- a/v3/internal/commands/build_assets/windows/Taskfile.yml +++ b/v3/internal/commands/build_assets/windows/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" diff --git a/v3/internal/commands/entitlements_setup.go b/v3/internal/commands/entitlements_setup.go new file mode 100644 index 000000000..44633189d --- /dev/null +++ b/v3/internal/commands/entitlements_setup.go @@ -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, "") && strings.HasSuffix(line, "") { + key := strings.TrimPrefix(line, "") + key = strings.TrimSuffix(key, "") + + // Check if next line is + if i+1 < len(lines) { + nextLine := strings.TrimSpace(lines[i+1]) + if nextLine == "" { + result[key] = true + } + } + } + } + + return result, nil +} + +func generateEntitlementsPlist(entitlements []string) string { + var sb strings.Builder + + sb.WriteString(` + + + +`) + + for _, key := range entitlements { + sb.WriteString(fmt.Sprintf("\t%s\n", key)) + sb.WriteString("\t\n") + } + + sb.WriteString(` + +`) + + return sb.String() +} diff --git a/v3/internal/commands/sign.go b/v3/internal/commands/sign.go new file mode 100644 index 000000000..b862f83fa --- /dev/null +++ b/v3/internal/commands/sign.go @@ -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 +} diff --git a/v3/internal/commands/signing_setup.go b/v3/internal/commands/signing_setup.go new file mode 100644 index 000000000..7bbe64817 --- /dev/null +++ b/v3/internal/commands/signing_setup.go @@ -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 +} diff --git a/v3/internal/commands/task_wrapper.go b/v3/internal/commands/task_wrapper.go index 4e4073090..908c0e53c 100644 --- a/v3/internal/commands/task_wrapper.go +++ b/v3/internal/commands/task_wrapper.go @@ -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) } diff --git a/v3/internal/commands/tool_lipo.go b/v3/internal/commands/tool_lipo.go new file mode 100644 index 000000000..a33ebd777 --- /dev/null +++ b/v3/internal/commands/tool_lipo.go @@ -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 +} diff --git a/v3/internal/doctor/doctor_common.go b/v3/internal/doctor/doctor_common.go index 4befcffe8..dab765586 100644 --- a/v3/internal/doctor/doctor_common.go +++ b/v3/internal/doctor/doctor_common.go @@ -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)" + } } diff --git a/v3/internal/flags/lipo.go b/v3/internal/flags/lipo.go new file mode 100644 index 000000000..90f2893fd --- /dev/null +++ b/v3/internal/flags/lipo.go @@ -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:""` +} diff --git a/v3/internal/flags/sign.go b/v3/internal/flags/sign.go new file mode 100644 index 000000000..9e8442405 --- /dev/null +++ b/v3/internal/flags/sign.go @@ -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)"` +} diff --git a/v3/internal/flags/signing.go b/v3/internal/flags/signing.go new file mode 100644 index 000000000..72dd71f3c --- /dev/null +++ b/v3/internal/flags/signing.go @@ -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)"` +} diff --git a/v3/internal/flags/task_wrapper.go b/v3/internal/flags/task_wrapper.go index 4a6ade362..d77535f6c 100644 --- a/v3/internal/flags/task_wrapper.go +++ b/v3/internal/flags/task_wrapper.go @@ -11,3 +11,7 @@ type Dev struct { type Package struct { Common } + +type SignWrapper struct { + Common +} diff --git a/v3/internal/keychain/keychain.go b/v3/internal/keychain/keychain.go new file mode 100644 index 000000000..67e99e5ac --- /dev/null +++ b/v3/internal/keychain/keychain.go @@ -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) +} diff --git a/v3/internal/templates/_common/Taskfile.tmpl.yml b/v3/internal/templates/_common/Taskfile.tmpl.yml index f075aa3ea..93648708a 100644 --- a/v3/internal/templates/_common/Taskfile.tmpl.yml +++ b/v3/internal/templates/_common/Taskfile.tmpl.yml @@ -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