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 + + + +