Compare commits

...
Author SHA1 Message Date
adminandClaude Opus 4.5 99e331b40b Fix symbol exports for SparklePlugin native library
The C functions need to be exported with visibility("default") and
explicit linker flags for Unity P/Invoke to find them. Without this,
the bundle binary had no exported symbols.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 22:04:51 -08:00
adminandClaude Opus 4.5 0d832ea8f2 Convert Sparkle plugin build from clang to Bazel
- Add Sparkle framework as http_archive dependency in MODULE.bazel
- Add BUILD.sparkle to import the framework
- Add BUILD.bazel for SparklePlugin using macos_bundle rule
- Update build_sparkle_plugin.sh to use Bazel instead of direct clang
- Register Apple CC toolchain extension

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 17:10:10 -08:00
adminandClaude Opus 4.5 e296a31033 Add native Sparkle plugin to enable Mac auto-updates
The Sparkle framework was being injected into the app bundle, but
nothing was initializing it. This adds:

- Native Objective-C plugin (SparklePlugin.m) that initializes
  SPUStandardUpdaterController at runtime
- C# wrapper (SparkleUpdater.cs) for Unity to call the native plugin
- SparkleInitializer.cs uses RuntimeInitializeOnLoadMethod to
  automatically initialize Sparkle at app startup
- Build script to compile the plugin as a universal binary

The plugin is weak-linked against Sparkle.framework, which is injected
separately by inject_sparkle.sh during the build process.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 17:00:34 -08:00
15 changed files with 416 additions and 1 deletions
+18
View File
@@ -130,6 +130,13 @@ bazel_dep(name = "apple_support", version = "1.21.1", repo_name = "build_bazel_a
bazel_dep(name = "rules_apple", version = "4.3.3", repo_name = "build_bazel_rules_apple")
bazel_dep(name = "rules_swift", version = "2.4.0", repo_name = "build_bazel_rules_swift")
# Register Apple CC toolchain for Objective-C compilation
apple_cc_configure = use_extension(
"@build_bazel_apple_support//crosstool:setup.bzl",
"apple_cc_configure_extension",
)
use_repo(apple_cc_configure, "local_config_apple_cc")
#
# Protocol Buffers & RPC
#
@@ -293,6 +300,17 @@ http_archive(
],
)
# Sparkle framework for macOS auto-updates
SPARKLE_VERSION = "2.6.4"
http_archive(
name = "sparkle",
build_file = "@//external:BUILD.sparkle",
sha256 = "50612a06038abc931f16011d7903b8326a362c1074dabccb718404ce8e585f0b",
strip_prefix = "",
url = "https://github.com/sparkle-project/Sparkle/releases/download/%s/Sparkle-%s.tar.xz" % (SPARKLE_VERSION, SPARKLE_VERSION),
)
# Busybox static binary for Docker health checks (provides nc, wget, etc.)
# Primary: DigitalOcean Spaces (public, reliable)
# Fallback: busybox.net (can be unreliable/slow)
+1 -1
View File
@@ -396,7 +396,7 @@
"@@apple_support~//crosstool:setup.bzl%apple_cc_configure_extension": {
"general": {
"bzlTransitiveDigest": "Z3yAd66IJL0GAZUTSeMOjoHiE1SZPPwiIs/XQui5BvE=",
"usagesDigest": "tl3VVeQX3Hzh7FhM2gjnkCwEJpRMlY5S6a850WY/xvc=",
"usagesDigest": "DqQsfZN5lA8z+nLEEY+EpKGzQ8M73mDm/A8lofDSyus=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
+8
View File
@@ -0,0 +1,8 @@
load("@build_bazel_rules_apple//apple:apple.bzl", "apple_dynamic_framework_import")
# Import pre-built Sparkle framework
apple_dynamic_framework_import(
name = "Sparkle",
framework_imports = glob(["Sparkle.framework/**"]),
visibility = ["//visibility:public"],
)
+3
View File
@@ -8,3 +8,6 @@ ZIP_LOCATION=$(bazel cquery --config=mactools --output=files @net_eagle0_unity_g
/usr/bin/unzip -o $ZIP_LOCATION -d src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Plugins/
/usr/bin/plutil -convert xml1 src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Plugins/DarwinGodiceBundle.bundle/Contents/Info.plist
/bin/echo "building sparkle plugin"
./scripts/build_sparkle_plugin.sh
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
#
# Build the SparklePlugin native library for Unity using Bazel
#
# Usage: build_sparkle_plugin.sh [output_dir]
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
OUTPUT_DIR="${1:-$PROJECT_ROOT/src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Plugins/macOS}"
echo "=== Building SparklePlugin with Bazel ==="
bazel build --config=mactools //src/main/objc/net/eagle0/sparkle:SparklePlugin
# Get the zip path from bazel
ZIP_PATH=$(bazel cquery --config=mactools --output=files //src/main/objc/net/eagle0/sparkle:SparklePlugin 2>/dev/null)
echo "=== Extracting SparklePlugin.bundle ==="
mkdir -p "$OUTPUT_DIR"
rm -rf "$OUTPUT_DIR/SparklePlugin.bundle"
unzip -o "$ZIP_PATH" -d "$OUTPUT_DIR/"
echo "=== SparklePlugin built successfully ==="
ls -la "$OUTPUT_DIR/SparklePlugin.bundle/"
@@ -0,0 +1,13 @@
using UnityEngine;
/// <summary>
/// Initializes Sparkle auto-updater at app startup.
/// Uses RuntimeInitializeOnLoadMethod to ensure early initialization.
/// </summary>
public static class SparkleInitializer {
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
private static void Initialize() {
Debug.Log("SparkleInitializer: Initializing Sparkle at startup");
SparkleUpdater.Initialize();
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2b4c6d8e0f1a3b5c7d9e1f3a5b7c9d1e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: -100
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,120 @@
using System.Runtime.InteropServices;
using UnityEngine;
/// <summary>
/// C# wrapper for the native SparklePlugin on macOS.
/// Provides auto-update functionality via the Sparkle framework.
/// On non-macOS platforms, all methods are no-ops.
/// </summary>
public static class SparkleUpdater {
#if UNITY_STANDALONE_OSX && !UNITY_EDITOR
private const string PluginName = "SparklePlugin";
[DllImport(PluginName)]
private static extern void SparklePlugin_Initialize();
[DllImport(PluginName)]
private static extern void SparklePlugin_CheckForUpdates();
[DllImport(PluginName)]
private static extern void SparklePlugin_CheckForUpdatesInBackground();
[DllImport(PluginName)]
private static extern int SparklePlugin_IsCheckingForUpdates();
[DllImport(PluginName)]
private static extern int SparklePlugin_GetAutomaticallyChecksForUpdates();
[DllImport(PluginName)]
private static extern void SparklePlugin_SetAutomaticallyChecksForUpdates(int enabled);
private static bool _initialized = false;
/// <summary>
/// Initialize the Sparkle updater. Should be called once at app startup.
/// This starts automatic background update checks based on Info.plist settings.
/// </summary>
public static void Initialize() {
if (_initialized) {
Debug.Log("SparkleUpdater: Already initialized");
return;
}
Debug.Log("SparkleUpdater: Initializing Sparkle");
try {
SparklePlugin_Initialize();
_initialized = true;
Debug.Log("SparkleUpdater: Initialization complete");
} catch (System.Exception e) {
Debug.LogError($"SparkleUpdater: Failed to initialize: {e.Message}");
}
}
/// <summary>
/// Manually check for updates. Shows the update UI to the user.
/// </summary>
public static void CheckForUpdates() {
if (!_initialized) {
Debug.LogWarning("SparkleUpdater: Not initialized");
return;
}
SparklePlugin_CheckForUpdates();
}
/// <summary>
/// Check for updates silently in the background.
/// Only shows UI if an update is found.
/// </summary>
public static void CheckForUpdatesInBackground() {
if (!_initialized) {
Debug.LogWarning("SparkleUpdater: Not initialized");
return;
}
SparklePlugin_CheckForUpdatesInBackground();
}
/// <summary>
/// Returns true if an update check is currently in progress.
/// </summary>
public static bool IsCheckingForUpdates {
get {
if (!_initialized) return false;
return SparklePlugin_IsCheckingForUpdates() != 0;
}
}
/// <summary>
/// Gets or sets whether automatic update checks are enabled.
/// </summary>
public static bool AutomaticallyChecksForUpdates {
get {
if (!_initialized) return false;
return SparklePlugin_GetAutomaticallyChecksForUpdates() != 0;
}
set {
if (!_initialized) return;
SparklePlugin_SetAutomaticallyChecksForUpdates(value? 1: 0);
}
}
#else
// Stub implementations for non-macOS platforms and Editor
public static void Initialize() { Debug.Log("SparkleUpdater: Not available on this platform"); }
public static void CheckForUpdates() {
Debug.Log("SparkleUpdater: Not available on this platform");
}
public static void CheckForUpdatesInBackground() {
Debug.Log("SparkleUpdater: Not available on this platform");
}
public static bool IsCheckingForUpdates => false;
public static bool AutomaticallyChecksForUpdates {
get => false;
set {}
}
#endif
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8a3b5c7d9e1f2a3b4c5d6e7f8a9b0c1d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,3 +1,6 @@
System.Runtime.CompilerServices.Unsafe/**
!**/*.psd
**/*.dll
# Native plugins built at CI time
DarwinGodiceBundle.bundle/
macOS/SparklePlugin.bundle/
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 3c5d7e9f1a2b4c6d8e0f2a4b6c8d0e2f
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,45 @@
fileFormatVersion: 2
guid: 4d6e8f0a2b3c5d7e9f1a3b5c7d9e1f3a
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 0
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
: Any
second:
enabled: 0
settings:
Exclude Editor: 1
Exclude Linux64: 1
Exclude OSXUniversal: 0
Exclude Win: 1
Exclude Win64: 1
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
CPU: AnyCPU
DefaultValueInitialized: true
OS: AnyOS
- first:
Standalone: OSXUniversal
second:
enabled: 1
settings:
CPU: AnyCPU
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,40 @@
load("@build_bazel_rules_apple//apple:macos.bzl", "macos_bundle")
# Native Sparkle plugin for Unity
# This plugin initializes Sparkle auto-updater at runtime
objc_library(
name = "sparkle_plugin_lib",
srcs = ["SparklePlugin.m"],
copts = [
"-fmodules",
"-fobjc-arc",
"-fvisibility=default", # Export symbols for Unity P/Invoke
],
sdk_frameworks = ["Foundation"],
deps = ["@sparkle//:Sparkle"],
)
macos_bundle(
name = "SparklePlugin",
bundle_id = "net.eagle0.SparklePlugin",
bundle_name = "SparklePlugin",
infoplists = ["Info.plist"],
linkopts = [
# Export symbols for Unity P/Invoke
"-exported_symbol",
"_SparklePlugin_Initialize",
"-exported_symbol",
"_SparklePlugin_CheckForUpdates",
"-exported_symbol",
"_SparklePlugin_CheckForUpdatesInBackground",
"-exported_symbol",
"_SparklePlugin_IsCheckingForUpdates",
"-exported_symbol",
"_SparklePlugin_GetAutomaticallyChecksForUpdates",
"-exported_symbol",
"_SparklePlugin_SetAutomaticallyChecksForUpdates",
],
minimum_os_version = "10.13",
visibility = ["//visibility:public"],
deps = [":sparkle_plugin_lib"],
)
@@ -0,0 +1,22 @@
<?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>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>SparklePlugin</string>
<key>CFBundleIdentifier</key>
<string>net.eagle0.SparklePlugin</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>SparklePlugin</string>
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>
@@ -0,0 +1,86 @@
//
// SparklePlugin.m
// Native macOS plugin for Unity to initialize Sparkle auto-updates
//
// Copyright 2026 Dan Crosby
//
#import <Foundation/Foundation.h>
#import <Sparkle/Sparkle.h>
// The updater controller - retained for the lifetime of the app
static SPUStandardUpdaterController *updaterController = nil;
// Export macro for Unity P/Invoke
#define EXPORT __attribute__((visibility("default")))
// Called from Unity to initialize Sparkle
// This should be called once at app startup
EXPORT void SparklePlugin_Initialize(void) {
if (updaterController != nil) {
NSLog(@"SparklePlugin: Already initialized");
return;
}
NSLog(@"SparklePlugin: Initializing Sparkle auto-updater");
// Create the updater controller with automatic update checking enabled
// The feed URL and public key are read from Info.plist (SUFeedURL, SUPublicEDKey)
updaterController = [[SPUStandardUpdaterController alloc]
initWithStartingUpdater:YES
updaterDelegate:nil
userDriverDelegate:nil];
if (updaterController != nil) {
NSLog(@"SparklePlugin: Sparkle initialized successfully");
} else {
NSLog(@"SparklePlugin: Failed to initialize Sparkle");
}
}
// Manually trigger an update check (shows UI)
EXPORT void SparklePlugin_CheckForUpdates(void) {
if (updaterController == nil) {
NSLog(@"SparklePlugin: Not initialized, call Initialize first");
return;
}
NSLog(@"SparklePlugin: Checking for updates");
[updaterController checkForUpdates:nil];
}
// Check for updates silently in background (no UI unless update found)
EXPORT void SparklePlugin_CheckForUpdatesInBackground(void) {
if (updaterController == nil) {
NSLog(@"SparklePlugin: Not initialized, call Initialize first");
return;
}
NSLog(@"SparklePlugin: Checking for updates in background");
[[updaterController updater] checkForUpdatesInBackground];
}
// Returns 1 if an update check is in progress, 0 otherwise
EXPORT int SparklePlugin_IsCheckingForUpdates(void) {
if (updaterController == nil) {
return 0;
}
return [[updaterController updater] sessionInProgress] ? 1 : 0;
}
// Returns 1 if automatic update checks are enabled, 0 otherwise
EXPORT int SparklePlugin_GetAutomaticallyChecksForUpdates(void) {
if (updaterController == nil) {
return 0;
}
return [[updaterController updater] automaticallyChecksForUpdates] ? 1 : 0;
}
// Enable or disable automatic update checks
EXPORT void SparklePlugin_SetAutomaticallyChecksForUpdates(int enabled) {
if (updaterController == nil) {
NSLog(@"SparklePlugin: Not initialized");
return;
}
[[updaterController updater] setAutomaticallyChecksForUpdates:(enabled != 0)];
}