diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ec0300f..b379653 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -11,6 +11,8 @@ jobs:
rid: linux-x64
- os: windows
rid: win-x64
+ - os: macos
+ rid: macos-universal
runs-on: ${{ matrix.os }}-latest
steps:
@@ -23,12 +25,21 @@ jobs:
- name: Restore
run: dotnet restore
- name: Build
+ if: runner.os != 'macOS'
run: dotnet build -c Release --no-restore
- name: Test
+ if: runner.os != 'macOS'
run: dotnet test --no-restore
- name: Publish
working-directory: src/openlauncher
+ if: runner.os != 'macOS'
run: dotnet publish -c Release -r ${{ matrix.rid }} --self-contained
+ - name: Build (macOS)
+ if: runner.os == 'macOS'
+ run: ./build-mac.sh
+ - name: Package (macOS)
+ if: runner.os == 'macOS'
+ run: ./package-mac.sh
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
@@ -108,4 +119,5 @@ jobs:
artifacts/openlauncher*.exe
artifacts/openlauncher*.rpm
artifacts/openlauncher*.deb
+ artifacts/OpenLauncher*.zip
diff --git a/.gitignore b/.gitignore
index 8a30d25..ee3585b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -396,3 +396,40 @@ FodyWeavers.xsd
# JetBrains Rider
*.sln.iml
+# Created by https://www.toptal.com/developers/gitignore/api/macos
+# Edit at https://www.toptal.com/developers/gitignore?templates=macos
+
+### macOS ###
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+### macOS Patch ###
+# iCloud generated files
+*.icloud
+
+# End of https://www.toptal.com/developers/gitignore/api/macos
+
diff --git a/build-mac.sh b/build-mac.sh
new file mode 100755
index 0000000..a93ddd4
--- /dev/null
+++ b/build-mac.sh
@@ -0,0 +1,41 @@
+#!/bin/bash
+#Modified from Avalonia's documentation: https://docs.avaloniaui.net/docs/0.10.x/distribution-publishing/macos
+
+set -e
+trap 'echo "Error packaging app"; exit 1' ERR
+
+PROJECT_NAME="openlauncher"
+OUTPUT_DIR="./src/openlauncher/bin/Release/net8.0"
+FINAL_OUTPUT_DIR="./src/openlauncher/bin/Release/net8.0/macos-universal"
+
+# Function to build for a specific architecture
+build_for_arch() {
+ local arch=$1
+ echo "Building for $arch..."
+ dotnet publish -r osx-$arch -c Release
+}
+
+# Build for both architectures
+build_for_arch "x64"
+build_for_arch "arm64"
+
+# Create the final output directory
+mkdir -p "$FINAL_OUTPUT_DIR"
+
+ARM_OUTPUT="$OUTPUT_DIR/osx-arm64/publish"
+X64_OUTPUT="$OUTPUT_DIR/osx-x64/publish"
+
+#Copy libraries into final output dir
+for FILE in "$ARM_OUTPUT"/*; do
+ BASENAME=$(basename "$FILE")
+ if [ -f "$FILE" ] && [[ "$BASENAME" != "openlauncher" ]]; then
+ cp "$FILE" "$FINAL_OUTPUT_DIR"/.
+ fi
+done
+
+# Create universal binary
+echo "Creating universal binary..."
+lipo -create \
+ "$OUTPUT_DIR/osx-x64/publish/$PROJECT_NAME" \
+ "$OUTPUT_DIR/osx-arm64/publish/$PROJECT_NAME" \
+ -output "$FINAL_OUTPUT_DIR/$PROJECT_NAME"
diff --git a/info.plist b/info.plist
new file mode 100644
index 0000000..2653b37
--- /dev/null
+++ b/info.plist
@@ -0,0 +1,26 @@
+
+
+
+
+ CFBundleIconFile
+ AppIcon.icns
+ CFBundleIdentifier
+ io.openrct2.openlauncher
+ CFBundleName
+ OpenLauncher
+ CFBundleVersion
+ 1.0.0
+ LSMinimumSystemVersion
+ 10.15
+ CFBundleExecutable
+ openlauncher
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ APP_VERSION_NUMBER
+ NSHighResolutionCapable
+
+
+
\ No newline at end of file
diff --git a/package-mac.sh b/package-mac.sh
new file mode 100755
index 0000000..d5142b6
--- /dev/null
+++ b/package-mac.sh
@@ -0,0 +1,48 @@
+#!/bin/bash
+#Modified from Avalonia's documentation: https://docs.avaloniaui.net/docs/0.10.x/distribution-publishing/macos
+set -e
+trap 'echo "Error packaging app"; exit 1' ERR
+
+APP_NAME="./src/openlauncher/bin/Release/net8.0/macos-universal/OpenLauncher.app"
+PUBLISH_OUTPUT_DIRECTORY="./src/openlauncher/bin/Release/net8.0/macos-universal/."
+
+INFO_PLIST="./info.plist"
+
+ICON_FILE="./src/openlauncher/resources/logo-mac.icns"
+
+if [ -d "$APP_NAME" ]
+then
+ rm -rf "$APP_NAME"
+fi
+
+echo "Creating OpenLauncher.app"
+
+rm -r -f "$APP_NAME"
+mkdir "$APP_NAME"
+
+mkdir "$APP_NAME/Contents"
+mkdir "$APP_NAME/Contents/MacOS"
+mkdir "$APP_NAME/Contents/Resources"
+
+cp "$INFO_PLIST" "$APP_NAME/Contents/Info.plist"
+cp "$ICON_FILE" "$APP_NAME/Contents/Resources/AppIcon.icns"
+for FILE in "$PUBLISH_OUTPUT_DIRECTORY"/*; do
+ if [ -f "$FILE" ]; then
+ cp -a "$FILE" "$APP_NAME/Contents/MacOS/."
+ fi
+done
+
+#Get version number from csproj
+VERSION_NUMBER=$(sed -n 's/.*\(.*\)<\/AssemblyVersion>.*/\1/p' ./src/openlauncher/openlauncher.csproj)
+#Replace placeholder version with real version number
+sed -i -e "s/APP_VERSION_NUMBER/$VERSION_NUMBER/" "$APP_NAME/Contents/Info.plist"
+#For whatever reason, sed on macOS creates a backup file when replacing text, so we need to remove it
+rm "$APP_NAME/Contents/Info.plist-e"
+
+codesign --sign - --force --deep "./src/openlauncher/bin/Release/net8.0/macos-universal/OpenLauncher.app"
+
+mkdir "$PUBLISH_OUTPUT_DIRECTORY/publish"
+
+echo "Zipping OpenLauncher.app..."
+
+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"
diff --git a/src/IntelOrca.OpenLauncher.Core/InstallService.cs b/src/IntelOrca.OpenLauncher.Core/InstallService.cs
index 3cf0235..a4410bd 100644
--- a/src/IntelOrca.OpenLauncher.Core/InstallService.cs
+++ b/src/IntelOrca.OpenLauncher.Core/InstallService.cs
@@ -29,8 +29,15 @@ public string ExecutablePath
{
get
{
- var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
- var binaryName = isWindows ? $"{_game.BinaryName}.exe" : _game.BinaryName;
+ string binaryName;
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
+ binaryName = $"{_game.BinaryName}.exe";
+ } else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) {
+ // We need to use Name and not BinaryName since BinaryName isn't capitalized
+ binaryName = $"{_game.Name}.app/Contents/MacOS/{_game.Name}";
+ } else {
+ binaryName = _game.BinaryName;
+ }
return Path.Combine(_game.BinPath, binaryName);
}
}
@@ -157,7 +164,11 @@ private void ExtractArchive(Shell shell, Uri uri, string archivePath, string out
{
if (uri.LocalPath.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))
{
- ZipFile.ExtractToDirectory(archivePath, outDirectory, overwriteFiles: true);
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) {
+ ExtractArchiveMac(archivePath, outDirectory);
+ } else {
+ ZipFile.ExtractToDirectory(archivePath, outDirectory, overwriteFiles: true);
+ }
}
else if (uri.LocalPath.EndsWith(".AppImage", StringComparison.OrdinalIgnoreCase))
{
@@ -191,5 +202,13 @@ private void ExtractArchive(Shell shell, Uri uri, string archivePath, string out
throw new Exception("Unknown file format to extract.");
}
}
+
+ private void ExtractArchiveMac(string archivePath, string outDirectory) {
+ var dittoProcess = new Process();
+ var args = $"-k -x \"{archivePath}\" \"{outDirectory}\"";
+ dittoProcess.StartInfo = new ProcessStartInfo("/usr/bin/ditto", args);
+ dittoProcess.Start();
+ dittoProcess.WaitForExit();
+ }
}
}
diff --git a/src/openlauncher/MainWindow.axaml.cs b/src/openlauncher/MainWindow.axaml.cs
index 8f02f48..71c6ee7 100644
--- a/src/openlauncher/MainWindow.axaml.cs
+++ b/src/openlauncher/MainWindow.axaml.cs
@@ -8,6 +8,7 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Media.Imaging;
+using Avalonia.Platform;
using Avalonia.Threading;
using IntelOrca.OpenLauncher.Core;
using StringResources = openlauncher.Properties.Resources;