Skip to content

Commit 358d5c8

Browse files
Add support for macOS
This PR adds support for OpenLauncher on macOS. Currently the launcher builds and runs on macOS, but can't download new releases or launch the game. - OpenLauncher now uses the correct binary name on macOS so that it can launch the executable properly. - On all platforms, OpenLauncher currently uses `ZipFile.ExtractToDirectory` to extract the release once it's been downloaded. However, this method will not preserve file aliases on macOS, and since `OpenRCT2.app` contains several of them, the extracted application won't launch. To fix this, this PR calls`/usr/bin/ditto` in a subprocess to extract the `.zip`, which will preserve the files properly. - Since OpenLoco isn't currently supported on macOS, it doesn't appear in the launcher. - `.gitignore` is updated to ignore `.ds_store` and other hidden files that macOS likes to make Co-authored-by: Michael Steenbeek <[email protected]>
1 parent 3efbe0d commit 358d5c8

File tree

7 files changed

+187
-3
lines changed

7 files changed

+187
-3
lines changed

.github/workflows/ci.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ jobs:
1111
rid: linux-x64
1212
- os: windows
1313
rid: win-x64
14+
- os: macos
15+
rid: macos-universal
1416

1517
runs-on: ${{ matrix.os }}-latest
1618
steps:
@@ -23,12 +25,21 @@ jobs:
2325
- name: Restore
2426
run: dotnet restore
2527
- name: Build
28+
if: runner.os != 'macOS'
2629
run: dotnet build -c Release --no-restore
2730
- name: Test
31+
if: runner.os != 'macOS'
2832
run: dotnet test --no-restore
2933
- name: Publish
3034
working-directory: src/openlauncher
35+
if: runner.os != 'macOS'
3136
run: dotnet publish -c Release -r ${{ matrix.rid }} --self-contained
37+
- name: Build (macOS)
38+
if: runner.os == 'macOS'
39+
run: ./build-mac.sh
40+
- name: Package (macOS)
41+
if: runner.os == 'macOS'
42+
run: ./package-mac.sh
3243
- name: Upload artifacts
3344
uses: actions/upload-artifact@v4
3445
with:
@@ -108,4 +119,5 @@ jobs:
108119
artifacts/openlauncher*.exe
109120
artifacts/openlauncher*.rpm
110121
artifacts/openlauncher*.deb
122+
artifacts/OpenLauncher*.zip
111123

.gitignore

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,3 +396,40 @@ FodyWeavers.xsd
396396

397397
# JetBrains Rider
398398
*.sln.iml
399+
# Created by https://www.toptal.com/developers/gitignore/api/macos
400+
# Edit at https://www.toptal.com/developers/gitignore?templates=macos
401+
402+
### macOS ###
403+
# General
404+
.DS_Store
405+
.AppleDouble
406+
.LSOverride
407+
408+
# Icon must end with two \r
409+
Icon
410+
411+
# Thumbnails
412+
._*
413+
414+
# Files that might appear in the root of a volume
415+
.DocumentRevisions-V100
416+
.fseventsd
417+
.Spotlight-V100
418+
.TemporaryItems
419+
.Trashes
420+
.VolumeIcon.icns
421+
.com.apple.timemachine.donotpresent
422+
423+
# Directories potentially created on remote AFP share
424+
.AppleDB
425+
.AppleDesktop
426+
Network Trash Folder
427+
Temporary Items
428+
.apdisk
429+
430+
### macOS Patch ###
431+
# iCloud generated files
432+
*.icloud
433+
434+
# End of https://www.toptal.com/developers/gitignore/api/macos
435+

build-mac.sh

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#!/bin/bash
2+
#Modified from Avalonia's documentation: https://docs.avaloniaui.net/docs/0.10.x/distribution-publishing/macos
3+
4+
set -e
5+
trap 'echo "Error packaging app"; exit 1' ERR
6+
7+
PROJECT_NAME="openlauncher"
8+
OUTPUT_DIR="./src/openlauncher/bin/Release/net8.0"
9+
FINAL_OUTPUT_DIR="./src/openlauncher/bin/Release/net8.0/macos-universal"
10+
11+
# Function to build for a specific architecture
12+
build_for_arch() {
13+
local arch=$1
14+
echo "Building for $arch..."
15+
dotnet publish -r osx-$arch -c Release
16+
}
17+
18+
# Build for both architectures
19+
build_for_arch "x64"
20+
build_for_arch "arm64"
21+
22+
# Create the final output directory
23+
mkdir -p "$FINAL_OUTPUT_DIR"
24+
25+
ARM_OUTPUT="$OUTPUT_DIR/osx-arm64/publish"
26+
X64_OUTPUT="$OUTPUT_DIR/osx-x64/publish"
27+
28+
#Copy libraries into final output dir
29+
for FILE in "$ARM_OUTPUT"/*; do
30+
BASENAME=$(basename "$FILE")
31+
if [ -f "$FILE" ] && [[ "$BASENAME" != "openlauncher" ]]; then
32+
cp "$FILE" "$FINAL_OUTPUT_DIR"/.
33+
fi
34+
done
35+
36+
# Create universal binary
37+
echo "Creating universal binary..."
38+
lipo -create \
39+
"$OUTPUT_DIR/osx-x64/publish/$PROJECT_NAME" \
40+
"$OUTPUT_DIR/osx-arm64/publish/$PROJECT_NAME" \
41+
-output "$FINAL_OUTPUT_DIR/$PROJECT_NAME"

info.plist

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>CFBundleIconFile</key>
6+
<string>AppIcon.icns</string>
7+
<key>CFBundleIdentifier</key>
8+
<string>io.openrct2.openlauncher</string>
9+
<key>CFBundleName</key>
10+
<string>OpenLauncher</string>
11+
<key>CFBundleVersion</key>
12+
<string>1.0.0</string>
13+
<key>LSMinimumSystemVersion</key>
14+
<string>10.15</string>
15+
<key>CFBundleExecutable</key>
16+
<string>openlauncher</string>
17+
<key>CFBundleInfoDictionaryVersion</key>
18+
<string>6.0</string>
19+
<key>CFBundlePackageType</key>
20+
<string>APPL</string>
21+
<key>CFBundleShortVersionString</key>
22+
<string>APP_VERSION_NUMBER</string>
23+
<key>NSHighResolutionCapable</key>
24+
<true/>
25+
</dict>
26+
</plist>

package-mac.sh

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
#!/bin/bash
2+
#Modified from Avalonia's documentation: https://docs.avaloniaui.net/docs/0.10.x/distribution-publishing/macos
3+
set -e
4+
trap 'echo "Error packaging app"; exit 1' ERR
5+
6+
APP_NAME="./src/openlauncher/bin/Release/net8.0/macos-universal/OpenLauncher.app"
7+
PUBLISH_OUTPUT_DIRECTORY="./src/openlauncher/bin/Release/net8.0/macos-universal/."
8+
9+
INFO_PLIST="./info.plist"
10+
11+
ICON_FILE="./src/openlauncher/resources/logo-mac.icns"
12+
13+
if [ -d "$APP_NAME" ]
14+
then
15+
rm -rf "$APP_NAME"
16+
fi
17+
18+
echo "Creating OpenLauncher.app"
19+
20+
rm -r -f "$APP_NAME"
21+
mkdir "$APP_NAME"
22+
23+
mkdir "$APP_NAME/Contents"
24+
mkdir "$APP_NAME/Contents/MacOS"
25+
mkdir "$APP_NAME/Contents/Resources"
26+
27+
cp "$INFO_PLIST" "$APP_NAME/Contents/Info.plist"
28+
cp "$ICON_FILE" "$APP_NAME/Contents/Resources/AppIcon.icns"
29+
for FILE in "$PUBLISH_OUTPUT_DIRECTORY"/*; do
30+
if [ -f "$FILE" ]; then
31+
cp -a "$FILE" "$APP_NAME/Contents/MacOS/."
32+
fi
33+
done
34+
35+
#Get version number from csproj
36+
VERSION_NUMBER=$(sed -n 's/.*<AssemblyVersion>\(.*\)<\/AssemblyVersion>.*/\1/p' ./src/openlauncher/openlauncher.csproj)
37+
#Replace placeholder version with real version number
38+
sed -i -e "s/APP_VERSION_NUMBER/$VERSION_NUMBER/" "$APP_NAME/Contents/Info.plist"
39+
#For whatever reason, sed on macOS creates a backup file when replacing text, so we need to remove it
40+
rm "$APP_NAME/Contents/Info.plist-e"
41+
42+
codesign --sign - --force --deep "./src/openlauncher/bin/Release/net8.0/macos-universal/OpenLauncher.app"
43+
44+
mkdir "$PUBLISH_OUTPUT_DIRECTORY/publish"
45+
46+
echo "Zipping OpenLauncher.app..."
47+
48+
ditto -c -k --keepParent "./src/openlauncher/bin/Release/net8.0/macos-universal/OpenLauncher.app" "./src/openlauncher/bin/Release/net8.0/macos-universal/publish/OpenLauncher.zip"

src/IntelOrca.OpenLauncher.Core/InstallService.cs

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,15 @@ public string ExecutablePath
2929
{
3030
get
3131
{
32-
var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
33-
var binaryName = isWindows ? $"{_game.BinaryName}.exe" : _game.BinaryName;
32+
string binaryName;
33+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
34+
binaryName = $"{_game.BinaryName}.exe";
35+
} else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) {
36+
// We need to use Name and not BinaryName since BinaryName isn't capitalized
37+
binaryName = $"{_game.Name}.app/Contents/MacOS/{_game.Name}";
38+
} else {
39+
binaryName = _game.BinaryName;
40+
}
3441
return Path.Combine(_game.BinPath, binaryName);
3542
}
3643
}
@@ -157,7 +164,11 @@ private void ExtractArchive(Shell shell, Uri uri, string archivePath, string out
157164
{
158165
if (uri.LocalPath.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))
159166
{
160-
ZipFile.ExtractToDirectory(archivePath, outDirectory, overwriteFiles: true);
167+
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) {
168+
ExtractArchiveMac(archivePath, outDirectory);
169+
} else {
170+
ZipFile.ExtractToDirectory(archivePath, outDirectory, overwriteFiles: true);
171+
}
161172
}
162173
else if (uri.LocalPath.EndsWith(".AppImage", StringComparison.OrdinalIgnoreCase))
163174
{
@@ -191,5 +202,13 @@ private void ExtractArchive(Shell shell, Uri uri, string archivePath, string out
191202
throw new Exception("Unknown file format to extract.");
192203
}
193204
}
205+
206+
private void ExtractArchiveMac(string archivePath, string outDirectory) {
207+
var dittoProcess = new Process();
208+
var args = $"-k -x \"{archivePath}\" \"{outDirectory}\"";
209+
dittoProcess.StartInfo = new ProcessStartInfo("/usr/bin/ditto", args);
210+
dittoProcess.Start();
211+
dittoProcess.WaitForExit();
212+
}
194213
}
195214
}

src/openlauncher/MainWindow.axaml.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using Avalonia.Controls;
99
using Avalonia.Interactivity;
1010
using Avalonia.Media.Imaging;
11+
using Avalonia.Platform;
1112
using Avalonia.Threading;
1213
using IntelOrca.OpenLauncher.Core;
1314
using StringResources = openlauncher.Properties.Resources;

0 commit comments

Comments
 (0)