Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -409,3 +409,12 @@ FodyWeavers.xsd
# JetBrains Rider
*.sln.iml
flatpak.bat

# macOS build output (regenerated by build-app.sh)
dist/

# Serena tool working directory
.serena/

# Downloaded SDL2 native (fetched + verified by build-app.sh, not vendored)
native/osx-arm64/*.dylib
5 changes: 4 additions & 1 deletion App.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,10 @@ public async Task CheckForAppUpdatesManually()
private async Task CheckForUpdatesAndApplyAsync(bool isManualCheck = false)
{
string currentAppDirectory = AppDomain.CurrentDomain.BaseDirectory;
string updateCheckFilePath = Path.Combine(currentAppDirectory, UpdateCheckFileName);
// Update-check state is writable runtime data: keep it out of the (signed,
// immutable) app bundle on macOS. The executable-replacement logic below
// still uses currentAppDirectory.
string updateCheckFilePath = Path.Combine(AppPaths.DataDirectory, UpdateCheckFileName);

UpdateCheckInfo updateCheckInfo = await LoadUpdateCheckInfo(updateCheckFilePath);
string currentVersionString = updateCheckInfo.CurrentVersion;
Expand Down
20 changes: 19 additions & 1 deletion GithubLauncher.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<Platforms>AnyCPU;x64</Platforms>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RuntimeIdentifiers>win-x64;linux-x64;linux-arm64;osx-x64</RuntimeIdentifiers>
<RuntimeIdentifiers>win-x64;linux-x64;linux-arm64;osx-x64;osx-arm64</RuntimeIdentifiers>
</PropertyGroup>

<PropertyGroup Condition="$([MSBuild]::IsOSPlatform('Windows'))">
Expand Down Expand Up @@ -42,6 +42,24 @@
<AvaloniaResource Include="Assets/Icons/**" />
</ItemGroup>

<!-- ppy.SDL2-CS ships native SDL2 for osx-x64/linux/win but NOT osx-arm64.
Bundle a self-contained official SDL2 (arm64) so the published app loads
SDL2 with no external/Homebrew dependency. The .dylib is git-ignored and
fetched + sha256-verified by build-app.sh (not vendored in the repo). -->
<ItemGroup Condition="'$(RuntimeIdentifier)' == 'osx-arm64'">
<None Include="native\osx-arm64\libSDL2.dylib">
<Link>libSDL2.dylib</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</None>
</ItemGroup>

<Target Name="EnsureSdl2Native" BeforeTargets="ResolveReferences"
Condition="'$(RuntimeIdentifier)' == 'osx-arm64'">
<Error Condition="!Exists('native\osx-arm64\libSDL2.dylib')"
Text="native/osx-arm64/libSDL2.dylib is missing. Run ./build-app.sh (it downloads and sha256-verifies the official SDL2). This native is intentionally not vendored in the repo." />
</Target>

<ItemGroup>
<ProjectReference Include="lib\GitHubLauncher.Core\GitHubLauncher.Core.csproj" />
</ItemGroup>
Expand Down
14 changes: 7 additions & 7 deletions MainWindow.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -919,7 +919,7 @@ private void LoadCurrentVersion()
try
{
string currentAppDirectory = AppDomain.CurrentDomain.BaseDirectory;
string updateCheckFilePath = Path.Combine(currentAppDirectory, "update_check.json");
string updateCheckFilePath = Path.Combine(AppPaths.DataDirectory, "update_check.json");

if (File.Exists(updateCheckFilePath))
{
Expand Down Expand Up @@ -3565,7 +3565,7 @@ private async void ExportGames_Click(object sender, RoutedEventArgs e)
};

var options = new JsonSerializerOptions { WriteIndented = true };
var exportPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "apps_export.json");
var exportPath = Path.Combine(AppPaths.DataDirectory, "apps_export.json");
await File.WriteAllTextAsync(exportPath, JsonSerializer.Serialize(exportData, options));

await ShowMessageBoxAsync($"Apps exported to {exportPath}", "Export Complete");
Expand Down Expand Up @@ -3609,8 +3609,8 @@ private async void ValidateGames_Click(object sender, RoutedEventArgs e)
}
private async Task<List<GameInfo>> LoadGamesFromJsonAsync()
{
var appsPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "apps.json");
var legacyPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "games.json");
var appsPath = Path.Combine(AppPaths.DataDirectory, "apps.json");
var legacyPath = Path.Combine(AppPaths.DataDirectory, "games.json");
var sourcePath = File.Exists(appsPath) ? appsPath : legacyPath;
if (!File.Exists(sourcePath)) return [];

Expand Down Expand Up @@ -3687,7 +3687,7 @@ private static object SerializeGame(GameInfo game)
}
private async Task SaveGamesToJsonAsync(List<GameInfo> appsToSave)
{
var appsPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "apps.json");
var appsPath = Path.Combine(AppPaths.DataDirectory, "apps.json");
var data = new
{
apps = appsToSave.Select(SerializeGame).ToList()
Expand Down Expand Up @@ -4256,10 +4256,10 @@ private void RefreshAppCatalog_Click(object sender, RoutedEventArgs e)

// App Catalog
private static readonly string AppCatalogCachePath = Path.Combine(
AppDomain.CurrentDomain.BaseDirectory, "app_catalog_cache.json");
AppPaths.DataDirectory, "app_catalog_cache.json");

private static readonly string AppCatalogVersionPath = Path.Combine(
AppDomain.CurrentDomain.BaseDirectory, "app_catalog_version.txt");
AppPaths.DataDirectory, "app_catalog_version.txt");

private async Task LoadAppCatalogAsync(bool forceRefresh)
{
Expand Down
41 changes: 41 additions & 0 deletions Services/AppPaths.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System;
using System.IO;
using System.Runtime.InteropServices;

namespace GithubLauncher
{
/// <summary>
/// Resolves where the launcher stores writable data.
///
/// On macOS the launcher ships as a signed <c>.app</c> bundle whose contents
/// must stay immutable — writing next to the executable (inside
/// <c>Contents/MacOS</c>) invalidates the code signature and prevents the app
/// from living in a read-only location such as <c>/Applications</c>. So on
/// macOS user data lives in <c>~/Library/Application Support/GithubLauncher</c>.
///
/// On Windows/Linux behavior is unchanged: data stays next to the executable
/// (portable app layout).
/// </summary>
public static class AppPaths
{
/// <summary>Executable location. Use only for self-update; never for user data on macOS.</summary>
public static string AppDirectory => AppContext.BaseDirectory;

private static readonly Lazy<string> _dataDirectory = new(() =>
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
string appSupport = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"Library", "Application Support", "GithubLauncher");
Directory.CreateDirectory(appSupport);
return appSupport;
}

return AppContext.BaseDirectory;
});

/// <summary>Writable data root (settings, app registry, caches, installed apps). Created on first access.</summary>
public static string DataDirectory => _dataDirectory.Value;
}
}
2 changes: 1 addition & 1 deletion Services/AppSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public class AppSettings
public string AppListRepository { get; set; } = "SirDiabo/GHLAppList";
public string AppListCachedVersion { get; set; } = string.Empty;
private static readonly string SettingsPath = Path.Combine(
AppDomain.CurrentDomain.BaseDirectory,
AppPaths.DataDirectory,
"settings.json"
);

Expand Down
2 changes: 1 addition & 1 deletion Services/CLIHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ private void LoadVersion()
try
{
string currentAppDirectory = AppDomain.CurrentDomain.BaseDirectory;
string updateCheckFilePath = Path.Combine(currentAppDirectory, "update_check.json");
string updateCheckFilePath = Path.Combine(AppPaths.DataDirectory, "update_check.json");

if (File.Exists(updateCheckFilePath))
{
Expand Down
12 changes: 6 additions & 6 deletions Services/GameManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,11 @@ public GameManager()

_appsFolder = !string.IsNullOrEmpty(_settings?.AppsPath)
? _settings.AppsPath
: Path.Combine(AppDomain.CurrentDomain.BaseDirectory, Profile.DefaultInstallFolderName);
: Path.Combine(AppPaths.DataDirectory, Profile.DefaultInstallFolderName);

_cacheFolder = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Cache");
_appsConfigPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "apps.json");
_legacyGamesConfigPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "games.json");
_cacheFolder = Path.Combine(AppPaths.DataDirectory, "Cache");
_appsConfigPath = Path.Combine(AppPaths.DataDirectory, "apps.json");
_legacyGamesConfigPath = Path.Combine(AppPaths.DataDirectory, "games.json");

try
{
Expand Down Expand Up @@ -419,7 +419,7 @@ public async Task UpdateGamesFolderAsync(string newPath)
}
else
{
targetPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, Profile.DefaultInstallFolderName);
targetPath = Path.Combine(AppPaths.DataDirectory, Profile.DefaultInstallFolderName);
Directory.CreateDirectory(targetPath);
}

Expand All @@ -435,7 +435,7 @@ public async Task UpdateGamesFolderAsync(string newPath)
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error updating apps folder: {ex.Message}");
_appsFolder = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, Profile.DefaultInstallFolderName);
_appsFolder = Path.Combine(AppPaths.DataDirectory, Profile.DefaultInstallFolderName);
Directory.CreateDirectory(_appsFolder);
throw;
}
Expand Down
106 changes: 106 additions & 0 deletions build-app.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
#!/usr/bin/env bash
#
# Build a native arm64 macOS .app bundle for Github Launcher.
# Self-contained: bundles the .NET runtime and the official SDL2 (arm64).
# No Homebrew or other external runtime dependencies.
#
# ppy.SDL2-CS ships no arm64-macOS native, so this script downloads the official
# SDL2 release, verifies its sha256, and thins it to arm64 before publishing.
#
set -euo pipefail

cd "$(dirname "$0")"

RID="osx-arm64"
CONFIG="Release"
TFM="net9.0"
APP_NAME="Github Launcher"
APP="dist/${APP_NAME}.app"
PUBLISH="bin/${CONFIG}/${TFM}/${RID}/publish"

# Official SDL2 release (universal, self-contained: links only system frameworks).
SDL2_VERSION="2.32.10"
SDL2_DMG_URL="https://github.com/libsdl-org/SDL/releases/download/release-${SDL2_VERSION}/SDL2-${SDL2_VERSION}.dmg"
SDL2_DMG_SHA256="4a7ac31640d70214e848f994be8a12849c0f97918a7e6c2e27a40036166d1a7f"
SDL2_NATIVE="native/osx-arm64/libSDL2.dylib"

echo ">> Ensuring submodule is present..."
git submodule update --init --recursive

if [ -f "$SDL2_NATIVE" ]; then
echo ">> SDL2 native already present ($SDL2_NATIVE)"
else
echo ">> Fetching official SDL2 ${SDL2_VERSION}..."
TMP="$(mktemp -d)"
curl -fsSL -o "$TMP/sdl2.dmg" "$SDL2_DMG_URL"
echo "${SDL2_DMG_SHA256} ${TMP}/sdl2.dmg" | shasum -a 256 -c -
MNT="$(hdiutil attach "$TMP/sdl2.dmg" -nobrowse -readonly | grep -o '/Volumes/.*' | head -1)"
mkdir -p "$(dirname "$SDL2_NATIVE")"
# Thin the universal framework binary to arm64 for this RID.
lipo "$MNT/SDL2.framework/Versions/A/SDL2" -thin arm64 -output "$SDL2_NATIVE"
hdiutil detach "$MNT" >/dev/null
rm -rf "$TMP"
echo ">> SDL2 native ready: $SDL2_NATIVE ($(file -b "$SDL2_NATIVE"))"
fi

echo ">> Publishing ${RID} (self-contained)..."
rm -rf "bin/${CONFIG}/${TFM}/${RID}"
dotnet publish GithubLauncher.csproj -c "$CONFIG" -r "$RID" --self-contained true

echo ">> Assembling ${APP}..."
rm -rf "$APP"
mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources"
cp -R "$PUBLISH/." "$APP/Contents/MacOS/"

echo ">> Generating icon..."
ICONSET="$(mktemp -d)/AppIcon.iconset"
mkdir -p "$ICONSET"
sips -s format png icon.ico --out "$ICONSET/base.png" >/dev/null
for sz in 16 32 128 256 512; do
sips -z "$sz" "$sz" "$ICONSET/base.png" --out "$ICONSET/icon_${sz}x${sz}.png" >/dev/null
dbl=$((sz * 2))
sips -z "$dbl" "$dbl" "$ICONSET/base.png" --out "$ICONSET/icon_${sz}x${sz}@2x.png" >/dev/null
done
rm "$ICONSET/base.png"
iconutil -c icns "$ICONSET" -o "$APP/Contents/Resources/AppIcon.icns"

echo ">> Writing Info.plist..."
cat > "$APP/Contents/Info.plist" <<'PLIST'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleName</key>
<string>Github Launcher</string>
<key>CFBundleDisplayName</key>
<string>Github Launcher</string>
<key>CFBundleIdentifier</key>
<string>com.sirdiabo.githublauncher</string>
<key>CFBundleVersion</key>
<string>1.0.0.0</string>
<key>CFBundleShortVersionString</key>
<string>1.0.0</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleExecutable</key>
<string>GithubLauncher</string>
<key>CFBundleIconFile</key>
<string>AppIcon</string>
<key>LSMinimumSystemVersion</key>
<string>11.0</string>
<key>NSHighResolutionCapable</key>
<true/>
<key>LSApplicationCategoryType</key>
<string>public.app-category.games</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
</dict>
</plist>
PLIST

echo ">> Code signing (ad-hoc)..."
chmod +x "$APP/Contents/MacOS/GithubLauncher"
codesign --force --deep --sign - "$APP"
codesign --verify --strict "$APP"

echo ">> Done: $APP"