diff --git a/.github/workflows/packaging.yml b/.github/workflows/packaging.yml index 7d7236f079..8337e46502 100644 --- a/.github/workflows/packaging.yml +++ b/.github/workflows/packaging.yml @@ -65,8 +65,8 @@ jobs: file_glob: true file: build/linux/* - macos-net: - name: macOS .NET + macos: + name: macOS Disk Image runs-on: macos-11 steps: - name: Clone Repository @@ -89,43 +89,7 @@ jobs: MACOS_DEVELOPER_PASSWORD: ${{ secrets.MACOS_DEVELOPER_PASSWORD }} run: | mkdir -p build/macos - ./packaging/macos/buildpackage.sh "${GIT_TAG}" "${PWD}/build/macos" "standard" "build.dmg" - - - name: Upload Package - uses: svenstaro/upload-release-action@v2 - with: - repo_token: ${{ secrets.GITHUB_TOKEN }} - tag: ${{ github.ref }} - overwrite: true - file_glob: true - file: build/macos/* - - macos-mono: - name: macOS Mono - runs-on: macos-11 - - steps: - - name: Clone Repository - uses: actions/checkout@v3 - - - name: Install .NET 6 - uses: actions/setup-dotnet@v1 - with: - dotnet-version: '6.0.x' - - - name: Prepare Environment - run: echo "GIT_TAG=${GITHUB_REF#refs/tags/}" >> ${GITHUB_ENV} - - - name: Package Disk Image - env: - MACOS_DEVELOPER_IDENTITY: ${{ secrets.MACOS_DEVELOPER_IDENTITY }} - MACOS_DEVELOPER_CERTIFICATE_BASE64: ${{ secrets.MACOS_DEVELOPER_CERTIFICATE_BASE64 }} - MACOS_DEVELOPER_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_DEVELOPER_CERTIFICATE_PASSWORD }} - MACOS_DEVELOPER_USERNAME: ${{ secrets.MACOS_DEVELOPER_USERNAME }} - MACOS_DEVELOPER_PASSWORD: ${{ secrets.MACOS_DEVELOPER_PASSWORD }} - run: | - mkdir -p build/macos - ./packaging/macos/buildpackage.sh "${GIT_TAG}" "${PWD}/build/macos" "mono" "build-mono.dmg" + ./packaging/macos/buildpackage.sh "${GIT_TAG}" "${PWD}/build/macos" - name: Upload Package uses: svenstaro/upload-release-action@v2 diff --git a/packaging/macos/apphost-mono.c b/packaging/macos/apphost-mono.c new file mode 100644 index 0000000000..dce735444c --- /dev/null +++ b/packaging/macos/apphost-mono.c @@ -0,0 +1,48 @@ +/* + * Copyright 2007-2022 The OpenRA Developers (see AUTHORS) + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation. For more information, + * see COPYING. + */ + +// +// A custom apphost is required (instead of just invoking `mono OpenRA.dll ...` directly) +// because macOS will only properly associate dock icons and tooltips to windows that are +// created by a process in the Contents/MacOS directory (not subdirectories). +// +// Based on https://github.com/mono/monodevelop/blob/main/main/build/MacOSX/monostub.mm + +#include +#include +#include + +typedef int (* mono_main)(int argc, char **argv); + +int main(int argc, char **argv) +{ + // TODO: This snippet increasing the open file limit was copied from + // the monodevelop launcher stub. It may not be needed for OpenRA. + struct rlimit limit; + if (getrlimit(RLIMIT_NOFILE, &limit) == 0 && limit.rlim_cur < 1024) + { + limit.rlim_cur = limit.rlim_max < 1024 ? limit.rlim_max : 1024; + setrlimit(RLIMIT_NOFILE, &limit); + } + + void *libmono = dlopen(argv[1], RTLD_LAZY); + if (libmono == NULL) + { + fprintf(stderr, "Failed to load libmonosgen-2.0.dylib: %s\n", dlerror()); + return 1; + } + + mono_main _mono_main = (mono_main)dlsym(libmono, "mono_main"); + if (!_mono_main) + { + fprintf(stderr, "Could not load mono_main(): %s\n", dlerror()); + return 1; + } + + return _mono_main(argc - 1, &argv[1]); +} diff --git a/packaging/macos/apphost.c b/packaging/macos/apphost.c new file mode 100644 index 0000000000..1692a753b4 --- /dev/null +++ b/packaging/macos/apphost.c @@ -0,0 +1,84 @@ +/* + * Copyright 2007-2022 The OpenRA Developers (see AUTHORS) + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation. For more information, + * see COPYING. + */ + +// +// A custom apphost is required (instead of just invoking /OpenRA directly) +// because macOS will only properly associate dock icons and tooltips to windows that are +// created by a process in the Contents/MacOS directory (not subdirectories). +// +// .NET 6 does not support universal binaries, and the apphost that is created when +// publishing requires the runtime files to exist in the same directory as the launcher. +// + +#include +#include +#include + +typedef void* hostfxr_handle; +struct hostfxr_initialize_parameters +{ + size_t size; + char *host_path; + char *dotnet_root; +}; + +typedef int32_t(*hostfxr_initialize_for_dotnet_command_line_fn)( + int argc, + char **argv, + struct hostfxr_initialize_parameters *parameters, + hostfxr_handle *host_context_handle); + +typedef int32_t(*hostfxr_run_app_fn)(const hostfxr_handle host_context_handle); +typedef int32_t(*hostfxr_close_fn)(const hostfxr_handle host_context_handle); + +int main(int argc, char **argv) +{ + void *lib = dlopen(argv[1], RTLD_LAZY); + if (lib == NULL) + { + fprintf(stderr, "Failed to load %s: %s\n", argv[1], dlerror()); + return 1; + } + + hostfxr_initialize_for_dotnet_command_line_fn hostfxr_initialize_for_dotnet_command_line = (hostfxr_initialize_for_dotnet_command_line_fn)dlsym(lib, "hostfxr_initialize_for_dotnet_command_line"); + if (!hostfxr_initialize_for_dotnet_command_line) + { + fprintf(stderr, "Could not load hostfxr_initialize_for_dotnet_command_line(): %s\n", dlerror()); + return 1; + } + + hostfxr_run_app_fn hostfxr_run_app = (hostfxr_run_app_fn)dlsym(lib, "hostfxr_run_app"); + if (!hostfxr_run_app) + { + fprintf(stderr, "Could not load hostfxr_run_app(): %s\n", dlerror()); + return 1; + } + + hostfxr_close_fn hostfxr_close = (hostfxr_close_fn)dlsym(lib, "hostfxr_close"); + if (!hostfxr_close) + { + fprintf(stderr, "Could not load hostfxr_close(): %s\n", dlerror()); + return 1; + } + + struct hostfxr_initialize_parameters params; + params.size = sizeof(params); + params.host_path = argv[0]; + params.dotnet_root = dirname(argv[1]); + + hostfxr_handle host_context_handle; + hostfxr_initialize_for_dotnet_command_line( + argc - 2, + &argv[2], + ¶ms, + &host_context_handle); + + hostfxr_run_app(host_context_handle); + + return hostfxr_close(host_context_handle); +} diff --git a/packaging/macos/buildpackage.sh b/packaging/macos/buildpackage.sh index b7f7437f19..b9d7ffe506 100755 --- a/packaging/macos/buildpackage.sh +++ b/packaging/macos/buildpackage.sh @@ -21,8 +21,10 @@ if [[ "${OSTYPE}" != "darwin"* ]]; then exit 1 fi -if [ $# -ne "4" ]; then - echo "Usage: $(basename "$0") tag outputdir platform dmg" +command -v clang >/dev/null 2>&1 || { echo >&2 "macOS packaging requires clang."; exit 1; } + +if [ $# -ne "2" ]; then + echo "Usage: $(basename "$0") tag outputdir" exit 1 fi @@ -45,6 +47,7 @@ fi TAG="${1}" OUTPUTDIR="${2}" + SRCDIR="$(pwd)/../.." BUILTDIR="$(pwd)/build" ARTWORK_DIR="$(pwd)/../artwork/" @@ -55,15 +58,13 @@ modify_plist() { # Copies the game files and sets metadata build_app() { - PLATFORM="${1}" - TEMPLATE_DIR="${2}" - LAUNCHER_DIR="${3}" - MOD_ID="${4}" - MOD_NAME="${5}" - DISCORD_APPID="${6}" + TEMPLATE_DIR="${1}" + LAUNCHER_DIR="${2}" + MOD_ID="${3}" + MOD_NAME="${4}" + DISCORD_APPID="${5}" LAUNCHER_CONTENTS_DIR="${LAUNCHER_DIR}/Contents" - LAUNCHER_ASSEMBLY_DIR="${LAUNCHER_CONTENTS_DIR}/MacOS" LAUNCHER_RESOURCES_DIR="${LAUNCHER_CONTENTS_DIR}/Resources" cp -r "${TEMPLATE_DIR}" "${LAUNCHER_DIR}" @@ -74,12 +75,10 @@ build_app() { fi # Install engine and mod files - RUNTIME="net6" - if [ "${PLATFORM}" = "mono" ]; then - RUNTIME="mono" - fi + install_assemblies "${SRCDIR}" "${LAUNCHER_CONTENTS_DIR}/MacOS/x86_64" "osx-x64" "net6" "True" "True" "${IS_D2K}" + install_assemblies "${SRCDIR}" "${LAUNCHER_CONTENTS_DIR}/MacOS/arm64" "osx-arm64" "net6" "True" "True" "${IS_D2K}" + install_assemblies "${SRCDIR}" "${LAUNCHER_CONTENTS_DIR}/MacOS/mono" "osx-x64" "mono" "True" "True" "${IS_D2K}" - install_assemblies "${SRCDIR}" "${LAUNCHER_ASSEMBLY_DIR}" "osx-x64" "${RUNTIME}" "True" "True" "${IS_D2K}" install_data "${SRCDIR}" "${LAUNCHER_RESOURCES_DIR}" "${MOD_ID}" set_engine_version "${TAG}" "${LAUNCHER_RESOURCES_DIR}" set_mod_version "${TAG}" "${LAUNCHER_RESOURCES_DIR}/mods/${MOD_ID}/mod.yaml" "${LAUNCHER_RESOURCES_DIR}/mods/modcontent/mod.yaml" @@ -110,82 +109,101 @@ build_app() { fi } -build_platform() { - PLATFORM="${1}" - DMG_PATH="${2}" - echo "Building launchers (${PLATFORM})" +echo "Building launchers" - # Prepare generic template for the mods to duplicate and customize - TEMPLATE_DIR="${BUILTDIR}/template.app" - mkdir -p "${TEMPLATE_DIR}/Contents/Resources" - mkdir -p "${TEMPLATE_DIR}/Contents/MacOS" - echo "APPL????" > "${TEMPLATE_DIR}/Contents/PkgInfo" - cp Info.plist.in "${TEMPLATE_DIR}/Contents/Info.plist" - modify_plist "{DEV_VERSION}" "${TAG}" "${TEMPLATE_DIR}/Contents/Info.plist" - modify_plist "{FAQ_URL}" "http://wiki.openra.net/FAQ" "${TEMPLATE_DIR}/Contents/Info.plist" +# Prepare generic template for the mods to duplicate and customize +TEMPLATE_DIR="${BUILTDIR}/template.app" +mkdir -p "${TEMPLATE_DIR}/Contents/Resources" +mkdir -p "${TEMPLATE_DIR}/Contents/MacOS/mono" +mkdir -p "${TEMPLATE_DIR}/Contents/MacOS/x86_64" +mkdir -p "${TEMPLATE_DIR}/Contents/MacOS/arm64" - if [ "${PLATFORM}" = "mono" ]; then - modify_plist "{MINIMUM_SYSTEM_VERSION}" "10.9" "${TEMPLATE_DIR}/Contents/Info.plist" - clang -m64 launcher-mono.m -o "${TEMPLATE_DIR}/Contents/MacOS/Launcher" -framework AppKit -mmacosx-version-min=10.9 - else - modify_plist "{MINIMUM_SYSTEM_VERSION}" "10.14" "${TEMPLATE_DIR}/Contents/Info.plist" - clang -m64 launcher.m -o "${TEMPLATE_DIR}/Contents/MacOS/Launcher" -framework AppKit -mmacosx-version-min=10.14 - fi +echo "APPL????" > "${TEMPLATE_DIR}/Contents/PkgInfo" +cp Info.plist.in "${TEMPLATE_DIR}/Contents/Info.plist" +modify_plist "{DEV_VERSION}" "${TAG}" "${TEMPLATE_DIR}/Contents/Info.plist" +modify_plist "{FAQ_URL}" "http://wiki.openra.net/FAQ" "${TEMPLATE_DIR}/Contents/Info.plist" +modify_plist "{MINIMUM_SYSTEM_VERSION}" "10.11" "${TEMPLATE_DIR}/Contents/Info.plist" - build_app "${PLATFORM}" "${TEMPLATE_DIR}" "${BUILTDIR}/OpenRA - Red Alert.app" "ra" "Red Alert" "699222659766026240" - build_app "${PLATFORM}" "${TEMPLATE_DIR}" "${BUILTDIR}/OpenRA - Tiberian Dawn.app" "cnc" "Tiberian Dawn" "699223250181292033" - build_app "${PLATFORM}" "${TEMPLATE_DIR}" "${BUILTDIR}/OpenRA - Dune 2000.app" "d2k" "Dune 2000" "712711732770111550" +# Compile universal (x86_64 + arm64) Launcher and arch-specific apphosts +clang apphost.c -o "${TEMPLATE_DIR}/Contents/MacOS/apphost-x86_64" -framework AppKit -target x86_64-apple-macos10.15 +clang apphost.c -o "${TEMPLATE_DIR}/Contents/MacOS/apphost-arm64" -framework AppKit -target arm64-apple-macos10.15 +clang apphost-mono.c -o "${TEMPLATE_DIR}/Contents/MacOS/apphost-mono" -framework AppKit -target x86_64-apple-macos10.11 +clang checkmono.c -o "${TEMPLATE_DIR}/Contents/MacOS/checkmono" -framework AppKit -target x86_64-apple-macos10.11 +clang launcher.m -o "${TEMPLATE_DIR}/Contents/MacOS/Launcher-x86_64" -framework AppKit -target x86_64-apple-macos10.11 +clang launcher.m -o "${TEMPLATE_DIR}/Contents/MacOS/Launcher-arm64" -framework AppKit -target arm64-apple-macos10.15 +lipo -create -output "${TEMPLATE_DIR}/Contents/MacOS/Launcher" "${TEMPLATE_DIR}/Contents/MacOS/Launcher-x86_64" "${TEMPLATE_DIR}/Contents/MacOS/Launcher-arm64" +rm "${TEMPLATE_DIR}/Contents/MacOS/Launcher-x86_64" "${TEMPLATE_DIR}/Contents/MacOS/Launcher-arm64" - rm -rf "${TEMPLATE_DIR}" +build_app "${TEMPLATE_DIR}" "${BUILTDIR}/OpenRA - Red Alert.app" "ra" "Red Alert" "699222659766026240" +build_app "${TEMPLATE_DIR}" "${BUILTDIR}/OpenRA - Tiberian Dawn.app" "cnc" "Tiberian Dawn" "699223250181292033" +build_app "${TEMPLATE_DIR}" "${BUILTDIR}/OpenRA - Dune 2000.app" "d2k" "Dune 2000" "712711732770111550" - echo "Packaging disk image" - hdiutil create "${DMG_PATH}" -format UDRW -volname "OpenRA" -fs HFS+ -srcfolder build - DMG_DEVICE=$(hdiutil attach -readwrite -noverify -noautoopen "${DMG_PATH}" | egrep '^/dev/' | sed 1q | awk '{print $1}') - sleep 2 +rm -rf "${TEMPLATE_DIR}" - # Background image is created from source svg in artsrc repository - mkdir "/Volumes/OpenRA/.background/" - tiffutil -cathidpicheck "${ARTWORK_DIR}/macos-background.png" "${ARTWORK_DIR}/macos-background-2x.png" -out "/Volumes/OpenRA/.background/background.tiff" +echo "Packaging disk image" +hdiutil create "build.dmg" -format UDRW -volname "OpenRA" -fs HFS+ -srcfolder build +DMG_DEVICE=$(hdiutil attach -readwrite -noverify -noautoopen "build.dmg" | egrep '^/dev/' | sed 1q | awk '{print $1}') +sleep 2 - cp "${BUILTDIR}/OpenRA - Red Alert.app/Contents/Resources/ra.icns" "/Volumes/OpenRA/.VolumeIcon.icns" +# Background image is created from source svg in artsrc repository +mkdir "/Volumes/OpenRA/.background/" +tiffutil -cathidpicheck "${ARTWORK_DIR}/macos-background.png" "${ARTWORK_DIR}/macos-background-2x.png" -out "/Volumes/OpenRA/.background/background.tiff" - echo ' - tell application "Finder" - tell disk "'OpenRA'" - open - set current view of container window to icon view - set toolbar visible of container window to false - set statusbar visible of container window to false - set the bounds of container window to {400, 100, 1040, 580} - set theViewOptions to the icon view options of container window - set arrangement of theViewOptions to not arranged - set icon size of theViewOptions to 72 - set background picture of theViewOptions to file ".background:background.tiff" - make new alias file at container window to POSIX file "/Applications" with properties {name:"Applications"} - set position of item "'OpenRA - Tiberian Dawn.app'" of container window to {160, 106} - set position of item "'OpenRA - Red Alert.app'" of container window to {320, 106} - set position of item "'OpenRA - Dune 2000.app'" of container window to {480, 106} - set position of item "Applications" of container window to {320, 298} - set position of item ".background" of container window to {160, 298} - set position of item ".fseventsd" of container window to {160, 298} - set position of item ".VolumeIcon.icns" of container window to {160, 298} - update without registering applications - delay 5 - close - end tell - end tell - ' | osascript +cp "${BUILTDIR}/OpenRA - Red Alert.app/Contents/Resources/ra.icns" "/Volumes/OpenRA/.VolumeIcon.icns" - # HACK: Copy the volume icon again - something in the previous step seems to delete it...? - cp "${BUILTDIR}/OpenRA - Red Alert.app/Contents/Resources/ra.icns" "/Volumes/OpenRA/.VolumeIcon.icns" - SetFile -c icnC "/Volumes/OpenRA/.VolumeIcon.icns" - SetFile -a C "/Volumes/OpenRA" +echo ' + tell application "Finder" + tell disk "'OpenRA'" + open + set current view of container window to icon view + set toolbar visible of container window to false + set statusbar visible of container window to false + set the bounds of container window to {400, 100, 1040, 580} + set theViewOptions to the icon view options of container window + set arrangement of theViewOptions to not arranged + set icon size of theViewOptions to 72 + set background picture of theViewOptions to file ".background:background.tiff" + make new alias file at container window to POSIX file "/Applications" with properties {name:"Applications"} + set position of item "'OpenRA - Tiberian Dawn.app'" of container window to {160, 106} + set position of item "'OpenRA - Red Alert.app'" of container window to {320, 106} + set position of item "'OpenRA - Dune 2000.app'" of container window to {480, 106} + set position of item "Applications" of container window to {320, 298} + set position of item ".background" of container window to {160, 298} + set position of item ".fseventsd" of container window to {160, 298} + set position of item ".VolumeIcon.icns" of container window to {160, 298} + update without registering applications + delay 5 + close + end tell + end tell +' | osascript - # Replace duplicate .NET runtime files with hard links to improve compression - if [ "${PLATFORM}" != "mono" ]; then - for MOD in "Red Alert" "Tiberian Dawn"; do - for f in "/Volumes/OpenRA/OpenRA - ${MOD}.app/Contents/MacOS"/*; do - g="/Volumes/OpenRA/OpenRA - Dune 2000.app/Contents/MacOS/"$(basename "${f}") +# HACK: Copy the volume icon again - something in the previous step seems to delete it...? +cp "${BUILTDIR}/OpenRA - Red Alert.app/Contents/Resources/ra.icns" "/Volumes/OpenRA/.VolumeIcon.icns" +SetFile -c icnC "/Volumes/OpenRA/.VolumeIcon.icns" +SetFile -a C "/Volumes/OpenRA" + +# Replace duplicate .NET runtime files with hard links to improve compression +for MOD in "Red Alert" "Tiberian Dawn"; do + for p in "x86_64" "arm64" "mono"; do + for f in "/Volumes/OpenRA/OpenRA - ${MOD}.app/Contents/MacOS/${p}"/*; do + g="/Volumes/OpenRA/OpenRA - Dune 2000.app/Contents/MacOS/${p}/"$(basename "${f}") + hashf=$(shasum "${f}" | awk '{ print $1 }') || : + hashg=$(shasum "${g}" | awk '{ print $1 }') || : + if [ -n "${hashf}" ] && [ "${hashf}" = "${hashg}" ]; then + echo "Deduplicating ${f}" + rm "${f}" + ln "${g}" "${f}" + fi + done + done +done + +for MOD in "Red Alert" "Tiberian Dawn" "Dune 2000"; do + for p in "arm64" "mono"; do + for f in "/Volumes/OpenRA/OpenRA - ${MOD}.app/Contents/MacOS/x86_64"/*; do + g="/Volumes/OpenRA/OpenRA - ${MOD}.app/Contents/MacOS/${p}/"$(basename "${f}") + if [ -e "${g}" ]; then hashf=$(shasum "${f}" | awk '{ print $1 }') || : hashg=$(shasum "${g}" | awk '{ print $1 }') || : if [ -n "${hashf}" ] && [ "${hashf}" = "${hashg}" ]; then @@ -193,35 +211,37 @@ build_platform() { rm "${f}" ln "${g}" "${f}" fi - done + fi done - fi + done +done - chmod -Rf go-w /Volumes/OpenRA - sync - sync +chmod -Rf go-w /Volumes/OpenRA +sync +sync - hdiutil detach "${DMG_DEVICE}" - rm -rf "${BUILTDIR}" -} +hdiutil detach "${DMG_DEVICE}" +rm -rf "${BUILTDIR}" -notarize_package() { - DMG_PATH="${1}" - NOTARIZE_DMG_PATH="${DMG_PATH%.*}"-notarization.dmg - echo "Submitting ${DMG_PATH} for notarization" +if [ -n "${MACOS_DEVELOPER_CERTIFICATE_BASE64}" ] && [ -n "${MACOS_DEVELOPER_CERTIFICATE_PASSWORD}" ] && [ -n "${MACOS_DEVELOPER_IDENTITY}" ]; then + security delete-keychain build.keychain +fi + +if [ -n "${MACOS_DEVELOPER_USERNAME}" ] && [ -n "${MACOS_DEVELOPER_PASSWORD}" ] && [ -n "${MACOS_DEVELOPER_IDENTITY}" ]; then + echo "Submitting build for notarization" # Reset xcode search path to fix xcrun not finding altool sudo xcode-select -r # Create a temporary read-only dmg for submission (notarization service rejects read/write images) - hdiutil convert "${DMG_PATH}" -format ULFO -ov -o "${NOTARIZE_DMG_PATH}" + hdiutil convert "build.dmg" -format ULFO -ov -o "build-notarization.dmg" - xcrun notarytool submit "${NOTARIZE_DMG_PATH}" --wait --apple-id "${MACOS_DEVELOPER_USERNAME}" --password "${MACOS_DEVELOPER_PASSWORD}" --team-id "${MACOS_DEVELOPER_IDENTITY}" + xcrun notarytool submit "build-notarization.dmg" --wait --apple-id "${MACOS_DEVELOPER_USERNAME}" --password "${MACOS_DEVELOPER_PASSWORD}" --team-id "${MACOS_DEVELOPER_IDENTITY}" - rm "${NOTARIZE_DMG_PATH}" + rm "build-notarization.dmg" - echo "${DMG_PATH}: Stapling tickets" - DMG_DEVICE=$(hdiutil attach -readwrite -noverify -noautoopen "${DMG_PATH}" | egrep '^/dev/' | sed 1q | awk '{print $1}') + echo "Stapling tickets" + DMG_DEVICE=$(hdiutil attach -readwrite -noverify -noautoopen "build.dmg" | egrep '^/dev/' | sed 1q | awk '{print $1}') sleep 2 xcrun stapler staple "/Volumes/OpenRA/OpenRA - Red Alert.app" @@ -232,35 +252,7 @@ notarize_package() { sync hdiutil detach "${DMG_DEVICE}" - break -} - -finalize_package() { - PLATFORM="${1}" - INPUT_PATH="${2}" - OUTPUT_PATH="${3}" - - if [ "${PLATFORM}" = "mono" ]; then - hdiutil convert "${INPUT_PATH}" -format UDZO -imagekey zlib-level=9 -ov -o "${OUTPUT_PATH}-mono.dmg" - else - # ULFO offers better compression and faster decompression speeds, but is only supported by 10.11+ - hdiutil convert "${INPUT_PATH}" -format ULFO -ov -o "${OUTPUT_PATH}.dmg" - fi - - rm "${INPUT_PATH}" -} - -PLATFORM="$3" -DISK_IMAGE="$4" - -build_platform "${PLATFORM}" "${DISK_IMAGE}" - -if [ -n "${MACOS_DEVELOPER_CERTIFICATE_BASE64}" ] && [ -n "${MACOS_DEVELOPER_CERTIFICATE_PASSWORD}" ] && [ -n "${MACOS_DEVELOPER_IDENTITY}" ]; then - security delete-keychain build.keychain fi -if [ -n "${MACOS_DEVELOPER_USERNAME}" ] && [ -n "${MACOS_DEVELOPER_PASSWORD}" ] && [ -n "${MACOS_DEVELOPER_IDENTITY}" ]; then - notarize_package "${DISK_IMAGE}" -fi - -finalize_package "${PLATFORM}" "${DISK_IMAGE}" "${OUTPUTDIR}/OpenRA-${TAG}" +hdiutil convert "build.dmg" -format ULFO -ov -o "${OUTPUTDIR}/OpenRA-${TAG}.dmg" +rm "build.dmg" diff --git a/packaging/macos/checkmono.c b/packaging/macos/checkmono.c new file mode 100644 index 0000000000..5e4b61cd27 --- /dev/null +++ b/packaging/macos/checkmono.c @@ -0,0 +1,73 @@ +/* + * Copyright 2007-2022 The OpenRA Developers (see AUTHORS) + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation. For more information, + * see COPYING. + */ + +// +// NOTE: Mono.framework only ships intel dylibs, so cannot be loaded by the arm64 slice of the Launcher utility. +// Splitting checkmono into its own intel-only utility allows it to be called through rosetta if the user +// wants to force the game to run under mono-through-rosetta. +// +// Based on https://github.com/mono/monodevelop/blob/main/main/build/MacOSX/monostub.mm and https://github.com/mono/monodevelop/blob/main/main/build/MacOSX/monostub-utils.h + +#include +#include +#include + +#define SYSTEM_MONO_PATH "/Library/Frameworks/Mono.framework/Versions/Current/" +#define SYSTEM_MONO_MIN_VERSION "6.4" + +typedef char *(* mono_get_runtime_build_info)(void); + +int main(int argc, char **argv) +{ + void *libmono = dlopen(SYSTEM_MONO_PATH "lib/libmonosgen-2.0.dylib", RTLD_LAZY); + if (libmono == NULL) + { + fprintf (stderr, "Failed to load libmonosgen-2.0.dylib: %s\n", dlerror()); + return 1; + } + + mono_get_runtime_build_info _mono_get_runtime_build_info = (mono_get_runtime_build_info)dlsym(libmono, "mono_get_runtime_build_info"); + if (!_mono_get_runtime_build_info) + { + fprintf(stderr, "Could not load mono_get_runtime_build_info(): %s\n", dlerror()); + return 1; + } + + char *version = _mono_get_runtime_build_info(); + char *req_end, *end; + long req_val, val; + char *req_version = SYSTEM_MONO_MIN_VERSION; + + while (*req_version && *version) + { + req_val = strtol(req_version, &req_end, 10); + if (req_version == req_end || (*req_end && *req_end != '.')) + { + fprintf(stderr, "Bad version requirement string '%s'\n", req_end); + return 1; + } + + val = strtol(version, &end, 10); + if (version == end || val < req_val) + return 1; + + if (val > req_val) + return 0; + + if (*req_end == '.' && *end != '.') + return 1; + + req_version = req_end; + if (*req_version) + req_version++; + + version = end + 1; + } + + return 0; +} diff --git a/packaging/macos/launcher-mono.m b/packaging/macos/launcher-mono.m deleted file mode 100644 index f6489f803f..0000000000 --- a/packaging/macos/launcher-mono.m +++ /dev/null @@ -1,386 +0,0 @@ -/* - * Copyright 2007-2022 The OpenRA Developers (see AUTHORS) - * This file is part of OpenRA, which is free software. It is made - * available to you under the terms of the GNU General Public License - * as published by the Free Software Foundation. For more information, - * see COPYING. - */ - -#import -#include - -#define SYSTEM_MONO_PATH @"/Library/Frameworks/Mono.framework/Versions/Current/" -#define SYSTEM_MONO_MIN_VERSION @"6.4" - -typedef int (* mono_main)(int argc, char **argv); -typedef void (* mono_free)(void *ptr); -typedef char *(* mono_get_runtime_build_info)(void); - -@interface OpenRALauncher : NSObject -- (void)launchGameWithArgs: (NSArray *)gameArgs; -@end - -@implementation OpenRALauncher - -BOOL launched = NO; -NSTask *gameTask; - -static int check_mono_version(const char *version, const char *req_version) -{ - char *req_end, *end; - long req_val, val; - - while (*req_version) - { - req_val = strtol(req_version, &req_end, 10); - if (req_version == req_end || (*req_end && *req_end != '.')) - { - fprintf(stderr, "Bad version requirement string '%s'\n", req_end); - return FALSE; - } - - req_version = req_end; - if (*req_version) - req_version++; - - val = strtol (version, &end, 10); - if (version == end || val < req_val) - return FALSE; - - if (val > req_val) - return TRUE; - - if (*req_version == '.' && *end != '.') - return FALSE; - - version = end + 1; - } - - return TRUE; -} - -- (int)hasValidMono -{ - void *libmono = dlopen([[SYSTEM_MONO_PATH stringByAppendingPathComponent: @"/lib/libmonosgen-2.0.dylib"] UTF8String], RTLD_LAZY); - - if (libmono == NULL) - { - fprintf (stderr, "Failed to load libmonosgen-2.0.dylib: %s\n", dlerror()); - return FALSE; - } - - mono_get_runtime_build_info _mono_get_runtime_build_info = (mono_get_runtime_build_info)dlsym(libmono, "mono_get_runtime_build_info"); - if (!_mono_get_runtime_build_info) - { - fprintf(stderr, "Could not load mono_get_runtime_build_info(): %s\n", dlerror()); - return FALSE; - } - - char *mono_version = _mono_get_runtime_build_info(); - return check_mono_version(mono_version, [SYSTEM_MONO_MIN_VERSION UTF8String]); -} - -- (NSString *)modName -{ - NSDictionary *plist = [[NSBundle mainBundle] infoDictionary]; - if (plist) - { - NSString *title = [plist objectForKey:@"CFBundleDisplayName"]; - if (title && [title length] > 0) - return title; - } - - return @"OpenRA"; -} - -- (void)exitWithMonoPrompt -{ - [NSApp setActivationPolicy: NSApplicationActivationPolicyRegular]; - [[NSApplication sharedApplication] activateIgnoringOtherApps:YES]; - - NSString *modName = [self modName]; - NSString *title = [NSString stringWithFormat: @"Cannot launch %@", modName]; - NSString *message = [NSString stringWithFormat: @"%@ requires Mono %@ or later. Please install Mono and try again.", modName, SYSTEM_MONO_MIN_VERSION]; - - NSAlert *alert = [[NSAlert alloc] init]; - [alert setMessageText:title]; - [alert setInformativeText:message]; - [alert addButtonWithTitle:@"Download Mono"]; - [alert addButtonWithTitle:@"Quit"]; - NSInteger answer = [alert runModal]; - [alert release]; - - if (answer == NSAlertFirstButtonReturn) - [[NSWorkspace sharedWorkspace] openURL: [NSURL URLWithString:@"https://www.mono-project.com/download/"]]; - - exit(1); -} - -- (void)exitWithCrashPrompt -{ - [NSApp setActivationPolicy: NSApplicationActivationPolicyRegular]; - [[NSApplication sharedApplication] activateIgnoringOtherApps:YES]; - - NSString *modName = [self modName]; - NSString *message = [NSString stringWithFormat: @"%@ has encountered a fatal error and must close.\nPlease refer to the crash logs and FAQ for more information.", modName]; - - NSAlert *alert = [[NSAlert alloc] init]; - [alert setMessageText:@"Fatal Error"]; - [alert setInformativeText:message]; - [alert addButtonWithTitle:@"View Logs"]; - [alert addButtonWithTitle:@"View FAQ"]; - [alert addButtonWithTitle:@"Quit"]; - - NSInteger answer = [alert runModal]; - [alert release]; - - if (answer == NSAlertFirstButtonReturn) - { - NSString *logDir = [@"~/Library/Application Support/OpenRA/Logs/" stringByExpandingTildeInPath]; - [[NSWorkspace sharedWorkspace] openFile: logDir withApplication:@"Finder"]; - } - else if (answer == NSAlertSecondButtonReturn) - { - NSDictionary *plist = [[NSBundle mainBundle] infoDictionary]; - if (plist) - { - NSString *faqUrl = [plist objectForKey:@"FaqUrl"]; - if (faqUrl && [faqUrl length] > 0) - [[NSWorkspace sharedWorkspace] openURL: [NSURL URLWithString:faqUrl]]; - } - } - - exit(1); -} - -// Application was launched via a URL handler -- (void)getUrl:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent -{ - NSMutableArray *gameArgs = [[[NSProcessInfo processInfo] arguments] mutableCopy]; - NSDictionary *plist = [[NSBundle mainBundle] infoDictionary]; - NSString *url = [[event paramDescriptorForKeyword:keyDirectObject] stringValue]; - - if (plist) - { - NSString *joinServerUrl = [plist objectForKey:@"JoinServerUrlScheme"]; - if (joinServerUrl && [joinServerUrl length] > 0) - { - NSString *prefix = [joinServerUrl stringByAppendingString: @"://"]; - if ([url hasPrefix: prefix]) - { - NSString *trimmed = [url substringFromIndex:[prefix length]]; - NSArray *parts = [trimmed componentsSeparatedByString:@":"]; - NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init]; - - if ([parts count] == 2 && [formatter numberFromString: [parts objectAtIndex:1]] != nil) - [gameArgs addObject: [NSString stringWithFormat: @"Launch.Connect=%@", trimmed]]; - - [formatter release]; - } - } - } - - [self launchGameWithArgs: gameArgs]; - [gameArgs release]; -} - -- (void)applicationWillFinishLaunching:(NSNotification *)aNotification -{ - // Register for url events - NSDictionary *plist = [[NSBundle mainBundle] infoDictionary]; - if (plist) - { - NSString *joinServerUrl = [plist objectForKey:@"JoinServerUrlScheme"]; - NSString *bundleIdentifier = [plist objectForKey:@"CFBundleIdentifier"]; - if (joinServerUrl && [joinServerUrl length] > 0 && bundleIdentifier) - { - LSSetDefaultHandlerForURLScheme((CFStringRef)joinServerUrl, (CFStringRef)bundleIdentifier); - [[NSAppleEventManager sharedAppleEventManager] setEventHandler:self andSelector:@selector(getUrl:withReplyEvent:) forEventClass:kInternetEventClass andEventID:kAEGetURL]; - } - } -} - -- (void)applicationDidFinishLaunching:(NSNotification *)aNotification -{ - [self launchGameWithArgs: [[NSProcessInfo processInfo] arguments]]; -} - -- (BOOL)applicationShouldTerminateAfterLastWindowClosed: (NSApplication *)theApplication -{ - return YES; -} - -- (void)launchGameWithArgs: (NSArray *)gameArgs -{ - if (launched) - { - NSLog(@"launchgame is already running... ignoring request."); - return; - } - - launched = YES; - - if (![self hasValidMono]) - [self exitWithMonoPrompt]; - - // Default values - can be overriden by setting certain keys Info.plist - NSString *gameName = @"OpenRA.dll"; - NSString *modId = nil; - - NSDictionary *plist = [[NSBundle mainBundle] infoDictionary]; - if (plist) - { - NSString *exeValue = [plist objectForKey:@"MonoGameExe"]; - if (exeValue && [exeValue length] > 0) - gameName = exeValue; - - NSString *modIdValue = [plist objectForKey:@"ModId"]; - if (modIdValue && [modIdValue length] > 0) - modId = modIdValue; - } - - NSString *exePath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent: @"Contents/MacOS/"]; - NSString *gamePath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent: @"Contents/Resources/"]; - NSString *appPath = [exePath stringByAppendingPathComponent: @"Launcher"]; - NSString *engineLaunchPath = [self resolveTranslocatedPath: appPath]; - - NSMutableArray *launchArgs = [NSMutableArray arrayWithCapacity: [gameArgs count] + 2]; - [launchArgs addObject: @"--debug"]; - [launchArgs addObject: [exePath stringByAppendingPathComponent: gameName]]; - [launchArgs addObject: [NSString stringWithFormat:@"Engine.LaunchPath=\"%@\"", engineLaunchPath]]; - [launchArgs addObject: [NSString stringWithFormat:@"Engine.EngineDir=../Resources"]]; - - if (modId) - [launchArgs addObject: [NSString stringWithFormat:@"Game.Mod=%@", modId]]; - - [launchArgs addObjectsFromArray: gameArgs]; - - NSLog(@"Running launchgame with arguments:"); - for (size_t i = 0; i < [launchArgs count]; i++) - NSLog(@"%@", [launchArgs objectAtIndex: i]); - - gameTask = [[NSTask alloc] init]; - [gameTask setCurrentDirectoryPath: gamePath]; - [gameTask setLaunchPath: appPath]; - [gameTask setArguments: launchArgs]; - - [[NSNotificationCenter defaultCenter] - addObserver: self - selector: @selector(taskExited:) - name: NSTaskDidTerminateNotification - object: gameTask - ]; - - [gameTask launch]; -} - -- (NSString *)resolveTranslocatedPath: (NSString *)path -{ - // macOS 10.12 introduced the "App Translocation" feature, which runs quarantined applications - // from a transient read-only disk image. The read-only image isn't a problem, but the transient - // path breaks the mod registration/switching feature. - // This resolves the original path which can then be written into the mod metadata for future - // launches (which will then be re-translocated) - - // Running on macOS < 10.12 - if (floor(NSAppKitVersionNumber) <= 1404) - return path; - - void *handle = dlopen("/System/Library/Frameworks/Security.framework/Security", RTLD_LAZY); - - // Failed to load security framework - if (handle == NULL) - return path; - - Boolean (*mySecTranslocateIsTranslocatedURL)(CFURLRef path, bool *isTranslocated, CFErrorRef * __nullable error); - mySecTranslocateIsTranslocatedURL = dlsym(handle, "SecTranslocateIsTranslocatedURL"); - - CFURLRef __nullable (*mySecTranslocateCreateOriginalPathForURL)(CFURLRef translocatedPath, CFErrorRef * __nullable error); - mySecTranslocateCreateOriginalPathForURL = dlsym(handle, "SecTranslocateCreateOriginalPathForURL"); - - // Failed to resolve required functions - if (mySecTranslocateIsTranslocatedURL == NULL || mySecTranslocateCreateOriginalPathForURL == NULL) - return path; - - bool isTranslocated = false; - CFURLRef pathURLRef = CFURLCreateWithFileSystemPath(kCFAllocatorDefault, (__bridge CFStringRef)path, kCFURLPOSIXPathStyle, false); - - if (mySecTranslocateIsTranslocatedURL(pathURLRef, &isTranslocated, NULL)) - { - if (isTranslocated) - { - CFURLRef resolvedURL = mySecTranslocateCreateOriginalPathForURL(pathURLRef, NULL); - path = [(NSURL *)(resolvedURL) path]; - } - } - - CFRelease(pathURLRef); - return path; -} - -- (void)taskExited:(NSNotification *)note -{ - [[NSNotificationCenter defaultCenter] - removeObserver:self - name:NSTaskDidTerminateNotification - object:gameTask - ]; - - int ret = [gameTask terminationStatus]; - NSLog(@"launchgame exited with code %d", ret); - [gameTask release]; - gameTask = nil; - - // We're done here - if (ret != 0) - [self exitWithCrashPrompt]; - - exit(0); -} - -@end - -int main(int argc, char **argv) -{ - NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; - - if (argc > 1) - { - struct rlimit limit; - if (getrlimit (RLIMIT_NOFILE, &limit) == 0 && limit.rlim_cur < 1024) - { - limit.rlim_cur = MIN(limit.rlim_max, 1024); - setrlimit(RLIMIT_NOFILE, &limit); - } - - void *libmono = dlopen([[SYSTEM_MONO_PATH stringByAppendingPathComponent: @"/lib/libmonosgen-2.0.dylib"] UTF8String], RTLD_LAZY); - if (libmono == NULL) - { - fprintf (stderr, "Failed to load libmonosgen-2.0.dylib: %s\n", dlerror()); - return EXIT_FAILURE; - } - - mono_main _mono_main = (mono_main)dlsym(libmono, "mono_main"); - if (!_mono_main) - { - fprintf(stderr, "Could not load mono_main(): %s\n", dlerror()); - return EXIT_FAILURE; - } - - [pool drain]; - - return _mono_main(argc, argv); - } - - NSApplication *application = [NSApplication sharedApplication]; - OpenRALauncher *launcher = [[OpenRALauncher alloc] init]; - [NSApp setActivationPolicy: NSApplicationActivationPolicyProhibited]; - - [application setDelegate:launcher]; - [application run]; - - [launcher release]; - [pool drain]; - - return EXIT_SUCCESS; -} diff --git a/packaging/macos/launcher.m b/packaging/macos/launcher.m index d1f7bf64b0..632ff38b67 100644 --- a/packaging/macos/launcher.m +++ b/packaging/macos/launcher.m @@ -8,6 +8,13 @@ #import #include +#include +#include +#include + +#define SYSTEM_MONO_PATH @"/Library/Frameworks/Mono.framework/Versions/Current/" +#define SYSTEM_MONO_MIN_VERSION @"6.4" +#define DOTNET_MIN_MACOS_VERSION 10.15 @interface OpenRALauncher : NSObject - (void)launchGameWithArgs: (NSArray *)gameArgs; @@ -31,8 +38,34 @@ NSTask *gameTask; return @"OpenRA"; } -- (void)showCrashPrompt +- (void)exitWithMonoPrompt { + [NSApp setActivationPolicy: NSApplicationActivationPolicyRegular]; + [[NSApplication sharedApplication] activateIgnoringOtherApps:YES]; + + NSString *modName = [self modName]; + NSString *title = [NSString stringWithFormat: @"Cannot launch %@", modName]; + NSString *message = [NSString stringWithFormat: @"%@ requires Mono %@ or later. Please install Mono and try again.", modName, SYSTEM_MONO_MIN_VERSION]; + + NSAlert *alert = [[NSAlert alloc] init]; + [alert setMessageText:title]; + [alert setInformativeText:message]; + [alert addButtonWithTitle:@"Download Mono"]; + [alert addButtonWithTitle:@"Quit"]; + NSInteger answer = [alert runModal]; + [alert release]; + + if (answer == NSAlertFirstButtonReturn) + [[NSWorkspace sharedWorkspace] openURL: [NSURL URLWithString:@"https://www.mono-project.com/download/"]]; + + exit(1); +} + +- (void)exitWithCrashPrompt +{ + [NSApp setActivationPolicy: NSApplicationActivationPolicyRegular]; + [[NSApplication sharedApplication] activateIgnoringOtherApps:YES]; + NSString *modName = [self modName]; NSString *message = [NSString stringWithFormat: @"%@ has encountered a fatal error and must close.\nPlease refer to the crash logs and FAQ for more information.", modName]; @@ -61,6 +94,8 @@ NSTask *gameTask; [[NSWorkspace sharedWorkspace] openURL: [NSURL URLWithString:faqUrl]]; } } + + exit(1); } // Application was launched via a URL handler @@ -120,6 +155,16 @@ NSTask *gameTask; return YES; } +- (int)hasValidMono +{ + NSTask *task = [[NSTask alloc] init]; + [task setLaunchPath: [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent: @"Contents/MacOS/checkmono"]]; + [task launch]; + [task waitUntilExit]; + + return [task terminationStatus] == 0; +} + - (void)launchGameWithArgs: (NSArray *)gameArgs { if (launched) @@ -130,6 +175,16 @@ NSTask *gameTask; launched = YES; + BOOL useMono = NO; + + if (@available(macOS 10.15, *)) + useMono = [[[NSProcessInfo processInfo] environment]objectForKey:@"OPENRA_PREFER_MONO"] != nil; + else + useMono = YES; + + if (useMono && ![self hasValidMono]) + [self exitWithMonoPrompt]; + // Default values - can be overriden by setting certain keys Info.plist NSString *modId = nil; @@ -144,13 +199,44 @@ NSTask *gameTask; NSString *exePath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent: @"Contents/MacOS/"]; NSString *gamePath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent: @"Contents/Resources/"]; - NSString *launchPath = [exePath stringByAppendingPathComponent: @"OpenRA"]; + NSString *launchPath; + NSString *dllPath; + NSString *hostPath; + + if (useMono) + { + launchPath = [exePath stringByAppendingPathComponent: @"apphost-mono"]; + hostPath = [SYSTEM_MONO_PATH stringByAppendingPathComponent: @"lib/libmonosgen-2.0.dylib"];; + dllPath = [exePath stringByAppendingPathComponent: @"mono/OpenRA.dll"]; + } + else + { + size_t size; + cpu_type_t type; + size = sizeof(type); + + if (sysctlbyname("hw.cputype", &type, &size, NULL, 0) == 0 && (type & 0xFF) == CPU_TYPE_ARM) + { + launchPath = [exePath stringByAppendingPathComponent: @"apphost-arm64"]; + hostPath = [exePath stringByAppendingPathComponent: @"arm64/libhostfxr.dylib"];; + dllPath = [exePath stringByAppendingPathComponent: @"arm64/OpenRA.dll"]; + } + else + { + launchPath = [exePath stringByAppendingPathComponent: @"apphost-x86_64"]; + hostPath = [exePath stringByAppendingPathComponent: @"x86_64/libhostfxr.dylib"];; + dllPath = [exePath stringByAppendingPathComponent: @"x86_64/OpenRA.dll"]; + } + } + NSString *appPath = [exePath stringByAppendingPathComponent: @"Launcher"]; NSString *engineLaunchPath = [self resolveTranslocatedPath: appPath]; - NSMutableArray *launchArgs = [NSMutableArray arrayWithCapacity: [gameArgs count] + 2]; + NSMutableArray *launchArgs = [NSMutableArray arrayWithCapacity: [gameArgs count] + 5]; + [launchArgs addObject: hostPath]; + [launchArgs addObject: dllPath]; [launchArgs addObject: [NSString stringWithFormat:@"Engine.LaunchPath=\"%@\"", engineLaunchPath]]; - [launchArgs addObject: [NSString stringWithFormat:@"Engine.EngineDir=../Resources"]]; + [launchArgs addObject: [NSString stringWithFormat:@"Engine.EngineDir=../../Resources"]]; if (modId) [launchArgs addObject: [NSString stringWithFormat:@"Game.Mod=%@", modId]]; @@ -229,21 +315,15 @@ NSTask *gameTask; ]; int ret = [gameTask terminationStatus]; - NSLog(@"launchgame exited with code %d", ret); [gameTask release]; gameTask = nil; // We're done here - if (ret == 0) - exit(0); + if (ret != 0) + [self exitWithCrashPrompt]; - // Make the error dialog visible - [NSApp setActivationPolicy: NSApplicationActivationPolicyRegular]; - [[NSApplication sharedApplication] activateIgnoringOtherApps:YES]; - [self showCrashPrompt]; - - exit(1); + exit(0); } @end