diff --git a/.github/workflows/dotnetcore.yml b/.github/workflows/dotnetcore.yml
index d9b6ddeb..5cbac806 100644
--- a/.github/workflows/dotnetcore.yml
+++ b/.github/workflows/dotnetcore.yml
@@ -213,11 +213,86 @@ jobs:
name: ${{ matrix.os }}-${{ matrix.rid }}-${{ matrix.configuration }}
path: ${{ github.workspace }}/artifacts/**/*
+ macos:
+ runs-on: ${{ matrix.os }}
+ name: Bootstrap ${{ matrix.rid }}-${{ matrix.configuration }}
+ needs: [setup]
+ strategy:
+ matrix:
+ configuration: [Debug, Release]
+ rid: [osx-x64, osx-arm64]
+ os: [macos-latest]
+ env:
+ SNAPX_VERSION: ${{ needs.setup.outputs.SNAPX_VERSION }}
+ SNAPX_DOTNET_FRAMEWORK_VERSION: ${{ needs.setup.outputs.SNAPX_DOTNET_FRAMEWORK_VERSION }}
+ DOTNET_NET100_VERSION: ${{ needs.setup.outputs.DOTNET_NET100_VERSION }}
+ DOTNET_NET90_VERSION: ${{ needs.setup.outputs.DOTNET_NET90_VERSION }}
+ DOTNET_NET80_VERSION: ${{ needs.setup.outputs.DOTNET_NET80_VERSION }}
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v5
+ with:
+ lfs: true
+ submodules: true
+
+ - name: Install .NET 8.0
+ uses: actions/setup-dotnet@v4.2.0
+ with:
+ dotnet-version: ${{ env.DOTNET_NET80_VERSION }}
+
+ - name: Install .NET 9.0
+ uses: actions/setup-dotnet@v4.2.0
+ with:
+ dotnet-version: ${{ env.DOTNET_NET90_VERSION }}
+
+ - name: Install .NET 10.0
+ uses: actions/setup-dotnet@v4.2.0
+ with:
+ dotnet-version: ${{ env.DOTNET_NET100_VERSION }}
+
+ - name: Bootstrap ${{ matrix.rid }}-${{ matrix.configuration }}
+ shell: pwsh
+ run: ./build.ps1 Bootstrap-Unix -Version ${{ env.SNAPX_VERSION }} -Configuration ${{ matrix.configuration }} -CIBuild -NetCoreAppVersion ${{ env.SNAPX_DOTNET_FRAMEWORK_VERSION }} -Rid ${{ matrix.rid }}
+
+ - name: Test native
+ shell: pwsh
+ run: ./build.ps1 Run-Native-UnitTests -Version ${{ env.SNAPX_VERSION }} -Configuration ${{ matrix.configuration }} -CIBuild -NetCoreAppVersion ${{ env.SNAPX_DOTNET_FRAMEWORK_VERSION }} -Rid ${{ matrix.rid }}
+
+ - name: Test .NET
+ shell: pwsh
+ run: ./build.ps1 Run-Dotnet-UnitTests -Version ${{ env.SNAPX_VERSION }} -Configuration ${{ matrix.configuration }} -CIBuild -NetCoreAppVersion ${{ env.SNAPX_DOTNET_FRAMEWORK_VERSION }} -Rid ${{ matrix.rid }}
+
+ - name: Collect artifacts
+ env:
+ SNAPX_MACOS_SETUP_ZIP_REL_DIR: build/dotnet/${{ matrix.rid }}/Snap.Installer/${{ env.SNAPX_DOTNET_FRAMEWORK_VERSION }}/${{ matrix.configuration }}/publish
+ SNAPX_MACOS_CORERUN_REL_DIR: build/native/Unix/${{ matrix.rid }}/${{ matrix.configuration }}/Snap.CoreRun
+ SNAPX_MACOS_PAL_REL_DIR: build/native/Unix/${{ matrix.rid }}/${{ matrix.configuration }}/Snap.CoreRun.Pal
+ SNAPX_MACOS_BSDIFF_REL_DIR: build/native/Unix/${{ matrix.rid }}/${{ matrix.configuration }}/Snap.Bsdiff
+ run: |
+ mkdir -p ${{ github.workspace }}/artifacts/${{ env.SNAPX_MACOS_SETUP_ZIP_REL_DIR }}
+ cp ${{ github.workspace }}/${{ env.SNAPX_MACOS_SETUP_ZIP_REL_DIR }}/Setup-${{ matrix.rid }}.zip ${{ github.workspace }}/artifacts/${{ env.SNAPX_MACOS_SETUP_ZIP_REL_DIR }}/Setup-${{ matrix.rid }}.zip
+
+ mkdir -p ${{ github.workspace }}/artifacts/${{ env.SNAPX_MACOS_CORERUN_REL_DIR }}
+ cp ${{ github.workspace }}/${{ env.SNAPX_MACOS_CORERUN_REL_DIR }}/corerun ${{ github.workspace }}/artifacts/${{ env.SNAPX_MACOS_CORERUN_REL_DIR }}/corerun.bin
+
+ mkdir -p ${{ github.workspace }}/artifacts/${{ env.SNAPX_MACOS_PAL_REL_DIR }}
+ cp ${{ github.workspace }}/${{ env.SNAPX_MACOS_PAL_REL_DIR }}/libpal.dylib ${{ github.workspace }}/artifacts/${{ env.SNAPX_MACOS_PAL_REL_DIR }}/libpal.dylib
+
+ mkdir -p ${{ github.workspace }}/artifacts/${{ env.SNAPX_MACOS_BSDIFF_REL_DIR }}
+ cp ${{ github.workspace }}/${{ env.SNAPX_MACOS_BSDIFF_REL_DIR }}/libsnap_bsdiff.dylib ${{ github.workspace }}/artifacts/${{ env.SNAPX_MACOS_BSDIFF_REL_DIR }}/libsnap_bsdiff.dylib
+
+ - name: Upload artifacts
+ if: success()
+ uses: actions/upload-artifact@v4
+ with:
+ name: ${{ matrix.os }}-${{ matrix.rid }}-${{ matrix.configuration }}
+ path: ${{ github.workspace }}/artifacts/**/*
+
publish:
if: success()
runs-on: ubuntu-latest
name: Nupkg
- needs: [setup, windows, linux] # todo: enable me when github actions supports arm64: test-linux-arm64
+ needs: [setup, windows, linux, macos] # todo: enable me when github actions supports arm64: test-linux-arm64
env:
SNAPX_VERSION: ${{ needs.setup.outputs.SNAPX_VERSION }}
DOTNET_NET100_VERSION: ${{ needs.setup.outputs.DOTNET_NET100_VERSION }}
diff --git a/bootstrap.ps1 b/bootstrap.ps1
index 8f676f54..6b77e482 100644
--- a/bootstrap.ps1
+++ b/bootstrap.ps1
@@ -7,7 +7,7 @@ param(
[Parameter(Position = 2, ValueFromPipelineByPropertyName = $true)]
[switch] $Lto,
[Parameter(Position = 3, ValueFromPipelineByPropertyName = $true)]
- [ValidateSet("win-x86", "win-x64", "linux-x64", "linux-arm64")]
+ [ValidateSet("win-x86", "win-x64", "linux-x64", "linux-arm64", "osx-x64", "osx-arm64")]
[string] $Rid = $null,
[Parameter(Position = 4, ValueFromPipelineByPropertyName = $true, Mandatory = $true)]
[string] $NetCoreAppVersion,
@@ -211,6 +211,12 @@ function Invoke-Build-Snap-Installer {
"linux-arm64" {
$SnapInstallerExeName = "Snap.Installer"
}
+ "osx-x64" {
+ $SnapInstallerExeName = "Snap.Installer"
+ }
+ "osx-arm64" {
+ $SnapInstallerExeName = "Snap.Installer"
+ }
default {
Write-Error "Rid not supported: $Rid"
}
diff --git a/build.ps1 b/build.ps1
index 3bfb6603..101bd2a1 100644
--- a/build.ps1
+++ b/build.ps1
@@ -116,6 +116,8 @@ function Invoke-Build-Rids-Array {
if($Rid -eq "any") {
$Rids += "linux-x64"
$Rids += "linux-arm64"
+ $Rids += "osx-x64"
+ $Rids += "osx-arm64"
} else {
$Rids += $Rid
}
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index a08a81f9..30878ea2 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -73,7 +73,23 @@
- $(DefineConstants);PLATFORM_MAXOSX;
+ $(DefineConstants);PLATFORM_MACOSX;
+
+
+
+ $(DefineConstants);PLATFORM_MACOSX_X64;
+
+
+
+ $(DefineConstants);PLATFORM_MACOSX_ARM64;
+
+
+
+ 6.4.0
+ net10.0
+ 8.0.414
+ 9.0.305
+ 10.0.100-rc.1.25451.107
diff --git a/src/Snap.CoreRun.Pal/CMakeLists.txt b/src/Snap.CoreRun.Pal/CMakeLists.txt
index 512c2f2b..866e929b 100644
--- a/src/Snap.CoreRun.Pal/CMakeLists.txt
+++ b/src/Snap.CoreRun.Pal/CMakeLists.txt
@@ -82,9 +82,15 @@ elseif (UNIX)
libstdc++.a
)
- list(APPEND pal_DEFINES
- PAL_PLATFORM_LINUX
- )
+ if(APPLE)
+ list(APPEND pal_DEFINES
+ PAL_PLATFORM_MACOS
+ )
+ else()
+ list(APPEND pal_DEFINES
+ PAL_PLATFORM_LINUX
+ )
+ endif()
endif ()
diff --git a/src/Snap.CoreRun.Pal/src/include/pal/pal.hpp b/src/Snap.CoreRun.Pal/src/include/pal/pal.hpp
index 23651ab7..31a1eb4a 100644
--- a/src/Snap.CoreRun.Pal/src/include/pal/pal.hpp
+++ b/src/Snap.CoreRun.Pal/src/include/pal/pal.hpp
@@ -53,6 +53,24 @@
#define PAL_API
#define PAL_CALLING_CONVENTION
#endif
+#elif PAL_PLATFORM_MACOS
+#include
+#include
+#include
+#include
+#include // mode_t
+#define PAL_MAX_PATH PATH_MAX
+#define PAL_DIRECTORY_SEPARATOR_STR "/"
+#define PAL_DIRECTORY_SEPARATOR_C '/'
+#define PAL_CORECLR_TPA_SEPARATOR_STR ":"
+#define PAL_CORECLR_TPA_SEPARATOR_C ':'
+#if defined(__GNUC__)
+#define PAL_API __attribute__((visibility("default")))
+#define PAL_CALLING_CONVENTION
+#else
+#define PAL_API
+#define PAL_CALLING_CONVENTION
+#endif
#else
#error Unsupported platform
#endif
@@ -79,6 +97,10 @@ typedef DWORD pal_exit_code_t;
typedef pid_t pal_pid_t;
typedef mode_t pal_mode_t;
typedef int pal_exit_code_t;
+#elif defined(PAL_PLATFORM_MACOS)
+typedef pid_t pal_pid_t;
+typedef mode_t pal_mode_t;
+typedef int pal_exit_code_t;
#endif
// - Callbacks
@@ -135,7 +157,7 @@ PAL_API BOOL PAL_CALLING_CONVENTION pal_is_windows_8_or_greater();
PAL_API BOOL PAL_CALLING_CONVENTION pal_is_windows_7_or_greater();
PAL_API BOOL PAL_CALLING_CONVENTION pal_is_linux();
-
+PAL_API BOOL PAL_CALLING_CONVENTION pal_is_macos();
PAL_API BOOL PAL_CALLING_CONVENTION pal_is_unknown_os();
// - Environment
diff --git a/src/Snap.CoreRun.Pal/src/pal.cpp b/src/Snap.CoreRun.Pal/src/pal.cpp
index 37ec438f..13b8914f 100644
--- a/src/Snap.CoreRun.Pal/src/pal.cpp
+++ b/src/Snap.CoreRun.Pal/src/pal.cpp
@@ -20,6 +20,17 @@
#include // kill
#include // nanosleep
static const char *symlink_entrypoint_executable = "/proc/self/exe";
+#elif defined(PAL_PLATFORM_MACOS)
+#include // wait
+#include // getcwd
+#include // open
+#include // opendir
+#include // dirname
+#include // dlopen
+#include // kill
+#include // nanosleep
+#include // _NSGetExecutablePath
+static const char* symlink_entrypoint_executable = nullptr; // macOS uses different approach
#endif
#include
@@ -607,7 +618,7 @@ PAL_API BOOL PAL_CALLING_CONVENTION pal_sleep_ms(const uint32_t milliseconds) {
#if defined(PAL_PLATFORM_WINDOWS)
Sleep(milliseconds);
return TRUE;
-#elif defined(PAL_PLATFORM_LINUX)
+#elif defined(PAL_PLATFORM_LINUX) || defined(PAL_PLATFORM_MACOS)
struct timespec ts = {0};
ts.tv_sec = milliseconds / 1000;
ts.tv_nsec = (milliseconds % 1000) * 1000000;
@@ -650,9 +661,18 @@ PAL_API BOOL PAL_CALLING_CONVENTION pal_is_linux() {
#endif
}
+PAL_API BOOL PAL_CALLING_CONVENTION pal_is_macos() {
+#if defined(PAL_PLATFORM_MACOS)
+ return TRUE;
+#else
+ return FALSE;
+#endif
+}
+
PAL_API BOOL PAL_CALLING_CONVENTION pal_is_unknown_os() {
return pal_is_linux()
|| pal_is_windows()
+ || pal_is_macos()
? FALSE
: TRUE;
}
diff --git a/src/Snap.Tests/AnyOS/MacOS/SnapOsMacOSTests.cs b/src/Snap.Tests/AnyOS/MacOS/SnapOsMacOSTests.cs
new file mode 100644
index 00000000..77f1e2a8
--- /dev/null
+++ b/src/Snap.Tests/AnyOS/MacOS/SnapOsMacOSTests.cs
@@ -0,0 +1,77 @@
+#if PLATFORM_MACOSX
+using Snap.AnyOS;
+using Snap.AnyOS.MacOS;
+using Snap.Core;
+using Snap.Shared.Tests;
+using Xunit;
+
+namespace Snap.Tests.AnyOS.MacOS;
+
+public class SnapOsMacOSTests : IClassFixture
+{
+ readonly BaseFixture _baseFixture;
+ readonly ISnapFilesystem _snapFilesystem;
+ readonly ISnapOs _snapOs;
+ readonly SnapOsMacOS _snapOsMacOS;
+
+ public SnapOsMacOSTests(BaseFixture baseFixture)
+ {
+ _baseFixture = baseFixture;
+ _snapFilesystem = new SnapFilesystem();
+ _snapOsMacOS = new SnapOsMacOS(_snapFilesystem, new SnapOsProcessManager(), new SnapOsSpecialFoldersMacOS());
+ _snapOs = new SnapOs(_snapOsMacOS);
+ }
+
+ [Fact]
+ public void TestOsPlatform()
+ {
+ Assert.Equal(System.Runtime.InteropServices.OSPlatform.OSX, _snapOs.OsPlatform);
+ }
+
+ [Fact]
+ public void TestDistroType()
+ {
+ Assert.Equal(SnapOsDistroType.MacOS, _snapOs.DistroType);
+ }
+
+ [Fact]
+ public void TestSpecialFolders()
+ {
+ var specialFolders = _snapOs.SpecialFolders;
+
+ Assert.NotNull(specialFolders.ApplicationData);
+ Assert.NotNull(specialFolders.LocalApplicationData);
+ Assert.NotNull(specialFolders.DesktopDirectory);
+ Assert.NotNull(specialFolders.StartupDirectory);
+ Assert.NotNull(specialFolders.StartMenu);
+ Assert.NotNull(specialFolders.InstallerCacheDirectory);
+ Assert.NotNull(specialFolders.NugetCacheDirectory);
+
+ // macOS-specific path checks
+ Assert.Contains("Library/LaunchAgents", specialFolders.StartupDirectory);
+ Assert.Equal("/Applications", specialFolders.StartMenu);
+ Assert.Contains("snapx", specialFolders.InstallerCacheDirectory);
+ }
+
+ [Fact]
+ public void TestGetProcesses()
+ {
+ var processes = _snapOs.GetProcesses();
+ Assert.NotEmpty(processes);
+ }
+
+ [Fact]
+ public void TestUsername()
+ {
+ Assert.NotNull(_snapOsMacOS.Username);
+ Assert.NotEmpty(_snapOsMacOS.Username);
+ }
+
+ [Fact]
+ public void TestExitSignalHandler()
+ {
+ var exitSignalHandler = _snapOsMacOS.InstallExitSignalHandler();
+ Assert.NotNull(exitSignalHandler);
+ }
+ }
+#endif
\ No newline at end of file
diff --git a/src/Snap.Tests/AnyOS/MacOS/SnapOsSpecialFoldersMacOSTests.cs b/src/Snap.Tests/AnyOS/MacOS/SnapOsSpecialFoldersMacOSTests.cs
new file mode 100644
index 00000000..6a945697
--- /dev/null
+++ b/src/Snap.Tests/AnyOS/MacOS/SnapOsSpecialFoldersMacOSTests.cs
@@ -0,0 +1,47 @@
+#if PLATFORM_MACOSX
+using Snap.AnyOS;
+using Snap.Shared.Tests;
+using Xunit;
+
+namespace Snap.Tests.AnyOS.MacOS;
+
+public class SnapOsSpecialFoldersMacOSTests : IClassFixture
+{
+ readonly BaseFixture _baseFixture;
+
+ public SnapOsSpecialFoldersMacOSTests(BaseFixture baseFixture)
+ {
+ _baseFixture = baseFixture;
+ }
+
+ [Fact]
+ public void TestMacOSSpecialFolders()
+ {
+ var specialFolders = new SnapOsSpecialFoldersMacOS();
+
+ Assert.NotNull(specialFolders.ApplicationData);
+ Assert.NotNull(specialFolders.LocalApplicationData);
+ Assert.NotNull(specialFolders.DesktopDirectory);
+ Assert.NotNull(specialFolders.StartupDirectory);
+ Assert.NotNull(specialFolders.StartMenu);
+ Assert.NotNull(specialFolders.InstallerCacheDirectory);
+ Assert.NotNull(specialFolders.NugetCacheDirectory);
+
+ // Test macOS-specific paths
+ Assert.Contains("Library/LaunchAgents", specialFolders.StartupDirectory);
+ Assert.Equal("/Applications", specialFolders.StartMenu);
+ Assert.Contains("/snapx", specialFolders.InstallerCacheDirectory);
+ Assert.Contains("/temp/nuget", specialFolders.NugetCacheDirectory);
+ }
+
+ [Fact]
+ public void TestAnyOsReturnsMacOSOnMacOS()
+ {
+ if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.OSX))
+ {
+ var anyOs = SnapOsSpecialFolders.AnyOs;
+ Assert.IsType(anyOs);
+ }
+ }
+ }
+#endif
\ No newline at end of file
diff --git a/src/Snap.Tests/Snap.Tests.csproj b/src/Snap.Tests/Snap.Tests.csproj
index bc214a2f..bf1cc7c5 100644
--- a/src/Snap.Tests/Snap.Tests.csproj
+++ b/src/Snap.Tests/Snap.Tests.csproj
@@ -5,7 +5,7 @@
false
true
- net8.0;net9.0;net10.0
+ net8.0;net9.0
diff --git a/src/Snap/AnyOS/MacOS/SnapOS.MacOS.cs b/src/Snap/AnyOS/MacOS/SnapOS.MacOS.cs
new file mode 100644
index 00000000..1a1faa9d
--- /dev/null
+++ b/src/Snap/AnyOS/MacOS/SnapOS.MacOS.cs
@@ -0,0 +1,269 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using JetBrains.Annotations;
+using Snap.AnyOS.Windows;
+using Snap.Core;
+using Snap.Extensions;
+using Snap.Logging;
+
+namespace Snap.AnyOS.MacOS;
+
+// macOS exit signal handler using POSIX signals
+internal sealed class SnapOsMacOSExitSignal : ISnapOsExitSignal
+{
+ public event EventHandler Exit;
+
+ public SnapOsMacOSExitSignal()
+ {
+ PosixSignalRegistration.Create(PosixSignal.SIGTERM, _ => OnExitSignalHandler());
+ PosixSignalRegistration.Create(PosixSignal.SIGINT, _ => OnExitSignalHandler());
+ }
+
+ void OnExitSignalHandler() => Exit?.Invoke(null, EventArgs.Empty);
+}
+
+internal sealed class SnapOsMacOS : ISnapOsImpl
+{
+ readonly ILog _logger = LogProvider.For();
+
+ public ISnapOsTaskbar Taskbar => throw new PlatformNotSupportedException("Todo: Implement taskbar progressbar for macOS");
+ public OSPlatform OsPlatform => OSPlatform.OSX;
+ public ISnapFilesystem Filesystem { get; }
+ public ISnapOsProcessManager OsProcessManager { get; }
+ public SnapOsDistroType DistroType { get; private set; } = SnapOsDistroType.MacOS;
+ public ISnapOsSpecialFolders SpecialFolders { get; }
+ public string Username { get; private set; }
+
+ public SnapOsMacOS([NotNull] ISnapFilesystem filesystem, ISnapOsProcessManager snapOsProcessManager,
+ [NotNull] ISnapOsSpecialFolders snapOsSpecialFolders)
+ {
+ SpecialFolders = snapOsSpecialFolders ?? throw new ArgumentNullException(nameof(snapOsSpecialFolders));
+ OsProcessManager = snapOsProcessManager;
+ Filesystem = filesystem ?? throw new ArgumentNullException(nameof(filesystem));
+
+ SnapOsMacOSInit();
+ }
+
+ void SnapOsMacOSInit()
+ {
+ Username = Environment.UserName;
+
+ if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ {
+ throw new PlatformNotSupportedException();
+ }
+
+ DistroType = SnapOsDistroType.MacOS;
+ }
+
+ public async Task CreateShortcutsForExecutableAsync(SnapOsShortcutDescription shortcutDescription, ILog logger = null,
+ CancellationToken cancellationToken = default)
+ {
+ if (shortcutDescription == null) throw new ArgumentNullException(nameof(shortcutDescription));
+ var exeName = Filesystem.PathGetFileName(shortcutDescription.ExeAbsolutePath);
+ if (Username == null)
+ {
+ _logger?.Error($"Unable to create shortcut because username is null. Executable: {exeName}");
+ return;
+ }
+
+ logger?.Info($"Creating shortcuts for executable: {shortcutDescription.ExeAbsolutePath}");
+
+ var autoStartEnabled = shortcutDescription.ShortcutLocations.HasFlag(SnapShortcutLocation.Startup);
+ var desktopEnabled = shortcutDescription.ShortcutLocations.HasFlag(SnapShortcutLocation.Desktop);
+ var startMenuEnabled = shortcutDescription.ShortcutLocations.HasFlag(SnapShortcutLocation.StartMenu);
+
+ // Create .app bundle for desktop and applications
+ if (desktopEnabled || startMenuEnabled)
+ {
+ await CreateMacOSAppBundle(shortcutDescription, logger, cancellationToken);
+ }
+
+ // Create LaunchAgent plist for startup
+ if (autoStartEnabled)
+ {
+ await CreateLaunchAgentPlist(shortcutDescription, logger, cancellationToken);
+ }
+ }
+
+ async Task CreateMacOSAppBundle(SnapOsShortcutDescription shortcutDescription, ILog logger, CancellationToken cancellationToken)
+ {
+ var appName = shortcutDescription.SnapApp.Id;
+ var appBundleName = $"{appName}.app";
+ var applicationsDir = "/Applications";
+ var appBundlePath = Filesystem.PathCombine(applicationsDir, appBundleName);
+ var contentsDir = Filesystem.PathCombine(appBundlePath, "Contents");
+ var macOSDir = Filesystem.PathCombine(contentsDir, "MacOS");
+ var resourcesDir = Filesystem.PathCombine(contentsDir, "Resources");
+
+ logger?.Info($"Creating macOS app bundle: {appBundlePath}");
+
+ // Create app bundle structure
+ Filesystem.DirectoryCreateIfNotExists(appBundlePath);
+ Filesystem.DirectoryCreateIfNotExists(contentsDir);
+ Filesystem.DirectoryCreateIfNotExists(macOSDir);
+ Filesystem.DirectoryCreateIfNotExists(resourcesDir);
+
+ // Create Info.plist
+ var infoPlistContent = BuildInfoPlist(shortcutDescription);
+ var infoPlistPath = Filesystem.PathCombine(contentsDir, "Info.plist");
+ await Filesystem.FileWriteUtf8StringAsync(infoPlistContent, infoPlistPath, cancellationToken);
+
+ // Create executable launcher script
+ var launcherScript = BuildLauncherScript(shortcutDescription);
+ var launcherPath = Filesystem.PathCombine(macOSDir, appName);
+ await Filesystem.FileWriteUtf8StringAsync(launcherScript, launcherPath, cancellationToken);
+
+ // Make launcher executable
+ await OsProcessManager.ChmodExecuteAsync(launcherPath, cancellationToken);
+
+ // Copy icon if available
+ if (!string.IsNullOrWhiteSpace(shortcutDescription.IconAbsolutePath) &&
+ Filesystem.FileExists(shortcutDescription.IconAbsolutePath))
+ {
+ var iconName = $"{appName}.icns";
+ var iconPath = Filesystem.PathCombine(resourcesDir, iconName);
+ try
+ {
+ await Filesystem.FileCopyAsync(shortcutDescription.IconAbsolutePath, iconPath, cancellationToken, overwrite: true);
+ logger?.Info($"Copied icon to: {iconPath}");
+ }
+ catch (Exception ex)
+ {
+ logger?.Warn($"Failed to copy icon: {ex.Message}");
+ }
+ }
+ }
+
+ async Task CreateLaunchAgentPlist(SnapOsShortcutDescription shortcutDescription, ILog logger, CancellationToken cancellationToken)
+ {
+ var launchAgentsDir = Filesystem.PathCombine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
+ "Library", "LaunchAgents");
+ var plistName = $"com.snapx.{shortcutDescription.SnapApp.Id}.plist";
+ var plistPath = Filesystem.PathCombine(launchAgentsDir, plistName);
+
+ logger?.Info($"Creating LaunchAgent plist: {plistPath}");
+
+ Filesystem.DirectoryCreateIfNotExists(launchAgentsDir);
+
+ var plistContent = BuildLaunchAgentPlist(shortcutDescription);
+ await Filesystem.FileWriteUtf8StringAsync(plistContent, plistPath, cancellationToken);
+
+ logger?.Info($"LaunchAgent plist created successfully");
+ }
+
+ public bool EnsureConsole()
+ {
+ return false; // macOS Terminal handles this
+ }
+
+ public List GetProcesses()
+ {
+ var processes = Process.GetProcesses().Select(process => OsProcessManager.Build(process.Id, process.ProcessName)).ToList();
+ return processes;
+ }
+
+ public ISnapOsExitSignal InstallExitSignalHandler()
+ {
+ return new SnapOsMacOSExitSignal();
+ }
+
+ string BuildInfoPlist(SnapOsShortcutDescription shortcutDescription)
+ {
+ var appName = shortcutDescription.SnapApp.Id;
+ var version = shortcutDescription.SnapApp.Version.ToString();
+ var description = shortcutDescription.NuspecReader.GetDescription();
+
+ return $@"
+
+
+
+ CFBundleExecutable
+ {appName}
+ CFBundleIdentifier
+ com.snapx.{appName}
+ CFBundleName
+ {appName}
+ CFBundleVersion
+ {version}
+ CFBundleShortVersionString
+ {version}
+ CFBundlePackageType
+ APPL
+ CFBundleIconFile
+ {appName}.icns
+ CFBundleInfoDictionaryVersion
+ 6.0
+ LSMinimumSystemVersion
+ 10.15
+ NSHighResolutionCapable
+
+
+";
+ }
+
+ string BuildLauncherScript(SnapOsShortcutDescription shortcutDescription)
+ {
+ var workingDirectory = Filesystem.PathGetDirectoryName(shortcutDescription.ExeAbsolutePath);
+ var environmentVariables = new List();
+
+ foreach (var (name, value) in shortcutDescription.Environment)
+ {
+ environmentVariables.Add($"export {name}=\"{value}\"");
+ }
+
+ var envVarsString = environmentVariables.Count > 0 ?
+ string.Join("\n", environmentVariables) + "\n" : "";
+
+ return $@"#!/bin/bash
+{envVarsString}
+cd ""{workingDirectory}""
+exec ""{shortcutDescription.ExeAbsolutePath}""
+";
+ }
+
+ string BuildLaunchAgentPlist(SnapOsShortcutDescription shortcutDescription)
+ {
+ var appName = shortcutDescription.SnapApp.Id;
+ var workingDirectory = Filesystem.PathGetDirectoryName(shortcutDescription.ExeAbsolutePath);
+
+ var environmentDict = "";
+ if (shortcutDescription.Environment.Count > 0)
+ {
+ var envEntries = shortcutDescription.Environment
+ .Select(kv => $@" {kv.Key}
+ {kv.Value}");
+ environmentDict = $@" EnvironmentVariables
+
+{string.Join("\n", envEntries)}
+ ";
+ }
+
+ return $@"
+
+
+
+ Label
+ com.snapx.{appName}
+ ProgramArguments
+
+ {shortcutDescription.ExeAbsolutePath}
+
+ WorkingDirectory
+ {workingDirectory}
+ RunAtLoad
+
+ KeepAlive
+
+{environmentDict}
+
+";
+ }
+}
\ No newline at end of file
diff --git a/src/Snap/AnyOS/SnapOs.cs b/src/Snap/AnyOS/SnapOs.cs
index e00be1b7..419a0c11 100644
--- a/src/Snap/AnyOS/SnapOs.cs
+++ b/src/Snap/AnyOS/SnapOs.cs
@@ -8,6 +8,7 @@
using JetBrains.Annotations;
using Snap.AnyOS.Unix;
using Snap.AnyOS.Windows;
+using Snap.AnyOS.MacOS;
using Snap.Core;
using Snap.Logging;
@@ -23,7 +24,8 @@ public enum SnapOsDistroType
Unknown,
Windows,
Ubuntu,
- RaspberryPi
+ RaspberryPi,
+ MacOS
}
internal interface ISnapOs
@@ -81,6 +83,11 @@ internal static ISnapOs AnyOs
return new SnapOs(new SnapOsUnix(snapFilesystem, snapProcess, snapSpecialFolders));
}
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ {
+ return new SnapOs(new SnapOsMacOS(snapFilesystem, snapProcess, snapSpecialFolders));
+ }
+
throw new PlatformNotSupportedException();
}
}
@@ -109,6 +116,10 @@ public SnapOs(ISnapFilesystem snapFilesystem, ISnapOsProcessManager snapOsProces
{
OsImpl = new SnapOsUnix(snapFilesystem, snapOsProcessManager, isUnitTest ?
(ISnapOsSpecialFolders) new SnapOsSpecialFoldersUnitTest(snapFilesystem, workingDirectory) : new SnapOsSpecialFoldersUnix());
+ } else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ {
+ OsImpl = new SnapOsMacOS(snapFilesystem, snapOsProcessManager, isUnitTest ?
+ (ISnapOsSpecialFolders) new SnapOsSpecialFoldersUnitTest(snapFilesystem, workingDirectory) : new SnapOsSpecialFoldersMacOS());
}
else
{
diff --git a/src/Snap/AnyOS/SnapOsSpecialFolders.cs b/src/Snap/AnyOS/SnapOsSpecialFolders.cs
index 37f0e9ef..f3a86bb1 100644
--- a/src/Snap/AnyOS/SnapOsSpecialFolders.cs
+++ b/src/Snap/AnyOS/SnapOsSpecialFolders.cs
@@ -42,6 +42,11 @@ public static ISnapOsSpecialFolders AnyOs
return new SnapOsSpecialFoldersUnix();
}
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ {
+ return new SnapOsSpecialFoldersMacOS();
+ }
+
throw new PlatformNotSupportedException();
}
}
@@ -69,6 +74,17 @@ internal sealed class SnapOsSpecialFoldersUnix : SnapOsSpecialFolders
public override string NugetCacheDirectory => $"{InstallerCacheDirectory}/temp/nuget";
}
+internal sealed class SnapOsSpecialFoldersMacOS : SnapOsSpecialFolders
+{
+ public override string ApplicationData { get; } = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
+ public override string LocalApplicationData { get; } = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
+ public override string DesktopDirectory { get; } = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory);
+ public override string StartupDirectory => $"{Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)}/Library/LaunchAgents";
+ public override string StartMenu => "/Applications";
+ public override string InstallerCacheDirectory => $"{ApplicationData}/snapx";
+ public override string NugetCacheDirectory => $"{InstallerCacheDirectory}/temp/nuget";
+}
+
internal sealed class SnapOsSpecialFoldersUnitTest : SnapOsSpecialFolders, IAsyncDisposable
{
readonly ISnapFilesystem _snapFilesystem;
diff --git a/src/Snap/Snap.csproj b/src/Snap/Snap.csproj
index 80a82d8a..43ceb76e 100644
--- a/src/Snap/Snap.csproj
+++ b/src/Snap/Snap.csproj
@@ -8,7 +8,7 @@
true
true
true
- net8.0;net9.0;net10.0
+ net8.0;net9.0
true
false
@@ -98,6 +98,7 @@
+
PreserveNewest
PreserveNewest
@@ -113,6 +114,23 @@
PreserveNewest
true
+
+
+
+ PreserveNewest
+ PreserveNewest
+ true
+
+
+ PreserveNewest
+ PreserveNewest
+ true
+
+
+ PreserveNewest
+ PreserveNewest
+ true
+
@@ -152,4 +170,78 @@
+
+
+
+
+ PreserveNewest
+ PreserveNewest
+ true
+
+
+ PreserveNewest
+ PreserveNewest
+ true
+
+
+ PreserveNewest
+ PreserveNewest
+ true
+
+
+
+
+ PreserveNewest
+ PreserveNewest
+ true
+
+
+ PreserveNewest
+ PreserveNewest
+ true
+
+
+ PreserveNewest
+ PreserveNewest
+ true
+
+
+
+
+
+ PreserveNewest
+ PreserveNewest
+ true
+
+
+ PreserveNewest
+ PreserveNewest
+ true
+
+
+ PreserveNewest
+ PreserveNewest
+ true
+
+
+
+
+
+ PreserveNewest
+ PreserveNewest
+ true
+
+
+ PreserveNewest
+ PreserveNewest
+ true
+
+
+ PreserveNewest
+ PreserveNewest
+ true
+
+
+
+