diff --git a/packaging/macos/Eluant.dll.config b/packaging/macos/Eluant.dll.config new file mode 100644 index 0000000000..43be429756 --- /dev/null +++ b/packaging/macos/Eluant.dll.config @@ -0,0 +1,3 @@ + + + diff --git a/packaging/macos/Info.plist.in b/packaging/macos/Info.plist.in new file mode 100644 index 0000000000..4990c6a69e --- /dev/null +++ b/packaging/macos/Info.plist.in @@ -0,0 +1,50 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleDisplayName + {MOD_NAME} + CFBundleExecutable + OpenRA + CFBundleIconFile + {MOD_ID}.icns + CFBundleIdentifier + net.openra.mod.{MOD_ID} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + {MOD_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + {DEV_VERSION} + CFBundleSignature + ???? + CFBundleVersion + {DEV_VERSION} + LSMinimumSystemVersion + {MINIMUM_SYSTEM_VERSION} + NSPrincipalClass + NSApplication + CFBundleURLTypes + + + CFBundleURLName + OpenRA Server + CFBundleURLSchemes + + {JOIN_SERVER_URL_SCHEME} + {DISCORD_URL_SCHEME} + + + + ModId + {MOD_ID} + FaqUrl + {FAQ_URL} + JoinServerUrlScheme + {JOIN_SERVER_URL_SCHEME} + + diff --git a/packaging/macos/buildpackage.sh b/packaging/macos/buildpackage.sh index 17b978d547..6ee4e851f6 100755 --- a/packaging/macos/buildpackage.sh +++ b/packaging/macos/buildpackage.sh @@ -13,7 +13,7 @@ # MACOS_DEVELOPER_PASSWORD: App-specific password for the developer account # -LAUNCHER_TAG="osx-launcher-20200525" +MONO_TAG="osx-launcher-20200830" if [ $# -ne "2" ]; then echo "Usage: $(basename "$0") tag outputdir" @@ -56,6 +56,7 @@ populate_bundle() { MOD_ID=${2} MOD_NAME=${3} DISCORD_APPID=${4} + cp -r "${BUILTDIR}/OpenRA.app" "${TEMPLATE_DIR}" # Assemble multi-resolution icon @@ -76,7 +77,7 @@ populate_bundle() { modify_plist "{MOD_ID}" "${MOD_ID}" "${TEMPLATE_DIR}/Contents/Info.plist" modify_plist "{MOD_NAME}" "${MOD_NAME}" "${TEMPLATE_DIR}/Contents/Info.plist" modify_plist "{JOIN_SERVER_URL_SCHEME}" "openra-${MOD_ID}-${TAG}" "${TEMPLATE_DIR}/Contents/Info.plist" - modify_plist "{ADDITIONAL_URL_SCHEMES}" "discord-${DISCORD_APPID}" "${TEMPLATE_DIR}/Contents/Info.plist" + modify_plist "{DISCORD_URL_SCHEME}" "discord-${DISCORD_APPID}" "${TEMPLATE_DIR}/Contents/Info.plist" } # Deletes from the first argument's mod dirs all the later arguments @@ -95,124 +96,145 @@ sign_bundle() { fi } -echo "Building launchers" -curl -s -L -O https://github.com/OpenRA/OpenRALauncherOSX/releases/download/${LAUNCHER_TAG}/launcher.zip || exit 3 -unzip -qq -d "${BUILTDIR}" launcher.zip -rm launcher.zip +build_platform() { + PLATFORM="${1}" + DMG_PATH="${2}" + echo "Building launchers (${PLATFORM})" -modify_plist "{DEV_VERSION}" "${TAG}" "${BUILTDIR}/OpenRA.app/Contents/Info.plist" -modify_plist "{FAQ_URL}" "http://wiki.openra.net/FAQ" "${BUILTDIR}/OpenRA.app/Contents/Info.plist" -echo "Building core files" + mkdir -p "${BUILTDIR}/OpenRA.app/Contents/Resources" + mkdir -p "${BUILTDIR}/OpenRA.app/Contents/MacOS" + echo "APPL????" > "${BUILTDIR}/OpenRA.app/Contents/PkgInfo" + cp Eluant.dll.config "${BUILTDIR}/OpenRA.app/Contents/Resources" + cp Info.plist.in "${BUILTDIR}/OpenRA.app/Contents/Info.plist" + modify_plist "{DEV_VERSION}" "${TAG}" "${BUILTDIR}/OpenRA.app/Contents/Info.plist" + modify_plist "{FAQ_URL}" "http://wiki.openra.net/FAQ" "${BUILTDIR}/OpenRA.app/Contents/Info.plist" -pushd "${SRCDIR}" > /dev/null || exit 1 -make clean -make core TARGETPLATFORM=osx-x64 -make version VERSION="${TAG}" -make install-core gameinstalldir="/Contents/Resources/" DESTDIR="${BUILTDIR}/OpenRA.app" -make install-dependencies TARGETPLATFORM=osx-x64 gameinstalldir="/Contents/Resources/" DESTDIR="${BUILTDIR}/OpenRA.app" -popd > /dev/null || exit 1 + if [ "${PLATFORM}" = "compat" ]; then + modify_plist "{MINIMUM_SYSTEM_VERSION}" "10.9" "${BUILTDIR}/OpenRA.app/Contents/Info.plist" + clang -m64 launcher-mono.m -o "${BUILTDIR}/OpenRA.app/Contents/MacOS/OpenRA" -framework AppKit -mmacosx-version-min=10.9 + else + modify_plist "{MINIMUM_SYSTEM_VERSION}" "10.13" "${BUILTDIR}/OpenRA.app/Contents/Info.plist" + clang -m64 launcher.m -o "${BUILTDIR}/OpenRA.app/Contents/MacOS/OpenRA" -framework AppKit -mmacosx-version-min=10.13 -populate_bundle "OpenRA - Red Alert.app" "ra" "Red Alert" "699222659766026240" -delete_mods "OpenRA - Red Alert.app" "cnc" "d2k" -sign_bundle "OpenRA - Red Alert.app" + curl -s -L -O https://github.com/OpenRA/OpenRALauncherOSX/releases/download/${MONO_TAG}/mono.zip || exit 3 + unzip -qq -d "${BUILTDIR}/mono" mono.zip + mv "${BUILTDIR}/mono/mono" "${BUILTDIR}/OpenRA.app/Contents/MacOS/" + mv "${BUILTDIR}/mono/etc" "${BUILTDIR}/OpenRA.app/Contents/Resources" + mv "${BUILTDIR}/mono/lib" "${BUILTDIR}/OpenRA.app/Contents/Resources" + rm mono.zip + rmdir "${BUILTDIR}/mono" + fi -populate_bundle "OpenRA - Tiberian Dawn.app" "cnc" "Tiberian Dawn" "699223250181292033" -delete_mods "OpenRA - Tiberian Dawn.app" "ra" "d2k" -sign_bundle "OpenRA - Tiberian Dawn.app" + echo "Building core files" -populate_bundle "OpenRA - Dune 2000.app" "d2k" "Dune 2000" "712711732770111550" -delete_mods "OpenRA - Dune 2000.app" "ra" "cnc" -sign_bundle "OpenRA - Dune 2000.app" + pushd "${SRCDIR}" > /dev/null || exit 1 + make clean + make core TARGETPLATFORM=osx-x64 + make version VERSION="${TAG}" + make install-core gameinstalldir="/Contents/Resources/" DESTDIR="${BUILTDIR}/OpenRA.app" + make install-dependencies TARGETPLATFORM=osx-x64 gameinstalldir="/Contents/Resources/" DESTDIR="${BUILTDIR}/OpenRA.app" + popd > /dev/null || exit 1 -rm -rf "${BUILTDIR}/OpenRA.app" + populate_bundle "OpenRA - Red Alert.app" "ra" "Red Alert" "699222659766026240" + delete_mods "OpenRA - Red Alert.app" "cnc" "d2k" + sign_bundle "OpenRA - Red Alert.app" -if [ -n "${MACOS_DEVELOPER_CERTIFICATE_BASE64}" ] && [ -n "${MACOS_DEVELOPER_CERTIFICATE_PASSWORD}" ] && [ -n "${MACOS_DEVELOPER_IDENTITY}" ]; then - security delete-keychain build.keychain -fi + populate_bundle "OpenRA - Tiberian Dawn.app" "cnc" "Tiberian Dawn" "699223250181292033" + delete_mods "OpenRA - Tiberian Dawn.app" "ra" "d2k" + sign_bundle "OpenRA - Tiberian Dawn.app" -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 + populate_bundle "OpenRA - Dune 2000.app" "d2k" "Dune 2000" "712711732770111550" + delete_mods "OpenRA - Dune 2000.app" "ra" "cnc" + sign_bundle "OpenRA - Dune 2000.app" -# 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" + rm -rf "${BUILTDIR}/OpenRA.app" -cp "${BUILTDIR}/OpenRA - Red Alert.app/Contents/Resources/ra.icns" "/Volumes/OpenRA/.VolumeIcon.icns" + 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 -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 + # 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" -# 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" + cp "${BUILTDIR}/OpenRA - Red Alert.app/Contents/Resources/ra.icns" "/Volumes/OpenRA/.VolumeIcon.icns" -chmod -Rf go-w /Volumes/OpenRA -sync -sync + 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 -hdiutil detach "${DMG_DEVICE}" + # 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" -# Submit for notarization -if [ -n "${MACOS_DEVELOPER_USERNAME}" ] && [ -n "${MACOS_DEVELOPER_PASSWORD}" ]; then - echo "Submitting disk image for notarization" + chmod -Rf go-w /Volumes/OpenRA + sync + sync + + hdiutil detach "${DMG_DEVICE}" + rm -rf "${BUILTDIR}" +} + +notarize_package() { + DMG_PATH="${1}" + NOTARIZE_DMG_PATH="${DMG_PATH%.*}"-notarization.dmg + echo "Submitting ${PACKAGE_NAME} 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 build.dmg -format UDZO -imagekey zlib-level=9 -ov -o notarization.dmg + hdiutil convert "${DMG_PATH}" -format UDZO -imagekey zlib-level=9 -ov -o "${NOTARIZE_DMG_PATH}" - NOTARIZATION_UUID=$(xcrun altool --notarize-app --primary-bundle-id "net.openra.packaging" -u "${MACOS_DEVELOPER_USERNAME}" -p "${MACOS_DEVELOPER_PASSWORD}" --file notarization.dmg 2>&1 | awk -F' = ' '/RequestUUID/ { print $2; exit }') + NOTARIZATION_UUID=$(xcrun altool --notarize-app --primary-bundle-id "net.openra.packaging" -u "${MACOS_DEVELOPER_USERNAME}" -p "${MACOS_DEVELOPER_PASSWORD}" --file "${NOTARIZE_DMG_PATH}" 2>&1 | awk -F' = ' '/RequestUUID/ { print $2; exit }') if [ -z "${NOTARIZATION_UUID}" ]; then echo "Submission failed" exit 1 fi - echo "Submission UUID is ${NOTARIZATION_UUID}" - rm notarization.dmg + echo "${DMG_PATH} submission UUID is ${NOTARIZATION_UUID}" + rm "${NOTARIZE_DMG_PATH}" while :; do sleep 30 NOTARIZATION_RESULT=$(xcrun altool --notarization-info "${NOTARIZATION_UUID}" -u "${MACOS_DEVELOPER_USERNAME}" -p "${MACOS_DEVELOPER_PASSWORD}" 2>&1 | awk -F': ' '/Status/ { print $2; exit }') - echo "Submission status: ${NOTARIZATION_RESULT}" + echo "${DMG_PATH}: ${NOTARIZATION_RESULT}" if [ "${NOTARIZATION_RESULT}" == "invalid" ]; then NOTARIZATION_LOG_URL=$(xcrun altool --notarization-info "${NOTARIZATION_UUID}" -u "${MACOS_DEVELOPER_USERNAME}" -p "${MACOS_DEVELOPER_PASSWORD}" 2>&1 | awk -F': ' '/LogFileURL/ { print $2; exit }') - echo "Notarization failed with error:" + echo "${NOTARIZATION_UUID} failed notarization with error:" curl -s "${NOTARIZATION_LOG_URL}" -w "\n" exit 1 fi if [ "${NOTARIZATION_RESULT}" == "success" ]; then - echo "Stapling notarization tickets" - DMG_DEVICE=$(hdiutil attach -readwrite -noverify -noautoopen "build.dmg" | egrep '^/dev/' | sed 1q | awk '{print $1}') + echo "${DMG_PATH}: Stapling tickets" + DMG_DEVICE=$(hdiutil attach -readwrite -noverify -noautoopen "${DMG_PATH}" | egrep '^/dev/' | sed 1q | awk '{print $1}') sleep 2 xcrun stapler staple "/Volumes/OpenRA/OpenRA - Red Alert.app" @@ -226,9 +248,29 @@ if [ -n "${MACOS_DEVELOPER_USERNAME}" ] && [ -n "${MACOS_DEVELOPER_PASSWORD}" ]; break fi done +} + +finalize_package() { + INPUT_PATH="${1}" + OUTPUT_PATH="${2}" + + hdiutil convert "${INPUT_PATH}" -format UDZO -imagekey zlib-level=9 -ov -o "${OUTPUT_PATH}" + rm "${INPUT_PATH}" +} + +build_platform "standard" "build.dmg" +build_platform "compat" "build-compat.dmg" + +if [ -n "${MACOS_DEVELOPER_CERTIFICATE_BASE64}" ] && [ -n "${MACOS_DEVELOPER_CERTIFICATE_PASSWORD}" ] && [ -n "${MACOS_DEVELOPER_IDENTITY}" ]; then + security delete-keychain build.keychain fi -hdiutil convert build.dmg -format UDZO -imagekey zlib-level=9 -ov -o "${OUTPUTDIR}/OpenRA-${TAG}.dmg" +if [ -n "${MACOS_DEVELOPER_USERNAME}" ] && [ -n "${MACOS_DEVELOPER_PASSWORD}" ]; then + # Parallelize processing + (notarize_package "build.dmg") & + (notarize_package "build-compat.dmg") & + wait +fi -# Clean up -rm -rf "${BUILTDIR}" build.dmg +finalize_package "build.dmg" "${OUTPUTDIR}/OpenRA-${TAG}.dmg" +finalize_package "build-compat.dmg" "${OUTPUTDIR}/OpenRA-${TAG}-compat.dmg" diff --git a/packaging/macos/launcher-mono.m b/packaging/macos/launcher-mono.m new file mode 100644 index 0000000000..de3d7ab421 --- /dev/null +++ b/packaging/macos/launcher-mono.m @@ -0,0 +1,372 @@ +/* + * Copyright 2007-2020 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_main _mono_main = (mono_main)dlsym(libmono, "mono_main"); + if (!_mono_main) + { + fprintf(stderr, "Could not load mono_main(): %s\n", dlerror()); + return FALSE; + } + + mono_free _mono_free = (mono_free)dlsym(libmono, "mono_free"); + if (!_mono_free) + { + fprintf(stderr, "Could not load mono_free(): %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.Game.exe"; + 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 *launchPath = [SYSTEM_MONO_PATH stringByAppendingPathComponent: @"Commands/mono"]; + NSString *appPath = [exePath stringByAppendingPathComponent: @"OpenRA"]; + NSString *engineLaunchPath = [self resolveTranslocatedPath: appPath]; + + NSMutableArray *launchArgs = [NSMutableArray arrayWithCapacity: [gameArgs count] + 2]; + [launchArgs addObject: @"--debug"]; + [launchArgs addObject: [gamePath stringByAppendingPathComponent: gameName]]; + [launchArgs addObject: [NSString stringWithFormat:@"Engine.LaunchPath=\"%@\"", engineLaunchPath]]; + + 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: launchPath]; + [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]; + 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 new file mode 100644 index 0000000000..47f89e9416 --- /dev/null +++ b/packaging/macos/launcher.m @@ -0,0 +1,277 @@ +/* + * Copyright 2007-2020 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 + +@interface OpenRALauncher : NSObject +- (void)launchGameWithArgs: (NSArray *)gameArgs; +@end + +@implementation OpenRALauncher + +BOOL launched = NO; +NSTask *gameTask; + +- (NSString *)modName +{ + NSDictionary *plist = [[NSBundle mainBundle] infoDictionary]; + if (plist) + { + NSString *title = [plist objectForKey:@"CFBundleDisplayName"]; + if (title && [title length] > 0) + return title; + } + + return @"OpenRA"; +} + +- (void)showCrashPrompt +{ + 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]]; + } + } +} + +// 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; + + // Default values - can be overriden by setting certain keys Info.plist + NSString *gameName = @"OpenRA.Game.exe"; + 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 *launchPath = [exePath stringByAppendingPathComponent: @"mono"]; + NSString *appPath = [exePath stringByAppendingPathComponent: @"OpenRA"]; + NSString *engineLaunchPath = [self resolveTranslocatedPath: appPath]; + + NSMutableArray *launchArgs = [NSMutableArray arrayWithCapacity: [gameArgs count] + 2]; + [launchArgs addObject: @"--debug"]; + [launchArgs addObject: [gamePath stringByAppendingPathComponent: gameName]]; + [launchArgs addObject: [NSString stringWithFormat:@"Engine.LaunchPath=\"%@\"", engineLaunchPath]]; + + if (modId) + [launchArgs addObject: [NSString stringWithFormat:@"Game.Mod=%@", modId]]; + + [launchArgs addObjectsFromArray: gameArgs]; + + NSLog(@"Running mono with arguments:"); + for (size_t i = 0; i < [launchArgs count]; i++) + NSLog(@"%@", [launchArgs objectAtIndex: i]); + + gameTask = [[NSTask alloc] init]; + [gameTask setCurrentDirectoryPath: gamePath]; + [gameTask setLaunchPath: launchPath]; + [gameTask setArguments: launchArgs]; + + NSMutableDictionary *environment = [NSMutableDictionary dictionaryWithDictionary: [[NSProcessInfo processInfo] environment]]; + [environment setObject: [gamePath stringByAppendingPathComponent: @"lib/mono/4.5"] forKey: @"MONO_PATH"]; + [environment setObject: [gamePath stringByAppendingPathComponent: @"etc"] forKey: @"MONO_CFG_DIR"]; + [environment setObject: [gamePath stringByAppendingPathComponent: @"etc/mono/config"] forKey: @"MONO_CONFIG"]; + [gameTask setEnvironment: environment]; + + [[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) + exit(0); + + // Make the error dialog visible + [NSApp setActivationPolicy: NSApplicationActivationPolicyRegular]; + [[NSApplication sharedApplication] activateIgnoringOtherApps:YES]; + [self showCrashPrompt]; + + exit(1); +} + +@end + +int main(int argc, char **argv) +{ + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + NSApplication *application = [NSApplication sharedApplication]; + OpenRALauncher *launcher = [[OpenRALauncher alloc] init]; + [NSApp setActivationPolicy: NSApplicationActivationPolicyProhibited]; + + [application setDelegate:launcher]; + [application run]; + + [launcher release]; + [pool drain]; + + return EXIT_SUCCESS; +}