diff --git a/.changeset/sad-worms-tap.md b/.changeset/sad-worms-tap.md new file mode 100644 index 00000000..8b9ec38c --- /dev/null +++ b/.changeset/sad-worms-tap.md @@ -0,0 +1,5 @@ +--- +"wine": major +--- + +feat: update wine to latest 11.0 diff --git a/.github/workflows/build-wine.yaml b/.github/workflows/build-wine.yaml new file mode 100644 index 00000000..809b042d --- /dev/null +++ b/.github/workflows/build-wine.yaml @@ -0,0 +1,49 @@ +name: Build Wine + +on: + workflow_call: + workflow_dispatch: + +concurrency: + group: ${{ github.head_ref }} + cancel-in-progress: true # for the love of all that is holy, cancel in progress runs on new pushes. linux builds take forever + +jobs: + mac: + runs-on: macos-15 + timeout-minutes: 60 + steps: + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + + - name: Build for macOS + run: bash ./build.sh + shell: bash + working-directory: ./packages/wine + + - name: Upload appimage artifact + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 + with: + name: wine-macos + path: packages/wine/out/**/*.tar.gz + if-no-files-found: error + retention-days: 1 + linux: + runs-on: ubuntu-latest + timeout-minutes: 120 # holy moly docker builds take forever + steps: + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + + - name: Build for Linux + run: bash ./build.sh + shell: bash + working-directory: ./packages/wine + + - name: Upload appimage artifact + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 + with: + name: wine-linux + path: packages/wine/out/**/*.tar.gz + if-no-files-found: error + retention-days: 1 diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 17788146..e83d6d8e 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -80,6 +80,11 @@ jobs: uses: ./.github/workflows/build-squirrel.yaml needs: detect + wine: + if: contains(needs.detect.outputs.matrix, '"wine"') + uses: ./.github/workflows/build-wine.yaml + needs: detect + combine: runs-on: macos-latest needs: [ @@ -89,7 +94,8 @@ jobs: ran, nsis, dmg-builder, - squirrel + squirrel, + wine ] if: | always() && diff --git a/.gitignore b/.gitignore index f85e5270..9739fc0a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,7 @@ artifacts artifacts-staging **/build .buildx-cache + +packages/**/*.zip +packages/**/*.tar.gz +packages/**/*.7z \ No newline at end of file diff --git a/packages/wine/README.md b/packages/wine/README.md new file mode 100644 index 00000000..fa374396 --- /dev/null +++ b/packages/wine/README.md @@ -0,0 +1,150 @@ +# Wine Portable Bundle Builder + +Compile Wine from source to create portable, self-contained bundles. + +**Simple approach:** Builds for current architecture only, uses Rosetta when needed. + +## Quick Start + +```bash +./build.sh +``` + +Builds Wine for your current architecture (30-60 minutes). + +## Prerequisites + +```bash +# Install Xcode Command Line Tools +xcode-select --install +``` + +**That's it.** No Homebrew, no dependencies, nothing. + +Builds Wine **without FreeType** (fonts work via macOS fallback). + +## Architecture Strategy + +**Always builds x86_64 Wine** (works on both Intel and ARM): +- **Intel Mac** → Builds x86_64 natively +- **ARM Mac** → Builds x86_64 via Rosetta (uses x86_64 Homebrew at `/usr/local`) + +**Why x86_64 on ARM?** +- ✅ Much simpler (no PE cross-compilation tools needed) +- ✅ Works perfectly via Rosetta +- ✅ Same binary works on both Intel and ARM Macs +- ✅ Avoids ARM64 Wine complications + +**Native ARM64 Wine is not worth it:** +- ❌ Requires llvm-mingw cross-compilation setup +- ❌ Much more complex to build +- ❌ Limited benefit (Rosetta works great) + +## Environment Variables + +| Variable | Description | Default | +|----------|-------------|---------| +| `WINE_VERSION` | Wine version to build | `11.0` | +| `BUILD_DIR` | Build output directory | `./build` | + +## Examples + +```bash +# Build Wine 9.0 (default) +./build.sh + +# Build Wine 8.0 +WINE_VERSION=8.0 ./build.sh + +# Build for Linux (requires Docker) +OS_TARGET=linux ./build.sh +``` + +## GitHub Actions Usage + +Perfect for CI/CD with separate runners: + +```yaml +jobs: + build-intel: + runs-on: macos-15-intel # Intel runner + steps: + - run: ./build.sh + # Produces: wine-11.0-darwin-x86_64.tar.gz + + build-arm: + runs-on: macos-15 # ARM runner + steps: + - run: ./build.sh + # Produces: wine-11.0-darwin-x86_64.tar.gz (via Rosetta) +``` + +Both produce x86_64 binaries that work everywhere! + +## What You Get + +A **portable Wine bundle**: +- Latest Wine version (not stuck on 4.0.3!) +- Pre-initialized Wine prefix +- Cleaned up (~60% smaller) +- Self-contained, no dependencies +- Works on both Intel and ARM Macs + +## Output Structure + +``` +wine-9.0-darwin-x86_64/ +├── bin/wine64 # Wine binary +├── lib/ # Libraries +├── share/wine/ # Data files +├── wine-home/ # Pre-initialized prefix +│ ├── dosdevices/ +│ │ ├── c: -> ../drive_c +│ │ └── z: -> / +│ ├── drive_c/ +│ └── *.reg +├── wine-launcher.sh # Launcher +└── README.md +``` + +## Using the Bundle + +```bash +tar -xzf wine-9.0-darwin-x86_64.tar.gz +cd wine-9.0-darwin-x86_64 +./wine-launcher.sh notepad +``` + +## Build Time + +- **30-60 minutes** depending on your machine +- First build downloads dependencies (~5-10 min extra) +- Subsequent builds reuse cached dependencies + +## Cleanup + +```bash +# Remove build artifacts but keep dependencies (for faster rebuilds) +rm -rf build/wine-* build/downloads + +# Remove everything +rm -rf build/ +``` + +## Available Wine Versions + +- **9.0** (latest stable, recommended) +- **8.0** +- **7.0** + +Source: https://dl.winehq.org/wine/source/ + +## Why Not Use Homebrew? + +Homebrew installs to system directories with external dependencies. Not portable! + +This creates **self-contained bundles** you can distribute. + +## License + +Wine is free software released under the GNU LGPL. \ No newline at end of file diff --git a/packages/wine/assets/Brewfile b/packages/wine/assets/Brewfile new file mode 100644 index 00000000..73ef79ca --- /dev/null +++ b/packages/wine/assets/Brewfile @@ -0,0 +1,19 @@ + + + +# AUTO-GENERATED – DO NOT EDIT +# Build helpers +# Generated from Wine config.log +# Toolchain +# Wine dependencies +brew "autoconf" +brew "automake" +brew "bison" +brew "flex" +brew "gettext" +brew "gnutls" +brew "libtool" +brew "make" +brew "mingw-w64" +brew "pkg-config" +brew "xz" diff --git a/packages/wine/assets/Dockerfile b/packages/wine/assets/Dockerfile new file mode 100644 index 00000000..85633fc5 --- /dev/null +++ b/packages/wine/assets/Dockerfile @@ -0,0 +1,64 @@ +# Dockerfile for building Wine for Linux x86_64 +# Can be built on ARM64 macOS using Docker Desktop with --platform flag + +FROM --platform=linux/amd64 ubuntu:22.04 + +# Avoid interactive prompts during package installation +ENV DEBIAN_FRONTEND=noninteractive + +# Install build dependencies +RUN apt-get update && apt-get install -y \ + build-essential \ + curl \ + flex \ + bison \ + gcc-mingw-w64 \ + libc6-dev \ + libfreetype6-dev \ + libgnutls28-dev \ + libpng-dev \ + libxml2-dev \ + libxslt1-dev \ + zlib1g-dev \ + libncurses-dev \ + libgstreamer1.0-dev \ + libgstreamer-plugins-base1.0-dev \ + libvulkan-dev \ + libudev-dev \ + libsdl2-dev \ + libcups2-dev \ + libdbus-1-dev \ + libfontconfig1-dev \ + libfreetype6-dev \ + libgphoto2-dev \ + liblcms2-dev \ + libldap2-dev \ + libpulse-dev \ + libsane-dev \ + libtiff-dev \ + libunwind-dev \ + libusb-1.0-0-dev \ + libx11-dev \ + libxcomposite-dev \ + libxcursor-dev \ + libxext-dev \ + libxi-dev \ + libxinerama-dev \ + libxrandr-dev \ + libxrender-dev \ + libxxf86vm-dev \ + xz-utils \ + && rm -rf /var/lib/apt/lists/* + +# Create build directory +WORKDIR /build + +# Script will be mounted or copied +COPY build-in-docker.sh /build/build-in-docker.sh +COPY generate-trace-exes.sh /build/generate-trace-exes.sh +RUN chmod +x /build/build-in-docker.sh /build/generate-trace-exes.sh + +# Output directory +VOLUME /output + +CMD ["/build/build-in-docker.sh"] \ No newline at end of file diff --git a/packages/wine/assets/build-in-docker.sh b/packages/wine/assets/build-in-docker.sh new file mode 100644 index 00000000..90e52401 --- /dev/null +++ b/packages/wine/assets/build-in-docker.sh @@ -0,0 +1,324 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Enable command tracing +set -x + +WINE_VERSION=${WINE_VERSION:-11.0} +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +BUILD_DIR=${BUILD_DIR:-$ROOT_DIR/build} + +# NOTE: update the checksums here as new versions are added +get_checksum() { + case "$1" in + 11.0) echo "c07a6857933c1fc60dff5448d79f39c92481c1e9db5aa628db9d0358446e0701" ;; + *) exit 1 ;; + esac +} + +PLATFORM_ARCH="x86_64" +OS_TARGET="${OS_TARGET:-$(uname -s | tr '[:upper:]' '[:lower:]')}" +HOST_ARCH=$(arch) + +IS_DARWIN=false +if [ "$OS_TARGET" = "darwin" ]; then + IS_DARWIN=true +fi + +ARCH_CMD='' +if $IS_DARWIN; then + echo "🍺 Ensuring Homebrew dependencies (brew bundle)" + + if ! command -v brew >/dev/null 2>&1; then + echo "❌ Homebrew not found" + exit 1 + fi + + if [ ! -f "$SCRIPT_DIR/Brewfile" ]; then + echo "❌ Brewfile not found" + exit 1 + fi + + ( + cd "$SCRIPT_DIR" + if ! brew bundle check; then + echo "📦 Installing missing dependencies" + brew bundle install + else + echo "🍻 Brewfile dependencies already satisfied" + fi + ) + + BREW_PREFIX="$(brew --prefix)" + export PATH="$BREW_PREFIX/opt/bison/bin:$PATH" + export PATH="$BREW_PREFIX/opt/flex/bin:$PATH" + export PATH="$BREW_PREFIX/opt/make/libexec/gnubin:$PATH" + export PKG_CONFIG_PATH="$BREW_PREFIX/x86_64-w64-mingw32/lib/pkgconfig:${PKG_CONFIG_PATH:$BREW_PREFIX/lib/pkgconfig}" + + # Sanity checks (fail fast) + bison --version | grep -E '3\.' >/dev/null || { + echo "❌ Wrong bison in PATH" + which bison + bison --version + exit 1 + } + + if [ "$HOST_ARCH" = 'arm64' ]; then + echo "🔄 ARM64 - building x86_64 via Rosetta" + ARCH_CMD='arch -x86_64' + export SDKROOT="$(xcode-select -p)/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk" + fi +fi + +if $IS_DARWIN; then + export CC=x86_64-w64-mingw32-gcc + export CXX=x86_64-w64-mingw32-g++ +else + unset CC + unset CXX +fi + +execute_cmd() { + if [[ -n "$ARCH_CMD" ]]; then + $ARCH_CMD "$@" + else + "$@" + fi +} + +CHECKSUM=$(get_checksum "$WINE_VERSION") +WINE_MAJOR=$(echo "$WINE_VERSION" | cut -d. -f1) +WINE_URL="https://dl.winehq.org/wine/source/${WINE_MAJOR}.0/wine-${WINE_VERSION}.tar.xz" + +DOWNLOAD_DIR="$BUILD_DIR/downloads" +SOURCE_DIR="$BUILD_DIR/wine-${WINE_VERSION}" +BUILD_WINE_DIR="$BUILD_DIR/wine64-build" +STAGE_DIR="$BUILD_DIR/wine-stage" +OUTPUT_DIR="$BUILD_DIR/wine-${WINE_VERSION}-darwin-${PLATFORM_ARCH}" +TRACE_LOG="$BUILD_DIR/dll-trace.log" +SYS32_ALLOW="$BUILD_DIR/system32.allow" +WINE_ALLOW="$BUILD_DIR/wine.allow" + +mkdir -p "$DOWNLOAD_DIR" + +# Download and verify archive +ARCHIVE="$DOWNLOAD_DIR/wine-${WINE_VERSION}.tar.xz" +if [ ! -f "$ARCHIVE" ]; then + echo "📥 Downloading Wine ${WINE_VERSION}..." + curl -L --progress-bar "$WINE_URL" -o "$ARCHIVE" + + if [ -n "$CHECKSUM" ]; then + ACTUAL=$(shasum -a 256 "$ARCHIVE" | awk '{print $1}') + if [ "$ACTUAL" != "$CHECKSUM" ]; then + echo "❌ Checksum failed: expected $CHECKSUM, got $ACTUAL" + exit 1 + fi + echo "✅ Verified" + fi +fi + +# Extract source +if [ ! -d "$SOURCE_DIR" ]; then + echo "📂 Extracting..." + tar -xJf "$ARCHIVE" -C "$BUILD_DIR" +fi + +# Configure Wine +echo "⚙️ Configuring Wine (without FreeType)..." +rm -rf "$BUILD_WINE_DIR" "$STAGE_DIR" +mkdir -p "$BUILD_WINE_DIR" "$STAGE_DIR" +cd "$BUILD_WINE_DIR" + +CONFIGURE_FLAGS=( + --prefix="$STAGE_DIR" + --enable-win64 + --without-x + --without-cups + --without-dbus + --without-freetype +) + +if $IS_DARWIN; then + CONFIGURE_FLAGS+=( + --host=x86_64-w64-mingw32 + ) +fi + +execute_cmd "$SOURCE_DIR/configure" \ + "${CONFIGURE_FLAGS[@]}" \ + 2>&1 | tee configure.log + +if [ "$OS_TARGET" = "darwin" ]; then + # 🧠 Auto-update Brewfile if dependencies changed + bash "$SCRIPT_DIR/generate-brewfile.sh" "$BUILD_WINE_DIR/config.log" +fi + +echo "🔨 Building..." +execute_cmd make -j$(sysctl -n hw.ncpu) + +echo "📦 Installing..." +execute_cmd make install + +cd "$ROOT_DIR" + +# Remove unnecessary directories +rm -rf "$STAGE_DIR/share/man" "$STAGE_DIR/share/doc" "$STAGE_DIR/share/gtk-doc" "$STAGE_DIR/include" "$STAGE_DIR/share/applications" + +# Adjust RPATHs for all binaries +add_rpath_if_missing() { + local binary="$1" + local rpath="$2" + + echo "🔍 Checking RPATH in: $binary" + + # List existing rpaths + if otool -l "$binary" | grep -A2 LC_RPATH | grep -q "$rpath"; then + echo "✅ RPATH already present: $rpath — skipping 🛑" + return 0 + fi + + echo "➕ Adding RPATH: $rpath" + install_name_tool -add_rpath "$rpath" "$binary" +} + +for binary in wine64 wine wineserver wineboot winecfg; do + binary_path="$STAGE_DIR/bin/$binary" + [ -f "$binary_path" ] && add_rpath_if_missing "$binary_path" "@executable_path/../lib" +done + +# Initialize Wine prefix +echo "🍇 Initializing Wine prefix..." +export WINEPREFIX="$STAGE_DIR/wine-home" +export WINEARCH=win64 +export WINEDEBUG=-all +export DISPLAY=:99 # Virtual display for headless + +if $IS_DARWIN; then + execute_cmd "$STAGE_DIR/bin/wineboot" --init + sleep 2 +else + # Start a virtual X server if not running + if ! command -v Xvfb &> /dev/null; then + echo "⚠️ Xvfb not available" + exit 1 + else + Xvfb :99 -screen 0 1024x768x24 & + XVFB_PID=$! + sleep 2 + + "$STAGE_DIR/bin/wineboot" --init + sleep 2 + + # Kill Xvfb + kill $XVFB_PID + fi +fi + +############################################ +# 🧪 DLL TRACE +############################################ + +echo "🧪 Generating DLL load traces" +TRACE_EXES_FILE=$( + sh "$SCRIPT_DIR/generate-trace-exes.sh" \ + | grep '^EXE_LIST_FILE=' \ + | cut -d= -f2 +) + +echo "🧪 Tracing DLL loads" +: > "$TRACE_LOG" + +export WINEDEBUG=+loaddll + +while IFS= read -r exe; do + [ -z "$exe" ] && continue + echo "▶️ Tracing $exe" + "$STAGE_DIR/bin/wine" "$exe" >> "$TRACE_LOG" 2>&1 || true +done < "$TRACE_EXES_FILE" + +############################################ +# 🧠 GENERATE ALLOW-LISTS +############################################ + +echo "🧠 Generating allow-lists" + +# Extract system32 DLL names +grep -o 'system32\\\\[^"]*\.dll' "$TRACE_LOG" \ +| sed 's|.*system32\\\\||' \ +| tr 'A-Z' 'a-z' \ +| sort -u > "$SYS32_ALLOW" + +# Convert foo.dll → foo (for dll.so matching) +sed 's/\.dll$//' "$SYS32_ALLOW" \ +| sort -u > "$WINE_ALLOW" + +echo "✅ Allowed system32 DLLs:" +cat "$SYS32_ALLOW" + +############################################ +# 🔥 PRUNE lib/wine/*-windows +############################################ + +echo "🔥 Pruning Wine Windows DLLs" +WINE_WINDOWS_DIR="$STAGE_DIR/lib/wine/${PLATFORM_ARCH}-windows" + +for f in "$WINE_WINDOWS_DIR"/*.dll.so; do + [ ! -f "$f" ] && continue + base="$(basename "$f" .dll.so)" + if ! grep -qx "$base" "$WINE_ALLOW"; then + rm -f "$f" + fi +done + +############################################ +# 🔥 PRUNE PREFIX system32 +############################################ + +echo "🔥 Pruning prefix system32" +SYSTEM32_DIR="$WINEPREFIX/drive_c/windows/system32" + +for f in "$SYSTEM32_DIR"/*.dll; do + [ ! -f "$f" ] && continue + lower="$(basename "$f" | tr 'A-Z' 'a-z')" + if ! grep -qx "$lower" "$SYS32_ALLOW"; then + rm -f "$f" + fi +done + +############################################ +# 🧹 REMOVE BULK WINDOWS CONTENT +############################################ + +echo "🧹 Removing Windows bulk" +WINDOWS_DIR="$WINEPREFIX/drive_c/windows" + +rm -rf \ +"$WINDOWS_DIR/Installer" \ +"$WINDOWS_DIR/Microsoft.NET" \ +"$WINDOWS_DIR/mono" \ +"$WINDOWS_DIR/syswow64" \ +"$WINDOWS_DIR/logs" \ +"$WINDOWS_DIR/inf" \ +"$WINDOWS_DIR/Temp" \ +"$WINDOWS_DIR/system32/gecko" + +############################################ +# 🪓 STRIP BINARIES +############################################ + +echo "🪓 Stripping binaries" +find "$STAGE_DIR/bin" "$STAGE_DIR/lib" -type f -perm +111 -exec strip -x {} \; 2>/dev/null || true + +############################################ +# 📦 PACKAGE +############################################ + +echo "📦 Packaging archive" +mkdir -p "$OUTPUT_DIR" +cp -R "$STAGE_DIR/"* "$OUTPUT_DIR/" + +tar -C "$BUILD_DIR" -cJf "$ROOT_DIR/wine-${WINE_VERSION}-${PLATFORM}-${PLATFORM_ARCH}.tar.xz" "$(basename "$OUTPUT_DIR")" + +echo "✅ DONE" +du -sh "$ROOT_DIR/wine-${WINE_VERSION}-${PLATFORM}-${PLATFORM_ARCH}.tar.xz" diff --git a/packages/wine/assets/build-linux.sh b/packages/wine/assets/build-linux.sh new file mode 100644 index 00000000..fd8af989 --- /dev/null +++ b/packages/wine/assets/build-linux.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Build Wine for Linux x86_64 using Docker on ARM64 macOS +# This script handles building Wine in a Docker container with the correct platform + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +ROOT_DIR="${ROOT_DIR:-$SCRIPT_DIR/..}" +WINE_VERSION="${WINE_VERSION:-11.0}" +OUTPUT_DIR="${OUTPUT_DIR:-$ROOT_DIR/out/wine}" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +log_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Check if Docker is available +if ! command -v docker &> /dev/null; then + log_error "Docker is not installed or not in PATH" + log_info "Please install Docker Desktop for Mac from https://www.docker.com/products/docker-desktop" + exit 1 +fi + +# Check if Docker daemon is running +if ! docker info &> /dev/null; then + log_error "Docker daemon is not running" + log_info "Please start Docker Desktop" + exit 1 +fi + +# Detect host architecture +HOST_ARCH=$(uname -m) +log_info "Host architecture: $HOST_ARCH" + +if [ "$HOST_ARCH" = "arm64" ]; then + log_info "ARM64 Mac detected - will build x86_64 Linux Wine via Docker emulation" +else + log_info "x86_64 Mac detected - will build natively in Docker" +fi + +# Create output directory +mkdir -p "$OUTPUT_DIR" + +# Build Docker image +IMAGE_NAME="wine-builder-linux" +log_info "Building Docker image: $IMAGE_NAME" + +docker build \ +--platform linux/amd64 \ +-t "$IMAGE_NAME" \ +-f "$SCRIPT_DIR/Dockerfile" \ +"$SCRIPT_DIR" + +if [ $? -ne 0 ]; then + log_error "Failed to build Docker image" + exit 1 +fi + +log_info "Docker image built successfully" + +# Run the build in Docker +log_info "Starting Wine build for Linux (Wine $WINE_VERSION)..." +log_warn "This may take 30-60 minutes on ARM64 Mac due to emulation" + +docker run \ + --platform linux/amd64 \ + --rm \ + -v "$OUTPUT_DIR:/output" \ + -e WINE_VERSION="$WINE_VERSION" \ + "$IMAGE_NAME" + +if [ $? -ne 0 ]; then + log_error "Wine build failed" + exit 1 +fi + +log_info "Wine build completed successfully!" +log_info "Output location: $OUTPUT_DIR" + +# List the built artifacts +if [ -f "$OUTPUT_DIR/wine-${WINE_VERSION}-linux-x86_64.tar.xz" ]; then + log_info "Build artifact:" + ls -lh "$OUTPUT_DIR/wine-${WINE_VERSION}-linux-x86_64.tar.xz" +else + log_warn "Expected output file not found" + log_info "Contents of output directory:" + ls -lh "$OUTPUT_DIR" + exit 1 +fi \ No newline at end of file diff --git a/packages/wine/assets/build-mac.sh b/packages/wine/assets/build-mac.sh new file mode 100644 index 00000000..e014bd75 --- /dev/null +++ b/packages/wine/assets/build-mac.sh @@ -0,0 +1,264 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Enable command tracing +set -x + +WINE_VERSION=${WINE_VERSION:-11.0} +BUILD_DIR=${BUILD_DIR:-$ROOT_DIR/build} + +# NOTE: update the checksums here as new versions are added +get_checksum() { + case "$1" in + 11.0) echo "c07a6857933c1fc60dff5448d79f39c92481c1e9db5aa628db9d0358446e0701" ;; + *) exit 1 ;; + esac +} + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +PLATFORM_ARCH="x86_64" +HOST_ARCH=$(arch) + +if [ "$HOST_ARCH" = 'arm64' ]; then + echo "🔄 ARM64 - building x86_64 via Rosetta" + ARCH_CMD='arch -x86_64' + export SDKROOT="$(xcode-select -p)/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk" +else + echo "🍺 Intel - building x86_64" + ARCH_CMD='' +fi + +execute_cmd() { + if [ -n "$ARCH_CMD" ]; then + $ARCH_CMD "$@" + else + "$@" + fi +} + +echo "🍺 Ensuring Homebrew dependencies (brew bundle)" + +if ! command -v brew >/dev/null 2>&1; then + echo "❌ Homebrew not found" + exit 1 +fi + +if [ ! -f "$SCRIPT_DIR/Brewfile" ]; then + echo "❌ Brewfile not found" + exit 1 +fi + +( + cd "$SCRIPT_DIR" + if ! brew bundle check; then + echo "📦 Installing missing dependencies" + brew bundle install + else + echo "🍻 Brewfile dependencies already satisfied" + fi +) + +CHECKSUM=$(get_checksum "$WINE_VERSION") +WINE_MAJOR=$(echo "$WINE_VERSION" | cut -d. -f1) +WINE_URL="https://dl.winehq.org/wine/source/${WINE_MAJOR}.0/wine-${WINE_VERSION}.tar.xz" + +DOWNLOAD_DIR="$BUILD_DIR/downloads" +SOURCE_DIR="$BUILD_DIR/wine-${WINE_VERSION}" +BUILD_WINE_DIR="$BUILD_DIR/wine64-build" +STAGE_DIR="$BUILD_DIR/wine-stage" +OUTPUT_DIR="$BUILD_DIR/wine-${WINE_VERSION}-darwin-${PLATFORM_ARCH}" +TRACE_LOG="$BUILD_DIR/dll-trace.log" +SYS32_ALLOW="$BUILD_DIR/system32.allow" +WINE_ALLOW="$BUILD_DIR/wine.allow" + +mkdir -p "$DOWNLOAD_DIR" + +# Download and verify archive +ARCHIVE="$DOWNLOAD_DIR/wine-${WINE_VERSION}.tar.xz" +if [ ! -f "$ARCHIVE" ]; then + echo "📥 Downloading Wine ${WINE_VERSION}..." + curl -L --progress-bar "$WINE_URL" -o "$ARCHIVE" + + if [ -n "$CHECKSUM" ]; then + ACTUAL=$(shasum -a 256 "$ARCHIVE" | awk '{print $1}') + if [ "$ACTUAL" != "$CHECKSUM" ]; then + echo "❌ Checksum failed: expected $CHECKSUM, got $ACTUAL" + exit 1 + fi + echo "✅ Verified" + fi +fi + +# Extract source +if [ ! -d "$SOURCE_DIR" ]; then + echo "📂 Extracting..." + tar -xJf "$ARCHIVE" -C "$BUILD_DIR" +fi + +# Configure Wine +echo "⚙️ Configuring Wine (without FreeType)..." +rm -rf "$BUILD_WINE_DIR" "$STAGE_DIR" +mkdir -p "$BUILD_WINE_DIR" "$STAGE_DIR" +cd "$BUILD_WINE_DIR" + +execute_cmd "$SOURCE_DIR/configure" \ + --prefix="$STAGE_DIR" \ + --enable-win64 \ + --without-x \ + --without-cups \ + --without-dbus \ + --without-freetype \ + 2>&1 | tee configure.log + +# 🧠 Auto-update Brewfile if dependencies changed +bash "$SCRIPT_DIR/generate-brewfile.sh" "$BUILD_WINE_DIR/config.log" + +echo "🔨 Building..." +execute_cmd make -j$(sysctl -n hw.ncpu) + +echo "📦 Installing..." +execute_cmd make install + +cd "$ROOT_DIR" + +# Remove unnecessary directories +rm -rf "$STAGE_DIR/share/man" "$STAGE_DIR/share/doc" "$STAGE_DIR/share/gtk-doc" "$STAGE_DIR/include" "$STAGE_DIR/share/applications" + +# Adjust RPATHs for all binaries +add_rpath_if_missing() { + local binary="$1" + local rpath="$2" + + echo "🔍 Checking RPATH in: $binary" + + # List existing rpaths + if otool -l "$binary" | grep -A2 LC_RPATH | grep -q "$rpath"; then + echo "✅ RPATH already present: $rpath — skipping 🛑" + return 0 + fi + + echo "➕ Adding RPATH: $rpath" + install_name_tool -add_rpath "$rpath" "$binary" +} + +for binary in wine64 wine wineserver wineboot winecfg; do + binary_path="$STAGE_DIR/bin/$binary" + [ -f "$binary_path" ] && add_rpath_if_missing "$binary_path" "@executable_path/../lib" +done + +# Initialize Wine prefix +echo "🍇 Initializing Wine prefix..." +export WINEPREFIX="$STAGE_DIR/wine-home" +export WINEARCH=win64 +export WINEDEBUG=-all +execute_cmd "$STAGE_DIR/bin/wineboot" --init +sleep 2 + +############################################ +# 🧪 DLL TRACE +############################################ + +echo "🧪 Generating DLL load traces" +TRACE_EXES_FILE=$( + sh "$SCRIPT_DIR/generate-trace-exes.sh" \ + | grep '^EXE_LIST_FILE=' \ + | cut -d= -f2 +) + +echo "🧪 Tracing DLL loads" +: > "$TRACE_LOG" + +export WINEDEBUG=+loaddll + +while IFS= read -r exe; do + [ -z "$exe" ] && continue + echo "▶️ Tracing $exe" + "$STAGE_DIR/bin/wine" "$exe" >> "$TRACE_LOG" 2>&1 || true +done < "$TRACE_EXES_FILE" + +############################################ +# 🧠 GENERATE ALLOW-LISTS +############################################ + +echo "🧠 Generating allow-lists" + +# Extract system32 DLL names +grep -o 'system32\\\\[^"]*\.dll' "$TRACE_LOG" \ +| sed 's|.*system32\\\\||' \ +| tr 'A-Z' 'a-z' \ +| sort -u > "$SYS32_ALLOW" + +# Convert foo.dll → foo (for dll.so matching) +sed 's/\.dll$//' "$SYS32_ALLOW" \ +| sort -u > "$WINE_ALLOW" + +echo "✅ Allowed system32 DLLs:" +cat "$SYS32_ALLOW" + +############################################ +# 🔥 PRUNE lib/wine/*-windows +############################################ + +echo "🔥 Pruning Wine Windows DLLs" +WINE_WINDOWS_DIR="$STAGE_DIR/lib/wine/${PLATFORM_ARCH}-windows" + +for f in "$WINE_WINDOWS_DIR"/*.dll.so; do + [ ! -f "$f" ] && continue + base="$(basename "$f" .dll.so)" + if ! grep -qx "$base" "$WINE_ALLOW"; then + rm -f "$f" + fi +done + +############################################ +# 🔥 PRUNE PREFIX system32 +############################################ + +echo "🔥 Pruning prefix system32" +SYSTEM32_DIR="$WINEPREFIX/drive_c/windows/system32" + +for f in "$SYSTEM32_DIR"/*.dll; do + [ ! -f "$f" ] && continue + lower="$(basename "$f" | tr 'A-Z' 'a-z')" + if ! grep -qx "$lower" "$SYS32_ALLOW"; then + rm -f "$f" + fi +done + +############################################ +# 🧹 REMOVE BULK WINDOWS CONTENT +############################################ + +echo "🧹 Removing Windows bulk" +WINDOWS_DIR="$WINEPREFIX/drive_c/windows" + +rm -rf \ +"$WINDOWS_DIR/Installer" \ +"$WINDOWS_DIR/Microsoft.NET" \ +"$WINDOWS_DIR/mono" \ +"$WINDOWS_DIR/syswow64" \ +"$WINDOWS_DIR/logs" \ +"$WINDOWS_DIR/inf" \ +"$WINDOWS_DIR/Temp" \ +"$WINDOWS_DIR/system32/gecko" + +############################################ +# 🪓 STRIP BINARIES +############################################ + +echo "🪓 Stripping binaries" +find "$STAGE_DIR/bin" "$STAGE_DIR/lib" -type f -perm +111 -exec strip -x {} \; 2>/dev/null || true + +############################################ +# 📦 PACKAGE +############################################ + +echo "📦 Packaging archive" +mkdir -p "$OUTPUT_DIR" +cp -R "$STAGE_DIR/"* "$OUTPUT_DIR/" + +tar -C "$BUILD_DIR" -cJf "$ROOT_DIR/wine-${WINE_VERSION}-darwin-${PLATFORM_ARCH}.tar.xz" "$(basename "$OUTPUT_DIR")" + +echo "✅ DONE" +du -sh "$ROOT_DIR/wine-${WINE_VERSION}-darwin-${PLATFORM_ARCH}.tar.xz" diff --git a/packages/wine/assets/generate-brewfile.sh b/packages/wine/assets/generate-brewfile.sh new file mode 100644 index 00000000..f909b6ae --- /dev/null +++ b/packages/wine/assets/generate-brewfile.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +CONFIG_LOG="${CONFIG_LOG:-$SCRIPT_DIR/../build/wine64-build/config.log}" +BREWFILE="$SCRIPT_DIR/Brewfile" +GENERATED="$SCRIPT_DIR/Brewfile.generated" + +if [[ ! -f "$CONFIG_LOG" ]]; then + echo "❌ config.log not found: $CONFIG_LOG" + exit 1 +fi + +echo "🍺 Generating Brewfile from config.log" + +# Map linker libs → Homebrew formulae +map_lib_to_brew() { + case "$1" in + xml2) echo "libxml2" ;; + png) echo "libpng" ;; + jpeg) echo "jpeg-turbo" ;; + gnutls) echo "gnutls" ;; + gmp) echo "gmp" ;; + z) echo "zlib" ;; + bz2) echo "bzip2" ;; + lzma) echo "xz" ;; + *) return 1 ;; + esac +} + +{ + echo "# AUTO-GENERATED – DO NOT EDIT" + echo "# Generated from Wine config.log" + echo + echo "# Toolchain" + echo 'brew "pkg-config"' + echo 'brew "bison"' + echo 'brew "libtool"' + echo 'brew "gettext"' + echo 'brew "flex"' + echo 'brew "mingw-w64"' + echo + + echo "# Wine dependencies" + + grep -oE -e '-l[a-zA-Z0-9_]+' "$CONFIG_LOG" \ + | sed 's/^-l//' \ + | sort -u \ + | while read -r lib; do + if brew_pkg=$(map_lib_to_brew "$lib"); then + echo "brew \"$brew_pkg\"" + fi + done + + echo + echo "# Build helpers" + echo 'brew "autoconf"' + echo 'brew "automake"' + echo 'brew "make"' + echo 'brew "xz"' +} > "$GENERATED" + +# Normalize ordering +sort "$GENERATED" -o "$GENERATED" + +echo "✅ Generated $GENERATED" + +# Compare with committed Brewfile. If CI, fail if out of date. +if [[ "${CI:-}" == "true" ]]; then + if ! diff -u "$BREWFILE" "$GENERATED"; then + echo "❌ Brewfile out of date. Run build locally and commit changes." + exit 1 + fi +fi + +if diff -u "$BREWFILE" "$GENERATED" >/dev/null; then + echo "🍻 Brewfile unchanged" + exit 0 +fi + +echo "⚠️ Brewfile changed – updating" +cp "$GENERATED" "$BREWFILE" +rm "$GENERATED" + +echo +echo "📝 Brewfile updated." +echo "👉 Please review and commit:" +echo " git add Brewfile" +echo " git commit -m 'chore: update Brewfile from Wine build'" diff --git a/packages/wine/assets/generate-trace-exes.sh b/packages/wine/assets/generate-trace-exes.sh new file mode 100644 index 00000000..ecdd5716 --- /dev/null +++ b/packages/wine/assets/generate-trace-exes.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +set -exo pipefail + +echo "🧪 Electron Builder EXE discovery" +echo "--------------------------------" + +# ------------------------------------------------------------ +# Config +# ------------------------------------------------------------ + +BASE_URL="https://github.com/electron-userland/electron-builder-binaries/releases/download/win-codesign@1.1.0" + +TMP_ROOT="$(mktemp -d)" +DOWNLOAD_DIR="$TMP_ROOT/downloads" +UNPACK_DIR="$TMP_ROOT/unpacked" +OUT_FILE="$TMP_ROOT/exe-list.txt" + +mkdir -p "$DOWNLOAD_DIR" "$UNPACK_DIR" + +echo "📂 Temp dir: $TMP_ROOT" +echo "📄 Output: $OUT_FILE" +echo + +# ------------------------------------------------------------ +# filename | sha256 +# ------------------------------------------------------------ + +ARCHIVES=" +windows-kits-bundle-10_0_26100_0.zip 284f18a2fde66e6ecfbefc3065926c9bfdf641761a9e6cd2bd26e18d1e328bf7 +win-codesign-windows-x64.zip 6e5dcc5d7af7c00a7387e2101d1ad986aef80e963a3526da07bd0e65de484c30 +rcedit-windows-2_0_0.zip c66591ebe0919c60231f0bf79ff223e6504bfa69bc13edc1fa8bfc6177b73402 +" + +# ------------------------------------------------------------ +# Download + verify + unpack +# ------------------------------------------------------------ + +echo "$ARCHIVES" | while read NAME SHA; do + [ -z "$NAME" ] && continue + + URL="$BASE_URL/$NAME" + DEST="$DOWNLOAD_DIR/$NAME" + OUT="$UNPACK_DIR/${NAME%.zip}" + + echo "⬇️ Fetching $NAME" + curl -L --retry 3 --retry-delay 2 --progress-bar "$URL" -o "$DEST" + + echo "🔍 Verifying SHA-256" + ACTUAL="$(shasum -a 256 "$DEST" | awk '{print $1}')" + + if [ "$ACTUAL" != "$SHA" ]; then + echo "❌ CHECKSUM FAILURE: $NAME" + echo " Expected: $SHA" + echo " Actual: $ACTUAL" + exit 1 + fi + + echo "✅ Verified" + + echo "📦 Unpacking" + mkdir -p "$OUT" + unzip -q "$DEST" -d "$OUT" + echo +done + +# ------------------------------------------------------------ +# Discover .exe files +# ------------------------------------------------------------ + +echo "🔎 Scanning for Windows executables" +echo "----------------------------------" + +find "$UNPACK_DIR" -type f -iname "*.exe" \ + | sed 's|^.*/||' \ + | sort -u \ + | tee "$OUT_FILE" + +echo +echo "🎉 EXE discovery complete" +echo "📄 File: $OUT_FILE" + +# Print path so caller can capture it +echo +echo "EXE_LIST_FILE=$OUT_FILE" diff --git a/packages/wine/build.sh b/packages/wine/build.sh new file mode 100644 index 00000000..e65ed934 --- /dev/null +++ b/packages/wine/build.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +set -e + +# Wine Portable Bundle Builder +# Compiles Wine from source for distribution + +WINE_VERSION=${WINE_VERSION:-11.0} +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +BUILD_DIR=${BUILD_DIR:-$SCRIPT_DIR/build} +PLATFORM_ARCH=${PLATFORM_ARCH:-$(uname -m)} +OS_TARGET=${OS_TARGET:-$(uname | tr '[:upper:]' '[:lower:]')} + +case "$PLATFORM_ARCH" in + x86_64|amd64|x64) PLATFORM_ARCH="x86_64" ;; + arm64|aarch64) PLATFORM_ARCH="arm64" ;; +esac + +case "$OS_TARGET" in + darwin|macos) OS_TARGET="darwin" ;; + linux) OS_TARGET="linux" ;; +esac + +export WINE_VERSION BUILD_DIR PLATFORM_ARCH OS_TARGET + +echo "" +echo "🍷 Wine Portable Bundle Builder" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "Wine Version: $WINE_VERSION" +echo "Platform: $OS_TARGET" +echo "Architecture: $PLATFORM_ARCH" +echo "Build Dir: $BUILD_DIR" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "" + +mkdir -p "$BUILD_DIR" + +if [ "$OS_TARGET" = "darwin" ]; then + bash "$SCRIPT_DIR/assets/build-in-docker.sh" +elif [ "$OS_TARGET" = "linux" ]; then + bash "$SCRIPT_DIR/assets/build-linux.sh" +else + echo "❌ Unsupported OS: $OS_TARGET" + exit 1 +fi + +echo "" +echo "✅ Portable Wine bundle complete!" +echo "📦 Output: $BUILD_DIR/wine-${WINE_VERSION}-${OS_TARGET}-${PLATFORM_ARCH}.tar.gz" +echo "" \ No newline at end of file