diff --git a/.github/actions/prepare_macos_tooling/action.yml b/.github/actions/prepare_macos_tooling/action.yml new file mode 100644 index 000000000..ce199194a --- /dev/null +++ b/.github/actions/prepare_macos_tooling/action.yml @@ -0,0 +1,181 @@ +name: Cache TRX MacOS Dependencies + +inputs: + FFMPEG_INSTALL_TMP_ARM64: + required: false + default: /opt/local/install_arm64 + FFMPEG_INSTALL_TMP_X86_64: + required: false + default: /opt/local/install_x86_64 + CACHE_SRC_DIR: + required: false + default: /opt/local + CACHE_DIR: + required: true + +runs: + using: "composite" + steps: + - name: Select latest stable Xcode + if: steps.restore-cache.outputs.cache-hit != 'true' + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest-stable + + - name: Install and update MacPorts + if: steps.restore-cache.outputs.cache-hit != 'true' + shell: bash + run: | + wget -O ${{ github.workspace }}/macports.pkg https://github.com/macports/macports-base/releases/download/v2.9.2/MacPorts-2.9.2-14-Sonoma.pkg + sudo installer -pkg ${{ github.workspace }}/macports.pkg -target / + + - name: Install build and deployment tools + if: steps.restore-cache.outputs.cache-hit != 'true' + shell: bash + run: | + # Install Python first to avoid multiple Python in the dep tree later on. + sudo port -N install python312 py312-pip + sudo port select --set python python312 + sudo port select --set python3 python312 + sudo port select --set pip pip312 + sudo port select --set pip3 pip312 + + # Install the rest. + sudo port -N install create-dmg meson ninja pkgconfig + sudo pip3 install pyjson5 + + - name: "Build dependencies: Compression libraries (universal)" + if: steps.restore-cache.outputs.cache-hit != 'true' + shell: bash + run: | + sudo port -N install zlib +universal + sudo port -N install bzip2 +universal + sudo port -N install xz +universal + + - name: "Build dependency: pcre2 (universal)" + if: steps.restore-cache.outputs.cache-hit != 'true' + shell: bash + run: sudo port -N install pcre2 +universal + + - name: "Build dependency: libsdl2 (universal)" + if: steps.restore-cache.outputs.cache-hit != 'true' + shell: bash + run: sudo port -N install libsdl2 +universal + + - name: "Build dependency: uthash (universal)" + if: steps.restore-cache.outputs.cache-hit != 'true' + shell: bash + run: sudo port -N install uthash +universal + + - name: "Build dependency: ffmpeg (universal)" + if: steps.restore-cache.outputs.cache-hit != 'true' + shell: bash + run: | + # Install to separate staging paths for all architectures in + # preparation for fusing universal libraries in a follow-up step. + cd "$RUNNER_TEMP" + git clone --depth 1 --branch "n4.4.1" https://github.com/FFmpeg/FFmpeg + cd FFmpeg + + # Common FFmpeg configure options + FFMPEG_CONFIG_OPTIONS=" \ + --enable-shared \ + --disable-static \ + $(cat $GITHUB_WORKSPACE/tools/ffmpeg_flags.txt)" + + # Configure for arm64. + ./configure \ + --arch=arm64 \ + --prefix=${{ inputs.FFMPEG_INSTALL_TMP_ARM64 }} \ + --cc='clang' \ + $FFMPEG_CONFIG_OPTIONS + + # Build and install. + make -j$(sysctl -n hw.ncpu) + sudo make install + + # Reset build directory. + make clean + + # Configure for x86-64. + ./configure \ + --arch=x86_64 \ + --enable-cross-compile \ + --prefix=${{ inputs.FFMPEG_INSTALL_TMP_X86_64 }} \ + --cc='clang -arch x86_64' \ + $FFMPEG_CONFIG_OPTIONS + + # Build and install. + make -j$(sysctl -n hw.ncpu) + sudo make install + + - name: "Build dependency: ffmpeg (fuse universal libraries)" + if: steps.restore-cache.outputs.cache-hit != 'true' + shell: bash + run: | + # Libs + FFMPEG_LIBS=( + "libavcodec" + "libavdevice" + "libavfilter" + "libavformat" + "libavutil" + "libpostproc" + "libswresample" + "libswscale" + ) + + # Recreate include tree in MacPorts install prefix. + sudo rsync -arvL ${{ inputs.FFMPEG_INSTALL_TMP_ARM64 }}/include/ ${{ inputs.CACHE_SRC_DIR }}/include/ + + # Recreate library symlinks in MacPorts install prefix. + sudo find ${{ inputs.FFMPEG_INSTALL_TMP_ARM64 }}/lib/ -type l -exec cp -P '{}' ${{ inputs.CACHE_SRC_DIR }}/lib/ ';' + + # Fuse platform-specific binaries into a universal binary. + for LIB in ${FFMPEG_LIBS[@]}; do + RESOLVED_LIB=$(ls -l ${{ inputs.FFMPEG_INSTALL_TMP_ARM64 }}/lib/${LIB}* \ + | grep -v '^l' \ + | awk -F'/' '{print $NF}') + + sudo lipo -create \ + ${{ inputs.FFMPEG_INSTALL_TMP_ARM64 }}/lib/$RESOLVED_LIB \ + ${{ inputs.FFMPEG_INSTALL_TMP_X86_64 }}/lib/$RESOLVED_LIB \ + -output ${{ inputs.CACHE_SRC_DIR }}/lib/$RESOLVED_LIB + + sudo ln -s -f \ + ${{ inputs.CACHE_SRC_DIR }}/lib/$RESOLVED_LIB \ + ${{ inputs.FFMPEG_INSTALL_TMP_ARM64 }}/lib/$RESOLVED_LIB + sudo ln -s -f \ + ${{ inputs.CACHE_SRC_DIR }}/lib/$RESOLVED_LIB \ + ${{ inputs.FFMPEG_INSTALL_TMP_X86_64 }}/lib/$RESOLVED_LIB + done + + # Update and install pkgconfig files. + for file in "${{ inputs.FFMPEG_INSTALL_TMP_ARM64 }}/lib/pkgconfig"/*.pc; do + sudo sed -i '' "s:${{ inputs.FFMPEG_INSTALL_TMP_ARM64 }}:${{ inputs.CACHE_SRC_DIR }}:g" "$file" + done + sudo mv ${{ inputs.FFMPEG_INSTALL_TMP_ARM64 }}/lib/pkgconfig/* ${{ inputs.CACHE_SRC_DIR }}/lib/pkgconfig/ + + - name: "Prepare dependencies for caching" + if: steps.restore-cache.outputs.cache-hit != 'true' + shell: bash + run: | + # Remove MacPorts leftover build and download files + sudo rm -rf /opt/local/var/macports/build/* + sudo rm -rf /opt/local/var/macports/distfiles/* + sudo rm -rf /opt/local/var/macports/packages/* + + # Delete broken symlinks + sudo find ${{ inputs.CACHE_SRC_DIR }} -type l ! -exec test -e {} \; -exec rm {} \; + + # Trying to cache the source directory directly leads to permission errors, + # so copy it to an intermediate temporary directory. Also, expands all the symlinks to hard copies. + sudo rsync -arvqL ${{ inputs.CACHE_SRC_DIR }}/ ${{ inputs.CACHE_DIR }} + + - name: "Save dependencies to cache" + if: steps.restore-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 + with: + key: ${{ runner.os }}-tooling-${{ hashFiles('.github/actions/prepare_macos_tooling/action.yml') }} + path: | + ${{ inputs.CACHE_DIR }} diff --git a/.github/workflows/job_build_tr1_macos.yml b/.github/workflows/job_build_tr1_macos.yml index 466943276..08af166de 100644 --- a/.github/workflows/job_build_tr1_macos.yml +++ b/.github/workflows/job_build_tr1_macos.yml @@ -59,174 +59,23 @@ jobs: echo -e "/opt/local/bin" >> $GITHUB_PATH echo -e "/opt/local/sbin" >> $GITHUB_PATH - - name: "Restore dependencies from cache" + - name: "Try restore dependencies from cache" id: restore-cache uses: actions/cache/restore@v4 with: - key: ${{ runner.os }}-tooling-${{ hashFiles('.github/workflows/job_build_tr1_macos.yml') }} + key: ${{ runner.os }}-tooling-${{ hashFiles('.github/actions/prepare_macos_tooling/action.yml') }} path: | - ${{ env.CACHE_TMP_DIR }} - ${{ env.FFMPEG_INSTALL_TMP_ARM64 }} - ${{ env.FFMPEG_INSTALL_TMP_X86_64 }} - ${{ env.FFMPEG_INSTALL_TMP_UNIVERSAL }} - + /tmp/opt_local/ + - name: "Build MacOS dependencies" + if: steps.restore-cache.outputs.cache-hit != 'true' + uses: ./.github/actions/prepare_macos_tooling + with: + CACHE_DIR: /tmp/opt_local/ - name: "Prepare cached dependencies for use" if: steps.restore-cache.outputs.cache-hit == 'true' + shell: bash run: | - sudo rsync -arvq ${{ env.CACHE_TMP_DIR }} ${{ env.CACHE_DST_DIR }} - - - name: Select latest stable Xcode - if: steps.restore-cache.outputs.cache-hit != 'true' - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: latest-stable - - - name: Install and update MacPorts - if: steps.restore-cache.outputs.cache-hit != 'true' - run: | - wget -O ${{ github.workspace }}/macports.pkg https://github.com/macports/macports-base/releases/download/v2.9.2/MacPorts-2.9.2-14-Sonoma.pkg - sudo installer -pkg ${{ github.workspace }}/macports.pkg -target / - - - name: Install build and deployment tools - if: steps.restore-cache.outputs.cache-hit != 'true' - run: | - # Install Python first to avoid multiple Python in the dep tree later on. - sudo port -N install python312 py312-pip - sudo port select --set python python312 - sudo port select --set python3 python312 - sudo port select --set pip pip312 - sudo port select --set pip3 pip312 - - # Install the rest. - sudo port -N install create-dmg meson ninja pkgconfig - sudo pip3 install pyjson5 - - - name: "Build dependencies: Compression libraries (universal)" - if: steps.restore-cache.outputs.cache-hit != 'true' - run: | - sudo port -N install zlib +universal - sudo port -N install bzip2 +universal - sudo port -N install xz +universal - - - name: "Build dependency: pcre2 (universal)" - if: steps.restore-cache.outputs.cache-hit != 'true' - run: sudo port -N install pcre2 +universal - - - name: "Build dependency: libsdl2 (universal)" - if: steps.restore-cache.outputs.cache-hit != 'true' - run: sudo port -N install libsdl2 +universal - - - name: "Build dependency: uthash (universal)" - if: steps.restore-cache.outputs.cache-hit != 'true' - run: sudo port -N install uthash +universal - - - name: "Build dependency: ffmpeg (universal)" - if: steps.restore-cache.outputs.cache-hit != 'true' - run: | - # Install to separate staging paths for all architectures in - # preparation for fusing universal libraries in a follow-up step. - cd "$RUNNER_TEMP" - git clone --depth 1 --branch "n4.4.1" https://github.com/FFmpeg/FFmpeg - cd FFmpeg - - # Common FFmpeg configure options - FFMPEG_CONFIG_OPTIONS=" \ - --enable-shared \ - --disable-static \ - $(cat $GITHUB_WORKSPACE/tools/ffmpeg_flags.txt)" - - # Configure for arm64. - ./configure \ - --arch=arm64 \ - --prefix=$FFMPEG_INSTALL_TMP_ARM64 \ - --cc='clang' \ - $FFMPEG_CONFIG_OPTIONS - - # Build and install. - make -j$(sysctl -n hw.ncpu) - sudo make install - - # Reset build directory. - make clean - - # Configure for x86-64. - ./configure \ - --arch=x86_64 \ - --enable-cross-compile \ - --prefix=$FFMPEG_INSTALL_TMP_X86_64 \ - --cc='clang -arch x86_64' \ - $FFMPEG_CONFIG_OPTIONS - - # Build and install. - make -j$(sysctl -n hw.ncpu) - sudo make install - - - name: "Build dependency: ffmpeg (fuse universal libraries)" - if: steps.restore-cache.outputs.cache-hit != 'true' - run: | - # Libs - FFMPEG_LIBS=( - "libavcodec" - "libavdevice" - "libavfilter" - "libavformat" - "libavutil" - "libpostproc" - "libswresample" - "libswscale" - ) - - # Recreate library symlinks in MacPorts install prefix. - sudo find $FFMPEG_INSTALL_TMP_ARM64/lib -type l -exec cp -P '{}' $FFMPEG_INSTALL_FINAL/LIB ';' - - # `lipo` cannot overwrite binaries in place, so we stage the - # fused binaries in a temporary directory. - mkdir -p $FFMPEG_INSTALL_TMP_UNIVERSAL - for LIB in ${FFMPEG_LIBS[@]}; do - RESOLVED_LIB=$(ls -l $FFMPEG_INSTALL_TMP_ARM64/lib/${LIB}* \ - | grep -v '^l' \ - | awk -F'/' '{print $NF}') - - lipo -create \ - $FFMPEG_INSTALL_TMP_ARM64/lib/$RESOLVED_LIB \ - $FFMPEG_INSTALL_TMP_X86_64/lib/$RESOLVED_LIB \ - -output $FFMPEG_INSTALL_TMP_UNIVERSAL/$RESOLVED_LIB - - # Replace the arch-specific libraries with links to the universal - # binary, so `bundle_dylibs` will always gather a universal build. - sudo ln -s -f $FFMPEG_INSTALL_TMP_UNIVERSAL/$RESOLVED_LIB $FFMPEG_INSTALL_TMP_ARM64/lib/$RESOLVED_LIB - sudo ln -s -f $FFMPEG_INSTALL_TMP_UNIVERSAL/$RESOLVED_LIB $FFMPEG_INSTALL_TMP_X86_64/lib/$RESOLVED_LIB - done - - # Copy the fused binaries to the MacPorts install prefix. - sudo cp $FFMPEG_INSTALL_TMP_UNIVERSAL/*.dylib $FFMPEG_INSTALL_FINAL/lib/ - - # Update and install pkgconfig files. - for file in "$FFMPEG_INSTALL_TMP_ARM64/lib/pkgconfig"/*.pc; do - sudo sed -i '' "s|^prefix=.*|prefix=$FFMPEG_INSTALL_FINAL|" "$file" - sudo sed -i '' "s|^libdir=.*|libdir=$FFMPEG_INSTALL_FINAL/lib|" "$file" - done - sudo mv $FFMPEG_INSTALL_TMP_ARM64/lib/pkgconfig/* $FFMPEG_INSTALL_FINAL/lib/pkgconfig/ - - - name: "Prepare dependencies for caching" - if: steps.restore-cache.outputs.cache-hit != 'true' - run: | - # Remove MacPorts leftover build and download files - sudo rm -rf /opt/local/var/macports/build/* - sudo rm -rf /opt/local/var/macports/distfiles/* - sudo rm -rf /opt/local/var/macports/packages/* - sudo rsync -arvq ${{ env.CACHE_DST_DIR }} ${{ env.CACHE_TMP_DIR }} - - - name: "Save dependencies to cache" - if: steps.restore-cache.outputs.cache-hit != 'true' - uses: actions/cache/save@v4 - with: - key: ${{ steps.restore-cache.outputs.cache-primary-key }} - path: | - ${{ env.CACHE_TMP_DIR }} - ${{ env.FFMPEG_INSTALL_TMP_ARM64 }} - ${{ env.FFMPEG_INSTALL_TMP_X86_64 }} - ${{ env.FFMPEG_INSTALL_TMP_UNIVERSAL }} + sudo rsync -arvq /tmp/opt_local/ /opt/local/ - name: Setup CA run: | @@ -242,7 +91,7 @@ jobs: - name: Build x86-64 run: | BUILD_DIR=build-x86-64 - BUILD_OPTIONS="src/tr1 --prefix=/tmp/TR1X.app --bindir=Contents/MacOS --cross-file tools/tr1/mac/x86-64_cross_file.txt --buildtype ${{ inputs.target }}" + BUILD_OPTIONS="src/tr1 --prefix=/tmp/TR1X.app --bindir=Contents/MacOS --cross-file tools/shared/mac/x86-64_cross_file.txt --buildtype ${{ inputs.target }}" meson setup $BUILD_DIR $BUILD_OPTIONS meson compile -C $BUILD_DIR @@ -255,7 +104,7 @@ jobs: mv $BUNDLE_EXEC_DIR/TR1X_universal $BUNDLE_EXEC_DIR/TR1X # Update dynamic library links in the fused executable. - ./tools/tr1/mac/bundle_dylibs --links-only + ./tools/shared/mac/bundle_dylibs -a TR1X --links-only - name: Sign app bundle run: | @@ -272,7 +121,7 @@ jobs: run: | KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db IDENTITY=$(security find-identity -v -p codesigning $KEYCHAIN_PATH | awk -F'"' '{print $2}') - tools/tr1/mac/create_installer + tools/shared/mac/create_installer -a TR1X -i data/tr1/mac/icon.icns xattr -cr TR1X-Installer.dmg /usr/bin/codesign --force --options runtime -s "${IDENTITY}" --keychain $KEYCHAIN_PATH -v TR1X-Installer.dmg xcrun notarytool submit --wait --apple-id "$MACOS_APPLEID" --password "$MACOS_APP_PWD" --team-id "$MACOS_TEAMID" TR1X-Installer.dmg diff --git a/.github/workflows/job_build_tr2_macos.yml b/.github/workflows/job_build_tr2_macos.yml new file mode 100644 index 000000000..2c7e51d21 --- /dev/null +++ b/.github/workflows/job_build_tr2_macos.yml @@ -0,0 +1,132 @@ +name: Build TR2X and the installer (macOS) + +on: + workflow_call: + inputs: + target: + type: string + description: "Target to build for" + required: true + let_mac_fail: + type: boolean + description: "Do not require Mac builds to pass" + required: false + default: false + +env: + C_INCLUDE_PATH: /opt/local/include/uthash/:/opt/local/include/ + +jobs: + build: + name: Build release assets (mac) + runs-on: macos-14 + continue-on-error: ${{ inputs.let_mac_fail == true || inputs.let_mac_fail == 'true' }} + steps: + - name: Set up signing certificate + env: + MACOS_KEYCHAIN_PWD: ${{ secrets.MACOS_KEYCHAIN_PWD }} + MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} + MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_PWD }} + run: | + CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12 + KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db + echo -n "$MACOS_CERTIFICATE" | base64 --decode -o $CERTIFICATE_PATH + security create-keychain -p "$MACOS_KEYCHAIN_PWD" $KEYCHAIN_PATH + security set-keychain-settings -lut 21600 $KEYCHAIN_PATH + security unlock-keychain -p "$MACOS_KEYCHAIN_PWD" $KEYCHAIN_PATH + security import $CERTIFICATE_PATH -P "$MACOS_KEYCHAIN_PWD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH -T /usr/bin/codesign + security list-keychain -d user -s $KEYCHAIN_PATH + security set-key-partition-list -S "apple-tool:,apple:,codesign:" -s -k $MACOS_KEYCHAIN_PWD $KEYCHAIN_PATH + + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: 'true' + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha || github.sha }} + + - name: Extend PATH for MacPorts + run: | + echo -e "/opt/local/bin" >> $GITHUB_PATH + echo -e "/opt/local/sbin" >> $GITHUB_PATH + + - name: "Try restore dependencies from cache" + id: restore-cache + uses: actions/cache/restore@v4 + with: + key: ${{ runner.os }}-tooling-${{ hashFiles('.github/actions/prepare_macos_tooling/action.yml') }} + path: | + /tmp/opt_local/ + - name: "Build MacOS dependencies" + if: steps.restore-cache.outputs.cache-hit != 'true' + uses: ./.github/actions/prepare_macos_tooling + with: + CACHE_DIR: /tmp/opt_local/ + - name: "Prepare cached dependencies for use" + if: steps.restore-cache.outputs.cache-hit == 'true' + shell: bash + run: | + sudo rsync -arvq /tmp/opt_local/ /opt/local/ + + - name: Setup CA + run: | + sudo port -N install apple-pki-bundle curl-ca-bundle + + - name: Build arm64 and create app bundle + run: | + BUILD_DIR=build-arm64 + BUILD_OPTIONS="src/tr2 --prefix=/tmp/TR2X.app --bindir=Contents/MacOS --buildtype ${{ inputs.target }}" + meson setup $BUILD_DIR $BUILD_OPTIONS + meson install -C $BUILD_DIR + + - name: Build x86-64 + run: | + BUILD_DIR=build-x86-64 + BUILD_OPTIONS="src/tr2 --prefix=/tmp/TR2X.app --bindir=Contents/MacOS --cross-file tools/shared/mac/x86-64_cross_file.txt --buildtype ${{ inputs.target }}" + meson setup $BUILD_DIR $BUILD_OPTIONS + meson compile -C $BUILD_DIR + + - name: Fuse universal executable + run: | + BUNDLE_EXEC_DIR=/tmp/TR2X.app/Contents/MacOS + + # Fuse executable and move it into the app bundle. + lipo -create build-x86-64/TR2X $BUNDLE_EXEC_DIR/TR2X -output $BUNDLE_EXEC_DIR/TR2X_universal + mv $BUNDLE_EXEC_DIR/TR2X_universal $BUNDLE_EXEC_DIR/TR2X + + # Update dynamic library links in the fused executable. + ./tools/shared/mac/bundle_dylibs -a TR2X --links-only + + - name: Sign app bundle + run: | + KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db + IDENTITY=$(security find-identity -v -p codesigning $KEYCHAIN_PATH | awk -F'"' '{print $2}') + xattr -cr /tmp/TR2X.app + /usr/bin/codesign --force --deep --options runtime -s "${IDENTITY}" --keychain $KEYCHAIN_PATH -v /tmp/TR2X.app + + - name: Create, sign and notarize disk image + env: + MACOS_APPLEID: ${{ secrets.MACOS_APPLEID }} + MACOS_APP_PWD: ${{ secrets.MACOS_APP_PWD }} + MACOS_TEAMID: ${{ secrets.MACOS_TEAMID }} + run: | + KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db + IDENTITY=$(security find-identity -v -p codesigning $KEYCHAIN_PATH | awk -F'"' '{print $2}') + tools/shared/mac/create_installer -a TR2X -i data/tr2/mac/icon.icns + xattr -cr TR2X-Installer.dmg + /usr/bin/codesign --force --options runtime -s "${IDENTITY}" --keychain $KEYCHAIN_PATH -v TR2X-Installer.dmg + xcrun notarytool submit --wait --apple-id "$MACOS_APPLEID" --password "$MACOS_APP_PWD" --team-id "$MACOS_TEAMID" TR2X-Installer.dmg + xcrun stapler staple -v TR2X-Installer.dmg + mv TR2X-Installer.dmg "TR2X-$(tools/get_version 1)-Installer.dmg" + + - id: vars + name: Prepare variables + run: echo "version=$(tools/get_version 1)" >> $GITHUB_OUTPUT + + - name: Upload signed+notarized installer image + uses: actions/upload-artifact@v4 + with: + name: TR2X-${{ steps.vars.outputs.version }}-mac + path: | + *.dmg + compression-level: 0 # .dmg is already compressed. diff --git a/.github/workflows/pr_builds.yml b/.github/workflows/pr_builds.yml index 7eb5fe6a6..ba549eff8 100644 --- a/.github/workflows/pr_builds.yml +++ b/.github/workflows/pr_builds.yml @@ -33,3 +33,12 @@ jobs: with: target: 'debug' secrets: inherit + + package_tr2_mac: + name: Build TR2 + if: vars.MACOS_ENABLE == 'true' + uses: ./.github/workflows/job_build_tr2_macos.yml + with: + target: 'debug' + let_mac_fail: true + secrets: inherit diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 8d283f489..b6dbe4548 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -36,6 +36,17 @@ jobs: target: 'debug' secrets: inherit + package_tr2_mac: + name: Build TR2 + if: | + vars.PRERELEASE_ENABLE == 'true' && + vars.MACOS_ENABLE == 'true' + uses: ./.github/workflows/job_build_tr2_macos.yml + with: + target: 'debug' + let_mac_fail: true + secrets: inherit + publish_prerelease: if: vars.PRERELEASE_ENABLE == 'true' name: Create a prerelease @@ -43,6 +54,7 @@ jobs: - package_tr1_multiplatform - package_tr1_mac - package_tr2_multiplatform + - package_tr2_mac with: draft: false prerelease: true diff --git a/data/tr2/mac/Info.plist b/data/tr2/mac/Info.plist new file mode 100644 index 000000000..c523722d5 --- /dev/null +++ b/data/tr2/mac/Info.plist @@ -0,0 +1,12 @@ + + + + + CFBundleIdentifier + com.lostartifacts.tr1x + CFBundleName + TR2X + CFBundleIconFile + icon + + diff --git a/data/tr2/mac/icon.icns b/data/tr2/mac/icon.icns new file mode 100644 index 000000000..01c5a1b7e Binary files /dev/null and b/data/tr2/mac/icon.icns differ diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index e5cbd35f2..ed5d29317 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -50,6 +50,28 @@ Subsequent builds: Run WSL and continue with the instructions from the `Compiling on Ubuntu` section. +### Compiling on MacOS + +MacPorts: +https://github.com/macports/macports-base/releases + + + +- **With Docker**: + + Make sure to install Docker and [just](https://github.com/casey/just). + To see the list of all possible build targets, run `just -l`. To build the + images, use the `just *-build-*` commands relevant to the game and platform + you want to build for. The binaries should appear in the `build/` + directory. + +- **Without Docker**: + + This scenario is not officially supported, but you can see how it's done by + examining the files in the `tools/*/docker/` directory for the external + dependencies and `meson.build` for the local files, then tailoring your + system to match the process. + ### Supported compilers diff --git a/docs/tr1/SECRETS.md b/docs/tr1/SECRETS.md index 97e2a8dbc..0fc711e1b 100644 --- a/docs/tr1/SECRETS.md +++ b/docs/tr1/SECRETS.md @@ -75,6 +75,7 @@ MacOS builds require a paid Apple Developer account. 5. Serialize the key in base-64 without spaces - run: > base64 TR1X.pem|tr -d '\n' + > base64 -i BUILD_CERTIFICATE.p12 | pbcopy (macos) The result is to be put as the value of the `MACOS_CERTIFICATE` secret. diff --git a/docs/tr2/CHANGELOG.md b/docs/tr2/CHANGELOG.md index 77626fabf..4738b81ed 100644 --- a/docs/tr2/CHANGELOG.md +++ b/docs/tr2/CHANGELOG.md @@ -1,5 +1,6 @@ ## [Unreleased](https://github.com/LostArtefacts/TRX/compare/tr2-0.8...develop) - ××××-××-×× - added Linux builds and toolchain (#1598) +- added macOS builds (for both Apple Silicon and Intel) (#2226) - added pause dialog (#1638) - added a photo mode feature (#2277) - added fade-out effect to the demos diff --git a/docs/tr2/README.md b/docs/tr2/README.md index 7a7ad3b25..e88da1f44 100644 --- a/docs/tr2/README.md +++ b/docs/tr2/README.md @@ -131,6 +131,7 @@ game with new enhancements and features. #### Miscellaneous - added Linux builds +- added macOS builds - added .jpeg/.png screenshots - added ability to skip FMVs with both the Action key - added ability to skip end credits with the Action and Escape keys diff --git a/src/tr1/meson.build b/src/tr1/meson.build index 430261feb..30a1a8963 100644 --- a/src/tr1/meson.build +++ b/src/tr1/meson.build @@ -301,5 +301,5 @@ if host_machine.system() == 'darwin' install_subdir('../../data/tr1/ship/shaders', install_dir : 'Contents/Resources') install_data('../../data/tr1/mac/icon.icns', install_dir : 'Contents/Resources') install_data('../../data/tr1/mac/Info.plist', install_dir : 'Contents') - meson.add_install_script('../../tools/tr1/mac/bundle_dylibs') + meson.add_install_script('../../tools/shared/mac/bundle_dylibs', '-a', 'TR1X') endif diff --git a/src/tr2/game/output.c b/src/tr2/game/output.c index af4e18d6a..235240537 100644 --- a/src/tr2/game/output.c +++ b/src/tr2/game/output.c @@ -824,8 +824,8 @@ int32_t Output_GetObjectBounds(const BOUNDS_16 *const bounds) return 0; } - constexpr int32_t vtx_count = 8; - const XYZ_32 vtx[vtx_count] = { + const int32_t vtx_count = 8; + const XYZ_32 vtx[] = { { .x = bounds->min.x, .y = bounds->min.y, .z = bounds->min.z }, { .x = bounds->max.x, .y = bounds->min.y, .z = bounds->min.z }, { .x = bounds->max.x, .y = bounds->max.y, .z = bounds->min.z }, diff --git a/src/tr2/game/render/common.c b/src/tr2/game/render/common.c index 6bf23aad7..c52d5270b 100644 --- a/src/tr2/game/render/common.c +++ b/src/tr2/game/render/common.c @@ -15,6 +15,9 @@ #include #include +#include +#include + static RENDERER m_Renderer_SW = {}; static RENDERER m_Renderer_HW = {}; static RENDERER *m_PreviousRenderer = NULL; @@ -32,6 +35,7 @@ static struct { static RENDERER *M_GetRenderer(void); static void M_ReuploadBackground(void); static void M_ResetPolyList(void); +static void M_SetGLBackend(GFX_GL_BACKEND backend); static RENDERER *M_GetRenderer(void) { @@ -69,15 +73,60 @@ static void M_ResetPolyList(void) } } +static void M_SetGLBackend(const GFX_GL_BACKEND backend) +{ + switch (backend) { + case GFX_GL_21: + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 1); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, 0); + break; + + case GFX_GL_33C: + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3); + SDL_GL_SetAttribute( + SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); + break; + + case GFX_GL_INVALID_BACKEND: + ASSERT_FAIL(); + break; + } +} + void Render_Init(void) { - LOG_DEBUG(""); - GFX_Context_Attach(g_SDLWindow, GFX_GL_33C); - GFX_Context_SetRenderingMode(GFX_RM_FRAMEBUFFER); - m_FadeRenderer = GFX_FadeRenderer_Create(); - m_BackgroundRenderer = GFX_2D_Renderer_Create(); - Renderer_SW_Prepare(&m_Renderer_SW); - Renderer_HW_Prepare(&m_Renderer_HW); + // TODO Move to libtrx later and combine with S_Shell_CreateWindow. + const GFX_GL_BACKEND backends_to_try[] = { + // clang-format off + GFX_GL_33C, + GFX_GL_21, + GFX_GL_INVALID_BACKEND, // guard + // clang-format on + }; + + for (int32_t i = 0; backends_to_try[i] != GFX_GL_INVALID_BACKEND; i++) { + const GFX_GL_BACKEND backend = backends_to_try[i]; + + M_SetGLBackend(backend); + + int32_t major; + int32_t minor; + SDL_GL_GetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, &major); + SDL_GL_GetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, &minor); + LOG_DEBUG("Trying GL backend %d.%d", major, minor); + if (GFX_Context_Attach(g_SDLWindow, backend)) { + GFX_Context_SetRenderingMode(GFX_RM_FRAMEBUFFER); + m_FadeRenderer = GFX_FadeRenderer_Create(); + m_BackgroundRenderer = GFX_2D_Renderer_Create(); + Renderer_SW_Prepare(&m_Renderer_SW); + Renderer_HW_Prepare(&m_Renderer_HW); + return; + } + } + + Shell_ExitSystem("System Error: cannot attach opengl context"); } void Render_Shutdown(void) diff --git a/src/tr2/meson.build b/src/tr2/meson.build index af7ae420b..2d5ab8086 100644 --- a/src/tr2/meson.build +++ b/src/tr2/meson.build @@ -36,6 +36,11 @@ build_opts = [ add_project_arguments(build_opts, language: 'c') +# Always dynamically link on macOS +if host_machine.system() == 'darwin' + staticdeps = false +endif + null_dep = dependency('', required: false) dep_trx = trx.get_variable('dep_trx') dep_sdl2 = dependency('SDL2', static: staticdeps) @@ -301,4 +306,14 @@ executable( link_args: link_args, dependencies: dependencies, win_subsystem: 'windows', + install: true, ) + +if host_machine.system() == 'darwin' + install_subdir('../../data/tr2/ship/cfg', install_dir : 'Contents/Resources') + install_subdir('../../data/tr2/ship/data', install_dir : 'Contents/Resources') + install_subdir('../../data/tr2/ship/shaders', install_dir : 'Contents/Resources') + install_data('../../data/tr2/mac/icon.icns', install_dir : 'Contents/Resources') + install_data('../../data/tr2/mac/Info.plist', install_dir : 'Contents') + meson.add_install_script('../../tools/shared/mac/bundle_dylibs', '-a', 'TR2X') +endif diff --git a/tools/tr1/mac/bundle_dylibs b/tools/shared/mac/bundle_dylibs similarity index 61% rename from tools/tr1/mac/bundle_dylibs rename to tools/shared/mac/bundle_dylibs index 6d86e6a3c..69b2f2616 100755 --- a/tools/tr1/mac/bundle_dylibs +++ b/tools/shared/mac/bundle_dylibs @@ -2,28 +2,19 @@ import argparse import re import shutil +from collections.abc import Iterable from pathlib import Path from subprocess import check_output, run -# Configuration -APP_NAME = "TR1X" -APP_BUNDLE_PATH = Path(f"/tmp/{APP_NAME}.app") -APP_BINARY_PATH = APP_BUNDLE_PATH / f"Contents/MacOS/{APP_NAME}" -FRAMEWORKS_PATH = APP_BUNDLE_PATH / "Contents/Frameworks" IGNORE_LIB_PREFIXES = ("/usr/lib/", "/System/", "@executable_path") - - -# Other global state -LIBRARY_PATHS: set[Path] = set() +RPATH_PATTERN = re.compile(r"@rpath/(.*)") def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( - description=( - f"Copies shared libraries into the macOS app bundle " - "for {APP_NAME}." - ) + description="Copies shared libraries into the macOS app bundle." ) + parser.add_argument("-a", "--app-name") parser.add_argument( "--copy-only", action="store_true", @@ -41,32 +32,33 @@ def should_ignore_lib(lib_path: str) -> bool: return any(lib_path.startswith(prefix) for prefix in IGNORE_LIB_PREFIXES) -def gather_libs(binary_path: Path) -> None: +def gather_libs( + binary_path: Path, visited_paths: set[Path] | None = None +) -> Iterable[Path]: + if visited_paths is None: + visited_paths = set() + visited_paths.add(binary_path) + parent_path = binary_path.parent output = check_output(["otool", "-L", str(binary_path)], text=True) libs = [line.split()[0] for line in output.split("\n")[1:] if line] - rpath_pattern = re.compile(r"@rpath/(.*)") - for lib in libs: - match = rpath_pattern.match(lib) + match = RPATH_PATTERN.match(lib) lib_path = parent_path / (match.group(1) if match else lib) - if ( - should_ignore_lib(str(lib_path)) - or lib_path in LIBRARY_PATHS - or lib_path == binary_path - ): + if should_ignore_lib(str(lib_path)) or lib_path == binary_path: continue - LIBRARY_PATHS.add(lib_path) - gather_libs(lib_path) + yield lib_path + if lib_path not in visited_paths: + yield from gather_libs(lib_path, visited_paths=visited_paths) -def copy_libs() -> None: - FRAMEWORKS_PATH.mkdir(parents=True, exist_ok=True) - for lib_path in LIBRARY_PATHS: - target_path = FRAMEWORKS_PATH / lib_path.name +def copy_libs(frameworks_path: Path, library_paths: set[Path]) -> None: + frameworks_path.mkdir(parents=True, exist_ok=True) + for lib_path in library_paths: + target_path = frameworks_path / lib_path.name if not target_path.exists(): print(f"Copying {lib_path} to {target_path}") shutil.copy2(lib_path, target_path) @@ -93,17 +85,21 @@ def update_links(binary_path: Path) -> None: def main() -> None: args = parse_args() + app_bundle_path = Path(f"/tmp/{args.app_name}.app") + app_binary_path = app_bundle_path / f"Contents/MacOS/{args.app_name}" + frameworks_path = app_bundle_path / "Contents/Frameworks" + if args.copy_only or not args.links_only: - gather_libs(APP_BINARY_PATH) - copy_libs() + library_paths = set(gather_libs(app_binary_path)) + copy_libs(frameworks_path, library_paths) if args.links_only or not args.copy_only: - for lib_path in FRAMEWORKS_PATH.glob("*"): + for lib_path in frameworks_path.glob("*"): update_links(lib_path) - update_links(APP_BINARY_PATH) + update_links(app_binary_path) - print(f"Libraries for {APP_NAME} copied and updated.") + print(f"Libraries for {args.app_name} copied and updated.") if __name__ == "__main__": diff --git a/tools/tr1/mac/create_installer b/tools/shared/mac/create_installer similarity index 55% rename from tools/tr1/mac/create_installer rename to tools/shared/mac/create_installer index 921a5b054..24345c647 100755 --- a/tools/tr1/mac/create_installer +++ b/tools/shared/mac/create_installer @@ -4,27 +4,21 @@ import os import subprocess from pathlib import Path -# Configuration -APP_NAME = "TR1X" -APP_BUNDLE_PATH = Path(f"/tmp/{APP_NAME}.app") -DMG_NAME = Path(f"{APP_NAME}-Installer.dmg") - - def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser( - description=f"Create a DMG installer for {APP_NAME}." - ) + parser = argparse.ArgumentParser(description=f"Create a DMG installer.") + parser.add_argument("-a", "--app-name") + parser.add_argument("-i", "--icon-path", type=Path) return parser.parse_args() -def create_dmg(app_name: str, dmg_name: str, app_bundle_path: Path) -> None: +def create_dmg(app_name: str, dmg_name: str, icon_path: Path, app_bundle_path: Path) -> None: subprocess.run( ( "create-dmg", "--volname", - f"{APP_NAME} Installer", + f"{app_name} Installer", "--volicon", - "data/tr1/mac/icon.icns", + str(icon_path), "--window-pos", "200", "120", @@ -34,11 +28,11 @@ def create_dmg(app_name: str, dmg_name: str, app_bundle_path: Path) -> None: "--icon-size", "100", "--icon", - f"{APP_NAME}.app", + f"{app_name}.app", "200", "190", "--hide-extension", - f"{APP_NAME}.app", + f"{app_name}.app", "--app-drop-link", "600", "185", @@ -51,10 +45,12 @@ def create_dmg(app_name: str, dmg_name: str, app_bundle_path: Path) -> None: def main() -> None: args = parse_args() - if DMG_NAME.is_file(): - DMG_NAME.unlink() + dmg_name = Path(f"{args.app_name}-Installer.dmg") + if dmg_name.is_file(): + dmg_name.unlink() - create_dmg(APP_NAME, DMG_NAME, APP_BUNDLE_PATH) + app_bundle_path = Path(f"/tmp/{args.app_name}.app") + create_dmg(args.app_name, dmg_name, args.icon_path, app_bundle_path) if __name__ == "__main__": diff --git a/tools/tr1/mac/x86-64_cross_file.txt b/tools/shared/mac/x86-64_cross_file.txt similarity index 100% rename from tools/tr1/mac/x86-64_cross_file.txt rename to tools/shared/mac/x86-64_cross_file.txt