Package macOS releases as a universal binary.

* Minimum macOS version is raised to 10.11.
* App bundles ship 3 versions of the runtime and engine binaries,
  and a fat launcher that selects the appropriate runtime/apphost.
* Mono is used for macOS 10.11 - 10.14, or if OPENRA_PREFER_MONO
  environment variable has been set.
This commit is contained in:
Paul Chote
2022-12-18 11:24:50 +13:00
committed by Gustas
parent 363a0e1d1e
commit e2e541a251
7 changed files with 420 additions and 565 deletions

View File

@@ -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 <dlfcn.h>
#include <stdio.h>
#include <sys/resource.h>
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]);
}

84
packaging/macos/apphost.c Normal file
View File

@@ -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 <arch-dir>/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 <dlfcn.h>
#include <libgen.h>
#include <stdio.h>
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],
&params,
&host_context_handle);
hostfxr_run_app(host_context_handle);
return hostfxr_close(host_context_handle);
}

View File

@@ -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"

View File

@@ -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 <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#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;
}

View File

@@ -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 <Cocoa/Cocoa.h>
#include <dlfcn.h>
#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 <NSApplicationDelegate>
- (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;
}

View File

@@ -8,6 +8,13 @@
#import <Cocoa/Cocoa.h>
#include <dlfcn.h>
#include <sys/types.h>
#include <sys/sysctl.h>
#include <mach/machine.h>
#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 <NSApplicationDelegate>
- (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