diff --git a/.github/dylib_bundler.zsh b/.github/dylib_bundler.zsh new file mode 100644 index 000000000000..2317f549dfaf --- /dev/null +++ b/.github/dylib_bundler.zsh @@ -0,0 +1,161 @@ +#!/bin/zsh + +# this is a script created for Mythic to bundle its dependencies' dylibs and copy them to the appropriate location. + +set -e + +# Output directory (can be overridden via environment variable) +ENGINE_DIR="${ENGINE_DIR:-Engine/wine}" +BREW_PREFIX=$(brew --prefix) + +# List of GStreamer plugins to bundle, minus the 'libgst' prefix +GSTREAMER_PLUGINS=( + applemedia + asf + audioconvert + audioparsers + audioresample + avi + coreelements + debug + deinterlace + id3demux + isomp4 + libav + opengl + playback + typefindfunctions + videoconvertscale + videofilter + videoparsersbad + wavparse +) + +# list of keg-only formulae and their main dylib names +BUNDLE_LIBS=( + molten-vk:libMoltenVK + sdl2:libSDL2-2.0.0 + freetype:libfreetype + gnutls:libgnutls + libpng:libpng16 + libtiff:libtiff + libpcap:libpcap + jpeg:libjpeg + libffi:libffi +) + +typeset -A seen_dylibs +typeset -a all_dylibs + +get_lib_path() { + local formula="$1" libname="$2" + local prefix=$(brew --prefix "$formula" 2>/dev/null) || return 1 + find "$prefix/lib" -maxdepth 1 -name "${libname}*.dylib" -type f 2>/dev/null | head -1 +} + +resolve_rpath() { + local name="${1#@rpath/}" + + # Check main lib dir first + [[ -f "${BREW_PREFIX}/lib/${name}" ]] && { echo "${BREW_PREFIX}/lib/${name}"; return; } + + # Search keg-only formula lib dirs + for entry in "${BUNDLE_LIBS[@]}"; do + local formula="${entry%%:*}" + local lib_path="$(brew --prefix "$formula" 2>/dev/null)/lib/${name}" + [[ -f "$lib_path" ]] && { echo "$lib_path"; return; } + done +} + +normalize_path() { + python3 -c "import os; print(os.path.realpath('$1'))" 2>/dev/null || echo "$1" +} + +find_dependencies() { + local dylib="$1" + local norm=$(normalize_path "$dylib") + + [[ -n "${seen_dylibs[$norm]}" ]] && return + seen_dylibs[$norm]=1 + all_dylibs+=("$dylib") + + local -a queue=("$dylib") + + while [[ ${#queue[@]} -gt 0 ]]; do + local current="${queue[1]}" + queue=("${queue[@]:1}") + + for ref in $(otool -L "$current" 2>/dev/null | tail -n +2 | awk '/^\t/ {print $1}' | grep '\.dylib'); do + [[ "$ref" == /usr/lib/* || "$ref" == /System/* ]] && continue + [[ "$ref" == @loader_path/* || "$ref" == @executable_path/* ]] && continue + + [[ "$ref" == @rpath/* ]] && { ref=$(resolve_rpath "$ref"); [[ -z "$ref" ]] && continue; } + + norm=$(normalize_path "$ref") + [[ -z "${seen_dylibs[$norm]}" ]] && { + seen_dylibs[$norm]=1 + all_dylibs+=("$ref") + queue+=("$ref") + } + done + done +} + +fix_install_names() { + local file="$1" prefix="$2" + chmod u+w "$file" + install_name_tool -id "${prefix}$(basename "$file")" "$file" 2>/dev/null || true + + otool -L "$file" | grep -v "$file" | awk '{print $1}' | while read -r lib_path; do + [[ "$lib_path" != /usr/lib* && "$lib_path" != /System/* ]] && \ + install_name_tool -change "$lib_path" "${prefix}${lib_path##*/}" "$file" 2>/dev/null || true + done + codesign -fs- "$file" 2>/dev/null || true +} + +copy_dylib() { + local lib="$1" dest_dir="${ENGINE_DIR}/lib" prefix="@loader_path/" + + [[ "$lib" == *"/gstreamer-1.0/"* ]] && { dest_dir="${ENGINE_DIR}/lib/gstreamer-1.0"; prefix="@loader_path/../"; } + + local dest="${dest_dir}/$(basename "$lib")" + mkdir -p "$dest_dir" + [[ -f "$dest" ]] && return 0 + + cp -L "$lib" "$dest" + fix_install_names "$dest" "$prefix" +} + +main() { + local gst_prefix=$(brew --prefix gstreamer) + + echo "=== Processing GStreamer plugins ===" + for plugin in "${GSTREAMER_PLUGINS[@]}"; do + local lib_path="${gst_prefix}/lib/gstreamer-1.0/libgst${plugin}.dylib" + [[ -f "$lib_path" ]] && find_dependencies "$lib_path" || echo "Warning: $plugin not found" + done + + echo "=== Processing libraries ===" + for entry in "${BUNDLE_LIBS[@]}"; do + local formula="${entry%%:*}" libname="${entry#*:}" + local lib_path=$(get_lib_path "$formula" "$libname") + [[ -n "$lib_path" ]] && find_dependencies "$lib_path" || echo "Warning: $formula not found" + done + + echo "=== Copying ${#all_dylibs[@]} dylibs ===" + for dylib in "${all_dylibs[@]}"; do copy_dylib "$dylib"; done + + [[ -d "${gst_prefix}/lib/gstreamer-1.0/include" ]] && { + mkdir -p "${ENGINE_DIR}/lib/gstreamer-1.0" + cp -a "${gst_prefix}/lib/gstreamer-1.0/include" "${ENGINE_DIR}/lib/gstreamer-1.0/" + } + + echo "=== Fixing Wine .so files ===" + for so in "${ENGINE_DIR}"/lib/wine/x86_64-unix/*.so(N); do + otool -L "$so" 2>/dev/null | grep -q "/usr/local\|/opt/homebrew" && fix_install_names "$so" "@rpath/" + done + + echo "=== Done: ${#all_dylibs[@]} dylibs bundled ===" +} + +main "$@" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4758a7ce2c3d..3cd6109a0c7c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,7 +7,7 @@ on: env: MACOSX_DEPLOYMENT_TARGET: 10.15 SDKROOT: /Library/Developer/CommandLineTools/SDKs/MacOSX12.3.sdk - TOOLCHAINS: com.applex.dt.toolchain.Xcode13 + TOOLCHAINS: com.applex.dt.toolchain.Xcode13 # from gcenx PATH: /usr/local/bin:/usr/local/opt/bison/bin:/usr/bin:/bin:/usr/sbin:/sbin @@ -57,7 +57,8 @@ jobs: sudo mkdir -p /Library/Developer/CommandLineTools/SDKs curl -fL https://github.com/Gcenx/macos-sdk/releases/download/12.3/MacOSX12.3.tar.bz2 \ | sudo tar -xj -C /Library/Developer/CommandLineTools/SDKs - + + # ensure that when updating dependencies, the dylib bundler script is also updated - name: install build & runtime dependencies run: | brew install bison \ @@ -86,7 +87,7 @@ jobs: brew install local/mingw-w64/mingw-w64 - name: create build directory - run: mkdir -p build + run: mkdir build - name: configure wine working-directory: build @@ -131,24 +132,49 @@ jobs: run: make -j$(sysctl -n hw.ncpu) - name: create install directory - run: mkdir -p install + run: mkdir install - name: install wine working-directory: build run: make install-lib DESTDIR=${{ github.workspace }}/install - - name: add wine64 symlink for backward compatibility + - name: add wine64 as an executable wrapper for backward compatibility working-directory: install/bin - run: ln -s wine wine64 + run: | + cat > wine64 << 'EOF' + #!/usr/bin/env bash + + # this is a wrapper created for Mythic to forward `wine64` calls to `wine`. + # for older versions of Mythic, which expected a wine64 binary to be present. + + set -e + DIR="$(cd "$(dirname "$0")" && pwd)" + exec "$DIR/wine" "$@" + EOF + + chmod +x wine64 + + - name: install wine mono + working-directory: install/share/wine + run: | + mkdir -p mono + curl -fL https://github.com/wine-mono/wine-mono/releases/download/wine-mono-8.1.0/wine-mono-8.1.0-x86.tar.xz \ + | tar -xJ -C mono + + - name: install wine gecko + working-directory: install/share/wine + run: | + mkdir -p gecko + curl -fL https://dl.winehq.org/wine/wine-gecko/2.47.4/wine-gecko-2.47.4-x86.tar.xz \ + | tar -xJ -C gecko - name: create Engine directory - run: mkdir -p Engine + run: mkdir Engine - name: copy build artifacts run: | - # copy wine installation mkdir -p Engine/wine - cp -R install/* Engine/wine + ditto --rsrc install Engine/wine - name: copy externals run: | @@ -158,12 +184,17 @@ jobs: # copy gptk libraries ditto src/external/gptk-*/redist/lib/ Engine/wine/lib/ + + - name: bundle dylib dependencies + run: | + chmod +x src/.github/dylib_bundler.zsh + ENGINE_DIR=Engine/wine src/.github/dylib_bundler.zsh - name: copy engine properties file run: cp -R src/Mythic/Properties.plist Engine/ - name: compress engine artifact - run: tar -cJf Engine.tar.xz -C Engine . + run: tar -cf Engine.tar.xz -C Engine --use-compress-program="xz -9 -T0" . - name: upload engine artifact uses: actions/upload-artifact@v5