mirror of
https://github.com/wailsapp/wails.git
synced 2026-03-14 14:45:49 +01:00
Merge branch 'v3-alpha' into refactor/runtime-init-scripts
This commit is contained in:
commit
c0862bf6d4
22 changed files with 840 additions and 278 deletions
334
.github/workflows/build-cross-image.yml
vendored
334
.github/workflows/build-cross-image.yml
vendored
|
|
@ -94,10 +94,18 @@ jobs:
|
|||
build-args: |
|
||||
ZIG_VERSION=${{ steps.vars.outputs.zig_version }}
|
||||
MACOS_SDK_VERSION=${{ steps.vars.outputs.sdk_version }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
cache-from: |
|
||||
type=gha
|
||||
type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache
|
||||
cache-to: |
|
||||
type=gha,mode=max
|
||||
type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache,mode=max
|
||||
|
||||
# Test cross-compilation for all platforms
|
||||
# Test cross-compilation using wails3 task system (3 parallel jobs)
|
||||
# Runs on Linux/amd64 runner - tests actual cross-compilation only:
|
||||
# - darwin: cross-platform (arm64)
|
||||
# - windows: cross-platform (arm64)
|
||||
# - linux: cross-architecture (arm64 from amd64 runner)
|
||||
test-cross-compile:
|
||||
needs: build
|
||||
if: ${{ inputs.skip_tests != 'true' }}
|
||||
|
|
@ -106,33 +114,24 @@ jobs:
|
|||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
# Darwin targets (Zig + macOS SDK) - no platform emulation needed
|
||||
# Darwin arm64 (Intel Macs are EOL, skip amd64)
|
||||
- os: darwin
|
||||
arch: arm64
|
||||
platform: ""
|
||||
expected_file: "Mach-O 64-bit.*arm64"
|
||||
- os: darwin
|
||||
arch: amd64
|
||||
platform: ""
|
||||
expected_file: "Mach-O 64-bit.*x86_64"
|
||||
# Linux targets (GCC) - need platform to match architecture
|
||||
- os: linux
|
||||
arch: amd64
|
||||
platform: "linux/amd64"
|
||||
expected_file: "ELF 64-bit LSB.*x86-64"
|
||||
- os: linux
|
||||
arch: arm64
|
||||
platform: "linux/arm64"
|
||||
expected_file: "ELF 64-bit LSB.*ARM aarch64"
|
||||
# Windows targets (Zig + mingw) - no platform emulation needed
|
||||
expected: "Mach-O 64-bit.*arm64"
|
||||
# Windows - both archs
|
||||
- os: windows
|
||||
arch: amd64
|
||||
platform: ""
|
||||
expected_file: "PE32\\+ executable.*x86-64"
|
||||
expected: "PE32.*x86-64"
|
||||
- os: windows
|
||||
arch: arm64
|
||||
platform: ""
|
||||
expected_file: "PE32\\+ executable.*Aarch64"
|
||||
expected: "PE32.*Aarch64"
|
||||
# Linux - both archs via Docker
|
||||
- os: linux
|
||||
arch: amd64
|
||||
expected: "ELF 64-bit LSB.*x86-64"
|
||||
- os: linux
|
||||
arch: arm64
|
||||
expected: "ELF 64-bit LSB.*ARM aarch64"
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
|
@ -140,8 +139,29 @@ jobs:
|
|||
with:
|
||||
ref: ${{ inputs.branch || github.ref }}
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.24'
|
||||
cache: false
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install Linux dev dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev
|
||||
|
||||
- name: Install wails3 CLI
|
||||
run: |
|
||||
cd v3
|
||||
go install ./cmd/wails3
|
||||
|
||||
- name: Set up QEMU
|
||||
if: matrix.platform != ''
|
||||
if: matrix.os == 'linux'
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Log in to Container Registry
|
||||
|
|
@ -151,95 +171,74 @@ jobs:
|
|||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Create test CGO project
|
||||
- name: Pull and tag Docker image
|
||||
run: |
|
||||
mkdir -p test-project
|
||||
cd test-project
|
||||
# Pull both platform variants and tag them for the task system
|
||||
# The task uses --platform flag which requires matching arch variant
|
||||
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.build.outputs.image_tag || 'latest' }}"
|
||||
|
||||
# Create a minimal CGO test program
|
||||
cat > main.go << 'EOF'
|
||||
package main
|
||||
|
||||
/*
|
||||
#include <stdlib.h>
|
||||
|
||||
int add(int a, int b) {
|
||||
return a + b;
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
result := C.add(1, 2)
|
||||
fmt.Printf("CGO test: 1 + 2 = %d\n", result)
|
||||
}
|
||||
EOF
|
||||
|
||||
cat > go.mod << 'EOF'
|
||||
module test-cgo
|
||||
|
||||
go 1.21
|
||||
EOF
|
||||
|
||||
- name: Build ${{ matrix.os }}/${{ matrix.arch }} (CGO)
|
||||
run: |
|
||||
cd test-project
|
||||
PLATFORM_FLAG=""
|
||||
if [ -n "${{ matrix.platform }}" ]; then
|
||||
PLATFORM_FLAG="--platform ${{ matrix.platform }}"
|
||||
# For Linux arm64 test, we need the arm64 variant
|
||||
if [ "${{ matrix.os }}" = "linux" ] && [ "${{ matrix.arch }}" = "arm64" ]; then
|
||||
docker pull --platform linux/arm64 "$IMAGE"
|
||||
docker tag "$IMAGE" wails-cross
|
||||
else
|
||||
docker pull "$IMAGE"
|
||||
docker tag "$IMAGE" wails-cross
|
||||
fi
|
||||
|
||||
docker run --rm $PLATFORM_FLAG \
|
||||
-v "$(pwd):/app" \
|
||||
-e APP_NAME="test-cgo" \
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.build.outputs.image_tag || 'latest' }} \
|
||||
${{ matrix.os }} ${{ matrix.arch }}
|
||||
|
||||
- name: Verify binary format
|
||||
- name: Generate wails3 test project
|
||||
run: |
|
||||
cd test-project/bin
|
||||
cd /tmp
|
||||
wails3 init -n test-wails-app -t vanilla
|
||||
cd test-wails-app
|
||||
|
||||
# Update replace directive to use absolute path (wails3 init adds relative path)
|
||||
sed -i 's|replace github.com/wailsapp/wails/v3 => .*|replace github.com/wailsapp/wails/v3 => ${{ github.workspace }}/v3|' go.mod
|
||||
|
||||
- name: Cross-compile ${{ matrix.os }}/${{ matrix.arch }} via task
|
||||
run: |
|
||||
cd /tmp/test-wails-app
|
||||
# For Linux, always use docker build to test the Docker image
|
||||
if [ "${{ matrix.os }}" = "linux" ]; then
|
||||
wails3 task linux:build:docker ARCH=${{ matrix.arch }}
|
||||
else
|
||||
wails3 task ${{ matrix.os }}:build ARCH=${{ matrix.arch }}
|
||||
fi
|
||||
|
||||
- name: Verify binary
|
||||
run: |
|
||||
cd /tmp/test-wails-app/bin
|
||||
ls -la
|
||||
|
||||
# Find the built binary
|
||||
if [ "${{ matrix.os }}" = "windows" ]; then
|
||||
BINARY=$(ls test-cgo-${{ matrix.os }}-${{ matrix.arch }}.exe 2>/dev/null || ls *.exe | head -1)
|
||||
BINARY="test-wails-app.exe"
|
||||
else
|
||||
BINARY=$(ls test-cgo-${{ matrix.os }}-${{ matrix.arch }} 2>/dev/null || ls test-cgo* | grep -v '.exe' | head -1)
|
||||
BINARY="test-wails-app"
|
||||
fi
|
||||
|
||||
echo "Binary: $BINARY"
|
||||
echo "Checking: $BINARY"
|
||||
FILE_OUTPUT=$(file "$BINARY")
|
||||
echo "File output: $FILE_OUTPUT"
|
||||
echo " $FILE_OUTPUT"
|
||||
|
||||
# Verify the binary format matches expected
|
||||
if echo "$FILE_OUTPUT" | grep -qE "${{ matrix.expected_file }}"; then
|
||||
echo "✅ Binary format verified: ${{ matrix.os }}/${{ matrix.arch }}"
|
||||
if echo "$FILE_OUTPUT" | grep -qE "${{ matrix.expected }}"; then
|
||||
echo " ✅ Cross-compilation verified: ${{ matrix.os }}/${{ matrix.arch }}"
|
||||
else
|
||||
echo "❌ Binary format mismatch!"
|
||||
echo "Expected pattern: ${{ matrix.expected_file }}"
|
||||
echo "Got: $FILE_OUTPUT"
|
||||
echo " ❌ Format mismatch! Expected: ${{ matrix.expected }}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Check library dependencies (Linux only)
|
||||
if: matrix.os == 'linux'
|
||||
run: |
|
||||
cd test-project/bin
|
||||
BINARY=$(ls test-cgo-${{ matrix.os }}-${{ matrix.arch }} 2>/dev/null || ls test-cgo* | grep -v '.exe' | head -1)
|
||||
cd /tmp/test-wails-app/bin
|
||||
|
||||
echo "## Library Dependencies for $BINARY"
|
||||
echo "## Library Dependencies"
|
||||
echo ""
|
||||
|
||||
# Use readelf to show dynamic dependencies
|
||||
echo "### NEEDED libraries:"
|
||||
readelf -d "$BINARY" | grep NEEDED || echo "No dynamic dependencies (statically linked)"
|
||||
|
||||
# Verify expected libraries are linked
|
||||
readelf -d test-wails-app | grep NEEDED || echo "No dynamic dependencies"
|
||||
echo ""
|
||||
echo "### Verifying required libraries..."
|
||||
NEEDED=$(readelf -d "$BINARY" | grep NEEDED)
|
||||
|
||||
NEEDED=$(readelf -d test-wails-app | grep NEEDED)
|
||||
MISSING=""
|
||||
for lib in libwebkit2gtk-4.1.so libgtk-3.so libglib-2.0.so libc.so; do
|
||||
if echo "$NEEDED" | grep -q "$lib"; then
|
||||
|
|
@ -251,139 +250,13 @@ jobs:
|
|||
done
|
||||
|
||||
if [ -n "$MISSING" ]; then
|
||||
echo ""
|
||||
echo "ERROR: Missing required libraries:$MISSING"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test non-CGO builds (pure Go cross-compilation)
|
||||
test-non-cgo:
|
||||
needs: build
|
||||
if: ${{ inputs.skip_tests != 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: darwin
|
||||
arch: arm64
|
||||
expected_file: "Mach-O 64-bit.*arm64"
|
||||
- os: darwin
|
||||
arch: amd64
|
||||
expected_file: "Mach-O 64-bit.*x86_64"
|
||||
- os: linux
|
||||
arch: amd64
|
||||
expected_file: "ELF 64-bit LSB"
|
||||
- os: linux
|
||||
arch: arm64
|
||||
expected_file: "ELF 64-bit LSB.*ARM aarch64"
|
||||
- os: windows
|
||||
arch: amd64
|
||||
expected_file: "PE32\\+ executable.*x86-64"
|
||||
- os: windows
|
||||
arch: arm64
|
||||
expected_file: "PE32\\+ executable.*Aarch64"
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.branch || github.ref }}
|
||||
|
||||
- name: Log in to Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Create test non-CGO project
|
||||
run: |
|
||||
mkdir -p test-project
|
||||
cd test-project
|
||||
|
||||
# Create a pure Go test program (no CGO)
|
||||
cat > main.go << 'EOF'
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
fmt.Println("Pure Go cross-compilation test")
|
||||
}
|
||||
EOF
|
||||
|
||||
cat > go.mod << 'EOF'
|
||||
module test-pure-go
|
||||
|
||||
go 1.21
|
||||
EOF
|
||||
|
||||
- name: Build ${{ matrix.os }}/${{ matrix.arch }} (non-CGO)
|
||||
run: |
|
||||
cd test-project
|
||||
|
||||
# For non-CGO, we can use any platform since Go handles cross-compilation
|
||||
# We set CGO_ENABLED=0 to ensure pure Go build
|
||||
docker run --rm \
|
||||
-v "$(pwd):/app" \
|
||||
-e APP_NAME="test-pure-go" \
|
||||
-e CGO_ENABLED=0 \
|
||||
--entrypoint /bin/sh \
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.build.outputs.image_tag || 'latest' }} \
|
||||
-c "GOOS=${{ matrix.os }} GOARCH=${{ matrix.arch }} go build -o bin/test-pure-go-${{ matrix.os }}-${{ matrix.arch }}${{ matrix.os == 'windows' && '.exe' || '' }} ."
|
||||
|
||||
- name: Verify binary format
|
||||
run: |
|
||||
cd test-project/bin
|
||||
ls -la
|
||||
|
||||
# Find the built binary
|
||||
if [ "${{ matrix.os }}" = "windows" ]; then
|
||||
BINARY="test-pure-go-${{ matrix.os }}-${{ matrix.arch }}.exe"
|
||||
else
|
||||
BINARY="test-pure-go-${{ matrix.os }}-${{ matrix.arch }}"
|
||||
fi
|
||||
|
||||
echo "Binary: $BINARY"
|
||||
FILE_OUTPUT=$(file "$BINARY")
|
||||
echo "File output: $FILE_OUTPUT"
|
||||
|
||||
# Verify the binary format matches expected
|
||||
if echo "$FILE_OUTPUT" | grep -qE "${{ matrix.expected_file }}"; then
|
||||
echo "✅ Binary format verified: ${{ matrix.os }}/${{ matrix.arch }} (non-CGO)"
|
||||
else
|
||||
echo "❌ Binary format mismatch!"
|
||||
echo "Expected pattern: ${{ matrix.expected_file }}"
|
||||
echo "Got: $FILE_OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Check library dependencies (Linux only)
|
||||
if: matrix.os == 'linux'
|
||||
run: |
|
||||
cd test-project/bin
|
||||
BINARY="test-pure-go-${{ matrix.os }}-${{ matrix.arch }}"
|
||||
|
||||
echo "## Library Dependencies for $BINARY (non-CGO)"
|
||||
echo ""
|
||||
|
||||
# Non-CGO builds should have minimal dependencies (just libc or statically linked)
|
||||
echo "### NEEDED libraries:"
|
||||
readelf -d "$BINARY" | grep NEEDED || echo "No dynamic dependencies (statically linked)"
|
||||
|
||||
# Verify NO GTK/WebKit libraries (since CGO is disabled)
|
||||
NEEDED=$(readelf -d "$BINARY" | grep NEEDED || true)
|
||||
if echo "$NEEDED" | grep -q "libwebkit\|libgtk"; then
|
||||
echo "❌ ERROR: Non-CGO binary should not link to GTK/WebKit!"
|
||||
exit 1
|
||||
else
|
||||
echo "✅ Confirmed: No GTK/WebKit dependencies (expected for non-CGO)"
|
||||
fi
|
||||
|
||||
# Summary job
|
||||
test-summary:
|
||||
needs: [build, test-cross-compile, test-non-cgo]
|
||||
needs: [build, test-cross-compile]
|
||||
if: always() && inputs.skip_tests != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
|
@ -393,30 +266,23 @@ jobs:
|
|||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
if [ "${{ needs.test-cross-compile.result }}" = "success" ]; then
|
||||
echo "✅ **CGO Tests**: All passed" >> $GITHUB_STEP_SUMMARY
|
||||
echo "✅ **All Tests Passed**" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "❌ **CGO Tests**: Failed" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
if [ "${{ needs.test-non-cgo.result }}" = "success" ]; then
|
||||
echo "✅ **Non-CGO Tests**: All passed" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "❌ **Non-CGO Tests**: Failed" >> $GITHUB_STEP_SUMMARY
|
||||
echo "❌ **Some Tests Failed**" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### Tested Platforms" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Platform | Architecture | CGO | Non-CGO |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|----------|-------------|-----|---------|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Darwin | arm64 | ✅ | ✅ |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Darwin | amd64 | ✅ | ✅ |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Linux | arm64 | ✅ | ✅ |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Linux | amd64 | ✅ | ✅ |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Windows | arm64 | ✅ | ✅ |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Windows | amd64 | ✅ | ✅ |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### Cross-Compilation Tests (from Linux/amd64 runner)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Target | Status |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|--------|--------|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Darwin/arm64 | ✅ |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Windows/amd64 | ✅ |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Windows/arm64 | ✅ |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Linux/amd64 | ✅ |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Linux/arm64 | ✅ |" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# Fail if any test failed
|
||||
if [ "${{ needs.test-cross-compile.result }}" != "success" ] || [ "${{ needs.test-non-cgo.result }}" != "success" ]; then
|
||||
if [ "${{ needs.test-cross-compile.result }}" != "success" ]; then
|
||||
echo ""
|
||||
echo "❌ Some tests failed. Check the individual job logs for details."
|
||||
exit 1
|
||||
|
|
|
|||
143
.github/workflows/cross-compile-test-v3.yml
vendored
Normal file
143
.github/workflows/cross-compile-test-v3.yml
vendored
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
name: Cross-Compile Test v3
|
||||
|
||||
on:
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
branches:
|
||||
- v3-alpha
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: 'PR number to test (optional, uses current branch if not specified)'
|
||||
required: false
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
check_approval:
|
||||
name: Check PR Approval
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'workflow_dispatch' || github.event.review.state == 'approved'
|
||||
outputs:
|
||||
approved: ${{ steps.check.outputs.approved }}
|
||||
steps:
|
||||
- name: Check if PR is approved or manual dispatch
|
||||
id: check
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
|
||||
echo "Manual dispatch, proceeding with cross-compile tests"
|
||||
else
|
||||
echo "PR approved, proceeding with cross-compile tests"
|
||||
fi
|
||||
echo "approved=true" >> $GITHUB_OUTPUT
|
||||
|
||||
cross_compile:
|
||||
name: Cross-Compile (${{ matrix.target_os }}/${{ matrix.target_arch }})
|
||||
needs: check_approval
|
||||
runs-on: ubuntu-latest
|
||||
if: needs.check_approval.outputs.approved == 'true'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- target_os: darwin
|
||||
target_arch: arm64
|
||||
- target_os: darwin
|
||||
target_arch: amd64
|
||||
- target_os: linux
|
||||
target_arch: arm64
|
||||
- target_os: linux
|
||||
target_arch: amd64
|
||||
- target_os: windows
|
||||
target_arch: arm64
|
||||
- target_os: windows
|
||||
target_arch: amd64
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Checkout PR (if specified)
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.pr_number != ''
|
||||
run: gh pr checkout ${{ inputs.pr_number }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25'
|
||||
cache: true
|
||||
cache-dependency-path: "v3/go.sum"
|
||||
|
||||
- name: Install Task
|
||||
uses: arduino/setup-task@v2
|
||||
with:
|
||||
version: 3.x
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install Linux dependencies
|
||||
uses: awalsh128/cache-apt-pkgs-action@latest
|
||||
with:
|
||||
packages: libgtk-3-dev libwebkit2gtk-4.1-dev build-essential pkg-config
|
||||
version: 1.0
|
||||
|
||||
- name: Install Wails3 CLI
|
||||
working-directory: v3
|
||||
run: |
|
||||
go install ./cmd/wails3
|
||||
wails3 version
|
||||
|
||||
- name: Create test project
|
||||
run: |
|
||||
mkdir -p test-cross-compile
|
||||
cd test-cross-compile
|
||||
wails3 init -n crosstest -t vanilla
|
||||
|
||||
- name: Set up QEMU for cross-platform emulation
|
||||
if: matrix.target_os == 'linux' && matrix.target_arch != 'amd64'
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Setup Docker cross-compile image
|
||||
working-directory: test-cross-compile/crosstest
|
||||
run: |
|
||||
# For Linux cross-arch builds, build the Docker image for the target platform
|
||||
if [[ "${{ matrix.target_os }}" == "linux" && "${{ matrix.target_arch }}" != "amd64" ]]; then
|
||||
echo "Building Docker image for linux/${{ matrix.target_arch }}..."
|
||||
docker buildx build --platform linux/${{ matrix.target_arch }} --load -t wails-cross -f build/docker/Dockerfile.cross build/docker/
|
||||
else
|
||||
task common:setup:docker
|
||||
fi
|
||||
|
||||
- name: Fix replace directive for Docker build
|
||||
working-directory: test-cross-compile/crosstest
|
||||
run: |
|
||||
# Change the replace directive to use absolute path that matches Docker mount
|
||||
go mod edit -dropreplace github.com/wailsapp/wails/v3
|
||||
go mod edit -replace github.com/wailsapp/wails/v3=${{ github.workspace }}/v3
|
||||
|
||||
- name: Cross-compile for ${{ matrix.target_os }}/${{ matrix.target_arch }}
|
||||
working-directory: test-cross-compile/crosstest
|
||||
run: |
|
||||
echo "Cross-compiling for ${{ matrix.target_os }}/${{ matrix.target_arch }}..."
|
||||
task ${{ matrix.target_os }}:build ARCH=${{ matrix.target_arch }}
|
||||
echo "Cross-compilation successful!"
|
||||
ls -la bin/
|
||||
|
||||
cross_compile_results:
|
||||
if: ${{ always() }}
|
||||
runs-on: ubuntu-latest
|
||||
name: Cross-Compile Results
|
||||
needs: [cross_compile]
|
||||
steps:
|
||||
- run: |
|
||||
result="${{ needs.cross_compile.result }}"
|
||||
echo "Cross-compile result: $result"
|
||||
if [[ $result == "success" ]]; then
|
||||
echo "All cross-compile tests passed!"
|
||||
exit 0
|
||||
else
|
||||
echo "One or more cross-compile tests failed"
|
||||
exit 1
|
||||
fi
|
||||
|
|
@ -17,7 +17,7 @@ wails3 package GOOS=darwin
|
|||
|
||||
This creates `bin/<AppName>.app` containing:
|
||||
- The compiled binary in `Contents/MacOS/`
|
||||
- App icon in `Contents/Resources/`
|
||||
- App icon in `Contents/Resources/` (from `icons.icns` or, when present, from an asset catalog `Assets.car`)
|
||||
- `Info.plist` with app metadata
|
||||
|
||||
### Universal Binary
|
||||
|
|
@ -40,10 +40,23 @@ Edit `build/darwin/Info.plist` to customize:
|
|||
- File associations
|
||||
- URL schemes
|
||||
|
||||
The app icon is generated from `build/appicon.png`. Regenerate with:
|
||||
The app icon is generated from assets in the `build/` directory. Use the `generate:icons` task:
|
||||
|
||||
```bash
|
||||
wails3 generate icons -input build/appicon.png
|
||||
wails3 task generate:icons
|
||||
```
|
||||
|
||||
This uses `build/appicon.png` to produce `darwin/icons.icns` and `windows/icon.ico`. On macOS you can also provide `build/appicon.icon` (Icon Composer format): the task passes `-iconcomposerinput appicon.icon -macassetdir darwin`, which produces `Assets.car` and `darwin/icons.icns` from the `.icon` file (skipped on non-macOS platforms). When `Assets.car` is present, run the `update:build-assets` task so that `Info.plist` and `CFBundleIconName` are updated accordingly:
|
||||
|
||||
```bash
|
||||
wails3 task update:build-assets
|
||||
```
|
||||
|
||||
To run the icon command manually from the `build/` directory:
|
||||
|
||||
```bash
|
||||
cd build
|
||||
wails3 generate icons -input appicon.png -macfilename darwin/icons.icns -windowsfilename windows/icon.ico -iconcomposerinput appicon.icon -macassetdir darwin
|
||||
```
|
||||
|
||||
## Code Signing
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ In this tutorial, you'll build a notes application that demonstrates file operat
|
|||
|
||||
// SaveToFile saves notes to a file
|
||||
func (n *NotesService) SaveToFile() error {
|
||||
path, err := application.SaveFileDialog().
|
||||
path, err := application.Get().Dialog.SaveFile().
|
||||
SetFilename("notes.json").
|
||||
AddFilter("JSON Files", "*.json").
|
||||
PromptForSingleSelection()
|
||||
|
|
@ -133,7 +133,7 @@ In this tutorial, you'll build a notes application that demonstrates file operat
|
|||
return err
|
||||
}
|
||||
|
||||
application.InfoDialog().
|
||||
application.Get().Dialog.Info().
|
||||
SetTitle("Success").
|
||||
SetMessage("Notes saved successfully!").
|
||||
Show()
|
||||
|
|
@ -143,7 +143,7 @@ In this tutorial, you'll build a notes application that demonstrates file operat
|
|||
|
||||
// LoadFromFile loads notes from a file
|
||||
func (n *NotesService) LoadFromFile() error {
|
||||
path, err := application.OpenFileDialog().
|
||||
path, err := application.Get().Dialog.OpenFile().
|
||||
AddFilter("JSON Files", "*.json").
|
||||
PromptForSingleSelection()
|
||||
|
||||
|
|
@ -163,7 +163,7 @@ In this tutorial, you'll build a notes application that demonstrates file operat
|
|||
|
||||
n.notes = notes
|
||||
|
||||
application.InfoDialog().
|
||||
application.Get().Dialog.Info().
|
||||
SetTitle("Success").
|
||||
SetMessage("Notes loaded successfully!").
|
||||
Show()
|
||||
|
|
@ -180,8 +180,8 @@ In this tutorial, you'll build a notes application that demonstrates file operat
|
|||
|
||||
- **Note struct**: Defines the data structure with JSON tags (lowercase) for proper serialization
|
||||
- **CRUD operations**: GetAll, Create, Update, and Delete for managing notes in memory
|
||||
- **File dialogs**: Uses package-level functions `application.SaveFileDialog()` and `application.OpenFileDialog()`
|
||||
- **Info dialogs**: Shows success messages using `application.InfoDialog()`
|
||||
- **File dialogs**: Uses `application.Get().Dialog.SaveFile()` and `application.Get().Dialog.OpenFile()` to access native dialogs
|
||||
- **Info dialogs**: Shows success messages using `application.Get().Dialog.Info()`
|
||||
- **ID generation**: Simple timestamp-based ID generator
|
||||
|
||||
3. **Update main.go**
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ After processing, the content will be moved to the main changelog and this file
|
|||
-->
|
||||
|
||||
## Added
|
||||
<!-- New features, capabilities, or enhancements -->
|
||||
- Add support for using `.icon` files (Apple Icon Composer format) for generating Liquid Glass icons and asset catalogs (macOS) (#4934) by @wimaha
|
||||
|
||||
## Changed
|
||||
<!-- Changes in existing functionality -->
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ type BuildAssetsOptions struct {
|
|||
ProductCopyright string `description:"The copyright notice" default:"\u00a9 now, My Company"`
|
||||
ProductComments string `description:"Comments to add to the generated files" default:"This is a comment"`
|
||||
ProductIdentifier string `description:"The product identifier, e.g com.mycompany.myproduct"`
|
||||
CFBundleIconName string `description:"The macOS icon name (for Assets.car icon bundles)"`
|
||||
Publisher string `description:"Publisher name for MSIX package (e.g., CN=CompanyName)"`
|
||||
ProcessorArchitecture string `description:"Processor architecture for MSIX package" default:"x64"`
|
||||
ExecutablePath string `description:"Path to executable for MSIX package"`
|
||||
|
|
@ -71,6 +72,7 @@ type UpdateBuildAssetsOptions struct {
|
|||
ProductCopyright string `description:"The copyright notice" default:"© now, My Company"`
|
||||
ProductComments string `description:"Comments to add to the generated files" default:"This is a comment"`
|
||||
ProductIdentifier string `description:"The product identifier, e.g com.mycompany.myproduct"`
|
||||
CFBundleIconName string `description:"The macOS icon name (for Assets.car icon bundles)"`
|
||||
Config string `description:"The path to the config file"`
|
||||
Silent bool `description:"Suppress output to console"`
|
||||
}
|
||||
|
|
@ -146,10 +148,17 @@ func GenerateBuildAssets(options *BuildAssetsOptions) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Check if Assets.car exists - if so, set CFBundleIconName if not already set
|
||||
// This must happen BEFORE the updatable_build_assets extraction so CFBundleIconName is available in Info.plist templates
|
||||
checkAndSetCFBundleIconNameCommon(options.Dir, &buildCFBundleIconNameSetter{options, &config})
|
||||
// Update config with the potentially modified options
|
||||
config.BuildAssetsOptions = *options
|
||||
|
||||
tfs, err = fs.Sub(updatableBuildAssets, "updatable_build_assets")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = gosod.New(tfs).Extract(options.Dir, config)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -185,6 +194,7 @@ type WailsConfig struct {
|
|||
Copyright string `yaml:"copyright"`
|
||||
Comments string `yaml:"comments"`
|
||||
Version string `yaml:"version"`
|
||||
CFBundleIconName string `yaml:"cfBundleIconName,omitempty"`
|
||||
} `yaml:"info"`
|
||||
FileAssociations []FileAssociation `yaml:"fileAssociations,omitempty"`
|
||||
Protocols []ProtocolConfig `yaml:"protocols,omitempty"`
|
||||
|
|
@ -233,6 +243,9 @@ func UpdateBuildAssets(options *UpdateBuildAssetsOptions) error {
|
|||
if options.ProductVersion == "0.1.0" && wailsConfig.Info.Version != "" {
|
||||
options.ProductVersion = wailsConfig.Info.Version
|
||||
}
|
||||
if options.CFBundleIconName == "" && wailsConfig.Info.CFBundleIconName != "" {
|
||||
options.CFBundleIconName = wailsConfig.Info.CFBundleIconName
|
||||
}
|
||||
config.FileAssociations = wailsConfig.FileAssociations
|
||||
config.Protocols = wailsConfig.Protocols
|
||||
}
|
||||
|
|
@ -247,6 +260,11 @@ func UpdateBuildAssets(options *UpdateBuildAssetsOptions) error {
|
|||
}
|
||||
}
|
||||
|
||||
// Check if Assets.car exists - if so, set CFBundleIconName if not already set
|
||||
checkAndSetCFBundleIconNameCommon(options.Dir, &updateCFBundleIconNameSetter{options, &config})
|
||||
// Update config with the potentially modified options
|
||||
config.UpdateBuildAssetsOptions = *options
|
||||
|
||||
tfs, err := fs.Sub(updatableBuildAssets, "updatable_build_assets")
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -284,6 +302,51 @@ func normaliseName(name string) string {
|
|||
return strings.ToLower(strings.ReplaceAll(name, " ", "-"))
|
||||
}
|
||||
|
||||
// CFBundleIconNameSetter is implemented by types that can get and set CFBundleIconName
|
||||
// (used to keep options and config in sync when defaulting the macOS icon name).
|
||||
type CFBundleIconNameSetter interface {
|
||||
GetCFBundleIconName() string
|
||||
SetCFBundleIconName(string)
|
||||
}
|
||||
|
||||
// checkAndSetCFBundleIconNameCommon checks if Assets.car exists in the darwin folder
|
||||
// and sets CFBundleIconName via setter if not already set. The icon name should be configured
|
||||
// in config.yml under info.cfBundleIconName and should match the name of the .icon file without the extension
|
||||
// with which Assets.car was generated. If not set, defaults to "appicon".
|
||||
func checkAndSetCFBundleIconNameCommon(dir string, setter CFBundleIconNameSetter) {
|
||||
darwinDir := filepath.Join(dir, "darwin")
|
||||
assetsCarPath := filepath.Join(darwinDir, "Assets.car")
|
||||
if _, err := os.Stat(assetsCarPath); err == nil {
|
||||
if setter.GetCFBundleIconName() == "" {
|
||||
setter.SetCFBundleIconName("appicon")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// buildCFBundleIconNameSetter sets CFBundleIconName on both options and config for GenerateBuildAssets.
|
||||
type buildCFBundleIconNameSetter struct {
|
||||
options *BuildAssetsOptions
|
||||
config *BuildConfig
|
||||
}
|
||||
|
||||
func (s *buildCFBundleIconNameSetter) GetCFBundleIconName() string { return s.options.CFBundleIconName }
|
||||
func (s *buildCFBundleIconNameSetter) SetCFBundleIconName(v string) {
|
||||
s.options.CFBundleIconName = v
|
||||
s.config.CFBundleIconName = v
|
||||
}
|
||||
|
||||
// updateCFBundleIconNameSetter sets CFBundleIconName on both options and config for UpdateBuildAssets.
|
||||
type updateCFBundleIconNameSetter struct {
|
||||
options *UpdateBuildAssetsOptions
|
||||
config *UpdateConfig
|
||||
}
|
||||
|
||||
func (s *updateCFBundleIconNameSetter) GetCFBundleIconName() string { return s.options.CFBundleIconName }
|
||||
func (s *updateCFBundleIconNameSetter) SetCFBundleIconName(v string) {
|
||||
s.options.CFBundleIconName = v
|
||||
s.config.CFBundleIconName = v
|
||||
}
|
||||
|
||||
// mergeMaps recursively merges src into dst.
|
||||
// For nested maps, it merges recursively. For other types, src overwrites dst.
|
||||
func mergeMaps(dst, src map[string]any) {
|
||||
|
|
|
|||
|
|
@ -168,6 +168,7 @@ func TestUpdateBuildAssets(t *testing.T) {
|
|||
Copyright string `yaml:"copyright"`
|
||||
Comments string `yaml:"comments"`
|
||||
Version string `yaml:"version"`
|
||||
CFBundleIconName string `yaml:"cfBundleIconName,omitempty"`
|
||||
}{
|
||||
CompanyName: "Config Company",
|
||||
ProductName: "Config Product",
|
||||
|
|
@ -351,6 +352,161 @@ func TestPlistMerge(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestCFBundleIconNameDetection(t *testing.T) {
|
||||
tempDir, err := os.MkdirTemp("", "wails-icon-name-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp directory: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
createAssetsCar bool
|
||||
configIconName string
|
||||
expectedIconName string
|
||||
expectIconNameInPlist bool
|
||||
}{
|
||||
{
|
||||
name: "Assets.car exists, no config - should default to appicon",
|
||||
createAssetsCar: true,
|
||||
configIconName: "",
|
||||
expectedIconName: "appicon",
|
||||
expectIconNameInPlist: true,
|
||||
},
|
||||
{
|
||||
name: "Assets.car exists, config set - should use config",
|
||||
createAssetsCar: true,
|
||||
configIconName: "custom-icon",
|
||||
expectedIconName: "custom-icon",
|
||||
expectIconNameInPlist: true,
|
||||
},
|
||||
{
|
||||
name: "No Assets.car, no config - should not set",
|
||||
createAssetsCar: false,
|
||||
configIconName: "",
|
||||
expectedIconName: "",
|
||||
expectIconNameInPlist: false,
|
||||
},
|
||||
{
|
||||
name: "No Assets.car, config set - should use config",
|
||||
createAssetsCar: false,
|
||||
configIconName: "config-icon",
|
||||
expectedIconName: "config-icon",
|
||||
expectIconNameInPlist: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
buildDir := filepath.Join(tempDir, tt.name)
|
||||
darwinDir := filepath.Join(buildDir, "darwin")
|
||||
err := os.MkdirAll(darwinDir, 0755)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create darwin directory: %v", err)
|
||||
}
|
||||
|
||||
// Create Assets.car BEFORE calling UpdateBuildAssets if needed
|
||||
// The check happens before template extraction, so CFBundleIconName will be available in the template
|
||||
if tt.createAssetsCar {
|
||||
assetsCarPath := filepath.Join(darwinDir, "Assets.car")
|
||||
err = os.WriteFile(assetsCarPath, []byte("fake assets.car content"), 0644)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create Assets.car: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create config file if icon name is set
|
||||
configFile := ""
|
||||
if tt.configIconName != "" {
|
||||
configDir := filepath.Join(tempDir, "config-"+tt.name)
|
||||
err = os.MkdirAll(configDir, 0755)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create config directory: %v", err)
|
||||
}
|
||||
|
||||
configFile = filepath.Join(configDir, "wails.yaml")
|
||||
config := WailsConfig{
|
||||
Info: struct {
|
||||
CompanyName string `yaml:"companyName"`
|
||||
ProductName string `yaml:"productName"`
|
||||
ProductIdentifier string `yaml:"productIdentifier"`
|
||||
Description string `yaml:"description"`
|
||||
Copyright string `yaml:"copyright"`
|
||||
Comments string `yaml:"comments"`
|
||||
Version string `yaml:"version"`
|
||||
CFBundleIconName string `yaml:"cfBundleIconName,omitempty"`
|
||||
}{
|
||||
CompanyName: "Test Company",
|
||||
ProductName: "Test Product",
|
||||
ProductIdentifier: "com.test.product",
|
||||
CFBundleIconName: tt.configIconName,
|
||||
},
|
||||
}
|
||||
|
||||
configBytes, err := yaml.Marshal(config)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal config: %v", err)
|
||||
}
|
||||
|
||||
err = os.WriteFile(configFile, configBytes, 0644)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to write config file: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
options := &UpdateBuildAssetsOptions{
|
||||
Dir: buildDir,
|
||||
Name: "TestApp",
|
||||
ProductName: "Test App",
|
||||
ProductVersion: "1.0.0",
|
||||
ProductCompany: "Test Company",
|
||||
ProductIdentifier: "com.test.app",
|
||||
CFBundleIconName: tt.configIconName,
|
||||
Config: configFile,
|
||||
Silent: true,
|
||||
}
|
||||
|
||||
err = UpdateBuildAssets(options)
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateBuildAssets failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify CFBundleIconName was set correctly in options
|
||||
if options.CFBundleIconName != tt.expectedIconName {
|
||||
t.Errorf("Expected CFBundleIconName to be '%s', got '%s'", tt.expectedIconName, options.CFBundleIconName)
|
||||
}
|
||||
|
||||
// Check Info.plist if it exists
|
||||
infoPlistPath := filepath.Join(darwinDir, "Info.plist")
|
||||
if _, err := os.Stat(infoPlistPath); err == nil {
|
||||
plistContent, err := os.ReadFile(infoPlistPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read Info.plist: %v", err)
|
||||
}
|
||||
|
||||
var plistDict map[string]any
|
||||
_, err = plist.Unmarshal(plistContent, &plistDict)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse Info.plist: %v", err)
|
||||
}
|
||||
|
||||
iconName, exists := plistDict["CFBundleIconName"]
|
||||
if tt.expectIconNameInPlist {
|
||||
if !exists {
|
||||
t.Errorf("Expected CFBundleIconName to be present in Info.plist")
|
||||
} else if iconName != tt.expectedIconName {
|
||||
t.Errorf("Expected CFBundleIconName in Info.plist to be '%s', got '%v'", tt.expectedIconName, iconName)
|
||||
}
|
||||
} else {
|
||||
if exists {
|
||||
t.Errorf("Expected CFBundleIconName to not be present in Info.plist, but found '%v'", iconName)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNestedPlistMerge(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
|
|
|||
|
|
@ -97,15 +97,16 @@ tasks:
|
|||
- wails3 generate bindings -f {{ "'{{.BUILD_FLAGS}}'" }} -clean=true {{- if .Typescript}} -ts{{end}}
|
||||
|
||||
generate:icons:
|
||||
summary: Generates Windows `.ico` and Mac `.icns` files from an image
|
||||
summary: Generates Windows `.ico` and Mac `.icns` from an image; on macOS, `-iconcomposerinput appicon.icon -macassetdir darwin` also produces `Assets.car` from a `.icon` file (skipped on other platforms).
|
||||
dir: build
|
||||
sources:
|
||||
- "appicon.png"
|
||||
- "appicon.icon"
|
||||
generates:
|
||||
- "darwin/icons.icns"
|
||||
- "windows/icon.ico"
|
||||
cmds:
|
||||
- wails3 generate icons -input appicon.png -macfilename darwin/icons.icns -windowsfilename windows/icon.ico
|
||||
- wails3 generate icons -input appicon.png -macfilename darwin/icons.icns -windowsfilename windows/icon.ico -iconcomposerinput appicon.icon -macassetdir darwin
|
||||
|
||||
dev:frontend:
|
||||
summary: Runs the frontend in development mode
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 583 533" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(1,0,0,1,-246,-251)">
|
||||
<g id="Ebene1">
|
||||
<path d="M246,251L265,784L401,784L506,450L507,450L505,784L641,784L829,251L682,251L596,567L595,567L596,251L478,251L378,568L391,251L246,251Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 698 B |
51
v3/internal/commands/build_assets/appicon.icon/icon.json
Normal file
51
v3/internal/commands/build_assets/appicon.icon/icon.json
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
{
|
||||
"fill" : {
|
||||
"automatic-gradient" : "extended-gray:1.00000,1.00000"
|
||||
},
|
||||
"groups" : [
|
||||
{
|
||||
"layers" : [
|
||||
{
|
||||
"fill-specializations" : [
|
||||
{
|
||||
"appearance" : "dark",
|
||||
"value" : {
|
||||
"solid" : "srgb:0.92143,0.92145,0.92144,1.00000"
|
||||
}
|
||||
},
|
||||
{
|
||||
"appearance" : "tinted",
|
||||
"value" : {
|
||||
"solid" : "srgb:0.83742,0.83744,0.83743,1.00000"
|
||||
}
|
||||
}
|
||||
],
|
||||
"image-name" : "wails_icon_vector.svg",
|
||||
"name" : "wails_icon_vector",
|
||||
"position" : {
|
||||
"scale" : 1.25,
|
||||
"translation-in-points" : [
|
||||
36.890625,
|
||||
4.96875
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"shadow" : {
|
||||
"kind" : "neutral",
|
||||
"opacity" : 0.5
|
||||
},
|
||||
"specular" : true,
|
||||
"translucency" : {
|
||||
"enabled" : true,
|
||||
"value" : 0.5
|
||||
}
|
||||
}
|
||||
],
|
||||
"supported-platforms" : {
|
||||
"circles" : [
|
||||
"watchOS"
|
||||
],
|
||||
"squares" : "shared"
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,9 @@ info:
|
|||
copyright: "(c) 2025, My Company" # Copyright text
|
||||
comments: "Some Product Comments" # Comments
|
||||
version: "0.0.1" # The application version
|
||||
# cfBundleIconName: "appicon" # The macOS icon name in Assets.car icon bundles (optional)
|
||||
# # Should match the name of your .icon file without the extension
|
||||
# # If not set and Assets.car exists, defaults to "appicon"
|
||||
|
||||
# iOS build configuration (uncomment to customise iOS project generation)
|
||||
# Note: Keys under `ios` OVERRIDE values under `info` when set.
|
||||
|
|
|
|||
BIN
v3/internal/commands/build_assets/darwin/Assets.car
Normal file
BIN
v3/internal/commands/build_assets/darwin/Assets.car
Normal file
Binary file not shown.
|
|
@ -141,6 +141,10 @@ tasks:
|
|||
- mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/MacOS"
|
||||
- mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources"
|
||||
- cp build/darwin/icons.icns "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources"
|
||||
- |
|
||||
if [ -f build/darwin/Assets.car ]; then
|
||||
cp build/darwin/Assets.car "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources"
|
||||
fi
|
||||
- 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}}'
|
||||
|
|
@ -162,6 +166,10 @@ tasks:
|
|||
- mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS"
|
||||
- mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Resources"
|
||||
- cp build/darwin/icons.icns "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Resources"
|
||||
- |
|
||||
if [ -f build/darwin/Assets.car ]; then
|
||||
cp build/darwin/Assets.car "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Resources"
|
||||
fi
|
||||
- cp "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS"
|
||||
- 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"
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1,7 +1,7 @@
|
|||
# Cross-compile Wails v3 apps to any platform
|
||||
#
|
||||
# Darwin: Zig + macOS SDK
|
||||
# Linux: GCC (use --platform to match target arch)
|
||||
# Linux: Native GCC when host matches target, Zig for cross-arch
|
||||
# Windows: Zig + bundled mingw
|
||||
#
|
||||
# Usage:
|
||||
|
|
@ -12,9 +12,6 @@
|
|||
# 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
|
||||
#
|
||||
# Multi-arch build:
|
||||
# docker buildx build --platform linux/amd64,linux/arm64 -t wails-cross -f Dockerfile.cross .
|
||||
|
||||
FROM golang:1.25-bookworm
|
||||
|
||||
|
|
@ -27,9 +24,6 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||
libgtk-4-dev libwebkitgtk-6.0-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Note: For Linux cross-compilation, use --platform to match target architecture
|
||||
# e.g., docker run --platform linux/amd64 ... for amd64 targets
|
||||
|
||||
# Install Zig - automatically selects correct binary for host architecture
|
||||
ARG ZIG_VERSION=0.14.0
|
||||
RUN ZIG_ARCH=$(case "${TARGETARCH}" in arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \
|
||||
|
|
@ -45,8 +39,8 @@ RUN curl -L "https://github.com/joseluisq/macosx-sdks/releases/download/${MACOS_
|
|||
|
||||
ENV MACOS_SDK_PATH=/opt/macos-sdk
|
||||
|
||||
# Create Zig CC wrappers for Darwin and Windows targets
|
||||
# (Linux uses native GCC with --platform flag for architecture matching)
|
||||
# Create Zig CC wrappers for cross-compilation targets
|
||||
# Darwin and Windows use Zig; Linux uses native GCC (run with --platform for cross-arch)
|
||||
|
||||
# Darwin arm64
|
||||
COPY <<'ZIGWRAP' /usr/local/bin/zcc-darwin-arm64
|
||||
|
|
|
|||
|
|
@ -17,8 +17,11 @@ 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}}'
|
||||
# Linux requires CGO - use Docker when:
|
||||
# 1. Cross-compiling from non-Linux, OR
|
||||
# 2. No C compiler is available, OR
|
||||
# 3. Target architecture differs from host architecture (cross-arch compilation)
|
||||
- task: '{{if and (eq OS "linux") (eq .HAS_CC "true") (eq .TARGET_ARCH ARCH)}}build:native{{else}}build:docker{{end}}'
|
||||
vars:
|
||||
ARCH: '{{.ARCH}}'
|
||||
DEV: '{{.DEV}}'
|
||||
|
|
@ -26,6 +29,8 @@ tasks:
|
|||
vars:
|
||||
DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
|
||||
OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
|
||||
# Determine target architecture (defaults to host ARCH if not specified)
|
||||
TARGET_ARCH: '{{.ARCH | default ARCH}}'
|
||||
# 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"'
|
||||
|
|
@ -55,7 +60,7 @@ tasks:
|
|||
GOARCH: '{{.ARCH | default ARCH}}'
|
||||
|
||||
build:docker:
|
||||
summary: Cross-compiles for Linux using Docker with Zig (for macOS/Windows hosts)
|
||||
summary: Cross-compiles for Linux using Docker (uses QEMU for cross-arch builds)
|
||||
internal: true
|
||||
deps:
|
||||
- task: common:build:frontend
|
||||
|
|
@ -69,6 +74,7 @@ tasks:
|
|||
Docker image '{{.CROSS_IMAGE}}' not found.
|
||||
Build it first: wails3 task setup:docker
|
||||
cmds:
|
||||
# Use --platform to enable QEMU emulation when target arch differs from host
|
||||
- docker run --rm --platform linux/{{.DOCKER_ARCH}} -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}}
|
||||
|
|
|
|||
|
|
@ -2,24 +2,36 @@ package commands
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/png"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jackmordaunt/icns/v2"
|
||||
"github.com/leaanthony/winicon"
|
||||
"github.com/wailsapp/wails/v3/internal/operatingsystem"
|
||||
"howett.net/plist"
|
||||
)
|
||||
|
||||
// ErrMacAssetNotSupported is returned by generateMacAsset when mac asset generation
|
||||
// is not supported on the current platform (e.g., non-macOS systems).
|
||||
var ErrMacAssetNotSupported = errors.New("mac asset generation is only supported on macOS")
|
||||
|
||||
type IconsOptions struct {
|
||||
Example bool `description:"Generate example icon file (appicon.png) in the current directory"`
|
||||
Input string `description:"The input image file"`
|
||||
Sizes string `description:"The sizes to generate in .ico file (comma separated)" default:"256,128,64,48,32,16"`
|
||||
WindowsFilename string `description:"The output filename for the Windows icon" default:"icon.ico"`
|
||||
MacFilename string `description:"The output filename for the Mac icon bundle" default:"icons.icns"`
|
||||
Example bool `description:"Generate example icon file (appicon.png) in the current directory"`
|
||||
Input string `description:"The input image file"`
|
||||
Sizes string `description:"The sizes to generate in .ico file (comma separated)" default:"256,128,64,48,32,16"`
|
||||
WindowsFilename string `description:"The output filename for the Windows icon"`
|
||||
MacFilename string `description:"The output filename for the Mac icon bundle"`
|
||||
IconComposerInput string `description:"The input Icon Composer file (.icon)"`
|
||||
MacAssetDir string `description:"The output directory for the Mac assets (Assets.car and icons.icns)"`
|
||||
}
|
||||
|
||||
func GenerateIcons(options *IconsOptions) error {
|
||||
|
|
@ -29,12 +41,16 @@ func GenerateIcons(options *IconsOptions) error {
|
|||
return generateExampleIcon()
|
||||
}
|
||||
|
||||
if options.Input == "" {
|
||||
return fmt.Errorf("input is required")
|
||||
if options.Input == "" && options.IconComposerInput == "" {
|
||||
return fmt.Errorf("either input or icon composer input is required")
|
||||
}
|
||||
|
||||
if options.WindowsFilename == "" && options.MacFilename == "" {
|
||||
return fmt.Errorf("at least one output filename is required")
|
||||
if options.Input != "" && options.WindowsFilename == "" && options.MacFilename == "" {
|
||||
return fmt.Errorf("either windows filename or mac filename is required")
|
||||
}
|
||||
|
||||
if options.IconComposerInput != "" && options.MacAssetDir == "" {
|
||||
return fmt.Errorf("mac asset directory is required")
|
||||
}
|
||||
|
||||
// Parse sizes
|
||||
|
|
@ -46,23 +62,49 @@ func GenerateIcons(options *IconsOptions) error {
|
|||
return err
|
||||
}
|
||||
}
|
||||
iconData, err := os.ReadFile(options.Input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if options.WindowsFilename != "" {
|
||||
err := generateWindowsIcon(iconData, sizes, options)
|
||||
if err != nil {
|
||||
return err
|
||||
// Generate Icons from Icon Composer input
|
||||
macIconsGenerated := false
|
||||
if options.IconComposerInput != "" {
|
||||
if options.MacAssetDir != "" {
|
||||
err := generateMacAsset(options)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrMacAssetNotSupported) {
|
||||
// No fallback: Icon Composer path requires macOS; return so callers see unsupported-platform failure
|
||||
if options.Input == "" {
|
||||
return fmt.Errorf("icon composer input requires macOS for mac asset generation: %w", err)
|
||||
}
|
||||
// Fallback to input-based generation will run below
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
macIconsGenerated = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if options.MacFilename != "" {
|
||||
err := generateMacIcon(iconData, options)
|
||||
// Generate Icons from input image
|
||||
if options.Input != "" {
|
||||
iconData, err := os.ReadFile(options.Input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if options.WindowsFilename != "" {
|
||||
err := generateWindowsIcon(iconData, sizes, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Generate Icons from input image if no Mac icons were generated from Icon Composer input
|
||||
if options.MacFilename != "" && !macIconsGenerated {
|
||||
err := generateMacIcon(iconData, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -116,6 +158,150 @@ func generateMacIcon(iconData []byte, options *IconsOptions) error {
|
|||
return icns.Encode(dest, srcImg)
|
||||
}
|
||||
|
||||
func generateMacAsset(options *IconsOptions) error {
|
||||
//Check if running on darwin (macOS), because this will only run on a mac
|
||||
if runtime.GOOS != "darwin" {
|
||||
return ErrMacAssetNotSupported
|
||||
}
|
||||
// Get system info, because this will only run on macOS 26 or later
|
||||
info, err := operatingsystem.Info()
|
||||
if err != nil {
|
||||
return ErrMacAssetNotSupported
|
||||
}
|
||||
majorStr, _, found := strings.Cut(info.Version, ".")
|
||||
if !found {
|
||||
return ErrMacAssetNotSupported
|
||||
}
|
||||
major, err := strconv.Atoi(majorStr)
|
||||
if err != nil {
|
||||
return ErrMacAssetNotSupported
|
||||
}
|
||||
if major < 26 {
|
||||
return ErrMacAssetNotSupported
|
||||
}
|
||||
|
||||
cmd := exec.Command("/usr/bin/actool", "--version")
|
||||
versionPlist, err := cmd.Output()
|
||||
if err != nil {
|
||||
return ErrMacAssetNotSupported
|
||||
}
|
||||
|
||||
// Parse the plist to extract short-bundle-version
|
||||
var plistData map[string]any
|
||||
if _, err := plist.Unmarshal(versionPlist, &plistData); err != nil {
|
||||
return ErrMacAssetNotSupported
|
||||
}
|
||||
|
||||
// Navigate to com.apple.actool.version -> short-bundle-version
|
||||
actoolVersion, ok := plistData["com.apple.actool.version"].(map[string]any)
|
||||
if !ok {
|
||||
return ErrMacAssetNotSupported
|
||||
}
|
||||
|
||||
shortVersion, ok := actoolVersion["short-bundle-version"].(string)
|
||||
if !ok {
|
||||
return ErrMacAssetNotSupported
|
||||
}
|
||||
|
||||
// Parse the major version number (e.g., "26.2" -> 26)
|
||||
actoolMajorStr, _, _ := strings.Cut(shortVersion, ".")
|
||||
actoolMajor, err := strconv.Atoi(actoolMajorStr)
|
||||
if err != nil {
|
||||
return ErrMacAssetNotSupported
|
||||
}
|
||||
|
||||
if actoolMajor < 26 {
|
||||
return ErrMacAssetNotSupported
|
||||
}
|
||||
|
||||
// Convert paths to absolute paths (required for actool)
|
||||
iconComposerPath, err := filepath.Abs(options.IconComposerInput)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get absolute path for icon composer input: %w", err)
|
||||
}
|
||||
macAssetDirPath, err := filepath.Abs(options.MacAssetDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get absolute path for mac asset directory: %w", err)
|
||||
}
|
||||
|
||||
// Get Filename from Icon Composer input without extension
|
||||
iconComposerFilename := filepath.Base(iconComposerPath)
|
||||
iconComposerFilename = strings.TrimSuffix(iconComposerFilename, filepath.Ext(iconComposerFilename))
|
||||
|
||||
cmd = exec.Command("/usr/bin/actool", iconComposerPath,
|
||||
"--compile", macAssetDirPath,
|
||||
"--notices", "--warnings", "--errors",
|
||||
"--output-partial-info-plist", filepath.Join(macAssetDirPath, "temp.plist"),
|
||||
"--app-icon", iconComposerFilename,
|
||||
"--enable-on-demand-resources", "NO",
|
||||
"--development-region", "en",
|
||||
"--target-device", "mac",
|
||||
"--minimum-deployment-target", "26.0",
|
||||
"--platform", "macosx")
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to run actool: %w", err)
|
||||
}
|
||||
|
||||
// Parse the plist output to verify compilation results
|
||||
var compilationResults map[string]any
|
||||
if _, err := plist.Unmarshal(out, &compilationResults); err != nil {
|
||||
return fmt.Errorf("failed to parse actool compilation results: %w", err)
|
||||
}
|
||||
|
||||
// Navigate to com.apple.actool.compilation-results -> output-files
|
||||
compilationData, ok := compilationResults["com.apple.actool.compilation-results"].(map[string]any)
|
||||
if !ok {
|
||||
return fmt.Errorf("failed to find com.apple.actool.compilation-results in plist")
|
||||
}
|
||||
|
||||
outputFiles, ok := compilationData["output-files"].([]any)
|
||||
if !ok {
|
||||
return fmt.Errorf("failed to find output-files array in compilation results")
|
||||
}
|
||||
|
||||
// Check that we have one .car file and one .plist file
|
||||
var carFile, plistFile, icnsFile string
|
||||
for _, file := range outputFiles {
|
||||
filePath, ok := file.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("output file is not a string: %v", file)
|
||||
}
|
||||
ext := filepath.Ext(filePath)
|
||||
switch ext {
|
||||
case ".car":
|
||||
carFile = filePath
|
||||
case ".plist":
|
||||
plistFile = filePath
|
||||
case ".icns":
|
||||
icnsFile = filePath
|
||||
// Ignore other output files that may be added in future actool versions
|
||||
}
|
||||
}
|
||||
|
||||
if carFile == "" {
|
||||
return fmt.Errorf("no .car file found in output files")
|
||||
}
|
||||
if plistFile == "" {
|
||||
return fmt.Errorf("no .plist file found in output files")
|
||||
}
|
||||
if icnsFile == "" {
|
||||
return fmt.Errorf("no .icns file found in output files")
|
||||
}
|
||||
|
||||
// Remove the temporary plist file since compilation was successful
|
||||
if err := os.Remove(plistFile); err != nil {
|
||||
return fmt.Errorf("failed to remove temporary plist file: %w", err)
|
||||
}
|
||||
|
||||
// Rename the .icns file to icons.icns
|
||||
if err := os.Rename(icnsFile, filepath.Join(macAssetDirPath, "icons.icns")); err != nil {
|
||||
return fmt.Errorf("failed to rename .icns file to icons.icns: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateWindowsIcon(iconData []byte, sizes []int, options *IconsOptions) error {
|
||||
|
||||
var output bytes.Buffer
|
||||
|
|
|
|||
|
|
@ -10,10 +10,11 @@ import (
|
|||
|
||||
func TestGenerateIcon(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func() *IconsOptions
|
||||
wantErr bool
|
||||
test func() error
|
||||
name string
|
||||
setup func() *IconsOptions
|
||||
wantErr bool
|
||||
requireDarwin bool
|
||||
test func() error
|
||||
}{
|
||||
{
|
||||
name: "should generate an icon when using the `example` flag",
|
||||
|
|
@ -123,6 +124,54 @@ func TestGenerateIcon(t *testing.T) {
|
|||
return nil
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "should generate a Assets.car and icons.icns file when using the `IconComposerInput` flag and `MacAssetDir` flag",
|
||||
requireDarwin: true,
|
||||
setup: func() *IconsOptions {
|
||||
// Get the directory of this file
|
||||
_, thisFile, _, _ := runtime.Caller(1)
|
||||
localDir := filepath.Dir(thisFile)
|
||||
// Get the path to the example icon
|
||||
exampleIcon := filepath.Join(localDir, "build_assets", "appicon.icon")
|
||||
return &IconsOptions{
|
||||
IconComposerInput: exampleIcon,
|
||||
MacAssetDir: localDir,
|
||||
}
|
||||
},
|
||||
wantErr: false,
|
||||
test: func() error {
|
||||
_, thisFile, _, _ := runtime.Caller(1)
|
||||
localDir := filepath.Dir(thisFile)
|
||||
carPath := filepath.Join(localDir, "Assets.car")
|
||||
icnsPath := filepath.Join(localDir, "icons.icns")
|
||||
defer func() {
|
||||
_ = os.Remove(carPath)
|
||||
_ = os.Remove(icnsPath)
|
||||
}()
|
||||
f, err := os.Stat(carPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if f.IsDir() {
|
||||
return fmt.Errorf("Assets.car is a directory")
|
||||
}
|
||||
if f.Size() == 0 {
|
||||
return fmt.Errorf("Assets.car is empty")
|
||||
}
|
||||
f, err = os.Stat(icnsPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if f.IsDir() {
|
||||
return fmt.Errorf("icons.icns is a directory")
|
||||
}
|
||||
if f.Size() == 0 {
|
||||
return fmt.Errorf("icons.icns is empty")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should generate a small .ico file when using the `input` flag and `sizes` flag",
|
||||
setup: func() *IconsOptions {
|
||||
|
|
@ -266,6 +315,10 @@ func TestGenerateIcon(t *testing.T) {
|
|||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.requireDarwin && (runtime.GOOS != "darwin" || os.Getenv("CI") != "") {
|
||||
t.Skip("Assets.car generation is only supported on macOS and not in CI")
|
||||
}
|
||||
|
||||
options := tt.setup()
|
||||
err := GenerateIcons(options)
|
||||
if (err != nil) != tt.wantErr {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,10 @@
|
|||
<string>{{.ProductVersion}}</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>icons</string>
|
||||
{{- if .CFBundleIconName}}
|
||||
<key>CFBundleIconName</key>
|
||||
<string>{{.CFBundleIconName}}</string>
|
||||
{{- end}}
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.15.0</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
|
|
|
|||
|
|
@ -17,6 +17,10 @@
|
|||
<string>{{.ProductVersion}}</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>icons</string>
|
||||
{{- if .CFBundleIconName}}
|
||||
<key>CFBundleIconName</key>
|
||||
<string>{{.CFBundleIconName}}</string>
|
||||
{{- end}}
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.15.0</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
# syntax=docker/dockerfile:1
|
||||
# Build Linux binaries for Wails v3 examples using Ubuntu 24.04 (ARM64 native)
|
||||
FROM ubuntu:24.04
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
# syntax=docker/dockerfile:1
|
||||
# Build Linux binaries for Wails v3 examples using Ubuntu 24.04 (x86_64 native)
|
||||
FROM --platform=linux/amd64 ubuntu:24.04
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue