Initial commit: OpenRA game engine
Fork from OpenRA/OpenRA with one-click launch script (start-ra.cmd) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
56
packaging/macos/Info.plist.in
Normal file
56
packaging/macos/Info.plist.in
Normal file
@@ -0,0 +1,56 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>English</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>{MOD_NAME}</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>Launcher</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>{MOD_ID}.icns</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>net.openra.mod.{MOD_ID}</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>{MOD_NAME}</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>{DEV_VERSION}</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>{DEV_VERSION}</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>{MINIMUM_SYSTEM_VERSION}</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.games</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>OpenRA Server</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>{JOIN_SERVER_URL_SCHEME}</string>
|
||||
<string>{DISCORD_URL_SCHEME}</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>ModId</key>
|
||||
<string>{MOD_ID}</string>
|
||||
<key>FaqUrl</key>
|
||||
<string>{FAQ_URL}</string>
|
||||
<key>JoinServerUrlScheme</key>
|
||||
<string>{JOIN_SERVER_URL_SCHEME}</string>
|
||||
<key>NSRequiresAquaSystemAppearance</key>
|
||||
<false/>
|
||||
<key>NSPrefersDisplaySafeAreaCompatibilityMode</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
84
packaging/macos/apphost.c
Normal file
84
packaging/macos/apphost.c
Normal file
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright (c) The OpenRA Developers and Contributors
|
||||
* 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 8 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],
|
||||
¶ms,
|
||||
&host_context_handle);
|
||||
|
||||
hostfxr_run_app(host_context_handle);
|
||||
|
||||
return hostfxr_close(host_context_handle);
|
||||
}
|
||||
264
packaging/macos/buildpackage.sh
Normal file
264
packaging/macos/buildpackage.sh
Normal file
@@ -0,0 +1,264 @@
|
||||
#!/bin/bash
|
||||
# OpenRA packaging script for macOS
|
||||
#
|
||||
# The application bundles will be signed if the following environment variables are defined:
|
||||
# MACOS_DEVELOPER_IDENTITY: The alphanumeric identifier listed in the certificate name ("Developer ID Application: <your name> (<identity>)")
|
||||
# or as Team ID in your Apple Developer account Membership Details.
|
||||
# If the identity is not already in the default keychain, specify the following environment variables to import it:
|
||||
# MACOS_DEVELOPER_CERTIFICATE_BASE64: base64 content of the exported .p12 developer ID certificate.
|
||||
# Generate using `base64 certificate.p12 | pbcopy`
|
||||
# MACOS_DEVELOPER_CERTIFICATE_PASSWORD: password to unlock the MACOS_DEVELOPER_CERTIFICATE_BASE64 certificate
|
||||
#
|
||||
# The application bundles will be notarized if the following environment variables are defined:
|
||||
# MACOS_DEVELOPER_USERNAME: Email address for the developer account
|
||||
# MACOS_DEVELOPER_PASSWORD: App-specific password for the developer account
|
||||
#
|
||||
|
||||
set -o errexit -o pipefail || exit $?
|
||||
|
||||
if [[ "${OSTYPE}" != "darwin"* ]]; then
|
||||
echo >&2 "macOS packaging requires a macOS host"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
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
|
||||
|
||||
# Set the working dir to the location of this script
|
||||
HERE=$(dirname "${0}")
|
||||
cd "${HERE}"
|
||||
. ../functions.sh
|
||||
|
||||
# Import code signing certificate
|
||||
if [ -n "${MACOS_DEVELOPER_CERTIFICATE_BASE64}" ] && [ -n "${MACOS_DEVELOPER_CERTIFICATE_PASSWORD}" ] && [ -n "${MACOS_DEVELOPER_IDENTITY}" ]; then
|
||||
echo "Importing signing certificate"
|
||||
echo "${MACOS_DEVELOPER_CERTIFICATE_BASE64}" | base64 --decode > build.p12
|
||||
security create-keychain -p build build.keychain
|
||||
security default-keychain -s build.keychain
|
||||
security unlock-keychain -p build build.keychain
|
||||
security import build.p12 -k build.keychain -P "${MACOS_DEVELOPER_CERTIFICATE_PASSWORD}" -T /usr/bin/codesign >/dev/null 2>&1
|
||||
security set-key-partition-list -S apple-tool:,apple: -s -k build build.keychain >/dev/null 2>&1
|
||||
rm -fr build.p12
|
||||
fi
|
||||
|
||||
TAG="${1}"
|
||||
OUTPUTDIR="${2}"
|
||||
|
||||
SRCDIR="$(pwd)/../.."
|
||||
BUILTDIR="$(pwd)/build"
|
||||
ARTWORK_DIR="$(pwd)/../artwork/"
|
||||
|
||||
modify_plist() {
|
||||
sed "s|${1}|${2}|g" "${3}" > "${3}.tmp" && mv "${3}.tmp" "${3}"
|
||||
}
|
||||
|
||||
# Copies the game files and sets metadata
|
||||
build_app() {
|
||||
TEMPLATE_DIR="${1}"
|
||||
LAUNCHER_DIR="${2}"
|
||||
MOD_ID="${3}"
|
||||
MOD_NAME="${4}"
|
||||
DISCORD_APPID="${5}"
|
||||
|
||||
LAUNCHER_CONTENTS_DIR="${LAUNCHER_DIR}/Contents"
|
||||
LAUNCHER_RESOURCES_DIR="${LAUNCHER_CONTENTS_DIR}/Resources"
|
||||
|
||||
cp -r "${TEMPLATE_DIR}" "${LAUNCHER_DIR}"
|
||||
|
||||
IS_D2K="False"
|
||||
if [ "${MOD_ID}" = "d2k" ]; then
|
||||
IS_D2K="True"
|
||||
fi
|
||||
|
||||
# Install engine and mod files
|
||||
install_assemblies "${SRCDIR}" "${LAUNCHER_CONTENTS_DIR}/MacOS/x86_64" "osx-x64" "True" "True" "${IS_D2K}"
|
||||
install_assemblies "${SRCDIR}" "${LAUNCHER_CONTENTS_DIR}/MacOS/arm64" "osx-arm64" "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/${MOD_ID}-content/mod.yaml"
|
||||
|
||||
# Assemble multi-resolution icon
|
||||
mkdir "${MOD_ID}.iconset"
|
||||
cp "${ARTWORK_DIR}/${MOD_ID}_16x16.png" "${MOD_ID}.iconset/icon_16x16.png"
|
||||
cp "${ARTWORK_DIR}/${MOD_ID}_32x32.png" "${MOD_ID}.iconset/icon_16x16@2.png"
|
||||
cp "${ARTWORK_DIR}/${MOD_ID}_32x32.png" "${MOD_ID}.iconset/icon_32x32.png"
|
||||
cp "${ARTWORK_DIR}/${MOD_ID}_64x64.png" "${MOD_ID}.iconset/icon_32x32@2x.png"
|
||||
cp "${ARTWORK_DIR}/${MOD_ID}_128x128.png" "${MOD_ID}.iconset/icon_128x128.png"
|
||||
cp "${ARTWORK_DIR}/${MOD_ID}_256x256.png" "${MOD_ID}.iconset/icon_128x128@2x.png"
|
||||
cp "${ARTWORK_DIR}/${MOD_ID}_256x256.png" "${MOD_ID}.iconset/icon_256x256.png"
|
||||
cp "${ARTWORK_DIR}/${MOD_ID}_512x512.png" "${MOD_ID}.iconset/icon_256x256@2x.png"
|
||||
cp "${ARTWORK_DIR}/${MOD_ID}_1024x1024.png" "${MOD_ID}.iconset/icon_512x512@2x.png"
|
||||
iconutil --convert icns "${MOD_ID}.iconset" -o "${LAUNCHER_RESOURCES_DIR}/${MOD_ID}.icns"
|
||||
rm -rf "${MOD_ID}.iconset"
|
||||
|
||||
# Set launcher metadata
|
||||
modify_plist "{MOD_ID}" "${MOD_ID}" "${LAUNCHER_CONTENTS_DIR}/Info.plist"
|
||||
modify_plist "{MOD_NAME}" "${MOD_NAME}" "${LAUNCHER_CONTENTS_DIR}/Info.plist"
|
||||
modify_plist "{JOIN_SERVER_URL_SCHEME}" "openra-${MOD_ID}-${TAG}" "${LAUNCHER_CONTENTS_DIR}/Info.plist"
|
||||
modify_plist "{DISCORD_URL_SCHEME}" "discord-${DISCORD_APPID}" "${LAUNCHER_CONTENTS_DIR}/Info.plist"
|
||||
|
||||
# Sign binaries with developer certificate
|
||||
if [ -n "${MACOS_DEVELOPER_IDENTITY}" ]; then
|
||||
codesign --sign "${MACOS_DEVELOPER_IDENTITY}" --timestamp --options runtime -f --entitlements entitlements.plist --deep "${LAUNCHER_DIR}"
|
||||
fi
|
||||
}
|
||||
|
||||
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/x86_64"
|
||||
mkdir -p "${TEMPLATE_DIR}/Contents/MacOS/arm64"
|
||||
|
||||
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}" "https://wiki.openra.net/FAQ" "${TEMPLATE_DIR}/Contents/Info.plist"
|
||||
modify_plist "{MINIMUM_SYSTEM_VERSION}" "10.15" "${TEMPLATE_DIR}/Contents/Info.plist"
|
||||
|
||||
# Compile universal (x86_64 + arm64) 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
|
||||
|
||||
# Compile universal (x86_64 + arm64) Launcher
|
||||
clang launcher.m -o "${TEMPLATE_DIR}/Contents/MacOS/Launcher-x86_64" -framework AppKit -target x86_64-apple-macos10.15
|
||||
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"
|
||||
|
||||
# Compile universal (x86_64 + arm64) Utility
|
||||
clang utility.m -o "${TEMPLATE_DIR}/Contents/MacOS/Utility-x86_64" -framework AppKit -target x86_64-apple-macos10.15
|
||||
clang utility.m -o "${TEMPLATE_DIR}/Contents/MacOS/Utility-arm64" -framework AppKit -target arm64-apple-macos10.15
|
||||
lipo -create -output "${TEMPLATE_DIR}/Contents/MacOS/Utility" "${TEMPLATE_DIR}/Contents/MacOS/Utility-x86_64" "${TEMPLATE_DIR}/Contents/MacOS/Utility-arm64"
|
||||
rm "${TEMPLATE_DIR}/Contents/MacOS/Utility-x86_64" "${TEMPLATE_DIR}/Contents/MacOS/Utility-arm64"
|
||||
|
||||
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"
|
||||
|
||||
rm -rf "${TEMPLATE_DIR}"
|
||||
|
||||
echo "Packaging disk image"
|
||||
if hdiutil info | grep -q "/Volumes/OpenRA"; then
|
||||
echo "Some process is stealing our resources! /Volumes/OpenRA is already mounted!"
|
||||
fi
|
||||
|
||||
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
|
||||
|
||||
# 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"
|
||||
|
||||
cp "${BUILTDIR}/OpenRA - Red Alert.app/Contents/Resources/ra.icns" "/Volumes/OpenRA/.VolumeIcon.icns"
|
||||
|
||||
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
|
||||
|
||||
# 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"; 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 f in "/Volumes/OpenRA/OpenRA - ${MOD}.app/Contents/MacOS/x86_64"/*; do
|
||||
g="/Volumes/OpenRA/OpenRA - ${MOD}.app/Contents/MacOS/arm64/"$(basename "${f}")
|
||||
if [ -e "${g}" ]; then
|
||||
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
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
chmod -Rf go-w /Volumes/OpenRA
|
||||
sync
|
||||
sync
|
||||
|
||||
hdiutil detach "${DMG_DEVICE}"
|
||||
rm -rf "${BUILTDIR}"
|
||||
|
||||
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 "build.dmg" -format ULFO -ov -o "build-notarization.dmg"
|
||||
|
||||
xcrun notarytool submit "build-notarization.dmg" --wait --apple-id "${MACOS_DEVELOPER_USERNAME}" --password "${MACOS_DEVELOPER_PASSWORD}" --team-id "${MACOS_DEVELOPER_IDENTITY}"
|
||||
|
||||
rm "build-notarization.dmg"
|
||||
|
||||
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"
|
||||
xcrun stapler staple "/Volumes/OpenRA/OpenRA - Tiberian Dawn.app"
|
||||
xcrun stapler staple "/Volumes/OpenRA/OpenRA - Dune 2000.app"
|
||||
|
||||
sync
|
||||
sync
|
||||
|
||||
hdiutil detach "${DMG_DEVICE}"
|
||||
fi
|
||||
|
||||
hdiutil convert "build.dmg" -format ULFO -ov -o "${OUTPUTDIR}/OpenRA-${TAG}.dmg"
|
||||
rm "build.dmg"
|
||||
16
packaging/macos/entitlements.plist
Normal file
16
packaging/macos/entitlements.plist
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.disable-executable-page-protection</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.disable-library-validation</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
292
packaging/macos/launcher.m
Normal file
292
packaging/macos/launcher.m
Normal file
@@ -0,0 +1,292 @@
|
||||
/*
|
||||
* Copyright (c) The OpenRA Developers and Contributors
|
||||
* 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>
|
||||
#include <sys/types.h>
|
||||
#include <sys/sysctl.h>
|
||||
#include <mach/machine.h>
|
||||
|
||||
#define DOTNET_MIN_MACOS_VERSION 10.15
|
||||
|
||||
@interface OpenRALauncher : NSObject <NSApplicationDelegate>
|
||||
- (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)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;
|
||||
|
||||
// Default values - can be overriden by setting certain keys Info.plist
|
||||
NSString *modId = nil;
|
||||
|
||||
NSDictionary *plist = [[NSBundle mainBundle] infoDictionary];
|
||||
if (plist)
|
||||
{
|
||||
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;
|
||||
NSString *dllPath;
|
||||
NSString *hostPath;
|
||||
|
||||
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] + 5];
|
||||
[launchArgs addObject: hostPath];
|
||||
[launchArgs addObject: dllPath];
|
||||
[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 OpenRA 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;
|
||||
}
|
||||
145
packaging/macos/utility.m
Normal file
145
packaging/macos/utility.m
Normal file
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* Copyright (c) The OpenRA Developers and Contributors
|
||||
* 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>
|
||||
#include <libgen.h>
|
||||
#include <stdio.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/sysctl.h>
|
||||
#include <sys/resource.h>
|
||||
#include <mach/machine.h>
|
||||
|
||||
#define DOTNET_MIN_MACOS_VERSION 10.15
|
||||
|
||||
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 launch_dotnet(int argc, char **argv, char *modId, bool isArmArchitecture)
|
||||
{
|
||||
NSString *exePath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent: @"Contents/MacOS/"];
|
||||
NSString *dllPath;
|
||||
NSString *hostPath;
|
||||
|
||||
if (isArmArchitecture)
|
||||
{
|
||||
hostPath = [exePath stringByAppendingPathComponent: @"arm64/libhostfxr.dylib"];;
|
||||
dllPath = [exePath stringByAppendingPathComponent: @"arm64/OpenRA.Utility.dll"];
|
||||
}
|
||||
else
|
||||
{
|
||||
hostPath = [exePath stringByAppendingPathComponent: @"x86_64/libhostfxr.dylib"];;
|
||||
dllPath = [exePath stringByAppendingPathComponent: @"x86_64/OpenRA.Utility.dll"];
|
||||
}
|
||||
|
||||
void *lib = dlopen([hostPath UTF8String], RTLD_LAZY);
|
||||
if (!lib)
|
||||
{
|
||||
NSLog(@"Failed to load %@: %s\n", hostPath, 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)
|
||||
{
|
||||
NSLog(@"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)
|
||||
{
|
||||
NSLog(@"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)
|
||||
{
|
||||
NSLog(@"Could not load hostfxr_close(): %s\n", dlerror());
|
||||
return 1;
|
||||
}
|
||||
|
||||
struct hostfxr_initialize_parameters params;
|
||||
params.size = sizeof(params);
|
||||
params.host_path = (char*)[[exePath stringByAppendingPathComponent: @"Utility"] UTF8String];
|
||||
params.dotnet_root = dirname((char*)[hostPath UTF8String]);
|
||||
|
||||
// Insert dll and modId as arguments. Overwrite the first argument which was used to launch this application.
|
||||
char **newv = malloc((argc + 1) * sizeof(char*));
|
||||
if (!newv)
|
||||
{
|
||||
NSLog(@"Failed to allocate memory for args array.\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
newv[0] = (char*)[dllPath UTF8String];
|
||||
newv[1] = modId;
|
||||
for (int i = 1; i < argc; i++)
|
||||
newv[i + 1] = argv[i];
|
||||
|
||||
hostfxr_handle host_context_handle;
|
||||
hostfxr_initialize_for_dotnet_command_line(
|
||||
argc + 1,
|
||||
newv,
|
||||
¶ms,
|
||||
&host_context_handle);
|
||||
|
||||
hostfxr_run_app(host_context_handle);
|
||||
|
||||
int ret = hostfxr_close(host_context_handle);
|
||||
free(newv);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
|
||||
|
||||
size_t size;
|
||||
cpu_type_t type;
|
||||
size = sizeof(type);
|
||||
bool isArmArchitecture = sysctlbyname("hw.cputype", &type, &size, NULL, 0) == 0 && (type & 0xFF) == CPU_TYPE_ARM;
|
||||
|
||||
NSDictionary *plist = [[NSBundle mainBundle] infoDictionary];
|
||||
char *modId;
|
||||
if (plist)
|
||||
{
|
||||
NSString *modIdValue = [plist objectForKey:@"ModId"];
|
||||
if (modIdValue && [modIdValue length] > 0)
|
||||
modId = (char*)[modIdValue UTF8String];
|
||||
}
|
||||
|
||||
if (!modId)
|
||||
{
|
||||
NSLog(@"Could not detect ModId\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
setenv("ENGINE_DIR", (char*)[[[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent: @"Contents/Resources/"] UTF8String], 1);
|
||||
|
||||
int ret = launch_dotnet(argc, argv, modId, isArmArchitecture);
|
||||
|
||||
[pool release];
|
||||
return ret;
|
||||
}
|
||||
Reference in New Issue
Block a user