diff --git a/.github/workflows/build-cross-image.yml b/.github/workflows/build-cross-image.yml new file mode 100644 index 000000000..83b40f2be --- /dev/null +++ b/.github/workflows/build-cross-image.yml @@ -0,0 +1,423 @@ +name: Build Cross-Compiler Image + +on: + workflow_dispatch: + inputs: + branch: + description: 'Branch containing Dockerfile' + required: true + default: 'v3-alpha' + sdk_version: + description: 'macOS SDK version' + required: true + default: '14.5' + zig_version: + description: 'Zig version' + required: true + default: '0.14.0' + image_version: + description: 'Image version tag' + required: true + default: 'latest' + skip_tests: + description: 'Skip cross-compilation tests' + required: false + default: 'false' + type: boolean + push: + branches: + - v3-alpha + paths: + - 'v3/internal/commands/build_assets/docker/Dockerfile.cross' + +env: + REGISTRY: ghcr.io + IMAGE_NAME: wailsapp/wails-cross + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + outputs: + image_tag: ${{ steps.vars.outputs.image_version }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ inputs.branch || github.ref }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set build variables + id: vars + run: | + echo "sdk_version=${{ inputs.sdk_version || '14.5' }}" >> $GITHUB_OUTPUT + echo "zig_version=${{ inputs.zig_version || '0.14.0' }}" >> $GITHUB_OUTPUT + echo "image_version=${{ inputs.image_version || 'latest' }}" >> $GITHUB_OUTPUT + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=raw,value=latest + type=raw,value=${{ steps.vars.outputs.image_version }} + type=raw,value=sdk-${{ steps.vars.outputs.sdk_version }} + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: v3/internal/commands/build_assets/docker + file: v3/internal/commands/build_assets/docker/Dockerfile.cross + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: | + ${{ steps.meta.outputs.labels }} + io.wails.zig.version=${{ steps.vars.outputs.zig_version }} + io.wails.sdk.version=${{ steps.vars.outputs.sdk_version }} + 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 + + # Test cross-compilation for all platforms + test-cross-compile: + needs: build + if: ${{ inputs.skip_tests != 'true' }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + # Darwin targets (Zig + macOS SDK) - no platform emulation needed + - 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 + - os: windows + arch: amd64 + platform: "" + expected_file: "PE32\\+ executable.*x86-64" + - os: windows + arch: arm64 + platform: "" + expected_file: "PE32\\+ executable.*Aarch64" + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ inputs.branch || github.ref }} + + - name: Set up QEMU + if: matrix.platform != '' + uses: docker/setup-qemu-action@v3 + + - 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 CGO project + run: | + mkdir -p test-project + cd test-project + + # Create a minimal CGO test program + cat > main.go << 'EOF' + package main + + /* + #include + + 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 }}" + 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 + run: | + cd test-project/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) + else + BINARY=$(ls test-cgo-${{ matrix.os }}-${{ matrix.arch }} 2>/dev/null || ls test-cgo* | grep -v '.exe' | head -1) + 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 }}" + 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=$(ls test-cgo-${{ matrix.os }}-${{ matrix.arch }} 2>/dev/null || ls test-cgo* | grep -v '.exe' | head -1) + + echo "## Library Dependencies for $BINARY" + 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 + echo "" + echo "### Verifying required libraries..." + NEEDED=$(readelf -d "$BINARY" | 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 + echo "✅ $lib" + else + echo "❌ $lib MISSING" + MISSING="$MISSING $lib" + fi + 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] + if: always() && inputs.skip_tests != 'true' + runs-on: ubuntu-latest + steps: + - name: Check test results + run: | + echo "## Cross-Compilation Test Results" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "${{ needs.test-cross-compile.result }}" = "success" ]; then + echo "✅ **CGO Tests**: All 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 + 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 + + # Fail if any test failed + if [ "${{ needs.test-cross-compile.result }}" != "success" ] || [ "${{ needs.test-non-cgo.result }}" != "success" ]; then + echo "" + echo "❌ Some tests failed. Check the individual job logs for details." + exit 1 + fi diff --git a/v3/internal/commands/build_assets/docker/Dockerfile.cross b/v3/internal/commands/build_assets/docker/Dockerfile.cross index dc4c9602f..3b24529f9 100644 --- a/v3/internal/commands/build_assets/docker/Dockerfile.cross +++ b/v3/internal/commands/build_assets/docker/Dockerfile.cross @@ -1,7 +1,8 @@ # Cross-compile Wails v3 apps to any platform # -# Uses Zig as C compiler + macOS SDK for darwin targets -# Supports both amd64 and arm64 host architectures +# Darwin: Zig + macOS SDK +# Linux: GCC (use --platform to match target arch) +# Windows: Zig + bundled mingw # # Usage: # docker build -t wails-cross -f Dockerfile.cross . @@ -15,13 +16,22 @@ # Multi-arch build: # docker buildx build --platform linux/amd64,linux/arm64 -t wails-cross -f Dockerfile.cross . -FROM golang:1.25-alpine +FROM golang:1.25-bookworm -RUN apk add --no-cache curl xz nodejs npm +ARG TARGETARCH + +# Install base tools, GCC, and GTK/WebKit dev packages +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl xz-utils nodejs npm pkg-config gcc libc6-dev \ + libgtk-3-dev libwebkit2gtk-4.1-dev \ + 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 -ARG TARGETARCH RUN ZIG_ARCH=$(case "${TARGETARCH}" in arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \ curl -L "https://ziglang.org/download/${ZIG_VERSION}/zig-linux-${ZIG_ARCH}-${ZIG_VERSION}.tar.xz" \ | tar -xJ -C /opt \ @@ -35,7 +45,9 @@ RUN curl -L "https://github.com/joseluisq/macosx-sdks/releases/download/${MACOS_ ENV MACOS_SDK_PATH=/opt/macos-sdk -# Create zig cc wrappers for each target +# Create Zig CC wrappers for Darwin and Windows targets +# (Linux uses native GCC with --platform flag for architecture matching) + # Darwin arm64 COPY <<'ZIGWRAP' /usr/local/bin/zcc-darwin-arm64 #!/bin/sh @@ -76,45 +88,7 @@ exec zig cc -fno-sanitize=all -target x86_64-macos-none -isysroot /opt/macos-sdk 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 +# Windows amd64 - uses Zig's bundled mingw COPY <<'ZIGWRAP' /usr/local/bin/zcc-windows-amd64 #!/bin/sh ARGS="" @@ -134,7 +108,7 @@ exec zig cc -target x86_64-windows-gnu $ARGS ZIGWRAP RUN chmod +x /usr/local/bin/zcc-windows-amd64 -# Windows arm64 +# Windows arm64 - uses Zig's bundled mingw COPY <<'ZIGWRAP' /usr/local/bin/zcc-windows-arm64 #!/bin/sh ARGS="" @@ -163,13 +137,42 @@ 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 ;; + 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=gcc + export GOARCH=arm64 + export GOOS=linux + ;; + linux-amd64|linux-x86_64) + export CC=gcc + 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 diff --git a/v3/internal/commands/build_assets/linux/Taskfile.yml b/v3/internal/commands/build_assets/linux/Taskfile.yml index c29558671..e6a3d22cc 100644 --- a/v3/internal/commands/build_assets/linux/Taskfile.yml +++ b/v3/internal/commands/build_assets/linux/Taskfile.yml @@ -69,7 +69,7 @@ tasks: Docker image '{{.CROSS_IMAGE}}' not found. Build it first: wails3 task setup:docker cmds: - - docker run --rm -v "{{.ROOT_DIR}}:/app" {{.GO_CACHE_MOUNT}} {{.REPLACE_MOUNTS}} -e APP_NAME="{{.APP_NAME}}" "{{.CROSS_IMAGE}}" linux {{.DOCKER_ARCH}} + - docker run --rm --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}} - mv "bin/{{.APP_NAME}}-linux-{{.DOCKER_ARCH}}" "{{.OUTPUT}}"