# Running on macOS and Linux (experimental) IntuneManagement.Next runs outside Windows through the **Avalonia** UI backend. The engine is the same one the Windows build uses; only the presentation layer differs. > **Experimental.** Linux has been exercised during development. macOS has been > started end to end (module import, sign-in, policy browsing) on Apple Silicon with > PowerShell 7.6.6, but the native GUI paths have had far less mileage than Windows. > Treat a macOS run as a bug hunt. The app says so itself: on the first non-Windows launch it shows a notice with a **Do not show this message again** checkbox. See [Turning the notice back on](#turning-the-notice-back-on). ## Requirements | | | |---|---| | PowerShell | **7.4 or newer** (`pwsh`) on Linux and macOS - 7.6 / 7.7 included; nothing is pinned to a particular 7.x. pwsh bundles its own .NET runtime, so **no separate .NET install is needed** on either platform. Windows PowerShell 5.1 remains supported by WPF, not Avalonia. | | Display | A desktop session. X11 or Wayland on Linux, Aqua on macOS. | ## Launching ```sh ./Start-Avalonia.command # macOS (also double-clickable in Finder) and Linux ``` or directly: ```sh pwsh -NoProfile -File ./UI/Avalonia/Start-Avalonia.ps1 ``` `-ThemeVariant Dark` and `-Provider OAuth` (or `MSAL`, `MgGraph`: the authentication provider for this session only, without touching the saved setting) are supported by both entry points. On macOS both entry points run the script through the **main-thread hook** (`Bin/MainThreadHook/IntuneManagement.MainThreadHook.dll`, ~10 KB). Cocoa only allows the GUI on the process's first thread, and a normal `pwsh` pipeline runs on a worker thread - even when single-threaded. The hook is a .NET *startup hook*: `pwsh` loads it before its own `Main` runs, on the main thread, and it opens a `UseCurrentThread` runspace there and runs `Start-Avalonia.ps1` inside the very same `pwsh`. No second engine, no second runtime: whatever pwsh you have brings its engine, its .NET and its `$PSHOME/ref` compile references, all consistent with each other. Direct script invocation re-launches itself through the hook when it notices it is not on the main thread. See [The macOS main-thread hook](#the-macos-main-thread-hook). `Start-Avalonia.ps1` sets `IM_UI_BACKEND=Avalonia` for you. On **Linux or Windows**, setting that variable and importing the module by hand works too (on Windows use `pwsh -STA`): ```powershell $env:IM_UI_BACKEND = 'Avalonia' Import-Module ./IntuneManagement.psd1 -Force Show-IMMainWindow -View 'IntuneManagement' ``` On macOS do not import the GUI manually in an ordinary `pwsh` session: the native host rejects initialization off the main thread. Use one of the entry points above. Headless imports with the `None` backend are unaffected. Without `IM_UI_BACKEND`, the module defaults to the headless `None` backend off Windows - useful for automation, and the reason `Connect-IMIntuneManagement` and the bulk cmdlets work fine on a Mac or a Linux box with no display at all. ## What does not work off Windows These degrade with a log message rather than an error: | Feature | Why | |---|---| | **Word documentation output** | Needs `Microsoft.Office.Interop.Word` COM automation. HTML, Markdown, CSV and JSON output are unaffected. | | **MSI property extraction** on app import | Needs the `WindowsInstaller.Installer` COM object. Other app types import normally. | | **WAM / broker sign-in** | Windows-only. Authentication falls back to the system browser. | The `Default` theme follows the OS on all three platforms: the Windows app theme, the macOS appearance setting, and the GNOME colour scheme on Linux. Linux desktops that are not GNOME have no common way to report a preference and resolve to Light; pick Light or Dark explicitly in Settings there. Token cache persistence *is* implemented on both platforms - macOS uses the Keychain and Linux uses libsecret (gnome-keyring / KWallet), via `MsalCacheHelper`. On a Linux box with no keyring daemon, expect to sign in every session. ## Turning the notice back on The startup notice is a normal setting. Clear **Settings -> General -> Hide experimental platform notice** to see it again. To preview it on Windows, where it never appears on its own: ```powershell $env:IM_EXPERIMENTAL_NOTICE = '1' ./UI/Avalonia/Start-Avalonia.ps1 ``` ## Rebuilding the Avalonia binaries `Bin/Avalonia` is committed and holds the natives for all three platforms side by side - `.dll` for Windows, `.so` for Linux, `.dylib` for macOS - plus the managed Avalonia assemblies, which are platform-neutral. To rebuild for the machine you are on: ```powershell ./UI/Avalonia/Bootstrap/Restore-AvaloniaBinaries.ps1 ``` To build another platform's natives **without that platform** - how the committed macOS binaries were produced, from Windows: ```powershell ./UI/Avalonia/Bootstrap/Restore-AvaloniaBinaries.ps1 -RuntimeIdentifier osx-arm64 ``` NuGet serves the RID-specific native packages regardless of the host OS. The macOS dylibs are **universal binaries** (x86_64 + arm64 slices), so `osx-arm64` and `osx-x64` produce identical files and either one covers every Mac. `dotnet publish` does not clean its output directory, which is what lets one `Bin/Avalonia` hold all three platforms at once. It does overwrite `AvaloniaPayload.deps.json` with the last RID published; that file is inert here, because the module loads the assemblies with `Add-Type -Path` rather than through the `dotnet` host. ## The macOS main-thread hook Source: [`UI/Avalonia/Bootstrap/MainThreadHook/StartupHook.cs`](../UI/Avalonia/Bootstrap/MainThreadHook/StartupHook.cs). Binary: `Bin/MainThreadHook/IntuneManagement.MainThreadHook.dll` - **committed**, so a plain clone or source ZIP is a complete, runnable download. `Start-Avalonia.command` sets two environment variables and runs `pwsh -File`: | Variable | Purpose | |---|---| | `DOTNET_STARTUP_HOOKS` | Path of the hook DLL. The .NET runtime calls its `StartupHook.Initialize()` on the main thread before `pwsh`'s own `Main`. | | `IM_MAIN_THREAD_HOOK=1` | Engages the hook. Without it `Initialize` returns at once and pwsh starts normally, so child processes are unaffected (the hook also clears both variables from its own environment). | `Initialize` then parses `-File