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;